digitaldemocratic/admin/src/moodle_saml.py

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()