#!/usr/bin/env python # coding=utf-8 import time, os from datetime import datetime, timedelta import pprint import logging as log import traceback import yaml, json import psycopg2 from admin.lib.postgres import Postgres from admin.lib.keycloak_client import KeycloakClient import string, random 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-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" : "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" : "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" } } ], "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()