#!/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 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') # 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.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=KeycloakClient() 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=KeycloakClient() keycloak.add_moodle_client() keycloak=None def delete_keycloak_moodle_saml_plugin(self): keycloak=KeycloakClient() 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=KeycloakClient() keycloak.add_client(client) keycloak=None def add_client_roles(self): keycloak=KeycloakClient() keycloak.add_client_role('a92d5417-92b6-4678-9cb9-51bc0edcee8c','admin','Moodle admins') keycloak.add_client_role('a92d5417-92b6-4678-9cb9-51bc0edcee8c','manager','Moodle managers') keycloak.add_client_role('a92d5417-92b6-4678-9cb9-51bc0edcee8c','teacher','Moodle teachers') keycloak.add_client_role('a92d5417-92b6-4678-9cb9-51bc0edcee8c','student','Moodle students') keycloak=None m=MoodleSaml()