259 lines
13 KiB
Python
259 lines
13 KiB
Python
#!/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 '<md:EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Name="urn:keycloak"><md:EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master"><md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor use="signing"><ds:KeyInfo><ds:KeyName>'+rsa['name']+'</ds:KeyName><ds:X509Data><ds:X509Certificate>'+rsa['certificate']+'</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml/resolve" index="0"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/></md:IDPSSODescriptor></md:EntityDescriptor></md:EntitiesDescriptor>'
|
|
|
|
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()
|