diff --git a/dd-apps/docker/nextcloud/Dockerfile b/dd-apps/docker/nextcloud/Dockerfile index 137c8c3..aecb664 100644 --- a/dd-apps/docker/nextcloud/Dockerfile +++ b/dd-apps/docker/nextcloud/Dockerfile @@ -69,6 +69,7 @@ COPY supervisord.conf / # Temporary replacement for a real queue RUN echo '*/1 * * * * /nc-queue.sh' >> /etc/crontabs/www-data COPY nc-queue.sh / +COPY saml.sh / ENV NEXTCLOUD_UPDATE=1 diff --git a/dd-apps/docker/nextcloud/nextcloud.yml b/dd-apps/docker/nextcloud/nextcloud.yml index fcd34dd..d8c49f0 100644 --- a/dd-apps/docker/nextcloud/nextcloud.yml +++ b/dd-apps/docker/nextcloud/nextcloud.yml @@ -33,8 +33,10 @@ services: - /etc/localtime:/etc/localtime:ro - ${SRC_FOLDER}/nextcloud:/var/www/html - ${DATA_FOLDER}/nextcloud:/var/www/html/data + - ${DATA_FOLDER}/saml/nextcloud:/saml:ro - ${DATA_FOLDER}/nc-mail-queue:/nc-mail-queue:rw environment: + - DOMAIN=${DOMAIN} - NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER} - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD} - POSTGRES_DB=nextcloud diff --git a/dd-apps/docker/nextcloud/saml.sh b/dd-apps/docker/nextcloud/saml.sh new file mode 100755 index 0000000..4dbd522 --- /dev/null +++ b/dd-apps/docker/nextcloud/saml.sh @@ -0,0 +1,40 @@ +#!/bin/sh -eu +occ="./occ" +current_nc_saml="$("${occ}" saml:config:get --output=json)" +prov_id="1" +if [ "${current_nc_saml}" = "{}" ] || [ "${current_nc_saml}" = "[]" ]; then + prov_id="$("${occ}" saml:config:create)" +fi +# Gather variables +## When keycloak gets updated, /auth disappears +idp_entityid="https://sso.${DOMAIN}/auth/realms/master" +idp_sso_url="${idp_entityid}/protocol/saml" +## This one has no PEM headers or newlines +idp_x509cert="$(curl -s "${idp_entityid}" | sed -E 's!.*public_key":"([^"]+)".*!\1!')" +## PEM format +sp_x509cert="$(cat /saml/public.crt)" +## PEM format +sp_privatekey="$(cat /saml/private.key)" + +# Actually set up Nextcloud +"${occ}" saml:config:set --no-interaction --no-ansi \ + --general-idp0_display_name="SAML Login" \ + --general-uid_mapping=username \ + --idp-entityId="${idp_entityid}" \ + --idp-singleLogoutService.url="${idp_sso_url}" \ + --idp-singleSignOnService.url="${idp_sso_url}" \ + --idp-x509cert="${idp_x509cert}" \ + --security-authnRequestsSigned=1 \ + --security-logoutRequestSigned=1 \ + --security-logoutResponseSigned=1 \ + --security-wantAssertionsSigned=1 \ + --security-wantMessagesSigned=1 \ + --saml-attribute-mapping-displayName_mapping=displayname \ + --saml-attribute-mapping-email_mapping=email \ + --saml-attribute-mapping-group_mapping=member \ + --saml-attribute-mapping-quota_mapping=quota \ + --sp-x509cert="${sp_x509cert}" \ + --sp-privateKey="${sp_privatekey}" \ + "${prov_id}" +# And set type, else it won't be active +"${occ}" config:app:set user_saml type --value saml diff --git a/dd-apps/docker/wordpress/wordpress.yml b/dd-apps/docker/wordpress/wordpress.yml index 00f19e4..b6a88a0 100644 --- a/dd-apps/docker/wordpress/wordpress.yml +++ b/dd-apps/docker/wordpress/wordpress.yml @@ -24,6 +24,7 @@ x-volumes: - /etc/localtime:/etc/localtime:ro - ${BUILD_APPS_ROOT_PATH}/docker/wordpress/src/config/php.conf.ini:/usr/local/etc/php/conf.d/conf.ini - ${SRC_FOLDER}/wordpress:/var/www/html + - ${DATA_FOLDER}/saml/wordpress:/saml:ro - ${BUILD_APPS_ROOT_PATH}/docker/wordpress/plugins:/plugins - ${BUILD_APPS_ROOT_PATH}/docker/wordpress/.htaccess:/var/www/html/.htaccess:ro - ${DATA_FOLDER}/wordpress:/var/www/html/wp-content/uploads diff --git a/dd-ctl b/dd-ctl index 9a74227..2c43cfa 100755 --- a/dd-ctl +++ b/dd-ctl @@ -20,6 +20,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later OPERATION="$1" +shift # We need docker-compose >= 1.28 to catch configuration variables REQUIRED_DOCKER_COMPOSE_VERSION="1.28" @@ -108,7 +109,7 @@ if [ "$OPERATION" != "prerequisites" ]; then fi fi -REPO_BRANCH="${2:-main}" +REPO_BRANCH="${1:-main}" cp dd.conf .env @@ -477,32 +478,85 @@ setup_wordpress(){ } setup_keycloak(){ - # configure keycloak: realm and client_scopes - echo " --> Setting up SAML for moodle" - docker exec -ti dd-sso-admin sh -c "export PYTHONWARNINGS='ignore:Unverified HTTPS request' && cd /admin/saml_scripts/ && python3 keycloak_config.py" + echo " --> Setting up SAML: Keycloak realm and client_scopes" + docker exec -i dd-sso-admin sh -s <<-EOF + export PYTHONWARNINGS='ignore:Unverified HTTPS request' + cd /admin/saml_scripts/ && python3 keycloak_config.py + EOF } -saml_certificates(){ +saml_generate_certificates(){ + saml_dir="${DATA_FOLDER}/saml" + mkdir -p "${saml_dir}/public" + + # Moodle generates its own certificate earlier, only NC and Wp + for saml_info in nextcloud:82 wordpress:33; do + saml_sp="$(echo "${saml_info}" | cut -d ':' -f 1)" + sp_uid="$(echo "${saml_info}" | cut -d ':' -f 2)" + sp_dir="${saml_dir}/${saml_sp}" + mkdir -p "${sp_dir}" + C=CA + L=Barcelona + O=localdomain + CN_CA=$O + # Generate certificate + echo " --> Generating SAML certificates for SP: ${saml_sp}" + openssl req -nodes -new -x509 \ + -keyout "${sp_dir}/private.key" \ + -out "${sp_dir}/public.crt" \ + -subj "/C=$C/L=$L/O=$O/CN=$CN_CA" -days 3650 + # Fix permissions + chown -R "${sp_uid}:${sp_uid}" "${sp_dir}" + chmod 0550 "${sp_dir}" + # Propagate public part + cp "${sp_dir}/public.crt" "${saml_dir}/public/${saml_sp}.crt" + done + # TODO: Rework wordpress_saml.py so we can get rid of this + cp "${saml_dir}/wordpress/private.key" "${saml_dir}/public/wordpress.key" + + chmod 0555 "${saml_dir}/public" + find "${saml_dir}/public" -type f -exec chmod 0444 '{}' '+' +} + +saml_register_sps_with_idp(){ wait_for_moodle - echo " --> Setting up SAML for moodle" - docker exec -ti dd-sso-admin sh -c "export PYTHONWARNINGS='ignore:Unverified HTTPS request' && cd /admin/saml_scripts/ && python3 moodle_saml.py" - docker exec -ti dd-apps-moodle php7 admin/cli/purge_caches.php - # CERTIFICATES FOR SAML - echo " --> Generating certificates for nextcloud and wordpress" - docker exec -ti dd-sso-admin /bin/sh -c "/admin/generate_certificates.sh" + # Setup SAML for each SP + for saml_sp in moodle nextcloud wordpress email; do + echo " --> Registering SAML SP '${saml_sp}' in IDP" + docker exec -i dd-sso-admin sh -s <<-EOF + export PYTHONWARNINGS='ignore:Unverified HTTPS request' + cd /admin/saml_scripts/ && python3 ${saml_sp}_saml.py + EOF + test $? == 0 || printf "\tError setting up SAML for %s...\n" "${saml_sp}" >&2 + done - # SAML PLUGIN NEXTCLOUD - echo " --> Setting up SAML for nextcloud" - docker exec -ti dd-sso-admin sh -c "export PYTHONWARNINGS='ignore:Unverified HTTPS request' && cd /admin/saml_scripts/ && python3 nextcloud_saml.py" + # Purge cache for moodle + docker exec -i dd-apps-moodle php7 admin/cli/purge_caches.php +} - # SAML PLUGIN WORDPRESS - echo " --> Setting up SAML for wordpress" - docker exec -ti dd-sso-admin sh -c "export PYTHONWARNINGS='ignore:Unverified HTTPS request' && cd /admin/saml_scripts/ && python3 wordpress_saml.py" +saml_setup_idp_in_sps(){ + # We need to support this for newer Nextcloud versions + saml_info="dd-apps-nextcloud-app:82" + saml_sp="$(echo "${saml_info}" | cut -d ':' -f 1)" + sp_uid="$(echo "${saml_info}" | cut -d ':' -f 2)" + echo " --> Setting up SAML IDP in ${saml_sp}" + docker exec -i -u "${sp_uid}" "${saml_sp}" /saml.sh +} - # SAML PLUGIN EMAIL - echo " --> Setting up SAML for email" - docker exec -ti dd-sso-admin sh -c "export PYTHONWARNINGS='ignore:Unverified HTTPS request' && cd /admin/saml_scripts/ && python3 email_saml.py" +saml(){ + if [ "${1:-}" != "--no-up" ]; then + up + wait_for_moodle + fi + # Ensure realm is OK and write public IDP cert + setup_keycloak + # Make sure each SP has its own certs and fix permissions if needed + saml_generate_certificates + # Register all SPs with the IDP + saml_register_sps_with_idp + # Setup IDP in each SP + saml_setup_idp_in_sps } wait_for_moodle(){ @@ -519,9 +573,9 @@ wait_for_moodle(){ } upgrade_moodle(){ - docker exec -ti dd-apps-moodle php7 admin/cli/maintenance.php --enable - docker exec -ti dd-apps-moodle php7 admin/cli/upgrade.php --non-interactive --allow-unstable - docker exec -ti dd-apps-moodle php7 admin/cli/maintenance.php --disable + docker exec -i dd-apps-moodle php7 admin/cli/maintenance.php --enable + docker exec -i dd-apps-moodle php7 admin/cli/upgrade.php --non-interactive --allow-unstable + docker exec -i dd-apps-moodle php7 admin/cli/maintenance.php --disable } extras_adminer(){ @@ -649,7 +703,7 @@ upgrade_plugins_moodle(){ cp -R /tmp/moodle/* "$SRC_FOLDER/moodle/" rm -rf /tmp/moodle - docker exec -ti dd-apps-moodle php7 admin/cli/purge_caches.php + docker exec -i dd-apps-moodle php7 admin/cli/purge_caches.php } upgrade_plugins_nextcloud(){ @@ -689,11 +743,11 @@ upgrade_plugins_wp(){ } update_logos_and_menu(){ - # docker exec -ti dd-sso-keycloak sh -c "/opt/jboss/keycloak/bin/jboss-cli.sh --connect --command='/subsystem=keycloak-server/theme=defaults/:write-attribute(name=cacheThemes,value=false)'" - # docker exec -ti dd-sso-keycloak sh -c "/opt/jboss/keycloak/bin/jboss-cli.sh --connect --command='/subsystem=keycloak-server/theme=defaults/:write-attribute(name=cacheTemplates,value=false)'" - # docker exec -ti dd-sso-keycloak sh -c "/opt/jboss/keycloak/bin/jboss-cli.sh --connect --command='/subsystem=keycloak-server/theme=defaults/:write-attribute(name=staticMaxAge,value=-1)'" - # docker exec -ti dd-sso-keycloak sh -c "/opt/jboss/keycloak/bin/jboss-cli.sh --connect --command='reload'" - docker exec -ti --user root dd-sso-keycloak sh -c 'rm -rf /opt/jboss/keycloak/standalone/tmp/kc-gzip-cache/*' + # docker exec -i dd-sso-keycloak sh -c "/opt/jboss/keycloak/bin/jboss-cli.sh --connect --command='/subsystem=keycloak-server/theme=defaults/:write-attribute(name=cacheThemes,value=false)'" + # docker exec -i dd-sso-keycloak sh -c "/opt/jboss/keycloak/bin/jboss-cli.sh --connect --command='/subsystem=keycloak-server/theme=defaults/:write-attribute(name=cacheTemplates,value=false)'" + # docker exec -i dd-sso-keycloak sh -c "/opt/jboss/keycloak/bin/jboss-cli.sh --connect --command='/subsystem=keycloak-server/theme=defaults/:write-attribute(name=staticMaxAge,value=-1)'" + # docker exec -i dd-sso-keycloak sh -c "/opt/jboss/keycloak/bin/jboss-cli.sh --connect --command='reload'" + docker exec -i --user root dd-sso-keycloak sh -c 'rm -rf /opt/jboss/keycloak/standalone/tmp/kc-gzip-cache/*' docker-compose build dd-sso-api && docker-compose up -d dd-sso-api configure_nextcloud_logo } @@ -817,8 +871,7 @@ case "$OPERATION" in setup_moodle setup_wordpress - setup_keycloak - saml_certificates + saml --no-up cat <<-EOF @@ -884,16 +937,13 @@ case "$OPERATION" in docker restart dd-sso-api ;; saml) - up - wait_for_moodle - setup_keycloak - saml_certificates + saml "$@" ;; securize) securize ;; setconf) - setconf "$2" "$3" + setconf "$@" ;; up) up diff --git a/dd-install.sh b/dd-install.sh index c3fadb1..8708ef2 100755 --- a/dd-install.sh +++ b/dd-install.sh @@ -238,9 +238,6 @@ sleep 30 ./dd-ctl down ./dd-ctl up -## Ensure SAML is set up (it errors always) -./dd-ctl saml || true - # Address Moodle custom settings, plugins and registration NEXTCLOUD_ADMIN_USER="$(grep -E '^NEXTCLOUD_ADMIN_USER=' dd.conf | cut -d = -f 2-)" NEXTCLOUD_ADMIN_PASSWORD="$(grep -E '^NEXTCLOUD_ADMIN_PASSWORD=' dd.conf | cut -d = -f 2-)" @@ -256,8 +253,9 @@ php theme/cbe/postinstall.php \ --contactemail="${DD_LETSENCRYPT_EMAIL}" php admin/cli/maintenance.php --disable EOF -# And fix moodle SAML with the admin -docker exec dd-sso-admin python3 /admin/saml_scripts/moodle_saml.py + +## Ensure SAML is set up +./dd-ctl saml --no-up # Proceed further with manual steps cat < None: self.conn = mysql.connector.connect( diff --git a/dd-sso/admin/src/generate_certificates.sh b/dd-sso/admin/src/generate_certificates.sh deleted file mode 100755 index 63ecdf3..0000000 --- a/dd-sso/admin/src/generate_certificates.sh +++ /dev/null @@ -1,29 +0,0 @@ -# -# Copyright © 2021,2022 IsardVDI S.L. -# -# This file is part of DD -# -# DD is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or (at your -# option) any later version. -# -# DD is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License -# along with DD. If not, see . -# -# SPDX-License-Identifier: AGPL-3.0-or-later -cd /admin/saml_certs -C=CA -L=Barcelona -O=localdomain -CN_CA=$O -CN_HOST=*.$O -OU=$O -openssl req -nodes -new -x509 -keyout private.key -out public.cert -subj "/C=$C/L=$L/O=$O/CN=$CN_CA" -days 3650 -cd /admin -echo "Now run the python nextcloud and wordpress scripts" diff --git a/dd-sso/admin/src/nextcloud_saml_onlycerts.py b/dd-sso/admin/src/nextcloud_saml_onlycerts.py deleted file mode 100644 index bb1f267..0000000 --- a/dd-sso/admin/src/nextcloud_saml_onlycerts.py +++ /dev/null @@ -1,164 +0,0 @@ -# -# Copyright © 2021,2022 IsardVDI S.L. -# -# This file is part of DD -# -# DD is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or (at your -# option) any later version. -# -# DD is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License -# along with DD. If not, see . -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -import json -import logging as log -import os -import pprint -import random -import string -import time -import traceback -from datetime import datetime, timedelta - -import psycopg2 -import yaml -from admin.lib.keycloak_client import KeycloakClient -from admin.lib.postgres import Postgres - -app = {} -app["config"] = {} - - -class NextcloudSaml: - def __init__(self): - self.url = "http://dd-sso-keycloak:8080/auth/" - self.username = os.environ["KEYCLOAK_USER"] - self.password = os.environ["KEYCLOAK_PASSWORD"] - self.realm = "master" - self.verify = True - - ready = False - while not ready: - try: - self.pg = Postgres( - "dd-apps-postgresql", - "nextcloud", - os.environ["NEXTCLOUD_POSTGRES_USER"], - os.environ["NEXTCLOUD_POSTGRES_PASSWORD"], - ) - ready = True - except: - log.warning("Could not connect to nextcloud database. Retrying...") - time.sleep(2) - log.info("Connected to nextcloud database.") - - ready = False - while not ready: - try: - with open(os.path.join("./saml_certs/public.cert"), "r") as crt: - app["config"]["PUBLIC_CERT"] = crt.read() - ready = True - except IOError: - log.warning( - "Could not get public certificate to be used in nextcloud. Retrying..." - ) - log.warning( - " You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert" - ) - time.sleep(2) - except: - log.error(traceback.format_exc()) - log.info("Got moodle srt certificate to be used in nextcloud.") - - ready = False - while not ready: - try: - with open(os.path.join("./saml_certs/private.key"), "r") as pem: - app["config"]["PRIVATE_KEY"] = pem.read() - ready = True - except IOError: - log.warning( - "Could not get private key to be used in nextcloud. Retrying..." - ) - log.warning( - " You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert" - ) - time.sleep(2) - log.info("Got moodle pem certificate to be used in nextcloud.") - - # ## This seems related to the fact that the certificate generated the first time does'nt work. - # ## And when regenerating the certificate de privatekeypass seems not to be used and instead it - # ## will use always this code as filename: 0f635d0e0f3874fff8b581c132e6c7a7 - # ## As this bug I'm not able to solve, the process is: - # ## 1.- Bring up moodle and regenerate certificates on saml2 plugin in plugins-authentication - # ## 2.- Execute this script - # ## 3.- Cleanup all caches in moodle (Development tab) - # # with open(os.path.join("./moodledata/saml2/"+os.environ['NEXTCLOUD_SAML_PRIVATEKEYPASS'].replace("moodle."+os.environ['DOMAIN'],'')+'.idp.xml'),"w") as xml: - # # xml.write(self.parse_idp_metadata()) - # with open(os.path.join("./moodledata/saml2/0f635d0e0f3874fff8b581c132e6c7a7.idp.xml"),"w") as xml: - # xml.write(self.parse_idp_metadata()) - - try: - self.reset_saml_certs() - except: - print("Error resetting saml on nextcloud") - - try: - self.set_nextcloud_saml_plugin_certs() - except: - log.error(traceback.format_exc()) - print("Error adding saml on nextcloud") - - def connect(self): - self.keycloak = KeycloakClient( - url=self.url, - username=self.username, - password=self.password, - realm=self.realm, - verify=self.verify, - ) - - # def activate_saml_plugin(self): - # ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php - # return self.pg.update("""UPDATE "mdl_config" SET value = 'email,saml2' WHERE "name" = 'auth'""") - - # def get_privatekey_pass(self): - # return self.pg.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2] - - def parse_idp_cert(self): - self.connect() - rsa = self.keycloak.get_server_rsa_key() - self.keycloak = None - return rsa["certificate"] - - def set_nextcloud_saml_plugin_certs(self): - self.pg.update( - """INSERT INTO "oc_appconfig" ("appid", "configkey", "configvalue") VALUES -('user_saml', 'sp-privateKey', '%s'), -('user_saml', 'idp-x509cert', '%s'), -('user_saml', 'sp-x509cert', '%s');""" - % ( - app["config"]["PRIVATE_KEY"], - self.parse_idp_cert(), - app["config"]["PUBLIC_CERT"], - ) - ) - - def reset_saml_certs(self): - cfg_list = ["sp-privateKey", "idp-x509cert", "sp-x509cert"] - for cfg in cfg_list: - self.pg.update( - """DELETE FROM "oc_appconfig" WHERE appid = 'user_saml' AND configkey = '%s'""" - % (cfg) - ) - - -n = NextcloudSaml() diff --git a/dd-sso/admin/src/postgres.py b/dd-sso/admin/src/postgres.py deleted file mode 100644 index d0cea49..0000000 --- a/dd-sso/admin/src/postgres.py +++ /dev/null @@ -1,70 +0,0 @@ -# -# Copyright © 2021,2022 IsardVDI S.L. -# -# This file is part of DD -# -# DD is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or (at your -# option) any later version. -# -# DD is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License -# along with DD. If not, see . -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -import json -import logging as log -import pprint -import time -import traceback -from datetime import datetime, timedelta - -import psycopg2 -import yaml - - -class Postgres: - def __init__(self, host, database, user, password): - self.conn = psycopg2.connect( - host=host, database=database, user=user, password=password - ) - - # def __del__(self): - # self.cur.close() - # self.conn.close() - - def select(self, sql): - self.cur = self.conn.cursor() - self.cur.execute(sql) - data = self.cur.fetchall() - self.cur.close() - return data - - def update(self, sql): - self.cur = self.conn.cursor() - self.cur.execute(sql) - self.conn.commit() - self.cur.close() - # return self.cur.fetchall() - - def select_with_headers(self, sql): - self.cur = self.conn.cursor() - self.cur.execute(sql) - data = self.cur.fetchall() - fields = [a.name for a in self.cur.description] - self.cur.close() - return (fields, data) - - # def update_moodle_saml_plugin(self): - # plugin[('idpmetadata', 'NrtA5ynG0htowP3SXw7dBJRIAMxn-1PwuuXwOwNhlRwMIICmzCCAYMCBgF5jb0RCTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwNTIxMDcwMjI4WhcNMzEwNTIxMDcwNDA4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCI8xh/C0+frz3kgWiUbziTDls71R2YiXLSVE+bw7gbEgZUGCLhoEI679azMtIxmnzM/snIX+yTb12+XoYkgbiLTMPQfnH+Kiab6g3HL3KPfhqS+yWkFxOoCp6Ibmp7yPlVWuHH+MBfO8OBr/r8Ao7heFbuzjiLd1KG67rcoaxfDgMuBoEomg1bgEjFgHaQIrSC6OZzH0h987/arqufZXeXlfyiqScMPUi+u5IpDWSwz06UKP0k8mxzNSlpZ93CKOUSsV0SMLxqg7FQ3SGiOk577bGW9o9BDTkkmSo3Up6smc0LzwvvUwuNd0B1irGkWZFQN9OXJnJYf1InEebIMtmPAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADM34+qEGeBQ22luphVTuVJtGxcbxLx7DfsT0QfJD/OuxTTbNAa1VRyarb5juIAkqdj4y2quZna9ZXLecVo4RkwpzPoKoAkYA8b+kHnWqEwJi9iPrDvKb+GR0bBkLPN49YxIZ8IdKX/PRa3yuLHe+loiNsCaS/2ZK2KO46COsqU4QX1iVhF9kWphNLybjNAX45B6cJLsa1g0vXLdm3kv3SB4I2fErFVaOoDtFIjttoYlXdpUiThkPXBfr7N67P3dZHaS4tjJh+IZ8I6TINpcsH8dBkUhzYEIPHCePwSiC1w6WDBLNDuKt1mj1CZrLq+1x+Yhrs+QNRheEKGi89HZ8N0=urn:oasis:names:tc:SAML:2.0:nameid-format:persistenturn:oasis:names:tc:SAML:2.0:nameid-format:transienturn:oasis:names:tc:SAML:1.1:nameid-format:unspecifiedurn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress')] - # pg_update = """UPDATE mdl_config_plugins set title = %s where plugin = auth_saml2 and name =""" - # cursor.execute(pg_update, (title, bookid)) - # connection.commit() - # count = cursor.rowcount - # print(count, "Successfully Updated!") diff --git a/dd-sso/admin/src/saml_scripts/email_saml.py b/dd-sso/admin/src/saml_scripts/email_saml.py index 9e7e4dc..635f0ce 100644 --- a/dd-sso/admin/src/saml_scripts/email_saml.py +++ b/dd-sso/admin/src/saml_scripts/email_saml.py @@ -18,95 +18,18 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later -import logging as log import os -import time -import traceback -from typing import Any, Dict, Optional +from typing import Any, Dict -from lib.keycloak_client import KeycloakClient - -try: - # Python 3.9+ - from functools import cache # type: ignore # Currently targetting 3.8 -except ImportError: - # Up to python 3.8 - from functools import cached_property as cache - - -app: Dict[str, Dict[str, str]] = {} -app["config"] = {} - - -class SamlService(object): - """ - Generic class to manage a SAML service on keycloak - """ - - keycloak: KeycloakClient - domain: str = os.environ["DOMAIN"] - - def __init__(self): - self.keycloak = KeycloakClient() - - @cache - def public_cert(self) -> str: - """ - Read the public SAML certificate as used by keycloak - """ - ready = False - basepath = os.path.dirname(__file__) - while not ready: - # TODO: Check why they were using a loop - try: - with open( - os.path.abspath( - os.path.join(basepath, "../saml_certs/public.cert") - ), - "r", - ) as crt: - app["config"]["PUBLIC_CERT"] = crt.read() - ready = True - except IOError: - log.warning("Could not get public certificate for SAML. Retrying...") - log.warning( - " You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert" - ) - time.sleep(2) - except: - log.error(traceback.format_exc()) - log.info("Got public SAML certificate") - return app["config"]["PUBLIC_CERT"] - - def get_client(self, client_id: str) -> Any: - # TODO: merge with keycloak_config.py - self.keycloak.connect() - k = self.keycloak.keycloak_admin - - clients = k.get_clients() - client = next(filter(lambda c: c["clientId"] == client_id, clients), None) - return (k, client) - - def set_client(self, client_id: str, client_overrides: Dict[str, Any]) -> str: - (k, client) = self.get_client(client_id) - if client is None: - client_uid = k.create_client(client_overrides) - else: - client_uid = client["id"] - k.update_client(client_uid, client_overrides) - return client_id - - def configure(self) -> None: - pass +from saml_service import SamlService class EmailSaml(SamlService): - client_name: str = "correu" client_description: str = "Client for the DD-managed email service" email_domain: str - def __init__(self, email_domain: str, enabled: bool = False): - super(EmailSaml, self).__init__() + def __init__(self, email_domain: str): + super(EmailSaml, self).__init__(client_name="correu") self.email_domain = email_domain @property @@ -182,6 +105,7 @@ class EmailSaml(SamlService): if __name__ == "__main__": + import logging as log email_domain = os.environ.get("MANAGED_EMAIL_DOMAIN", "") log.info("Configuring SAML client for Email") - EmailSaml(email_domain).configure() + EmailSaml(email_domain=email_domain).configure() diff --git a/dd-sso/admin/src/saml_scripts/keycloak_config.py b/dd-sso/admin/src/saml_scripts/keycloak_config.py index 60640fa..17b5c45 100644 --- a/dd-sso/admin/src/saml_scripts/keycloak_config.py +++ b/dd-sso/admin/src/saml_scripts/keycloak_config.py @@ -23,7 +23,7 @@ import logging as log import os import os.path -from lib.keycloak_client import KeycloakClient +from admin.lib.keycloak_client import KeycloakClient class KeycloakConfig: diff --git a/dd-sso/admin/src/saml_scripts/lib/keycloak_client.py b/dd-sso/admin/src/saml_scripts/lib/keycloak_client.py deleted file mode 100644 index 3480ea1..0000000 --- a/dd-sso/admin/src/saml_scripts/lib/keycloak_client.py +++ /dev/null @@ -1,534 +0,0 @@ -# -# Copyright © 2021,2022 IsardVDI S.L. -# -# This file is part of DD -# -# DD is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or (at your -# option) any later version. -# -# DD is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License -# along with DD. If not, see . -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -import json -import logging as log -import os -import time -import traceback -from datetime import datetime, timedelta -from pprint import pprint - -import yaml -from jinja2 import Environment, FileSystemLoader -from keycloak import KeycloakAdmin - -from .postgres import Postgres - -# from admin import app - - -class KeycloakClient: - """https://www.keycloak.org/docs-api/13.0/rest-api/index.html - https://github.com/marcospereirampj/python-keycloak - https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f - """ - - def __init__( - self, - url="http://dd-sso-keycloak:8080/auth/", - username=os.environ["KEYCLOAK_USER"], - password=os.environ["KEYCLOAK_PASSWORD"], - realm="master", - verify=True, - ): - self.url = url - self.username = username - self.password = password - self.realm = realm - self.verify = verify - - self.keycloak_pg = Postgres( - "dd-apps-postgresql", - "keycloak", - os.environ["KEYCLOAK_DB_USER"], - os.environ["KEYCLOAK_DB_PASSWORD"], - ) - - def connect(self): - self.keycloak_admin = KeycloakAdmin( - server_url=self.url, - username=self.username, - password=self.password, - realm_name=self.realm, - verify=self.verify, - ) - - # from keycloak import KeycloakAdmin - # keycloak_admin = KeycloakAdmin(server_url="http://dd-sso-keycloak:8080/auth/",username="admin",password="keycloakkeycloak",realm_name="master",verify=False) - - ######## Example create group and subgroup - - # try: - # self.add_group('level1') - # except: - # self.delete_group(self.get_group('/level1')['id']) - # self.add_group('level1') - # self.add_group('level2',parent=self.get_group('/level1')['id']) - # pprint(self.get_groups()) - - ######## Example roles - # try: - # self.add_role('superman') - # except: - # self.delete_role('superman') - # self.add_role('superman') - # pprint(self.get_roles()) - - """ USERS """ - - def get_user_id(self, username): - self.connect() - return self.keycloak_admin.get_user_id(username) - - def get_users(self): - self.connect() - return self.keycloak_admin.get_users({}) - - def get_users_with_groups_and_roles(self): - q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, u.enabled, ua.value as quota - ,json_agg(g."id") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2 - ,json_agg(r.name) as role - from user_entity as u - left join user_attribute as ua on ua.user_id=u.id and ua.name = 'quota' - left join user_group_membership as ugm on ugm.user_id = u.id - left join keycloak_group as g on g.id = ugm.group_id - left join keycloak_group as g_parent on g.parent_group = g_parent.id - left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id - left join user_role_mapping as rm on rm.user_id = u.id - left join keycloak_role as r on r.id = rm.role_id - group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, u.enabled, ua.value - order by u.username""" - - (headers, users) = self.keycloak_pg.select_with_headers(q) - - users_with_lists = [ - list(l[:-4]) - + ([[]] if l[-4] == [None] else [list(set(l[-4]))]) - + ([[]] if l[-3] == [None] else [list(set(l[-3]))]) - + ([[]] if l[-3] == [None] else [list(set(l[-2]))]) - + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) - for l in users - ] - - users_with_lists = [ - list(l[:-4]) - + ([[]] if l[-4] == [None] else [list(set(l[-4]))]) - + ([[]] if l[-3] == [None] else [list(set(l[-3]))]) - + ([[]] if l[-3] == [None] else [list(set(l[-2]))]) - + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) - for l in users_with_lists - ] - - list_dict_users = [dict(zip(headers, r)) for r in users_with_lists] - - # self.connect() - # groups = self.keycloak_admin.get_groups() - - # for user in list_dict_users: - # new_user_groups = [] - # for group_id in user['group']: - # found = [g for g in groups if g['id'] == group_id][0] - # new_user_groups.append({'id':found['id'], - # 'name':found['name'], - # 'path':found['path']}) - # user['group']=new_user_groups - return list_dict_users - - def getparent(self, group_id, data): - # Recursively get full path from any group_id in the tree - path = "" - for item in data: - if group_id == item[0]: - path = self.getparent(item[2], data) - path = f"{path}/{item[1]}" - return path - - def get_group_path(self, group_id): - # Get full path using getparent recursive func - # RETURNS: String with full path - q = """SELECT * FROM keycloak_group""" - groups = self.keycloak_pg.select(q) - return self.getparent(group_id, groups) - - def get_user_groups_paths(self, user_id): - # Get full paths for user grups - # RETURNS list of paths - q = """SELECT group_id FROM user_group_membership WHERE user_id = '%s'""" % ( - user_id - ) - user_group_ids = self.keycloak_pg.select(q) - - paths = [] - for g in user_group_ids: - paths.append(self.get_group_path(g[0])) - return paths - - ## Too slow. Used the direct postgres - # def get_users_with_groups_and_roles(self): - # self.connect() - # users=self.keycloak_admin.get_users({}) - # for user in users: - # user['groups']=[g['path'] for g in self.keycloak_admin.get_user_groups(user_id=user['id'])] - # user['roles']= [r['name'] for r in self.keycloak_admin.get_realm_roles_of_user(user_id=user['id'])] - # return users - - def add_user( - self, - username, - first, - last, - email, - password, - group=False, - temporary=True, - enabled=True, - ): - # RETURNS string with keycloak user id (the main id in this app) - self.connect() - username = username.lower() - try: - uid = self.keycloak_admin.create_user( - { - "email": email, - "username": username, - "enabled": enabled, - "firstName": first, - "lastName": last, - "credentials": [ - {"type": "password", "value": password, "temporary": temporary} - ], - } - ) - except: - log.error(traceback.format_exc()) - - if group: - path = "/" + group if group[1:] != "/" else group - try: - gid = self.keycloak_admin.get_group_by_path( - path=path, search_in_subgroups=False - )["id"] - except: - self.keycloak_admin.create_group({"name": group}) - gid = self.keycloak_admin.get_group_by_path(path)["id"] - self.keycloak_admin.group_user_add(uid, gid) - return uid - - def update_user_pwd(self, user_id, password, temporary=True): - # Updates - payload = { - "credentials": [ - {"type": "password", "value": password, "temporary": temporary} - ] - } - self.connect() - return self.keycloak_admin.update_user(user_id, payload) - - def user_update(self, user_id, enabled, email, first, last, groups=[], roles=[]): - ## NOTE: Roles didn't seem to be updated/added. Also not confident with groups - # Updates - payload = { - "enabled": enabled, - "email": email, - "firstName": first, - "lastName": last, - "groups": groups, - "realmRoles": roles, - } - self.connect() - return self.keycloak_admin.update_user(user_id, payload) - - def user_enable(self, user_id): - payload = {"enabled": True} - self.connect() - return self.keycloak_admin.update_user(user_id, payload) - - def user_disable(self, user_id): - payload = {"enabled": False} - self.connect() - return self.keycloak_admin.update_user(user_id, payload) - - def group_user_remove(self, user_id, group_id): - self.connect() - return self.keycloak_admin.group_user_remove(user_id, group_id) - - # def add_user_role(self,user_id,role_id): - # self.connect() - # return self.keycloak_admin.assign_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test") - - def remove_user_realm_roles(self, user_id, roles): - self.connect() - roles = [ - r - for r in self.get_user_realm_roles(user_id) - if r["name"] in ["admin", "manager", "teacher", "student"] - ] - return self.keycloak_admin.delete_realm_roles_of_user(user_id, roles) - - def delete_user(self, userid): - self.connect() - return self.keycloak_admin.delete_user(user_id=userid) - - def get_user_groups(self, userid): - self.connect() - return self.keycloak_admin.get_user_groups(user_id=userid) - - def get_user_realm_roles(self, userid): - self.connect() - return self.keycloak_admin.get_realm_roles_of_user(user_id=userid) - - def add_user_client_role(self, client_id, user_id, role_id, role_name): - self.connect() - return self.keycloak_admin.assign_client_role( - client_id=client_id, user_id=user_id, role_id=role_id, role_name="test" - ) - - ## GROUPS - def get_all_groups(self): - ## RETURNS ONLY MAIN GROUPS WITH NESTED subGroups list - self.connect() - return self.keycloak_admin.get_groups() - - def get_recursive_groups(self, l_groups, l=[]): - for d_group in l_groups: - d = {} - for key, value in d_group.items(): - if key == "subGroups": - self.get_recursive_groups(value, l) - else: - d[key] = value - l.append(d) - return l - - def get_groups(self, with_subgroups=True): - ## RETURNS ALL GROUPS in root list - self.connect() - groups = self.keycloak_admin.get_groups() - return self.get_recursive_groups(groups) - subgroups = [] - subgroups1 = [] - # This needs to be recursive function - if with_subgroups: - for group in groups: - if len(group["subGroups"]): - for sg in group["subGroups"]: - subgroups.append(sg) - # for sgroup in subgroups: - # if len(sgroup['subGroups']): - # for sg1 in sgroup['subGroups']: - # subgroups1.append(sg1) - - return groups + subgroups + subgroups1 - - def get_group_by_id(self, group_id): - self.connect() - return self.keycloak_admin.get_group(group_id=group_id) - - def get_group_by_path(self, path, recursive=True): - self.connect() - return self.keycloak_admin.get_group_by_path( - path=path, search_in_subgroups=recursive - ) - - def add_group(self, name, parent=None, skip_exists=False): - self.connect() - if parent != None: - parent = self.get_group_by_path(parent)["id"] - return self.keycloak_admin.create_group({"name": name}, parent=parent) - - def delete_group(self, group_id): - self.connect() - return self.keycloak_admin.delete_group(group_id=group_id) - - def group_user_add(self, user_id, group_id): - self.connect() - return self.keycloak_admin.group_user_add(user_id, group_id) - - def add_group_tree(self, path): - parts = path.split("/") - parent_path = "/" - for i in range(1, len(parts)): - if i == 1: - try: - self.add_group(parts[i], None, skip_exists=True) - except: - log.warning("KEYCLOAK: Group :" + parts[i] + " already exists.") - parent_path = parent_path + parts[i] - else: - try: - self.add_group(parts[i], parent_path, skip_exists=True) - except: - log.warning("KEYCLOAK: Group :" + parts[i] + " already exists.") - parent_path = parent_path + parts[i] - - # parts=path.split('/') - # parent_path=None - # for i in range(1,len(parts)): - # # print('Adding group name '+parts[i]+' with parent path '+str(parent_path)) - # try: - # self.add_group(parts[i],parent_path,skip_exists=True) - # except: - # if parent_path==None: - # parent_path='/'+parts[i] - # else: - # parent_path=self.get_group_by_path(parent_path)['path'] - # parent_path=parent_path+'/'+parts[i] - # continue - - # if parent_path==None: - # parent_path='/'+parts[i] - # else: - # parent_path=parent_path+'/'+parts[i] - - # try: - # if i == 1: parent_id=self.add_group(parts[i]) - # except: - # # Main already exists?? What a fail! - # parent_id=self.get_group(parent_id)['id'] - # continue - # self.add_group(parts[i],parent_id) - - def add_user_with_groups_and_role( - self, username, first, last, email, password, role, groups - ): - ## Add user - uid = self.add_user(username, first, last, email, password) - ## Add user to role - log.info("User uid: " + str(uid) + " role: " + str(role)) - try: - therole = role[0] - except: - therole = "" - log.info(self.assign_realm_roles(uid, role)) - ## Create groups in user - for g in groups: - log.warning("Creating keycloak group: " + g) - parts = g.split("/") - parent_path = None - for i in range(1, len(parts)): - # parent_id=None if parent_path==None else self.get_group(parent_path)['id'] - try: - self.add_group(parts[i], parent_path, skip_exists=True) - except: - log.warning( - "Group " - + str(parent_path) - + " already exists. Skipping creation" - ) - pass - if parent_path is None: - thepath = "/" + parts[i] - else: - thepath = parent_path + "/" + parts[i] - if thepath == "/": - log.warning( - "Not adding the user " - + username - + " to any group as does not have any..." - ) - continue - gid = self.get_group_by_path(path=thepath)["id"] - - log.warning( - "Adding " - + username - + " with uuid: " - + uid - + " to group " - + g - + " with uuid: " - + gid - ) - self.keycloak_admin.group_user_add(uid, gid) - - if parent_path == None: - parent_path = "" - parent_path = parent_path + "/" + parts[i] - - # self.group_user_add(uid,gid) - - ## ROLES - def get_roles(self): - self.connect() - return self.keycloak_admin.get_realm_roles() - - def get_role(self, name): - self.connect() - return self.keycloak_admin.get_realm_role(name) - - def add_role(self, name, description=""): - self.connect() - return self.keycloak_admin.create_realm_role( - {"name": name, "description": description} - ) - - def delete_role(self, name): - self.connect() - return self.keycloak_admin.delete_realm_role(name) - - ## CLIENTS - - def get_client_roles(self, client_id): - self.connect() - return self.keycloak_admin.get_client_roles(client_id=client_id) - - def add_client_role(self, client_id, name, description=""): - self.connect() - return self.keycloak_admin.create_client_role( - client_id, {"name": name, "description": description, "clientRole": True} - ) - - ## SYSTEM - def get_server_info(self): - self.connect() - return self.keycloak_admin.get_server_info() - - def get_server_clients(self): - self.connect() - return self.keycloak_admin.get_clients() - - def get_server_rsa_key(self): - self.connect() - rsa_key = [ - k for k in self.keycloak_admin.get_keys()["keys"] if k["type"] == "RSA" - ][0] - return {"name": rsa_key["kid"], "certificate": rsa_key["certificate"]} - - ## REALM - def assign_realm_roles(self, user_id, role): - self.connect() - try: - role = [ - r for r in self.keycloak_admin.get_realm_roles() if r["name"] == role - ] - except: - return False - return self.keycloak_admin.assign_realm_roles(user_id=user_id, roles=role) - # return self.keycloak_admin.assign_realm_roles(user_id=user_id, client_id=None, roles=role) - - ## CLIENTS - def delete_client(self, clientid): - self.connect() - return self.keycloak_admin.delete_client(clientid) - - def add_client(self, client): - self.connect() - return self.keycloak_admin.create_client(client) diff --git a/dd-sso/admin/src/saml_scripts/lib/mysql.py b/dd-sso/admin/src/saml_scripts/lib/mysql.py deleted file mode 100644 index 9f3f140..0000000 --- a/dd-sso/admin/src/saml_scripts/lib/mysql.py +++ /dev/null @@ -1,50 +0,0 @@ -# -# Copyright © 2021,2022 IsardVDI S.L. -# -# This file is part of DD -# -# DD is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or (at your -# option) any later version. -# -# DD is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License -# along with DD. If not, see . -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -import json -import logging as log -import time -import traceback -from datetime import datetime, timedelta - -import mysql.connector -import yaml - -# from admin import app - - -class Mysql: - def __init__(self, host, database, user, password): - self.conn = mysql.connector.connect( - host=host, database=database, user=user, password=password - ) - - def select(self, sql): - self.cur = self.conn.cursor() - self.cur.execute(sql) - data = self.cur.fetchall() - self.cur.close() - return data - - def update(self, sql): - self.cur = self.conn.cursor() - self.cur.execute(sql) - self.conn.commit() - self.cur.close() diff --git a/dd-sso/admin/src/saml_scripts/lib/postgres.py b/dd-sso/admin/src/saml_scripts/lib/postgres.py deleted file mode 100644 index 84b82ad..0000000 --- a/dd-sso/admin/src/saml_scripts/lib/postgres.py +++ /dev/null @@ -1,71 +0,0 @@ -# -# Copyright © 2021,2022 IsardVDI S.L. -# -# This file is part of DD -# -# DD is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or (at your -# option) any later version. -# -# DD is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License -# along with DD. If not, see . -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -import json -import logging as log -import time -import traceback -from datetime import datetime, timedelta - -import psycopg2 -import yaml - -# from admin import app - - -class Postgres: - def __init__(self, host, database, user, password): - self.conn = psycopg2.connect( - host=host, database=database, user=user, password=password - ) - - # def __del__(self): - # self.cur.close() - # self.conn.close() - - def select(self, sql): - self.cur = self.conn.cursor() - self.cur.execute(sql) - data = self.cur.fetchall() - self.cur.close() - return data - - def update(self, sql): - self.cur = self.conn.cursor() - self.cur.execute(sql) - self.conn.commit() - self.cur.close() - # return self.cur.fetchall() - - def select_with_headers(self, sql): - self.cur = self.conn.cursor() - self.cur.execute(sql) - data = self.cur.fetchall() - fields = [a.name for a in self.cur.description] - self.cur.close() - return (fields, data) - - # def update_moodle_saml_plugin(self): - # plugin[('idpmetadata', 'NrtA5ynG0htowP3SXw7dBJRIAMxn-1PwuuXwOwNhlRwMIICmzCCAYMCBgF5jb0RCTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwNTIxMDcwMjI4WhcNMzEwNTIxMDcwNDA4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCI8xh/C0+frz3kgWiUbziTDls71R2YiXLSVE+bw7gbEgZUGCLhoEI679azMtIxmnzM/snIX+yTb12+XoYkgbiLTMPQfnH+Kiab6g3HL3KPfhqS+yWkFxOoCp6Ibmp7yPlVWuHH+MBfO8OBr/r8Ao7heFbuzjiLd1KG67rcoaxfDgMuBoEomg1bgEjFgHaQIrSC6OZzH0h987/arqufZXeXlfyiqScMPUi+u5IpDWSwz06UKP0k8mxzNSlpZ93CKOUSsV0SMLxqg7FQ3SGiOk577bGW9o9BDTkkmSo3Up6smc0LzwvvUwuNd0B1irGkWZFQN9OXJnJYf1InEebIMtmPAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADM34+qEGeBQ22luphVTuVJtGxcbxLx7DfsT0QfJD/OuxTTbNAa1VRyarb5juIAkqdj4y2quZna9ZXLecVo4RkwpzPoKoAkYA8b+kHnWqEwJi9iPrDvKb+GR0bBkLPN49YxIZ8IdKX/PRa3yuLHe+loiNsCaS/2ZK2KO46COsqU4QX1iVhF9kWphNLybjNAX45B6cJLsa1g0vXLdm3kv3SB4I2fErFVaOoDtFIjttoYlXdpUiThkPXBfr7N67P3dZHaS4tjJh+IZ8I6TINpcsH8dBkUhzYEIPHCePwSiC1w6WDBLNDuKt1mj1CZrLq+1x+Yhrs+QNRheEKGi89HZ8N0=urn:oasis:names:tc:SAML:2.0:nameid-format:persistenturn:oasis:names:tc:SAML:2.0:nameid-format:transienturn:oasis:names:tc:SAML:1.1:nameid-format:unspecifiedurn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress')] - # pg_update = """UPDATE mdl_config_plugins set title = %s where plugin = auth_saml2 and name =""" - # cursor.execute(pg_update, (title, bookid)) - # connection.commit() - # count = cursor.rowcount - # print(count, "Successfully Updated!") diff --git a/dd-sso/admin/src/saml_scripts/moodle_saml.py b/dd-sso/admin/src/saml_scripts/moodle_saml.py index 20d39b3..cf85418 100644 --- a/dd-sso/admin/src/saml_scripts/moodle_saml.py +++ b/dd-sso/admin/src/saml_scripts/moodle_saml.py @@ -30,8 +30,8 @@ from datetime import datetime, timedelta import psycopg2 import yaml -from lib.keycloak_client import KeycloakClient -from lib.postgres import Postgres +from admin.lib.keycloak_client import KeycloakClient +from admin.lib.postgres import Postgres app = {} app["config"] = {} @@ -93,7 +93,7 @@ class MoodleSaml: time.sleep(2) except: log.error(traceback.format_exc()) - log.info("Got moodle srt certificate.") + log.info("Got moodle crt certificate.") ready = False while not ready: diff --git a/dd-sso/admin/src/saml_scripts/nextcloud_saml.py b/dd-sso/admin/src/saml_scripts/nextcloud_saml.py index 863cde6..c09559a 100644 --- a/dd-sso/admin/src/saml_scripts/nextcloud_saml.py +++ b/dd-sso/admin/src/saml_scripts/nextcloud_saml.py @@ -1,4 +1,5 @@ # +# Copyright © 2022 Evilham # Copyright © 2021,2022 IsardVDI S.L. # # This file is part of DD @@ -18,255 +19,57 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later -import json -import logging as log import os -import pprint -import random -import string -import time -import traceback -from datetime import datetime, timedelta +from typing import Any, Dict, Optional -import psycopg2 -import yaml -from lib.keycloak_client import KeycloakClient -from lib.postgres import Postgres +from admin.lib.postgres import Postgres -app = {} -app["config"] = {} +from saml_service import SamlService +class NextcloudSaml(SamlService): + client_description : str = "Nextcloud Service Provider" + pg : Optional[Postgres] -class NextcloudSaml: - def __init__(self): - self.url = "http://dd-sso-keycloak:8080/auth/" - self.username = os.environ["KEYCLOAK_USER"] - self.password = os.environ["KEYCLOAK_PASSWORD"] - self.realm = "master" - self.verify = True + def __init__(self) -> None: + super(NextcloudSaml, self).__init__("nextcloud") - ready = False - while not ready: - try: - self.pg = Postgres( - "dd-apps-postgresql", - "nextcloud", - os.environ["NEXTCLOUD_POSTGRES_USER"], - os.environ["NEXTCLOUD_POSTGRES_PASSWORD"], - ) - ready = True - except: - log.warning("Could not connect to nextcloud database. Retrying...") - time.sleep(2) - log.info("Connected to nextcloud database.") - - basepath = os.path.dirname(__file__) - - ready = False - while not ready: - try: - with open( - os.path.abspath( - os.path.join(basepath, "../saml_certs/public.cert") - ), - "r", - ) as crt: - app["config"]["PUBLIC_CERT"] = crt.read() - ready = True - except IOError: - log.warning( - "Could not get public certificate to be used in nextcloud. Retrying..." - ) - log.warning( - " You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert" - ) - time.sleep(2) - except: - log.error(traceback.format_exc()) - log.info("Got moodle srt certificate to be used in nextcloud.") - - ready = False - while not ready: - try: - with open( - os.path.abspath( - os.path.join(basepath, "../saml_certs/private.key") - ), - "r", - ) as pem: - app["config"]["PRIVATE_KEY"] = pem.read() - ready = True - except IOError: - log.warning( - "Could not get private key to be used in nextcloud. Retrying..." - ) - log.warning( - " You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert" - ) - time.sleep(2) - log.info("Got moodle pem certificate to be used in nextcloud.") - - # ## This seems related to the fact that the certificate generated the first time does'nt work. - # ## And when regenerating the certificate de privatekeypass seems not to be used and instead it - # ## will use always this code as filename: 0f635d0e0f3874fff8b581c132e6c7a7 - # ## As this bug I'm not able to solve, the process is: - # ## 1.- Bring up moodle and regenerate certificates on saml2 plugin in plugins-authentication - # ## 2.- Execute this script - # ## 3.- Cleanup all caches in moodle (Development tab) - # # with open(os.path.join("./moodledata/saml2/"+os.environ['NEXTCLOUD_SAML_PRIVATEKEYPASS'].replace("moodle."+os.environ['DOMAIN'],'')+'.idp.xml'),"w") as xml: - # # xml.write(self.parse_idp_metadata()) - # with open(os.path.join("./moodledata/saml2/0f635d0e0f3874fff8b581c132e6c7a7.idp.xml"),"w") as xml: - # xml.write(self.parse_idp_metadata()) - - try: - self.reset_saml() - except: - print("Error resetting saml on nextcloud") - - try: - self.delete_keycloak_nextcloud_saml_plugin() - except: - print("Error resetting saml on keycloak") - - try: - self.set_nextcloud_saml_plugin() - except: - log.error(traceback.format_exc()) - print("Error adding saml on nextcloud") - - try: - self.add_keycloak_nextcloud_saml() - except: - print("Error adding saml on keycloak") - - def connect(self): - self.keycloak = KeycloakClient( - url=self.url, - username=self.username, - password=self.password, - realm=self.realm, - verify=self.verify, + def connect_to_db(self) -> None: + """ + This is defined, though not currently used. + """ + self.pg = Postgres( + "dd-apps-postgresql", + "nextcloud", + os.environ["NEXTCLOUD_POSTGRES_USER"], + os.environ["NEXTCLOUD_POSTGRES_PASSWORD"], ) - # def activate_saml_plugin(self): - # ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php - # return self.pg.update("""UPDATE "mdl_config" SET value = 'email,saml2' WHERE "name" = 'auth'""") + def configure(self) -> None: + srv_base = f"https://nextcloud.{self.domain}" + nc_saml_url= f"{srv_base}/apps/user_saml/saml" + nc_redir_url = f"{nc_saml_url}/acs" + nc_logout_url = f"{nc_saml_url}/sls" + client_id = f"{nc_saml_url}/metadata" + client_overrides : Dict[str, Any] = { + "name": self.client_name, + "description": self.client_description, + "clientId": client_id, - # def get_privatekey_pass(self): - # return self.pg.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2] - - def parse_idp_cert(self): - self.connect() - rsa = self.keycloak.get_server_rsa_key() - self.keycloak = None - return rsa["certificate"] - - def set_keycloak_nextcloud_saml_plugin(self): - self.connect() - self.keycloak.add_nextcloud_client() - self.keycloak = None - - def delete_keycloak_nextcloud_saml_plugin(self): - self.connect() - self.keycloak.delete_client("bef873f0-2079-4876-8657-067de27d01b7") - self.keycloak = None - - def set_nextcloud_saml_plugin(self): - self.pg.update( - """INSERT INTO "oc_appconfig" ("appid", "configkey", "configvalue") VALUES -('user_saml', 'general-uid_mapping', 'username'), -('user_saml', 'type', 'saml'), -('user_saml', 'sp-privateKey', '%s'), -('user_saml', 'saml-attribute-mapping-email_mapping', 'email'), -('user_saml', 'saml-attribute-mapping-quota_mapping', 'quota'), -('user_saml', 'saml-attribute-mapping-displayName_mapping', 'displayname'), -('user_saml', 'saml-attribute-mapping-group_mapping', 'member'), -('user_saml', 'idp-entityId', 'https://sso.%s/auth/realms/master'), -('user_saml', 'idp-singleSignOnService.url', 'https://sso.%s/auth/realms/master/protocol/saml'), -('user_saml', 'idp-x509cert', '%s'), -('user_saml', 'security-authnRequestsSigned', '1'), -('user_saml', 'security-logoutRequestSigned', '1'), -('user_saml', 'security-logoutResponseSigned', '1'), -('user_saml', 'security-wantMessagesSigned', '1'), -('user_saml', 'security-wantAssertionsSigned', '1'), -('user_saml', 'general-idp0_display_name', 'SAML Login'), -('user_saml', 'sp-x509cert', '%s'), -('user_saml', 'idp-singleLogoutService.url', 'https://sso.%s/auth/realms/master/protocol/saml');""" - % ( - app["config"]["PRIVATE_KEY"], - os.environ["DOMAIN"], - os.environ["DOMAIN"], - self.parse_idp_cert(), - app["config"]["PUBLIC_CERT"], - os.environ["DOMAIN"], - ) - ) - - def reset_saml(self): - cfg_list = [ - "general-uid_mapping", - "sp-privateKey", - "saml-attribute-mapping-email_mapping", - "saml-attribute-mapping-quota_mapping", - "saml-attribute-mapping-displayName_mapping", - "saml-attribute-mapping-group_mapping", - "idp-entityId", - "idp-singleSignOnService.url", - "idp-x509cert", - "security-authnRequestsSigned", - "security-logoutRequestSigned", - "security-logoutResponseSigned", - "security-wantMessagesSigned", - "security-wantAssertionsSigned", - "general-idp0_display_name", - "type", - "sp-x509cert", - "idp-singleLogoutService.url", - ] - for cfg in cfg_list: - self.pg.update( - """DELETE FROM "oc_appconfig" WHERE appid = 'user_saml' AND configkey = '%s'""" - % (cfg) - ) - - def add_keycloak_nextcloud_saml(self): - client = { - "id": "bef873f0-2079-4876-8657-067de27d01b7", - "name": "nextcloud", - "description": "nextcloud", - "clientId": "https://nextcloud." - + os.environ["DOMAIN"] - + "/apps/user_saml/saml/metadata", - "surrogateAuthRequired": False, "enabled": True, - "alwaysDisplayInConsole": False, - "clientAuthenticatorType": "client-secret", "redirectUris": [ - "https://nextcloud." + os.environ["DOMAIN"] + "/apps/user_saml/saml/acs" + nc_redir_url ], - "webOrigins": ["https://nextcloud." + os.environ["DOMAIN"]], - "notBefore": 0, - "bearerOnly": False, - "consentRequired": False, - "standardFlowEnabled": True, - "implicitFlowEnabled": False, - "directAccessGrantsEnabled": False, - "serviceAccountsEnabled": False, - "publicClient": False, "frontchannelLogout": True, "protocol": "saml", + "webOrigins": [srv_base], "attributes": { "saml.assertion.signature": True, "saml.force.post.binding": True, - "saml_assertion_consumer_url_post": "https://nextcloud." - + os.environ["DOMAIN"] - + "/apps/user_saml/saml/acs", + "saml_assertion_consumer_url_post": nc_redir_url, "saml.server.signature": True, "saml.server.signature.keyinfo.ext": False, - "saml.signing.certificate": app["config"]["PUBLIC_CERT"], - "saml_single_logout_service_url_redirect": "https://nextcloud." - + os.environ["DOMAIN"] - + "/apps/user_saml/saml/sls", + "saml.signing.certificate": self.public_cert, + "saml_single_logout_service_url_redirect": nc_logout_url, "saml.signature.algorithm": "RSA_SHA256", "saml_force_name_id_format": False, "saml.client.signature": False, @@ -274,12 +77,8 @@ class NextcloudSaml: "saml_name_id_format": "username", "saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#", }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": True, - "nodeReRegistrationTimeout": -1, "protocolMappers": [ { - "id": "e8e4acff-da2b-46aa-8bdb-ba42171671d6", "name": "username", "protocol": "saml", "protocolMapper": "saml-user-attribute-mapper", @@ -292,7 +91,6 @@ class NextcloudSaml: }, }, { - "id": "8ab13cd7-822a-40d5-a1e1-9f556aed2332", "name": "quota", "protocol": "saml", "protocolMapper": "saml-user-attribute-mapper", @@ -305,7 +103,6 @@ class NextcloudSaml: }, }, { - "id": "28206b59-757b-4e3c-81cb-0b6053b1fd3d", "name": "email", "protocol": "saml", "protocolMapper": "saml-user-property-mapper", @@ -318,7 +115,6 @@ class NextcloudSaml: }, }, { - "id": "5176a593-180f-4924-b294-b83a0d8d5972", "name": "displayname", "protocol": "saml", "protocolMapper": "saml-javascript-mapper", @@ -332,7 +128,6 @@ class NextcloudSaml: }, }, { - "id": "e51e04b9-f71a-42de-819e-dd9285246ada", "name": "Roles", "protocol": "saml", "protocolMapper": "saml-role-list-mapper", @@ -345,7 +140,6 @@ class NextcloudSaml: }, }, { - "id": "9c101249-bb09-4cc8-8f75-5a18fcb307e6", "name": "group_list", "protocol": "saml", "protocolMapper": "saml-group-membership-mapper", @@ -359,24 +153,11 @@ class NextcloudSaml: }, }, ], - "defaultClientScopes": [ - "web-origins", - "role_list", - "roles", - "profile", - "email", - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt", - ], - "access": {"view": True, "configure": True, "manage": True}, } - self.connect() - self.keycloak.add_client(client) - self.keycloak = None + self.set_client(client_id, client_overrides) -n = NextcloudSaml() +if __name__ == "__main__": + import logging as log + log.info("Configuring SAML client for Nextcloud") + NextcloudSaml().configure() diff --git a/dd-sso/admin/src/saml_scripts/saml_service.py b/dd-sso/admin/src/saml_scripts/saml_service.py new file mode 100644 index 0000000..1e3f45e --- /dev/null +++ b/dd-sso/admin/src/saml_scripts/saml_service.py @@ -0,0 +1,119 @@ +# +# Copyright © 2022 Evilham +# +# This file is part of DD +# +# DD is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. +# +# DD is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License +# along with DD. If not, see . +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +import logging as log +import os +import time +import traceback +from typing import Any, Dict + +from admin.lib.keycloak_client import KeycloakClient + +try: + # Python 3.9+ + from functools import cache # type: ignore # Currently targetting 3.8 +except ImportError: + # Up to python 3.8 + from functools import cached_property as cache + + +class SamlService(object): + """ + Generic class to manage a SAML service on keycloak. + + This is currently only used by Email and Nextcloud, but the plan is to + migrate other *_saml.py scripts to use this. + """ + + keycloak: KeycloakClient + client_name: str + domain: str = os.environ["DOMAIN"] + + def __init__(self, client_name : str): + self.keycloak = KeycloakClient() + self.client_name = client_name + + @cache + def public_cert(self) -> str: + """ + Read the public SAML certificate as used by keycloak + """ + ready = False + basepath = os.path.dirname(__file__) + crt = "" + while not ready: + # TODO: Check why they were using a loop + try: + with open( + os.path.abspath( + os.path.join(basepath, f"../saml_certs/{self.client_name}.crt") + ), + "r", + ) as crtf: + crt = crtf.read() + ready = True + except IOError: + log.warning(f"Could not get public certificate for {self.client_name} SAML. Retrying...") + time.sleep(2) + except: + log.error(traceback.format_exc()) + log.info("Got public SAML certificate") + return crt + + def wait_for_database(self) -> None: + """ + Helper function to wait for a database to be up. + """ + ready = False + while not ready: + try: + self.connect_to_db() + ready = True + except: + log.warning("Could not connect to {self.client_name} database. Retrying...") + time.sleep(2) + log.info("Connected to {self.client_name} database.") + + def connect_to_db(self) -> None: + """ + This should be overriden in subclasses. + """ + pass + + def get_client(self, client_id: str) -> Any: + # TODO: merge with keycloak_config.py + self.keycloak.connect() + k = self.keycloak.keycloak_admin + + clients = k.get_clients() + client = next(filter(lambda c: c["clientId"] == client_id, clients), None) + return (k, client) + + def set_client(self, client_id: str, client_overrides: Dict[str, Any]) -> str: + (k, client) = self.get_client(client_id) + if client is None: + client_uid = k.create_client(client_overrides) + else: + client_uid = client["id"] + k.update_client(client_uid, client_overrides) + return client_id + + def configure(self) -> None: + pass diff --git a/dd-sso/admin/src/saml_scripts/wordpress_saml.py b/dd-sso/admin/src/saml_scripts/wordpress_saml.py index 7aa7706..73cfe42 100644 --- a/dd-sso/admin/src/saml_scripts/wordpress_saml.py +++ b/dd-sso/admin/src/saml_scripts/wordpress_saml.py @@ -30,8 +30,8 @@ from datetime import datetime, timedelta import psycopg2 import yaml -from lib.keycloak_client import KeycloakClient -from lib.mysql import Mysql +from admin.lib.keycloak_client import KeycloakClient +from admin.lib.mysql import Mysql app = {} app["config"] = {} @@ -72,7 +72,7 @@ class WordpressSaml: try: with open( os.path.abspath( - os.path.join(basepath, "../saml_certs/public.cert") + os.path.join(basepath, "../saml_certs/wordpress.crt") ), "r", ) as crt: @@ -98,7 +98,7 @@ class WordpressSaml: try: with open( os.path.abspath( - os.path.join(basepath, "../saml_certs/private.key") + os.path.join(basepath, "../saml_certs/wordpress.key") ), "r", ) as pem: diff --git a/dd-sso/admin/src/wordpress_saml_onlycerts.py b/dd-sso/admin/src/wordpress_saml_onlycerts.py deleted file mode 100644 index db3eac6..0000000 --- a/dd-sso/admin/src/wordpress_saml_onlycerts.py +++ /dev/null @@ -1,178 +0,0 @@ -# -# Copyright © 2021,2022 IsardVDI S.L. -# -# This file is part of DD -# -# DD is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or (at your -# option) any later version. -# -# DD is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License -# along with DD. If not, see . -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -import json -import logging as log -import os -import pprint -import random -import string -import time -import traceback -from datetime import datetime, timedelta - -import psycopg2 -import yaml -from admin.lib.keycloak_client import KeycloakClient -from admin.lib.mysql import Mysql - -app = {} -app["config"] = {} - - -class WordpressSaml: - def __init__(self): - self.url = "http://dd-sso-keycloak:8080/auth/" - self.username = os.environ["KEYCLOAK_USER"] - self.password = os.environ["KEYCLOAK_PASSWORD"] - self.realm = "master" - self.verify = True - - ready = False - while not ready: - try: - self.db = Mysql( - "dd-apps-mariadb", - "wordpress", - os.environ["WORDPRESS_MARIADB_USER"], - os.environ["WORDPRESS_MARIADB_PASSWORD"], - ) - ready = True - except: - log.warning("Could not connect to wordpress database. Retrying...") - time.sleep(2) - log.info("Connected to wordpress database.") - - ready = False - while not ready: - try: - with open(os.path.join("./saml_certs/public.cert"), "r") as crt: - app["config"]["PUBLIC_CERT_RAW"] = crt.read() - app["config"]["PUBLIC_CERT"] = self.cert_prepare( - app["config"]["PUBLIC_CERT_RAW"] - ) - ready = True - except IOError: - log.warning( - "Could not get public certificate to be used in wordpress. Retrying..." - ) - log.warning( - " You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert" - ) - time.sleep(2) - except: - log.error(traceback.format_exc()) - log.info("Got moodle srt certificate to be used in wordpress.") - - ready = False - while not ready: - try: - with open(os.path.join("./saml_certs/private.key"), "r") as pem: - app["config"]["PRIVATE_KEY"] = self.cert_prepare(pem.read()) - ready = True - except IOError: - log.warning( - "Could not get private key to be used in wordpress. Retrying..." - ) - log.warning( - " You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert" - ) - time.sleep(2) - log.info("Got moodle pem certificate to be used in wordpress.") - - # ## This seems related to the fact that the certificate generated the first time does'nt work. - # ## And when regenerating the certificate de privatekeypass seems not to be used and instead it - # ## will use always this code as filename: 0f635d0e0f3874fff8b581c132e6c7a7 - # ## As this bug I'm not able to solve, the process is: - # ## 1.- Bring up moodle and regenerate certificates on saml2 plugin in plugins-authentication - # ## 2.- Execute this script - # ## 3.- Cleanup all caches in moodle (Development tab) - # # with open(os.path.join("./moodledata/saml2/"+os.environ['NEXTCLOUD_SAML_PRIVATEKEYPASS'].replace("moodle."+os.environ['DOMAIN'],'')+'.idp.xml'),"w") as xml: - # # xml.write(self.parse_idp_metadata()) - # with open(os.path.join("./moodledata/saml2/0f635d0e0f3874fff8b581c132e6c7a7.idp.xml"),"w") as xml: - # xml.write(self.parse_idp_metadata()) - - try: - self.reset_saml_certs() - except: - print(traceback.format_exc()) - print("Error resetting saml certs on wordpress") - - try: - self.set_wordpress_saml_plugin_certs() - except: - print(traceback.format_exc()) - print("Error adding saml on wordpress") - - # SAML clients don't work well with composite roles so disabling and adding on realm - # self.add_client_roles() - - # def activate_saml_plugin(self): - # ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php - # return self.db.update("""UPDATE "mdl_config" SET value = 'email,saml2' WHERE "name" = 'auth'""") - - # def get_privatekey_pass(self): - # return self.db.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2] - - def connect(self): - self.keycloak = KeycloakClient( - url=self.url, - username=self.username, - password=self.password, - realm=self.realm, - verify=self.verify, - ) - - def cert_prepare(self, cert): - return "".join(cert.split("-----")[2].splitlines()) - - def parse_idp_cert(self): - self.connect() - rsa = self.keycloak.get_server_rsa_key() - self.keycloak = None - return rsa["certificate"] - - def set_wordpress_saml_plugin_certs(self): - # ('active_plugins', 'a:2:{i:0;s:33:\"edwiser-bridge/edwiser-bridge.php\";i:1;s:17:\"onelogin_saml.php\";}', 'yes'), - self.db.update( - """INSERT INTO wp_options (option_name, option_value, autoload) VALUES -('onelogin_saml_idp_x509cert', '%s', 'yes'), -('onelogin_saml_advanced_settings_sp_x509cert', '%s', 'yes'), -('onelogin_saml_advanced_settings_sp_privatekey', '%s', 'yes');""" - % ( - self.parse_idp_cert(), - app["config"]["PUBLIC_CERT"], - app["config"]["PRIVATE_KEY"], - ) - ) - - def reset_saml_certs(self): - self.db.update( - """DELETE FROM wp_options WHERE option_name = 'onelogin_saml_idp_x509cert'""" - ) - self.db.update( - """DELETE FROM wp_options WHERE option_name = 'onelogin_saml_advanced_settings_sp_x509cert'""" - ) - self.db.update( - """DELETE FROM wp_options WHERE option_name = 'onelogin_saml_advanced_settings_sp_privatekey'""" - ) - - -nw = WordpressSaml() diff --git a/dd-sso/docker-compose-parts/admin.yml b/dd-sso/docker-compose-parts/admin.yml index 4dd8489..054baf9 100644 --- a/dd-sso/docker-compose-parts/admin.yml +++ b/dd-sso/docker-compose-parts/admin.yml @@ -35,7 +35,7 @@ services: - ${CUSTOM_PATH}/custom:/admin/custom:rw - ${DATA_FOLDER}/avatars:/admin/avatars:ro - ${DATA_FOLDER}/moodle/saml2:/admin/moodledata/saml2:rw - - ${DATA_FOLDER}/saml_certs:/admin/saml_certs:rw + - ${DATA_FOLDER}/saml/public:/admin/saml_certs:ro - ${DATA_FOLDER}/legal:/admin/admin/static/templates/pages/legal:rw - ${DATA_FOLDER}/dd-admin:/data:rw - ${DATA_FOLDER}/nc-mail-queue:/nc-mail-queue:rw @@ -49,6 +49,7 @@ services: - CUSTOM_FOLDER=/admin/custom - NC_MAIL_QUEUE_FOLDER=/nc-mail-queue - LEGAL_PATH=/admin/admin/static/templates/pages/legal + - PYTHONPATH=/admin - AVATARS_SERVER_HOST=dd-sso-avatars:9000 - AVATARS_ACCESS_KEY=${AVATARS_ACCESS_KEY:-AKIAIOSFODNN7EXAMPLE} - AVATARS_SECRET_KEY=${AVATARS_SECRET_KEY:-wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY}