From 443a8392cec552a5153141dc30712d748b6793c2 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Mon, 14 Oct 2024 17:41:42 +0200 Subject: [PATCH 01/50] Add service templates --- app/build.gradle | 1 + .../main/resources/META-INF/spring.factories | 1 - ...ot.autoconfigure.AutoConfiguration.imports | 1 - resources/etc/cas/config/cas.properties.tpl | 9 ++++++- .../services/cas-service-template.json.tpl | 19 +++++++++++++++ .../services/oauth-service-template.json.tpl | 24 +++++++++++++++++++ .../services/oidc-service-template.json.tpl | 24 +++++++++++++++++++ .../config/services/service-template.json.tpl | 19 +++++++++++++++ .../services/development/all-10000001.json | 12 ++++++++++ .../etc/cas/services/production/.gitkeep | 0 .../services/templates/DefaultCasService.json | 14 +++++++++++ .../cas/services/templates/WithLogoutURI.json | 5 ++++ 12 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 resources/etc/cas/config/services/cas-service-template.json.tpl create mode 100644 resources/etc/cas/config/services/oauth-service-template.json.tpl create mode 100644 resources/etc/cas/config/services/oidc-service-template.json.tpl create mode 100644 resources/etc/cas/config/services/service-template.json.tpl create mode 100644 resources/etc/cas/services/development/all-10000001.json create mode 100644 resources/etc/cas/services/production/.gitkeep create mode 100644 resources/etc/cas/services/templates/DefaultCasService.json create mode 100644 resources/etc/cas/services/templates/WithLogoutURI.json diff --git a/app/build.gradle b/app/build.gradle index 00f470d8..307ba590 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -159,6 +159,7 @@ dependencies { implementation "org.apereo.cas:cas-server-support-throttle" implementation "org.apereo.cas:cas-server-support-throttle-core" + implementation "org.apereo.cas:cas-server-support-json-service-registry" implementation 'org.mousio:etcd4j:2.18.0' implementation 'com.googlecode.json-simple:json-simple:1.1.1' diff --git a/app/src/main/resources/META-INF/spring.factories b/app/src/main/resources/META-INF/spring.factories index f0152af8..ace8a8af 100644 --- a/app/src/main/resources/META-INF/spring.factories +++ b/app/src/main/resources/META-INF/spring.factories @@ -1,5 +1,4 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ - de.triology.cas.services.CesServicesSpringConfiguration,\ de.triology.cas.oidc.config.CesOidcConfiguration,\ de.triology.cas.oidc.config.CesOAuthConfiguration,\ de.triology.cas.ldap.CesAuthenticationEventExecutionPlanConfiguration,\ diff --git a/app/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/app/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 54fcd6d9..42b0bf83 100644 --- a/app/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/app/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,4 +1,3 @@ - de.triology.cas.services.CesServicesSpringConfiguration de.triology.cas.oidc.config.CesOidcConfiguration de.triology.cas.oidc.config.CesOAuthConfiguration de.triology.cas.ldap.CesAuthenticationEventExecutionPlanConfiguration diff --git a/resources/etc/cas/config/cas.properties.tpl b/resources/etc/cas/config/cas.properties.tpl index b40183cb..eee15abf 100644 --- a/resources/etc/cas/config/cas.properties.tpl +++ b/resources/etc/cas/config/cas.properties.tpl @@ -271,4 +271,11 @@ cas.authn.oauth.code.numberOfUses=1 # Access Token (Session) is valid for 1 day (= 86000 seconds) cas.authn.oauth.accessToken.timeToKillInSeconds=86000 cas.authn.oauth.accessToken.maxTimeToLiveInSeconds=86000 -######################################################################################################################## \ No newline at end of file +######################################################################################################################## + + +######################################################################################################################## +# JSON Registry +cas.service-registry.json.location={{if eq (.GlobalConfig.GetOrDefault "stage" "production") "production"}}file:/etc/cas/services/production{{else}}file:/etc/cas/services/development{{end}} +cas.service-registry.json.watcher-enabled=true +cas.service-registry.templates.directory.location=file:/etc/cas/services/templates \ No newline at end of file diff --git a/resources/etc/cas/config/services/cas-service-template.json.tpl b/resources/etc/cas/config/services/cas-service-template.json.tpl new file mode 100644 index 00000000..840016c1 --- /dev/null +++ b/resources/etc/cas/config/services/cas-service-template.json.tpl @@ -0,0 +1,19 @@ +{ + "@class" : "org.apereo.cas.services.CasRegisteredService", + "serviceId" : "https://((?i){{FQDN}})(:443)?/{{SERVICE}}(/.*)?", + "name" : "{{SERVICE}}", + "id" : {{SERVICE_ID}}, + "templateName": "{{TEMPLATES}}", + "attributeReleasePolicy": { + "@class" : "org.apereo.cas.services.ReturnAllowedAttributeReleasePolicy", + "allowedAttributes" : [ "java.util.ArrayList", [ "username", "cn", "mail", "givenName", "surname", "displayName", "groups" ] ] + }, + "logoutType" : "BACK_CHANNEL", + "properties" : { + "@class" : "java.util.HashMap", + "LogoutUrl": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", + "values": [ "java.util.LinkedHashSet", [ "{{LOGOUT_URL}}" ] ] + } + } +} \ No newline at end of file diff --git a/resources/etc/cas/config/services/oauth-service-template.json.tpl b/resources/etc/cas/config/services/oauth-service-template.json.tpl new file mode 100644 index 00000000..044c8a5c --- /dev/null +++ b/resources/etc/cas/config/services/oauth-service-template.json.tpl @@ -0,0 +1,24 @@ +{ + "@class" : "org.apereo.cas.support.oauth.services.OAuthRegisteredService", + "serviceId" : "https://((?i){{FQDN}})(:443)?/{{SERVICE}}(/.*)?", + "name" : "{{SERVICE}}", + "id" : {{SERVICE_ID}}, + "templateName": "{{TEMPLATES}}", + "clientId": "{{SERVICE}}", + "clientSecret": "{{CLIENT_SECRET_HASH}}", + "bypassApprovalPrompt": true, + "supportedResponseTypes": [ "java.util.HashSet", [ "code" ] ], + "supportedGrantTypes": [ "java.util.HashSet", [ "authorization_code" ] ], + "logoutType" : "BACK_CHANNEL", + "properties" : { + "@class" : "java.util.HashMap", + "LogoutUrl": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", + "values": [ "java.util.LinkedHashSet", [ "{{LOGOUT_URL}}" ] ] + }, + "ServiceClass": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", + "values": [ "java.util.LinkedHashSet", [ "org.apereo.cas.services.OidcRegisteredService" ] ] + } + } +} \ No newline at end of file diff --git a/resources/etc/cas/config/services/oidc-service-template.json.tpl b/resources/etc/cas/config/services/oidc-service-template.json.tpl new file mode 100644 index 00000000..be534952 --- /dev/null +++ b/resources/etc/cas/config/services/oidc-service-template.json.tpl @@ -0,0 +1,24 @@ +{ + "@class" : "org.apereo.cas.services.OidcRegisteredService", + "serviceId" : "https://((?i){{FQDN}})(:443)?/{{SERVICE}}(/.*)?", + "name" : "{{SERVICE}}", + "id" : {{SERVICE_ID}}, + "templateName": "{{TEMPLATES}}", + "clientId": "{{SERVICE}}", + "clientSecret": "{{CLIENT_SECRET_HASH}}", + "bypassApprovalPrompt": true, + "supportedResponseTypes": [ "java.util.HashSet", [ "code" ] ], + "supportedGrantTypes": [ "java.util.HashSet", [ "authorization_code" ] ], + "logoutType" : "BACK_CHANNEL", + "properties" : { + "@class" : "java.util.HashMap", + "LogoutUrl": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", + "values": [ "java.util.LinkedHashSet", [ "{{LOGOUT_URL}}" ] ] + }, + "ServiceClass": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", + "values": [ "java.util.LinkedHashSet", [ "org.apereo.cas.services.OidcRegisteredService" ] ] + } + } +} \ No newline at end of file diff --git a/resources/etc/cas/config/services/service-template.json.tpl b/resources/etc/cas/config/services/service-template.json.tpl new file mode 100644 index 00000000..831dd864 --- /dev/null +++ b/resources/etc/cas/config/services/service-template.json.tpl @@ -0,0 +1,19 @@ +{ + "@class" : "${ServiceClass}", + "serviceId" : "https://((?i){{FQDN}})(:443)?/{{SERVICE}}(/.*)?", + "name" : "{{SERVICE}}", + "id" : {{SERVICE_ID}}, + "templateName": "{{TEMPLATES}}", + "logoutType" : "BACK_CHANNEL", + "properties" : { + "@class" : "java.util.HashMap", + "LogoutUrl": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", + "values": [ "java.util.LinkedHashSet", [ "{{LOGOUT_URL}}" ] ] + }, + "ServiceClass": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", + "values": [ "java.util.LinkedHashSet", [ "{{SERVICE_CLASS}}" ] ] + } + } +} \ No newline at end of file diff --git a/resources/etc/cas/services/development/all-10000001.json b/resources/etc/cas/services/development/all-10000001.json new file mode 100644 index 00000000..cae297ea --- /dev/null +++ b/resources/etc/cas/services/development/all-10000001.json @@ -0,0 +1,12 @@ +{ + "@class" : "org.apereo.cas.services.CasRegisteredService", + "serviceId" : "^(https|http)://.*", + "name" : "all", + "id" : 10000001, + "description": "development service allowing any https / http access to cas", + "templateName": "AllowServiceProxyPolicy", + "attributeReleasePolicy" : { + "@class" : "org.apereo.cas.services.ReturnAllAttributeReleasePolicy" + }, + "logoutType" : "BACK_CHANNEL" +} \ No newline at end of file diff --git a/resources/etc/cas/services/production/.gitkeep b/resources/etc/cas/services/production/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/resources/etc/cas/services/templates/DefaultCasService.json b/resources/etc/cas/services/templates/DefaultCasService.json new file mode 100644 index 00000000..3178d394 --- /dev/null +++ b/resources/etc/cas/services/templates/DefaultCasService.json @@ -0,0 +1,14 @@ +{ + "@class" : "org.apereo.cas.services.CasRegisteredService", + "templateName": "WithLogoutURI", + "attributeReleasePolicy": { + "@class" : "org.apereo.cas.services.ReturnAllowedAttributeReleasePolicy", + "allowedAttributes" : [ "java.util.ArrayList", [ "username", "cn", "mail", "givenName", "surname", "displayName", "groups" ] ] + }, + "proxyPolicy" : { + "@class" : "org.apereo.cas.services.RegexMatchingRegisteredServiceProxyPolicy", + "pattern" : "^https?://.*", + "useServiceId": false, + "exactMatch": false + } +} \ No newline at end of file diff --git a/resources/etc/cas/services/templates/WithLogoutURI.json b/resources/etc/cas/services/templates/WithLogoutURI.json new file mode 100644 index 00000000..cb79038d --- /dev/null +++ b/resources/etc/cas/services/templates/WithLogoutURI.json @@ -0,0 +1,5 @@ +{ + "@class" : "${ServiceClass}", + "templateName": "WithLogoutURI", + "logoutUrl" : "${LogoutUrl}" +} \ No newline at end of file From b18f425471c24a92966e762e4c078587a8dcefe5 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Mon, 14 Oct 2024 17:43:10 +0200 Subject: [PATCH 02/50] Add methods to create services from bash - Add method find_next_serviceID - Add method escape_dots - Add const SERVICE_REGISTRY for service path --- resources/util.sh | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/resources/util.sh b/resources/util.sh index 28f1d241..ff32894d 100755 --- a/resources/util.sh +++ b/resources/util.sh @@ -3,6 +3,9 @@ set -o errexit set -o nounset set -o pipefail +# Defines the path to store service definitions +SERVICE_REGISTRY="etc/cas/services/production" + # This function prints an error to the console and waits 5 minutes before exiting the process. # Requires two arguments: # 1 - Error state @@ -162,3 +165,42 @@ function configureCAS() { renderCASPropertiesTpl renderCustomMessagesTpl } + +# Function to find the next serviceID from JSON filenames in the service registry +find_next_serviceID() { + local dir="$1" + + # Initialize max_number as 0 + local max_number=0 + + # Loop through all JSON files in the directory + for file in $dir/*.json; do + # Check if the folder contains any JSON files + if [[ -f "$file" ]]; then + # Extract the number from the filename using regex (ignores prefix and extracts numbers) + local number=$(echo "$file" | awk -F'[-.]' '{print $(NF-1)}') + + # Update max_number if the extracted number is greater + if [[ $number -gt $max_number ]]; then + max_number=$number + fi + fi + done + + # If no files were found, start with 1, otherwise increment the max_number + local next_number=$((max_number + 1)) + + # Return the next number + echo "$next_number" +} + +# Function to double-escape dots in the FQDN to use it within a regex of the service registry +escape_dots() { + local fqdn="$1" + + # Use parameter substitution to replace each '.' with '\\.' + local escaped_fqdn="${fqdn//./\\\\\\\\.}" + + # Return the double-escaped FQDN + echo "$escaped_fqdn" +} From 2014c099d187beeb036d59cfe40f6715cef69a68 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Wed, 16 Oct 2024 15:58:32 +0200 Subject: [PATCH 03/50] export attributeReleasePolicy to template --- .../cas/config/services/cas-service-template.json.tpl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/resources/etc/cas/config/services/cas-service-template.json.tpl b/resources/etc/cas/config/services/cas-service-template.json.tpl index 840016c1..69fd0ff9 100644 --- a/resources/etc/cas/config/services/cas-service-template.json.tpl +++ b/resources/etc/cas/config/services/cas-service-template.json.tpl @@ -4,16 +4,15 @@ "name" : "{{SERVICE}}", "id" : {{SERVICE_ID}}, "templateName": "{{TEMPLATES}}", - "attributeReleasePolicy": { - "@class" : "org.apereo.cas.services.ReturnAllowedAttributeReleasePolicy", - "allowedAttributes" : [ "java.util.ArrayList", [ "username", "cn", "mail", "givenName", "surname", "displayName", "groups" ] ] - }, - "logoutType" : "BACK_CHANNEL", "properties" : { "@class" : "java.util.HashMap", "LogoutUrl": { "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", "values": [ "java.util.LinkedHashSet", [ "{{LOGOUT_URL}}" ] ] + }, + "ServiceClass": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", + "values": [ "java.util.LinkedHashSet", [ "org.apereo.cas.services.CasRegisteredService" ] ] } } } \ No newline at end of file From 39d042cc90a8ceb46b15d7f50c7fb90f87742c3a Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Wed, 23 Oct 2024 11:52:22 +0200 Subject: [PATCH 04/50] create templates for CAS/OAuth/OIDC services --- .../cas/services/templates/AllowProxyPolicy.json | 10 ++++++++++ .../etc/cas/services/templates/BaseService.json | 7 +++++++ .../templates/DefaultAttributeReleasePolicy.json | 8 ++++++++ .../cas/services/templates/DefaultCasService.json | 14 -------------- .../services/templates/DefaultOAuthService.json | 7 +++++++ 5 files changed, 32 insertions(+), 14 deletions(-) create mode 100644 resources/etc/cas/services/templates/AllowProxyPolicy.json create mode 100644 resources/etc/cas/services/templates/BaseService.json create mode 100644 resources/etc/cas/services/templates/DefaultAttributeReleasePolicy.json delete mode 100644 resources/etc/cas/services/templates/DefaultCasService.json create mode 100644 resources/etc/cas/services/templates/DefaultOAuthService.json diff --git a/resources/etc/cas/services/templates/AllowProxyPolicy.json b/resources/etc/cas/services/templates/AllowProxyPolicy.json new file mode 100644 index 00000000..dec284bc --- /dev/null +++ b/resources/etc/cas/services/templates/AllowProxyPolicy.json @@ -0,0 +1,10 @@ +{ + "@class" : "org.apereo.cas.services.CasRegisteredService", + "templateName": "AllowProxyPolicy", + "proxyPolicy" : { + "@class" : "org.apereo.cas.services.RegexMatchingRegisteredServiceProxyPolicy", + "pattern" : "^https?://.*", + "useServiceId": false, + "exactMatch": false + } +} \ No newline at end of file diff --git a/resources/etc/cas/services/templates/BaseService.json b/resources/etc/cas/services/templates/BaseService.json new file mode 100644 index 00000000..5d0a7507 --- /dev/null +++ b/resources/etc/cas/services/templates/BaseService.json @@ -0,0 +1,7 @@ +{ + "@class" : "${ServiceClass}", + "templateName": "BaseService", + "name" : "${ServiceName}", + "serviceId" : "^https://((?i)${Fqdn})(:443)?/${ServiceName}(/.*)?", + "logoutType" : "BACK_CHANNEL" +} \ No newline at end of file diff --git a/resources/etc/cas/services/templates/DefaultAttributeReleasePolicy.json b/resources/etc/cas/services/templates/DefaultAttributeReleasePolicy.json new file mode 100644 index 00000000..5f7eabfc --- /dev/null +++ b/resources/etc/cas/services/templates/DefaultAttributeReleasePolicy.json @@ -0,0 +1,8 @@ +{ + "@class" : "${ServiceClass}", + "templateName": "DefaultAttributeReleasePolicy", + "attributeReleasePolicy": { + "@class" : "org.apereo.cas.services.ReturnAllowedAttributeReleasePolicy", + "allowedAttributes" : [ "java.util.ArrayList", [ "username", "cn", "mail", "givenName", "surname", "displayName", "groups" ] ] + } +} \ No newline at end of file diff --git a/resources/etc/cas/services/templates/DefaultCasService.json b/resources/etc/cas/services/templates/DefaultCasService.json deleted file mode 100644 index 3178d394..00000000 --- a/resources/etc/cas/services/templates/DefaultCasService.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "@class" : "org.apereo.cas.services.CasRegisteredService", - "templateName": "WithLogoutURI", - "attributeReleasePolicy": { - "@class" : "org.apereo.cas.services.ReturnAllowedAttributeReleasePolicy", - "allowedAttributes" : [ "java.util.ArrayList", [ "username", "cn", "mail", "givenName", "surname", "displayName", "groups" ] ] - }, - "proxyPolicy" : { - "@class" : "org.apereo.cas.services.RegexMatchingRegisteredServiceProxyPolicy", - "pattern" : "^https?://.*", - "useServiceId": false, - "exactMatch": false - } -} \ No newline at end of file diff --git a/resources/etc/cas/services/templates/DefaultOAuthService.json b/resources/etc/cas/services/templates/DefaultOAuthService.json new file mode 100644 index 00000000..3a622063 --- /dev/null +++ b/resources/etc/cas/services/templates/DefaultOAuthService.json @@ -0,0 +1,7 @@ +{ + "@class" : "${ServiceClass}", + "templateName": "DefaultOAuthService", + "bypassApprovalPrompt": true, + "supportedResponseTypes": [ "java.util.HashSet", [ "code" ] ], + "supportedGrantTypes": [ "java.util.HashSet", [ "authorization_code" ] ] +} \ No newline at end of file From f5d202c4ae020cc2c61003c54705694ffd7c0379 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Wed, 23 Oct 2024 12:09:35 +0200 Subject: [PATCH 05/50] Adjust templates to use for services. Set properties in config templates which are used within the cas specific templates that you can find etc/cas/services/templates. --- .../services/cas-service-template.json.tpl | 18 +++++++++----- .../services/oauth-service-template.json.tpl | 24 ++++++++++--------- .../services/oidc-service-template.json.tpl | 24 ------------------- 3 files changed, 25 insertions(+), 41 deletions(-) delete mode 100644 resources/etc/cas/config/services/oidc-service-template.json.tpl diff --git a/resources/etc/cas/config/services/cas-service-template.json.tpl b/resources/etc/cas/config/services/cas-service-template.json.tpl index 69fd0ff9..7a9e0848 100644 --- a/resources/etc/cas/config/services/cas-service-template.json.tpl +++ b/resources/etc/cas/config/services/cas-service-template.json.tpl @@ -1,18 +1,24 @@ { "@class" : "org.apereo.cas.services.CasRegisteredService", - "serviceId" : "https://((?i){{FQDN}})(:443)?/{{SERVICE}}(/.*)?", - "name" : "{{SERVICE}}", "id" : {{SERVICE_ID}}, "templateName": "{{TEMPLATES}}", "properties" : { "@class" : "java.util.HashMap", + "ServiceClass": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", + "values": [ "java.util.LinkedHashSet", [ "org.apereo.cas.services.CasRegisteredService" ] ] + }, + "Fqdn": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", + "values": [ "java.util.LinkedHashSet", [ "{{FQDN}}" ] ] + }, + "ServiceName": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", + "values": [ "java.util.LinkedHashSet", [ "{{SERVICE}}" ] ] + }, "LogoutUrl": { "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", "values": [ "java.util.LinkedHashSet", [ "{{LOGOUT_URL}}" ] ] }, - "ServiceClass": { - "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", - "values": [ "java.util.LinkedHashSet", [ "org.apereo.cas.services.CasRegisteredService" ] ] - } } } \ No newline at end of file diff --git a/resources/etc/cas/config/services/oauth-service-template.json.tpl b/resources/etc/cas/config/services/oauth-service-template.json.tpl index 044c8a5c..7a7bbdff 100644 --- a/resources/etc/cas/config/services/oauth-service-template.json.tpl +++ b/resources/etc/cas/config/services/oauth-service-template.json.tpl @@ -1,24 +1,26 @@ { - "@class" : "org.apereo.cas.support.oauth.services.OAuthRegisteredService", - "serviceId" : "https://((?i){{FQDN}})(:443)?/{{SERVICE}}(/.*)?", - "name" : "{{SERVICE}}", + "@class" : "{{SERVICE_CLASS}}", "id" : {{SERVICE_ID}}, "templateName": "{{TEMPLATES}}", "clientId": "{{SERVICE}}", "clientSecret": "{{CLIENT_SECRET_HASH}}", - "bypassApprovalPrompt": true, - "supportedResponseTypes": [ "java.util.HashSet", [ "code" ] ], - "supportedGrantTypes": [ "java.util.HashSet", [ "authorization_code" ] ], - "logoutType" : "BACK_CHANNEL", "properties" : { "@class" : "java.util.HashMap", + "ServiceClass": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", + "values": [ "java.util.LinkedHashSet", [ "{{SERVICE_CLASS}}" ] ] + }, + "Fqdn": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", + "values": [ "java.util.LinkedHashSet", [ "{{FQDN}}" ] ] + }, + "ServiceName": { + "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", + "values": [ "java.util.LinkedHashSet", [ "{{SERVICE}}" ] ] + }, "LogoutUrl": { "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", "values": [ "java.util.LinkedHashSet", [ "{{LOGOUT_URL}}" ] ] }, - "ServiceClass": { - "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", - "values": [ "java.util.LinkedHashSet", [ "org.apereo.cas.services.OidcRegisteredService" ] ] - } } } \ No newline at end of file diff --git a/resources/etc/cas/config/services/oidc-service-template.json.tpl b/resources/etc/cas/config/services/oidc-service-template.json.tpl deleted file mode 100644 index be534952..00000000 --- a/resources/etc/cas/config/services/oidc-service-template.json.tpl +++ /dev/null @@ -1,24 +0,0 @@ -{ - "@class" : "org.apereo.cas.services.OidcRegisteredService", - "serviceId" : "https://((?i){{FQDN}})(:443)?/{{SERVICE}}(/.*)?", - "name" : "{{SERVICE}}", - "id" : {{SERVICE_ID}}, - "templateName": "{{TEMPLATES}}", - "clientId": "{{SERVICE}}", - "clientSecret": "{{CLIENT_SECRET_HASH}}", - "bypassApprovalPrompt": true, - "supportedResponseTypes": [ "java.util.HashSet", [ "code" ] ], - "supportedGrantTypes": [ "java.util.HashSet", [ "authorization_code" ] ], - "logoutType" : "BACK_CHANNEL", - "properties" : { - "@class" : "java.util.HashMap", - "LogoutUrl": { - "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", - "values": [ "java.util.LinkedHashSet", [ "{{LOGOUT_URL}}" ] ] - }, - "ServiceClass": { - "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", - "values": [ "java.util.LinkedHashSet", [ "org.apereo.cas.services.OidcRegisteredService" ] ] - } - } -} \ No newline at end of file From 39b6004060fad4370d2290c7f51bb71f48f81474 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Wed, 23 Oct 2024 12:10:34 +0200 Subject: [PATCH 06/50] Remove unused config template for services --- .../config/services/service-template.json.tpl | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 resources/etc/cas/config/services/service-template.json.tpl diff --git a/resources/etc/cas/config/services/service-template.json.tpl b/resources/etc/cas/config/services/service-template.json.tpl deleted file mode 100644 index 831dd864..00000000 --- a/resources/etc/cas/config/services/service-template.json.tpl +++ /dev/null @@ -1,19 +0,0 @@ -{ - "@class" : "${ServiceClass}", - "serviceId" : "https://((?i){{FQDN}})(:443)?/{{SERVICE}}(/.*)?", - "name" : "{{SERVICE}}", - "id" : {{SERVICE_ID}}, - "templateName": "{{TEMPLATES}}", - "logoutType" : "BACK_CHANNEL", - "properties" : { - "@class" : "java.util.HashMap", - "LogoutUrl": { - "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", - "values": [ "java.util.LinkedHashSet", [ "{{LOGOUT_URL}}" ] ] - }, - "ServiceClass": { - "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", - "values": [ "java.util.LinkedHashSet", [ "{{SERVICE_CLASS}}" ] ] - } - } -} \ No newline at end of file From b49c1d4184f307ab01762d03318ce1d113fe6773 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Wed, 23 Oct 2024 12:11:09 +0200 Subject: [PATCH 07/50] Adjust scripts for creating and removing service accounts in cas. --- resources/create-sa.sh | 52 ++++++++++++++++++++++++++++++++++++++---- resources/remove-sa.sh | 18 ++++++++++++++- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/resources/create-sa.sh b/resources/create-sa.sh index 068ecfa7..6e4b5783 100755 --- a/resources/create-sa.sh +++ b/resources/create-sa.sh @@ -3,6 +3,8 @@ set -o errexit set -o nounset set -o pipefail +source util.sh + { if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then echo "usage create-sa.sh account_type [logout_uri] servicename" @@ -17,23 +19,63 @@ set -o pipefail echo "Create sa for ${SERVICE} with account type: ${TYPE}..." + #FQDN="192.168.56.2" + FQDN=$(doguctl config -g fqdn) + # escape fqdn to use it within regex + EFQDN=$(escape_dots "$FQDN") + SERVICE_ID=$(find_next_serviceID "$SERVICE_REGISTRY") + # build logout url + LOGOUT_URL="https://${FQDN}/${SERVICE}${LOGOUT_URI:-}" + + # Initialize TEMPLATES as an empty array + TEMPLATES=("BaseService,DefaultAttributeReleasePolicy") + + if [ -n "${LOGOUT_URI+x}" ]; then + TEMPLATES+=("WithLogoutURI") + doguctl config "service_accounts/${TYPE}/${SERVICE}/logout_uri" "${LOGOUT_URI}" + fi + if [ "${TYPE}" == "oidc" ] || [ "${TYPE}" == "oauth" ]; then + TEMPLATES+=("DefaultOAuthService") + + SERVICE_CLASS="org.apereo.cas.support.oauth.services.OAuthRegisteredService" + if [ "${TYPE}" == "oidc" ] ; then + SERVICE_CLASS="org.apereo.cas.services.OidcRegisteredService" + fi + CLIENT_SECRET=$(doguctl random -l 16) + #CLIENT_SECRET="secret" CLIENT_SECRET_HASH=$(echo -n "${CLIENT_SECRET}" | sha256sum | awk '{print $1}') + #CLIENT_SECRET_HASH="HASH" + + # Using `sed` to replace placeholders + sed -e "s|{{SERVICE}}|$SERVICE|g" \ + -e "s|{{SERVICE_ID}}|$SERVICE_ID|g" \ + -e "s|{{FQDN}}|$EFQDN|g" \ + -e "s|{{TEMPLATES}}|$(IFS=, ; echo "${TEMPLATES[*]}")|g" \ + -e "s|{{CLIENT_SECRET_HASH}}|$CLIENT_SECRET_HASH|g" \ + -e "s|{{SERVICE_CLASS}}|$SERVICE_CLASS|g" \ + -e "s|{{LOGOUT_URL}}|$LOGOUT_URL|g" etc/cas/config/services/oauth-service-template.json.tpl > $SERVICE_REGISTRY/${SERVICE}-${SERVICE_ID}.json doguctl config "service_accounts/${TYPE}/${SERVICE}/secret" "${CLIENT_SECRET_HASH}" elif [ "${TYPE}" == "cas" ]; then # Set value `created` because doguctl requires a value to be set doguctl config "service_accounts/${TYPE}/${SERVICE}/created" "true" + + # Allow Service to use PGTs as default behavior - could be changed in the future as it is not recommended + TEMPLATES+=("AllowProxyPolicy") + + # Using `sed` to replace placeholders + sed -e "s|{{SERVICE}}|$SERVICE|g" \ + -e "s|{{SERVICE_ID}}|$SERVICE_ID|g" \ + -e "s|{{FQDN}}|$EFQDN|g" \ + -e "s|{{TEMPLATES}}|$(IFS=, ; echo "${TEMPLATES[*]}")|g" \ + -e "s|{{LOGOUT_URL}}|$LOGOUT_URL|g" etc/cas/config/services/cas-service-template.json.tpl > $SERVICE_REGISTRY/${SERVICE}-${SERVICE_ID}.json + else echo "only the account_types: oidc, oauth, cas are allowed" exit 1 fi - - if [ -n "${LOGOUT_URI+x}" ]; then - doguctl config "service_accounts/${TYPE}/${SERVICE}/logout_uri" "${LOGOUT_URI}" - fi - } >/dev/null 2>&1 # print client-id so that the service-account can be removed again diff --git a/resources/remove-sa.sh b/resources/remove-sa.sh index 44de0354..dbf0cdb3 100755 --- a/resources/remove-sa.sh +++ b/resources/remove-sa.sh @@ -3,8 +3,10 @@ set -o errexit set -o nounset set -o pipefail +source util.sh + if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then - echo "usage create-sa.sh account_type [logout_uri] servicename" + echo "usage remove-sa.sh account_type [logout_uri] servicename" exit 1 fi @@ -28,4 +30,18 @@ fi if [ -n "${LOGOUT_URI+x}" ]; then echo "Removing service_accounts/${TYPE}/${SERVICE}/logout_uri key..." doguctl config --rm "service_accounts/${TYPE}/${SERVICE}/logout_uri" +fi + +echo "Removing service ${SERVICE} from JSON registry ${SERVICE_REGISTRY}" +FILES=$(ls $SERVICE_REGISTRY/${SERVICE}-*.json 2>/dev/null || echo "") + +# Check if FILES is empty before counting +if [ -z "$FILES" ]; then + echo "No files found matching the service ${SERVICE}." +else + # Count the number of matching files + FILE_COUNT=$(echo "$FILES" | wc -l) + echo "Found $FILE_COUNT file(s) matching service ${SERVICE}." + rm $SERVICE_REGISTRY/${SERVICE}-*.json + echo "Successfully deleted service ${SERVICE}." fi \ No newline at end of file From 20c0f269868c78f2fe92d2250fe6ed3c51ed4c1e Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Wed, 23 Oct 2024 12:11:46 +0200 Subject: [PATCH 08/50] Escape dots even more as multiple template mechanism is used. --- resources/util.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/util.sh b/resources/util.sh index ff32894d..27763edf 100755 --- a/resources/util.sh +++ b/resources/util.sh @@ -199,7 +199,7 @@ escape_dots() { local fqdn="$1" # Use parameter substitution to replace each '.' with '\\.' - local escaped_fqdn="${fqdn//./\\\\\\\\.}" + local escaped_fqdn="${fqdn//./\\\\\\\\\\\\\\\\.}" # Return the double-escaped FQDN echo "$escaped_fqdn" From 0a4e4b4c66b39d892421ff84bc6ea67217f04ae7 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Thu, 24 Oct 2024 21:12:58 +0200 Subject: [PATCH 09/50] format json --- resources/etc/cas/config/services/cas-service-template.json.tpl | 2 +- .../etc/cas/config/services/oauth-service-template.json.tpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/etc/cas/config/services/cas-service-template.json.tpl b/resources/etc/cas/config/services/cas-service-template.json.tpl index 7a9e0848..f98f02b4 100644 --- a/resources/etc/cas/config/services/cas-service-template.json.tpl +++ b/resources/etc/cas/config/services/cas-service-template.json.tpl @@ -19,6 +19,6 @@ "LogoutUrl": { "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", "values": [ "java.util.LinkedHashSet", [ "{{LOGOUT_URL}}" ] ] - }, + } } } \ No newline at end of file diff --git a/resources/etc/cas/config/services/oauth-service-template.json.tpl b/resources/etc/cas/config/services/oauth-service-template.json.tpl index 7a7bbdff..db31bf2c 100644 --- a/resources/etc/cas/config/services/oauth-service-template.json.tpl +++ b/resources/etc/cas/config/services/oauth-service-template.json.tpl @@ -21,6 +21,6 @@ "LogoutUrl": { "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", "values": [ "java.util.LinkedHashSet", [ "{{LOGOUT_URL}}" ] ] - }, + } } } \ No newline at end of file From 40f63d9bbd94656914cb4fa78314963a7bb9decd Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Thu, 24 Oct 2024 22:16:44 +0200 Subject: [PATCH 10/50] adjust services when update to fqdn has occurred --- resources/create-sa.sh | 11 +++--- resources/remove-sa.sh | 4 +-- resources/startup.sh | 4 +++ resources/util.sh | 80 +++++++++++++++++++++++++++++++++++------- 4 files changed, 78 insertions(+), 21 deletions(-) diff --git a/resources/create-sa.sh b/resources/create-sa.sh index 6e4b5783..d9b07ebd 100755 --- a/resources/create-sa.sh +++ b/resources/create-sa.sh @@ -19,11 +19,10 @@ source util.sh echo "Create sa for ${SERVICE} with account type: ${TYPE}..." - #FQDN="192.168.56.2" FQDN=$(doguctl config -g fqdn) # escape fqdn to use it within regex - EFQDN=$(escape_dots "$FQDN") - SERVICE_ID=$(find_next_serviceID "$SERVICE_REGISTRY") + EFQDN=$(escapeDots "$FQDN") + SERVICE_ID=$(findNextServiceID "$SERVICE_REGISTRY_PRODUCTION") # build logout url LOGOUT_URL="https://${FQDN}/${SERVICE}${LOGOUT_URI:-}" @@ -44,9 +43,7 @@ source util.sh fi CLIENT_SECRET=$(doguctl random -l 16) - #CLIENT_SECRET="secret" CLIENT_SECRET_HASH=$(echo -n "${CLIENT_SECRET}" | sha256sum | awk '{print $1}') - #CLIENT_SECRET_HASH="HASH" # Using `sed` to replace placeholders sed -e "s|{{SERVICE}}|$SERVICE|g" \ @@ -55,7 +52,7 @@ source util.sh -e "s|{{TEMPLATES}}|$(IFS=, ; echo "${TEMPLATES[*]}")|g" \ -e "s|{{CLIENT_SECRET_HASH}}|$CLIENT_SECRET_HASH|g" \ -e "s|{{SERVICE_CLASS}}|$SERVICE_CLASS|g" \ - -e "s|{{LOGOUT_URL}}|$LOGOUT_URL|g" etc/cas/config/services/oauth-service-template.json.tpl > $SERVICE_REGISTRY/${SERVICE}-${SERVICE_ID}.json + -e "s|{{LOGOUT_URL}}|$LOGOUT_URL|g" etc/cas/config/services/oauth-service-template.json.tpl > $SERVICE_REGISTRY_PRODUCTION/${SERVICE}-${SERVICE_ID}.json doguctl config "service_accounts/${TYPE}/${SERVICE}/secret" "${CLIENT_SECRET_HASH}" elif [ "${TYPE}" == "cas" ]; then @@ -70,7 +67,7 @@ source util.sh -e "s|{{SERVICE_ID}}|$SERVICE_ID|g" \ -e "s|{{FQDN}}|$EFQDN|g" \ -e "s|{{TEMPLATES}}|$(IFS=, ; echo "${TEMPLATES[*]}")|g" \ - -e "s|{{LOGOUT_URL}}|$LOGOUT_URL|g" etc/cas/config/services/cas-service-template.json.tpl > $SERVICE_REGISTRY/${SERVICE}-${SERVICE_ID}.json + -e "s|{{LOGOUT_URL}}|$LOGOUT_URL|g" etc/cas/config/services/cas-service-template.json.tpl > $SERVICE_REGISTRY_PRODUCTION/${SERVICE}-${SERVICE_ID}.json else echo "only the account_types: oidc, oauth, cas are allowed" diff --git a/resources/remove-sa.sh b/resources/remove-sa.sh index dbf0cdb3..a04f9b88 100755 --- a/resources/remove-sa.sh +++ b/resources/remove-sa.sh @@ -33,7 +33,7 @@ if [ -n "${LOGOUT_URI+x}" ]; then fi echo "Removing service ${SERVICE} from JSON registry ${SERVICE_REGISTRY}" -FILES=$(ls $SERVICE_REGISTRY/${SERVICE}-*.json 2>/dev/null || echo "") +FILES=$(ls $SERVICE_REGISTRY_PRODUCTION/${SERVICE}-*.json 2>/dev/null || echo "") # Check if FILES is empty before counting if [ -z "$FILES" ]; then @@ -42,6 +42,6 @@ else # Count the number of matching files FILE_COUNT=$(echo "$FILES" | wc -l) echo "Found $FILE_COUNT file(s) matching service ${SERVICE}." - rm $SERVICE_REGISTRY/${SERVICE}-*.json + rm $SERVICE_REGISTRY_PRODUCTION/${SERVICE}-*.json echo "Successfully deleted service ${SERVICE}." fi \ No newline at end of file diff --git a/resources/startup.sh b/resources/startup.sh index 2f8adae8..a96d8819 100755 --- a/resources/startup.sh +++ b/resources/startup.sh @@ -24,6 +24,10 @@ while [[ "$(doguctl config "local_state" -d "empty")" == "upgrading" ]]; do sleep 3 done +# check whether fqdn has changed and update services +echo "check for fqdn updates" +checkFqdnUpdate + # If an error occurs in logging.sh the whole scripting quits because of -o errexit. Catching the sourced exit code # leads to an zero exit code which enables further error handling. loggingExitCode=0 diff --git a/resources/util.sh b/resources/util.sh index 27763edf..5a9d4b12 100755 --- a/resources/util.sh +++ b/resources/util.sh @@ -4,7 +4,9 @@ set -o nounset set -o pipefail # Defines the path to store service definitions -SERVICE_REGISTRY="etc/cas/services/production" +SERVICE_REGISTRY="etc/cas/services" +SERVICE_REGISTRY_PRODUCTION="${SERVICE_REGISTRY}"/production +SERVICE_REGISTRY_DEVELOPMENT="${SERVICE_REGISTRY}"/development # This function prints an error to the console and waits 5 minutes before exiting the process. # Requires two arguments: @@ -166,8 +168,39 @@ function configureCAS() { renderCustomMessagesTpl } +function checkFqdnUpdate() { + # Copy fqdn from global to local config so we can detect changes to it + if [ "$(doguctl config "fqdn" -d "empty")" == "empty" ]; then + doguctl config "fqdn" "$(doguctl config -g "fqdn")" + return 0 + fi + + local globalFQDN=$(doguctl config -g fqdn) + local localFQDN=$(doguctl config fqdn) + + if [ "$localFQDN" == "$globalFQDN" ]; then + return 0 + fi + + echo "FQDN has change, update services ..." + + doguctl config "fqdn" "$globalFQDN" + updateFqdnInServices $globalFQDN +} + +# Function to double-escape dots in the FQDN to use it within a regex of the service registry +function escapeDots() { + local fqdn="$1" + + # Use parameter substitution to replace each '.' with '\\.' + local escaped_fqdn="${fqdn//./\\\\\\\\\\\\\\\\.}" + + # Return the double-escaped FQDN + echo "$escaped_fqdn" +} + # Function to find the next serviceID from JSON filenames in the service registry -find_next_serviceID() { +function findNextServiceID() { local dir="$1" # Initialize max_number as 0 @@ -194,13 +227,36 @@ find_next_serviceID() { echo "$next_number" } -# Function to double-escape dots in the FQDN to use it within a regex of the service registry -escape_dots() { - local fqdn="$1" - - # Use parameter substitution to replace each '.' with '\\.' - local escaped_fqdn="${fqdn//./\\\\\\\\\\\\\\\\.}" - - # Return the double-escaped FQDN - echo "$escaped_fqdn" -} +# Function to update the services in the registry with the provided fqdn +function updateFqdnInServices() { + echo "Updating services with new fqdn ${1}" + + local nFQDN=$(escapeDots "${1}") + local tmpFqdnService=/tmp/new-fqdn.json + + # create temporary new service with new fqdn property + sed -e 's|{{SERVICE}}||g' \ + -e "s|{{SERVICE_ID}}|0|g" \ + -e "s|{{FQDN}}|$nFQDN|g" \ + -e 's|{{TEMPLATES}}||g' \ + -e 's|{{LOGOUT_URL}}||g' etc/cas/config/services/cas-service-template.json.tpl > $tmpFqdnService + + # Extract the Fqdn value from the source JSON + local fqdnObject=$(jq '.properties.Fqdn' "$tmpFqdnService") + + # Loop through production service files + for service in "$SERVICE_REGISTRY_PRODUCTION"/*.json; do + # Check if the file exists + if [ -f "$service" ]; then + # Update the Fqdn property in the target service with the extracted fqdn object + jq --argjson fqdn "$fqdnObject" '.properties.Fqdn = $fqdn' "$service" > /tmp/updateService.json && mv /tmp/updateService.json "$service" + echo "Updated FQDN in service $service." + else + echo "No target files found." + fi + done + + rm $tmpFqdnService + + echo "Successfully finished fqdn update." +} \ No newline at end of file From 92a74cbc7e909981730d193974230c0ba5314849 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Fri, 25 Oct 2024 14:35:35 +0200 Subject: [PATCH 11/50] upgrade java base image to use new doguctl version --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 118cb0b9..d665f0c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,7 +41,7 @@ RUN apk update && apk add wget && wget -O "apache-tomcat-${TOMCAT_VERSION}.tar. # registry.cloudogu.com/official/cas -FROM registry.cloudogu.com/official/java:21.0.4-1 +FROM registry.cloudogu.com/official/java:21.0.4-4 LABEL NAME="official/cas" \ VERSION="7.0.8-2" \ From 9e49280501ac4db75336f4e594cea2dd91b8fef1 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Fri, 25 Oct 2024 14:42:50 +0200 Subject: [PATCH 12/50] add migration step for json service registry --- resources/post-upgrade.sh | 83 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/resources/post-upgrade.sh b/resources/post-upgrade.sh index 0a901c97..17803578 100755 --- a/resources/post-upgrade.sh +++ b/resources/post-upgrade.sh @@ -3,6 +3,8 @@ set -o errexit set -o nounset set -o pipefail +source util.sh + checkSameVersion() { echo "Checking the CAS versions..." if [ "${FROM_VERSION}" = "${TO_VERSION}" ]; then @@ -137,6 +139,86 @@ migrateServiceAccounts() { echo "Migrating service accounts... Done!" } +migrateServicesFromETCD() { + echo "Start to migrate services from etcd to json registry..." + + if [[ $(doguctl config "service_accounts/migrated" -d "false") == "true" ]]; then + echo "Service accounts have already been migrated to json service registry, skip migration." + return 0 + fi + + # Declare associative arrays to hold values for each application + declare -A types + declare -A secrets + declare -A logout_uris + + keys=$(doguctl ls service_accounts) + + # Loop through keys representing service values + for key in $keys; do + # Check if the key ends with 'secret', 'created' or 'logout_uri + if [[ $key == *"/secret" || $key == *"/created" || $key == *"/logout_uri" ]]; then + # Extract the type (two levels above 'secret' or 'created') + type=$(echo "$key" | awk -F'/' '{print $(NF-2)}') + + # Extract the application name (entry before 'secret', 'created' or 'logout_uri') + app=$(echo "$key" | awk -F'/' '{print $(NF-1)}') + + # Get the value associated with this key + value=$(doguctl config "$key") + + echo "Type: $type" + echo "Application: $app" + echo "Key: $key" + echo "Value: $value" + echo "--------------------" + + # Store values based on the key type + types["$app"]="$type" + if [[ $key == *"/secret" ]]; then + secrets["$app"]="$value" + elif [[ $key == *"/logout_uri" ]]; then + logout_uris["$app"]="$value" + fi + fi + done + + # migrate extracted services to json registry + for app in "${!types[@]}"; do + account_type="${types[$app]}" + logout_uri=${logout_uris[$app]:-} # This will be empty if not set + secret=${secrets[$app]:-} # This will be empty if not set + + if [ -n "$logout_uri" ]; then + # Assuming the random secret is in the generated file, replace it with your secret + ./create-sa.sh "$account_type" "$logout_uri" "$app" + else + ./create-sa.sh "$account_type" "$app" + fi + + echo "created json service $app from type $account_type" + + # Now replace the random secret in the generated JSON with your extracted secret + if [ -n "$secret" ]; then + # Assume the created file name format is -.json + json_output_file=$(ls -1 "$SERVICE_REGISTRY_PRODUCTION"/${app}-*.json | head -n 1) # Get the most recently created file + + if [[ -f "$json_output_file" ]]; then + # Assuming the random secret is in the generated file, replace it with secret from migration + sed -i "s|\"clientSecret\": \".*\"|\"clientSecret\": \"$secret\"|" "$json_output_file" + else + echo "Error: JSON file for $app not found." + fi + fi + + echo "Service configuration for $app created." + done + + doguctl config "service_accounts/migrated" "true" + + echo "Migration completed. Individual files created for each application." +} + runPostUpgrade() { FROM_VERSION="${1}" TO_VERSION="${2}" @@ -148,6 +230,7 @@ runPostUpgrade() { checkSameVersion removeDeprecatedKeys migrateServiceAccounts + migrateServicesFromETCD echo "Set registry flag so startup script can start afterwards..." doguctl state "upgrade done" From 055cdf50f8442c2e985f70fb715607dd0643c708 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Fri, 25 Oct 2024 14:47:41 +0200 Subject: [PATCH 13/50] use volume for services --- dogu.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dogu.json b/dogu.json index 9ade393a..453d2e6c 100644 --- a/dogu.json +++ b/dogu.json @@ -392,6 +392,13 @@ "Owner": "1000", "Group": "1000", "NeedsBackup": true + }, + { + "Name": "services", + "Path": "/etc/cas/services/production", + "Owner": "1000", + "Group": "1000", + "NeedsBackup": true } ], "ExposedCommands": [ From 552a047257a0199a7440212eaa87ee6d598269d5 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Fri, 25 Oct 2024 14:58:36 +0200 Subject: [PATCH 14/50] update CHANGELOG.md --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f678743..88b85847 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- Use JSON service registry [#221] + - services are read from and stored in json files instead of local config + - native implementation from CAS is used for this, which reduces custom overlay implementation +- Changed logic to create and remove service accounts [#221] + +### Removed +- Reading service information directly from ETCD [#221] + - Removed java classes for service creation ## [v7.0.8-2] - 2024-10-02 ### Fixed From 4fe6cbadc226dfa387b1c60ab30bc4e0e1c6b88f Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Fri, 25 Oct 2024 15:03:57 +0200 Subject: [PATCH 15/50] update release_notes --- docs/gui/release_notes_de.md | 4 ++++ docs/gui/release_notes_en.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/gui/release_notes_de.md b/docs/gui/release_notes_de.md index b8779e1d..6b9dc990 100644 --- a/docs/gui/release_notes_de.md +++ b/docs/gui/release_notes_de.md @@ -4,6 +4,10 @@ Im Folgenden finden Sie die Release Notes für das CAS-Dogu. Technische Details zu einem Release finden Sie im zugehörigen [Changelog](https://docs.cloudogu.com/de/docs/dogus/cas/CHANGELOG/). +## [Unreleased] +- Das Dogu wurde intern auf eine JSON Registry umgestellt, wodurch sich die Logik zum Anlegen und Löschen von Service-Accounts geändert hat. +- Einheitliche Verwendung von Service-Accounts sowohl in einer Multinode- als Singlenode-Umgebung. + ## Release 7.0.8-2 Es wurde ein technischer Fehler behoben der in Multinode-Umgebungen verhindert hat, dass Dogus mit Service-Accounts `cas` erreichbar sind. diff --git a/docs/gui/release_notes_en.md b/docs/gui/release_notes_en.md index ba19608d..55318eed 100644 --- a/docs/gui/release_notes_en.md +++ b/docs/gui/release_notes_en.md @@ -4,6 +4,10 @@ Below you will find the release notes for CAS-Dogu. Technical details on a release can be found in the corresponding [Changelog](https://docs.cloudogu.com/de/docs/dogus/cas/CHANGELOG/). +## [Unreleased] +- The Dogu has been internally converted to a JSON registry, which has changed the logic for creating and deleting service accounts. +- Consistent use of service accounts in both multinode and singlenode environments. + ## Release 7.0.8-2 Resolved a technical issue in multinode environment, that caused that dogus with service accounts `cas` are not available. From be495867dafce4ff949d0aea0eba487c1775d935 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Fri, 25 Oct 2024 19:57:56 +0200 Subject: [PATCH 16/50] update makefiles to 9.3.2 --- Makefile | 2 +- build/make/build.mk | 2 +- build/make/k8s-dogu.tpl | 2 +- build/make/static-analysis.mk | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 3a6b9089..bceea0bb 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -MAKEFILES_VERSION=9.2.1 +MAKEFILES_VERSION=9.3.2 .DEFAULT_GOAL:=dogu-release diff --git a/build/make/build.mk b/build/make/build.mk index 857c11d4..d3581de3 100644 --- a/build/make/build.mk +++ b/build/make/build.mk @@ -3,7 +3,7 @@ ADDITIONAL_LDFLAGS?=-extldflags -static LDFLAGS?=-ldflags "$(ADDITIONAL_LDFLAGS) -X main.Version=$(VERSION) -X main.CommitID=$(COMMIT_ID)" GOIMAGE?=golang -GOTAG?=1.22 +GOTAG?=1.23 GOOS?=linux GOARCH?=amd64 PRE_COMPILE?= diff --git a/build/make/k8s-dogu.tpl b/build/make/k8s-dogu.tpl index 296da650..91e2bb2f 100644 --- a/build/make/k8s-dogu.tpl +++ b/build/make/k8s-dogu.tpl @@ -1,4 +1,4 @@ -apiVersion: k8s.cloudogu.com/v1 +apiVersion: k8s.cloudogu.com/v2 kind: Dogu metadata: name: NAME diff --git a/build/make/static-analysis.mk b/build/make/static-analysis.mk index 0ed0de33..00c406f2 100644 --- a/build/make/static-analysis.mk +++ b/build/make/static-analysis.mk @@ -2,12 +2,12 @@ STATIC_ANALYSIS_DIR=$(TARGET_DIR)/static-analysis GOIMAGE?=golang -GOTAG?=1.22 +GOTAG?=1.23 CUSTOM_GO_MOUNT?=-v /tmp:/tmp REVIEW_DOG=$(TMP_DIR)/bin/reviewdog LINT=$(TMP_DIR)/bin/golangci-lint -LINT_VERSION?=v1.58.2 +LINT_VERSION?=v1.61.0 # ignore tests and mocks LINTFLAGS=--tests=false --exclude-files="^.*_mock.go$$" --exclude-files="^.*/mock.*.go$$" --timeout 10m --issues-exit-code 0 ADDITIONAL_LINTER=-E bodyclose -E containedctx -E contextcheck -E decorder -E dupl -E errname -E forcetypeassert -E funlen -E unparam From 1a9374a7186fda3b25d0af1ccc6f0bf75e5b34a5 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Sat, 26 Oct 2024 17:16:58 +0200 Subject: [PATCH 17/50] Add documentation for service registry. --- docs/development/Service_Registry_de.md | 21 +++++++++++++++++++++ docs/development/Service_Registry_en.md | 18 ++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 docs/development/Service_Registry_de.md create mode 100644 docs/development/Service_Registry_en.md diff --git a/docs/development/Service_Registry_de.md b/docs/development/Service_Registry_de.md new file mode 100644 index 00000000..8d71d9d6 --- /dev/null +++ b/docs/development/Service_Registry_de.md @@ -0,0 +1,21 @@ +# Service Registry + +CAS erlaubt es Services zu deklarieren, die für eine CAS-Authentifizierung genutzt werden dürfen. Die Service-Registry +speichert die Services zusammen mit Metadaten und steuert somit das Verhalten des CAS. + +Für die Service-Registry gibt es [verschiedene Implementierungen](https://apereo.github.io/cas/7.0.x/services/Service-Management.html#storage), die genutzt werden können. +Aktuell wird von uns die [JSON Service-Registry](https://apereo.github.io/cas/7.0.x/services/JSON-Service-Management.html) genutzt, bei der die Services als JSON abgelegt werden und zur Laufzeit in den Speicher geladen werden. + +Für die Registry wird ein zentraler Ablageort definiert, der sich jedoch in Abhängigkeit von der aktuell genutzten [Stage](develop_stage_de.md) unterscheiden kann. +Befindet sich der CAS im Produktionsmodus, werden die Services vom Pfad `/etc/cas/services/production` geladen. Dabei werden die Services dynamisch, basierend auf den tatsächlich +installierten Dogus, erstellt. Für den [Entwicklungsmodus](develop_stage_de.md) sind statische Services für die Protokolle CAS, OAUTH und OIDC unter dem Pfad `/etc/cas/services/development` +deklariert, die generisch für die jeweiligen Protokolle gelten. Eine Unterscheidung zwischen einzelnen Applikationen findet nicht statt. + +Services werden in der Regel über den Installationsprozess eines Dogus erzeugt. Besitzt ein Dogu eine Abhängigkeit auf den CAS, wird während der Installation das ExposedCommand `service-account-create` +aufgerufen, welches den Service als JSON-Konfiguration in der Service-Registry erstellt. Hierfür werden Konfigurationsvorlagen verwendet, die unter dem Pfad `/etc/cas/config/services` hinterlegt sind. Anhängig von den +Eingabeparametern sowie dem Typ des genutzten Protokolls werden verschiedene [Properties](https://apereo.github.io/cas/7.0.x/services/Configuring-Service-Custom-Properties.html) befüllt, die dann wiederum von [CAS-internen Templates](https://apereo.github.io/cas/7.0.x/services/Configuring-Service-Template-Definitions.html) genutzt werden. Diese Templates werden über ihren Namen +referenziert und dienen als Vorlage für die im Speicher erzeugten Services des CAS. Sie sind unter `/etc/cas/services/templates` zu finden. Die final erzeugten JSON-Dateien in der Service-Registry folgen +dabei stets der Namenskonvention `-.json`. + + + diff --git a/docs/development/Service_Registry_en.md b/docs/development/Service_Registry_en.md new file mode 100644 index 00000000..d1809675 --- /dev/null +++ b/docs/development/Service_Registry_en.md @@ -0,0 +1,18 @@ +# Service Registry + +CAS allows you to declare services that may be used for CAS authentication. The service registry stores the services +together with metadata and thus controls the behavior of the CAS. + +There are [various implementations](https://apereo.github.io/cas/7.0.x/services/Service-Management.html#storage) that can be used for the service registry. +We currently use the [JSON Service Registry](https://apereo.github.io/cas/7.0.x/services/JSON-Service-Management.html), in which the services are stored as JSON and are loaded into the memory at runtime. + +A central storage location is defined for the registry, but this may differ depending on the [Stage](develop_stage_en.md) currently used. +If the CAS is in production mode, the services are loaded from the path `/etc/cas/services/production`. The services are created dynamically based on the actually installed Dogus. +For [development mode](develop_stage_en.md), static services for the protocols CAS, OAUTH and OIDC are declared under the path `/etc/cas/services/development`, which apply generically for the respective protocols. +There is no differentiation between individual applications. + +Services are usually created during the installation process of a dogu. If a dogu has a dependency on the CAS, the ExposedCommand `service-account-create` is called during the installation, which creates the service as a JSON configuration in the service registry. +Configuration templates are used for this, which are stored under the path `/etc/cas/config/services`. Depending on the input parameters and the type of protocol used, various [Properties](https://apereo.github.io/cas/7.0.x/services/Configuring-Service-Custom-Properties.html) are filled, +which are then used by [CAS internal templates](https://apereo.github.io/cas/7.0.x/services/Configuring-Service-Template-Definitions.html). +These templates are referenced by their name and serve as a template for the CAS services generated in the memory. They can be found under `/etc/cas/services/templates`. +The final generated JSON files in the service registry always follow the naming convention `-.json`. \ No newline at end of file From ba223c1bb28396763ccaa8f204c7321cad1ec645 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Sat, 26 Oct 2024 17:31:55 +0200 Subject: [PATCH 18/50] Add static services for development --- .../cas/services/development/all-10000001.json | 12 ------------ .../cas/services/development/cas-10000001.json | 17 +++++++++++++++++ .../services/development/oauth-10000002.json | 17 +++++++++++++++++ .../cas/services/development/oidc-10000003.json | 17 +++++++++++++++++ 4 files changed, 51 insertions(+), 12 deletions(-) delete mode 100644 resources/etc/cas/services/development/all-10000001.json create mode 100644 resources/etc/cas/services/development/cas-10000001.json create mode 100644 resources/etc/cas/services/development/oauth-10000002.json create mode 100644 resources/etc/cas/services/development/oidc-10000003.json diff --git a/resources/etc/cas/services/development/all-10000001.json b/resources/etc/cas/services/development/all-10000001.json deleted file mode 100644 index cae297ea..00000000 --- a/resources/etc/cas/services/development/all-10000001.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "@class" : "org.apereo.cas.services.CasRegisteredService", - "serviceId" : "^(https|http)://.*", - "name" : "all", - "id" : 10000001, - "description": "development service allowing any https / http access to cas", - "templateName": "AllowServiceProxyPolicy", - "attributeReleasePolicy" : { - "@class" : "org.apereo.cas.services.ReturnAllAttributeReleasePolicy" - }, - "logoutType" : "BACK_CHANNEL" -} \ No newline at end of file diff --git a/resources/etc/cas/services/development/cas-10000001.json b/resources/etc/cas/services/development/cas-10000001.json new file mode 100644 index 00000000..3552726e --- /dev/null +++ b/resources/etc/cas/services/development/cas-10000001.json @@ -0,0 +1,17 @@ +{ + "@class" : "org.apereo.cas.services.CasRegisteredService", + "serviceId" : "^(https|http)://.*", + "name" : "CAS", + "id" : 10000001, + "description": "development service allowing any cas service https / http access to cas", + "attributeReleasePolicy" : { + "@class" : "org.apereo.cas.services.ReturnAllAttributeReleasePolicy" + }, + "proxyPolicy" : { + "@class" : "org.apereo.cas.services.RegexMatchingRegisteredServiceProxyPolicy", + "pattern" : "^https?://.*", + "useServiceId": false, + "exactMatch": false + }, + "logoutType" : "BACK_CHANNEL" +} \ No newline at end of file diff --git a/resources/etc/cas/services/development/oauth-10000002.json b/resources/etc/cas/services/development/oauth-10000002.json new file mode 100644 index 00000000..ebe7d589 --- /dev/null +++ b/resources/etc/cas/services/development/oauth-10000002.json @@ -0,0 +1,17 @@ +{ + "@class" : "org.apereo.cas.support.oauth.services.OAuthRegisteredService", + "serviceId" : "^(https|http)://.*", + "name" : "OAuth", + "id" : 10000002, + "description": "development service allowing any oauth service https / http access to cas", + "clientId": "oauth", + /* client secret is "oauth" */ + "clientSecret": "6e306c515177ca5a1968fe96b4d727833af2214eaf0922dfda29a1d0d063cf34", + "attributeReleasePolicy" : { + "@class" : "org.apereo.cas.services.ReturnAllAttributeReleasePolicy" + }, + "logoutType" : "BACK_CHANNEL", + "bypassApprovalPrompt": true, + "supportedResponseTypes": [ "java.util.HashSet", [ "code" ] ], + "supportedGrantTypes": [ "java.util.HashSet", [ "authorization_code" ] ] +} \ No newline at end of file diff --git a/resources/etc/cas/services/development/oidc-10000003.json b/resources/etc/cas/services/development/oidc-10000003.json new file mode 100644 index 00000000..01997c77 --- /dev/null +++ b/resources/etc/cas/services/development/oidc-10000003.json @@ -0,0 +1,17 @@ +{ + "@class" : "org.apereo.cas.services.OidcRegisteredService", + "serviceId" : "^(https|http)://.*", + "name" : "OIDC", + "id" : 10000003, + "description": "development service allowing any oidc service https / http access to cas", + "clientId": "oidc", + /* client secret is "oidc" */ + "clientSecret": "4dcdefd0d389cd15882de8a808334bb06a586b7a74fd0932d0e13fdb945e223c", + "attributeReleasePolicy" : { + "@class" : "org.apereo.cas.services.ReturnAllAttributeReleasePolicy" + }, + "logoutType" : "BACK_CHANNEL", + "bypassApprovalPrompt": true, + "supportedResponseTypes": [ "java.util.HashSet", [ "code" ] ], + "supportedGrantTypes": [ "java.util.HashSet", [ "authorization_code" ] ] +} \ No newline at end of file From bfc251101f0e84c939f2bceff74da0b3b506df06 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Sat, 26 Oct 2024 17:32:16 +0200 Subject: [PATCH 19/50] remove general service --- resources/etc/cas/services/all-01.json | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 resources/etc/cas/services/all-01.json diff --git a/resources/etc/cas/services/all-01.json b/resources/etc/cas/services/all-01.json deleted file mode 100644 index 00970173..00000000 --- a/resources/etc/cas/services/all-01.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "@class" : "org.apereo.cas.services.RegexRegisteredService", - "serviceId" : "^(https|imaps|http)://.*", - "name" : "HTTPS and IMAPS", - "id" : 10000001, - "attributeReleasePolicy" : { - "@class" : "org.apereo.cas.services.ReturnAllAttributeReleasePolicy" - }, - "proxyPolicy" : { - "@class" : "org.apereo.cas.services.RegexMatchingRegisteredServiceProxyPolicy", - "pattern" : "^https?://.*" - } -} \ No newline at end of file From 75a612e42b08965d43e36396d9b83595f2744840 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Mon, 4 Nov 2024 16:15:49 +0100 Subject: [PATCH 20/50] Add CesServicesSpringConfiguration again for CesServiceMatchingStrategy --- app/src/main/resources/META-INF/spring.factories | 1 + ....springframework.boot.autoconfigure.AutoConfiguration.imports | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/resources/META-INF/spring.factories b/app/src/main/resources/META-INF/spring.factories index ace8a8af..f0152af8 100644 --- a/app/src/main/resources/META-INF/spring.factories +++ b/app/src/main/resources/META-INF/spring.factories @@ -1,4 +1,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + de.triology.cas.services.CesServicesSpringConfiguration,\ de.triology.cas.oidc.config.CesOidcConfiguration,\ de.triology.cas.oidc.config.CesOAuthConfiguration,\ de.triology.cas.ldap.CesAuthenticationEventExecutionPlanConfiguration,\ diff --git a/app/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/app/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 42b0bf83..54fcd6d9 100644 --- a/app/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/app/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,4 @@ + de.triology.cas.services.CesServicesSpringConfiguration de.triology.cas.oidc.config.CesOidcConfiguration de.triology.cas.oidc.config.CesOAuthConfiguration de.triology.cas.ldap.CesAuthenticationEventExecutionPlanConfiguration From a544584b16c6e2b600f601ccc4be764f0dd26536 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Mon, 4 Nov 2024 16:25:43 +0100 Subject: [PATCH 21/50] Limit CesServicesSpringConfiguration to ServiceMatchingStrategy --- .../CesServicesSpringConfiguration.java | 106 +----------------- 1 file changed, 1 insertion(+), 105 deletions(-) diff --git a/app/src/main/java/de/triology/cas/services/CesServicesSpringConfiguration.java b/app/src/main/java/de/triology/cas/services/CesServicesSpringConfiguration.java index 4698b9ba..1bd6a3a3 100644 --- a/app/src/main/java/de/triology/cas/services/CesServicesSpringConfiguration.java +++ b/app/src/main/java/de/triology/cas/services/CesServicesSpringConfiguration.java @@ -1,125 +1,21 @@ package de.triology.cas.services; import lombok.extern.slf4j.Slf4j; -import mousio.etcd4j.EtcdClient; import org.apereo.cas.authentication.principal.ServiceMatchingStrategy; import org.apereo.cas.configuration.CasConfigurationProperties; -import org.apereo.cas.services.*; -import org.apereo.cas.services.mgmt.DefaultChainingServicesManager; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - @Configuration("CesServicesSpringConfiguration") @ComponentScan("de.triology.cas.services") @EnableConfigurationProperties(CasConfigurationProperties.class) @Slf4j -public class CesServicesSpringConfiguration implements ServicesManagerExecutionPlanConfigurer { - - @Value("${ces.services.stage:production}") - private String stage; - - @Value("${ces.services.allowedAttributes}") - private List allowedAttributes; - - @Value("${ces.services.attributeMapping:#{\"\"}}") - private String attributesMappingRulesString; - - @Value("${ces.services.oidcPrincipalsAttribute:#{\"\"}}") - private String oidcPrincipalsAttribute; - - @Value("${cas.authn.pac4j.oidc[0].generic.enabled:#{false}}") - private boolean oidcAuthenticationDelegationEnabled; - - @Value("${cas.authn.pac4j.oidc[0].generic.client-name:#{\"\"}}") - private String oidcClientName; - - public EtcdClient createEtcdClient() { - EtcdClientFactory factory = new EtcdClientFactory(); - return factory.createDefaultClient(); - } - - @Bean(name = ServicesManager.BEAN_NAME) - public ChainingServicesManager servicesManager() { - DefaultChainingServicesManager chain = new DefaultChainingServicesManager(); - chain.registerServiceManager(configureServicesManager()); - return chain; - } +public class CesServicesSpringConfiguration { @Bean(name = "serviceMatchingStrategy") public ServiceMatchingStrategy serviceMatchingStrategy() { return new CesServiceMatchingStrategy(); } - - @Override - public ServicesManager configureServicesManager() { - Registry registry; - if (isMultinode()) { - registry = new RegistryLocal(); - } else { - EtcdClient etcdClient = createEtcdClient(); - registry = new RegistryEtcd(etcdClient); - } - - LOGGER.debug("------- Found attribute mappings [{}]", attributesMappingRulesString); - Map attributesMappingRules = propertyStringToMap(attributesMappingRulesString); - var managerConfig = new CesServiceManagerConfiguration(stage, allowedAttributes, attributesMappingRules, oidcAuthenticationDelegationEnabled, oidcClientName, oidcPrincipalsAttribute); - return new CesServicesManager(managerConfig, registry); - } - - private static boolean isMultinode() { - ProcessBuilder pb = new ProcessBuilder("/usr/bin/doguctl", "multinode"); - String result; - try { - Process process = pb.start(); - process.waitFor(); - BufferedReader reader = - new BufferedReader(new InputStreamReader(process.getInputStream())); - StringBuilder builder = new StringBuilder(); - String line; - while ( (line = reader.readLine()) != null) { - builder.append(line); - } - result = builder.toString(); - } catch (IOException e) { - LOGGER.error("failed to check if environment is multinode", e); - throw new RuntimeException(e); - } catch (InterruptedException e) { - LOGGER.error("failed to check if environment is multinode", e); - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - return "true".equals(result); - } - - /** - * Generates a map from a given property (string) of the following format: - * this.is.my.property.key=value1:key1,value2:key2 - * - * @param propertyString The content of the properties value - * @return Map - */ - public static Map propertyStringToMap(String propertyString) { - Map propertyMap = new HashMap<>(); - if (propertyString.isBlank()) { - return propertyMap; - } - - String[] rules = propertyString.split(","); - Arrays.stream(rules).forEach(rule -> { - String[] keyValue = rule.split(":"); - propertyMap.put(keyValue[0], keyValue[1]); - }); - return propertyMap; - } } From 6adb2085fe926aef5bce4a7bb9df4a45d17bdb72 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Mon, 4 Nov 2024 20:31:04 +0100 Subject: [PATCH 22/50] Remove allowedAttributes from config The attributes defined in allowedAttributes are statically set without the option to configure them. The attributes defined are now set in the DefaultAttributeReleasePolicy.json --- app/etc/cas/config/cas.properties | 1 - resources/etc/cas/config/cas.properties.tpl | 1 - 2 files changed, 2 deletions(-) diff --git a/app/etc/cas/config/cas.properties b/app/etc/cas/config/cas.properties index 760a05e5..013bf1fe 100644 --- a/app/etc/cas/config/cas.properties +++ b/app/etc/cas/config/cas.properties @@ -76,7 +76,6 @@ cas.authn.ldap[0].use-start-tls=false cas.authn.ldap[0].principal-attribute-id=uid cas.authn.ldap[0].principal-attribute-list=uid:username,cn,mail:mail,givenName:givenName,sn:surname,displayName,memberOf:groups cas.authn.attributeRepository.ldap[0].attributes.groups=memberOf -ces.services.allowedAttributes=username,cn,mail,givenName,surname,displayName,groups #======================================== # LDAP connection pool configuration diff --git a/resources/etc/cas/config/cas.properties.tpl b/resources/etc/cas/config/cas.properties.tpl index eee15abf..20a438be 100644 --- a/resources/etc/cas/config/cas.properties.tpl +++ b/resources/etc/cas/config/cas.properties.tpl @@ -68,7 +68,6 @@ cas.authn.ldap[0].use-start-tls={{ .Env.Get "LDAP_STARTTLS" }} cas.authn.ldap[0].principal-attribute-id={{ .Config.Get "ldap/attribute_id"}} cas.authn.ldap[0].principal-attribute-list={{ .Config.Get "ldap/attribute_id"}}:username,cn,{{ .Config.Get "ldap/attribute_mail"}}:mail,{{ .Config.GetOrDefault "ldap/given_name" "givenName"}}:givenName,{{ .Config.GetOrDefault "ldap/surname" "sn"}}:surname,displayName,{{ .Config.Get "ldap/attribute_group"}}:groups cas.authn.attributeRepository.ldap[0].attributes.groups={{ .Config.Get "ldap/attribute_group"}} -ces.services.allowedAttributes=username,cn,mail,givenName,surname,displayName,groups #======================================== # LDAP connection pool configuration From a52d3ec52fee1205053236ce6475f8f94e4cdfc6 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Mon, 4 Nov 2024 21:01:55 +0100 Subject: [PATCH 23/50] Fix unit tests after deleting service factories --- ...esOAuthSingleLogoutMessageCreatorTest.java | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/app/src/test/java/de/triology/cas/oidc/beans/CesOAuthSingleLogoutMessageCreatorTest.java b/app/src/test/java/de/triology/cas/oidc/beans/CesOAuthSingleLogoutMessageCreatorTest.java index c5f64c9b..e1494c88 100644 --- a/app/src/test/java/de/triology/cas/oidc/beans/CesOAuthSingleLogoutMessageCreatorTest.java +++ b/app/src/test/java/de/triology/cas/oidc/beans/CesOAuthSingleLogoutMessageCreatorTest.java @@ -1,19 +1,15 @@ package de.triology.cas.oidc.beans; -import de.triology.cas.oidc.services.CesOAuthServiceFactory; -import de.triology.cas.services.CesServiceData; import org.apereo.cas.logout.slo.SingleLogoutExecutionRequest; import org.apereo.cas.logout.slo.SingleLogoutMessage; import org.apereo.cas.logout.slo.SingleLogoutRequestContext; import org.apereo.cas.services.OidcRegisteredService; -import org.apereo.cas.services.RegisteredService; import org.apereo.cas.ticket.Ticket; import org.apereo.cas.ticket.TicketGrantingTicket; import org.apereo.cas.ticket.accesstoken.OAuth20AccessToken; import org.apereo.cas.ticket.registry.TicketRegistry; import org.junit.Test; -import java.net.URI; import java.util.*; import static org.junit.Assert.assertEquals; @@ -42,18 +38,17 @@ public void testCreate_Successful() throws Exception { CesOAuthSingleLogoutMessageCreator builder = new CesOAuthSingleLogoutMessageCreator(ticketRegistryMock); // given - data - var factory = new CesOAuthServiceFactory<>(OidcRegisteredService::new); - Map serviceAttributes = new HashMap<>(); - serviceAttributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_ID, "testOAuthClient"); - serviceAttributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH, "clientSecret"); - CesServiceData expectedData = new CesServiceData("testOAuthClient", factory, serviceAttributes); - RegisteredService expectedService = factory.createNewService(1, "localhost", URI.create("org/custom/logout"), expectedData); + OidcRegisteredService service = new OidcRegisteredService(); + service.setId(1); + service.setClientId("testOAuthClient"); + service.setClientSecret("testClientSecret"); + service.setLogoutUrl("org/custom/logout"); // given - mocks SingleLogoutRequestContext contextMock = mock(SingleLogoutRequestContext.class); SingleLogoutExecutionRequest singleLogoutExecutionRequestMock = mock(SingleLogoutExecutionRequest.class); - when(contextMock.getRegisteredService()).thenReturn(expectedService); + when(contextMock.getRegisteredService()).thenReturn(service); when(contextMock.getExecutionRequest()).thenReturn(singleLogoutExecutionRequestMock); when(singleLogoutExecutionRequestMock.getTicketGrantingTicket()).thenReturn(tgtMock); @@ -87,18 +82,17 @@ public void testCreate_WrongAtTicket() throws Exception { CesOAuthSingleLogoutMessageCreator builder = new CesOAuthSingleLogoutMessageCreator(ticketRegistryMock); // given - data - var factory = new CesOAuthServiceFactory<>(OidcRegisteredService::new); - Map serviceAttributes = new HashMap<>(); - serviceAttributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_ID, "testOAuthClient"); - serviceAttributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH, "clientSecret"); - CesServiceData expectedData = new CesServiceData("testOAuthClient", factory, serviceAttributes); - RegisteredService expectedService = factory.createNewService(1, "localhost", URI.create("org/custom/logout"), expectedData); + OidcRegisteredService service = new OidcRegisteredService(); + service.setId(1); + service.setClientId("testOAuthClient"); + service.setClientSecret("testClientSecret"); + service.setLogoutUrl("org/custom/logout"); // given - mocks SingleLogoutRequestContext contextMock = mock(SingleLogoutRequestContext.class); SingleLogoutExecutionRequest singleLogoutExecutionRequestMock = mock(SingleLogoutExecutionRequest.class); - when(contextMock.getRegisteredService()).thenReturn(expectedService); + when(contextMock.getRegisteredService()).thenReturn(service); when(contextMock.getExecutionRequest()).thenReturn(singleLogoutExecutionRequestMock); when(singleLogoutExecutionRequestMock.getTicketGrantingTicket()).thenReturn(tgtMock); From b26c3e07fae701c8253bb56ca8bdfa0ea9cc923a Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Tue, 5 Nov 2024 19:11:58 +0100 Subject: [PATCH 24/50] Add breaking change information to release notes --- docs/gui/release_notes_de.md | 5 ++++- docs/gui/release_notes_en.md | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/gui/release_notes_de.md b/docs/gui/release_notes_de.md index 6b9dc990..ced50498 100644 --- a/docs/gui/release_notes_de.md +++ b/docs/gui/release_notes_de.md @@ -6,7 +6,10 @@ Technische Details zu einem Release finden Sie im zugehörigen [Changelog](https ## [Unreleased] - Das Dogu wurde intern auf eine JSON Registry umgestellt, wodurch sich die Logik zum Anlegen und Löschen von Service-Accounts geändert hat. -- Einheitliche Verwendung von Service-Accounts sowohl in einer Multinode- als Singlenode-Umgebung. +- Einheitliche Verwendung von Service-Accounts sowohl in einer Multinode- als Singlenode-Umgebung. + +### Breaking Change +- Neu zu installierende Dogus müssen explizit die Erstellung eines Serviceaccounts im CAS über die dogu.json anfordern. Weitere Informationen hierfür finden Sie in der [Entwicklerdokumentation](https://github.com/cloudogu/dogu-development-docs/blob/main/docs/important/relevant_functionalities_de.md#authentifizierung) ## Release 7.0.8-2 Es wurde ein technischer Fehler behoben der in Multinode-Umgebungen verhindert hat, dass Dogus mit Service-Accounts `cas` erreichbar sind. diff --git a/docs/gui/release_notes_en.md b/docs/gui/release_notes_en.md index 55318eed..4b4b9d79 100644 --- a/docs/gui/release_notes_en.md +++ b/docs/gui/release_notes_en.md @@ -8,6 +8,9 @@ Technical details on a release can be found in the corresponding [Changelog](htt - The Dogu has been internally converted to a JSON registry, which has changed the logic for creating and deleting service accounts. - Consistent use of service accounts in both multinode and singlenode environments. +## Breaking Change +- Newly installed dogus must explicitly request the creation of a service account in the CAS via dogu.json. Further information on this can be found in the [developer documentation](https://github.com/cloudogu/dogu-development-docs/blob/main/docs/important/relevant_functionalities_en.md#authentifizierung) + ## Release 7.0.8-2 Resolved a technical issue in multinode environment, that caused that dogus with service accounts `cas` are not available. From 6b3cbd07e3562aeb2669c44b281ae87bb781ce43 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Tue, 5 Nov 2024 19:14:26 +0100 Subject: [PATCH 25/50] Delete unnecessary files because of new service registry --- .../oidc/services/CesOAuthServiceFactory.java | 99 --- .../triology/cas/services/CesServiceData.java | 40 - .../CesServiceManagerConfiguration.java | 18 - .../cas/services/CesServicesManager.java | 200 ----- .../cas/services/CesServicesManagerStage.java | 111 --- .../CesServicesManagerStageDevelopment.java | 63 -- .../CesServicesManagerStageProductive.java | 200 ----- .../cas/services/EtcdClientFactory.java | 47 -- .../services/GetCasLogoutUriException.java | 11 - .../de/triology/cas/services/Registry.java | 78 -- .../triology/cas/services/RegistryEtcd.java | 288 ------- .../cas/services/RegistryException.java | 19 - .../triology/cas/services/RegistryLocal.java | 312 -------- .../ReturnMappedAttributesPolicy.java | 87 --- .../services/dogu/CesDoguServiceFactory.java | 51 -- .../dogu/CesServiceCreationException.java | 7 - .../cas/services/dogu/CesServiceFactory.java | 27 - .../services/CesOAuthServiceFactoryTest.java | 146 ---- .../services/CesOidcServiceFactoryTest.java | 131 ---- ...esServicesManagerStageDevelopmentTest.java | 37 - ...CesServicesManagerStageProductiveTest.java | 355 --------- .../cas/services/CesServicesManagerTest.java | 369 --------- .../CesServicesSpringConfigurationTest.java | 30 - .../cas/services/EtcdClientFactoryTest.java | 78 -- .../cas/services/RegistryEtcdTest.java | 356 --------- .../cas/services/RegistryLocalTest.java | 701 ------------------ .../ReturnMappedAttributesPolicyTest.java | 70 -- .../dogu/CesDoguServiceFactoryTest.java | 91 --- 28 files changed, 4022 deletions(-) delete mode 100644 app/src/main/java/de/triology/cas/oidc/services/CesOAuthServiceFactory.java delete mode 100644 app/src/main/java/de/triology/cas/services/CesServiceData.java delete mode 100644 app/src/main/java/de/triology/cas/services/CesServiceManagerConfiguration.java delete mode 100644 app/src/main/java/de/triology/cas/services/CesServicesManager.java delete mode 100644 app/src/main/java/de/triology/cas/services/CesServicesManagerStage.java delete mode 100644 app/src/main/java/de/triology/cas/services/CesServicesManagerStageDevelopment.java delete mode 100644 app/src/main/java/de/triology/cas/services/CesServicesManagerStageProductive.java delete mode 100644 app/src/main/java/de/triology/cas/services/EtcdClientFactory.java delete mode 100644 app/src/main/java/de/triology/cas/services/GetCasLogoutUriException.java delete mode 100644 app/src/main/java/de/triology/cas/services/Registry.java delete mode 100644 app/src/main/java/de/triology/cas/services/RegistryEtcd.java delete mode 100644 app/src/main/java/de/triology/cas/services/RegistryException.java delete mode 100644 app/src/main/java/de/triology/cas/services/RegistryLocal.java delete mode 100644 app/src/main/java/de/triology/cas/services/attributes/ReturnMappedAttributesPolicy.java delete mode 100644 app/src/main/java/de/triology/cas/services/dogu/CesDoguServiceFactory.java delete mode 100644 app/src/main/java/de/triology/cas/services/dogu/CesServiceCreationException.java delete mode 100644 app/src/main/java/de/triology/cas/services/dogu/CesServiceFactory.java delete mode 100644 app/src/test/java/de/triology/cas/oidc/services/CesOAuthServiceFactoryTest.java delete mode 100644 app/src/test/java/de/triology/cas/oidc/services/CesOidcServiceFactoryTest.java delete mode 100644 app/src/test/java/de/triology/cas/services/CesServicesManagerStageDevelopmentTest.java delete mode 100644 app/src/test/java/de/triology/cas/services/CesServicesManagerStageProductiveTest.java delete mode 100644 app/src/test/java/de/triology/cas/services/CesServicesManagerTest.java delete mode 100644 app/src/test/java/de/triology/cas/services/CesServicesSpringConfigurationTest.java delete mode 100644 app/src/test/java/de/triology/cas/services/EtcdClientFactoryTest.java delete mode 100644 app/src/test/java/de/triology/cas/services/RegistryEtcdTest.java delete mode 100644 app/src/test/java/de/triology/cas/services/RegistryLocalTest.java delete mode 100644 app/src/test/java/de/triology/cas/services/attributes/ReturnMappedAttributesPolicyTest.java delete mode 100644 app/src/test/java/de/triology/cas/services/dogu/CesDoguServiceFactoryTest.java diff --git a/app/src/main/java/de/triology/cas/oidc/services/CesOAuthServiceFactory.java b/app/src/main/java/de/triology/cas/oidc/services/CesOAuthServiceFactory.java deleted file mode 100644 index 43f6cdeb..00000000 --- a/app/src/main/java/de/triology/cas/oidc/services/CesOAuthServiceFactory.java +++ /dev/null @@ -1,99 +0,0 @@ -package de.triology.cas.oidc.services; - -import de.triology.cas.services.CesServiceData; -import de.triology.cas.services.dogu.CesDoguServiceFactory; -import de.triology.cas.services.dogu.CesServiceCreationException; -import de.triology.cas.services.dogu.CesServiceFactory; -import lombok.extern.slf4j.Slf4j; -import org.apereo.cas.services.BaseWebBasedRegisteredService; -import org.apereo.cas.support.oauth.services.OAuthRegisteredService; - -import java.net.URI; -import java.util.function.Supplier; - -/** - * This factory is responsible to create and to configure new OAuth services. - */ -@Slf4j -public class CesOAuthServiceFactory implements CesServiceFactory { - - private final Supplier supplier; - - public static final String ATTRIBUTE_KEY_OAUTH_CLIENT_ID = "oauth_client_id"; - public static final String ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH = "oauth_client_secret"; - - public CesOAuthServiceFactory(Supplier supplier) { - this.supplier = supplier; - } - - /** - * Creates a new empty service. - * - * @return the created service. - */ - protected T createEmptyService() { - return this.supplier.get(); - } - - /** - * OAUTH in CAS require one client for each OAUTH application. - * This method creates a new client with the given information. - * - * @param id internal ID of the service - * @param name name of the service - * @param serviceID a regex that describes which requests should be accepted (e.g., '${server.prefix}/dogu' - * only process request send over the named address) - * @param clientID public client id of the OAUTH application used for identification - * @param clientSecretHash secret key from the OAUTH application used for authentication - * @return a new client server for the given information of the OAUTH application - */ - protected T createOAUTHClientService(long id, String logoutURI, String name, String serviceID, String clientID, String clientSecretHash) { - var service = createEmptyService(); - service.setId(id); - service.setName(name); - service.setServiceId(serviceID); - if (logoutURI != null) { - service.setLogoutUrl(logoutURI); - } - - service.getSupportedResponseTypes().add("application/json"); - service.getSupportedResponseTypes().add("code"); - service.getSupportedGrantTypes().add("authorization_code"); - service.setClientId(clientID); - service.setClientSecret(clientSecretHash); - service.setBypassApprovalPrompt(true); - - String clientSecretObfuscated = clientSecretHash.substring(0, 5) + "****" + clientSecretHash.substring(clientSecretHash.length() - 5); - LOGGER.debug("Created Service: N:{} - ID:{} - SecHash:{} - SID:{}", name, clientID, clientSecretObfuscated, serviceID); - LOGGER.debug("Partition: {}", service.getSingleSignOnParticipationPolicy()); - return service; - } - - @Override - public BaseWebBasedRegisteredService createNewService(long id, String fqdn, URI casLogoutUri, CesServiceData serviceData) throws CesServiceCreationException { - if (serviceData.getAttributes() == null) { - throw new CesServiceCreationException("Cannot create service; Cannot find attributes"); - } - - // Get client id - String clientID = serviceData.getAttributes().get(ATTRIBUTE_KEY_OAUTH_CLIENT_ID); - if (clientID == null) { - throw new CesServiceCreationException("Cannot create service; Cannot find attribute: " + ATTRIBUTE_KEY_OAUTH_CLIENT_ID); - } - - // Get client secret - String clientSecret = serviceData.getAttributes().get(ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH); - if (clientSecret == null) { - throw new CesServiceCreationException("Cannot create service; Cannot find attribute: " + ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH); - } - - String serviceId = String.format("https://%s(:443)?/%s(/.*)?", CesDoguServiceFactory.generateServiceIdFqdnRegex(fqdn), serviceData.getName()); - if (casLogoutUri != null) { - String logoutUri = String.format("https://%s/%s%s", fqdn, serviceData.getName(), casLogoutUri); - return createOAUTHClientService(id, logoutUri, serviceData.getIdentifier(), serviceId, clientID, clientSecret); - } - - return createOAUTHClientService(id, null, serviceData.getIdentifier(), serviceId, clientID, clientSecret); - - } -} diff --git a/app/src/main/java/de/triology/cas/services/CesServiceData.java b/app/src/main/java/de/triology/cas/services/CesServiceData.java deleted file mode 100644 index ce7bdd82..00000000 --- a/app/src/main/java/de/triology/cas/services/CesServiceData.java +++ /dev/null @@ -1,40 +0,0 @@ -package de.triology.cas.services; - -import de.triology.cas.services.dogu.CesServiceFactory; - -import java.util.HashMap; -import java.util.Map; - -public class CesServiceData { - private String name; - private CesServiceFactory factory; - private Map attributes; - - public CesServiceData(String name, CesServiceFactory factory, Map attributes) { - this.name = name; - this.factory = factory; - this.attributes = attributes; - } - - public CesServiceData(String name, CesServiceFactory factory) { - this.name = name; - this.factory = factory; - this.attributes = new HashMap<>(); - } - - public String getName() { - return this.name; - } - - public CesServiceFactory getFactory() { - return this.factory; - } - - public Map getAttributes() { - return this.attributes; - } - - public String getIdentifier() { - return factory.getClass().getSimpleName() + " " + name; - } -} diff --git a/app/src/main/java/de/triology/cas/services/CesServiceManagerConfiguration.java b/app/src/main/java/de/triology/cas/services/CesServiceManagerConfiguration.java deleted file mode 100644 index 418321e9..00000000 --- a/app/src/main/java/de/triology/cas/services/CesServiceManagerConfiguration.java +++ /dev/null @@ -1,18 +0,0 @@ -package de.triology.cas.services; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.util.List; -import java.util.Map; - -@Getter -@AllArgsConstructor -public class CesServiceManagerConfiguration { - private final String stage; - private final List allowedAttributes; - private final Map attributesMappingRules; - private final boolean oidcAuthenticationDelegationEnabled; - private final String oidcClientDisplayName; - private final String oidcPrincipalsAttribute; -} \ No newline at end of file diff --git a/app/src/main/java/de/triology/cas/services/CesServicesManager.java b/app/src/main/java/de/triology/cas/services/CesServicesManager.java deleted file mode 100644 index 4171fa18..00000000 --- a/app/src/main/java/de/triology/cas/services/CesServicesManager.java +++ /dev/null @@ -1,200 +0,0 @@ -package de.triology.cas.services; - -import lombok.extern.slf4j.Slf4j; -import org.apereo.cas.authentication.principal.Service; -import org.apereo.cas.services.OidcRegisteredService; -import org.apereo.cas.services.RegisteredService; -import org.apereo.cas.services.ServicesManager; -import org.apereo.cas.services.query.RegisteredServiceQuery; -import org.apereo.cas.support.oauth.services.OAuthRegisteredService; - -import java.lang.reflect.Method; -import java.util.*; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Manages the Dogus that are accessible via CAS within the Cloudogu Ecosystem. - * Depending on the {@link CesServicesManagerStage} ({@link CesServicesManagerStageDevelopment} or - * {@link CesServicesManagerStageProductive}), a number of {@link RegisteredService}s is returned. - */ -@Slf4j -public class CesServicesManager implements ServicesManager { - - /** - * This triggers operation in development stage. - */ - static final String STAGE_DEVELOPMENT = "development"; - - private final CesServicesManagerStage serviceStage; - - public CesServicesManager(CesServiceManagerConfiguration managerConfig, Registry registry) { - serviceStage = createStage(managerConfig, registry); - } - - @Override - public Collection getAllServices() { - LOGGER.debug("Entered getAllServices method with return {}", Collections.unmodifiableCollection(serviceStage.getRegisteredServices().values())); - return Collections.unmodifiableCollection(serviceStage.getRegisteredServices().values()); - } - - @Override - public Collection getAllServicesOfType(final Class clazz) { - LOGGER.debug("Entered getAllServicesOfType method with type: {}", clazz); - if (supports(clazz)) { - return serviceStage.getRegisteredServices().values() - .stream() - .filter(s -> clazz.isAssignableFrom(s.getClass())) - .sorted() - .peek(RegisteredService::initialize) - .collect(Collectors.toList()); - } else { - return new ArrayList<>(); - } - } - - @Override - public Collection load() { - LOGGER.info("Cas wants to reload registered services."); - serviceStage.updateRegisteredServices(); - return serviceStage.getRegisteredServices().values(); - } - - @Override - public Collection getServicesForDomain(String domain) { - LOGGER.debug("getServicesForDomain: {}", domain); - throw new UnsupportedOperationException("Operation getServicesForDomain is not supported."); - } - - @Override - public Stream findServicesBy(RegisteredServiceQuery... queries) { - final Collection registeredServices = serviceStage.getRegisteredServices().values(); - final Collection resultServiceList = new ArrayList<>(); - - for (RegisteredServiceQuery query : queries) { - if (!query.getName().equals("clientId")) { - LOGGER.warn("RegisteredServiceQuery has property name of value: {} that does not match expected value 'clientId', unable to handle this query.", query.getName()); - continue; - } - - final Collection queryMatchingRegisteredServices = registeredServices.stream() - .filter(reg -> query.getType().isAssignableFrom(reg.getClass())).toList(); - - for (final RegisteredService registeredService : queryMatchingRegisteredServices) { - if (registeredService instanceof OAuthRegisteredService oauthService) { - if (oauthService.getClientId().equals(query.getValue())) { - resultServiceList.add(registeredService); - } else { - LOGGER.debug("Unable to match query {} to actual {} client id", query.getValue(), oauthService.getClientId()); - } - } else { - LOGGER.error("unexpected class of type {}, expected OAuthRegisteredService. Unable to handle.", registeredService.getClass().getSimpleName()); - } - } - - } - - return resultServiceList.stream(); - } - - @Override - public RegisteredService findServiceBy(final Service service) { - LOGGER.debug("findServiceBy: {}", service); - final Collection registeredServices = serviceStage.getRegisteredServices().values(); - - for (final RegisteredService registeredService : registeredServices) { - if (registeredService.matches(service)) { - return registeredService; - } - } - - return null; - } - - @Override - public Collection findServiceBy(Predicate clazz) { - LOGGER.debug("findServiceBy1: {}", clazz); - throw new UnsupportedOperationException("Operation findServiceBy is not supported."); - } - - @Override - public RegisteredService findServiceBy(final Service requestedService, final Class clazz) { - if (requestedService == null) { - return null; - } - RegisteredService service = findServiceBy(requestedService); - if (service != null && clazz.isAssignableFrom(service.getClass())) { - return service; - } - return null; - } - - @Override - public RegisteredService findServiceBy(final long id) { - LOGGER.debug("findServiceBy: {}", id); - return serviceStage.getRegisteredServices().get(id); - } - - @Override - public RegisteredService findServiceByName(String name) { - LOGGER.debug("findServiceByName: {}", name); - throw new UnsupportedOperationException("Operation findServiceByName is not supported."); - } - - @Override - public void save(Stream toSave) { - LOGGER.debug("save1: {}", toSave); - throw new UnsupportedOperationException("Operation save is not supported."); - } - - @Override - public RegisteredService save(final RegisteredService registeredService) { - LOGGER.debug("save2: {}", registeredService); - throw new UnsupportedOperationException("Operation save is not supported."); - } - - @Override - public RegisteredService save(RegisteredService registeredService, boolean publishEvent) { - LOGGER.debug("save3: {} - {}", registeredService, publishEvent); - throw new UnsupportedOperationException("Operation save is not supported."); - } - - @Override - public void save(Supplier supplier, Consumer andThenConsume, - long countExclusive) { - LOGGER.debug("save4: {} - {}", supplier, andThenConsume); - throw new UnsupportedOperationException("Operation save is not supported."); - } - - @Override - public void deleteAll() { - LOGGER.debug("deleteAll:"); - throw new UnsupportedOperationException("Operation deleteAll is not supported."); - } - - @Override - public RegisteredService delete(final long id) { - LOGGER.debug("delete1: {}", id); - throw new UnsupportedOperationException("Operation delete is not supported."); - } - - @Override - public RegisteredService delete(RegisteredService svc) { - LOGGER.debug("delete2: {}", svc); - throw new UnsupportedOperationException("Operation delete is not supported."); - } - - /** - * @return a new instance of the {@link CesServicesManagerStage}, depending on the stageString parameter. - */ - protected CesServicesManagerStage createStage(CesServiceManagerConfiguration managerConfig, Registry registry) { - if (!STAGE_DEVELOPMENT.equals(managerConfig.getStage())) { - return new CesServicesManagerStageProductive(managerConfig, registry); - } else { - return new CesServicesManagerStageDevelopment(managerConfig); - } - } -} diff --git a/app/src/main/java/de/triology/cas/services/CesServicesManagerStage.java b/app/src/main/java/de/triology/cas/services/CesServicesManagerStage.java deleted file mode 100644 index c13a3547..00000000 --- a/app/src/main/java/de/triology/cas/services/CesServicesManagerStage.java +++ /dev/null @@ -1,111 +0,0 @@ -package de.triology.cas.services; - -import de.triology.cas.services.attributes.ReturnMappedAttributesPolicy; -import lombok.extern.slf4j.Slf4j; -import org.apereo.cas.services.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * An abstract class that encapsulates the stages {@link CesServicesManager} operates in. - * It provides it's registered services via {@link #getRegisteredServices()}. - *

- * Implementations must initialize their registered services by implementing the template method - * {@link #initRegisteredServices()}. - */ -@Slf4j -abstract class CesServicesManagerStage { - - private final CesServiceManagerConfiguration managerConfig; - - /** - * Map to store all registeredServices. - */ - protected final Map registeredServices = new ConcurrentHashMap<>(); - - CesServicesManagerStage(CesServiceManagerConfiguration managerConfig) { - this.managerConfig = managerConfig; - } - - - /** - * @return the services registered in this stage. - */ - public Map getRegisteredServices() { - if (this.registeredServices.isEmpty()) { - initRegisteredServices(); - } - return this.registeredServices; - } - - /** - * Template method that add the stage-specific services to the registeredServices. - */ - protected abstract void initRegisteredServices(); - - - protected abstract void updateRegisteredServices(); - - /** - * Registers a new service - * - * @param service service object to register - */ - protected void addNewService(BaseRegisteredService service) { - service.setEvaluationOrder((int) service.getId()); - service.setAttributeReleasePolicy(new ReturnMappedAttributesPolicy(managerConfig.getAllowedAttributes(), managerConfig.getAttributesMappingRules())); - - if (managerConfig.isOidcAuthenticationDelegationEnabled()) { - configureOidcDelegationService(service); - } - - LOGGER.debug("Adding new service to service manager {}", service); - registeredServices.put(service.getId(), service); - } - - /** - * Applies basic configuration to all services when the OIDC delegation authentication is enabled. - * - * @param service The service that should be configured - */ - private void configureOidcDelegationService(BaseRegisteredService service) { - if (managerConfig.getOidcPrincipalsAttribute() != null && !managerConfig.getOidcPrincipalsAttribute().isEmpty()) { - var principalProvider = new PrincipalAttributeRegisteredServiceUsernameProvider(); - principalProvider.setUsernameAttribute(managerConfig.getOidcPrincipalsAttribute()); - service.setUsernameAttributeProvider(principalProvider); - } - - List allowedProviders = new ArrayList<>(); - allowedProviders.add(managerConfig.getOidcClientDisplayName()); - var delegatedAuthenticationPolicy = new DefaultRegisteredServiceDelegatedAuthenticationPolicy(); - delegatedAuthenticationPolicy.setAllowedProviders(allowedProviders); - var accessStrategy = new DefaultRegisteredServiceAccessStrategy(); - accessStrategy.setDelegatedAuthenticationPolicy(delegatedAuthenticationPolicy); - service.setAccessStrategy(accessStrategy); - } - - /** - * @return a new numeric ID for a registered service - */ - protected long createId() { - return findHighestId(registeredServices) + 1; - } - - /** - * @return the highest number within the keyset of map - */ - private static long findHighestId(Map map) { - long id = 0; - - for (Map.Entry entry : map.entrySet()) { - if (entry.getKey() > id) { - id = entry.getKey(); - } - } - return id; - } - -} diff --git a/app/src/main/java/de/triology/cas/services/CesServicesManagerStageDevelopment.java b/app/src/main/java/de/triology/cas/services/CesServicesManagerStageDevelopment.java deleted file mode 100644 index 9cd4c991..00000000 --- a/app/src/main/java/de/triology/cas/services/CesServicesManagerStageDevelopment.java +++ /dev/null @@ -1,63 +0,0 @@ -package de.triology.cas.services; - -import de.triology.cas.oidc.services.CesOAuthServiceFactory; -import de.triology.cas.services.dogu.CesServiceCreationException; -import lombok.extern.slf4j.Slf4j; -import org.apereo.cas.services.BaseWebBasedRegisteredService; -import org.apereo.cas.services.CasRegisteredService; -import org.apereo.cas.services.OidcRegisteredService; - -import java.util.HashMap; -import java.util.Map; - -/** - * Special stage in which a {@link CesServicesManager} operates during development. - * - *

Never use in production, as it accepts all requests from https imaps.

- */ -@Slf4j -class CesServicesManagerStageDevelopment extends CesServicesManagerStage { - - CesServicesManagerStageDevelopment(CesServiceManagerConfiguration managerConfig) { - super(managerConfig); - } - - @Override - protected void initRegisteredServices() { - LOGGER.debug("Cas started in development stage. All services can get an ST."); - LOGGER.debug("The development stage does not support OAuth services."); - addDevService(); - } - - @Override - protected void updateRegisteredServices() { - LOGGER.debug("Cas started in development stage. No services need to be updated"); - } - - /** - * The dev service accepts all services - */ - private void addDevService() { - CasRegisteredService devService = new CasRegisteredService(); - devService.setId(createId()); - devService.setServiceId("^(https?|imaps?)://.*"); - devService.setName("10000001"); - addNewService(devService); - LOGGER.debug("Creating development service..."); - - try { - var factory = new CesOAuthServiceFactory<>(OidcRegisteredService::new); - var oidcClientName = "cas-oidc-client"; - Map attributes = new HashMap<>(); - attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_ID, oidcClientName); - attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH, "df0576c3d0b3b449eef75f71894fffe86baa555eba1d52ed18ec324c96025d10"); - BaseWebBasedRegisteredService service = factory.createNewService(createId(), "", null, new CesServiceData(oidcClientName, factory, attributes)); - service.setName(oidcClientName); - service.setServiceId(".*"); - addNewService(service); - LOGGER.debug("Creating oidc development service... Use the secret: `T0OpxpbdyFixfwMc` and id `{}` for your client.", oidcClientName); - } catch (CesServiceCreationException e) { - LOGGER.error("could not start oidc service in development mode: ", e); - } - } -} diff --git a/app/src/main/java/de/triology/cas/services/CesServicesManagerStageProductive.java b/app/src/main/java/de/triology/cas/services/CesServicesManagerStageProductive.java deleted file mode 100644 index bd71639a..00000000 --- a/app/src/main/java/de/triology/cas/services/CesServicesManagerStageProductive.java +++ /dev/null @@ -1,200 +0,0 @@ -package de.triology.cas.services; - -import de.triology.cas.oidc.services.CesOAuthServiceFactory; -import de.triology.cas.services.dogu.CesDoguServiceFactory; -import de.triology.cas.services.dogu.CesServiceCreationException; -import lombok.extern.slf4j.Slf4j; -import org.apereo.cas.services.OidcRegisteredService; -import org.apereo.cas.services.RegisteredService; -import org.apereo.cas.support.oauth.services.OAuthRegisteredService; - -import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * The stage in which a {@link CesServicesManager} operates in production. - * Services accesible via CAS ({@link RegisteredService}s) are queried from a {@link Registry}. - * For each Dogu that is accessible via CAS, one {@link RegisteredService} is returned. An additional service allows - * CAS to access itself. - * For each OAuth client that is accessuble via CAS, one {@link RegisteredService} is returned. An additional service - * for the OAuth callback is also created. - */ -@Slf4j -class CesServicesManagerStageProductive extends CesServicesManagerStage { - - private String fqdn; - private final Registry registry; - private final List persistentServices; - - public final CesOAuthServiceFactory oAuthServiceFactory; - public final CesOAuthServiceFactory oidcServiceFactory; - public final CesDoguServiceFactory doguServiceFactory; - - private boolean initialized = false; - - CesServicesManagerStageProductive(CesServiceManagerConfiguration managerConfig, Registry registry) { - super(managerConfig); - this.registry = registry; - this.persistentServices = new ArrayList<>(); - this.doguServiceFactory = new CesDoguServiceFactory(); - this.oAuthServiceFactory = new CesOAuthServiceFactory<>(OAuthRegisteredService::new); - this.oidcServiceFactory = new CesOAuthServiceFactory<>(OidcRegisteredService::new); - } - - /** - * Initialize the registered services found in registry. - * This is synchronized because otherwise two parallel calls could lead - * to multiple initializations and an inconsistent state (e.g. cas-service multiple times). - * Parallel calls can happen since we call {@code initRegisteredServices()} in {@link #getRegisteredServices()}. - * This will not be an performance issue because this method is only called once, after startup. - */ - @Override - protected synchronized void initRegisteredServices() { - if (isInitialized()) { - LOGGER.info("Already initialized CesServicesManager. Doing nothing."); - return; - } - LOGGER.debug("Cas started in production stage. Only installed dogus can get an ST."); - fqdn = registry.getFqdn(); - addPersistentServices(); - synchronizeServicesWithRegistry(); - registerChangeListener(); - initialized = true; - LOGGER.debug("Finished initialization of registered services"); - } - - private boolean isInitialized() { - return initialized; - } - - @Override - protected void updateRegisteredServices() { - if (isInitialized()) { - synchronizeServicesWithRegistry(); - } else { - initRegisteredServices(); - } - } - - /** - * Synchronize services from {@link #registry} to registeredServices. - * That is, remove the ones that are not present in{@link #registry} and add the ones that are only present - * in {@link #registry} to registeredServices. - */ - private void synchronizeServicesWithRegistry() { - LOGGER.debug("Synchronize services with registry"); - - var casServices = registry.getInstalledCasServiceAccountsOfType(Registry.SERVICE_ACCOUNT_TYPE_CAS, doguServiceFactory); - var oauthServices = registry.getInstalledCasServiceAccountsOfType(Registry.SERVICE_ACCOUNT_TYPE_OAUTH, oAuthServiceFactory); - var oidcServices = registry.getInstalledCasServiceAccountsOfType(Registry.SERVICE_ACCOUNT_TYPE_OIDC, oidcServiceFactory); - - // use map to filter duplicates - var newServices = persistentServices.stream().collect(Collectors.toMap(CesServiceData::getName, v -> v)); - newServices.putAll(casServices.stream().collect(Collectors.toMap(CesServiceData::getName, v -> v))); - newServices.putAll(oauthServices.stream().collect(Collectors.toMap(CesServiceData::getName, v -> v))); - newServices.putAll(oidcServices.stream().collect(Collectors.toMap(CesServiceData::getName, v -> v))); - - synchronizeServices(newServices.values().stream().toList()); - LOGGER.info("Loaded {} services:", registeredServices.size()); - registeredServices.values().forEach(e -> LOGGER.debug("[{}]", e)); - } - - /** - * Detects when a new dogu is installed or an existing one is removed - */ - private void registerChangeListener() { - LOGGER.debug("Entered registerChangeListener"); - registry.addDoguChangeListener(() -> { - LOGGER.debug("Registered change in dogu service accounts"); - synchronizeServicesWithRegistry(); - }); - } - - /** - * Creates and registers a new service for an given name - */ - void addNewService(CesServiceData serviceData) { - String serviceName = serviceData.getName(); - LOGGER.debug("Add new service: {}", serviceName); - try { - addNewService(serviceName, serviceData); - } catch (CesServiceCreationException e) { - LOGGER.error("Failed to create service [{}]. Skip service creation - {}", serviceName, e.toString()); - } - } - - /** - * Creates and registers a new service for an given name - */ - void addNewService(String serviceName, CesServiceData serviceData) throws CesServiceCreationException { - try { - URI logoutUri = registry.getCasLogoutUri(serviceName); - var service = serviceData.getFactory().createNewService(createId(), fqdn, logoutUri, serviceData); - addNewService(service); - } catch (GetCasLogoutUriException e) { - LOGGER.debug("GetCasLogoutUriException: CAS logout URI of service {} could not be retrieved: {}", serviceName, e.toString()); - LOGGER.info("Adding service without CAS logout URI"); - var service = serviceData.getFactory().createNewService(createId(), fqdn, null, serviceData); - addNewService(service); - } - } - - /** - * Synchronize services from newServices to registeredServices. - * That is, remove the ones that are not present in newServices and add the ones that are only present - * in newServices to registeredServices. - */ - private void synchronizeServices(List newServiceNames) { - removeServicesThatNoLongerExist(newServiceNames); - addServicesThatDoNotExistYet(newServiceNames); - } - - /** - * First operation of {@link #synchronizeServices(List)}: Remove Services that are not present in - * newServices from registeredServices. - */ - private void removeServicesThatNoLongerExist(List newServiceNames) { - List newServicesIdentifiers = newServiceNames.stream() - .map(CesServiceData::getIdentifier) - .collect(Collectors.toList()); - - List toBeRemovedServices = registeredServices.values() - .stream().map(RegisteredService::getName) - .filter(serviceName -> !newServicesIdentifiers.contains(serviceName)) - .collect(Collectors.toList()); - - registeredServices.values().stream() - .filter(service -> toBeRemovedServices.contains(service.getName())) - .forEach(service -> registeredServices.remove(service.getId())); - } - - /** - * Second operation of {@link #synchronizeServices(List)}: Add services that are only present in - * newServices to registeredServices. - */ - private void addServicesThatDoNotExistYet(List newServiceNames) { - Set existingServiceNames = - registeredServices.values().stream().map(RegisteredService::getName).collect(Collectors.toSet()); - - Set newServices = newServiceNames.stream() - .map(CesServiceData::getIdentifier) - .filter(serviceName -> !existingServiceNames.contains(serviceName)) - .collect(Collectors.toSet()); - - newServiceNames.stream().filter(service -> newServices.contains(service.getIdentifier())) - .forEach(this::addNewService); - } - - /** - * persistent services will not be removed - */ - public void addPersistentServices() { - //This is necessary for the oauth workflow - LOGGER.info("Creating cas service for oauth/oidc workflow"); - addNewService(doguServiceFactory.createCASService(createId(), fqdn)); - persistentServices.add(new CesServiceData(CesDoguServiceFactory.SERVICE_CAS_IDENTIFIER, doguServiceFactory)); - } -} diff --git a/app/src/main/java/de/triology/cas/services/EtcdClientFactory.java b/app/src/main/java/de/triology/cas/services/EtcdClientFactory.java deleted file mode 100644 index e458cf14..00000000 --- a/app/src/main/java/de/triology/cas/services/EtcdClientFactory.java +++ /dev/null @@ -1,47 +0,0 @@ -package de.triology.cas.services; - -import mousio.etcd4j.EtcdClient; -import org.apache.commons.lang.StringUtils; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.net.URI; - -public class EtcdClientFactory { - - private static final String DEFAULT_NODE_MASTER_FILE = "/etc/ces/node_master"; - - public EtcdClient createDefaultClient() { - return createEtcdClient(EtcdClientFactory.DEFAULT_NODE_MASTER_FILE); - } - - public EtcdClient createEtcdClient(String nodeMasterFilepath) { - try { - // TODO when is this resource closed? Can spring be used to call etcd.close()? - return createEtcdClient(URI.create(getEtcdUri(nodeMasterFilepath))); - } catch (IOException e) { - throw new RegistryException("Cannot create etcd client: ", e); - } - } - - public EtcdClient createEtcdClient(URI uri) { - return new EtcdClient(uri); - } - - private String getEtcdUri(String nodeMasterFilePath) throws IOException { - File nodeMasterFile = new File(nodeMasterFilePath); - if (!nodeMasterFile.exists()) { - return "http://localhost:4001"; - } - - try (BufferedReader reader = new BufferedReader(new FileReader(nodeMasterFile))) { - String nodeMaster = reader.readLine(); - if (StringUtils.isBlank(nodeMaster)) { - throw new IOException("failed to read " + nodeMasterFilePath + " file"); - } - return "http://".concat(nodeMaster).concat(":4001"); - } - } -} diff --git a/app/src/main/java/de/triology/cas/services/GetCasLogoutUriException.java b/app/src/main/java/de/triology/cas/services/GetCasLogoutUriException.java deleted file mode 100644 index 5d6e3a06..00000000 --- a/app/src/main/java/de/triology/cas/services/GetCasLogoutUriException.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.triology.cas.services; - -public class GetCasLogoutUriException extends Exception { - GetCasLogoutUriException(String message) { - super(message); - } - - GetCasLogoutUriException(Exception cause) { - super(cause); - } -} diff --git a/app/src/main/java/de/triology/cas/services/Registry.java b/app/src/main/java/de/triology/cas/services/Registry.java deleted file mode 100644 index 8c07292b..00000000 --- a/app/src/main/java/de/triology/cas/services/Registry.java +++ /dev/null @@ -1,78 +0,0 @@ -package de.triology.cas.services; - -import de.triology.cas.services.dogu.CesServiceFactory; - -import java.net.URI; -import java.util.List; - -/** - * Abstraction of a registry that provides service information. - */ -public interface Registry { - enum CasServiceAccountTypes { - OAUTH("oauth"), - OIDC("oidc"), - CAS("cas"), - UNDEFINED(""); - - private final String id; - - CasServiceAccountTypes(String id) { - this.id = id; - } - - public static CasServiceAccountTypes fromString(String id) { - for (CasServiceAccountTypes e : values()) { - if (e.id.equals(id)) return e; - } - return UNDEFINED; - } - - @Override - public String toString() { - return id; - } - } - - String SERVICE_ACCOUNT_TYPE_OAUTH = "oauth"; - String SERVICE_ACCOUNT_TYPE_OIDC = "oidc"; - String SERVICE_ACCOUNT_TYPE_CAS = "cas"; - - - - /** - * Retrieves all CAS Services Accounts which are currently registered in etcd. - * - * @param factory The factory responsible to create a service of the given type - * @param serviceAccountType The type of service account that should be searched in the registry - * @return an list of {@link CesServiceData} containing the information for all installed oauth service accounts - * of the given type - */ - List getInstalledCasServiceAccountsOfType(String serviceAccountType, CesServiceFactory factory); - - /** - * @return the fully qualified domain name - */ - String getFqdn(); - - /** - * @return the dogu specific CAS logout URI - * @throws GetCasLogoutUriException wrapper for all technical exceptions - */ - URI getCasLogoutUri(String doguname) throws GetCasLogoutUriException; - - /** - * Adds a listener that is called when a new dogu is added to or delted from etcd. - * - * @param doguChangeListener listener to be called on dogu change - */ - void addDoguChangeListener(DoguChangeListener doguChangeListener); - - /** - * Functional interface for reacting on changes of dogus registerd in etcd - */ - @FunctionalInterface - interface DoguChangeListener { - void onChange(); - } -} diff --git a/app/src/main/java/de/triology/cas/services/RegistryEtcd.java b/app/src/main/java/de/triology/cas/services/RegistryEtcd.java deleted file mode 100644 index c31ea6a0..00000000 --- a/app/src/main/java/de/triology/cas/services/RegistryEtcd.java +++ /dev/null @@ -1,288 +0,0 @@ -package de.triology.cas.services; - -import de.triology.cas.oidc.services.CesOAuthServiceFactory; -import de.triology.cas.services.dogu.CesServiceFactory; -import lombok.extern.slf4j.Slf4j; -import mousio.etcd4j.EtcdClient; -import mousio.etcd4j.promises.EtcdResponsePromise; -import mousio.etcd4j.responses.EtcdAuthenticationException; -import mousio.etcd4j.responses.EtcdErrorCode; -import mousio.etcd4j.responses.EtcdException; -import mousio.etcd4j.responses.EtcdKeysResponse; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.TimeoutException; - -/** - * Default implementation of {@link Registry} using {@link EtcdClient}. - *

- * The Dogus are queried from etcd: Installed Dogus and the version information are stored in a directory - * /dogu/${name of dogu}/current. In addition, 'cas' has to be in the dependencies of the Dogu. - * Changes of the /dogu directory can be recognized using {@link #addDoguChangeListener(DoguChangeListener)}. - */ -@Slf4j -class RegistryEtcd implements Registry { - private static final JSONParser PARSER = new JSONParser(); - private static final String DOGU_DIR = "/dogu"; - private static final String CAS_SERVICE_ACCOUNT_DIR = "/config/cas/service_accounts"; - private final EtcdClient etcd; - - /** - * Creates a etcd client that loads its URI from /etc/ces/node_master. - * - * @throws RegistryException when the URI cannot be read - */ - public RegistryEtcd(EtcdClient etcd) { - this.etcd = etcd; - } - - @Override - public List getInstalledCasServiceAccountsOfType(String type, CesServiceFactory factory) { - LOGGER.debug("Get [{}] service accounts from registry", type); - try { - return Registry.SERVICE_ACCOUNT_TYPE_CAS.equals(type) ? - getInstalledDogusWhichAreUsingCAS(factory) : - getInstalledDogusWhichAreUsingSecrets(type, factory); - } catch (EtcdException e) { - if (e.isErrorCode(EtcdErrorCode.KeyNotFound)) { - return new ArrayList<>(); - } else { - throw new RegistryException("Failed to getInstalledCasServiceAccountsOfType: " + type, e); - } - } catch (IOException | EtcdAuthenticationException | TimeoutException e) { - throw new RegistryException("Failed to getInstalledCasServiceAccountsOfType: " + type, e); - } - } - - List getInstalledDogusWhichAreUsingCAS(CesServiceFactory factory) throws IOException, EtcdAuthenticationException, EtcdException, TimeoutException { - List nodes = etcd.getDir(DOGU_DIR).send().get().getNode().getNodes(); - return extractDogusFromDoguRootDir(nodes, factory); - } - - List getInstalledDogusWhichAreUsingSecrets(String type, CesServiceFactory factory) throws IOException, EtcdAuthenticationException, EtcdException, TimeoutException { - List nodes = etcd.getDir(String.format("%s/%s", CAS_SERVICE_ACCOUNT_DIR, type)).send().get().getNode().getNodes(); - return extractServiceAccountClientsByType(nodes, type, factory); - } - - /** - * Iterates over all available etcd-Keys of CASs service accounts. - * - * @param nodesFromEtcd a list containing all child nodes of the `service_accounts` directory of the cas in the etcd - * @param type the type of service accounts that should be extracted - * @return a list containing the identifier for all registered service accounts of cas - */ - private List extractServiceAccountClientsByType(List nodesFromEtcd, String type, CesServiceFactory factory) { - LOGGER.debug("Entered extractServiceAccountClientsByType"); - var clientPathPrefix = String.format("%s/%s/", CAS_SERVICE_ACCOUNT_DIR, type); - List serviceDataList = new ArrayList<>(); - for (EtcdKeysResponse.EtcdNode oAuthClient : nodesFromEtcd) { - try { - System.out.println("key: " + oAuthClient.getKey() + " prefix: " + clientPathPrefix); - var clientID = oAuthClient.getKey().substring(clientPathPrefix.length()); - var attributes = new HashMap(); - - var accountType = CasServiceAccountTypes.fromString(type); - if (accountType == CasServiceAccountTypes.OIDC || accountType == CasServiceAccountTypes.OAUTH) { - var clientSecret = getEtcdValueForKeyIfPresent(String.format("%s%s/secret", clientPathPrefix, clientID)); - attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_ID, clientID); - attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH, clientSecret); - } - - serviceDataList.add(new CesServiceData(clientID, factory, attributes)); - } catch (RegistryException ex) { - throw new RuntimeException("registry exception occurred ", ex); - } - } - return serviceDataList; - } - - private List extractDogusFromDoguRootDir(List nodesFromEtcd, CesServiceFactory factory) { - LOGGER.debug("Entered extractDogusFromDoguRootDir"); - List doguServices = new ArrayList<>(); - for (EtcdKeysResponse.EtcdNode dogu : nodesFromEtcd) { - JSONObject json; - try { - var doguName = dogu.getKey().substring(DOGU_DIR.length() + 1); - json = getCurrentDoguNode(doguName); - if (hasCasDependency(json)) { - doguServices.add(new CesServiceData(doguName, factory)); - } - } catch (ParseException ex) { - throw new RuntimeException("failed to parse EtcdNode to json: ", ex); - } catch (RegistryException ex) { - throw new RuntimeException("registry exception occurred: ", ex); - } - } - return doguServices; - } - - @Override - public String getFqdn() { - return getEtcdValueForKey("/config/_global/fqdn"); - } - - public String getEtcdValueForKey(String key) { - LOGGER.debug("Get {} from registry", key); - try { - var node = etcd.get(key).send().get().getNode(); - if (node.isDir()) { - throw new RegistryException(String.format("Failed to getEtcdValueForKey: key %s is a directory, not a file", key), null); - } - - return node.getValue(); - } catch (EtcdException e) { - throw new RegistryException(String.format("Failed to getEtcdValueForKey: %s", key), e); - } catch (IOException | EtcdAuthenticationException | TimeoutException e) { - throw new RegistryException("Failed to getEtcdValueForKey: ", e); - } - } - - /** - * Retrieves the value of a given key from the etcd. If the key does not exists then an empty string is returned. - * - * @param key Identifier for the wanted value - * @return the value for the given key if present, otherwise an empty string. - */ - public String getEtcdValueForKeyIfPresent(String key) { - LOGGER.debug("Get {} from registry", key); - try { - var node = etcd.get(key).send().get().getNode(); - if (node.isDir()) { - throw new RegistryException(String.format("Failed to getEtcdValueForKeyIfPresent: key %s is a directory, not a file", key), null); - } - - return node.getValue(); - } catch (EtcdException e) { - if (e.isErrorCode(EtcdErrorCode.KeyNotFound)) { - LOGGER.debug("Failed to getEtcdValueForKeyIfPresent: key \"{}\" not found", key); - //Valid case if key is not found return an empty string - return ""; - } else { - throw new RegistryException("Failed to getEtcdValueForKeyIfPresent: ", e); - } - } catch (IOException | EtcdAuthenticationException | TimeoutException e) { - throw new RegistryException("Failed to getEtcdValueForKeyIfPresent: ", e); - } - } - - @Override - public URI getCasLogoutUri(String doguname) throws GetCasLogoutUriException { - try { - String logoutUri; - for (var accountType : Registry.CasServiceAccountTypes.values()) { - try { - logoutUri = getEtcdValueForKey(String.format("/config/cas/service_accounts/%s/%s/logout_uri", accountType.toString(), doguname)); - if (logoutUri.isEmpty()) { - throw new GetCasLogoutUriException("logout_uri is empty"); - } - return new URI(logoutUri); - } catch (RegistryException ignored) { - } - } - - LOGGER.warn("Failed to find logout URI in service_accounts directory, falling back to dogu descriptor..."); - return getLogoutUriFromDoguDescriptor(doguname); - } catch (URISyntaxException e) { - throw new GetCasLogoutUriException(e); - } - } - - private URI getLogoutUriFromDoguDescriptor(String doguname) throws GetCasLogoutUriException, URISyntaxException { - JSONObject doguMetaData; - try { - doguMetaData = getCurrentDoguNode(doguname); - JSONObject properties; - if (doguMetaData != null) { - properties = getPropertiesFromMetaData(doguMetaData); - } else { - throw new GetCasLogoutUriException("Could not get dogu metadata"); - } - return getLogoutUriFromProperties(properties); - } catch (ClassCastException | NullPointerException | ParseException | RegistryException e) { - throw new GetCasLogoutUriException(e); - } - } - - private URI getLogoutUriFromProperties(JSONObject properties) throws GetCasLogoutUriException, URISyntaxException { - Object logoutUri = properties.get("logoutUri"); - if (logoutUri != null) { - String logoutUriString = logoutUri.toString(); - if (logoutUriString != null) { - return new URI(logoutUriString); - } else { - throw new GetCasLogoutUriException("Could not get logoutUri from properties"); - } - } else { - throw new GetCasLogoutUriException("Could not get logoutUri from properties"); - } - } - - private JSONObject getPropertiesFromMetaData(JSONObject doguMetaData) { - Object propertiesObject = doguMetaData.get("Properties"); - if (propertiesObject != null) { - if (propertiesObject instanceof JSONObject) { - return (JSONObject) propertiesObject; - } else { - throw new ClassCastException("Properties are not in JSONObject format"); - } - } else { - throw new NullPointerException("No Properties are set"); - } - } - - @Override - public void addDoguChangeListener(DoguChangeListener doguChangeListener) { - Thread t = new Thread(() -> { - try { - while (true) { - EtcdResponsePromise responsePromise = etcd.getDir(DOGU_DIR).recursive().waitForChange().send(); - LOGGER.info("wait for changes under /dogu"); - responsePromise.get(); - doguChangeListener.onChange(); - } - } catch (IOException | EtcdException | TimeoutException | EtcdAuthenticationException e) { - throw new RegistryException("Failed to addDoguChangeListener for dogus: ", e); - } - }); - t.start(); - - Thread t2 = new Thread(() -> { - try { - while (true) { - EtcdResponsePromise responsePromise = etcd.getDir(CAS_SERVICE_ACCOUNT_DIR).recursive().waitForChange().send(); - LOGGER.info("wait for changes under /config/cas/service_accounts"); - responsePromise.get(); - doguChangeListener.onChange(); - } - } catch (IOException | EtcdException | TimeoutException | EtcdAuthenticationException e) { - throw new RegistryException("Failed to addDoguChangeListener for service accounts: ", e); - } - }); - t2.start(); - } - - private boolean hasCasDependency(JSONObject json) { - return json != null && json.get("Dependencies") != null && ((JSONArray) json.get("Dependencies")).contains("cas"); - } - - protected JSONObject getCurrentDoguNode(String doguName) throws ParseException { - JSONObject json = null; - // get used dogu version - String doguVersion = getEtcdValueForKeyIfPresent(String.format("%s/%s/current", DOGU_DIR, doguName)); - // empty if dogu isnt used - if (!doguVersion.isEmpty()) { - String doguDescription = getEtcdValueForKey(String.format("%s/%s/%s", DOGU_DIR, doguName, doguVersion)); - json = (JSONObject) PARSER.parse(doguDescription); - } - return json; - } -} diff --git a/app/src/main/java/de/triology/cas/services/RegistryException.java b/app/src/main/java/de/triology/cas/services/RegistryException.java deleted file mode 100644 index a22824f6..00000000 --- a/app/src/main/java/de/triology/cas/services/RegistryException.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.triology.cas.services; - -/** - * Wraps exceptions thrown when accessing the {@link Registry}. - */ -class RegistryException extends RuntimeException { - - /** - * Constructs a new runtime exception with the specified cause and a detail message of - * (cause==null ? null : cause.toString()) (which typically contains the class and detail message of - * cause). - * - * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). - * (A null value is permitted, and indicates that the cause is nonexistent or unknown.) - */ - RegistryException(String message, Exception cause) { - super(message, cause); - } -} diff --git a/app/src/main/java/de/triology/cas/services/RegistryLocal.java b/app/src/main/java/de/triology/cas/services/RegistryLocal.java deleted file mode 100644 index a4f18d91..00000000 --- a/app/src/main/java/de/triology/cas/services/RegistryLocal.java +++ /dev/null @@ -1,312 +0,0 @@ -package de.triology.cas.services; - -import de.triology.cas.oidc.services.CesOAuthServiceFactory; -import de.triology.cas.services.dogu.CesServiceFactory; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import org.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.Constructor; -import org.yaml.snakeyaml.error.YAMLException; - -import java.io.*; -import java.lang.reflect.InvocationTargetException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.*; -import java.util.*; -import java.util.stream.Collectors; - -@Slf4j -public class RegistryLocal implements Registry { - - private static final String LOCAL_CONFIG_DIR = "/var/ces/config"; - private static final String LOCAL_CONFIG_SUB_PATH = "/var/ces/config"; - private static final String LOCAL_CONFIG_FILE_NAME = "local.yaml"; - private static final String LOCAL_CONFIG_FILE = LOCAL_CONFIG_SUB_PATH + "/" + LOCAL_CONFIG_FILE_NAME; - private static final String GLOBAL_CONFIG_FILE = "/etc/ces/config/global/config.yaml"; - - FileSystem fileSystem = FileSystems.getDefault(); - - @Getter - @Setter - protected static class GlobalConfig { - private String fqdn; - } - - @Getter - @Setter - protected static class LocalConfig { - private ServiceAccounts service_accounts = new ServiceAccounts(); - } - - @Getter - @Setter - protected static class ServiceAccountSecret { - private String secret; - private String logout_uri; - - boolean deepEquals(ServiceAccountSecret other) { - if (other == null) return false; - return stringsEqual(secret, other.secret) && stringsEqual(logout_uri, other.logout_uri); - } - } - - @Getter - @Setter - protected static class ServiceAccountCas { - private String created; - private String logout_uri; - - boolean deepEquals(ServiceAccountCas other) { - if (other == null) return false; - return stringsEqual(created, other.created) && stringsEqual(logout_uri, other.logout_uri); - } - } - - private static boolean stringsEqual(String a, String b) { - if (a == null && b == null) return true; - else return a != null && a.equals(b); - } - - @Getter - @Setter - protected static class ServiceAccounts { - private Map cas = new HashMap<>(); - private Map oidc = new HashMap<>(); - private Map oauth = new HashMap<>(); - - private String getLogoutUri(String doguName) { - var serviceAccounts = new HashMap(); - - serviceAccounts.putAll(this.cas.entrySet().stream() - .filter(e -> e.getValue().logout_uri != null) - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().logout_uri))); - serviceAccounts.putAll(this.oidc.entrySet().stream() - .filter(e -> e.getValue().logout_uri != null) - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().logout_uri))); - serviceAccounts.putAll(this.oauth.entrySet().stream() - .filter(e -> e.getValue().logout_uri != null) - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().logout_uri))); - - return serviceAccounts.get(doguName); - } - - List generateByType(String serviceAccountType, CesServiceFactory factory) throws RuntimeException { - return switch (CasServiceAccountTypes.fromString(serviceAccountType)) { - case OIDC -> extractServiceDataSecret(this.oidc, factory); - case OAUTH -> extractServiceDataSecret(this.oauth, factory); - case CAS -> extractCasServiceData(this.cas, factory); - default -> - throw new RegistryException(String.format("Unknown service account type %s", serviceAccountType), null); - }; - } - - boolean deepEquals(ServiceAccounts other) { - if ((other == null) || - (this.cas.size() != other.cas.size()) || - (this.oidc.size() != other.oidc.size()) || - (this.oauth.size() != other.oauth.size())) - return false; - - for (var entry : this.cas.entrySet()) { - if (!entry.getValue().deepEquals(other.cas.get(entry.getKey()))) return false; - } - for (var entry : this.oidc.entrySet()) { - if (!entry.getValue().deepEquals(other.oidc.get(entry.getKey()))) return false; - } - for (var entry : this.oauth.entrySet()) { - if (!entry.getValue().deepEquals(other.oauth.get(entry.getKey()))) return false; - } - - return true; - } - - private static List extractServiceDataSecret(Map serviceAccounts, CesServiceFactory factory) { - var serviceDataList = new ArrayList(); - - for (var serviceAccount : serviceAccounts.entrySet()) { - var clientID = serviceAccount.getKey(); - var clientSecret = serviceAccount.getValue().secret; - - var attributes = new HashMap(); - - attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_ID, clientID); - attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH, clientSecret); - - serviceDataList.add(new CesServiceData(clientID, factory, attributes)); - } - - return serviceDataList; - } - - private static List extractCasServiceData(Map serviceAccounts, CesServiceFactory factory) { - var serviceDataList = new ArrayList(); - - for (var clientID : serviceAccounts.keySet()) { - serviceDataList.add(new CesServiceData(clientID, factory)); - } - - return serviceDataList; - } - - static ServiceAccounts setDefaults(ServiceAccounts serviceAccounts) { - if (serviceAccounts == null) { - return new ServiceAccounts(); - } - - if (serviceAccounts.cas == null) { - serviceAccounts.cas = new HashMap<>(); - } - if (serviceAccounts.oidc == null) { - serviceAccounts.oidc = new HashMap<>(); - } - if (serviceAccounts.oauth == null) { - serviceAccounts.oauth = new HashMap<>(); - } - - return serviceAccounts; - } - } - - @Override - public List getInstalledCasServiceAccountsOfType(String serviceAccountType, CesServiceFactory factory) { - return readServiceAccounts().generateByType(serviceAccountType, factory); - } - - ServiceAccounts readServiceAccounts() { - try (var fis = getInputStreamForFile(LOCAL_CONFIG_FILE)) { - var serviceAccounts = readYaml(LocalConfig.class, fis).getService_accounts(); - return ServiceAccounts.setDefaults(serviceAccounts); - } catch (IOException e) { - throw new RegistryException("Failed to close local config file after reading service accounts.", e); - } - } - - @Override - public String getFqdn() { - try (var fis = getInputStreamForFile(GLOBAL_CONFIG_FILE)) { - Object fqdn = readKeyOutOfYaml(fis, "fqdn"); - return fqdn != null ? fqdn.toString() : null; - } catch (IOException e) { - throw new RegistryException("Failed to close global config file after reading fqdn.", e); - } - } - - InputStream getInputStreamForFile(String path) { - FileInputStream fis; - try { - fis = new FileInputStream(path); - } catch (FileNotFoundException e) { - throw new RegistryException(String.format("Could not find file %s", path), e); - } - return fis; - } - - @Override - public URI getCasLogoutUri(String doguname) throws GetCasLogoutUriException { - var serviceAccounts = readServiceAccounts(); - try { - String logoutUri = serviceAccounts.getLogoutUri(doguname); - - if (logoutUri == null || logoutUri.isEmpty()) { - throw new GetCasLogoutUriException(String.format("Could not get logoutUri for dogu %s", doguname)); - } else { - return new URI(logoutUri); - } - } catch (URISyntaxException e) { - throw new GetCasLogoutUriException(e); - } - } - - @Override - public void addDoguChangeListener(DoguChangeListener doguChangeListener) { - Thread t1 = new Thread(() -> doguChangeListenerThreadHandler(doguChangeListener)); - t1.start(); - } - - private void doguChangeListenerThreadHandler(DoguChangeListener doguChangeListener) { - var path = Paths.get(LOCAL_CONFIG_DIR); - WatchService watchService; - try { - watchService = initializeWatchService(path, null); - var previousServiceAccounts = readServiceAccounts(); - WatchKey key; - LOGGER.info("wait for changes under {}", LOCAL_CONFIG_DIR); - while ((key = watchService.take()) != null) { // take does not return null - if (!key.isValid()) { - LOGGER.info("watch key was cancelled or watch service was closed"); - watchService = initializeWatchService(path, watchService); - continue; - } - - List> watchEvents = key.pollEvents(); - boolean serviceAccountRegistryChanged = false; - for (WatchEvent event : watchEvents) { - if (event.context() != null && event.context().toString().equals(LOCAL_CONFIG_FILE_NAME)) { - serviceAccountRegistryChanged = true; - break; - } - } - - if (serviceAccountRegistryChanged) { - var currentServiceAccounts = readServiceAccounts(); - if (!previousServiceAccounts.deepEquals(currentServiceAccounts)) { - LOGGER.info("services changed. call doguChangeListener"); - doguChangeListener.onChange(); - previousServiceAccounts = currentServiceAccounts; - } - } - - // Resetting the watchKey is very important. Otherwise, the key returned from take() (or poll()) will not return any more events. - if (!key.reset()) { - LOGGER.info("watch key is no longer valid"); - watchService = initializeWatchService(path, watchService); - } - } - } catch (IOException e) { - throw new RegistryException("Failed to addDoguChangeListener", e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RegistryException("Failed to addDoguChangeListener", e); - } - } - - private WatchService initializeWatchService(Path path, WatchService oldWatchService) throws IOException { - if (oldWatchService != null) { - LOGGER.info("close old watch service"); - oldWatchService.close(); - } - LOGGER.info("initialize watch service"); - WatchService watchService = fileSystem.newWatchService(); - path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY); - - return watchService; - } - private static T readYaml(Class tClass, InputStream yamlStream) { - var yaml = new Yaml(new Constructor(tClass, new LoaderOptions())); - try { - T result = yaml.load(yamlStream); - if (result == null) { - LOGGER.warn("Parsed yaml result for class {} is null; Replacing with non-null instance.", tClass.getName()); - return tClass.getDeclaredConstructor().newInstance(); - } - - return result; - } catch (YAMLException e) { - throw new RegistryException(String.format("Failed to parse yaml stream to class %s", tClass.getName()), e); - } catch (InstantiationException | - IllegalAccessException | - InvocationTargetException | - NoSuchMethodException e) { - throw new RegistryException(String.format("Failed to construct new instance of %s", tClass.getName()), e); - } - } - - private static Object readKeyOutOfYaml(InputStream yamlStream, String key) { - Yaml yaml = new Yaml(); - Map obj = yaml.load(yamlStream); - return obj != null ? obj.get(key) : null; - } -} diff --git a/app/src/main/java/de/triology/cas/services/attributes/ReturnMappedAttributesPolicy.java b/app/src/main/java/de/triology/cas/services/attributes/ReturnMappedAttributesPolicy.java deleted file mode 100644 index 6f9966d0..00000000 --- a/app/src/main/java/de/triology/cas/services/attributes/ReturnMappedAttributesPolicy.java +++ /dev/null @@ -1,87 +0,0 @@ -package de.triology.cas.services.attributes; - -import lombok.*; -import lombok.extern.slf4j.Slf4j; -import org.apereo.cas.services.AbstractRegisteredServiceAttributeReleasePolicy; -import org.apereo.cas.services.RegisteredServiceAttributeReleasePolicyContext; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -/** - * Return all attributes required for the ecosystem - */ -@ToString(callSuper = true) -@Getter -@Setter -@EqualsAndHashCode(callSuper = true) -@AllArgsConstructor -@Slf4j -public class ReturnMappedAttributesPolicy extends AbstractRegisteredServiceAttributeReleasePolicy { - - private List allowedAttributes; - private Map attributesMappingRules; - - @Override - public Map> getAttributesInternal(RegisteredServiceAttributeReleasePolicyContext context, - Map> attributes) { - return authorizeReleaseOfAllowedAttributes(context, attributes); - } - - /** - * Authorize release of allowed attributes map. - * - * @param attrs the attributes - * @return the map - */ - protected Map> authorizeReleaseOfAllowedAttributes(RegisteredServiceAttributeReleasePolicyContext context, - Map> attrs) { - HashMap> attributesToRelease = new HashMap<>(); - if (allowedAttributes == null) { - return attributesToRelease; - } - - // map attributes - Map> mappedAttributes = mapAttributes(attrs); - - // order attributes - TreeMap> resolvedAttributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - resolvedAttributes.putAll(mappedAttributes); - - // filter attributes - getAllowedAttributes() - .stream() - .filter(resolvedAttributes::containsKey) - .forEach(attr -> { - LOGGER.debug("Found attribute [{}] in the list of allowed attributes", attr); - attributesToRelease.put(attr, resolvedAttributes.get(attr)); - }); - - - LOGGER.debug("Attributes to release [{}]", resolvedAttributes); - return attributesToRelease; - } - - @Override - protected List determineRequestedAttributeDefinitions(final RegisteredServiceAttributeReleasePolicyContext context) { - return getAllowedAttributes(); - } - - protected Map> mapAttributes(Map> attributes) { - if (attributesMappingRules == null) { - return attributes; - } - - LOGGER.debug("Start mapping of attributes with the following rules [{}]", attributesMappingRules); - - Map> mappedAttributes = new TreeMap<>(); - attributes.keySet().stream().filter(attributesMappingRules::containsKey).forEach(s -> { - LOGGER.debug("Transform attribute [{}] -> [{}]", s, attributesMappingRules.get(s)); - mappedAttributes.put(attributesMappingRules.get(s), attributes.get(s)); - }); - attributes.keySet().stream().filter(s -> !attributesMappingRules.containsKey(s)).forEach(s -> mappedAttributes.put(s, attributes.get(s))); - return mappedAttributes; - } -} diff --git a/app/src/main/java/de/triology/cas/services/dogu/CesDoguServiceFactory.java b/app/src/main/java/de/triology/cas/services/dogu/CesDoguServiceFactory.java deleted file mode 100644 index 27b0ccad..00000000 --- a/app/src/main/java/de/triology/cas/services/dogu/CesDoguServiceFactory.java +++ /dev/null @@ -1,51 +0,0 @@ -package de.triology.cas.services.dogu; - -import de.triology.cas.services.CesServiceData; -import org.apereo.cas.services.CasRegisteredService; -import org.apereo.cas.services.RegexMatchingRegisteredServiceProxyPolicy; -import org.apereo.cas.services.RegisteredServiceProxyPolicy; - -import java.net.URI; - -public class CesDoguServiceFactory implements CesServiceFactory { - - public static final String SERVICE_CAS_IDENTIFIER = "cas"; - - public CasRegisteredService createCASService(long id, String fqdn) { - CasRegisteredService casService = new CasRegisteredService(); - casService.setId(id); - casService.setServiceId(String.format("https://%s(:443)?/cas(/.*)?", generateServiceIdFqdnRegex(fqdn))); - casService.setName(CesDoguServiceFactory.class.getSimpleName() + " " + SERVICE_CAS_IDENTIFIER); - return casService; - } - - @Override - public CasRegisteredService createNewService(long id, String fqdn, URI casLogoutURI, CesServiceData serviceData) throws CesServiceCreationException { - String fqdnRegex = generateServiceIdFqdnRegex(fqdn); - - CasRegisteredService service = new CasRegisteredService(); - service.setId(id); - - String serviceId = String.format("https://%s(:443)?/%s(/.*)?", fqdnRegex, serviceData.getName()); - service.setServiceId(serviceId); - service.setName(serviceData.getIdentifier()); - - RegisteredServiceProxyPolicy proxyPolicy = new RegexMatchingRegisteredServiceProxyPolicy().setPattern("^https?://.*"); - service.setProxyPolicy(proxyPolicy); - - if (casLogoutURI != null) { - String logoutUri = String.format("https://%s/%s%s", fqdn, serviceData.getName(), casLogoutURI); - service.setLogoutUrl(logoutUri); - } - - return service; - } - - public static String generateServiceIdFqdnRegex(String fqdn) { - if (fqdn == null) { - return ""; - } - - return "((?i)" + fqdn.replace(".", "\\.") + ")"; - } -} \ No newline at end of file diff --git a/app/src/main/java/de/triology/cas/services/dogu/CesServiceCreationException.java b/app/src/main/java/de/triology/cas/services/dogu/CesServiceCreationException.java deleted file mode 100644 index 29c373c4..00000000 --- a/app/src/main/java/de/triology/cas/services/dogu/CesServiceCreationException.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.triology.cas.services.dogu; - -public class CesServiceCreationException extends Exception { - public CesServiceCreationException(String message) { - super(message); - } -} diff --git a/app/src/main/java/de/triology/cas/services/dogu/CesServiceFactory.java b/app/src/main/java/de/triology/cas/services/dogu/CesServiceFactory.java deleted file mode 100644 index a339ee28..00000000 --- a/app/src/main/java/de/triology/cas/services/dogu/CesServiceFactory.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.triology.cas.services.dogu; - -import de.triology.cas.services.CesServiceData; -import org.apereo.cas.services.BaseRegisteredService; -import org.apereo.cas.services.BaseWebBasedRegisteredService; - -import java.net.URI; - -/** - * Interface for Factories which create Services - */ -public interface CesServiceFactory { - - /** - * Creates and registers a new service. Additional attributes can be provided with the serviceData. - * - * @param id id of the service - * @param fqdn fqdn of the service - * @param serviceData data for the service - */ - BaseWebBasedRegisteredService createNewService(long id, String fqdn, URI casLogoutUri, CesServiceData serviceData) throws CesServiceCreationException; - - static CesServiceFactory getDefault() { - return new CesDoguServiceFactory(); - } - -} diff --git a/app/src/test/java/de/triology/cas/oidc/services/CesOAuthServiceFactoryTest.java b/app/src/test/java/de/triology/cas/oidc/services/CesOAuthServiceFactoryTest.java deleted file mode 100644 index 8eb9a8c9..00000000 --- a/app/src/test/java/de/triology/cas/oidc/services/CesOAuthServiceFactoryTest.java +++ /dev/null @@ -1,146 +0,0 @@ -package de.triology.cas.oidc.services; - -import de.triology.cas.services.CesServiceData; -import de.triology.cas.services.dogu.CesServiceCreationException; -import junit.framework.TestCase; -import org.apereo.cas.services.CasRegisteredService; -import org.apereo.cas.services.RegisteredService; -import org.apereo.cas.support.oauth.services.OAuthRegisteredService; - -import java.net.URI; -import java.util.HashMap; -import java.util.Map; - -public class CesOAuthServiceFactoryTest extends TestCase { - - protected void verifyService(RegisteredService service) { - assertEquals(1, service.getId()); - assertEquals("CesOAuthServiceFactory clientName", service.getName()); - assertEquals("https://((?i)192\\.168\\.56\\.2)(:443)?/clientName(/.*)?", service.getServiceId()); - - assertTrue(service instanceof OAuthRegisteredService); - OAuthRegisteredService oidcService = (OAuthRegisteredService) service; - assertEquals("[application/json, code]", oidcService.getSupportedResponseTypes().toString()); - assertEquals("[authorization_code]", oidcService.getSupportedGrantTypes().toString()); - assertEquals("superID", oidcService.getClientId()); - assertEquals("superSecretHash", oidcService.getClientSecret()); - assertTrue(oidcService.isBypassApprovalPrompt()); - } - - /** - * Test for {@link CesOAuthServiceFactory#createEmptyService()} - */ - public void testCreateEmptyService() { - // given - CesOAuthServiceFactory factory = new CesOAuthServiceFactory<>(OAuthRegisteredService::new); - - // when - OAuthRegisteredService service = factory.createEmptyService(); - - // then - assertNotNull(service); - } - - /** - * Test for {@link CesOAuthServiceFactory#createNewService(long, String, URI, CesServiceData)} - */ - public void testCreateNewService_noAttributes() { - // given - CesOAuthServiceFactory factory = new CesOAuthServiceFactory<>(OAuthRegisteredService::new); - CesServiceData serviceData = new CesServiceData("clientName", factory, null); - - try { - // when - factory.createNewService(1, null, null, serviceData); - fail(); - } catch (CesServiceCreationException e) { - // then - assertNotNull(e); - assertEquals("Cannot create service; Cannot find attributes", e.getMessage()); - } - } - - /** - * Test for {@link CesOAuthServiceFactory#createNewService(long, String, URI, CesServiceData)} - */ - public void testCreateNewService_noClientID() { - // given - CesOAuthServiceFactory factory = new CesOAuthServiceFactory<>(OAuthRegisteredService::new); - Map attributes = new HashMap<>(); - CesServiceData serviceData = new CesServiceData("clientName", factory, attributes); - - try { - // when - factory.createNewService(1, null, null, serviceData); - fail(); - } catch (CesServiceCreationException e) { - // then - assertNotNull(e); - assertEquals("Cannot create service; Cannot find attribute: oauth_client_id", e.getMessage()); - } - } - - /** - * Test for {@link CesOAuthServiceFactory#createNewService(long, String, URI, CesServiceData)} - */ - public void testCreateNewService_noClientSecret() { - // given - CesOAuthServiceFactory factory = new CesOAuthServiceFactory<>(OAuthRegisteredService::new); - Map attributes = new HashMap<>(); - attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_ID, "superID"); - CesServiceData serviceData = new CesServiceData("clientName", factory, attributes); - - try { - // when - factory.createNewService(1, null, null, serviceData); - fail(); - } catch (CesServiceCreationException e) { - // then - assertNotNull(e); - assertEquals("Cannot create service; Cannot find attribute: oauth_client_secret", e.getMessage()); - } - } - -// /** -// * Test for {@link CesOAuthServiceFactory#createNewService(long, String, URI, CesServiceData)} -// */ -// public void testCreateNewService_emptyLogoutURI() throws CesServiceCreationException { -// // given -// CesOAuthServiceFactory factory = new CesOAuthServiceFactory<>(CasOAuthRegisteredService::new); -// -// String fqdn = "192.168.56.2"; -// Map attributes = new HashMap<>(); -// attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_ID, "superID"); -// attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH, "superSecretHash"); -// CesServiceData serviceData = new CesServiceData("clientName", factory, attributes); -// -// // when -// OAuthRegisteredService service = (OAuthRegisteredService)factory.createNewService(1, fqdn, null, serviceData); -// -// // then -// verifyService(service); -// assertNull(service.getLogoutUrl()); -// } - -// /** -// * Test for {@link CesOAuthServiceFactory#createNewService(long, String, URI, CesServiceData)} -// */ -// public void testCreateNewService_givenLogoutURI() throws CesServiceCreationException { -// // given -// CesOAuthServiceFactory factory = new CesOAuthServiceFactory<>(CasOAuthRegisteredService::new); -// -// String fqdn = "192.168.56.2"; -// URI logoutUri = URI.create("/api/auth/oidc/logout"); -// Map attributes = new HashMap<>(); -// attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_ID, "superID"); -// attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH, "superSecretHash"); -// CesServiceData serviceData = new CesServiceData("clientName", factory, attributes); -// -// // when -// OAuthRegisteredService service = (OAuthRegisteredService)factory.createNewService(1, fqdn, logoutUri, serviceData); -// -// // then -// verifyService(service); -// assertEquals("https://192.168.56.2/clientName/api/auth/oidc/logout", service.getLogoutUrl()); -// } -} \ No newline at end of file diff --git a/app/src/test/java/de/triology/cas/oidc/services/CesOidcServiceFactoryTest.java b/app/src/test/java/de/triology/cas/oidc/services/CesOidcServiceFactoryTest.java deleted file mode 100644 index 7e15f57e..00000000 --- a/app/src/test/java/de/triology/cas/oidc/services/CesOidcServiceFactoryTest.java +++ /dev/null @@ -1,131 +0,0 @@ -package de.triology.cas.oidc.services; - -import de.triology.cas.services.CesServiceData; -import de.triology.cas.services.dogu.CesServiceCreationException; -import junit.framework.TestCase; -import org.apereo.cas.services.OidcRegisteredService; -import org.apereo.cas.services.RegisteredService; - -import java.net.URI; -import java.util.HashMap; -import java.util.Map; - -public class CesOidcServiceFactoryTest extends TestCase { - - /** - * Test for {@link CesOAuthServiceFactory#createNewService(long, String, URI, CesServiceData)} - */ - public void testCreateNewService_noAttributes() { - // given - var factory = new CesOAuthServiceFactory<>(OidcRegisteredService::new); - CesServiceData serviceData = new CesServiceData("oidcClient", factory, null); - - try { - // when - factory.createNewService(1, null, null, serviceData); - fail(); - } catch (CesServiceCreationException e) { - // then - assertNotNull(e); - assertEquals("Cannot create service; Cannot find attributes", e.getMessage()); - } - } - - /** - * Test for {@link CesOAuthServiceFactory#createNewService(long, String, URI, CesServiceData)} - */ - public void testCreateNewService_noClientID() { - // given - var factory = new CesOAuthServiceFactory<>(OidcRegisteredService::new); - Map attributes = new HashMap<>(); - CesServiceData serviceData = new CesServiceData("oidcClient", factory, attributes); - - try { - // when - factory.createNewService(1, null, null, serviceData); - fail(); - } catch (CesServiceCreationException e) { - // then - assertNotNull(e); - assertEquals("Cannot create service; Cannot find attribute: oauth_client_id", e.getMessage()); - } - } - - /** - * Test for {@link CesOAuthServiceFactory#createNewService(long, String, URI, CesServiceData)} - */ - public void testCreateNewService_noClientSecret() { - // given - var factory = new CesOAuthServiceFactory<>(OidcRegisteredService::new); - Map attributes = new HashMap<>(); - attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_ID, "superID"); - CesServiceData serviceData = new CesServiceData("oidcClient", factory, attributes); - - try { - // when - factory.createNewService(1, null, null, serviceData); - fail(); - } catch (CesServiceCreationException e) { - // then - assertNotNull(e); - assertEquals("Cannot create service; Cannot find attribute: oauth_client_secret", e.getMessage()); - } - } - -// /** -// * Test for {@link CesOAuthServiceFactory#createNewService(long, String, URI, CesServiceData)} -// */ -// public void testCreateNewService_emptyLogoutURI() throws CesServiceCreationException { -// // given -// var factory = new CesOAuthServiceFactory<>(CasOidcRegisteredService::new); -// -// String fqdn = "192.168.56.2"; -// Map attributes = new HashMap<>(); -// attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_ID, "superID"); -// attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH, "superSecretHash"); -// CesServiceData serviceData = new CesServiceData("oidcClient", factory, attributes); -// -// // when -// CasOidcRegisteredService service = (CasOidcRegisteredService)factory.createNewService(1, fqdn, null, serviceData); -// -// // then -// verifyService(service); -// assertNull(service.getLogoutUrl()); -// } - -// /** -// * Test for {@link CesOAuthServiceFactory#createNewService(long, String, URI, CesServiceData)} -// */ -// public void testCreateNewService_givenLogoutURI() throws CesServiceCreationException { -// // given -// var factory = new CesOAuthServiceFactory<>(CasOidcRegisteredService::new); -// -// String fqdn = "192.168.56.2"; -// URI logoutUri = URI.create("/api/auth/oidc/logout"); -// Map attributes = new HashMap<>(); -// attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_ID, "superID"); -// attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH, "superSecretHash"); -// CesServiceData serviceData = new CesServiceData("oidcClient", factory, attributes); -// -// // when -// CasOidcRegisteredService service = (CasOidcRegisteredService)factory.createNewService(1, fqdn, logoutUri, serviceData); -// -// // then -// verifyService(service); -// assertEquals("https://192.168.56.2/oidcClient/api/auth/oidc/logout", service.getLogoutUrl()); -// } - - private void verifyService(RegisteredService service) { - assertEquals(1, service.getId()); - assertEquals("CesOAuthServiceFactory oidcClient", service.getName()); - assertEquals("https://((?i)192\\.168\\.56\\.2)(:443)?/oidcClient(/.*)?", service.getServiceId()); - - assertTrue(service instanceof OidcRegisteredService); - OidcRegisteredService oidcService = (OidcRegisteredService) service; - assertEquals("[application/json, code]", oidcService.getSupportedResponseTypes().toString()); - assertEquals("[authorization_code]", oidcService.getSupportedGrantTypes().toString()); - assertEquals("superID", oidcService.getClientId()); - assertEquals("superSecretHash", oidcService.getClientSecret()); - assertTrue(oidcService.isBypassApprovalPrompt()); - } -} \ No newline at end of file diff --git a/app/src/test/java/de/triology/cas/services/CesServicesManagerStageDevelopmentTest.java b/app/src/test/java/de/triology/cas/services/CesServicesManagerStageDevelopmentTest.java deleted file mode 100644 index ff13c6aa..00000000 --- a/app/src/test/java/de/triology/cas/services/CesServicesManagerStageDevelopmentTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package de.triology.cas.services; - -import org.apereo.cas.services.RegisteredService; -import org.junit.Test; - -import java.util.Collection; - -import static org.junit.Assert.assertEquals; - -/** - * Tests for {@link CesServicesManagerStageDevelopment}. - */ -public class CesServicesManagerStageDevelopmentTest { - /** - * ID of the service used in development mode. - */ - private static final long DEVELOPMENT_SERVICE_ID = 1; - /** - * Service ID of the service used in development mode. - */ - private static final String DEVELOPMENT_SERVICE_SERVICE_ID = "^(https?|imaps?)://.*"; - - CesServicesManagerStageDevelopment stage = new CesServicesManagerStageDevelopment(new CesServiceManagerConfiguration("development", null, null, false, null, null)); - - /** - * Test for {@link CesServicesManagerStageDevelopment#getRegisteredServices()} - */ - @Test - public void getRegisteredServices() { - Collection allServices = stage.getRegisteredServices().values(); - assertEquals("Unexpected amount of services returned in development mode", 2, allServices.size()); - assertEquals("Development service not returned by getRegisteredServices(). Id mismatch.", - DEVELOPMENT_SERVICE_ID, allServices.iterator().next().getId()); - assertEquals("Development service not returned by getRegisteredServices(). ServiceId mismatch.", - DEVELOPMENT_SERVICE_SERVICE_ID, allServices.iterator().next().getServiceId()); - } -} \ No newline at end of file diff --git a/app/src/test/java/de/triology/cas/services/CesServicesManagerStageProductiveTest.java b/app/src/test/java/de/triology/cas/services/CesServicesManagerStageProductiveTest.java deleted file mode 100644 index 0bb40e62..00000000 --- a/app/src/test/java/de/triology/cas/services/CesServicesManagerStageProductiveTest.java +++ /dev/null @@ -1,355 +0,0 @@ -package de.triology.cas.services; - -import de.triology.cas.oidc.services.CesOAuthServiceFactory; -import de.triology.cas.services.Registry.DoguChangeListener; -import de.triology.cas.services.attributes.ReturnMappedAttributesPolicy; -import de.triology.cas.services.dogu.CesDoguServiceFactory; -import de.triology.cas.services.dogu.CesServiceCreationException; -import org.apereo.cas.services.*; -import org.apereo.cas.support.oauth.services.OAuthRegisteredService; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -import java.util.*; -import java.util.stream.Collectors; - -import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -/** - * Tests for {@link CesServicesManagerStageProductive}. - */ -public class CesServicesManagerStageProductiveTest { - private static final String EXPECTED_FULLY_QUALIFIED_DOMAIN_NAME = "fully/qualified"; - private static final String EXPECTED_FULLY_QUALIFIED_DOMAIN_NAME_REGEX = CesDoguServiceFactory.generateServiceIdFqdnRegex("fully/qualified"); - private static final CesDoguServiceFactory doguServiceFactory = new CesDoguServiceFactory(); - private static final CesOAuthServiceFactory oAuthServiceFactory = new CesOAuthServiceFactory<>(OAuthRegisteredService::new); - private static final CesOAuthServiceFactory oidcServiceFactory = new CesOAuthServiceFactory<>(OidcRegisteredService::new); - private static final CesServiceData EXPECTED_SERVICE_DATA_1 = new CesServiceData("nexus", doguServiceFactory); - private static final CesServiceData EXPECTED_SERVICE_DATA_2 = new CesServiceData("smeagol", doguServiceFactory); - private static final CesServiceData EXPECTED_SERVICE_CAS = new CesServiceData("cas", doguServiceFactory); - private static final CesServiceData EXPECTED_OAUTH_SERVICE_DATA = new CesServiceData("portainer", oAuthServiceFactory); - private static final CesServiceData EXPECTED_OIDC_SERVICE_DATA = new CesServiceData("cas-oidc-client", oidcServiceFactory); - - private List expectedAllowedAttributes = Arrays.asList("attribute a", "attribute b"); - private Map attributesMappingRules = Map.of("attribute z", "attribute a"); - private List expectedServices; - private Registry registry = mock(Registry.class); - private CesServiceManagerConfiguration managerConfig = new CesServiceManagerConfiguration("stage", expectedAllowedAttributes, attributesMappingRules, false, null, "username"); - private CesServiceManagerConfiguration managerConfigWithOIDC = new CesServiceManagerConfiguration("stage", expectedAllowedAttributes, attributesMappingRules, true, "my-test-name", "username"); - private CesServicesManagerStageProductive stage = - new CesServicesManagerStageProductive(managerConfig, registry); - private CesServicesManagerStageProductive stageWithOIDC = - new CesServicesManagerStageProductive(managerConfigWithOIDC, registry); - - @Before - public void setUp() { - when(registry.getFqdn()).thenReturn(EXPECTED_FULLY_QUALIFIED_DOMAIN_NAME); - when(registry.getInstalledCasServiceAccountsOfType(any(), any())) - .thenReturn(List.of(EXPECTED_SERVICE_DATA_1, EXPECTED_SERVICE_DATA_2)); - - expectedServices = new LinkedList<>(Arrays.asList( - new ExpectedService().name(EXPECTED_SERVICE_DATA_1.getIdentifier()) - .serviceId("https://" + EXPECTED_FULLY_QUALIFIED_DOMAIN_NAME_REGEX + "(:443)?/nexus(/.*)?") - .serviceIdExample("https://" + EXPECTED_FULLY_QUALIFIED_DOMAIN_NAME + "/nexus/something"), - new ExpectedService().name(EXPECTED_SERVICE_DATA_2.getIdentifier()) - .serviceId("https://" + EXPECTED_FULLY_QUALIFIED_DOMAIN_NAME_REGEX + "(:443)?/smeagol(/.*)?") - .serviceIdExample("https://" + EXPECTED_FULLY_QUALIFIED_DOMAIN_NAME + "/smeagol/somethingElse"), - new ExpectedService().name(EXPECTED_SERVICE_CAS.getIdentifier()) - .serviceId("https://" + EXPECTED_FULLY_QUALIFIED_DOMAIN_NAME_REGEX + "(:443)?/cas(/.*)?") - .serviceIdExample("https://" + EXPECTED_FULLY_QUALIFIED_DOMAIN_NAME + "/cas/somethingElse") - )); - } - - /** - * Test for listener, when a dogu is added after initialization. - */ - @Test - public void doguChangeListenerAddDogu() { - // Initialize expectedServices - DoguChangeListener doguChangeListener = initialize(); - - // Add service - String expectedServiceName3 = "scm"; - CesServiceData serviceDataSCM = new CesServiceData(expectedServiceName3, doguServiceFactory); - - doReturn(new LinkedList<>(Arrays.asList(EXPECTED_SERVICE_DATA_1, EXPECTED_SERVICE_DATA_2, serviceDataSCM))) - .when(registry).getInstalledCasServiceAccountsOfType(eq(Registry.SERVICE_ACCOUNT_TYPE_CAS), any()); - expectedServices.add(new ExpectedService().name(serviceDataSCM.getIdentifier()) - .serviceId("https://" + EXPECTED_FULLY_QUALIFIED_DOMAIN_NAME_REGEX + "(:443)?/scm(/.*)?")); - //Do not expect the o auth service as the attributes are missing - - // Notify manager of change - doguChangeListener.onChange(); - - Collection allServices = stage.getRegisteredServices().values(); - - for (ExpectedService expectedService : expectedServices) { - expectedService.assertContainedIn(allServices); - } - } - - /** - * Test for listener, when a dogu is added after initialization. - */ - @Test - public void managerAddDelegatedAuthenticationProvider() { - doReturn(new LinkedList<>(Arrays.asList(EXPECTED_SERVICE_DATA_1, EXPECTED_SERVICE_DATA_2))) - .when(registry).getInstalledCasServiceAccountsOfType(any(), any()); - - // Check services of oidc stage - Collection allServicesOfOIDCStage = stageWithOIDC.getRegisteredServices().values(); - for (RegisteredService expectedService : allServicesOfOIDCStage) { - assertTrue(expectedService.getAccessStrategy() instanceof DefaultRegisteredServiceAccessStrategy); - assertTrue(expectedService.getAccessStrategy().getDelegatedAuthenticationPolicy() instanceof DefaultRegisteredServiceDelegatedAuthenticationPolicy); - assertTrue(expectedService.getUsernameAttributeProvider() instanceof PrincipalAttributeRegisteredServiceUsernameProvider); - assertEquals("username", ((PrincipalAttributeRegisteredServiceUsernameProvider) expectedService.getUsernameAttributeProvider()).getUsernameAttribute()); - List allowedProviders = new ArrayList<>(expectedService.getAccessStrategy().getDelegatedAuthenticationPolicy().getAllowedProviders()); - assertEquals(1, allowedProviders.size()); - assertEquals(managerConfigWithOIDC.getOidcClientDisplayName(), allowedProviders.getFirst()); - } - - // Check services of default stage - Collection allServicesOfDefaultStage = stage.getRegisteredServices().values(); - for (RegisteredService expectedService : allServicesOfDefaultStage) { - assertTrue(expectedService.getAccessStrategy() instanceof DefaultRegisteredServiceAccessStrategy); - assertTrue(expectedService.getUsernameAttributeProvider() instanceof DefaultRegisteredServiceUsernameProvider); - assertTrue(expectedService.getAccessStrategy().getDelegatedAuthenticationPolicy() instanceof DefaultRegisteredServiceDelegatedAuthenticationPolicy); - assertNull(null, expectedService.getAccessStrategy().getDelegatedAuthenticationPolicy().getAllowedProviders()); - } - } - - /** - * Test for listener, when a dogu is added after initialization. - */ - @Test - public void doguChangeListenerAddDoguNoFail() { - // Initialize expectedServices - DoguChangeListener doguChangeListener = initialize(); - - // Add service - String expectedServiceName3 = "scm"; - CesServiceData serviceDataSCM = new CesServiceData(expectedServiceName3, doguServiceFactory); - - HashMap attributes = new HashMap<>(); - attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_ID, EXPECTED_OAUTH_SERVICE_DATA.getName()); - attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH, "supersecret"); - CesServiceData correctOAuthService = new CesServiceData(EXPECTED_OAUTH_SERVICE_DATA.getName(), oAuthServiceFactory, attributes); - - attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_ID, EXPECTED_OIDC_SERVICE_DATA.getName()); - attributes.put(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH, "supersecret"); - CesServiceData correctOidcService = new CesServiceData(EXPECTED_OIDC_SERVICE_DATA.getName(), oidcServiceFactory, attributes); - - doReturn(new LinkedList<>(Arrays.asList(EXPECTED_SERVICE_DATA_1, EXPECTED_SERVICE_DATA_2, serviceDataSCM))) - .when(registry).getInstalledCasServiceAccountsOfType(Registry.SERVICE_ACCOUNT_TYPE_CAS, stage.doguServiceFactory); - doReturn(new LinkedList<>(Collections.singletonList(correctOAuthService))) - .when(registry).getInstalledCasServiceAccountsOfType(Registry.SERVICE_ACCOUNT_TYPE_OAUTH, stage.oAuthServiceFactory); - doReturn(new LinkedList<>(Collections.singletonList(correctOidcService))) - .when(registry).getInstalledCasServiceAccountsOfType(Registry.SERVICE_ACCOUNT_TYPE_OIDC, stage.oidcServiceFactory); - - expectedServices.add(new ExpectedService().name(serviceDataSCM.getIdentifier()) - .serviceId("https://" + EXPECTED_FULLY_QUALIFIED_DOMAIN_NAME_REGEX + "(:443)?/scm(/.*)?")); - expectedServices.add(new ExpectedService().name(correctOAuthService.getIdentifier()) - .serviceId("https://" + EXPECTED_FULLY_QUALIFIED_DOMAIN_NAME_REGEX + "(:443)?/portainer(/.*)?")); - expectedServices.add(new ExpectedService().name(correctOidcService.getIdentifier()) - .serviceId("https://" + EXPECTED_FULLY_QUALIFIED_DOMAIN_NAME_REGEX + "(:443)?/cas-oidc-client(/.*)?")); - - // Notify manager of change - doguChangeListener.onChange(); - - Collection allServices = stage.getRegisteredServices().values(); - - for (ExpectedService expectedService : expectedServices) { - expectedService.assertContainedIn(allServices); - } - } - - /** - * Test for update-method when a dogu is added after initialization. - */ - @Test - public void updateRegisteredServicesAddService() { - stage.initRegisteredServices(); - - // Add service - String expectedServiceName3 = "scm"; - CesServiceData serviceDataSCM = new CesServiceData(expectedServiceName3, doguServiceFactory); - - when(registry.getInstalledCasServiceAccountsOfType(any(), any())).thenReturn(new LinkedList<>( - Arrays.asList(EXPECTED_SERVICE_DATA_1, EXPECTED_SERVICE_DATA_2, serviceDataSCM))); - - ExpectedService service3 = new ExpectedService().name(serviceDataSCM.getIdentifier()) - .serviceId("https://" + EXPECTED_FULLY_QUALIFIED_DOMAIN_NAME_REGEX + "(:443)?/scm(/.*)?"); - expectedServices.add(service3); - - Collection allServices = stage.getRegisteredServices().values(); - - // assure if the new service is NOT already in allServices before update is executed - service3.assertNotContainedIn(allServices); - // do update - stage.updateRegisteredServices(); - // now check if update was executed successful - for (ExpectedService expectedService : expectedServices) { - expectedService.assertContainedIn(allServices); - } - } - - /** - * Test for update-method without an initialization. - * This happens in production if the user does not use cas before the first automatic update. - */ - @Test - public void updateRegisteredServicesWithoutInit() { - stage.updateRegisteredServices(); - - Collection allServices = stage.getRegisteredServices().values(); - - for (ExpectedService expectedService : expectedServices) { - expectedService.assertContainedIn(allServices); - } - } - - /** - * Stage should still be in the same state after calling initRegisteredServices - * a second time. - */ - @Test - public void initNotPerformedTwice() { - stage.initRegisteredServices(); - stage.initRegisteredServices(); - - Collection allServices = stage.getRegisteredServices().values(); - // ensures that init only happened once - for (ExpectedService expectedService : expectedServices) { - expectedService.assertContainedIn(allServices); - } - } - - @Test - public void addNewServiceWhichHasNoLogoutUri() throws GetCasLogoutUriException, CesServiceCreationException { - // given - RegistryEtcd etcdRegistry = mock(RegistryEtcd.class); - CesServicesManagerStageProductive productiveStage = - new CesServicesManagerStageProductive(managerConfig, etcdRegistry); - GetCasLogoutUriException expectedException = new GetCasLogoutUriException("expected exception"); - when(etcdRegistry.getCasLogoutUri(any())).thenThrow(expectedException); - CesServiceData testServiceData = new CesServiceData("testService", doguServiceFactory); - var testService = doguServiceFactory.createNewService( - productiveStage.createId(), EXPECTED_FULLY_QUALIFIED_DOMAIN_NAME, null, testServiceData); - - // when - productiveStage.addNewService(testService); - - // then - CasRegisteredService registeredService = (CasRegisteredService) productiveStage.getRegisteredServices().get(1L); - assertNull(registeredService.getLogoutUrl()); - } - - /** - * Test for listener, when a dogu is removed after initialization. - */ - @Test - public void doguChangeListenerAddDoguRemoveDogu() { - // Initialize expectedServices - DoguChangeListener doguChangeListener = initialize(); - - // Remove service - when(registry.getInstalledCasServiceAccountsOfType(any(), any())).thenReturn(new LinkedList<>(Collections.singletonList(EXPECTED_SERVICE_DATA_2))); - expectedServices = expectedServices.stream().filter(expectedService -> !EXPECTED_SERVICE_DATA_1.getIdentifier() - .equals(expectedService.name)).collect(Collectors.toList()); - - // Notify manager of change - doguChangeListener.onChange(); - - Collection allServices = stage.getRegisteredServices().values(); - assertEquals(expectedServices.size(), allServices.size()); - for (ExpectedService expectedService : expectedServices) { - expectedService.assertContainedIn(allServices); - } - } - - /** - * Calls {@link CesServicesManagerStageProductive#getRegisteredServices()} and returns the - * {@link DoguChangeListener} passed to {@link Registry#addDoguChangeListener(DoguChangeListener)}. - */ - private DoguChangeListener initialize() { - Collection allServices = stage.getRegisteredServices().values(); - for (ExpectedService expectedService : expectedServices) { - expectedService.assertContainedIn(allServices); - } - ArgumentCaptor doguChangeListener = ArgumentCaptor.forClass(DoguChangeListener.class); - verify(registry).addDoguChangeListener(doguChangeListener.capture()); - return doguChangeListener.getValue(); - } - - /** - * Helper class for storing test data and asserting services. - */ - private class ExpectedService { - boolean allowedToProxy = true; - List allowedAttributes = expectedAllowedAttributes; - String name; - String serviceId; - String serviceIdExample; - - ExpectedService name(String name) { - this.name = name; - return this; - } - - ExpectedService serviceId(String serviceId) { - this.serviceId = serviceId; - return this; - } - - /** - * An example that matches the service ID. This attrbitute is ignored in {@link #assertContainedIn(Collection)}. - */ - ExpectedService serviceIdExample(String serviceIdExample) { - this.serviceIdExample = serviceIdExample; - return this; - } - - /** - * Asserts that a service with the specified name is contained within services and that this - * service's attributes equal the one specified in this {@link ExpectedService}. - */ - void assertContainedIn(Collection services) { - List matchingServices = - services.stream().filter(registeredService -> name.equals(registeredService.getName())) - .collect(Collectors.toList()); - Assert.assertEquals("Unexpected amount of services matching name=\"" + name + "\" found within services " - + services, 1, matchingServices.size()); - RegisteredService actualService = matchingServices.get(0); - - assertEquals("Service \" + name \": ID is not unique", 1, services.stream() - .filter(registeredService -> actualService.getId() == registeredService.getId()) - .count()); - assertEqualsService(actualService); - } - - /** - * Asserts that a service with the specified name is not contained within services - */ - void assertNotContainedIn(Collection services) { - List matchingServices = - services.stream().filter(registeredService -> name.equals(registeredService.getName())) - .collect(Collectors.toList()); - Assert.assertEquals("Unexpected amount of services matching name=\"" + name + "\" found within services " - + services, 0, matchingServices.size()); - } - - /** - * Asserts that this service's attributes equal the one specified in this {@link ExpectedService}. - */ - void assertEqualsService(RegisteredService actualService) { - assertEquals("Service \" + name \": Unexpected value allowedAttributes", allowedAttributes, - ((ReturnMappedAttributesPolicy) actualService.getAttributeReleasePolicy()).getAllowedAttributes()); - assertEquals("Service \" + name \": Unexpected value serviceId", serviceId, - actualService.getServiceId()); - } - } -} diff --git a/app/src/test/java/de/triology/cas/services/CesServicesManagerTest.java b/app/src/test/java/de/triology/cas/services/CesServicesManagerTest.java deleted file mode 100644 index c7a92e6b..00000000 --- a/app/src/test/java/de/triology/cas/services/CesServicesManagerTest.java +++ /dev/null @@ -1,369 +0,0 @@ -package de.triology.cas.services; - -import org.apereo.cas.authentication.principal.Service; -import org.apereo.cas.services.OidcRegisteredService; -import org.apereo.cas.services.RegisteredService; -import org.apereo.cas.services.query.RegisteredServiceQuery; -import org.apereo.cas.support.oauth.services.OAuthRegisteredService; -import org.hamcrest.MatcherAssert; -import org.junit.Test; - -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.Stream; - -import static de.triology.cas.services.CesServicesManager.STAGE_DEVELOPMENT; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; - -/** - * Tests for {@link CesServicesManager} - */ -public class CesServicesManagerTest { - CesServicesManagerStage servicesManagerStage = mock(CesServicesManagerStage.class); - CesServiceManagerConfiguration managerDevelopmentConfig = new CesServiceManagerConfiguration(STAGE_DEVELOPMENT, null, null, false, null, null); - CesServiceManagerConfiguration managerConfig = new CesServiceManagerConfiguration("don't care", null, null, false, null, null); - CesServicesManager etcdServicesManger = new CesServiceManagerUnderTest(managerConfig, mock(Registry.class)); - - /** - * Test for {@link CesServicesManager#CesServicesManager(CesServiceManagerConfiguration, Registry)} )} for production. - */ - @Test - public void constructForProduction() { - new CesServicesManager(managerConfig, null) { - @Override - protected CesServicesManagerStage createStage(CesServiceManagerConfiguration managerConfig, Registry registry) { - CesServicesManagerStage stage = super.createStage(managerConfig, registry); - MatcherAssert.assertThat(stage, instanceOf(CesServicesManagerStageProductive.class)); - return stage; - } - }; - } - - /** - * Test for {@link CesServicesManager#CesServicesManager(CesServiceManagerConfiguration, Registry)} for production. - */ - @Test - public void constructForDevelopment() { - new CesServicesManager(managerDevelopmentConfig, null) { - @Override - protected CesServicesManagerStage createStage(CesServiceManagerConfiguration managerConfig, Registry registry) { - CesServicesManagerStage stage = super.createStage(managerConfig, registry); - MatcherAssert.assertThat(stage, instanceOf(CesServicesManagerStageDevelopment.class)); - return stage; - } - }; - } - - /** - * Test for {@link CesServicesManager#getAllServices()}. - */ - @Test - public void getAllServices() { - RegisteredService service1 = mock(RegisteredService.class); - RegisteredService service2 = mock(RegisteredService.class); - HashMap expectedServices = new HashMap<>() {{ - put(0L, service1); - put(23L, service2); - }}; - when(servicesManagerStage.getRegisteredServices()).thenReturn(expectedServices); - Collection allServices = etcdServicesManger.getAllServices(); - MatcherAssert.assertThat(allServices, containsInAnyOrder(service1, service2)); - } - - /** - * Test for {@link CesServicesManager#getAllServices()}. - */ - @Test - public void getAllServicesOfType() { - // given - RegisteredService service1 = mock(RegisteredService.class); - RegisteredService service2 = mock(RegisteredService.class); - RegisteredService service3 = mock(OAuthRegisteredService.class); - RegisteredService service4 = mock(OidcRegisteredService.class); - HashMap expectedServices = new HashMap<>() {{ - put(0L, service1); - put(23L, service2); - put(50L, service3); - put(51L, service4); - }}; - when(servicesManagerStage.getRegisteredServices()).thenReturn(expectedServices); - - // when - Collection allRegisteredServices = etcdServicesManger.getAllServicesOfType(RegisteredService.class); - // then - MatcherAssert.assertThat(allRegisteredServices, containsInAnyOrder(service1, service2, service3, service4)); - assertEquals(allRegisteredServices.size(), 4); - - // when - Collection allOauthServices = etcdServicesManger.getAllServicesOfType(OAuthRegisteredService.class); - // then - MatcherAssert.assertThat(allOauthServices, containsInAnyOrder(service3, service4)); - assertEquals(allOauthServices.size(), 2); - - // when - Collection allOIDCServices = etcdServicesManger.getAllServicesOfType(OidcRegisteredService.class); - // then - MatcherAssert.assertThat(allOIDCServices, containsInAnyOrder(service4)); - assertEquals(allOIDCServices.size(), 1); - - // when - Collection noServices = etcdServicesManger.getAllServicesOfType(CesServicesManager.class); - // then - assertEquals(noServices.size(), 0); - } - - /** - * Test for {@link CesServicesManager#getAllServices()} where the result is modified. - */ - @Test - public void assertGetAllServicesModify() { - when(servicesManagerStage.getRegisteredServices()).thenReturn(new HashMap<>()); - assertThrows(UnsupportedOperationException.class, () -> { - Collection allServices = etcdServicesManger.getAllServices(); - allServices.add(mock(RegisteredService.class)); - }); - } - - /** - * Test for {@link CesServicesManager#getServicesForDomain(String)}. - */ - @Test - public void getServicesForDomain() { - assertThrows(UnsupportedOperationException.class, () -> { - etcdServicesManger.getServicesForDomain("someDomain"); - }); - } - - /** - * Test for {@link CesServicesManager#findServiceBy(Service)}. - */ - @Test - public void findServiceBy() { - RegisteredService expectedRegisteredService = mock(RegisteredService.class); - HashMap expectedServices = new HashMap<>() {{ - put(0L, mock(RegisteredService.class)); - put(23L, expectedRegisteredService); - }}; - when(servicesManagerStage.getRegisteredServices()).thenReturn(expectedServices); - - Service service = mock(Service.class); - when(expectedRegisteredService.matches(service)).thenReturn(true); - - RegisteredService actualRegisteredService = etcdServicesManger.findServiceBy(service); - assertEquals("findServiceBy(Service) did not return registered service", expectedRegisteredService, - actualRegisteredService); - } - - /** - * Test for {@link CesServicesManager#findServiceBy(Service)} for a service that does not exist. - */ - @Test - public void findServiceByNegative() { - HashMap expectedServices = new HashMap<>() {{ - put(0L, mock(RegisteredService.class)); - put(23L, mock(RegisteredService.class)); - }}; - when(servicesManagerStage.getRegisteredServices()).thenReturn(expectedServices); - - RegisteredService registeredService = etcdServicesManger.findServiceBy(mock(Service.class)); - assertNull("findServiceBy(Service) unexpectedly returned registered service", registeredService); - } - - /** - * Test for {@link CesServicesManager#findServicesBy(RegisteredServiceQuery[])}. - */ - @Test - public void findServiceByQuery() { - OAuthRegisteredService expectedRegisteredService = mock(OAuthRegisteredService.class); - when(expectedRegisteredService.getClientId()).thenReturn("portainer"); - - OAuthRegisteredService otherRegisteredService = mock(OAuthRegisteredService.class); - when(otherRegisteredService.getClientId()).thenReturn("otherService"); - - HashMap expectedServices = new HashMap<>() {{ - put(0L, otherRegisteredService); - put(4L, mock(RegisteredService.class)); - put(23L, expectedRegisteredService); - }}; - when(servicesManagerStage.getRegisteredServices()).thenReturn(expectedServices); - - RegisteredServiceQuery query = mock(RegisteredServiceQuery.class); - when(query.getType()).thenReturn(OAuthRegisteredService.class); - when(query.getName()).thenReturn("clientId"); - when(query.getValue()).thenReturn("portainer"); - when(query.isIncludeAssignableTypes()).thenReturn(true); - - RegisteredServiceQuery query2 = mock(RegisteredServiceQuery.class); - when(query2.getName()).thenReturn("notSupported"); - - List actualRegisteredServices = etcdServicesManger.findServicesBy(query, query2).toList(); - assertEquals(1, actualRegisteredServices.size()); - assertEquals("findServicesBy(Query) did not return registered service", expectedRegisteredService, - actualRegisteredServices.getFirst()); - } - - /** - * Test for {@link CesServicesManager#findServiceBy(long)}. - */ - @Test - public void findServiceById() { - RegisteredService expectedService = mock(RegisteredService.class); - HashMap expectedServices = new HashMap<>() {{ - put(0L, mock(RegisteredService.class)); - put(23L, expectedService); - }}; - when(servicesManagerStage.getRegisteredServices()).thenReturn(expectedServices); - RegisteredService actualService = etcdServicesManger.findServiceBy(23); - assertEquals("findServiceBy(long) did not return registered service", expectedService, actualService); - } - - /** - * Test for {@link CesServicesManager#findServiceBy(long)} for a service that does not exist. - */ - @Test - public void findServiceByIdNegative() { - HashMap expectedServices = new HashMap<>() {{ - put(0L, mock(RegisteredService.class)); - put(23L, mock(RegisteredService.class)); - }}; - when(servicesManagerStage.getRegisteredServices()).thenReturn(expectedServices); - RegisteredService registeredService = etcdServicesManger.findServiceBy(42); - assertNull("findServiceBy(long) unexpectedly returned registered service", registeredService); - } - - /** - * Test for {@link CesServicesManager#findServiceBy(Predicate)}. - */ - @Test - public void findServiceByPredicate() { - assertThrows(UnsupportedOperationException.class, () -> { - etcdServicesManger.findServiceBy(registeredService -> false); - }); - } - - /** - * Test for {@link CesServicesManager#findServiceBy(Service, Class)} . - */ - @Test - public void findServiceByServiceAndClass() { - RegisteredService expectedRegisteredService = mock(RegisteredService.class); - HashMap expectedServices = new HashMap<>() {{ - put(0L, mock(RegisteredService.class)); - put(23L, expectedRegisteredService); - }}; - when(servicesManagerStage.getRegisteredServices()).thenReturn(expectedServices); - - Service service = mock(Service.class); - when(expectedRegisteredService.matches(service)).thenReturn(true); - - RegisteredService actualRegisteredService = etcdServicesManger.findServiceBy(service, RegisteredService.class); - assertEquals("findServiceBy(Service, Class) did not return registered service", expectedRegisteredService, - actualRegisteredService); - - - actualRegisteredService = etcdServicesManger.findServiceBy(null, RegisteredService.class); - assertNull("findServiceBy(Service, Class) did not return null", actualRegisteredService); - - actualRegisteredService = etcdServicesManger.findServiceBy(service, OAuthRegisteredService.class); - assertNull("findServiceBy(Service, Class) did not return null", actualRegisteredService); - } - - /** - * Test for {@link CesServicesManager#findServiceByName(String)} - */ - @Test - public void findServiceByName() { - assertThrows(UnsupportedOperationException.class, () -> { - etcdServicesManger.findServiceByName("name"); - }); - } - - /** - * Test for {@link CesServicesManager#load()}. - */ - @Test - public void load() { - etcdServicesManger.load(); - verify(etcdServicesManger.createStage(managerConfig, null)).updateRegisteredServices(); - } - - /** - * Test for {@link CesServicesManager#save(RegisteredService)}. - */ - @Test - public void save() { - assertThrows(UnsupportedOperationException.class, () -> etcdServicesManger.save(mock(RegisteredService.class))); - } - - /** - * Test for {@link CesServicesManager#save(Stream)} . - */ - @Test - public void save1() { - assertThrows(UnsupportedOperationException.class, () -> etcdServicesManger.save(Stream.empty())); - } - - /** - * Test for {@link CesServicesManager#save(RegisteredService, boolean)} . - */ - @Test - public void save3() { - assertThrows(UnsupportedOperationException.class, () -> etcdServicesManger.save(mock(RegisteredService.class), true)); - } - - /** - * Test for {@link CesServicesManager#save(Supplier, Consumer, long)} . - */ - @Test - public void save4() { - assertThrows(UnsupportedOperationException.class, () -> etcdServicesManger.save(mock(Supplier.class), mock(Consumer.class), 3)); - } - - /** - * Test for {@link CesServicesManager#deleteAll()} . - */ - @Test - public void deleteAll() { - assertThrows(UnsupportedOperationException.class, () -> etcdServicesManger.deleteAll()); - } - - /** - * Test for {@link CesServicesManager#delete(long)}. - */ - @Test - public void delete() { - assertThrows(UnsupportedOperationException.class, () -> etcdServicesManger.delete(42L)); - } - - /** - * Test for {@link CesServicesManager#delete(RegisteredService)} . - */ - @Test - public void delete2() { - assertThrows(UnsupportedOperationException.class, () -> etcdServicesManger.delete(mock(RegisteredService.class))); - } - - /** - * Special {@link CesServicesManager} that return a mocked stage for unit testing in isolation. - */ - class CesServiceManagerUnderTest extends CesServicesManager { - public CesServiceManagerUnderTest(CesServiceManagerConfiguration managerConfig, Registry registry) { - super(managerConfig, registry); - } - - @Override - protected CesServicesManagerStage createStage(CesServiceManagerConfiguration managerConfig, Registry registry) { - return servicesManagerStage; - } - } - -} \ No newline at end of file diff --git a/app/src/test/java/de/triology/cas/services/CesServicesSpringConfigurationTest.java b/app/src/test/java/de/triology/cas/services/CesServicesSpringConfigurationTest.java deleted file mode 100644 index 14bb7145..00000000 --- a/app/src/test/java/de/triology/cas/services/CesServicesSpringConfigurationTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package de.triology.cas.services; - -import junit.framework.TestCase; - -import java.util.Map; - -public class CesServicesSpringConfigurationTest extends TestCase { - - public void testPropertyStringToMap_emptyString() { - // given - // when - Map propertyMap = CesServicesSpringConfiguration.propertyStringToMap(""); - - // then - assertEquals(propertyMap.size(), 0); - } - - public void testPropertyStringToMap_withValues() { - // given - String mapProperty = "key1:value1,key2:value2"; - - // when - Map propertyMap = CesServicesSpringConfiguration.propertyStringToMap(mapProperty); - - // then - assertEquals(propertyMap.size(), 2); - assertEquals(propertyMap.get("key1"), "value1"); - assertEquals(propertyMap.get("key2"), "value2"); - } -} \ No newline at end of file diff --git a/app/src/test/java/de/triology/cas/services/EtcdClientFactoryTest.java b/app/src/test/java/de/triology/cas/services/EtcdClientFactoryTest.java deleted file mode 100644 index e4285485..00000000 --- a/app/src/test/java/de/triology/cas/services/EtcdClientFactoryTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package de.triology.cas.services; - -import com.google.common.base.Charsets; -import com.google.common.io.Files; -import mousio.etcd4j.EtcdClient; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.mockito.ArgumentMatcher; - -import java.io.File; -import java.io.IOException; -import java.net.URI; - -import static org.junit.Assert.assertNotNull; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - -public class EtcdClientFactoryTest { - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void shouldUseDefaultConfigFileForUrl() { - EtcdClientFactory etcdClientFactory = spy(new EtcdClientFactory()); - EtcdClient etcdClient = etcdClientFactory.createDefaultClient(); - assertNotNull(etcdClient); - verify(etcdClientFactory).createEtcdClient("/etc/ces/node_master"); - } - - @Test - public void shouldReadExistingConfigFirForUrl() throws IOException { - File file = temporaryFolder.newFile(); - Files.asCharSink(file, Charsets.UTF_8).write("example.com"); - - EtcdClientFactory etcdClientFactory = spy(new EtcdClientFactory()); - EtcdClient etcdClient = etcdClientFactory.createEtcdClient(file.getAbsolutePath()); - assertNotNull(etcdClient); - - verify(etcdClientFactory).createEtcdClient(argThat(new URIMatcher("http://example.com:4001"))); - } - - @Test - public void shouldFallbackToLocalhostIfConfigFileDoesNotExists() throws IOException { - File file = temporaryFolder.newFolder(); - File notExistingFile = new File(file, "doesNotExist"); - - EtcdClientFactory etcdClientFactory = spy(new EtcdClientFactory()); - EtcdClient etcdClient = etcdClientFactory.createEtcdClient(notExistingFile.getAbsolutePath()); - assertNotNull(etcdClient); - - verify(etcdClientFactory).createEtcdClient(argThat(new URIMatcher("http://localhost:4001"))); - } - - @Test(expected = RegistryException.class) - public void shouldFailIfConfigFileIsEmpty() throws IOException { - File file = temporaryFolder.newFile(); - - EtcdClientFactory etcdClientFactory = spy(new EtcdClientFactory()); - etcdClientFactory.createEtcdClient(file.getAbsolutePath()); - } - - private static class URIMatcher implements ArgumentMatcher { - - private final String expectedUrl; - - private URIMatcher(String expectedUrl) { - this.expectedUrl = expectedUrl; - } - - @Override - public boolean matches(URI uri) { - return expectedUrl.equals(uri.toString()); - } - } -} \ No newline at end of file diff --git a/app/src/test/java/de/triology/cas/services/RegistryEtcdTest.java b/app/src/test/java/de/triology/cas/services/RegistryEtcdTest.java deleted file mode 100644 index 9c6c03cc..00000000 --- a/app/src/test/java/de/triology/cas/services/RegistryEtcdTest.java +++ /dev/null @@ -1,356 +0,0 @@ -package de.triology.cas.services; - -import com.github.tomakehurst.wiremock.common.ConsoleNotifier; -import com.github.tomakehurst.wiremock.core.WireMockConfiguration; -import com.github.tomakehurst.wiremock.junit.WireMockRule; -import de.triology.cas.oidc.services.CesOAuthServiceFactory; -import de.triology.cas.services.dogu.CesDoguServiceFactory; -import mousio.etcd4j.EtcdClient; -import mousio.etcd4j.promises.EtcdResponsePromise; -import mousio.etcd4j.requests.EtcdKeyGetRequest; -import mousio.etcd4j.responses.EtcdAuthenticationException; -import mousio.etcd4j.responses.EtcdException; -import org.apereo.cas.services.OidcRegisteredService; -import org.apereo.cas.support.oauth.services.OAuthRegisteredService; -import org.hamcrest.MatcherAssert; -import org.json.simple.JSONObject; -import org.json.simple.parser.ParseException; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.mockito.ArgumentMatchers; - -import java.io.IOException; -import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeoutException; -import java.util.stream.Collectors; - -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.isA; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * Tests for {@link RegistryEtcd}. - */ -public class RegistryEtcdTest { - - private static final String OAUTH_CLIENT_PORTAINER_SECRET = "cdf022a1583367cf3fd6795be0eef0c8ce6f764143fcd9d851934750b0f4f39f"; - private static final String OIDC_CLIENT_CAS_OIDC_SECRET = "834251c84c1b88ce39351d888ee04df91e89785a28dbd86244e0e22c9d27b41f"; - - @Rule - public ExpectedException exceptionGrabber = ExpectedException.none(); - - @Rule - public WireMockRule wireMockRule = new WireMockRule( - WireMockConfiguration.options() - .dynamicPort() - .notifier(new ConsoleNotifier(false)) - ); - - @Test - public void getFqdn() { - RegistryEtcd registry = createRegistry(); - assertEquals("192.168.56.2", registry.getFqdn()); - } - - @Test - public void getDogusOfTypeCas() { - RegistryEtcd registry = createRegistry(); - var factory = new CesDoguServiceFactory(); - List installedDogus = registry.getInstalledCasServiceAccountsOfType(Registry.SERVICE_ACCOUNT_TYPE_CAS, factory) - .stream().map(CesServiceData::getName).toList(); - assertTrue(installedDogus.contains("redmine")); - assertTrue(installedDogus.contains("usermgt")); - assertTrue(installedDogus.contains("nexus")); - assertTrue(installedDogus.contains("portainer")); - assertTrue(installedDogus.contains("scm")); - assertTrue(installedDogus.contains("cas-oidc-client")); - assertTrue(installedDogus.contains("cockpit")); - } - - @Test - public void getDogusOfTypeOAuth() { - RegistryEtcd registry = createRegistry(); - var factory = new CesDoguServiceFactory(); - List installedDogus = registry.getInstalledCasServiceAccountsOfType(Registry.SERVICE_ACCOUNT_TYPE_OAUTH, factory) - .stream().map(CesServiceData::getName).toList(); - assertTrue(installedDogus.contains("portainer")); - } - - @Test - public void getDogusOfTypeOidc() { - RegistryEtcd registry = createRegistry(); - var factory = new CesDoguServiceFactory(); - List installedDogus = registry.getInstalledCasServiceAccountsOfType(Registry.SERVICE_ACCOUNT_TYPE_OIDC, factory) - .stream().map(CesServiceData::getName).toList(); - assertTrue(installedDogus.contains("cas-oidc-client")); - } - - @Test - public void getInstalledCasServiceAccountsOfTypeFailsWhenEtcdError() throws EtcdAuthenticationException, IOException, EtcdException, TimeoutException { - var registry = mock(RegistryEtcd.class); - var serviceFactory = new CesDoguServiceFactory(); - - when(registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory)).thenCallRealMethod(); - when(registry.getInstalledDogusWhichAreUsingCAS(serviceFactory)).thenThrow(EtcdException.class); - - exceptionGrabber.expect(RegistryException.class); - exceptionGrabber.expectMessage("Failed to getInstalledCasServiceAccountsOfType: cas"); - exceptionGrabber.expectCause(isA(EtcdException.class)); - - registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory); - } - - @Test - public void getInstalledCasServiceAccountsOfTypeFailsWhenFileNotFound() throws EtcdAuthenticationException, IOException, EtcdException, TimeoutException { - var registry = mock(RegistryEtcd.class); - var serviceFactory = new CesDoguServiceFactory(); - - when(registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory)).thenCallRealMethod(); - when(registry.getInstalledDogusWhichAreUsingCAS(serviceFactory)).thenThrow(IOException.class); - - exceptionGrabber.expect(RegistryException.class); - exceptionGrabber.expectMessage("Failed to getInstalledCasServiceAccountsOfType: cas"); - exceptionGrabber.expectCause(isA(IOException.class)); - - registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory); - } - - - private RegistryEtcd createRegistry() { - URI uri = URI.create("http://localhost:" + wireMockRule.port()); - return new RegistryEtcd(new EtcdClientFactory().createEtcdClient(uri)); - } - - @Test - public void getCorrectCasLogoutUri() throws GetCasLogoutUriException { - RegistryEtcd registry = mock(RegistryEtcd.class); - when(registry.getEtcdValueForKey("/config/cas/service_accounts/cas/testDogu/logout_uri")).thenReturn("testDogu/logout"); - when(registry.getEtcdValueForKey("/config/cas/service_accounts/oidc/testDogu/logout_uri")).thenThrow(RegistryException.class); - when(registry.getEtcdValueForKey("/config/cas/service_accounts/oauth/testDogu/logout_uri")).thenThrow(RegistryException.class); - when(registry.getCasLogoutUri("testDogu")).thenCallRealMethod(); - - URI logoutURI = registry.getCasLogoutUri("testDogu"); - assertEquals("testDogu/logout", logoutURI.toString()); - } - - @Test - public void getCorrectOidcLogoutUriTypeAccount() throws GetCasLogoutUriException { - RegistryEtcd registry = mock(RegistryEtcd.class); - when(registry.getEtcdValueForKey("/config/cas/service_accounts/cas/testDogu/logout_uri")).thenThrow(RegistryException.class); - when(registry.getEtcdValueForKey("/config/cas/service_accounts/oidc/testDogu/logout_uri")).thenReturn("testDogu/logout"); - when(registry.getEtcdValueForKey("/config/cas/service_accounts/oauth/testDogu/logout_uri")).thenThrow(RegistryException.class); - when(registry.getCasLogoutUri("testDogu")).thenCallRealMethod(); - - URI logoutURI = registry.getCasLogoutUri("testDogu"); - assertEquals("testDogu/logout", logoutURI.toString()); - } - - @Test - public void getCorrectOauthLogoutUri() throws GetCasLogoutUriException { - RegistryEtcd registry = mock(RegistryEtcd.class); - when(registry.getEtcdValueForKey("/config/cas/service_accounts/cas/testDogu/logout_uri")).thenThrow(RegistryException.class); - when(registry.getEtcdValueForKey("/config/cas/service_accounts/oidc/testDogu/logout_uri")).thenThrow(RegistryException.class); - when(registry.getEtcdValueForKey("/config/cas/service_accounts/oauth/testDogu/logout_uri")).thenReturn("testDogu/logout"); - when(registry.getCasLogoutUri("testDogu")).thenCallRealMethod(); - - URI logoutURI = registry.getCasLogoutUri("testDogu"); - assertEquals("testDogu/logout", logoutURI.toString()); - } - - @Test(expected = GetCasLogoutUriException.class) - public void getCasLogoutUriFromDoguDescriptorFallbackThrowsParseException() throws GetCasLogoutUriException, ParseException { - RegistryEtcd registry = mock(RegistryEtcd.class); - - when(registry.getEtcdValueForKey(ArgumentMatchers.any())).thenThrow(RegistryException.class); - when(registry.getCasLogoutUri("testDogu")).thenCallRealMethod(); - when(registry.getCurrentDoguNode("testDogu")).thenThrow(ParseException.class); - - registry.getCasLogoutUri("testDogu"); - } - - @Test - public void getCasLogoutUriFromDoguDescriptorFallback() throws GetCasLogoutUriException, ParseException { - RegistryEtcd registry = mock(RegistryEtcd.class); - - when(registry.getEtcdValueForKey(ArgumentMatchers.any())).thenThrow(RegistryException.class); - when(registry.getCasLogoutUri("testDogu")).thenCallRealMethod(); - JSONObject properties = new JSONObject(); - properties.put("logoutUri", "testDogu/logout"); - JSONObject doguMetaData = new JSONObject(); - doguMetaData.put("Properties", properties); - when(registry.getCurrentDoguNode(ArgumentMatchers.any())).thenReturn(doguMetaData); - - URI logoutURI = registry.getCasLogoutUri("testDogu"); - assertEquals("testDogu/logout", logoutURI.toString()); - } - - @Test(expected = GetCasLogoutUriException.class) - public void getCasLogoutUriFromDoguDescriptorFallbackWithoutProperties() throws ParseException, GetCasLogoutUriException { - RegistryEtcd registry = mock(RegistryEtcd.class); - - when(registry.getEtcdValueForKey(ArgumentMatchers.any())).thenThrow(RegistryException.class); - when(registry.getCasLogoutUri("testDogu")).thenCallRealMethod(); - JSONObject doguMetaData = new JSONObject(); - when(registry.getCurrentDoguNode(ArgumentMatchers.any())).thenReturn(doguMetaData); - when(registry.getCasLogoutUri("testDogu")).thenCallRealMethod(); - - registry.getCasLogoutUri("testDogu"); - } - - @Test(expected = GetCasLogoutUriException.class) - public void getCasLogoutUriFromDoguDescriptorFallbackWithMalformedProperties() throws ParseException, GetCasLogoutUriException { - RegistryEtcd registry = mock(RegistryEtcd.class); - - when(registry.getEtcdValueForKey(ArgumentMatchers.any())).thenThrow(RegistryException.class); - when(registry.getCasLogoutUri("testDogu")).thenCallRealMethod(); - JSONObject doguMetaData = new JSONObject(); - doguMetaData.put("Properties", "malformedPropertiesData"); - - when(registry.getCurrentDoguNode(ArgumentMatchers.any())).thenReturn(doguMetaData); - when(registry.getCasLogoutUri("testDogu")).thenCallRealMethod(); - - registry.getCasLogoutUri("testDogu"); - } - - @Test(expected = GetCasLogoutUriException.class) - public void getCasLogoutUriFromDoguDescriptorFallbackWithoutLogoutUriInProperties() throws ParseException, GetCasLogoutUriException { - RegistryEtcd registry = mock(RegistryEtcd.class); - - when(registry.getEtcdValueForKey(ArgumentMatchers.any())).thenThrow(RegistryException.class); - when(registry.getCasLogoutUri("testDogu")).thenCallRealMethod(); - JSONObject properties = new JSONObject(); - JSONObject doguMetaData = new JSONObject(); - doguMetaData.put("Properties", properties); - when(registry.getCurrentDoguNode(ArgumentMatchers.any())).thenReturn(doguMetaData); - when(registry.getCasLogoutUri("testDogu")).thenCallRealMethod(); - - registry.getCasLogoutUri("testDogu"); - } - - @Test(expected = GetCasLogoutUriException.class) - public void getCasLogoutUriFromDoguDescriptorFallbackWithEmptyLogoutUriInProperties() throws ParseException, GetCasLogoutUriException { - RegistryEtcd registry = mock(RegistryEtcd.class); - - when(registry.getEtcdValueForKey(ArgumentMatchers.any())).thenThrow(RegistryException.class); - when(registry.getCasLogoutUri("testDogu")).thenCallRealMethod(); - JSONObject properties = new JSONObject(); - properties.put("logoutUri", null); - JSONObject doguMetaData = new JSONObject(); - doguMetaData.put("Properties", properties); - when(registry.getCurrentDoguNode(ArgumentMatchers.any())).thenReturn(doguMetaData); - when(registry.getCasLogoutUri("testDogu")).thenCallRealMethod(); - - registry.getCasLogoutUri("testDogu"); - } - - @Test(expected = GetCasLogoutUriException.class) - public void getCasLogoutUriFromNonexistentDogu() throws GetCasLogoutUriException { - RegistryEtcd registry = mock(RegistryEtcd.class); - - when(registry.getEtcdValueForKey(ArgumentMatchers.any())).thenThrow(RegistryException.class); - when(registry.getCasLogoutUri("NonexistentDogu")).thenCallRealMethod(); - - registry.getCasLogoutUri("NonexistentDogu"); - } - - @Test(expected = GetCasLogoutUriException.class) - public void getEmptyCasLogoutUri() throws GetCasLogoutUriException { - RegistryEtcd registry = mock(RegistryEtcd.class); - when(registry.getEtcdValueForKey(ArgumentMatchers.any())).thenReturn(""); - when(registry.getCasLogoutUri("testDogu")).thenCallRealMethod(); - - registry.getCasLogoutUri("testDogu"); - } - - @Test - public void addDoguChangeListener() throws InterruptedException, IOException { - EtcdClient client = mock(EtcdClient.class); - EtcdResponsePromise responsePromise = mock(EtcdResponsePromise.class); - EtcdKeyGetRequest request = mock(EtcdKeyGetRequest.class); - when(client.getDir(ArgumentMatchers.any())).thenReturn(request); - when(request.recursive()).thenReturn(request); - when(request.waitForChange()).thenReturn(request); - when(request.send()).thenReturn(responsePromise); - RegistryEtcd registry = new RegistryEtcd(client); - ArrayList dogus = new ArrayList<>(); - - registry.addDoguChangeListener(() -> { - synchronized (dogus) { - if (dogus.contains("dogu")) { - try { - when(request.send()).thenThrow(new IOException("second call")); - } catch (IOException ignore) { - } - } - dogus.add("dogu"); - dogus.notify(); - } - }); - - synchronized (dogus) { - dogus.wait(); - } - assertTrue(dogus.contains("dogu")); - } - - - @Test - public void getOidcDogus() { - RegistryEtcd registry = createRegistry(); - var factory = new CesOAuthServiceFactory<>(OidcRegisteredService::new); - List installedServiceAccounts = registry.getInstalledCasServiceAccountsOfType(Registry.SERVICE_ACCOUNT_TYPE_OIDC, factory) - .stream().map(CesServiceData::getName).collect(Collectors.toList()); - MatcherAssert.assertThat(installedServiceAccounts, containsInAnyOrder("cas-oidc-client")); - assertEquals(1, registry.getInstalledCasServiceAccountsOfType(Registry.SERVICE_ACCOUNT_TYPE_OIDC, factory).size()); - } - - @Test - public void getOidcDogus_CheckSecrets() { - RegistryEtcd registry = createRegistry(); - var factory = new CesOAuthServiceFactory<>(OidcRegisteredService::new); - List installedServiceAccounts = registry.getInstalledCasServiceAccountsOfType(Registry.SERVICE_ACCOUNT_TYPE_OAUTH, factory); - assertEquals(1, installedServiceAccounts.size()); - - installedServiceAccounts.stream().filter(e -> e.getName().equals("cas-oidc-client")).forEach(e -> { - assertEquals(OIDC_CLIENT_CAS_OIDC_SECRET, e.getAttributes().get(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH)); - assertEquals("cas-oidc-client", e.getAttributes().get(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_ID)); - }); - } - - @Test - public void getOAuthDogus() { - RegistryEtcd registry = createRegistry(); - var factory = new CesOAuthServiceFactory<>(OAuthRegisteredService::new); - List installedServiceAccounts = registry.getInstalledCasServiceAccountsOfType(Registry.SERVICE_ACCOUNT_TYPE_OAUTH, factory) - .stream().map(CesServiceData::getName).collect(Collectors.toList()); - MatcherAssert.assertThat(installedServiceAccounts, containsInAnyOrder("portainer")); - assertEquals(1, registry.getInstalledCasServiceAccountsOfType(Registry.SERVICE_ACCOUNT_TYPE_OAUTH, factory).size()); - } - - @Test - public void getOAuthDogus_CheckSecrets() { - RegistryEtcd registry = createRegistry(); - var factory = new CesOAuthServiceFactory<>(OAuthRegisteredService::new); - List installedServiceAccounts = registry.getInstalledCasServiceAccountsOfType(Registry.SERVICE_ACCOUNT_TYPE_OAUTH, factory); - assertEquals(1, installedServiceAccounts.size()); - - installedServiceAccounts.stream().filter(e -> e.getName().equals("portainer")).forEach(e -> { - assertEquals(OAUTH_CLIENT_PORTAINER_SECRET, e.getAttributes().get(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_SECRET_HASH)); - assertEquals("portainer", e.getAttributes().get(CesOAuthServiceFactory.ATTRIBUTE_KEY_OAUTH_CLIENT_ID)); - }); - } - - @Test - public void getCurrentDoguNode_UserMgtDogu() throws ParseException { - RegistryEtcd registry = createRegistry(); - JSONObject test = registry.getCurrentDoguNode("usermgt"); - assertEquals(test.get("DisplayName"), "User Management"); - assertEquals(test.get("Image"), "registry.cloudogu.com/official/usermgt"); - } -} diff --git a/app/src/test/java/de/triology/cas/services/RegistryLocalTest.java b/app/src/test/java/de/triology/cas/services/RegistryLocalTest.java deleted file mode 100644 index 90699d75..00000000 --- a/app/src/test/java/de/triology/cas/services/RegistryLocalTest.java +++ /dev/null @@ -1,701 +0,0 @@ -package de.triology.cas.services; - -import de.triology.cas.oidc.services.CesOAuthServiceFactory; -import de.triology.cas.services.dogu.CesDoguServiceFactory; -import org.apereo.cas.services.OidcRegisteredService; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.yaml.snakeyaml.error.YAMLException; - -import java.io.ByteArrayInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.*; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.isA; -import static org.hamcrest.Matchers.*; -import static org.hamcrest.collection.IsMapWithSize.aMapWithSize; -import static org.hamcrest.collection.IsMapWithSize.anEmptyMap; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.*; - -public class RegistryLocalTest { - - @Rule - public ExpectedException exceptionGrabber = ExpectedException.none(); - - private static ByteArrayInputStream getServiceAccountYamlStream() { - var localConfigYaml = """ - service_accounts: - cas: - usermgt: - created: "true" - redmine: - created: "true" - oidc: - teamscale: - secret: "teamscale_secret" - openproject: - secret: "openproject_secret" - oauth: - portainer: - secret: "portainer_secret" - some_oauth_dogu: - secret: "some_oauth_dogu_secret" - """; - return new ByteArrayInputStream(localConfigYaml.getBytes(StandardCharsets.UTF_8)); - } - - @Test - public void getInstalledCasServiceAccountsOfTypeFailsWhenFileNotFound() { - var registry = mock(RegistryLocal.class); - var serviceFactory = new CesDoguServiceFactory(); - when(registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory)).thenCallRealMethod(); - when(registry.readServiceAccounts()).thenCallRealMethod(); - when(registry.getInputStreamForFile("/var/ces/config/local.yaml")).thenCallRealMethod(); - - exceptionGrabber.expect(RegistryException.class); - exceptionGrabber.expectMessage("Could not find file /var/ces/config/local.yaml"); - exceptionGrabber.expectCause(isA(FileNotFoundException.class)); - - registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory); - } - - @Test - public void getInstalledCasServiceAccountsOfTypeWithInvalidYaml() { - var registry = mock(RegistryLocal.class); - var serviceFactory = new CesDoguServiceFactory(); - var yamlStream = new ByteArrayInputStream("invalid".getBytes(StandardCharsets.UTF_8)); - when(registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory)).thenCallRealMethod(); - when(registry.readServiceAccounts()).thenCallRealMethod(); - when(registry.getInputStreamForFile("/var/ces/config/local.yaml")).thenReturn(yamlStream); - - exceptionGrabber.expect(RegistryException.class); - exceptionGrabber.expectMessage("Failed to parse yaml stream to class de.triology.cas.services.RegistryLocal$LocalConfig"); - exceptionGrabber.expectCause(isA(YAMLException.class)); - - registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory); - } - - @Test - public void getInstalledCasServiceAccountsOfTypeWithNullYaml() { - var registry = mock(RegistryLocal.class); - var serviceFactory = new CesDoguServiceFactory(); - var yamlStream = new ByteArrayInputStream("null".getBytes(StandardCharsets.UTF_8)); - when(registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory)).thenCallRealMethod(); - when(registry.readServiceAccounts()).thenCallRealMethod(); - when(registry.getInputStreamForFile("/var/ces/config/local.yaml")).thenReturn(yamlStream); - - var result = registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory); - assertThat(result, is(empty())); - } - - @Test - public void getInstalledCasServiceAccountsOfTypeFailsWithUnknownType() { - var registry = mock(RegistryLocal.class); - var serviceFactory = new CesDoguServiceFactory(); - var yamlStream = new ByteArrayInputStream("null".getBytes(StandardCharsets.UTF_8)); - when(registry.getInstalledCasServiceAccountsOfType("other", serviceFactory)).thenCallRealMethod(); - when(registry.readServiceAccounts()).thenCallRealMethod(); - when(registry.getInputStreamForFile("/var/ces/config/local.yaml")).thenReturn(yamlStream); - - exceptionGrabber.expect(RegistryException.class); - exceptionGrabber.expectMessage("Unknown service account type other"); - - registry.getInstalledCasServiceAccountsOfType("other", serviceFactory); - } - - @Test - public void getInstalledCasServiceAccountsOfTypeShouldFailToCloseInputStream() throws IOException { - var registry = mock(RegistryLocal.class); - var serviceFactory = new CesDoguServiceFactory(); - var yamlStream = spy(new ByteArrayInputStream("null".getBytes(StandardCharsets.UTF_8))); - doThrow(IOException.class).when(yamlStream).close(); - when(registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory)).thenCallRealMethod(); - when(registry.readServiceAccounts()).thenCallRealMethod(); - when(registry.getInputStreamForFile("/var/ces/config/local.yaml")).thenReturn(yamlStream); - - exceptionGrabber.expect(RegistryException.class); - exceptionGrabber.expectMessage("Failed to close local config file after reading service accounts."); - exceptionGrabber.expectCause(isA(IOException.class)); - - registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory); - } - - @Test - public void getInstalledCasServiceAccountsOfTypeWithEmptyYaml() { - var registry = mock(RegistryLocal.class); - var serviceFactory = new CesDoguServiceFactory(); - var yamlStream = new ByteArrayInputStream("{}".getBytes(StandardCharsets.UTF_8)); - when(registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory)).thenCallRealMethod(); - when(registry.readServiceAccounts()).thenCallRealMethod(); - when(registry.getInputStreamForFile("/var/ces/config/local.yaml")).thenReturn(yamlStream); - - var result = registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory); - assertThat(result, is(empty())); - } - - @Test - public void getInstalledCasServiceAccountsOfTypeWithNullServiceAccounts() { - var registry = mock(RegistryLocal.class); - var serviceFactory = new CesDoguServiceFactory(); - var yamlStream = new ByteArrayInputStream("service_accounts: null".getBytes(StandardCharsets.UTF_8)); - when(registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory)).thenCallRealMethod(); - when(registry.readServiceAccounts()).thenCallRealMethod(); - when(registry.getInputStreamForFile("/var/ces/config/local.yaml")).thenReturn(yamlStream); - - var result = registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory); - assertThat(result, is(empty())); - } - - @Test - public void getInstalledCasServiceAccountsOfTypeWithEmptyServiceAccounts() { - var registry = mock(RegistryLocal.class); - var serviceFactory = new CesDoguServiceFactory(); - var yamlStream = new ByteArrayInputStream("service_accounts: {}".getBytes(StandardCharsets.UTF_8)); - when(registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory)).thenCallRealMethod(); - when(registry.readServiceAccounts()).thenCallRealMethod(); - when(registry.getInputStreamForFile("/var/ces/config/local.yaml")).thenReturn(yamlStream); - - var result = registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory); - assertThat(result, is(empty())); - } - - @Test - public void getInstalledCasServiceAccountsOfTypeWithNullServiceAccountsInner() { - var registry = mock(RegistryLocal.class); - var serviceFactory = new CesDoguServiceFactory(); - var localConfigYaml = """ - service_accounts: - cas: null - oidc: null - oauth: null - """; - var yamlStream = new ByteArrayInputStream(localConfigYaml.getBytes(StandardCharsets.UTF_8)); - when(registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory)).thenCallRealMethod(); - when(registry.readServiceAccounts()).thenCallRealMethod(); - when(registry.getInputStreamForFile("/var/ces/config/local.yaml")).thenReturn(yamlStream); - - var result = registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory); - assertThat(result, is(empty())); - } - - @Test - public void getInstalledCasServiceAccountsOfTypeWithEmptyServiceAccountsInner() { - var registry = mock(RegistryLocal.class); - var serviceFactory = new CesDoguServiceFactory(); - var localConfigYaml = """ - service_accounts: - cas: {} - oidc: {} - oauth: {} - """; - var yamlStream = new ByteArrayInputStream(localConfigYaml.getBytes(StandardCharsets.UTF_8)); - when(registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory)).thenCallRealMethod(); - when(registry.readServiceAccounts()).thenCallRealMethod(); - when(registry.getInputStreamForFile("/var/ces/config/local.yaml")).thenReturn(yamlStream); - - var result = registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory); - assertThat(result, is(empty())); - } - - @Test - public void getInstalledCasServiceAccountsOfTypeCas() { - var registry = mock(RegistryLocal.class); - var serviceFactory = new CesDoguServiceFactory(); - var yamlStream = getServiceAccountYamlStream(); - when(registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory)).thenCallRealMethod(); - when(registry.readServiceAccounts()).thenCallRealMethod(); - when(registry.getInputStreamForFile("/var/ces/config/local.yaml")).thenReturn(yamlStream); - - var result = registry.getInstalledCasServiceAccountsOfType("cas", serviceFactory); - assertThat(result, hasSize(2)); - assertThat(result, hasItem(allOf( - hasProperty("name", equalTo("usermgt")), - hasProperty("factory", equalTo(serviceFactory)), - hasProperty("attributes", is(anEmptyMap())) - ))); - assertThat(result, hasItem(allOf( - hasProperty("name", equalTo("redmine")), - hasProperty("factory", equalTo(serviceFactory)), - hasProperty("attributes", is(anEmptyMap())) - ))); - } - - @Test - public void getInstalledCasServiceAccountsOfTypeOidc() { - var registry = mock(RegistryLocal.class); - var serviceFactory = new CesOAuthServiceFactory<>(OidcRegisteredService::new); - var yamlStream = getServiceAccountYamlStream(); - when(registry.getInstalledCasServiceAccountsOfType("oidc", serviceFactory)).thenCallRealMethod(); - when(registry.readServiceAccounts()).thenCallRealMethod(); - when(registry.getInputStreamForFile("/var/ces/config/local.yaml")).thenReturn(yamlStream); - - var result = registry.getInstalledCasServiceAccountsOfType("oidc", serviceFactory); - assertThat(result, hasSize(2)); - assertThat(result, hasItem(allOf( - hasProperty("name", equalTo("teamscale")), - hasProperty("factory", equalTo(serviceFactory)), - hasProperty("attributes", allOf( - is(aMapWithSize(2)), - hasEntry("oauth_client_id", "teamscale"), - hasEntry("oauth_client_secret", "teamscale_secret") - )) - ))); - assertThat(result, hasItem(allOf( - hasProperty("name", equalTo("openproject")), - hasProperty("factory", equalTo(serviceFactory)), - hasProperty("attributes", allOf( - is(aMapWithSize(2)), - hasEntry("oauth_client_id", "openproject"), - hasEntry("oauth_client_secret", "openproject_secret") - )) - ))); - } - - @Test - public void getInstalledCasServiceAccountsOfTypeOAuth() { - var registry = mock(RegistryLocal.class); - var serviceFactory = new CesOAuthServiceFactory<>(OidcRegisteredService::new); - var yamlStream = getServiceAccountYamlStream(); - when(registry.getInstalledCasServiceAccountsOfType("oauth", serviceFactory)).thenCallRealMethod(); - when(registry.readServiceAccounts()).thenCallRealMethod(); - when(registry.getInputStreamForFile("/var/ces/config/local.yaml")).thenReturn(yamlStream); - - var result = registry.getInstalledCasServiceAccountsOfType("oauth", serviceFactory); - assertThat(result, hasSize(2)); - assertThat(result, hasItem(allOf( - hasProperty("name", equalTo("portainer")), - hasProperty("factory", equalTo(serviceFactory)), - hasProperty("attributes", allOf( - is(aMapWithSize(2)), - hasEntry("oauth_client_id", "portainer"), - hasEntry("oauth_client_secret", "portainer_secret") - )) - ))); - assertThat(result, hasItem(allOf( - hasProperty("name", equalTo("some_oauth_dogu")), - hasProperty("factory", equalTo(serviceFactory)), - hasProperty("attributes", allOf( - is(aMapWithSize(2)), - hasEntry("oauth_client_id", "some_oauth_dogu"), - hasEntry("oauth_client_secret", "some_oauth_dogu_secret") - )) - ))); - } - - @Test - public void getCasLogoutUriFailsForNonExistentDogu() throws GetCasLogoutUriException { - var registry = spy(RegistryLocal.class); - var localConfigYaml = """ - service_accounts: - cas: - usermgt: - created: "true" - logout_uri: "/var/ces/config/local.yaml" - """; - var yamlStream = new ByteArrayInputStream(localConfigYaml.getBytes(StandardCharsets.UTF_8)); - doReturn(yamlStream).when(registry).getInputStreamForFile("/var/ces/config/local.yaml"); - - exceptionGrabber.expect(GetCasLogoutUriException.class); - exceptionGrabber.expectMessage("Could not get logoutUri for dogu my_dogu"); - - registry.getCasLogoutUri("my_dogu"); - } - - @Test - public void getCasLogoutUriFailsForEmptyOrNullUri() throws GetCasLogoutUriException { - var registry = spy(RegistryLocal.class); - var localConfigYaml = """ - service_accounts: - cas: - my_dogu: - created: "true" - logout_uri: "" - oidc: - my_dogu: - secret: "my_secret" - logout_uri: null - """; - var yamlStream = new ByteArrayInputStream(localConfigYaml.getBytes(StandardCharsets.UTF_8)); - doReturn(yamlStream).when(registry).getInputStreamForFile("/var/ces/config/local.yaml"); - - exceptionGrabber.expect(GetCasLogoutUriException.class); - exceptionGrabber.expectMessage("Could not get logoutUri for dogu my_dogu"); - - registry.getCasLogoutUri("my_dogu"); - } - - @Test - public void getCasLogoutUriFailsForInvalidUri() throws GetCasLogoutUriException { - var registry = spy(RegistryLocal.class); - var localConfigYaml = """ - service_accounts: - cas: - my_dogu: - created: "true" - logout_uri: "" - """; - var yamlStream = new ByteArrayInputStream(localConfigYaml.getBytes(StandardCharsets.UTF_8)); - doReturn(yamlStream).when(registry).getInputStreamForFile("/var/ces/config/local.yaml"); - - exceptionGrabber.expect(GetCasLogoutUriException.class); - exceptionGrabber.expectCause(isA(URISyntaxException.class)); - - registry.getCasLogoutUri("my_dogu"); - } - - @Test - public void getCasLogoutUriSuccess() throws GetCasLogoutUriException, URISyntaxException { - var registry = spy(RegistryLocal.class); - var localConfigYaml = """ - service_accounts: - cas: - my_dogu: - created: "true" - logout_uri: "/api/logout" - """; - var yamlStream = new ByteArrayInputStream(localConfigYaml.getBytes(StandardCharsets.UTF_8)); - doReturn(yamlStream).when(registry).getInputStreamForFile("/var/ces/config/local.yaml"); - - var result = registry.getCasLogoutUri("my_dogu"); - assertThat(result, is(new URI("/api/logout"))); - } - - @Test - public void getCasLogoutUriSuccessMultiple() throws GetCasLogoutUriException, URISyntaxException { - var registry = spy(RegistryLocal.class); - var localConfigYaml = """ - service_accounts: - cas: - my_dogu: - created: "true" - logout_uri: "/api/logout" - oidc: - my_dogu: - secret: "my_secret" - logout_uri: "/api/logout" - oauth: - my_dogu: - secret: "my_secret" - logout_uri: "/api/logout" - """; - var yamlStream = new ByteArrayInputStream(localConfigYaml.getBytes(StandardCharsets.UTF_8)); - doReturn(yamlStream).when(registry).getInputStreamForFile("/var/ces/config/local.yaml"); - - var result = registry.getCasLogoutUri("my_dogu"); - assertThat(result, is(new URI("/api/logout"))); - } - - @Test - public void getFqdnFromNullConfig() { - var registry = spy(RegistryLocal.class); - - var yamlStream = new ByteArrayInputStream("null".getBytes(StandardCharsets.UTF_8)); - doReturn(yamlStream).when(registry).getInputStreamForFile("/etc/ces/config/global/config.yaml"); - - var fqdn = registry.getFqdn(); - assertThat(fqdn, is(nullValue())); - } - - @Test - public void getFqdnWhenNull() { - var registry = spy(RegistryLocal.class); - - var yamlStream = new ByteArrayInputStream("fqdn: null".getBytes(StandardCharsets.UTF_8)); - doReturn(yamlStream).when(registry).getInputStreamForFile("/etc/ces/config/global/config.yaml"); - - var fqdn = registry.getFqdn(); - assertThat(fqdn, is(nullValue())); - } - - @Test - public void getFqdnWhenEmpty() { - var registry = spy(RegistryLocal.class); - - var yamlStream = new ByteArrayInputStream("fqdn: \"\"".getBytes(StandardCharsets.UTF_8)); - doReturn(yamlStream).when(registry).getInputStreamForFile("/etc/ces/config/global/config.yaml"); - - var fqdn = registry.getFqdn(); - assertThat(fqdn, isEmptyString()); - } - - @Test - public void getFqdnSuccess() { - var registry = spy(RegistryLocal.class); - - var yamlStream = new ByteArrayInputStream("fqdn: \"ces.example.com\"".getBytes(StandardCharsets.UTF_8)); - doReturn(yamlStream).when(registry).getInputStreamForFile("/etc/ces/config/global/config.yaml"); - - var fqdn = registry.getFqdn(); - assertThat(fqdn, is("ces.example.com")); - } - - @Test - public void getFqdnFailToCloseStream() throws IOException { - var yamlStream = spy(new ByteArrayInputStream("fqdn: \"ces.example.com\"".getBytes(StandardCharsets.UTF_8))); - doThrow(IOException.class).when(yamlStream).close(); - - var registry = spy(RegistryLocal.class); - doReturn(yamlStream).when(registry).getInputStreamForFile("/etc/ces/config/global/config.yaml"); - - exceptionGrabber.expect(RegistryException.class); - exceptionGrabber.expectMessage("Failed to close global config file after reading fqdn."); - exceptionGrabber.expectCause(isA(IOException.class)); - - registry.getFqdn(); - } - - @Test - public void ChangeListener() throws IOException, InterruptedException { - var watchKey = mock(WatchKey.class); - when(watchKey.isValid()) - .thenReturn(true); - List> watchEvents = new ArrayList<>(); - watchEvents.add(new EventMock("local.yaml")); - when(watchKey.pollEvents()) - .thenReturn(watchEvents); - when(watchKey.reset()) - .thenReturn(true); - Class wsClass; - try (var ws = FileSystems.getDefault().newWatchService()) { - wsClass = ws.getClass(); - } - var watchService = mock(wsClass); - when(watchService.take()) - .thenReturn(watchKey) - .thenThrow(InterruptedException.class); // finish on fourth invocation - var fileSystem = mock(FileSystem.class); - when(fileSystem.newWatchService()).thenReturn(watchService); - - var registry = spy(RegistryLocal.class); - registry.fileSystem = fileSystem; - var initialServiceAccounts = new RegistryLocal.ServiceAccounts(); - var changedServiceAccounts = new RegistryLocal.ServiceAccounts(); - changedServiceAccounts.setCas(Map.of("usermgt", new RegistryLocal.ServiceAccountCas())); - var unchangedServiceAccounts2 = new RegistryLocal.ServiceAccounts(); - unchangedServiceAccounts2.setCas(Map.of("usermgt", new RegistryLocal.ServiceAccountCas())); - doReturn(initialServiceAccounts, // get initial state for comparison - changedServiceAccounts, // detect change - unchangedServiceAccounts2) // no change to previous state - .when(registry).readServiceAccounts(); - - ArrayList dogus = new ArrayList<>(); - - registry.addDoguChangeListener(() -> { - synchronized (dogus) { - dogus.add("dogu " + dogus.size()); - dogus.notify(); - } - }); - - synchronized (dogus) { - dogus.wait(); - } - - assertThat(dogus, hasSize(1)); - } - - @Test - public void ChangeListenerWithReInitialization() throws IOException, InterruptedException { - var watchKey = mock(WatchKey.class); - when(watchKey.isValid()) - .thenReturn(false) - .thenReturn(true) - .thenReturn(true); - List> watchEvents = new ArrayList<>(); - watchEvents.add(new EventMock("local.yaml")); - when(watchKey.pollEvents()) - .thenReturn(watchEvents); - when(watchKey.reset()) - .thenReturn(false) - .thenReturn(true); - Class wsClass; - try (var ws = FileSystems.getDefault().newWatchService()) { - wsClass = ws.getClass(); - } - var watchService = mock(wsClass); - when(watchService.take()) - .thenReturn(watchKey) - .thenReturn(watchKey) - .thenReturn(watchKey) - .thenThrow(InterruptedException.class); // finish on fourth invocation - var fileSystem = mock(FileSystem.class); - when(fileSystem.newWatchService()) - .thenReturn(watchService) - .thenReturn(watchService) - .thenReturn(watchService); - - var registry = spy(RegistryLocal.class); - registry.fileSystem = fileSystem; - var initialServiceAccounts = new RegistryLocal.ServiceAccounts(); - var changedServiceAccounts = new RegistryLocal.ServiceAccounts(); - changedServiceAccounts.setCas(Map.of("usermgt", new RegistryLocal.ServiceAccountCas())); - var unchangedServiceAccounts = new RegistryLocal.ServiceAccounts(); - unchangedServiceAccounts.setCas(Map.of("usermgt", new RegistryLocal.ServiceAccountCas())); - doReturn(initialServiceAccounts, // get initial state for comparison - initialServiceAccounts, // no change - changedServiceAccounts, // detect change - unchangedServiceAccounts) // no change to previous state - .when(registry).readServiceAccounts(); - - ArrayList dogus = new ArrayList<>(); - - registry.addDoguChangeListener(() -> { - synchronized (dogus) { - dogus.add("dogu " + dogus.size()); - dogus.notify(); - } - }); - - synchronized (dogus) { - dogus.wait(); - } - - assertThat(dogus, hasSize(1)); - } - - private record EventMock(String context) implements WatchEvent { - @Override - public Kind kind() { - return null; - } - - @Override - public int count() { - return 0; - } - } - - @Test - public void deepEquals() { - Map cas = new HashMap<>(); - cas.put("usermgt", createServiceAccountCas("true", "/logout")); - cas.put("scm", createServiceAccountCas("false", "/logout2")); - Map oidc = new HashMap<>(); - oidc.put("teamscale", createServiceAccountSecret("abc", "/logout3")); - oidc.put("cas-oidc-dogu", createServiceAccountSecret("supersecret", "")); - Map oauth = new HashMap<>(); - oauth.put("portainer", createServiceAccountSecret("def", "/logout4")); - oauth.put("scm", createServiceAccountSecret("notSecret", "/logout5")); - - RegistryLocal.ServiceAccounts serviceAccountsA = new RegistryLocal.ServiceAccounts(); - serviceAccountsA.setCas(cas); - serviceAccountsA.setOidc(oidc); - serviceAccountsA.setOauth(oauth); - RegistryLocal.ServiceAccounts serviceAccountsB = new RegistryLocal.ServiceAccounts(); - serviceAccountsB.setCas(cas); - serviceAccountsB.setOidc(oidc); - serviceAccountsB.setOauth(oauth); - - assertTrue(serviceAccountsA.deepEquals(serviceAccountsB)); - } - - @Test - public void deepEqualsFalseWhenCasDifferent() { - Map cas = new HashMap<>(); - cas.put("usermgt", createServiceAccountCas("true", "/logout")); - cas.put("scm", createServiceAccountCas("false", "/logout2")); - Map oidc = new HashMap<>(); - oidc.put("teamscale", createServiceAccountSecret("abc", "/logout3")); - oidc.put("cas-oidc-dogu", createServiceAccountSecret("supersecret", "")); - Map oauth = new HashMap<>(); - oauth.put("portainer", createServiceAccountSecret("def", "/logout4")); - oauth.put("scm", createServiceAccountSecret("notSecret", "/logout5")); - - RegistryLocal.ServiceAccounts serviceAccountsA = new RegistryLocal.ServiceAccounts(); - serviceAccountsA.setCas(cas); - serviceAccountsA.setOidc(oidc); - serviceAccountsA.setOauth(oauth); - - RegistryLocal.ServiceAccounts serviceAccountsB = new RegistryLocal.ServiceAccounts(); - Map casDifferent = new HashMap<>(); - casDifferent.put("usermgt", createServiceAccountCas("false", "/logout")); - casDifferent.put("scm", createServiceAccountCas("false", "/logout2")); - serviceAccountsB.setCas(casDifferent); - serviceAccountsB.setOidc(oidc); - serviceAccountsB.setOauth(oauth); - - assertFalse(serviceAccountsA.deepEquals(serviceAccountsB)); - } - - @Test - public void deepEqualsFalseWhenOidcDifferent() { - Map cas = new HashMap<>(); - cas.put("usermgt", createServiceAccountCas("true", "/logout")); - cas.put("scm", createServiceAccountCas("false", "/logout2")); - Map oidc = new HashMap<>(); - oidc.put("teamscale", createServiceAccountSecret("abc", "/logout3")); - oidc.put("cas-oidc-dogu", createServiceAccountSecret("supersecret", "")); - Map oauth = new HashMap<>(); - oauth.put("portainer", createServiceAccountSecret("def", "/logout4")); - oauth.put("scm", createServiceAccountSecret("notSecret", "/logout5")); - - RegistryLocal.ServiceAccounts serviceAccountsA = new RegistryLocal.ServiceAccounts(); - serviceAccountsA.setCas(cas); - serviceAccountsA.setOidc(oidc); - serviceAccountsA.setOauth(oauth); - - RegistryLocal.ServiceAccounts serviceAccountsB = new RegistryLocal.ServiceAccounts(); - serviceAccountsB.setCas(cas); - Map oidcDifferent = new HashMap<>(); - oidc.put("teamscale", createServiceAccountSecret("abc", "")); - oidc.put("cas-oidc-dogu", createServiceAccountSecret("supersecret", "")); - serviceAccountsB.setOidc(oidcDifferent); - serviceAccountsB.setOauth(oauth); - - assertFalse(serviceAccountsA.deepEquals(serviceAccountsB)); - } - - @Test - public void deepEqualsFalseWhenOauthDifferent() { - Map cas = new HashMap<>(); - cas.put("usermgt", createServiceAccountCas("true", "/logout")); - cas.put("scm", createServiceAccountCas("false", "/logout2")); - Map oidc = new HashMap<>(); - oidc.put("teamscale", createServiceAccountSecret("abc", "/logout3")); - oidc.put("cas-oidc-dogu", createServiceAccountSecret("supersecret", "")); - Map oauth = new HashMap<>(); - oauth.put("portainer", createServiceAccountSecret("def", "/logout4")); - oauth.put("scm", createServiceAccountSecret("notSecret", "/logout5")); - - RegistryLocal.ServiceAccounts serviceAccountsA = new RegistryLocal.ServiceAccounts(); - serviceAccountsA.setCas(cas); - serviceAccountsA.setOidc(oidc); - serviceAccountsA.setOauth(oauth); - - RegistryLocal.ServiceAccounts serviceAccountsB = new RegistryLocal.ServiceAccounts(); - serviceAccountsB.setCas(cas); - serviceAccountsB.setOidc(oidc); - Map oauthDifferent = new HashMap<>(); - oauthDifferent.put("portainerDifferent", createServiceAccountSecret("def", "/logout4")); - oauthDifferent.put("scm", createServiceAccountSecret("notSecret", "/logout5")); - serviceAccountsB.setOauth(oauthDifferent); - - assertFalse(serviceAccountsA.deepEquals(serviceAccountsB)); - } - - private RegistryLocal.ServiceAccountCas createServiceAccountCas(String created, String logout_uri) { - RegistryLocal.ServiceAccountCas cas = new RegistryLocal.ServiceAccountCas(); - cas.setCreated(created); - cas.setLogout_uri(logout_uri); - return cas; - } - - private RegistryLocal.ServiceAccountSecret createServiceAccountSecret(String secret, String logout_uri) { - RegistryLocal.ServiceAccountSecret serviceAccount = new RegistryLocal.ServiceAccountSecret(); - serviceAccount.setSecret(secret); - serviceAccount.setLogout_uri(logout_uri); - return serviceAccount; - } -} \ No newline at end of file diff --git a/app/src/test/java/de/triology/cas/services/attributes/ReturnMappedAttributesPolicyTest.java b/app/src/test/java/de/triology/cas/services/attributes/ReturnMappedAttributesPolicyTest.java deleted file mode 100644 index 5f8d1cb1..00000000 --- a/app/src/test/java/de/triology/cas/services/attributes/ReturnMappedAttributesPolicyTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package de.triology.cas.services.attributes; - -import junit.framework.TestCase; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class ReturnMappedAttributesPolicyTest extends TestCase { - - public void testAuthorizeReleaseOfAllowedAttributes_withMapping() { - // given - List allowedAttributes = List.of("username", "mail"); - Map attributesMappingRules = Map.of("preferred_username", "username"); - ReturnMappedAttributesPolicy policy = new ReturnMappedAttributesPolicy(allowedAttributes, attributesMappingRules); - - Map> inputAttributes = new HashMap<>(); - inputAttributes.put("preferred_username", List.of("testUsername")); - inputAttributes.put("mail", List.of("super@duper.de")); - inputAttributes.put("ignoreMe", List.of("ignoring")); - - // when - Map> returnedAttributes = policy.authorizeReleaseOfAllowedAttributes(null, inputAttributes); - - // then - assertEquals(returnedAttributes.size(), 2); - assertEquals(returnedAttributes.get("mail"), List.of("super@duper.de")); - assertEquals(returnedAttributes.get("username"), List.of("testUsername")); - assertFalse(returnedAttributes.containsKey("ignoreMe")); - } - - public void testAuthorizeReleaseOfAllowedAttributes_withoutMapping() { - // given - List allowedAttributes = List.of("username", "mail"); - ReturnMappedAttributesPolicy policy = new ReturnMappedAttributesPolicy(allowedAttributes, null); - - Map> inputAttributes = new HashMap<>(); - inputAttributes.put("preferred_username", List.of("testUsername")); - inputAttributes.put("mail", List.of("super@duper.de")); - inputAttributes.put("ignoreMe", List.of("ignoring")); - - // when - Map> returnedAttributes = policy.authorizeReleaseOfAllowedAttributes(null, inputAttributes); - - // then - assertEquals(returnedAttributes.size(), 1); - assertEquals(returnedAttributes.get("mail"), List.of("super@duper.de")); - assertFalse(returnedAttributes.containsKey("username")); - assertFalse(returnedAttributes.containsKey("ignoreMe")); - } - - public void testAuthorizeReleaseOfAllowedAttributes_noAllowedAttributes() { - // given - ReturnMappedAttributesPolicy policy = new ReturnMappedAttributesPolicy(null, null); - - Map> inputAttributes = new HashMap<>(); - inputAttributes.put("preferred_username", List.of("testUsername")); - inputAttributes.put("mail", List.of("super@duper.de")); - inputAttributes.put("ignoreMe", List.of("ignoring")); - - // when - Map> returnedAttributes = policy.authorizeReleaseOfAllowedAttributes(null, inputAttributes); - - // then - assertEquals(returnedAttributes.size(), 0); - assertFalse(returnedAttributes.containsKey("mail")); - assertFalse(returnedAttributes.containsKey("username")); - assertFalse(returnedAttributes.containsKey("ignoreMe")); - } -} \ No newline at end of file diff --git a/app/src/test/java/de/triology/cas/services/dogu/CesDoguServiceFactoryTest.java b/app/src/test/java/de/triology/cas/services/dogu/CesDoguServiceFactoryTest.java deleted file mode 100644 index da1bcfd9..00000000 --- a/app/src/test/java/de/triology/cas/services/dogu/CesDoguServiceFactoryTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package de.triology.cas.services.dogu; - -import de.triology.cas.services.CesServiceData; -import junit.framework.TestCase; -import org.apereo.cas.services.CasRegisteredService; - -import java.net.URI; -import java.net.URISyntaxException; - -public class CesDoguServiceFactoryTest extends TestCase { - - /** - * Test case for {@link CesDoguServiceFactory#generateServiceIdFqdnRegex(String)} - */ - public void testGenerateServiceIdFqdnRegex() { - // given - String fqdn = "local.CLOUDOGU.com"; - - // when - String fqdnRegex = CesDoguServiceFactory.generateServiceIdFqdnRegex(fqdn); - - // then - assertTrue("local.cloudogu.com".matches(fqdnRegex)); - assertTrue("LOCAL.CLOUDOGU.COM".matches(fqdnRegex)); - assertTrue("LoCaL.cLoUdOgU.cOm".matches(fqdnRegex)); - assertFalse("local.cloudoguAcom".matches(fqdnRegex)); - assertFalse("localBcloudoguAcom".matches(fqdnRegex)); - } - - /** - * Test case for {@link CesDoguServiceFactory#generateServiceIdFqdnRegex(String)} - */ - public void testGenerateServiceIdFqdnRegex_IP() { - // given - String fqdn = "192.168.56.2"; - - // when - String fqdnRegex = CesDoguServiceFactory.generateServiceIdFqdnRegex(fqdn); - - // then - assertTrue("192.168.56.2".matches(fqdnRegex)); - assertFalse("192A168.56.2".matches(fqdnRegex)); - } - - /** - * Test case for {@link CesDoguServiceFactory#createNewService(long, String, URI, CesServiceData)} - */ - public void testCreateNewService_withGivenLogoutProperty() throws URISyntaxException, CesServiceCreationException { - // given - CesDoguServiceFactory factory = new CesDoguServiceFactory(); - long id = 1; - String fqdn = "192.168.56.2"; - String serviceName = "testService"; - URI logoutProperty = new URI("/api/mylogout"); - CesServiceData data = new CesServiceData(serviceName, factory); - - // when - CasRegisteredService service = factory.createNewService(id, fqdn, logoutProperty, data); - - // then - assertEquals(1, service.getId()); - assertEquals("CesDoguServiceFactory testService", service.getName()); - assertTrue("https://192.168.56.2/testService/test/WOW".matches(service.getServiceId())); - assertTrue("https://192.168.56.2:443/testService/test/WOW".matches(service.getServiceId())); - assertFalse("https://192.168.56.2:443/TESTService/test/WOW".matches(service.getServiceId())); - assertEquals("https://192.168.56.2/testService/api/mylogout", service.getLogoutUrl()); - } - - /** - * Test case for {@link CesDoguServiceFactory#createNewService(long, String, URI, CesServiceData)} - */ - public void testCreateNewService_withNoLogoutProperty() throws CesServiceCreationException { - // given - CesDoguServiceFactory factory = new CesDoguServiceFactory(); - long id = 1; - String fqdn = "local.cloudogu.com"; - String serviceName = "testService"; - CesServiceData data = new CesServiceData(serviceName, factory); - - // when - CasRegisteredService service = factory.createNewService(id, fqdn, null, data); - - // then - assertEquals(1, service.getId()); - assertEquals("CesDoguServiceFactory testService", service.getName()); - assertTrue("https://local.cloudogu.com/testService/test/WOW".matches(service.getServiceId())); - assertTrue("https://local.cloudogu.com:443/testService/test/WOW".matches(service.getServiceId())); - assertFalse("https://local.cloudogu.com:443/TESTService/test/WOW".matches(service.getServiceId())); - assertNull(service.getLogoutUrl()); - } -} \ No newline at end of file From 9f32af1ab8be8c25749766b9b850efc715d024bb Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Tue, 5 Nov 2024 19:32:45 +0100 Subject: [PATCH 26/50] Add userProfileViewType FLAT as defined in PR #220 --- resources/etc/cas/services/templates/DefaultOAuthService.json | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/etc/cas/services/templates/DefaultOAuthService.json b/resources/etc/cas/services/templates/DefaultOAuthService.json index 3a622063..02f77ec1 100644 --- a/resources/etc/cas/services/templates/DefaultOAuthService.json +++ b/resources/etc/cas/services/templates/DefaultOAuthService.json @@ -2,6 +2,7 @@ "@class" : "${ServiceClass}", "templateName": "DefaultOAuthService", "bypassApprovalPrompt": true, + "userProfileViewType": "FLAT", "supportedResponseTypes": [ "java.util.HashSet", [ "code" ] ], "supportedGrantTypes": [ "java.util.HashSet", [ "authorization_code" ] ] } \ No newline at end of file From 24d3ae1b32ca44b0da73e51367cac860b74ba4d9 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Tue, 5 Nov 2024 19:35:28 +0100 Subject: [PATCH 27/50] remove ces.service.stage configuration because it is not used anymore --- app/etc/cas/config/cas.properties | 3 --- resources/etc/cas/config/cas.properties.tpl | 2 -- 2 files changed, 5 deletions(-) diff --git a/app/etc/cas/config/cas.properties b/app/etc/cas/config/cas.properties index 013336e4..84a28ea9 100644 --- a/app/etc/cas/config/cas.properties +++ b/app/etc/cas/config/cas.properties @@ -18,9 +18,6 @@ cas.server.name=https://localhost:8443 cas.server.prefix=${cas.server.name}/cas -#ces.services.stage=production -ces.services.stage=development - # Unique CAS node name # host.name is used to generate unique Service Ticket IDs and SAMLArtifacts. This is usually set to the specific # hostname of the machine running the CAS node, but it could be any label so long as it is unique in the cluster. diff --git a/resources/etc/cas/config/cas.properties.tpl b/resources/etc/cas/config/cas.properties.tpl index 80707b29..f3b5f66f 100644 --- a/resources/etc/cas/config/cas.properties.tpl +++ b/resources/etc/cas/config/cas.properties.tpl @@ -5,8 +5,6 @@ cas.server.name=https://{{ .GlobalConfig.Get "fqdn" }} cas.server.prefix=${cas.server.name}/cas -ces.services.stage={{ .GlobalConfig.GetOrDefault "stage" "production" }} - # This property is very important. If this is not set to 0, the whole dogu can crash when ldap is not available ces.ldap-pool-size=0 From db8e086d64b6f0e076ff4d0e3d905f5e103f4444 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Tue, 5 Nov 2024 19:40:36 +0100 Subject: [PATCH 28/50] add userProfileViewType = FLAT to dev services --- resources/etc/cas/services/development/oauth-10000002.json | 1 + resources/etc/cas/services/development/oidc-10000003.json | 1 + 2 files changed, 2 insertions(+) diff --git a/resources/etc/cas/services/development/oauth-10000002.json b/resources/etc/cas/services/development/oauth-10000002.json index ebe7d589..bb0eb48f 100644 --- a/resources/etc/cas/services/development/oauth-10000002.json +++ b/resources/etc/cas/services/development/oauth-10000002.json @@ -12,6 +12,7 @@ }, "logoutType" : "BACK_CHANNEL", "bypassApprovalPrompt": true, + "userProfileViewType": "FLAT", "supportedResponseTypes": [ "java.util.HashSet", [ "code" ] ], "supportedGrantTypes": [ "java.util.HashSet", [ "authorization_code" ] ] } \ No newline at end of file diff --git a/resources/etc/cas/services/development/oidc-10000003.json b/resources/etc/cas/services/development/oidc-10000003.json index 01997c77..e79e4b8e 100644 --- a/resources/etc/cas/services/development/oidc-10000003.json +++ b/resources/etc/cas/services/development/oidc-10000003.json @@ -12,6 +12,7 @@ }, "logoutType" : "BACK_CHANNEL", "bypassApprovalPrompt": true, + "userProfileViewType": "FLAT", "supportedResponseTypes": [ "java.util.HashSet", [ "code" ] ], "supportedGrantTypes": [ "java.util.HashSet", [ "authorization_code" ] ] } \ No newline at end of file From e63fdd5210f58be40c3950ba109674b20e2cdeee Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Tue, 5 Nov 2024 19:53:21 +0100 Subject: [PATCH 29/50] fix comments, typo in release notes and format in configuration template --- docs/gui/release_notes_de.md | 2 +- resources/create-sa.sh | 2 +- resources/etc/cas/config/cas.properties.tpl | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/gui/release_notes_de.md b/docs/gui/release_notes_de.md index 99c1b7d3..1c680da8 100644 --- a/docs/gui/release_notes_de.md +++ b/docs/gui/release_notes_de.md @@ -8,7 +8,7 @@ Technische Details zu einem Release finden Sie im zugehörigen [Changelog](https - Bei der Anmeldung über eine delegierte Authentifizierung (durch einen OIDC-Provider) werden die Nutzer in den internen LDAP repliziert - Die replizierten Nutzer werden als "extern" gekennzeichnet und können, bis auf die Gruppenzuordnung, nicht editiert werden. - Das Dogu wurde intern auf eine JSON Registry umgestellt, wodurch sich die Logik zum Anlegen und Löschen von Service-Accounts geändert hat. -- Einheitliche Verwendung von Service-Accounts sowohl in einer Multinode- als Singlenode-Umgebung. +- Einheitliche Verwendung von Service-Accounts sowohl in einer Multinode- als auch Singlenode-Umgebung. ### Breaking Change - Neu zu installierende Dogus müssen explizit die Erstellung eines Serviceaccounts im CAS über die dogu.json anfordern. Weitere Informationen hierfür finden Sie in der [Entwicklerdokumentation](https://github.com/cloudogu/dogu-development-docs/blob/main/docs/important/relevant_functionalities_de.md#authentifizierung) diff --git a/resources/create-sa.sh b/resources/create-sa.sh index d9b07ebd..4abb23af 100755 --- a/resources/create-sa.sh +++ b/resources/create-sa.sh @@ -26,7 +26,7 @@ source util.sh # build logout url LOGOUT_URL="https://${FQDN}/${SERVICE}${LOGOUT_URI:-}" - # Initialize TEMPLATES as an empty array + # Initialize TEMPLATES with base templates TEMPLATES=("BaseService,DefaultAttributeReleasePolicy") if [ -n "${LOGOUT_URI+x}" ]; then diff --git a/resources/etc/cas/config/cas.properties.tpl b/resources/etc/cas/config/cas.properties.tpl index f3b5f66f..46fb221c 100644 --- a/resources/etc/cas/config/cas.properties.tpl +++ b/resources/etc/cas/config/cas.properties.tpl @@ -274,6 +274,9 @@ cas.authn.oauth.accessToken.maxTimeToLiveInSeconds=86000 ######################################################################################################################## # JSON Registry +# Configuration guide: https://apereo.github.io/cas/7.0.x/services/JSON-Service-Management.html +# ---------------------------------------------------------------------------------------------------------------------- cas.service-registry.json.location={{if eq (.GlobalConfig.GetOrDefault "stage" "production") "production"}}file:/etc/cas/services/production{{else}}file:/etc/cas/services/development{{end}} cas.service-registry.json.watcher-enabled=true -cas.service-registry.templates.directory.location=file:/etc/cas/services/templates \ No newline at end of file +cas.service-registry.templates.directory.location=file:/etc/cas/services/templates +######################################################################################################################## \ No newline at end of file From b8fc3b14e8bdbd94c84e0846bb9e746e9cae332d Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Tue, 5 Nov 2024 20:16:03 +0100 Subject: [PATCH 30/50] fix shellcheck in util.sh --- resources/util.sh | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/resources/util.sh b/resources/util.sh index 5a9d4b12..1ca3e208 100755 --- a/resources/util.sh +++ b/resources/util.sh @@ -6,7 +6,6 @@ set -o pipefail # Defines the path to store service definitions SERVICE_REGISTRY="etc/cas/services" SERVICE_REGISTRY_PRODUCTION="${SERVICE_REGISTRY}"/production -SERVICE_REGISTRY_DEVELOPMENT="${SERVICE_REGISTRY}"/development # This function prints an error to the console and waits 5 minutes before exiting the process. # Requires two arguments: @@ -175,8 +174,11 @@ function checkFqdnUpdate() { return 0 fi - local globalFQDN=$(doguctl config -g fqdn) - local localFQDN=$(doguctl config fqdn) + local globalFQDN + globalFQDN=$(doguctl config -g fqdn) + + local localFQDN + localFQDN=$(doguctl config fqdn) if [ "$localFQDN" == "$globalFQDN" ]; then return 0 @@ -185,7 +187,7 @@ function checkFqdnUpdate() { echo "FQDN has change, update services ..." doguctl config "fqdn" "$globalFQDN" - updateFqdnInServices $globalFQDN + updateFqdnInServices "$globalFQDN" } # Function to double-escape dots in the FQDN to use it within a regex of the service registry @@ -207,11 +209,12 @@ function findNextServiceID() { local max_number=0 # Loop through all JSON files in the directory - for file in $dir/*.json; do + for file in "$dir"/*.json; do # Check if the folder contains any JSON files if [[ -f "$file" ]]; then # Extract the number from the filename using regex (ignores prefix and extracts numbers) - local number=$(echo "$file" | awk -F'[-.]' '{print $(NF-1)}') + local number + number=$(echo "$file" | awk -F'[-.]' '{print $(NF-1)}') # Update max_number if the extracted number is greater if [[ $number -gt $max_number ]]; then @@ -231,7 +234,9 @@ function findNextServiceID() { function updateFqdnInServices() { echo "Updating services with new fqdn ${1}" - local nFQDN=$(escapeDots "${1}") + local nFQDN + nFQDN=$(escapeDots "${1}") + local tmpFqdnService=/tmp/new-fqdn.json # create temporary new service with new fqdn property @@ -242,7 +247,8 @@ function updateFqdnInServices() { -e 's|{{LOGOUT_URL}}||g' etc/cas/config/services/cas-service-template.json.tpl > $tmpFqdnService # Extract the Fqdn value from the source JSON - local fqdnObject=$(jq '.properties.Fqdn' "$tmpFqdnService") + local fqdnObject + fqdnObject=$(jq '.properties.Fqdn' "$tmpFqdnService") # Loop through production service files for service in "$SERVICE_REGISTRY_PRODUCTION"/*.json; do From c304a3d2507e094fbd10d84fa2ec5f77349f659f Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Tue, 5 Nov 2024 20:23:05 +0100 Subject: [PATCH 31/50] fix shellcheck in create-sa.sh and remove-sa.sh --- resources/create-sa.sh | 5 +++-- resources/remove-sa.sh | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/create-sa.sh b/resources/create-sa.sh index 4abb23af..c965b803 100755 --- a/resources/create-sa.sh +++ b/resources/create-sa.sh @@ -3,6 +3,7 @@ set -o errexit set -o nounset set -o pipefail +# shellcheck disable=SC1091 source util.sh { @@ -52,7 +53,7 @@ source util.sh -e "s|{{TEMPLATES}}|$(IFS=, ; echo "${TEMPLATES[*]}")|g" \ -e "s|{{CLIENT_SECRET_HASH}}|$CLIENT_SECRET_HASH|g" \ -e "s|{{SERVICE_CLASS}}|$SERVICE_CLASS|g" \ - -e "s|{{LOGOUT_URL}}|$LOGOUT_URL|g" etc/cas/config/services/oauth-service-template.json.tpl > $SERVICE_REGISTRY_PRODUCTION/${SERVICE}-${SERVICE_ID}.json + -e "s|{{LOGOUT_URL}}|$LOGOUT_URL|g" etc/cas/config/services/oauth-service-template.json.tpl > "$SERVICE_REGISTRY_PRODUCTION"/"${SERVICE}"-"${SERVICE_ID}".json doguctl config "service_accounts/${TYPE}/${SERVICE}/secret" "${CLIENT_SECRET_HASH}" elif [ "${TYPE}" == "cas" ]; then @@ -67,7 +68,7 @@ source util.sh -e "s|{{SERVICE_ID}}|$SERVICE_ID|g" \ -e "s|{{FQDN}}|$EFQDN|g" \ -e "s|{{TEMPLATES}}|$(IFS=, ; echo "${TEMPLATES[*]}")|g" \ - -e "s|{{LOGOUT_URL}}|$LOGOUT_URL|g" etc/cas/config/services/cas-service-template.json.tpl > $SERVICE_REGISTRY_PRODUCTION/${SERVICE}-${SERVICE_ID}.json + -e "s|{{LOGOUT_URL}}|$LOGOUT_URL|g" etc/cas/config/services/cas-service-template.json.tpl > "$SERVICE_REGISTRY_PRODUCTION"/"${SERVICE}"-"${SERVICE_ID}".json else echo "only the account_types: oidc, oauth, cas are allowed" diff --git a/resources/remove-sa.sh b/resources/remove-sa.sh index a04f9b88..c1db2a23 100755 --- a/resources/remove-sa.sh +++ b/resources/remove-sa.sh @@ -3,6 +3,7 @@ set -o errexit set -o nounset set -o pipefail +# shellcheck disable=SC1091 source util.sh if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then @@ -33,7 +34,7 @@ if [ -n "${LOGOUT_URI+x}" ]; then fi echo "Removing service ${SERVICE} from JSON registry ${SERVICE_REGISTRY}" -FILES=$(ls $SERVICE_REGISTRY_PRODUCTION/${SERVICE}-*.json 2>/dev/null || echo "") +FILES=$(ls "$SERVICE_REGISTRY_PRODUCTION"/"${SERVICE}"-*.json 2>/dev/null || echo "") # Check if FILES is empty before counting if [ -z "$FILES" ]; then @@ -42,6 +43,6 @@ else # Count the number of matching files FILE_COUNT=$(echo "$FILES" | wc -l) echo "Found $FILE_COUNT file(s) matching service ${SERVICE}." - rm $SERVICE_REGISTRY_PRODUCTION/${SERVICE}-*.json + rm "$SERVICE_REGISTRY_PRODUCTION"/"${SERVICE}"-*.json echo "Successfully deleted service ${SERVICE}." fi \ No newline at end of file From ac07948121661131ad202deaabc6028515528210 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Wed, 6 Nov 2024 08:19:48 +0100 Subject: [PATCH 32/50] Add STARTUP_DIR env to files that are sourced to be able to use it in BatsTests and add a check --- Dockerfile | 3 ++- resources/create-sa.sh | 8 ++++++-- resources/logging.sh | 8 ++++++-- resources/post-upgrade.sh | 7 ++++++- resources/remove-sa.sh | 8 ++++++-- resources/startup.sh | 8 ++++++-- 6 files changed, 32 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index d8c9eef7..7aa9af2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -67,7 +67,8 @@ ENV TOMCAT_VERSION=${TOMCAT_VERSION} \ SERVICE_TAGS=webapp \ USER=cas \ GROUP=cas \ - SSL_BASE_DIRECTORY="/etc/ssl" + SSL_BASE_DIRECTORY="/etc/ssl" \ + STARTUP_DIR=/ # setup user RUN set -x \ diff --git a/resources/create-sa.sh b/resources/create-sa.sh index c965b803..e5948e76 100755 --- a/resources/create-sa.sh +++ b/resources/create-sa.sh @@ -3,8 +3,12 @@ set -o errexit set -o nounset set -o pipefail -# shellcheck disable=SC1091 -source util.sh +sourcingExitCode=0 +# shellcheck disable=SC1090,SC1091 +source "${STARTUP_DIR}"/util.sh || sourcingExitCode=$? +if [[ ${sourcingExitCode} -ne 0 ]]; then + echo "ERROR: An error occurred while sourcing ${STARTUP_DIR}/util.sh." +fi { if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then diff --git a/resources/logging.sh b/resources/logging.sh index 558c9884..aac343ff 100755 --- a/resources/logging.sh +++ b/resources/logging.sh @@ -3,8 +3,12 @@ set -o errexit set -o nounset set -o pipefail -# shellcheck disable=SC1091 -source util.sh +sourcingExitCode=0 +# shellcheck disable=SC1090,SC1091 +source "${STARTUP_DIR}"/util.sh || sourcingExitCode=$? +if [[ ${sourcingExitCode} -ne 0 ]]; then + echo "ERROR: An error occurred while sourcing ${STARTUP_DIR}/util.sh." +fi # logging behaviour can be configured in logging/root with the following options DEFAULT_LOGGING_KEY="logging/root" diff --git a/resources/post-upgrade.sh b/resources/post-upgrade.sh index 17803578..3597b5cf 100755 --- a/resources/post-upgrade.sh +++ b/resources/post-upgrade.sh @@ -3,7 +3,12 @@ set -o errexit set -o nounset set -o pipefail -source util.sh +sourcingExitCode=0 +# shellcheck disable=SC1090,SC1091 +source "${STARTUP_DIR}"/util.sh || sourcingExitCode=$? +if [[ ${sourcingExitCode} -ne 0 ]]; then + echo "ERROR: An error occurred while sourcing ${STARTUP_DIR}/util.sh." +fi checkSameVersion() { echo "Checking the CAS versions..." diff --git a/resources/remove-sa.sh b/resources/remove-sa.sh index c1db2a23..71feb518 100755 --- a/resources/remove-sa.sh +++ b/resources/remove-sa.sh @@ -3,8 +3,12 @@ set -o errexit set -o nounset set -o pipefail -# shellcheck disable=SC1091 -source util.sh +sourcingExitCode=0 +# shellcheck disable=SC1090,SC1091 +source "${STARTUP_DIR}"/util.sh || sourcingExitCode=$? +if [[ ${sourcingExitCode} -ne 0 ]]; then + echo "ERROR: An error occurred while sourcing ${STARTUP_DIR}/util.sh." +fi if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then echo "usage remove-sa.sh account_type [logout_uri] servicename" diff --git a/resources/startup.sh b/resources/startup.sh index a96d8819..2f067737 100755 --- a/resources/startup.sh +++ b/resources/startup.sh @@ -15,8 +15,12 @@ echo " V/// '°°°° (/////) °°°°' //// " echo " V/////(////////\. '°°°' ./////////(///(/' " echo " 'V/(/////////////////////////////V' " -# shellcheck disable=SC1091 -source util.sh +sourcingExitCode=0 +# shellcheck disable=SC1090,SC1091 +source "${STARTUP_DIR}"/util.sh || sourcingExitCode=$? +if [[ ${sourcingExitCode} -ne 0 ]]; then + echo "ERROR: An error occurred while sourcing ${STARTUP_DIR}/util.sh." +fi # check whether post-upgrade script is still running while [[ "$(doguctl config "local_state" -d "empty")" == "upgrading" ]]; do From 000e66e25f71d90d69a8def668601d84d0b77c90 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Wed, 6 Nov 2024 09:31:11 +0100 Subject: [PATCH 33/50] try to create custom service in integration tests --- Jenkinsfile | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 6da53331..6f4f1cd2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -198,9 +198,29 @@ parallel( stage('Integration Tests') { echo "Create custom dogu to access OAuth endpoints for the integration tests" ecoSystem.vagrant.sshOut "etcdctl mkdir /dogu/inttest" - ecoSystem.vagrant.sshOut '''etcdctl set /dogu/inttest/0.0.1 '{\\"Name\\":\\"official/inttest\\",\\"Dependencies\\":[\\"cas\\"]}' ''' + ecoSystem.vagrant.sshOut '''etcdctl set /dogu/inttest/0.0.1 '{\\"Name\\":\\"official/inttest\\",\\"Dependencies\\":[\\"cas\\"],\\"ServiceAccounts\\":[{\\"Type\\":\\"cas\\",\\"Params\\":[\\"oauth\\"]}]}' ''' ecoSystem.vagrant.sshOut "etcdctl set /dogu/inttest/current \"0.0.1\"" + ecoSystem.vagrant.sshOut '''echo '{ + "@class" : "org.apereo.cas.support.oauth.services.OAuthRegisteredService", + "serviceId" : "^https://([a-zA-Z0-9.-]+)(:443)?/inttest(/.*)?", + "name" : "inttest", + "id" : 1000, + "description": "custom service for integration tests", + "clientId": "inttest", + "clientSecret": "fda8e031d07de22bf14e552ab12be4bc70b94a1fb61cb7605833765cb74f2dea", + "attributeReleasePolicy" : { + "@class" : "org.apereo.cas.services.ReturnAllAttributeReleasePolicy" + }, + "logoutType" : "BACK_CHANNEL", + "bypassApprovalPrompt": true, + "userProfileViewType": "FLAT", + "supportedResponseTypes": [ "java.util.HashSet", [ "code" ] ], + "supportedGrantTypes": [ "java.util.HashSet", [ "authorization_code" ] ] + }' > /etc/cas/services/production/inttest-1000.json''' + + ecoSystem.vagrant.sshOut "cat /etc/cas/services/production/inttest-1000.json" + ecoSystem.runCypressIntegrationTests([ cypressImage : "cypress/included:13.13.2", enableVideo : params.EnableVideoRecording, From 5a9b8fc87745a5fefe26edbfa6db7e94d1a5a2e9 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Wed, 6 Nov 2024 10:24:24 +0100 Subject: [PATCH 34/50] try to create custom service in integration tests --- Jenkinsfile | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 6f4f1cd2..c341b6f9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -178,11 +178,13 @@ parallel( ecoSystem.build("/dogu") } + /* stage('Trivy scan') { trivy.scanDogu("/dogu", TrivyScanFormat.HTML, params.TrivyScanLevels, params.TrivyStrategy) trivy.scanDogu("/dogu", TrivyScanFormat.JSON, params.TrivyScanLevels, params.TrivyStrategy) trivy.scanDogu("/dogu", TrivyScanFormat.PLAIN, params.TrivyScanLevels, params.TrivyStrategy) } + */ stage('Verify') { ecoSystem.verify("/dogu") @@ -201,25 +203,27 @@ parallel( ecoSystem.vagrant.sshOut '''etcdctl set /dogu/inttest/0.0.1 '{\\"Name\\":\\"official/inttest\\",\\"Dependencies\\":[\\"cas\\"],\\"ServiceAccounts\\":[{\\"Type\\":\\"cas\\",\\"Params\\":[\\"oauth\\"]}]}' ''' ecoSystem.vagrant.sshOut "etcdctl set /dogu/inttest/current \"0.0.1\"" - ecoSystem.vagrant.sshOut '''echo '{ - "@class" : "org.apereo.cas.support.oauth.services.OAuthRegisteredService", - "serviceId" : "^https://([a-zA-Z0-9.-]+)(:443)?/inttest(/.*)?", - "name" : "inttest", - "id" : 1000, - "description": "custom service for integration tests", - "clientId": "inttest", - "clientSecret": "fda8e031d07de22bf14e552ab12be4bc70b94a1fb61cb7605833765cb74f2dea", - "attributeReleasePolicy" : { - "@class" : "org.apereo.cas.services.ReturnAllAttributeReleasePolicy" - }, - "logoutType" : "BACK_CHANNEL", - "bypassApprovalPrompt": true, - "userProfileViewType": "FLAT", - "supportedResponseTypes": [ "java.util.HashSet", [ "code" ] ], - "supportedGrantTypes": [ "java.util.HashSet", [ "authorization_code" ] ] - }' > /etc/cas/services/production/inttest-1000.json''' - - ecoSystem.vagrant.sshOut "cat /etc/cas/services/production/inttest-1000.json" + ecoSystem.vagrant.sshOut 'cat > inittest-1000.json < Date: Wed, 6 Nov 2024 10:29:13 +0100 Subject: [PATCH 35/50] fix syntax error for new line --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index c341b6f9..9883bd3f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -203,7 +203,7 @@ parallel( ecoSystem.vagrant.sshOut '''etcdctl set /dogu/inttest/0.0.1 '{\\"Name\\":\\"official/inttest\\",\\"Dependencies\\":[\\"cas\\"],\\"ServiceAccounts\\":[{\\"Type\\":\\"cas\\",\\"Params\\":[\\"oauth\\"]}]}' ''' ecoSystem.vagrant.sshOut "etcdctl set /dogu/inttest/current \"0.0.1\"" - ecoSystem.vagrant.sshOut 'cat > inittest-1000.json < inittest-1000.json < Date: Wed, 6 Nov 2024 10:40:17 +0100 Subject: [PATCH 36/50] fix EOF --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 9883bd3f..50e5ce37 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -221,7 +221,7 @@ parallel( "supportedResponseTypes": [ "java.util.HashSet", [ "code" ] ], "supportedGrantTypes": [ "java.util.HashSet", [ "authorization_code" ] ] } - EOF"''' + EOF''' ecoSystem.vagrant.sshOut "cat inttest-1000.json" From a02a59442e380a22987c38eccf822ec493f5e013 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Wed, 6 Nov 2024 11:28:27 +0100 Subject: [PATCH 37/50] add file instead of generating --- Jenkinsfile | 23 ++------------------- integrationTests/services/inttest-1000.json | 17 +++++++++++++++ 2 files changed, 19 insertions(+), 21 deletions(-) create mode 100644 integrationTests/services/inttest-1000.json diff --git a/Jenkinsfile b/Jenkinsfile index 50e5ce37..2962e644 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -203,27 +203,8 @@ parallel( ecoSystem.vagrant.sshOut '''etcdctl set /dogu/inttest/0.0.1 '{\\"Name\\":\\"official/inttest\\",\\"Dependencies\\":[\\"cas\\"],\\"ServiceAccounts\\":[{\\"Type\\":\\"cas\\",\\"Params\\":[\\"oauth\\"]}]}' ''' ecoSystem.vagrant.sshOut "etcdctl set /dogu/inttest/current \"0.0.1\"" - ecoSystem.vagrant.sshOut '''cat > inittest-1000.json < Date: Wed, 6 Nov 2024 12:36:55 +0100 Subject: [PATCH 38/50] fix integration tests --- Jenkinsfile | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 2962e644..e4072e91 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -139,13 +139,6 @@ parallel( "terms_of_service": "https://docs.cloudogu.com/", "imprint": "https://cloudogu.com/" }, - "service_accounts": { - "oauth": { - "inttest": { - "secret": "fda8e031d07de22bf14e552ab12be4bc70b94a1fb61cb7605833765cb74f2dea" - } - } - }, "oidc": { "enabled": "true", "discovery_uri": "http://${ecoSystem.externalIP}:9000/auth/realms/Cloudogu/.well-known/openid-configuration", @@ -175,16 +168,16 @@ parallel( } stage('Build dogu') { + // force post-upgrade from cas version 7.0.8-3 to migrate existing services from defaultSetupConfig + ecoSystem.vagrant.sshOut "sed 's/7.0.8-3/7.0.8-4/g' -i /dogu/dogu.json" ecoSystem.build("/dogu") } - /* stage('Trivy scan') { trivy.scanDogu("/dogu", TrivyScanFormat.HTML, params.TrivyScanLevels, params.TrivyStrategy) trivy.scanDogu("/dogu", TrivyScanFormat.JSON, params.TrivyScanLevels, params.TrivyStrategy) trivy.scanDogu("/dogu", TrivyScanFormat.PLAIN, params.TrivyScanLevels, params.TrivyStrategy) } - */ stage('Verify') { ecoSystem.verify("/dogu") @@ -199,11 +192,7 @@ parallel( stage('Integration Tests') { echo "Create custom dogu to access OAuth endpoints for the integration tests" - ecoSystem.vagrant.sshOut "etcdctl mkdir /dogu/inttest" - ecoSystem.vagrant.sshOut '''etcdctl set /dogu/inttest/0.0.1 '{\\"Name\\":\\"official/inttest\\",\\"Dependencies\\":[\\"cas\\"],\\"ServiceAccounts\\":[{\\"Type\\":\\"cas\\",\\"Params\\":[\\"oauth\\"]}]}' ''' - ecoSystem.vagrant.sshOut "etcdctl set /dogu/inttest/current \"0.0.1\"" - - ecoSystem.vagrant.ssh "sudo docker cp /dogu/integrationTests/services/ cas:/etc/cas/services/production" + ecoSystem.vagrant.ssh "sudo docker cp /dogu/integrationTests/services/ cas:/etc/cas/services/production/" ecoSystem.vagrant.sshOut "sudo docker exec cas ls /etc/cas/services/production" ecoSystem.runCypressIntegrationTests([ From 91ddf940cf195d360ddd498c7994942dea4c41df Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Thu, 14 Nov 2024 16:07:49 +0100 Subject: [PATCH 39/50] migrate cas services in single node ces. Legacy cas service have not been migrated when explicit cas service accounts has been introduces, as they have been accepted as breaking change in multinode. cas service are now migrated to etcd under the key /config/cas/service_accounts/cas//created --- resources/post-upgrade.sh | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/resources/post-upgrade.sh b/resources/post-upgrade.sh index 3597b5cf..24eee7f9 100755 --- a/resources/post-upgrade.sh +++ b/resources/post-upgrade.sh @@ -152,6 +152,11 @@ migrateServicesFromETCD() { return 0 fi + if [[ "$(doguctl multinode)" = "false" ]]; then + echo "SingleNode environment detected, migrate legacy cas services" + migrateLegacyServicesFromETCD + fi + # Declare associative arrays to hold values for each application declare -A types declare -A secrets @@ -224,6 +229,53 @@ migrateServicesFromETCD() { echo "Migration completed. Individual files created for each application." } +migrateLegacyServicesFromETCD() { + basePath="/tmp/legacyEtcdCasMigration" + + mkdir -p "${basePath}" + + # Get all services listed in etcd + wget -O "${basePath}/ces.json" "http://$(getEtcdEndpoint):4001/v2/keys/dogu_v2?recursive=true" + + # extract the keys from json etcd response + jq '.node.nodes' "${basePath}/ces.json" > "${basePath}/nodes.json" + # filter services that have a dependency on cas + jq -r '.[] | .nodes[] | select(.value | contains("\"name\":\"cas\"")) | .value' "${basePath}/nodes.json" | jq -s '.' > "${basePath}/filter.json" + # filter services that don't have defined a service account for cas within dogu.json + jq '[.[] | select((.ServiceAccounts == null) or (.ServiceAccounts? | map(select(.Type == "cas")) | length) == 0)]' "${basePath}/filter.json" > "${basePath}/exclude.json" + # extract name from dogu.json + jq -r '.[] | .Name ' "${basePath}/exclude.json" > "${basePath}/migration.json" + # delete namespace from name, sort and delete duplicates + sed 's#.*/##' "${basePath}/migration.json" | sort | uniq > "${basePath}/migrationCandidates.txt" + + cat "${basePath}/migrationCandidates.txt" + + candidateFile="${basePath}/migrationCandidates.txt" + + # Check if names file exists + if [[ ! -f "$candidateFile" ]]; then + echo "candidate file not found: $candidateFile" >&2 + exit 1 + fi + + # Read each line from the file + while IFS= read -r name; do + echo "checking ${name}..." + #Check whether service is installed and has not migrated already + status_code=$(wget --spider -S "http://$(getEtcdEndpoint):4001/v2/keys/dogu_v2/${name}/current" 2>&1 | grep "HTTP/" | awk '{print $2}') + echo "status code from wget is: ${status_code}" + if [[ "$status_code" -eq 200 ]]; then + # We do not need to consider the LogoutUri as it has already been migrated in a previous step. + doguctl config "service_accounts/cas/${name}/created" "true" + echo "set service account entry in etcd for service ${name}" + fi + done < "$candidateFile" + + rm -r "${basePath}" + + echo "Legacy service account migration done." +} + runPostUpgrade() { FROM_VERSION="${1}" TO_VERSION="${2}" From 60173dacd9c35cb88ca4035bc560b4a60d118193 Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Thu, 14 Nov 2024 21:47:14 +0100 Subject: [PATCH 40/50] Replace FQDN . escape with [.] instead for serviceID regex --- CHANGELOG.md | 23 +++++++++++++---------- resources/util.sh | 4 ++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b29c343e..98dad265 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- Use JSON service registry [#221] + - services are read from and stored in json files instead of local config + - native implementation from CAS is used for this, which reduces custom overlay implementation +- Changed logic to create and remove service accounts [#221] + +### Removed +- Reading service information directly from ETCD [#221] + - Removed java classes for service creation + +### Fixed +- Fix ServiceIdFQDN regex by changing illegal url characters [#228] + ## [v7.0.8-4] - 2024-11-13 ### Added @@ -13,19 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - delegated authentication currently only works when using the embedded LDAP - Disclaimer for legal_urls without protocol [#230] -### Changed -- Use JSON service registry [#221] - - services are read from and stored in json files instead of local config - - native implementation from CAS is used for this, which reduces custom overlay implementation -- Changed logic to create and remove service accounts [#221] - ### Fixed - Fix configuration for delegated authentication with OIDC [#222] -### Removed -- Reading service information directly from ETCD [#221] - - Removed java classes for service creation - ## [v7.0.8-3] - 2024-10-11 ### Changed - Use flat instead of nested attributes for OAuth user profile. [#219] diff --git a/resources/util.sh b/resources/util.sh index 1ca3e208..0fd11981 100755 --- a/resources/util.sh +++ b/resources/util.sh @@ -194,8 +194,8 @@ function checkFqdnUpdate() { function escapeDots() { local fqdn="$1" - # Use parameter substitution to replace each '.' with '\\.' - local escaped_fqdn="${fqdn//./\\\\\\\\\\\\\\\\.}" + # Use parameter substitution to replace each '.' with '[.]' + local escaped_fqdn="${fqdn//./[.]}" # Return the double-escaped FQDN echo "$escaped_fqdn" From 0f9783becb6195614eb6a33dee7fe7894933cba5 Mon Sep 17 00:00:00 2001 From: Benjamin Ernst Date: Mon, 18 Nov 2024 16:52:08 +0100 Subject: [PATCH 41/50] #221 fix post-upgrade-script doguctl config remove in while-loop leads to exiting the loop to early ignore exit-code of wget while checking if a dogu is installed Co-authored-by: Nico Franzeck --- resources/post-upgrade.sh | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/resources/post-upgrade.sh b/resources/post-upgrade.sh index 24eee7f9..a83c81de 100755 --- a/resources/post-upgrade.sh +++ b/resources/post-upgrade.sh @@ -90,12 +90,16 @@ migrateServiceAccountsToFoldersByType() { # Parse services that have registered a service-account and their secret hash from ETCD response. # Formatted as tab-separated-values these can be iterated over in bash. - jq -r ".node // {} | .nodes // [] | .[] | select(.dir | not) | { service: .key | sub(\".*/${saType}/(?[^/]*)$\";\"\(.name)\"), clientSecretHash: .value } | [.service, .clientSecretHash] | @tsv" < "${outFile}" | - while IFS=$'\t' read -r service clientSecretHash; do - echo "Migrating service account directory for '${service}'" - doguctl config --remove "service_accounts/${saType}/${service}" - doguctl config "service_accounts/${saType}/${service}/secret" "${clientSecretHash}" - done + servicesFile="$(mktemp)" + jq -r ".node // {} | .nodes // [] | .[] | select(.dir | not) | { service: .key | sub(\".*/${saType}/(?[^/]*)$\";\"\(.name)\"), clientSecretHash: .value } | [.service, .clientSecretHash] | @tsv" < "${outFile}" > ${servicesFile} + # Read all lines from the file into an array + mapfile -t lines < "${servicesFile}" + for line in "${lines[@]}"; do + IFS=$'\t' read -r service clientSecretHash <<< "$line" + echo "Migrating service account directory for '${service}' with ${clientSecretHash}" + doguctl config --remove "service_accounts/${saType}/${service}" + doguctl config "service_accounts/${saType}/${service}/secret" "${clientSecretHash}" + done echo "Migrating service accounts of type '${saType}'... Done!" } @@ -262,7 +266,7 @@ migrateLegacyServicesFromETCD() { while IFS= read -r name; do echo "checking ${name}..." #Check whether service is installed and has not migrated already - status_code=$(wget --spider -S "http://$(getEtcdEndpoint):4001/v2/keys/dogu_v2/${name}/current" 2>&1 | grep "HTTP/" | awk '{print $2}') + status_code=$(wget --spider -S "http://$(getEtcdEndpoint):4001/v2/keys/dogu_v2/${name}/current" 2>&1 | grep "HTTP/" | awk '{print $2}'; exit 0) echo "status code from wget is: ${status_code}" if [[ "$status_code" -eq 200 ]]; then # We do not need to consider the LogoutUri as it has already been migrated in a previous step. From 3dbad38e24750c113494e9473d05cd9d1b89910d Mon Sep 17 00:00:00 2001 From: Nico Franzeck Date: Tue, 19 Nov 2024 16:21:35 +0100 Subject: [PATCH 42/50] add suggestions from pr review --- resources/post-upgrade.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/post-upgrade.sh b/resources/post-upgrade.sh index a83c81de..b0d38c2b 100755 --- a/resources/post-upgrade.sh +++ b/resources/post-upgrade.sh @@ -252,8 +252,6 @@ migrateLegacyServicesFromETCD() { # delete namespace from name, sort and delete duplicates sed 's#.*/##' "${basePath}/migration.json" | sort | uniq > "${basePath}/migrationCandidates.txt" - cat "${basePath}/migrationCandidates.txt" - candidateFile="${basePath}/migrationCandidates.txt" # Check if names file exists @@ -267,17 +265,19 @@ migrateLegacyServicesFromETCD() { echo "checking ${name}..." #Check whether service is installed and has not migrated already status_code=$(wget --spider -S "http://$(getEtcdEndpoint):4001/v2/keys/dogu_v2/${name}/current" 2>&1 | grep "HTTP/" | awk '{print $2}'; exit 0) - echo "status code from wget is: ${status_code}" if [[ "$status_code" -eq 200 ]]; then # We do not need to consider the LogoutUri as it has already been migrated in a previous step. doguctl config "service_accounts/cas/${name}/created" "true" echo "set service account entry in etcd for service ${name}" + else + echo "dogu ${name} is currently not installed, skip migration for it..." fi done < "$candidateFile" + # Delete temporary migration relevant files rm -r "${basePath}" - echo "Legacy service account migration done." + echo "Legacy service account migration done.\n" } runPostUpgrade() { From d537ef2aa8f7b62b293e6f035c9c78ec4b6e06bb Mon Sep 17 00:00:00 2001 From: Benjamin Ernst Date: Wed, 20 Nov 2024 15:07:09 +0100 Subject: [PATCH 43/50] #221 fix startup-error under high system-load When cas was started while the overall system-load was high, an exception was thrown and the start of the web-application was aborted. To fix this error the startup-delay of the service-registry-scheduler was increased to 1 minute. This seems to resolve the startup-error under high load. --- resources/etc/cas/config/cas.properties.tpl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/etc/cas/config/cas.properties.tpl b/resources/etc/cas/config/cas.properties.tpl index 46fb221c..2591cf1e 100644 --- a/resources/etc/cas/config/cas.properties.tpl +++ b/resources/etc/cas/config/cas.properties.tpl @@ -279,4 +279,6 @@ cas.authn.oauth.accessToken.maxTimeToLiveInSeconds=86000 cas.service-registry.json.location={{if eq (.GlobalConfig.GetOrDefault "stage" "production") "production"}}file:/etc/cas/services/production{{else}}file:/etc/cas/services/development{{end}} cas.service-registry.json.watcher-enabled=true cas.service-registry.templates.directory.location=file:/etc/cas/services/templates +# Increase start-delay of scheduler to prevent startup-errors on slow starts +cas.service-registry.schedule.start-delay=PT1M ######################################################################################################################## \ No newline at end of file From 5f18a4df4ee3a2f7310fc6eadaa7e323482d5986 Mon Sep 17 00:00:00 2001 From: Benjamin Ernst Date: Wed, 20 Nov 2024 15:10:06 +0100 Subject: [PATCH 44/50] #221 add http health-check --- CHANGELOG.md | 3 +++ dogu.json | 5 +++++ resources/etc/cas/config/cas.properties.tpl | 11 +++++++++++ 3 files changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98dad265..2b784405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Add http health-check, so that the dogu will get healthy when the start of the web-application is completed + ### Changed - Use JSON service registry [#221] - services are read from and stored in json files instead of local config diff --git a/dogu.json b/dogu.json index 4d8c2421..43f4a0f0 100644 --- a/dogu.json +++ b/dogu.json @@ -430,6 +430,11 @@ "Type": "tcp", "Port": 8080 }, + { + "Type": "http", + "Port": 8080, + "Path": "/cas/actuator/health" + }, { "Type": "state" } diff --git a/resources/etc/cas/config/cas.properties.tpl b/resources/etc/cas/config/cas.properties.tpl index 2591cf1e..e705f4e3 100644 --- a/resources/etc/cas/config/cas.properties.tpl +++ b/resources/etc/cas/config/cas.properties.tpl @@ -31,6 +31,17 @@ spring.mail.port=25 spring.mail.protocol=smtp ######################################################################################################################## +######################################################################################################################## +# Health-Endpoint configuration +# Configuration guide: https://apereo.github.io/cas/7.0.x/monitoring/actuators/Actuator-Endpoint-Health.html#casendppointpropshealth +# ---------------------------------------------------------------------------------------------------------------------- +management.endpoint.health.enabled=true +management.endpoint.health.show-details=always +management.endpoints.web.exposure.include=health +cas.monitor.endpoints.endpoint.health.access=ANONYMOUS +######################################################################################################################## + + ######################################################################################################################## # LDAP # Configuration guide: https://apereo.github.io/cas/6.3.x/installation/LDAP-Authentication.html From 456a2547c9182a4c87c7b4ec6a63f104f751eb6c Mon Sep 17 00:00:00 2001 From: Benjamin Ernst Date: Thu, 21 Nov 2024 09:23:45 +0100 Subject: [PATCH 45/50] #221 remove http health-check, because it is currently not working with cesapp verify --- CHANGELOG.md | 3 --- dogu.json | 5 ----- resources/etc/cas/config/cas.properties.tpl | 11 ----------- 3 files changed, 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b784405..98dad265 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -### Added -- Add http health-check, so that the dogu will get healthy when the start of the web-application is completed - ### Changed - Use JSON service registry [#221] - services are read from and stored in json files instead of local config diff --git a/dogu.json b/dogu.json index 43f4a0f0..4d8c2421 100644 --- a/dogu.json +++ b/dogu.json @@ -430,11 +430,6 @@ "Type": "tcp", "Port": 8080 }, - { - "Type": "http", - "Port": 8080, - "Path": "/cas/actuator/health" - }, { "Type": "state" } diff --git a/resources/etc/cas/config/cas.properties.tpl b/resources/etc/cas/config/cas.properties.tpl index e705f4e3..2591cf1e 100644 --- a/resources/etc/cas/config/cas.properties.tpl +++ b/resources/etc/cas/config/cas.properties.tpl @@ -31,17 +31,6 @@ spring.mail.port=25 spring.mail.protocol=smtp ######################################################################################################################## -######################################################################################################################## -# Health-Endpoint configuration -# Configuration guide: https://apereo.github.io/cas/7.0.x/monitoring/actuators/Actuator-Endpoint-Health.html#casendppointpropshealth -# ---------------------------------------------------------------------------------------------------------------------- -management.endpoint.health.enabled=true -management.endpoint.health.show-details=always -management.endpoints.web.exposure.include=health -cas.monitor.endpoints.endpoint.health.access=ANONYMOUS -######################################################################################################################## - - ######################################################################################################################## # LDAP # Configuration guide: https://apereo.github.io/cas/6.3.x/installation/LDAP-Authentication.html From edbdc6ed206b82c72b7df9416fec485e6b0c675e Mon Sep 17 00:00:00 2001 From: Benjamin Ernst Date: Thu, 21 Nov 2024 11:53:29 +0100 Subject: [PATCH 46/50] #221 update release-notes and changelog --- CHANGELOG.md | 3 +++ docs/gui/release_notes_de.md | 6 ++++-- docs/gui/release_notes_en.md | 8 +++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98dad265..e536c9ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - native implementation from CAS is used for this, which reduces custom overlay implementation - Changed logic to create and remove service accounts [#221] +### Breaking Change +- Newly installed dogus must explicitly request the creation of a service account in the CAS via dogu.json. Further information on this can be found in the [developer documentation](https://github.com/cloudogu/dogu-development-docs/blob/main/docs/important/relevant_functionalities_en.md#authentifizierung) + ### Removed - Reading service information directly from ETCD [#221] - Removed java classes for service creation diff --git a/docs/gui/release_notes_de.md b/docs/gui/release_notes_de.md index 1c680da8..7f46faed 100644 --- a/docs/gui/release_notes_de.md +++ b/docs/gui/release_notes_de.md @@ -5,14 +5,16 @@ Im Folgenden finden Sie die Release Notes für das CAS-Dogu. Technische Details zu einem Release finden Sie im zugehörigen [Changelog](https://docs.cloudogu.com/de/docs/dogus/cas/CHANGELOG/). ## [Unreleased] -- Bei der Anmeldung über eine delegierte Authentifizierung (durch einen OIDC-Provider) werden die Nutzer in den internen LDAP repliziert - - Die replizierten Nutzer werden als "extern" gekennzeichnet und können, bis auf die Gruppenzuordnung, nicht editiert werden. - Das Dogu wurde intern auf eine JSON Registry umgestellt, wodurch sich die Logik zum Anlegen und Löschen von Service-Accounts geändert hat. - Einheitliche Verwendung von Service-Accounts sowohl in einer Multinode- als auch Singlenode-Umgebung. ### Breaking Change - Neu zu installierende Dogus müssen explizit die Erstellung eines Serviceaccounts im CAS über die dogu.json anfordern. Weitere Informationen hierfür finden Sie in der [Entwicklerdokumentation](https://github.com/cloudogu/dogu-development-docs/blob/main/docs/important/relevant_functionalities_de.md#authentifizierung) +## Release 7.0.8-4 +- Bei der Anmeldung über eine delegierte Authentifizierung (durch einen OIDC-Provider) werden die Nutzer in den internen LDAP repliziert + - Die replizierten Nutzer werden als "extern" gekennzeichnet und können, bis auf die Gruppenzuordnung, nicht editiert werden. + ## Release 7.0.8-3 - Es wurde eine Anpassung gemacht, welche die Kompatibilität für Dogus erweitert, welche Open ID Connect nutzen. diff --git a/docs/gui/release_notes_en.md b/docs/gui/release_notes_en.md index 806e2890..ab0fa9f3 100644 --- a/docs/gui/release_notes_en.md +++ b/docs/gui/release_notes_en.md @@ -5,14 +5,16 @@ Below you will find the release notes for CAS-Dogu. Technical details on a release can be found in the corresponding [Changelog](https://docs.cloudogu.com/de/docs/dogus/cas/CHANGELOG/). ## [Unreleased] -- When logging in via delegated authentication (using an OIDC provider), the users are replicated in the embedded LDAP - - The replicated users are marked as “external” and cannot be edited, except for the group assignments. - The Dogu has been internally converted to a JSON registry, which has changed the logic for creating and deleting service accounts. - Consistent use of service accounts in both multinode and singlenode environments. -## Breaking Change +### Breaking Change - Newly installed dogus must explicitly request the creation of a service account in the CAS via dogu.json. Further information on this can be found in the [developer documentation](https://github.com/cloudogu/dogu-development-docs/blob/main/docs/important/relevant_functionalities_en.md#authentifizierung) +## Release 7.0.8-4 +- When logging in via delegated authentication (using an OIDC provider), the users are replicated in the embedded LDAP + - The replicated users are marked as “external” and cannot be edited, except for the group assignments. + ## Release 7.0.8-3 - An adjustment has been made that extends compatibility for Dogus that use Open ID Connect. From 0b373cbd287a1d79c297c86bb9d8c0309ff7a354 Mon Sep 17 00:00:00 2001 From: Benjamin Ernst Date: Thu, 21 Nov 2024 13:44:52 +0100 Subject: [PATCH 47/50] #221 change testUser password to comply with password policies --- integrationTests/.gitignore | 1 + integrationTests/cypress/fixtures/testuser_data.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/integrationTests/.gitignore b/integrationTests/.gitignore index 6a502bb9..6dc66c4a 100644 --- a/integrationTests/.gitignore +++ b/integrationTests/.gitignore @@ -2,3 +2,4 @@ node_modules cypress/videos cypress/screenshots /yarn-error.log +/cypress.env.json diff --git a/integrationTests/cypress/fixtures/testuser_data.json b/integrationTests/cypress/fixtures/testuser_data.json index 1260226b..56e0dd21 100644 --- a/integrationTests/cypress/fixtures/testuser_data.json +++ b/integrationTests/cypress/fixtures/testuser_data.json @@ -1,6 +1,6 @@ { "username": "testuser", - "password": "testuserpassword", + "password": "TestuserPassword24!", "givenname": "test", "surname": "test", "displayName": "test", From bbef1f2ffb7a3a0bda68c97d2d8d29e3b6f02e4b Mon Sep 17 00:00:00 2001 From: Benjamin Ernst Date: Thu, 21 Nov 2024 14:34:46 +0100 Subject: [PATCH 48/50] #221 fix integration-tests in pipeline --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index e4072e91..df1d1982 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -168,8 +168,8 @@ parallel( } stage('Build dogu') { - // force post-upgrade from cas version 7.0.8-3 to migrate existing services from defaultSetupConfig - ecoSystem.vagrant.sshOut "sed 's/7.0.8-3/7.0.8-4/g' -i /dogu/dogu.json" + // force post-upgrade from cas version 7.0.8-4 to migrate existing services from defaultSetupConfig + ecoSystem.vagrant.sshOut "sed 's/7.0.8-4/7.0.8-5/g' -i /dogu/dogu.json" ecoSystem.build("/dogu") } From ee5b07bf59b4c7611ceeafd9eaaf6d71b9619195 Mon Sep 17 00:00:00 2001 From: Benjamin Ernst Date: Thu, 21 Nov 2024 15:17:01 +0100 Subject: [PATCH 49/50] Bump version --- Dockerfile | 2 +- dogu.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5f42d9ca..e03a0f50 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,7 +44,7 @@ RUN apk update && apk add wget && wget -O "apache-tomcat-${TOMCAT_VERSION}.tar. FROM registry.cloudogu.com/official/java:21.0.4-4 LABEL NAME="official/cas" \ - VERSION="7.0.8-4" \ + VERSION="7.0.8-5" \ maintainer="hello@cloudogu.com" ARG TOMCAT_VERSION diff --git a/dogu.json b/dogu.json index 4d8c2421..7a1df3d7 100644 --- a/dogu.json +++ b/dogu.json @@ -1,6 +1,6 @@ { "Name": "official/cas", - "Version": "7.0.8-4", + "Version": "7.0.8-5", "DisplayName": "Central Authentication Service", "Description": "The Central Authentication Service (CAS) is a single sign-on protocol for the web.", "Url": "https://apereo.github.io/cas", diff --git a/package.json b/package.json index 622aec27..981bc879 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ces-style-generator", - "version": "7.0.8-4", + "version": "7.0.8-5", "description": "Npm project to use ces-theme to generate styling", "main": "index.js", "directories": { From e58ef88da97c5dc2c48bec688d04913ebdb0d169 Mon Sep 17 00:00:00 2001 From: Benjamin Ernst Date: Thu, 21 Nov 2024 15:17:30 +0100 Subject: [PATCH 50/50] Update changelog --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e536c9ef..2fefb6b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [v7.0.8-5] - 2024-11-21 +### Breaking Change +- Newly installed dogus must explicitly request the creation of a service account in the CAS via dogu.json. Further information on this can be found in the [developer documentation](https://github.com/cloudogu/dogu-development-docs/blob/main/docs/important/relevant_functionalities_en.md#authentifizierung) + ### Changed - Use JSON service registry [#221] - services are read from and stored in json files instead of local config - native implementation from CAS is used for this, which reduces custom overlay implementation - Changed logic to create and remove service accounts [#221] -### Breaking Change -- Newly installed dogus must explicitly request the creation of a service account in the CAS via dogu.json. Further information on this can be found in the [developer documentation](https://github.com/cloudogu/dogu-development-docs/blob/main/docs/important/relevant_functionalities_en.md#authentifizierung) - ### Removed - Reading service information directly from ETCD [#221] - Removed java classes for service creation