#!/usr/bin/env python # coding=utf-8 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://isard-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( "isard-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() 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 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_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" ], "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", "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.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.signature.algorithm": "RSA_SHA256", "saml_force_name_id_format": False, "saml.client.signature": False, "saml.authnstatement": True, "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", "consentRequired": False, "config": { "attribute.nameformat": "Basic", "user.attribute": "username", "friendly.name": "username", "attribute.name": "username", }, }, { "id": "8ab13cd7-822a-40d5-a1e1-9f556aed2332", "name": "quota", "protocol": "saml", "protocolMapper": "saml-user-attribute-mapper", "consentRequired": False, "config": { "attribute.nameformat": "Basic", "user.attribute": "quota", "friendly.name": "quota", "attribute.name": "quota", }, }, { "id": "28206b59-757b-4e3c-81cb-0b6053b1fd3d", "name": "email", "protocol": "saml", "protocolMapper": "saml-user-property-mapper", "consentRequired": False, "config": { "attribute.nameformat": "Basic", "user.attribute": "email", "friendly.name": "email", "attribute.name": "email", }, }, { "id": "5176a593-180f-4924-b294-b83a0d8d5972", "name": "displayname", "protocol": "saml", "protocolMapper": "saml-javascript-mapper", "consentRequired": False, "config": { "single": False, "Script": '/**\n * Available variables: \n * user - the current user\n * realm - the current realm\n * clientSession - the current clientSession\n * userSession - the current userSession\n * keycloakSession - the current keycloakSession\n */\n\n\n//insert your code here...\nvar Output = user.getFirstName()+" "+user.getLastName();\nOutput;\n', "attribute.nameformat": "Basic", "friendly.name": "displayname", "attribute.name": "displayname", }, }, { "id": "e51e04b9-f71a-42de-819e-dd9285246ada", "name": "Roles", "protocol": "saml", "protocolMapper": "saml-role-list-mapper", "consentRequired": False, "config": { "single": True, "attribute.nameformat": "Basic", "friendly.name": "Roles", "attribute.name": "Roles", }, }, { "id": "9c101249-bb09-4cc8-8f75-5a18fcb307e6", "name": "group_list", "protocol": "saml", "protocolMapper": "saml-group-membership-mapper", "consentRequired": False, "config": { "single": True, "attribute.nameformat": "Basic", "full.path": False, "friendly.name": "member", "attribute.name": "member", }, }, ], "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 n = NextcloudSaml()