digitaldemocratic/admin/src/moodle_saml.py

249 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 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 '<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=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()