#!/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 import Keycloak import string, random app={} app['config']={} class MoodleSaml(): def __init__(self): ready=False while not ready: try: self.pg=Postgres('isard-apps-postgresql','moodle',os.environ['MOODLE_POSTGRES_USER'],os.environ['MOODLE_POSTGRES_PASSWORD']) ready=True except: log.warning('Could not connect to moodle database. Retrying...') time.sleep(2) log.info('Connected to moodle database.') ready=False while not ready: try: privatekey_pass=self.get_privatekey_pass() log.warning("The key: "+str(privatekey_pass)) if privatekey_pass.endswith(os.environ['DOMAIN']): app['config']['MOODLE_SAML_PRIVATEKEYPASS']=privatekey_pass ready=True except: # print(traceback.format_exc()) log.warning('Could not get moodle site identifier. Retrying...') time.sleep(2) log.info('Got moodle site identifier.') ready=False while not ready: try: with open(os.path.join("./moodledata/saml2/moodle."+os.environ['DOMAIN']+".crt"),"r") as crt: app['config']['SP_CRT']=crt.read() ready=True except IOError: log.warning('Could not get moodle SAML2 crt certificate. Retrying...') time.sleep(2) except: log.error(traceback.format_exc()) log.info('Got moodle srt certificate.') ready=False while not ready: try: with open(os.path.join("./moodledata/saml2/moodle."+os.environ['DOMAIN']+".pem"),"r") as pem: app['config']['SP_PEM']=pem.read() ready=True except IOError: log.warning('Could not get moodle SAML2 pem certificate. Retrying...') time.sleep(2) log.info('Got moodle pem certificate.') ## 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['MOODLE_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()) log.info('Written SP file on moodledata.') try: self.activate_saml_plugin() except: print('Error activating saml on moodle') try: self.set_moodle_saml_plugin() except: print('Error setting saml on moodle') try: self.delete_keycloak_moodle_saml_plugin() except: print('Error deleting saml on keycloak') try: self.add_keycloak_moodle_saml() except: print('Error adding saml on keycloak') 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_metadata(self): keycloak=Keycloak() rsa=keycloak.get_server_rsa_key() keycloak=None return ''+rsa['name']+''+rsa['certificate']+'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' def set_keycloak_moodle_saml_plugin(self): keycloak=Keycloak() keycloak.add_moodle_client() keycloak=None def delete_keycloak_moodle_saml_plugin(self): keycloak=Keycloak() keycloak.delete_client('a92d5417-92b6-4678-9cb9-51bc0edcee8c') keycloak=None def set_moodle_saml_plugin(self): config={'idpmetadata': self.parse_idp_metadata(), 'certs_locked': '1', 'duallogin': '0', 'idpattr': 'username', 'autocreate': '1', 'saml_role_siteadmin_map': 'admin', 'saml_role_coursecreator_map': 'teacher', 'saml_role_manager_map': 'manager', 'field_map_email': 'email', 'field_map_firstname': 'givenName', 'field_map_lastname': 'sn'} for name in config.keys(): self.pg.update("""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'auth_saml2' AND "name" = '%s'""" % (config[name],name)) self.pg.update("""INSERT INTO "mdl_auth_saml2_idps" ("metadataurl", "entityid", "activeidp", "defaultidp", "adminidp", "defaultname", "displayname", "logo", "alias", "whitelist") VALUES ('xml', 'https://sso.%s/auth/realms/master', 1, 0, 0, 'Login via SAML2', '', NULL, NULL, NULL);""" % (os.environ['DOMAIN'])) def add_keycloak_moodle_saml(self): client={ "id" : "a92d5417-92b6-4678-9cb9-51bc0edcee8c", "name": "moodle", "description": "moodle", "clientId" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/metadata.php", "surrogateAuthRequired" : False, "enabled" : True, "alwaysDisplayInConsole" : False, "clientAuthenticatorType" : "client-secret", "redirectUris" : [ "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+os.environ['DOMAIN']+"" ], "webOrigins" : [ "https://moodle."+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.force.post.binding" : True, "saml.encrypt" : False, "saml_assertion_consumer_url_post" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+os.environ['DOMAIN']+"", "saml.server.signature" : True, "saml.server.signature.keyinfo.ext" : False, "saml.signing.certificate" : app['config']['SP_CRT'], "saml_single_logout_service_url_redirect" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-logout.php/moodle."+os.environ['DOMAIN']+"", "saml.signature.algorithm" : "RSA_SHA256", "saml_force_name_id_format" : False, "saml.client.signature" : True, "saml.encryption.certificate" : app['config']['SP_PEM'], "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" : "9296daa3-4fc4-4b80-b007-5070f546ae13", "name" : "X500 sn", "protocol" : "saml", "protocolMapper" : "saml-user-property-mapper", "consentRequired" : False, "config" : { "attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "user.attribute" : "lastName", "friendly.name" : "sn", "attribute.name" : "urn:oid:2.5.4.4" } }, { "id" : "ccecf6e4-d20a-4211-b67c-40200a6b2c5d", "name" : "username", "protocol" : "saml", "protocolMapper" : "saml-user-property-mapper", "consentRequired" : False, "config" : { "attribute.nameformat" : "Basic", "user.attribute" : "username", "friendly.name" : "username", "attribute.name" : "username" } }, { "id" : "53858403-eba2-4f6d-81d0-cced700b5719", "name" : "X500 givenName", "protocol" : "saml", "protocolMapper" : "saml-user-property-mapper", "consentRequired" : False, "config" : { "attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "user.attribute" : "firstName", "friendly.name" : "givenName", "attribute.name" : "urn:oid:2.5.4.42" } }, { "id" : "20034db5-1d0e-4e66-b815-fb0440c6d1e2", "name" : "X500 email", "protocol" : "saml", "protocolMapper" : "saml-user-property-mapper", "consentRequired" : False, "config" : { "attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "user.attribute" : "email", "friendly.name" : "email", "attribute.name" : "urn:oid:1.2.840.113549.1.9.1" } } ], "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ], "access" : { "view" : True, "configure" : True, "manage" : True } } keycloak=Keycloak() keycloak.add_client(client) keycloak=None m=MoodleSaml()