From 94609d9e08041edf0f5063b031768f807d2b759e Mon Sep 17 00:00:00 2001 From: Devrim Date: Sat, 20 Jul 2024 14:33:30 +0300 Subject: [PATCH 01/43] fix(jans-linux-setup): kc-scheduler.service upon uninstall (#8982) Signed-off-by: Mustafa Baser --- jans-linux-setup/jans_setup/install.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/jans-linux-setup/jans_setup/install.py b/jans-linux-setup/jans_setup/install.py index 79732c000a4..11cc0ce87fa 100755 --- a/jans-linux-setup/jans_setup/install.py +++ b/jans-linux-setup/jans_setup/install.py @@ -230,9 +230,6 @@ def uninstall_jans(): if os.path.exists('/opt/opa'): service_list.append('opa') - if os.path.exists('/opt/kc-scheduler'): - service_list.append('kc-scheduler') - for service in service_list: print("Stopping", service) @@ -252,7 +249,7 @@ def uninstall_jans(): os.system('systemctl daemon-reload') os.system('systemctl reset-failed') - remove_list = ['/etc/certs', '/etc/jans', '/opt/amazon-corretto*', '/opt/jre', '/opt/node*', '/opt/jetty*', '/opt/jython*', '/opt/keycloak', '/opt/idp', '/opt/opa', '/opt/kc-scheduler'] + remove_list = ['/etc/certs', '/etc/jans', '/opt/amazon-corretto*', '/opt/jre', '/opt/node*', '/opt/jetty*', '/opt/jython*', '/opt/keycloak', '/opt/idp', '/opt/opa', '/opt/kc-scheduler', '/etc/cron.d/kc-scheduler-cron'] if argsp.profile == 'jans': remove_list.append('/opt/opendj') if not argsp.keep_downloads: From 56ae412d3a174b69184f75b72ef12769f7defbfb Mon Sep 17 00:00:00 2001 From: Devrim Date: Mon, 22 Jul 2024 08:32:16 +0300 Subject: [PATCH 02/43] fix(jans-linux-setup): install with setup.properties (#8994) Signed-off-by: Mustafa Baser --- .../jans_setup/setup_app/utils/properties_utils.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/jans-linux-setup/jans_setup/setup_app/utils/properties_utils.py b/jans-linux-setup/jans_setup/setup_app/utils/properties_utils.py index 15eca9bd0c1..eb71031615f 100644 --- a/jans-linux-setup/jans_setup/setup_app/utils/properties_utils.py +++ b/jans-linux-setup/jans_setup/setup_app/utils/properties_utils.py @@ -194,13 +194,21 @@ def load_properties(self, prop_file, no_update=[]): if p.get('cb_install') == '0': p['cb_install'] = InstallTypes.NONE + if p.get('cb_install'): + p['opendj_install'] = InstallTypes.NONE + p['rdbm_install'] = InstallTypes.NONE + if p.get('opendj_install') == '0': p['opendj_install'] = InstallTypes.NONE + if base.as_bool(p.get('installLdap', False)): + p['opendj_install'] = InstallTypes.LOCAL + p['rdbm_install'] = InstallTypes.NONE + if p.get('enable-script'): base.argsp.enable_script = p['enable-script'].split() - if p.get('loadTestData'): + if base.as_bool(p.get('loadTestData', False)): base.argsp.t = True if p.get('rdbm_type') == 'pgsql' and not p.get('rdbm_port'): @@ -635,8 +643,6 @@ def pompt_for_jans_lock(self): self.getDefaultOption(Config.install_jans_lock) )[0].lower() - - if prompt == 'y': prompt = self.getPrompt(" Install Jans Lock as Server?", self.getDefaultOption(Config.install_jans_lock) From 7af36c9a563be16b2aa1fb9149d4d3ebc09cb2fa Mon Sep 17 00:00:00 2001 From: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> Date: Mon, 22 Jul 2024 10:07:44 +0300 Subject: [PATCH 03/43] ci: add cb spanner docker monolith (#8999) * feat:add couchabse and spanner Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: parse couchbase and spanner docker compose files Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix typo Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix typo Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix typo Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * fix: run spanner locally Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * fix: run spanner locally Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * fix: run spanner locally Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * fix: run spanner locally Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * fix: run spanner locally Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * fix: run spanner locally Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * fix: run spanner locally Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * fix: run spanner locally Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * fix: RUN_TEST parse Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * fix: RUN_TEST parse Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * fix: adjust spanner setup Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: default don't install KC Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * chore: break out of setup Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> --------- Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> --- .../workflows/test_docker_linux_installer.yml | 2 +- automation/startjanssenmonolithdemo.sh | 23 ++++++-- docker-jans-monolith/Dockerfile | 8 ++- docker-jans-monolith/clean.sh | 2 +- docker-jans-monolith/down.sh | 2 +- .../jans-couchbase-compose.yml | 59 +++++++++++++++++++ docker-jans-monolith/jans-ldap-compose.yml | 2 +- docker-jans-monolith/jans-mysql-compose.yml | 2 +- .../jans-postgres-compose.yml | 2 +- docker-jans-monolith/jans-spanner-compose.yml | 44 ++++++++++++++ docker-jans-monolith/scripts/entrypoint.sh | 20 ++++++- docker-jans-monolith/up.sh | 2 +- 12 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 docker-jans-monolith/jans-couchbase-compose.yml create mode 100644 docker-jans-monolith/jans-spanner-compose.yml diff --git a/.github/workflows/test_docker_linux_installer.yml b/.github/workflows/test_docker_linux_installer.yml index d3214f714ae..11602a99b02 100644 --- a/.github/workflows/test_docker_linux_installer.yml +++ b/.github/workflows/test_docker_linux_installer.yml @@ -21,7 +21,7 @@ jobs: max-parallel: 6 matrix: # add '"pgsql" when supported - persistence-backends: ["MYSQL", "PGSQL", "LDAP"] + persistence-backends: ["MYSQL", "PGSQL", "LDAP", "COUCHBASE", "SPANNER"] python-version: ["3.7"] fail-fast: false steps: diff --git a/automation/startjanssenmonolithdemo.sh b/automation/startjanssenmonolithdemo.sh index 89888cc4a79..5af5405a77f 100644 --- a/automation/startjanssenmonolithdemo.sh +++ b/automation/startjanssenmonolithdemo.sh @@ -12,7 +12,7 @@ if [[ ! "$JANS_FQDN" ]]; then read -rp "Enter Hostname [demoexample.jans.io]: " JANS_FQDN fi if [[ ! "$JANS_PERSISTENCE" ]]; then - read -rp "Enter persistence type [LDAP|MYSQL|PGSQL]: " JANS_PERSISTENCE + read -rp "Enter persistence type [LDAP|MYSQL|PGSQL|COUCHBASE[TEST]|SPANNER[TEST]]: " JANS_PERSISTENCE fi if [[ -z $EXT_IP ]]; then @@ -72,12 +72,15 @@ if [[ "$JANS_BUILD_COMMIT" ]]; then python3 -c "from pathlib import Path ; import ruamel.yaml ; compose = Path('/tmp/jans/docker-jans-monolith/jans-mysql-compose.yml') ; yaml = ruamel.yaml.YAML() ; data = yaml.load(compose) ; data['services']['jans']['build'] = '.' ; del data['services']['jans']['image'] ; yaml.dump(data, compose)" python3 -c "from pathlib import Path ; import ruamel.yaml ; compose = Path('/tmp/jans/docker-jans-monolith/jans-postgres-compose.yml') ; yaml = ruamel.yaml.YAML() ; data = yaml.load(compose) ; data['services']['jans']['build'] = '.' ; del data['services']['jans']['image'] ; yaml.dump(data, compose)" python3 -c "from pathlib import Path ; import ruamel.yaml ; compose = Path('/tmp/jans/docker-jans-monolith/jans-ldap-compose.yml') ; yaml = ruamel.yaml.YAML() ; data = yaml.load(compose) ; data['services']['jans']['build'] = '.' ; del data['services']['jans']['image'] ; yaml.dump(data, compose)" + python3 -c "from pathlib import Path ; import ruamel.yaml ; compose = Path('/tmp/jans/docker-jans-monolith/jans-couchbase-compose.yml') ; yaml = ruamel.yaml.YAML() ; data = yaml.load(compose) ; data['services']['jans']['build'] = '.' ; del data['services']['jans']['image'] ; yaml.dump(data, compose)" + python3 -c "from pathlib import Path ; import ruamel.yaml ; compose = Path('/tmp/jans/docker-jans-monolith/jans-spanner-compose.yml') ; yaml = ruamel.yaml.YAML() ; data = yaml.load(compose) ; data['services']['jans']['build'] = '.' ; del data['services']['jans']['image'] ; yaml.dump(data, compose)" fi # -- if [[ "$IS_FQDN_REGISTERED" ]]; then python3 -c "from dockerfile_parse import DockerfileParser ; dfparser = DockerfileParser('/tmp/jans/docker-jans-monolith') ; dfparser.envs['IS_FQDN_REGISTERED'] = 'true'" fi -if [[ "$RUN_TESTS" ]]; then +if [[ "$RUN_TESTS" == "true" ]]; then + echo "Activating RUN_TEST ENV.." python3 -c "from dockerfile_parse import DockerfileParser ; dfparser = DockerfileParser('/tmp/jans/docker-jans-monolith') ; dfparser.envs['RUN_TESTS'] = 'true'" fi if [[ $JANS_PERSISTENCE == "MYSQL" ]]; then @@ -86,6 +89,10 @@ elif [[ $JANS_PERSISTENCE == "PGSQL" ]]; then bash /tmp/jans/docker-jans-monolith/up.sh postgres elif [[ $JANS_PERSISTENCE == "LDAP" ]]; then bash /tmp/jans/docker-jans-monolith/up.sh ldap +elif [[ $JANS_PERSISTENCE == "COUCHBASE" ]]; then + bash /tmp/jans/docker-jans-monolith/up.sh couchbase +elif [[ $JANS_PERSISTENCE == "SPANNER" ]]; then + bash /tmp/jans/docker-jans-monolith/up.sh spanner fi echo "$EXT_IP $JANS_FQDN" | sudo tee -a /etc/hosts > /dev/null jans_status="unhealthy" @@ -119,9 +126,13 @@ docker exec docker-jans-monolith-jans-1 curl -f -k https://localhost/.well-known echo -e "Testing fido2-configuration endpoint.. \n" docker exec docker-jans-monolith-jans-1 curl -f -k https://localhost/.well-known/fido2-configuration mkdir -p /tmp/reports || echo "reports folder exists" -while ! docker exec docker-jans-monolith-jans-1 test -f "/tmp/httpd.crt"; do +end=$((SECONDS+180)) +while [ $SECONDS -lt $end ]; do echo "Waiting for the container to run java test preparations" - sleep 5 + if docker exec docker-jans-monolith-jans-1 test -f "/tmp/httpd.crt"; then + break + fi + sleep 10 done echo -e "Running build.. \n" docker exec -w /tmp/jans/jans-auth-server docker-jans-monolith-jans-1 mvn -Dcfg="$JANS_FQDN" -Dmaven.test.skip=true -fae clean compile install @@ -134,7 +145,9 @@ docker cp docker-jans-monolith-jans-1:/tmp/jans/jans-auth-server/test-model/targ docker cp docker-jans-monolith-jans-1:/tmp/jans/jans-auth-server/model/target/surefire-reports/testng-results.xml /tmp/reports/$JANS_PERSISTENCE-jans-auth-model-testng-results.xml EOF -sudo bash testendpoints.sh +if [[ "$RUN_TESTS" == "true" ]]; then + sudo bash testendpoints.sh +fi echo -e "You may re-execute bash testendpoints.sh to do a quick test to check the configuration endpoints." echo -e "Add the following record to your local computers' hosts file to engage with the services $EXT_IP $JANS_FQDN" echo -e "To stop run:" diff --git a/docker-jans-monolith/Dockerfile b/docker-jans-monolith/Dockerfile index 7ea8ba310de..98568c68cda 100644 --- a/docker-jans-monolith/Dockerfile +++ b/docker-jans-monolith/Dockerfile @@ -1,5 +1,6 @@ FROM ubuntu:22.04@sha256:e9569c25505f33ff72e88b2990887c9dcf230f23259da296eb814fc2b41af999 + # Don't start any optional services except for the few we need. RUN find /etc/systemd/system \ /lib/systemd/system \ @@ -32,6 +33,9 @@ RUN systemctl set-default multi-user.target \ RUN rm -f /lib/systemd/system/systemd*udev* \ && rm -f /lib/systemd/system/getty.target +# Install google cloud client +RUN curl https://sdk.cloud.google.com > install.sh && bash install.sh --disable-prompts + HEALTHCHECK --interval=35s --timeout=4s CMD /opt/dist/scripts/jans-auth check | grep "Jetty running pid" || exit 1 # Ports required by jetty @@ -66,6 +70,8 @@ ENV CN_HOSTNAME="demoexample.jans.io" \ TEST_CLIENT_ID="9876baac-de39-4c23-8a78-674b59df8c09" \ TEST_CLIENT_SECRET="" \ TEST_CLIENT_TRUSTED="true" \ + CN_INSTALL_COUCHBASE="false" \ + CN_INSTALL_SPANNER="false" \ CN_INSTALL_LDAP="false" \ CN_INSTALL_MYSQL="false" \ CN_INSTALL_PGSQL="false" \ @@ -76,7 +82,7 @@ ENV CN_HOSTNAME="demoexample.jans.io" \ CN_INSTALL_KC_LINK="true" \ CN_INSTALL_LINK="true" \ CN_INSTALL_LOCK="true" \ - CN_INSTALL_SAML="true" \ + CN_INSTALL_SAML="false" \ CN_INSTALL_OPA="true" \ RDBMS_DATABASE="jans" \ RDBMS_USER="jans" \ diff --git a/docker-jans-monolith/clean.sh b/docker-jans-monolith/clean.sh index 1356f9a5506..04b7afefbb7 100644 --- a/docker-jans-monolith/clean.sh +++ b/docker-jans-monolith/clean.sh @@ -5,7 +5,7 @@ if [ -z "$1" ]; then yaml="jans-mysql-compose.yml" else case "$1" in - mysql|ldap|postgres) + mysql|ldap|postgres|couchbase|spanner) yaml="jans-${1}-compose.yml" ;; *) diff --git a/docker-jans-monolith/down.sh b/docker-jans-monolith/down.sh index 017578f58b4..079877655c0 100644 --- a/docker-jans-monolith/down.sh +++ b/docker-jans-monolith/down.sh @@ -5,7 +5,7 @@ if [ -z "$1" ]; then yaml="jans-mysql-compose.yml" else case "$1" in - mysql|ldap|postgres) + mysql|ldap|postgres|couchbase|spanner) yaml="jans-${1}-compose.yml" ;; *) diff --git a/docker-jans-monolith/jans-couchbase-compose.yml b/docker-jans-monolith/jans-couchbase-compose.yml new file mode 100644 index 00000000000..1ee690468f7 --- /dev/null +++ b/docker-jans-monolith/jans-couchbase-compose.yml @@ -0,0 +1,59 @@ +version: "3.7" +services: + couchbase: + image: couchbase/server-sandbox:7.6.1 + restart: always + ports: + - "8091-8096:8091-8096" + - "11210-11211:11210-11211" + volumes: + - ./couchbase_demo:/opt/couchbase/var + networks: + - cloud_bridge + jans: + image: ${JANSSEN_IMAGE:-ghcr.io/janssenproject/jans/monolith:1.1.4_dev} + restart: always + ports: + - "443:443" + - "80:80" + depends_on: + - couchbase + networks: + - cloud_bridge + environment: + #- CN_HOSTNAME=demoexample.jans.io + - CN_ADMIN_PASS=1t5Fin3#security + - CN_ORG_NAME=Janssen + - CN_EMAIL=support@jans.io + - CN_CITY=Austin + - CN_STATE=TX + - CN_COUNTRY=US + - CN_INSTALL_COUCHBASE=true + - CN_INSTALL_CONFIG_API=true + - CN_INSTALL_SCIM=true + - CN_INSTALL_FIDO2=true + - CN_INSTALL_CASA=true + - CN_INSTALL_KC_LINK=true + - CN_INSTALL_LOCK=true + - CN_INSTALL_SAML=false + - CN_INSTALL_OPA=true + - TEST_CLIENT_ID=9876baac-de39-4c23-8a78-674b59df8c09 + - TEST_CLIENT_TRUSTED=true + - TEST_CLIENT_SECRET=1t5Fin3#security + - COUCHBASE_PASSWORD=password + - COUCHBASE_ADMIN=Administrator + - COUCHBASE_HOSTNAME=couchbase + volumes: + - ./jans-auth-custom:/opt/jans/jetty/jans-auth/custom + - ./jans-config-api-custom:/opt/jans/jetty/jans-config-api/custom + - ./jans-fido2-custom:/opt/jans/jetty/jans-fido2/custom + - ./jans-scim-custom:/opt/jans/jetty/jans-scim/custom + - ./jans-auth-log:/opt/jans/jetty/jans-auth/logs + - ./jans-config-api-log:/opt/jans/jetty/jans-config-api/logs + - ./jans-scim-log:/opt/jans/jetty/jans-scim/logs + - ./jans-fido2-log:/opt/jans/jetty/jans-fido2/log +volumes: + db-data: +networks: + cloud_bridge: + driver: bridge diff --git a/docker-jans-monolith/jans-ldap-compose.yml b/docker-jans-monolith/jans-ldap-compose.yml index 1be5a46d11f..8782bd46570 100644 --- a/docker-jans-monolith/jans-ldap-compose.yml +++ b/docker-jans-monolith/jans-ldap-compose.yml @@ -23,7 +23,7 @@ services: - CN_INSTALL_CASA=true - CN_INSTALL_KC_LINK=true - CN_INSTALL_LOCK=true - - CN_INSTALL_SAML=true + - CN_INSTALL_SAML=false - CN_INSTALL_OPA=true - TEST_CLIENT_ID=9876baac-de39-4c23-8a78-674b59df8c09 - TEST_CLIENT_TRUSTED=true diff --git a/docker-jans-monolith/jans-mysql-compose.yml b/docker-jans-monolith/jans-mysql-compose.yml index 1636788d354..c8b9e40687d 100644 --- a/docker-jans-monolith/jans-mysql-compose.yml +++ b/docker-jans-monolith/jans-mysql-compose.yml @@ -40,7 +40,7 @@ services: - CN_INSTALL_CASA=true - CN_INSTALL_KC_LINK=true - CN_INSTALL_LOCK=true - - CN_INSTALL_SAML=true + - CN_INSTALL_SAML=false - CN_INSTALL_OPA=true - TEST_CLIENT_ID=9876baac-de39-4c23-8a78-674b59df8c09 - TEST_CLIENT_TRUSTED=true diff --git a/docker-jans-monolith/jans-postgres-compose.yml b/docker-jans-monolith/jans-postgres-compose.yml index f63ab7c85b7..9974b463fb5 100644 --- a/docker-jans-monolith/jans-postgres-compose.yml +++ b/docker-jans-monolith/jans-postgres-compose.yml @@ -38,7 +38,7 @@ services: - CN_INSTALL_CASA=true - CN_INSTALL_KC_LINK=true - CN_INSTALL_LOCK=true - - CN_INSTALL_SAML=true + - CN_INSTALL_SAML=false - CN_INSTALL_OPA=true - TEST_CLIENT_ID=9876baac-de39-4c23-8a78-674b59df8c09 - TEST_CLIENT_TRUSTED=true diff --git a/docker-jans-monolith/jans-spanner-compose.yml b/docker-jans-monolith/jans-spanner-compose.yml new file mode 100644 index 00000000000..26ea5569c50 --- /dev/null +++ b/docker-jans-monolith/jans-spanner-compose.yml @@ -0,0 +1,44 @@ +version: "3.7" +services: + jans: + image: ${JANSSEN_IMAGE:-ghcr.io/janssenproject/jans/monolith:1.1.4_dev} + restart: always + ports: + - "443:443" + - "80:80" + networks: + - cloud_bridge + environment: + #- CN_HOSTNAME=demoexample.jans.io + - CN_ADMIN_PASS=1t5Fin3#security + - CN_ORG_NAME=Janssen + - CN_EMAIL=support@jans.io + - CN_CITY=Austin + - CN_STATE=TX + - CN_COUNTRY=US + - CN_INSTALL_SPANNER=true + - CN_INSTALL_CONFIG_API=true + - CN_INSTALL_SCIM=true + - CN_INSTALL_FIDO2=true + - CN_INSTALL_CASA=true + - CN_INSTALL_KC_LINK=true + - CN_INSTALL_LOCK=true + - CN_INSTALL_SAML=false + - CN_INSTALL_OPA=true + - TEST_CLIENT_ID=9876baac-de39-4c23-8a78-674b59df8c09 + - TEST_CLIENT_TRUSTED=true + - TEST_CLIENT_SECRET=1t5Fin3#security + volumes: + - ./jans-auth-custom:/opt/jans/jetty/jans-auth/custom + - ./jans-config-api-custom:/opt/jans/jetty/jans-config-api/custom + - ./jans-fido2-custom:/opt/jans/jetty/jans-fido2/custom + - ./jans-scim-custom:/opt/jans/jetty/jans-scim/custom + - ./jans-auth-log:/opt/jans/jetty/jans-auth/logs + - ./jans-config-api-log:/opt/jans/jetty/jans-config-api/logs + - ./jans-scim-log:/opt/jans/jetty/jans-scim/logs + - ./jans-fido2-log:/opt/jans/jetty/jans-fido2/log +volumes: + db-data: +networks: + cloud_bridge: + driver: bridge diff --git a/docker-jans-monolith/scripts/entrypoint.sh b/docker-jans-monolith/scripts/entrypoint.sh index faa663a9ddc..93bec5c516d 100644 --- a/docker-jans-monolith/scripts/entrypoint.sh +++ b/docker-jans-monolith/scripts/entrypoint.sh @@ -65,9 +65,27 @@ install_jans() { echo "Installing with Postgres" echo "rdbm_type=pgsql" | tee -a setup.properties > /dev/null echo "rdbm_port=5432" | tee -a setup.properties > /dev/null + elif [[ "${CN_INSTALL_COUCHBASE}" == "true" ]]; then + echo "Installing with Couchbase" + echo "cb_install=2" | tee -a setup.properties > /dev/null + echo "cb_password=${COUCHBASE_PASSWORD}" | tee -a setup.properties > /dev/null + echo "couchbase_hostname=${COUCHBASE_HOSTNAME}" | tee -a setup.properties > /dev/null + echo "couchebaseClusterAdmin=${COUCHBASE_ADMIN}" | tee -a setup.properties > /dev/null + elif [[ "${CN_INSTALL_SPANNER}" == "true" ]]; then + echo "Installing with SPANNER" + echo "rdbm_type=spanner" | tee -a setup.properties > /dev/null + echo "rdbm_install_type=2" | tee -a setup.properties > /dev/null + echo "spanner_emulator_host=localhost" | tee -a setup.properties > /dev/null + echo "spanner_project=jans-project" | tee -a setup.properties > /dev/null + echo "spanner_instance=jans-instance" | tee -a setup.properties > /dev/null + echo "spanner_database=jansdb" | tee -a setup.properties > /dev/null + "$HOME"/google-cloud-sdk/bin/gcloud emulators spanner start --quiet & + gcloud config configurations create emulator + gcloud config set auth/disable_credentials true + gcloud config set project jans-project + gcloud config set api_endpoint_overrides/spanner http://localhost:9020/ fi - echo "***** Running the setup script for ${CN_ORG_NAME}!! *****" echo "***** PLEASE NOTE THAT THIS MAY TAKE A WHILE TO FINISH. PLEASE BE PATIENT!! *****" echo "Executing https://raw.githubusercontent.com/JanssenProject/jans/${JANS_SOURCE_VERSION}/jans-linux-setup/jans_setup/install.py > install.py" diff --git a/docker-jans-monolith/up.sh b/docker-jans-monolith/up.sh index 932fd8f8bd5..c071b29cf32 100644 --- a/docker-jans-monolith/up.sh +++ b/docker-jans-monolith/up.sh @@ -5,7 +5,7 @@ if [ -z "$1" ]; then yaml="jans-mysql-compose.yml" else case "$1" in - mysql|ldap|postgres) + mysql|ldap|postgres|couchbase|spanner) yaml="jans-${1}-compose.yml" ;; *) From 1ec68304c012f6e2451b18ed4c4166554aa178a7 Mon Sep 17 00:00:00 2001 From: Adam Albright Date: Mon, 22 Jul 2024 04:23:19 -0400 Subject: [PATCH 04/43] docs: correct spelling of properties (#8710) Signed-off-by: Rehket Co-authored-by: Dhaval D <343411+ossdhaval@users.noreply.github.com> --- docs/admin/planning/security-best-practices.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/admin/planning/security-best-practices.md b/docs/admin/planning/security-best-practices.md index 58e16e6c41c..be9b1b4403c 100644 --- a/docs/admin/planning/security-best-practices.md +++ b/docs/admin/planning/security-best-practices.md @@ -55,7 +55,7 @@ flow, Review the CORS filter configuration for Jans Auth Server. CORS restricts access to trusted domains to execute browser application requests to Auth Server endpoints. By default, the filter allows any RP to call OpenID endpoints. -Review Auth Server propoerties chosen for `sessionIdUnusedLifetime` and +Review Auth Server properties chosen for `sessionIdUnusedLifetime` and `sessionIdLifetime`. Long sessions present higher risks of session hijacking and unauthorized access from shared devices. From dfc910fc2650d899ef71f26a977f59f0088aabdf Mon Sep 17 00:00:00 2001 From: Michael Schwartz Date: Mon, 22 Jul 2024 03:29:54 -0500 Subject: [PATCH 05/43] docs: more Cedarling overview docs (#8996) * More Cedarling overview docs * fix(docs): add links to navigation Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> --------- Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> Co-authored-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> Co-authored-by: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> --- docs/admin/lock/README.md | 11 +- docs/admin/lock/cedarling.md | 147 +++++++++++++++++++++++ docs/admin/lock/lock-master.md | 22 ++++ docs/assets/lock-cedarling-diagram-1.jpg | Bin 0 -> 62314 bytes docs/assets/lock-cedarling-diagram-2.jpg | Bin 0 -> 53251 bytes docs/assets/lock-cedarling-diagram-3.jpg | Bin 0 -> 37221 bytes docs/assets/lock-cedarling-diagram-4.jpg | Bin 0 -> 96286 bytes mkdocs.yml | 2 + 8 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 docs/admin/lock/cedarling.md create mode 100644 docs/assets/lock-cedarling-diagram-1.jpg create mode 100644 docs/assets/lock-cedarling-diagram-2.jpg create mode 100644 docs/assets/lock-cedarling-diagram-3.jpg create mode 100644 docs/assets/lock-cedarling-diagram-4.jpg diff --git a/docs/admin/lock/README.md b/docs/admin/lock/README.md index bf39ab513c3..05ac2770a59 100644 --- a/docs/admin/lock/README.md +++ b/docs/admin/lock/README.md @@ -21,11 +21,12 @@ traditional access management strategies like RBAC and more adaptive capabilitie grain decisions based on contextal data. The Cedar Engine does this without sacrificing performance or the security benefits of a deterministic policy engine. -There are three key components in a Lock topology: (1) Cedarling--a WebAssembly ("WASM") -component that runs the [Amazon Rust Cedar Engine](https://github.com/cedar-policy/cedar) and -performs JWT token validation; (2) Lock Master--a web service deployed by domains to manage a -network of distributed ephemeral Cedarlings; (3) [Agama Lab](https://cloud.gluu.org/agama-lab), -a policy authoring tool for developers to design policies and publish their policy store in Github. +There are three key components in a Lock topology: (1) [Cedarling](./cedarling.md)--a WebAssembly +("WASM") component that runs the [Amazon Rust Cedar Engine](https://github.com/cedar-policy/cedar) and +performs JWT token validation; (2)[Lock Master](./lock-master.md)--a web service deployed by domains +to manage a network of distributed ephemeral Cedarlings; (3) +[Agama Lab](https://cloud.gluu.org/agama-lab), a policy authoring tool for developers to design +policies and publish their policy store in Github. You don't need to deploy a Jans Lock Topology to derive utility from the Cedarling. Javascript developers can use the Cedarling to secure even a single browser-based application, especially if diff --git a/docs/admin/lock/cedarling.md b/docs/admin/lock/cedarling.md new file mode 100644 index 00000000000..de5668f150b --- /dev/null +++ b/docs/admin/lock/cedarling.md @@ -0,0 +1,147 @@ +--- +tags: + - administration + - lock + - authorization / authz + - Cedar + - Cedarling +--- + +# Authorization Using Cedarling + +## What Is Cedarling + +The Cedarling is a local, autonomous Policy Decision Point, or "PDP". It runs as a local +WebAssembly ("WASM") component--you can call it directly in the browser from a JavaScript +function. With each authorization call, the Cedarling has all the policies and data it +needs to make a fast, local decision. The Cedarling's authorization function is *deterministic*. +The Cedarling always returns either `permit` or `forbid`. You will never get an error +indicating a network timeout, or a divide by zero error. It is also very fast. + +![](../../assets/lock-cedarling-diagram-1.jpg) + +In a JavaScript browser framework, the Cedarling loads its policy store during initialization, as a +static JSON file or fetched via REST. Developers may consider the Cedarling policy store as part of +the code. Having the policies in one file makes it easier to audit the security features and +controls of an application. It facilitates creation of complex contextual policies without +cluttering application code with lots of `if` - `then` statements. Importantly, the Cedarling creates an +audit log of all decisions by an application to allow or forbid actions. In an enterprise deployment, +this audit log is sent for central archiving. + +Where does the Cedarling get the data for policy evaluation? The data is contained in OAuth and +OpenID JWTs that are sent as part of the authorization request to the Cedarling. This makes sense, +because most modern applications rely on a federated identity provider or "IDP". The Cedarling assumes +the use of OAuth and OpenID Connect--sorry SAML geeks. + +![](../../assets/lock-cedarling-diagram-2.jpg) + +Two JWT tokens in particular are typical: (1) an OpenID Connect id_token and (2) an OAuth access +token. The Cedarling can trust the id_token and access token to extract the User, +Role and Client pricipals. The tokens also contain other interesting contextual data. An OpenID +Connect id_token JWT is a record of an authentication event that tells you who authenticated, when +they authenticated, how they authenticatated, and other claims like the subject's Roles. An OAuth +Access Token JWT can tell you information about the software that obtained the the JWT, its extent +of access as defined by the OAuth Authorization Server (*i.e.* the values of the `scope` claim), or +other claims--domains frequently enhance the access token to contain business specific data needed +for policy evaluation. + +The Cedarling, as its name suggests, enables you to define the security rules for your application +in [Cedar](https://www.cedarpolicy.com/en) policy syntax. Cedar was invented by Amazon for their +[Verified Permission](https://aws.amazon.com/verified-permissions/) service. It uses the **PARC** +syntax: **P**rincipal, **A**ction, **R**esource, **C**ontext. Principal-Action-Resource is typical +for most authorization solutions. For example, you may have a policy that says *Admins* can *write* +to the *config* folder. In this example, the *Admin* Role is the Principal, *write* is the Action, +and the *config* folder is the Resource. The Context is used to specify information about the +enivironment, like the time of day or network address. + +The Cedarling authorizes a person using a certain piece of software to do something. From +a logical perspective, `person_allowed AND client_allowed` must be `True`. While this seems pretty +simple, a person may be either explicitly allowed or have a role that enables access. For example, +`person_allowed` may be equal to `True` if `user=mike OR role=SuperUser`. + +![](../../assets/lock-cedarling-diagram-3.jpg) + +The Action, Resource and Context is sent by the application in the authorization request. This +is where developers need to map security in their application to actions and resources. For +example, in the diagram above the application may have a policy that restricts access to Button 21 +to users with the Admin Role during business hours when they are not using a VPN. + +## Cedarling Token Validation + +The Cedarling can validate the signatures of the JWTs for developers, by setting the `SIGNATURE_VALIDATION` +environment variable to `True`. For testing, developers can set this property to `False` and submit +an unsigned JWT. Or developers may prefer to validate the signatures in code. + +On initiatilization, the Cedarling downloads the public keys of the Trusted IDPs specified in the +Cedarling policy store. Because all JWT's have an `iss` claim, this is used to deterimne which keys +to use for token signature validation. + +In an enterprise deployment, the Cedarling can also check if a JWT has been revoked. The Cedarling +uses a mechanism described in the [OAuth Status Lists](https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/) +draft. This might be handy for use cases where a token revocation needs to be communicated +immediately, such as an account takeover situation, or an implementation of a one-time transactions +in a cluster of web servers. Jans Auth Server supports the [Global Token Revocation](https://datatracker.ietf.org/doc/draft-parecki-oauth-global-token-revocation/) OAuth draft. This is how a client can inform the OAuth Server that a given token should be revoked. + +![](../../assets/lock-cedarling-diagram-4.jpg) + +## Policy Authoring + +The eaisest way to author your policy store is to use the Policy Designer in [Agama Lab](https://cloud.gluu.org/agama-lab). This tool helps you define the policies, schema and trusted IDPs and +to publish a policy store to Github. + +## Testing the Cedarling + +To call the Cedarling from your JavaScript application + +``` +input = { + "access_token": ["..."], + "id_token": "...", + "userinfo_token": ["..."], + "tx_token": ["..."], + "resource": "ChessApp", + "action": "Execute", + "context": { + "ip_address": "54.9.21.201", + "network_type": "VPN", + "user_agent": "Chrome 125.0.6422.77 (Official Build) (arm64)", + "time": "1719266610.98636", + } + } + +decision_result = authz(input) + +``` + +## Cedarling Bootstrap Properties + +* **`APPLICATION_NAME`** : Human friendly identifier for this application + +* **`LOCK`** : Enabled | Disabled. If Enabled, the Cedarling will connect to the Lock Master for policies, and subscribe for SSE events. + +* **`POLICY_STORE_URI`** : Location of policy store JSON, used if policy store is not local, or retreived from Lock Master. + +* **`LOCK_MASTER_CONFIGURATION_URI`** : Required if `LOCK` == `Enabled`. URI where Cedarling can get JSON file with all required metadata about Lock Master, i.e. `.well-known/lock-master-configuration`. + +* **`LOCK_SSA_JWT`** : SSA for DCR in a Lock Master deployment. The Cedarling will validate this SSA JWT prior to DCR. + +* **`POLICY_STORE_ID`** : The identifier of the policy stored needed only for Lock Master deployments. + +* **`LOG_LEVEL`** : Controls the verbosity of Cedar logging. + +* **`AUDIT_LOG_INTERVAL`** : How often to send log messages to Lock Master (0 to turn off trasmission) + +* **`AUDIT_HEALTH_INTERVAL`** : How often to send health messages to Lock Master (0 to turn off transmission) + +* **`AUDIT_TELEMETRY_INTERVAL`** : How often to send telemetry messages to Lock Master (0 to turn off transmission) + +* **`DYNAMIC_CONFIGURATION`** : Enabled | Disabled, controls whether Cedarling should listen for SSE config updates + +* **`GET_TOKEN_STATUS_LIST_UPDATES`** : Whether the Cedarling should register for SSE updates for Lock Master deployments. + +* **`SIGNATURE_ALGORITHMS_SUPPORTED`** : .... + +* **`SIGNATURE_VALIDATION`** : Enabled | Disabled + +* **`REQUIRE_AUD_VALIDATION`** : Enabled | Disabled. Controls if Cedarling will discard id_token without an access token with the corresponding client_id. + diff --git a/docs/admin/lock/lock-master.md b/docs/admin/lock/lock-master.md index 5f00f673e7f..75680f16c48 100644 --- a/docs/admin/lock/lock-master.md +++ b/docs/admin/lock/lock-master.md @@ -1,2 +1,24 @@ +--- +tags: + - administration + - lock + - authorization / authz + - Cedar + - Cedarling +--- + # Lock Master +## Jans Lock Overview + +This content is in progress + +The Janssen Project documentation is currently in development. Topic pages are being created in order of broadest relevance, and this page is coming in the near future. + +## Have questions in the meantime? + +While this documentation is in progress, you can ask questions through [GitHub Discussions](https://github.com/JanssenProject/jans/discussions) or the [community chat on Gitter](https://gitter.im/JanssenProject/Lobby). Any questions you have will help determine what information our documentation should cover. + +## Want to contribute? + +If you have content you'd like to contribute to this page in the meantime, you can get started with our [Contribution guide](https://docs.jans.io/head/CONTRIBUTING/). \ No newline at end of file diff --git a/docs/assets/lock-cedarling-diagram-1.jpg b/docs/assets/lock-cedarling-diagram-1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..93a9520c6630d365722c20b28445cceb034ffcc9 GIT binary patch literal 62314 zcmeFZ1yozz)-D_hMM|+!TwA0#g%T`KN^vMq+)Hr@?pi3cKyjydad%0AJH=fS+=>L( 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 literal 0 HcmV?d00001 diff --git a/docs/assets/lock-cedarling-diagram-2.jpg b/docs/assets/lock-cedarling-diagram-2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c8e808c5750ef4b7fbbfd548e0ec2b71addaabff GIT binary patch literal 53251 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ⅈA`` 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%0AZDTE-MzcIex_|F??U8uZ*%5Cn4P3FS_cFM_%Tc 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 G{7d>@KD^^A($6Af!?({0Nx;u<% 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�a8!`Y_0()LoD+8pt=YT6x*v`HJ*Lh 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 B2A2Q; literal 0 HcmV?d00001 diff --git a/mkdocs.yml b/mkdocs.yml index a9a441eea04..63e7e7ec4fb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -400,6 +400,8 @@ nav: - Lock Client Configuration: admin/lock/lock_client_config.md - Implement Lock PDP Plugin: admin/lock/lock_pdp_plugin.md - Policy Store Integration: admin/lock/lock_opa.md + - Lock Master: admin/lock/lock-master.md + - Authorization Using Cedarling: admin/lock/cedarling.md - Janssen Recipes: - admin/recipes/README.md - Benchmark: admin/recipes/benchmark.md From 1bde1316a3abc8f4e48462d0a2670db901c70aca Mon Sep 17 00:00:00 2001 From: Devrim Date: Mon, 22 Jul 2024 12:07:55 +0300 Subject: [PATCH 06/43] fix(jans-linux-setup): remove/rename old variable/file names (#8991) Signed-off-by: Mustafa Baser --- jans-linux-setup/docs/README.md | 10 +- jans-linux-setup/jans_setup/jans_setup.py | 18 +- .../schema/jans_schema_mappings.json | 4 +- .../jans_setup/setup.properties.sample | 3 +- .../jans_setup/setup_app/config.py | 32 ++-- .../setup_app/installers/config_api.py | 8 +- .../jans_setup/setup_app/installers/fido.py | 4 +- .../jans_setup/setup_app/installers/httpd.py | 2 +- .../jans_setup/setup_app/installers/jans.py | 26 +-- .../setup_app/installers/jans_auth.py | 40 ++--- .../setup_app/installers/jans_casa.py | 2 +- .../installers/jans_keycloak_link.py | 2 +- .../setup_app/installers/jans_link.py | 2 +- .../setup_app/installers/jans_lock.py | 4 +- .../setup_app/installers/jans_saml.py | 4 +- .../jans_setup/setup_app/installers/jetty.py | 8 +- .../jans_setup/setup_app/installers/jre.py | 2 +- .../jans_setup/setup_app/installers/jython.py | 2 +- .../jans_setup/setup_app/installers/oxd.py | 169 ------------------ .../jans_setup/setup_app/installers/scim.py | 2 +- .../jans_setup/setup_app/messages.py | 41 ++--- .../jans_setup/setup_app/setup_options.py | 13 +- .../jans_setup/setup_app/test_data_loader.py | 78 ++++---- .../jans_setup/setup_app/utils/arg_parser.py | 3 +- .../setup_app/utils/collect_properties.py | 59 +++--- .../jans_setup/setup_app/utils/crypto64.py | 26 +-- .../jans_setup/setup_app/utils/db_utils.py | 30 ++-- .../setup_app/utils/properties_utils.py | 97 ++-------- .../jans_setup/static/oxd/oxd-server.default | 26 --- .../jans_setup/static/scripts/renew_certs.py | 2 +- .../templates/jans-auth/configuration.ldif | 8 +- .../templates/jans-auth/jans-auth-config.json | 4 +- ...g-update.md => jans-auth-config-update.md} | 8 +- ... => config-jans-auth-test-data.properties} | 4 +- ...ser.ldif => jans-auth-test-data-user.ldif} | 0 ...est-data.ldif => jans-auth-test-data.ldif} | 18 +- ...auth_test.ldif => 102-jans-auth_test.ldif} | 0 .../{oxauth_index.txt => jans-auth_index.txt} | 0 ...-jans-auth-test-couchbase.properties.nrnd} | 0 ... => config-jans-auth-test-data.properties} | 4 +- ...onfig-jans-auth-test-ldap.properties.nrnd} | 0 ...ig-jans-auth-test-spanner.properties.nrnd} | 0 ...config-jans-auth-test-sql.properties.nrnd} | 0 ...properties => config-jans-auth.properties} | 0 .../jans_setup/tests/sample1.properties | 11 +- .../jans_setup/tests/sample2.properties | 11 +- .../jans_setup/tests/sample3.properties | 11 +- .../jans_setup/tests/test_functions.py | 21 +-- .../jans_setup/tests/test_setup.py | 44 ++--- jans-linux-setup/tools/key_regeneration.py | 2 +- 50 files changed, 257 insertions(+), 608 deletions(-) delete mode 100644 jans-linux-setup/jans_setup/setup_app/installers/oxd.py delete mode 100644 jans-linux-setup/jans_setup/static/oxd/oxd-server.default rename jans-linux-setup/jans_setup/templates/test/docs/{oxauth-config-update.md => jans-auth-config-update.md} (94%) rename jans-linux-setup/jans_setup/templates/test/jans-auth/client/{config-oxauth-test-data.properties => config-jans-auth-test-data.properties} (87%) rename jans-linux-setup/jans_setup/templates/test/jans-auth/data/{oxauth-test-data-user.ldif => jans-auth-test-data-user.ldif} (100%) rename jans-linux-setup/jans_setup/templates/test/jans-auth/data/{oxauth-test-data.ldif => jans-auth-test-data.ldif} (95%) rename jans-linux-setup/jans_setup/templates/test/jans-auth/schema/{102-oxauth_test.ldif => 102-jans-auth_test.ldif} (100%) rename jans-linux-setup/jans_setup/templates/test/jans-auth/schema/{oxauth_index.txt => jans-auth_index.txt} (100%) rename jans-linux-setup/jans_setup/templates/test/jans-auth/server/{config-oxauth-test-couchbase.properties.nrnd => config-jans-auth-test-couchbase.properties.nrnd} (100%) rename jans-linux-setup/jans_setup/templates/test/jans-auth/server/{config-oxauth-test-data.properties => config-jans-auth-test-data.properties} (89%) rename jans-linux-setup/jans_setup/templates/test/jans-auth/server/{config-oxauth-test-ldap.properties.nrnd => config-jans-auth-test-ldap.properties.nrnd} (100%) rename jans-linux-setup/jans_setup/templates/test/jans-auth/server/{config-oxauth-test-spanner.properties.nrnd => config-jans-auth-test-spanner.properties.nrnd} (100%) rename jans-linux-setup/jans_setup/templates/test/jans-auth/server/{config-oxauth-test-sql.properties.nrnd => config-jans-auth-test-sql.properties.nrnd} (100%) rename jans-linux-setup/jans_setup/templates/test/jans-auth/server/{config-oxauth.properties => config-jans-auth.properties} (100%) diff --git a/jans-linux-setup/docs/README.md b/jans-linux-setup/docs/README.md index c4d03dfea3f..fa98316584b 100644 --- a/jans-linux-setup/docs/README.md +++ b/jans-linux-setup/docs/README.md @@ -48,7 +48,7 @@ All files related to SetupApp are saved under this directory and subdirectories. variables are defined as class variables, so the can be accesible without any construction. You need to call `init()` static method with argument installaltion directory. Since we don't create varbiles related to services/applications unless they are installed, you can query a variable with static method `get()` by providing variable name and default value if you need. - For example if you code `Config.get('installJans', False)`, this will return value of variable `installJans` if defined, + For example if you code `Config.get('install_jans', False)`, this will return value of variable `install_jans` if defined, otherwise returns `False`. This is the third file you need to import if you are going to write another application and sould be constructed by `init()`. You won't need initialize this class. @@ -85,8 +85,8 @@ Collection of utilities used by SetupApp. - `import_lidf(ldif_files)`: imports to list of ldif files to database. It automatically determines database location (ldap or couchbase) and which bucket to import according to `dn` - - `set_oxAuthConfDynamic(entries)`: takes entries as dctionary and updates `oxAuthConfDynamic` in database - - `set_oxAuthConfDynamic(entries)`: the same for `oxAuthConfDynamic` + - `set_jans_auth_conf_dynamic(entries)`: takes entries as dctionary and updates `jans_auth_conf_dynamic` in database + - `set_jans_auth_conf_dynamic(entries)`: the same for `jans_auth_conf_dynamic` - `enable_script(inum)`: enables script - `enable_service(service)`: enables jans service in oxtrust ui - `add_client2script(inum)`: adds client to script's allowed client property @@ -120,7 +120,7 @@ desciribng base installers So include these code here, - `render_import_templates()`: template renderings are done under this function, then imported to database - `update_backend()`: if you need to update database (other than importing templates) write here. For example enabling script, - updating oxauth and/or oxtrust configurations etc. + updating jans_auth and/or other service configurations etc. This module also contains start/stop/restart/enable linux services. @@ -250,7 +250,7 @@ class SampleInstaller(JettyInstaller): # this is the first function called automatically after binding database # deploy jetty application - self.installJettyService(self.jetty_app_configuration[self.service_name], True) + self.install_jettyService(self.jetty_app_configuration[self.service_name], True) jettyServiceWebapps = os.path.join(self.jetty_base, self.service_name, 'webapps') src_war = os.path.join(Config.distGluuFolder, 'app.war') self.copyFile(src_war, jettyServiceWebapps) diff --git a/jans-linux-setup/jans_setup/jans_setup.py b/jans-linux-setup/jans_setup/jans_setup.py index b1ee6967a10..ce4fbb60156 100755 --- a/jans-linux-setup/jans_setup/jans_setup.py +++ b/jans-linux-setup/jans_setup/jans_setup.py @@ -151,8 +151,6 @@ def ami_packaged(): from setup_app.installers.config_api import ConfigApiInstaller from setup_app.installers.jans_cli import JansCliInstaller from setup_app.installers.rdbm import RDBMInstaller -# from setup_app.installers.oxd import OxdInstaller - if base.snap: try: @@ -272,8 +270,6 @@ def ami_packaged(): jansCliInstaller = JansCliInstaller() -# oxdInstaller = OxdInstaller() - rdbmInstaller.packageUtils = packageUtils if not Config.installed_instance: @@ -422,12 +418,12 @@ def do_installation(): jansInstaller.order_services() - if (Config.installed_instance and 'installHttpd' in Config.addPostSetupService) or ( - not Config.installed_instance and Config.installHttpd): + if (Config.installed_instance and 'install_httpd' in Config.addPostSetupService) or ( + not Config.installed_instance and Config.install_httpd): httpdinstaller.configure() - if (Config.installed_instance and 'installOxAuth' in Config.addPostSetupService) or ( - not Config.installed_instance and Config.installOxAuth): + if (Config.installed_instance and 'install_jans_auth' in Config.addPostSetupService) or ( + not Config.installed_instance and Config.install_jans_auth): jansAuthInstaller.start_installation() if (Config.installed_instance and configApiInstaller.install_var in Config.addPostSetupService) or ( @@ -438,8 +434,8 @@ def do_installation(): if Config.profile == 'jans': - if (Config.installed_instance and 'installFido2' in Config.addPostSetupService) or ( - not Config.installed_instance and Config.installFido2): + if (Config.installed_instance and 'install_fido2' in Config.addPostSetupService) or ( + not Config.installed_instance and Config.install_fido2): fidoInstaller.start_installation() if (Config.installed_instance and 'install_scim_server' in Config.addPostSetupService) or ( @@ -472,8 +468,6 @@ def do_installation(): not Config.installed_instance and Config.get(jans_lock_installer.install_var)): jans_lock_installer.start_installation() - # if (Config.installed_instance and 'installOxd' in Config.addPostSetupService) or (not Config.installed_instance and Config.installOxd): - # oxdInstaller.start_installation() jansInstaller.post_install_before_saving_properties() jansProgress.progress(PostSetup.service_name, "Saving properties") propertiesUtils.save_properties() diff --git a/jans-linux-setup/jans_setup/schema/jans_schema_mappings.json b/jans-linux-setup/jans_setup/schema/jans_schema_mappings.json index 3f217b3f331..7c356ed1d2b 100644 --- a/jans-linux-setup/jans_setup/schema/jans_schema_mappings.json +++ b/jans-linux-setup/jans_setup/schema/jans_schema_mappings.json @@ -204,7 +204,7 @@ "oxAuthClientSecret": "jansClntSecret", "oxAuthClientSecretExpiresAt": "jansClntSecretExpAt", "oxAuthClientURI": "jansClntURI", - "oxAuthConfDynamic": "jansConfDyn", + "jans_auth_conf_dynamic": "jansConfDyn", "oxAuthConfErrors": "jansConfErrors", "oxAuthConfStatic": "jansConfStatic", "oxAuthConfWebKeys": "jansConfWebKeys", @@ -665,7 +665,7 @@ "oxAuthClientSecret": "jansClntSecret", "oxAuthClientSecretExpiresAt": "jansClntSecretExpAt", "oxAuthClientURI": "jansClntURI", - "oxAuthConfDynamic": "jansConfDyn", + "jans_auth_conf_dynamic": "jansConfDyn", "oxAuthConfErrors": "jansConfErrors", "oxAuthConfStatic": "jansConfStatic", "oxAuthConfWebKeys": "jansConfWebKeys", diff --git a/jans-linux-setup/jans_setup/setup.properties.sample b/jans-linux-setup/jans_setup/setup.properties.sample index ca8f0cb8dfb..0d2c1031da9 100644 --- a/jans-linux-setup/jans_setup/setup.properties.sample +++ b/jans-linux-setup/jans_setup/setup.properties.sample @@ -58,13 +58,12 @@ application_max_ram= install_config_api= ### If you want to install Fido2 Server, set this to True -installFido2= +install_fido2= ### If you want to install Scim Server, set this to True install_scim_server= ### To use remote MySQL as backend -# installLdap=False # rdbm_install=2 # rdbm_db = gluudb # rdbm_user = gluu diff --git a/jans-linux-setup/jans_setup/setup_app/config.py b/jans-linux-setup/jans_setup/setup_app/config.py index d2d7b9fcf68..ef478926d75 100644 --- a/jans-linux-setup/jans_setup/setup_app/config.py +++ b/jans-linux-setup/jans_setup/setup_app/config.py @@ -132,11 +132,10 @@ def progress(self, service_name, msg, incr=False): self.downloadWars = None self.templateRenderingDict = { - 'oxauthClient_2_inum': 'AB77-1A2B', - 'oxauthClient_3_inum': '3E20', - 'oxauthClient_4_inum': 'FF81-2D39', + 'jans_auth_client_2_inum': 'AB77-1A2B', + 'jans_auth_client_3_inum': '3E20', + 'jans_auth_client_4_inum': 'FF81-2D39', 'idp_attribute_resolver_ldap.search_filter': '(|(uid=$requestContext.principalName)(mail=$requestContext.principalName))', - 'oxd_port': '8443', 'server_time_zone': 'UTC' + time.strftime("%z"), } @@ -191,21 +190,16 @@ def progress(self, service_name, msg, incr=False): # Jans components installation status self.loadData = True - self.installJans = True - self.installJre = True - self.installJetty = True - self.installJython = True - self.installOxAuth = True - self.installOxTrust = True - self.installHttpd = True - self.installSaml = False - self.installPassport = False - self.installJansRadius = False + self.install_jans = True + self.install_jre = True + self.install_jetty = True + self.install_jython = True + self.install_jans_auth = True + self.install_httpd = True self.install_scim_server = True - self.installFido2 = True + self.install_fido2 = True self.install_config_api = True self.install_casa = False - self.installOxd = False self.install_jans_cli = True self.install_jans_link = False self.loadTestData = False @@ -263,8 +257,6 @@ def progress(self, service_name, msg, incr=False): self.ldapCertFn = self.opendj_cert_fn = os.path.join(self.certFolder, 'opendj.crt') self.ldapTrustStoreFn = self.opendj_p12_fn = os.path.join(self.certFolder, 'opendj.pkcs12') - self.oxd_package = base.determine_package(os.path.join(self.dist_jans_dir, 'oxd-server*.tgz')) - self.opendj_p12_pass = None self.ldap_binddn = 'cn=directory manager' @@ -365,10 +357,8 @@ def progress(self, service_name, msg, incr=False): 'jans-scim': ['opendj jans-auth', 75], 'idp': ['opendj jans-auth', 76], 'casa': ['opendj jans-auth', 78], - 'oxd-server': ['opendj jans-auth', 80], 'passport': ['opendj jans-auth', 82], 'jans-auth-rp': ['opendj jans-auth', 84], - 'jans-radius': ['opendj jans-auth', 86], 'jans-config-api': ['opendj jans-auth', 85], } @@ -424,7 +414,7 @@ def progress(self, service_name, msg, incr=False): self.mapping_locations = { group: 'rdbm' for group in self.couchbaseBucketDict } self.non_setup_properties = { - 'oxauth_client_jar_fn': os.path.join(self.dist_jans_dir, 'jans-auth-client-jar-with-dependencies.jar') + 'jans_auth_client_jar_fn': os.path.join(self.dist_jans_dir, 'jans-auth-client-jar-with-dependencies.jar') } Config.addPostSetupService = [] diff --git a/jans-linux-setup/jans_setup/setup_app/installers/config_api.py b/jans-linux-setup/jans_setup/setup_app/installers/config_api.py index a4efe224ea2..e3f1c327f77 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/config_api.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/config_api.py @@ -54,7 +54,7 @@ def __init__(self): def install(self): self.copyFile(self.source_files[1][0], '/usr/sbin') self.run([paths.cmd_chmod, '+x', '/usr/sbin/facter']) - self.installJettyService(self.jetty_app_configuration[self.service_name], True) + self.install_jettyService(self.jetty_app_configuration[self.service_name], True) self.logIt("Copying fido.war into jetty webapps folder...") jettyServiceWebapps = os.path.join(self.jetty_base, self.service_name, 'webapps') self.copyFile(self.source_files[0][0], jettyServiceWebapps) @@ -64,7 +64,7 @@ def install(self): if Config.install_scim_server: self.install_plugin('scim-plugin') - if Config.installFido2: + if Config.install_fido2: self.install_plugin('fido2-plugin') if Config.install_jans_link: @@ -177,9 +177,9 @@ def render_import_templates(self): Config.templateRenderingDict['configOauthEnabled'] = 'false' if base.argsp.disable_config_api_security else 'true' Config.templateRenderingDict['apiApprovedIssuer'] = base.argsp.approved_issuer or 'https://{}'.format(Config.hostname) - _, oxauth_config = self.dbUtils.get_oxAuthConfDynamic() + _, jans_auth_config = self.dbUtils.get_jans_auth_conf_dynamic() for param in ('issuer', 'openIdConfigurationEndpoint', 'introspectionEndpoint', 'tokenEndpoint', 'tokenRevocationEndpoint'): - Config.templateRenderingDict[param] = oxauth_config[param] + Config.templateRenderingDict[param] = jans_auth_config[param] Config.templateRenderingDict['apiProtectionType'] = 'oauth2' Config.templateRenderingDict['endpointInjectionEnabled'] = 'false' diff --git a/jans-linux-setup/jans_setup/setup_app/installers/fido.py b/jans-linux-setup/jans_setup/setup_app/installers/fido.py index 2164868c91c..42f5ea5bc06 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/fido.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/fido.py @@ -24,7 +24,7 @@ def __init__(self): self.needdb = True self.app_type = AppType.SERVICE self.install_type = InstallOption.OPTONAL - self.install_var = 'installFido2' + self.install_var = 'install_fido2' self.register_progess() self.fido2ConfigFolder = os.path.join(Config.configFolder, 'fido2') @@ -37,7 +37,7 @@ def __init__(self): def install(self): - self.installJettyService(self.jetty_app_configuration[self.service_name], True) + self.install_jettyService(self.jetty_app_configuration[self.service_name], True) self.logIt("Copying fido.war into jetty webapps folder...") jettyServiceWebapps = os.path.join(self.jetty_base, self.service_name, 'webapps') diff --git a/jans-linux-setup/jans_setup/setup_app/installers/httpd.py b/jans-linux-setup/jans_setup/setup_app/installers/httpd.py index b4cc062c189..1c52a349ce3 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/httpd.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/httpd.py @@ -17,7 +17,7 @@ def __init__(self): self.pbar_text = "Configuring " + base.httpd_name self.app_type = AppType.SERVICE self.install_type = InstallOption.OPTONAL - self.install_var = 'installHttpd' + self.install_var = 'install_httpd' self.register_progess() self.needdb = False # we don't need backend connection in this class diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans.py b/jans-linux-setup/jans_setup/setup_app/installers/jans.py index 34235e48497..5c1d236dd72 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans.py @@ -24,7 +24,7 @@ class JansInstaller(BaseInstaller, SetupUtils): - install_var = 'installJans' + install_var = 'install_jans' def __repr__(self): setattr(base.current_app, self.__class__.__name__, self) @@ -73,12 +73,12 @@ def get_install_string(prefix, install_var): return '' return prefix.ljust(30) + repr(getattr(Config, install_var, False)).rjust(35) + (' *' if install_var in Config.addPostSetupService else '') + '\n' - txt += get_install_string('Install Apache 2 web server', 'installHttpd') - txt += get_install_string('Install Auth Server', 'installOxAuth') + txt += get_install_string('Install Apache 2 web server', 'install_httpd') + txt += get_install_string('Install Auth Server', 'install_jans_auth') txt += get_install_string('Install Jans Config API', 'install_config_api') if Config.profile == 'jans': for prompt_str, install_var in ( - ('Install Fido2 Server', 'installFido2'), + ('Install Fido2 Server', 'install_fido2'), ('Install Scim Server', 'install_scim_server'), ('Install Jans Link Server', 'install_jans_link'), ('Install Jans KC Link Server', 'install_jans_keycloak_link'), @@ -117,15 +117,15 @@ def initialize(self): sys.exit(1) #Download jans-auth-client-jar-with-dependencies - if not os.path.exists(Config.non_setup_properties['oxauth_client_jar_fn']): - oxauth_client_jar_url = os.path.join(base.current_app.app_info['JANS_MAVEN'], 'maven/io/jans/jans-auth-client/{0}/jans-auth-client-{0}-jar-with-dependencies.jar').format(base.current_app.app_info['jans_version']) - self.logIt("Downloading {}".format(os.path.basename(oxauth_client_jar_url))) - base.download(oxauth_client_jar_url, Config.non_setup_properties['oxauth_client_jar_fn']) + if not os.path.exists(Config.non_setup_properties['jans_auth_client_jar_fn']): + jans_auth_client_jar_url = os.path.join(base.current_app.app_info['JANS_MAVEN'], 'maven/io/jans/jans-auth-client/{0}/jans-auth-client-{0}-jar-with-dependencies.jar').format(base.current_app.app_info['jans_version']) + self.logIt("Downloading {}".format(os.path.basename(jans_auth_client_jar_url))) + base.download(jans_auth_client_jar_url, Config.non_setup_properties['jans_auth_client_jar_fn']) self.logIt("Determining key generator path") - oxauth_client_jar_zf = zipfile.ZipFile(Config.non_setup_properties['oxauth_client_jar_fn']) + jans_auth_client_jar_zf = zipfile.ZipFile(Config.non_setup_properties['jans_auth_client_jar_fn']) - for f in oxauth_client_jar_zf.namelist(): + for f in jans_auth_client_jar_zf.namelist(): if os.path.basename(f) == 'KeyGenerator.class': p, e = os.path.splitext(f) Config.non_setup_properties['key_gen_path'] = p.replace(os.path.sep, '.') @@ -134,7 +134,7 @@ def initialize(self): Config.non_setup_properties['key_export_path'] = p.replace(os.path.sep, '.') if (not 'key_gen_path' in Config.non_setup_properties) or (not 'key_export_path' in Config.non_setup_properties): - self.logIt("Can't determine key generator and/or key exporter path form {}".format(Config.non_setup_properties['oxauth_client_jar_fn']), True, True) + self.logIt("Can't determine key generator and/or key exporter path form {}".format(Config.non_setup_properties['jans_auth_client_jar_fn']), True, True) else: self.logIt("Key generator path was determined as {}".format(Config.non_setup_properties['key_export_path'])) @@ -646,10 +646,10 @@ def generate_smtp_config(self): def order_services(self): service_list = [ - ('jans-auth', 'installOxAuth'), + ('jans-auth', 'install_jans_auth'), ('jans-config-api', 'install_config_api'), ('jans-casa', 'install_casa'), - ('jans-fido2', 'installFido2'), + ('jans-fido2', 'install_fido2'), ('jans-link', 'install_jans_link'), ('jans-scim', 'install_scim_server'), ('jans-lock', 'install_jans_lock_as_server'), diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans_auth.py b/jans-linux-setup/jans_setup/setup_app/installers/jans_auth.py index 7c555a050fe..afd4fe1d9d2 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans_auth.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans_auth.py @@ -31,7 +31,7 @@ def __init__(self): self.service_name = 'jans-auth' self.app_type = AppType.SERVICE self.install_type = InstallOption.OPTONAL - self.install_var = 'installOxAuth' + self.install_var = 'install_jans_auth' self.register_progess() self.templates_folder = os.path.join(Config.templateFolder, self.service_name) @@ -39,11 +39,11 @@ def __init__(self): self.ldif_config = os.path.join(self.output_folder, 'configuration.ldif') self.ldif_role_scope_mappings = os.path.join(self.output_folder, 'role-scope-mappings.ldif') - self.oxauth_config_json = os.path.join(self.output_folder, 'jans-auth-config.json') - self.oxauth_static_conf_json = os.path.join(self.templates_folder, 'jans-auth-static-conf.json') - self.oxauth_error_json = os.path.join(self.templates_folder, 'jans-auth-errors.json') - self.oxauth_openid_jwks_fn = os.path.join(self.output_folder, 'jans-auth-keys.json') - self.oxauth_openid_jks_fn = os.path.join(Config.certFolder, 'jans-auth-keys.' + Config.default_store_type.lower()) + self.jans_auth_config_json = os.path.join(self.output_folder, 'jans-auth-config.json') + self.jans_auth_static_conf_json = os.path.join(self.templates_folder, 'jans-auth-static-conf.json') + self.jans_auth_error_json = os.path.join(self.templates_folder, 'jans-auth-errors.json') + self.jans_auth_openid_jwks_fn = os.path.join(self.output_folder, 'jans-auth-keys.json') + self.jans_auth_openid_jks_fn = os.path.join(Config.certFolder, 'jans-auth-keys.' + Config.default_store_type.lower()) self.ldif_people = os.path.join(self.output_folder, 'people.ldif') self.ldif_groups = os.path.join(self.output_folder, 'groups.ldif') self.agama_root = os.path.join(self.jetty_base, self.service_name, 'agama') @@ -56,7 +56,7 @@ def __init__(self): def install(self): self.logIt("Copying auth.war into jetty webapps folder...") self.make_pairwise_calculation_salt() - self.installJettyService(self.jetty_app_configuration[self.service_name], True) + self.install_jettyService(self.jetty_app_configuration[self.service_name], True) self.copyFile(self.source_files[0][0], self.jetty_service_webapps) self.external_libs() self.set_class_path([os.path.join(self.custom_lib_dir, '*')]) @@ -64,8 +64,8 @@ def install(self): self.enable() def generate_configuration(self): - if not Config.get('oxauth_openid_jks_pass'): - Config.oxauth_openid_jks_pass = self.getPW() + if not Config.get('jans_auth_openid_jks_pass'): + Config.jans_auth_openid_jks_pass = self.getPW() if not Config.get('admin_inum'): Config.admin_inum = str(uuid.uuid4()) @@ -75,13 +75,13 @@ def generate_configuration(self): self.logIt("Generating OAuth openid keys", pbar=self.service_name) jwks = self.gen_openid_jwks_jks_keys( - jks_path=self.oxauth_openid_jks_fn, - jks_pwd=Config.oxauth_openid_jks_pass, + jks_path=self.jans_auth_openid_jks_fn, + jks_pwd=Config.jans_auth_openid_jks_pass, key_expiration=2, key_algs=Config.default_sig_key_algs, enc_keys=Config.default_enc_key_algs ) - self.write_openid_keys(self.oxauth_openid_jwks_fn, jwks) + self.write_openid_keys(self.jans_auth_openid_jwks_fn, jwks) if Config.get('use_external_key'): self.import_openbanking_key() @@ -125,7 +125,7 @@ def render_import_templates(self): Config.templateRenderingDict['person_custom_object_class_list'] = '[]' if Config.mapping_locations['default'] == 'rdbm' else '["jansCustomPerson", "jansPerson"]' - templates = [self.oxauth_config_json, self.ldif_people, self.ldif_groups] + templates = [self.jans_auth_config_json, self.ldif_people, self.ldif_groups] for tmp in templates: self.renderTemplateInOut(tmp, self.templates_folder, self.output_folder) @@ -139,10 +139,10 @@ def render_import_templates(self): self.prepare_base64_extension_scripts() - Config.templateRenderingDict['oxauth_config_base64'] = self.generate_base64_ldap_file(self.oxauth_config_json) - Config.templateRenderingDict['oxauth_static_conf_base64'] = self.generate_base64_ldap_file(self.oxauth_static_conf_json) - Config.templateRenderingDict['oxauth_error_base64'] = self.generate_base64_ldap_file(self.oxauth_error_json) - Config.templateRenderingDict['oxauth_openid_key_base64'] = self.generate_base64_ldap_file(self.oxauth_openid_jwks_fn) + Config.templateRenderingDict['jans_auth_config_base64'] = self.generate_base64_ldap_file(self.jans_auth_config_json) + Config.templateRenderingDict['jans_auth_static_conf_base64'] = self.generate_base64_ldap_file(self.jans_auth_static_conf_json) + Config.templateRenderingDict['jans_auth_error_base64'] = self.generate_base64_ldap_file(self.jans_auth_error_json) + Config.templateRenderingDict['jans_auth_openid_key_base64'] = self.generate_base64_ldap_file(self.jans_auth_openid_jwks_fn) self.ldif_scripts = os.path.join(Config.output_dir, 'scripts.ldif') self.renderTemplateInOut(self.ldif_scripts, Config.templateFolder, Config.output_dir) @@ -174,8 +174,8 @@ def copy_static(self): def import_openbanking_certificate(self): self.logIt("Importing openbanking ssl certificate") - oxauth_config_json = base.readJsonFile(self.oxauth_config_json) - jwks_uri = oxauth_config_json['jwksUri'] + jans_auth_config_json = base.readJsonFile(self.jans_auth_config_json) + jwks_uri = jans_auth_config_json['jwksUri'] o = urlparse(jwks_uri) jwks_addr = o.netloc open_banking_cert = self.get_server_certificate(jwks_addr) @@ -194,7 +194,7 @@ def import_openbanking_key(self): self.download_ob_cert(Config.ob_cert_fn) if os.path.isfile(Config.ob_key_fn) and os.path.isfile(Config.ob_cert_fn): - self.import_key_cert_into_keystore('obsigning', self.oxauth_openid_jks_fn, Config.oxauth_openid_jks_pass, Config.ob_key_fn, Config.ob_cert_fn, Config.ob_alias) + self.import_key_cert_into_keystore('obsigning', self.jans_auth_openid_jks_fn, Config.jans_auth_openid_jks_pass, Config.ob_key_fn, Config.ob_cert_fn, Config.ob_alias) def external_libs(self): extra_libs = [] diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans_casa.py b/jans-linux-setup/jans_setup/setup_app/installers/jans_casa.py index 1761513b89f..caf07db7c0d 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans_casa.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans_casa.py @@ -51,7 +51,7 @@ def __init__(self): def install(self): - self.installJettyService(self.jetty_app_configuration[self.service_name], True) + self.install_jettyService(self.jetty_app_configuration[self.service_name], True) self.copyFile(self.source_files[0][0], self.jetty_service_webapps) base.extract_subdir(base.current_app.jans_zip, 'jans-casa/extras', self.pylib_dir) diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans_keycloak_link.py b/jans-linux-setup/jans_setup/setup_app/installers/jans_keycloak_link.py index 4f642ed7d95..9961ed6dc59 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans_keycloak_link.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans_keycloak_link.py @@ -35,7 +35,7 @@ def __init__(self): self.snapshots_dir = os.path.join(self.vendor_dir, 'keycloak-link-snapshots') def install(self): - self.installJettyService(self.jetty_app_configuration[self.service_name], True) + self.install_jettyService(self.jetty_app_configuration[self.service_name], True) self.copyFile(self.source_files[0][0], self.jetty_service_webapps) base.current_app.ConfigApiInstaller.source_files.append(self.source_files[1]) base.current_app.ConfigApiInstaller.install_plugin('kc-link-plugin') diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans_link.py b/jans-linux-setup/jans_setup/setup_app/installers/jans_link.py index 256c0d7f9c3..500638c347e 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans_link.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans_link.py @@ -34,7 +34,7 @@ def __init__(self): self.snapshots_dir = os.path.join(self.vendor_dir, 'link-snapshots') def install(self): - self.installJettyService(self.jetty_app_configuration[self.service_name], True) + self.install_jettyService(self.jetty_app_configuration[self.service_name], True) self.copyFile(self.source_files[0][0], self.jetty_service_webapps) if Config.installed_instance and Config.install_config_api: diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans_lock.py b/jans-linux-setup/jans_setup/setup_app/installers/jans_lock.py index 5c1243425b7..c7e7a8b5a4e 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans_lock.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans_lock.py @@ -62,7 +62,7 @@ def install(self): self.install_opa() if Config.persistence_type == 'sql' and Config.rdbm_type == 'pgsql': - self.dbUtils.set_oxAuthConfDynamic({'lockMessageConfig': {'enableTokenMessages': True, 'tokenMessagesChannel': 'jans_token'}}) + self.dbUtils.set_jans_auth_conf_dynamic({'lockMessageConfig': {'enableTokenMessages': True, 'tokenMessagesChannel': 'jans_token'}}) Config.lock_message_provider_type = 'POSTGRES' # we don't need to install lock-plugin to jans-config-api for remote client @@ -74,7 +74,7 @@ def install(self): self.apache_lock_config() def install_as_server(self): - self.installJettyService(self.jetty_app_configuration[self.service_name], True) + self.install_jettyService(self.jetty_app_configuration[self.service_name], True) self.logIt(f"Copying {self.source_files[0][0]} into jetty webapps folder...") self.copyFile(self.source_files[0][0], self.jetty_service_webapps) self.enable() diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans_saml.py b/jans-linux-setup/jans_setup/setup_app/installers/jans_saml.py index 5a1150d7192..7aa056f4762 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans_saml.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans_saml.py @@ -156,7 +156,7 @@ def install_keycloak(self): base.unpack_zip(self.source_files[1][0], self.idp_config_data_dir, with_par_dir=False) # retreive auth config - _, jans_auth_config = self.dbUtils.get_oxAuthConfDynamic() + _, jans_auth_config = self.dbUtils.get_jans_auth_conf_dynamic() Config.templateRenderingDict['jans_auth_token_endpoint'] = jans_auth_config['tokenEndpoint'] self.update_rendering_dict() @@ -290,7 +290,7 @@ def install_keycloak_scheduler(self): self.copyFile(self.source_files[4][0], os.path.join(Config.scheduler_dir, 'lib')) # configuration rendering identifiers - _, jans_auth_config = self.dbUtils.get_oxAuthConfDynamic() + _, jans_auth_config = self.dbUtils.get_jans_auth_conf_dynamic() self.check_clients([('kc_scheduler_api_client_id', '2102.')]) rendering_dict = { diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jetty.py b/jans-linux-setup/jans_setup/setup_app/installers/jetty.py index 56337e0d300..bc0320f23a8 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jetty.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jetty.py @@ -34,7 +34,7 @@ def __init__(self): setattr(base.current_app, self.__class__.__name__, self) self.service_name = 'jetty' self.needdb = False # we don't need backend connection in this class - self.install_var = 'installJetty' + self.install_var = 'install_jetty' self.app_type = AppType.APPLICATION self.install_type = InstallOption.MANDATORY if not base.snap: @@ -152,7 +152,7 @@ def jetty_service_dir(self): def jetty_service_webapps(self): return os.path.join(self.jetty_service_dir, 'webapps') - def installJettyService(self, serviceConfiguration, supportCustomizations=False, supportOnlyPageCustomizations=False): + def install_jettyService(self, serviceConfiguration, supportCustomizations=False, supportOnlyPageCustomizations=False): service_name = serviceConfiguration['name'] self.logIt("Installing jetty service %s..." % service_name) self.logIt("Deploying Jetty Service", pbar=service_name) @@ -355,9 +355,9 @@ def calculate_selected_aplications_memory(self): installedComponents = [] # Jetty apps - for config_var, service in [('installOxAuth', 'jans-auth'), + for config_var, service in [('install_jans_auth', 'jans-auth'), ('install_scim_server', 'jans-scim'), - ('installFido2', 'jans-fido2'), + ('install_fido2', 'jans-fido2'), ('install_config_api', 'jans-config-api'), ('install_jans_lock_as_server', 'jans-lock'), ]: diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jre.py b/jans-linux-setup/jans_setup/setup_app/installers/jre.py index 0b77a1c291d..62bd6c6b9d1 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jre.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jre.py @@ -23,7 +23,7 @@ def __init__(self): setattr(base.current_app, self.__class__.__name__, self) self.service_name = 'jre' self.needdb = False # we don't need backend connection in this class - self.install_var = 'installJre' + self.install_var = 'install_jre' self.app_type = AppType.APPLICATION self.install_type = InstallOption.MANDATORY if not base.snap: diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jython.py b/jans-linux-setup/jans_setup/setup_app/installers/jython.py index dee5e470794..293c655bc9c 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jython.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jython.py @@ -19,7 +19,7 @@ class JythonInstaller(BaseInstaller, SetupUtils): def __init__(self): setattr(base.current_app, self.__class__.__name__, self) self.service_name = 'jython' - self.install_var = 'installJython' + self.install_var = 'install_jython' self.app_type = AppType.APPLICATION self.install_type = InstallOption.MANDATORY if not base.snap: diff --git a/jans-linux-setup/jans_setup/setup_app/installers/oxd.py b/jans-linux-setup/jans_setup/setup_app/installers/oxd.py deleted file mode 100644 index 5ed2fa3b799..00000000000 --- a/jans-linux-setup/jans_setup/setup_app/installers/oxd.py +++ /dev/null @@ -1,169 +0,0 @@ -import os -import glob -import ruamel.yaml -import shutil -import tempfile - -from setup_app import paths -from setup_app.static import AppType, InstallOption -from setup_app.utils import base -from setup_app.config import Config -from setup_app.utils.setup_utils import SetupUtils -from setup_app.installers.base import BaseInstaller - -class OxdInstaller(SetupUtils, BaseInstaller): - - def __init__(self): - setattr(base.current_app, self.__class__.__name__, self) - self.service_name = 'oxd-server' - self.oxd_root = '/opt/oxd-server/' - self.needdb = False # we don't need backend connection in this class - self.app_type = AppType.SERVICE - self.install_type = InstallOption.OPTONAL - self.install_var = 'installOxd' - self.register_progess() - - self.oxd_server_yml_fn = os.path.join(self.oxd_root, 'conf/oxd-server.yml') - - def install(self): - self.logIt("Installing", pbar=self.service_name) - self.shutil.unpack_archive(Config.oxd_package, self.oxd_root) - self.run(['chown', '-R', 'jetty:jetty', self.oxd_root]) - - if base.snap: - self.log_dir = os.path.join(base.snap_common, 'jans/oxd-server/log/') - else: - self.log_dir = '/var/log/oxd-server' - service_file = os.path.join(self.oxd_root, 'oxd-server.service') - if os.path.exists(service_file): - self.run(['cp', service_file, '/lib/systemd/system']) - else: - self.run([Config.cmd_ln, service_file, '/etc/init.d/oxd-server']) - - if not os.path.exists(self.log_dir): - self.run([paths.cmd_mkdir, self.log_dir]) - - self.run([ - 'cp', - os.path.join(Config.install_dir, 'static/oxd/oxd-server.default'), - os.path.join(Config.osDefault, 'oxd-server') - ]) - - self.log_file = os.path.join(self.log_dir, 'oxd-server.log') - if not os.path.exists(self.log_file): - open(self.log_file, 'w').close() - - if not base.snap: - self.run(['chown', '-R', 'jetty:jetty', self.log_dir]) - - for fn in glob.glob(os.path.join(self.oxd_root,'bin/*')): - self.run([paths.cmd_chmod, '+x', fn]) - - self.modify_config_yml() - self.generate_keystore() - - self.enable() - - def modify_config_yml(self): - self.logIt("Configuring", pbar=self.service_name) - yml_str = self.readFile(self.oxd_server_yml_fn) - oxd_yaml = ruamel.yaml.load(yml_str, ruamel.yaml.RoundTripLoader) - - if 'bind_ip_addresses' in oxd_yaml: - oxd_yaml['bind_ip_addresses'].append(Config.ip) - else: - for i, k in enumerate(oxd_yaml): - if k == 'storage': - break - else: - i = 1 - addr_list = [Config.ip] - if base.snap: - addr_list.append('127.0.0.1') - oxd_yaml.insert(i, 'bind_ip_addresses', addr_list) - - if Config.get('oxd_use_jans_storage'): - - oxd_yaml['storage_configuration'].pop('dbFileLocation') - oxd_yaml['storage'] = 'jans_server_configuration' - oxd_yaml['storage_configuration']['baseDn'] = 'o=jans' - oxd_yaml['storage_configuration']['type'] = Config.jans_properties_fn - oxd_yaml['storage_configuration']['connection'] = Config.ox_ldap_properties if Config.mapping_locations['default'] == 'ldap' else Config.jansCouchebaseProperties - oxd_yaml['storage_configuration']['salt'] = os.path.join(Config.configFolder, "salt") - - if base.snap: - for appenders in oxd_yaml['logging']['appenders']: - if appenders['type'] == 'file': - appenders['currentLogFilename'] = self.log_file - appenders['archivedLogFilenamePattern'] = os.path.join(base.snap_common, 'jans/oxd-server/log/oxd-server-%d{yyyy-MM-dd}-%i.log.gz') - - yml_str = ruamel.yaml.dump(oxd_yaml, Dumper=ruamel.yaml.RoundTripDumper) - self.writeFile(self.oxd_server_yml_fn, yml_str) - - - def generate_keystore(self): - self.logIt("Generating certificate", pbar=self.service_name) - # generate oxd-server.keystore for the hostname - self.run([ - paths.cmd_openssl, - 'req', '-x509', '-newkey', 'rsa:4096', '-nodes', - '-out', '/tmp/oxd.crt', - '-keyout', '/tmp/oxd.key', - '-days', '3650', - '-subj', '/C={}/ST={}/L={}/O={}/CN={}/emailAddress={}'.format(Config.countryCode, Config.state, Config.city, Config.orgName, Config.hostname, Config.admin_email), - ]) - - self.run([ - paths.cmd_openssl, - 'pkcs12', '-export', - '-in', '/tmp/oxd.crt', - '-inkey', '/tmp/oxd.key', - '-out', '/tmp/oxd.p12', - '-name', Config.hostname, - '-passout', 'pass:example' - ]) - - self.run([ - Config.cmd_keytool, - '-importkeystore', - '-deststorepass', 'example', - '-destkeypass', 'example', - '-destkeystore', '/tmp/oxd.keystore', - '-srckeystore', '/tmp/oxd.p12', - '-srcstoretype', 'PKCS12', - '-srcstorepass', 'example', - '-alias', Config.hostname, - ]) - - oxd_keystore_fn = os.path.join(self.oxd_root, 'conf/oxd-server.keystore') - self.run(['cp', '-f', '/tmp/oxd.keystore', oxd_keystore_fn]) - self.run([paths.cmd_chown, 'jetty:jetty', oxd_keystore_fn]) - - for f in ('/tmp/oxd.crt', '/tmp/oxd.key', '/tmp/oxd.p12', '/tmp/oxd.keystore'): - self.run([paths.cmd_rm, '-f', f]) - - def installed(self): - return os.path.exists(self.oxd_server_yml_fn) - - def download_files(self, force=False): - oxd_url = os.path.join(base.current_app.app_info['JANS_MAVEN'], 'maven/io/jans/oxd-server/{0}/oxd-server-{0}-distribution.zip').format(base.current_app.app_info['jans_version']) - - self.logIt("Downloading {} and preparing package".format(os.path.basename(oxd_url))) - - with tempfile.TemporaryDirectory() as tmp_dir: - oxd_zip_fn = os.path.join(tmp_dir, 'oxd-server.zip') - oxd_tmp_dir = os.path.join(tmp_dir, 'oxd-server') - self.download_file(oxd_url, oxd_zip_fn) - shutil.unpack_archive(oxd_zip_fn, oxd_tmp_dir) - self.createDirs(os.path.join(oxd_tmp_dir, 'data')) - - oxd_server_sh_url = 'https://raw.githubusercontent.com/GluuFederation/oxd/master/debian/oxd-server' - self.download_file(oxd_server_sh_url, os.path.join(oxd_tmp_dir, 'bin/oxd-server')) - Config.oxd_package = shutil.make_archive(os.path.join(Config.dist_jans_dir, 'oxd-server'), "gztar", root_dir=tmp_dir, base_dir="oxd-server") - - Config.oxd_package = oxd_tgz_fn - - def create_folders(self): - if not os.path.exists(self.oxd_root): - self.run([paths.cmd_mkdir, self.oxd_root]) - diff --git a/jans-linux-setup/jans_setup/setup_app/installers/scim.py b/jans-linux-setup/jans_setup/setup_app/installers/scim.py index a0c316081ee..a4a49acd6b1 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/scim.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/scim.py @@ -44,7 +44,7 @@ def extract_files(self): def install(self): self.logIt("Copying scim.war into jetty webapps folder...") - self.installJettyService(self.jetty_app_configuration[self.service_name], True) + self.install_jettyService(self.jetty_app_configuration[self.service_name], True) jettyServiceWebapps = os.path.join(self.jetty_base, self.service_name, 'webapps') self.copyFile(self.source_files[0][0], jettyServiceWebapps) diff --git a/jans-linux-setup/jans_setup/setup_app/messages.py b/jans-linux-setup/jans_setup/setup_app/messages.py index 5c6cbbdce2a..5eb5831ece6 100644 --- a/jans-linux-setup/jans_setup/setup_app/messages.py +++ b/jans-linux-setup/jans_setup/setup_app/messages.py @@ -25,20 +25,15 @@ class msg: hosts_label = "Hosts" username_label = "Username" - installOxAuth_label = "Install OxAuth" - installOxTrust_label = "Install OxTrust" + install_jans_auth_label = "Install Jans Auth" backend_types_label = "Backend Types" java_type_label = "Java Type" - installHttpd_label = "Install Apache" - installSaml_label = "Install Saml" - installPassport_label = "Install Passport" - installJansRadius_label = "Install Radius" + install_httpd_label = "Install Apache" opendj_storages_label = "Store on OpenDJ" installing_label = "Current" - installOxd_label = "Install Oxd" - installCasa_label = "Install Casa" + install_casa_label = "Install Casa" install_scim_server_label = "Install Scim" - installFido2_label = "Install Fido2" + install_fido2_label = "Install Fido2" insufficient_free_disk_space = "Available free disk space was determined to be {1:0.1f} GB. This is less than the required disk space of {} GB." insufficient_mem_size = "RAM size was determined to be {:0.1f} GB. This is less than the suggested RAM size of {} GB" @@ -71,35 +66,23 @@ class msg: enter_valid_countryCode = "Please enter two letter country code" - ask_installHttpd = "Install Apache HTTPD Server" - ask_installSaml = "Install Shibboleth SAML IDP" - ask_installOxAuthRP = "Install oxAuth RP" - ask_installPassport = "Install Passport" - ask_installJansRadius = "Install Janssen Radius" - ask_installCasa = "Install Casa" - ask_installOxd = "Install Oxd" + ask_install_httpd = "Install Apache HTTPD Server" + ask_install_casa = "Install Casa" ask_opendj_install = "Install OpenDJ" ask_install_scim_server = "Install Scim Server" - ask_installFido2 = "Install Fido2" + ask_install_fido2 = "Install Fido2" opendj_install_options = ["Don't Install","Install Locally","Use Remote OpenDJ"] - oxd_url_label = "oxd Server URL" - install_oxd_or_url_warning = "Please either enter oxd Server URL or check Install Oxd" - oxd_connection_error = "Can't connect to oxd-server with url {}. Reason: {}" - oxd_ssl_cert_error = "Hostname of oxd ssl certificate is {} which does not match {} casa won't start properly" ask_cb_install = "Couchbase Installation" cb_install_options = ["Don't Install","Install Locally","Use Remote Couchbase"] - - ask_use_jans_storage_oxd = "By default oxd uses its own db. Do you want to use Janssen Storage for Oxd?" - ask_use_jans_storage_oxd_title = "Use Janssen Storage for Oxd?" - + notify_select_backend = "Please select one of the backends either local install or remote" weak_password = "Password for {} must be at least 6 characters and include one uppercase letter, one lowercase letter, one digit, and one special character." unselected_storages = "Note: Unselected storages will go Couchbase Server" no_help = "No help is provided for this screen." - + MainFromHelp = "Detected OS type, system init type, and Apache version is displayed. Inorder to continue to next step, you must check lisecnce acknowledgement." HostFromHelp = ("IP Address: ip address of this server. Detected ip address will be provided\n" "Hostname: hostname of this server. Detected hostname will be provided.\n" @@ -110,16 +93,12 @@ class msg: installation_description_java = "Corretto is a build of the Open Java Development Kit (OpenJDK) with long-term support from Amazon. Corretto is certified using the Java Technical Compatibility Kit (TCK) to ensure it meets the Java SE standard." installation_description_opendj = "OpenDJ is an LDAPv3 compliant directory service, which has been developed for the Java platform, providing a high performance, highly available, and secure store for the identities managed by your organization." - installation_description_oxauth = "oxAuth is an open source OpenID Connect Provider (OP) and UMA Authorization Server (AS). The project also includes OpenID Connect Client code which can be used by websites to validate tokens." - installation_description_oxtrust = "oxTrust is a Weld based web application for Janssen Server administration." + installation_description_jans_auth = "Jans Auth is an open source OpenID Connect Provider (OP) and UMA Authorization Server (AS). The project also includes OpenID Connect Client code which can be used by websites to validate tokens." installation_description_saml = "The Janssen Server acts as a SAML identity provider (IDP) to support outbound SAML single sign-on (SSO)." - installation_description_passport = "Janssen bundles the Passport.js authentication middleware project to support user authentication at external SAML, OAuth, and OpenID Connect providers " - installation_description_radius = "The Janssen Server now ships with a RADIUS server called Janssen Radius. It is based on the TinyRadius Java library." installation_description_jans = "Janssen Server is identity & access management (IAM) platform for web & mobile single sign-on (SSO), two-factor authentication (2FA) and API access management." installation_description_jetty = "Eclipse Jetty provides a Web server and jakarta.servlet container, plus support for HTTP/2, WebSocket, OSGi, JMX, JNDI, JAAS and many other integrations." installation_description_jython = "Jython is a Java implementation of Python that combines expressive power with clarity. Jython is freely available for both commercial and non-commercial use and is distributed with source code under the PSF License v2." installation_description_node = "As an asynchronous event-driven JavaScript runtime, Node.js is designed to build scalable network applications." - installation_description_oxd = "oxd exposes simple, static APIs web application developers can use to implement user authentication and authorization against an OAuth 2.0 authorization server like Janssen." installation_description_casa = "Janssen Casa is a self-service web portal for end-users to manage authentication and authorization preferences for their account in a Janssen Server." installation_description_scim = "The Janssen Server implements SCIM to offer standard REST APIs for performing CRUD operations (create, read, update and delete) against user data." installation_description_fido2 = "FIDO 2.0 (FIDO2) is an open authentication standard that enables people to leverage common devices to authenticate to online services in both mobile and desktop environments" diff --git a/jans-linux-setup/jans_setup/setup_app/setup_options.py b/jans-linux-setup/jans_setup/setup_app/setup_options.py index e06d7bfd620..abaf7bcb19e 100644 --- a/jans-linux-setup/jans_setup/setup_app/setup_options.py +++ b/jans-linux-setup/jans_setup/setup_app/setup_options.py @@ -11,12 +11,11 @@ def get_setup_options(): 'setup_properties': None, 'noPrompt': False, 'downloadWars': False, - 'installOxAuth': True, + 'install_jans_auth': True, 'install_config_api': True, - 'installHTTPD': True, + 'install_httpd': True, 'install_scim_server': True if base.current_app.profile == 'jans' else False, - 'installOxd': False, - 'installFido2': True, + 'install_fido2': True, 'install_jans_link': False, 'install_jans_keycloak_link': False, 'install_casa': False, @@ -90,7 +89,7 @@ def get_setup_options(): setupOptions['couchbase_hostname'] = base.argsp.couchbase_hostname if base.argsp.no_jsauth: - setupOptions['installOxAuth'] = False + setupOptions['install_jans_auth'] = False if base.argsp.no_config_api: setupOptions['install_config_api'] = False @@ -99,7 +98,7 @@ def get_setup_options(): setupOptions['install_scim_server'] = False if base.argsp.no_fido2: - setupOptions['installFido2'] = False + setupOptions['install_fido2'] = False if base.argsp.install_jans_link: setupOptions['install_jans_link'] = True @@ -197,7 +196,7 @@ def get_setup_options(): setupOptions['noPrompt'] = base.argsp.n if base.argsp.no_httpd: - setupOptions['installHTTPD'] = False + setupOptions['install_httpd'] = False return setupOptions diff --git a/jans-linux-setup/jans_setup/setup_app/test_data_loader.py b/jans-linux-setup/jans_setup/setup_app/test_data_loader.py index 36497bfd6e7..784218ffc66 100644 --- a/jans-linux-setup/jans_setup/setup_app/test_data_loader.py +++ b/jans-linux-setup/jans_setup/setup_app/test_data_loader.py @@ -48,7 +48,7 @@ def create_test_client_keystore(self): self.run(' '.join(args), shell=True) args = [Config.cmd_java, '-Dlog4j.defaultInitOverride=true', - '-cp', Config.non_setup_properties['oxauth_client_jar_fn'], Config.non_setup_properties['key_gen_path'], + '-cp', Config.non_setup_properties['jans_auth_client_jar_fn'], Config.non_setup_properties['key_gen_path'], '-key_ops_type', 'ALL', '-keystore', client_keystore_fn, '-keypasswd', 'secret', @@ -96,11 +96,11 @@ def load_agama_test_data(self): prop_src_fn = os.path.join(agama_out_dir, 'config-agama-test.properties') self.renderTemplateInOut(prop_src_fn, agama_temp_dir, os.path.join(Config.output_dir, 'test/jans-auth')) - dn, oxauth_conf_dynamic = self.dbUtils.get_oxAuthConfDynamic() - agama_config=oxauth_conf_dynamic["agamaConfiguration"].copy() + dn, jans_auth_conf_dynamic = self.dbUtils.get_jans_auth_conf_dynamic() + agama_config=jans_auth_conf_dynamic["agamaConfiguration"].copy() agama_config['disableTCHV'] = True agama_config['enabled'] = True - self.dbUtils.set_oxAuthConfDynamic({'agamaConfiguration': agama_config}) + self.dbUtils.set_jans_auth_conf_dynamic({'agamaConfiguration': agama_config}) self.dbUtils.enable_script('BADA-BADA') def load_test_data(self): @@ -130,43 +130,43 @@ def load_test_data(self): if Config.rdbm_type == 'spanner': Config.rdbm_password_enc = '' - Config.templateRenderingDict['config_oxauth_test_ldap'] = '# Not available' - Config.templateRenderingDict['config_oxauth_test_couchbase'] = '# Not available' + Config.templateRenderingDict['config_jans_auth_test_ldap'] = '# Not available' + Config.templateRenderingDict['config_jans_auth_test_couchbase'] = '# Not available' - config_oxauth_test_properties = self.fomatWithDict( + config_jans_auth_test_properties = self.fomatWithDict( 'server.name=%(hostname)s\nconfig.oxauth.issuer=http://localhost:80\nconfig.oxauth.contextPath=http://localhost:80\nconfig.oxauth.salt=%(encode_salt)s\nconfig.persistence.type=%(persistence_type)s\n\n', self.merge_dicts(Config.__dict__, Config.templateRenderingDict) ) if self.getMappingType('ldap'): - template_text = self.readFile(os.path.join(self.template_base, 'jans-auth/server/config-oxauth-test-ldap.properties.nrnd')) - rendered_text = self.fomatWithDict(template_text, self.merge_dicts(Config.__dict__, Config.templateRenderingDict)) - config_oxauth_test_properties += '#ldap\n' + rendered_text + template_text = self.readFile(os.path.join(self.template_base, 'jans-auth/server/config-jans-auth-test-ldap.properties.nrnd')) + rendered_text = self.fomatWithDict(template_text, self.merge_dicts(Config.__dict__, Config.templateRenderingDict)) + config_jans_auth_test_properties += '#ldap\n' + rendered_text if self.getMappingType('couchbase'): couchbaseDict = base.current_app.CouchbaseInstaller.couchbaseDict() - template_text = self.readFile(os.path.join(self.template_base, 'jans-auth/server/config-oxauth-test-couchbase.properties.nrnd')) + template_text = self.readFile(os.path.join(self.template_base, 'jans-auth/server/config-jans-auth-test-couchbase.properties.nrnd')) rendered_text = self.fomatWithDict(template_text, self.merge_dicts(Config.__dict__, Config.templateRenderingDict, couchbaseDict)) - config_oxauth_test_properties += '\n#couchbase\n' + rendered_text + config_jans_auth_test_properties += '\n#couchbase\n' + rendered_text if self.getMappingType('rdbm'): if Config.rdbm_type == 'spanner': - template_text = self.readFile(os.path.join(self.template_base, 'jans-auth/server/config-oxauth-test-spanner.properties.nrnd')) + template_text = self.readFile(os.path.join(self.template_base, 'jans-auth/server/config-jans-auth-test-spanner.properties.nrnd')) rendered_text = self.fomatWithDict(template_text, self.merge_dicts(Config.__dict__, Config.templateRenderingDict)) - config_oxauth_test_properties += '\n#spanner\n' + rendered_text + config_jans_auth_test_properties += '\n#spanner\n' + rendered_text else: - template_text = self.readFile(os.path.join(self.template_base, 'jans-auth/server/config-oxauth-test-sql.properties.nrnd')) + template_text = self.readFile(os.path.join(self.template_base, 'jans-auth/server/config-jans-auth-test-sql.properties.nrnd')) rendered_text = self.fomatWithDict(template_text, self.merge_dicts(Config.__dict__, Config.templateRenderingDict)) - config_oxauth_test_properties += '\n#sql\n' + rendered_text + config_jans_auth_test_properties += '\n#sql\n' + rendered_text self.logIt("Adding custom attributs and indexes") schema2json( - os.path.join(Config.templateFolder, 'test/jans-auth/schema/102-oxauth_test.ldif'), + os.path.join(Config.templateFolder, 'test/jans-auth/schema/102-jans-auth_test.ldif'), os.path.join(Config.output_dir, 'test/jans-auth/schema/') ) schema2json( @@ -174,9 +174,9 @@ def load_test_data(self): os.path.join(Config.output_dir, 'test/scim-client/schema/'), ) - oxauth_json_schema_fn =os.path.join(Config.output_dir, 'test/jans-auth/schema/102-oxauth_test.json') + jans_auth_json_schema_fn =os.path.join(Config.output_dir, 'test/jans-auth/schema/102-jans-auth_test.json') scim_json_schema_fn = os.path.join(Config.output_dir, 'test/scim-client/schema/103-scim_test.json') - jans_schema_json_files = [ oxauth_json_schema_fn, scim_json_schema_fn ] + jans_schema_json_files = [ jans_auth_json_schema_fn, scim_json_schema_fn ] scim_schema = base.readJsonFile(scim_json_schema_fn) may_list = [] @@ -206,8 +206,8 @@ def load_test_data(self): self.dbUtils.rdm_automapper(force=True) self.writeFile( - os.path.join(Config.output_dir, 'test/jans-auth/server/config-oxauth-test.properties'), - config_oxauth_test_properties + os.path.join(Config.output_dir, 'test/jans-auth/server/config-jans-auth-test.properties'), + config_jans_auth_test_properties ) ignoredirs = [] @@ -218,13 +218,13 @@ def load_test_data(self): self.render_templates_folder(self.template_base, ignoredirs=ignoredirs) if Config.get('jca_client_id') or Config.get('jca_test_client_id'): - config_oxauth_test_data_server_properties_fn = os.path.join(Config.output_dir, 'test/jans-auth/server/config-oxauth-test-data.properties') - config_oxauth_test_data_server_properties = Properties() + jans_auth_test_data_server_properties_fn = os.path.join(Config.output_dir, 'test/jans-auth/server/config-jans-auth-test-data.properties') + jans_auth_test_data_server_properties = Properties() - with open(config_oxauth_test_data_server_properties_fn, 'rb') as f: - config_oxauth_test_data_server_properties.load(f, 'utf-8') + with open(jans_auth_test_data_server_properties_fn, 'rb') as f: + jans_auth_test_data_server_properties.load(f, 'utf-8') - keep_clients = config_oxauth_test_data_server_properties["test.keep.clients"].data.split(',') + keep_clients = jans_auth_test_data_server_properties["test.keep.clients"].data.split(',') keep_clients = [client_id.strip() for client_id in keep_clients] if Config.get('jca_client_id'): @@ -232,16 +232,16 @@ def load_test_data(self): if Config.get('jca_test_client_id'): keep_clients.append(Config.jca_test_client_id) - config_oxauth_test_data_server_properties["test.keep.clients"] = ', '.join(keep_clients) + jans_auth_test_data_server_properties["test.keep.clients"] = ', '.join(keep_clients) - with open(config_oxauth_test_data_server_properties_fn, 'wb') as w: - config_oxauth_test_data_server_properties.store(w) + with open(jans_auth_test_data_server_properties_fn, 'wb') as w: + jans_auth_test_data_server_properties.store(w) self.logIt("Loading test ldif files") Config.pbar.progress(self.service_name, "Importing ldif files", False) - ox_auth_test_ldif = os.path.join(Config.output_dir, 'test/jans-auth/data/oxauth-test-data.ldif') - ox_auth_test_user_ldif = os.path.join(Config.output_dir, 'test/jans-auth/data/oxauth-test-data-user.ldif') + ox_auth_test_ldif = os.path.join(Config.output_dir, 'test/jans-auth/data/jans-auth-test-data.ldif') + ox_auth_test_user_ldif = os.path.join(Config.output_dir, 'test/jans-auth/data/jans-auth-test-data-user.ldif') scim_test_ldif = os.path.join(Config.output_dir, 'test/scim-client/data/scim-test-data.ldif') scim_test_user_ldif = os.path.join(Config.output_dir, 'test/scim-client/data/scim-test-data-user.ldif') @@ -258,8 +258,8 @@ def load_test_data(self): self.chown(os.path.join(base.current_app.HttpdInstaller.server_root, 'jans-auth-client'), base.current_app.HttpdInstaller.apache_user, base.current_app.HttpdInstaller.apache_group, recursive=True) - Config.pbar.progress(self.service_name, "Updating oxauth config", False) - oxAuthConfDynamic_changes = { + Config.pbar.progress(self.service_name, "Updating jans auth config", False) + jans_auth_conf_dynamic_changes = { 'dynamicRegistrationCustomObjectClass': 'jansClntCustomAttributes', 'dynamicRegistrationCustomAttributes': [ "jansTrustedClnt", "myCustomAttr1", "myCustomAttr2", "jansInclClaimsInIdTkn" ], 'dynamicRegistrationExpirationTime': 86400, @@ -327,10 +327,10 @@ def load_test_data(self): self.logIt("Can't decode json for auto test ciba patch", True) if datajs: - oxAuthConfDynamic_changes.update(datajs) - self.logIt("oxAuthConfDynamic was updated with auto test ciba patch") + jans_auth_conf_dynamic_changes.update(datajs) + self.logIt("jans_auth_conf_dynamic was updated with auto test ciba patch") - self.dbUtils.set_oxAuthConfDynamic(oxAuthConfDynamic_changes) + self.dbUtils.set_jans_auth_conf_dynamic(jans_auth_conf_dynamic_changes) self.enable_cusom_scripts() @@ -341,7 +341,7 @@ def load_test_data(self): # Update LDAP schema Config.pbar.progress(self.service_name, "Updating schema", False) openDjSchemaFolder = os.path.join(Config.ldap_base_dir, 'config/schema/') - self.copyFile(os.path.join(Config.output_dir, 'test/jans-auth/schema/102-oxauth_test.ldif'), openDjSchemaFolder) + self.copyFile(os.path.join(Config.output_dir, 'test/jans-auth/schema/102-jans-auth_test.ldif'), openDjSchemaFolder) self.copyFile(os.path.join(Config.output_dir, 'test/scim-client/schema/103-scim_test.ldif'), openDjSchemaFolder) schema_fn = os.path.join(openDjSchemaFolder, '77-customAttributes.ldif') @@ -440,6 +440,8 @@ def load_test_data(self): self.writeFile(super_gluu_creds_fn, json.dumps(super_gluu_creds, indent=2), backup=False) self.chown(super_gluu_creds_fn, Config.jetty_user, Config.root_user) + Config.pbar.progress(self.service_name, "Restarting Services", False) + # Disable token binding module if base.os_name in ('ubuntu18', 'ubuntu20'): self.run(['a2dismod', 'mod_token_binding']) @@ -450,7 +452,7 @@ def load_test_data(self): if Config.install_scim_server: self.restart('jans-scim') - if Config.installFido2: + if Config.install_fido2: self.restart('jans-fido2') if Config.install_config_api: diff --git a/jans-linux-setup/jans_setup/setup_app/utils/arg_parser.py b/jans-linux-setup/jans_setup/setup_app/utils/arg_parser.py index eba4fa55adc..8d2b46e6178 100644 --- a/jans-linux-setup/jans_setup/setup_app/utils/arg_parser.py +++ b/jans-linux-setup/jans_setup/setup_app/utils/arg_parser.py @@ -11,7 +11,7 @@ PROFILE = os.environ.get('JANS_PROFILE') parser_description='''Use this script to configure your Jans Server and to add initial data required for -oxAuth and oxTrust to start. If setup.properties is found in this folder, these +Jans Auth and other Jans services to start. If setup.properties is found in this folder, these properties will automatically be used instead of the interactive setup. ''' @@ -105,7 +105,6 @@ parser.add_argument('--install-jans-lock', help="Install Jans Lock", action='store_true') parser.add_argument('--install-opa', help="Install OPA", action='store_true') - #parser.add_argument('--oxd-use-jans-storage', help="Use Jans Storage for Oxd Server", action='store_true') parser.add_argument('--load-config-api-test', help="Load Config Api Test Data", action='store_true') parser.add_argument('-config-patch-creds', help="password:username for downloading auto test ciba password") diff --git a/jans-linux-setup/jans_setup/setup_app/utils/collect_properties.py b/jans-linux-setup/jans_setup/setup_app/utils/collect_properties.py index bc6854b12ef..32e1938aa27 100644 --- a/jans-linux-setup/jans_setup/setup_app/utils/collect_properties.py +++ b/jans-linux-setup/jans_setup/setup_app/utils/collect_properties.py @@ -38,7 +38,7 @@ def collect(self): jans_prop = base.read_properties_file(Config.jans_properties_fn) Config.persistence_type = jans_prop['persistence.type'] - oxauth_ConfigurationEntryDN = jans_prop['jansAuth_ConfigurationEntryDN'] + jans_auth_ConfigurationEntryDN = jans_prop['jansAuth_ConfigurationEntryDN'] jans_ConfigurationDN = 'ou=configuration,o=jans' if Config.persistence_type in ('couchbase', 'sql', 'spanner'): @@ -114,19 +114,6 @@ def collect(self): result = dbUtils.search('ou=clients,o=jans', search_filter='(&(inum=1701.*)(objectClass=jansClnt))', search_scope=ldap3.SUBTREE) - if result: - Config.jans_radius_client_id = result['inum'] - Config.jans_ro_encoded_pw = result['jansClntSecret'] - Config.jans_ro_pw = self.unobscure(Config.jans_ro_encoded_pw) - - result = dbUtils.search('inum=5866-4202,ou=scripts,o=jans', search_scope=ldap3.BASE) - if result: - Config.enableRadiusScripts = result['jansEnabled'] - - result = dbUtils.search('ou=clients,o=jans', search_filter='(&(inum=1402.*)(objectClass=jansClnt))', search_scope=ldap3.SUBTREE) - if result: - Config.oxtrust_requesting_party_client_id = result['inum'] - oxConfiguration = dbUtils.search(jans_ConfigurationDN, search_filter='(objectClass=jansAppConf)', search_scope=ldap3.BASE) if 'jansIpAddress' in oxConfiguration: Config.ip = oxConfiguration['jansIpAddress'] @@ -140,7 +127,7 @@ def collect(self): # Other clients client_var_id_list = [ - ('oxauth_client_id', '1001.'), + ('jans_auth_client_id', '1001.'), ('jca_client_id', '1800.', {'pw': 'jca_client_pw', 'encoded':'jca_client_encoded_pw'}), ('jca_test_client_id', '1802.', {'pw': 'jca_test_client_pw', 'encoded':'jca_test_client_encoded_pw'}), ('scim_client_id', '1201.', {'pw': 'scim_client_pw', 'encoded':'scim_client_encoded_pw'}), @@ -151,31 +138,31 @@ def collect(self): self.check_clients(client_var_id_list, create=False) result = dbUtils.search( - search_base='inum={},ou=clients,o=jans'.format(Config.get('oxauth_client_id', '-1')), + search_base='inum={},ou=clients,o=jans'.format(Config.get('jans_auth_client_id', '-1')), search_filter='(objectClass=jansClnt)', search_scope=ldap3.BASE, ) if result and result.get('jansClntSecret'): - Config.oxauthClient_encoded_pw = result['jansClntSecret'] - Config.oxauthClient_pw = self.unobscure(Config.oxauthClient_encoded_pw) + Config.jans_auth_client_encoded_pw = result['jansClntSecret'] + Config.jans_auth_client_pw = self.unobscure(Config.jans_auth_client_encoded_pw) - dn_oxauth, oxAuthConfDynamic = dbUtils.get_oxAuthConfDynamic() + dn_jans_auth, jans_auth_conf_dynamic = dbUtils.get_jans_auth_conf_dynamic() - o_issuer = urlparse(oxAuthConfDynamic['issuer']) + o_issuer = urlparse(jans_auth_conf_dynamic['issuer']) Config.hostname = str(o_issuer.netloc) - Config.oxauth_openidScopeBackwardCompatibility = oxAuthConfDynamic.get('openidScopeBackwardCompatibility', False) + Config.jans_auth_openidScopeBackwardCompatibility = jans_auth_conf_dynamic.get('openidScopeBackwardCompatibility', False) - if 'pairwiseCalculationSalt' in oxAuthConfDynamic: - Config.pairwiseCalculationSalt = oxAuthConfDynamic['pairwiseCalculationSalt'] - if 'legacyIdTokenClaims' in oxAuthConfDynamic: - Config.oxauth_legacyIdTokenClaims = oxAuthConfDynamic['legacyIdTokenClaims'] - if 'pairwiseCalculationKey' in oxAuthConfDynamic: - Config.pairwiseCalculationKey = oxAuthConfDynamic['pairwiseCalculationKey'] - if 'keyStoreFile' in oxAuthConfDynamic: - Config.oxauth_openid_jks_fn = oxAuthConfDynamic['keyStoreFile'] - if 'keyStoreSecret' in oxAuthConfDynamic: - Config.oxauth_openid_jks_pass = oxAuthConfDynamic['keyStoreSecret'] + if 'pairwiseCalculationSalt' in jans_auth_conf_dynamic: + Config.pairwiseCalculationSalt = jans_auth_conf_dynamic['pairwiseCalculationSalt'] + if 'legacyIdTokenClaims' in jans_auth_conf_dynamic: + Config.jans_auth_legacyIdTokenClaims = jans_auth_conf_dynamic['legacyIdTokenClaims'] + if 'pairwiseCalculationKey' in jans_auth_conf_dynamic: + Config.pairwiseCalculationKey = jans_auth_conf_dynamic['pairwiseCalculationKey'] + if 'keyStoreFile' in jans_auth_conf_dynamic: + Config.jans_auth_openid_jks_fn = jans_auth_conf_dynamic['keyStoreFile'] + if 'keyStoreSecret' in jans_auth_conf_dynamic: + Config.jans_auth_openid_jks_pass = jans_auth_conf_dynamic['keyStoreSecret'] httpd_crt_fn = '/etc/certs/httpd.crt' crt_fn = httpd_crt_fn if os.path.exists(httpd_crt_fn) else '/etc/certs/ob/server.crt' @@ -203,7 +190,7 @@ def collect(self): default_dir = '/etc/default' usedRatio = 0.001 - oxauth_max_heap_mem = 0 + jans_auth_max_heap_mem = 0 jetty_services = JettyInstaller.jetty_app_configuration @@ -214,11 +201,11 @@ def collect(self): if service == 'jans-auth': service_prop = base.read_properties_file(service_default_fn) m = re.search('-Xmx(\d*)m', service_prop['JAVA_OPTIONS']) - oxauth_max_heap_mem = int(m.groups()[0]) + jans_auth_max_heap_mem = int(m.groups()[0]) - if oxauth_max_heap_mem: + if jans_auth_max_heap_mem: ratioMultiplier = 1.0 + (1.0 - usedRatio)/usedRatio - applicationMemory = oxauth_max_heap_mem / jetty_services['jans-auth']['memory']['jvm_heap_ration'] + applicationMemory = jans_auth_max_heap_mem / jetty_services['jans-auth']['memory']['jvm_heap_ration'] allowedRatio = jetty_services['jans-auth']['memory']['ratio'] * ratioMultiplier application_max_ram = int(round(applicationMemory / allowedRatio)) @@ -229,7 +216,7 @@ def collect(self): Config.ip = self.detect_ip() Config.install_scim_server = os.path.exists(os.path.join(Config.jetty_base, 'jans-scim/start.d')) - Config.installFido2 = os.path.exists(os.path.join(Config.jetty_base, 'jans-fido2/start.d')) + Config.install_fido2 = os.path.exists(os.path.join(Config.jetty_base, 'jans-fido2/start.d')) Config.install_config_api = os.path.exists(os.path.join(Config.jansOptFolder, 'jans-config-api')) Config.install_jans_link = os.path.exists(os.path.join(Config.jansOptFolder, 'jans-link')) Config.install_casa = os.path.exists(os.path.join(Config.jetty_base, 'casa/start.d')) diff --git a/jans-linux-setup/jans_setup/setup_app/utils/crypto64.py b/jans-linux-setup/jans_setup/setup_app/utils/crypto64.py index c496e1c7387..f94309f868c 100644 --- a/jans-linux-setup/jans_setup/setup_app/utils/crypto64.py +++ b/jans-linux-setup/jans_setup/setup_app/utils/crypto64.py @@ -227,7 +227,7 @@ def import_key_cert_into_keystore(self, suffix, keystore_fn, keystore_pw, in_key def gen_openid_jwks_jks_keys(self, jks_path, jks_pwd, key_expiration=None, dn_name=None, key_algs=None, enc_keys=None): - self.logIt("Generating oxAuth OpenID Connect keys") + self.logIt("Generating Jans Auth OpenID Connect keys") if dn_name == None: dn_name = Config.default_openid_jks_dn_name @@ -243,7 +243,7 @@ def gen_openid_jwks_jks_keys(self, jks_path, jks_pwd, key_expiration=None, dn_na args = [Config.cmd_java, '-Dlog4j.defaultInitOverride=true', - "-cp", Config.non_setup_properties['oxauth_client_jar_fn'], + "-cp", Config.non_setup_properties['jans_auth_client_jar_fn'], Config.non_setup_properties['key_gen_path'], '-key_ops_type', 'ALL', '-keystore', jks_path, @@ -263,12 +263,12 @@ def gen_openid_jwks_jks_keys(self, jks_path, jks_pwd, key_expiration=None, dn_na return output.splitlines() def export_openid_key(self, jks_path, jks_pwd, cert_alias, cert_path): - self.logIt("Exporting oxAuth OpenID Connect keys") + self.logIt("Exporting Jans Auth OpenID Connect keys") cmd = " ".join([Config.cmd_java, "-Dlog4j.defaultInitOverride=true", "-cp", - Config.non_setup_properties['oxauth_client_jar_fn'], + Config.non_setup_properties['jans_auth_client_jar_fn'], Config.non_setup_properties['key_export_path'], '-key_ops_type', 'ALL', "-keystore", @@ -282,10 +282,10 @@ def export_openid_key(self, jks_path, jks_pwd, cert_alias, cert_path): self.run(['/bin/sh', '-c', cmd]) def write_openid_keys(self, fn, jwks): - self.logIt("Writing oxAuth OpenID Connect keys") + self.logIt("Writing jans Auth OpenID Connect keys") if not jwks: - self.logIt("Failed to write oxAuth OpenID Connect key to %s" % fn) + self.logIt("Failed to write jans Auth OpenID Connect key to %s" % fn) return self.backupFile(fn) @@ -295,7 +295,7 @@ def write_openid_keys(self, fn, jwks): self.writeFile(fn, jwks_text) self.run([Config.cmd_chown, 'jetty:jetty', fn]) self.run([Config.cmd_chmod, '600', fn]) - self.logIt("Wrote oxAuth OpenID Connect key to %s" % fn) + self.logIt("Wrote jans Auth OpenID Connect key to %s" % fn) except: self.logIt("Error writing command : %s" % fn, True) @@ -330,14 +330,14 @@ def encode_test_passwords(self): self.logIt("Encoding test passwords") hostname = Config.hostname.split('.')[0] try: - Config.templateRenderingDict['oxauthClient_2_pw'] = Config.templateRenderingDict['oxauthClient_2_inum'] + '-' + hostname - Config.templateRenderingDict['oxauthClient_2_encoded_pw'] = self.obscure(Config.templateRenderingDict['oxauthClient_2_pw']) + Config.templateRenderingDict['jans_auth_client_2_pw'] = Config.templateRenderingDict['jans_auth_client_2_inum'] + '-' + hostname + Config.templateRenderingDict['jans_auth_client_2_encoded_pw'] = self.obscure(Config.templateRenderingDict['jans_auth_client_2_pw']) - Config.templateRenderingDict['oxauthClient_3_pw'] = Config.templateRenderingDict['oxauthClient_3_inum'] + '-' + hostname - Config.templateRenderingDict['oxauthClient_3_encoded_pw'] = self.obscure(Config.templateRenderingDict['oxauthClient_3_pw']) + Config.templateRenderingDict['jans_auth_client_3_pw'] = Config.templateRenderingDict['jans_auth_client_3_inum'] + '-' + hostname + Config.templateRenderingDict['jans_auth_client_3_encoded_pw'] = self.obscure(Config.templateRenderingDict['jans_auth_client_3_pw']) - Config.templateRenderingDict['oxauthClient_4_pw'] = Config.templateRenderingDict['oxauthClient_4_inum'] + '-' + hostname - Config.templateRenderingDict['oxauthClient_4_encoded_pw'] = self.obscure(Config.templateRenderingDict['oxauthClient_4_pw']) + Config.templateRenderingDict['jans_auth_client_4_pw'] = Config.templateRenderingDict['jans_auth_client_4_inum'] + '-' + hostname + Config.templateRenderingDict['jans_auth_client_4_encoded_pw'] = self.obscure(Config.templateRenderingDict['jans_auth_client_4_pw']) except: self.logIt("Error encoding test passwords", True) diff --git a/jans-linux-setup/jans_setup/setup_app/utils/db_utils.py b/jans-linux-setup/jans_setup/setup_app/utils/db_utils.py index eb96aacd3a2..2e52ca08613 100644 --- a/jans-linux-setup/jans_setup/setup_app/utils/db_utils.py +++ b/jans-linux-setup/jans_setup/setup_app/utils/db_utils.py @@ -200,7 +200,7 @@ def exec_rdbm_query(self, query, getresult=False): def set_cbm(self): self.cbm = CBM(Config.get('cb_query_node'), Config.get('couchebaseClusterAdmin'), Config.get('cb_password')) - def get_oxAuthConfDynamic(self): + def get_jans_auth_conf_dynamic(self): if self.moddb == BackendTypes.LDAP: self.ldap_conn.search( search_base='ou=jans-auth,ou=configuration,o=jans', @@ -210,47 +210,47 @@ def get_oxAuthConfDynamic(self): ) dn = self.ldap_conn.response[0]['dn'] - oxAuthConfDynamic = json.loads(self.ldap_conn.response[0]['attributes']['jansConfDyn'][0]) + jans_auth_conf_dynamic = json.loads(self.ldap_conn.response[0]['attributes']['jansConfDyn'][0]) elif self.moddb in (BackendTypes.MYSQL, BackendTypes.PGSQL, BackendTypes.SPANNER): result = self.search(search_base='ou=jans-auth,ou=configuration,o=jans', search_filter='(objectClass=jansAppConf)', search_scope=ldap3.BASE) dn = result['dn'] - oxAuthConfDynamic = json.loads(result['jansConfDyn']) + jans_auth_conf_dynamic = json.loads(result['jansConfDyn']) elif self.moddb == BackendTypes.COUCHBASE: n1ql = 'SELECT * FROM `{}` USE KEYS "configuration_jans-auth"'.format(self.default_bucket) result = self.cbm.exec_query(n1ql) js = result.json() dn = js['results'][0][self.default_bucket]['dn'] - oxAuthConfDynamic = js['results'][0][self.default_bucket]['jansConfDyn'] + jans_auth_conf_dynamic = js['results'][0][self.default_bucket]['jansConfDyn'] - return dn, oxAuthConfDynamic + return dn, jans_auth_conf_dynamic - def set_oxAuthConfDynamic(self, entries): + def set_jans_auth_conf_dynamic(self, entries): if self.moddb == BackendTypes.LDAP: - dn, oxAuthConfDynamic = self.get_oxAuthConfDynamic() - oxAuthConfDynamic.update(entries) + dn, jans_auth_conf_dynamic = self.get_jans_auth_conf_dynamic() + jans_auth_conf_dynamic.update(entries) ldap_operation_result = self.ldap_conn.modify( dn, - {"jansConfDyn": [ldap3.MODIFY_REPLACE, json.dumps(oxAuthConfDynamic, indent=2)]} + {"jansConfDyn": [ldap3.MODIFY_REPLACE, json.dumps(jans_auth_conf_dynamic, indent=2)]} ) self.log_ldap_result(ldap_operation_result) elif self.moddb in (BackendTypes.MYSQL, BackendTypes.PGSQL): - dn, oxAuthConfDynamic = self.get_oxAuthConfDynamic() - oxAuthConfDynamic.update(entries) + dn, jans_auth_conf_dynamic = self.get_jans_auth_conf_dynamic() + jans_auth_conf_dynamic.update(entries) sqlalchemyObj = self.get_sqlalchObj_for_dn(dn) - sqlalchemyObj.jansConfDyn = json.dumps(oxAuthConfDynamic, indent=2) + sqlalchemyObj.jansConfDyn = json.dumps(jans_auth_conf_dynamic, indent=2) self.session.commit() elif self.moddb in (BackendTypes.SPANNER,): - dn, oxAuthConfDynamic = self.get_oxAuthConfDynamic() - oxAuthConfDynamic.update(entries) + dn, jans_auth_conf_dynamic = self.get_jans_auth_conf_dynamic() + jans_auth_conf_dynamic.update(entries) doc_id = self.get_doc_id_from_dn(dn) - self.spanner_client.write_data(table='jansAppConf', columns=['doc_id', 'jansConfDyn'], values=[doc_id, json.dumps(oxAuthConfDynamic)], mutation='update') + self.spanner_client.write_data(table='jansAppConf', columns=['doc_id', 'jansConfDyn'], values=[doc_id, json.dumps(jans_auth_conf_dynamic)], mutation='update') elif self.moddb == BackendTypes.COUCHBASE: for k in entries: diff --git a/jans-linux-setup/jans_setup/setup_app/utils/properties_utils.py b/jans-linux-setup/jans_setup/setup_app/utils/properties_utils.py index eb71031615f..8c313ca66a3 100644 --- a/jans-linux-setup/jans_setup/setup_app/utils/properties_utils.py +++ b/jans-linux-setup/jans_setup/setup_app/utils/properties_utils.py @@ -132,8 +132,6 @@ def check_properties(self): if not Config.opendj_p12_pass: Config.opendj_p12_pass = self.getPW() - self.check_oxd_server_https() - if not Config.encode_salt: Config.encode_salt = self.getPW() + self.getPW() @@ -141,16 +139,6 @@ def check_properties(self): Config.jans_max_mem = int(base.current_mem_size * .83 * 1000) # 83% of physical memory - def check_oxd_server_https(self): - - if Config.get('oxd_server_https'): - Config.templateRenderingDict['oxd_hostname'], Config.templateRenderingDict['oxd_port'] = self.parse_url(Config.oxd_server_https) - if not Config.templateRenderingDict['oxd_port']: - Config.templateRenderingDict['oxd_port'] = 8443 - else: - Config.templateRenderingDict['oxd_hostname'] = Config.hostname - Config.oxd_server_https = 'https://{}:8443'.format(Config.hostname) - def decrypt_properties(self, fn, passwd): out_file = fn[:-4] + '.' + uuid.uuid4().hex[:8] + '-DEC~' @@ -244,8 +232,6 @@ def load_properties(self, prop_file, no_update=[]): if p.get('ldap_hostname') != 'localhost': if p.get('remoteLdap','').lower() == 'true': Config.opendj_install = InstallTypes.REMOTE - elif p.get('installLdap','').lower() == 'true': - Config.opendj_install = InstallTypes.LOCAL elif p.get('opendj_install'): Config.opendj_install = p['opendj_install'] else: @@ -438,45 +424,6 @@ def check_remote_ldap(self, ldap_host, ldap_binddn, ldap_password): return result - def check_oxd_server(self, oxd_url, error_out=True, log_error=True): - - oxd_url = os.path.join(oxd_url, 'health-check') - - ctx = ssl.create_default_context() - ctx.check_hostname = True - ctx.verify_mode = ssl.CERT_NONE - - try: - result = urllib.request.urlopen( - oxd_url, - timeout=2, - context=ctx - ) - if result.code == 200: - oxd_status = json.loads(result.read().decode()) - if oxd_status['status'] == 'running': - return True - except Exception as e: - if log_error: - if Config.thread_queue: - return str(e) - if error_out: - print(colors.DANGER) - print("Can't connect to oxd-server with url {}".format(oxd_url)) - print("Reason: ", e) - print(colors.ENDC) - - def check_oxd_ssl_cert(self, oxd_hostname, oxd_port): - - oxd_cert = ssl.get_server_certificate((oxd_hostname, oxd_port)) - with tempfile.TemporaryDirectory() as tmpdirname: - oxd_crt_fn = os.path.join(tmpdirname, 'oxd.crt') - self.writeFile(oxd_crt_fn, oxd_cert) - ssl_subjects = self.get_ssl_subject(oxd_crt_fn) - - if ssl_subjects.get('commonName') != oxd_hostname: - return ssl_subjects - def promptForBackendMappings(self): @@ -525,17 +472,17 @@ def set_persistence_type(self): def promptForHTTPD(self): - if Config.installed_instance and Config.installHttpd: + if Config.installed_instance and Config.install_httpd: return prompt_for_httpd = self.getPrompt("Install Apache HTTPD Server", - self.getDefaultOption(Config.installHTTPD) + self.getDefaultOption(Config.install_httpd) )[0].lower() - Config.installHttpd = prompt_for_httpd == 'y' + Config.install_httpd = prompt_for_httpd == 'y' - if Config.installed_instance and Config.installHttpd: - Config.addPostSetupService.append('installHttpd') + if Config.installed_instance and Config.install_httpd: + Config.addPostSetupService.append('install_httpd') def promptForScimServer(self): @@ -552,37 +499,17 @@ def promptForScimServer(self): Config.addPostSetupService.append('install_scim_server') def promptForFido2Server(self): - if Config.installed_instance and Config.installFido2: + if Config.installed_instance and Config.install_fido2: return prompt_for_fido2_server = self.getPrompt("Install Fido2 Server?", - self.getDefaultOption(Config.installFido2) - )[0].lower() - Config.installFido2 = prompt_for_fido2_server == 'y' - - if Config.installed_instance and Config.installFido2: - Config.addPostSetupService.append('installFido2') - - - def promptForOxd(self): - - if Config.installed_instance and Config.installOxd: - return - - prompt_for_oxd = self.getPrompt("Install Oxd?", - self.getDefaultOption(Config.installOxd) + self.getDefaultOption(Config.install_fido2) )[0].lower() - Config.installOxd = prompt_for_oxd == 'y' - - if Config.installOxd: - use_jans_storage = self.getPrompt(" Use Janssen Storage for Oxd?", - self.getDefaultOption(Config.get('oxd_use_jans_storage')) - )[0].lower() - Config.oxd_use_jans_storage = use_jans_storage == 'y' + Config.install_fido2 = prompt_for_fido2_server == 'y' + if Config.installed_instance and Config.install_fido2: + Config.addPostSetupService.append('install_fido2') - if Config.installed_instance and Config.installOxd: - Config.addPostSetupService.append('installOxd') def prompt_for_jans_link(self): if Config.installed_instance and Config.install_jans_link: @@ -949,7 +876,7 @@ def openbanking_properties(self): def prompt_for_http_cert_info(self): # IP address needed only for Apache2 and hosts file update - if Config.installHttpd: + if Config.install_httpd: Config.ip = self.get_ip() if base.argsp.host_name: @@ -971,8 +898,6 @@ def prompt_for_http_cert_info(self): else: print("Hostname can't be \033[;1mlocalhost\033[0;0m") - Config.oxd_server_https = 'https://{}:8443'.format(Config.hostname) - # Get city and state|province code Config.city = self.getPrompt("Enter your city or locality", Config.city) Config.state = self.getPrompt("Enter your state or province two letter code", Config.state) diff --git a/jans-linux-setup/jans_setup/static/oxd/oxd-server.default b/jans-linux-setup/jans_setup/static/oxd/oxd-server.default deleted file mode 100644 index cd1a11735d4..00000000000 --- a/jans-linux-setup/jans_setup/static/oxd/oxd-server.default +++ /dev/null @@ -1,26 +0,0 @@ -SERVICE_NAME=oxd-server -JAVA_HOME=/opt/jre -JAVA=$JAVA_HOME/bin/java -if [ -z "$OXD_LOGS" ] -then - OXD_LOGS=/var/log/oxd-server -fi - -OXD_HOME=/opt/oxd-server -OXD_CONF=$OXD_HOME/conf - -if [ -z "$OXD_USER" ] -then - OXD_USER=jetty -fi -LIB=$OXD_HOME/lib -OXD_RUN=$OXD_HOME -OXD_PID_FILE=$OXD_RUN/oxd-server.pid -OXD_STATE=$OXD_HOME/run/oxd-server.state -OXD_INIT_LOG=$OXD_LOGS/oxd-server.log - -BCPROV=`ls $LIB/bcprov-jdk* -t | sort -r | head -n 1` - -CLASSPATH="$BCPROV:$OXD_HOME/lib/oxd-server.jar org.jans.oxd.server.OxdServerApplication" - -JAVA_OPTIONS="-server -Xms256m -Xmx512m -XX:MaxMetaspaceSize=256m -XX:+DisableExplicitGC -Djava.net.preferIPv4Stack=true -cp $CLASSPATH server $OXD_CONF/oxd-server.yml" diff --git a/jans-linux-setup/jans_setup/static/scripts/renew_certs.py b/jans-linux-setup/jans_setup/static/scripts/renew_certs.py index f0f558fb04f..482c49c524b 100644 --- a/jans-linux-setup/jans_setup/static/scripts/renew_certs.py +++ b/jans-linux-setup/jans_setup/static/scripts/renew_certs.py @@ -75,7 +75,7 @@ def create_new_certs(): os.rename('/etc/certs/saml.pem.crt', '/etc/certs/saml.pem') - os.system('chown jetty:jetty /etc/certs/oxauth-keys.*') + os.system('chown jetty:jetty /etc/certs/jans-auth-keys.*') create_new_certs() diff --git a/jans-linux-setup/jans_setup/templates/jans-auth/configuration.ldif b/jans-linux-setup/jans_setup/templates/jans-auth/configuration.ldif index b7273a8c411..f0c6f63896e 100644 --- a/jans-linux-setup/jans_setup/templates/jans-auth/configuration.ldif +++ b/jans-linux-setup/jans_setup/templates/jans-auth/configuration.ldif @@ -1,8 +1,8 @@ dn: ou=jans-auth,ou=configuration,o=jans -jansConfDyn::%(oxauth_config_base64)s -jansConfErrors::%(oxauth_error_base64)s -jansConfStatic::%(oxauth_static_conf_base64)s -jansConfWebKeys::%(oxauth_openid_key_base64)s +jansConfDyn::%(jans_auth_config_base64)s +jansConfErrors::%(jans_auth_error_base64)s +jansConfStatic::%(jans_auth_static_conf_base64)s +jansConfWebKeys::%(jans_auth_openid_key_base64)s jansRevision: 1 objectClass: top objectClass: jansAppConf diff --git a/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json b/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json index e0954498a39..0add1c72d5f 100644 --- a/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json +++ b/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json @@ -453,8 +453,8 @@ "shareSubjectIdBetweenClientsWithSameSectorId": true, "webKeysStorage": "keystore", "dnName": "%(default_openid_jks_dn_name)s", - "keyStoreFile": "%(oxauth_openid_jks_fn)s", - "keyStoreSecret": "%(oxauth_openid_jks_pass)s", + "keyStoreFile": "%(jans_auth_openid_jks_fn)s", + "keyStoreSecret": "%(jans_auth_openid_jks_pass)s", "endSessionWithAccessToken":false, "clientWhiteList": ["*"], "clientBlackList": ["*.attacker.com/*"], diff --git a/jans-linux-setup/jans_setup/templates/test/docs/oxauth-config-update.md b/jans-linux-setup/jans_setup/templates/test/docs/jans-auth-config-update.md similarity index 94% rename from jans-linux-setup/jans_setup/templates/test/docs/oxauth-config-update.md rename to jans-linux-setup/jans_setup/templates/test/docs/jans-auth-config-update.md index 84c889ea19c..d8c80d6a4fa 100644 --- a/jans-linux-setup/jans_setup/templates/test/docs/oxauth-config-update.md +++ b/jans-linux-setup/jans_setup/templates/test/docs/jans-auth-config-update.md @@ -2,9 +2,9 @@ I. Install CE with `-t` option to load data II. Client keys deployment. - cd /var/www/html/ -- wget --no-check-certificate https://raw.githubusercontent.com/JansFederation/oxAuth/master/Client/src/test/resources/oxauth_test_client_keys.zip -- unzip oxauth_test_client_keys.zip -- rm -rf oxauth_test_client_keys.zip +- wget --no-check-certificate https://raw.githubusercontent.com/JanssenProject/jans/main/jans-auth-server/client/src/test/resources/jans_test_client_keys.zip +- unzip jans_test_client_keys.zip +- rm -rf jans_test_client_keys.zip - chown -R root.www-data jans-auth-client III. These changes should be applied to oxAuth config. @@ -58,7 +58,7 @@ V. Update system configuration VI. Restart oxAuth server VII. Update LDAP schema (this is not needed for Couchbase) -1. cp ./output/test/oxauth/schema/102-oxauth_test.ldif /opt/opendj/config/schema/ +1. cp ./output/test/oxauth/schema/102-jans-auth_test.ldif /opt/opendj/config/schema/ 2. cp ./output/test/scim-client/schema/103-scim_test.ldif /opt/opendj/config/schema/ 3. Apply manual schema changes described in ./output/test/scim-client/schema/scim_test_manual_update.schema 4. Create /home/ldap/.pw with LDAP admin user pwd diff --git a/jans-linux-setup/jans_setup/templates/test/jans-auth/client/config-oxauth-test-data.properties b/jans-linux-setup/jans_setup/templates/test/jans-auth/client/config-jans-auth-test-data.properties similarity index 87% rename from jans-linux-setup/jans_setup/templates/test/jans-auth/client/config-oxauth-test-data.properties rename to jans-linux-setup/jans_setup/templates/test/jans-auth/client/config-jans-auth-test-data.properties index 870f9d39927..3ad52461118 100644 --- a/jans-linux-setup/jans_setup/templates/test/jans-auth/client/config-oxauth-test-data.properties +++ b/jans-linux-setup/jans_setup/templates/test/jans-auth/client/config-jans-auth-test-data.properties @@ -12,12 +12,12 @@ auth.user2.inum=B1F3-AEAE-B799 auth.user2.email=test_user2@test.org auth.client.id=FF81-2D39 -auth.client.secret=%(oxauthClient_4_pw)s +auth.client.secret=%(jans_auth_client_4_pw)s uma.user.uid=test_user uma.user.password=test_user_password uma.pat.client.id=AB77-1A2B -uma.pat.client.secret=%(oxauthClient_2_pw)s +uma.pat.client.secret=%(jans_auth_client_2_pw)s sector.identifier.id=a55ede29-8f5a-461d-b06e-76caee8d40b5 sector.identifier.id.bad=840ef58d-a7d0-4986-af7b-71ed0089ce61 diff --git a/jans-linux-setup/jans_setup/templates/test/jans-auth/data/oxauth-test-data-user.ldif b/jans-linux-setup/jans_setup/templates/test/jans-auth/data/jans-auth-test-data-user.ldif similarity index 100% rename from jans-linux-setup/jans_setup/templates/test/jans-auth/data/oxauth-test-data-user.ldif rename to jans-linux-setup/jans_setup/templates/test/jans-auth/data/jans-auth-test-data-user.ldif diff --git a/jans-linux-setup/jans_setup/templates/test/jans-auth/data/oxauth-test-data.ldif b/jans-linux-setup/jans_setup/templates/test/jans-auth/data/jans-auth-test-data.ldif similarity index 95% rename from jans-linux-setup/jans_setup/templates/test/jans-auth/data/oxauth-test-data.ldif rename to jans-linux-setup/jans_setup/templates/test/jans-auth/data/jans-auth-test-data.ldif index f01029a6449..7c63685c6bd 100644 --- a/jans-linux-setup/jans_setup/templates/test/jans-auth/data/oxauth-test-data.ldif +++ b/jans-linux-setup/jans_setup/templates/test/jans-auth/data/jans-auth-test-data.ldif @@ -145,12 +145,12 @@ jansScopeTyp: openid objectClass: jansScope objectClass: top -dn: inum=%(oxauthClient_4_inum)s,ou=clients,o=jans +dn: inum=%(jans_auth_client_4_inum)s,ou=clients,o=jans displayName: Jans Test Client (don't remove) -inum: %(oxauthClient_4_inum)s +inum: %(jans_auth_client_4_inum)s jansAppTyp: web jansClaimRedirectURI: https://%(hostname)s/jans-auth/restv1/uma/gather_claims -jansClntSecret: %(oxauthClient_4_encoded_pw)s +jansClntSecret: %(jans_auth_client_4_encoded_pw)s jansGrantTyp: authorization_code jansGrantTyp: implicit jansGrantTyp: refresh_token @@ -175,12 +175,12 @@ jansTrustedClnt: true objectClass: top objectClass: jansClnt -dn: inum=%(oxauthClient_2_inum)s,ou=clients,o=jans +dn: inum=%(jans_auth_client_2_inum)s,ou=clients,o=jans displayName: Jans Test Resource Server Client (don't remove) -inum: %(oxauthClient_2_inum)s +inum: %(jans_auth_client_2_inum)s jansAppTyp: web jansClaimRedirectURI: https://%(hostname)s/jans-auth/restv1/uma/gather_claims -jansClntSecret: %(oxauthClient_2_encoded_pw)s +jansClntSecret: %(jans_auth_client_2_encoded_pw)s jansGrantTyp: authorization_code jansGrantTyp: implicit jansGrantTyp: refresh_token @@ -197,11 +197,11 @@ jansTrustedClnt: true objectClass: top objectClass: jansClnt -dn: inum=%(oxauthClient_3_inum)s,ou=clients,o=jans +dn: inum=%(jans_auth_client_3_inum)s,ou=clients,o=jans displayName: Jans Test Requesting Party Client (don't remove) -inum: %(oxauthClient_3_inum)s +inum: %(jans_auth_client_3_inum)s jansAppTyp: web -jansClntSecret: %(oxauthClient_3_encoded_pw)s +jansClntSecret: %(jans_auth_client_3_encoded_pw)s jansGrantTyp: authorization_code jansGrantTyp: implicit jansGrantTyp: refresh_token diff --git a/jans-linux-setup/jans_setup/templates/test/jans-auth/schema/102-oxauth_test.ldif b/jans-linux-setup/jans_setup/templates/test/jans-auth/schema/102-jans-auth_test.ldif similarity index 100% rename from jans-linux-setup/jans_setup/templates/test/jans-auth/schema/102-oxauth_test.ldif rename to jans-linux-setup/jans_setup/templates/test/jans-auth/schema/102-jans-auth_test.ldif diff --git a/jans-linux-setup/jans_setup/templates/test/jans-auth/schema/oxauth_index.txt b/jans-linux-setup/jans_setup/templates/test/jans-auth/schema/jans-auth_index.txt similarity index 100% rename from jans-linux-setup/jans_setup/templates/test/jans-auth/schema/oxauth_index.txt rename to jans-linux-setup/jans_setup/templates/test/jans-auth/schema/jans-auth_index.txt diff --git a/jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-oxauth-test-couchbase.properties.nrnd b/jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-jans-auth-test-couchbase.properties.nrnd similarity index 100% rename from jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-oxauth-test-couchbase.properties.nrnd rename to jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-jans-auth-test-couchbase.properties.nrnd diff --git a/jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-oxauth-test-data.properties b/jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-jans-auth-test-data.properties similarity index 89% rename from jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-oxauth-test-data.properties rename to jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-jans-auth-test-data.properties index 51cdc171999..9a846d2d04c 100644 --- a/jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-oxauth-test-data.properties +++ b/jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-jans-auth-test-data.properties @@ -12,14 +12,14 @@ auth.user2.inum=B1F3-AEAE-B799 auth.user2.email=test_user2@test.org auth.client.id=FF81-2D39 -auth.client.secret=%(oxauthClient_4_pw)s +auth.client.secret=%(jans_auth_client_4_pw)s ldap.admin.password=ldap_admin_test_password uma.user.uid=test_user uma.user.password=test_user_password uma.pat.client.id=AB77-1A2B -uma.pat.client.secret=%(oxauthClient_2_pw)s +uma.pat.client.secret=%(jans_auth_client_2_pw)s sector.identifier.id=a55ede29-8f5a-461d-b06e-76caee8d40b5 sector.identifier.id.bad=840ef58d-a7d0-4986-af7b-71ed0089ce61 diff --git a/jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-oxauth-test-ldap.properties.nrnd b/jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-jans-auth-test-ldap.properties.nrnd similarity index 100% rename from jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-oxauth-test-ldap.properties.nrnd rename to jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-jans-auth-test-ldap.properties.nrnd diff --git a/jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-oxauth-test-spanner.properties.nrnd b/jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-jans-auth-test-spanner.properties.nrnd similarity index 100% rename from jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-oxauth-test-spanner.properties.nrnd rename to jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-jans-auth-test-spanner.properties.nrnd diff --git a/jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-oxauth-test-sql.properties.nrnd b/jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-jans-auth-test-sql.properties.nrnd similarity index 100% rename from jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-oxauth-test-sql.properties.nrnd rename to jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-jans-auth-test-sql.properties.nrnd diff --git a/jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-oxauth.properties b/jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-jans-auth.properties similarity index 100% rename from jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-oxauth.properties rename to jans-linux-setup/jans_setup/templates/test/jans-auth/server/config-jans-auth.properties diff --git a/jans-linux-setup/jans_setup/tests/sample1.properties b/jans-linux-setup/jans_setup/tests/sample1.properties index 5f99a43a925..ef388a85613 100644 --- a/jans-linux-setup/jans_setup/tests/sample1.properties +++ b/jans-linux-setup/jans_setup/tests/sample1.properties @@ -2,10 +2,7 @@ install_dir=. setup_properties=None noPrompt=False downloadWars=False -installOxAuth=False -installOxTrust=False -installLdap=False -installHttpd=False -installSaml=False -installCas=False -installOxAuthRP=False +install_jans_auth=False +opendj_install=False +install_httpd=False +install_casa=False diff --git a/jans-linux-setup/jans_setup/tests/sample2.properties b/jans-linux-setup/jans_setup/tests/sample2.properties index 34d082304fd..5c3c27299c5 100644 --- a/jans-linux-setup/jans_setup/tests/sample2.properties +++ b/jans-linux-setup/jans_setup/tests/sample2.properties @@ -2,11 +2,8 @@ install_dir=. setup_properties=None noPrompt=False downloadWars=False -installOxAuth=True -installOxTrust=True -installLdap=True -installHttpd=True -installSaml=True -installCas=True -installOxAuthRP=True +install_jans_auth=True +opendj_install=True +install_httpd=True +install_casa=True diff --git a/jans-linux-setup/jans_setup/tests/sample3.properties b/jans-linux-setup/jans_setup/tests/sample3.properties index b6b8a569d76..9c99ac1fd97 100644 --- a/jans-linux-setup/jans_setup/tests/sample3.properties +++ b/jans-linux-setup/jans_setup/tests/sample3.properties @@ -2,11 +2,8 @@ install_dir=. setup_properties=None noPrompt=False downloadWars=False -installOxAuth=False -installOxTrust=True -installLdap=False -installHttpd=True -installSaml=False -installCas=False -installOxAuthRP=True +install_jans_auth=False +opendj_install=False +install_httpd=True +install_casa=False diff --git a/jans-linux-setup/jans_setup/tests/test_functions.py b/jans-linux-setup/jans_setup/tests/test_functions.py index 70d606d0a93..744b85519e4 100644 --- a/jans-linux-setup/jans_setup/tests/test_functions.py +++ b/jans-linux-setup/jans_setup/tests/test_functions.py @@ -4,7 +4,6 @@ def test_getOpts(): """Options: - -r Install oxAuth RP -c Install CAS -d specify the directory where community-edition-setup is located. -f specify setup.properties file @@ -21,20 +20,15 @@ def test_getOpts(): 'setup_properties': None, 'noPrompt': False, 'downloadWars': False, - 'installOxAuth': True, - 'installOxTrust': True, - 'installLDAP': True, - 'installHTTPD': True, - 'installSaml': False, - 'installCas': False, - 'installOxAuthRP': False + 'install_jans_auth': True, + 'opendj_install': True, + 'install_httpd': True, + 'install_casa': False, } setupOptions = getOpts(['-r'], setupOptions) - assert_true(setupOptions['installOxAuthRP']) - setupOptions = getOpts(['-c'], setupOptions) - assert_true(setupOptions['installCas']) + assert_true(setupOptions['install_casa']) # existing path setupOptions = getOpts(['-d', '/tmp'], setupOptions) @@ -54,10 +48,7 @@ def test_getOpts(): assert_true(setupOptions['noPrompt']) setupOptions = getOpts(['-N'], setupOptions) - assert_false(setupOptions['installHTTPD']) - - setupOptions = getOpts(['-s'], setupOptions) - assert_true(setupOptions['installSaml']) + assert_false(setupOptions['install_httpd']) setupOptions = getOpts(['-w'], setupOptions) assert_true(setupOptions['downloadWars']) diff --git a/jans-linux-setup/jans_setup/tests/test_setup.py b/jans-linux-setup/jans_setup/tests/test_setup.py index 1ee340bc82b..6f523966f7c 100644 --- a/jans-linux-setup/jans_setup/tests/test_setup.py +++ b/jans-linux-setup/jans_setup/tests/test_setup.py @@ -8,40 +8,28 @@ def test_setup_load_properties(mock_logIt): obj = Setup() - assert_equal(obj.installOxAuth, True) - assert_equal(obj.installOxTrust, True) - assert_equal(obj.installLdap, True) - assert_equal(obj.installHttpd, True) - assert_equal(obj.installSaml, False) - assert_equal(obj.installCas, False) - assert_equal(obj.installOxAuthRP, False) + assert_equal(obj.install_jans_auth, True) + assert_equal(obj.opendj_install, True) + assert_equal(obj.install_httpd, True) + assert_equal(obj.install_casa, False) # all false obj.load_properties('tests/sample1.properties') - assert_equal(obj.installOxAuth, False) - assert_equal(obj.installOxTrust, False) - assert_equal(obj.installLdap, False) - assert_equal(obj.installHttpd, False) - assert_equal(obj.installSaml, False) - assert_equal(obj.installCas, False) - assert_equal(obj.installOxAuthRP, False) + assert_equal(obj.install_jans_auth, False) + assert_equal(obj.opendj_install, False) + assert_equal(obj.install_httpd, False) + assert_equal(obj.install_casa, False) # all true obj.load_properties('tests/sample2.properties') - assert_equal(obj.installOxAuth, True) - assert_equal(obj.installOxTrust, True) - assert_equal(obj.installLdap, True) - assert_equal(obj.installHttpd, True) - assert_equal(obj.installSaml, True) - assert_equal(obj.installCas, True) - assert_equal(obj.installOxAuthRP, True) + assert_equal(obj.install_jans_auth, True) + assert_equal(obj.opendj_install, True) + assert_equal(obj.install_httpd, True) + assert_equal(obj.install_casa, True) # mix of both true and false obj.load_properties('tests/sample3.properties') - assert_equal(obj.installOxAuth, False) - assert_equal(obj.installOxTrust, True) - assert_equal(obj.installLdap, False) - assert_equal(obj.installHttpd, True) - assert_equal(obj.installSaml, False) - assert_equal(obj.installCas, False) - assert_equal(obj.installOxAuthRP, True) + assert_equal(obj.install_jans_auth, False) + assert_equal(obj.opendj_install, False) + assert_equal(obj.install_httpd, True) + assert_equal(obj.install_casa, False) diff --git a/jans-linux-setup/tools/key_regeneration.py b/jans-linux-setup/tools/key_regeneration.py index f7716397a41..263b1897705 100644 --- a/jans-linux-setup/tools/key_regeneration.py +++ b/jans-linux-setup/tools/key_regeneration.py @@ -195,7 +195,7 @@ def __init__(self): # vendor specific definitions if _VENDOR_ == 'gluu': - self.conf_dyn = 'oxAuthConfDynamic' + self.conf_dyn = 'jans_auth_conf_dynamic' self.conf_web_keys = 'oxAuthConfWebKeys' self.conf_rev = 'oxRevision' self.conf_objc = 'oxAuthConfiguration' From d4a6178c06991566f6cae0e692609157ea57eb4d Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Mon, 22 Jul 2024 13:16:35 +0300 Subject: [PATCH 07/43] doc(jans-auth-server): added global token revocation page to mkdocs (#9002) doc(jans-auth-server): added global token revocation page to mkdocs #8398 Signed-off-by: YuriyZ --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index 63e7e7ec4fb..093a6afe38f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -204,6 +204,7 @@ nav: - SSA: admin/auth-server/endpoints/ssa.md - Userinfo: admin/auth-server/endpoints/userinfo.md - Token Revocation: admin/auth-server/endpoints/token-revocation.md + - Global Token Revocation: admin/auth-server/endpoints/global-token-revocation.md - Session Revocation: admin/auth-server/endpoints/session-revocation.md - End Session: admin/auth-server/endpoints/end-session.md - Clientinfo: admin/auth-server/endpoints/clientinfo.md From eb0a4e96cc2f70abe8abf5ecdde8a7beb9eff499 Mon Sep 17 00:00:00 2001 From: Michael Schwartz Date: Tue, 23 Jul 2024 04:07:25 -0500 Subject: [PATCH 08/43] Mike cedarling docs 01 (#9016) * Prefixed bootstrap properties CEDERLING_ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Signed-off-by: Michael Schwartz * Edits to Cedarling docs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Signed-off-by: Michael Schwartz --- docs/admin/lock/cedarling.md | 173 +++++++++++++++-------- docs/assets/lock-cedarling-diagram-3.jpg | Bin 37221 -> 36712 bytes 2 files changed, 112 insertions(+), 61 deletions(-) diff --git a/docs/admin/lock/cedarling.md b/docs/admin/lock/cedarling.md index de5668f150b..4cc077db0fa 100644 --- a/docs/admin/lock/cedarling.md +++ b/docs/admin/lock/cedarling.md @@ -7,43 +7,44 @@ tags: - Cedarling --- -# Authorization Using Cedarling - -## What Is Cedarling +## Cedarling Authorization The Cedarling is a local, autonomous Policy Decision Point, or "PDP". It runs as a local WebAssembly ("WASM") component--you can call it directly in the browser from a JavaScript -function. With each authorization call, the Cedarling has all the policies and data it +function. It can also run as a cloud function to provide authorization for server-side apps. +With each authorization call, the Cedarling has all the policies and data it needs to make a fast, local decision. The Cedarling's authorization function is *deterministic*. The Cedarling always returns either `permit` or `forbid`. You will never get an error indicating a network timeout, or a divide by zero error. It is also very fast. ![](../../assets/lock-cedarling-diagram-1.jpg) -In a JavaScript browser framework, the Cedarling loads its policy store during initialization, as a -static JSON file or fetched via REST. Developers may consider the Cedarling policy store as part of -the code. Having the policies in one file makes it easier to audit the security features and -controls of an application. It facilitates creation of complex contextual policies without -cluttering application code with lots of `if` - `then` statements. Importantly, the Cedarling creates an -audit log of all decisions by an application to allow or forbid actions. In an enterprise deployment, -this audit log is sent for central archiving. +In a JavaScript browser framework, the Cedarling loads its Policy Store during initialization, as a +static JSON file or fetched via REST. Developers may consider the Cedarling Policy Store as part of +the code. Externalizing the policies makes it easier to audit the security features and +controls of an application. The Cedarling enables developers to create complex, contextual policies +without cluttering application code with lots of `if` - `then` statements. Importantly, the Cedarling +creates an audit log of all decisions by an application to `permit` or `forbid` actions. In an +enterprise deployment, this audit log is sent for central archiving. -Where does the Cedarling get the data for policy evaluation? The data is contained in OAuth and -OpenID JWTs that are sent as part of the authorization request to the Cedarling. This makes sense, -because most modern applications rely on a federated identity provider or "IDP". The Cedarling assumes -the use of OAuth and OpenID Connect--sorry SAML geeks. +Where does the Cedarling get the data for policy evaluation? The data is contained in the +authorization request itself which has the OAuth and OpenID JWTs and details about the resource +and requested action. Most modern applications rely on a federated identity provider or "IDP". +Leveraing the JWT tokens to identify the person and software making the request ![](../../assets/lock-cedarling-diagram-2.jpg) Two JWT tokens in particular are typical: (1) an OpenID Connect id_token and (2) an OAuth access -token. The Cedarling can trust the id_token and access token to extract the User, +token. The id_token represents a user authentication event, and the access token represents a +client authentication event. The Cedarling can trust the id_token and access token to extract the User, Role and Client pricipals. The tokens also contain other interesting contextual data. An OpenID -Connect id_token JWT is a record of an authentication event that tells you who authenticated, when +Connect id_token JWT, as a record of an authentication event, tells you who authenticated, when they authenticated, how they authenticatated, and other claims like the subject's Roles. An OAuth -Access Token JWT can tell you information about the software that obtained the the JWT, its extent +Access Token JWT can tell you information about the software that obtained the JWT, its extent of access as defined by the OAuth Authorization Server (*i.e.* the values of the `scope` claim), or other claims--domains frequently enhance the access token to contain business specific data needed -for policy evaluation. +for policy evaluation. If an OpenID Userinfo token is sent to the cedarling, it is combined with the +id_token to paint a fuller picture of the User's claims. The Cedarling, as its name suggests, enables you to define the security rules for your application in [Cedar](https://www.cedarpolicy.com/en) policy syntax. Cedar was invented by Amazon for their @@ -56,92 +57,142 @@ enivironment, like the time of day or network address. The Cedarling authorizes a person using a certain piece of software to do something. From a logical perspective, `person_allowed AND client_allowed` must be `True`. While this seems pretty -simple, a person may be either explicitly allowed or have a role that enables access. For example, +simple, a person may be either explicitly allowed, or have a role that enables access. For example, `person_allowed` may be equal to `True` if `user=mike OR role=SuperUser`. ![](../../assets/lock-cedarling-diagram-3.jpg) -The Action, Resource and Context is sent by the application in the authorization request. This -is where developers need to map security in their application to actions and resources. For -example, in the diagram above the application may have a policy that restricts access to Button 21 -to users with the Admin Role during business hours when they are not using a VPN. +The JWT's, Action, Resource and Context is sent by the application in the authorization request. For +example, this is a sample request from a hypothetical JS application: + +``` +input = { + "access_token": "eyJhbGc....", + "id_token": "eyJjbGc...", + "userinfo_token": "eyJjbGc...", + "resource": {"Ticket": {"id": "12345", "creator": "foo@bar.com", "organization": "Acme"}}, + "action": "View", + "context": { + "ip_address": "54.9.21.201", + "network_type": "VPN", + "user_agent": "Chrome 125.0.6422.77 (Official Build) (arm64)", + "time": "1719266610.98636", + } + } + +decision_result = authz(input) +``` ## Cedarling Token Validation -The Cedarling can validate the signatures of the JWTs for developers, by setting the `SIGNATURE_VALIDATION` -environment variable to `True`. For testing, developers can set this property to `False` and submit -an unsigned JWT. Or developers may prefer to validate the signatures in code. +The Cedarling can validate the signatures of the JWTs for developers, by setting the +`CEDARLING_JWT_VALIDATION` environment variable to `True`. For testing, developers can set this +property to `False` and submit an unsigned JWT, for example one you generate with +[JWT.io](https://jwt.io). Or developers may prefer to validate the signatures in code--that's ok. On initiatilization, the Cedarling downloads the public keys of the Trusted IDPs specified in the -Cedarling policy store. Because all JWT's have an `iss` claim, this is used to deterimne which keys +Cedarling policy store. Because all JWT's have an `iss` claim, this is used to determine which keys to use for token signature validation. In an enterprise deployment, the Cedarling can also check if a JWT has been revoked. The Cedarling -uses a mechanism described in the [OAuth Status Lists](https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/) +uses a mechanism described in the +[OAuth Status Lists](https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/) draft. This might be handy for use cases where a token revocation needs to be communicated immediately, such as an account takeover situation, or an implementation of a one-time transactions in a cluster of web servers. Jans Auth Server supports the [Global Token Revocation](https://datatracker.ietf.org/doc/draft-parecki-oauth-global-token-revocation/) OAuth draft. This is how a client can inform the OAuth Server that a given token should be revoked. +Here is a summary of the ways the Cedarling may validate a JWT, depending on your bootstrap properties: +* Validate signature from Trusted Issuer +* Discard id_token if `aud` does not match access_token `client_id` +* Discard Userinfo token not associated with a `sub` from the id_token +* If Cedarling is Locked, check token status +* Check access token and id_token `exp` and `nbf` claims if time sent in Context + ![](../../assets/lock-cedarling-diagram-4.jpg) ## Policy Authoring The eaisest way to author your policy store is to use the Policy Designer in [Agama Lab](https://cloud.gluu.org/agama-lab). This tool helps you define the policies, schema and trusted IDPs and -to publish a policy store to Github. +to publish a policy store to any Github repository to which you have access. ## Testing the Cedarling -To call the Cedarling from your JavaScript application +You can perform end to end testing by running the Cedarling in your JS browser application. If +you are using a server side application (e.g. a Wordpress application), you'll need to deploy +the Cedarling as a cloud function. Remember to run the Cedarling init function when your application +starts. This is necessary to load the policy store and to download the current public keys from +an IDP if you are using the Cedarling for token validation. If you want to use roles, make sure to +populate the User.role claim in your id_token or Userinfo token. Call the authz function when +you want the Cederling to opine on a security RBAC decision. + +## Cedarling Policy Store + +The Cedarling Policy Store is a JSON file that contains all the data the Cedarling needs to verify JWT tokens and evaluate policies: + +1. Cedar Schema - JSON formatted Schema file +2. Cedar Policies - JSON formatted Policy Set file +3. Trusted Issuers - JSON file with below syntax + +By convention, the filename is `cedarling_store.json`. The JSON schema looks like this: ``` -input = { - "access_token": ["..."], - "id_token": "...", - "userinfo_token": ["..."], - "tx_token": ["..."], - "resource": "ChessApp", - "action": "Execute", - "context": { - "ip_address": "54.9.21.201", - "network_type": "VPN", - "user_agent": "Chrome 125.0.6422.77 (Official Build) (arm64)", - "time": "1719266610.98636", - } - } +{ + "policies": {...}, + "schema": {...}, + "trusted_idps": [] +} +``` -decision_result = authz(input) +### Trusted Issuer Schema + +This is a hypothetical example. +``` +[ +{"name": "Acme", + "Description": "Acme IDP", + "openid_configuration_endpoint": "https://acme.com/.well-known/openid-configuration", + "access_tokens": {"trusted": True}, + "id_tokens": {"trusted":True, "principal_identifier": "email"}, + "userinfo_tokens": {"trusted": True, "role_mapping": "role"} +}, +... +] ``` ## Cedarling Bootstrap Properties -* **`APPLICATION_NAME`** : Human friendly identifier for this application +* **`CEDARLING_APPLICATION_NAME`** : Human friendly identifier for this application + +* **`CEDARLING_POLICY_STORE_URI`** : Location of policy store JSON, used if policy store is not local, or retreived from Lock Master. + +* **`CEDARLING_JWT_VALIDATION`** : Enabled | Disabled + +* **`CEDARLING_JWT_SIGNATURE_ALGORITHMS_SUPPORTED`** : .... -* **`LOCK`** : Enabled | Disabled. If Enabled, the Cedarling will connect to the Lock Master for policies, and subscribe for SSE events. +* **`CEDARLING_REQUIRE_AUD_VALIDATION`** : Enabled | Disabled. Controls if Cedarling will discard id_token without an access token with the corresponding client_id. -* **`POLICY_STORE_URI`** : Location of policy store JSON, used if policy store is not local, or retreived from Lock Master. +* **`CEDARLING_LOG_LEVEL`** : Controls the verbosity of Cedar logging. -* **`LOCK_MASTER_CONFIGURATION_URI`** : Required if `LOCK` == `Enabled`. URI where Cedarling can get JSON file with all required metadata about Lock Master, i.e. `.well-known/lock-master-configuration`. +The following bootstrap properties are only needed for enterprise deployments. -* **`LOCK_SSA_JWT`** : SSA for DCR in a Lock Master deployment. The Cedarling will validate this SSA JWT prior to DCR. +* **`CEDARLING_LOCK`** : Enabled | Disabled. If Enabled, the Cedarling will connect to the Lock Master for policies, and subscribe for SSE events. -* **`POLICY_STORE_ID`** : The identifier of the policy stored needed only for Lock Master deployments. +* **`CEDARLING_LOCK_MASTER_CONFIGURATION_URI`** : Required if `LOCK` == `Enabled`. URI where Cedarling can get JSON file with all required metadata about Lock Master, i.e. `.well-known/lock-master-configuration`. -* **`LOG_LEVEL`** : Controls the verbosity of Cedar logging. +* **`CEDARLING_LOCK_SSA_JWT`** : SSA for DCR in a Lock Master deployment. The Cedarling will validate this SSA JWT prior to DCR. -* **`AUDIT_LOG_INTERVAL`** : How often to send log messages to Lock Master (0 to turn off trasmission) +* **`CEDARLING_POLICY_STORE_ID`** : The identifier of the policy stored needed only for Lock Master deployments. -* **`AUDIT_HEALTH_INTERVAL`** : How often to send health messages to Lock Master (0 to turn off transmission) +* **`CEDARLING_AUDIT_LOG_INTERVAL`** : How often to send log messages to Lock Master (0 to turn off trasmission) -* **`AUDIT_TELEMETRY_INTERVAL`** : How often to send telemetry messages to Lock Master (0 to turn off transmission) +* **`CEDARLING_AUDIT_HEALTH_INTERVAL`** : How often to send health messages to Lock Master (0 to turn off transmission) -* **`DYNAMIC_CONFIGURATION`** : Enabled | Disabled, controls whether Cedarling should listen for SSE config updates +* **`CEDARLING_AUDIT_TELEMETRY_INTERVAL`** : How often to send telemetry messages to Lock Master (0 to turn off transmission) -* **`GET_TOKEN_STATUS_LIST_UPDATES`** : Whether the Cedarling should register for SSE updates for Lock Master deployments. +* **`CEDARLING_DYNAMIC_CONFIGURATION`** : Enabled | Disabled, controls whether Cedarling should listen for SSE config updates -* **`SIGNATURE_ALGORITHMS_SUPPORTED`** : .... +* **`CEDARLING_GET_TOKEN_STATUS_LIST_UPDATES`** : Enabled | Disabled, controls whether Cedarling should listen for SSE OAuth Status List updates -* **`SIGNATURE_VALIDATION`** : Enabled | Disabled -* **`REQUIRE_AUD_VALIDATION`** : Enabled | Disabled. Controls if Cedarling will discard id_token without an access token with the corresponding client_id. diff --git a/docs/assets/lock-cedarling-diagram-3.jpg b/docs/assets/lock-cedarling-diagram-3.jpg index a8b8c8bf61c8e1349130618bf305b943dddad3b2..269be3a82f4bc4eadee09fafca9cbf639028e99f 100644 GIT binary patch delta 31224 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 delta 31774 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%0AWzm&o^0pWmc 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 Date: Tue, 23 Jul 2024 12:39:10 +0300 Subject: [PATCH 09/43] feat(jans-linux-setup): overwrite minimum mem allocation for couchbase buckets (#9003) * feat(jans-linux-setup): overwrite minimum mem allocation for couchbase buckets Signed-off-by: Mustafa Baser * fix(jans-linux-setup): add custom libs to jans-auth if any Signed-off-by: Mustafa Baser --------- Signed-off-by: Mustafa Baser --- .../jans_setup/setup_app/config.py | 57 +++--------------- .../setup_app/data/couchbase_buckets.json | 58 +++++++++++++++++++ .../setup_app/installers/couchbase.py | 23 ++++---- .../setup_app/installers/jans_auth.py | 17 ++++-- .../jans_setup/setup_app/installers/rdbm.py | 1 + .../jans_setup/setup_app/setup_options.py | 3 + .../jans_setup/setup_app/utils/arg_parser.py | 6 +- .../jans_setup/setup_app/utils/base.py | 2 + .../setup_app/utils/properties_utils.py | 5 ++ 9 files changed, 106 insertions(+), 66 deletions(-) create mode 100644 jans-linux-setup/jans_setup/setup_app/data/couchbase_buckets.json diff --git a/jans-linux-setup/jans_setup/setup_app/config.py b/jans-linux-setup/jans_setup/setup_app/config.py index ef478926d75..456c8e9654b 100644 --- a/jans-linux-setup/jans_setup/setup_app/config.py +++ b/jans-linux-setup/jans_setup/setup_app/config.py @@ -40,7 +40,6 @@ class Config: installed_instance = False - @classmethod def get(self, attr, default=None): return getattr(self, attr) if hasattr(self, attr) else default @@ -364,57 +363,15 @@ def progress(self, service_name, msg, incr=False): self.install_time_ldap = None - - self.couchbaseBucketDict = OrderedDict(( - ('default', { 'ldif':[ - self.ldif_base, - self.ldif_attributes, - self.ldif_scopes, - self.ldif_configuration, - self.ldif_metric, - self.ldif_agama, - ], - 'memory_allocation': 100, - 'mapping': '', - 'document_key_prefix': [] - }), - - ('user', { 'ldif': [], - 'memory_allocation': 300, - 'mapping': 'people, groups, authorizations', - 'document_key_prefix': ['groups_', 'people_', 'authorizations_'], - }), - - ('site', { 'ldif': [self.ldif_site], - 'memory_allocation': 100, - 'mapping': 'jans-link', - 'document_key_prefix': ['site_', 'jans-link_'], - }), - - ('cache', { 'ldif': [], - 'memory_allocation': 100, - 'mapping': 'cache', - 'document_key_prefix': ['cache_'], - }), - - ('token', { 'ldif': [], - 'memory_allocation': 300, - 'mapping': 'tokens', - 'document_key_prefix': ['tokens_'], - }), - - ('session', { 'ldif': [], - 'memory_allocation': 200, - 'mapping': 'sessions', - 'document_key_prefix': [], - }), - - )) - - self.mapping_locations = { group: 'rdbm' for group in self.couchbaseBucketDict } - self.non_setup_properties = { 'jans_auth_client_jar_fn': os.path.join(self.dist_jans_dir, 'jans-auth-client-jar-with-dependencies.jar') } + #re-map couchbase buckets ldif + for bucket in self.couchbaseBucketDict: + for i, ldifs in enumerate(self.couchbaseBucketDict[bucket]['ldif']): + self.couchbaseBucketDict[bucket]['ldif'][i] = getattr(self, ldifs) + + self.mapping_locations = { group: 'rdbm' for group in self.couchbaseBucketDict } + Config.addPostSetupService = [] diff --git a/jans-linux-setup/jans_setup/setup_app/data/couchbase_buckets.json b/jans-linux-setup/jans_setup/setup_app/data/couchbase_buckets.json new file mode 100644 index 00000000000..96c90a92313 --- /dev/null +++ b/jans-linux-setup/jans_setup/setup_app/data/couchbase_buckets.json @@ -0,0 +1,58 @@ +{ + "default": { + "ldif": [ + "ldif_base", + "ldif_attributes", + "ldif_scopes", + "ldif_configuration", + "ldif_metric", + "ldif_agama" + ], + "memory_allocation": 100, + "mapping": "", + "document_key_prefix": [] + }, + "user": { + "ldif": [], + "memory_allocation": 300, + "mapping": "people, groups, authorizations", + "document_key_prefix": [ + "groups_", + "people_", + "authorizations_" + ] + }, + "site": { + "ldif": [ + "ldif_site" + ], + "memory_allocation": 100, + "mapping": "jans-link", + "document_key_prefix": [ + "site_", + "jans-link_" + ] + }, + "cache": { + "ldif": [], + "memory_allocation": 100, + "mapping": "cache", + "document_key_prefix": [ + "cache_" + ] + }, + "token": { + "ldif": [], + "memory_allocation": 300, + "mapping": "tokens", + "document_key_prefix": [ + "tokens_" + ] + }, + "session": { + "ldif": [], + "memory_allocation": 200, + "mapping": "sessions", + "document_key_prefix": [] + } +} diff --git a/jans-linux-setup/jans_setup/setup_app/installers/couchbase.py b/jans-linux-setup/jans_setup/setup_app/installers/couchbase.py index 1f622aeab64..78b462ada7c 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/couchbase.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/couchbase.py @@ -45,7 +45,7 @@ def install(self): if not Config.get('couchebaseClusterAdmin'): Config.couchebaseClusterAdmin = 'admin' - + if Config.cb_install == InstallTypes.LOCAL: Config.isCouchbaseUserAdmin = True @@ -130,7 +130,7 @@ def couchbaseInstall(self): def couchebaseCreateCluster(self): - + self.logIt("Initializing Couchbase Node") result = self.dbUtils.cbm.initialize_node() if result.ok: @@ -200,9 +200,9 @@ def exec_n1ql_query(self, query): def couchbaseExecQuery(self, queryFile): self.logIt("Running Couchbase query from file " + queryFile) - + query_file = open(queryFile) - + for line in query_file: query = line.strip() if query: @@ -221,10 +221,10 @@ def couchbaseMakeIndex(self, bucket, ind): attrquoted.append(a) attrquoteds = ', '.join(attrquoted) - + index_name = '{0}_static_{1}'.format(bucket, str(uuid.uuid4()).split('-')[1]) cmd = 'CREATE INDEX `{0}` ON `{1}`({2}) WHERE ({3})'.format(index_name, bucket, attrquoteds, wherec) - + else: if '(' in ''.join(ind): attr_ = ind[0] @@ -242,7 +242,7 @@ def couchbaseMakeIndex(self, bucket, ind): def couchebaseCreateIndexes(self, bucket): - + Config.couchbase_buckets.append(bucket) couchbase_index_str = self.readFile(self.couchbaseIndexJson) couchbase_index_str = couchbase_index_str.replace('!bucket_prefix!', Config.couchbase_bucket_prefix) @@ -282,7 +282,7 @@ def checkIfJansBucketReady(self): def couchbaseSSL(self): self.logIt("Exporting Couchbase SSL certificate to " + self.couchebaseCert) - + for cb_host in base.re_split_host.findall(Config.couchbase_hostname): cbm_ = CBM(cb_host.strip(), Config.couchebaseClusterAdmin, Config.cb_password) @@ -331,7 +331,7 @@ def couchbaseDict(self): prop_dict['couchbase_test_mappings'] = '\n'.join(couchbase_test_mappings) return prop_dict - + def couchbaseProperties(self): prop_file = os.path.basename(Config.jansCouchebaseProperties) prop = open(os.path.join(Config.templateFolder, prop_file)).read() @@ -349,10 +349,10 @@ def create_couchbase_buckets(self): couchbase_mappings = self.getMappingType('couchbase') min_cb_ram = 0 - + for group in couchbase_mappings: min_cb_ram += Config.couchbaseBucketDict[group]['memory_allocation'] - + min_cb_ram += Config.couchbaseBucketDict['default']['memory_allocation'] if couchbaseClusterRamsize < min_cb_ram: @@ -411,6 +411,7 @@ def extract_libs(self): shutil.unpack_archive(self.source_files[0][0], self.common_lib_dir) self.chown(os.path.join(Config.jetty_base, 'common'), Config.jetty_user, Config.jetty_user, True) + def installed(self): if os.path.exists(self.couchebaseInstallDir): diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans_auth.py b/jans-linux-setup/jans_setup/setup_app/installers/jans_auth.py index afd4fe1d9d2..849b4219c42 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans_auth.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans_auth.py @@ -58,8 +58,8 @@ def install(self): self.make_pairwise_calculation_salt() self.install_jettyService(self.jetty_app_configuration[self.service_name], True) self.copyFile(self.source_files[0][0], self.jetty_service_webapps) - self.external_libs() self.set_class_path([os.path.join(self.custom_lib_dir, '*')]) + self.external_libs() self.setup_agama() self.enable() @@ -197,14 +197,23 @@ def import_openbanking_key(self): self.import_key_cert_into_keystore('obsigning', self.jans_auth_openid_jks_fn, Config.jans_auth_openid_jks_pass, Config.ob_key_fn, Config.ob_cert_fn, Config.ob_alias) def external_libs(self): - extra_libs = [] - for extra_lib in (self.source_files[2][0], self.source_files[3][0]): self.copyFile(extra_lib, self.custom_lib_dir) extra_lib_path = os.path.join(self.custom_lib_dir, os.path.basename(extra_lib)) - extra_libs.append(extra_lib_path) self.chown(extra_lib_path, Config.jetty_user, Config.jetty_group) + # add custom libs if any + common_lib_dir = None + if Config.cb_install: + common_lib_dir = base.current_app.CouchbaseInstaller.common_lib_dir + elif Config.rdbm_install and Config.rdbm_type == 'spanner': + common_lib_dir = base.current_app.RDBMInstaller.common_lib_dir + if common_lib_dir: + class_path = os.path.join(common_lib_dir, '*') + current_plugins = self.get_plugins(paths=True) + if not class_path in current_plugins: + current_plugins.append(class_path) + self.set_class_path(current_plugins) def setup_agama(self): self.createDirs(self.agama_root) diff --git a/jans-linux-setup/jans_setup/setup_app/installers/rdbm.py b/jans-linux-setup/jans_setup/setup_app/installers/rdbm.py index 326480d9182..239276124ec 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/rdbm.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/rdbm.py @@ -542,6 +542,7 @@ def extract_libs(self): shutil.unpack_archive(self.source_files[0][0], self.common_lib_dir) self.chown(os.path.join(Config.jetty_base, 'common'), Config.jetty_user, Config.jetty_user, True) + def installed(self): # to be implemented return True diff --git a/jans-linux-setup/jans_setup/setup_app/setup_options.py b/jans-linux-setup/jans_setup/setup_app/setup_options.py index abaf7bcb19e..481f9cdb794 100644 --- a/jans-linux-setup/jans_setup/setup_app/setup_options.py +++ b/jans-linux-setup/jans_setup/setup_app/setup_options.py @@ -88,6 +88,9 @@ def get_setup_options(): if base.argsp.couchbase_hostname: setupOptions['couchbase_hostname'] = base.argsp.couchbase_hostname + for bucket in base.coucbase_bucket_dict: + base.coucbase_bucket_dict[bucket]['memory_allocation'] = getattr(base.argsp, f'couchbase_{bucket}_mem') + if base.argsp.no_jsauth: setupOptions['install_jans_auth'] = False diff --git a/jans-linux-setup/jans_setup/setup_app/utils/arg_parser.py b/jans-linux-setup/jans_setup/setup_app/utils/arg_parser.py index 8d2b46e6178..1cd5fc6838d 100644 --- a/jans-linux-setup/jans_setup/setup_app/utils/arg_parser.py +++ b/jans-linux-setup/jans_setup/setup_app/utils/arg_parser.py @@ -3,9 +3,10 @@ import uuid import argparse -from setup_app import static +from setup_app import static, paths from setup_app.version import __version__ from setup_app.utils import base +from setup_app.config import Config OPENBANKING_PROFILE = 'openbanking' PROFILE = os.environ.get('JANS_PROFILE') @@ -90,6 +91,9 @@ parser.add_argument('-couchbase-bucket-prefix', help="Set prefix for couchbase buckets", default='jans') parser.add_argument('-couchbase-hostname', help="Remote couchbase server hostname") + for bucket in base.coucbase_bucket_dict: + parser.add_argument(f'-couchbase-{bucket}-mem', help=f"Memory allocation in MB for Couchbase bucket {bucket}", default=base.coucbase_bucket_dict[bucket]['memory_allocation'], type=int) + parser.add_argument('--no-data', help="Do not import any data to database backend, used for clustering", action='store_true') parser.add_argument('--no-jsauth', help="Do not install OAuth2 Authorization Server", action='store_true') parser.add_argument('-ldap-admin-password', help="Used as the LDAP directory manager password") diff --git a/jans-linux-setup/jans_setup/setup_app/utils/base.py b/jans-linux-setup/jans_setup/setup_app/utils/base.py index d906878364a..c35f87f5583 100644 --- a/jans-linux-setup/jans_setup/setup_app/utils/base.py +++ b/jans-linux-setup/jans_setup/setup_app/utils/base.py @@ -454,6 +454,8 @@ def unpack_zip(zip_fn, extract_dir, with_par_dir=True): app_info_fn = os.environ.get('JANS_APP_INFO') or os.path.join(par_dir, 'app_info.json') current_app.app_info = readJsonFile(app_info_fn) current_app.jans_zip = os.path.join(Config.distFolder, 'jans/jans.zip') +coucbase_bucket_dict = readJsonFile(os.path.join(paths.APP_ROOT, 'data/couchbase_buckets.json'), ordered=True) +Config.couchbaseBucketDict = coucbase_bucket_dict def as_bool(val): return str(val).lower() in ('t', 'true', 'y', 'yes', 'on', 'ok', '1') diff --git a/jans-linux-setup/jans_setup/setup_app/utils/properties_utils.py b/jans-linux-setup/jans_setup/setup_app/utils/properties_utils.py index 8c313ca66a3..49740f09cb3 100644 --- a/jans-linux-setup/jans_setup/setup_app/utils/properties_utils.py +++ b/jans-linux-setup/jans_setup/setup_app/utils/properties_utils.py @@ -186,6 +186,11 @@ def load_properties(self, prop_file, no_update=[]): p['opendj_install'] = InstallTypes.NONE p['rdbm_install'] = InstallTypes.NONE + for bucket in Config.couchbaseBucketDict: + if p.get(f'couchbase_{bucket}_mem'): + Config.couchbaseBucketDict[bucket]['memory_allocation'] = int(p[f'couchbase_{bucket}_mem']) + + if p.get('opendj_install') == '0': p['opendj_install'] = InstallTypes.NONE From d97d63e79550532a9f481283b5919e68899a3818 Mon Sep 17 00:00:00 2001 From: Dhaval D <343411+ossdhaval@users.noreply.github.com> Date: Tue, 23 Jul 2024 19:34:43 +0530 Subject: [PATCH 10/43] docs(lock): add missing files with placeholder content (#9017) Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> Co-authored-by: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> --- docs/admin/lock/lock-auth-server-config.md | 13 +++++++++++++ docs/admin/lock/lock-client-config.md | 13 +++++++++++++ docs/admin/lock/lock-installation.md | 14 ++++++++++++++ docs/admin/lock/lock-opa.md | 13 +++++++++++++ docs/admin/lock/lock-pdp-plugin.md | 13 +++++++++++++ mkdocs.yml | 10 +++++----- 6 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 docs/admin/lock/lock-auth-server-config.md create mode 100644 docs/admin/lock/lock-client-config.md create mode 100644 docs/admin/lock/lock-installation.md create mode 100644 docs/admin/lock/lock-opa.md create mode 100644 docs/admin/lock/lock-pdp-plugin.md diff --git a/docs/admin/lock/lock-auth-server-config.md b/docs/admin/lock/lock-auth-server-config.md new file mode 100644 index 00000000000..5f5a81f3c89 --- /dev/null +++ b/docs/admin/lock/lock-auth-server-config.md @@ -0,0 +1,13 @@ +# Configuring Janssen Server for Lock + +## This content is in progress + +The Janssen Project documentation is currently in development. Topic pages are being created in order of broadest relevance, and this page is coming in the near future. + +## Have questions in the meantime? + +While this documentation is in progress, you can ask questions through [GitHub Discussions](https://github.com/JanssenProject/jans/discussions) or the [community chat on Gitter](https://gitter.im/JanssenProject/Lobby). Any questions you have will help determine what information our documentation should cover. + +## Want to contribute? + +If you have content you'd like to contribute to this page in the meantime, you can get started with our [Contribution guide](https://docs.jans.io/head/CONTRIBUTING/). \ No newline at end of file diff --git a/docs/admin/lock/lock-client-config.md b/docs/admin/lock/lock-client-config.md new file mode 100644 index 00000000000..bac7bae700d --- /dev/null +++ b/docs/admin/lock/lock-client-config.md @@ -0,0 +1,13 @@ +# Lock Client Configuration + +## This content is in progress + +The Janssen Project documentation is currently in development. Topic pages are being created in order of broadest relevance, and this page is coming in the near future. + +## Have questions in the meantime? + +While this documentation is in progress, you can ask questions through [GitHub Discussions](https://github.com/JanssenProject/jans/discussions) or the [community chat on Gitter](https://gitter.im/JanssenProject/Lobby). Any questions you have will help determine what information our documentation should cover. + +## Want to contribute? + +If you have content you'd like to contribute to this page in the meantime, you can get started with our [Contribution guide](https://docs.jans.io/head/CONTRIBUTING/). \ No newline at end of file diff --git a/docs/admin/lock/lock-installation.md b/docs/admin/lock/lock-installation.md new file mode 100644 index 00000000000..f13cd726dca --- /dev/null +++ b/docs/admin/lock/lock-installation.md @@ -0,0 +1,14 @@ +## Lock Installation + + +## This content is in progress + +The Janssen Project documentation is currently in development. Topic pages are being created in order of broadest relevance, and this page is coming in the near future. + +## Have questions in the meantime? + +While this documentation is in progress, you can ask questions through [GitHub Discussions](https://github.com/JanssenProject/jans/discussions) or the [community chat on Gitter](https://gitter.im/JanssenProject/Lobby). Any questions you have will help determine what information our documentation should cover. + +## Want to contribute? + +If you have content you'd like to contribute to this page in the meantime, you can get started with our [Contribution guide](https://docs.jans.io/head/CONTRIBUTING/). \ No newline at end of file diff --git a/docs/admin/lock/lock-opa.md b/docs/admin/lock/lock-opa.md new file mode 100644 index 00000000000..d8d45578228 --- /dev/null +++ b/docs/admin/lock/lock-opa.md @@ -0,0 +1,13 @@ +# Lock OPA + +## This content is in progress + +The Janssen Project documentation is currently in development. Topic pages are being created in order of broadest relevance, and this page is coming in the near future. + +## Have questions in the meantime? + +While this documentation is in progress, you can ask questions through [GitHub Discussions](https://github.com/JanssenProject/jans/discussions) or the [community chat on Gitter](https://gitter.im/JanssenProject/Lobby). Any questions you have will help determine what information our documentation should cover. + +## Want to contribute? + +If you have content you'd like to contribute to this page in the meantime, you can get started with our [Contribution guide](https://docs.jans.io/head/CONTRIBUTING/). \ No newline at end of file diff --git a/docs/admin/lock/lock-pdp-plugin.md b/docs/admin/lock/lock-pdp-plugin.md new file mode 100644 index 00000000000..3bb6cc618f5 --- /dev/null +++ b/docs/admin/lock/lock-pdp-plugin.md @@ -0,0 +1,13 @@ +# Lock PDP Plugin + +## This content is in progress + +The Janssen Project documentation is currently in development. Topic pages are being created in order of broadest relevance, and this page is coming in the near future. + +## Have questions in the meantime? + +While this documentation is in progress, you can ask questions through [GitHub Discussions](https://github.com/JanssenProject/jans/discussions) or the [community chat on Gitter](https://gitter.im/JanssenProject/Lobby). Any questions you have will help determine what information our documentation should cover. + +## Want to contribute? + +If you have content you'd like to contribute to this page in the meantime, you can get started with our [Contribution guide](https://docs.jans.io/head/CONTRIBUTING/). \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 093a6afe38f..c271df65446 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -396,11 +396,11 @@ nav: - Jans Keycloak Link: admin/link/jans-keycloak-link.md - Lock Guide: - admin/lock/README.md - - Lock Installation: admin/lock/lock_installation.md - - Auth Server Configuration: admin/lock/lock_auth_server_config.md - - Lock Client Configuration: admin/lock/lock_client_config.md - - Implement Lock PDP Plugin: admin/lock/lock_pdp_plugin.md - - Policy Store Integration: admin/lock/lock_opa.md + - Lock Installation: admin/lock/lock-installation.md + - Auth Server Configuration: admin/lock/lock-auth-server-config.md + - Lock Client Configuration: admin/lock/lock-client-config.md + - Implement Lock PDP Plugin: admin/lock/lock-pdp-plugin.md + - Policy Store Integration: admin/lock/lock-opa.md - Lock Master: admin/lock/lock-master.md - Authorization Using Cedarling: admin/lock/cedarling.md - Janssen Recipes: From 51eaf5945fe705482550186c88c642aa2eb50f4d Mon Sep 17 00:00:00 2001 From: Isman Firmansyah Date: Wed, 24 Jul 2024 02:04:20 +0700 Subject: [PATCH 11/43] feat(cloud-native): modify images to conform to configuration schema (#8960) * feat(cloud-native): modify images to conform to configuration schema Signed-off-by: iromli * fix: allow empty value for configmaps and secrets Signed-off-by: iromli * docs(cloud-native): conform to new configuration schema Signed-off-by: iromli * fix: revert allow empty value for configmaps and secrets Signed-off-by: iromli * chore(charts): conform to new configuration schema Signed-off-by: iromli * chore: update JANS_SOURCE_VERSION Signed-off-by: iromli * chore: conform to optional_scopes configmap changes Signed-off-by: iromli * chore: update JANS_SOURCE_VERSION Signed-off-by: iromli * docs(charts): update configurator reference docs Signed-off-by: iromli --------- Signed-off-by: iromli Co-authored-by: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> --- .../janssen-all-in-one/templates/_helpers.tpl | 9 - .../templates/deployment.yml | 4 +- .../janssen-all-in-one/templates/secret.yaml | 48 +-- .../charts/config/templates/_helpers.tpl | 11 +- .../config/templates/load-init-config.yml | 4 +- .../charts/config/templates/secrets.yaml | 48 +-- docker-jans-all-in-one/Dockerfile | 2 +- docker-jans-auth-server/Dockerfile | 2 +- docker-jans-casa/Dockerfile | 2 +- docker-jans-certmanager/Dockerfile | 2 +- docker-jans-config-api/Dockerfile | 2 +- docker-jans-configurator/Dockerfile | 2 +- docker-jans-configurator/README.md | 110 +++--- docker-jans-configurator/requirements.txt | 2 - docker-jans-configurator/scripts/bootstrap.py | 370 ++++++------------ docker-jans-configurator/scripts/parameter.py | 165 -------- docker-jans-fido2/Dockerfile | 2 +- docker-jans-kc-scheduler/Dockerfile | 2 +- docker-jans-keycloak-link/Dockerfile | 2 +- docker-jans-link/Dockerfile | 2 +- docker-jans-persistence-loader/Dockerfile | 2 +- .../scripts/utils.py | 2 +- docker-jans-saml/Dockerfile | 2 +- docker-jans-scim/Dockerfile | 2 +- .../kubernetes/docker-jans-configurator.md | 110 +++--- 25 files changed, 311 insertions(+), 598 deletions(-) delete mode 100644 docker-jans-configurator/scripts/parameter.py diff --git a/charts/janssen-all-in-one/templates/_helpers.tpl b/charts/janssen-all-in-one/templates/_helpers.tpl index 1588dd55710..d652e97a800 100644 --- a/charts/janssen-all-in-one/templates/_helpers.tpl +++ b/charts/janssen-all-in-one/templates/_helpers.tpl @@ -81,15 +81,6 @@ Create optional scopes list {{ if eq .Values.cnPersistenceType "sql" }} {{ $newList = append $newList ("sql" | quote) }} {{- end }} -{{- if .Values.fido2.enabled}} -{{ $newList = append $newList ("fido2" | quote) }} -{{- end}} -{{- if .Values.casa.enabled}} -{{ $newList = append $newList ("casa" | quote) }} -{{- end}} -{{- if .Values.scim.enabled}} -{{ $newList = append $newList ("scim" | quote) }} -{{- end}} {{ toJson $newList }} {{- end }} diff --git a/charts/janssen-all-in-one/templates/deployment.yml b/charts/janssen-all-in-one/templates/deployment.yml index c4e402224c0..32339fc0325 100644 --- a/charts/janssen-all-in-one/templates/deployment.yml +++ b/charts/janssen-all-in-one/templates/deployment.yml @@ -111,9 +111,9 @@ spec: mountPath: /etc/certs/vault_secret_id subPath: vault_secret_id {{- end }} - - mountPath: /opt/jans/configurator/db/generate.json + - mountPath: /opt/jans/configurator/db/configuration.json name: {{ include "janssen-all-in-one.name" . }}-mount-gen-file - subPath: generate.json + subPath: configuration.json - mountPath: /scripts/tls_generator.py name: {{ include "janssen-all-in-one.name" . }}-tls-script subPath: tls_generator.py diff --git a/charts/janssen-all-in-one/templates/secret.yaml b/charts/janssen-all-in-one/templates/secret.yaml index 6dd28fc6900..d5f44ecf747 100644 --- a/charts/janssen-all-in-one/templates/secret.yaml +++ b/charts/janssen-all-in-one/templates/secret.yaml @@ -15,29 +15,33 @@ metadata: {{- end }} type: Opaque stringData: - generate.json: |- + configuration.json: |- { - "hostname": {{ .Values.fqdn | quote }}, - "country_code": {{ .Values.countryCode | quote }}, - "state": {{ .Values.state | quote }}, - "city": {{ .Values.city | quote }}, - "admin_pw": {{ .Values.adminPassword | quote }}, - "ldap_pw": {{ .Values.adminPassword | quote }}, - "redis_pw": {{ .Values.redisPassword | quote }}, - "email": {{ .Values.email | quote }}, - "org_name": {{ .Values.orgName | quote }}, - {{ if eq .Values.cnPersistenceType "sql" }} - "sql_pw": {{ .Values.configmap.cnSqldbUserPassword | quote }}, - {{- end }} - {{ if or ( eq .Values.cnPersistenceType "couchbase" ) ( eq .Values.cnPersistenceType "hybrid" ) }} - "couchbase_pw": {{ .Values.configmap.cnCouchbasePassword | quote }}, - "couchbase_superuser_pw": {{ .Values.configmap.cnCouchbaseSuperUserPassword | quote }}, - {{- end }} - "auth_sig_keys": {{ index .Values "auth-server" "authSigKeys" | quote }}, - "auth_enc_keys": {{ index .Values "auth-server" "authEncKeys" | quote }}, - "optional_scopes": {{ list (include "janssen-all-in-one.optionalScopes" . | fromJsonArray | join ",") }}, - "salt": {{ .Values.salt | quote }}, - "init_keys_exp": {{ index .Values "auth-server-key-rotation" "initKeysLife" }} + "_configmap": { + "hostname": {{ .Values.fqdn | quote }}, + "country_code": {{ .Values.countryCode | quote }}, + "state": {{ .Values.state | quote }}, + "city": {{ .Values.city | quote }}, + "admin_email": {{ .Values.email | quote }}, + "orgName": {{ .Values.orgName | quote }}, + "auth_sig_keys": {{ index .Values "auth-server" "authSigKeys" | quote }}, + "auth_enc_keys": {{ index .Values "auth-server" "authEncKeys" | quote }}, + "optional_scopes": {{ list (include "janssen-all-in-one.optionalScopes" . | fromJsonArray | join ",") | quote }}, + "init_keys_exp": {{ index .Values "auth-server-key-rotation" "initKeysLife" }} + }, + "_secret": { + "admin_password": {{ .Values.adminPassword | quote }}, + "ldap_password": {{ .Values.adminPassword | quote }}, + "redis_password": {{ .Values.redisPassword | quote }}, + {{ if eq .Values.cnPersistenceType "sql" }} + "sql_password": {{ .Values.configmap.cnSqldbUserPassword | quote }}, + {{- end }} + {{ if or ( eq .Values.cnPersistenceType "couchbase" ) ( eq .Values.cnPersistenceType "hybrid" ) }} + "couchbase_password": {{ .Values.configmap.cnCouchbasePassword | quote }}, + "couchbase_superuser_password": {{ .Values.configmap.cnCouchbaseSuperUserPassword | quote }}, + {{- end }} + "encoded_salt": {{ .Values.salt | quote }} + } } {{ if or ( eq .Values.cnPersistenceType "couchbase" ) ( eq .Values.cnPersistenceType "hybrid" ) }} diff --git a/charts/janssen/charts/config/templates/_helpers.tpl b/charts/janssen/charts/config/templates/_helpers.tpl index d2805f71eb1..bd1ba6d70ed 100644 --- a/charts/janssen/charts/config/templates/_helpers.tpl +++ b/charts/janssen/charts/config/templates/_helpers.tpl @@ -84,14 +84,5 @@ Create optional scopes list {{- if .Values.global.opendj.enabled}} {{ $newList = append $newList ("ldap" | quote) }} {{- end}} -{{- if .Values.global.fido2.enabled}} -{{ $newList = append $newList ("fido2" | quote) }} -{{- end}} -{{- if .Values.global.casa.enabled}} -{{ $newList = append $newList ("casa" | quote) }} -{{- end}} -{{- if .Values.global.scim.enabled}} -{{ $newList = append $newList ("scim" | quote) }} -{{- end}} {{ toJson $newList }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/charts/janssen/charts/config/templates/load-init-config.yml b/charts/janssen/charts/config/templates/load-init-config.yml index dc94fa1da91..e5741c72fbb 100644 --- a/charts/janssen/charts/config/templates/load-init-config.yml +++ b/charts/janssen/charts/config/templates/load-init-config.yml @@ -125,9 +125,9 @@ spec: name: aws-secrets-replica-regions subPath: aws_secrets_replica_regions {{- end }} - - mountPath: /app/db/generate.json + - mountPath: /app/db/configuration.json name: {{ include "config.fullname" . }}-mount-gen-file - subPath: generate.json + subPath: configuration.json - mountPath: /scripts/tls_generator.py name: {{ include "config.fullname" . }}-tls-script subPath: tls_generator.py diff --git a/charts/janssen/charts/config/templates/secrets.yaml b/charts/janssen/charts/config/templates/secrets.yaml index 53daef2d4f8..6e282fb0513 100644 --- a/charts/janssen/charts/config/templates/secrets.yaml +++ b/charts/janssen/charts/config/templates/secrets.yaml @@ -15,29 +15,33 @@ metadata: {{- end }} type: Opaque stringData: - generate.json: |- + configuration.json: |- { - "hostname": {{ .Values.global.fqdn | quote }}, - "country_code": {{ .Values.countryCode | quote }}, - "state": {{ .Values.state | quote }}, - "city": {{ .Values.city | quote }}, - "admin_pw": {{ .Values.adminPassword | quote }}, - "ldap_pw": {{ .Values.ldapPassword | quote }}, - "redis_pw": {{ .Values.redisPassword | quote }}, - "email": {{ .Values.email | quote }}, - "org_name": {{ .Values.orgName | quote }}, - {{ if eq .Values.global.cnPersistenceType "sql" }} - "sql_pw": {{ .Values.configmap.cnSqldbUserPassword | quote }}, - {{- end }} - {{ if or ( eq .Values.global.cnPersistenceType "couchbase" ) ( eq .Values.global.cnPersistenceType "hybrid" ) }} - "couchbase_pw": {{ .Values.configmap.cnCouchbasePassword | quote }}, - "couchbase_superuser_pw": {{ .Values.configmap.cnCouchbaseSuperUserPassword | quote }}, - {{- end }} - "auth_sig_keys": {{ index .Values "global" "auth-server" "authSigKeys" | quote }}, - "auth_enc_keys": {{ index .Values "global" "auth-server" "authEncKeys" | quote }}, - "optional_scopes": {{ list (include "config.optionalScopes" . | fromJsonArray | join ",") }}, - "salt": {{ .Values.salt | quote }}, - "init_keys_exp": {{ index .Values "global" "auth-server-key-rotation" "initKeysLife" }} + "_configmap": { + "hostname": {{ .Values.global.fqdn | quote }}, + "country_code": {{ .Values.countryCode | quote }}, + "state": {{ .Values.state | quote }}, + "city": {{ .Values.city | quote }}, + "admin_email": {{ .Values.email | quote }}, + "orgName": {{ .Values.orgName | quote }}, + "auth_sig_keys": {{ index .Values "global" "auth-server" "authSigKeys" | quote }}, + "auth_enc_keys": {{ index .Values "global" "auth-server" "authEncKeys" | quote }}, + "optional_scopes": {{ list (include "config.optionalScopes" . | fromJsonArray | join ",") | quote }}, + "init_keys_exp": {{ index .Values "global" "auth-server-key-rotation" "initKeysLife" }} + }, + "_secret": { + "admin_password": {{ .Values.adminPassword | quote }}, + "ldap_password": {{ .Values.ldapPassword | quote }}, + "redis_password": {{ .Values.redisPassword | quote }}, + {{ if eq .Values.global.cnPersistenceType "sql" }} + "sql_password": {{ .Values.configmap.cnSqldbUserPassword | quote }}, + {{- end }} + {{ if or ( eq .Values.global.cnPersistenceType "couchbase" ) ( eq .Values.global.cnPersistenceType "hybrid" ) }} + "couchbase_password": {{ .Values.configmap.cnCouchbasePassword | quote }}, + "couchbase_superuser_password": {{ .Values.configmap.cnCouchbaseSuperUserPassword | quote }}, + {{- end }} + "encoded_salt": {{ .Values.salt | quote }} + } } {{ if or ( eq .Values.global.cnPersistenceType "couchbase" ) ( eq .Values.global.cnPersistenceType "hybrid" ) }} diff --git a/docker-jans-all-in-one/Dockerfile b/docker-jans-all-in-one/Dockerfile index ef05584c306..5f28014af27 100644 --- a/docker-jans-all-in-one/Dockerfile +++ b/docker-jans-all-in-one/Dockerfile @@ -58,7 +58,7 @@ RUN apk update \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=cc79f2b4c65b1e4361b6b790b576992866a21b8d +ENV JANS_SOURCE_VERSION=0538d19e269bb26ddbd81c7971251d6375d389c3 # note that as we're pulling from a monorepo (with multiple project in it) # we are using partial-clone and sparse-checkout to get the assets diff --git a/docker-jans-auth-server/Dockerfile b/docker-jans-auth-server/Dockerfile index 3442f458019..215d1481298 100644 --- a/docker-jans-auth-server/Dockerfile +++ b/docker-jans-auth-server/Dockerfile @@ -103,7 +103,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-auth/agama/fl \ /app/static/rdbm \ /app/schema -ENV JANS_SOURCE_VERSION=cc79f2b4c65b1e4361b6b790b576992866a21b8d +ENV JANS_SOURCE_VERSION=0538d19e269bb26ddbd81c7971251d6375d389c3 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) diff --git a/docker-jans-casa/Dockerfile b/docker-jans-casa/Dockerfile index 8976633594b..38b8fa5b423 100644 --- a/docker-jans-casa/Dockerfile +++ b/docker-jans-casa/Dockerfile @@ -56,7 +56,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-casa/plugins \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=cc79f2b4c65b1e4361b6b790b576992866a21b8d +ENV JANS_SOURCE_VERSION=0538d19e269bb26ddbd81c7971251d6375d389c3 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup ARG JANS_CASA_EXTRAS_DIR=jans-casa/extras diff --git a/docker-jans-certmanager/Dockerfile b/docker-jans-certmanager/Dockerfile index 7f5467c1e3b..ada0a7bc235 100644 --- a/docker-jans-certmanager/Dockerfile +++ b/docker-jans-certmanager/Dockerfile @@ -25,7 +25,7 @@ RUN wget -q ${CN_SOURCE_URL} -P /app/javalibs/ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=cc79f2b4c65b1e4361b6b790b576992866a21b8d +ENV JANS_SOURCE_VERSION=0538d19e269bb26ddbd81c7971251d6375d389c3 # note that as we're pulling from a monorepo (with multiple project in it) # we are using partial-clone and sparse-checkout to get the assets diff --git a/docker-jans-config-api/Dockerfile b/docker-jans-config-api/Dockerfile index 38ed082c6b1..f83e7b2ac72 100644 --- a/docker-jans-config-api/Dockerfile +++ b/docker-jans-config-api/Dockerfile @@ -78,7 +78,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-config-api/_plugins \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=cc79f2b4c65b1e4361b6b790b576992866a21b8d +ENV JANS_SOURCE_VERSION=0538d19e269bb26ddbd81c7971251d6375d389c3 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup ARG JANS_CONFIG_API_RESOURCES=jans-config-api/server/src/main/resources diff --git a/docker-jans-configurator/Dockerfile b/docker-jans-configurator/Dockerfile index a5647fdaa6e..95e40a81c45 100644 --- a/docker-jans-configurator/Dockerfile +++ b/docker-jans-configurator/Dockerfile @@ -27,7 +27,7 @@ RUN mkdir -p /opt/jans/configurator/javalibs \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=cc79f2b4c65b1e4361b6b790b576992866a21b8d +ENV JANS_SOURCE_VERSION=0538d19e269bb26ddbd81c7971251d6375d389c3 RUN git clone --depth 500 --filter blob:none --no-checkout https://github.com/janssenproject/jans /tmp/jans \ && cd /tmp/jans \ diff --git a/docker-jans-configurator/README.md b/docker-jans-configurator/README.md index 0bcf76453c0..011e3ac1589 100644 --- a/docker-jans-configurator/README.md +++ b/docker-jans-configurator/README.md @@ -8,7 +8,7 @@ tags: ## Overview -Configuration manager is a special container used to load (generate/restore) and dump (backup) the configuration and secrets. +Configurator is a tool to load (generate/restore) and/or dump (backup) the configuration (consists of configmaps and secrets). ## Versions @@ -72,52 +72,62 @@ The following commands are supported by the container: ### load -The load command can be used either to generate or restore config and secret for the cluster. +The load command can be used either to generate/restore configmaps and secrets for the cluster. + +For fresh installation, generate the initial configuration by creating `/path/to/host/volume/configuration.json` similar to example below: -For fresh installation, generate the initial configuration and secret by creating `/path/to/host/volume/generate.json` similar to example below: ```json { - "hostname": "demoexample.jans.io", - "country_code": "US", - "state": "TX", - "city": "Austin", - "admin_pw": "S3cr3t+pass", - "ldap_pw": "S3cr3t+pass", - "email": "s@jans.io", - "org_name": "Gluu Inc." + "_configmap": { + "hostname": "demoexample.jans.io", + "country_code": "US", + "state": "TX", + "city": "Austin", + "admin_email": "s@jans.io", + "orgName": "Gluu Inc." + }, + "_secret": { + "admin_password": "S3cr3t+pass", + "ldap_password": "S3cr3t+pass" + } } ``` -**NOTE**: `generate.json` has optional attributes as seen below. +**NOTE**: `configuration.json` has optional attributes as seen below. -- `auth_sig_keys`: space-separated key algorithm for signing (default to `RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512`) -- `auth_enc_keys`: space-separated key algorithm for encryption (default to `RSA1_5 RSA-OAEP`) -- `optional_scopes`: list of scopes that will be used (supported scopes are `ldap`, `scim`, `fido2`, `couchbase`, `redis`, `sql`, `casa`; default to empty list) -- `ldap_pw`: user's password to access LDAP database (only used if `optional_scopes` list contains `ldap` scope) -- `sql_pw`: user's password to access SQL database (only used if `optional_scopes` list contains `sql` scope) -- `couchbase_pw`: user's password to access Couchbase database (only used if `optional_scopes` list contains `couchbase` scope) -- `couchbase_superuser_pw`: superusers password to access Couchbase database (only used if `optional_scopes` list contains `couchbase` scope) -- `salt`: user-defined salt (24 characters length); if omitted, salt will be generated automatically -- `init_keys_exp`: the initial keys expiration time in hours (default to `48`; extra 1 hour will be added for hard limit) +1. `_configmap`: -Example of generating `salt` value: + - `auth_sig_keys`: space-separated key algorithm for signing (default to `RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512`) + - `auth_enc_keys`: space-separated key algorithm for encryption (default to `RSA1_5 RSA-OAEP`) + - `optional_scopes`: list of optional scopes (as JSON string) that will be used (supported scopes are `ldap`, `couchbase`, `redis`, `sql`; default to empty list) + - `init_keys_exp`: the initial keys expiration time in hours (default to `48`; extra 1 hour will be added for hard limit) -``` -# using shell script -cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 24 | head -n 1 -# output: NFAG5g4R0NSkAZXHL8t2DScL +2. `_secret`: -# using python oneliner -python -c 'import random, string; print("".join(random.choices(string.ascii_letters + string.digits, k=24)))' -# ouput: HsPzqiPkRzNySWlOVui8Ilmw -``` + - `ldap_password`: user's password to access LDAP database (only used if `optional_scopes` list contains `ldap` scope) + - `sql_password`: user's password to access SQL database (only used if `optional_scopes` list contains `sql` scope) + - `couchbase_password`: user's password to access Couchbase database (only used if `optional_scopes` list contains `couchbase` scope) + - `couchbase_superuser_password`: superusers password to access Couchbase database (only used if `optional_scopes` list contains `couchbase` scope) + - `encoded_salt`: user-defined salt (24 characters length); if omitted, salt will be generated automatically -To generate initial config and secrets: + Example of generating `encoded_salt` value: -1. Create config map `config-generate-params` to store the contents of `generate.json` + ``` + # using shell script + cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 24 | head -n 1 + # output: NFAG5g4R0NSkAZXHL8t2DScL + + # using python oneliner + python -c 'import random, string; print("".join(random.choices(string.ascii_letters + string.digits, k=24)))' + # ouput: HsPzqiPkRzNySWlOVui8Ilmw + ``` + +To generate initial configmaps and secrets: + +1. Create config map `config-generate-params` to store the contents of `configuration.json` ```sh - kubectl create cm config-generate-params --from-file=generate.json + kubectl create cm config-generate-params --from-file=configuration.json ``` 1. Mount the configmap into container and apply the yaml: @@ -139,22 +149,23 @@ To generate initial config and secrets: - name: configurator-load image: ghcr.io/janssenproject/jans/configurator:1.1.4_dev volumeMounts: - - mountPath: /app/db/generate.json + - mountPath: /app/db/configuration.json name: config-generate-params - subPath: generate.json + subPath: configuration.json envFrom: - configMapRef: name: config-cm args: ["load"] ``` -To restore configuration and secrets from a backup of `/path/to/host/volume/config.json` and `/path/to/host/volume/secret.json`: mount the directory as `/app/db` inside the container: +A successful `load` command will dump the pre-populated configuration into `/app/db/configuration.out.json`. + +To restore configuration from `configuration.out.json` file: -1. Create config map `config-params` and `secret-params`: +1. Create config map `config-dump-params`: ```sh - kubectl create cm config-params --from-file=config.json - kubectl create cm secret-params --from-file=secret.json + kubectl create cm config-dump-params --from-file=configuration.out.json ``` 2. Mount the configmap into container and apply the yaml: @@ -169,32 +180,25 @@ To restore configuration and secrets from a backup of `/path/to/host/volume/conf spec: restartPolicy: Never volumes: - - name: config-params + - name: config-dump-params configMap: - name: config-params - - name: secret-params - configMap: - name: secret-params + name: config-dump-params containers: - name: configurator-load image: ghcr.io/janssenproject/jans/configurator:1.1.4_dev volumeMounts: - - mountPath: /app/db/config.json - name: config-params - subPath: config.json - - mountPath: /app/db/secret.json - name: secret-params - subPath: secret.json + - mountPath: /app/db/configuration.out.json + name: config-dump-params + subPath: configuration.out.json envFrom: - configMapRef: name: config-cm args: ["load"] ``` - ### dump -The dump command will dump all configuration and secrets from the backends saved into the `/app/db/config.json` and `/app/db/secret.json` files. +The dump command will dump all configuration from the backends saved into the `/app/db/configuration.out.json` file. ```yaml apiVersion: batch/v1 @@ -221,4 +225,4 @@ spec: Copy over the files to host -`kubectl cp config-init-load-job:/app/db .` +`kubectl cp configurator-dump-job:/app/db .` diff --git a/docker-jans-configurator/requirements.txt b/docker-jans-configurator/requirements.txt index 4d08419f9d6..46cc021090c 100644 --- a/docker-jans-configurator/requirements.txt +++ b/docker-jans-configurator/requirements.txt @@ -1,6 +1,4 @@ # pinned to py3-grpcio version to avoid failure on native extension build grpcio==1.59.3 click==8.1.7 -marshmallow==3.21.2 -fqdn==1.5.1 /tmp/jans/jans-pycloudlib diff --git a/docker-jans-configurator/scripts/bootstrap.py b/docker-jans-configurator/scripts/bootstrap.py index ca16463334a..61fa93f7db3 100644 --- a/docker-jans-configurator/scripts/bootstrap.py +++ b/docker-jans-configurator/scripts/bootstrap.py @@ -23,12 +23,11 @@ from jans.pycloudlib.utils import safe_render from jans.pycloudlib.utils import ldap_encode from jans.pycloudlib.utils import get_server_certificate -from jans.pycloudlib.utils import generate_ssl_certkey from jans.pycloudlib.utils import generate_ssl_ca_certkey from jans.pycloudlib.utils import generate_signed_ssl_certkey from jans.pycloudlib.utils import as_boolean +from jans.pycloudlib.schema import load_schema_from_file -from parameter import params_from_file from settings import LOGGING_CONFIG DEFAULT_SIG_KEYS = "RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512" @@ -39,9 +38,8 @@ CERTS_DIR = os.environ.get("CN_CONFIGURATOR_CERTS_DIR", f"{CONFIGURATOR_DIR}/certs") JAVALIBS_DIR = f"{CONFIGURATOR_DIR}/javalibs" -DEFAULT_CONFIG_FILE = os.environ.get("CN_CONFIGURATOR_CONFIG_FILE", f"{DB_DIR}/config.json") -DEFAULT_SECRET_FILE = os.environ.get("CN_CONFIGURATOR_SECRET_FILE", f"{DB_DIR}/secret.json") -DEFAULT_GENERATE_FILE = os.environ.get("CN_CONFIGURATOR_GENERATE_FILE", f"{DB_DIR}/generate.json") +DEFAULT_CONFIGURATION_FILE = os.environ.get("CN_CONFIGURATOR_CONFIGURATION_FILE", f"{DB_DIR}/configuration.json") +DEFAULT_DUMP_FILE = os.environ.get("CN_CONFIGURATOR_DUMP_FILE", f"{DB_DIR}/configuration.out.json") logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("configurator") @@ -99,9 +97,9 @@ def generate_pkcs12(suffix, passwd, hostname): class CtxManager: def __init__(self, manager): self.manager = manager - self.ctx = {"config": {}, "secret": {}} - self._remote_config_ctx = None - self._remote_secret_ctx = None + self.ctx = {"_configmap": {}, "_secret": {}} + self._remote_config_ctx = {} + self._remote_secret_ctx = {} @property def remote_config_ctx(self): @@ -117,40 +115,47 @@ def remote_secret_ctx(self): def set_config(self, key, value, reuse_if_exists=True): if reuse_if_exists and key in self.remote_config_ctx: - logger.info(f"re-using config {key}") - self.ctx["config"][key] = self.remote_config_ctx[key] - return self.ctx["config"][key] + logger.info(f"re-using configmap {key!r}") + self.ctx["_configmap"][key] = self.remote_config_ctx[key] + return self.ctx["_configmap"][key] - logger.info(f"adding config {key}") + logger.info(f"adding configmap {key!r}") if callable(value): value = value() - self.ctx["config"][key] = value - return self.ctx["config"][key] + if isinstance(value, bytes): + value = value.decode() + + self.ctx["_configmap"][key] = value + return self.ctx["_configmap"][key] def set_secret(self, key, value, reuse_if_exists=True): if reuse_if_exists and key in self.remote_secret_ctx: - logger.info(f"re-using secret {key}") - self.ctx["secret"][key] = self.remote_secret_ctx[key] - return self.ctx["secret"][key] + logger.info(f"re-using secret {key!r}") + self.ctx["_secret"][key] = self.remote_secret_ctx[key] + return self.ctx["_secret"][key] - logger.info(f"adding secret {key}") + logger.info(f"adding secret {key!r}") if callable(value): value = value() - self.ctx["secret"][key] = value + if isinstance(value, bytes): + value = value.decode() + + self.ctx["_secret"][key] = value return value def get_config(self, key, default=None): - return self.ctx["config"].get(key) or default + return self.ctx["_configmap"].get(key) or default def get_secret(self, key, default=None): - return self.ctx["secret"].get(key) or default + return self.ctx["_secret"].get(key) or default class CtxGenerator: def __init__(self, manager, params): - self.params = params + self.configmap_params = params["_configmap"] + self.secret_params = params["_secret"] self.manager = manager self.ctx_manager = CtxManager(self.manager) @@ -170,93 +175,36 @@ def get_config(self, key, default=None): def get_secret(self, key, default=None): return self.ctx_manager.get_secret(key, default) - def base_ctx(self): - if self.params["salt"]: - self.set_secret("encoded_salt", self.params["salt"]) + def transform_base_ctx(self): + if self.secret_params["encoded_salt"]: + self.set_secret("encoded_salt", self.secret_params["encoded_salt"]) else: self.set_secret("encoded_salt", partial(get_random_chars, 24)) - self.set_config("orgName", self.params["org_name"]) - self.set_config("country_code", self.params["country_code"]) - self.set_config("state", self.params["state"]) - self.set_config("city", self.params["city"]) - self.set_config("hostname", self.params["hostname"]) - self.set_config("admin_email", self.params["email"]) - # self.set_config("jetty_base", "/opt/jans/jetty") + + self.set_config("orgName", self.configmap_params["orgName"]) + self.set_config("country_code", self.configmap_params["country_code"]) + self.set_config("state", self.configmap_params["state"]) + self.set_config("city", self.configmap_params["city"]) + self.set_config("hostname", self.configmap_params["hostname"]) + self.set_config("admin_email", self.configmap_params["admin_email"]) self.set_config("admin_inum", lambda: f"{uuid4()}") - self.set_secret("encoded_admin_password", partial(ldap_encode, self.params["admin_pw"])) + self.set_secret("encoded_admin_password", partial(ldap_encode, self.secret_params["admin_password"])) - opt_scopes = self.params["optional_scopes"] - self.set_config("optional_scopes", list(set(opt_scopes)), False) + opt_scopes = self.configmap_params["optional_scopes"] + self.set_config("optional_scopes", opt_scopes, False) - def ldap_ctx(self): + def transform_ldap_ctx(self): encoded_salt = self.get_secret("encoded_salt") - # self.set_secret("encoded_ldap_pw", ldap_encode(self.params["admin_pw"])) self.set_secret( "encoded_ox_ldap_pw", - partial(encode_text, self.params["ldap_pw"], encoded_salt), - ) - self.set_config("ldap_init_host", "localhost") - self.set_config("ldap_init_port", 1636) - self.set_config("ldap_port", 1389) - self.set_config("ldaps_port", 1636) - self.set_config("ldap_binddn", "cn=directory manager") - self.set_config("ldap_site_binddn", "cn=directory manager") - ldap_truststore_pass = self.set_secret("ldap_truststore_pass", get_random_chars) - self.set_config("ldapTrustStoreFn", "/etc/certs/opendj.pkcs12") - hostname = self.get_config("hostname") - - generate_ssl_certkey( - "opendj", - self.get_config("admin_email"), - hostname, - self.get_config("orgName"), - self.get_config("country_code"), - self.get_config("state"), - self.get_config("city"), - extra_dns=["ldap"], - base_dir=CERTS_DIR, - ) - with open(f"{CERTS_DIR}/opendj.pem", "w") as fw: - with open(f"{CERTS_DIR}/opendj.crt") as fr: - ldap_ssl_cert = fr.read() - self.set_secret( - "ldap_ssl_cert", - partial(encode_text, ldap_ssl_cert, encoded_salt), - ) - - with open(f"{CERTS_DIR}/opendj.key") as fr: - ldap_ssl_key = fr.read() - self.set_secret( - "ldap_ssl_key", - partial(encode_text, ldap_ssl_key, encoded_salt), - ) - - ldap_ssl_cacert = "".join([ldap_ssl_cert, ldap_ssl_key]) - fw.write(ldap_ssl_cacert) - self.set_secret( - "ldap_ssl_cacert", - partial(encode_text, ldap_ssl_cacert, encoded_salt), - ) - - generate_pkcs12("opendj", ldap_truststore_pass, hostname) - - with open(f"{CERTS_DIR}/opendj.pkcs12", "rb") as fr: - self.set_secret( - "ldap_pkcs12_base64", - partial(encode_text, fr.read(), encoded_salt), - ) - - self.set_secret( - "encoded_ldapTrustStorePass", - partial(encode_text, ldap_truststore_pass, encoded_salt), + partial(encode_text, self.secret_params["ldap_password"], encoded_salt), ) - def redis_ctx(self): - # TODO: move this to persistence-loader - self.set_secret("redis_pw", self.params.get("redis_pw", "")) + def transform_redis_ctx(self): + self.set_secret("redis_password", self.secret_params["redis_password"]) - def auth_ctx(self): + def transform_auth_ctx(self): encoded_salt = self.get_secret("encoded_salt") self.set_config("default_openid_jks_dn_name", "CN=Janssen Auth CA Certificates") @@ -269,38 +217,11 @@ def auth_ctx(self): self.set_config("auth_legacyIdTokenClaims", "false") self.set_config("auth_openidScopeBackwardCompatibility", "false") - # get user-input signing keys - allowed_sig_keys = DEFAULT_SIG_KEYS.split() - sig_keys = [] - - for k in self.params.get("auth_sig_keys", "").split(): - k = k.strip() - if k not in allowed_sig_keys: - continue - sig_keys.append(k) - - # if empty, fallback to default - sig_keys = sig_keys or allowed_sig_keys - sig_keys = " ".join(sig_keys) - self.set_config("auth_sig_keys", sig_keys) - - # get user-input encryption keys - allowed_enc_keys = DEFAULT_ENC_KEYS.split() - enc_keys = [] - - for k in self.params.get("auth_enc_keys", "").split(): - k = k.strip() - if k not in allowed_enc_keys: - continue - enc_keys.append(k) - - # if empty, fallback to default - enc_keys = enc_keys or allowed_enc_keys - enc_keys = " ".join(enc_keys) - self.set_config("auth_enc_keys", enc_keys) + self.set_config("auth_sig_keys", self.configmap_params["auth_sig_keys"]) + self.set_config("auth_enc_keys", self.configmap_params["auth_enc_keys"]) # default exp = 48 hours + token lifetime (in hour) - exp = int(self.params["init_keys_exp"] + (3600 / 3600)) + exp = int(self.configmap_params["init_keys_exp"] + (3600 / 3600)) _, err, retcode = generate_openid_keys_hourly( self.get_secret("auth_openid_jks_pass"), @@ -308,8 +229,8 @@ def auth_ctx(self): f"{CERTS_DIR}/auth-keys.json", self.get_config("default_openid_jks_dn_name"), exp=exp, - sig_keys=sig_keys, - enc_keys=enc_keys, + sig_keys=self.configmap_params["auth_sig_keys"], + enc_keys=self.configmap_params["auth_enc_keys"], ) if retcode != 0: logger.error(f"Unable to generate auth keys; reason={err}") @@ -329,7 +250,7 @@ def auth_ctx(self): partial(encode_text, fr.read(), encoded_salt), ) - def web_ctx(self): + def transform_web_ctx(self): ssl_cert = f"{CERTS_DIR}/web_https.crt" ssl_key = f"{CERTS_DIR}/web_https.key" ssl_csr = f"{CERTS_DIR}/web_https.csr" @@ -348,14 +269,14 @@ def web_ctx(self): # check from mounted files if not (os.path.isfile(ssl_cert) and os.path.isfile(ssl_key)): # no mounted files, hence download from frontend - addr = os.environ.get("CN_INGRESS_ADDRESS") or self.ctx["config"]["hostname"] + addr = os.environ.get("CN_INGRESS_ADDRESS") or self.ctx["_configmap"]["hostname"] servername = os.environ.get("CN_INGRESS_SERVERNAME") or addr logger.warning( f"Unable to find mounted {ssl_cert} and {ssl_key}; " f"trying to download from {addr}:443 (servername {servername})" ) - cert_from_domain(addr, servername, 443, ssl_cert, ssl_key, self.ctx["config"]["hostname"]) + cert_from_domain(addr, servername, 443, ssl_cert, ssl_key, self.ctx["_configmap"]["hostname"]) # no mounted nor downloaded files, hence we need to create self-generated files if not (os.path.isfile(ssl_cert) and os.path.isfile(ssl_key)): @@ -417,87 +338,77 @@ def web_ctx(self): with open(ssl_key) as f: self.set_secret("ssl_key", f.read) - def couchbase_ctx(self): + def transform_couchbase_ctx(self): # TODO: move this to persistence-loader? self.set_config("couchbaseTrustStoreFn", "/etc/certs/couchbase.pkcs12") self.set_secret("couchbase_shib_user_password", get_random_chars) - self.set_secret("couchbase_password", self.params["couchbase_pw"]) - self.set_secret("couchbase_superuser_password", self.params["couchbase_superuser_pw"]) - - def sql_ctx(self): - self.set_secret("sql_password", self.params["sql_pw"]) + self.set_secret("couchbase_password", self.secret_params["couchbase_password"]) + self.set_secret("couchbase_superuser_password", self.secret_params["couchbase_superuser_password"]) - def generate(self): - opt_scopes = self.params["optional_scopes"] + def transform_sql_ctx(self): + self.set_secret("sql_password", self.secret_params["sql_password"]) - self.base_ctx() - self.auth_ctx() - self.web_ctx() + def transform_misc_ctx(self): + # pre-populate the rest of configmaps + for k, v in self.configmap_params.items(): + if v and k not in self.ctx["_configmap"]: + self.set_config(k, v) - # if "ldap" in opt_scopes: - # self.ldap_ctx() + # pre-populate the rest of secrets + for k, v in self.secret_params.items(): + if v and k not in self.ctx["_secret"]: + self.set_secret(k, v) - if "redis" in opt_scopes: - self.redis_ctx() - - # if "couchbase" in opt_scopes: - # self.couchbase_ctx() - - # if "sql" in opt_scopes: - # self.sql_ctx() - - # populated config - return self.ctx + def transform(self): + """Transform configmaps and secrets (if needed).""" + opt_scopes = json.loads(self.configmap_params["optional_scopes"]) + self.transform_base_ctx() + self.transform_auth_ctx() + self.transform_web_ctx() -def _save_generated_ctx(manager, data, type_): - if type_ == "config": - backend = manager.config - else: - backend = manager.secret + if "ldap" in opt_scopes: + self.transform_ldap_ctx() - logger.info(f"Saving {type_} to backend") - backend.set_all(data) + if "redis" in opt_scopes: + self.transform_redis_ctx() + if "couchbase" in opt_scopes: + self.transform_couchbase_ctx() -def _load_from_file(manager, filepath, type_): - ctx_manager = CtxManager(manager) - if type_ == "config": - setter = ctx_manager.set_config - backend = manager.config - else: - setter = ctx_manager.set_secret - backend = manager.secret + if "sql" in opt_scopes: + self.transform_sql_ctx() - logger.info(f"Loading {type_} from {filepath}") + self.transform_misc_ctx() - with open(filepath, "r") as f: - data = json.loads(f.read()) + # populated configuration + return self.ctx - ctx = data.get(f"_{type_}") - if not ctx: - logger.warning(f"Missing '_{type_}' key") - return + def save_loaded_ctx(self): + logger.info("Saving configuration to backends") - # tolerancy before checking existing key - time.sleep(5) + for type_ in ["_configmap", "_secret"]: + if type_ == "_configmap": + backend = self.manager.config + else: + backend = self.manager.secret + backend.set_all(self.ctx[type_]) - data = {k: setter(k, v) for k, v in ctx.items()} - backend.set_all(data) +def dump_to_file(manager, filepath): + logger.info(f"Saving configuration to {filepath}") -def _dump_to_file(manager, filepath, type_): - if type_ == "config": - backend = manager.config - else: - backend = manager.secret + data = {"_configmap": {}, "_secret": {}} - logger.info(f"Saving {type_} to {filepath}") + for type_ in ["_configmap", "_secret"]: + if type_ == "_configmap": + backend = manager.config + else: + backend = manager.secret + data[type_] = backend.get_all() - data = {f"_{type_}": backend.get_all()} - data = json.dumps(data, sort_keys=True, indent=4) with open(filepath, "w") as f: - f.write(data) + f.write(json.dumps(data, sort_keys=True, indent=4)) def cert_from_domain(addr, servername, port, certfile, keyfile, dns): @@ -581,91 +492,62 @@ def cli(): @cli.command() @click.option( - "--generate-file", - type=click.Path(exists=False), - help="Absolute path to file containing parameters for generating config and secret", - default=DEFAULT_GENERATE_FILE, - show_default=True, -) -@click.option( - "--config-file", + "--configuration-file", type=click.Path(exists=False), - help="Absolute path to file contains config", - default=DEFAULT_CONFIG_FILE, + help="Absolute path to file contains configmaps and secrets", + default=DEFAULT_CONFIGURATION_FILE, show_default=True, ) @click.option( - "--secret-file", + "--dump-file", type=click.Path(exists=False), - help="Absolute path to file contains secret", - default=DEFAULT_SECRET_FILE, + help="Absolute path to file contains dumped configmaps and secrets", + default=DEFAULT_DUMP_FILE, show_default=True, ) -def load(generate_file, config_file, secret_file): - """Loads config and secret from JSON files (generate if not exist). +def load(configuration_file, dump_file): + """Loads configmaps and secrets from JSON file (generate if not exist). """ deps = ["config_conn", "secret_conn"] wait_for(manager, deps=deps) # check whether config and secret in backend have been initialized - should_skip = as_boolean(os.environ.get("CN_CONFIGURATION_SKIP_INITIALIZED", False)) + should_skip = as_boolean(os.environ.get("CN_CONFIGURATOR_SKIP_INITIALIZED", False)) if should_skip and manager.config.get("hostname") and manager.secret.get("ssl_cert"): # config and secret may have been initialized - logger.info("Config and secret have been initialized") + logger.info("Configmaps and secrets have been initialized") return with manager.lock.create_lock("configurator-load"): - # there's no config and secret in backend, check whether to load from files - if os.path.isfile(config_file) and os.path.isfile(secret_file): - # load from existing files - logger.info(f"Re-using config and secret from {config_file} and {secret_file}") - _load_from_file(manager, config_file, "config") - _load_from_file(manager, secret_file, "secret") - return - - # no existing files, hence generate new config and secret from parameters - logger.info(f"Loading parameters from {generate_file}") - params, err, code = params_from_file(generate_file) + logger.info(f"Loading configmaps and secrets from {configuration_file}") + + params, err, code = load_schema_from_file(configuration_file) if code != 0: - logger.error(f"Unable to load parameters; reason={err}") + logger.error(f"Unable to load configmaps and secrets; reason={err}") raise click.Abort() - logger.info("Generating new config and secret") ctx_generator = CtxGenerator(manager, params) - ctx = ctx_generator.generate() - - # save config to its backend and file - _save_generated_ctx(manager, ctx["config"], "config") - _dump_to_file(manager, config_file, "config") + ctx_generator.transform() + ctx_generator.save_loaded_ctx() - # save secret to its backend and file - _save_generated_ctx(manager, ctx["secret"], "secret") - _dump_to_file(manager, secret_file, "secret") + # dump saved configuration to file + dump_to_file(manager, dump_file) @cli.command() @click.option( - "--config-file", + "--dump-file", type=click.Path(exists=False), - help="Absolute path to file to save config", - default=DEFAULT_CONFIG_FILE, + help="Absolute path to file contains dumped configmaps and secrets", + default=DEFAULT_DUMP_FILE, show_default=True, ) -@click.option( - "--secret-file", - type=click.Path(exists=False), - help="Absolute path to file to save secret", - default=DEFAULT_SECRET_FILE, - show_default=True, -) -def dump(config_file, secret_file): - """Dumps config and secret into JSON files. +def dump(dump_file): + """Dumps configmaps and secrets into JSON files. """ deps = ["config_conn", "secret_conn"] wait_for(manager, deps=deps) - - _dump_to_file(manager, config_file, "config") - _dump_to_file(manager, secret_file, "secret") + dump_to_file(manager, dump_file) if __name__ == "__main__": diff --git a/docker-jans-configurator/scripts/parameter.py b/docker-jans-configurator/scripts/parameter.py deleted file mode 100644 index d711946b9f8..00000000000 --- a/docker-jans-configurator/scripts/parameter.py +++ /dev/null @@ -1,165 +0,0 @@ -import json -import re - -from fqdn import FQDN -from marshmallow import EXCLUDE -from marshmallow import Schema -from marshmallow import validates -from marshmallow import validates_schema -from marshmallow import ValidationError -from marshmallow.fields import Email -from marshmallow.fields import List -from marshmallow.fields import Str -from marshmallow.fields import Int -from marshmallow.validate import ContainsOnly -from marshmallow.validate import Length -from marshmallow.validate import Predicate -from marshmallow.validate import Range - -PASSWD_RGX = re.compile( - r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*\W)[a-zA-Z0-9\S]{6,}$" -) - -DEFAULT_SCOPES = ( - "auth", - "config-api", -) - -OPTIONAL_SCOPES = ( - "ldap", - "couchbase", - "redis", - "sql", - - # these scopes are no longer needed; not removed for backward-compat - "fido2", - "casa", - "scim", -) - - -class ParamSchema(Schema): - class Meta: - unknown = EXCLUDE - - admin_pw = Str(required=True) - - city = Str(required=True) - - country_code = Str( - validate=[ - Length(2, 2), - Predicate( - "isupper", - error="Non-uppercased characters aren't allowed", - ), - ], - required=True, - ) - - email = Email(required=True) - - hostname = Str(required=True) - - org_name = Str(required=True) - - state = Str(required=True) - - optional_scopes = List( - Str(), - validate=ContainsOnly(OPTIONAL_SCOPES), - load_default=[], - ) - - ldap_pw = Str(load_default="", dump_default="") - - sql_pw = Str(load_default="", dump_default="") - - couchbase_pw = Str(load_default="", dump_default="") - - couchbase_superuser_pw = Str(load_default="", dump_default="") - - auth_sig_keys = Str(load_default="") - - auth_enc_keys = Str(load_default="") - - salt = Str(load_default="", dump_default="") - - init_keys_exp = Int( - validate=[ - Range(1), - ], - load_default=48, - dump_default=48, - strict=True, - ) - - @validates("hostname") - def validate_fqdn(self, value): - fqdn = FQDN(value) - if not fqdn.is_valid: - raise ValidationError("Invalid FQDN format.") - - @validates("admin_pw") - def validate_password(self, value, **kwargs): - if not PASSWD_RGX.search(value): - raise ValidationError( - "Must be at least 6 characters and include " - "one uppercase letter, one lowercase letter, one digit, " - "and one special character." - ) - - @validates_schema - def validate_ldap_pw(self, data, **kwargs): - if "ldap" in data["optional_scopes"]: - try: - self.validate_password(data["ldap_pw"]) - except ValidationError as exc: - raise ValidationError({"ldap_pw": exc.messages}) - - @validates_schema - def validate_ext_persistence_pw(self, data, **kwargs): - err = {} - scope_attr_map = [ - ("sql", "sql_pw"), - ("couchbase", "couchbase_pw"), - ] - - for scope, attr in scope_attr_map: - # note we don't enforce custom password validation as cloud-based - # databases may use password that not conform to our policy - # hence we simply check for empty password only - if scope in data["optional_scopes"] and data[attr] == "": - err[attr] = ["Empty password isn't allowed"] - - if err: - raise ValidationError(err) - - @validates("salt") - def validate_salt(self, value): - if value and len(value) != 24: - raise ValidationError("Length must be 24.") - - if value and not value.isalnum(): - raise ValidationError("Only alphanumeric characters are allowed") - - -def params_from_file(path): - out = {} - err = {} - code = 0 - - try: - with open(path) as f: - docs = json.loads(f.read()) - except (IOError, ValueError) as exc: - err = exc - code = 1 - return out, err, code - - try: - out = ParamSchema().load(docs) - except ValidationError as exc: - err = exc.messages - code = 1 - return out, err, code diff --git a/docker-jans-fido2/Dockerfile b/docker-jans-fido2/Dockerfile index ca2127a3283..34c46cb420f 100644 --- a/docker-jans-fido2/Dockerfile +++ b/docker-jans-fido2/Dockerfile @@ -61,7 +61,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-fido2/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=cc79f2b4c65b1e4361b6b790b576992866a21b8d +ENV JANS_SOURCE_VERSION=0538d19e269bb26ddbd81c7971251d6375d389c3 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) diff --git a/docker-jans-kc-scheduler/Dockerfile b/docker-jans-kc-scheduler/Dockerfile index 9fe2dca869f..e6923e03ec8 100644 --- a/docker-jans-kc-scheduler/Dockerfile +++ b/docker-jans-kc-scheduler/Dockerfile @@ -38,7 +38,7 @@ RUN wget -q https://repo1.maven.org/maven2/org/codehaus/janino/janino/3.1.9/jani # Assets sync # =========== -ENV JANS_SOURCE_VERSION=cc79f2b4c65b1e4361b6b790b576992866a21b8d +ENV JANS_SOURCE_VERSION=0538d19e269bb26ddbd81c7971251d6375d389c3 # note that as we're pulling from a monorepo (with multiple project in it) # we are using partial-clone and sparse-checkout to get the assets diff --git a/docker-jans-keycloak-link/Dockerfile b/docker-jans-keycloak-link/Dockerfile index 12a049dd1ce..78947acda49 100644 --- a/docker-jans-keycloak-link/Dockerfile +++ b/docker-jans-keycloak-link/Dockerfile @@ -61,7 +61,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-keycloak-link/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=cc79f2b4c65b1e4361b6b790b576992866a21b8d +ENV JANS_SOURCE_VERSION=0538d19e269bb26ddbd81c7971251d6375d389c3 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) diff --git a/docker-jans-link/Dockerfile b/docker-jans-link/Dockerfile index 3d2634495dc..1f0aa8f474f 100644 --- a/docker-jans-link/Dockerfile +++ b/docker-jans-link/Dockerfile @@ -61,7 +61,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-link/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=cc79f2b4c65b1e4361b6b790b576992866a21b8d +ENV JANS_SOURCE_VERSION=0538d19e269bb26ddbd81c7971251d6375d389c3 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) diff --git a/docker-jans-persistence-loader/Dockerfile b/docker-jans-persistence-loader/Dockerfile index a14b0a0a494..4fb59653144 100644 --- a/docker-jans-persistence-loader/Dockerfile +++ b/docker-jans-persistence-loader/Dockerfile @@ -16,7 +16,7 @@ RUN apk update \ # =========== # janssenproject/jans SHA commit -ENV JANS_SOURCE_VERSION=cc79f2b4c65b1e4361b6b790b576992866a21b8d +ENV JANS_SOURCE_VERSION=0538d19e269bb26ddbd81c7971251d6375d389c3 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup ARG JANS_SCRIPT_CATALOG_DIR=docs/script-catalog ARG JANS_CONFIG_API_RESOURCES=jans-config-api/server/src/main/resources diff --git a/docker-jans-persistence-loader/scripts/utils.py b/docker-jans-persistence-loader/scripts/utils.py index cc29ba722e2..bf01ba0b212 100644 --- a/docker-jans-persistence-loader/scripts/utils.py +++ b/docker-jans-persistence-loader/scripts/utils.py @@ -50,7 +50,7 @@ def get_jackrabbit_rmi_url(): def get_base_ctx(manager): - redis_pw = manager.secret.get("redis_pw") or "" + redis_pw = manager.secret.get("redis_password") or "" redis_pw_encoded = "" if redis_pw: diff --git a/docker-jans-saml/Dockerfile b/docker-jans-saml/Dockerfile index 396af50f979..e511a1bb6d9 100644 --- a/docker-jans-saml/Dockerfile +++ b/docker-jans-saml/Dockerfile @@ -35,7 +35,7 @@ RUN wget -q https://jenkins.jans.io/maven/io/jans/kc-jans-spi/${CN_VERSION}/kc-j # Assets sync # =========== -ENV JANS_SOURCE_VERSION=cc79f2b4c65b1e4361b6b790b576992866a21b8d +ENV JANS_SOURCE_VERSION=0538d19e269bb26ddbd81c7971251d6375d389c3 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) diff --git a/docker-jans-scim/Dockerfile b/docker-jans-scim/Dockerfile index c6ba1843021..8a7e11eb1c8 100644 --- a/docker-jans-scim/Dockerfile +++ b/docker-jans-scim/Dockerfile @@ -60,7 +60,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-scim/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=cc79f2b4c65b1e4361b6b790b576992866a21b8d +ENV JANS_SOURCE_VERSION=0538d19e269bb26ddbd81c7971251d6375d389c3 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup ARG JANS_SCIM_RESOURCE_DIR=jans-scim/server/src/main/resources diff --git a/docs/admin/reference/kubernetes/docker-jans-configurator.md b/docs/admin/reference/kubernetes/docker-jans-configurator.md index 0bcf76453c0..011e3ac1589 100644 --- a/docs/admin/reference/kubernetes/docker-jans-configurator.md +++ b/docs/admin/reference/kubernetes/docker-jans-configurator.md @@ -8,7 +8,7 @@ tags: ## Overview -Configuration manager is a special container used to load (generate/restore) and dump (backup) the configuration and secrets. +Configurator is a tool to load (generate/restore) and/or dump (backup) the configuration (consists of configmaps and secrets). ## Versions @@ -72,52 +72,62 @@ The following commands are supported by the container: ### load -The load command can be used either to generate or restore config and secret for the cluster. +The load command can be used either to generate/restore configmaps and secrets for the cluster. + +For fresh installation, generate the initial configuration by creating `/path/to/host/volume/configuration.json` similar to example below: -For fresh installation, generate the initial configuration and secret by creating `/path/to/host/volume/generate.json` similar to example below: ```json { - "hostname": "demoexample.jans.io", - "country_code": "US", - "state": "TX", - "city": "Austin", - "admin_pw": "S3cr3t+pass", - "ldap_pw": "S3cr3t+pass", - "email": "s@jans.io", - "org_name": "Gluu Inc." + "_configmap": { + "hostname": "demoexample.jans.io", + "country_code": "US", + "state": "TX", + "city": "Austin", + "admin_email": "s@jans.io", + "orgName": "Gluu Inc." + }, + "_secret": { + "admin_password": "S3cr3t+pass", + "ldap_password": "S3cr3t+pass" + } } ``` -**NOTE**: `generate.json` has optional attributes as seen below. +**NOTE**: `configuration.json` has optional attributes as seen below. -- `auth_sig_keys`: space-separated key algorithm for signing (default to `RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512`) -- `auth_enc_keys`: space-separated key algorithm for encryption (default to `RSA1_5 RSA-OAEP`) -- `optional_scopes`: list of scopes that will be used (supported scopes are `ldap`, `scim`, `fido2`, `couchbase`, `redis`, `sql`, `casa`; default to empty list) -- `ldap_pw`: user's password to access LDAP database (only used if `optional_scopes` list contains `ldap` scope) -- `sql_pw`: user's password to access SQL database (only used if `optional_scopes` list contains `sql` scope) -- `couchbase_pw`: user's password to access Couchbase database (only used if `optional_scopes` list contains `couchbase` scope) -- `couchbase_superuser_pw`: superusers password to access Couchbase database (only used if `optional_scopes` list contains `couchbase` scope) -- `salt`: user-defined salt (24 characters length); if omitted, salt will be generated automatically -- `init_keys_exp`: the initial keys expiration time in hours (default to `48`; extra 1 hour will be added for hard limit) +1. `_configmap`: -Example of generating `salt` value: + - `auth_sig_keys`: space-separated key algorithm for signing (default to `RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512`) + - `auth_enc_keys`: space-separated key algorithm for encryption (default to `RSA1_5 RSA-OAEP`) + - `optional_scopes`: list of optional scopes (as JSON string) that will be used (supported scopes are `ldap`, `couchbase`, `redis`, `sql`; default to empty list) + - `init_keys_exp`: the initial keys expiration time in hours (default to `48`; extra 1 hour will be added for hard limit) -``` -# using shell script -cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 24 | head -n 1 -# output: NFAG5g4R0NSkAZXHL8t2DScL +2. `_secret`: -# using python oneliner -python -c 'import random, string; print("".join(random.choices(string.ascii_letters + string.digits, k=24)))' -# ouput: HsPzqiPkRzNySWlOVui8Ilmw -``` + - `ldap_password`: user's password to access LDAP database (only used if `optional_scopes` list contains `ldap` scope) + - `sql_password`: user's password to access SQL database (only used if `optional_scopes` list contains `sql` scope) + - `couchbase_password`: user's password to access Couchbase database (only used if `optional_scopes` list contains `couchbase` scope) + - `couchbase_superuser_password`: superusers password to access Couchbase database (only used if `optional_scopes` list contains `couchbase` scope) + - `encoded_salt`: user-defined salt (24 characters length); if omitted, salt will be generated automatically -To generate initial config and secrets: + Example of generating `encoded_salt` value: -1. Create config map `config-generate-params` to store the contents of `generate.json` + ``` + # using shell script + cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 24 | head -n 1 + # output: NFAG5g4R0NSkAZXHL8t2DScL + + # using python oneliner + python -c 'import random, string; print("".join(random.choices(string.ascii_letters + string.digits, k=24)))' + # ouput: HsPzqiPkRzNySWlOVui8Ilmw + ``` + +To generate initial configmaps and secrets: + +1. Create config map `config-generate-params` to store the contents of `configuration.json` ```sh - kubectl create cm config-generate-params --from-file=generate.json + kubectl create cm config-generate-params --from-file=configuration.json ``` 1. Mount the configmap into container and apply the yaml: @@ -139,22 +149,23 @@ To generate initial config and secrets: - name: configurator-load image: ghcr.io/janssenproject/jans/configurator:1.1.4_dev volumeMounts: - - mountPath: /app/db/generate.json + - mountPath: /app/db/configuration.json name: config-generate-params - subPath: generate.json + subPath: configuration.json envFrom: - configMapRef: name: config-cm args: ["load"] ``` -To restore configuration and secrets from a backup of `/path/to/host/volume/config.json` and `/path/to/host/volume/secret.json`: mount the directory as `/app/db` inside the container: +A successful `load` command will dump the pre-populated configuration into `/app/db/configuration.out.json`. + +To restore configuration from `configuration.out.json` file: -1. Create config map `config-params` and `secret-params`: +1. Create config map `config-dump-params`: ```sh - kubectl create cm config-params --from-file=config.json - kubectl create cm secret-params --from-file=secret.json + kubectl create cm config-dump-params --from-file=configuration.out.json ``` 2. Mount the configmap into container and apply the yaml: @@ -169,32 +180,25 @@ To restore configuration and secrets from a backup of `/path/to/host/volume/conf spec: restartPolicy: Never volumes: - - name: config-params + - name: config-dump-params configMap: - name: config-params - - name: secret-params - configMap: - name: secret-params + name: config-dump-params containers: - name: configurator-load image: ghcr.io/janssenproject/jans/configurator:1.1.4_dev volumeMounts: - - mountPath: /app/db/config.json - name: config-params - subPath: config.json - - mountPath: /app/db/secret.json - name: secret-params - subPath: secret.json + - mountPath: /app/db/configuration.out.json + name: config-dump-params + subPath: configuration.out.json envFrom: - configMapRef: name: config-cm args: ["load"] ``` - ### dump -The dump command will dump all configuration and secrets from the backends saved into the `/app/db/config.json` and `/app/db/secret.json` files. +The dump command will dump all configuration from the backends saved into the `/app/db/configuration.out.json` file. ```yaml apiVersion: batch/v1 @@ -221,4 +225,4 @@ spec: Copy over the files to host -`kubectl cp config-init-load-job:/app/db .` +`kubectl cp configurator-dump-job:/app/db .` From 634e380d76217f1b7694631520fa33e2e0fbc8de Mon Sep 17 00:00:00 2001 From: Adam Albright Date: Thu, 25 Jul 2024 04:29:09 -0400 Subject: [PATCH 12/43] docs: correct typos in scim docs (#9013) * docs: correct typos in scim docs * docs: correct typos in scim docs Signed-off-by: Rehket --------- Signed-off-by: Rehket Co-authored-by: Dhaval D <343411+ossdhaval@users.noreply.github.com> --- docs/admin/scim/README.md | 8 ++++---- docs/admin/scim/bulk-users.md | 3 +-- docs/admin/scim/custom-attributes.md | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/admin/scim/README.md b/docs/admin/scim/README.md index 5e2c9c38eaa..bf7d6487461 100644 --- a/docs/admin/scim/README.md +++ b/docs/admin/scim/README.md @@ -6,10 +6,10 @@ tags: # Overview -**S**ystem for **C**ross-domain **I**dentity **M**anagement, in short **SCIM**, is a specification that simplifies the exchange of user identity information across different domains. The Janssen Server provides implementation for the SCIM specification. +**S**ystem for **C**ross-domain **I**dentity **M**anagement, in short **SCIM**, is a specification that simplifies the exchange of user identity information across different domains. The Janssen Server provides an implementation for the SCIM specification. -The specification defines reference schemas for users and groups along with REST API to manage them. For more details, refer to the current version of the specification that is governed by the following documents: [RFC 7642](https://tools.ietf.org/html/rfc7642), [RFC 7643](https://tools.ietf.org/html/rfc7643), and [RFC 7644](https://tools.ietf.org/html/rfc7644). +The specification defines reference schemas for users and groups along with REST API to manage them. For more details, refer to the current version of the specification governed by the following documents: [RFC 7642](https://tools.ietf.org/html/rfc7642), [RFC 7643](https://tools.ietf.org/html/rfc7643), and [RFC 7644](https://tools.ietf.org/html/rfc7644). -Developers can think of **SCIM** merely as a **REST API** with endpoints exposing **CRUD** functionality (create, read, update and delete). +Developers can think of **SCIM** merely as a **REST API** with endpoints exposing **CRUD** functionality (create, read, update, and delete). -This section covers how to configure, protect and monitor the Janssen Server SCIM module and its APIs. +This section covers how to configure, protect, and monitor the Janssen Server SCIM module and its APIs. diff --git a/docs/admin/scim/bulk-users.md b/docs/admin/scim/bulk-users.md index 2acd34992ae..9e678023f7f 100644 --- a/docs/admin/scim/bulk-users.md +++ b/docs/admin/scim/bulk-users.md @@ -70,10 +70,9 @@ An operation is a JSON document with the following: } ] } -} ``` -The above paylod illustrates how to insert three users with different details each. +The above payload illustrates how to insert three users with different details each. ## Bulk response diff --git a/docs/admin/scim/custom-attributes.md b/docs/admin/scim/custom-attributes.md index 4bd64165a6e..120a9cd4249 100644 --- a/docs/admin/scim/custom-attributes.md +++ b/docs/admin/scim/custom-attributes.md @@ -13,7 +13,7 @@ Although the schema covers many attributes one might think of, at times you will * Add an attribute to LDAP schema -* Include the new attribute in an LDAP's objectclass such as jansPerson +* Include the new attribute in an LDAP's object class such as jansPerson * Register and activate your new attribute through **Jans TUI**. @@ -39,7 +39,7 @@ To customize the URI associated to the extension (whose default value is `urn:ie * Set a value in the field `User Extension Schema URI` * Save the changes -![scim-extention](https://github.com/JanssenProject/jans/assets/43112579/fb5b9d5c-8b17-4be0-af6c-d36389de82d2) +![scim-extension](https://github.com/JanssenProject/jans/assets/43112579/fb5b9d5c-8b17-4be0-af6c-d36389de82d2) From f4c9e0b8a23a32e97328ed35cf9ea393487db330 Mon Sep 17 00:00:00 2001 From: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:40:16 +0300 Subject: [PATCH 13/43] ci: fix doc check (#9022) Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> --- .github/workflows/documenation_check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/documenation_check.yml b/.github/workflows/documenation_check.yml index f2c8aafbd12..642d4e84e64 100644 --- a/.github/workflows/documenation_check.yml +++ b/.github/workflows/documenation_check.yml @@ -41,7 +41,7 @@ jobs: run: | PULL_NUMBER=${{ github.event.pull_request.number }} echo "Parsing commits from PR $PULL_NUMBER" - MESSAGE=$(gh pr view "$PULL_NUMBER" --json commits | jq '.' | grep "messageHeadline" | cut -d: -f2- | grep "^docs" || echo "") + MESSAGE=$(gh pr view "$PULL_NUMBER" --json commits | jq -r '.commits[].messageHeadline' | grep "^docs" || echo "") echo "$MESSAGE" if [[ -z "$MESSAGE" ]]; then echo "conventional commit starting with docs: does not exist. Checking if user confirmed no impact on docs in PR body" From 93571285c31d70878e6647f9f70ad682255e8336 Mon Sep 17 00:00:00 2001 From: Isman Firmansyah Date: Thu, 25 Jul 2024 17:02:27 +0700 Subject: [PATCH 14/43] fix(cloud-native): remove oxauth variable naming inside templates (#9027) Signed-off-by: iromli Co-authored-by: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> --- docker-jans-all-in-one/Dockerfile | 2 +- docker-jans-persistence-loader/Dockerfile | 2 +- docker-jans-persistence-loader/scripts/hooks.py | 2 +- docker-jans-persistence-loader/scripts/utils.py | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docker-jans-all-in-one/Dockerfile b/docker-jans-all-in-one/Dockerfile index 5f28014af27..1f4b6b25526 100644 --- a/docker-jans-all-in-one/Dockerfile +++ b/docker-jans-all-in-one/Dockerfile @@ -58,7 +58,7 @@ RUN apk update \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=0538d19e269bb26ddbd81c7971251d6375d389c3 +ENV JANS_SOURCE_VERSION=1bde1316a3abc8f4e48462d0a2670db901c70aca # note that as we're pulling from a monorepo (with multiple project in it) # we are using partial-clone and sparse-checkout to get the assets diff --git a/docker-jans-persistence-loader/Dockerfile b/docker-jans-persistence-loader/Dockerfile index 4fb59653144..dac9f3f4dc9 100644 --- a/docker-jans-persistence-loader/Dockerfile +++ b/docker-jans-persistence-loader/Dockerfile @@ -16,7 +16,7 @@ RUN apk update \ # =========== # janssenproject/jans SHA commit -ENV JANS_SOURCE_VERSION=0538d19e269bb26ddbd81c7971251d6375d389c3 +ENV JANS_SOURCE_VERSION=1bde1316a3abc8f4e48462d0a2670db901c70aca ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup ARG JANS_SCRIPT_CATALOG_DIR=docs/script-catalog ARG JANS_CONFIG_API_RESOURCES=jans-config-api/server/src/main/resources diff --git a/docker-jans-persistence-loader/scripts/hooks.py b/docker-jans-persistence-loader/scripts/hooks.py index 9584e8bbf4e..1ac02d6f81c 100644 --- a/docker-jans-persistence-loader/scripts/hooks.py +++ b/docker-jans-persistence-loader/scripts/hooks.py @@ -13,7 +13,7 @@ def merge_auth_keystore_ctx_hook(manager, ctx: dict[str, _t.Any]) -> dict[str, _t.Any]: # maintain compatibility with upstream template - ctx["oxauth_openid_jks_fn"] = manager.config.get("auth_openid_jks_fn") + ctx["jans_auth_openid_jks_fn"] = manager.config.get("auth_openid_jks_fn") return ctx diff --git a/docker-jans-persistence-loader/scripts/utils.py b/docker-jans-persistence-loader/scripts/utils.py index bf01ba0b212..a69c217208f 100644 --- a/docker-jans-persistence-loader/scripts/utils.py +++ b/docker-jans-persistence-loader/scripts/utils.py @@ -104,7 +104,7 @@ def get_base_ctx(manager): 'hostname': manager.config.get('hostname'), 'idp_client_id': manager.config.get('idp_client_id'), 'idpClient_encoded_pw': manager.secret.get('idpClient_encoded_pw'), - 'oxauth_openid_key_base64': manager.secret.get('auth_openid_key_base64'), + 'jans_auth_openid_key_base64': manager.secret.get('auth_openid_key_base64'), "encoded_admin_password": manager.secret.get('encoded_admin_password'), 'admin_email': manager.config.get('admin_email'), @@ -121,7 +121,7 @@ def get_base_ctx(manager): "pairwiseCalculationSalt": manager.secret.get("pairwiseCalculationSalt"), "default_openid_jks_dn_name": manager.config.get("default_openid_jks_dn_name"), # maintain compatibility with upstream template - "oxauth_openid_jks_pass": manager.secret.get("auth_openid_jks_pass"), + "jans_auth_openid_jks_pass": manager.secret.get("auth_openid_jks_pass"), "auth_legacyIdTokenClaims": manager.config.get("auth_legacyIdTokenClaims"), "auth_openidScopeBackwardCompatibility": manager.config.get("auth_openidScopeBackwardCompatibility"), @@ -160,9 +160,9 @@ def merge_auth_ctx(ctx): basedir = '/app/templates/jans-auth' file_mappings = { - 'oxauth_static_conf_base64': 'jans-auth-static-conf.json', - 'oxauth_error_base64': 'jans-auth-errors.json', - "oxauth_config_base64": "jans-auth-config.json", + 'jans_auth_static_conf_base64': 'jans-auth-static-conf.json', + 'jans_auth_error_base64': 'jans-auth-errors.json', + "jans_auth_config_base64": "jans-auth-config.json", } for key, file_ in file_mappings.items(): From 1a3bd72293c59ad77220550a3e95a6974453b4c7 Mon Sep 17 00:00:00 2001 From: Amro Misbah Date: Thu, 25 Jul 2024 21:59:02 +0300 Subject: [PATCH 15/43] fix(charts): pass correct indentation (#9033) --- charts/janssen/charts/auth-server/templates/deployment.yml | 4 ++-- charts/janssen/charts/fido2/templates/deployment.yml | 4 ++-- charts/janssen/charts/scim/templates/deployment.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/charts/janssen/charts/auth-server/templates/deployment.yml b/charts/janssen/charts/auth-server/templates/deployment.yml index 1a5274221c2..810354541a0 100644 --- a/charts/janssen/charts/auth-server/templates/deployment.yml +++ b/charts/janssen/charts/auth-server/templates/deployment.yml @@ -48,8 +48,8 @@ spec: env: - name: CN_AUTH_JAVA_OPTIONS value: {{ include "auth-server.customJavaOptions" . | trim }} - {{- include "auth-server.usr-envs" . | indent 12 }} - {{- include "auth-server.usr-secret-envs" . | indent 12 }} + {{- include "auth-server.usr-envs" . | indent 10 }} + {{- include "auth-server.usr-secret-envs" . | indent 10 }} securityContext: runAsUser: 1000 runAsNonRoot: true diff --git a/charts/janssen/charts/fido2/templates/deployment.yml b/charts/janssen/charts/fido2/templates/deployment.yml index e7849ac2a3d..8f861fce7df 100644 --- a/charts/janssen/charts/fido2/templates/deployment.yml +++ b/charts/janssen/charts/fido2/templates/deployment.yml @@ -51,8 +51,8 @@ spec: env: - name: CN_FIDO2_JAVA_OPTIONS value: {{ include "fido2.customJavaOptions" . | trim }} - {{- include "fido2.usr-envs" . | indent 12 }} - {{- include "fido2.usr-secret-envs" . | indent 12 }} + {{- include "fido2.usr-envs" . | indent 10 }} + {{- include "fido2.usr-secret-envs" . | indent 10 }} {{- if or (eq .Values.global.storageClass.provisioner "kubernetes.io/aws-ebs") (eq .Values.global.storageClass.provisioner "openebs.io/local") ( .Values.customScripts) }} command: - /bin/sh diff --git a/charts/janssen/charts/scim/templates/deployment.yml b/charts/janssen/charts/scim/templates/deployment.yml index 63d3512e28a..9fabcbc80e8 100644 --- a/charts/janssen/charts/scim/templates/deployment.yml +++ b/charts/janssen/charts/scim/templates/deployment.yml @@ -51,8 +51,8 @@ spec: env: - name: CN_SCIM_JAVA_OPTIONS value: {{ include "scim.customJavaOptions" . | trim }} - {{- include "scim.usr-envs" . | indent 12 }} - {{- include "scim.usr-secret-envs" . | indent 12 }} + {{- include "scim.usr-envs" . | indent 10 }} + {{- include "scim.usr-secret-envs" . | indent 10 }} {{- if or (eq .Values.global.storageClass.provisioner "kubernetes.io/aws-ebs") (eq .Values.global.storageClass.provisioner "openebs.io/local") ( .Values.customScripts) }} command: - /bin/sh From 3b635fe04dd1cfa82c0e8a34a41cac0658791c5f Mon Sep 17 00:00:00 2001 From: Isman Firmansyah Date: Fri, 26 Jul 2024 02:01:47 +0700 Subject: [PATCH 16/43] feat(cloud-native): import SSL cert of internal proxy service (#9029) * feat(cloud-native): import SSL cert of internal proxy service Signed-off-by: iromli * docs(cloud-native): update docker-jans-config-api reference Signed-off-by: iromli --------- Signed-off-by: iromli Co-authored-by: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> --- docker-jans-config-api/Dockerfile | 2 +- docker-jans-config-api/README.md | 3 +- docker-jans-config-api/scripts/bootstrap.py | 8 ++-- docker-jans-config-api/scripts/plugins.py | 37 ++++++++++++++++++- .../kubernetes/docker-jans-config-api.md | 3 +- 5 files changed, 44 insertions(+), 9 deletions(-) diff --git a/docker-jans-config-api/Dockerfile b/docker-jans-config-api/Dockerfile index f83e7b2ac72..4d214b4ad16 100644 --- a/docker-jans-config-api/Dockerfile +++ b/docker-jans-config-api/Dockerfile @@ -41,7 +41,7 @@ RUN wget -q https://maven.jans.io/maven/io/jans/jython-installer/${JYTHON_VERSIO # ========== ENV CN_VERSION=1.1.4-SNAPSHOT -ENV CN_BUILD_DATE='2024-07-08 10:01' +ENV CN_BUILD_DATE='2024-07-24 10:59' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-config-api-server/${CN_VERSION}/jans-config-api-server-${CN_VERSION}.war diff --git a/docker-jans-config-api/README.md b/docker-jans-config-api/README.md index 101accbe53d..576682b65db 100644 --- a/docker-jans-config-api/README.md +++ b/docker-jans-config-api/README.md @@ -76,8 +76,8 @@ The following environment variables are supported by the container: - `CN_GOOGLE_SPANNER_DATABASE_ID`: Google Spanner database ID. - `CN_CONFIG_API_APP_LOGGERS`: Custom logging configuration in JSON-string format with hash type (see [Configure app loggers](#configure-app-loggers) section for details). - `CN_CONFIG_API_PLUGINS`: Comma-separated plugin names that should be enabled (available plugins are `admin-ui`, `scim`, `fido2`, `user-mgt`, `jans-link`, `kc-saml`, `kc-link`, `lock`). Note that unknown plugin name will be ignored. +- `CN_TOKEN_SERVER_BASE_URL`: Base URL of token server (default to empty). - `CN_TOKEN_SERVER_CERT_FILE`: Path to token server certificate (default to `/etc/certs/token_server.crt`). -- `CN_TOKEN_SERVER_BASE_HOSTNAME`: Hostname of token server (default to empty string). - `CN_ADMIN_UI_PLUGIN_LOGGERS`: Custom logging configuration for AdminUI plugin in JSON-string format with hash type (see [Configure plugin loggers](#configure-plugin-loggers) section for details). - `CN_PROMETHEUS_PORT`: Port used by Prometheus JMX agent (default to empty string). To enable Prometheus JMX agent, set the value to a number. See [Exposing metrics](#exposing-metrics) for details. - `CN_SQL_DB_HOST`: Hostname of the SQL database (default to `localhost`). @@ -215,4 +215,3 @@ i.e. `http://container:9093/metrics`. Note that Prometheus JMX exporter uses pre-defined config file (see `conf/prometheus-config.yaml`). To customize the config, mount custom config file to `/opt/prometheus/prometheus-config.yaml` inside the container. - diff --git a/docker-jans-config-api/scripts/bootstrap.py b/docker-jans-config-api/scripts/bootstrap.py index f38312643f7..c78aaf97380 100644 --- a/docker-jans-config-api/scripts/bootstrap.py +++ b/docker-jans-config-api/scripts/bootstrap.py @@ -363,9 +363,11 @@ def ctx(self) -> dict[str, _t.Any]: hostname = self.manager.config.get("hostname") approved_issuer = [hostname] - token_server_hostname = os.environ.get("CN_TOKEN_SERVER_BASE_HOSTNAME") - if token_server_hostname and token_server_hostname not in approved_issuer: - approved_issuer.append(token_server_hostname) + if token_server_url := os.environ.get("CN_TOKEN_SERVER_BASE_URL"): + token_server_hostname = urlparse(token_server_url).hostname + + if token_server_hostname and token_server_hostname not in approved_issuer: + approved_issuer.append(token_server_hostname) ctx = { "hostname": hostname, diff --git a/docker-jans-config-api/scripts/plugins.py b/docker-jans-config-api/scripts/plugins.py index dd7e954fe27..7959cf5eaa1 100644 --- a/docker-jans-config-api/scripts/plugins.py +++ b/docker-jans-config-api/scripts/plugins.py @@ -1,8 +1,10 @@ import logging.config import os import shutil +from urllib.parse import urlparse from jans.pycloudlib.utils import cert_to_truststore +from jans.pycloudlib.utils import get_server_certificate from settings import LOGGING_CONFIG @@ -61,8 +63,16 @@ def setup(self): def import_token_server_cert(self): cert_file = os.environ.get("CN_TOKEN_SERVER_CERT_FILE", "/etc/certs/token_server.crt") + if not os.path.isfile(cert_file): - self.manager.secret.to_file("ssl_cert", cert_file) + # check if token server is not the fqdn + base_url = os.environ.get("CN_TOKEN_SERVER_BASE_URL") + + if base_url: + # download from given URL + self.pull_token_server_cert(base_url, cert_file) + else: + self.manager.secret.to_file("ssl_cert", cert_file) cert_to_truststore( "token_server", @@ -70,3 +80,28 @@ def import_token_server_cert(self): "/opt/java/lib/security/cacerts", "changeit", ) + + def pull_token_server_cert(self, base_url, cert_file): + logger.info(f"Downloading certificate from {base_url}") + + parsed_url = urlparse(base_url) + host = parsed_url.hostname + port = parsed_url.port + + # port might not be defined in the given URL + if not port: + # resolve port as last segment of netloc + port = parsed_url.netloc.split(":")[-1] + + # possible edge-cases while parsing netloc: + # + # - empty port, e.g. `localhost:` + # - missing port, e.g. `localhost` + if (not port or port == host): + if parsed_url.scheme == "https": + port = 443 + else: + port = 80 + + # download the cert (if possible) + get_server_certificate(host, port, cert_file) diff --git a/docs/admin/reference/kubernetes/docker-jans-config-api.md b/docs/admin/reference/kubernetes/docker-jans-config-api.md index 101accbe53d..576682b65db 100644 --- a/docs/admin/reference/kubernetes/docker-jans-config-api.md +++ b/docs/admin/reference/kubernetes/docker-jans-config-api.md @@ -76,8 +76,8 @@ The following environment variables are supported by the container: - `CN_GOOGLE_SPANNER_DATABASE_ID`: Google Spanner database ID. - `CN_CONFIG_API_APP_LOGGERS`: Custom logging configuration in JSON-string format with hash type (see [Configure app loggers](#configure-app-loggers) section for details). - `CN_CONFIG_API_PLUGINS`: Comma-separated plugin names that should be enabled (available plugins are `admin-ui`, `scim`, `fido2`, `user-mgt`, `jans-link`, `kc-saml`, `kc-link`, `lock`). Note that unknown plugin name will be ignored. +- `CN_TOKEN_SERVER_BASE_URL`: Base URL of token server (default to empty). - `CN_TOKEN_SERVER_CERT_FILE`: Path to token server certificate (default to `/etc/certs/token_server.crt`). -- `CN_TOKEN_SERVER_BASE_HOSTNAME`: Hostname of token server (default to empty string). - `CN_ADMIN_UI_PLUGIN_LOGGERS`: Custom logging configuration for AdminUI plugin in JSON-string format with hash type (see [Configure plugin loggers](#configure-plugin-loggers) section for details). - `CN_PROMETHEUS_PORT`: Port used by Prometheus JMX agent (default to empty string). To enable Prometheus JMX agent, set the value to a number. See [Exposing metrics](#exposing-metrics) for details. - `CN_SQL_DB_HOST`: Hostname of the SQL database (default to `localhost`). @@ -215,4 +215,3 @@ i.e. `http://container:9093/metrics`. Note that Prometheus JMX exporter uses pre-defined config file (see `conf/prometheus-config.yaml`). To customize the config, mount custom config file to `/opt/prometheus/prometheus-config.yaml` inside the container. - From 16b2c3f5d6905cf1f78616d4aacaea95dfd18e91 Mon Sep 17 00:00:00 2001 From: Michael Schwartz Date: Thu, 25 Jul 2024 15:10:56 -0500 Subject: [PATCH 17/43] docs: Cedarling Overview edits for readability. (#9030) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Signed-off-by: Michael Schwartz Co-authored-by: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> --- docs/admin/lock/cedarling.md | 50 ++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/admin/lock/cedarling.md b/docs/admin/lock/cedarling.md index 4cc077db0fa..8c87b260bb5 100644 --- a/docs/admin/lock/cedarling.md +++ b/docs/admin/lock/cedarling.md @@ -9,13 +9,15 @@ tags: ## Cedarling Authorization -The Cedarling is a local, autonomous Policy Decision Point, or "PDP". It runs as a local -WebAssembly ("WASM") component--you can call it directly in the browser from a JavaScript -function. It can also run as a cloud function to provide authorization for server-side apps. -With each authorization call, the Cedarling has all the policies and data it -needs to make a fast, local decision. The Cedarling's authorization function is *deterministic*. -The Cedarling always returns either `permit` or `forbid`. You will never get an error -indicating a network timeout, or a divide by zero error. It is also very fast. +The Cedarling, as its name suggests, enables you to define the security rules for your application +in [Cedar](https://www.cedarpolicy.com/en) policy syntax. + +The Cedarling is a local, autonomous Policy Decision Point, or "PDP", distributed as a +WebAssembly ("WASM") component. WASM components can run directly in the browser or in a cloud +function. The Cedarling's main job is to `permit` or `forbid` requests based on enterprise-approved +policies. The input to the policies are JWT tokens (which provides the Principal), the requested +Action and Resource. The Cedarling rapidly evaluates authorization requests because it has all the +policies and data it needs to make a local decision. ![](../../assets/lock-cedarling-diagram-1.jpg) @@ -29,25 +31,22 @@ enterprise deployment, this audit log is sent for central archiving. Where does the Cedarling get the data for policy evaluation? The data is contained in the authorization request itself which has the OAuth and OpenID JWTs and details about the resource -and requested action. Most modern applications rely on a federated identity provider or "IDP". -Leveraing the JWT tokens to identify the person and software making the request +and requested action. ![](../../assets/lock-cedarling-diagram-2.jpg) Two JWT tokens in particular are typical: (1) an OpenID Connect id_token and (2) an OAuth access -token. The id_token represents a user authentication event, and the access token represents a +token. The id_token represents a user authentication event. The access token represents a client authentication event. The Cedarling can trust the id_token and access token to extract the User, -Role and Client pricipals. The tokens also contain other interesting contextual data. An OpenID -Connect id_token JWT, as a record of an authentication event, tells you who authenticated, when -they authenticated, how they authenticatated, and other claims like the subject's Roles. An OAuth -Access Token JWT can tell you information about the software that obtained the JWT, its extent -of access as defined by the OAuth Authorization Server (*i.e.* the values of the `scope` claim), or -other claims--domains frequently enhance the access token to contain business specific data needed -for policy evaluation. If an OpenID Userinfo token is sent to the cedarling, it is combined with the -id_token to paint a fuller picture of the User's claims. - -The Cedarling, as its name suggests, enables you to define the security rules for your application -in [Cedar](https://www.cedarpolicy.com/en) policy syntax. Cedar was invented by Amazon for their +Role and Client Principals. These tokens also contain other interesting contextual data. An OpenID +Connect id_token JWT tells you who authenticated, when they authenticated, how they authenticatated, +and other claims like the User's roles. An OAuth Access Token JWT can tell you information about the +software that obtained the JWT, its extent of access as defined by the OAuth Authorization Server +(*i.e.* the values of the `scope` claim), or other claims--domains frequently enhance the access token to +contain business specific data needed for policy evaluation. If an OpenID Userinfo token is sent to the +Cedarling, it is combined with the id_token to paint a fuller picture of the User's claims. + +Cedar was invented by Amazon for their [Verified Permission](https://aws.amazon.com/verified-permissions/) service. It uses the **PARC** syntax: **P**rincipal, **A**ction, **R**esource, **C**ontext. Principal-Action-Resource is typical for most authorization solutions. For example, you may have a policy that says *Admins* can *write* @@ -55,10 +54,10 @@ to the *config* folder. In this example, the *Admin* Role is the Principal, *wri and the *config* folder is the Resource. The Context is used to specify information about the enivironment, like the time of day or network address. -The Cedarling authorizes a person using a certain piece of software to do something. From -a logical perspective, `person_allowed AND client_allowed` must be `True`. While this seems pretty -simple, a person may be either explicitly allowed, or have a role that enables access. For example, -`person_allowed` may be equal to `True` if `user=mike OR role=SuperUser`. +The Cedarling authorizes a person using a certain piece of software. From a logical perspective, +`person_allowed AND client_allowed` must be `True`. A person may be either explicitly allowed, or +have a role that enables access. For example, `person_allowed` is `True` if +`user=mike OR role=SuperUser`. ![](../../assets/lock-cedarling-diagram-3.jpg) @@ -70,6 +69,7 @@ input = { "access_token": "eyJhbGc....", "id_token": "eyJjbGc...", "userinfo_token": "eyJjbGc...", + "tx_token": "eyJjbGc...", "resource": {"Ticket": {"id": "12345", "creator": "foo@bar.com", "organization": "Acme"}}, "action": "View", "context": { From bb229b14e0a8d74c70f735a0e3941fe63d8f5e35 Mon Sep 17 00:00:00 2001 From: Safin Wasi <6601566+SafinWasi@users.noreply.github.com> Date: Fri, 26 Jul 2024 01:58:57 -0500 Subject: [PATCH 18/43] docs(jans-lock): add default schema (#9020) * docs(jans-lock): add default schema Signed-off-by: SafinWasi <6601566+SafinWasi@users.noreply.github.com> * docs(jans-lock): update names and location Signed-off-by: SafinWasi <6601566+SafinWasi@users.noreply.github.com> --------- Signed-off-by: SafinWasi <6601566+SafinWasi@users.noreply.github.com> Co-authored-by: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> --- jans-lock/schema/cedarling_core_schema.json | 384 ++++++++++++++++++ jans-lock/schema/cedarling_core_schema.schema | 118 ++++++ 2 files changed, 502 insertions(+) create mode 100644 jans-lock/schema/cedarling_core_schema.json create mode 100644 jans-lock/schema/cedarling_core_schema.schema diff --git a/jans-lock/schema/cedarling_core_schema.json b/jans-lock/schema/cedarling_core_schema.json new file mode 100644 index 00000000000..83b69585589 --- /dev/null +++ b/jans-lock/schema/cedarling_core_schema.json @@ -0,0 +1,384 @@ +{ + "Jans": { + "commonTypes": { + "email_address": { + "type": "Record", + "attributes": { + "domain": { + "type": "String" + }, + "id": { + "type": "String" + } + } + }, + "Context": { + "type": "Record", + "attributes": { + "browser": { + "type": "String" + }, + "current_time": { + "type": "Long" + }, + "device_health": { + "type": "Set", + "element": { + "type": "String" + } + }, + "fraud_indicators": { + "type": "Set", + "element": { + "type": "String" + } + }, + "geolocation": { + "type": "Set", + "element": { + "type": "String" + } + }, + "network": { + "type": "Extension", + "name": "ipaddr" + }, + "network_type": { + "type": "String" + }, + "operating_system": { + "type": "String" + } + } + }, + "Url": { + "type": "Record", + "attributes": { + "host": { + "type": "String" + }, + "path": { + "type": "String" + }, + "protocol": { + "type": "String" + } + } + } + }, + "entityTypes": { + "TrustedIssuer": { + "shape": { + "type": "Record", + "attributes": { + "issuer_entity_id": { + "type": "Url" + } + } + } + }, + "HTTP_Request": { + "shape": { + "type": "Record", + "attributes": { + "accept": { + "type": "Set", + "element": { + "type": "String" + } + }, + "header": { + "type": "Set", + "element": { + "type": "String" + } + }, + "url": { + "type": "Url" + } + } + } + }, + "Userinfo_token": { + "shape": { + "type": "Record", + "attributes": { + "aud": { + "type": "String" + }, + "birthdate": { + "type": "String" + }, + "email": { + "type": "email_address" + }, + "exp": { + "type": "Long" + }, + "iat": { + "type": "Long" + }, + "iss": { + "type": "Entity", + "name": "TrustedIssuer" + }, + "jti": { + "type": "String" + }, + "name": { + "type": "String" + }, + "phone_number": { + "type": "String" + }, + "role": { + "type": "Set", + "element": { + "type": "String" + } + }, + "sub": { + "type": "String" + } + } + } + }, + "id_token": { + "shape": { + "type": "Record", + "attributes": { + "acr": { + "type": "Set", + "element": { + "type": "String" + } + }, + "amr": { + "type": "String" + }, + "aud": { + "type": "String" + }, + "azp": { + "type": "String" + }, + "birthdate": { + "type": "String" + }, + "email": { + "type": "email_address" + }, + "exp": { + "type": "Long" + }, + "iat": { + "type": "Long" + }, + "iss": { + "type": "Entity", + "name": "TrustedIssuer" + }, + "jti": { + "type": "String" + }, + "name": { + "type": "String" + }, + "phone_number": { + "type": "String" + }, + "role": { + "type": "Set", + "element": { + "type": "String" + } + }, + "sub": { + "type": "String" + } + } + } + }, + "Client": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { + "type": "String" + }, + "iss": { + "type": "Entity", + "name": "TrustedIssuer" + } + } + } + }, + "User": { + "memberOfTypes": [ + "Role" + ], + "shape": { + "type": "Record", + "attributes": { + "email": { + "type": "email_address" + }, + "phone_number": { + "type": "String" + }, + "role": { + "type": "Set", + "element": { + "type": "String" + } + }, + "sub": { + "type": "String" + }, + "username": { + "type": "String" + } + } + } + }, + "Access_token": { + "shape": { + "type": "Record", + "attributes": { + "aud": { + "type": "String" + }, + "exp": { + "type": "Long" + }, + "iat": { + "type": "Long" + }, + "iss": { + "type": "Entity", + "name": "TrustedIssuer" + }, + "jti": { + "type": "String" + }, + "nbf": { + "type": "Long" + }, + "scope": { + "type": "String" + } + } + } + }, + "Role": {}, + "Application": { + "shape": { + "type": "Record", + "attributes": { + "client": { + "type": "Entity", + "name": "Client" + }, + "name": { + "type": "String" + } + } + } + } + }, + "actions": { + "HEAD": { + "appliesTo": { + "resourceTypes": [ + "HTTP_Request" + ], + "principalTypes": [ + "Client" + ], + "context": { + "type": "Context" + } + } + }, + "Access": { + "appliesTo": { + "resourceTypes": [ + "Application" + ], + "principalTypes": [ + "User", + "Role" + ], + "context": { + "type": "Context" + } + } + }, + "GET": { + "appliesTo": { + "resourceTypes": [ + "HTTP_Request" + ], + "principalTypes": [ + "Client" + ], + "context": { + "type": "Context" + } + } + }, + "PATCH": { + "appliesTo": { + "resourceTypes": [ + "HTTP_Request" + ], + "principalTypes": [ + "Client" + ], + "context": { + "type": "Context" + } + } + }, + "PUT": { + "appliesTo": { + "resourceTypes": [ + "HTTP_Request" + ], + "principalTypes": [ + "Client" + ], + "context": { + "type": "Context" + } + } + }, + "POST": { + "appliesTo": { + "resourceTypes": [ + "HTTP_Request" + ], + "principalTypes": [ + "Client" + ], + "context": { + "type": "Context" + } + } + }, + "DELETE": { + "appliesTo": { + "resourceTypes": [ + "HTTP_Request" + ], + "principalTypes": [ + "Client" + ], + "context": { + "type": "Context" + } + } + } + } + } +} diff --git a/jans-lock/schema/cedarling_core_schema.schema b/jans-lock/schema/cedarling_core_schema.schema new file mode 100644 index 00000000000..f7d37f85153 --- /dev/null +++ b/jans-lock/schema/cedarling_core_schema.schema @@ -0,0 +1,118 @@ +namespace Jans { + type Context = { + "network": ipaddr, + "network_type": String, + "browser": String, + "operating_system": String, + "device_health": Set, + "current_time": Long, + "geolocation": Set, + "fraud_indicators": Set, + }; + type Url = { + "protocol": String, + "host": String, + "path": String, + }; + type email_address = { + id: String, + domain: String, + }; + entity TrustedIssuer = { + "issuer_entity_id": Url, + }; + entity Client = { + "client_id": String, + "iss": TrustedIssuer, + }; + entity HTTP_Request = { + "url": Url, + "header": Set, + "accept": Set, + }; + entity Application = { + "name": String, + "client": Client, + }; + entity Role; + entity User in [Role] { + "sub": String, + "username": String, + "email": email_address, + "phone_number": String, + "role": Set, + }; + entity Access_token = { + "aud": String, + "exp": Long, + "iat": Long, + "iss": TrustedIssuer, + "jti": String, + "nbf": Long, + "scope": String, + }; + entity id_token = { + "acr": Set, + "amr": String, + "aud": String, + "azp": String, + "birthdate": String, + "email": email_address, + "exp": Long, + "iat": Long, + "iss": TrustedIssuer, + "jti": String, + "name": String, + "phone_number": String, + "role": Set, + "sub": String, + }; + entity Userinfo_token = { + "aud": String, + "birthdate": String, + "email": email_address, + "exp": Long, + "iat": Long, + "iss": TrustedIssuer, + "jti": String, + "name": String, + "phone_number": String, + "role": Set, + "sub": String, + }; + action POST appliesTo { + principal: Client, + resource: HTTP_Request, + context: Context, + }; + action GET appliesTo { + principal: Client, + resource: HTTP_Request, + context: Context, + }; + action PUT appliesTo { + principal: Client, + resource: HTTP_Request, + context: Context, + }; + action DELETE appliesTo { + principal: Client, + resource: HTTP_Request, + context: Context, + }; + action HEAD appliesTo { + principal: Client, + resource: HTTP_Request, + context: Context, + }; + action PATCH appliesTo { + principal: Client, + resource: HTTP_Request, + context: Context, + }; + action Access appliesTo { + principal: [User, Role], + resource: Application, + context: Context, + }; +} From 6f6ca6271453b9977ca8f02613ca627d6576cde0 Mon Sep 17 00:00:00 2001 From: Devrim Date: Mon, 29 Jul 2024 11:25:54 +0300 Subject: [PATCH 19/43] fix(jans-linux-setup): client kc_saml_openid is trusted (#9041) Signed-off-by: Mustafa Baser --- .../jans_setup/setup_app/installers/jans_saml.py | 4 +++- .../jans_setup/templates/jans-saml/clients.json | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans_saml.py b/jans-linux-setup/jans_setup/setup_app/installers/jans_saml.py index 7aa056f4762..36b7340dd00 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans_saml.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans_saml.py @@ -147,8 +147,10 @@ def create_clients(self): grant_types=client_info['grant_types'], authorization_methods=client_info['authorization_methods'], application_type=client_info['application_type'], - response_types=client_info['response_types'] + response_types=client_info['response_types'], + trusted_client=client_info['trusted_client'] ) + self.dbUtils.import_ldif(client_ldif_fns) def install_keycloak(self): diff --git a/jans-linux-setup/jans_setup/templates/jans-saml/clients.json b/jans-linux-setup/jans_setup/templates/jans-saml/clients.json index ac09a77cefc..d95a90292ba 100644 --- a/jans-linux-setup/jans_setup/templates/jans-saml/clients.json +++ b/jans-linux-setup/jans_setup/templates/jans-saml/clients.json @@ -11,7 +11,8 @@ "grant_types": ["client_credentials"], "authorization_methods": ["client_secret_basic", "client_secret_post"], "response_types": null, - "application_type": "web" + "application_type": "web", + "trusted_client": "false" }, { "client_prefix": "2101.", @@ -25,7 +26,8 @@ "grant_types": ["authorization_code"], "authorization_methods": ["client_secret_basic"], "response_types": ["code", "token"], - "application_type": "native" + "application_type": "native", + "trusted_client": "true" }, { "client_prefix": "2102.", @@ -39,7 +41,8 @@ "grant_types": ["client_credentials"], "authorization_methods": ["client_secret_basic"], "response_types": ["token"], - "application_type": "native" + "application_type": "native", + "trusted_client": "false" }, { "client_prefix": "2103.", @@ -53,6 +56,7 @@ "grant_types": ["authorization_code"], "authorization_methods": ["client_secret_basic"], "response_types": ["code", "token"], - "application_type": "web" + "application_type": "web", + "trusted_client": "false" } ] From 7df0c741735a6ee82942d71855150cd8f9126636 Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Mon, 29 Jul 2024 11:51:30 +0300 Subject: [PATCH 20/43] feat(jans-auth-server): introduced client authentication custom script (#9024) * feat(jans-auth-server): introduce client authentication custom script #8081 Signed-off-by: YuriyZ * feat(jans-auth-server): added external client authn context for custom script #8081 Signed-off-by: YuriyZ * feat(jans-auth-server): added external client authn service #8081 Signed-off-by: YuriyZ * feat(jans-auth-server): injected external client authn service into authentication filter #8081 Signed-off-by: YuriyZ * feat(jans-auth-server): added client authn sample script #8081 Signed-off-by: YuriyZ * doc(jans-auth-server): added documentation for new client authn custom script #8081 Signed-off-by: YuriyZ --------- Signed-off-by: YuriyZ --- docs/admin/auth-server/endpoints/token.md | 2 + docs/admin/developer/scripts/client-authn.md | 147 ++++++++++++++++++ .../client_authn/ClientAuthn.java | 93 +++++++++++ .../as/server/auth/AuthenticationFilter.java | 11 ++ .../external/ExternalClientAuthnService.java | 83 ++++++++++ .../context/ExternalClientAuthnContext.java | 59 +++++++ .../model/custom/script/CustomScriptType.java | 3 + .../script/type/client/ClientAuthnType.java | 17 ++ .../type/client/DummyClientAuthnType.java | 36 +++++ mkdocs.yml | 1 + 10 files changed, 452 insertions(+) create mode 100644 docs/admin/developer/scripts/client-authn.md create mode 100644 docs/script-catalog/client_authn/ClientAuthn.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalClientAuthnService.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalClientAuthnContext.java create mode 100644 jans-core/script/src/main/java/io/jans/model/custom/script/type/client/ClientAuthnType.java create mode 100644 jans-core/script/src/main/java/io/jans/model/custom/script/type/client/DummyClientAuthnType.java diff --git a/docs/admin/auth-server/endpoints/token.md b/docs/admin/auth-server/endpoints/token.md index d15c281ec21..720cbadb123 100644 --- a/docs/admin/auth-server/endpoints/token.md +++ b/docs/admin/auth-server/endpoints/token.md @@ -113,6 +113,8 @@ authentication method listed below: Refer to [Client Authentication](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication) section of OpenID Connect core specification for more details on these authentication methods. +AS provides ability to customer Client Authentication behavior via [Client Authentication custom script](../../../admin/developer/scripts/client-authn.md) + Client can specify the default authentication method. To set default authentication method using [Janssen Text-based UI(TUI)](../../config-guide/config-tools/jans-tui/README.md), navigate via `Auth Server`->`Clients`->`Add Client`->`Basic`-> `Authn Method Token Endpoint`. diff --git a/docs/admin/developer/scripts/client-authn.md b/docs/admin/developer/scripts/client-authn.md new file mode 100644 index 00000000000..e477f62b425 --- /dev/null +++ b/docs/admin/developer/scripts/client-authn.md @@ -0,0 +1,147 @@ +--- +tags: + - administration + - developer + - scripts +--- + +# Client Authentication Custom Script + +## Overview + +AS support different types of client authentications such as : + +- client_secret_basic +- client_secret_post +- client_secret_jwt +- private_key_jwt + +Sometimes it's convenient to customize default AS client authentication process. For this reason Client Authentication custom script was introduced. + +If script successfully authenticated client, it should return it in `authenticateClient`. +If client is not returned then AS performs built-in authentication. + +## Interface +The Client Authentication script implements the [ClientAuthnType](https://github.com/JanssenProject/jans/blob/main/jans-core/script/src/main/java/io/jans/model/custom/script/type/clientauthn/ClientAuthnType.java) interface. This extends methods from the base script type in addition to adding new methods: + +### Inherited Methods +| Method header | Method description | +|:-----|:------| +| `def init(self, customScript, configurationAttributes)` | This method is only called once during the script initialization. It can be used for global script initialization, initiate objects etc | +| `def destroy(self, configurationAttributes)` | This method is called once to destroy events. It can be used to free resource and objects created in the `init()` method | +| `def getApiVersion(self, configurationAttributes, customScript)` | The getApiVersion method allows API changes in order to do transparent migration from an old script to a new API. Only include the customScript variable if the value for getApiVersion is greater than 10 | + +### New methods +| Method header | Method description | +|:-----|:------| +|`def authenticateClient(self, context)`| Called when the request is received. | + +`authenticateClient` method returns authenticated `Client` object or null if authentication failed. + + +### Objects +| Object name | Object description | +|:-----|:------| +|`customScript`| The custom script object. [Reference](https://github.com/JanssenProject/jans/blob/main/jans-core/script/src/main/java/io/jans/model/custom/script/model/CustomScript.java) | +|`context`| [Reference](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalClientAuthnContext.java) | + + +## Sample script which demonstrates basic client authentication + +### Script Type: Java + +```java +import io.jans.as.common.model.registration.Client; +import io.jans.as.model.config.Constants; +import io.jans.as.server.service.ClientService; +import io.jans.as.server.service.external.context.ExternalClientAuthnContext; +import io.jans.as.server.service.token.TokenService; +import io.jans.model.SimpleCustomProperty; +import io.jans.model.custom.script.model.CustomScript; +import io.jans.model.custom.script.type.client.ClientAuthnType; +import io.jans.service.cdi.util.CdiUtil; +import io.jans.service.custom.script.CustomScriptManager; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +/** + * @author Yuriy Z + */ +public class ClientAuthn implements ClientAuthnType { + + private static final Logger scriptLogger = LoggerFactory.getLogger(CustomScriptManager.class); + + @Override + public Object authenticateClient(Object clientAuthnContext) { + final ExternalClientAuthnContext context = (ExternalClientAuthnContext) clientAuthnContext; + + final HttpServletRequest request = context.getHttpRequest(); + final HttpServletResponse response = context.getHttpResponse(); + + String authorization = request.getHeader(Constants.AUTHORIZATION); + if (!StringUtils.startsWith(authorization, "Basic")) { + context.sendUnauthorizedError(); + return null; + } + + TokenService tokenService = CdiUtil.bean(TokenService.class); + ClientService clientService = CdiUtil.bean(ClientService.class); + + String base64Token = tokenService.getBasicToken(authorization); + String token = new String(Base64.decodeBase64(base64Token), StandardCharsets.UTF_8); + + int delim = token.indexOf(":"); + + if (delim != -1) { + String clientId = URLDecoder.decode(token.substring(0, delim), StandardCharsets.UTF_8); + String clientSecret = URLDecoder.decode(token.substring(delim + 1), StandardCharsets.UTF_8); + + final boolean authenticated = clientService.authenticate(clientId, clientSecret); + if (authenticated) { + final Client client = clientService.getClient(clientId); + scriptLogger.debug("Successfully performed basic client authentication, clientId: {}", clientId); + return client; + } + } + + context.sendUnauthorizedError(); + return null; + } + + @Override + public boolean init(Map configurationAttributes) { + scriptLogger.info("Initialized ClientAuthn Java custom script."); + return true; + } + + @Override + public boolean init(CustomScript customScript, Map configurationAttributes) { + scriptLogger.info("Initialized ClientAuthn Java custom script."); + return true; + } + + @Override + public boolean destroy(Map configurationAttributes) { + scriptLogger.info("Destroyed ClientAuthn Java custom script."); + return false; + } + + @Override + public int getApiVersion() { + return 11; + } +} + +``` + + +## Sample Scripts +- [ClientAuthentication](../../../script-catalog/client_authn/ClientAuthn.java) diff --git a/docs/script-catalog/client_authn/ClientAuthn.java b/docs/script-catalog/client_authn/ClientAuthn.java new file mode 100644 index 00000000000..d8bd8c0fbad --- /dev/null +++ b/docs/script-catalog/client_authn/ClientAuthn.java @@ -0,0 +1,93 @@ +/* + Copyright (c) 2024, Gluu + Author: Yuriy Z + */ + +import io.jans.as.common.model.registration.Client; +import io.jans.as.model.config.Constants; +import io.jans.as.server.service.ClientService; +import io.jans.as.server.service.external.context.ExternalClientAuthnContext; +import io.jans.as.server.service.token.TokenService; +import io.jans.model.SimpleCustomProperty; +import io.jans.model.custom.script.model.CustomScript; +import io.jans.model.custom.script.type.client.ClientAuthnType; +import io.jans.service.cdi.util.CdiUtil; +import io.jans.service.custom.script.CustomScriptManager; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +/** + * @author Yuriy Z + */ +public class ClientAuthn implements ClientAuthnType { + + private static final Logger scriptLogger = LoggerFactory.getLogger(CustomScriptManager.class); + + @Override + public Object authenticateClient(Object clientAuthnContext) { + final ExternalClientAuthnContext context = (ExternalClientAuthnContext) clientAuthnContext; + + final HttpServletRequest request = context.getHttpRequest(); + final HttpServletResponse response = context.getHttpResponse(); + + String authorization = request.getHeader(Constants.AUTHORIZATION); + if (!StringUtils.startsWith(authorization, "Basic")) { + context.sendUnauthorizedError(); + return null; + } + + TokenService tokenService = CdiUtil.bean(TokenService.class); + ClientService clientService = CdiUtil.bean(ClientService.class); + + String base64Token = tokenService.getBasicToken(authorization); + String token = new String(Base64.decodeBase64(base64Token), StandardCharsets.UTF_8); + + int delim = token.indexOf(":"); + + if (delim != -1) { + String clientId = URLDecoder.decode(token.substring(0, delim), StandardCharsets.UTF_8); + String clientSecret = URLDecoder.decode(token.substring(delim + 1), StandardCharsets.UTF_8); + + final boolean authenticated = clientService.authenticate(clientId, clientSecret); + if (authenticated) { + final Client client = clientService.getClient(clientId); + scriptLogger.debug("Successfully performed basic client authentication, clientId: {}", clientId); + return client; + } + } + + context.sendUnauthorizedError(); + return null; + } + + @Override + public boolean init(Map configurationAttributes) { + scriptLogger.info("Initialized ClientAuthn Java custom script."); + return true; + } + + @Override + public boolean init(CustomScript customScript, Map configurationAttributes) { + scriptLogger.info("Initialized ClientAuthn Java custom script."); + return true; + } + + @Override + public boolean destroy(Map configurationAttributes) { + scriptLogger.info("Destroyed ClientAuthn Java custom script."); + return false; + } + + @Override + public int getApiVersion() { + return 11; + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/auth/AuthenticationFilter.java b/jans-auth-server/server/src/main/java/io/jans/as/server/auth/AuthenticationFilter.java index 0b51789292c..cb2393817ec 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/auth/AuthenticationFilter.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/auth/AuthenticationFilter.java @@ -32,6 +32,7 @@ import io.jans.as.server.model.token.ClientAssertion; import io.jans.as.server.model.token.HttpAuthTokenType; import io.jans.as.server.service.*; +import io.jans.as.server.service.external.ExternalClientAuthnService; import io.jans.as.server.service.token.TokenService; import io.jans.as.server.token.ws.rs.TxTokenValidator; import io.jans.as.server.util.ServerUtil; @@ -143,6 +144,9 @@ public class AuthenticationFilter implements Filter { @Inject private TxTokenValidator txTokenValidator; + @Inject + private ExternalClientAuthnService externalClientAuthnService; + private String realm; @Override @@ -166,6 +170,13 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo return; } + final Client client = externalClientAuthnService.externalAuthenticateClient(httpRequest, httpResponse); + if (client != null) { + log.debug("Client {} is authenticated by external script.", client.getClientId()); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + boolean tokenEndpoint = requestUrl.endsWith("/token"); boolean tokenRevocationEndpoint = requestUrl.endsWith("/revoke"); boolean backchannelAuthenticationEnpoint = requestUrl.endsWith("/bc-authorize"); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalClientAuthnService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalClientAuthnService.java new file mode 100644 index 00000000000..4d99e32e51a --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalClientAuthnService.java @@ -0,0 +1,83 @@ +package io.jans.as.server.service.external; + +import io.jans.as.common.model.registration.Client; +import io.jans.as.server.auth.Authenticator; +import io.jans.as.server.service.external.context.ExternalClientAuthnContext; +import io.jans.model.custom.script.CustomScriptType; +import io.jans.model.custom.script.conf.CustomScriptConfiguration; +import io.jans.model.custom.script.type.client.ClientAuthnType; +import io.jans.service.custom.script.ExternalScriptService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +/** + * Client Authentication service responsible for external script interaction. + * + * @author Yuriy Z + */ +@ApplicationScoped +public class ExternalClientAuthnService extends ExternalScriptService { + + @Inject + private Authenticator authenticator; + + public ExternalClientAuthnService() { + super(CustomScriptType.CLIENT_AUTHN); + } + + public Client externalAuthenticateClient(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException, ServletException { + final List scripts = getCustomScriptConfigurations(); + if (scripts == null || scripts.isEmpty()) { + log.trace("Unable to perform client authentication by custom script because there is no `client_authn` scripts."); + return null; + } + + for (CustomScriptConfiguration script : scripts) { + final Client client = externalAuthenticateClient(script, servletRequest, servletResponse); + if (client != null) { + log.trace("Client {} authenticated successfully by custom script {}.", getClientId(client), script.getName()); + return client; + } + } + + log.trace("All `client_authn` scripts returned false."); + return null; + } + + private Client externalAuthenticateClient(CustomScriptConfiguration customScript, HttpServletRequest servletRequest, HttpServletResponse servletResponse) { + + ClientAuthnType script = (ClientAuthnType) customScript.getExternalType(); + + log.trace("Executing external 'authenticateClient' method, script name: {}, requestParameters: {}", + customScript.getName(), servletRequest.getParameterMap()); + + ExternalClientAuthnContext context = new ExternalClientAuthnContext(servletRequest, servletResponse); + + Client client = null; + + try { + client = (Client) script.authenticateClient(context); + if (client != null) { + authenticator.configureSessionClient(client); + } + } catch (Exception e) { + log.error("Failed to run external 'authenticateClient' method of script " + customScript.getName(), e); + client = null; + } + + log.trace("Executed external 'authenticateClient' method, client {}, script name: {}, requestParameters: {}", + getClientId(client), customScript.getName(), servletRequest.getParameterMap()); + return client; + } + + + private static String getClientId(Client client) { + return client != null ? client.getClientId() : "null"; + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalClientAuthnContext.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalClientAuthnContext.java new file mode 100644 index 00000000000..f89b032ff51 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalClientAuthnContext.java @@ -0,0 +1,59 @@ +package io.jans.as.server.service.external.context; + +import io.jans.as.model.config.Constants; +import io.jans.as.model.error.ErrorResponseFactory; +import io.jans.as.model.token.TokenErrorResponseType; +import io.jans.service.cdi.util.CdiUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.WebApplicationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.PrintWriter; + +/** + * @author Yuriy Z + */ +public class ExternalClientAuthnContext extends ExternalScriptContext { + + private static final Logger log = LoggerFactory.getLogger(ExternalClientAuthnContext.class); + + private String realm = "jans-auth"; + + public ExternalClientAuthnContext(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + super(httpRequest, httpResponse); + } + + public void sendUnauthorizedError() { + ErrorResponseFactory errorResponseFactory = CdiUtil.bean(ErrorResponseFactory.class); + try (PrintWriter out = httpResponse.getWriter()) { + httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + httpResponse.addHeader(Constants.WWW_AUTHENTICATE, "Basic realm=\"" + getRealm() + "\""); + httpResponse.setContentType(Constants.CONTENT_TYPE_APPLICATION_JSON_UTF_8); + out.write(errorResponseFactory.errorAsJson(TokenErrorResponseType.INVALID_CLIENT, "Unable to authenticate client.")); + } catch (IOException ex) { + log.error(ex.getMessage(), ex); + } + } + + public void sendResponse(HttpServletResponse httpResponse, WebApplicationException e) { + try (PrintWriter out = httpResponse.getWriter()) { + httpResponse.setStatus(e.getResponse().getStatus()); + httpResponse.addHeader(Constants.WWW_AUTHENTICATE, "Basic realm=\"" + getRealm() + "\""); + httpResponse.setContentType(Constants.CONTENT_TYPE_APPLICATION_JSON_UTF_8); + out.write(e.getResponse().getEntity().toString()); + } catch (IOException ex) { + log.error(ex.getMessage(), ex); + } + } + + public String getRealm() { + return realm; + } + + public void setRealm(String realm) { + this.realm = realm; + } +} diff --git a/jans-core/script/src/main/java/io/jans/model/custom/script/CustomScriptType.java b/jans-core/script/src/main/java/io/jans/model/custom/script/CustomScriptType.java index 1511d70333e..6c664e4e652 100644 --- a/jans-core/script/src/main/java/io/jans/model/custom/script/CustomScriptType.java +++ b/jans-core/script/src/main/java/io/jans/model/custom/script/CustomScriptType.java @@ -19,7 +19,9 @@ import io.jans.model.custom.script.type.authzdetails.DummyAuthzDetail; import io.jans.model.custom.script.type.ciba.DummyEndUserNotificationType; import io.jans.model.custom.script.type.ciba.EndUserNotificationType; +import io.jans.model.custom.script.type.client.ClientAuthnType; import io.jans.model.custom.script.type.client.ClientRegistrationType; +import io.jans.model.custom.script.type.client.DummyClientAuthnType; import io.jans.model.custom.script.type.client.DummyClientRegistrationType; import io.jans.model.custom.script.type.configapi.ConfigApiType; import io.jans.model.custom.script.type.configapi.DummyConfigApiType; @@ -103,6 +105,7 @@ public enum CustomScriptType implements AttributeEnum { SPONTANEOUS_SCOPE("spontaneous_scope", "Spontaneous Scopes", SpontaneousScopeType.class, CustomScript.class, "SpontaneousScope", new DummySpontaneousScopeType()), END_SESSION("end_session", "End Session", EndSessionType.class, CustomScript.class, "EndSession", new DummyEndSessionType()), POST_AUTHN("post_authn", "Post Authentication", PostAuthnType.class, CustomScript.class, "PostAuthn", new DummyPostAuthnType()), + CLIENT_AUTHN("client_authn", "Client Authentication", ClientAuthnType.class, CustomScript.class, "ClientAuthn", new DummyClientAuthnType()), SELECT_ACCOUNT("select_account", "Select Account", SelectAccountType.class, CustomScript.class, "SelectAccount", new DummySelectAccountType()), CREATE_USER("create_user", "Create User", CreateUserType.class, CustomScript.class, "CreateUser", new DummyCreateUserType()), SCIM("scim", "SCIM", ScimType.class, CustomScript.class, "ScimEventHandler", new DummyScimType()), diff --git a/jans-core/script/src/main/java/io/jans/model/custom/script/type/client/ClientAuthnType.java b/jans-core/script/src/main/java/io/jans/model/custom/script/type/client/ClientAuthnType.java new file mode 100644 index 00000000000..7873b13de91 --- /dev/null +++ b/jans-core/script/src/main/java/io/jans/model/custom/script/type/client/ClientAuthnType.java @@ -0,0 +1,17 @@ +package io.jans.model.custom.script.type.client; + +import io.jans.model.custom.script.type.BaseExternalType; + +/** + * @author Yuriy Z + */ +public interface ClientAuthnType extends BaseExternalType { + + /** + * Performs client authentication. + * + * @param context external client authentication context - io.jans.as.server.service.external.context.ExternalClientAuthnContext + * @return authenticated client - io.jans.as.common.model.registration.Client + */ + Object authenticateClient(Object context); +} diff --git a/jans-core/script/src/main/java/io/jans/model/custom/script/type/client/DummyClientAuthnType.java b/jans-core/script/src/main/java/io/jans/model/custom/script/type/client/DummyClientAuthnType.java new file mode 100644 index 00000000000..2c933e11f77 --- /dev/null +++ b/jans-core/script/src/main/java/io/jans/model/custom/script/type/client/DummyClientAuthnType.java @@ -0,0 +1,36 @@ +package io.jans.model.custom.script.type.client; + +import io.jans.model.SimpleCustomProperty; +import io.jans.model.custom.script.model.CustomScript; + +import java.util.Map; + +/** + * @author Yuriy Z + */ +public class DummyClientAuthnType implements ClientAuthnType { + @Override + public boolean init(Map configurationAttributes) { + return true; + } + + @Override + public boolean init(CustomScript customScript, Map configurationAttributes) { + return true; + } + + @Override + public boolean destroy(Map configurationAttributes) { + return true; + } + + @Override + public int getApiVersion() { + return 1; + } + + @Override + public Object authenticateClient(Object context) { + return null; + } +} diff --git a/mkdocs.yml b/mkdocs.yml index c271df65446..c8dd9d90f69 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -198,6 +198,7 @@ nav: - admin/auth-server/endpoints/README.md - OpenID Configuration: admin/auth-server/endpoints/configuration.md - Client Registration: admin/auth-server/endpoints/client-registration.md + - Client Authentication: admin/auth-server/endpoints/client-authn.md - Authorization: admin/auth-server/endpoints/authorization.md - Authorization Challenge: admin/auth-server/endpoints/authorization-challenge.md - Token: admin/auth-server/endpoints/token.md From 093f63e38f223c1a85cd58964989cf3c65b618a1 Mon Sep 17 00:00:00 2001 From: Devrim Date: Mon, 29 Jul 2024 12:46:47 +0300 Subject: [PATCH 21/43] fix(jans-linux-setup): remove saml_scim_client (#9048) Signed-off-by: Mustafa Baser --- .../jans_setup/templates/jans-saml/clients.json | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/jans-linux-setup/jans_setup/templates/jans-saml/clients.json b/jans-linux-setup/jans_setup/templates/jans-saml/clients.json index d95a90292ba..943ec878b7b 100644 --- a/jans-linux-setup/jans_setup/templates/jans-saml/clients.json +++ b/jans-linux-setup/jans_setup/templates/jans-saml/clients.json @@ -1,19 +1,4 @@ [ - { - "client_prefix": "2100.", - "client_var": "saml_scim_client_id", - "client_id": "saml_scim_client", - "display_name": "Jans SCIM Client for SAML", - "description": "Jans SCIM Client for SAML", - "scopes_dns": ["inum=F0C4,ou=scopes,o=jans"], - "scopes_ids": ["https://jans.io/scim/users.write", "https://jans.io/scim/users.read"], - "redirect_uri": ["https://%(hostname)s/admin-ui", "http://localhost:4100"], - "grant_types": ["client_credentials"], - "authorization_methods": ["client_secret_basic", "client_secret_post"], - "response_types": null, - "application_type": "web", - "trusted_client": "false" - }, { "client_prefix": "2101.", "client_var": "kc_saml_openid_client_id", From 7106f6e5639de624911b1cba3deabce14b2a1ce0 Mon Sep 17 00:00:00 2001 From: Devrim Date: Mon, 29 Jul 2024 13:30:27 +0300 Subject: [PATCH 22/43] fix(jans-cli-tui): asset upload (#9050) Signed-off-by: Mustafa Baser --- jans-cli-tui/cli_tui/cli/config_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jans-cli-tui/cli_tui/cli/config_cli.py b/jans-cli-tui/cli_tui/cli/config_cli.py index 4e913f625a0..81ebe94d707 100755 --- a/jans-cli-tui/cli_tui/cli/config_cli.py +++ b/jans-cli-tui/cli_tui/cli/config_cli.py @@ -884,7 +884,7 @@ def post_requests(self, endpoint, data, params=None, method='post'): mime_type = self.get_mime_for_endpoint(endpoint) if mime_type == 'multipart/form-data': - data_js = json.loads(data) if isinstance(data, str) else copy.deepcopy(data) + data_js = json.loads(data) if (isinstance(data, str) or isinstance(data, bytes)) else copy.deepcopy(data) schema_ref = endpoint.info['requestBody']['content'][mime_type]['schema']['$ref'] schema = self.get_schema_from_reference(endpoint.info['__plugin__'], schema_ref) multi_part_fields = {} From 731aaa6b1746bf8222927a1183105680a4bbc118 Mon Sep 17 00:00:00 2001 From: Devrim Date: Mon, 29 Jul 2024 13:58:54 +0300 Subject: [PATCH 23/43] fix(jans-cli-tui): asset creation date (#9052) Signed-off-by: Mustafa Baser --- jans-cli-tui/cli_tui/plugins/130_assets/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jans-cli-tui/cli_tui/plugins/130_assets/main.py b/jans-cli-tui/cli_tui/plugins/130_assets/main.py index 544d64d8338..754a246497f 100644 --- a/jans-cli-tui/cli_tui/plugins/130_assets/main.py +++ b/jans-cli-tui/cli_tui/plugins/130_assets/main.py @@ -276,7 +276,7 @@ async def get_assets(self, pattern='') -> None: self.assets_list_box.clear() self.assets_list_box.all_data = self.data['entries'] for asset_info in self.data['entries']: - self.assets_list_box.add_item((asset_info['inum'], asset_info['displayName'], asset_info['jansEnabled'], asset_info['creationDate'])) + self.assets_list_box.add_item((asset_info['inum'], asset_info['displayName'], asset_info['jansEnabled'], asset_info.get('creationDate', '---'))) self.assets_container = self.assets_list_box From fa9d079853a659587845934cd6ae1500c77e51d4 Mon Sep 17 00:00:00 2001 From: Michael Schwartz Date: Mon, 29 Jul 2024 06:41:35 -0500 Subject: [PATCH 24/43] jans-docs: lock updates to README, cedarling and lock master docs. (#9042) * Lock Docs Update ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Signed-off-by: Michael Schwartz * Lock Diagram Update ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Signed-off-by: Michael Schwartz * docs(lock): proofread and fix Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> * docs(lock): proofread and fix Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> * docs(lock): nav changes Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> --------- Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> Signed-off-by: Dhaval D <343411+ossdhaval@users.noreply.github.com> Co-authored-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> --- docs/admin/lock/README.md | 163 ++++++++--------- docs/admin/lock/cedarling.md | 194 +++++++++++++-------- docs/admin/lock/lock-auth-server-config.md | 13 -- docs/admin/lock/lock-client-config.md | 13 -- docs/admin/lock/lock-installation.md | 14 -- docs/admin/lock/lock-master.md | 36 +++- docs/admin/lock/lock-opa.md | 13 -- docs/admin/lock/lock-pdp-plugin.md | 13 -- docs/assets/lock-cedarling-diagram-2.jpg | Bin 53251 -> 56140 bytes docs/assets/lock-wasm-master-OP.jpg | Bin 0 -> 62006 bytes mkdocs.yml | 5 - 11 files changed, 217 insertions(+), 247 deletions(-) delete mode 100644 docs/admin/lock/lock-auth-server-config.md delete mode 100644 docs/admin/lock/lock-client-config.md delete mode 100644 docs/admin/lock/lock-installation.md delete mode 100644 docs/admin/lock/lock-opa.md delete mode 100644 docs/admin/lock/lock-pdp-plugin.md create mode 100644 docs/assets/lock-wasm-master-OP.jpg diff --git a/docs/admin/lock/README.md b/docs/admin/lock/README.md index 05ac2770a59..3ccf124d3cd 100644 --- a/docs/admin/lock/README.md +++ b/docs/admin/lock/README.md @@ -1,128 +1,102 @@ --- tags: - - administration - - lock - - authorization / authz - - Cedar - - Cedarling + - administration + - lock + - authorization / authz + - Cedar + - Cedarling --- -## Jans Lock Overview - -Lock provides a centralized control plane for domains to use [Cedar](https://www.cedarpolicy.com/en) -to secure a network of distributed applications and audit the activity of both people and software. -Lock makes it easy for Javascript developers to write access policies based on security tokens as -input, for example OAuth access tokens, or OpenID Connect id_tokens. - -Using a declarative syntax like Cedar for authorization policies is a best practice for enterprise -application security. A policy engine that executes declarative policies enables developers to define -security rules without resorting to implementing such rules in their application code. Cedar supports -traditional access management strategies like RBAC and more adaptive capabilities that offer fine -grain decisions based on contextal data. The Cedar Engine does this without sacrificing performance -or the security benefits of a deterministic policy engine. - -There are three key components in a Lock topology: (1) [Cedarling](./cedarling.md)--a WebAssembly -("WASM") component that runs the [Amazon Rust Cedar Engine](https://github.com/cedar-policy/cedar) and -performs JWT token validation; (2)[Lock Master](./lock-master.md)--a web service deployed by domains -to manage a network of distributed ephemeral Cedarlings; (3) -[Agama Lab](https://cloud.gluu.org/agama-lab), a policy authoring tool for developers to design -policies and publish their policy store in Github. - -You don't need to deploy a Jans Lock Topology to derive utility from the Cedarling. Javascript -developers can use the Cedarling to secure even a single browser-based application, especially if -they are using OAuth or OpenID. The Cedarling evaluates a request to perform an action on a -resource by first validating the tokens, then instantiating a Person and Client entity based -on the JWT data payloads, and evaluating the request based on its existing policies. - -The Lock Master is designed for domains that want control over a **network of Cedarlings** used for different applications, each with their own policy store. Communication in this Lock topology is bi-directional. Cedarlings can send information to the Lock Master, and the Lock Master can push updates to the Cedarlings. Notifications from the Lock Master to the Cedarlings are connectionless-- -a Cedarling subscibes to event notifications using -[Server Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events) or "SSE". Requests from the Cedarling to the Lock Master are sent via HTTP Post to OAuth protected endpoints. +## Janssen Lock Overview + +Janssen Lock (or just "Lock") provides a centralized control plane for domains +to use [Cedar](https://www.cedarpolicy.com/en) to secure a network of +distributed applications and audit the activity of both people and software. -Using the Cedarling with Lock Master can enable a temporal improvement in OAuth security: in addition -to validating the JWT signature, the Cedarling can check to ensure the token is not revoked. -Using SSE, Lock Master sends an updated [OAuth Token Status List](https://www.ietf.org/archive/id/ -draft-ietf-oauth-status-list-02.html) from Jans Auth Server. The Cedarling can thus checks the -status of a token without a slow and potentially unreliable OAuth introspection request. +A Lock topology has three software components: -On startup, the Cedarling can make a POST request to fetch a Policy Store from Lock Master, load a -local Policy Store or retreive it from a TLS-protected Web URI. A Policy Store is a JSON document that -contains the Cedar policies, Cedar schema, and list of trusted issuers. How you write your policies is out of scope of the Janssen Project--you can do this manually or use the Gluu -[Agama Lab](https://cloud.gluu.org/agama-lab) online authoring tool. +1. [Cedarling](./cedarling.md): a WebAssembly +("WASM") application that runs the +[Amazon Rust Cedar Engine](https://github.com/cedar-policy/cedar) and +validates JWTs +2. [Lock Master](./lock-master.md): a Java Weld application that connects +ephemeral Cedarlings to the enterprise +3. Jans Auth Server: which provides the OAuth and OpenID services -One of the challenges a network of distributed Cedarlings poses is the consolidation of audit logs. -As Cedarlings are ephemeral, the logs need to be archived centrally. Jans Lock addresses this gap by -providing an audit endpoint. Other enterprise management features are available in the -[Gluu Flex](https://gluu.org/flex) AdminUI. +![](../../assets/lock-wasm-master-OP.jpg) + +Lock is designed for domains that deploy a **network of Cedarlings**. +Communication in this Lock topology +is bi-directional. Cedarlings can send information to the Lock Master, and +the Lock Master can push +updates to the Cedarlings. Notifications from the Lock Master to the Cedarlings + are connectionless-- +a Cedarling subscibes to event notifications using +[Server Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events) +or "SSE". Requests from the Cedarling to the Lock Master are sent via HTTP Post + to OAuth protected endpoints. ## Authz Theoretical Background -For years, security architects have conceptualized distributed authorization model in line with +For years, security architects have conceptualized a distributed authorization +model in line with [RFC 2409](https://datatracker.ietf.org/doc/html/rfc2904#section-4.4) and [XACML](https://docs.oasis-open.org/xacml/3.0/xacml-3.0-core-spec-cos01-en.html), which describe several common roles: -| Role | Acronym | Description | +| Role | Acronym | Description | | ----- | :--: | ----------- | -| Policy Decision Point | PDP | Service which evaluates access requests against authorization policies before issuing access decisions | -| Policy Information Point | PIP | The source of "data", e.g. about people, clients and resources | -| Policy Enforcement Point | PEP | Service, website or API which queries the PDP for authorization | -| Policy Administration Point | PAP | Where admins manage the authorization infrastructure | -| Policy Retrieval Point | PRP | Repository where policies are stored | +| Policy Decision Point | PDP | Service which evaluates access requests against authorization policies before issuing access decisions | +| Policy Information Point | PIP | The source of "data", e.g. about people, clients, and resources | +| Policy Enforcement Point | PEP | Service, website, or API that queries the PDP for authorization | +| Policy Administration Point | PAP | Where admins manage the authorization infrastructure | +| Policy Retrieval Point | PRP | Repository where policies are stored | Jans Lock aligns with this model: -| Role | Lock | Description | +| Role | Lock | Description | | ----- | :--: | ----------- | -| PDP | Cedarling | Evaluates policies versus input data | -| PIP | JWT tokens | Contain data to instantiate entities | -| PEP | Application | Must rely on Cedarling for decision | -| PAP | Jans Config API | Endpoints for Lock admin configuration | -| PRP | Lock Master | Endpoints to publish Policy Store and other PDP configuration | - -## Lock Design Goals - -Following are a list of goals that informed the design of Jans Lock and the Cedarling: - -* Move the PDP to the far edge of the network--the browser itself. -* Make the PDP performant and deterministic (i.e. milliseond statup time and always return a - PERMIT/DENY response). -* Empower application developers to author policies appropriate for the resources and actions -they need to protect. -* Centralilze audit and health data collection -* Send updates to the Cedarlings from the Lock Master to enable realtime attack mitigation +| PDP | Cedarling | Evaluates policies versus input data | +| PIP | JWT tokens | Contain data to instantiate entities | +| PEP | Application | Must rely on Cedarling for decision | +| PAP | Jans Config API | Endpoints for Lock admin configuration | +| PRP | Lock Master | Endpoints to publish Policy Store and other PDP configuration | ## Policy Store -By convention the filename of the Cedarling Policy Store is `cedarling_store.json`. It is a JSON -file that contains all the data the Cedarling needs to verify JWT tokens, evaluate policies, -and instantiate the Cedar entities requied to evaluate the policies for a given resource and -action. The policy store contains three things: +By convention, the filename of the Cedarling Policy Store is +`cedarling_store.json`. It is a JSON +file that contains all the data Cedarling needs to evaluate policies and verify + JWT tokens. +The Policy Store contains three things: -1. [Cedar Schema](https://docs.cedarpolicy.com/schema/schema.html) - JSON format Schema file. -Lock comes with a common schema, but domains should extend this schema to fit their exact -requirements. -2. [Cedar Policies](https://docs.cedarpolicy.com/policies/syntax-policy.html) - JSON format Policy Set file. These policies need to be authored in Agama Lab developer tool or manually. -3. [Trusted Issuers](.) - List of which domains are authorized to issue tokens. +1. [Cedar Schema](https://docs.cedarpolicy.com/schema/schema.html): Cedar +schema file (base64 encoded human readible format). Developers can extend the +schema to align with the application model, especially for Resources. +2. [Cedar Policies](https://docs.cedarpolicy.com/policies/syntax-policy.html): Cedar Policy Set file (base64 encoded human readible format). +3. [Trusted Issuers](.): A list of domains are authorized to issue tokens. In JSON it looks like this: -``` +```json { - "policies": {...}, - "schema": {...}, + "policies": "...", + "schema": "...", "trusted_idps": [] } ``` ### Trusted Issuer Schema -At initialization, the Cedarling iterates the list of Trusted IDPs and fetches the current public -keys. The trusted issuer schema provides guidance on how to uniquely identify a person, and how +At initialization, the Cedarling iterates the list of Trusted IDPs and fetches +the current public +keys. The trusted issuer schema provides guidance on how to uniquely identify a +person, and how to build the roles based on a user claim. Here is a non-normative example: -``` +```json [ {"name": "Google", "Description": "Consumer IDP", @@ -138,12 +112,11 @@ Here is a non-normative example: ### Entity Mapping -The Cedar schema is like the object defintion, and a Cedar entity is like an instance of the object. -Without entities, there is no data for the policies to evaluate. The Cedarling creates the entities -from the tokens provided as input to the requested authoriztaion in conjunction with its -configuration: +A Cedar Entity is an instance of the object defined in the Cedar Schema. +Without entities, there +is no data for the policies to evaluate. The Cedarling creates the Resource and + tokens sent in the authz request. -* **TrustedIssuer**: Created on startup from Policy Store * **Client**: Created from access token * **Application**: Created if input supplies an Application name * **Role**: Created for each `role` claim value in the joined id_token and userinfo token @@ -151,10 +124,14 @@ configuration: * **Access_token**: 1:1 mapping from claims in token * **id_token**: 1:1 mapping from claims in token * **Userinfo_token**: 1:1 mapping from claims in token +* **TrustedIssuer**: Created if Policy Store contains trusted IDPs +* **Application**: ## More information * Lock Master configuration and operation [docs](./lock-master.md) +* Cedarling [docs](./cedarling.md) * Cedarling [Readme](https://github.com/JanssenProject/jans/blob/main/jans-lock/cedarling/README.md) * Cedarling [Training](.) (coming soon) + diff --git a/docs/admin/lock/cedarling.md b/docs/admin/lock/cedarling.md index 8c87b260bb5..e2fa9d1eda6 100644 --- a/docs/admin/lock/cedarling.md +++ b/docs/admin/lock/cedarling.md @@ -7,59 +7,75 @@ tags: - Cedarling --- -## Cedarling Authorization +## What is Cedar + +[Cedar](https://www.cedarpolicy.com/en) was invented by Amazon for their +[Verified Permission](https://aws.amazon.com/verified-permissions/) service. +Cedar enables developers to create complex, contextual policies without cluttering application code +with lots of `if` - `then` statements. Externalizing policies makes it easier to audit the security +controls of an application. Cedar is a deterministic policy engine--if the schema +and policies are validated, the engine will always return `permit` or `forbid`. + +Cedar uses the **PARC** syntax: **P**rincipal, **A**ction, **R**esource, **C**ontext. For example, +you may have a policy that says *Admins* can *write* to the *config* folder. In this example, the +*Admin* Role is the Principal, *write* is the Action, and the *config* folder is the Resource. The +Context is used to specify information about the enivironment, like the time of day or network address. +Like RBAC, Cedar is deterministic. But it's also less reductive then RBAC, and in fact enables +security admins to express quite powerful policies. + +![](../../assets/lock-cedarling-diagram-3.jpg) + +## What is the Cedarling The Cedarling, as its name suggests, enables you to define the security rules for your application -in [Cedar](https://www.cedarpolicy.com/en) policy syntax. +in Cedar policy syntax. Optionally, the Cedarling can validate JWTs from a list of trusted IDPs-- +both the JWT signature and the current status. The Cedarling rapidly evaluates authorization requests +because it has all the policies and data it needs to make a local decision. + +Architecturally, the Cedarling is a local, autonomous Policy Decision Point, or "PDP", distributed +as a WebAssembly ("WASM") component. WASM components run directly in a browser. They can also run +as a cloud native function. + +A key feature of the Cedarling is to log all `permit` or `forbid` decisions returned to the +application. It can also log the validation of tokens. In an enterprise deployment, this audit +log is sent for central archiving. -The Cedarling is a local, autonomous Policy Decision Point, or "PDP", distributed as a -WebAssembly ("WASM") component. WASM components can run directly in the browser or in a cloud -function. The Cedarling's main job is to `permit` or `forbid` requests based on enterprise-approved -policies. The input to the policies are JWT tokens (which provides the Principal), the requested -Action and Resource. The Cedarling rapidly evaluates authorization requests because it has all the -policies and data it needs to make a local decision. +For developers, the Cedarling is a more productive and flexible way to handle authz. The Cedarling maps +Roles, and provides RBAC out-of-the-box. But developers can also express a variety of policies beyond +the limitations of "person with role has access". For example, what if you want to allow access only +to people who use a certain type of passkey? Or what if you want to incorporate a fraud score, and +elevate security for riskier transactions. Or maybe, partners can view tickets for the customers +they serve? These policies are easily and rapidly evaluated by a Cedar policy engine. ![](../../assets/lock-cedarling-diagram-1.jpg) -In a JavaScript browser framework, the Cedarling loads its Policy Store during initialization, as a -static JSON file or fetched via REST. Developers may consider the Cedarling Policy Store as part of -the code. Externalizing the policies makes it easier to audit the security features and -controls of an application. The Cedarling enables developers to create complex, contextual policies -without cluttering application code with lots of `if` - `then` statements. Importantly, the Cedarling -creates an audit log of all decisions by an application to `permit` or `forbid` actions. In an -enterprise deployment, this audit log is sent for central archiving. +The Cedarling loads its Policy Store during initialization, as a static JSON file or fetched via REST. +The Policy Store contains the Cedar Policies, Cedar Schema, and optionally, a list of the Trusted IDPs. +Developers may consider the Cedarling Policy Store as part of the code. The Cedar schema for resources +aligns with the application model. The policies control the expected functionality, and +need to be unit tested--including both positive and negative tests. -Where does the Cedarling get the data for policy evaluation? The data is contained in the -authorization request itself which has the OAuth and OpenID JWTs and details about the resource -and requested action. +Where does the Cedarling get the data for policy evaluation? First, the request includes the resource +details. Based on this, the Cedarling creates the Resource entity. The Principal entities +are derived from the JWTs--the combined OpenID id_token and Userinfo tokens enable the Cedarling to +create a User and Role entities; the OAuth access token is used to create a Client entity. +Hypothetically, you could also pass the roles in an access token claim. But a user claim for roles +is preferred. ![](../../assets/lock-cedarling-diagram-2.jpg) -Two JWT tokens in particular are typical: (1) an OpenID Connect id_token and (2) an OAuth access -token. The id_token represents a user authentication event. The access token represents a -client authentication event. The Cedarling can trust the id_token and access token to extract the User, -Role and Client Principals. These tokens also contain other interesting contextual data. An OpenID +The id_token represents a user authentication event. The access token represents a +client authentication event. These tokens contain other interesting contextual data. An OpenID Connect id_token JWT tells you who authenticated, when they authenticated, how they authenticatated, -and other claims like the User's roles. An OAuth Access Token JWT can tell you information about the -software that obtained the JWT, its extent of access as defined by the OAuth Authorization Server +and optionally other claims like the User's roles. An OAuth Access Token JWT can tell you information +about the software that obtained the JWT, its extent of access as defined by the OAuth Authorization Server (*i.e.* the values of the `scope` claim), or other claims--domains frequently enhance the access token to -contain business specific data needed for policy evaluation. If an OpenID Userinfo token is sent to the -Cedarling, it is combined with the id_token to paint a fuller picture of the User's claims. - -Cedar was invented by Amazon for their -[Verified Permission](https://aws.amazon.com/verified-permissions/) service. It uses the **PARC** -syntax: **P**rincipal, **A**ction, **R**esource, **C**ontext. Principal-Action-Resource is typical -for most authorization solutions. For example, you may have a policy that says *Admins* can *write* -to the *config* folder. In this example, the *Admin* Role is the Principal, *write* is the Action, -and the *config* folder is the Resource. The Context is used to specify information about the -enivironment, like the time of day or network address. +contain business specific data needed for policy evaluation. The Cedarling authorizes a person using a certain piece of software. From a logical perspective, `person_allowed AND client_allowed` must be `True`. A person may be either explicitly allowed, or have a role that enables access. For example, `person_allowed` is `True` if -`user=mike OR role=SuperUser`. - -![](../../assets/lock-cedarling-diagram-3.jpg) +`user=mike OR role=SuperUser` and application is from `org_id=Acme`. The JWT's, Action, Resource and Context is sent by the application in the authorization request. For example, this is a sample request from a hypothetical JS application: @@ -70,8 +86,13 @@ input = { "id_token": "eyJjbGc...", "userinfo_token": "eyJjbGc...", "tx_token": "eyJjbGc...", - "resource": {"Ticket": {"id": "12345", "creator": "foo@bar.com", "organization": "Acme"}}, "action": "View", + "resource": {"Ticket": { + "id": "ticket-10101", + "owner": "bob@acme.com", + "org_id": "Acme" + } + }, "context": { "ip_address": "54.9.21.201", "network_type": "VPN", @@ -85,21 +106,22 @@ decision_result = authz(input) ## Cedarling Token Validation -The Cedarling can validate the signatures of the JWTs for developers, by setting the -`CEDARLING_JWT_VALIDATION` environment variable to `True`. For testing, developers can set this -property to `False` and submit an unsigned JWT, for example one you generate with -[JWT.io](https://jwt.io). Or developers may prefer to validate the signatures in code--that's ok. +Optionally, the Cedarling can validate the signatures of the JWTs for developers. To enable this, +set the `CEDARLING_JWT_VALIDATION` bootstrap property to `True`. For testing, developers can set +this property to `False` and submit an unsigned JWT, for example one you generate with +[JWT.io](https://jwt.io). -On initiatilization, the Cedarling downloads the public keys of the Trusted IDPs specified in the -Cedarling policy store. Because all JWT's have an `iss` claim, this is used to determine which keys -to use for token signature validation. +If token validation is enabled, on initiatilization the Cedarling downloads the public keys of +the Trusted IDPs specified in the Cedarling policy store. The Cedarling uses the JWT `iss` +claim to determine the right keys for validation. In an enterprise deployment, the Cedarling can also check if a JWT has been revoked. The Cedarling -uses a mechanism described in the +checks the status following a mechanism described in the [OAuth Status Lists](https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/) -draft. This might be handy for use cases where a token revocation needs to be communicated -immediately, such as an account takeover situation, or an implementation of a one-time transactions -in a cluster of web servers. Jans Auth Server supports the [Global Token Revocation](https://datatracker.ietf.org/doc/draft-parecki-oauth-global-token-revocation/) OAuth draft. This is how a client can inform the OAuth Server that a given token should be revoked. +draft. Enforcing the status of tokens helps limit the damage of account takeover--i.e. to immediately +recursively revoke all the tokens issued to an attacker. Domains may want to use Token Status also to +implement single-transaction tokens; for example, a token that is good for one and only one wire +transfer. Here is a summary of the ways the Cedarling may validate a JWT, depending on your bootstrap properties: * Validate signature from Trusted Issuer @@ -110,58 +132,74 @@ Here is a summary of the ways the Cedarling may validate a JWT, depending on you ![](../../assets/lock-cedarling-diagram-4.jpg) -## Policy Authoring - -The eaisest way to author your policy store is to use the Policy Designer in [Agama Lab](https://cloud.gluu.org/agama-lab). This tool helps you define the policies, schema and trusted IDPs and -to publish a policy store to any Github repository to which you have access. - -## Testing the Cedarling - -You can perform end to end testing by running the Cedarling in your JS browser application. If -you are using a server side application (e.g. a Wordpress application), you'll need to deploy -the Cedarling as a cloud function. Remember to run the Cedarling init function when your application -starts. This is necessary to load the policy store and to download the current public keys from -an IDP if you are using the Cedarling for token validation. If you want to use roles, make sure to -populate the User.role claim in your id_token or Userinfo token. Call the authz function when -you want the Cederling to opine on a security RBAC decision. ## Cedarling Policy Store -The Cedarling Policy Store is a JSON file that contains all the data the Cedarling needs to verify JWT tokens and evaluate policies: +By convention, the filename is `cedarling_store.json`. It contains all the data the +Cedarling needs to evaluate policies and verify JWT tokens: -1. Cedar Schema - JSON formatted Schema file -2. Cedar Policies - JSON formatted Policy Set file -3. Trusted Issuers - JSON file with below syntax +1. Cedar Schema - Base64 encoded human format +2. Cedar Policies - Base64 encoded human format +3. Trusted Issuers - See below syntax -By convention, the filename is `cedarling_store.json`. The JSON schema looks like this: +The JSON schema looks like this: ``` { - "policies": {...}, - "schema": {...}, - "trusted_idps": [] + "policies": "...", + "schema": "...", + "trusted_idps": [...] } ``` ### Trusted Issuer Schema -This is a hypothetical example. - +* **`name`** : String, no spaces +* **`description`** : String +* **`openid_configuration_endpoint`** : String with `https` url of `.well-known` for domain. +* **`access_tokens`** : Object with claims: + * `trusted`: `True | False` +* **`id_tokens`** : Object with claims: + * `trusted`: `True | False` + * `principal_identifier`: the token claim used to identify the User entity (in SAML jargon it's + the "NameID format"). This claim is optional--it may be present in the Userinfo token. Defaults to `sub`. + * `role_mapping`: A list of the User's roles +* **`userinfo_tokens`** : + * `trusted`: `True | False` + * `principal_identifier`: the token claim used to identify the User entity (in SAML jargon it's + the "NameID format"). This claim is optional--it may be present in the Userinfo token. Defaults to `sub`. + * `role_mapping`: A list of the User's roles +* **`tx_tokens`** : + * `trusted`: `True | False` + +Non-normative example: ``` [ -{"name": "Acme", - "Description": "Acme IDP", +{"name": "IDP1", + "description": "Acme IDP", "openid_configuration_endpoint": "https://acme.com/.well-known/openid-configuration", "access_tokens": {"trusted": True}, "id_tokens": {"trusted":True, "principal_identifier": "email"}, - "userinfo_tokens": {"trusted": True, "role_mapping": "role"} + "userinfo_tokens": {"trusted": True, "role_mapping": ["hr-admin", "staff"]}, + "tx_tokens": {"trusted": True} }, -... +{IDP-2}, +{IDP-3}... ] ``` +### Policy and Schema Authoring + +You can hand create your Cedar policies and schema in [Visual Studio](https://marketplace.visualstudio.com/items?itemName=cedar-policy.vscode-cedar). Make sure you run the cedar command line tool to validate both your +schema and policies. The eaisest way to author your policy store is to use the Policy Designer in +[Agama Lab](https://cloud.gluu.org/agama-lab). This tool helps you define the policies, schema and +trusted IDPs and to publish a policy store to a Github repository. + + ## Cedarling Bootstrap Properties +These Bootstrap Properties control default application level behavior. + * **`CEDARLING_APPLICATION_NAME`** : Human friendly identifier for this application * **`CEDARLING_POLICY_STORE_URI`** : Location of policy store JSON, used if policy store is not local, or retreived from Lock Master. @@ -172,6 +210,8 @@ This is a hypothetical example. * **`CEDARLING_REQUIRE_AUD_VALIDATION`** : Enabled | Disabled. Controls if Cedarling will discard id_token without an access token with the corresponding client_id. +* **`CEDARLING_ROLE_MAPPING`** : Default: `{"id_token": "role", "userinfo_token": "role"}` but the role may be sent as an access token, or with a different identifier. For example, for Ping Identity, you might see `{"userinfo_token": "memberOf"}`. + * **`CEDARLING_LOG_LEVEL`** : Controls the verbosity of Cedar logging. The following bootstrap properties are only needed for enterprise deployments. diff --git a/docs/admin/lock/lock-auth-server-config.md b/docs/admin/lock/lock-auth-server-config.md deleted file mode 100644 index 5f5a81f3c89..00000000000 --- a/docs/admin/lock/lock-auth-server-config.md +++ /dev/null @@ -1,13 +0,0 @@ -# Configuring Janssen Server for Lock - -## This content is in progress - -The Janssen Project documentation is currently in development. Topic pages are being created in order of broadest relevance, and this page is coming in the near future. - -## Have questions in the meantime? - -While this documentation is in progress, you can ask questions through [GitHub Discussions](https://github.com/JanssenProject/jans/discussions) or the [community chat on Gitter](https://gitter.im/JanssenProject/Lobby). Any questions you have will help determine what information our documentation should cover. - -## Want to contribute? - -If you have content you'd like to contribute to this page in the meantime, you can get started with our [Contribution guide](https://docs.jans.io/head/CONTRIBUTING/). \ No newline at end of file diff --git a/docs/admin/lock/lock-client-config.md b/docs/admin/lock/lock-client-config.md deleted file mode 100644 index bac7bae700d..00000000000 --- a/docs/admin/lock/lock-client-config.md +++ /dev/null @@ -1,13 +0,0 @@ -# Lock Client Configuration - -## This content is in progress - -The Janssen Project documentation is currently in development. Topic pages are being created in order of broadest relevance, and this page is coming in the near future. - -## Have questions in the meantime? - -While this documentation is in progress, you can ask questions through [GitHub Discussions](https://github.com/JanssenProject/jans/discussions) or the [community chat on Gitter](https://gitter.im/JanssenProject/Lobby). Any questions you have will help determine what information our documentation should cover. - -## Want to contribute? - -If you have content you'd like to contribute to this page in the meantime, you can get started with our [Contribution guide](https://docs.jans.io/head/CONTRIBUTING/). \ No newline at end of file diff --git a/docs/admin/lock/lock-installation.md b/docs/admin/lock/lock-installation.md deleted file mode 100644 index f13cd726dca..00000000000 --- a/docs/admin/lock/lock-installation.md +++ /dev/null @@ -1,14 +0,0 @@ -## Lock Installation - - -## This content is in progress - -The Janssen Project documentation is currently in development. Topic pages are being created in order of broadest relevance, and this page is coming in the near future. - -## Have questions in the meantime? - -While this documentation is in progress, you can ask questions through [GitHub Discussions](https://github.com/JanssenProject/jans/discussions) or the [community chat on Gitter](https://gitter.im/JanssenProject/Lobby). Any questions you have will help determine what information our documentation should cover. - -## Want to contribute? - -If you have content you'd like to contribute to this page in the meantime, you can get started with our [Contribution guide](https://docs.jans.io/head/CONTRIBUTING/). \ No newline at end of file diff --git a/docs/admin/lock/lock-master.md b/docs/admin/lock/lock-master.md index 75680f16c48..7e44e945a4e 100644 --- a/docs/admin/lock/lock-master.md +++ b/docs/admin/lock/lock-master.md @@ -11,14 +11,38 @@ tags: ## Jans Lock Overview -This content is in progress +Lock Master is a Java Weld application that connects ephemeral Cedarlings to the enterprise by +providing a number of [endpoints](https://gluu.org/swagger-ui/?url=https://raw.githubusercontent.com/JanssenProject/jans/main/jans-lock/lock-master.yaml) -The Janssen Project documentation is currently in development. Topic pages are being created in order of broadest relevance, and this page is coming in the near future. +## Installation -## Have questions in the meantime? +Admins can deploy Lock Master as part of Jans Auth Server or as a stanalone +web server. -While this documentation is in progress, you can ask questions through [GitHub Discussions](https://github.com/JanssenProject/jans/discussions) or the [community chat on Gitter](https://gitter.im/JanssenProject/Lobby). Any questions you have will help determine what information our documentation should cover. +## Configuration -## Want to contribute? +A list of server-level configuration properties. -If you have content you'd like to contribute to this page in the meantime, you can get started with our [Contribution guide](https://docs.jans.io/head/CONTRIBUTING/). \ No newline at end of file +## Logs + +Lock Master creates the following logs: + +* lock_master_config.log +* lock_master_audit.log -- RDBMS option +* lock_master_jwt_status.log + +## CLI / TUI + +Admins can manage Lock Master runtime configuration and see activity using the +Jans CLI or TUI. + +- Create/Read/Update/Delete Policy Stores +- Total Number of Authz requests per day +- View/Search current Cedarling clients by searching for username + - View authz activity for this Cedarling client + +## OAuth Security + +Cedarling should present an SSA during client registration. This will enable +Cedarlings to obtain access tokens with scopes for OAuth protected Lock Master +endpoints. \ No newline at end of file diff --git a/docs/admin/lock/lock-opa.md b/docs/admin/lock/lock-opa.md deleted file mode 100644 index d8d45578228..00000000000 --- a/docs/admin/lock/lock-opa.md +++ /dev/null @@ -1,13 +0,0 @@ -# Lock OPA - -## This content is in progress - -The Janssen Project documentation is currently in development. Topic pages are being created in order of broadest relevance, and this page is coming in the near future. - -## Have questions in the meantime? - -While this documentation is in progress, you can ask questions through [GitHub Discussions](https://github.com/JanssenProject/jans/discussions) or the [community chat on Gitter](https://gitter.im/JanssenProject/Lobby). Any questions you have will help determine what information our documentation should cover. - -## Want to contribute? - -If you have content you'd like to contribute to this page in the meantime, you can get started with our [Contribution guide](https://docs.jans.io/head/CONTRIBUTING/). \ No newline at end of file diff --git a/docs/admin/lock/lock-pdp-plugin.md b/docs/admin/lock/lock-pdp-plugin.md deleted file mode 100644 index 3bb6cc618f5..00000000000 --- a/docs/admin/lock/lock-pdp-plugin.md +++ /dev/null @@ -1,13 +0,0 @@ -# Lock PDP Plugin - -## This content is in progress - -The Janssen Project documentation is currently in development. Topic pages are being created in order of broadest relevance, and this page is coming in the near future. - -## Have questions in the meantime? - -While this documentation is in progress, you can ask questions through [GitHub Discussions](https://github.com/JanssenProject/jans/discussions) or the [community chat on Gitter](https://gitter.im/JanssenProject/Lobby). Any questions you have will help determine what information our documentation should cover. - -## Want to contribute? - -If you have content you'd like to contribute to this page in the meantime, you can get started with our [Contribution guide](https://docs.jans.io/head/CONTRIBUTING/). \ No newline at end of file diff --git a/docs/assets/lock-cedarling-diagram-2.jpg b/docs/assets/lock-cedarling-diagram-2.jpg index c8e808c5750ef4b7fbbfd548e0ec2b71addaabff..fc8ff51ef2459b4e9d79c0f2a0b540ad7cd1a5b5 100644 GIT binary patch literal 56140 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ⅈA`` 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
w;RRYdTX-i3nj7JUh;xodfh=-5G2N$21OSCP3g zQqWu+WDD0+U0xDO@W!JtXM}x5j&-6bP<~RRUq=ju4z@4so@8#TrS-zbj zInH2rgS+mmGf{cgLmh7&BfA1SITsgMH@h-v3Ox77Ze@18+AO8rr~|dlc4UZBZq|CK zRnCMU@q_OScq?f2lm#`ws^QUu1_ZzKU-jSs(V%m$;QoRiz6ob!QPhPA`ZUk>e4ywt z#lbm=fXG5n&HdESvE0_l{3kA?%HD~u2%_g7hNbd zgxSR05c>M7CP+Qv5IT`%zDxbiiMI*}m_pu&22g7GJHR7T)-<<#HndIt>V^b?Lu;z~ z8Vfe{Hlr-+rRT$F7;3;rkJ%0F*yVYPfD~=)>`=Nib;}_pQMz}be%yc zW`HlgH`N5JB$Ly5Xhtw7pj^Q3qgP(gClU&I6h+04nB{EF*`^EkUL6NBU(C<_RX-PO zup0ENAtg-}!*hcc!(SdgJab~mn*hDH1HvPEg;BoX0s91ZX-xzVA98v)vMl$D*% zn3M_6MZM(0+|uaD^f6@-DAy9~Sq#TOP3EIDeI{3aSDVJaYQrl74v$PCvmQ_-1bM}@ zV{m-xsqY&=rEjw%LALg>Vd}35og4;LeO64`gaV!-i(jzcU4 zr5mV$o33=wGmqf_b-K~3RC(Eg9eB*NR6~!Vqa75X%bL#daA%`LhGuIkOL5C-E^qHP zU1p2b;>_5G_I1M*IUVSxV@yuwT2}RQ&f0~tp&2m{Nckf)5lD6lRT{u^R25+wLWVRw zwXm;)l>|Y-cgP-%Cs&DxO-`AAK|*qK?t4Pm z;4ErbWXMLi&`y(53rjT3`l``^u$0FvR@9UnuJ;*DJeVb7hmQQ$GRz|%!D@73#&1WbEXovcQ=`$ONY8+XEc7~(rteLEE94^%fKpHz_#Jk(>tJ%EFfSEhs zRI7`CMmz3wC@PiU2&}nU!-{@u^Gt&?T(Hs34f9DW`I&BGh1lxF^&VqbaElhxyWsG4 zO@^g%O09aG*--c3vuRCHSShJmf5^2@-8DK&hAKNBlaTC$RvnQxx?N~EsiB__5@ zM7M=pel%l7-ZAyvm6h9}Pqn_k-myE0RWV>_)98Y4vkn@C*kd|~fbPGis}yCUg(iuq zi1?H%nGaFPuj!Bs$rEEheBO6_{gSng@5KXT*p&jEdmk1&{{(=mh=_Wc-^)8 zIEwg$gnjmts9oAwqd6~Dlm#pKcK;9R>bD zT_Jj|qC0Kj{?*2{bCz*J^yS1!%SVlu-Q+?JYUj$nQ^%qrXEY0&RcgJRDTuXZc$O`% zrh!^rxoxGC-z_$G7TC1gkY?mYw8-Qz3L+1WJqwo>PS7_t--J(eydnqEV+v^l%uBtKpfTZsRESfV1=#Vw%t1}ZfIwoxf0>y#)t9w5pO+8 zrt(fykA&i!@{w!&0?JzWNJVoHU1d^gtrCgW`Dmd~t zX2rl$ex>@R3Pypx+5M-C)vTX1>;KiUUL?SZ(NeoGmia`|W@Se`TlU!@%?|G>DIAvy z!;_Nm+vI6+T!t!1A=nsNwH61cbX{o6%hR(H8tBf^t)C>gb;WPL|Ls!b>i&eR7Yzdb zP%KQFC-)1e8oHJN^XWr&qw1;{97(REjIQlNz3Xr`zNc*;6q|Eww@s?NZSFfT8)i~s z@wCc`Y+anVf>8^L#>L$OvRl{9#&leJ-xpQxZfgp>Kb?wVF2r$tl~h#|oN{Pw<-vaU zdQDs|za3b-l3FQ2&G$hZp{Xr4#|$GQI=6{dwoe($@S`Ya^)yHv|awX zb6iko$>X6iCe`OwQR#F)IwI0RRXPJ3xZc+iYI`y_kx=l=5@n*=x`yiz&5+^PTz?gx zYUkF25EVP%ov4>qETj8DW2-!;DV@9#Z>9AH{Q;~6CCm!HQN=$o7p4_L40iLBzLlku zRk6tAkY{bUAq=GQPIay?^YFb8ss|7#OzgVlNIJCtCXvjQq3&v6=lu` zXlG?s%0JNNbb}#oVdrBzh4ih9w$k#=?F_Qqi&+p|mw+bG!10{uA;siMkwo@X7V!D) z*k#jHp$R?IWn|%rABKs)7+cQeOfR8wihVSVS3e?6(gGTIe3XOb<>sXohN>wUP}W`( z>5Sho0~6gV*QcY2<13V=THzyUOelHNFe+<@J5qlsva|@LIlDFR6c9+UTE{W`!gn6b6$IPz<*8Wn`72ewsF!IB;;)!rDefP!p?SMueji!p*_scJo1RFWua_7l!#yOfaUz zX1}3^MMkd;@9!I_-hYU)y4*Nf)ucI0FfT3MU~lp>0xPIt>`m3ubjy#-bH@uv)y)!k zgEK`re`)Qejl40*D)yxigMw-_z)+y!p?$3X&NAhb|~N|0cv40-R-<)(&YNu zNy@f~yp5{_mMM`F1E3A;N(Kw(azI(5iMkQX5F$7y#AWO8qe#liQIoql16$8_veg8< zJneO>O+vcG)eF@#rq8cBX9H-;g`?b8|HhAFbu-qZc_|(LGI32y1Z$L|N?KChdL^ZGV_I zPQZjD1>Z29+8k(ZYq1`HLPP1Zf^CI4BHpSmqn3TV+kc15ApZQ;_}_ro53?WW8mUe$ zxT*bEaDkI4N0c>L%-BMba_ZP)?lDFTl}vhEAePre)}lbH-9@7$V$Cp{Cvr6((>~>w z@!Uj1BaS3zbaJCSAjgH(@38jDqP|qiq0(Uk$+9Z(;ZbQ3Q#1ii^2qsB#oBorBT8@w z)#6K>L8?EeynS4NU&bX#|Lpz+Yr=(ZCQ%7|17SS#>8jMw`75|BI5GG%Ja=E5iWF=5 ze*N1*_LUW%8sY+Co#kO2%Ycl6WUA@3*zEgrQBAPJ;{)*aL(|X_du^(gV(^CeEd$Bu zF#)ji*u7-gU?{TmQGTG`NgDBM`?f zV^n>5SmPfZ{ex2*X+eYY=p;K!F$+9vu%vylTf0NGVgC%-yje|RU2}U*;g7Rxs(E5A z79$#>c#J>)1WZ38WaL#U87ZYN?ovHhZ|x*o;}>E&c0U(qU)s%wj5ET;q;(Y(s49( z9N8ulLq^*(Gq6)3VXC&{TC9-!C7iD)8NzaJPTePuykD4!(hA4~twfTg7>T(|{_q+A zMvN{NDZxS6&RY=@b`i4Uw!@8Tyr`m%T9@^kdK5aGycx*)CC{v%unyk~=5x)L+gU^BVvQ|Q z|H#2eZZ$o4?z37>T)*Zoq4;N(^35;&t<(&sNSWr44Y_~h?4pIDPoHBi3WV^{j#OyAp9X;ViiY4v+$^^irmFGJX`3?<>@GW@c$}hM-^0t2O zxz`XXQRd@F(v57Mx&RH0=UsT!Wa0hNX0kZ%*a{$k<|oNLN^_o0H{6s|n0umikG*d` z@0&~k8zyyHTc?o(QCN0;)PBT2WFdp#0|<;2^c^$eC^=JddL)7Elj@$*6shI$D|K@0QK;1yK6((#Bs%R= zTdEm1&(ypE;eT8qs_93TSMv(Z^=N&uja^{0X6V8|&xcvAsey-0!e=nUu{QZ9$-`Cw z3>OaLaj$)SHJ|0xg#0^tp7JQ^EZ+%OE~9&XTA33Iv<`6W2KLqnC3 z2DLoTsHFvS3qEva!@04@*@S&TAHp5tGz*@Y4?j#6QgxxuD9*}#Ga*QM-Pb}XoStz6 zSq=bQA3g;DZvSm8Vhl>Ll~h3;JtLaYqZNhnL^HZdP*8fF8Q4&K@6?pGOI-#t+1>Ms zS#S;4Or<+0`QOC0^p;gGI;VrM{2e8shl9aTM-0(zB?elr9 zw+A`?_iLK&d4;fgvSCNfNI4fXnq3kfy$m|c3bzD8lVu?ZWvurrEfB9=43mCvs)yt zWGhDFa*hPSEP?`mkD&DMVvKy3ZP`o2Ds=tz2ebTcc0tRRrg}XrCBC3)%{1)5TufhW z@l&epYA4Gr0xm1z@b>)pbej&B`Rj5spT>&GmjhbK}kq|fi<%-2fzv}_6`j- z+NCzW6%rjagd&{&YV~|R7;`4$pRT2|nZ&sWgGMIko6=)7eR98oL(a5{Hy&r5g!IVR zLbOh9{LX6oLw=0phSD;>&_&LH%HqTJi7S7|Uo|2DGRD^r9cGO1N0<`{cYnx}Ig*%T ztKG_~UVkhNdtAYAcpce*91F>8`(P7H@i~6Dl5##{RAigoz8PT=M2V2MY|{MisYU<6 zl5KB53gz1_KhGIdpq32B;&#dErZ5D_9x6D^%1GoQN3|cu7VYm(E-3lNH~XFxo^;1O_5B9Px(m=N&qUcf}d1 z@X}3CtKmbay+2^LGWu~ah%Uv>{mL77Yb#q{pBM)PP01xBMT^+v-P#$`z3z0R-i*> zz8l9W4tH?>1IaIf%$+;+vV|Tvgq(qj(I7T}P!cajCoJya8x(crosD^OIo6HW)k^TMRg)n6qs>9OMZW8 zOAIwf5s2h)_A)}mR020~E=cYtAKj-Dmtjy;v$p-5>IqWvohU+{_d0}PNb{RK^r%e z`^Y?zW#L^TJnulmah780b&PB!qIP}&PHme)i^QM_96qL_Qg;==a2EM+m@CgV$Ie_^ zb5x!P0x3mEJ;x9sBTqd*zt^d-1+a&F{68PwE2*ZXdAwT1HCkRw7BSgcYbSkXIC@)e z2`?Y?@DR3vWxB6S*4uUV>Ae$#Sew4$h2$U&u$8ka18R>Rqm{D*m9HtE^>D*992Ulj z`b%%lN#3=BZ__w_4M*Pn|21r45=7XZ9pc&@6k=q9j26@$qwn9Su zQB@IZon-itiw9dixHEqrKuQY)P1N-4*?v$kIm470-dt4PZ0q)&a`GNL7e@3X%m@nX zYQ35vUr^FX#(s@oP;GuO(qY2dJ|^C#F)duJ&hEFVQJ~P&JUc@*&#do{$nOf{UW4_u zk1pL|@%c#t6$qA>hx=&~LA8#F)$ylUOmUEW`0nSh(F<|usI{BaM&hec4k^a@I=oT? z4o+s$F071OY&Z}%5zo9t20JVA#IU$CM6FhYrVk>b)oR@Y5~LdzhY*=S?`{|98D6$2 zJJp%iwocrzFrO3z$JLMra>XkBB-#7Qn);Ju{c>l-+u&0+X|3v+o-nNk?R-yhYqDoX zQhBP}42S8r2n)*r=u@Ci#_Y<_$nr%-&MH9py!(y4u+r9BC(KWhH)bP8B{`{(x;qG8 zeG@3;C&?((5NpTV0CVilmYr6eLiLz%?Vm;_o!#pB@O+6E*ThDq7FDu@?>1*a9_-%F z+n&Y@4cEF*6!4T1+y}eKm?{V1L{d3`<`6(J?knHU)z*I~$rkrLVb9KMduU=;n`U7> zgWR*{CW+QLQCMB40FOSn$T^j-EpO{Xtxv=kEA>fywE>N!$w7x&stMGV-1nQanpboTU(bYy@4mzz~IyI@U9tc3-tO>QaB?TFP)FD}JB!B&@QPDHW1 z1X5+U2#j-nhCf6LUM%D9^)at*&j9B}dIv*8e+{4%m33&e3VnR=lZ23|E<}t%mQn4Y zQ#EA1aPaW;CGaTBudjO}o&sCFJp-PPQ!9+m8^P#ZFA?o}rBqU&Yu}pjI=dFis?>id z-G{u8TMGk5HTO8ts(xvR1BQB;GPK4{pSv=R4wk|y4XX4k5(^!>oEtlLR3_eij z_6oa5dT&@~Hqd6!P6bEXDwD{Ji2&60oL&0g1N znfvfY^jLFY88#4K+h?iyRR7^@s!*bO%dj#MP-r19Z}Q$t3r-u#3Xkz1+FuStc>OtF zdlC^h#%`en_(^io643OMWVnE6mYsrVRP-n^j}=oqRgOobM@8r!of(Al8Dkds79u$p zw2L;^KpdTrPG-J091LVF)E`|-^%F=HE<-}-m$R1c8%XaL?$f4^?y(Xu3F72&a&mZcQjAY+Cv2fTQ34>@Ta^UB9D@zXWDeO(M;|z z=x%MSk?eB*;N{y45g+Qa4Vz^$d;L9#>}R3niaO#$4aT=KNzH+W4x_iZGdE~%Z6!mQ&g!o7eVLSn%HyNT7IQoKB7uN7$)L*C%1_+EV~ISm+7Yf* zN-jpx*hy+V?K#i)Wp1=lRd$kPUcPxymqXQR3#A&$aEsKY`$v(oS)OhgRJ8;IR7_i2}$hJwlktj|9JgzhwHVvhQp|SWqAbX5&2#AwY zXbZoc6@IPbS%Enpy}zQRl8b|TihXQ=y-)0_bkH#RCrS0S=eIyBm)!Ig%c6YW>Bcd^ zc_B1FjPVo_sF-yZi4H+@r&0PnsANnPiL==^7$xl~r*Zb-%G~@(g0F^Y{v`2=zy0GL z1G1DN(7tbRnE1k=5Sh5yEMm25>z6~4dgaRU)Z=3W;ZjZGfMgFD;VX#nW6M5$^FPP@ zf3FR$oajXf2)deBO~hnyK{M_1Isudohtq_4k*?a3)kKMG6-@g>ZJnKV0cOigs`qp0`RS!TH7O9jq#oAHOQ5`w(<}0s+IG0_p}I$Sc%^<-1j*B!>|`Tc~CB|9D)k z|51hXf0rcvkDdO-w+qb-Yv|k~X3Jh=)4Pr$k29GeT8+yes{?(euD?)b|9F>EkS3YH z-0XLjm`qP(q(Q@WcI$oe*A`Hag+ag7FjPmch5*o^#b_zEuIBXchuDaBy+3J=XyO7?y3qDiYQEE8=*cXgQnWMU%}&2r_c} zmqVH^Hp!Sf%Y^h8!o1dVC$PQG@QW*Dy9qCMfBZ|UyrSf8R5 zpZvV$uDh^`3M9cPurK{~lTupbeeLMP(sG_blDT zBY|$3(C3iUCCvTmdH3}VZ_h#m@2Dw%!KU3(w?T6k_vOXx%4KES^;Tay!(2>vkO01k z{!kAlSC~@;!&>%tbR13lu*`k^&8e?w8Zt^P9yal%?t|h*sP2@5Gy&D;vY``@iH!40 zhb>JXjiXHGxQ&WEg9i*}R69x#E6cDbX6D3~FtJ@ps$Dw5p11rvO-@N*N|$Vi%eZc5 z;P^?O;&OPolby8TZsiOe2g$7jiWo+>xp|2BmyAg2l{PiMpCqo?L^W$y6x%>f_Fd&C z*0#K%7>AOw)pd@-$nbbZw-hhfJXaXAYWSxeu^t?a7cIWi^s@EdXp zB#^bQ|5bdATJvfRRtsj3dDFOH=)c5C$w>^`6=d>^8!u@IHLIp-+n71mjCYn=RSEv^ z)I3pJDdY);KyM0s5x#G2HQ@T=utIT6-{nqyh)83=ozpZ9oG}!x&;2-6-2vHkZYZ`{ z*fz_e1UC?K?h0xH-r0`qz$LT8CNS5-$r%ZO0@1dx8P;ZN+dte+{ zr>$4(0sBK3hG4R0GM5@;X8?$=MPDKvzwS|I6RCcDlri=hT7W6_2>*icep$83K4;i7 z<7=llI!C&c@=cYs=y zFsn)%m8A4#7HMbfyKzG0I3e7agi4hWR!*D)fe4Iuf1d-HCkvw&f1d-zvB9c-p99Uh z*%JLHhM)e$u=D55*F;pXM0rXf`7g^F@fo(H)R^E-a%9bcbnsIeU7l0c+B5%`AQ5}6 z@vy#*#u)m@=|F&MQ}`KjbJrvsjP0*OZL~ZDDXnyauKX(57V$L3Y}B!xB}3 zrSY&=Nmcxsz=sEO^Wef+#P?@QinS#+sC+=gK=iGfjf<+~g(suNGo?ocn>EgWU8)b) zkw8R#y?<_@Si1oPJ&ZGu%LUI48z+GxH`li=rozXfC+oJaQk)uaZ_JsIP|b2ycznIV zxL>M2@p(z4=e?0j~l8J5XrG9+HcsEfW z_3b55TpolS*Rb@$B}W$I>fWl8Db8XR93a( zHTUe3v2$Ctic@>pJ~_oILF8W~<~80EVjoQ3WeF`Q#bo#N5#mpLC;(y>JJE zrE@ibKw4FF2QgtVLA8H-dj4;mFC3HKgHApTU>5Gcypn3#zqeQ&No7$@q#p4{6X(4q zN;^>4ptS)YQ=ugCw1e5*%Q{59rO^`AVU!C18$bPQYLGVJWHz2w;VLK+TRAdx zND-0XWY&1Uaiu&@McR6Jx0o@{cVq?sZ5+TGBzl>rvv5~t`W@53cD+W5v;)70mBp03 zV4(LPf_(1TFmzID8N{ZT3u`lKr`2%SEz;}Cpy79Jx;^x|j(Wxe5>oFo85F8P_^5KO zp0~N?6?@S4#mq-0oZjK_pbBs8{_L={y6FXBi?0hhf?sv`Ag7tdo#`z<#+mBESBeEjd=$Yj+O4wX@@Z--#-kgy){H+Uv`Ggc*{GM79>*XC6rx3W44ns?glU z4i3=BKR%Ss6Ltf$?xFiD^5tBNuXeJ`)4n-?IWUH4#Zi(Dcj~wk4mGT8_;eB~1zESO zN*pZYEgQXsYyeaz-qNDl_Ho^Uu_CGb?8qP71?OVCfc-MG?|p!LR?1J354$RN4r^4S zeU7EFNh^6VeKD>3<0m?nB^3EOR-v-_L09P{M!6&2INK=+l}LOJgWN#VBw+?X|iv8$S{}9NX7d^e~5q9Xd?`WF(_`)~z!?1;E{o#1X4EVvUf*^pdd&v|~LOc4C zq*lc^;BlR2=*;l2GSrRvC02G3c@exRQ;%|BL|5fkc6t zLphGc%;m9I9S>@-*|@xhgpcKzNqI4OSW#!?;Ivz{Q-2mt$enf4-3%z5p+76|e$G3S zX$lWo<^Lv8bzPt^1?mms5Q*rH8NG*)&n6;IZ+{1W=f%DyyU#z&b5pAM1MG7d#*f)z zmj^lToYjM~5uzUkgB%8<;je~X&*g?eN6&cqKen5VJKb!S^jr$@C@-~`QArPVxl+#6 zCTgEvf(ghiLIe-frl{ONL@AjJ6pg933u!BXxdOqwY<M((RmHqVT$1_U70ot%qr~(?5K^ToO&6|wf@yXx(vLc z7&T6mT3c6B`Jo$6G^D?Zjfwmu5!>A*(&t3a-UvSf>L)|sdHcKSbh|eb&b3__8VWxy z>DOAL#=C`p`z$b>qPkon$vD+F_R7uvu9dw4W6Q7k7?Gap)ytyYsh3VwGFm<+ucoi+ zUQudu>r-%1;H+2Ccbp2M(`N~lZNTh>79)G^%Iew5qR~e%G=?~ef2jZSWaTeP6xSvq z^V5_+#(%eB;8yV(*{~u8mrDpYTSMJC3v_$aT3X|*hhJ1}pzUTxW|;jltI|>QHuQ`- z!e#|#z-T)c=RH=XC9Bkwj|wQ~G6EcG?HNEaoR%u_F06Bpx*)R6a<5847mR$twf&Dg@fx)Yqf(h_+kDNNy zql8clHqeb_O$(;27%fH&N0ogy-+}c|S{GyyL)14IRD{e(cUV|@s_{J)ZtSw?K<1hI zQ2KDP8Wlh8z?`V@Q4U%*E?SlxJf{BJb^eJN`KQ8vaml~`d}Xg{tHhcR_s!hf>-h<*##%nW$TK`b(Ik!FBE1+G)Ob1_oQeP>YHr1zEEGpK z^z<|kT*91`#zy!`Yo|00haL%@_?C5nlY)dh*q029>q8{%L`Rs7YVIt9Fh}Q4;&+FOIVrBeXNc@t0V`Db?Rh9nagQE3fW5?2%3ddm;TK4*#; zC&l;DNO@rx7lhVi@KFbh-WujanS=HLDaozcRr&W8+|on#dDru_cJ1B314kjjHNxQ#=mIqm1{{RE;!36mR5$vLx(zwkuuI}qnx z*`b<9PHq_1hxj&yV7kqv<<4>wPVSQpJxt(L;r6NjEPuW$+q8Dj!-45EZD`$%^s5b$ zalc^9qs3*`v#qT0WR+2uri3xS7#x0L5zy&$Yod~tZ%=S4uXY2T)86})&b3`rY<~Xf zrq_r>(G=h}F}$i)_XW__8a@7|OZ7o{<-$?S8 zXrxW~HZQ4I2Dw$FUi&90~5ZFtFIyY;}Z?JVv!cZH%{3^ro^w?LCZ0vLSh4;{0DnXWS|O> zp|7dn6yGCC&r6Ett%|0XIkc~}c8qF{fRZPk=W*7r7L=!wGT=`JGHqSVA(OEI`xElU z_tN_->~;+)+U#AkuCTs8k0nMIxLWl#%-Jx}R!ezgf~VWE zvtob?0*uOMBqS@UrM5Z}LZ9!ENyWM8l| zWk`4%+nUm2YXkMlzPCUOOy82~lt2wnBL;S;jkxWJo$q^3SOFVG(W=;Z#o-Ki$(a;t zbz*4bScr))P*JLxy?m~TG@RMEG2ZCwY7C*mrO1Snlc&wFCKTbIFBSJ#%+@eElT)(? zVP}Bt7$u77i-mrPo_BDn_t3I?R+tc}xT6hsw~&9GWTVc-T?BNY5rDpwS`2^6>12#H z*|ooWqem+m@!XPX_gRX3OtK3&m~$2tQV!jKM=~BpWL&IIGBRGgLwC2iugpf##6Cd1 zM_Ir6#AsFa^t3+|`?{22RJ=tx?yHGh3$oC1-T)F3VoOD?OV))OQe&^&i8uj)J5Ct| z?>N?V5HQgSnQyx4yOv^1Vi{XR(nqo0S-kHecozz~tCPed;~sZ}WObl!pq3FRwRsbE zTbY{_GD8KS;jKy-f!vruO4CuNQOnw?EYuVz7Tx6eyEeg6-#wifXZmY-RY-_eiBxMY zUm>Xf(BU&03hS$noTOE*o1Ser`l35}DxMO%76>j~X`H$rW$n*B3Ukl@G6@B^2`8LE znlk&_@<;?>M3o2fK|{NNe9vL=Fyw?nX;M5zEj-d;%2}Mcbh@Jo3UshAUXUv;DX#SI z3DsHIV^-FX)M$6(6PZU1=dxK?Uc75|Qc{WdBC&po*3%@DFEg`nm72(@3y-m|thsYN zh;;I*wB2Z^4WGFFOD3ajjV!dT^NfDZ=(&1NE6^i$4HBMfkNVZ-1K7h8rOJwZa$z@u zvB%N)Z>gx&LFjPUgXpoF06HxM&eF2 z)Tp*HSkyXOclC|Dt6aMw!QKwBhQ@CfRcU-ifJSI6X`3m>LMu zFCB^Qgs38WF6e)f(2Z0#EleP|;}mo@v_W~bm=qJM{H!X_bpkxwO$1@AlKc7_<)?qX z@}HbRY)4jW4sC+J(P{=wjLlUJtj;7mCBTZjReJTBlWbMU^ps)Gph^CK#4Loh<_0|C z!mEeKDbrVN3Kn5}`DW@3aV`E|aP6)-TtT1wVdtPf687$f>7gp~%ml)B;MukD@B==J znk%KNTN|gfXNhqI&jabKuui<@aU+%{*881J)Ovlz8&dPn8ExkHT3u2TTav|nX^@qW zle2DzQt3ZX=Sb`y3vBZp#~#xW$$yZb@!ytaDQB;GTb?U@JiB+}%TfZcbPnO}O zmw;c0=xmSf-S|Hh{G|NH7r%_KlpI3MPB@4X(Mk;$AgG*nNfvHUQ;qjqV%l$59#g z3Ki`^U8S?&9GLL6M$Xs;gHgy$?iRjzxX$x(4TU(WFK`D|;u4Do&;!V*1qOpzm~cDn zU$&B+_}_CSa-%i*m_>@IjGw&yfwq&;KikA@)>5RMR!V1lXN4xgQN5&9C6NHVYNO7Z zwHnE5!AnirufKf?`TRA}Yu z=!{|2d2A_!UN)N!OZyQy!e1V?t}OJS_CPr9ka{7<({G_pPIxhWxXcw}#qNs^OBnfr zR2N@Y;Y;c8D0ZUml35E2isIuD>s7^pvRKNa3}>7yqCrF^Z9;e&_1BUIY9+5sg{RCv z7baK+&jxygHhIKb8Lk$c`ZX`#yR=K0$i%xjJYeMrlmC7=onRwfcBD}Q8g3VLx__iD z)UHl$B8u0X=-IvIB0Oj9I0UEf-R$|oJ3D@%JRW|!_=Jpa&I;WqvO8W|)na@QqsUVc zg~^gb3=KY9W)VqOf&|A78}|a;wwX;m*Q%Sk&eOiB!_yYeT^BhG|Bu)9CqaPv#oq2#k%en?djcsp9n zr7F%+-dawN8287}RX9^K6BkLTHb%EOnA}+f-=^gtX}5C==O0>(@cdsb6KnKy#)UAQn6ORbXC?Z#4gZys1Lh($UBN@@JkM%F9Z z)^etYjaE+yW9}6ocYFyC*E1gJf~Fmd2+&2*4-@0ra8}(KOdluI#uZpGypW0snOJGV zTU7H~%(75jG==5?hRlDG$i>@jbR5DeU$&uy_Fk>ls{1@Vtdq5456!O44^#f^oLd@o zqo#)6Q^9y>d7#!3E*W|KaWE%jaKXM-seft>zV<7apTA=0hTCWb17t)%hXy}TF#TvD*6*h_sFvw5#Z2{uZ(0Ae1bt$b3nroT=uQODA0$& zbRqoj?#}C&8|uASt7iqN6#!PC}|W&WrR!#Z16S#vJE*4r6lFqSa6ACi5d_ZT}v>Jy#I6kYp z002ah*0#BgJ%kiQOY)mhm%g@1Wo<=rJXe1RgnC z7g#1;e{r_<;VQdP5toh5glDRX^KyS-3fyIb8NTDx!$z7b6ja9pgm<N}$vS%%0&_e+D-En&Yo-YEX#!7Qnwk?Yej?r3QkszK8p)j*?cCd@Q!GPR}c*!-2&$)51oOWrefgM@+kQKnaj0ql95<1HJ*jMY8{@=%snIyZCkN3A3JpyZPD?9r8w| z-x>?UkDh~1BTgzR+4i_ZTr$CSESPIT;r5MJ-I}krT4RX%kDC##P)l7o zH=palp+lXeufE)$WR2;->T|{ZFZSLutf{SS7sW2p1O=o^2@skRdR6HOO}Y?@lmtTW z(sfDiBqWs3q<4@OIxguYfP~&edI#w!;M!T=`+ocNz0P&M{at6D{p+0hBXi`+Ge_ne zbIdWvc%J*YZ&+KmP3EC6&^CEVAE#4X;T{!1_dxu+4+>s8I_&IE&+#xvw_ZMjti{9g zhqSQ~tOUc@_uSB!1#^fY;+q?HxF>0=2&K@-HdBR+w1V+=5Gg-2oAqyk#I>_lKh#j{7X;Aq@YnoAyukC zllgKk?WhR1wrcowAGkYbRh+uD!|Wi^;aY-oD1*T&c?Ty2W-Y063KPHQL3!tNf5fDL zorVw~PKKfMZuaGN~&+3R#ap7;SU$A#|GXidWc?x!C|;>()W_(Dm|!!(Q6>Y!2-F-tf-W0 z#}2?|3570r!N3l;$v72S77;oWejN-mvm<1cWidkSjB)uNw@e|1W}715Od`SRi=LlO zJk-UuT7oBVQRSsOv9l`gc4M4pm<;W!-PG4OiEhM#2UU#G6|?l_yV0RudX;2-%sp0o zg1U#>%Fkh%Y4Euq&r`&zk^W9bwDlI$@rEr42}4`kFJ(eNVEeN*zmW#=XBKZcHs$3P z*^cDw=KdZyeAu-@t(o(U@K$=a^FO4++D)Vqa*3R&n~gv|aB@-az?fvNURA8GhjbIB zi$cpEi?CX4D48-bdsZES7kQ>k=W5JdX2ZZ!5?gQP@@Zs4#qU?*$ox!ItWAK7y%3Lf z4iIJ+98(o8Ij~}<7m;9A6PNAuq#fMFn=~vTEP!cDO`*8F&BAtlY4g#_hOT6I2Xm9R zjV*_k);$CwrBAW*0^-nqB_;@Y-51c+ugQy(4;$ZfRruptmu?pO%kJGi0o*5)lbn-Y7 z1e@q5a&Qu7bMkqTDu(E77g8}lo>Hm#iF?0<^C#p1O=hxEF+qdka34@yZqFxPo8RU1_s9~UwYphk#bDqUMkMFq{M&M=IDr^kL=T!OwGf8-JM8Wb%E^_XpgBFXf#y{=8tF^ z6c3F67;eh5ek^dL=E5&HjrAs}g!?v~Ms7eBDuo|W&mkB)ELJ9~>#T~~Ht?b;C^3T}Z z*xce+`*wW-dyd*THhl`a(wh3*8AdSi6OA{&)->cvpCT!+!Jst>(v=oyZO)2^wNBn> zqmE=$V~=BHT@77OGkJ&ez@tBm=x(cmc^(fODjq;I(Vy3l(_ISM@C%juZASO&(~l$Y zcidK6)GS}E70ztnRV^U{AC`1M5eIotm57cl$KfQ#bdyG`7ns9!P)#&HSAr^=Ok}1e z)Vd+;8QR|5D8{nCEs|wS!a9&az2^1*gatnxEu zVffQcU#7e6Ei;PuWVjh6XzacRo1}`o@UwqGoWwn58F*9lh*gB)fxGO?`x5D;^p#fW zzNGs_od|FX#3UlNT7pR^Xm=vBRSm2=KWgLURw=ygV~DAto>5J`r@&^pvR@Bkeu348 zB2PHkzzT6vB>$qoNBVZxX)KgMvecD5FZ#U)Ot&H>jc~vI(1KK=FlZl#9 z-8tb~!A)sLJw3Pnlyargf^o+ZiU5_DYW4!2_<2Eb)CQWm=#*mv`Yt}Q^J^9kzp{W9iUtflHka8s%V7xQc9gzPB!r~VKj=T~a*S-Jyp+g1L0UoZOMzOwJEV z74ApoArRw0-nD5tHT>w;L}tr8B$M-`hLhVlW&7tnpWxn|k6{!wPN+Qh91Hl0H$J*1 zZn-aEnJBflYRup%Z1iPYP`2yqm$J$TPXkWPkzm;U&HS%VlO3#W?zOmSp?(nT)Jww> zxjY>0xr!1S&+ZT)m!}Kn&4)?f%NXO?dn<(mLZp)x9DkTPsU(5`c|RGoi`jg>krRhCip9T3?u@nxdCF*k z?l~G>uhuY|K~#+;uRQZ<)Q1K)Y`_$=O^bKFUKu}g{$YprLiViu3yt#PpG zlrWqt7S8tO5J`i5^~2B1y_T3$Pk-Gf^xj zFxSFPS4J$cCH>RZmEb9}fi-4B(40TXwjrz%&Mdio6Or`9cyot;64vAXJ*iD8!(q@} zU2%(h3uVQEV<0Wp@E5Kb;xw~)wPWIBIwa|m6hTo>o|??b#R*V@3-97n!M5p< zTos;$Bb-z3-UP`Mn|LM94)+|$UNAS+mWTkqJCwgls&h(}*XGG^aH?zQ>2XyHDe*U)y`;*YYO{`5r!DjdUDZE3Sy`-g-&z*~ zX1ecd%h-$VL6bGFtX{h%-=WZ%-JS?cwq*FqpD!peZ%#?9`_{2srnGk#(6B&D)6g(vC^nTu(e04&i5W$_EqKHpvc zOy`K&4<{6SqvYFJe5?w(9Ejf)ayTf-M;TtVE;gI>2X>FJ+eL2|&XYjT8 zWX2bg*ikumJQ7H4FZWfEf+15kn1?bjTFGr$pjAWooS034Fzr7hZw}W((x{>T2&$q)g)aBrMh5j_mq_oHVIx`c>fm4Ac?3h?afWoTTvP zc1N6exG70Txn#jeIekG%x7Zo9s>nA+Hi!aKb*lALcWepF=Nm@KoeP<+^8v?rm0??93(i%6aPXL=-$jg~85M6Eb6-Y>HU%i;(A1Q$URnG0- z&%rgew?y7mG~obW5ZAU=;19<)&`~2fWJBH>bWf;Yk`*>=Y(UeDZ%Rz81$IO&xRfVk z(Y2UGU(rcZMJ_E5*#>d4T{eWuw7Ue$sJ>?wkf#dFKoLQrBTQk_>7IsOdNA{`>u}Ub zD^nr?lu|Pm5+pq}4Y9XI%r$04Ee}#>cDMc{n<`!ZY5m7m> zx1y9U%$}=`=k1M%)hy?mEx5R+U+Z9=SR#X|R2!Ft;<(hTbVQig%i^oB8Hz<*HCAXO zjcy-qbiHkDdrR(}!v{XBZ}F6K;kZx_ZS6Xe9!Y0_dg=ITTt4r}M|P@odTN;ZO?D^p z=YTs%r0!ilT>44=kCo%Om6Q-=X#FQ!H&W*pH4Yx4Jr1re4_YNW*N>c?@M^w3{ zK}zKVg^Pr>S*`&X`#m+g4!1A zV%mNCg0Q+v{%nG8s2|xFSsU8JoVwGXeY#81M z?kTc3Y<<*rm%oB#MH7GMILtJ0^}Nd{NekZJ-d2uPwK`XK%~b^CcJOC;$<$C}4Ag?w zI6)i>mWvl(5BF!%41yG60^_cCUvE5XU0tXXFS!hVbipkD8;QWXk7;&diD4mFf{Idk z6O!$%&Lxywa*5Rq4eUN!G0uw_{S?%*Mf(>?>%I_CEZ1m7vS(`X_sWRQIVZO~sofwi z^%#+^+XW?pv6|>J!;(vzId$aMkPsS;5^=6nh({n%KwYaF%6iylb)-ELuiy59sQ7zh zIQ8{uBOrty%Y3@em_?dj)gs@wu`gc3DL)d9$^$A7Y?NAo<}P|#gBLMVfa|;ttB@r4 z5n*K@sTz8iCtU-rXs}T-?bnU@Nlot>bwrO18uQCPB)8sq_j{TC`NL1bzJsqEJxnR3eIZVrzCd68jvLF=HW^g7QT}?+Ql97>94nOS0R{DV$ zlesJ|NLpgivlFZT#1RwixxY~LGyMBBOe_BgaeGS_0T~3~mHeHRf);lyzO#M2cCJq3 zTtkb7B)*MMLJ3*yoZVSddR0w8_s5L(DMTW8?nAhWnq%tsKxeAxzt7tzuPatK*UHp8 z3jt-w?xaEazws1aCAm%{x!{9D;caT&k9|C45EYTQfzPNKt;*%OpCq3^KF(_V-8QMyC4=y*~ zifJCcP)+DXt6-=Z*c3V0zFb?1r4tp`V`cVELCEZceF1}A_NYBsU;cef|FQix^LhXO zcASfnw|eMfE9j!&%n;k`M#gH(J}T)dtgdbhgmZ?Z1`uT$4Q)aM)gkx@eElkD-vn$D znoQ@_Xx_9m!}fuE%yzh8sSy6={+jm};hD}e#!LrKA;qY9UPm+$!-J%|5^Gf?I6WON zS>qFXrhRZ(z-5qzoWWJvgP+zApD=9i;62a=JR{z6_w`S&cL2FOTEZ>ztV(SK6aHMAT);U+_A|g)rIi}98b<|7G z=k+cvKz&NPI8}OqqShK5FTqH|+B9z?8dyUfBdf}aKR}++neKu{AJ)hf##44`{2)Ne z68m(b%x*|buVhk8Q|ymL%-2Vk-T;W=I)_Wm@cfVZ1|@xOGkQZ9%Y6MgU=H>9Z>+l@`iRc3!t6OdGfAO(g?5<~iOC6jo z$s|h16BAP6T0ZdS$#NX7af9`30%iL-MCN+YHfH3#x5@I)SplW8ep_uH%)Tg9hvtc* zUm1!QYv!+|1a_Mu0d@fDon`o9{oq%nt=5qtiMvOYp%&s!911?MOY(Ajq;f;cV$Mi_ z?Or}BrGN692Kf(!r4*b5_Yhnr*1$lmDXI9>xjk%_EDflnMEJ zG*8@wF+CJ6_8fg!X-nsIwviUC$&fcL{Uf!dArDRcw2%U_F6xeLj1N4!HYh8uyKEBN zWkDUFV%izWdq5<}A+l=VFg|s(U%u!y1K5_1%GJaeyk^}OtU=6`5kg`65K%4p9I#ci z=E;D`A@eM|QVln|r*!Z-6a=}jI3rRb8`O6R+Rg5N6^fYYA z)^uFAQ{mS8M8Q0-6#!Q<-TLuZj)+ki-jZ=Q*QA^sZy&GvG?u5=vm19KVW)PMy9_So z_jP?fQYR^5DAC>}<%$B85rK~wEAIsj1Z1gs?e9fyD$}L(=Qp~jxIr{LW2l-7D|DlQM zCFR$9QhoML#b8b&n;OBS{>C(8PhEY!twAxU+GDl12HUcA-AmpC*8YR3^hEZaHQlpR zeQ3Fw6BAEuMBzkhy*Lwx2oniOc$Ww{R65_9qho&&I;jTHkECV+PzLq-r5J<= zBmxahT2Xin|G^zW=&Q#hZ?59!?(ks`o`HriyxfjPjULP!>YVX{5U{&<1!q0PPkiep ztx7dKFvd1BDj zKOds~#yzGLE;3omFz1zDsd^&9+iu2{DnaV5^|dr*0LIXn;;_OgZ{XMw^AoYQFzT63 z@%pwhz5JwxuzGwkR*wx3HuDnP{Mllc!X7j=*gvhVK1eUj;5b_C4Zy+uy`z+KDJNo| z4D*zvF%2y7&9^o~e2d&E(#)gcM?(REy{b!+s`&R)vu-@&_c+Lk{^LzY{)uBrRb4ne zXPFXP*6x&lV^_IaQ=Y=t9{(miZY)H+a7oE4*TR||g9XEFw0HvJKO6`B4CK2qD*n{o zlDqSr@jH3=ej%Z9+%N5tzh8cssx1rh$zaRndnNCM?Cq8t>=&EIr6=YEf05ifjIZhV z-SnDe-zS-d%C6=GB1Le7sLtE^N;?m%Is2L(usIo5_xt{f|AhF+7LXOZ; z*hr0ZKLO3vr(Uep4xfkxD^21l&Seo# zv4K2yA3OSNY$okeuY3Ki|pwv?CKAlgu_Gd42Ei!~b{EaEM1s z020ONWE?O75nWuEHLi8C?07`Q7?lPPB zv&N(N0}5n4&8&`lJ|r(Em{C|fnygSDPce+Z$jL!u;NjO*8}P)n#=EvR^y5p~);rQw z?WHaIhhKUd2q?!tN{AF+eSP^V7LdoVj~ir+l-pFmzs~aDjyi(oB6c+1H?Fz1u^89x zFnNBuWr=>Jg*r5QqMOLbA3?lr>T%S`%2XDikW}eUWvuof-kt|q4eo>Pru_$--01E^ z$Hx7F)_WpykplT3*11!`$aeX(N0jB`PA(G}%!|vnKVYLS!y^P0xcT@TX5zx8_$AcR z6E<;~86hfT0RxwMqPb~SI;q;O$(=Uy`ir-dEl#-gSeaPchRn8+uebp6H!n6#P}81* zvoOA?){%Fw4dz}Rh?I?<4-vqAfo8GQb9m_Wc&uXj&qTx7kYq;&r_je~rr|W3jOc*5 zwA=%=m~CcJhv;;H@5N=9negzQes!%n!yqY->on*wrB3v=pdbN|Z9C;1<AiyM=%00QxvtYXrw3& zZ?&tu>!(*4#3@6t_e&1{X0=ps+5XI76AEyUaggBkV`!Hmk? zTWO+`zc_1yHN7CfOx3UrucQ~KS|`-Y`K}h5N!b}fmQs=myh3-wY;)teG~_Ij(UCv& z-Vo;@UBAw7S7~{!!fTxAkE2J9Q!z8@9&2vvx)~AiRzxa5VaFcjgn1hl3`UNx=tcM4?@&FrnA~H2KHw85L+OEU9Ccy*nM8 zZD*oJ$V+u3GQOq2If;$Fl^RDx%XDlbw6Oe+q(G%a$jcmE zy08q+sn}L@qTxr*{evy?YOJ{wdSr5LY}j!+`FcBuOEn>Wt6`>>P`yDUWq+qe(%mv= zUy$_byhF7*i&yx)3kJ{6ja|BUAmNK{po8$R+QNyf@DUZLsg3uL6b=y?6{hl6a5L{R zaf`5JDSw1+Mz;%A14@BH>5lagyDW|X0T)kI$c}hWGoEc+|M?$H!EZc-Mf@sw4zGXK zu~nHGwJ68bVGZc&_C#YI#8e*47&FoY^n&CFW`1P6Bz;z!lpkKl0LpnL$Skf~dx?Ql z@{N$PEOH(48X$*NJ zTCGc$sL+dsD4=Xq5;@MuxO;}V4H>;h5`A7m@E?HwvJGL2I2QoVc^?1?P=Ov|k%RTp zZz})$jwlP+x>;?>y|GO#K+r{CVmC zlC{N9h7So~%8uVv9fDaFx0b~}HIJ7Ds&$M++YGP0Y_Zjy>t4-~+3ZHp1jOO;0ctwb zf^tMSlv1EtcPH^4aGLiP=dMt@*}|QOz@ZxBIL%@eN6|chgBZ1YUZmFJR!$Jnd+0;X)}qB2K%%=K(?pQ%GTnt+(1fg~f{KPWn?G1M4TGF_RQ-yB!k(w$sZEMNkJ8V!p&2ocG^ona+ zO8z7p*nl&tHs`wjfeE@dlo0q0XH=>?WElt!NC>}+4hX}zj5ZJ*5W@0O>U374!UMJD zKaYT|-quHl#KCk>$4@xRSMA9a7Yb@|k_OS6Q!IV{zYwt}x@pbFSe|zrS5nlCG5^|a ziV4D?f;=agpdkbbCDbt!Bq>)6NTjo8NKK`_f9B?x87|&kQp0T0*L=^vL4%mPH=~dK z#4<)RYfM9&JLfAAz1PWZ$VY3GV(zQ+w$jk-s;8#mV&QzF1EvM2Fc})IRz1_&Q^E4W zIj1_&ogM~&hd>i3XUm^@*@v|RHbq6+xQA&!j3@g8BBNd>tEyl*mtBb zfySozu4$m^crZ*fG4BHDXF=(4CCo535>2l79@>hU?a>yS9WLn$g7;e3hkTL$C^*?af9+me}VPA@ zG#6M7rKyHqQk3|LY^rOTu3<(KuEuqONK6ezD6}d%Ob}D5KAzkD!Vh1Te_(lZKk=tT3yUvtn?*$L59^!>SbV}1MZu!tnMCp190r;;gWo({0LrVWT)E34D(Nh`9qAj0RZ9wplm6iU{IPB{n*S6Wc5C=oz(+Av*-S?~B&HU-XT>xXOk0kU=dQ3nB1(l|Xfm7VM z=lmxKU#2|NTtg`j)=NpL@^kT$h|vvA>sFKa zv*Q#Y*-@>AzTK|{iS@$tdJC_Q4_U<;oV%*|%XAmSaQp(^4w%!3=qOW&y=d5}ShltZ zty3zJfgzQbNMrYa;SMeKTI_3|y@FTv4|IjgaNzcWG-C@P;v|Zz9O5L{jp(7t;ht;+ za@in2a`7M52kDbLj{^oGZfI?d6ugo8FVKNa4)U0E(RY1X7G()~1K)G5izWHqLMs00n#1-8{ z8UvA0FI~ja!DL#F52qpZl)H91b3VHVJ?nhvQsqJ8y=CT?bX@sqI8;}sd>4|+3WS)7 zX(z_airf;)?{HEoR*%xii$sWGWBr}Yme|Y~OWww?vh;^`BQdpW&sUCY{vsh=0##Z_ zJsTkE3Z|`%NDPnGiin;7C_P5!Y$Gqz{%}N;+y z17^8~oton3zt$n*q)L4DqD)r>dj{PbDhG>7c!$e%A0a3O9VBRiovOL2_vW^=CD@V9 zfp><>x@W3Y3YTOxULPN23m0IGgJB~}((sjnO?n|y-ve}cbe5fVQ?3qO|4t+^t)<7a zC$DF$P4f6G!rMZrU(q9 zSv*JAFj25iiO9(_P_Z0*P-6D7qn4hi*35CF?UnVSqW^SpMk9=?KMkhfH90QM$K}*t zN7L+?+BQSm=!fHA`6DNlPAb`Mk1BDAmdZe9#(Mm)^tEiR?vjeBCzE5biY?wwt8c+$ z!Rbuy?3(94CZ~UY{-5+n6RQvD{224(TDg1L%}$_j|5=_86iHlSh4n9(&+TCuA-WER zT}?6da?x&^orEiqKP(dTKaaLp$ud_>W_^koSMn5dUhcHnTK#~m4HZD!STkXkHRS$y`Wk>iZG4nx0$Ipxe3M zB$w3&=n7p3Q3W)H13Cc|deT~aLWgY=quY|U^p`AM4Se(% z;N2}PA=IWmGsOGG9gPFE^RC=zKQDg$D&%#&XporT)Joi-y$D2O5G*1B_J2RjC~BvJ z4X2cGeA-Sksww&&^1B?5V*h{Ms()v}*Ax3!Qexw~|FrqnzI45b7@hU<7yHpI!P=Pv z)KWyLLmxbD$U7|VC76h|GIVHdiq+QgTZ0y?>E2z78*ReUFZ*qhTAd+lp>R_L&AK-3Mz_Qi{iwpn$3 zBO{TfT&)cAO^4FxQs-tz8>=t8CTTfSK}Ae-dYLsjeXl`|hN}-j0hC``@0l3pi9L=u zb5eP~hCo1x7-uydhIei6dRA@ya`UJSPfH0utYdq2nLYTrEqU`5VO;sxwQ*!|K!k$# zu2uP}pjiM*4xg(gtN5wCO7K}rKHkM@Ud4M!cJn+%~yhDWEe^igl$xAOhse03FeedBuaw-&ui+D)Sa#-5$0-_3!)?j4t& z$6B90qOyl4zaBl1_?W7nenQo6Ixe;-beeg;K6X`F>&QE#ZA7KK&rGJd>27~3`6yqc z?pjxNU?1I6vx>}>XwoMo4wON{&xRIH3+okh>p?;xe{)d|zdz9aZj^7)>A?1}uY3^1rI6{cHh=uvx^AG2|goP!+_^EetW1Ey& z5_EB#qUx|mbL(Xe59fLa4f{=t4Vy=(A81T5Wd5EA7`cm^zt#EDKZsZUo@o7fwDsv@ z@5`3~4-qa0bUi!I>AyW>U5KK3A1nBaW9lk`<)NJ`T0#>y$O zhOn!AY9?7&5u0dzrpK)`SU4C+ZZy-Cz66CC2m%~*tr_pecG7JTX{;r_6KChp$U;d+ zmggVGC68xe;+Pn{zK^EihPI@b;_hk4!(2rKpd2Evls9JER6iB^MtE^q<>-PrXYxsV zY84WzSEcA@pUi$YjUt(%TQ`rrUBOgg*s}@6^PtKxXDTd)MEFuQB#6zmP|!yIwK+A% z&(t};@e3KLBq6OzSAxCeGHUC`=muAGP%+0U80pC>%O9e7>rdxzE9h6{(YU&Vh4Nrf z&*Q4}omW~orxFqP#rCRALN3P(PpvgrbyI8t34A@~rI>O6qe>H7nhP+t7gB zWZE_S527Yxp6!<=r^fx9cjT58R6aqr)Ttqb_p9BjZO$k1+(9x@G}fB&gHyfn?9=!B zWFprM`w@saw2eP90zVu^ZdIyuI4@0z5n-vxK)2va#gD*=wmGYIZf!32lAMLWPikHy zI@82g8w0@FFIq_`)xPZA=TyMEdq$tj(8sHFWk6#3J?5x@AA;T*EQo}%@p%(d zV;8JuHUII%KQzWN<$3!>lpELoY42D)iYPFt&72HrWS~gu!OhnpkNThSR z{zZa(nJMNFs$uX3jjA3KA9`Waj;%vMcov+xR{&0^M$EY=ZyKjgDpw4?yPZU&TxfA! z)f4Wy?IhE|R9Ri)n3`7~oR+Yed=ixz}$oci4+D8s^1#pQOD~60kV&>nmZ%5a_@vwo^A;R}_qVrBsr|uA)-N5;D*S zQ^|dz)~JS=beF?B)t!8|f2~ql8rG@iI<4e$Fy)vqE6C1;ChBxYg8wPF_MgM?-}mUQ z9N;e!%eerXhF@-wieCGGE1eWDCT})&T*1GYxU`z>V1=#^)qG_J8NsCr%|_(6I?_V_EU8zsk%vY7a7$Wsehl zqJ20mB*yySK^9M`kU12-m{a|hk6M5L2I<)=x^P|0a)_pGefU><@ES|3zo9*^_rDx} zS#@tu^T&KwkRVZ*MoeGe`M<3GyQS+?;I~t6*cgPKrMqhV-V6Sucm2DLf1_rhjmf=h zuIvF#crhSUWILgZp{OrSqz=pl(Y4P(Mgie^=Ob7Uhy%1eS#*3E+#keP^9-dI`1}_B zmPoH`;bN)C4}iqjjeiwMO#jrA{g>Tu#sB}1|1Y=L4Z}}>50~(sXG50JDLH+k{4xL~ z<+TzR&W&Rz^0gZqyquzG1wqj^A}|vcdUzW9co?GuSH@i_Z~Rx?UUgx4i~%Ov;sMO9 zY%I>4t`WKwVxtz%&Nl8=hs=k4X<}d?Qc-o6AYkz^qW-|c;78m)Oxvpw6;G-}-lVGk zVUy#w=I!#`E^1RSE`qE0J*UX@^IK#T6J;Vo&4x@*I^5D*Ym2p(B?h?#!}`ELdz(mz zJrn`p8rv~qwQG!s^*^T1_04Naq8NKn3Q0mUMyC+*b#S8WL@|-B2;rYomr3MXDIVXM zTz@p@PZN0a-gal=^um) zWN*Sy6-;e)9V;Jy0G$}w-q(MbMWj8N25H+lX2Vs==L5!v61MD`B=V%g%Nb|AvNjFb z5?_g=K93q}j*_e~R|LcmiqatS3TQQeARNp&>&1Ho=lON=i}9^(Ea)e|VkhJdc@yTw zD}a4mzTw!baS{LI7YRjl^Z93t2?+ z2p^GJmCcWsu<)#=J2jA|@>H>}jP3X&ld15o+-RVC>SzM2$>28W!|=O*k+^4Qc)s0? z8C5+h>uR~9Ejp}~tyi+9C4YtQ^S&y{Rf$ODL&&h_0t6jgPcN-FI8OGzh`w!o81fQq19j5-6d!^*kZpXgeVYF)&U(*m#A;NC_jy?>@uM$E*c$Z zWL^HjAHl!#Ry%LSC`ff0@3$l;ri?Mj_9=lZs7lx!V9_h?2%vET=H%FqwFm#g;s5qe zH#X_2H3vay8mxyMbTwJG$-FhDtaT@jB~o>N(1p_A$HMPdn1|Um^2S@8Y`>PeFcf7| z{Mc{m0_Zq)xRp~Mp1BxAWeJVzhz4W^pc=_myl{Qs`FpzB)>{zJp71y))#O5NlXiTA z>gBMB1aX;=wbk8>$c&aSiP_p++i{^=)<3z=E+^eR*y(=6Aq%G|WCpEit=9uR=+>T# zk~|WnQflp%k#K&>Q_A`Poh?|`{P*Q_tl;yVR#c7lwl+JR$%)AhmEvz6sxW0P8Wg=r>aXL*0xf8h1=t@!z@RD1$$;!-5W zK>kyK^J6^esL8T(F4!Y!UXCmEtt(zQC_T_NBv4{hTI9)Bb2foH%I_8fMvbbSs3uu~ zCsg-)I2LA9-e^8nsX%REofTFAQ)_XazxZ%IIQcx)ez=O+t-navmvZ$*xtF;Lmt<(W z%y=7Y@cnUyK#mt;oS#jd3LWNwGNpCvftf~Xk+(6<2+oZc<1$Fr?!a%7t(U_X0zVEUO1lDieKpv3JES)j= zq_a2|bWEYUGt6bKI}39|memy2CQoC_8%Y7#PQ_eMB-RjOgtW+y^D}7TxjT<*jthI!kcre+aQP1XJd-uZbq^w~X za{TZU+Tse8b!j)O_3W8Arkza8!0u$bk7Me^vfWGLtp$6>CqG$H6}^+Z5ou0Q%cSjT zq|JQfp?|6}q#U-KfW z%Zh6&Zbu3)9ptS=7BF|8>^-I%ABY;=h+0WXILd>Eja379qd!^g3$Lg96($;7(iPZ3VqFD9-1;HQ~c z%$AwAb*LJFb_h6B0byx^QMD_q3s$(><(GFoCgjugDa=D~BG;x~OZHGNL!W&dyL@)L zmur(e)wnJ_DvRe}=MQi3hUGrkc&$eU0ur}hFwNds-`8({tO%B>YTJ20ZZ7Y_ zjXE2UgV%Rz{+fWsE5FXDa@#YIt~u6SvB^iD0V=|vNRr(T%!8%OhN~vdziiBOilkU^ zZ@8dPJ{)HrvovvOixuT@H{Qu{ZG20sM5Oi^YSQ_OB&n?}z@p>r_rklo@Nv)0pk|ow zE%_8Nq{Z@#^wl<1kJn);u+|t0VdK-7uNoXNlyv0oWrdo zIn1VJ&C`#-$)_&gw>+m@9Y2Ni09yHn`EPB}>&?Z(aOnoT- zd7{sifDugY?!0(9KwKr%c@sMb4_1{*J+Tl@)_iCF?w@m${~9;+Urk{Cw|{#|{yVz? zNED7dJ~FBcj7g*#yOp!f9}2VJuDsb?8_g||I8M$u!ojR+B9|F=NZwcv5Vx^WupY*7 z?yDNS4K;o?{f%dUVtq_PWK&#aGbg=-Lum)6;Vu!|lfJelZ*82YjO(hB*j^}jtM?%} zB|z2?PMzKp{Wf-74szV+6}IDl*IC;6P%y_69Sj*xiAf6d|M})sz=f6kv6s=gLE=>= zYhV?dpLFRZ9}&}a&B8i-8lz!gt^?n45UIdrd7iXH++2RZMtqH;D})6id39e-b45Z6 z6JnYo?_J$Tm6|Y>)rh|3z(8TvFfkEJWF7Bn_l^u*npo;n0+}_}PqR0B3+@R=yAc;7 z;}(mC+NVCgr=5WtTb18Si2$=Z!S{1Njm&s-@@=}4w`bF^`h8q_$c-^$fQV=qRA|%! zznTrhX#C?{bg2E`48r|_jlYSxy35`RAf?cZmA1-OgDWI9GEs`L>JD~~LF8>pt@el< zMO2z!**|h~i+*SSctA9}d+|-wUZ=od91!7Z(*Z&C6$qCr5!IqvF8hxgf1Z)4X3ahMiYtCg z26|g`=eIJ+*>`d#z7KU(y&?i55+d;0Id5yl@Ncd;wZS6tDv^*ilbAEH)cq>zH}_j& z7KZoe_o4cqLQ|iO-T19cB6fBtl(@V{{TGSIL@Y5bm}jpH|1e>F-u5JXOQvRiunPEg z)9=k&(y*!6x7QW-8ZIO35Vh<{>Bhwl930@dG?Isdnj_^x6Oe8`FA37JHhYfP-nd@J zQ4Ypor%9G$2I=9q(?7kIi0BSqk#Eus?d`t?=mm#wf)8a`Hj!4G0t5>diE$iUHncI@PQsHj&l#tW0PMeeNY0K z80A|Nqft*pqim$rNIW#}keQS|d+E6^J)RL@&83Cytg0-JG>c<#&#wVpUy?l9Lu0Z+ z9G&%RWL(cgzsjyium}%aBSv{wQuBTGr6wy|Mr`HP1wN|;7fyQ6q1_Trn)hl6o# z(_k(}C`ju98}HQGcgejvy+?-C+4@!U%;62XM?z$VshsY@Tz1cgidd94iquFD(6scuXTpJu6h#SZF%e>DYUl zV{R-_uwLlu?9Xe1O|75B1&c2w7U!gm)*cisF516;x!W%+$KDBQUv5!{7*0DMX-h+< zeyB};0wNuG#5zJQZB0ZpfdlkDO@_7YgETF-Qd)Dp-Dp&`*rg=v4y-!+k%a~+w%vbVQ_<`&le@E$eBpxuTdwxjW za&TYf5?{wapdv9dd%5>vWY`?3SRO(#h%$LBIj6kM5ZymMe$ytOcN}l1NxFD=q*>F_ zY~T@-^nQ|OAm{VDpTb*I51%7a=Ce_;72{pfhhv-A43qk4Aklm2HT&ls`Bhn2`yUQk zMrDlJm6(S2HFs$fY$pdh9ynAfC`LMg-E?PWgT~=R)XDA&ei%53P{j=X z`L)BqLsfIB(8OoQKsdFW#^y<4LbLc_IX<~RSB#pMU`9eBe$~gE2UpYTsq&hu59IHv7GTq=x|HGXvs$6^{D{0Y zz&pNvtMRVOh~ow8$dDp;-_4*@BvEPB=?7%&f4<9q^R?dm7EG`>W$Ecwk6&lL_U3<@ z{2wk{Q}`XLZRGAUw8wwoeDM}CN9EGTM=5JV|9pPV^90>fw>Q#UjR32&`^4Q z>MtrwKe1}#^?aJjcm(Gh?9=~f@4BLz(6T7#h+?4|3{|=T1dVhxJ~|0KRKWm`5PFd+ zf(VKtB^n5wh=BAeC`IB!)C3YB2!;+pkP=7?Akq{hPTtH~v)=odubH>je9Zki_ved}kb}YWYK59RH{a@L^jG`SIc_kQWqu!R}U%tZlGY98cLFR`YMe{{c9Ed7nM2 z_aJdn$JZPT>n+L~>5VJx*V?sP1p*D7EO%(wSSeCVdW=}M>93J!+%Orw0$MyDbf}qS zvQej+6PQsnw_Ua5OKDyHwA&`LSJ|2vXUE5ES{;?A3(91#G!=*#q1t0G&){39`2TT5 z`#Tmq3dE!rOhAr{hx0ZLDUzEr(NVi)(G;LSUMVMDm^|G>3~BvQV-24QS1U8}07)n! zBgC50>AyLk9M0E;9Zys&L|~xlcdw4Z3AUz>ra+DhLrFn(qsY5$rg_4*M_CP7awgKKr??2UG4-YY?7eEm`w3N_ zD4KR9PEAM#Y&G=wD7xfO5&C7rSPho9 z>uwk3{yuD^;sr=T&;N-6#vb-&U3tuA#fv0gz3_z68URnmn)Xgq^~lGg5$4F#l4=L@ zA};^t|!0MjVa5AD|J9XwvEzqAfKrfwf-P}YwV#=gr`mZ%>zJ{edU zu8pPzVxgKs?Nl#s)0Zb+9zNr{G_Cw;=W3+TGmB3Sbe)lMc9+GfzRQpx%i?=GZ%gY2 zPOM*9z92=ubVB|XsTVwbX#OPSu;N^%%G@#AI&Y*iB+wz^98CkymJi9Yg$4NJ6T2ei%N)Zn;-fQ4fL7U zEr+WzLo0brf%oqQVbcJ8&WqdX=ts=2O*@H~!jW4sxq3S7x+MBw?pEb$>X-dud|HOz?dWbBTwT;LVW-*b6+H^! z=83b%8%rb5w1w!bTMPFxWNzn%N}JB-hCaU*^Gq&jsNS?Ci8zXjNY1&Zch?1KWP)lJ zN{d79CUB&@|I`EubTZwz0dG1-CI?=$dF^w^`D^4r@;-{pi~=vChVejtCmSV8q_l&v zUdDXvlAYZ(b2+e5p&b1>$76;%1dUt_^tx5A68uV_ooa7TTye+n+9R{9c z%|dWjK|xUiwK-Yt)r3FI{1GX$4Qz1D|h_TEMw}2mIc{f&~g_BxtD+`qu zdCy@+ChBAJ&!pAZ8v$mej^qfa3Qqn~K+Fn+(NV;@R>(XJGkR&31BGr|s{4RCU%cc; zwyj}V5~qBl{Q=dJ9#{ErUYl7_`4D5 z@?|s`ACnv>!X>TNOmGf$AHC!4duCWP;AP1uR zKETE`s)ic7R3a`qf?pqMakV>h?z8glr3V-9WI7IMOPaI{H|B#v>A0y#jm(^}GK4M? ziCgvZ^9zUYT4L5j^_tZhMulo};MT4z{wEv3VUprox3 zV_5|bDk_ZuL1?2+$n#d0{A|Ah!AX`JenNwn_pJ?1hx9SB6Cus9_z-)RJOKaBh{UE>0A(gW(02E^pmjSoVI!h zJO6!T@YCi|Zy!omoIFvU>cgDgQrdIfC+b~DmUPb=6PtXH#55`KDzG${YRVf?N}Tsq zp;e70@t%kd2m&qQI-rW>;;0Fkzd_*^1I*%eHF;Bc?X%>1&~|c9oBdtQV$5&JdwtiX z0h%zs6fQ$$s2;cw4-<>2jfde_g$j&1f{Z#}0>) zMLs&6gX+z6kqFGBc)^s+sNRO6qzXn3veY>$(Q6mepsjlWP?JWd))ZVpx$ER?uWC*S zin@J>&H1qqte%&g%yC>~UDbwe_(_(Cg4DCxTX(djG~V1z31~D@6b2EofbUvHb65n? ze=7X6^u^n=ys1Tbt*Ng|r)CCoi*AjaHRkBe&^md7RZ3Wyw$7f><_yXO8!K{ZlCFj~n?*<4bVQZk%MOx2fN@q?$u3lh=&n zF=;wT%HncR^i#`rR&Q<`x&(*qXog?51p2h^JfUNpw|?|4&%9LwZX9>_R`WQJPj@n5 z^Ijjw^V~bPp;8+Ex0gPY^Kp6Y2}gbM5#rwz2#{aXSOiML!yiJQiq3Uyd(yc;Pf!sd zv$@B=C#k>S2jjKBm?zyLh)$&CaaG*R{?K{%+6DTl*OW{DpXR>^+WECnMr$D)KQ;V~ oPxnyw4c3@gbyC*D9l3ETb}DXwt*H8^^!ZCq-G4avm7f!T2e)R5ApigX literal 0 HcmV?d00001 diff --git a/mkdocs.yml b/mkdocs.yml index c8dd9d90f69..bfe4bf81018 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -397,11 +397,6 @@ nav: - Jans Keycloak Link: admin/link/jans-keycloak-link.md - Lock Guide: - admin/lock/README.md - - Lock Installation: admin/lock/lock-installation.md - - Auth Server Configuration: admin/lock/lock-auth-server-config.md - - Lock Client Configuration: admin/lock/lock-client-config.md - - Implement Lock PDP Plugin: admin/lock/lock-pdp-plugin.md - - Policy Store Integration: admin/lock/lock-opa.md - Lock Master: admin/lock/lock-master.md - Authorization Using Cedarling: admin/lock/cedarling.md - Janssen Recipes: From f809e454c070ede9969cb74ebb709cdfcb60bfe2 Mon Sep 17 00:00:00 2001 From: Isman Firmansyah Date: Mon, 29 Jul 2024 19:05:54 +0700 Subject: [PATCH 25/43] fix(docker-jans-saml): set kc_saml_openid client as trusted client (#9044) Signed-off-by: iromli Co-authored-by: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> --- docker-jans-saml/templates/jans-saml/clients.ldif | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-jans-saml/templates/jans-saml/clients.ldif b/docker-jans-saml/templates/jans-saml/clients.ldif index a4bf62c77e3..58736435c5c 100644 --- a/docker-jans-saml/templates/jans-saml/clients.ldif +++ b/docker-jans-saml/templates/jans-saml/clients.ldif @@ -24,7 +24,7 @@ jansScope: inum=764C,ou=scopes,o=jans jansScope: inum=10B2,ou=scopes,o=jans jansSubjectTyp: pairwise jansTknEndpointAuthMethod: client_secret_basic -jansTrustedClnt: false +jansTrustedClnt: true jansRedirectURI: https://%(hostname)s/kc/realms/jans/kc-jans-authn-rest-bridge/auth-complete dn: inum=%(kc_scheduler_api_client_id)s,ou=clients,o=jans From b0aaac74bae7cbfbae2cde728ca1804240ae9c8c Mon Sep 17 00:00:00 2001 From: pujavs <43700552+pujavs@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:42:25 +0530 Subject: [PATCH 26/43] feat(jans-lock): lock audit endpoints (#9034) * feat(config-api): lock endpoind wip Signed-off-by: pujavs * feat(config-api): telemetry audit endpoint Signed-off-by: pujavs * feat(config-api): lock audit endpoints Signed-off-by: pujavs * feat(config-api): lock audit endpoints Signed-off-by: pujavs * feat(config-api): lock audit endpoints Signed-off-by: pujavs * feat(config-api): lock audit endpoints Signed-off-by: pujavs * feat(config-api): lock audit endpoint Signed-off-by: pujavs * feat(config-api): lock audit endpoint Signed-off-by: pujavs * feat(config-api): lock telemery endpoint - wip Signed-off-by: pujavs * feat(config-api): lock endpoint Signed-off-by: pujavs * feat:(config-api): lock telemetry endpoint Signed-off-by: pujavs * feat(config-api): lock audit endpoint Signed-off-by: pujavs * feat(config-api): lock audit endpoint Signed-off-by: pujavs * feat(config-api): lock endpoint Signed-off-by: pujavs * feat(config-api): lock audit endpoint Signed-off-by: pujavs * feat(config-api): lock audit endpoint Signed-off-by: pujavs * feat(config-api): lock audit endpoint Signed-off-by: pujavs * feat(config-api): lock audit endpoint Signed-off-by: pujavs * feat(config-api): sync with origin Signed-off-by: pujavs * feat(config-api): lock audit endpoint Signed-off-by: pujavs * feat(config-api): lock audit endpoint Signed-off-by: pujavs * feat(config-api): lock telemetry endpoint Signed-off-by: pujavs * feat(config-api): lock telemetry audit endpoint Signed-off-by: pujavs * feat(config-api): lock audit endpoint Signed-off-by: pujavs * feat(config-api): lock audit endpoint Signed-off-by: pujavs * feat(config-api): lock endpoint Signed-off-by: pujavs * feat(config-api): lock endpoint Signed-off-by: pujavs * feat(config-api): lock endpoint Signed-off-by: pujavs * feat: audit health endpoint Signed-off-by: pujavs * feat(config-api); lock health endpoint Signed-off-by: pujavs * feat(config-api): lock log audit Signed-off-by: pujavs * feat(config-api) lock telemetry endpoint Signed-off-by: pujavs * feat(config-api): lock audit endpoints Signed-off-by: pujavs * feat(config-api): lock audit endpoint Signed-off-by: pujavs * feat(config-api): lock audit endpoint Signed-off-by: pujavs * feat(jans-linux-setup): jans-lock client Signed-off-by: Mustafa Baser --------- Signed-off-by: pujavs Signed-off-by: Mustafa Baser Co-authored-by: Mustafa Baser Co-authored-by: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> --- .../docs/jans-config-api-swagger.yaml | 24 +- .../plugins/docs/lock-plugin-swagger.yaml | 265 +++++++++++++ .../plugin/lock/model/stat/HealthEntry.java | 104 +++++ .../plugin/lock/model/stat/LogEntry.java | 138 +++++++ .../lock/model/stat/TelemetryEntry.java | 165 ++++++++ .../plugin/lock/rest/ApiApplication.java | 28 +- .../plugin/lock/rest/AuditResource.java | 101 +++++ .../plugin/lock/rest/LockConfigResource.java | 8 +- .../plugin/lock/service/AuditService.java | 249 ++++++++++++ .../configapi/plugin/lock/util/Constants.java | 28 +- .../default/config-api-test.properties | 2 +- .../profiles/jans-ui.jans.io/test.properties | 2 +- .../test.properties | 2 +- .../profiles/local/test.properties | 2 +- jans-config-api/server/pom.xml | 1 + .../main/resources/config-api-rs-protect.json | 184 +++++++++ .../io/jans/service/net/BaseHttpService.java | 22 +- .../jans_setup/schema/jans_schema.json | 274 ++++++++++++- .../setup_app/installers/jans_lock.py | 39 +- .../jans_setup/setup_app/utils/base.py | 6 + .../jans_setup/setup_app/utils/db_utils.py | 20 +- .../jans_setup/templates/base.ldif | 15 + .../templates/jans-lock/dynamic-conf.json | 75 ++-- .../lock/model/config/AppConfiguration.java | 366 ++++++++++-------- jans-lock/lock-master/service/pom.xml | 12 + .../io/jans/lock/service/net/HttpService.java | 0 .../ws/rs/audit/AuditRestWebServiceImpl.java | 88 +++-- .../main/java/io/jans/lock/util/LockUtil.java | 346 +++++++++++++++++ 28 files changed, 2310 insertions(+), 256 deletions(-) create mode 100644 jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/model/stat/HealthEntry.java create mode 100644 jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/model/stat/LogEntry.java create mode 100644 jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/model/stat/TelemetryEntry.java create mode 100644 jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/AuditResource.java create mode 100644 jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/service/AuditService.java rename jans-lock/lock-master/{server => service}/src/main/java/io/jans/lock/service/net/HttpService.java (100%) create mode 100644 jans-lock/lock-master/service/src/main/java/io/jans/lock/util/LockUtil.java diff --git a/jans-config-api/docs/jans-config-api-swagger.yaml b/jans-config-api/docs/jans-config-api-swagger.yaml index d1a23211fed..18357d90c32 100644 --- a/jans-config-api/docs/jans-config-api-swagger.yaml +++ b/jans-config-api/docs/jans-config-api-swagger.yaml @@ -8313,20 +8313,20 @@ components: type: string selected: type: boolean - adminCanAccess: + whitePagesCanView: type: boolean adminCanView: type: boolean - adminCanEdit: - type: boolean - userCanAccess: - type: boolean userCanView: type: boolean - whitePagesCanView: + adminCanEdit: type: boolean userCanEdit: type: boolean + userCanAccess: + type: boolean + adminCanAccess: + type: boolean baseDn: type: string PatchRequest: @@ -9162,8 +9162,6 @@ components: type: boolean lockMessageConfig: $ref: '#/components/schemas/LockMessageConfig' - fapi: - type: boolean allResponseTypesSupported: uniqueItems: true type: array @@ -9173,6 +9171,8 @@ components: - code - token - id_token + fapi: + type: boolean AuthenticationFilter: required: - baseDn @@ -9939,10 +9939,10 @@ components: type: array items: type: object - displayValue: - type: string value: type: object + displayValue: + type: string LocalizedString: type: object properties: @@ -10729,10 +10729,10 @@ components: ttl: type: integer format: int32 - opbrowserState: - type: string persisted: type: boolean + opbrowserState: + type: string SessionIdAccessMap: type: object properties: diff --git a/jans-config-api/plugins/docs/lock-plugin-swagger.yaml b/jans-config-api/plugins/docs/lock-plugin-swagger.yaml index 140a981295f..08c2847496b 100644 --- a/jans-config-api/plugins/docs/lock-plugin-swagger.yaml +++ b/jans-config-api/plugins/docs/lock-plugin-swagger.yaml @@ -14,7 +14,146 @@ servers: description: The Jans server tags: - name: Lock - Configuration +- name: Lock - Audit paths: + /lock/audit/health: + post: + tags: + - Lock - Audit + summary: Save health data + description: Save health data + operationId: save-health-data + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/HealthEntry' + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/HealthEntry' + examples: + Response example: + description: Response example + value: "" + "400": + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + "401": + description: Unauthorized + "404": + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + "500": + description: InternalServerError + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + security: + - oauth2: + - https://jans.io/oauth/lock/health.write + /lock/audit/log: + post: + tags: + - Lock - Audit + summary: Save log data + description: Save log data + operationId: save-log-data + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/LogEntry' + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/LogEntry' + examples: + Response example: + description: Response example + value: "" + "400": + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + "401": + description: Unauthorized + "404": + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + "500": + description: InternalServerError + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + security: + - oauth2: + - https://jans.io/oauth/lock/log.write + /lock/audit/telemetry: + post: + tags: + - Lock - Audit + summary: Save telemetry data + description: Save telemetry data + operationId: save-telemetry-data + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TelemetryEntry' + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/TelemetryEntry' + examples: + Response example: + description: Response example + value: "" + "400": + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + "401": + description: Unauthorized + "404": + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + "500": + description: InternalServerError + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + security: + - oauth2: + - https://jans.io/oauth/lock/telemetry.write /lock/lockConfig: get: tags: @@ -100,20 +239,134 @@ paths: - https://jans.io/oauth/lock-config.write components: schemas: + HealthEntry: + type: object + properties: + dn: + type: string + lastPolicyLoadTime: + type: string + format: date-time + status: + type: string + cedarEngineStatus: + type: string + cedarPolicyStatus: + type: string + tokenDataStatus: + type: string + inum: + type: string + ApiError: + type: object + properties: + code: + type: string + message: + type: string + description: + type: string + LogEntry: + type: object + properties: + dn: + type: string + lastUpdate: + type: string + format: date-time + eventTime: + type: string + format: date-time + eventType: + type: string + severetyLevel: + type: string + policyResult: + type: string + userAccountId: + type: string + clientId: + type: string + sourceInformation: + type: string + inum: + type: string + TelemetryEntry: + type: object + properties: + dn: + type: string + lastPolicyLoadTime: + type: string + format: date-time + lastPolicyLoadSize: + type: integer + format: int32 + status: + type: string + policySuccessLoadCounter: + type: integer + format: int64 + policyFailedLoadCounter: + type: integer + format: int64 + lastPolicyEvaluationTimeNs: + type: string + format: date-time + avgPolicyEvaluationTimeNs: + type: string + format: date-time + evaluationRequestsCount: + type: integer + format: int64 + policyStats: + type: object + additionalProperties: + type: string + inum: + type: string + memoryUsage: + type: string AppConfiguration: type: object properties: baseDN: type: string + description: Entry Base distinguished name (DN) that identifies the starting + point of a search baseEndpoint: type: string description: Lock base endpoint URL + openIdIssuer: + type: string + description: OpenID issuer URL tokenChannels: type: array description: List of token channel names items: type: string description: List of token channel names + clientId: + type: string + description: Lock Client ID + clientPassword: + type: string + description: Lock client password + tokenUrl: + type: string + description: Jans URL of the OpenID Connect Provider's OAuth 2.0 Token Endpoint + endpointDetails: + type: object + additionalProperties: + type: array + description: Jans URL of config-api audit endpoints and corresponding + scope details + items: + type: string + description: Jans URL of config-api audit endpoints and corresponding + scope details + description: Jans URL of config-api audit endpoints and corresponding scope + details disableJdkLogger: type: boolean description: Choose whether to disable JDK loggers @@ -186,7 +439,19 @@ components: clientCredentials: tokenUrl: "https://{op-hostname}/.../token" scopes: + https://jans.io/oauth/lock/read-all: View Lock related information + https://jans.io/oauth/lock/write-all: View Lock related information https://jans.io/oauth/lock-config.readonly: View Lock configuration related information https://jans.io/oauth/lock-config.write: Manage Lock configuration related information + https://jans.io/oauth/lock/audit.readonly: View Lock audit related information + https://jans.io/oauth/lock/audit.write: View Lock audit related information + https://jans.io/oauth/lock/health.readonly: View Lock health related information + https://jans.io/oauth/lock/health.write: Manage Lock health related information + https://jans.io/oauth/lock/log.readonly: View Lock log related information + https://jans.io/oauth/lock/log.write: Manage Lock log health related information + https://jans.io/oauth/lock/telemetry.readonly: View Lock telemetry related + information + https://jans.io/oauth/lock/telemetry.write: Manage Lock telemetry related + information diff --git a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/model/stat/HealthEntry.java b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/model/stat/HealthEntry.java new file mode 100644 index 00000000000..af8be3429ce --- /dev/null +++ b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/model/stat/HealthEntry.java @@ -0,0 +1,104 @@ +package io.jans.configapi.plugin.lock.model.stat; + +import io.jans.orm.annotation.AttributeName; +import io.jans.orm.annotation.DN; +import io.jans.orm.annotation.DataEntry; +import io.jans.orm.annotation.ObjectClass; + +import java.io.Serializable; +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonProperty; + +@DataEntry +@ObjectClass(value = "jansHealthEntry") +public class HealthEntry implements Serializable { + + private static final long serialVersionUID = 1L; + + @DN + private String dn; + + @JsonProperty("inum") + @AttributeName(name = "inum", ignoreDuringUpdate = true) + private String inum; + + @AttributeName(name = "jansLastUpd") + private Date lastPolicyLoadTime; + + @AttributeName(name = "jansStatus") + private String status; + + @AttributeName(name = "cedarEngineStatus") + private String cedarEngineStatus; + + @AttributeName(name = "cedarPolicyStatus") + private String cedarPolicyStatus; + + @AttributeName(name = "tokenDataStatus") + private String tokenDataStatus; + + public String getDn() { + return dn; + } + + public void setDn(String dn) { + this.dn = dn; + } + + public String getInum() { + return inum; + } + + public void setInum(String inum) { + this.inum = inum; + } + + public Date getLastPolicyLoadTime() { + return lastPolicyLoadTime; + } + + public void setLastPolicyLoadTime(Date lastPolicyLoadTime) { + this.lastPolicyLoadTime = lastPolicyLoadTime; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getCedarEngineStatus() { + return cedarEngineStatus; + } + + public void setCedarEngineStatus(String cedarEngineStatus) { + this.cedarEngineStatus = cedarEngineStatus; + } + + public String getCedarPolicyStatus() { + return cedarPolicyStatus; + } + + public void setCedarPolicyStatus(String cedarPolicyStatus) { + this.cedarPolicyStatus = cedarPolicyStatus; + } + + public String getTokenDataStatus() { + return tokenDataStatus; + } + + public void setTokenDataStatus(String tokenDataStatus) { + this.tokenDataStatus = tokenDataStatus; + } + + @Override + public String toString() { + return "HealthEntry [dn=" + dn + ", inum=" + inum + ", lastPolicyLoadTime=" + lastPolicyLoadTime + ", status=" + + status + ", cedarEngineStatus=" + cedarEngineStatus + ", cedarPolicyStatus=" + cedarPolicyStatus + + ", tokenDataStatus=" + tokenDataStatus + "]"; + } + +} diff --git a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/model/stat/LogEntry.java b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/model/stat/LogEntry.java new file mode 100644 index 00000000000..803ec37bbd4 --- /dev/null +++ b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/model/stat/LogEntry.java @@ -0,0 +1,138 @@ +package io.jans.configapi.plugin.lock.model.stat; + +import io.jans.orm.annotation.AttributeName; +import io.jans.orm.annotation.DN; +import io.jans.orm.annotation.DataEntry; +import io.jans.orm.annotation.ObjectClass; + +import java.io.Serializable; +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonProperty; + +@DataEntry +@ObjectClass(value = "jansLogEntry") +public class LogEntry implements Serializable { + + private static final long serialVersionUID = 1L; + + @DN + private String dn; + + @JsonProperty("inum") + @AttributeName(name = "inum", ignoreDuringUpdate = true) + private String inum; + + @AttributeName(name = "jansLastUpd") + private Date lastUpdate; + + @AttributeName(name = "eventTime") + private Date eventTime; + + @AttributeName(name = "eventType") + private String eventType; + + @AttributeName(name = "severetyLevel") + private String severetyLevel; + + @AttributeName(name = "policyResult") + private String policyResult; + + @AttributeName(name = "userAccountId") + private String userAccountId; + + @AttributeName(name = "clientId") + private String clientId; + + @AttributeName(name = "sourceInformation") + private String sourceInformation; + + public String getDn() { + return dn; + } + + public void setDn(String dn) { + this.dn = dn; + } + + public String getInum() { + return inum; + } + + public void setInum(String inum) { + this.inum = inum; + } + + public Date getLastUpdate() { + return lastUpdate; + } + + public void setLastUpdate(Date lastUpdate) { + this.lastUpdate = lastUpdate; + } + + public Date getEventTime() { + return eventTime; + } + + public void setEventTime(Date eventTime) { + this.eventTime = eventTime; + } + + public String getEventType() { + return eventType; + } + + public void setEventType(String eventType) { + this.eventType = eventType; + } + + public String getSeveretyLevel() { + return severetyLevel; + } + + public void setSeveretyLevel(String severetyLevel) { + this.severetyLevel = severetyLevel; + } + + public String getPolicyResult() { + return policyResult; + } + + public void setPolicyResult(String policyResult) { + this.policyResult = policyResult; + } + + public String getUserAccountId() { + return userAccountId; + } + + public void setUserAccountId(String userAccountId) { + this.userAccountId = userAccountId; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getSourceInformation() { + return sourceInformation; + } + + public void setSourceInformation(String sourceInformation) { + this.sourceInformation = sourceInformation; + } + + @Override + public String toString() { + return "LogEntry [dn=" + dn + ", inum=" + inum + ", lastUpdate=" + lastUpdate + ", eventTime=" + eventTime + + ", eventType=" + eventType + ", severetyLevel=" + severetyLevel + ", policyResult=" + policyResult + + ", userAccountId=" + userAccountId + ", clientId=" + clientId + ", sourceInformation=" + + sourceInformation + "]"; + } + +} diff --git a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/model/stat/TelemetryEntry.java b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/model/stat/TelemetryEntry.java new file mode 100644 index 00000000000..2a619e88d66 --- /dev/null +++ b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/model/stat/TelemetryEntry.java @@ -0,0 +1,165 @@ +package io.jans.configapi.plugin.lock.model.stat; + +import io.jans.orm.annotation.AttributeName; +import io.jans.orm.annotation.DN; +import io.jans.orm.annotation.DataEntry; +import io.jans.orm.annotation.JsonObject; +import io.jans.orm.annotation.ObjectClass; + +import java.io.Serializable; +import java.util.Date; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +@DataEntry +@ObjectClass(value = "jansTelemetryEntry") +public class TelemetryEntry implements Serializable { + + private static final long serialVersionUID = 1L; + + @DN + private String dn; + + @JsonProperty("inum") + @AttributeName(name = "inum", ignoreDuringUpdate = true) + private String inum; + + @AttributeName(name = "jansLastUpd") + private Date lastPolicyLoadTime; + + @AttributeName(name = "size") + private Integer lastPolicyLoadSize; + + @AttributeName(name = "jansStatus") + private String status; + + @AttributeName(name = "jansCounter") + private long policySuccessLoadCounter; + + @AttributeName(name = "jansFailCounter") + private long policyFailedLoadCounter; + + @AttributeName(name = "evaluationTimeNs") + private Date lastPolicyEvaluationTimeNs; + + @AttributeName(name = "averageTimeNs") + private Date avgPolicyEvaluationTimeNs; + + @JsonProperty("memoryUsage") + private String memoryUsage; + + @AttributeName(name = "requestCounter") + private long evaluationRequestsCount; + + @AttributeName(name = "policyStats") + @JsonObject + private Map policyStats; + + public String getDn() { + return dn; + } + + public void setDn(String dn) { + this.dn = dn; + } + + public String getInum() { + return inum; + } + + public void setInum(String inum) { + this.inum = inum; + } + + public Date getLastPolicyLoadTime() { + return lastPolicyLoadTime; + } + + public void setLastPolicyLoadTime(Date lastPolicyLoadTime) { + this.lastPolicyLoadTime = lastPolicyLoadTime; + } + + public Integer getLastPolicyLoadSize() { + return lastPolicyLoadSize; + } + + public void setLastPolicyLoadSize(Integer lastPolicyLoadSize) { + this.lastPolicyLoadSize = lastPolicyLoadSize; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public long getPolicySuccessLoadCounter() { + return policySuccessLoadCounter; + } + + public void setPolicySuccessLoadCounter(long policySuccessLoadCounter) { + this.policySuccessLoadCounter = policySuccessLoadCounter; + } + + public long getPolicyFailedLoadCounter() { + return policyFailedLoadCounter; + } + + public void setPolicyFailedLoadCounter(long policyFailedLoadCounter) { + this.policyFailedLoadCounter = policyFailedLoadCounter; + } + + public Date getLastPolicyEvaluationTimeNs() { + return lastPolicyEvaluationTimeNs; + } + + public void setLastPolicyEvaluationTimeNs(Date lastPolicyEvaluationTimeNs) { + this.lastPolicyEvaluationTimeNs = lastPolicyEvaluationTimeNs; + } + + public Date getAvgPolicyEvaluationTimeNs() { + return avgPolicyEvaluationTimeNs; + } + + public void setAvgPolicyEvaluationTimeNs(Date avgPolicyEvaluationTimeNs) { + this.avgPolicyEvaluationTimeNs = avgPolicyEvaluationTimeNs; + } + + public String getMemoryUsage() { + return memoryUsage; + } + + public void setMemoryUsage(String memoryUsage) { + this.memoryUsage = memoryUsage; + } + + public long getEvaluationRequestsCount() { + return evaluationRequestsCount; + } + + public void setEvaluationRequestsCount(long evaluationRequestsCount) { + this.evaluationRequestsCount = evaluationRequestsCount; + } + + public Map getPolicyStats() { + return policyStats; + } + + public void setPolicyStats(Map policyStats) { + this.policyStats = policyStats; + } + + @Override + public String toString() { + return "TelemetryEntry [dn=" + dn + ", inum=" + inum + ", lastPolicyLoadTime=" + lastPolicyLoadTime + + ", lastPolicyLoadSize=" + lastPolicyLoadSize + ", status=" + status + ", policySuccessLoadCounter=" + + policySuccessLoadCounter + ", policyFailedLoadCounter=" + policyFailedLoadCounter + + ", lastPolicyEvaluationTimeNs=" + lastPolicyEvaluationTimeNs + ", avgPolicyEvaluationTimeNs=" + + avgPolicyEvaluationTimeNs + ", memoryUsage=" + memoryUsage + ", evaluationRequestsCount=" + + evaluationRequestsCount + ", policyStats=" + policyStats + "]"; + } + +} diff --git a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/ApiApplication.java b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/ApiApplication.java index bef3341e958..5432433ade7 100644 --- a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/ApiApplication.java +++ b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/ApiApplication.java @@ -13,19 +13,30 @@ import java.util.HashSet; import java.util.Set; -@ApplicationPath("/lock") +@ApplicationPath(Constants.LOCK) @OpenAPIDefinition(info = @Info(title = "Jans Config API - Lock", version = "1.0.0", contact = @Contact(name = "Gluu Support", url = "https://support.gluu.org", email = "support@gluu.org"), -license = @License(name = "Apache 2.0", url = "https://github.com/JanssenProject/jans/blob/main/LICENSE")), + license = @License(name = "Apache 2.0", url = "https://github.com/JanssenProject/jans/blob/main/LICENSE")), -tags = { @Tag(name = "Lock - Configuration")}, + tags = { @Tag(name = "Lock - Configuration"), @Tag(name = "Lock - Audit") }, -servers = { @Server(url = "https://jans.io/", description = "The Jans server") }) + servers = { @Server(url = "https://jans.io/", description = "The Jans server") }) @SecurityScheme(name = "oauth2", type = SecuritySchemeType.OAUTH2, flows = @OAuthFlows(clientCredentials = @OAuthFlow(tokenUrl = "https://{op-hostname}/.../token", scopes = { -@OAuthScope(name = Constants.LOCK_CONFIG_READ_ACCESS, description = "View Lock configuration related information"), -@OAuthScope(name = Constants.LOCK_CONFIG_WRITE_ACCESS, description = "Manage Lock configuration related information")} -))) + @OAuthScope(name = Constants.LOCK_READ_ACCESS, description = "View Lock related information"), + @OAuthScope(name = Constants.LOCK_WRITE_ACCESS, description = "View Lock related information"), + @OAuthScope(name = Constants.LOCK_CONFIG_READ_ACCESS, description = "View Lock configuration related information"), + @OAuthScope(name = Constants.LOCK_CONFIG_WRITE_ACCESS, description = "Manage Lock configuration related information"), + @OAuthScope(name = Constants.LOCK_AUDIT_READ_ACCESS, description = "View Lock audit related information"), + @OAuthScope(name = Constants.LOCK_AUDIT_WRITE_ACCESS, description = "View Lock audit related information"), + @OAuthScope(name = Constants.LOCK_HEALTH_READ_ACCESS, description = "View Lock health related information"), + @OAuthScope(name = Constants.LOCK_HEALTH_WRITE_ACCESS, description = "Manage Lock health related information"), + @OAuthScope(name = Constants.LOCK_LOG_READ_ACCESS, description = "View Lock log related information"), + @OAuthScope(name = Constants.LOCK_LOG_WRITE_ACCESS, description = "Manage Lock log health related information"), + @OAuthScope(name = Constants.LOCK_TELEMETRY_READ_ACCESS, description = "View Lock telemetry related information"), + @OAuthScope(name = Constants.LOCK_TELEMETRY_WRITE_ACCESS, description = "Manage Lock telemetry related information") + +}))) public class ApiApplication extends Application { @Override @@ -33,7 +44,8 @@ public Set> getClasses() { HashSet> classes = new HashSet<>(); classes.add(LockConfigResource.class); - + classes.add(AuditResource.class); + return classes; } } diff --git a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/AuditResource.java b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/AuditResource.java new file mode 100644 index 00000000000..aec9c2722d8 --- /dev/null +++ b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/AuditResource.java @@ -0,0 +1,101 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.configapi.plugin.lock.rest; + +import io.jans.configapi.core.model.ApiError; +import io.jans.configapi.core.rest.BaseResource; +import io.jans.configapi.core.rest.ProtectedApi; +import io.jans.configapi.plugin.lock.model.stat.*; +import io.jans.configapi.plugin.lock.service.AuditService; +import io.jans.configapi.plugin.lock.util.Constants; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.*; + +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.slf4j.Logger; + + +@Path(Constants.AUDIT) +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class AuditResource extends BaseResource { + + @Inject + Logger logger; + + @Inject + AuditService auditService; + + @Operation(summary = "Save health data", description = "Save health data", operationId = "save-health-data", tags = { + "Lock - Audit" }, security = @SecurityRequirement(name = "oauth2", scopes = { + Constants.LOCK_HEALTH_WRITE_ACCESS })) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Created", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = HealthEntry.class), examples = @ExampleObject(name = "Response example", value = "example/lock/audit/health.json"))), + @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ApiError.class, description = "BadRequestException"))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "Not Found", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ApiError.class, description = "NotFoundException"))), + @ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ApiError.class, description = "InternalServerError"))), }) + @POST + @ProtectedApi(scopes = { Constants.LOCK_HEALTH_WRITE_ACCESS }, groupScopes = {}) + @Path(Constants.HEALTH) + public Response postHealthData(@Valid HealthEntry healthEntry) { + logger.debug("Save Health Data - healthEntry:{}", healthEntry); + healthEntry = auditService.addHealthEntry(healthEntry); + return Response.status(Response.Status.CREATED).entity(healthEntry).build(); + + } + + @Operation(summary = "Save log data", description = "Save log data", operationId = "save-log-data", tags = { + "Lock - Audit" }, security = @SecurityRequirement(name = "oauth2", scopes = { + Constants.LOCK_LOG_WRITE_ACCESS })) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Created", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LogEntry.class), examples = @ExampleObject(name = "Response example", value = "example/lock/audit/log.json"))), + @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ApiError.class, description = "BadRequestException"))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "Not Found", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ApiError.class, description = "NotFoundException"))), + @ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ApiError.class, description = "InternalServerError"))), }) + @POST + @ProtectedApi(scopes = { Constants.LOCK_LOG_WRITE_ACCESS }, groupScopes = {}) + @Path(Constants.LOG) + public Response postLogData(@Valid LogEntry logEntry) { + logger.debug("Save - logEntry:{}", logEntry); + logEntry = auditService.addLogData(logEntry); + return Response.status(Response.Status.CREATED).entity(logEntry).build(); + + } + + @Operation(summary = "Save telemetry data", description = "Save telemetry data", operationId = "save-telemetry-data", tags = { + "Lock - Audit" }, security = @SecurityRequirement(name = "oauth2", scopes = { + Constants.LOCK_TELEMETRY_WRITE_ACCESS })) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Created", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = TelemetryEntry.class), examples = @ExampleObject(name = "Response example", value = "example/lock/audit/telemetry.json"))), + @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ApiError.class, description = "BadRequestException"))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "Not Found", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ApiError.class, description = "NotFoundException"))), + @ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ApiError.class, description = "InternalServerError"))), }) + @POST + @ProtectedApi(scopes = { Constants.LOCK_TELEMETRY_WRITE_ACCESS }, groupScopes = {}) + @Path(Constants.TELEMETRY) + public Response postTelemetryData(@Valid TelemetryEntry telemetryEntry) { + logger.debug("Save telemetryEntry():{}", telemetryEntry); + telemetryEntry = auditService.addTelemetryData(telemetryEntry); + return Response.status(Response.Status.CREATED).entity(telemetryEntry).build(); + + } + +} \ No newline at end of file diff --git a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/LockConfigResource.java b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/LockConfigResource.java index 30ce8b43ff9..a9c0ce1dc50 100644 --- a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/LockConfigResource.java +++ b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/LockConfigResource.java @@ -43,6 +43,8 @@ @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public class LockConfigResource extends BaseResource { + + private static final String CONFIG_NULL_ERR_MSG = "It seems Lock module is not setup, kindly check."; @Inject Logger logger; @@ -66,7 +68,7 @@ public Response getlockConf() { AppConfiguration lockConfiguration = lockConfigService.find(); logger.info("Lock details lockConfiguration():{}", lockConfiguration); if(lockConfiguration==null) { - throwInternalServerException("It seems Lock module is not setup, kindly check."); + throwInternalServerException(CONFIG_NULL_ERR_MSG); } return Response.ok(lockConfiguration).build(); @@ -87,7 +89,7 @@ public Response updatelockConf(@Valid AppConfiguration lockAppConf) { Conf conf = lockConfigService.findLockConf(); logger.info("Lock conf:{} ", conf); if(conf==null) { - throwInternalServerException("It seems Lock module is not setup, kindly check."); + throwInternalServerException(CONFIG_NULL_ERR_MSG); } conf.setDynamic(lockAppConf); @@ -115,7 +117,7 @@ public Response patchlockConf(@NotNull String jsonPatchString) throws JsonPatchE Conf conf = lockConfigService.findLockConf(); logger.info("Lock conf:{} ", conf); if(conf==null) { - throwInternalServerException("It seems Lock module is not setup, kindly check."); + throwInternalServerException(CONFIG_NULL_ERR_MSG); } AppConfiguration lockAppConf = Jackson.applyPatch(jsonPatchString, conf.getDynamic()); diff --git a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/service/AuditService.java b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/service/AuditService.java new file mode 100644 index 00000000000..562ba1dcfc4 --- /dev/null +++ b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/service/AuditService.java @@ -0,0 +1,249 @@ +package io.jans.configapi.plugin.lock.service; + +import io.jans.as.common.service.OrganizationService; +import io.jans.as.common.service.common.ApplicationFactory; +import io.jans.as.common.service.common.InumService; +import io.jans.as.common.util.AttributeConstants; +import io.jans.configapi.configuration.ConfigurationFactory; +import io.jans.configapi.plugin.lock.model.stat.*; + +import io.jans.model.SearchRequest; +import io.jans.orm.PersistenceEntryManager; +import io.jans.orm.model.PagedResult; +import io.jans.orm.model.SortOrder; +import io.jans.orm.search.filter.Filter; + +import io.jans.util.StringHelper; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; + + +@ApplicationScoped +public class AuditService { + + @Inject + Logger logger; + + @Inject + @Named(ApplicationFactory.PERSISTENCE_ENTRY_MANAGER_NAME) + PersistenceEntryManager persistenceEntryManager; + + @Inject + ConfigurationFactory configurationFactory; + + @Inject + OrganizationService organizationService; + + @Inject + private InumService inumService; + + public TelemetryEntry addTelemetryData(TelemetryEntry telemetryEntry) { + if (telemetryEntry == null) { + return telemetryEntry; + } + String inum = telemetryEntry.getInum(); + if (StringUtils.isBlank(inum)) { + inum = this.generateInumForEntry("telemetry", TelemetryEntry.class); + telemetryEntry.setInum(inum); + + telemetryEntry.setDn(this.getDnForTelemetryEntry(inum)); + } + persistenceEntryManager.persist(telemetryEntry); + + telemetryEntry = this.getTelemetryEntryByDn(this.getDnForTelemetryEntry(inum)); + return telemetryEntry; + } + + public void removeTelemetryEntry(TelemetryEntry telemetryEntry) { + persistenceEntryManager.removeRecursively(telemetryEntry.getDn(), TelemetryEntry.class); + } + + public void updateTelemetryEntry(TelemetryEntry telemetryEntry) { + persistenceEntryManager.merge(telemetryEntry); + } + + public TelemetryEntry getTelemetryEntryByInum(String inum) { + TelemetryEntry result = null; + try { + result = persistenceEntryManager.find(TelemetryEntry.class, getTelemetryEntryByDn(inum)); + } catch (Exception ex) { + logger.error("Failed to load TelemetryEntry entry", ex); + } + return result; + } + + public List searchTelemetryEntrys(String pattern, int sizeLimit) { + + logger.debug("Search TelemetryEntrys with pattern:{}, sizeLimit:{}", pattern, sizeLimit); + + String[] targetArray = new String[] { pattern }; + Filter displayNameFilter = Filter.createSubstringFilter(AttributeConstants.DISPLAY_NAME, null, targetArray, + null); + Filter descriptionFilter = Filter.createSubstringFilter(AttributeConstants.DESCRIPTION, null, targetArray, + null); + Filter inumFilter = Filter.createSubstringFilter(AttributeConstants.INUM, null, targetArray, null); + Filter searchFilter = Filter.createORFilter(displayNameFilter, descriptionFilter, inumFilter); + + logger.debug("Search TelemetryEntrys with searchFilter:{}", searchFilter); + return persistenceEntryManager.findEntries(getDnForTelemetryEntry(null), TelemetryEntry.class, searchFilter, + sizeLimit); + } + + public List getAllTelemetryEntrys(int sizeLimit) { + return persistenceEntryManager.findEntries(getDnForTelemetryEntry(null), TelemetryEntry.class, null, sizeLimit); + } + + public List getAllTelemetryEntrys() { + return persistenceEntryManager.findEntries(getDnForTelemetryEntry(null), TelemetryEntry.class, null); + } + + public PagedResult getTelemetryEntrys(SearchRequest searchRequest) { + logger.debug("Search TelemetryEntrys with searchRequest:{}", searchRequest); + + Filter searchFilter = null; + List filters = new ArrayList<>(); + if (searchRequest.getFilterAssertionValue() != null && !searchRequest.getFilterAssertionValue().isEmpty()) { + + for (String assertionValue : searchRequest.getFilterAssertionValue()) { + String[] targetArray = new String[] { assertionValue }; + Filter displayNameFilter = Filter.createSubstringFilter(AttributeConstants.DISPLAY_NAME, null, + targetArray, null); + Filter descriptionFilter = Filter.createSubstringFilter(AttributeConstants.DESCRIPTION, null, + targetArray, null); + Filter inumFilter = Filter.createSubstringFilter(AttributeConstants.INUM, null, targetArray, null); + filters.add(Filter.createORFilter(displayNameFilter, descriptionFilter, inumFilter)); + } + searchFilter = Filter.createORFilter(filters); + } + + logger.trace("TelemetryEntrys pattern searchFilter:{}", searchFilter); + List fieldValueFilters = new ArrayList<>(); + if (searchRequest.getFieldValueMap() != null && !searchRequest.getFieldValueMap().isEmpty()) { + for (Map.Entry entry : searchRequest.getFieldValueMap().entrySet()) { + Filter dataFilter = Filter.createEqualityFilter(entry.getKey(), entry.getValue()); + logger.trace("TelemetryEntrys dataFilter:{}", dataFilter); + fieldValueFilters.add(Filter.createANDFilter(dataFilter)); + } + searchFilter = Filter.createANDFilter(Filter.createORFilter(filters), + Filter.createANDFilter(fieldValueFilters)); + } + + logger.debug("TelemetryEntrys searchFilter:{}", searchFilter); + + return persistenceEntryManager.findPagedEntries(getDnForTelemetryEntry(null), TelemetryEntry.class, + searchFilter, null, searchRequest.getSortBy(), SortOrder.getByValue(searchRequest.getSortOrder()), + searchRequest.getStartIndex(), searchRequest.getCount(), searchRequest.getMaxCount()); + + } + + public TelemetryEntry getTelemetryEntryByDn(String dn) { + try { + return persistenceEntryManager.find(TelemetryEntry.class, dn); + } catch (Exception e) { + logger.warn("", e); + return null; + } + } + + public String getDnForTelemetryEntry(String inum) { + String orgDn = organizationService.getDnForOrganization(); + if (StringHelper.isEmpty(inum)) { + return String.format("ou=lock-telemetry,%s", orgDn); + } + return String.format("inum=%s,ou=lock-telemetry,%s", inum, orgDn); + } + + public HealthEntry addHealthEntry(HealthEntry healthEntry) { + if (healthEntry == null) { + return healthEntry; + } + String inum = healthEntry.getInum(); + if (StringUtils.isBlank(inum)) { + inum = this.generateInumForEntry("health",HealthEntry.class); + healthEntry.setInum(inum); + + healthEntry.setDn(this.getDnForHealthEntry(inum)); + } + persistenceEntryManager.persist(healthEntry); + + healthEntry = this.getHealthEntryByDn(this.getDnForHealthEntry(inum)); + return healthEntry; + } + + public HealthEntry getHealthEntryByDn(String dn) { + try { + return persistenceEntryManager.find(HealthEntry.class, dn); + } catch (Exception e) { + logger.warn("", e); + return null; + } + } + + public String getDnForHealthEntry(String inum) { + String orgDn = organizationService.getDnForOrganization(); + if (StringHelper.isEmpty(inum)) { + return String.format("ou=lock-health,%s", orgDn); + } + return String.format("inum=%s,ou=lock-health,%s", inum, orgDn); + } + + public LogEntry addLogData(LogEntry logEntry) { + if (logEntry == null) { + return logEntry; + } + String inum = logEntry.getInum(); + if (StringUtils.isBlank(inum)) { + inum = this.generateInumForEntry("log", LogEntry.class); + logEntry.setInum(inum); + + logEntry.setDn(this.getDnForLogEntry(inum)); + } + persistenceEntryManager.persist(logEntry); + + logEntry = this.getLogEntryByDn(this.getDnForLogEntry(inum)); + return logEntry; + } + + + public LogEntry getLogEntryByDn(String dn) { + try { + return persistenceEntryManager.find(LogEntry.class, dn); + } catch (Exception e) { + logger.warn("", e); + return null; + } + } + + public String getDnForLogEntry(String inum) { + String orgDn = organizationService.getDnForOrganization(); + if (StringHelper.isEmpty(inum)) { + return String.format("ou=lock-log,%s", orgDn); + } + return String.format("inum=%s,ou=lock-log,%s", inum, orgDn); + } + + public String generateInumForEntry(String entryName, Class classObj) { + String newInum = null; + String newDn = null; + int trycount = 0; + do { + if (trycount < InumService.MAX_IDGEN_TRY_COUNT) { + newInum = inumService.generateId(entryName); + trycount++; + } else { + newInum = inumService.generateDefaultId(); + } + newDn = getDnForLogEntry(newInum); + } while (persistenceEntryManager.contains(newDn, classObj)); + return newInum; + } + +} diff --git a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/util/Constants.java b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/util/Constants.java index a1c4aa60e7c..d170248b556 100644 --- a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/util/Constants.java +++ b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/util/Constants.java @@ -8,12 +8,32 @@ public class Constants { - private Constants() {} + private Constants() { + } + public static final String LOCK = "/lock"; public static final String LOCK_CONFIG = "/lockConfig"; - - - public static final String LOCK_CONFIG_READ_ACCESS = "https://jans.io/oauth/lock-config.readonly"; + public static final String AUDIT = "/audit"; + public static final String HEALTH = "/health"; + public static final String LOG = "/log"; + public static final String TELEMETRY = "/telemetry"; + + public static final String LOCK_READ_ACCESS = "https://jans.io/oauth/lock/read-all"; + public static final String LOCK_WRITE_ACCESS = "https://jans.io/oauth/lock/write-all"; + + public static final String LOCK_CONFIG_READ_ACCESS = "https://jans.io/oauth/lock-config.readonly"; public static final String LOCK_CONFIG_WRITE_ACCESS = "https://jans.io/oauth/lock-config.write"; + public static final String LOCK_AUDIT_READ_ACCESS = "https://jans.io/oauth/lock/audit.readonly"; + public static final String LOCK_AUDIT_WRITE_ACCESS = "https://jans.io/oauth/lock/audit.write"; + + public static final String LOCK_HEALTH_READ_ACCESS = "https://jans.io/oauth/lock/health.readonly"; + public static final String LOCK_HEALTH_WRITE_ACCESS = "https://jans.io/oauth/lock/health.write"; + + public static final String LOCK_LOG_READ_ACCESS = "https://jans.io/oauth/lock/log.readonly"; + public static final String LOCK_LOG_WRITE_ACCESS = "https://jans.io/oauth/lock/log.write"; + + public static final String LOCK_TELEMETRY_READ_ACCESS = "https://jans.io/oauth/lock/telemetry.readonly"; + public static final String LOCK_TELEMETRY_WRITE_ACCESS = "https://jans.io/oauth/lock/telemetry.write"; + } \ No newline at end of file diff --git a/jans-config-api/profiles/default/config-api-test.properties b/jans-config-api/profiles/default/config-api-test.properties index 990186180fc..b08bf6d38c1 100644 --- a/jans-config-api/profiles/default/config-api-test.properties +++ b/jans-config-api/profiles/default/config-api-test.properties @@ -1,7 +1,7 @@ # The URL of your Jans installation test.server=https://jenkins-config-api.gluu.org -test.scopes=https://jans.io/oauth/config/acrs.readonly https://jans.io/oauth/config/acrs.write https://jans.io/oauth/config/attributes.readonly https://jans.io/oauth/config/attributes.write https://jans.io/oauth/config/attributes.delete https://jans.io/oauth/config/cache.readonly https://jans.io/oauth/config/cache.write https://jans.io/oauth/config/openid/clients.readonly https://jans.io/oauth/config/openid/clients.write https://jans.io/oauth/config/openid/clients.delete https://jans.io/oauth/jans-auth-server/config/properties.readonly https://jans.io/oauth/jans-auth-server/config/properties.write https://jans.io/oauth/config/smtp.readonly https://jans.io/oauth/config/smtp.write https://jans.io/oauth/config/smtp.delete https://jans.io/oauth/config/scripts.readonly https://jans.io/oauth/config/scripts.write https://jans.io/oauth/config/scripts.delete https://jans.io/oauth/config/fido2.readonly https://jans.io/oauth/config/fido2.write https://jans.io/oauth/config/jwks.readonly https://jans.io/oauth/config/jwks.write https://jans.io/oauth/config/jwks.delete https://jans.io/oauth/config/database/ldap.readonly https://jans.io/oauth/config/database/ldap.write https://jans.io/oauth/config/database/ldap.delete https://jans.io/oauth/config/logging.readonly https://jans.io/oauth/config/logging.write https://jans.io/oauth/config/scopes.readonly https://jans.io/oauth/config/scopes.write https://jans.io/oauth/config/scopes.delete https://jans.io/oauth/config/uma/resources.readonly https://jans.io/oauth/config/uma/resources.write https://jans.io/oauth/config/uma/resources.delete https://jans.io/oauth/config/database/sql.readonly https://jans.io/oauth/config/database/sql.write https://jans.io/oauth/config/database/sql.delete https://jans.io/oauth/config/stats.readonly jans_stat https://jans.io/scim/users.read https://jans.io/scim/users.write https://jans.io/oauth/config/scim/users.read https://jans.io/oauth/config/scim/users.write https://jans.io/scim/config.readonly https://jans.io/scim/config.write https://jans.io/oauth/config/organization.readonly https://jans.io/oauth/config/organization.write https://jans.io/oauth/config/user.readonly https://jans.io/oauth/config/user.write https://jans.io/oauth/config/user.delete https://jans.io/oauth/config/agama.readonly https://jans.io/oauth/config/agama.write https://jans.io/oauth/config/agama.delete https://jans.io/oauth/jans-auth-server/session.readonly https://jans.io/oauth/jans-auth-server/session.delete revoke_session https://jans.io/oauth/config/read-all https://jans.io/oauth/config/write-all https://jans.io/oauth/config/delete-all https://jans.io/oauth/config/openid-read https://jans.io/oauth/config/openid-write https://jans.io/oauth/config/openid-delete https://jans.io/oauth/config/uma-read https://jans.io/oauth/config/uma-write https://jans.io/oauth/config/uma-delete https://jans.io/oauth/jans-auth-server/config/adminui/user/role.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/role.write https://jans.io/oauth/jans-auth-server/config/adminui/read-all https://jans.io/oauth/jans-auth-server/config/adminui/write-all https://jans.io/oauth/jans-auth-server/config/adminui/user/role.delete https://jans.io/oauth/jans-auth-server/config/adminui/delete-all https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.delete https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.write https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.delete https://jans.io/oauth/jans-auth-server/config/adminui/license.readonly https://jans.io/oauth/jans-auth-server/config/adminui/license.write https://jans.io/oauth/config/plugin.readonly https://jans.io/oauth/client/authorizations.readonly https://jans.io/oauth/client/authorizations.delete https://jans.io/oauth/config/cacherefresh.readonly https://jans.io/oauth/config/cacherefresh.write https://jans.io/oauth/config/saml.readonly https://jans.io/oauth/config/saml.write https://jans.io/oauth/config/saml-config.readonly https://jans.io/oauth/config/saml-config.write https://jans.io/oauth/config/saml-client-scope.readonly https://jans.io/oauth/config/saml-client-scope.write https://jans.io/idp/config.readonly https://jans.io/idp/config.write https://jans.io/idp/realm.readonly https://jans.io/idp/realm.write https://jans.io/idp/realm.write https://jans.io/idp/saml.readonly https://jans.io/idp/saml.write https://jans.io/oauth/config/app-version.readonly https://jans.io/oauth/kc-link-config.readonly https://jans.io/oauth/kc-link-config.write https://jans.io/oauth/lock-config.readonly https://jans.io/oauth/lock-config.write https://pujavs-definite-dory.gluu.info/jans-config-api/api/v1/jans-assets/upload-asset https://jans.io/oauth/config/jans_asset-write https://jans.io/oauth/config/jans_asset-delete +test.scopes=https://jans.io/oauth/config/acrs.readonly https://jans.io/oauth/config/acrs.write https://jans.io/oauth/config/attributes.readonly https://jans.io/oauth/config/attributes.write https://jans.io/oauth/config/attributes.delete https://jans.io/oauth/config/cache.readonly https://jans.io/oauth/config/cache.write https://jans.io/oauth/config/openid/clients.readonly https://jans.io/oauth/config/openid/clients.write https://jans.io/oauth/config/openid/clients.delete https://jans.io/oauth/jans-auth-server/config/properties.readonly https://jans.io/oauth/jans-auth-server/config/properties.write https://jans.io/oauth/config/smtp.readonly https://jans.io/oauth/config/smtp.write https://jans.io/oauth/config/smtp.delete https://jans.io/oauth/config/scripts.readonly https://jans.io/oauth/config/scripts.write https://jans.io/oauth/config/scripts.delete https://jans.io/oauth/config/fido2.readonly https://jans.io/oauth/config/fido2.write https://jans.io/oauth/config/jwks.readonly https://jans.io/oauth/config/jwks.write https://jans.io/oauth/config/jwks.delete https://jans.io/oauth/config/database/ldap.readonly https://jans.io/oauth/config/database/ldap.write https://jans.io/oauth/config/database/ldap.delete https://jans.io/oauth/config/logging.readonly https://jans.io/oauth/config/logging.write https://jans.io/oauth/config/scopes.readonly https://jans.io/oauth/config/scopes.write https://jans.io/oauth/config/scopes.delete https://jans.io/oauth/config/uma/resources.readonly https://jans.io/oauth/config/uma/resources.write https://jans.io/oauth/config/uma/resources.delete https://jans.io/oauth/config/database/sql.readonly https://jans.io/oauth/config/database/sql.write https://jans.io/oauth/config/database/sql.delete https://jans.io/oauth/config/stats.readonly jans_stat https://jans.io/scim/users.read https://jans.io/scim/users.write https://jans.io/oauth/config/scim/users.read https://jans.io/oauth/config/scim/users.write https://jans.io/scim/config.readonly https://jans.io/scim/config.write https://jans.io/oauth/config/organization.readonly https://jans.io/oauth/config/organization.write https://jans.io/oauth/config/user.readonly https://jans.io/oauth/config/user.write https://jans.io/oauth/config/user.delete https://jans.io/oauth/config/agama.readonly https://jans.io/oauth/config/agama.write https://jans.io/oauth/config/agama.delete https://jans.io/oauth/jans-auth-server/session.readonly https://jans.io/oauth/jans-auth-server/session.delete revoke_session https://jans.io/oauth/config/read-all https://jans.io/oauth/config/write-all https://jans.io/oauth/config/delete-all https://jans.io/oauth/config/openid-read https://jans.io/oauth/config/openid-write https://jans.io/oauth/config/openid-delete https://jans.io/oauth/config/uma-read https://jans.io/oauth/config/uma-write https://jans.io/oauth/config/uma-delete https://jans.io/oauth/jans-auth-server/config/adminui/user/role.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/role.write https://jans.io/oauth/jans-auth-server/config/adminui/read-all https://jans.io/oauth/jans-auth-server/config/adminui/write-all https://jans.io/oauth/jans-auth-server/config/adminui/user/role.delete https://jans.io/oauth/jans-auth-server/config/adminui/delete-all https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.delete https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.write https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.delete https://jans.io/oauth/jans-auth-server/config/adminui/license.readonly https://jans.io/oauth/jans-auth-server/config/adminui/license.write https://jans.io/oauth/config/plugin.readonly https://jans.io/oauth/client/authorizations.readonly https://jans.io/oauth/client/authorizations.delete https://jans.io/oauth/config/cacherefresh.readonly https://jans.io/oauth/config/cacherefresh.write https://jans.io/oauth/config/saml.readonly https://jans.io/oauth/config/saml.write https://jans.io/oauth/config/saml-config.readonly https://jans.io/oauth/config/saml-config.write https://jans.io/oauth/config/saml-client-scope.readonly https://jans.io/oauth/config/saml-client-scope.write https://jans.io/idp/config.readonly https://jans.io/idp/config.write https://jans.io/idp/realm.readonly https://jans.io/idp/realm.write https://jans.io/idp/realm.write https://jans.io/idp/saml.readonly https://jans.io/idp/saml.write https://jans.io/oauth/config/app-version.readonly https://jans.io/oauth/kc-link-config.readonly https://jans.io/oauth/kc-link-config.write https://jans.io/oauth/lock-config.readonly https://jans.io/oauth/lock-config.write https://pujavs-definite-dory.gluu.info/jans-config-api/api/v1/jans-assets/upload-asset https://jans.io/oauth/config/jans_asset-write https://jans.io/oauth/config/jans_asset-delete https://jans.io/oauth/lock/read-all https://jans.io/oauth/lock/write-all https://jans.io/oauth/lock-config.readonly https://jans.io/oauth/lock-config.write https://jans.io/oauth/lock/audit.readonly https://jans.io/oauth/lock/audit.write https://jans.io/oauth/lock/health.readonly https://jans.io/oauth/lock/health.write https://jans.io/oauth/lock/log.readonly https://jans.io/oauth/lock/log.write https://jans.io/oauth/lock/telemetry.readonly https://jans.io/oauth/lock/telemetry.write token.endpoint=https://jenkins-config-api.gluu.org/jans-auth/restv1/token token.grant.type=client_credentials diff --git a/jans-config-api/profiles/jans-ui.jans.io/test.properties b/jans-config-api/profiles/jans-ui.jans.io/test.properties index 02eac4024bf..bf72142fb26 100644 --- a/jans-config-api/profiles/jans-ui.jans.io/test.properties +++ b/jans-config-api/profiles/jans-ui.jans.io/test.properties @@ -1,4 +1,4 @@ -test.scopes=https://jans.io/oauth/config/acrs.readonly https://jans.io/oauth/config/acrs.write https://jans.io/oauth/config/attributes.readonly https://jans.io/oauth/config/attributes.write https://jans.io/oauth/config/attributes.delete https://jans.io/oauth/config/cache.readonly https://jans.io/oauth/config/cache.write https://jans.io/oauth/config/openid/clients.readonly https://jans.io/oauth/config/openid/clients.write https://jans.io/oauth/config/openid/clients.delete https://jans.io/oauth/jans-auth-server/config/properties.readonly https://jans.io/oauth/jans-auth-server/config/properties.write https://jans.io/oauth/config/smtp.readonly https://jans.io/oauth/config/smtp.write https://jans.io/oauth/config/smtp.delete https://jans.io/oauth/config/scripts.readonly https://jans.io/oauth/config/scripts.write https://jans.io/oauth/config/scripts.delete https://jans.io/oauth/config/fido2.readonly https://jans.io/oauth/config/fido2.write https://jans.io/oauth/config/jwks.readonly https://jans.io/oauth/config/jwks.write https://jans.io/oauth/config/jwks.delete https://jans.io/oauth/config/database/ldap.readonly https://jans.io/oauth/config/database/ldap.write https://jans.io/oauth/config/database/ldap.delete https://jans.io/oauth/config/logging.readonly https://jans.io/oauth/config/logging.write https://jans.io/oauth/config/scopes.readonly https://jans.io/oauth/config/scopes.write https://jans.io/oauth/config/scopes.delete https://jans.io/oauth/config/uma/resources.readonly https://jans.io/oauth/config/uma/resources.write https://jans.io/oauth/config/uma/resources.delete https://jans.io/oauth/config/database/sql.readonly https://jans.io/oauth/config/database/sql.write https://jans.io/oauth/config/database/sql.delete https://jans.io/oauth/config/stats.readonly jans_stat https://jans.io/scim/users.read https://jans.io/scim/users.write https://jans.io/oauth/config/scim/users.read https://jans.io/oauth/config/scim/users.write https://jans.io/scim/config.readonly https://jans.io/scim/config.write https://jans.io/oauth/config/organization.readonly https://jans.io/oauth/config/organization.write https://jans.io/oauth/config/user.readonly https://jans.io/oauth/config/user.write https://jans.io/oauth/config/user.delete https://jans.io/oauth/config/agama.readonly https://jans.io/oauth/config/agama.write https://jans.io/oauth/config/agama.delete https://jans.io/oauth/jans-auth-server/session.readonly https://jans.io/oauth/jans-auth-server/session.delete revoke_session https://jans.io/oauth/config/read-all https://jans.io/oauth/config/write-all https://jans.io/oauth/config/delete-all https://jans.io/oauth/config/openid-read https://jans.io/oauth/config/openid-write https://jans.io/oauth/config/openid-delete https://jans.io/oauth/config/uma-read https://jans.io/oauth/config/uma-write https://jans.io/oauth/config/uma-delete https://jans.io/oauth/jans-auth-server/config/adminui/user/role.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/role.write https://jans.io/oauth/jans-auth-server/config/adminui/read-all https://jans.io/oauth/jans-auth-server/config/adminui/write-all https://jans.io/oauth/jans-auth-server/config/adminui/user/role.delete https://jans.io/oauth/jans-auth-server/config/adminui/delete-all https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.delete https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.write https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.delete https://jans.io/oauth/jans-auth-server/config/adminui/license.readonly https://jans.io/oauth/jans-auth-server/config/adminui/license.write https://jans.io/oauth/config/plugin.readonly https://jans.io/oauth/client/authorizations.readonly https://jans.io/oauth/client/authorizations.delete https://jans.io/oauth/config/cacherefresh.readonly https://jans.io/oauth/config/cacherefresh.write https://jans.io/oauth/config/saml.readonly https://jans.io/oauth/config/saml.write https://jans.io/oauth/config/saml-config.readonly https://jans.io/oauth/config/saml-config.write https://jans.io/oauth/config/saml-client-scope.readonly https://jans.io/oauth/config/saml-client-scope.write https://jans.io/idp/config.readonly https://jans.io/idp/config.write https://jans.io/idp/realm.readonly https://jans.io/idp/realm.write https://jans.io/idp/realm.write https://jans.io/idp/saml.readonly https://jans.io/idp/saml.write https://jans.io/oauth/config/app-version.readonly https://jans.io/oauth/kc-link-config.readonly https://jans.io/oauth/kc-link-config.write https://jans.io/oauth/lock-config.readonly https://jans.io/oauth/lock-config.write https://pujavs-definite-dory.gluu.info/jans-config-api/api/v1/jans-assets/upload-asset https://jans.io/oauth/config/jans_asset-write https://jans.io/oauth/config/jans_asset-delete +test.scopes=https://jans.io/oauth/config/acrs.readonly https://jans.io/oauth/config/acrs.write https://jans.io/oauth/config/attributes.readonly https://jans.io/oauth/config/attributes.write https://jans.io/oauth/config/attributes.delete https://jans.io/oauth/config/cache.readonly https://jans.io/oauth/config/cache.write https://jans.io/oauth/config/openid/clients.readonly https://jans.io/oauth/config/openid/clients.write https://jans.io/oauth/config/openid/clients.delete https://jans.io/oauth/jans-auth-server/config/properties.readonly https://jans.io/oauth/jans-auth-server/config/properties.write https://jans.io/oauth/config/smtp.readonly https://jans.io/oauth/config/smtp.write https://jans.io/oauth/config/smtp.delete https://jans.io/oauth/config/scripts.readonly https://jans.io/oauth/config/scripts.write https://jans.io/oauth/config/scripts.delete https://jans.io/oauth/config/fido2.readonly https://jans.io/oauth/config/fido2.write https://jans.io/oauth/config/jwks.readonly https://jans.io/oauth/config/jwks.write https://jans.io/oauth/config/jwks.delete https://jans.io/oauth/config/database/ldap.readonly https://jans.io/oauth/config/database/ldap.write https://jans.io/oauth/config/database/ldap.delete https://jans.io/oauth/config/logging.readonly https://jans.io/oauth/config/logging.write https://jans.io/oauth/config/scopes.readonly https://jans.io/oauth/config/scopes.write https://jans.io/oauth/config/scopes.delete https://jans.io/oauth/config/uma/resources.readonly https://jans.io/oauth/config/uma/resources.write https://jans.io/oauth/config/uma/resources.delete https://jans.io/oauth/config/database/sql.readonly https://jans.io/oauth/config/database/sql.write https://jans.io/oauth/config/database/sql.delete https://jans.io/oauth/config/stats.readonly jans_stat https://jans.io/scim/users.read https://jans.io/scim/users.write https://jans.io/oauth/config/scim/users.read https://jans.io/oauth/config/scim/users.write https://jans.io/scim/config.readonly https://jans.io/scim/config.write https://jans.io/oauth/config/organization.readonly https://jans.io/oauth/config/organization.write https://jans.io/oauth/config/user.readonly https://jans.io/oauth/config/user.write https://jans.io/oauth/config/user.delete https://jans.io/oauth/config/agama.readonly https://jans.io/oauth/config/agama.write https://jans.io/oauth/config/agama.delete https://jans.io/oauth/jans-auth-server/session.readonly https://jans.io/oauth/jans-auth-server/session.delete revoke_session https://jans.io/oauth/config/read-all https://jans.io/oauth/config/write-all https://jans.io/oauth/config/delete-all https://jans.io/oauth/config/openid-read https://jans.io/oauth/config/openid-write https://jans.io/oauth/config/openid-delete https://jans.io/oauth/config/uma-read https://jans.io/oauth/config/uma-write https://jans.io/oauth/config/uma-delete https://jans.io/oauth/jans-auth-server/config/adminui/user/role.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/role.write https://jans.io/oauth/jans-auth-server/config/adminui/read-all https://jans.io/oauth/jans-auth-server/config/adminui/write-all https://jans.io/oauth/jans-auth-server/config/adminui/user/role.delete https://jans.io/oauth/jans-auth-server/config/adminui/delete-all https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.delete https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.write https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.delete https://jans.io/oauth/jans-auth-server/config/adminui/license.readonly https://jans.io/oauth/jans-auth-server/config/adminui/license.write https://jans.io/oauth/config/plugin.readonly https://jans.io/oauth/client/authorizations.readonly https://jans.io/oauth/client/authorizations.delete https://jans.io/oauth/config/cacherefresh.readonly https://jans.io/oauth/config/cacherefresh.write https://jans.io/oauth/config/saml.readonly https://jans.io/oauth/config/saml.write https://jans.io/oauth/config/saml-config.readonly https://jans.io/oauth/config/saml-config.write https://jans.io/oauth/config/saml-client-scope.readonly https://jans.io/oauth/config/saml-client-scope.write https://jans.io/idp/config.readonly https://jans.io/idp/config.write https://jans.io/idp/realm.readonly https://jans.io/idp/realm.write https://jans.io/idp/realm.write https://jans.io/idp/saml.readonly https://jans.io/idp/saml.write https://jans.io/oauth/config/app-version.readonly https://jans.io/oauth/kc-link-config.readonly https://jans.io/oauth/kc-link-config.write https://jans.io/oauth/lock-config.readonly https://jans.io/oauth/lock-config.write https://pujavs-definite-dory.gluu.info/jans-config-api/api/v1/jans-assets/upload-asset https://jans.io/oauth/config/jans_asset-write https://jans.io/oauth/config/jans_asset-delete https://jans.io/oauth/lock/read-all https://jans.io/oauth/lock/write-all https://jans.io/oauth/lock-config.readonly https://jans.io/oauth/lock-config.write https://jans.io/oauth/lock/audit.readonly https://jans.io/oauth/lock/audit.write https://jans.io/oauth/lock/health.readonly https://jans.io/oauth/lock/health.write https://jans.io/oauth/lock/log.readonly https://jans.io/oauth/lock/log.write https://jans.io/oauth/lock/telemetry.readonly https://jans.io/oauth/lock/telemetry.write # Test env Setting token.endpoint=https://jans-ui.jans.io/jans-auth/restv1/token diff --git a/jans-config-api/profiles/jenkins-config-api.gluu.org/test.properties b/jans-config-api/profiles/jenkins-config-api.gluu.org/test.properties index 077b80ded60..5e63322174c 100644 --- a/jans-config-api/profiles/jenkins-config-api.gluu.org/test.properties +++ b/jans-config-api/profiles/jenkins-config-api.gluu.org/test.properties @@ -1,6 +1,6 @@ test.server=https://jenkins-config-api.gluu.org -test.scopes=https://jans.io/oauth/config/acrs.readonly https://jans.io/oauth/config/acrs.write https://jans.io/oauth/config/attributes.readonly https://jans.io/oauth/config/attributes.write https://jans.io/oauth/config/attributes.delete https://jans.io/oauth/config/cache.readonly https://jans.io/oauth/config/cache.write https://jans.io/oauth/config/openid/clients.readonly https://jans.io/oauth/config/openid/clients.write https://jans.io/oauth/config/openid/clients.delete https://jans.io/oauth/jans-auth-server/config/properties.readonly https://jans.io/oauth/jans-auth-server/config/properties.write https://jans.io/oauth/config/smtp.readonly https://jans.io/oauth/config/smtp.write https://jans.io/oauth/config/smtp.delete https://jans.io/oauth/config/scripts.readonly https://jans.io/oauth/config/scripts.write https://jans.io/oauth/config/scripts.delete https://jans.io/oauth/config/fido2.readonly https://jans.io/oauth/config/fido2.write https://jans.io/oauth/config/jwks.readonly https://jans.io/oauth/config/jwks.write https://jans.io/oauth/config/jwks.delete https://jans.io/oauth/config/database/ldap.readonly https://jans.io/oauth/config/database/ldap.write https://jans.io/oauth/config/database/ldap.delete https://jans.io/oauth/config/logging.readonly https://jans.io/oauth/config/logging.write https://jans.io/oauth/config/scopes.readonly https://jans.io/oauth/config/scopes.write https://jans.io/oauth/config/scopes.delete https://jans.io/oauth/config/uma/resources.readonly https://jans.io/oauth/config/uma/resources.write https://jans.io/oauth/config/uma/resources.delete https://jans.io/oauth/config/database/sql.readonly https://jans.io/oauth/config/database/sql.write https://jans.io/oauth/config/database/sql.delete https://jans.io/oauth/config/stats.readonly jans_stat https://jans.io/scim/users.read https://jans.io/scim/users.write https://jans.io/oauth/config/scim/users.read https://jans.io/oauth/config/scim/users.write https://jans.io/scim/config.readonly https://jans.io/scim/config.write https://jans.io/oauth/config/organization.readonly https://jans.io/oauth/config/organization.write https://jans.io/oauth/config/user.readonly https://jans.io/oauth/config/user.write https://jans.io/oauth/config/user.delete https://jans.io/oauth/config/agama.readonly https://jans.io/oauth/config/agama.write https://jans.io/oauth/config/agama.delete https://jans.io/oauth/jans-auth-server/session.readonly https://jans.io/oauth/jans-auth-server/session.delete revoke_session https://jans.io/oauth/config/read-all https://jans.io/oauth/config/write-all https://jans.io/oauth/config/delete-all https://jans.io/oauth/config/openid-read https://jans.io/oauth/config/openid-write https://jans.io/oauth/config/openid-delete https://jans.io/oauth/config/uma-read https://jans.io/oauth/config/uma-write https://jans.io/oauth/config/uma-delete https://jans.io/oauth/jans-auth-server/config/adminui/user/role.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/role.write https://jans.io/oauth/jans-auth-server/config/adminui/read-all https://jans.io/oauth/jans-auth-server/config/adminui/write-all https://jans.io/oauth/jans-auth-server/config/adminui/user/role.delete https://jans.io/oauth/jans-auth-server/config/adminui/delete-all https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.delete https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.write https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.delete https://jans.io/oauth/jans-auth-server/config/adminui/license.readonly https://jans.io/oauth/jans-auth-server/config/adminui/license.write https://jans.io/oauth/config/plugin.readonly https://jans.io/oauth/client/authorizations.readonly https://jans.io/oauth/client/authorizations.delete https://jans.io/oauth/config/cacherefresh.readonly https://jans.io/oauth/config/cacherefresh.write https://jans.io/oauth/config/saml.readonly https://jans.io/oauth/config/saml.write https://jans.io/oauth/config/saml-config.readonly https://jans.io/oauth/config/saml-config.write https://jans.io/oauth/config/saml-client-scope.readonly https://jans.io/oauth/config/saml-client-scope.write https://jans.io/idp/config.readonly https://jans.io/idp/config.write https://jans.io/idp/realm.readonly https://jans.io/idp/realm.write https://jans.io/idp/realm.write https://jans.io/idp/saml.readonly https://jans.io/idp/saml.write https://jans.io/oauth/config/app-version.readonly https://jans.io/oauth/kc-link-config.readonly https://jans.io/oauth/kc-link-config.write https://jans.io/oauth/lock-config.readonly https://jans.io/oauth/lock-config.write https://jans.io/oauth/config/acrs.readonly https://jans.io/oauth/config/acrs.write https://jans.io/oauth/config/attributes.readonly https://jans.io/oauth/config/attributes.write https://jans.io/oauth/config/attributes.delete https://jans.io/oauth/config/cache.readonly https://jans.io/oauth/config/cache.write https://jans.io/oauth/config/openid/clients.readonly https://jans.io/oauth/config/openid/clients.write https://jans.io/oauth/config/openid/clients.delete https://jans.io/oauth/jans-auth-server/config/properties.readonly https://jans.io/oauth/jans-auth-server/config/properties.write https://jans.io/oauth/config/smtp.readonly https://jans.io/oauth/config/smtp.write https://jans.io/oauth/config/smtp.delete https://jans.io/oauth/config/scripts.readonly https://jans.io/oauth/config/scripts.write https://jans.io/oauth/config/scripts.delete https://jans.io/oauth/config/fido2.readonly https://jans.io/oauth/config/fido2.write https://jans.io/oauth/config/jwks.readonly https://jans.io/oauth/config/jwks.write https://jans.io/oauth/config/jwks.delete https://jans.io/oauth/config/database/ldap.readonly https://jans.io/oauth/config/database/ldap.write https://jans.io/oauth/config/database/ldap.delete https://jans.io/oauth/config/logging.readonly https://jans.io/oauth/config/logging.write https://jans.io/oauth/config/scopes.readonly https://jans.io/oauth/config/scopes.write https://jans.io/oauth/config/scopes.delete https://jans.io/oauth/config/uma/resources.readonly https://jans.io/oauth/config/uma/resources.write https://jans.io/oauth/config/uma/resources.delete https://jans.io/oauth/config/database/sql.readonly https://jans.io/oauth/config/database/sql.write https://jans.io/oauth/config/database/sql.delete https://jans.io/oauth/config/stats.readonly jans_stat https://jans.io/scim/users.read https://jans.io/scim/users.write https://jans.io/oauth/config/scim/users.read https://jans.io/oauth/config/scim/users.write https://jans.io/scim/config.readonly https://jans.io/scim/config.write https://jans.io/oauth/config/organization.readonly https://jans.io/oauth/config/organization.write https://jans.io/oauth/config/user.readonly https://jans.io/oauth/config/user.write https://jans.io/oauth/config/user.delete https://jans.io/oauth/config/agama.readonly https://jans.io/oauth/config/agama.write https://jans.io/oauth/config/agama.delete https://jans.io/oauth/jans-auth-server/session.readonly https://jans.io/oauth/jans-auth-server/session.delete revoke_session https://jans.io/oauth/config/read-all https://jans.io/oauth/config/write-all https://jans.io/oauth/config/delete-all https://jans.io/oauth/config/openid-read https://jans.io/oauth/config/openid-write https://jans.io/oauth/config/openid-delete https://jans.io/oauth/config/uma-read https://jans.io/oauth/config/uma-write https://jans.io/oauth/config/uma-delete https://jans.io/oauth/jans-auth-server/config/adminui/user/role.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/role.write https://jans.io/oauth/jans-auth-server/config/adminui/read-all https://jans.io/oauth/jans-auth-server/config/adminui/write-all https://jans.io/oauth/jans-auth-server/config/adminui/user/role.delete https://jans.io/oauth/jans-auth-server/config/adminui/delete-all https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.delete https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.write https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.delete https://jans.io/oauth/jans-auth-server/config/adminui/license.readonly https://jans.io/oauth/jans-auth-server/config/adminui/license.write https://jans.io/oauth/config/plugin.readonly https://jans.io/oauth/client/authorizations.readonly https://jans.io/oauth/client/authorizations.delete https://jans.io/oauth/config/cacherefresh.readonly https://jans.io/oauth/config/cacherefresh.write https://jans.io/oauth/config/saml.readonly https://jans.io/oauth/config/saml.write https://jans.io/oauth/config/saml-config.readonly https://jans.io/oauth/config/saml-config.write https://jans.io/oauth/config/saml-client-scope.readonly https://jans.io/oauth/config/saml-client-scope.write https://jans.io/idp/config.readonly https://jans.io/idp/config.write https://jans.io/idp/realm.readonly https://jans.io/idp/realm.write https://jans.io/idp/realm.write https://jans.io/idp/saml.readonly https://jans.io/idp/saml.write https://jans.io/oauth/config/app-version.readonly https://jans.io/oauth/kc-link-config.readonly https://jans.io/oauth/kc-link-config.write https://jans.io/oauth/lock-config.readonly https://jans.io/oauth/lock-config.write https://pujavs-definite-dory.gluu.info/jans-config-api/api/v1/jans-assets/upload-asset https://jans.io/oauth/config/jans_asset-write https://jans.io/oauth/config/jans_asset-delete +test.scopes=https://jans.io/oauth/config/acrs.readonly https://jans.io/oauth/config/acrs.write https://jans.io/oauth/config/attributes.readonly https://jans.io/oauth/config/attributes.write https://jans.io/oauth/config/attributes.delete https://jans.io/oauth/config/cache.readonly https://jans.io/oauth/config/cache.write https://jans.io/oauth/config/openid/clients.readonly https://jans.io/oauth/config/openid/clients.write https://jans.io/oauth/config/openid/clients.delete https://jans.io/oauth/jans-auth-server/config/properties.readonly https://jans.io/oauth/jans-auth-server/config/properties.write https://jans.io/oauth/config/smtp.readonly https://jans.io/oauth/config/smtp.write https://jans.io/oauth/config/smtp.delete https://jans.io/oauth/config/scripts.readonly https://jans.io/oauth/config/scripts.write https://jans.io/oauth/config/scripts.delete https://jans.io/oauth/config/fido2.readonly https://jans.io/oauth/config/fido2.write https://jans.io/oauth/config/jwks.readonly https://jans.io/oauth/config/jwks.write https://jans.io/oauth/config/jwks.delete https://jans.io/oauth/config/database/ldap.readonly https://jans.io/oauth/config/database/ldap.write https://jans.io/oauth/config/database/ldap.delete https://jans.io/oauth/config/logging.readonly https://jans.io/oauth/config/logging.write https://jans.io/oauth/config/scopes.readonly https://jans.io/oauth/config/scopes.write https://jans.io/oauth/config/scopes.delete https://jans.io/oauth/config/uma/resources.readonly https://jans.io/oauth/config/uma/resources.write https://jans.io/oauth/config/uma/resources.delete https://jans.io/oauth/config/database/sql.readonly https://jans.io/oauth/config/database/sql.write https://jans.io/oauth/config/database/sql.delete https://jans.io/oauth/config/stats.readonly jans_stat https://jans.io/scim/users.read https://jans.io/scim/users.write https://jans.io/oauth/config/scim/users.read https://jans.io/oauth/config/scim/users.write https://jans.io/scim/config.readonly https://jans.io/scim/config.write https://jans.io/oauth/config/organization.readonly https://jans.io/oauth/config/organization.write https://jans.io/oauth/config/user.readonly https://jans.io/oauth/config/user.write https://jans.io/oauth/config/user.delete https://jans.io/oauth/config/agama.readonly https://jans.io/oauth/config/agama.write https://jans.io/oauth/config/agama.delete https://jans.io/oauth/jans-auth-server/session.readonly https://jans.io/oauth/jans-auth-server/session.delete revoke_session https://jans.io/oauth/config/read-all https://jans.io/oauth/config/write-all https://jans.io/oauth/config/delete-all https://jans.io/oauth/config/openid-read https://jans.io/oauth/config/openid-write https://jans.io/oauth/config/openid-delete https://jans.io/oauth/config/uma-read https://jans.io/oauth/config/uma-write https://jans.io/oauth/config/uma-delete https://jans.io/oauth/jans-auth-server/config/adminui/user/role.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/role.write https://jans.io/oauth/jans-auth-server/config/adminui/read-all https://jans.io/oauth/jans-auth-server/config/adminui/write-all https://jans.io/oauth/jans-auth-server/config/adminui/user/role.delete https://jans.io/oauth/jans-auth-server/config/adminui/delete-all https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.delete https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.write https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.delete https://jans.io/oauth/jans-auth-server/config/adminui/license.readonly https://jans.io/oauth/jans-auth-server/config/adminui/license.write https://jans.io/oauth/config/plugin.readonly https://jans.io/oauth/client/authorizations.readonly https://jans.io/oauth/client/authorizations.delete https://jans.io/oauth/config/cacherefresh.readonly https://jans.io/oauth/config/cacherefresh.write https://jans.io/oauth/config/saml.readonly https://jans.io/oauth/config/saml.write https://jans.io/oauth/config/saml-config.readonly https://jans.io/oauth/config/saml-config.write https://jans.io/oauth/config/saml-client-scope.readonly https://jans.io/oauth/config/saml-client-scope.write https://jans.io/idp/config.readonly https://jans.io/idp/config.write https://jans.io/idp/realm.readonly https://jans.io/idp/realm.write https://jans.io/idp/realm.write https://jans.io/idp/saml.readonly https://jans.io/idp/saml.write https://jans.io/oauth/config/app-version.readonly https://jans.io/oauth/kc-link-config.readonly https://jans.io/oauth/kc-link-config.write https://jans.io/oauth/lock-config.readonly https://jans.io/oauth/lock-config.write https://pujavs-definite-dory.gluu.info/jans-config-api/api/v1/jans-assets/upload-asset https://jans.io/oauth/config/jans_asset-write https://jans.io/oauth/config/jans_asset-delete https://jans.io/oauth/lock/read-all https://jans.io/oauth/lock/write-all https://jans.io/oauth/lock-config.readonly https://jans.io/oauth/lock-config.write https://jans.io/oauth/lock/audit.readonly https://jans.io/oauth/lock/audit.write https://jans.io/oauth/lock/health.readonly https://jans.io/oauth/lock/health.write https://jans.io/oauth/lock/log.readonly https://jans.io/oauth/lock/log.write https://jans.io/oauth/lock/telemetry.readonly https://jans.io/oauth/lock/telemetry.write token.endpoint=https://jenkins-config-api.gluu.org/jans-auth/restv1/token token.grant.type=client_credentials diff --git a/jans-config-api/profiles/local/test.properties b/jans-config-api/profiles/local/test.properties index 7ab22911b30..00d84095938 100644 --- a/jans-config-api/profiles/local/test.properties +++ b/jans-config-api/profiles/local/test.properties @@ -1,5 +1,5 @@ #LOCAL -test.scopes=https://jans.io/oauth/config/acrs.readonly https://jans.io/oauth/config/acrs.write https://jans.io/oauth/config/attributes.readonly https://jans.io/oauth/config/attributes.write https://jans.io/oauth/config/attributes.delete https://jans.io/oauth/config/cache.readonly https://jans.io/oauth/config/cache.write https://jans.io/oauth/config/openid/clients.readonly https://jans.io/oauth/config/openid/clients.write https://jans.io/oauth/config/openid/clients.delete https://jans.io/oauth/jans-auth-server/config/properties.readonly https://jans.io/oauth/jans-auth-server/config/properties.write https://jans.io/oauth/config/smtp.readonly https://jans.io/oauth/config/smtp.write https://jans.io/oauth/config/smtp.delete https://jans.io/oauth/config/scripts.readonly https://jans.io/oauth/config/scripts.write https://jans.io/oauth/config/scripts.delete https://jans.io/oauth/config/fido2.readonly https://jans.io/oauth/config/fido2.write https://jans.io/oauth/config/jwks.readonly https://jans.io/oauth/config/jwks.write https://jans.io/oauth/config/jwks.delete https://jans.io/oauth/config/database/ldap.readonly https://jans.io/oauth/config/database/ldap.write https://jans.io/oauth/config/database/ldap.delete https://jans.io/oauth/config/logging.readonly https://jans.io/oauth/config/logging.write https://jans.io/oauth/config/scopes.readonly https://jans.io/oauth/config/scopes.write https://jans.io/oauth/config/scopes.delete https://jans.io/oauth/config/uma/resources.readonly https://jans.io/oauth/config/uma/resources.write https://jans.io/oauth/config/uma/resources.delete https://jans.io/oauth/config/database/sql.readonly https://jans.io/oauth/config/database/sql.write https://jans.io/oauth/config/database/sql.delete https://jans.io/oauth/config/stats.readonly jans_stat https://jans.io/scim/users.read https://jans.io/scim/users.write https://jans.io/oauth/config/scim/users.read https://jans.io/oauth/config/scim/users.write https://jans.io/scim/config.readonly https://jans.io/scim/config.write https://jans.io/oauth/config/organization.readonly https://jans.io/oauth/config/organization.write https://jans.io/oauth/config/user.readonly https://jans.io/oauth/config/user.write https://jans.io/oauth/config/user.delete https://jans.io/oauth/config/agama.readonly https://jans.io/oauth/config/agama.write https://jans.io/oauth/config/agama.delete https://jans.io/oauth/jans-auth-server/session.readonly https://jans.io/oauth/jans-auth-server/session.delete revoke_session https://jans.io/oauth/config/read-all https://jans.io/oauth/config/write-all https://jans.io/oauth/config/delete-all https://jans.io/oauth/config/openid-read https://jans.io/oauth/config/openid-write https://jans.io/oauth/config/openid-delete https://jans.io/oauth/config/uma-read https://jans.io/oauth/config/uma-write https://jans.io/oauth/config/uma-delete https://jans.io/oauth/jans-auth-server/config/adminui/user/role.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/role.write https://jans.io/oauth/jans-auth-server/config/adminui/read-all https://jans.io/oauth/jans-auth-server/config/adminui/write-all https://jans.io/oauth/jans-auth-server/config/adminui/user/role.delete https://jans.io/oauth/jans-auth-server/config/adminui/delete-all https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.delete https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.write https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.delete https://jans.io/oauth/jans-auth-server/config/adminui/license.readonly https://jans.io/oauth/jans-auth-server/config/adminui/license.write https://jans.io/oauth/config/plugin.readonly https://jans.io/oauth/client/authorizations.readonly https://jans.io/oauth/client/authorizations.delete https://jans.io/oauth/config/cacherefresh.readonly https://jans.io/oauth/config/cacherefresh.write https://jans.io/oauth/config/saml.readonly https://jans.io/oauth/config/saml.write https://jans.io/oauth/config/saml-config.readonly https://jans.io/oauth/config/saml-config.write https://jans.io/oauth/config/saml-client-scope.readonly https://jans.io/oauth/config/saml-client-scope.write https://jans.io/idp/config.readonly https://jans.io/idp/config.write https://jans.io/idp/realm.readonly https://jans.io/idp/realm.write https://jans.io/idp/realm.write https://jans.io/idp/saml.readonly https://jans.io/idp/saml.write https://jans.io/oauth/config/app-version.readonly https://jans.io/oauth/kc-link-config.readonly https://jans.io/oauth/kc-link-config.write https://jans.io/oauth/lock-config.readonly https://jans.io/oauth/lock-config.write https://pujavs-definite-dory.gluu.info/jans-config-api/api/v1/jans-assets/upload-asset https://jans.io/oauth/config/jans_asset-write https://jans.io/oauth/config/jans_asset-delete +test.scopes=https://jans.io/oauth/config/acrs.readonly https://jans.io/oauth/config/acrs.write https://jans.io/oauth/config/attributes.readonly https://jans.io/oauth/config/attributes.write https://jans.io/oauth/config/attributes.delete https://jans.io/oauth/config/cache.readonly https://jans.io/oauth/config/cache.write https://jans.io/oauth/config/openid/clients.readonly https://jans.io/oauth/config/openid/clients.write https://jans.io/oauth/config/openid/clients.delete https://jans.io/oauth/jans-auth-server/config/properties.readonly https://jans.io/oauth/jans-auth-server/config/properties.write https://jans.io/oauth/config/smtp.readonly https://jans.io/oauth/config/smtp.write https://jans.io/oauth/config/smtp.delete https://jans.io/oauth/config/scripts.readonly https://jans.io/oauth/config/scripts.write https://jans.io/oauth/config/scripts.delete https://jans.io/oauth/config/fido2.readonly https://jans.io/oauth/config/fido2.write https://jans.io/oauth/config/jwks.readonly https://jans.io/oauth/config/jwks.write https://jans.io/oauth/config/jwks.delete https://jans.io/oauth/config/database/ldap.readonly https://jans.io/oauth/config/database/ldap.write https://jans.io/oauth/config/database/ldap.delete https://jans.io/oauth/config/logging.readonly https://jans.io/oauth/config/logging.write https://jans.io/oauth/config/scopes.readonly https://jans.io/oauth/config/scopes.write https://jans.io/oauth/config/scopes.delete https://jans.io/oauth/config/uma/resources.readonly https://jans.io/oauth/config/uma/resources.write https://jans.io/oauth/config/uma/resources.delete https://jans.io/oauth/config/database/sql.readonly https://jans.io/oauth/config/database/sql.write https://jans.io/oauth/config/database/sql.delete https://jans.io/oauth/config/stats.readonly jans_stat https://jans.io/scim/users.read https://jans.io/scim/users.write https://jans.io/oauth/config/scim/users.read https://jans.io/oauth/config/scim/users.write https://jans.io/scim/config.readonly https://jans.io/scim/config.write https://jans.io/oauth/config/organization.readonly https://jans.io/oauth/config/organization.write https://jans.io/oauth/config/user.readonly https://jans.io/oauth/config/user.write https://jans.io/oauth/config/user.delete https://jans.io/oauth/config/agama.readonly https://jans.io/oauth/config/agama.write https://jans.io/oauth/config/agama.delete https://jans.io/oauth/jans-auth-server/session.readonly https://jans.io/oauth/jans-auth-server/session.delete revoke_session https://jans.io/oauth/config/read-all https://jans.io/oauth/config/write-all https://jans.io/oauth/config/delete-all https://jans.io/oauth/config/openid-read https://jans.io/oauth/config/openid-write https://jans.io/oauth/config/openid-delete https://jans.io/oauth/config/uma-read https://jans.io/oauth/config/uma-write https://jans.io/oauth/config/uma-delete https://jans.io/oauth/jans-auth-server/config/adminui/user/role.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/role.write https://jans.io/oauth/jans-auth-server/config/adminui/read-all https://jans.io/oauth/jans-auth-server/config/adminui/write-all https://jans.io/oauth/jans-auth-server/config/adminui/user/role.delete https://jans.io/oauth/jans-auth-server/config/adminui/delete-all https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.write https://jans.io/oauth/jans-auth-server/config/adminui/user/permission.delete https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.readonly https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.write https://jans.io/oauth/jans-auth-server/config/adminui/user/rolePermissionMapping.delete https://jans.io/oauth/jans-auth-server/config/adminui/license.readonly https://jans.io/oauth/jans-auth-server/config/adminui/license.write https://jans.io/oauth/config/plugin.readonly https://jans.io/oauth/client/authorizations.readonly https://jans.io/oauth/client/authorizations.delete https://jans.io/oauth/config/cacherefresh.readonly https://jans.io/oauth/config/cacherefresh.write https://jans.io/oauth/config/saml.readonly https://jans.io/oauth/config/saml.write https://jans.io/oauth/config/saml-config.readonly https://jans.io/oauth/config/saml-config.write https://jans.io/oauth/config/saml-client-scope.readonly https://jans.io/oauth/config/saml-client-scope.write https://jans.io/idp/config.readonly https://jans.io/idp/config.write https://jans.io/idp/realm.readonly https://jans.io/idp/realm.write https://jans.io/idp/realm.write https://jans.io/idp/saml.readonly https://jans.io/idp/saml.write https://jans.io/oauth/config/app-version.readonly https://jans.io/oauth/kc-link-config.readonly https://jans.io/oauth/kc-link-config.write https://jans.io/oauth/lock-config.readonly https://jans.io/oauth/lock-config.write https://pujavs-definite-dory.gluu.info/jans-config-api/api/v1/jans-assets/upload-asset https://jans.io/oauth/config/jans_asset-write https://jans.io/oauth/config/jans_asset-delete https://jans.io/oauth/lock/read-all https://jans.io/oauth/lock/write-all https://jans.io/oauth/lock-config.readonly https://jans.io/oauth/lock-config.write https://jans.io/oauth/lock/audit.readonly https://jans.io/oauth/lock/audit.write https://jans.io/oauth/lock/health.readonly https://jans.io/oauth/lock/health.write https://jans.io/oauth/lock/log.readonly https://jans.io/oauth/lock/log.write https://jans.io/oauth/lock/telemetry.readonly https://jans.io/oauth/lock/telemetry.write # jans.server token.endpoint=https://jans.server3/jans-auth/restv1/token diff --git a/jans-config-api/server/pom.xml b/jans-config-api/server/pom.xml index df51e1ccb0b..04768957cad 100644 --- a/jans-config-api/server/pom.xml +++ b/jans-config-api/server/pom.xml @@ -314,6 +314,7 @@ io.jans.configapi.rest + diff --git a/jans-config-api/server/src/main/resources/config-api-rs-protect.json b/jans-config-api/server/src/main/resources/config-api-rs-protect.json index 62b6a94c1c4..73228b34040 100644 --- a/jans-config-api/server/src/main/resources/config-api-rs-protect.json +++ b/jans-config-api/server/src/main/resources/config-api-rs-protect.json @@ -2937,6 +2937,190 @@ ] } ] + }, + { + "path": "/jans-config-api/lock/audit", + "conditions": [ + { + "httpMethods": [ + "GET" + ], + "scopes": [ + { + "inum": "1800.01.78", + "name": "https://jans.io/oauth/lock/audit.readonly" + } + ], + "groupScopes": [ + { + "inum": "1800.01.79", + "name": "https://jans.io/oauth/lock/audit.write" + } + ], + "superScopes": [ + { + "inum": "1800.03.4", + "name": "https://jans.io/oauth/lock/read-all" + } + ] + }, + { + "httpMethods": [ + "POST" + ], + "scopes": [ + { + "inum": "1800.01.79", + "name": "https://jans.io/oauth/lock/audit.write" + } + ], + "groupScopes": [], + "superScopes": [ + { + "inum": "1800.03.5", + "name": "https://jans.io/oauth/lock/write-all" + } + ] + } + ] + }, + { + "path": "/jans-config-api/lock/audit/health", + "conditions": [ + { + "httpMethods": [ + "GET" + ], + "scopes": [ + { + "inum": "1800.01.80", + "name": "https://jans.io/oauth/lock/health.readonly" + } + ], + "groupScopes": [ + { + "inum": "1800.01.81", + "name": "https://jans.io/oauth/lock/health.write" + } + ], + "superScopes": [ + { + "inum": "1800.03.6", + "name": "https://jans.io/oauth/lock/audit.readonly" + } + ] + }, + { + "httpMethods": [ + "POST" + ], + "scopes": [ + { + "inum": "1800.01.81", + "name": "https://jans.io/oauth/lock/health.write" + } + ], + "groupScopes": [], + "superScopes": [ + { + "inum": "1800.03.7", + "name": "https://jans.io/oauth/lock/audit.write" + } + ] + } + ] + }, + { + "path": "/jans-config-api/lock/audit/log", + "conditions": [ + { + "httpMethods": [ + "GET" + ], + "scopes": [ + { + "inum": "1800.01.82", + "name": "https://jans.io/oauth/lock/log.readonly" + } + ], + "groupScopes": [ + { + "inum": "1800.01.83", + "name": "https://jans.io/oauth/lock/log.write" + } + ], + "superScopes": [ + { + "inum": "1800.03.6", + "name": "https://jans.io/oauth/lock/audit.readonly" + } + ] + }, + { + "httpMethods": [ + "POST" + ], + "scopes": [ + { + "inum": "1800.01.83", + "name": "https://jans.io/oauth/lock/log.write" + } + ], + "groupScopes": [], + "superScopes": [ + { + "inum": "1800.03.7", + "name": "https://jans.io/oauth/lock/audit.write" + } + ] + } + ] + }, + { + "path": "/jans-config-api/lock/audit/telemetry", + "conditions": [ + { + "httpMethods": [ + "GET" + ], + "scopes": [ + { + "inum": "1800.01.84", + "name": "https://jans.io/oauth/lock/telemetry.readonly" + } + ], + "groupScopes": [ + { + "inum": "1800.01.85", + "name": "https://jans.io/oauth/lock/telemetry.write" + } + ], + "superScopes": [ + { + "inum": "1800.03.6", + "name": "https://jans.io/oauth/lock/audit.readonly" + } + ] + }, + { + "httpMethods": [ + "POST" + ], + "scopes": [ + { + "inum": "1800.01.85", + "name": "https://jans.io/oauth/lock/telemetry.write" + } + ], + "groupScopes": [], + "superScopes": [ + { + "inum": "1800.03.7", + "name": "https://jans.io/oauth/lock/audit.write" + } + ] + } + ] } ] } \ No newline at end of file diff --git a/jans-core/service/src/main/java/io/jans/service/net/BaseHttpService.java b/jans-core/service/src/main/java/io/jans/service/net/BaseHttpService.java index 8c800f40405..7d6e2a6b950 100644 --- a/jans-core/service/src/main/java/io/jans/service/net/BaseHttpService.java +++ b/jans-core/service/src/main/java/io/jans/service/net/BaseHttpService.java @@ -55,6 +55,8 @@ import jakarta.annotation.PostConstruct; import jakarta.inject.Inject; import jakarta.servlet.http.HttpServletRequest; +import org.apache.commons.lang.StringUtils; +import org.json.JSONObject; /** * Provides operations with http/https requests @@ -144,10 +146,18 @@ public CloseableHttpClient getHttpsClient(String trustStoreType, String trustSto .setConnectionManager(connectionManager).build(); } - public HttpServiceResponse executePost(HttpClient httpClient, String uri, String authData, Map headers, String postData, ContentType contentType) { + public HttpServiceResponse executePost(HttpClient httpClient, String uri, String authData, Map headers, String postData, ContentType contentType, String authType) { + HttpPost httpPost = new HttpPost(uri); + + if(StringHelper.isEmpty(authType)) { + authType = "Basic "; + } + else { + authType = authType +" "; + } if (StringHelper.isNotEmpty(authData)) { - httpPost.setHeader("Authorization", "Basic " + authData); + httpPost.setHeader("Authorization", authType + authData); } if (headers != null) { @@ -171,11 +181,15 @@ public HttpServiceResponse executePost(HttpClient httpClient, String uri, String } public HttpServiceResponse executePost(HttpClient httpClient, String uri, String authData, Map headers, String postData) { - return executePost(httpClient, uri, authData, headers, postData, null); + return executePost(httpClient, uri, authData, headers, postData, null, null); } public HttpServiceResponse executePost(HttpClient httpClient, String uri, String authData, String postData, ContentType contentType) { - return executePost(httpClient, uri, authData, null, postData, contentType); + return executePost(httpClient, uri, authData, null, postData, contentType, null); + } + + public HttpServiceResponse executePost(String uri, String authData, Map headers, String postData, ContentType contentType, String authType) { + return executePost(this.getHttpsClient(), uri, authData, null, postData, contentType, authType); } public String encodeBase64(String value) { diff --git a/jans-linux-setup/jans_setup/schema/jans_schema.json b/jans-linux-setup/jans_setup/schema/jans_schema.json index a251e5e8d59..da7e73d0eb7 100644 --- a/jans-linux-setup/jans_setup/schema/jans_schema.json +++ b/jans-linux-setup/jans_setup/schema/jans_schema.json @@ -3911,6 +3911,203 @@ "substr": "caseIgnoreSubstringsMatch", "syntax": "1.3.6.1.4.1.1466.115.121.1.15", "x_origin": "Jans created attribute" + }, + { + "desc": "Policy stats details", + "equality": "caseIgnoreMatch", + "names": [ + "policyStats" + ], + "json": true, + "rdbm_json_column": true, + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" + }, + { + "desc": "Request load size", + "equality": "integerMatch", + "names": [ + "size" + ], + "oid": "jansAttr", + "syntax": "1.3.6.1.4.1.1466.115.121.1.27", + "x_origin": "Jans created attribute" + }, + { + "desc": "jansFailCounter", + "equality": "integerMatch", + "names": [ + "jansFailCounter" + ], + "oid": "jansAttr", + "syntax": "1.3.6.1.4.1.1466.115.121.1.27", + "x_origin": "Jans created attribute" + }, + { + "desc": "Time information. Seconds from 1970-01-01T0:0:0Z", + "equality": "generalizedTimeMatch", + "names": [ + "evaluationTimeNs" + ], + "oid": "jansAttr", + "ordering": "generalizedTimeOrderingMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.24", + "x_origin": "OpenID Connect 1.0 Standard Claim" + }, + { + "desc": "Time information. Seconds from 1970-01-01T0:0:0Z", + "equality": "generalizedTimeMatch", + "names": [ + "averageTimeNs" + ], + "oid": "jansAttr", + "ordering": "generalizedTimeOrderingMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.24", + "x_origin": "OpenID Connect 1.0 Standard Claim" + }, + { + "desc": "memoryUsage", + "equality": "caseIgnoreMatch", + "names": [ + "memoryUsage" + ], + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" + }, + { + "desc": "requestCounter", + "equality": "integerMatch", + "names": [ + "requestCounter" + ], + "oid": "jansAttr", + "syntax": "1.3.6.1.4.1.1466.115.121.1.27", + "x_origin": "Jans created attribute" + }, + { + "desc": "cedarEngineStatus", + "equality": "caseIgnoreMatch", + "names": [ + "cedarEngineStatus" + ], + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" + }, + { + "desc": "cedarPolicyStatus", + "equality": "caseIgnoreMatch", + "names": [ + "cedarPolicyStatus" + ], + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" + }, + { + "desc": "tokenDataStatus", + "equality": "caseIgnoreMatch", + "names": [ + "tokenDataStatus" + ], + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" + }, + { + "desc": "eventTime", + "equality": "generalizedTimeMatch", + "names": [ + "eventTime" + ], + "oid": "jansAttr", + "ordering": "generalizedTimeOrderingMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.24", + "x_origin": "Jans created attribute" + }, + { + "desc": "eventType", + "equality": "caseIgnoreMatch", + "names": [ + "eventType" + ], + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" + }, + { + "desc": "severetyLevel", + "equality": "caseIgnoreMatch", + "names": [ + "severetyLevel" + ], + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" + }, + { + "desc": "policyId", + "equality": "caseIgnoreMatch", + "names": [ + "policyId" + ], + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" + }, + { + "desc": "policyResult", + "equality": "caseIgnoreMatch", + "names": [ + "policyResult" + ], + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" + }, + { + "desc": "userAccountId", + "equality": "caseIgnoreMatch", + "names": [ + "userAccountId" + ], + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" + }, + { + "desc": "clientId", + "equality": "caseIgnoreMatch", + "names": [ + "clientId" + ], + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" + }, + { + "desc": "sourceInformation", + "equality": "caseIgnoreMatch", + "names": [ + "sourceInformation" + ], + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" } ], "objectClasses": [ @@ -5226,6 +5423,81 @@ ], "x_origin": "Jans created objectclass" }, + { + "kind": "STRUCTURAL", + "may": [ + "inum", + "jansLastUpd", + "jansStatus", + "cedarEngineStatus", + "cedarPolicyStatus", + "tokenDataStatus" + ], + "must": [ + "objectclass" + ], + "names": [ + "jansHealthEntry" + ], + "oid": "jansObjClass", + "sup": [ + "top" + ], + "x_origin": "Jans created objectclass" + }, + { + "kind": "STRUCTURAL", + "may": [ + "inum", + "jansLastUpd", + "eventTime", + "eventType ", + "severetyLevel", + "policyId", + "policyResult", + "userAccountId", + "clientId", + "sourceInformation" + ], + "must": [ + "objectclass" + ], + "names": [ + "jansLogEntry" + ], + "oid": "jansObjClass", + "sup": [ + "top" + ], + "x_origin": "Jans created objectclass" + }, + { + "kind": "STRUCTURAL", + "may": [ + "inum", + "jansLastUpd", + "size", + "jansStatus", + "jansCounter", + "jansFailCounter", + "evaluationTimeNs", + "averageTimeNs", + "memoryUsage", + "requestCounter", + "policyStats" + ], + "must": [ + "objectclass" + ], + "names": [ + "jansTelemetryEntry" + ], + "oid": "jansObjClass", + "sup": [ + "top" + ], + "x_origin": "Jans created objectclass" + }, { "kind": "STRUCTURAL", "may": [ @@ -5323,4 +5595,4 @@ "jansReserved": "jansOrgOID:0", "jansSyntax": "jansPublished:1" } -} +} \ No newline at end of file diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans_lock.py b/jans-linux-setup/jans_setup/setup_app/installers/jans_lock.py index c7e7a8b5a4e..df7b3a994a8 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans_lock.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans_lock.py @@ -1,6 +1,7 @@ import os import glob import shutil +import ruamel.yaml from pathlib import Path from setup_app import paths @@ -8,7 +9,7 @@ from setup_app.static import AppType, InstallOption from setup_app.config import Config from setup_app.installers.jetty import JettyInstaller -from setup_app.utils.ldif_utils import myLdifParser +from setup_app.utils.ldif_utils import myLdifParser, create_client_ldif Config.jans_lock_port = '8076' Config.jans_opa_host = 'localhost' @@ -50,6 +51,7 @@ def __init__(self): self.opa_bin_dir = os.path.join(self.opa_dir, 'bin') self.opa_log_dir = os.path.join(self.opa_dir, 'logs') self.base_endpoint = 'jans-lock' if Config.get('install_jans_lock_as_server') else 'jans-auth' + self.clients_ldif_fn = os.path.join(self.output_dir, 'clients.ldif') def install(self): if Config.get('install_jans_lock_as_server'): @@ -61,6 +63,8 @@ def install(self): if Config.get('install_opa'): self.install_opa() + self.create_client() + if Config.persistence_type == 'sql' and Config.rdbm_type == 'pgsql': self.dbUtils.set_jans_auth_conf_dynamic({'lockMessageConfig': {'enableTokenMessages': True, 'tokenMessagesChannel': 'jans_token'}}) Config.lock_message_provider_type = 'POSTGRES' @@ -73,6 +77,39 @@ def install(self): self.apache_lock_config() + def create_client(self): + + _, jans_auth_config = self.dbUtils.get_jans_auth_conf_dynamic() + Config.templateRenderingDict['jans_auth_token_endpoint'] = jans_auth_config['tokenEndpoint'] + + jans_scopes = self.dbUtils.get_scopes() + scope_openid = self.dbUtils.get_scope_by_jansid('openid') + scopes = [ scope_openid['dn'] ] + + swagger_yaml_fn = base.extract_file(base.current_app.jans_zip, 'jans-config-api/plugins/docs/lock-plugin-swagger.yaml', Config.data_dir) + swagger_yaml_str = self.readFile(swagger_yaml_fn) + swagger_yml = ruamel.yaml.load(swagger_yaml_str, ruamel.yaml.RoundTripLoader) + config_scopes = swagger_yml['components']['securitySchemes']['oauth2']['flows']['clientCredentials']['scopes'] + + for config_scope_id in config_scopes: + config_scope = self.dbUtils.get_scope_by_jansid(config_scope_id) + if config_scope: + scopes.append(config_scope['dn']) + + lock_client_prefix = '2200.' + check_result = self.check_clients([('lock_client_id', lock_client_prefix)]) + if check_result.get(lock_client_prefix) == -1: + create_client_ldif( + ldif_fn=self.clients_ldif_fn, + client_id=Config.lock_client_id, + encoded_pw=Config.lock_client_encoded_pw, + scopes=scopes, + redirect_uri=[f'https://{Config.hostname}/jans-lock'], + display_name="Jans Lock Config Api Client" + ) + + self.dbUtils.import_ldif([self.clients_ldif_fn]) + def install_as_server(self): self.install_jettyService(self.jetty_app_configuration[self.service_name], True) self.logIt(f"Copying {self.source_files[0][0]} into jetty webapps folder...") diff --git a/jans-linux-setup/jans_setup/setup_app/utils/base.py b/jans-linux-setup/jans_setup/setup_app/utils/base.py index c35f87f5583..6676292e559 100644 --- a/jans-linux-setup/jans_setup/setup_app/utils/base.py +++ b/jans-linux-setup/jans_setup/setup_app/utils/base.py @@ -365,7 +365,9 @@ def mylog(logs): urllib.request.install_opener(None) def extract_file(zip_file, source, target, ren=False): + fn = None zip_obj = zipfile.ZipFile(zip_file, "r") + for member in zip_obj.infolist(): if not member.is_dir() and member.filename.endswith(source): if ren: @@ -377,9 +379,13 @@ def extract_file(zip_file, source, target, ren=False): target_p.parent.mkdir(parents=True) logIt(f"Extracting {source} from {zip_file} to {target}") target_p.write_bytes(zip_obj.read(member)) + fn = target_p.as_posix() break + zip_obj.close() + return fn + def extract_from_zip(zip_file, sub_dir, target_dir, remove_target_dir=False): zipobj = zipfile.ZipFile(zip_file, "r") diff --git a/jans-linux-setup/jans_setup/setup_app/utils/db_utils.py b/jans-linux-setup/jans_setup/setup_app/utils/db_utils.py index 2e52ca08613..77e3d89f575 100644 --- a/jans-linux-setup/jans_setup/setup_app/utils/db_utils.py +++ b/jans-linux-setup/jans_setup/setup_app/utils/db_utils.py @@ -43,7 +43,7 @@ class DBUtils: cbm = None mariadb = False rdbm_json_types = MappingProxyType({ 'mysql': {'type': 'JSON'}, 'pgsql': {'type': 'JSONB'}, 'spanner': {'type': 'ARRAY'} }) - + jans_scopes = None def bind(self, use_ssl=True, force=False): @@ -1252,6 +1252,24 @@ def checkCBRoles(self, buckets=[]): return True, None + def get_scopes(self): + result = self.search( + search_base='ou=scopes,o=jans', + search_filter='(objectClass=jansScope)', + search_scope=ldap3.LEVEL, + fetchmany=True) + sopes = [ sope for _, sope in result ] + + return sopes + + def get_scope_by_jansid(self, jansid): + if not self.jans_scopes: + self.jans_scopes = self.get_scopes() + + for jans_scope in self.jans_scopes: + if jans_scope['jansId'] == jansid: + return jans_scope + def __del__(self): try: self.ldap_conn.unbind() diff --git a/jans-linux-setup/jans_setup/templates/base.ldif b/jans-linux-setup/jans_setup/templates/base.ldif index fdf0b9009dd..80dbddc5469 100644 --- a/jans-linux-setup/jans_setup/templates/base.ldif +++ b/jans-linux-setup/jans_setup/templates/base.ldif @@ -162,3 +162,18 @@ dn: ou=document,o=jans objectClass: top objectClass: organizationalUnit ou: document + +dn: ou=lock-health,o=jans +objectClass: top +objectClass: organizationalUnit +ou: lock-health + +dn: ou=lock-log,o=jans +objectClass: top +objectClass: organizationalUnit +ou: lock-log + +dn: ou=lock-telemetry,o=jans +objectClass: top +objectClass: organizationalUnit +ou: lock-telemetry diff --git a/jans-linux-setup/jans_setup/templates/jans-lock/dynamic-conf.json b/jans-linux-setup/jans_setup/templates/jans-lock/dynamic-conf.json index 9be1bf134bb..7f3f4d15a6b 100644 --- a/jans-linux-setup/jans_setup/templates/jans-lock/dynamic-conf.json +++ b/jans-linux-setup/jans_setup/templates/jans-lock/dynamic-conf.json @@ -1,37 +1,42 @@ { - "baseEndpoint" : "https://%(hostname)s/%(base_endpoint)s/v1", - "openIdIssuer":"https://%(hostname)s", - - "tokenChannels":[ - "jans_token" - ], - - "cleanServiceInterval":60, - "cleanServiceBatchChunkSize":10000, - - "disableJdkLogger":true, - - "loggingLevel":"INFO", - "loggingLayout":"text", - "externalLoggerConfiguration":"", - - "metricReporterInterval":300, - "metricReporterKeepDataDays":15, - "metricReporterEnabled":true, - "errorReasonEnabled": false, - - "opaConfiguration": { - "baseUrl" : "http://%(jans_opa_host)s:%(jans_opa_port)s/v1/", - "accessToken" : "" - }, - - "policiesJsonUris": [ - ], - "policiesJsonUrisAuthorizationToken" : "", - - "policiesZipUris": [ - ], - "policiesZipUrisAuthorizationToken" : "", - - "pdpType": "OPA" + "baseEndpoint": "https://%(hostname)s/%(base_endpoint)s/v1", + "openIdIssuer": "https://%(hostname)s", + "tokenChannels": [ + "jans_token" + ], + "clientId": "%(lock_client_id)s", + "clientPassword": "%(lock_client_encoded_pw)s", + "tokenUrl": "%(tokenEndpoint)s", + "endpointDetails": { + "jans-config-api/lock/audit/telemetry": [ + "https://jans.io/oauth/lock/telemetry.readonly", + "https://jans.io/oauth/lock/telemetry.write" + ], + "jans-config-api/lock/audit/log": [ + "https://jans.io/oauth/lock/log.write" + ], + "jans-config-api/lock/audit/health": [ + "https://jans.io/oauth/lock/health.readonly", + "https://jans.io/oauth/lock/health.write" + ] + }, + "cleanServiceInterval": 60, + "cleanServiceBatchChunkSize": 10000, + "disableJdkLogger": true, + "loggingLevel": "INFO", + "loggingLayout": "text", + "externalLoggerConfiguration": "", + "metricReporterInterval": 300, + "metricReporterKeepDataDays": 15, + "metricReporterEnabled": true, + "errorReasonEnabled": false, + "opaConfiguration": { + "baseUrl": "http://%(jans_opa_host)s:%(jans_opa_port)s/v1/", + "accessToken": "" + }, + "policiesJsonUris": [], + "policiesJsonUrisAuthorizationToken": "", + "policiesZipUris": [], + "policiesZipUrisAuthorizationToken": "", + "pdpType": "OPA" } diff --git a/jans-lock/lock-master/model/src/main/java/io/jans/lock/model/config/AppConfiguration.java b/jans-lock/lock-master/model/src/main/java/io/jans/lock/model/config/AppConfiguration.java index 017dde58570..b94b4e76606 100644 --- a/jans-lock/lock-master/model/src/main/java/io/jans/lock/model/config/AppConfiguration.java +++ b/jans-lock/lock-master/model/src/main/java/io/jans/lock/model/config/AppConfiguration.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -34,7 +35,9 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class AppConfiguration implements Configuration { - private String baseDN; + @DocProperty(description = "Entry Base distinguished name (DN) that identifies the starting point of a search") + @Schema(description = "Entry Base distinguished name (DN) that identifies the starting point of a search") + private String baseDN; @DocProperty(description = "Lock base endpoint URL") @Schema(description = "Lock base endpoint URL") @@ -46,15 +49,31 @@ public class AppConfiguration implements Configuration { @DocProperty(description = "List of token channel names", defaultValue = "jans_token") @Schema(description = "List of token channel names") - private List tokenChannels; + private List tokenChannels; - @DocProperty(description = "Choose whether to disable JDK loggers", defaultValue = "true") - @Schema(description = "Choose whether to disable JDK loggers") - private Boolean disableJdkLogger = true; + @DocProperty(description = "Lock Client ID") + @Schema(description = "Lock Client ID") + private String clientId; + + @DocProperty(description = "Lock client password") + @Schema(description = "Lock client password") + private String clientPassword; + + @DocProperty(description = "Jans URL of the OpenID Connect Provider's OAuth 2.0 Token Endpoint") + @Schema(description = "Jans URL of the OpenID Connect Provider's OAuth 2.0 Token Endpoint") + private String tokenUrl; + + @DocProperty(description = "Jans URL of config-api audit endpoints and corresponding scope details") + @Schema(description = "Jans URL of config-api audit endpoints and corresponding scope details") + private Map> endpointDetails; + + @DocProperty(description = "Choose whether to disable JDK loggers", defaultValue = "true") + @Schema(description = "Choose whether to disable JDK loggers") + private Boolean disableJdkLogger = true; @DocProperty(description = "Specify the logging level of loggers") @Schema(description = "Specify the logging level of loggers") - private String loggingLevel; + private String loggingLevel; @DocProperty(description = "Logging layout used for Jans Authorization Server loggers") @Schema(description = "Logging layout used for Jans Authorization Server loggers") @@ -66,202 +85,245 @@ public class AppConfiguration implements Configuration { @DocProperty(description = "Channel for metric reports", defaultValue = "jans_pdp_metric") @Schema(description = "Channel for metric reports") - private String metricChannel; + private String metricChannel; @DocProperty(description = "The interval for metric reporter in seconds") @Schema(description = "The interval for metric reporter in seconds") - private int metricReporterInterval; + private int metricReporterInterval; @DocProperty(description = "The days to keep metric reported data") @Schema(description = "The days to keep metric reported data") - private int metricReporterKeepDataDays; + private int metricReporterKeepDataDays; @DocProperty(description = "Enable metric reporter") @Schema(description = "Enable metric reporter") - private Boolean metricReporterEnabled; + private Boolean metricReporterEnabled; - // Period in seconds + // Period in seconds @DocProperty(description = "Time interval for the Clean Service in seconds") @Schema(description = "Time interval for the Clean Service in seconds") - private int cleanServiceInterval; - + private int cleanServiceInterval; + @Schema(description = "Opa Configuration") - private OpaConfiguration opaConfiguration; + private OpaConfiguration opaConfiguration; @DocProperty(description = "PDP type") @Schema(description = "PDP type") - private String pdpType; + private String pdpType; @DocProperty(description = "Authorization token to access Json Uris") @Schema(description = "Authorization token to access Json Uris") - private String policiesJsonUrisAuthorizationToken; + private String policiesJsonUrisAuthorizationToken; @DocProperty(description = "List of Json Uris with link to Rego policies") @Schema(description = "List of Json Uris with link to Rego policies") - private List policiesJsonUris; + private List policiesJsonUris; @DocProperty(description = "Authorization token to access Zip Uris") @Schema(description = "Authorization token to access Zip Uris") - private String policiesZipUrisAuthorizationToken; + private String policiesZipUrisAuthorizationToken; @DocProperty(description = "List of Zip Uris with policies") @Schema(description = "List of Zip Uris with policies") - private List policiesZipUris; - - public String getBaseDN() { - return baseDN; - } - - public void setBaseDN(String baseDN) { - this.baseDN = baseDN; - } - - public String getBaseEndpoint() { - return baseEndpoint; - } - - public void setBaseEndpoint(String baseEndpoint) { - this.baseEndpoint = baseEndpoint; - } - - public String getOpenIdIssuer() { - return openIdIssuer; - } - - public void setOpenIdIssuer(String openIdIssuer) { - this.openIdIssuer = openIdIssuer; - } - - public List getTokenChannels() { - if (tokenChannels == null) { - tokenChannels = new ArrayList<>(); - } - - return tokenChannels; - } - - public void setTokenChannels(List tokenChannels) { - this.tokenChannels = tokenChannels; - } - - public Boolean getDisableJdkLogger() { - return disableJdkLogger; - } - - public void setDisableJdkLogger(Boolean disableJdkLogger) { - this.disableJdkLogger = disableJdkLogger; - } - - public String getLoggingLevel() { - return loggingLevel; - } - - public void setLoggingLevel(String loggingLevel) { - this.loggingLevel = loggingLevel; - } - - public String getLoggingLayout() { - return loggingLayout; - } - - public void setLoggingLayout(String loggingLayout) { - this.loggingLayout = loggingLayout; - } - - public String getExternalLoggerConfiguration() { - return externalLoggerConfiguration; - } + private List policiesZipUris; - public void setExternalLoggerConfiguration(String externalLoggerConfiguration) { - this.externalLoggerConfiguration = externalLoggerConfiguration; - } + public String getBaseDN() { + return baseDN; + } - public String getMetricChannel() { - return metricChannel; - } + public void setBaseDN(String baseDN) { + this.baseDN = baseDN; + } - public void setMetricChannel(String metricChannel) { - this.metricChannel = metricChannel; - } + public String getBaseEndpoint() { + return baseEndpoint; + } - public int getMetricReporterInterval() { - return metricReporterInterval; - } + public void setBaseEndpoint(String baseEndpoint) { + this.baseEndpoint = baseEndpoint; + } - public void setMetricReporterInterval(int metricReporterInterval) { - this.metricReporterInterval = metricReporterInterval; - } + public String getOpenIdIssuer() { + return openIdIssuer; + } - public int getMetricReporterKeepDataDays() { - return metricReporterKeepDataDays; - } + public void setOpenIdIssuer(String openIdIssuer) { + this.openIdIssuer = openIdIssuer; + } - public void setMetricReporterKeepDataDays(int metricReporterKeepDataDays) { - this.metricReporterKeepDataDays = metricReporterKeepDataDays; - } + public List getTokenChannels() { + return tokenChannels; + } - public Boolean getMetricReporterEnabled() { - return metricReporterEnabled; - } + public void setTokenChannels(List tokenChannels) { + this.tokenChannels = tokenChannels; + } - public void setMetricReporterEnabled(Boolean metricReporterEnabled) { - this.metricReporterEnabled = metricReporterEnabled; - } + public String getClientId() { + return clientId; + } - public int getCleanServiceInterval() { - return cleanServiceInterval; - } + public void setClientId(String clientId) { + this.clientId = clientId; + } - public void setCleanServiceInterval(int cleanServiceInterval) { - this.cleanServiceInterval = cleanServiceInterval; - } + public String getClientPassword() { + return clientPassword; + } - public OpaConfiguration getOpaConfiguration() { - return opaConfiguration; - } + public void setClientPassword(String clientPassword) { + this.clientPassword = clientPassword; + } - public void setOpaConfiguration(OpaConfiguration opaConfiguration) { - this.opaConfiguration = opaConfiguration; - } + public String getTokenUrl() { + return tokenUrl; + } - public String getPdpType() { - return pdpType; - } + public void setTokenUrl(String tokenUrl) { + this.tokenUrl = tokenUrl; + } - public void setPdpType(String pdpType) { - this.pdpType = pdpType; - } + public Map> getEndpointDetails() { + return endpointDetails; + } - public String getPoliciesJsonUrisAuthorizationToken() { - return policiesJsonUrisAuthorizationToken; - } + public void setEndpointDetails(Map> endpointDetails) { + this.endpointDetails = endpointDetails; + } - public void setPoliciesJsonUrisAuthorizationToken(String policiesJsonUrisAuthorizationToken) { - this.policiesJsonUrisAuthorizationToken = policiesJsonUrisAuthorizationToken; - } + public Boolean getDisableJdkLogger() { + return disableJdkLogger; + } - public List getPoliciesJsonUris() { - return policiesJsonUris; - } + public void setDisableJdkLogger(Boolean disableJdkLogger) { + this.disableJdkLogger = disableJdkLogger; + } - public void setPoliciesJsonUris(List policiesJsonUris) { - this.policiesJsonUris = policiesJsonUris; - } + public String getLoggingLevel() { + return loggingLevel; + } - public String getPoliciesZipUrisAuthorizationToken() { - return policiesZipUrisAuthorizationToken; - } + public void setLoggingLevel(String loggingLevel) { + this.loggingLevel = loggingLevel; + } - public void setPoliciesZipUrisAuthorizationToken(String policiesZipUrisAuthorizationToken) { - this.policiesZipUrisAuthorizationToken = policiesZipUrisAuthorizationToken; - } + public String getLoggingLayout() { + return loggingLayout; + } - public List getPoliciesZipUris() { - return policiesZipUris; - } + public void setLoggingLayout(String loggingLayout) { + this.loggingLayout = loggingLayout; + } - public void setPoliciesZipUris(List policiesZipUris) { - this.policiesZipUris = policiesZipUris; - } + public String getExternalLoggerConfiguration() { + return externalLoggerConfiguration; + } + + public void setExternalLoggerConfiguration(String externalLoggerConfiguration) { + this.externalLoggerConfiguration = externalLoggerConfiguration; + } + + public String getMetricChannel() { + return metricChannel; + } + + public void setMetricChannel(String metricChannel) { + this.metricChannel = metricChannel; + } + + public int getMetricReporterInterval() { + return metricReporterInterval; + } + + public void setMetricReporterInterval(int metricReporterInterval) { + this.metricReporterInterval = metricReporterInterval; + } + + public int getMetricReporterKeepDataDays() { + return metricReporterKeepDataDays; + } + + public void setMetricReporterKeepDataDays(int metricReporterKeepDataDays) { + this.metricReporterKeepDataDays = metricReporterKeepDataDays; + } + + public Boolean getMetricReporterEnabled() { + return metricReporterEnabled; + } + + public void setMetricReporterEnabled(Boolean metricReporterEnabled) { + this.metricReporterEnabled = metricReporterEnabled; + } + + public int getCleanServiceInterval() { + return cleanServiceInterval; + } + + public void setCleanServiceInterval(int cleanServiceInterval) { + this.cleanServiceInterval = cleanServiceInterval; + } + + public OpaConfiguration getOpaConfiguration() { + return opaConfiguration; + } + + public void setOpaConfiguration(OpaConfiguration opaConfiguration) { + this.opaConfiguration = opaConfiguration; + } + + public String getPdpType() { + return pdpType; + } + + public void setPdpType(String pdpType) { + this.pdpType = pdpType; + } + + public String getPoliciesJsonUrisAuthorizationToken() { + return policiesJsonUrisAuthorizationToken; + } + + public void setPoliciesJsonUrisAuthorizationToken(String policiesJsonUrisAuthorizationToken) { + this.policiesJsonUrisAuthorizationToken = policiesJsonUrisAuthorizationToken; + } + + public List getPoliciesJsonUris() { + return policiesJsonUris; + } + + public void setPoliciesJsonUris(List policiesJsonUris) { + this.policiesJsonUris = policiesJsonUris; + } + + public String getPoliciesZipUrisAuthorizationToken() { + return policiesZipUrisAuthorizationToken; + } + + public void setPoliciesZipUrisAuthorizationToken(String policiesZipUrisAuthorizationToken) { + this.policiesZipUrisAuthorizationToken = policiesZipUrisAuthorizationToken; + } + + public List getPoliciesZipUris() { + return policiesZipUris; + } + + public void setPoliciesZipUris(List policiesZipUris) { + this.policiesZipUris = policiesZipUris; + } + + @Override + public String toString() { + return "AppConfiguration [baseDN=" + baseDN + ", baseEndpoint=" + baseEndpoint + ", openIdIssuer=" + + openIdIssuer + ", tokenChannels=" + tokenChannels + ", clientId=" + clientId + ", clientPassword=" + + clientPassword + ", tokenUrl=" + tokenUrl + ", endpointDetails=" + endpointDetails + + ", disableJdkLogger=" + disableJdkLogger + ", loggingLevel=" + loggingLevel + ", loggingLayout=" + + loggingLayout + ", externalLoggerConfiguration=" + externalLoggerConfiguration + ", metricChannel=" + + metricChannel + ", metricReporterInterval=" + metricReporterInterval + ", metricReporterKeepDataDays=" + + metricReporterKeepDataDays + ", metricReporterEnabled=" + metricReporterEnabled + + ", cleanServiceInterval=" + cleanServiceInterval + ", opaConfiguration=" + opaConfiguration + + ", pdpType=" + pdpType + ", policiesJsonUrisAuthorizationToken=" + policiesJsonUrisAuthorizationToken + + ", policiesJsonUris=" + policiesJsonUris + ", policiesZipUrisAuthorizationToken=" + + policiesZipUrisAuthorizationToken + ", policiesZipUris=" + policiesZipUris + "]"; + } } diff --git a/jans-lock/lock-master/service/pom.xml b/jans-lock/lock-master/service/pom.xml index ee2d6b03ff5..82878af796b 100644 --- a/jans-lock/lock-master/service/pom.xml +++ b/jans-lock/lock-master/service/pom.xml @@ -63,6 +63,18 @@ io.jans jans-core-service + + + io.jans + jans-auth-client + ${project.version} + + + + io.jans + jans-auth-model + ${project.version} + io.jans diff --git a/jans-lock/lock-master/server/src/main/java/io/jans/lock/service/net/HttpService.java b/jans-lock/lock-master/service/src/main/java/io/jans/lock/service/net/HttpService.java similarity index 100% rename from jans-lock/lock-master/server/src/main/java/io/jans/lock/service/net/HttpService.java rename to jans-lock/lock-master/service/src/main/java/io/jans/lock/service/net/HttpService.java diff --git a/jans-lock/lock-master/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebServiceImpl.java b/jans-lock/lock-master/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebServiceImpl.java index 5c3c047184b..ca5748e143e 100644 --- a/jans-lock/lock-master/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebServiceImpl.java +++ b/jans-lock/lock-master/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebServiceImpl.java @@ -16,9 +16,9 @@ package io.jans.lock.service.ws.rs.audit; -import org.slf4j.Logger; - +import io.jans.lock.util.LockUtil; import io.jans.lock.util.ServerUtil; + import jakarta.enterprise.context.Dependent; import jakarta.inject.Inject; import jakarta.servlet.http.HttpServletRequest; @@ -26,6 +26,11 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.SecurityContext; +import jakarta.ws.rs.core.Response.Status; + +import org.apache.http.entity.ContentType; +import org.json.JSONObject; +import org.slf4j.Logger; /** * Provides interface for audit REST web services @@ -36,43 +41,64 @@ @Path("/audit") public class AuditRestWebServiceImpl implements AuditRestWebService { - @Inject - private Logger log; + @Inject + private Logger log; + + @Inject + LockUtil lockUtil; + + @Override + public Response processHealthRequest(HttpServletRequest request, HttpServletResponse response, + SecurityContext sec) { + log.debug("Processing Health request - request:{}", request); + return processAuditRequest(request, "Health"); + } + + @Override + public Response processLogRequest(HttpServletRequest request, HttpServletResponse response, SecurityContext sec) { + log.debug("Processing Log request - request:{}", request); + return processAuditRequest(request, "log"); + + } - @Override - public Response processHealthRequest(HttpServletRequest request, HttpServletResponse response, SecurityContext sec) { - log.debug("Processing Health request"); - Response.ResponseBuilder builder = Response.ok(); + @Override + public Response processTelemetryRequest(HttpServletRequest request, HttpServletResponse response, + SecurityContext sec) { + log.debug("Processing Telemetry request - request:{}", request); + return processAuditRequest(request, "telemetry"); - builder.cacheControl(ServerUtil.cacheControlWithNoStoreTransformAndPrivate()); - builder.header(ServerUtil.PRAGMA, ServerUtil.NO_CACHE); - builder.entity("{\"res\" : \"ok\"}"); + } - return builder.build(); - } + private Response processAuditRequest(HttpServletRequest request, String requestType) { + log.debug("Processing request - request:{}, requestType:{}", request, requestType); - @Override - public Response processLogRequest(HttpServletRequest request, HttpServletResponse response, SecurityContext sec) { - log.debug("Processing Log request"); - Response.ResponseBuilder builder = Response.ok(); + Response.ResponseBuilder builder = Response.ok(); + builder.cacheControl(ServerUtil.cacheControlWithNoStoreTransformAndPrivate()); + builder.header(ServerUtil.PRAGMA, ServerUtil.NO_CACHE); - builder.cacheControl(ServerUtil.cacheControlWithNoStoreTransformAndPrivate()); - builder.header(ServerUtil.PRAGMA, ServerUtil.NO_CACHE); - builder.entity("{\"res\" : \"ok\"}"); + JSONObject json = this.lockUtil.getJSONObject(request); + Response response = this.lockUtil.post(requestType, json.toString(), ContentType.APPLICATION_JSON); + log.debug("response:{}", response); - return builder.build(); - } + if (response != null) { + log.trace( + "Response for Access Token - response.getStatus():{}, response.getStatusInfo():{}, response.getEntity().getClass():{}", + response.getStatus(), response.getStatusInfo(), response.getEntity().getClass()); + String entity = response.readEntity(String.class); + log.debug(" entity:{}", entity); + builder.entity(entity); - @Override - public Response processTelemetryRequest(HttpServletRequest request, HttpServletResponse response, SecurityContext sec) { - log.debug("Processing Telemetry request"); - Response.ResponseBuilder builder = Response.ok(); + if (response.getStatusInfo().equals(Status.CREATED)) { - builder.cacheControl(ServerUtil.cacheControlWithNoStoreTransformAndPrivate()); - builder.header(ServerUtil.PRAGMA, ServerUtil.NO_CACHE); - builder.entity("{\"res\" : \"ok\"}"); + log.debug(" Status.CREATED:{}, entity:{}", Status.CREATED, entity); + } else { + log.error("Error while saving audit data - response.getStatusInfo():{}, entity:{}", + response.getStatusInfo(), entity); + builder.status(response.getStatusInfo()); + } + } - return builder.build(); - } + return builder.build(); + } } diff --git a/jans-lock/lock-master/service/src/main/java/io/jans/lock/util/LockUtil.java b/jans-lock/lock-master/service/src/main/java/io/jans/lock/util/LockUtil.java new file mode 100644 index 00000000000..6d9bbad4ae6 --- /dev/null +++ b/jans-lock/lock-master/service/src/main/java/io/jans/lock/util/LockUtil.java @@ -0,0 +1,346 @@ +package io.jans.lock.util; + +import io.jans.as.client.TokenRequest; +import io.jans.as.client.TokenResponse; +import io.jans.as.model.common.GrantType; +import io.jans.as.model.common.ScopeType; +import io.jans.lock.model.config.AppConfiguration; +import io.jans.lock.service.net.HttpService; +import io.jans.model.net.HttpServiceResponse; +import io.jans.service.EncryptionService; +import io.jans.util.security.StringEncrypter.EncryptionException; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation.Builder; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +import org.apache.http.entity.ContentType; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.util.EntityUtils; + +import org.json.JSONObject; +import org.slf4j.Logger; + +@ApplicationScoped +public class LockUtil { + + private static final String CONTENT_TYPE = "Content-Type"; + private static final String AUTHORIZATION = "Authorization"; + + @Inject + Logger log; + + @Inject + AppConfiguration appConfiguration; + + @Inject + HttpService httpService; + + @Inject + EncryptionService encryptionService; + + public String getToken(String endpoint) { + + log.debug("Request for token for endpoint:{}", endpoint); + String tokenUrl = this.appConfiguration.getTokenUrl(); + String clientId = this.appConfiguration.getClientId(); + + String clientSecret = this.getDecryptedPassword(appConfiguration.getClientPassword()); + String scopes = this.getScopes(endpoint); + + return this.getToken(tokenUrl, clientId, clientSecret, scopes); + } + + public String getToken(String tokenUrl, String clientId, String clientSecret, String scopes) { + log.debug("Request for token tokenUrl:{}, clientId:{},scopes:{}", tokenUrl, clientId, scopes); + + String accessToken = null; + TokenResponse tokenResponse = this.requestAccessToken(tokenUrl, clientId, clientSecret, scopes); + if (tokenResponse != null) { + accessToken = tokenResponse.getAccessToken(); + } + + return accessToken; + } + + public TokenResponse requestAccessToken(final String tokenUrl, final String clientId, final String clientSecret, + final String scope) { + + Response response = null; + try { + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope(scope); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + Builder request = getClientBuilder(tokenUrl); + request.header(AUTHORIZATION, "Basic " + tokenRequest.getEncodedCredentials()); + request.header(CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED); + final MultivaluedHashMap multivaluedHashMap = new MultivaluedHashMap<>( + tokenRequest.getParameters()); + response = request.post(Entity.form(multivaluedHashMap)); + log.trace("Response for Access Token - response:{}", response); + if (response.getStatus() == 200) { + String entity = response.readEntity(String.class); + TokenResponse tokenResponse = new TokenResponse(); + tokenResponse.setEntity(entity); + tokenResponse.injectDataFromJson(entity); + return tokenResponse; + } + } finally { + + if (response != null) { + response.close(); + } + } + return null; + } + + public HttpServiceResponse postData(String endpoint, String postData, ContentType contentType) { + log.debug("postData - endpoint:{}, postData:{}", endpoint, postData); + String endpointPath = this.getEndpointPath(endpoint); + String token = this.getToken(endpointPath); + + return postData(this.getEndpointUrl(endpointPath), null, token, null, contentType, postData); + } + + public HttpServiceResponse postData(String uri, String authType, String token, Map headers, + ContentType contentType, String postData) { + log.debug("postData - uri:{}, token:{}, data", uri, token); + + if (StringUtils.isBlank(authType)) { + authType = "Bearer "; + } + if (contentType == null) { + contentType = ContentType.APPLICATION_JSON; + } + + if (headers == null) { + headers = new HashMap<>(); + } + headers.put(CONTENT_TYPE, ContentType.APPLICATION_JSON.toString()); + headers.put(AUTHORIZATION, authType + token); + + HttpServiceResponse response = httpService.executePost(uri, token, headers, postData, contentType, authType); + + log.debug("response:{}", response); + return response; + } + + public String getResponseEntityString(HttpServiceResponse serviceResponse, Status status) { + String jsonString = null; + + if (serviceResponse == null) { + return jsonString; + } + + if (serviceResponse.getHttpResponse() != null && serviceResponse.getHttpResponse().getStatusLine() != null + && serviceResponse.getHttpResponse().getStatusLine().getStatusCode() == status.getStatusCode()) { + HttpEntity entity = serviceResponse.getHttpResponse().getEntity(); + if (entity == null) { + return jsonString; + } + jsonString = entity.toString(); + + } + return jsonString; + } + + public String getResponseEntityString(HttpServiceResponse serviceResponse) { + String jsonString = null; + + if (serviceResponse == null || serviceResponse.getHttpResponse() == null) { + return jsonString; + } + + HttpEntity entity = serviceResponse.getHttpResponse().getEntity(); + if (entity == null) { + return jsonString; + } + jsonString = entity.toString(); + + try { + log.debug("serviceResponse.getHttpResponse().getEntity():{}", + serviceResponse.getHttpResponse().getEntity()); + String responseMsg = EntityUtils.toString(serviceResponse.getHttpResponse().getEntity(), "UTF-8"); + log.debug("New responseMsg:{}", responseMsg); + } catch (Exception ex) { + log.error("Error while getting entity using EntityUtils is ", ex); + } + return jsonString; + } + + public JSONObject getResponseJson(HttpServiceResponse serviceResponse) { + JSONObject jsonObj = null; + if (serviceResponse == null || serviceResponse.getHttpResponse() == null) { + return jsonObj; + } + + HttpResponse httpResponse = serviceResponse.getHttpResponse(); + if (httpResponse != null && httpResponse.getEntity() != null) { + jsonObj = new JSONObject(httpResponse.getEntity()); + log.debug("getResponseJson() - .jsonObj:{}", jsonObj); + } + + return jsonObj; + } + + public Status getResponseStatus(HttpServiceResponse serviceResponse) { + Status status = Status.INTERNAL_SERVER_ERROR; + + if (serviceResponse == null || serviceResponse.getHttpResponse() == null) { + return status; + } + + int statusCode = serviceResponse.getHttpResponse().getStatusLine().getStatusCode(); + + status = Status.fromStatusCode(statusCode); + if (status == null) { + status = Status.INTERNAL_SERVER_ERROR; + } + return status; + } + + public JSONObject getJSONObject(HttpServletRequest request) { + JSONObject jsonBody = null; + if (request == null) { + return jsonBody; + } + try { + String jsonBodyStr = IOUtils.toString(request.getInputStream()); + jsonBody = new JSONObject(jsonBodyStr); + log.debug(" jsonBody:{}", jsonBody); + } catch (Exception ex) { + ex.printStackTrace(); + log.error("Exception while retriving json from request is - ", ex); + } + return jsonBody; + } + + public String getDecryptedPassword(String clientPassword) { + String decryptedPassword = null; + if (clientPassword != null) { + try { + decryptedPassword = encryptionService.decrypt(clientPassword); + } catch (EncryptionException ex) { + log.error("Failed to decrypt password", ex); + } + } + return decryptedPassword; + } + + public String getScopes(String endpoint) { + String scopes = null; + List scopeList = null; + Map> endpointMap = this.appConfiguration.getEndpointDetails(); + log.debug("Get scope for endpoint:{} from endpointMap:{}", endpoint, endpointMap); + + if (endpointMap == null || endpointMap.isEmpty()) { + return scopes; + } + + scopeList = endpointMap.get(endpoint); + + if (scopeList == null || scopeList.isEmpty()) { + return scopes; + } + + Set scopesSet = new HashSet<>(scopeList); + + StringBuilder scope = new StringBuilder(ScopeType.OPENID.getValue()); + for (String s : scopesSet) { + scope.append(" ").append(s); + } + log.debug("endpoint:{}, endpointMap:{}, scope:{}", endpoint, endpointMap, scope); + return scope.toString(); + } + + private String getEndpointPath(String endpoint) { + Map> endpointMap = this.appConfiguration.getEndpointDetails(); + log.debug("Get endpoint URL for endpoint:{} from endpointMap:{}", endpoint, endpointMap); + + if (StringUtils.isBlank(endpoint) || endpointMap == null || endpointMap.isEmpty()) { + return endpoint; + } + + Set keys = endpointMap.keySet(); + String endpointPath = keys.stream() + .filter(e -> e != null && e.toLowerCase().endsWith("/" + endpoint.toLowerCase())).findFirst() + .orElse(null); + log.debug("Final endpoint:{}, keys:{}, endpointPath:{}", endpoint, keys, endpointPath); + return endpointPath; + } + + private String getEndpointUrl(String endpoint) { + if (StringUtils.isBlank(endpoint)) { + return endpoint; + } + + StringBuilder sb = new StringBuilder(); + sb.append(appConfiguration.getOpenIdIssuer()); + sb.append("/"); + sb.append(endpoint); + + log.debug("endpoint:{} url is sb:{}", endpoint, sb); + return sb.toString(); + } + + private static Builder getClientBuilder(String url) { + return ClientBuilder.newClient().target(url).request(); + } + + public Response post(String endpoint, String postData, ContentType contentType) { + log.debug("postData - endpoint:{}, postData:{}", endpoint, postData); + String endpointPath = this.getEndpointPath(endpoint); + String token = this.getToken(endpointPath); + + return post(this.getEndpointUrl(endpointPath), null, token, null, contentType, postData); + } + + private Response post(String url, String authType, String token, Map headers, + ContentType contentType, String postData) { + log.debug("postData - url:{}, authType:{}, token:{}, headers:{}, contentType:{}, postData:{}", url, authType, + token, headers, contentType, postData); + + if (StringUtils.isBlank(authType)) { + authType = "Bearer "; + } + if (contentType == null) { + contentType = ContentType.APPLICATION_JSON; + } + + Builder request = getClientBuilder(url); + request.header(AUTHORIZATION, authType + token); + request.header(CONTENT_TYPE, contentType); + + if (headers != null) { + for (Entry headerEntry : headers.entrySet()) { + request.header(headerEntry.getKey(), headerEntry.getValue()); + } + } + + log.debug(" request:{}}", request); + + Response response = request.post(Entity.entity(postData, MediaType.APPLICATION_JSON)); + log.debug(" response:{}", response); + + return response; + } + +} From 6d5dbf0a7e014c89749c4f9f9a35e7e4dea92d27 Mon Sep 17 00:00:00 2001 From: Devrim Date: Tue, 30 Jul 2024 13:07:47 +0300 Subject: [PATCH 27/43] fix(jans-linux-setup): set size of acr in jansToken is 1024 (#9055) Signed-off-by: Mustafa Baser --- jans-linux-setup/jans_setup/static/rdbm/sql_data_types.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/jans-linux-setup/jans_setup/static/rdbm/sql_data_types.json b/jans-linux-setup/jans_setup/static/rdbm/sql_data_types.json index f134fcb5a37..754c165cf3c 100644 --- a/jans-linux-setup/jans_setup/static/rdbm/sql_data_types.json +++ b/jans-linux-setup/jans_setup/static/rdbm/sql_data_types.json @@ -444,6 +444,12 @@ "type": "STRING" } }, + "jansToken:acr": { + "mysql": { + "size": 1024, + "type": "VARCHAR" + } + }, "jansJwks": { "mysql": { "type": "TEXT" From 382be5c00e8b1b6b4f457bfe5aef780fab182f1a Mon Sep 17 00:00:00 2001 From: Devrim Date: Tue, 30 Jul 2024 14:51:18 +0300 Subject: [PATCH 28/43] fix(jans-linux-setup): auto convert varchar to string for spanner (#9059) Signed-off-by: Mustafa Baser --- jans-linux-setup/jans_setup/setup_app/installers/rdbm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jans-linux-setup/jans_setup/setup_app/installers/rdbm.py b/jans-linux-setup/jans_setup/setup_app/installers/rdbm.py index 239276124ec..8272856bef4 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/rdbm.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/rdbm.py @@ -197,7 +197,8 @@ def get_sql_col_type(self, attrname, table=None): if table in type_.get('tables', {}): type_ = type_['tables'][table] if 'size' in type_: - data_type = '{}({})'.format(type_['type'], type_['size']) + dtype = 'STRING' if type_['type'] and Config.rdbm_type == 'spanner' else type_['type'] + data_type = '{}({})'.format(dtype, type_['size']) else: data_type = type_['type'] From 9cda8e84ab97a877b03f614355640b99b2f54095 Mon Sep 17 00:00:00 2001 From: Yuriy Movchan Date: Tue, 30 Jul 2024 20:51:02 +0400 Subject: [PATCH 29/43] fix(jans-lock): fix lock startup in jans-auth service mode (#9062) * fix(jans-lock): fix lock startup in jans-auth service mode Signed-off-by: Yuriy Movchan * fix(jans-lock): fix lock startup in jans-auth service mode --------- Signed-off-by: Yuriy Movchan --- .../src/main/java/io/jans/lock/service/net/HttpService.java | 0 .../service/src/main/java/io/jans/lock/util/LockUtil.java | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) rename jans-lock/lock-master/{service => server}/src/main/java/io/jans/lock/service/net/HttpService.java (100%) diff --git a/jans-lock/lock-master/service/src/main/java/io/jans/lock/service/net/HttpService.java b/jans-lock/lock-master/server/src/main/java/io/jans/lock/service/net/HttpService.java similarity index 100% rename from jans-lock/lock-master/service/src/main/java/io/jans/lock/service/net/HttpService.java rename to jans-lock/lock-master/server/src/main/java/io/jans/lock/service/net/HttpService.java diff --git a/jans-lock/lock-master/service/src/main/java/io/jans/lock/util/LockUtil.java b/jans-lock/lock-master/service/src/main/java/io/jans/lock/util/LockUtil.java index 6d9bbad4ae6..d0483384f88 100644 --- a/jans-lock/lock-master/service/src/main/java/io/jans/lock/util/LockUtil.java +++ b/jans-lock/lock-master/service/src/main/java/io/jans/lock/util/LockUtil.java @@ -5,9 +5,9 @@ import io.jans.as.model.common.GrantType; import io.jans.as.model.common.ScopeType; import io.jans.lock.model.config.AppConfiguration; -import io.jans.lock.service.net.HttpService; import io.jans.model.net.HttpServiceResponse; import io.jans.service.EncryptionService; +import io.jans.service.net.BaseHttpService; import io.jans.util.security.StringEncrypter.EncryptionException; import jakarta.enterprise.context.ApplicationScoped; @@ -48,10 +48,10 @@ public class LockUtil { Logger log; @Inject - AppConfiguration appConfiguration; + private AppConfiguration appConfiguration; @Inject - HttpService httpService; + private BaseHttpService httpService; @Inject EncryptionService encryptionService; From 649c747ac3da6bf59b5e07f0f022aa14b319abe1 Mon Sep 17 00:00:00 2001 From: Isman Firmansyah Date: Wed, 31 Jul 2024 14:32:26 +0700 Subject: [PATCH 30/43] fix(jans-linux-setup): removed trailing whitespace on eventType column (#9066) Signed-off-by: iromli --- jans-linux-setup/jans_setup/schema/jans_schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jans-linux-setup/jans_setup/schema/jans_schema.json b/jans-linux-setup/jans_setup/schema/jans_schema.json index da7e73d0eb7..cebbc8f49a2 100644 --- a/jans-linux-setup/jans_setup/schema/jans_schema.json +++ b/jans-linux-setup/jans_setup/schema/jans_schema.json @@ -5451,7 +5451,7 @@ "inum", "jansLastUpd", "eventTime", - "eventType ", + "eventType", "severetyLevel", "policyId", "policyResult", From 1566ca2cfb2471b62dddb03f2c8b1b6053fc4075 Mon Sep 17 00:00:00 2001 From: Dhaval D <343411+ossdhaval@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:38:09 +0530 Subject: [PATCH 31/43] docs(config): custom assets configuration via CLI and TUI (#9067) * docs: create custom asset config document Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> * docs(config): proofreading Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> * docs(config): minor updates Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> * docs: fix format issue Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> * docs(config): fix add and update sections Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> --------- Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> --- .../custom-assets-configuration.md | 401 ++++++++++++++++++ docs/assets/tui-asset-data.png | Bin 0 -> 26048 bytes docs/assets/tui-asset-screen.png | Bin 0 -> 20337 bytes 3 files changed, 401 insertions(+) create mode 100644 docs/assets/tui-asset-data.png create mode 100644 docs/assets/tui-asset-screen.png diff --git a/docs/admin/config-guide/custom-assets-configuration.md b/docs/admin/config-guide/custom-assets-configuration.md index e69de29bb2d..95d56f20faa 100644 --- a/docs/admin/config-guide/custom-assets-configuration.md +++ b/docs/admin/config-guide/custom-assets-configuration.md @@ -0,0 +1,401 @@ +--- +tags: + - administration + - configuration + - custom assets +--- + +# Custom Assets Configuration + +The Janssen Server provides multiple configuration tools to configure custom +assets. + +=== "Use Command-line" + + Use the command line to perform actions from the terminal. Learn how to + use Jans CLI [here](../config-guide/config-tools/jans-cli/README.md) or jump straight to + the [Using Command Line](#using-command-line) + +=== "Use Text-based UI" + + Use a fully functional text-based user interface from the terminal. + Learn how to use Jans Text-based UI (TUI) + [here](../config-guide/config-tools/jans-tui/README.md) or jump straight to the + [Using Text-based UI](#using-text-based-ui) + +=== "Use REST API" + + Use REST API for programmatic access or invoke via tools like CURL or + Postman. Learn how to use Janssen Server Config API + [here](../config-guide/config-tools/config-api/README.md) or Jump straight to the + [Using Configuration REST API](#using-configuration-rest-api) + +## Using The Command Line + + +In the Janssen Server, you can deploy custom assets using the +command line. To get the details of Janssen command line operations relevant to +the custom assets, check the operations under the `JansAssets` task using the +command below. + +```bash title="Command" +/opt/jans/jans-cli/config-cli.py --info JansAssets +``` + +```test title="Sample Output" linenums="1" +Operation ID: get-asset-by-inum + Description: Gets an asset by inum - unique identifier + Parameters: + inum: Asset Inum [string] +Operation ID: delete-asset + Description: Delete an asset + Parameters: + inum: Asset identifier [string] +Operation ID: get-asset-by-name + Description: Fetch asset by name. + Parameters: + name: Asset Name [string] +Operation ID: get-all-assets + Description: Gets all Jans assets. + Parameters: + limit: Search size - max size of the results to return [integer] + pattern: Search pattern [string] + status: Status of the attribute [string] + startIndex: The 1-based index of the first query result [integer] + sortBy: Attribute whose value will be used to order the returned response [string] + sortOrder: Order in which the sortBy param is applied. Allowed values are "ascending" and "descending" [string] + fieldValuePair: Field and value pair for searching [string] +Operation ID: get-asset-services + Description: Gets asset services +Operation ID: get-asset-types + Description: Get valid asset types +Operation ID: put-asset + Description: Update existing asset + Schema: AssetForm +Operation ID: post-new-asset + Description: Upload new asset + Schema: AssetForm + +To get sample schema type /opt/jans/jans-cli/config-cli.py --schema-sample , for example /opt/jans/jans-cli/config-cli.py --schema-sample AssetForm +``` + +### Get All Current Custom Assets + +Get all the currently configured custom assets on the Janssen Server by +performing this operation. + +```bash title="Command" +/opt/jans/jans-cli/config-cli.py --operation-id get-all-assets +``` +```json title="Sample Output" linenums="1" +{ + "start": 0, + "totalEntriesCount": 2, + "entriesCount": 2, + "entries": [ + { + "dn": "inum=36014ca4-0978-4d95-8858-964b815ea770,ou=document,o=jans", + "inum": "36014ca4-0978-4d95-8858-964b815ea770", + "displayName": "custom.xhtml", + "description": "custom page", + "document": "", + "creationDate": "2024-07-25T11:40:17", + "jansService": [ + "jans-auth" + ], + "jansLevel": 1, + "jansEnabled": true, + "baseDn": "inum=36014ca4-0978-4d95-8858-964b815ea770,ou=document,o=jans" + }, + { + "dn": "inum=b5ab08e4-17b2-487d-845a-bdc6b48fd5b4,ou=document,o=jans", + "inum": "b5ab08e4-17b2-487d-845a-bdc6b48fd5b4", + "displayName": "a.png", + "description": "custom image", + "document": "", + "creationDate": "2024-07-25T11:44:38", + "jansService": [ + "jans-auth" + ], + "jansLevel": 2, + "jansEnabled": true, + "baseDn": "inum=b5ab08e4-17b2-487d-845a-bdc6b48fd5b4,ou=document,o=jans" + } + ] +} +``` + +### Get Custom Asset By inum + +With `get-all-assets` operation-id, we can get any specific asset matched +with `inum`. If we know the `inum`, we can simply use the below command: + +```bash title="Command" +/opt/jans/jans-cli/config-cli.py --operation-id get-asset-by-inum \ +--url-suffix inum:36014ca4-0978-4d95-8858-964b815ea770 +``` +It returns the details as below: + + +```json title="Sample Output" linenums="1" + { + "dn": "inum=36014ca4-0978-4d95-8858-964b815ea770,ou=document,o=jans", + "inum": "36014ca4-0978-4d95-8858-964b815ea770", + "displayName": "custom.xhtml", + "description": "custom page", + "document": "", + "creationDate": "2024-07-25T11:40:17", + "jansService": [ + "jans-auth" + ], + "jansLevel": 1, + "jansEnabled": true, + "baseDn": "inum=36014ca4-0978-4d95-8858-964b815ea770,ou=document,o=jans" +} +``` + + +### Get Custom Asset By Name + +With `get-asset-by-name` operation-id, we can get any specific asset matched with `name`. + If we know the `name`, we can simply use the below command: + +```bash title="Command" +/opt/jans/jans-cli/config-cli.py --operation-id get-asset-by-name \ +--url-suffix name:a.png +``` +It returns the details as below: + +```json title="Sample Output" linenums="1" +{ + "start": 0, + "totalEntriesCount": 1, + "entriesCount": 1, + "entries": [ + { + "dn": "inum=b5ab08e4-17b2-487d-845a-bdc6b48fd5b4,ou=document,o=jans", + "inum": "b5ab08e4-17b2-487d-845a-bdc6b48fd5b4", + "displayName": "a.png", + "description": "custom image", + "document": "", + "creationDate": "2024-07-25T11:44:38", + "jansService": [ + "jans-auth" + ], + "jansLevel": 2, + "jansEnabled": true, + "baseDn": "inum=b5ab08e4-17b2-487d-845a-bdc6b48fd5b4,ou=document,o=jans" + } + ] +} +``` + +### Get Services + + +Get the list of Janssen Server services that support custom assets + by performing `get-asset-services` operation. + +```bash title="Command" +/opt/jans/jans-cli/config-cli.py --operation-id get-asset-services +``` + +```text title="Sample Output" linenums="1" +[ + "jans-auth", + "jans-casa", + "jans-config-api", + "jans-fido2", + "jans-link", + "jans-lock", + "jans-scim", + "jans-keycloak-link" +] +``` + + +### Get Valid Asset Types + +Get the asset types of your Janssen Server by performing `get-asset-types` +operation. + +```bash title="Command" +/opt/jans/jans-cli/config-cli.py --operation-id get-asset-types +``` + +```text title="Sample Output" linenums="1" +[ + "properties", + "jar", + "xhtml", + "js", + "css", + "png", + "gif", + "jpg", + "jpeg" +] +``` + +### Add New Custom Asset + +To create a new asset, we can use `post-new-asset` operation id. As shown in +the [output](#using-command-line) for `--info` command, the `post-new-asset` +operation requires data to be sent according to `AssetForm` schema. + + +To see the schema, use the command below: + +```bash title="Command" +/opt/jans/jans-cli/config-cli.py --schema AssetForm +``` + +For better understanding, the Janssen Server also provides a sample of data to +be sent to the server. This sample conforms to the schema above. Use the command +below to get the sample. + +```bash title="Command" +/opt/jans/jans-cli/config-cli.py --schema-sample AssetForm +``` + +Using the schema and the example above, we have added below data to the +file `/tmp/add-asset.json`. Example below will load `p.properties` file as +a custom asset to the `jans-auth` service. + +```json title="Input" linenums="1" +{ + "document": { + "displayName": "p.properties", + "description": "Valid text description", + "jansService": [ + "jans-auth" + ], + "jansLevel": 142, + "jansEnabled": true + }, + "assetFile": "/tmp/p.properties" +} + +``` +Now let's post this Assert to the Janssen Server to be added to the existing set: + +```bash title="Command" + /opt/jans/jans-cli/config-cli.py --operation-id post-new-asset \ + --data /tmp/add-asset.json +``` + + + +### Update Existing Custom Assets + +Use the `put-asset` operation to update an existing asset. This operation uses +same schema as [add new asset](#add-new-custom-asset) operation. For example, +assuming that there is an existing asset as show below: + +```json title="Existing Asset" + { + "dn": "inum=48f2e07b-b879-4db2-816b-36bc4d69701f,ou=document,o=jans", + "inum": "48f2e07b-b879-4db2-816b-36bc4d69701f", + "displayName": "p.properties", + "description": "Valid text description", + "document": "cHJvcGVydGllcyBoZXJlLiAK\r\n", + "creationDate": "2024-07-31T06:24:55", + "jansService": [ + "jans-auth" + ], + "jansLevel": 142, + "jansEnabled": true, + "baseDn": "inum=48f2e07b-b879-4db2-816b-36bc4d69701f,ou=document,o=jans" + } +``` + +Now to update level of this asset to 6, create a text file with following +content in it. Let's name this text file as `/tmp/update-asset.json` + +```json +{ + "document": { + "dn": "inum=48f2e07b-b879-4db2-816b-36bc4d69701f,ou=document,o=jans", + "inum": "48f2e07b-b879-4db2-816b-36bc4d69701f", + "displayName": "p.properties", + "description": "Valid text description", + "document": "cHJvcGVydGllcyBoZXJlLiAK\r\n", + "creationDate": "2024-07-31T06:24:55", + "jansService": [ + "jans-auth" + ], + "jansLevel": 6, + "jansEnabled": true + }, + "assetFile": "/tmp/p.properties" +} +``` + +Now use the command below to update the asset with new value for level. + +```bash title="Sample Command" +/opt/jans/jans-cli/config-cli.py --operation-id put-asset \ +--data /tmp/update-asset.json +``` + +Upon successful execution, this command will return with updated asset values. + +```json title="Return values" + +``` + +3. We have changed only the `` to ` ` in the existing asset. + Use the updated file to send the update to the Janssen Server using the command below + ```bash title="Command" + /opt/jans/jans-cli/config-cli.py --operation-id put-asset \ + --data /tmp/update-asset.json + ``` +This will update the existing asset matched with inum value. + + +### Delete Custom Asset + +You can delete any custom asset by its `inum` value. + +```bash title="Command" +/opt/jans/jans-cli/config-cli.py --operation-id delete-asset \ +--url-suffix inum:36014ca4-0978-4d95-8858-964b815ea770 +``` + +## Using Text-based UI + + +In Janssen, You can deploy custom asset using +the [Text-Based UI](./config-tools/jans-tui/README.md) also. + +You can start TUI using the command below: + +```bash title="Command" +sudo /opt/jans/jans-cli/jans_cli_tui.py +``` + +### Asset Screen + +Navigate to `Assets` tab to open the Assets screen as shown in the image below. + +* To get the list of currently added Assets, bring the control to the Search +box (using the tab key), and press Enter. Type the search string to search +for Asset with matching `Display Name` and `inum`. + + +![Image](../../assets/tui-asset-screen.png) + + +* Use the `Add Asset` button to create a new asset. +* From the screen below, select the custom asset that needs to be uploaded +and select the Janssen Server service to which the asset will be uploaded. + +![Image](../../assets/tui-asset-data.png) + + + +## Using Configuration REST API + +Janssen Server Configuration REST API exposes relevant endpoints for managing +and configuring the custom assets. Endpoint details are published in the [Swagger +document](./../reference/openapi.md). \ No newline at end of file diff --git a/docs/assets/tui-asset-data.png b/docs/assets/tui-asset-data.png new file mode 100644 index 0000000000000000000000000000000000000000..2dbad5876b9ff264be823de00cc4b12a5a48a2b8 GIT binary patch literal 26048 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 literal 0 HcmV?d00001 diff --git a/docs/assets/tui-asset-screen.png b/docs/assets/tui-asset-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..01e2da5df140e7b0af0a88ff9cb4469748237b4d GIT binary patch literal 20337 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 Ca-wqp literal 0 HcmV?d00001 From 9c79a6158939b232281974694a4c78bd69d52b4e Mon Sep 17 00:00:00 2001 From: Devrim Date: Wed, 31 Jul 2024 12:59:26 +0300 Subject: [PATCH 32/43] fix(jans-linux-setup): apache config for jans-lock wellknown endpoint (#9070) Signed-off-by: Mustafa Baser --- jans-linux-setup/jans_setup/templates/apache/https_jans.conf | 1 - 1 file changed, 1 deletion(-) diff --git a/jans-linux-setup/jans_setup/templates/apache/https_jans.conf b/jans-linux-setup/jans_setup/templates/apache/https_jans.conf index 5687f8bc7af..fedda79ddfb 100644 --- a/jans-linux-setup/jans_setup/templates/apache/https_jans.conf +++ b/jans-linux-setup/jans_setup/templates/apache/https_jans.conf @@ -118,7 +118,6 @@ ProxyPass /.well-known/scim-configuration http://localhost:8087/jans-scim/restv1/scim-configuration ProxyPass /firebase-messaging-sw.js http://localhost:%(jans_auth_port)s/jans-auth/firebase-messaging-sw.js ProxyPass /device-code http://localhost:%(jans_auth_port)s/jans-auth/device_authorization.htm - ProxyPass /.well-known/lock-master-configuration http://localhost:%(jans_lock_port)s/jans-lock/.well-known/lock-master-configuration ProxyErrorOverride On From 3c1ddaacd138e82cdf92988951aa02c518755ea6 Mon Sep 17 00:00:00 2001 From: Dhaval D <343411+ossdhaval@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:09:31 +0530 Subject: [PATCH 33/43] docs(config): update instructions in LDAP configuration document (#9056) * docs(ldap): ldap config add-update document changes Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> * docs(config): update LDAP conf instructions Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> --------- Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> --- .../auth-server-config/ldap-configuration.md | 295 +++++++----------- 1 file changed, 110 insertions(+), 185 deletions(-) diff --git a/docs/admin/config-guide/auth-server-config/ldap-configuration.md b/docs/admin/config-guide/auth-server-config/ldap-configuration.md index 8d0e97e9f89..273b814d640 100644 --- a/docs/admin/config-guide/auth-server-config/ldap-configuration.md +++ b/docs/admin/config-guide/auth-server-config/ldap-configuration.md @@ -1,8 +1,8 @@ --- tags: - - administration - - configuration - - ldap + - administration + - configuration + - ldap --- @@ -13,184 +13,133 @@ tasks. === "Use Command-line" - Use the command line to perform actions from the terminal. Learn how to - use Jans CLI [here](../config-tools/jans-cli/README.md) or jump straight to - the [Using Command Line](#using-command-line) + Use the command line to perform actions from the terminal. Learn how to + use Jans CLI [here](../config-tools/jans-cli/README.md) or jump straight to + the [Using Command Line](#using-command-line) === "Use Text-based UI" - LDAP Configuration is not possible in Text-based UI. + LDAP Configuration is not possible in Text-based UI. === "Use REST API" - Use REST API for programmatic access or invoke via tools like CURL or - Postman. Learn how to use Janssen Server Config API - [here](../config-tools/config-api/README.md) or Jump straight to the - [Using Configuration REST API](#using-configuration-rest-api) + Use REST API for programmatic access or invoke via tools like CURL or + Postman. Learn how to use Janssen Server Config API + [here](../config-tools/config-api/README.md) or Jump straight to the + [Using Configuration REST API](#using-configuration-rest-api) ## Using Command Line -In the Janssen Server, you can deploy and customize the LDAP using the +In the Janssen Server, you can configure the LDAP backend using the command Line. To get the details of Janssen command line operations relevant to -`LDAP`, you can check the operations under `DatabaseLdapConfiguration` task using the -command below: +LDAP configuration, check the operations under the `DatabaseLdapConfiguration` +task using the command below: ```bash title="Command" /opt/jans/jans-cli/config-cli.py --info DatabaseLdapConfiguration ``` -It comes with the following options: +It will list the relevant operations as listed below: ```text title="Sample Output" Operation ID: get-config-database-ldap - Description: Gets list of existing LDAP configurations. + Description: Gets list of existing LDAP configurations. Operation ID: put-config-database-ldap - Description: Updates LDAP configuration - Schema: GluuLdapConfiguration + Description: Updates LDAP configuration + Schema: GluuLdapConfiguration Operation ID: post-config-database-ldap - Description: Adds a new LDAP configuration - Schema: GluuLdapConfiguration + Description: Adds a new LDAP configuration + Schema: GluuLdapConfiguration Operation ID: get-config-database-ldap-by-name - Description: Gets an LDAP configuration by name. - Parameters: - name: Name of LDAP configuration [string] + Description: Gets an LDAP configuration by name. + Parameters: + name: Name of LDAP configuration [string] Operation ID: delete-config-database-ldap-by-name - Description: Deletes an LDAP configuration - Parameters: - name: No description is provided for this parameter [string] + Description: Deletes an LDAP configuration + Parameters: + name: No description is provided for this parameter [string] Operation ID: patch-config-database-ldap-by-name - Description: Patches a LDAP configuration by name - Parameters: - name: Name of LDAP configuration [string] - Schema: Array of JsonPatch + Description: Patches a LDAP configuration by name + Parameters: + name: Name of LDAP configuration [string] + Schema: Array of JsonPatch Operation ID: post-config-database-ldap-test - Description: Tests an LDAP configuration - Schema: GluuLdapConfiguration + Description: Tests an LDAP configuration + Schema: GluuLdapConfiguration -To get sample schema type /opt/jans/jans-cli/config-cli.py --schema , for example /opt/jans/jans-cli/config-cli.py --schema GluuLdapConfiguration +To get sample schema type /opt/jans/jans-cli/config-cli.py --schema , +for example /opt/jans/jans-cli/config-cli.py --schema GluuLdapConfiguration ``` ### Get Existing LDAP Configurations -To find the existing ldap configurations, let's run the following command: +To get information about existing LDAP configurations, run the following command: ```bash title="Command" /opt/jans/jans-cli/config-cli.py --operation-id get-config-database-ldap ``` -```json title="Sample Output" +```json title="Sample Output" linenums="1" [ - { + { "configId": "auth_ldap_server", "bindDN": "cn=directory manager", "bindPassword": "m+OTwmlCEho=", "servers": [ "localhost:1636" - ], + ], "maxConnections": 1000, "useSSL": true, "baseDNs": [ "ou=people,o=jans" - ], + ], "primaryKey": "uid", "localPrimaryKey": "uid", "useAnonymousBind": false, "enabled": false, "version": 0, "level": 0 - } + } ] ``` -### Adds a new LDAP Configuration +### Add a new LDAP Configuration -At first, we have checked the existing ldap database configurations the janssen server have. -Indeed we can create a new ldap configuration as well. +To add a new a LDAP configuration, use the `post-config-database-ldap` +operation id. As shown in the [output](#using-command-line) for +`--info` command. The `post-config-database-ldap` operation +requires data to be sent according to `GluuLdapConfiguration` schema. -```text -Operation ID: post-config-database-ldap - Description: Adds a new LDAP configuration - Schema: GluuLdapConfiguration -``` - - -Let's get the schema file and update it to push into the server. +To see the schema, use the command below: ```bash title="Command" -/opt/jans/jans-cli/config-cli.py --schema GluuLdapConfiguration > /tmp/ldap.json -``` -The `post-config-database-ldap` operation uses the `GluuLdapConfiguration` schema to -describe the configuration change. - -For your information, you can obtain the format of the `GluuLdapConfiguration` -schema by running the aforementioned command without a file. - -```text title="Schema Format" -configId string -bindDN string -bindPassword string -servers array of string -maxConnections integer - format: int32 -useSSL boolean -baseDNs array of string -primaryKey string -localPrimaryKey string -useAnonymousBind boolean -enabled boolean -version integer - format: int32 -level integer - format: int32 +/opt/jans/jans-cli/config-cli.py --schema GluuLdapConfiguration ``` -An example of the schema is provided in the ldap.json file. - -you can also use the following command for `GluuLdapConfiguration` schema example. +The Janssen Server also provides an example of data that adheres +to the above schema. To fetch the example, use the command below. ```bash title="Command" /opt/jans/jans-cli/config-cli.py --schema-sample GluuLdapConfiguration ``` -```json title="Schema Example" -{ - "configId": "string", - "bindDN": "string", - "bindPassword": "string", - "servers": [ - "string" - ], - "maxConnections": 117, - "useSSL": false, - "baseDNs": [ - "string" - ], - "primaryKey": "string", - "localPrimaryKey": "string", - "useAnonymousBind": false, - "enabled": false, - "version": 116, - "level": 179 -} - -``` +Using the schema and the example above, we have added below data to the +file `/tmp/ldap.json`. -You need to modify `ldap.json` file with valid information. In our case, -I have modified as below for testing only: -```json title="Input" +```json title="Input" linenums="1" { "configId": "test_ldap", "bindDN": "cn=directory manager", "bindPassword": "password", "servers": [ - "localhost:1636" - ], + "localhost:1636" + ], "maxConnections": 1000, "useSSL": "true", "baseDNs": ["ou=people,o=jans"], @@ -202,72 +151,46 @@ I have modified as below for testing only: "level": 0 } ``` - -Now, lets post this configuration into the database. +Now let's post this LDAP configuration +to the Janssen Server to be added to the existing set: ```bash title="Command" /opt/jans/jans-cli/config-cli.py --operation-id post-config-database-ldap\ --data /tmp/ldap.json ``` -```json title="Sample Output" -{ - "configId": "test_ldap", - "bindDN": "cn=directory manager", - "bindPassword": "m+OTwmlCEho=", - "servers": [ - "localhost:1636" - ], - "maxConnections": 1000, - "useSSL": true, - "baseDNs": [ - "ou=people,o=jans" - ], - "primaryKey": "uid", - "localPrimaryKey": "uid", - "useAnonymousBind": false, - "enabled": false, - "version": 0, - "level": 0 -} -``` - - -Please note that `configId` should be a unique identifier name for each configuration. -Otherwise you will get error while going to post duplicate configuration into the server. -In that case, you can go through the next option to replace instead of adding a new one. +Please note that `configId` should be a unique identifier for each configuration. +### Update LDAP Configuration -### Updating LDAP Database Configurations +To update an existing LDAP configuration, we can use `put-config-database-ldap` +operation id. As shown in the [output](#using-command-line) for `--info` command, +the `put-config-database-ldap` operation requires data to be sent according to +`GluuLdapConfiguration` schema. -With this operation, we can update any ldap database configuration. +For example, let's say we are going to change the `maxConnections` from `1000` +to `100` in the above `test_ldap` configuration. So let's update this value +in the `/tmp/ldap.json` file that we created earlier and use the command +below to update the configuration on the server: -```text -Operation ID: put-config-database-ldap - Description: Updates LDAP configuration - Schema: GluuLdapConfiguration -``` - -For example, let say we are going to change to `maxConnections` for `1000` to `100` in the above `test_ldap` configuration. -So lets modify the `/tmp/ldap.json` file as below: - -```bash title="Comaand" +```bash title="Command" /opt/jans/jans-cli/config-cli.py --operation-id put-config-database-ldap \ --data /tmp/ldap.json ``` -```json title="Sample Command" + +```json title="Sample Output" linenums="1" { "configId": "test_ldap", "bindDN": "cn=directory manager", "bindPassword": "zHDUXV5vAPc=", "servers": [ "localhost:1636" - ], + ], "maxConnections": 100, "useSSL": true, "baseDNs": [ "ou=people,o=jans" - ], + ], "primaryKey": "uid", "localPrimaryKey": "uid", "useAnonymousBind": false, @@ -277,32 +200,34 @@ So lets modify the `/tmp/ldap.json` file as below: } ``` -### Gets LDAP Database Configuration by its name +### Get LDAP Configuration by its name + +When retrieving LDAP configuration by `name`, the value of `name` parameter +in the query is matched with the `configId` of the LDAP configuration. -In the above operation, we have updated `test_ldap.json`. Let's check the updated result -with this operation by calling its name id. +Use the command below to retrieve LDAP configuration with `configId` as +`test_ldap`. -```bash title="" +```bash title="Command" /opt/jans/jans-cli/config-cli.py --operation-id get-config-database-ldap-by-name\ --url-suffix name:test_ldap ``` -Here name is the `configId` of the configuration. If we run this command, it returns the -configuration details matched with configId. +Here the `name` parameter is matched with the `configId` of the configuration. -```json title="Sample Output" +```json title="Sample Output" linenums="1" { "configId": "test_ldap", "bindDN": "cn=directory manager", "bindPassword": "3eFs1t1aRPsW4xtxvCiGQQ==", "servers": [ "localhost:1636" - ], + ], "maxConnections": 100, "useSSL": true, "baseDNs": [ "ou=people,o=jans" - ], + ], "primaryKey": "uid", "localPrimaryKey": "uid", "useAnonymousBind": false, @@ -312,75 +237,75 @@ configuration details matched with configId. } ``` -### Delete LDAP Database Configurations +### Delete LDAP Configurations -In case, we need to delete any existing LDAP Database configuration we can do that as well. +To delete any existing LDAP Database configuration, use the command as below. ```bash title="Command" /opt/jans/jans-cli/config-cli.py --operation-id delete-config-database-ldap-by-name \ --url-suffix name:test_ldap ``` -It will delete data ldap database configuration matched with the name. +It will delete LDAP configuration where `configId` matches with the `name`. -Now you check with `get-config-database-ldap` operation -### Patch LDAP Database Configurations +### Patch LDAP Configurations -If required, We can patch single information of a ldap database configuration by using its name id. -In that case, we have to make an array of operations in schema file. So, let's get the schema file first. +To patch a single configuration element in a given LDAP configuration +by using its name, use the `patch-config-database-ldap-by-name` operation. For +example, changing the `level` or `test_ldap` configuration. ```text Operation ID: patch-config-database-ldap-by-name - Description: Patches a LDAP configuration by name - Parameters: - name: Name of LDAP configuration [string] - Schema: Array of JsonPatch + Description: Patches a LDAP configuration by name + Parameters: + name: Name of LDAP configuration [string] + Schema: Array of JsonPatch ``` -```bash -/opt/jans/jans-cli/config-cli.py --schema JsonPatch > /tmp/patch.json -``` -The `patch-config-database-ldap-by-name` uses the [JSON Patch](https://jsonpatch.com/#the-patch) +The `patch-config-database-ldap-by-name` uses the +[JSON Patch](https://jsonpatch.com/#the-patch) schema to describe the configuration change. Refer [here](../config-tools/jans-cli/README.md#patch-request-schema) to know more about schema. -For example, let's say, we want to change the level of the `test_ldap` configuration. So, -Let's update the patch file as below: +Create a patch file with the contents as below to update the `level` of a LDAP +configuration. ```json title="Input" [ - { + { "op": "replace", "path": "level", "value": "100" - } + } ] ``` -To patch data, the command looks like for this: +Use the patch file above to update the configuration of `test_ldap` LDAP +configuration using the command below: ```bash title="Command" -/opt/jans/jans-cli/config-cli.py --operation-id patch-config-database-ldap-by-name\ - --url-suffix name:test_ldap --data /tmp/patch.json +/opt/jans/jans-cli/config-cli.py \ +--operation-id patch-config-database-ldap-by-name \ +--url-suffix name:test_ldap --data /tmp/patch.json ``` -It will update the configuration and will show the updated result as below display. +It will update the configuration and show the updated result as below. -```json title="Sample Output" +```json title="Sample Output" linenums="1" { "configId": "test_ldap", "bindDN": "cn=directory manager", "bindPassword": "Kn0cqLRFzk2ASG+kwAuY2Q==", "servers": [ "localhost:1636" - ], + ], "maxConnections": 1000, "useSSL": true, "baseDNs": [ "ou=people,o=jans" - ], + ], "primaryKey": "uid", "localPrimaryKey": "uid", "useAnonymousBind": false, @@ -389,9 +314,9 @@ It will update the configuration and will show the updated result as below displ "level": 100 } ``` - ## Using Configuration REST API Janssen Server Configuration REST API exposes relevant endpoints for managing -and configuring the Lightweight Directory Access Protocol. Endpoint details are published in the [Swagger +and configuring the Lightweight Directory Access Protocol. Endpoint details are +published in the [Swagger document](./../../reference/openapi.md). \ No newline at end of file From 4076d81b6d6e2c06c15288894208ccccd1936a6b Mon Sep 17 00:00:00 2001 From: Dhaval D <343411+ossdhaval@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:21:40 +0530 Subject: [PATCH 34/43] docs(config): updates to custom assets config page (#9076) * docs(config): fix format issue and add new outputs Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> * docs(custom-assests): fix format issue Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> --------- Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> --- .../custom-assets-configuration.md | 204 +++++++++--------- 1 file changed, 108 insertions(+), 96 deletions(-) diff --git a/docs/admin/config-guide/custom-assets-configuration.md b/docs/admin/config-guide/custom-assets-configuration.md index 95d56f20faa..0795dc6bf2c 100644 --- a/docs/admin/config-guide/custom-assets-configuration.md +++ b/docs/admin/config-guide/custom-assets-configuration.md @@ -81,8 +81,8 @@ To get sample schema type /opt/jans/jans-cli/config-cli.py --schema-sample Date: Thu, 1 Aug 2024 16:19:07 +0530 Subject: [PATCH 35/43] feat(jans-fido): add interception script for registration (#9075) Signed-off-by: shekhar16 --- .../extension/fido2/Fido2ExtensionSample.py | 108 ++++++++++++++++++ .../jans_setup/templates/scripts.ldif | 16 +++ 2 files changed, 124 insertions(+) create mode 100644 jans-linux-setup/jans_setup/static/extension/fido2/Fido2ExtensionSample.py diff --git a/jans-linux-setup/jans_setup/static/extension/fido2/Fido2ExtensionSample.py b/jans-linux-setup/jans_setup/static/extension/fido2/Fido2ExtensionSample.py new file mode 100644 index 00000000000..8b6033b99d2 --- /dev/null +++ b/jans-linux-setup/jans_setup/static/extension/fido2/Fido2ExtensionSample.py @@ -0,0 +1,108 @@ +# Janssen Project software is available under the Apache 2.0 License (2004). See http://www.apache.org/licenses/ for full text. +# Copyright (c) 2023, Janssen Project +# +# Author: Jorge Munoz +# Author: Yuriy Movchan +# +from io.jans.service.cdi.util import CdiUtil +from io.jans.model.custom.script.type.fido2 import Fido2ExtensionType +from io.jans.fido2.service.operation import AttestationService +from io.jans.fido2.service.operation import AssertionService +from io.jans.util import StringHelper +from org.json import JSONObject +from com.fasterxml.jackson.databind import JsonNode +from org.apache.logging.log4j import ThreadContext +from io.jans.fido2.model.u2f.error import Fido2ErrorResponseFactory +from io.jans.fido2.model.u2f.error import Fido2ErrorResponseType +from io.jans.as.model.config import Constants + +from java.lang import String + +class Fido2Extension(Fido2ExtensionType): + + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Fido2Extension. Initialization" + print "Fido2Extension. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Fido2Extension. Destroy" + print "Fido2Extension. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + # To generate a bad request WebApplicationException giving a message. This method is to be called inside (interceptRegisterAttestation, interceptVerifyAttestation, interceptAuthenticateAssertion, interceptVerifyAssertion) + def throwBadRequestException(self, title, message, context): + print "Fido2Extension. Setting Bad request exception" + + errorClaimException = Fido2ErrorResponseFactory.createBadRequestException(Fido2ErrorResponseType.BAD_REQUEST_INTERCEPTION, title, message, ThreadContext.get(Constants.CORRELATION_ID_HEADER)) + context.setWebApplicationException(errorClaimException) + + # This method is called in Attestation register endpoint before start the registration process + def registerAttestationStart(self, paramAsJsonNode, context): + print "Fido2Extension. registerAttestationStart" + attestationService = CdiUtil.bean(AttestationService) + + return True + + # This method is called in Attestation register endpoint after start the registration process + def registerAttestationFinish(self, paramAsJsonNode, context): + print "Fido2Extension. registerAttestationFinish" + attestationService = CdiUtil.bean(AttestationService) + + return True + + # This method is called in Attestation verify endpoint before finish the registration verification process + def verifyAttestationStart(self, paramAsJsonNode, context): + print "Fido2Extension. verifyAttestationStart" + attestationService = CdiUtil.bean(AttestationService) + + return True + + # This method is called in Attestation verify endpoint after finish the registration verification process + def verifyAttestationFinish(self, paramAsJsonNode, context): + print "Fido2Extension. verifyAttestationFinish" + attestationService = CdiUtil.bean(AttestationService) + + return True + + # This method is called in Assertion authenticate endpoint before start the authentication process + def authenticateAssertionStart(self, paramAsJsonNode, context): + print "Fido2Extension. authenticateAssertionStart" + + assertionService = CdiUtil.bean(AssertionService) + + if paramAsJsonNode.hasNonNull("username"): + print "Fido2Extension. Username: '%s'" % paramAsJsonNode.get("username").asText() + if paramAsJsonNode.get("username").asText() == 'test_user': + self.throwBadRequestException("Fido2Extension authenticateAssertionStart : test_user", "Description Error from script : test_user", context) + else: + self.throwBadRequestException("Fido2Extension authenticateAssertionStart. Username is missing.", "Description Error from script. Username is missing.", context) + + return True + + # This method is called in Assertion authenticate endpoint after start the authentication process + def authenticateAssertionFinish(self, paramAsJsonNode, context): + print "Fido2Extension. authenticateAssertionFinish" + assertionService = CdiUtil.bean(AssertionService) + + return True + + # This method is called in Assertion verify endpoint before finish the authentication verification process + def verifyAssertionStart(self, paramAsJsonNode, context): + print "Fido2Extension. verifyAssertionStart" + assertionService = CdiUtil.bean(AssertionService) + + return True + + # This method is called in Assertion verify endpoint after finish the authentication verification process + def verifyAssertionFinish(self, paramAsJsonNode, context): + print "Fido2Extension. verifyAssertionFinish" + assertionService = CdiUtil.bean(AssertionService) + + return True diff --git a/jans-linux-setup/jans_setup/templates/scripts.ldif b/jans-linux-setup/jans_setup/templates/scripts.ldif index 32045c4e0c8..62743a9657f 100644 --- a/jans-linux-setup/jans_setup/templates/scripts.ldif +++ b/jans-linux-setup/jans_setup/templates/scripts.ldif @@ -627,5 +627,21 @@ jansRevision: 0 jansScr::%(introspection_introspection_github_claims_introspection_github_claims)s jansScrTyp: introspection +dn: inum=9BAF-90D7,ou=scripts,o=jans +description: Fido2 authentication module +displayName: fido2 +inum: 9BAF-90D7 +jansConfProperty: {"value1":"fido2_server_uri","value2":"https://%(hostname)s","description":""} +jansLevel: 1 +jansModuleProperty: {"value1":"usage_type","value2":"interactive","description":""} +jansModuleProperty: {"value1":"location_type","value2":"db","description":""} +jansRevision: 1 +jansScr::%(fido2_extension_fido2extensionSample)s +jansScrTyp: fido2_extension +objectClass: top +objectClass: jansCustomScr +jansEnabled: false +jansProgLng: python + From ddacc32e5c705c59b694e40436b644e50ffece43 Mon Sep 17 00:00:00 2001 From: Devrim Date: Thu, 1 Aug 2024 13:57:10 +0300 Subject: [PATCH 36/43] feat(jans-linux-setup): updated jansServiceModule for config-api in post-setup (#9079) Signed-off-by: Mustafa Baser --- .../setup_app/installers/config_api.py | 30 +++++++++++++++++-- .../jans_setup/setup_app/installers/jans.py | 5 +++- .../setup_app/installers/jans_saml.py | 5 ++-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/jans-linux-setup/jans_setup/setup_app/installers/config_api.py b/jans-linux-setup/jans_setup/setup_app/installers/config_api.py index e3f1c327f77..dc08e87def6 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/config_api.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/config_api.py @@ -11,7 +11,7 @@ from setup_app import paths from setup_app.static import AppType, InstallOption from setup_app.utils import base -from setup_app.utils.ldif_utils import create_client_ldif +from setup_app.utils.ldif_utils import myLdifParser, create_client_ldif from setup_app.config import Config from setup_app.installers.jetty import JettyInstaller from setup_app.pylib.ldif4.ldif import LDIFWriter @@ -55,7 +55,7 @@ def install(self): self.copyFile(self.source_files[1][0], '/usr/sbin') self.run([paths.cmd_chmod, '+x', '/usr/sbin/facter']) self.install_jettyService(self.jetty_app_configuration[self.service_name], True) - self.logIt("Copying fido.war into jetty webapps folder...") + self.logIt("Copying jans-config-api.war into jetty webapps folder...") jettyServiceWebapps = os.path.join(self.jetty_base, self.service_name, 'webapps') self.copyFile(self.source_files[0][0], jettyServiceWebapps) @@ -247,3 +247,29 @@ def load_test_data(self): self.writeFile(out_fn, rendered_text) self.dbUtils.import_ldif([out_fn]) + def update_jansservicemodule(self): + # this function is called by jans.py: JansInstaller.post_install_tasks() + self.logIt("Updating jansServiceModule for Config Api") + + # find configuration dn + ldif_parser = myLdifParser(self.config_ldif_fn) + ldif_parser.parse() + for dn, _ in ldif_parser.entries: + if 'ou=configuration' in dn: + config_api_config_dn = dn + break + + jans_service_modules = [] + + for jans_service_dir in os.listdir(Config.jetty_base): + if os.path.exists(os.path.join(Config.jetty_base, jans_service_dir, f'webapps/{jans_service_dir}.war')): + jans_service_modules.append(jans_service_dir) + + self.logIt(f"Config Api jansConfDyn.assetMgtConfiguration.jansServiceModule list: {jans_service_modules}") + + configuration = self.dbUtils.dn_exists(config_api_config_dn) + dynamic_configuration = json.loads(configuration['jansConfDyn']) + dynamic_configuration['assetMgtConfiguration']['jansServiceModule'] = sorted(jans_service_modules) + self.dbUtils.set_configuration('jansConfDyn', json.dumps(dynamic_configuration, indent=2), config_api_config_dn) + + diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans.py b/jans-linux-setup/jans_setup/setup_app/installers/jans.py index 5c1d236dd72..4ff07c07888 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans.py @@ -511,7 +511,7 @@ def post_install_tasks(self): #write post-install.py script self.logIt("Writing snap-post-setup.py", pbar='post-setup') post_setup_script = self.readFile(os.path.join(Config.templateFolder, 'snap-post-setup.py')) - + for key, val in (('{{SNAP_NAME}}', os.environ['SNAP_NAME']), ('{{SNAP_PY3}}', paths.cmd_py3), ('{{SNAP}}', base.snap), @@ -560,6 +560,9 @@ def post_install_tasks(self): # write default Lock Configuration to DB base.current_app.JansLockInstaller.configure_message_conf() + # Update jansServiceModule for config-api on DB + base.current_app.ConfigApiInstaller.update_jansservicemodule() + def secure_files(self): self.run([paths.cmd_chown, '-R', 'jetty:root', Config.certFolder]) self.run([paths.cmd_chmod, '-R', '660', Config.certFolder]) diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans_saml.py b/jans-linux-setup/jans_setup/setup_app/installers/jans_saml.py index 36b7340dd00..03f9aa91ed7 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans_saml.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans_saml.py @@ -12,7 +12,8 @@ from setup_app.utils.package_utils import packageUtils from setup_app.static import AppType, InstallOption from setup_app.config import Config -from setup_app.installers.jetty import JettyInstaller +from setup_app.utils.setup_utils import SetupUtils +from setup_app.installers.base import BaseInstaller from setup_app.utils.ldif_utils import create_client_ldif # Config @@ -32,7 +33,7 @@ Config.kc_db_password = 'kcdbuserpassword' Config.kc_jdbc_url = 'jdbc:postgresql:kcdbuser:kcdbuserpassword@//localhost:1122/kc_service' -class JansSamlInstaller(JettyInstaller): +class JansSamlInstaller(BaseInstaller, SetupUtils): install_var = 'install_jans_saml' From e153479441e2175a6c17db5bf59e4b3687460cf1 Mon Sep 17 00:00:00 2001 From: Yuriy Movchan Date: Thu, 1 Aug 2024 21:42:10 +0300 Subject: [PATCH 37/43] fix(jans-core): register DB document store in Store factory (#9084) Signed-off-by: Yuriy Movchan --- .../document/store/provider/DocumentStoreProviderFactory.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jans-core/document-store/src/main/java/io/jans/service/document/store/provider/DocumentStoreProviderFactory.java b/jans-core/document-store/src/main/java/io/jans/service/document/store/provider/DocumentStoreProviderFactory.java index 6dfcb95cce8..3df3cbd6d95 100644 --- a/jans-core/document-store/src/main/java/io/jans/service/document/store/provider/DocumentStoreProviderFactory.java +++ b/jans-core/document-store/src/main/java/io/jans/service/document/store/provider/DocumentStoreProviderFactory.java @@ -62,6 +62,9 @@ public DocumentStoreProvider getDocumentStoreProvider(DocumentStoreConfiguration case WEB_DAV: documentStoreProvider = instance.select(WebDavDocumentStoreProvider.class).get(); break; + case DB: + documentStoreProvider = instance.select(DBDocumentStoreProvider.class).get(); + break; } if (documentStoreProvider == null) { From f01c038e218d454d0bfbd128cef2e8290020919d Mon Sep 17 00:00:00 2001 From: Devrim Date: Thu, 1 Aug 2024 22:38:44 +0300 Subject: [PATCH 38/43] fix(jans-linux-setup): scope id permission to role for inum: C4F5 (#9072) * fix(jans-linux-setup): scope id permission to role for inum: C4F5 Signed-off-by: Mustafa Baser * fix(jans-cli-tui): default scope type is openid when creating new Signed-off-by: Mustafa Baser --------- Signed-off-by: Mustafa Baser Co-authored-by: YuriyZ --- .../cli_tui/plugins/010_auth_server/edit_scope_dialog.py | 5 ++--- jans-linux-setup/jans_setup/templates/scopes.ldif | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/jans-cli-tui/cli_tui/plugins/010_auth_server/edit_scope_dialog.py b/jans-cli-tui/cli_tui/plugins/010_auth_server/edit_scope_dialog.py index d00f24917f2..7c47a50e62a 100755 --- a/jans-cli-tui/cli_tui/plugins/010_auth_server/edit_scope_dialog.py +++ b/jans-cli-tui/cli_tui/plugins/010_auth_server/edit_scope_dialog.py @@ -58,8 +58,7 @@ def __init__( self.prepare_attributes_data() self.prepare_tabs() self.create_window() - self.sope_type = self.data.get('scopeType') or 'oauth' - + self.sope_type = self.data.get('scopeType') or 'openid' def prepare_attributes_data(self): self.jans_attributes_data = [] @@ -132,7 +131,7 @@ def create_window(self) -> None: self.app.getTitledRadioButton( _("Scope Type"), name='scopeType', - current_value=self.data.get('scopeType'), + current_value=self.data.get('scopeType') or 'openid', values=scope_types, on_selection_changed=self.scope_selection_changed, jans_help=self.app.get_help_from_schema(self.schema, 'scopeType'), diff --git a/jans-linux-setup/jans_setup/templates/scopes.ldif b/jans-linux-setup/jans_setup/templates/scopes.ldif index 6c6ba73e7be..469a5199bc1 100644 --- a/jans-linux-setup/jans_setup/templates/scopes.ldif +++ b/jans-linux-setup/jans_setup/templates/scopes.ldif @@ -203,7 +203,7 @@ displayName: view_user_permissions_roles inum: C4F5 jansAttrs: {"spontaneousClientId":"","spontaneousClientScopes":[],"showInConfigurationEndpoint":true} jansDefScope: true -jansId: permission +jansId: role jansScopeTyp: dynamic jansScrDn: inum=CB5B-3211,ou=scripts,o=jans objectClass: top From a6a1850efa7b4a3f1336a054a9aa1bd8f4415a00 Mon Sep 17 00:00:00 2001 From: pujavs <43700552+pujavs@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:36:07 +0530 Subject: [PATCH 39/43] fix(config-api): asset mgt error handling and SAML TR spec (#9082) * doc(config-api): attribite spec for saml trust relationship Signed-off-by: pujavs * feat(config-api): asset mgt update enhancement to make asset file optional Signed-off-by: pujavs * fix(config-api): validate asset service dir Signed-off-by: pujavs * feat(config-api): asset mgt validation Signed-off-by: pujavs --------- Signed-off-by: pujavs --- .../docs/jans-config-api-swagger.yaml | 18 +-- .../plugins/docs/kc-saml-plugin-swagger.yaml | 31 +++++ .../plugins/docs/user-mgt-plugin-swagger.yaml | 4 +- .../plugin/saml/model/TrustRelationship.java | 41 +++--- .../rest/resource/auth/AssetResource.java | 26 ++-- .../configapi/service/auth/AssetService.java | 125 +++++++++++++----- 6 files changed, 178 insertions(+), 67 deletions(-) diff --git a/jans-config-api/docs/jans-config-api-swagger.yaml b/jans-config-api/docs/jans-config-api-swagger.yaml index 18357d90c32..631f5154e0a 100644 --- a/jans-config-api/docs/jans-config-api-swagger.yaml +++ b/jans-config-api/docs/jans-config-api-swagger.yaml @@ -4475,6 +4475,7 @@ paths: - spontaneous_scope - end_session - post_authn + - client_authn - select_account - create_user - scim @@ -8313,19 +8314,19 @@ components: type: string selected: type: boolean - whitePagesCanView: - type: boolean adminCanView: type: boolean userCanView: type: boolean + userCanEdit: + type: boolean adminCanEdit: type: boolean - userCanEdit: + adminCanAccess: type: boolean userCanAccess: type: boolean - adminCanAccess: + whitePagesCanView: type: boolean baseDn: type: string @@ -9162,6 +9163,8 @@ components: type: boolean lockMessageConfig: $ref: '#/components/schemas/LockMessageConfig' + fapi: + type: boolean allResponseTypesSupported: uniqueItems: true type: array @@ -9171,8 +9174,6 @@ components: - code - token - id_token - fapi: - type: boolean AuthenticationFilter: required: - baseDn @@ -9939,10 +9940,10 @@ components: type: array items: type: object - value: - type: object displayValue: type: string + value: + type: object LocalizedString: type: object properties: @@ -10251,6 +10252,7 @@ components: - spontaneous_scope - end_session - post_authn + - client_authn - select_account - create_user - scim diff --git a/jans-config-api/plugins/docs/kc-saml-plugin-swagger.yaml b/jans-config-api/plugins/docs/kc-saml-plugin-swagger.yaml index 275f281c5ec..35af61863a2 100644 --- a/jans-config-api/plugins/docs/kc-saml-plugin-swagger.yaml +++ b/jans-config-api/plugins/docs/kc-saml-plugin-swagger.yaml @@ -1062,6 +1062,7 @@ components: type: string signResponses: type: string + description: List of profile configuration. SAMLMetadata: type: object properties: @@ -1075,6 +1076,7 @@ components: type: string jansAssertionConsumerServicePostURL: type: string + description: SAML entity metadata. TrustRelationship: required: - description @@ -1087,36 +1089,51 @@ components: type: string inum: type: string + description: Unique identifier owner: type: string + description: Creator of Trust Relationship. name: maxLength: 60 minLength: 0 type: string + description: The alphanumeric ID string that is used to identify the Trust + Relationship. displayName: maxLength: 60 minLength: 0 type: string + description: Trust Relationship display name. description: maxLength: 4000 minLength: 0 type: string + description: Description of the Trust Relationship. baseUrl: type: string + description: URL to use when the auth server needs to redirect. enabled: type: boolean + description: Indicates if Trust Relationship is enabled. alwaysDisplayInConsole: type: boolean + description: Indicates if Trust Relationship should always be listed in + the UI. clientAuthenticatorType: type: string + description: Preferred Authenticator Type. secret: type: string + description: Client secret. registrationAccessToken: type: string + description: Registration access token. consentRequired: type: boolean + description: Boolean value if consent is required. spMetaDataSourceType: type: string + description: "Trust Relationship SP metadata type - file, URI." enum: - file - manual @@ -1124,21 +1141,31 @@ components: $ref: '#/components/schemas/SAMLMetadata' redirectUris: type: array + description: List of valid Redirect URI. items: type: string + description: List of valid Redirect URI. spMetaDataURL: type: string + description: SAML entity metadata file URL. metaLocation: type: string + description: Trust Relationship metadata file location. releasedAttributes: type: array + description: Trust Relationship attributes that will be released to SAML + server. items: type: string + description: Trust Relationship attributes that will be released to SAML + server. spLogoutURL: pattern: "^$|(^(https?|http)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|])" type: string + description: Logout request URL. status: type: string + description: Trust Relationship setup status. enum: - active - inactive @@ -1146,6 +1173,7 @@ components: - register validationStatus: type: string + description: Trust Relationship validation status. enum: - In Progress - Success @@ -1153,12 +1181,15 @@ components: - Failed validationLog: type: array + description: Validation log. items: type: string + description: Validation log. profileConfigurations: type: object additionalProperties: $ref: '#/components/schemas/ProfileConfiguration' + description: List of profile configuration. baseDn: type: string TrustRelationshipForm: diff --git a/jans-config-api/plugins/docs/user-mgt-plugin-swagger.yaml b/jans-config-api/plugins/docs/user-mgt-plugin-swagger.yaml index a23d9c4e828..9113a738b01 100644 --- a/jans-config-api/plugins/docs/user-mgt-plugin-swagger.yaml +++ b/jans-config-api/plugins/docs/user-mgt-plugin-swagger.yaml @@ -863,10 +863,10 @@ components: type: array items: type: object - value: - type: object displayValue: type: string + value: + type: object CustomUser: type: object properties: diff --git a/jans-config-api/plugins/kc-saml-plugin/src/main/java/io/jans/configapi/plugin/saml/model/TrustRelationship.java b/jans-config-api/plugins/kc-saml-plugin/src/main/java/io/jans/configapi/plugin/saml/model/TrustRelationship.java index e4b033ca46a..7ffeae0030b 100644 --- a/jans-config-api/plugins/kc-saml-plugin/src/main/java/io/jans/configapi/plugin/saml/model/TrustRelationship.java +++ b/jans-config-api/plugins/kc-saml-plugin/src/main/java/io/jans/configapi/plugin/saml/model/TrustRelationship.java @@ -29,6 +29,7 @@ import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; +import io.swagger.v3.oas.annotations.media.Schema; @DataEntry(sortBy = { "displayName" }) @ObjectClass(value = "jansTrustRelationship") @@ -38,98 +39,108 @@ public class TrustRelationship extends Entry implements Serializable { private static final long serialVersionUID = 7912166229997681502L; @AttributeName(ignoreDuringUpdate = true) + @Schema(description = "Unique identifier") private String inum; @AttributeName + @Schema(description = "Creator of Trust Relationship.") private String owner; @AttributeName(name = "name") @NotNull @Size(min = 0, max = 60, message = "Length of the name should not exceed 60") + @Schema(description = "The alphanumeric ID string that is used to identify the Trust Relationship.") private String name; @NotNull @Size(min = 0, max = 60, message = "Length of the Display Name should not exceed 60") @AttributeName + @Schema(description = "Trust Relationship display name.") private String displayName; @NotNull @Size(min = 0, max = 4000, message = "Length of the Description should not exceed 4000") @AttributeName + @Schema(description = "Description of the Trust Relationship.") private String description; - /** - * Default URL, Home URL to use when the auth server needs to redirect or link - * back to the client. - * - */ + @Schema(description = "URL to use when the auth server needs to redirect.") @AttributeName private String baseUrl; @AttributeName(name = "jansEnabled") + @Schema(description = "Indicates if Trust Relationship is enabled.") private boolean enabled; - /** - * Always list this in the Account UI, even if the user does not have an - * active session. - */ @AttributeName(name = "displayInConsole") + @Schema(description = "Indicates if Trust Relationship should always be listed in the UI.") private boolean alwaysDisplayInConsole; @AttributeName(name = "jansPreferredMethod") + @Schema(description = "Preferred Authenticator Type.") private String clientAuthenticatorType; @AttributeName(name = "jansClntSecret") + @Schema(description = "Client secret.") private String secret; @AttributeName(name = "jansRegistrationAccessTkn") + @Schema(description = "Registration access token.") private String registrationAccessToken; + @Schema(description = "Boolean value if consent is required.") private Boolean consentRequired; - /** - * Trust Relationship SP metadata type - file, URI, federation - */ @NotNull @AttributeName(name = "jansSAMLspMetaDataSourceTyp") + @Schema(description = "Trust Relationship SP metadata type - file, URI.") private MetadataSourceType spMetaDataSourceType; @JsonObject @AttributeName(name = "samlMetadata") + @Schema(description = "SAML entity metadata.") private SAMLMetadata samlMetadata; @AttributeName(name = "jansRedirectURI") + @Schema(description = "List of valid Redirect URI.") private String[] redirectUris; - /** - * Trust Relationship file location of metadata - */ + @AttributeName(name = "jansSAMLspMetaDataFN") @Hidden + @Schema(description = "Trust Relationship metadata file name.") private String spMetaDataFN; @AttributeName(name = "jansSAMLspMetaDataURL") + @Schema(description = "SAML entity metadata file URL.") private String spMetaDataURL; @AttributeName(name = "jansMetaLocation") + @Schema(description = "Trust Relationship metadata file location.") private String metaLocation; @AttributeName(name = "jansReleasedAttr") + @Schema(description = "Trust Relationship attributes that will be released to SAML server.") private List releasedAttributes; @Pattern(regexp = "^$|(^(https?|http)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|])", message = "Please enter a valid url, including protocol (http/https)") @AttributeName(name = "jansPostLogoutRedirectURI") + @Schema(description = "Logout request URL.") private String spLogoutURL; @AttributeName(name = "jansStatus") + @Schema(description = "Trust Relationship setup status.") private GluuStatus status; @AttributeName(name = "jansValidationStatus") + @Schema(description = "Trust Relationship validation status.") private ValidationStatus validationStatus; @AttributeName(name = "jansValidationLog") + @Schema(description = "Validation log.") private List validationLog; + @Schema(description = "List of profile configuration.") private Map profileConfigurations = new HashMap(); public String getInum() { diff --git a/jans-config-api/server/src/main/java/io/jans/configapi/rest/resource/auth/AssetResource.java b/jans-config-api/server/src/main/java/io/jans/configapi/rest/resource/auth/AssetResource.java index 7743025347c..1543b04e30b 100644 --- a/jans-config-api/server/src/main/java/io/jans/configapi/rest/resource/auth/AssetResource.java +++ b/jans-config-api/server/src/main/java/io/jans/configapi/rest/resource/auth/AssetResource.java @@ -18,7 +18,7 @@ import io.jans.model.SearchRequest; import io.jans.orm.model.PagedResult; import io.jans.service.document.store.service.Document; - +import io.jans.util.exception.InvalidAttributeException; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.parameters.RequestBody; @@ -56,7 +56,7 @@ public class AssetResource extends ConfigBaseResource { private static final String ASSET_DATA = "Asset Data"; private static final String ASSET_DATA_FORM = "Asset Data From"; private static final String ASSET_NAME_CONFLICT = "NAME_CONFLICT"; - private static final String ASSET_NAME_CONFLICT_MSG = "Asset with same name %s already exists!"; + private static final String ASSET_NAME_CONFLICT_MSG = "Asset with same name %s already exist!"; private static final String ASSET_NOT_FOUND = "Asset identified by %s not found!"; private static final String ASSET_INUM = "Asset Identifier Inum"; private static final String RESOURCE_NULL = "RESOURCE_NULL"; @@ -178,7 +178,7 @@ public Response getAssetByName( public Response getJansServices() { List services = assetService.getValidModuleName(); - if(services == null) { + if (services == null) { services = Collections.emptyList(); } @@ -232,7 +232,7 @@ public Response uploadAsset(@MultipartForm AssetForm assetForm) throws Exception checkResourceNotNull(asset, ASSET_DATA); checkNotNull(asset.getDisplayName(), AttributeNames.DISPLAY_NAME); - // check if asset with same name already exists + // check if asset with same name already exist List assets = assetService.getAssetByName(asset.getDisplayName()); if (assets != null && !assets.isEmpty()) { asset.setInum(assets.get(0).getInum()); @@ -249,10 +249,10 @@ public Response uploadAsset(@MultipartForm AssetForm assetForm) throws Exception // save asset try { - asset = assetService.saveAsset(asset, assetStream); + asset = assetService.saveAsset(asset, assetStream, false); log.debug("Saved asset:{} ", asset); } catch (Exception ex) { - log.error("Application Error while creating asset is - status:{}", ex.getMessage()); + log.error("Application Error while creating asset is - {}", ex.getMessage()); throwInternalServerException(APPLICATION_ERROR, ex); } @@ -267,7 +267,7 @@ public Response uploadAsset(@MultipartForm AssetForm assetForm) throws Exception @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Modified Asset", content = @Content(mediaType = MediaType.APPLICATION_JSON_PATCH_JSON, schema = @Schema(implementation = Document.class), examples = @ExampleObject(name = "Response json example", value = "example/assets/put-asset.json"))), @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ApiError.class, description = "BadRequestException"))), - @ApiResponse(responseCode = "401", description = "Unauthorized" , content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ApiError.class, description = "Unauthorized"))), + @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ApiError.class, description = "Unauthorized"))), @ApiResponse(responseCode = "404", description = "Not Found", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ApiError.class, description = "NotFoundException"))), @ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ApiError.class, description = "InternalServerError"))) }) @Consumes(MediaType.MULTIPART_FORM_DATA) @@ -288,10 +288,16 @@ public Response updateAsset(@MultipartForm AssetForm assetForm) throws Exception checkResourceNotNull(inum, ASSET_INUM); checkNotNull(asset.getDisplayName(), AttributeNames.DISPLAY_NAME); - // check if asset with same name already exists + // validate if asset exist + Document existingDoc = assetService.getAssetByInum(asset.getInum()); + if (existingDoc == null) { + throw new InvalidAttributeException("Asset with inum '" + asset.getInum() + "' does not exist!!!"); + } + + // check if asset with same name already exist List assets = assetService.getAssetByName(asset.getDisplayName()); log.info( - "Check if asset with inum different then:{} but with same name exists - asset.getDisplayName():{}, assets:{}", + "Check if asset with inum different then:{} but with same name exist - asset.getDisplayName():{}, assets:{}", inum, asset.getDisplayName(), assets); if (assets != null && !assets.isEmpty()) { List list = assets.stream().filter(e -> !e.getInum().equalsIgnoreCase(inum)) @@ -309,7 +315,7 @@ public Response updateAsset(@MultipartForm AssetForm assetForm) throws Exception // update asset try { - asset = assetService.saveAsset(asset, assetFile); + asset = assetService.saveAsset(asset, assetFile, true); log.debug(" Updated asset:{} ", asset); } catch (Exception ex) { log.error("Application Error while updated asset is:{}", ex.getMessage()); diff --git a/jans-config-api/server/src/main/java/io/jans/configapi/service/auth/AssetService.java b/jans-config-api/server/src/main/java/io/jans/configapi/service/auth/AssetService.java index 80736344058..423b91f49a8 100644 --- a/jans-config-api/server/src/main/java/io/jans/configapi/service/auth/AssetService.java +++ b/jans-config-api/server/src/main/java/io/jans/configapi/service/auth/AssetService.java @@ -155,27 +155,51 @@ public PagedResult searchAssetByName(SearchRequest searchRequest) thro searchRequest.getStartIndex(), searchRequest.getCount(), searchRequest.getMaxCount()); } - public Document saveAsset(Document asset, InputStream documentStream) throws Exception { - log.info("Save asset - asset:{}, documentStream:{}", asset, documentStream); + public Document saveAsset(Document asset, InputStream documentStream, boolean isUpdate) throws Exception { + log.info("Save asset - asset:{}, documentStream:{}, isUpdate:{}", asset, documentStream, isUpdate); if (asset == null) { throw new InvalidAttributeException("Asset object is null!!!"); } - if (documentStream == null) { + // For update request, asset file is optional. + if (!isUpdate && documentStream == null) { throw new InvalidAttributeException(" Document data stream object is null!!!"); } // validation - validateFileExtension(asset); validateModules(asset); + ByteArrayOutputStream bos = null; + if (documentStream != null) { + validateFileExtension(asset); + + bos = getByteArrayOutputStream(documentStream); + log.trace("Asset ByteArrayOutputStream :{}", bos); + + // get asset + try (InputStream is = new Base64InputStream(getInputStream(bos), true)) { + asset = setAssetContent(asset, is); + } + } + + if (isUpdate && documentStream == null) { + // update request without asset file, get the existing asset content from DB + Document existingDoc = getAssetByInum(asset.getInum()); + if (existingDoc == null) { + throw new InvalidAttributeException("Asset with inum '" + asset.getInum() + "' does not exist!!!"); + } else { + asset.setDocument(existingDoc.getDocument()); + } + } + + // update asset revision + updateRevision(asset); - ByteArrayOutputStream bos = getByteArrayOutputStream(documentStream); - log.trace("Asset ByteArrayOutputStream :{}", bos); + // copy asset on jans-server + if (documentStream != null && isAssetServerUploadEnabled()) { + String result = copyAssetOnServer(asset, bos); + log.info("Result of asset saved on server :{}", result); - // get asset - try (InputStream is = new Base64InputStream(getInputStream(bos), true)) { - asset = setAssetContent(asset, is); } // save asset in DB store @@ -193,13 +217,6 @@ public Document saveAsset(Document asset, InputStream documentStream) throws Exc dbDocumentService.updateDocument(asset); } - // copy asset on jans-server - if (isAssetServerUploadEnabled()) { - String result = copyAssetOnServer(asset, bos); - log.info("Result of asset saved on server :{}", result); - - } - // Get final asset asset = this.getAssetByInum(asset.getInum()); @@ -268,9 +285,6 @@ private Document setAssetContent(Document asset, InputStream documentStream) thr String documentContent = new String(documentStream.readAllBytes(), StandardCharsets.UTF_8); asset.setDocument(documentContent); - // update asset revision - updateRevision(asset); - log.info("Successfully updated asset"); return asset; } @@ -307,9 +321,7 @@ private String copyAssetOnServer(Document asset, ByteArrayOutputStream stream) t List serviceModules = asset.getJansService(); String assetFileName = asset.getDisplayName(); - String documentStoreModuleName = assetFileName; - log.info("Save asset for - serviceModules:{}, assetFileName:{}", serviceModules, assetFileName); - + log.info("Copy assetFileName:{} for serviceModules:{}", asset, serviceModules); if (StringUtils.isBlank(assetFileName)) { throw new InvalidConfigurationException("Asset name is null!"); } @@ -317,20 +329,18 @@ private String copyAssetOnServer(Document asset, ByteArrayOutputStream stream) t String assetDir = this.getAssetDir(assetFileName); log.info("For saving assetFileName:{} assetDir:{}", assetFileName, assetDir); + // validate service directory + validateServiceDirectory(assetFileName, assetDir, serviceModules); + for (String serviceName : serviceModules) { String serviceDirectory = this.getServiceDirectory(assetDir, serviceName); log.info("Save asset for - serviceName:{} in serviceDirectory:{}", serviceName, serviceDirectory); - - if (StringUtils.isBlank(serviceDirectory)) { - throw new InvalidConfigurationException("Service directory to save asset is null!"); - } - String filePath = serviceDirectory + File.separator + assetFileName; log.info("To save asset - documentStoreService:{}, filePath:{} ", documentStoreService, filePath); try (InputStream ins = getInputStream(stream)) { - result = documentStoreService.saveDocumentStream(filePath, null, ins, List.of(documentStoreModuleName)); + result = documentStoreService.saveDocumentStream(filePath, null, ins, List.of(assetFileName)); log.info("Result of asset saved on server :{}", result); } @@ -407,10 +417,20 @@ private String getAssetDir(String assetFileName) { } AssetMgtConfiguration assetMgtConfiguration = this.appConfiguration.getAssetMgtConfiguration(); + + if (assetMgtConfiguration == null || StringUtils.isBlank(assetMgtConfiguration.getAssetBaseDirectory())) { + throw new InvalidConfigurationException("Config for asset management is not defined!"); + } + sb.append(assetMgtConfiguration.getAssetBaseDirectory()); String assetDir = getAssetDirectory(assetFileName); - log.info("assetMgtConfiguration:{}, sb:{}, assetDir:{}", assetMgtConfiguration, sb, assetDir); + + if (StringUtils.isBlank(assetDir)) { + throw new InvalidConfigurationException( + "Directory to upload asset [" + assetFileName + "] is not defined in config!"); + } + if (StringUtils.isNotBlank(assetDir)) { sb.append(File.separator); sb.append(assetDir); @@ -428,6 +448,7 @@ private String getServiceDirectory(String assetDir, String serviceName) { return path; } path = String.format(assetDir, serviceName); + log.info("Service directory assetDir:{}, serviceName:{}, path:{}", assetDir, serviceName, path); return path; @@ -448,7 +469,7 @@ private String getAssetDirectory(String assetFileName) { return directory; } String fileExtension = this.getFileExtension(assetFileName); - log.info("Get asset Directory - fileExtension:{}", fileExtension); + log.info("Get asset Directory for fileExtension:{}", fileExtension); Optional assetDirMapping = dirMapping.stream().filter(e -> e.getType().contains(fileExtension)) .findFirst(); @@ -505,14 +526,14 @@ private void validateFileExtension(Document asset) { private void validateModules(Document asset) { if (asset == null || asset.getJansService() == null || asset.getJansService().isEmpty()) { - throw new InvalidConfigurationException("Service module list is null or empty!"); + throw new InvalidConfigurationException("Service module to save asset is not provided in request!"); } List validModules = getValidModuleName(); log.info("validModules:{} ", validModules); if (validModules == null || validModules.isEmpty() || !isModuleNameValidationEnabled()) { - return; + throw new InvalidConfigurationException("Service module not configured in system! "); } List invalidModuleList = authUtil.findMissingElements(asset.getJansService(), validModules); @@ -521,9 +542,49 @@ private void validateModules(Document asset) { if (invalidModuleList != null && !invalidModuleList.isEmpty()) { throw new InvalidConfigurationException( "Valid modules are '{" + validModules + "}', '{" + invalidModuleList + "}' not supported!"); + } + + } + private void validateServiceDirectory(String assetFileName, String assetDir, List serviceModules) { + log.info("validate service directory details - assetFileName,:{}, assetDir:{}, serviceModules:{}", assetDir, + assetFileName, serviceModules); + List invalidServiceDirList = new ArrayList<>(); + StringBuilder missingMapping = new StringBuilder(); + StringBuilder errorMsg = new StringBuilder(); + for (String serviceName : serviceModules) { + + String serviceDirectory = this.getServiceDirectory(assetDir, serviceName); + if (StringUtils.isBlank(serviceDirectory)) { + missingMapping.append(serviceName); + } + + // check if the asset directory exist + boolean serviceDirectoryExist = isServiceDirectoryExist(serviceDirectory); + if (!serviceDirectoryExist) { + serviceModules.add(serviceName); + } + + } + log.debug("missingMapping:{}, invalidServiceDirList:{}", invalidServiceDirList, missingMapping); + if (!invalidServiceDirList.isEmpty()) { + errorMsg.append("Service directory to save asset [" + invalidServiceDirList + "] does not exist!"); + } + + if (StringUtils.isNotBlank(missingMapping.toString())) { + errorMsg.append("Cannot save asset as service directory [" + missingMapping + "] is null!"); } + if (StringUtils.isNotBlank(errorMsg.toString())) { + throw new InvalidConfigurationException(errorMsg.toString()); + } } + private boolean isServiceDirectoryExist(String serviceDirectory) { + File dir = new File(serviceDirectory); + boolean serviceDirectoryExist = dir.exists(); + log.info("Check if serviceDirectory:{} - exist:{}", serviceDirectory, serviceDirectoryExist); + return serviceDirectoryExist; + } + } From 9fab110aea73a7f74cc932d008a1ecea2f294bd9 Mon Sep 17 00:00:00 2001 From: Dhaval D <343411+ossdhaval@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:11:23 +0530 Subject: [PATCH 40/43] docs(customization): update the customization instructions to aling with custom assets (#9088) * docs(customize): add intro and management sections Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> * docs(customization): add location details Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> * docs(customization): add web customization instructions Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> * docs(customization): fix proofreading issues Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> --------- Signed-off-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> --- .../customization/customize-web-pages.md | 184 ++++++++++++------ 1 file changed, 124 insertions(+), 60 deletions(-) diff --git a/docs/admin/developer/customization/customize-web-pages.md b/docs/admin/developer/customization/customize-web-pages.md index cd36daa8d1d..a7a93244137 100644 --- a/docs/admin/developer/customization/customize-web-pages.md +++ b/docs/admin/developer/customization/customize-web-pages.md @@ -1,83 +1,147 @@ --- tags: - - administration - - developer - - customization - - internationalization - - i18n - - locale - - css - - images - - header - - footer - - template + - administration + - developer + - customization + - internationalization + - i18n + - locale + - css + - images + - header + - footer + - template --- -All web pages are **xhtml** files. +# Customization Using Custom Assets -### Default pages bundled in the `jans-auth.war` are: -* Login page: [login.xhtml](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/webapp/login.xhtml) -* Authorization page: [authorize.xhtml](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/webapp/authorize.xhtml) -* Logout page: [logout.xhtml](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/webapp/logout.xhtml) -* Error page: [error.xhtml](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/webapp/error.xhtml) +Janssen Server allows customization and extension of current web page designs, +images, web templates etc. For +example, extending the list of languages supported by Janssen Server or maybe +customizing the login page to show the organization's logo and align styling to +meet the branding of the organization. -### To override default pages listed above: -Put a modified `login.xhtml` or `authorize.xhtml` or `error.xhtml` or `logout.xhtml` under `/opt/jans/jetty/jans-auth/custom/pages/` +All of the above need some amount of custom assets, like custom CSS stylesheets, +logo images, etc to be available on the Janssen Server. All these are called +`Custom Assets`. + +## Managing Custom Assets + +Janssen Server configuration tools like CLI and TUI provide the ability to add, +update, and delete custom assets. Refer to the +[custom assets configuration guide](../../config-guide/custom-assets-configuration.md) +to learn how to manage custom assets. + + ### Directory structure for customization + +When custom assets are added Janssen Server, the server makes the assets +available at following locations so that they can be referenced and used by +other components. + +| Directory | Asset Type | Description | +|--------------------------------|-------------------------------------|-------------------------------------| +| /opt/jans/jetty/``/custom/i18n | properties | Resource bundle file | +| /opt/jans/jetty/``/custom/libs | lib | java archive library | +| /opt/jans/jetty/``/custom/pages | xhtml | Web pages | +| /opt/jans/jetty/``/custom/static | js, css, png, gif , jpg, jpeg | Static resources like Java-script, style-sheet and images | + +## Customizing Web Pages + +Janssen Server uses [xhtml pages](https://github.com/JanssenProject/jans/tree/main/jans-auth-server/server/src/main/webapp) to render the web interface needed in +interactive web-flows. For example, password authentication flow. + +It is possible to override the built-in `xhtml` pages or to add completely +new pages. + +### Customizing Built-in Web Pages + +In order to customize built-in web pages, follow the steps below: + +- Create a new `xhtml` page as a copy of the relevant built-in xhtml page +- Make changes to the code of the new page according to the need +- Override the built-in page with the new page + +To override the built-in page with the new page, add the new page as +a [custom asset](#managing-custom-assets) +and make sure that the +new page has the same name and extension as the built-in page. +The Janssen Server will automatically use the custom page instead of the +built-in page. + +For example, the default login web page is rendered using +[login.xhtml](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/webapp/login.xhtml). +To override this page, add a new custom `login.xhtml`. Same can be done +for other built-in pages like [authorization page](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/webapp/authorize.xhtml), [logout page](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/webapp/logout.xhtml), [error page](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/webapp/error.xhtml). + + +### Adding New Web Pages + +If the authentication/authorization flow requires new web pages, the same can be +added as [custom asset](../../config-guide/custom-assets-configuration.md) and +then can be referenced using a relative path. + +For instance, if `enterOTP.xhtml` is your webpage for step 2 of +the authentication flow that is being implemented using a custom script, +then upload page as a custom asset under the relevant Janssen service and then +reference it in the custom script as follows: + ``` -/opt/jans/jetty/jans-auth/ -|-- custom -| |-- i18n (resource bundles) -| |-- libs (library files used by custom script) -| |-- pages (web pages) -| |-- static (images and css files) -``` -### Adding a new web page for Person Authentication scripts -1. If `enterOTP.xhtml` is your webpage for step 2 of authentication, place under `/opt/jans/jetty/jans-auth/custom/pages/auth/enterOTP.xhtml` -2. Reference it in the custom script as follows: -``` - def getPageForStep(self, configurationAttributes, step): - # Used to specify the page you want to return for a given step - if (step == 1): - return "/auth/login.xhtml" - if (step == 2) - return "/auth/enterOTP.xhtml" + def getPageForStep(self, configurationAttributes, step): + # Used to specify the page you want to return for a given step + if (step == 1): + return "/auth/login.xhtml" + if (step == 2) + return "/auth/enterOTP.xhtml" ``` -### Reference login pages: +#### Reference login pages: [Here](https://github.com/JanssenProject/jans/tree/main/jans-auth-server/server/src/main/webapp/auth) you will find several login pages for different authentication methods. -### Customized resource bundles: -1. Resource bundles that are present in the jans-auth.war are present in this [folder](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/resources/) -2. To override the defaults, custom `.properties` files should be placed in the following file under this path : `/opt/jans/jetty/jans-auth/custom/i18n/jans-auth.properties` -Resource bundle names to support other languages should be placed under the same folder `/opt/jans/jetty/jans-auth/custom/i18n/`. Some examples of file names are : - * jans-auth_en.properties - * jans-auth_bg.properties - * jans-auth_de.properties - * jans-auth_es.properties - * jans-auth_fr.properties - * jans-auth_it.properties - * jans-auth_ru.properties - * jans-auth_tr.properties +#### Customized resource bundles: + +Janssen Server provides language translation support using a set of +[built-in resource bundles](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/resources/). + +To override the defaults, the custom `.properties` files should be uploaded +as [custom assets](#managing-custom-assets). If the file name matches the +existing resource bundle, then the custom bundle will override the built-in +resource bundle. + +Examples of file names are: + + * jans-auth_en.properties + * jans-auth_bg.properties + -3. To add translation for a language that is not yet supported, create new properties file in resource folder and name it jans-auth_[language_code].properties, then add language code as supported-locale to the [faces-config.xml](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/resources/faces-config.xml) present in the same folder. +To add translations for a language that is not yet supported, create a new +properties file and name it +jans-auth_[language_code].properties. Then add it to the Janssen Server as +[custom asset](#managing-custom-assets). Janssen Server will automatically +add the support for new language using the new resource bundle. -### Custom CSS files: -1. Place the file in `/opt/jans/jetty/jans-auth/custom/static/stylesheet/theme.css` -2. Reference it in .xhtml file using the URL `https://your.jans.server/jans-auth/ext/resources/stylesheet/theme.css` or `/jans-auth/ext/resources/stylesheet/theme.css` +#### Custom CSS files: -### Custom image files: -1. All images should be placed under `/opt/jans/jetty/jans-auth/custom/static/img` -2. Reference it in .xhtml file using the URL `https://your.jans.server/jans-auth/ext/resources/img/fileName.png` or `/jans-auth/ext/resources/img/fileName.jpg` +Upload the custom CSS files as [custom assets](#managing-custom-assets) and then +reference it in `.xhtml` file using the URL `https://your.jans.server/jans-auth/ext/resources/stylesheet/theme.css` or `/jans-auth/ext/resources/stylesheet/theme.css` -### Page layout, header, footer (xhtml Template) customization +#### Custom image files: +Upload the custom image files as [custom assets](#managing-custom-assets) and +then reference it in `.xhtml` file using the URL +`https://your.jans.server/jans-auth/ext/resources/img/fileName.png` or +`/jans-auth/ext/resources/img/fileName.jpg`. -Templates refers to the common interface layout and style. For example, a banner, logo in common header and copyright information in footer. +#### Page layout, header, footer (xhtml Template) customization -1. `mkdir -p /opt/jans/jetty/jans-auth/custom/pages/WEB-INF/incl/layout/` -2. Place a modified `template.xhtml` in the above location which will override the [default template file](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/template.xhtml) from the war +Templates refer to the common interface layout and style. For example, +a banner, logo in the common header, and copyright information in the footer. +Upload the custom template `template.xhtml` file as +[custom assets](#managing-custom-assets).This file will automatically override +the built-in +[default template file](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/template.xhtml). + From da60eab30f20fe0576eda29bd106143141255be8 Mon Sep 17 00:00:00 2001 From: Safin Wasi <6601566+SafinWasi@users.noreply.github.com> Date: Fri, 2 Aug 2024 11:46:15 -0500 Subject: [PATCH 41/43] docs(jans-lock): add latest schema (#9081) Signed-off-by: SafinWasi <6601566+SafinWasi@users.noreply.github.com> --- jans-lock/schema/cedarling_core_schema.json | 216 +++++++++--------- jans-lock/schema/cedarling_core_schema.schema | 167 +++++++------- 2 files changed, 194 insertions(+), 189 deletions(-) diff --git a/jans-lock/schema/cedarling_core_schema.json b/jans-lock/schema/cedarling_core_schema.json index 83b69585589..8e6aac710f2 100644 --- a/jans-lock/schema/cedarling_core_schema.json +++ b/jans-lock/schema/cedarling_core_schema.json @@ -1,23 +1,9 @@ { "Jans": { "commonTypes": { - "email_address": { - "type": "Record", - "attributes": { - "domain": { - "type": "String" - }, - "id": { - "type": "String" - } - } - }, "Context": { "type": "Record", "attributes": { - "browser": { - "type": "String" - }, "current_time": { "type": "Long" }, @@ -48,6 +34,20 @@ }, "operating_system": { "type": "String" + }, + "user_agent": { + "type": "String" + } + } + }, + "email_address": { + "type": "Record", + "attributes": { + "domain": { + "type": "String" + }, + "id": { + "type": "String" } } }, @@ -67,45 +67,40 @@ } }, "entityTypes": { - "TrustedIssuer": { + "Client": { "shape": { "type": "Record", "attributes": { - "issuer_entity_id": { - "type": "Url" + "client_id": { + "type": "String" + }, + "iss": { + "type": "Entity", + "name": "TrustedIssuer" } } } }, - "HTTP_Request": { + "Role": {}, + "id_token": { "shape": { "type": "Record", "attributes": { - "accept": { + "acr": { "type": "Set", "element": { "type": "String" } }, - "header": { - "type": "Set", - "element": { - "type": "String" - } + "amr": { + "type": "String" }, - "url": { - "type": "Url" - } - } - } - }, - "Userinfo_token": { - "shape": { - "type": "Record", - "attributes": { "aud": { "type": "String" }, + "azp": { + "type": "String" + }, "birthdate": { "type": "String" }, @@ -143,25 +138,23 @@ } } }, - "id_token": { + "TrustedIssuer": { + "shape": { + "type": "Record", + "attributes": { + "issuer_entity_id": { + "type": "Url" + } + } + } + }, + "Userinfo_token": { "shape": { "type": "Record", "attributes": { - "acr": { - "type": "Set", - "element": { - "type": "String" - } - }, - "amr": { - "type": "String" - }, "aud": { "type": "String" }, - "azp": { - "type": "String" - }, "birthdate": { "type": "String" }, @@ -199,48 +192,6 @@ } } }, - "Client": { - "shape": { - "type": "Record", - "attributes": { - "client_id": { - "type": "String" - }, - "iss": { - "type": "Entity", - "name": "TrustedIssuer" - } - } - } - }, - "User": { - "memberOfTypes": [ - "Role" - ], - "shape": { - "type": "Record", - "attributes": { - "email": { - "type": "email_address" - }, - "phone_number": { - "type": "String" - }, - "role": { - "type": "Set", - "element": { - "type": "String" - } - }, - "sub": { - "type": "String" - }, - "username": { - "type": "String" - } - } - } - }, "Access_token": { "shape": { "type": "Record", @@ -270,7 +221,6 @@ } } }, - "Role": {}, "Application": { "shape": { "type": "Record", @@ -284,23 +234,52 @@ } } } + }, + "User": { + "memberOfTypes": [ + "Role" + ], + "shape": { + "type": "Record", + "attributes": { + "email": { + "type": "email_address" + }, + "phone_number": { + "type": "String" + }, + "role": { + "type": "Set", + "element": { + "type": "String" + } + }, + "sub": { + "type": "String" + }, + "username": { + "type": "String" + } + } + } } }, "actions": { - "HEAD": { + "Execute": { "appliesTo": { "resourceTypes": [ - "HTTP_Request" + "Application" ], "principalTypes": [ - "Client" + "User", + "Role" ], "context": { "type": "Context" } } }, - "Access": { + "Search": { "appliesTo": { "resourceTypes": [ "Application" @@ -314,65 +293,84 @@ } } }, - "GET": { + "Compare": { "appliesTo": { "resourceTypes": [ - "HTTP_Request" + "Application" ], "principalTypes": [ - "Client" + "User", + "Role" ], "context": { "type": "Context" } } }, - "PATCH": { + "Monitor": { "appliesTo": { "resourceTypes": [ - "HTTP_Request" + "Application" ], "principalTypes": [ - "Client" + "User", + "Role" ], "context": { "type": "Context" } } }, - "PUT": { + "Tag": { "appliesTo": { "resourceTypes": [ - "HTTP_Request" + "Application" + ], + "principalTypes": [ + "User", + "Role" + ], + "context": { + "type": "Context" + } + } + }, + "Read": { + "appliesTo": { + "resourceTypes": [ + "Application" ], "principalTypes": [ - "Client" + "User", + "Role" ], "context": { "type": "Context" } } }, - "POST": { + "Share": { "appliesTo": { "resourceTypes": [ - "HTTP_Request" + "Application" ], "principalTypes": [ - "Client" + "User", + "Role" ], "context": { "type": "Context" } } }, - "DELETE": { + "Write": { "appliesTo": { "resourceTypes": [ - "HTTP_Request" + "Application" ], "principalTypes": [ - "Client" + "User", + "Role" ], "context": { "type": "Context" diff --git a/jans-lock/schema/cedarling_core_schema.schema b/jans-lock/schema/cedarling_core_schema.schema index f7d37f85153..68458676f5c 100644 --- a/jans-lock/schema/cedarling_core_schema.schema +++ b/jans-lock/schema/cedarling_core_schema.schema @@ -1,118 +1,125 @@ namespace Jans { - type Context = { - "network": ipaddr, - "network_type": String, - "browser": String, - "operating_system": String, - "device_health": Set, - "current_time": Long, - "geolocation": Set, - "fraud_indicators": Set, - }; + // ****** TYPES ****** type Url = { - "protocol": String, - "host": String, - "path": String, + protocol: String, + host: String, + path: String, }; type email_address = { id: String, domain: String, }; + type Context = { + network: ipaddr, + network_type: String, + user_agent: String, + operating_system: String, + device_health: Set, + current_time: Long, + geolocation: Set, + fraud_indicators: Set, + }; + + // ****** Entities ****** entity TrustedIssuer = { - "issuer_entity_id": Url, + issuer_entity_id: Url, }; entity Client = { - "client_id": String, - "iss": TrustedIssuer, - }; - entity HTTP_Request = { - "url": Url, - "header": Set, - "accept": Set, + client_id: String, + iss: TrustedIssuer, }; entity Application = { - "name": String, - "client": Client, + name: String, + client: Client, }; entity Role; entity User in [Role] { - "sub": String, - "username": String, - "email": email_address, - "phone_number": String, - "role": Set, + sub: String, + username: String, + email: email_address, + phone_number: String, + role: Set, }; + entity Access_token = { - "aud": String, - "exp": Long, - "iat": Long, - "iss": TrustedIssuer, - "jti": String, - "nbf": Long, - "scope": String, + aud: String, + exp: Long, + iat: Long, + iss: TrustedIssuer, + jti: String, + nbf: Long, + scope: String, }; entity id_token = { - "acr": Set, - "amr": String, - "aud": String, - "azp": String, - "birthdate": String, - "email": email_address, - "exp": Long, - "iat": Long, - "iss": TrustedIssuer, - "jti": String, - "name": String, - "phone_number": String, - "role": Set, - "sub": String, + acr: Set, + amr: String, + aud: String, + azp: String, + birthdate: String, + email: email_address, + exp: Long, + iat: Long, + iss: TrustedIssuer, + jti: String, + name: String, + phone_number: String, + role: Set, + sub: String, }; entity Userinfo_token = { - "aud": String, - "birthdate": String, - "email": email_address, - "exp": Long, - "iat": Long, - "iss": TrustedIssuer, - "jti": String, - "name": String, - "phone_number": String, - "role": Set, - "sub": String, - }; - action POST appliesTo { - principal: Client, - resource: HTTP_Request, + aud: String, + birthdate: String, + email: email_address, + exp: Long, + iat: Long, + iss: TrustedIssuer, + jti: String, + name: String, + phone_number: String, + role: Set, + sub: String, + }; + + // ****** Actions ****** + action Compare appliesTo { + principal: [User, Role], + resource: Application, + context: Context, + }; + action Execute appliesTo { + principal: [User, Role], + resource: Application, context: Context, }; - action GET appliesTo { - principal: Client, - resource: HTTP_Request, + action Monitor appliesTo { + principal: [User, Role], + resource: Application, context: Context, }; - action PUT appliesTo { - principal: Client, - resource: HTTP_Request, + action Read appliesTo { + principal: [User, Role], + resource: Application, context: Context, }; - action DELETE appliesTo { - principal: Client, - resource: HTTP_Request, + action Search appliesTo { + principal: [User, Role], + resource: Application, context: Context, }; - action HEAD appliesTo { - principal: Client, - resource: HTTP_Request, + action Share appliesTo { + principal: [User, Role], + resource: Application, context: Context, }; - action PATCH appliesTo { - principal: Client, - resource: HTTP_Request, + action Tag appliesTo { + principal: [User, Role], + resource: Application, context: Context, }; - action Access appliesTo { + action Write appliesTo { principal: [User, Role], resource: Application, context: Context, }; } + From de37d9df219beb9d7978e2e74ee2aa536fc0e52e Mon Sep 17 00:00:00 2001 From: Yuriy Movchan Date: Sat, 3 Aug 2024 00:43:18 +0300 Subject: [PATCH 42/43] feat(jans-orm): mask password attribute values (#9104) Signed-off-by: Yuriy Movchan --- .../io/jans/orm/impl/BaseEntryManager.java | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/jans-orm/core/src/main/java/io/jans/orm/impl/BaseEntryManager.java b/jans-orm/core/src/main/java/io/jans/orm/impl/BaseEntryManager.java index 75597e4f49b..64cc46b6928 100644 --- a/jans-orm/core/src/main/java/io/jans/orm/impl/BaseEntryManager.java +++ b/jans-orm/core/src/main/java/io/jans/orm/impl/BaseEntryManager.java @@ -86,6 +86,9 @@ public abstract class BaseEntryManager im private static final Class[] LDAP_EXPIRATION_PROPERTY_ANNOTATION = { Expiration.class }; public static final String OBJECT_CLASS = "objectClass"; + public static final String USER_PASSWORD = "userPassword"; + private static final String MASKED = "*masked*"; + public static final String[] EMPTY_STRING_ARRAY = new String[0]; private static final Class[] GROUP_BY_ALLOWED_DATA_TYPES = { String.class, Date.class, Integer.class, @@ -136,7 +139,9 @@ public void persist(Object entry) { String[] objectClasses = getObjectClasses(entry, entryClass); attributes.add(new AttributeData(OBJECT_CLASS, objectClasses, true)); - LOG.debug(String.format("LDAP attributes for persist: %s", attributes)); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("LDAP attributes for persist: %s", maskSensetiveData(attributes))); + } persist(dnValue.toString(), objectClasses, attributes, expirationValue); } @@ -1520,8 +1525,14 @@ private AttributeData getAttributeValues(String propertyName, String ldapAttribu } if (LOG.isDebugEnabled()) { + String values; + if (StringHelper.equalsIgnoreCase(USER_PASSWORD, propertyName)) { + values = MASKED; + } else { + values = Arrays.toString(attributeValues); + } LOG.debug(String.format("Property: %s, LdapProperty: %s, PropertyValue: %s", propertyName, - ldapAttributeName, Arrays.toString(attributeValues))); + ldapAttributeName, values)); } if (attributeValues.length == 0) { @@ -2482,4 +2493,29 @@ protected void dumpAttributeDataModifications(String variableName, List attributes) { + if (attributes == null) { + return null; + } + + boolean added = false; + StringBuilder sb = new StringBuilder("["); + for (AttributeData attr : attributes) { + if (added) { + sb.append(", "); + } + + if (StringHelper.equalsIgnoreCase(USER_PASSWORD, attr.getName())) { + AttributeData clonedAttr = new AttributeData( + attr.getName(), MASKED, attr.getMultiValued(), attr.getJsonValue()); + sb.append(clonedAttr); + } else { + sb.append(attr); + } + added = true; + } + sb.append(']'); + + return sb.toString(); + } } From e157cd4c8ff92c04e400fea29c51ae54f842a678 Mon Sep 17 00:00:00 2001 From: shekhar16 Date: Sun, 4 Aug 2024 01:10:48 +0530 Subject: [PATCH 43/43] fix(jans-setup): corrected jansscr (#9100) * fix(jans-setup): corrected jansscr Signed-off-by: shekhar16 * fix(jans-setup): corrected jansscr Signed-off-by: shekhar16 --------- Signed-off-by: shekhar16 --- jans-linux-setup/jans_setup/templates/scripts.ldif | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jans-linux-setup/jans_setup/templates/scripts.ldif b/jans-linux-setup/jans_setup/templates/scripts.ldif index 62743a9657f..95421c3c36a 100644 --- a/jans-linux-setup/jans_setup/templates/scripts.ldif +++ b/jans-linux-setup/jans_setup/templates/scripts.ldif @@ -636,7 +636,7 @@ jansLevel: 1 jansModuleProperty: {"value1":"usage_type","value2":"interactive","description":""} jansModuleProperty: {"value1":"location_type","value2":"db","description":""} jansRevision: 1 -jansScr::%(fido2_extension_fido2extensionSample)s +jansScr::%(fido2_fido2extensionsample)s jansScrTyp: fido2_extension objectClass: top objectClass: jansCustomScr