automated wordpress saml

root 2021-05-29 10:10:57 +02:00
parent ab559dd35a
commit be28c1ae30
13 changed files with 532 additions and 53 deletions

View File

@ -16,3 +16,4 @@ zope.event==4.4
zope.interface==5.1.0 zope.interface==5.1.0
psycopg2==2.8.6 psycopg2==2.8.6
Flask-SocketIO==2.8.6 Flask-SocketIO==2.8.6
mysql-connector-python==8.0.25

View File

@ -1,11 +1,13 @@
from admin import app from admin import app
from .keycloak import Keycloak from .keycloak_client import KeycloakClient
from .moodle import Moodle from .moodle import Moodle
from .nextcloud import Nextcloud from .nextcloud import Nextcloud
import logging as log import logging as log
from pprint import pprint from pprint import pprint
import traceback import traceback, os
from time import sleep
from .nextcloud_exc import * from .nextcloud_exc import *
from .helpers import filter_roles_list, filter_roles_listofdicts from .helpers import filter_roles_list, filter_roles_listofdicts
@ -13,12 +15,42 @@ from .helpers import filter_roles_list, filter_roles_listofdicts
from flask_socketio import SocketIO, emit, join_room, leave_room, \ from flask_socketio import SocketIO, emit, join_room, leave_room, \
close_room, rooms, disconnect, send close_room, rooms, disconnect, send
socketio = SocketIO(app) socketio = SocketIO(app)
import json
class Admin(): class Admin():
def __init__(self): def __init__(self):
self.keycloak=Keycloak(verify=app.config['VERIFY']) ready=False
while not ready:
try:
self.keycloak=KeycloakClient(verify=app.config['VERIFY'])
ready=True
except:
log.error(traceback.format_exc())
log.error('Could not connect to keycloak, waiting to be online...')
sleep(2)
log.warning('Keycloak connected.')
ready=False
while not ready:
try:
self.moodle=Moodle(verify=app.config['VERIFY']) self.moodle=Moodle(verify=app.config['VERIFY'])
ready=True
except:
log.error('Could not connect to moodle, waiting to be online...')
sleep(2)
log.warning('Moodle connected.')
ready=False
while not ready:
try:
self.nextcloud=Nextcloud(verify=app.config['VERIFY']) self.nextcloud=Nextcloud(verify=app.config['VERIFY'])
ready=True
except:
log.error('Could not connect to nextcloud, waiting to be online...')
sleep(2)
log.warning('Nextcloud connected.')
self.default_setup()
self.internal={} self.internal={}
self.resync_data() self.resync_data()
@ -26,6 +58,52 @@ class Admin():
'groups':[], 'groups':[],
'roles':[]} 'roles':[]}
## This function should be moved to postup.py
def default_setup(self):
log.warning('Setting defaults...')
dduser=os.environ['DDADMIN_USER']
ddpassword=os.environ['DDADMIN_PASSWORD']
ddmail=os.environ['DDADMIN_EMAIL']
try:
log.warning('KEYCLOAK: Adding group admin and user admin to this group')
self.keycloak.add_group('admin')
## Add default admin user to group admin (for nextcloud, just in case we go there)
admin_uid=self.keycloak_admin.get_user_id('admin')
self.keycloak_admin.group_user_add(uid,gid)
log.warning('KEYCLOAK: OK')
except:
log.warning('KEYCLOAK: Seems to be there already')
try:
log.warning('KEYCLOAK: Adding user ddadmin and adding to group and role admin')
## Assign group admin to this dduser for nextcloud
uid=self.keycloak.add_user(dduser,'DD','Admin',ddmail,ddpassword,group='admin')
## Assign role admin to this user for keycloak, moodle and wordpress
self.keycloak.assign_realm_roles(uid,'admin')
log.warning('KEYCLOAK: OK')
except:
log.warning('KEYCLOAK: Seems to be there already')
try:
log.warning('NEXTCLOUD: Adding user ddadmin and adding to group admin')
self.nextcloud.add_user(dduser,ddpassword,group='admin',email=ddmail,displayname='DD Admin')
log.warning('NEXTCLOUD: OK')
except ProviderItemExists:
log.warning('NEXTCLOUD: Seems to be there already')
except:
log.error(traceback.format_exc())
exit(1)
try:
log.warning('MOODLE: Adding user ddadmin and adding to siteadmins')
self.moodle.create_user(ddmail,dduser,ddpassword,'DD','Admin')
uid=self.moodle.get_user_by('username',dduser)['users'][0]['id']
self.moodle.add_user_to_siteadmin(uid)
log.warning('MOODLE: OK')
except:
log.warning('MOODLE: Seems to be there already')
def resync_data(self): def resync_data(self):
self.internal={'users':self._get_mix_users(), self.internal={'users':self._get_mix_users(),
'groups':self._get_mix_groups(), 'groups':self._get_mix_groups(),
@ -146,15 +224,15 @@ class Admin():
return filter_roles_listofdicts(self.keycloak.get_roles()) return filter_roles_listofdicts(self.keycloak.get_roles())
def get_keycloak_groups(self): def get_keycloak_groups(self):
log.warning('Loading keycloak groups... can take a long time...') log.warning('Loading keycloak groups...')
return self.keycloak.get_groups() return self.keycloak.get_groups()
def get_moodle_groups(self): def get_moodle_groups(self):
log.warning('Loading moodle groups... can take a long time...') log.warning('Loading moodle groups...')
return self.moodle.get_cohorts() return self.moodle.get_cohorts()
def get_nextcloud_groups(self): def get_nextcloud_groups(self):
log.warning('Loading nextcloud groups... can take a long time...') log.warning('Loading nextcloud groups...')
return self.nextcloud.get_groups_list() return self.nextcloud.get_groups_list()
def get_mix_groups(self): def get_mix_groups(self):
@ -244,24 +322,24 @@ class Admin():
def sync_external(self): def sync_external(self):
for u in self.external['users']: for u in self.external['users']:
log.error('Creating user: '+u['username']) log.info('Creating user: '+u['username'])
self.keycloak.add_user(u['username'],u['first'],u['last'],u['email'],'1Provaprovaprova',group=u['groups'][0]) self.keycloak.add_user(u['username'],u['first'],u['last'],u['email'],'1Provaprovaprova',group=u['groups'][0])
def sync_to_moodle(self): def sync_to_moodle(self):
for u in self.internal['users']: for u in self.internal['users']:
if not u['moodle']: if not u['moodle']:
log.error('Creating moodle user: '+u['username']) log.info('Creating moodle user: '+u['username'])
self.moodle.create_user(u['email'],u['username'],'-1Provaprovaprova',u['first'],u['last']) self.moodle.create_user(u['email'],u['username'],'-1Provaprovaprova',u['first'],u['last'])
def sync_to_nextcloud(self): def sync_to_nextcloud(self):
for u in self.internal['users']: for u in self.internal['users']:
if not u['nextcloud']: if not u['nextcloud']:
log.error('Creating nextcloud user: '+u['username']) log.info('Creating nextcloud user: '+u['username'])
group=u['keycloak_groups'][0] if len(u['keycloak_groups']) else False group=u['keycloak_groups'][0] if len(u['keycloak_groups']) else False
try: try:
self.nextcloud.add_user(u['username'],'-1Provaprovaprova',1000,group,u['email'],u['first']+' '+u['last']) self.nextcloud.add_user(u['username'],'-1Provaprovaprova',1000,group,u['email'],u['first']+' '+u['last'])
except ProviderItemExists: except ProviderItemExists:
log.error('User '+u['username']+' already exists. Skipping...') log.info('User '+u['username']+' already exists. Skipping...')
continue continue
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())

View File

@ -14,7 +14,7 @@ from jinja2 import Environment, FileSystemLoader
from keycloak import KeycloakAdmin from keycloak import KeycloakAdmin
from .postgres import Postgres from .postgres import Postgres
class Keycloak(): class KeycloakClient():
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html """https://www.keycloak.org/docs-api/13.0/rest-api/index.html
https://github.com/marcospereirampj/python-keycloak https://github.com/marcospereirampj/python-keycloak
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
@ -31,7 +31,7 @@ class Keycloak():
self.realm=realm self.realm=realm
self.verify=verify self.verify=verify
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',app.config['KEYCLOAK_POSTGRES_USER'],app.config['KEYCLOAK_POSTGRES_PASSWORD']) self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD'])
def connect(self): def connect(self):
self.keycloak_admin = KeycloakAdmin(server_url=self.url, self.keycloak_admin = KeycloakAdmin(server_url=self.url,
@ -39,7 +39,7 @@ class Keycloak():
password=self.password, password=self.password,
realm_name=self.realm, realm_name=self.realm,
verify=self.verify) verify=self.verify)
# from keycloak import KeycloakAdmin
# keycloak_admin = KeycloakAdmin(server_url="http://isard-sso-keycloak:8080/auth/",username="admin",password="keycloakkeycloak",realm_name="master",verify=False) # keycloak_admin = KeycloakAdmin(server_url="http://isard-sso-keycloak:8080/auth/",username="admin",password="keycloakkeycloak",realm_name="master",verify=False)
######## Example create group and subgroup ######## Example create group and subgroup
@ -110,7 +110,6 @@ class Keycloak():
def add_user(self,username,first,last,email,password,group=False): def add_user(self,username,first,last,email,password,group=False):
# Returns user id # Returns user id
log.error('Creating group: '+str(group))
self.connect() self.connect()
username=username.lower() username=username.lower()
try: try:
@ -123,22 +122,21 @@ class Keycloak():
"value":password, "value":password,
"temporary":False}]}) "temporary":False}]})
except: except:
uid=self.keycloak_admin.get_user_id(username) log.error(traceback.format_exc())
log.error(uid)
if group: if group:
path = '/'+group if group[1:] != '/' else group
try: try:
gid=self.keycloak_admin.get_group_by_path(path=group,search_in_subgroups=False)['id'] gid=self.keycloak_admin.get_group_by_path(path=path,search_in_subgroups=False)['id']
log.error('group created with gid: '+str(gid))
except: except:
self.keycloak_admin.create_group({"name":group}) self.keycloak_admin.create_group({"name":group})
gid=self.keycloak_admin.get_group_by_path(group)['id'] gid=self.keycloak_admin.get_group_by_path(path)['id']
log.error(gid)
self.keycloak_admin.group_user_add(uid,gid) self.keycloak_admin.group_user_add(uid,gid)
return uid
def add_user_role(self,client_id,user_id,role_id,role_name): def add_user_role(self,client_id,user_id,role_id,role_name):
self.connect() self.connect()
return self.keycloak_admin.assign_client_role(client_id="client_id", user_id="user_id", role_id="role_id", role_name="test") return self.keycloak_admin.assign_client_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test")
def delete_user(self,userid): def delete_user(self,userid):
self.connect() self.connect()
@ -196,8 +194,9 @@ class Keycloak():
self.connect() self.connect()
return self.keycloak_admin.get_client_roles(client_id=client_id) return self.keycloak_admin.get_client_roles(client_id=client_id)
# def add_client_role(self,client_id,roleName): def add_client_role(self,client_id,name,description=''):
# return self.keycloak_admin.create_client_role(client_id=client_id, {'name': roleName, 'clientRole': True}) self.connect()
return self.keycloak_admin.create_client_role(client_id, {'name': name, 'description':description, 'clientRole': True})
## SYSTEM ## SYSTEM
@ -214,6 +213,15 @@ class Keycloak():
rsa_key = [k for k in self.keycloak_admin.get_keys()['keys'] if k['type']=='RSA'][0] rsa_key = [k for k in self.keycloak_admin.get_keys()['keys'] if k['type']=='RSA'][0]
return {'name':rsa_key['kid'],'certificate':rsa_key['certificate']} return {'name':rsa_key['kid'],'certificate':rsa_key['certificate']}
## REALM
def assign_realm_roles(self, user_id, role):
self.connect()
try:
role=[r for r in self.keycloak_admin.get_realm_roles() if r['name']==role]
except:
return False
return self.keycloak_admin.assign_realm_roles(user_id=user_id, client_id=None, roles=role)
## CLIENTS ## CLIENTS
def delete_client(self,clientid): def delete_client(self,clientid):
self.connect() self.connect()

View File

@ -122,3 +122,36 @@ class Moodle():
for cohort in cohorts: for cohort in cohorts:
if user_id in self.get_cohort_members(cohort['id']): user_cohorts.append(cohort) if user_id in self.get_cohort_members(cohort['id']): user_cohorts.append(cohort)
return user_cohorts return user_cohorts
def add_user_to_siteadmin(self,user_id):
q = """SELECT value FROM mdl_config WHERE name='siteadmins'"""
value=self.moodle_pg.select(q)[0][0]
if str(user_id) not in value:
value=value+','+str(user_id)
q = """UPDATE mdl_config SET value = '%s' WHERE name='siteadmins'""" % (value)
self.moodle_pg.update(q)
log.warning('MOODLE:ADDING THE USER TO ADMINS: This needs a purge cache in moodle!')
# def add_role_to_user(self, user_id, role='admin', context='missing'):
# if role=='admin':
# role_id=1
# else:
# return False
# assignments = [{'roleid': role_id, 'userid': user_id, 'contextid': 0}]
# self.call('core_role_assign_roles', assignments=assignments)
# userid=user_id, role_id=role_id)
# 'contextlevel': 1,
# define('CONTEXT_SYSTEM', 10);
# define('CONTEXT_USER', 30);
# define('CONTEXT_COURSECAT', 40);
# define('CONTEXT_COURSE', 50);
# define('CONTEXT_MODULE', 70);
# define('CONTEXT_BLOCK', 80);
# 'contextlevel': , 'instanceid'
# $assignment = array( 'roleid' => $role_id, 'userid' => $user_id, 'contextid' => $context_id );
# $assignments = array( $assignment );
# $params = array( 'assignments' => $assignments );
# $response = call_moodle( 'core_role_assign_roles', $params, $token );

View File

@ -0,0 +1,34 @@
#!/usr/bin/env python
# coding=utf-8
import time
from admin import app
from datetime import datetime, timedelta
import pprint
import logging as log
import traceback
import yaml, json
import mysql.connector
class Mysql():
def __init__(self,host,database,user,password):
self.conn = mysql.connector.connect(
host=host,
database=database,
user=user,
password=password)
def select(self,sql):
self.cur = self.conn.cursor()
self.cur.execute(sql)
data=self.cur.fetchall()
self.cur.close()
return data
def update(self,sql):
self.cur = self.conn.cursor()
self.cur.execute(sql)
self.conn.commit()
self.cur.close()

View File

@ -126,11 +126,14 @@ class Nextcloud():
# log.error(traceback.format_exc()) # log.error(traceback.format_exc())
# raise # raise
def add_user(self,userid,userpassword,quota,group=False,email='',displayname=''): def add_user(self,userid,userpassword,quota=False,group=False,email='',displayname=''):
if group:
data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname} data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname}
else: if not group: del data['group']
data={'userid':userid,'password':userpassword,'quota':quota,'email':email,'displayname':displayname} if not quota: del data['quota']
# if group:
# data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname}
# else:
# data={'userid':userid,'password':userpassword,'quota':quota,'email':email,'displayname':displayname}
url = self.apiurl + "users?format=json" url = self.apiurl + "users?format=json"
headers = { headers = {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',

View File

@ -82,6 +82,7 @@ class Postup():
(3, 'core_cohort_delete_cohorts'), (3, 'core_cohort_delete_cohorts'),
(3, 'core_cohort_search_cohorts'), (3, 'core_cohort_search_cohorts'),
(3, 'core_cohort_update_cohorts'), (3, 'core_cohort_update_cohorts'),
(3, 'core_role_assign_roles'),
(3, 'core_cohort_get_cohorts');""") (3, 'core_cohort_get_cohorts');""")
self.pg.update("""INSERT INTO "mdl_external_services_users" ("externalserviceid", "userid", "iprestriction", "validuntil", "timecreated") VALUES self.pg.update("""INSERT INTO "mdl_external_services_users" ("externalserviceid", "userid", "iprestriction", "validuntil", "timecreated") VALUES

View File

@ -11,7 +11,7 @@ import yaml, json
import psycopg2 import psycopg2
from admin.lib.postgres import Postgres from admin.lib.postgres import Postgres
from admin.lib.keycloak import Keycloak from admin.lib.keycloak_client import KeycloakClient
import string, random import string, random
@ -102,6 +102,8 @@ class MoodleSaml():
except: except:
print('Error adding saml on keycloak') print('Error adding saml on keycloak')
self.add_client_roles()
def activate_saml_plugin(self): def activate_saml_plugin(self):
## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php ## 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'""") return self.pg.update("""UPDATE "mdl_config" SET value = 'email,saml2' WHERE "name" = 'auth'""")
@ -110,18 +112,18 @@ class MoodleSaml():
return self.pg.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2] return self.pg.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2]
def parse_idp_metadata(self): def parse_idp_metadata(self):
keycloak=Keycloak() keycloak=KeycloakClient()
rsa=keycloak.get_server_rsa_key() rsa=keycloak.get_server_rsa_key()
keycloak=None 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>' 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): def set_keycloak_moodle_saml_plugin(self):
keycloak=Keycloak() keycloak=KeycloakClient()
keycloak.add_moodle_client() keycloak.add_moodle_client()
keycloak=None keycloak=None
def delete_keycloak_moodle_saml_plugin(self): def delete_keycloak_moodle_saml_plugin(self):
keycloak=Keycloak() keycloak=KeycloakClient()
keycloak.delete_client('a92d5417-92b6-4678-9cb9-51bc0edcee8c') keycloak.delete_client('a92d5417-92b6-4678-9cb9-51bc0edcee8c')
keycloak=None keycloak=None
@ -240,9 +242,16 @@ class MoodleSaml():
"manage" : True "manage" : True
} }
} }
keycloak=Keycloak() keycloak=KeycloakClient()
keycloak.add_client(client) keycloak.add_client(client)
keycloak=None 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() m=MoodleSaml()

View File

@ -11,7 +11,7 @@ import yaml, json
import psycopg2 import psycopg2
from admin.lib.postgres import Postgres from admin.lib.postgres import Postgres
from admin.lib.keycloak import Keycloak from admin.lib.keycloak_client import KeycloakClient
import string, random import string, random
@ -20,6 +20,12 @@ app['config']={}
class NextcloudSaml(): class NextcloudSaml():
def __init__(self): def __init__(self):
self.url="http://isard-sso-keycloak:8080/auth/"
self.username=os.environ['KEYCLOAK_USER']
self.password=os.environ['KEYCLOAK_PASSWORD']
self.realm='master'
self.verify=True
ready=False ready=False
while not ready: while not ready:
try: try:
@ -81,6 +87,7 @@ class NextcloudSaml():
try: try:
self.set_nextcloud_saml_plugin() self.set_nextcloud_saml_plugin()
except: except:
log.error(traceback.format_exc())
print('Error adding saml on nextcloud') print('Error adding saml on nextcloud')
try: try:
@ -88,6 +95,13 @@ class NextcloudSaml():
except: except:
print('Error adding saml on keycloak') print('Error adding saml on keycloak')
def connect(self):
self.keycloak= KeycloakClient(url=self.url,
username=self.username,
password=self.password,
realm=self.realm,
verify=self.verify)
# def activate_saml_plugin(self): # def activate_saml_plugin(self):
# ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php # ## 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'""") # return self.pg.update("""UPDATE "mdl_config" SET value = 'email,saml2' WHERE "name" = 'auth'""")
@ -96,20 +110,20 @@ class NextcloudSaml():
# return self.pg.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2] # return self.pg.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2]
def parse_idp_cert(self): def parse_idp_cert(self):
keycloak=Keycloak() self.connect()
rsa=keycloak.get_server_rsa_key() rsa=self.keycloak.get_server_rsa_key()
keycloak=None self.keycloak=None
return rsa['certificate'] return rsa['certificate']
def set_keycloak_nextcloud_saml_plugin(self): def set_keycloak_nextcloud_saml_plugin(self):
keycloak=Keycloak() self.connect()
keycloak.add_nextcloud_client() self.keycloak.add_nextcloud_client()
keycloak=None self.keycloak=None
def delete_keycloak_nextcloud_saml_plugin(self): def delete_keycloak_nextcloud_saml_plugin(self):
keycloak=Keycloak() self.connect()
keycloak.delete_client('bef873f0-2079-4876-8657-067de27d01b7') self.keycloak.delete_client('bef873f0-2079-4876-8657-067de27d01b7')
keycloak=None self.keycloak=None
def set_nextcloud_saml_plugin(self): def set_nextcloud_saml_plugin(self):
self.pg.update("""INSERT INTO "oc_appconfig" ("appid", "configkey", "configvalue") VALUES self.pg.update("""INSERT INTO "oc_appconfig" ("appid", "configkey", "configvalue") VALUES
@ -235,10 +249,8 @@ class NextcloudSaml():
"manage" : True "manage" : True
} }
} }
keycloak=Keycloak() self.connect()
keycloak.add_client(client) self.keycloak.add_client(client)
keycloak=None self.keycloak=None
n=NextcloudSaml() n=NextcloudSaml()

295
admin/src/wordpress_saml.py Normal file
View File

@ -0,0 +1,295 @@
#!/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.mysql import Mysql
from admin.lib.keycloak_client import KeycloakClient
import string, random
app={}
app['config']={}
class WordpressSaml():
def __init__(self):
self.url="http://isard-sso-keycloak:8080/auth/"
self.username=os.environ['KEYCLOAK_USER']
self.password=os.environ['KEYCLOAK_PASSWORD']
self.realm='master'
self.verify=True
ready=False
while not ready:
try:
self.db=Mysql('isard-apps-mariadb','wordpress','root',os.environ['MARIADB_PASSWORD'])
ready=True
except:
log.warning('Could not connect to wordpress database. Retrying...')
time.sleep(2)
log.info('Connected to wordpress database.')
ready=False
while not ready:
try:
with open(os.path.join("./saml_certs/public.cert"),"r") as crt:
app['config']['PUBLIC_CERT_RAW']=crt.read()
app['config']['PUBLIC_CERT']=self.cert_prepare(app['config']['PUBLIC_CERT_RAW'])
ready=True
except IOError:
log.warning('Could not get public certificate to be used in wordpress. Retrying...')
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert')
time.sleep(2)
except:
log.error(traceback.format_exc())
log.info('Got moodle srt certificate to be used in wordpress.')
ready=False
while not ready:
try:
with open(os.path.join("./saml_certs/private.key"),"r") as pem:
app['config']['PRIVATE_KEY']=self.cert_prepare(pem.read())
ready=True
except IOError:
log.warning('Could not get private key to be used in wordpress. Retrying...')
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert')
time.sleep(2)
log.info('Got moodle pem certificate to be used in wordpress.')
# ## 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['NEXTCLOUD_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())
try:
self.reset_saml()
except:
print(traceback.format_exc())
print('Error resetting saml on wordpress')
try:
self.delete_keycloak_wordpress_saml_plugin()
except:
print('Error resetting saml on keycloak')
try:
self.set_wordpress_saml_plugin()
except:
print(traceback.format_exc())
print('Error adding saml on wordpress')
try:
self.add_keycloak_wordpress_saml()
except:
print('Error adding saml on keycloak')
self.add_client_roles()
def connect(self):
self.keycloak= KeycloakClient(url=self.url,
username=self.username,
password=self.password,
realm=self.realm,
verify=self.verify)
# def activate_saml_plugin(self):
# ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php
# return self.db.update("""UPDATE "mdl_config" SET value = 'email,saml2' WHERE "name" = 'auth'""")
# def get_privatekey_pass(self):
# return self.db.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2]
def cert_prepare(self,cert):
return ''.join(cert.split('-----')[2].splitlines())
def parse_idp_cert(self):
self.connect()
rsa=self.keycloak.get_server_rsa_key()
self.keycloak=None
return rsa['certificate']
def set_keycloak_wordpress_saml_plugin(self):
self.connect()
self.keycloak.add_wordpress_client()
self.keycloak=None
def delete_keycloak_wordpress_saml_plugin(self):
self.connect()
self.keycloak.delete_client('630601f8-25d1-4822-8741-c93affd2cd84')
self.keycloak=None
def set_wordpress_saml_plugin(self):
# ('active_plugins', 'a:2:{i:0;s:33:\"edwiser-bridge/edwiser-bridge.php\";i:1;s:17:\"onelogin_saml.php\";}', 'yes'),
self.db.update("""INSERT INTO wp_options (option_name, option_value, autoload) VALUES
('onelogin_saml_enabled', 'on', 'yes'),
('onelogin_saml_idp_entityid', 'Saml Login', 'yes'),
('onelogin_saml_idp_sso', 'https://sso.%s/auth/realms/master/protocol/saml', 'yes'),
('onelogin_saml_idp_slo', 'https://sso.%s/auth/realms/master/protocol/saml', 'yes'),
('onelogin_saml_idp_x509cert', '%s', 'yes'),
('onelogin_saml_autocreate', 'on', 'yes'),
('onelogin_saml_updateuser', 'on', 'yes'),
('onelogin_saml_forcelogin', 'on', 'yes'),
('onelogin_saml_slo', 'on', 'yes'),
('onelogin_saml_keep_local_login', '', 'yes'),
('onelogin_saml_alternative_acs', '', 'yes'),
('onelogin_saml_account_matcher', 'username', 'yes'),
('onelogin_saml_trigger_login_hook', '', 'yes'),
('onelogin_saml_multirole', '', 'yes'),
('onelogin_saml_trusted_url_domains', '', 'yes'),
('onelogin_saml_attr_mapping_username', 'username', 'yes'),
('onelogin_saml_attr_mapping_mail', 'email', 'yes'),
('onelogin_saml_attr_mapping_firstname', 'givenName', 'yes'),
('onelogin_saml_attr_mapping_lastname', 'sn', 'yes'),
('onelogin_saml_attr_mapping_nickname', '', 'yes'),
('onelogin_saml_attr_mapping_role', 'Role', 'yes'),
('onelogin_saml_attr_mapping_rememberme', '', 'yes'),
('onelogin_saml_role_mapping_administrator', 'admin', 'yes'),
('onelogin_saml_role_mapping_editor', 'manager', 'yes'),
('onelogin_saml_role_mapping_author', 'coursecreator', 'yes'),
('onelogin_saml_role_mapping_contributor', 'teacher', 'yes'),
('onelogin_saml_role_mapping_subscriber', '', 'yes'),
('onelogin_saml_role_mapping_multivalued_in_one_attribute_value', 'on', 'yes'),
('onelogin_saml_role_mapping_multivalued_pattern', '', 'yes'),
('onelogin_saml_role_order_administrator', '', 'yes'),
('onelogin_saml_role_order_editor', '', 'yes'),
('onelogin_saml_role_order_author', '', 'yes'),
('onelogin_saml_role_order_contributor', '', 'yes'),
('onelogin_saml_role_order_subscriber', '', 'yes'),
('onelogin_saml_customize_action_prevent_local_login', '', 'yes'),
('onelogin_saml_customize_action_prevent_reset_password', '', 'yes'),
('onelogin_saml_customize_action_prevent_change_password', '', 'yes'),
('onelogin_saml_customize_action_prevent_change_mail', '', 'yes'),
('onelogin_saml_customize_stay_in_wordpress_after_slo', 'on', 'yes'),
('onelogin_saml_customize_links_user_registration', '', 'yes'),
('onelogin_saml_customize_links_lost_password', '', 'yes'),
('onelogin_saml_customize_links_saml_login', '', 'yes'),
('onelogin_saml_advanced_settings_debug', '', 'yes'),
('onelogin_saml_advanced_settings_strict_mode', '', 'yes'),
('onelogin_saml_advanced_settings_sp_entity_id', '', 'yes'),
('onelogin_saml_advanced_idp_lowercase_url_encoding', '', 'yes'),
('onelogin_saml_advanced_settings_nameid_encrypted', '', 'yes'),
('onelogin_saml_advanced_settings_authn_request_signed', 'on', 'yes'),
('onelogin_saml_advanced_settings_logout_request_signed', 'on', 'yes'),
('onelogin_saml_advanced_settings_logout_response_signed', 'on', 'yes'),
('onelogin_saml_advanced_settings_want_message_signed', '', 'yes'),
('onelogin_saml_advanced_settings_want_assertion_signed', '', 'yes'),
('onelogin_saml_advanced_settings_want_assertion_encrypted', '', 'yes'),
('onelogin_saml_advanced_settings_retrieve_parameters_from_server', '', 'yes'),
('onelogin_saml_advanced_nameidformat', 'unspecified', 'yes'),
('onelogin_saml_advanced_requestedauthncontext', '', 'yes'),
('onelogin_saml_advanced_settings_sp_x509cert', '%s', 'yes'),
('onelogin_saml_advanced_settings_sp_privatekey', '%s', 'yes'),
('onelogin_saml_advanced_signaturealgorithm', 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', 'yes'),
('onelogin_saml_advanced_digestalgorithm', 'http://www.w3.org/2000/09/xmldsig#sha1', 'yes');""" % (os.environ['DOMAIN'],os.environ['DOMAIN'],self.parse_idp_cert(),app['config']['PUBLIC_CERT'],app['config']['PRIVATE_KEY']))
def reset_saml(self):
self.db.update("""DELETE FROM wp_options WHERE option_name LIKE 'onelogin_saml_%'""")
def add_keycloak_wordpress_saml(self):
client={"id" : "630601f8-25d1-4822-8741-c93affd2cd84",
"clientId" : "php-saml",
"surrogateAuthRequired" : False,
"enabled" : True,
"alwaysDisplayInConsole" : False,
"clientAuthenticatorType" : "client-secret",
"redirectUris" : [ "https://wp."+os.environ['DOMAIN']+"/wp-login.php?saml_acs" ],
"webOrigins" : [ "https://wp."+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_assertion_consumer_url_post" : "https://wp."+os.environ['DOMAIN']+"/wp-login.php?saml_acs",
"saml.server.signature" : True,
"saml.server.signature.keyinfo.ext" : False,
"saml.signing.certificate" : app['config']['PUBLIC_CERT_RAW'],
"saml_single_logout_service_url_redirect" : "https://wp."+os.environ['DOMAIN']+"/wp-login.php?saml_sls",
"saml.signature.algorithm" : "RSA_SHA256",
"saml_force_name_id_format" : False,
"saml.client.signature" : True,
"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" : "72c6175e-bd07-4c27-abd6-4e4ae38d834b",
"name" : "username",
"protocol" : "saml",
"protocolMapper" : "saml-user-attribute-mapper",
"consentRequired" : False,
"config" : {
"attribute.nameformat" : "Basic",
"user.attribute" : "username",
"friendly.name" : "username",
"attribute.name" : "username"
}
}, {
"id" : "abd6562f-4732-4da9-987f-b1a6ad6605fa",
"name" : "roles",
"protocol" : "saml",
"protocolMapper" : "saml-role-list-mapper",
"consentRequired" : False,
"config" : {
"single" : True,
"attribute.nameformat" : "Basic",
"friendly.name" : "Roles",
"attribute.name" : "Role"
}
}, {
"id" : "50aafb71-d91c-4bc7-bb60-e1ae0222aab3",
"name" : "email",
"protocol" : "saml",
"protocolMapper" : "saml-user-property-mapper",
"consentRequired" : False,
"config" : {
"attribute.nameformat" : "Basic",
"user.attribute" : "email",
"friendly.name" : "email",
"attribute.name" : "email"
}
} ],
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ],
"access" : {
"view" : True,
"configure" : True,
"manage" : True
}
}
self.connect()
self.keycloak.add_client(client)
self.keycloak=None
def add_client_roles(self):
self.connect()
self.keycloak.add_client_role('630601f8-25d1-4822-8741-c93affd2cd84','admin','Moodle admins')
self.keycloak.add_client_role('630601f8-25d1-4822-8741-c93affd2cd84','manager','Moodle managers')
self.keycloak.add_client_role('630601f8-25d1-4822-8741-c93affd2cd84','teacher','Moodle teachers')
self.keycloak.add_client_role('630601f8-25d1-4822-8741-c93affd2cd84','student','Moodle students')
self.keycloak=None
nw=WordpressSaml()

View File

@ -28,6 +28,8 @@ services:
- KEYCLOAK_PASSWORD=${KEYCLOAK_PASSWORD} - KEYCLOAK_PASSWORD=${KEYCLOAK_PASSWORD}
- PROXY_ADDRESS_FORWARDING=true - PROXY_ADDRESS_FORWARDING=true
- KEYCLOAK_FRONTEND_URL=https://sso.${DOMAIN}/auth/ - KEYCLOAK_FRONTEND_URL=https://sso.${DOMAIN}/auth/
- DDADMIN_USER=${DDADMIN_USER}
- DDADMIN_PASSWORD=${DDADMIN_PASSWORD}
#- KEYCLOAK_LOGLEVEL=ALL #- KEYCLOAK_LOGLEVEL=ALL
#- Dkeycloak.profile.feature.upload_scripts=enabled #- Dkeycloak.profile.feature.upload_scripts=enabled
depends_on: depends_on:

View File

@ -15,3 +15,6 @@
# #curl https://moodle.isardvdi.site/auth/saml2/sp/metadata.php # #curl https://moodle.isardvdi.site/auth/saml2/sp/metadata.php
# # Import as client provider # # Import as client provider
# get-roles --cclientid test-client --rolename operations