Merge branch 'admin-black' into 'master'

refator(admin): black

See merge request isard/isard-sso!65
Josep Maria Viñolas Auquer 2021-12-28 22:27:20 +00:00
commit cb187f6bef
32 changed files with 4156 additions and 2695 deletions

View File

@ -1,77 +1,96 @@
#!flask/bin/python #!flask/bin/python
# coding=utf-8 # coding=utf-8
import os
import logging as log import logging as log
import os
from flask import Flask, send_from_directory, render_template from flask import Flask, render_template, send_from_directory
app = Flask(__name__, static_url_path='')
app = Flask(__name__, template_folder='static/templates') app = Flask(__name__, static_url_path="")
app = Flask(__name__, template_folder="static/templates")
app.url_map.strict_slashes = False app.url_map.strict_slashes = False
''' """
App secret key for encrypting cookies App secret key for encrypting cookies
You can generate one with: You can generate one with:
import os import os
os.urandom(24) os.urandom(24)
And paste it here. And paste it here.
''' """
app.secret_key = "Change this key!/\xf7\x83\xbe\x17\xfa\xa3zT\n\\]m\xa6\x8bF\xdd\r\xf7\x9e\x1d\x1f\x14'" app.secret_key = "Change this key!/\xf7\x83\xbe\x17\xfa\xa3zT\n\\]m\xa6\x8bF\xdd\r\xf7\x9e\x1d\x1f\x14'"
print('Starting isard-sso api...') print("Starting isard-sso api...")
from admin.lib.load_config import loadConfig from admin.lib.load_config import loadConfig
try: try:
loadConfig(app) loadConfig(app)
except: except:
print('Could not get environment variables...') print("Could not get environment variables...")
from admin.lib.postup import Postup from admin.lib.postup import Postup
Postup() Postup()
from admin.lib.admin import Admin from admin.lib.admin import Admin
app.admin=Admin()
app.ready=False app.admin = Admin()
''' app.ready = False
"""
Debug should be removed on production! Debug should be removed on production!
''' """
if app.debug: if app.debug:
log.warning('Debug mode: {}'.format(app.debug)) log.warning("Debug mode: {}".format(app.debug))
else: else:
log.info('Debug mode: {}'.format(app.debug)) log.info("Debug mode: {}".format(app.debug))
''' """
Serve static files Serve static files
''' """
@app.route('/build/<path:path>')
def send_build(path):
return send_from_directory(os.path.join(app.root_path, 'node_modules/gentelella/build'), path)
@app.route('/vendors/<path:path>')
def send_vendors(path):
return send_from_directory(os.path.join(app.root_path, 'node_modules/gentelella/vendors'), path)
@app.route('/templates/<path:path>')
@app.route("/build/<path:path>")
def send_build(path):
return send_from_directory(
os.path.join(app.root_path, "node_modules/gentelella/build"), path
)
@app.route("/vendors/<path:path>")
def send_vendors(path):
return send_from_directory(
os.path.join(app.root_path, "node_modules/gentelella/vendors"), path
)
@app.route("/templates/<path:path>")
def send_templates(path): def send_templates(path):
return send_from_directory(os.path.join(app.root_path, 'templates'), path) return send_from_directory(os.path.join(app.root_path, "templates"), path)
# @app.route('/templates/<path:path>') # @app.route('/templates/<path:path>')
# def send_templates(path): # def send_templates(path):
# return send_from_directory(os.path.join(app.root_path, 'static/templates'), path) # return send_from_directory(os.path.join(app.root_path, 'static/templates'), path)
@app.route('/static/<path:path>')
@app.route("/static/<path:path>")
def send_static_js(path): def send_static_js(path):
return send_from_directory(os.path.join(app.root_path, 'static'), path) return send_from_directory(os.path.join(app.root_path, "static"), path)
@app.route('/avatars/<path:path>')
@app.route("/avatars/<path:path>")
def send_avatars_img(path): def send_avatars_img(path):
return send_from_directory(os.path.join(app.root_path, '../avatars/master-avatars'), path) return send_from_directory(
os.path.join(app.root_path, "../avatars/master-avatars"), path
)
@app.route('/custom/<path:path>')
@app.route("/custom/<path:path>")
def send_custom(path): def send_custom(path):
return send_from_directory(os.path.join(app.root_path, '../custom'), path) return send_from_directory(os.path.join(app.root_path, "../custom"), path)
# @app.errorhandler(404) # @app.errorhandler(404)
# def not_found_error(error): # def not_found_error(error):
@ -81,15 +100,7 @@ def send_custom(path):
# def internal_error(error): # def internal_error(error):
# return render_template('page_500.html'), 500 # return render_template('page_500.html'), 500
''' """
Import all views Import all views
''' """
from .views import LoginViews from .views import ApiViews, InternalViews, LoginViews, WebViews
from .views import WebViews
from .views import ApiViews
from .views import InternalViews

View File

@ -1,8 +1,10 @@
from admin import app
from flask_login import LoginManager, UserMixin
import os import os
''' OIDC TESTS ''' from flask_login import LoginManager, UserMixin
from admin import app
""" OIDC TESTS """
# from flask_oidc import OpenIDConnect # from flask_oidc import OpenIDConnect
# app.config.update({ # app.config.update({
# 'SECRET_KEY': 'u\x91\xcf\xfa\x0c\xb9\x95\xe3t\xba2K\x7f\xfd\xca\xa3\x9f\x90\x88\xb8\xee\xa4\xd6\xe4', # 'SECRET_KEY': 'u\x91\xcf\xfa\x0c\xb9\x95\xe3t\xba2K\x7f\xfd\xca\xa3\x9f\x90\x88\xb8\xee\xa4\xd6\xe4',
@ -18,7 +20,7 @@ import os
# # 'OVERWRITE_REDIRECT_URI': 'https://sso.mydomain.duckdns.org//custom_callback', # # 'OVERWRITE_REDIRECT_URI': 'https://sso.mydomain.duckdns.org//custom_callback',
# # 'OIDC_CALLBACK_ROUTE': '//custom_callback' # # 'OIDC_CALLBACK_ROUTE': '//custom_callback'
# oidc = OpenIDConnect(app) # oidc = OpenIDConnect(app)
''' OIDC TESTS ''' """ OIDC TESTS """
login_manager = LoginManager() login_manager = LoginManager()
@ -26,31 +28,33 @@ login_manager.init_app(app)
login_manager.login_view = "login" login_manager.login_view = "login"
ram_users={ ram_users = {
os.environ["ADMINAPP_USER"]: { os.environ["ADMINAPP_USER"]: {
'id': os.environ["ADMINAPP_USER"], "id": os.environ["ADMINAPP_USER"],
'password': os.environ["ADMINAPP_PASSWORD"], "password": os.environ["ADMINAPP_PASSWORD"],
'role': 'manager' "role": "manager",
}, },
os.environ["KEYCLOAK_USER"]: { os.environ["KEYCLOAK_USER"]: {
'id': os.environ["KEYCLOAK_USER"], "id": os.environ["KEYCLOAK_USER"],
'password': os.environ["KEYCLOAK_PASSWORD"], "password": os.environ["KEYCLOAK_PASSWORD"],
'role': 'admin', "role": "admin",
}, },
os.environ["WORDPRESS_MARIADB_USER"]: { os.environ["WORDPRESS_MARIADB_USER"]: {
'id': os.environ["WORDPRESS_MARIADB_USER"], "id": os.environ["WORDPRESS_MARIADB_USER"],
'password': os.environ["WORDPRESS_MARIADB_PASSWORD"], "password": os.environ["WORDPRESS_MARIADB_PASSWORD"],
'role': 'manager', "role": "manager",
} },
} }
class User(UserMixin): class User(UserMixin):
def __init__(self, dict): def __init__(self, dict):
self.id = dict['id'] self.id = dict["id"]
self.username = dict['id'] self.username = dict["id"]
self.password = dict['password'] self.password = dict["password"]
self.role = dict['role'] self.role = dict["role"]
@login_manager.user_loader @login_manager.user_loader
def user_loader(username): def user_loader(username):
return User(ram_users[username]) return User(ram_users[username])

File diff suppressed because it is too large Load Diff

View File

@ -1,49 +1,61 @@
from requests import get, post
from admin import app
import logging as log import logging as log
from pprint import pprint
import os import os
from pprint import pprint
from minio import Minio from minio import Minio
from minio.commonconfig import REPLACE, CopySource from minio.commonconfig import REPLACE, CopySource
from minio.deleteobjects import DeleteObject from minio.deleteobjects import DeleteObject
from requests import get, post
class Avatars(): from admin import app
class Avatars:
def __init__(self): def __init__(self):
self.mclient = Minio( self.mclient = Minio(
"isard-sso-avatars:9000", "isard-sso-avatars:9000",
access_key="AKIAIOSFODNN7EXAMPLE", access_key="AKIAIOSFODNN7EXAMPLE",
secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
secure=False secure=False,
) )
self.bucket='master-avatars' self.bucket = "master-avatars"
self._minio_set_realm() self._minio_set_realm()
# self.update_missing_avatars() # self.update_missing_avatars()
def add_user_default_avatar(self,userid,role='unknown'): def add_user_default_avatar(self, userid, role="unknown"):
self.mclient.fput_object( self.mclient.fput_object(
self.bucket, userid, os.path.join(app.root_path,"../custom/avatars/"+role+'.jpg'), self.bucket,
content_type="image/jpeg ", userid,
os.path.join(app.root_path, "../custom/avatars/" + role + ".jpg"),
content_type="image/jpeg ",
)
log.warning(
" AVATARS: Updated avatar for user " + userid + " with role " + role
) )
log.warning(' AVATARS: Updated avatar for user '+userid+' with role '+role)
def delete_user_avatar(self,userid): def delete_user_avatar(self, userid):
self.minio_delete_object(userid) self.minio_delete_object(userid)
def update_missing_avatars(self,users): def update_missing_avatars(self, users):
sys_roles=['admin','manager','teacher','student'] sys_roles = ["admin", "manager", "teacher", "student"]
for u in self.get_users_without_image(users): for u in self.get_users_without_image(users):
try: try:
img=[r+'.jpg' for r in sys_roles if r in u['roles']][0] img = [r + ".jpg" for r in sys_roles if r in u["roles"]][0]
except: except:
img='unknown.jpg' img = "unknown.jpg"
self.mclient.fput_object( self.mclient.fput_object(
self.bucket, u['id'], os.path.join(app.root_path,"../custom/avatars/"+img), self.bucket,
content_type="image/jpeg ", u["id"],
os.path.join(app.root_path, "../custom/avatars/" + img),
content_type="image/jpeg ",
)
log.warning(
" AVATARS: Updated avatar for user "
+ u["username"]
+ " with role "
+ img.split(".")[0]
) )
log.warning(' AVATARS: Updated avatar for user '+u['username']+' with role '+img.split('.')[0])
def _minio_set_realm(self): def _minio_set_realm(self):
if not self.mclient.bucket_exists(self.bucket): if not self.mclient.bucket_exists(self.bucket):
@ -57,14 +69,14 @@ class Avatars():
lambda x: DeleteObject(x.object_name), lambda x: DeleteObject(x.object_name),
self.mclient.list_objects(self.bucket), self.mclient.list_objects(self.bucket),
) )
errors=self.mclient.remove_objects(self.bucket, delete_object_list) errors = self.mclient.remove_objects(self.bucket, delete_object_list)
for error in errors: for error in errors:
log.error(" AVATARS: Error occured when deleting avatar object: "+ error) log.error(" AVATARS: Error occured when deleting avatar object: " + error)
def minio_delete_object(self,oid): def minio_delete_object(self, oid):
errors=self.mclient.remove_objects(self.bucket, [DeleteObject(oid)]) errors = self.mclient.remove_objects(self.bucket, [DeleteObject(oid)])
for error in errors: for error in errors:
log.error(" AVATARS: Error occured when deleting avatar object: "+ error) log.error(" AVATARS: Error occured when deleting avatar object: " + error)
def get_users_without_image(self,users): def get_users_without_image(self, users):
return [u for u in users if u['id'] and u['id'] not in self.minio_get_objects()] return [u for u in users if u["id"] and u["id"] not in self.minio_get_objects()]

View File

@ -1,110 +1,166 @@
#!flask/bin/python #!flask/bin/python
# coding=utf-8 # coding=utf-8
from admin import app
import logging as log
import traceback
from uuid import uuid4
import json
from time import sleep
import sys,os
from flask import render_template, Response, request, redirect, url_for, jsonify
from flask_socketio import SocketIO, emit, join_room, leave_room, \
close_room, rooms, disconnect, send
import base64 import base64
import json
import logging as log
import os
import sys
import traceback
from time import sleep
from uuid import uuid4
class Events(): from flask import Response, jsonify, redirect, render_template, request, url_for
def __init__(self,title,text='',total=0,table=False,type='info'): from flask_socketio import (
SocketIO,
close_room,
disconnect,
emit,
join_room,
leave_room,
rooms,
send,
)
from admin import app
class Events:
def __init__(self, title, text="", total=0, table=False, type="info"):
# notice, info, success, and error # notice, info, success, and error
self.eid=str(base64.b64encode(os.urandom(32))[:8]) self.eid = str(base64.b64encode(os.urandom(32))[:8])
self.title=title self.title = title
self.text=text self.text = text
self.total=total self.total = total
self.table=table self.table = table
self.item=0 self.item = 0
self.type=type self.type = type
self.create() self.create()
def create(self): def create(self):
log.info('START '+self.eid+': '+self.text) log.info("START " + self.eid + ": " + self.text)
app.socketio.emit('notify-create', app.socketio.emit(
json.dumps({'id':self.eid, "notify-create",
'title':self.title, json.dumps(
'text':self.text, {
'type':self.type}), "id": self.eid,
namespace='/sio', "title": self.title,
room='admin') "text": self.text,
"type": self.type,
}
),
namespace="/sio",
room="admin",
)
sleep(0.001) sleep(0.001)
def __del__(self): def __del__(self):
log.info('END '+self.eid+': '+self.text) log.info("END " + self.eid + ": " + self.text)
app.socketio.emit('notify-destroy', app.socketio.emit(
json.dumps({'id':self.eid}), "notify-destroy",
namespace='/sio', json.dumps({"id": self.eid}),
room='admin') namespace="/sio",
room="admin",
)
sleep(0.001) sleep(0.001)
def update_text(self,text): def update_text(self, text):
self.text=text self.text = text
app.socketio.emit('notify-update', app.socketio.emit(
json.dumps({'id':self.eid, "notify-update",
'text':self.text,}), json.dumps(
namespace='/sio', {
room='admin') "id": self.eid,
"text": self.text,
}
),
namespace="/sio",
room="admin",
)
sleep(0.001) sleep(0.001)
def append_text(self,text): def append_text(self, text):
self.text=self.text+'<br>'+text self.text = self.text + "<br>" + text
app.socketio.emit('notify-update', app.socketio.emit(
json.dumps({'id':self.eid, "notify-update",
'text':self.text,}), json.dumps(
namespace='/sio', {
room='admin') "id": self.eid,
"text": self.text,
}
),
namespace="/sio",
room="admin",
)
sleep(0.001) sleep(0.001)
def increment(self,data={'name':'','data':[]}): def increment(self, data={"name": "", "data": []}):
self.item+=1 self.item += 1
log.info('INCREMENT '+self.eid+': '+self.text) log.info("INCREMENT " + self.eid + ": " + self.text)
app.socketio.emit('notify-increment', app.socketio.emit(
json.dumps({'id':self.eid, "notify-increment",
'title':self.title, json.dumps(
'text': '['+str(self.item)+'/'+str(self.total)+'] '+self.text+' '+data['name'], {
'item':self.item, "id": self.eid,
'total':self.total, "title": self.title,
'table':self.table, "text": "["
'type':self.type, + str(self.item)
'data':data}), + "/"
namespace='/sio', + str(self.total)
room='admin') + "] "
+ self.text
+ " "
+ data["name"],
"item": self.item,
"total": self.total,
"table": self.table,
"type": self.type,
"data": data,
}
),
namespace="/sio",
room="admin",
)
sleep(0.0001) sleep(0.0001)
def decrement(self,data={'name':'','data':[]}): def decrement(self, data={"name": "", "data": []}):
self.item-=1 self.item -= 1
log.info('DECREMENT '+self.eid+': '+self.text) log.info("DECREMENT " + self.eid + ": " + self.text)
app.socketio.emit('notify-decrement', app.socketio.emit(
json.dumps({'id':self.eid, "notify-decrement",
'title':self.title, json.dumps(
'text': '['+str(self.item)+'/'+str(self.total)+'] '+self.text+' '+data['name'], {
'item':self.item, "id": self.eid,
'total':self.total, "title": self.title,
'table':self.table, "text": "["
'type':self.type, + str(self.item)
'data':data}), + "/"
namespace='/sio', + str(self.total)
room='admin') + "] "
+ self.text
+ " "
+ data["name"],
"item": self.item,
"total": self.total,
"table": self.table,
"type": self.type,
"data": data,
}
),
namespace="/sio",
room="admin",
)
sleep(0.001) sleep(0.001)
def reload(self): def reload(self):
app.socketio.emit('reload', app.socketio.emit("reload", json.dumps({}), namespace="/sio", room="admin")
json.dumps({}),
namespace='/sio',
room='admin')
sleep(0.0001) sleep(0.0001)
def table(self,event,table,data={}): def table(self, event, table, data={}):
# refresh, add, delete, update # refresh, add, delete, update
app.socketio.emit('table_'+event, app.socketio.emit(
json.dumps({'table':table,'data':data}), "table_" + event,
namespace='/sio', json.dumps({"table": table, "data": data}),
room='admin') namespace="/sio",
sleep(0.0001) room="admin",
)
sleep(0.0001)

View File

@ -3,5 +3,6 @@
class UserExists(Exception): class UserExists(Exception):
pass pass
class UserNotFound(Exception): class UserNotFound(Exception):
pass pass

View File

@ -1,55 +1,74 @@
import random, string import random
from pprint import pprint import string
from collections import Counter from collections import Counter
from pprint import pprint
def system_username(username): def system_username(username):
return True if username in ['guest','ddadmin','admin'] or username.startswith('system_') else False return (
True
if username in ["guest", "ddadmin", "admin"] or username.startswith("system_")
else False
)
def system_group(groupname): def system_group(groupname):
return True if groupname in ['admin','manager','teacher','student'] else False return True if groupname in ["admin", "manager", "teacher", "student"] else False
def get_group_from_group_id(group_id,groups):
return next((d for d in groups if d.get('id') == group_id), None)
def get_gid_from_kgroup_id(kgroup_id,groups): def get_group_from_group_id(group_id, groups):
return next((d for d in groups if d.get("id") == group_id), None)
def get_gid_from_kgroup_id(kgroup_id, groups):
# print(kgroup_id) # print(kgroup_id)
# pprint(groups) # pprint(groups)
# return get_group_from_group_id(kgroup_id,groups)['path'].replace('/','.')[1:] # return get_group_from_group_id(kgroup_id,groups)['path'].replace('/','.')[1:]
return [g['path'].replace('/','.')[1:] for g in groups if g['id'] == kgroup_id][0] return [g["path"].replace("/", ".")[1:] for g in groups if g["id"] == kgroup_id][0]
def get_gids_from_kgroup_ids(kgroup_ids, groups):
return [get_gid_from_kgroup_id(kgroup_id, groups) for kgroup_id in kgroup_ids]
def get_gids_from_kgroup_ids(kgroup_ids,groups):
return [get_gid_from_kgroup_id(kgroup_id,groups) for kgroup_id in kgroup_ids]
def kpath2gid(path): def kpath2gid(path):
# print(path.replace('/','.')[1:]) # print(path.replace('/','.')[1:])
return path.replace('/','.')[1:] return path.replace("/", ".")[1:]
def gid2kpath(gid): def gid2kpath(gid):
return '/'+gid.replace('.','/') return "/" + gid.replace(".", "/")
def count_repeated(itemslist): def count_repeated(itemslist):
print(Counter(itemslist)) print(Counter(itemslist))
def groups_kname2gid(groups): def groups_kname2gid(groups):
return [name.replace('.','/') for name in groups] return [name.replace(".", "/") for name in groups]
def groups_path2id(groups): def groups_path2id(groups):
return [g.replace('/','.')[1:] for g in groups] return [g.replace("/", ".")[1:] for g in groups]
def groups_id2path(groups): def groups_id2path(groups):
return ['/'+g.replace('.','/') for g in groups] return ["/" + g.replace(".", "/") for g in groups]
def filter_roles_list(role_list): def filter_roles_list(role_list):
client_roles=['admin','manager','teacher','student'] client_roles = ["admin", "manager", "teacher", "student"]
return [r for r in role_list if r in client_roles] return [r for r in role_list if r in client_roles]
def filter_roles_listofdicts(role_listofdicts): def filter_roles_listofdicts(role_listofdicts):
client_roles=['admin','manager','teacher','student'] client_roles = ["admin", "manager", "teacher", "student"]
return [r for r in role_listofdicts if r['name'] in client_roles] return [r for r in role_listofdicts if r["name"] in client_roles]
def rand_password(lenght): def rand_password(lenght):
characters = string.ascii_letters + string.digits + string.punctuation characters = string.ascii_letters + string.digits + string.punctuation
passwd = ''.join(random.choice(characters) for i in range(lenght)) passwd = "".join(random.choice(characters) for i in range(lenght))
while not any(ele.isupper() for ele in passwd): while not any(ele.isupper() for ele in passwd):
passwd = ''.join(random.choice(characters) for i in range(lenght)) passwd = "".join(random.choice(characters) for i in range(lenght))
return passwd return passwd

View File

@ -1,68 +1,82 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf-8 # coding=utf-8
import time ,os import json
from admin import app
from datetime import datetime, timedelta
import logging as log import logging as log
import os
import time
import traceback import traceback
import yaml, json from datetime import datetime, timedelta
from pprint import pprint from pprint import pprint
import yaml
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from keycloak import KeycloakAdmin from keycloak import KeycloakAdmin
from admin import app
from .postgres import Postgres from .postgres import Postgres
class KeycloakClient():
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
https://github.com/marcospereirampj/python-keycloak
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
"""
def __init__(self,
url="http://isard-sso-keycloak:8080/auth/",
username=os.environ['KEYCLOAK_USER'],
password=os.environ['KEYCLOAK_PASSWORD'],
realm='master',
verify=True):
self.url=url
self.username=username
self.password=password
self.realm=realm
self.verify=verify
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD']) class KeycloakClient:
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
https://github.com/marcospereirampj/python-keycloak
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
"""
def __init__(
self,
url="http://isard-sso-keycloak:8080/auth/",
username=os.environ["KEYCLOAK_USER"],
password=os.environ["KEYCLOAK_PASSWORD"],
realm="master",
verify=True,
):
self.url = url
self.username = username
self.password = password
self.realm = realm
self.verify = verify
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(
username=self.username, server_url=self.url,
password=self.password, username=self.username,
realm_name=self.realm, password=self.password,
verify=self.verify) realm_name=self.realm,
# from keycloak import KeycloakAdmin verify=self.verify,
# 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 # from keycloak import KeycloakAdmin
# keycloak_admin = KeycloakAdmin(server_url="http://isard-sso-keycloak:8080/auth/",username="admin",password="keycloakkeycloak",realm_name="master",verify=False)
# try:
# self.add_group('level1')
# except:
# self.delete_group(self.get_group('/level1')['id'])
# self.add_group('level1')
# self.add_group('level2',parent=self.get_group('/level1')['id'])
# pprint(self.get_groups())
######## Example roles ######## Example create group and subgroup
# try:
# self.add_role('superman')
# except:
# self.delete_role('superman')
# self.add_role('superman')
# pprint(self.get_roles())
''' USERS ''' # try:
# self.add_group('level1')
# except:
# self.delete_group(self.get_group('/level1')['id'])
# self.add_group('level1')
# self.add_group('level2',parent=self.get_group('/level1')['id'])
# pprint(self.get_groups())
def get_user_id(self,username): ######## Example roles
# try:
# self.add_role('superman')
# except:
# self.delete_role('superman')
# self.add_role('superman')
# pprint(self.get_roles())
""" USERS """
def get_user_id(self, username):
self.connect() self.connect()
return self.keycloak_admin.get_user_id(username) return self.keycloak_admin.get_user_id(username)
@ -85,24 +99,31 @@ class KeycloakClient():
group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, u.enabled, ua.value group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, u.enabled, ua.value
order by u.username""" order by u.username"""
(headers,users)=self.keycloak_pg.select_with_headers(q) (headers, users) = self.keycloak_pg.select_with_headers(q)
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\ users_with_lists = [
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\ list(l[:-4])
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\ + ([[]] if l[-4] == [None] else [list(set(l[-4]))])
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users] + ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\ + ([[]] if l[-1] == [None] else [list(set(l[-1]))])
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\ for l in users
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\ ]
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
users_with_lists = [
list(l[:-4])
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users_with_lists
]
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists] list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
# self.connect() # self.connect()
# groups = self.keycloak_admin.get_groups() # groups = self.keycloak_admin.get_groups()
# for user in list_dict_users: # for user in list_dict_users:
# new_user_groups = [] # new_user_groups = []
# for group_id in user['group']: # for group_id in user['group']:
@ -113,31 +134,31 @@ class KeycloakClient():
# user['group']=new_user_groups # user['group']=new_user_groups
return list_dict_users return list_dict_users
def getparent(self, group_id, data):
def getparent(self,group_id, data):
# Recursively get full path from any group_id in the tree # Recursively get full path from any group_id in the tree
path = "" path = ""
for item in data: for item in data:
if group_id == item[0]: if group_id == item[0]:
path = self.getparent(item[2], data) path = self.getparent(item[2], data)
path = f'{path}/{item[1]}' path = f"{path}/{item[1]}"
return path return path
def get_group_path(self,group_id): def get_group_path(self, group_id):
# Get full path using getparent recursive func # Get full path using getparent recursive func
# RETURNS: String with full path # RETURNS: String with full path
q = """SELECT * FROM keycloak_group""" q = """SELECT * FROM keycloak_group"""
groups=self.keycloak_pg.select(q) groups = self.keycloak_pg.select(q)
return self.getparent(group_id,groups) return self.getparent(group_id, groups)
def get_user_groups_paths(self,user_id): def get_user_groups_paths(self, user_id):
# Get full paths for user grups # Get full paths for user grups
# RETURNS list of paths # RETURNS list of paths
q = """SELECT group_id FROM user_group_membership WHERE user_id = '%s'""" % (user_id) q = """SELECT group_id FROM user_group_membership WHERE user_id = '%s'""" % (
user_group_ids=self.keycloak_pg.select(q) user_id
)
user_group_ids = self.keycloak_pg.select(q)
paths=[] paths = []
for g in user_group_ids: for g in user_group_ids:
paths.append(self.get_group_path(g[0])) paths.append(self.get_group_path(g[0]))
return paths return paths
@ -151,90 +172,116 @@ class KeycloakClient():
# user['roles']= [r['name'] for r in self.keycloak_admin.get_realm_roles_of_user(user_id=user['id'])] # user['roles']= [r['name'] for r in self.keycloak_admin.get_realm_roles_of_user(user_id=user['id'])]
# return users # return users
def add_user(self,username,first,last,email,password,group=False,temporary=True,enabled=True): def add_user(
self,
username,
first,
last,
email,
password,
group=False,
temporary=True,
enabled=True,
):
# RETURNS string with keycloak user id (the main id in this app) # RETURNS string with keycloak user id (the main id in this app)
self.connect() self.connect()
username=username.lower() username = username.lower()
try: try:
uid=self.keycloak_admin.create_user({"email": email, uid = self.keycloak_admin.create_user(
"username": username, {
"enabled": enabled, "email": email,
"firstName": first, "username": username,
"lastName": last, "enabled": enabled,
"credentials":[{"type":"password", "firstName": first,
"value":password, "lastName": last,
"temporary":temporary}]}) "credentials": [
{"type": "password", "value": password, "temporary": temporary}
],
}
)
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
if group: if group:
path = '/'+group if group[1:] != '/' else group path = "/" + group if group[1:] != "/" else group
try: try:
gid=self.keycloak_admin.get_group_by_path(path=path,search_in_subgroups=False)['id'] gid = self.keycloak_admin.get_group_by_path(
path=path, search_in_subgroups=False
)["id"]
except: except:
self.keycloak_admin.create_group({"name":group}) self.keycloak_admin.create_group({"name": group})
gid=self.keycloak_admin.get_group_by_path(path)['id'] gid = self.keycloak_admin.get_group_by_path(path)["id"]
self.keycloak_admin.group_user_add(uid,gid) self.keycloak_admin.group_user_add(uid, gid)
return uid return uid
def update_user_pwd(self,user_id,password,temporary=True): def update_user_pwd(self, user_id, password, temporary=True):
# Updates # Updates
payload={"credentials":[{"type":"password", payload = {
"value":password, "credentials": [
"temporary":temporary}]} {"type": "password", "value": password, "temporary": temporary}
]
}
self.connect() self.connect()
return self.keycloak_admin.update_user( user_id, payload) return self.keycloak_admin.update_user(user_id, payload)
def user_update(self,user_id,enabled,email,first,last,groups=[],roles=[]): def user_update(self, user_id, enabled, email, first, last, groups=[], roles=[]):
## NOTE: Roles didn't seem to be updated/added. Also not confident with groups ## NOTE: Roles didn't seem to be updated/added. Also not confident with groups
# Updates # Updates
payload={"enabled":enabled, payload = {
"email":email, "enabled": enabled,
"firstName":first, "email": email,
"lastName":last, "firstName": first,
"groups":groups, "lastName": last,
"realmRoles":roles} "groups": groups,
"realmRoles": roles,
}
self.connect() self.connect()
return self.keycloak_admin.update_user( user_id, payload) return self.keycloak_admin.update_user(user_id, payload)
def user_enable(self,user_id): def user_enable(self, user_id):
payload={"enabled":True} payload = {"enabled": True}
self.connect() self.connect()
return self.keycloak_admin.update_user( user_id, payload) return self.keycloak_admin.update_user(user_id, payload)
def user_disable(self,user_id): def user_disable(self, user_id):
payload={"enabled":False} payload = {"enabled": False}
self.connect() self.connect()
return self.keycloak_admin.update_user( user_id, payload) return self.keycloak_admin.update_user(user_id, payload)
def group_user_remove(self,user_id,group_id): def group_user_remove(self, user_id, group_id):
self.connect() self.connect()
return self.keycloak_admin.group_user_remove(user_id,group_id) return self.keycloak_admin.group_user_remove(user_id, group_id)
# def add_user_role(self,user_id,role_id): # def add_user_role(self,user_id,role_id):
# self.connect() # self.connect()
# return self.keycloak_admin.assign_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test") # return self.keycloak_admin.assign_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test")
def remove_user_realm_roles(self,user_id,roles): def remove_user_realm_roles(self, user_id, roles):
self.connect() self.connect()
roles = [r for r in self.get_user_realm_roles(user_id) if r['name'] in ['admin','manager','teacher','student']] roles = [
return self.keycloak_admin.delete_realm_roles_of_user(user_id,roles) r
for r in self.get_user_realm_roles(user_id)
if r["name"] in ["admin", "manager", "teacher", "student"]
]
return self.keycloak_admin.delete_realm_roles_of_user(user_id, roles)
def delete_user(self,userid): def delete_user(self, userid):
self.connect() self.connect()
return self.keycloak_admin.delete_user(user_id=userid) return self.keycloak_admin.delete_user(user_id=userid)
def get_user_groups(self,userid): def get_user_groups(self, userid):
self.connect() self.connect()
return self.keycloak_admin.get_user_groups(user_id=userid) return self.keycloak_admin.get_user_groups(user_id=userid)
def get_user_realm_roles(self,userid): def get_user_realm_roles(self, userid):
self.connect() self.connect()
return self.keycloak_admin.get_realm_roles_of_user(user_id=userid) return self.keycloak_admin.get_realm_roles_of_user(user_id=userid)
def add_user_client_role(self,client_id,user_id,role_id,role_name): def add_user_client_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"
)
## GROUPS ## GROUPS
def get_all_groups(self): def get_all_groups(self):
@ -242,75 +289,77 @@ class KeycloakClient():
self.connect() self.connect()
return self.keycloak_admin.get_groups() return self.keycloak_admin.get_groups()
def get_recursive_groups(self, l_groups,l=[]): def get_recursive_groups(self, l_groups, l=[]):
for d_group in l_groups: for d_group in l_groups:
d = {} d = {}
for key, value in d_group.items(): for key, value in d_group.items():
if key == 'subGroups': if key == "subGroups":
self.get_recursive_groups(value,l) self.get_recursive_groups(value, l)
else: else:
d[key]=value d[key] = value
l.append(d) l.append(d)
return l return l
def get_groups(self,with_subgroups=True): def get_groups(self, with_subgroups=True):
## RETURNS ALL GROUPS in root list ## RETURNS ALL GROUPS in root list
self.connect() self.connect()
groups = self.keycloak_admin.get_groups() groups = self.keycloak_admin.get_groups()
return self.get_recursive_groups(groups) return self.get_recursive_groups(groups)
subgroups=[] subgroups = []
subgroups1=[] subgroups1 = []
# This needs to be recursive function # This needs to be recursive function
if with_subgroups: if with_subgroups:
for group in groups: for group in groups:
if len(group['subGroups']): if len(group["subGroups"]):
for sg in group['subGroups']: for sg in group["subGroups"]:
subgroups.append(sg) subgroups.append(sg)
# for sgroup in subgroups: # for sgroup in subgroups:
# if len(sgroup['subGroups']): # if len(sgroup['subGroups']):
# for sg1 in sgroup['subGroups']: # for sg1 in sgroup['subGroups']:
# subgroups1.append(sg1) # subgroups1.append(sg1)
return groups+subgroups+subgroups1
def get_group_by_id(self,group_id): return groups + subgroups + subgroups1
def get_group_by_id(self, group_id):
self.connect() self.connect()
return self.keycloak_admin.get_group(group_id=group_id) return self.keycloak_admin.get_group(group_id=group_id)
def get_group_by_path(self,path,recursive=True): def get_group_by_path(self, path, recursive=True):
self.connect() self.connect()
return self.keycloak_admin.get_group_by_path(path=path,search_in_subgroups=recursive) return self.keycloak_admin.get_group_by_path(
path=path, search_in_subgroups=recursive
)
def add_group(self,name,parent=None,skip_exists=False): def add_group(self, name, parent=None, skip_exists=False):
self.connect() self.connect()
if parent != None: if parent != None:
parent=self.get_group_by_path(parent)['id'] parent = self.get_group_by_path(parent)["id"]
return self.keycloak_admin.create_group({"name":name}, parent=parent) return self.keycloak_admin.create_group({"name": name}, parent=parent)
def delete_group(self,group_id): def delete_group(self, group_id):
self.connect() self.connect()
return self.keycloak_admin.delete_group(group_id=group_id) return self.keycloak_admin.delete_group(group_id=group_id)
def group_user_add(self,user_id,group_id): def group_user_add(self, user_id, group_id):
self.connect() self.connect()
return self.keycloak_admin.group_user_add(user_id, group_id) return self.keycloak_admin.group_user_add(user_id, group_id)
def add_group_tree(self,path): def add_group_tree(self, path):
parts=path.split('/') parts = path.split("/")
parent_path='/' parent_path = "/"
for i in range(1,len(parts)): for i in range(1, len(parts)):
if i == 1: if i == 1:
try: try:
self.add_group(parts[i],None,skip_exists=True) self.add_group(parts[i], None, skip_exists=True)
except: except:
log.warning('KEYCLOAK: Group :'+parts[i]+ ' already exists.') log.warning("KEYCLOAK: Group :" + parts[i] + " already exists.")
parent_path=parent_path+parts[i] parent_path = parent_path + parts[i]
else: else:
try: try:
self.add_group(parts[i],parent_path,skip_exists=True) self.add_group(parts[i], parent_path, skip_exists=True)
except: except:
log.warning('KEYCLOAK: Group :'+parts[i]+ ' already exists.') log.warning("KEYCLOAK: Group :" + parts[i] + " already exists.")
parent_path=parent_path+parts[i] parent_path = parent_path + parts[i]
# parts=path.split('/') # parts=path.split('/')
# parent_path=None # parent_path=None
@ -319,126 +368,149 @@ class KeycloakClient():
# try: # try:
# self.add_group(parts[i],parent_path,skip_exists=True) # self.add_group(parts[i],parent_path,skip_exists=True)
# except: # except:
# if parent_path==None: # if parent_path==None:
# parent_path='/'+parts[i] # parent_path='/'+parts[i]
# else: # else:
# parent_path=self.get_group_by_path(parent_path)['path'] # parent_path=self.get_group_by_path(parent_path)['path']
# parent_path=parent_path+'/'+parts[i] # parent_path=parent_path+'/'+parts[i]
# continue # continue
# if parent_path==None: # if parent_path==None:
# parent_path='/'+parts[i] # parent_path='/'+parts[i]
# else: # else:
# parent_path=parent_path+'/'+parts[i] # parent_path=parent_path+'/'+parts[i]
# try:
# if i == 1: parent_id=self.add_group(parts[i])
# except:
# # Main already exists?? What a fail!
# parent_id=self.get_group(parent_id)['id']
# continue
# self.add_group(parts[i],parent_id)
def add_user_with_groups_and_role(self,username,first,last,email,password,role,groups): # try:
# if i == 1: parent_id=self.add_group(parts[i])
# except:
# # Main already exists?? What a fail!
# parent_id=self.get_group(parent_id)['id']
# continue
# self.add_group(parts[i],parent_id)
def add_user_with_groups_and_role(
self, username, first, last, email, password, role, groups
):
## Add user ## Add user
uid=self.add_user(username,first,last,email,password) uid = self.add_user(username, first, last, email, password)
## Add user to role ## Add user to role
log.info('User uid: '+str(uid)+ ' role: '+str(role)) log.info("User uid: " + str(uid) + " role: " + str(role))
try: try:
therole=role[0] therole = role[0]
except: except:
therole='' therole = ""
log.info(self.assign_realm_roles(uid,role)) log.info(self.assign_realm_roles(uid, role))
## Create groups in user ## Create groups in user
for g in groups: for g in groups:
log.warning('Creating keycloak group: '+g) log.warning("Creating keycloak group: " + g)
parts=g.split('/') parts = g.split("/")
parent_path=None parent_path = None
for i in range(1,len(parts)): for i in range(1, len(parts)):
# parent_id=None if parent_path==None else self.get_group(parent_path)['id'] # parent_id=None if parent_path==None else self.get_group(parent_path)['id']
try: try:
self.add_group(parts[i],parent_path,skip_exists=True) self.add_group(parts[i], parent_path, skip_exists=True)
except: except:
log.warning('Group '+str(parent_path)+ ' already exists. Skipping creation') log.warning(
"Group "
+ str(parent_path)
+ " already exists. Skipping creation"
)
pass pass
if parent_path is None: if parent_path is None:
thepath='/'+parts[i] thepath = "/" + parts[i]
else: else:
thepath=parent_path+'/'+parts[i] thepath = parent_path + "/" + parts[i]
if thepath=='/': if thepath == "/":
log.warning('Not adding the user '+username+' to any group as does not have any...') log.warning(
"Not adding the user "
+ username
+ " to any group as does not have any..."
)
continue continue
gid=self.get_group_by_path(path=thepath)['id'] gid = self.get_group_by_path(path=thepath)["id"]
log.warning('Adding '+username+' with uuid: '+uid+' to group '+g+' with uuid: '+gid) log.warning(
self.keycloak_admin.group_user_add(uid,gid) "Adding "
+ username
+ " with uuid: "
+ uid
+ " to group "
+ g
+ " with uuid: "
+ gid
)
self.keycloak_admin.group_user_add(uid, gid)
if parent_path == None:
if parent_path==None: parent_path='' parent_path = ""
parent_path=parent_path+'/'+parts[i] parent_path = parent_path + "/" + parts[i]
# self.group_user_add(uid,gid) # self.group_user_add(uid,gid)
## ROLES ## ROLES
def get_roles(self): def get_roles(self):
self.connect() self.connect()
return self.keycloak_admin.get_realm_roles() return self.keycloak_admin.get_realm_roles()
def get_role(self,name): def get_role(self, name):
self.connect() self.connect()
return self.keycloak_admin.get_realm_role(name) return self.keycloak_admin.get_realm_role(name)
def add_role(self,name,description=''): def add_role(self, name, description=""):
self.connect() self.connect()
return self.keycloak_admin.create_realm_role({"name":name, "description":description}) return self.keycloak_admin.create_realm_role(
{"name": name, "description": description}
)
def delete_role(self,name): def delete_role(self, name):
self.connect() self.connect()
return self.keycloak_admin.delete_realm_role(name) return self.keycloak_admin.delete_realm_role(name)
## CLIENTS ## CLIENTS
def get_client_roles(self,client_id): def get_client_roles(self, client_id):
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,name,description=''): def add_client_role(self, client_id, name, description=""):
self.connect() self.connect()
return self.keycloak_admin.create_client_role(client_id, {'name': name, 'description':description, 'clientRole': True}) return self.keycloak_admin.create_client_role(
client_id, {"name": name, "description": description, "clientRole": True}
)
## SYSTEM ## SYSTEM
def get_server_info(self): def get_server_info(self):
self.connect() self.connect()
return self.keycloak_admin.get_server_info() return self.keycloak_admin.get_server_info()
def get_server_clients(self): def get_server_clients(self):
self.connect() self.connect()
return self.keycloak_admin.get_clients() return self.keycloak_admin.get_clients()
def get_server_rsa_key(self): def get_server_rsa_key(self):
self.connect() self.connect()
rsa_key = [k for k in self.keycloak_admin.get_keys()['keys'] if k['type']=='RSA'][0] rsa_key = [
return {'name':rsa_key['kid'],'certificate':rsa_key['certificate']} k for k in self.keycloak_admin.get_keys()["keys"] if k["type"] == "RSA"
][0]
return {"name": rsa_key["kid"], "certificate": rsa_key["certificate"]}
## REALM ## REALM
def assign_realm_roles(self, user_id, role): def assign_realm_roles(self, user_id, role):
self.connect() self.connect()
try: try:
role=[r for r in self.keycloak_admin.get_realm_roles() if r['name']==role] role = [
r for r in self.keycloak_admin.get_realm_roles() if r["name"] == role
]
except: except:
return False return False
return self.keycloak_admin.assign_realm_roles(user_id=user_id, roles=role) return self.keycloak_admin.assign_realm_roles(user_id=user_id, roles=role)
# return self.keycloak_admin.assign_realm_roles(user_id=user_id, client_id=None, roles=role) # 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()
return self.keycloak_admin.delete_client(clientid) return self.keycloak_admin.delete_client(clientid)
def add_client(self,client): def add_client(self, client):
self.connect() self.connect()
return self.keycloak_admin.create_client(client) return self.keycloak_admin.create_client(client)

View File

@ -1,24 +1,39 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf-8 # coding=utf-8
from admin import app
import os, sys
import logging as log import logging as log
import os
import sys
import traceback import traceback
class loadConfig(): from admin import app
def __init__(self, app=None):
class loadConfig:
def __init__(self, app=None):
try: try:
app.config.setdefault('DOMAIN', os.environ['DOMAIN']) app.config.setdefault("DOMAIN", os.environ["DOMAIN"])
app.config.setdefault('KEYCLOAK_POSTGRES_USER', os.environ['KEYCLOAK_DB_USER']) app.config.setdefault(
app.config.setdefault('KEYCLOAK_POSTGRES_PASSWORD', os.environ['KEYCLOAK_DB_PASSWORD']) "KEYCLOAK_POSTGRES_USER", os.environ["KEYCLOAK_DB_USER"]
app.config.setdefault('MOODLE_POSTGRES_USER', os.environ['MOODLE_POSTGRES_USER']) )
app.config.setdefault('MOODLE_POSTGRES_PASSWORD', os.environ['MOODLE_POSTGRES_PASSWORD']) app.config.setdefault(
app.config.setdefault('NEXTCLOUD_POSTGRES_USER', os.environ['NEXTCLOUD_POSTGRES_USER']) "KEYCLOAK_POSTGRES_PASSWORD", os.environ["KEYCLOAK_DB_PASSWORD"]
app.config.setdefault('NEXTCLOUD_POSTGRES_PASSWORD', os.environ['NEXTCLOUD_POSTGRES_PASSWORD']) )
app.config.setdefault('VERIFY', True if os.environ['VERIFY']=="true" else False) app.config.setdefault(
"MOODLE_POSTGRES_USER", os.environ["MOODLE_POSTGRES_USER"]
)
app.config.setdefault(
"MOODLE_POSTGRES_PASSWORD", os.environ["MOODLE_POSTGRES_PASSWORD"]
)
app.config.setdefault(
"NEXTCLOUD_POSTGRES_USER", os.environ["NEXTCLOUD_POSTGRES_USER"]
)
app.config.setdefault(
"NEXTCLOUD_POSTGRES_PASSWORD", os.environ["NEXTCLOUD_POSTGRES_PASSWORD"]
)
app.config.setdefault(
"VERIFY", True if os.environ["VERIFY"] == "true" else False
)
except Exception as e: except Exception as e:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise

View File

@ -1,35 +1,43 @@
import logging as log
import traceback
from pprint import pprint
from requests import get, post from requests import get, post
from admin import app from admin import app
import logging as log
from pprint import pprint
import traceback
from .postgres import Postgres
from .exceptions import UserExists, UserNotFound from .exceptions import UserExists, UserNotFound
from .postgres import Postgres
# Module variables to connect to moodle api # Module variables to connect to moodle api
class Moodle():
class Moodle:
"""https://github.com/mrcinv/moodle_api.py """https://github.com/mrcinv/moodle_api.py
https://docs.moodle.org/dev/Web_service_API_functions https://docs.moodle.org/dev/Web_service_API_functions
https://docs.moodle.org/311/en/Using_web_services https://docs.moodle.org/311/en/Using_web_services
""" """
def __init__(self, def __init__(
key=app.config["MOODLE_WS_TOKEN"], self,
url="https://moodle."+app.config["DOMAIN"], key=app.config["MOODLE_WS_TOKEN"],
endpoint="/webservice/rest/server.php", url="https://moodle." + app.config["DOMAIN"],
verify=app.config["VERIFY"]): endpoint="/webservice/rest/server.php",
verify=app.config["VERIFY"],
):
self.key = key self.key = key
self.url = url self.url = url
self.endpoint = endpoint self.endpoint = endpoint
self.verify=verify self.verify = verify
self.moodle_pg=Postgres('isard-apps-postgresql','moodle',app.config['MOODLE_POSTGRES_USER'],app.config['MOODLE_POSTGRES_PASSWORD']) self.moodle_pg = Postgres(
"isard-apps-postgresql",
"moodle",
app.config["MOODLE_POSTGRES_USER"],
app.config["MOODLE_POSTGRES_PASSWORD"],
)
def rest_api_parameters(self, in_args, prefix="", out_dict=None):
def rest_api_parameters(self, in_args, prefix='', out_dict=None):
"""Transform dictionary/array structure to a flat dictionary, with key names """Transform dictionary/array structure to a flat dictionary, with key names
defining the structure. defining the structure.
Example usage: Example usage:
@ -37,23 +45,23 @@ class Moodle():
{'courses[0][id]':1, {'courses[0][id]':1,
'courses[0][name]':'course1'} 'courses[0][name]':'course1'}
""" """
if out_dict==None: if out_dict == None:
out_dict = {} out_dict = {}
if not type(in_args) in (list,dict): if not type(in_args) in (list, dict):
out_dict[prefix] = in_args out_dict[prefix] = in_args
return out_dict return out_dict
if prefix == '': if prefix == "":
prefix = prefix + '{0}' prefix = prefix + "{0}"
else: else:
prefix = prefix + '[{0}]' prefix = prefix + "[{0}]"
if type(in_args)==list: if type(in_args) == list:
for idx, item in enumerate(in_args): for idx, item in enumerate(in_args):
self.rest_api_parameters(item, prefix.format(idx), out_dict) self.rest_api_parameters(item, prefix.format(idx), out_dict)
elif type(in_args)==dict: elif type(in_args) == dict:
for key, item in in_args.items(): for key, item in in_args.items():
self.rest_api_parameters(item, prefix.format(key), out_dict) self.rest_api_parameters(item, prefix.format(key), out_dict)
return out_dict return out_dict
def call(self, fname, **kwargs): def call(self, fname, **kwargs):
"""Calls moodle API function with function name fname and keyword arguments. """Calls moodle API function with function name fname and keyword arguments.
Example: Example:
@ -61,54 +69,69 @@ class Moodle():
courses = [{'id': 1, 'fullname': 'My favorite course'}]) courses = [{'id': 1, 'fullname': 'My favorite course'}])
""" """
parameters = self.rest_api_parameters(kwargs) parameters = self.rest_api_parameters(kwargs)
parameters.update({"wstoken": self.key, 'moodlewsrestformat': 'json', "wsfunction": fname}) parameters.update(
response = post(self.url+self.endpoint, parameters, verify=self.verify) {"wstoken": self.key, "moodlewsrestformat": "json", "wsfunction": fname}
)
response = post(self.url + self.endpoint, parameters, verify=self.verify)
response = response.json() response = response.json()
if type(response) == dict and response.get('exception'): if type(response) == dict and response.get("exception"):
raise SystemError(response) raise SystemError(response)
return response return response
def create_user(self, email, username, password, first_name='-', last_name='-'): def create_user(self, email, username, password, first_name="-", last_name="-"):
if len(self.get_user_by('username',username)['users']): if len(self.get_user_by("username", username)["users"]):
raise UserExists raise UserExists
try: try:
data = [{'username': username, 'email':email, data = [
'password': password, 'firstname':first_name, 'lastname':last_name}] {
user = self.call('core_user_create_users', users=data) "username": username,
return user #[{'id': 8, 'username': 'asdfw'}] "email": email,
"password": password,
"firstname": first_name,
"lastname": last_name,
}
]
user = self.call("core_user_create_users", users=data)
return user # [{'id': 8, 'username': 'asdfw'}]
except SystemError as se: except SystemError as se:
raise SystemError(se.args[0]['message']) raise SystemError(se.args[0]["message"])
def update_user(self, username, email, first_name, last_name, enabled=True): def update_user(self, username, email, first_name, last_name, enabled=True):
user = self.get_user_by('username',username)['users'][0] user = self.get_user_by("username", username)["users"][0]
if not len(user): if not len(user):
raise UserNotFound raise UserNotFound
try: try:
data = [{'id':user['id'],'username': username, 'email':email, data = [
'firstname':first_name, 'lastname':last_name, {
'suspended':0 if enabled else 1}] "id": user["id"],
user = self.call('core_user_update_users', users=data) "username": username,
"email": email,
"firstname": first_name,
"lastname": last_name,
"suspended": 0 if enabled else 1,
}
]
user = self.call("core_user_update_users", users=data)
return user return user
except SystemError as se: except SystemError as se:
raise SystemError(se.args[0]['message']) raise SystemError(se.args[0]["message"])
def delete_user(self, user_id): def delete_user(self, user_id):
user = self.call('core_user_delete_users', userids=[user_id]) user = self.call("core_user_delete_users", userids=[user_id])
return user return user
def delete_users(self, userids): def delete_users(self, userids):
user = self.call('core_user_delete_users', userids=userids) user = self.call("core_user_delete_users", userids=userids)
return user return user
def get_user_by(self, key, value): def get_user_by(self, key, value):
criteria = [{'key': key, 'value': value}] criteria = [{"key": key, "value": value}]
try: try:
user = self.call('core_user_get_users', criteria=criteria) user = self.call("core_user_get_users", criteria=criteria)
except: except:
raise SystemError("Error calling Moodle API\n", traceback.format_exc()) raise SystemError("Error calling Moodle API\n", traceback.format_exc())
return user return user
#{'users': [{'id': 8, 'username': 'asdfw', 'firstname': 'afowie', 'lastname': 'aokjdnfwe', 'fullname': 'afowie aokjdnfwe', 'email': 'awfewe@ads.com', 'department': '', 'firstaccess': 0, 'lastaccess': 0, 'auth': 'manual', 'suspended': False, 'confirmed': True, 'lang': 'ca', 'theme': '', 'timezone': '99', 'mailformat': 1, 'profileimageurlsmall': 'https://moodle.mydomain.duckdns.org/theme/image.php/cbe/core/1630941606/u/f2', 'profileimageurl': 'https://DOMAIN/theme/image.php/cbe/core/1630941606/u/f1'}], 'warnings': []} # {'users': [{'id': 8, 'username': 'asdfw', 'firstname': 'afowie', 'lastname': 'aokjdnfwe', 'fullname': 'afowie aokjdnfwe', 'email': 'awfewe@ads.com', 'department': '', 'firstaccess': 0, 'lastaccess': 0, 'auth': 'manual', 'suspended': False, 'confirmed': True, 'lang': 'ca', 'theme': '', 'timezone': '99', 'mailformat': 1, 'profileimageurlsmall': 'https://moodle.mydomain.duckdns.org/theme/image.php/cbe/core/1630941606/u/f2', 'profileimageurl': 'https://DOMAIN/theme/image.php/cbe/core/1630941606/u/f1'}], 'warnings': []}
def get_users_with_groups_and_roles(self): def get_users_with_groups_and_roles(self):
q = """select u.id as id, username, firstname as first, lastname as last, email, json_agg(h.name) as groups, json_agg(r.shortname) as roles q = """select u.id as id, username, firstname as first, lastname as last, email, json_agg(h.name) as groups, json_agg(r.shortname) as roles
@ -119,8 +142,13 @@ class Moodle():
left join mdl_role as r on r.id = ra.roleid left join mdl_role as r on r.id = ra.roleid
where u.deleted = 0 where u.deleted = 0
group by u.id , username, first, last, email""" group by u.id , username, first, last, email"""
(headers,users)=self.moodle_pg.select_with_headers(q) (headers, users) = self.moodle_pg.select_with_headers(q)
users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users] users_with_lists = [
list(l[:-2])
+ ([[]] if l[-2] == [None] else [list(set(l[-2]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users
]
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists] list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
return list_dict_users return list_dict_users
@ -134,26 +162,32 @@ class Moodle():
def enroll_user_to_course(self, user_id, course_id, role_id=5): def enroll_user_to_course(self, user_id, course_id, role_id=5):
# 5 is student # 5 is student
data = [{'roleid': role_id, 'userid': user_id, 'courseid': course_id}] data = [{"roleid": role_id, "userid": user_id, "courseid": course_id}]
enrolment = self.call('enrol_manual_enrol_users', enrolments=data) enrolment = self.call("enrol_manual_enrol_users", enrolments=data)
return enrolment return enrolment
def get_quiz_attempt(self, quiz_id, user_id): def get_quiz_attempt(self, quiz_id, user_id):
attempts = self.call('mod_quiz_get_user_attempts', quizid=quiz_id, userid=user_id) attempts = self.call(
"mod_quiz_get_user_attempts", quizid=quiz_id, userid=user_id
)
return attempts return attempts
def get_cohorts(self): def get_cohorts(self):
cohorts = self.call('core_cohort_get_cohorts') cohorts = self.call("core_cohort_get_cohorts")
return cohorts return cohorts
def add_system_cohort(self,name,description='',visible=True): def add_system_cohort(self, name, description="", visible=True):
visible=1 if visible else 0 visible = 1 if visible else 0
data = [{'categorytype': {'type': 'system', 'value': ''}, data = [
'name': name, {
'idnumber': name, "categorytype": {"type": "system", "value": ""},
'description': description, "name": name,
'visible': visible}] "idnumber": name,
cohort = self.call('core_cohort_create_cohorts', cohorts=data) "description": description,
"visible": visible,
}
]
cohort = self.call("core_cohort_create_cohorts", cohorts=data)
return cohort return cohort
# def add_users_to_cohort(self,users,cohort): # def add_users_to_cohort(self,users,cohort):
@ -161,52 +195,61 @@ class Moodle():
# user = self.call('core_cohort_add_cohort_members', criteria=criteria) # user = self.call('core_cohort_add_cohort_members', criteria=criteria)
# return user # return user
def add_user_to_cohort(self,userid,cohortid): def add_user_to_cohort(self, userid, cohortid):
members=[{'cohorttype':{'type':'id','value':cohortid}, members = [
'usertype':{'type':'id','value':userid}}] {
user = self.call('core_cohort_add_cohort_members', members=members) "cohorttype": {"type": "id", "value": cohortid},
"usertype": {"type": "id", "value": userid},
}
]
user = self.call("core_cohort_add_cohort_members", members=members)
return user return user
def delete_user_in_cohort(self,userid,cohortid): def delete_user_in_cohort(self, userid, cohortid):
members=[{'cohortid':cohortid, members = [{"cohortid": cohortid, "userid": userid}]
'userid':userid}] user = self.call("core_cohort_delete_cohort_members", members=members)
user = self.call('core_cohort_delete_cohort_members', members=members)
return user return user
def get_cohort_members(self, cohort_ids): def get_cohort_members(self, cohort_ids):
members = self.call('core_cohort_get_cohort_members', cohortids=cohort_ids) members = self.call("core_cohort_get_cohort_members", cohortids=cohort_ids)
#[0]['userids'] # [0]['userids']
return members return members
def delete_cohorts(self, cohortids): def delete_cohorts(self, cohortids):
deleted = self.call('core_cohort_delete_cohorts', cohortids=cohortids) deleted = self.call("core_cohort_delete_cohorts", cohortids=cohortids)
return deleted return deleted
def get_user_cohorts(self, user_id): def get_user_cohorts(self, user_id):
user_cohorts=[] user_cohorts = []
cohorts=self.get_cohorts() cohorts = self.get_cohorts()
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): def add_user_to_siteadmin(self, user_id):
q = """SELECT value FROM mdl_config WHERE name='siteadmins'""" q = """SELECT value FROM mdl_config WHERE name='siteadmins'"""
value=self.moodle_pg.select(q)[0][0] value = self.moodle_pg.select(q)[0][0]
if str(user_id) not in value: if str(user_id) not in value:
value=value+','+str(user_id) value = value + "," + str(user_id)
q = """UPDATE mdl_config SET value = '%s' WHERE name='siteadmins'""" % (value) q = """UPDATE mdl_config SET value = '%s' WHERE name='siteadmins'""" % (
value
)
self.moodle_pg.update(q) self.moodle_pg.update(q)
log.warning('MOODLE:ADDING THE USER TO ADMINS: This needs a purge cache in moodle!') 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'): # def add_role_to_user(self, user_id, role='admin', context='missing'):
# if role=='admin': # if role=='admin':
# role_id=1 # role_id=1
# else: # else:
# return False # return False
# assignments = [{'roleid': role_id, 'userid': user_id, 'contextid': 0}] # assignments = [{'roleid': role_id, 'userid': user_id, 'contextid': 0}]
# self.call('core_role_assign_roles', assignments=assignments) # self.call('core_role_assign_roles', assignments=assignments)
# userid=user_id, role_id=role_id) # userid=user_id, role_id=role_id)
# 'contextlevel': 1, # 'contextlevel': 1,
# define('CONTEXT_SYSTEM', 10); # define('CONTEXT_SYSTEM', 10);
# define('CONTEXT_USER', 30); # define('CONTEXT_USER', 30);

View File

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

View File

@ -1,38 +1,63 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf-8 # coding=utf-8
#from ..lib.log import * import json
from admin import app
import time,requests,json,pprint,os
import urllib
import traceback
import logging as log import logging as log
from .nextcloud_exc import * import os
import pprint
import time
import traceback
import urllib
import requests
# from ..lib.log import *
from admin import app
from .nextcloud_exc import *
from .postgres import Postgres from .postgres import Postgres
class Nextcloud():
def __init__(self,
url="https://nextcloud."+app.config['DOMAIN'],
username=os.environ['NEXTCLOUD_ADMIN_USER'],
password=os.environ['NEXTCLOUD_ADMIN_PASSWORD'],
verify=True):
self.verify_cert=verify class Nextcloud:
self.apiurl=url+'/ocs/v1.php/cloud/' def __init__(
self.shareurl=url+'/ocs/v2.php/apps/files_sharing/api/v1/' self,
self.davurl=url+'/remote.php/dav/files/' url="https://nextcloud." + app.config["DOMAIN"],
self.auth=(username,password) username=os.environ["NEXTCLOUD_ADMIN_USER"],
self.user=username password=os.environ["NEXTCLOUD_ADMIN_PASSWORD"],
verify=True,
):
self.nextcloud_pg=Postgres('isard-apps-postgresql','nextcloud',app.config['NEXTCLOUD_POSTGRES_USER'],app.config['NEXTCLOUD_POSTGRES_PASSWORD']) self.verify_cert = verify
self.apiurl = url + "/ocs/v1.php/cloud/"
self.shareurl = url + "/ocs/v2.php/apps/files_sharing/api/v1/"
self.davurl = url + "/remote.php/dav/files/"
self.auth = (username, password)
self.user = username
def _request(self,method,url,data={},headers={'OCS-APIRequest':'true'},auth=False): self.nextcloud_pg = Postgres(
if auth == False: auth=self.auth "isard-apps-postgresql",
"nextcloud",
app.config["NEXTCLOUD_POSTGRES_USER"],
app.config["NEXTCLOUD_POSTGRES_PASSWORD"],
)
def _request(
self, method, url, data={}, headers={"OCS-APIRequest": "true"}, auth=False
):
if auth == False:
auth = self.auth
try: try:
response = requests.request(method, url, data=data, auth=auth, verify=self.verify_cert, headers=headers) response = requests.request(
if 'meta' in response.text: method,
if '<statuscode>997</statuscode>' in response.text: raise ProviderUnauthorized url,
data=data,
auth=auth,
verify=self.verify_cert,
headers=headers,
)
if "meta" in response.text:
if "<statuscode>997</statuscode>" in response.text:
raise ProviderUnauthorized
# if '<statuscode>998</statuscode>' in response.text: raise ProviderInvalidQuery # if '<statuscode>998</statuscode>' in response.text: raise ProviderInvalidQuery
return response.text return response.text
@ -48,15 +73,16 @@ class Nextcloud():
# except requests.exceptions.RequestException as err: # except requests.exceptions.RequestException as err:
# raise ProviderError # raise ProviderError
except Exception as e: except Exception as e:
if str(e) == 'an integer is required (got type bytes)': if str(e) == "an integer is required (got type bytes)":
raise ProviderConnError raise ProviderConnError
raise ProviderError raise ProviderError
def check_connection(self): def check_connection(self):
url = self.apiurl + "users/"+self.user+"?format=json" url = self.apiurl + "users/" + self.user + "?format=json"
try: try:
result = self._request('GET',url) result = self._request("GET", url)
if json.loads(result)['ocs']['meta']['statuscode'] == 100: return True if json.loads(result)["ocs"]["meta"]["statuscode"] == 100:
return True
raise ProviderError raise ProviderError
except requests.exceptions.HTTPError as errh: except requests.exceptions.HTTPError as errh:
raise ProviderConnError raise ProviderConnError
@ -69,43 +95,43 @@ class Nextcloud():
except requests.exceptions.RequestException as err: except requests.exceptions.RequestException as err:
raise ProviderError raise ProviderError
except Exception as e: except Exception as e:
if str(e) == 'an integer is required (got type bytes)': if str(e) == "an integer is required (got type bytes)":
raise ProviderConnError raise ProviderConnError
raise ProviderError raise ProviderError
def get_user(self,userid): def get_user(self, userid):
url = self.apiurl + "users/"+userid+"?format=json" url = self.apiurl + "users/" + userid + "?format=json"
try: try:
result = json.loads(self._request('GET',url)) result = json.loads(self._request("GET", url))
if result['ocs']['meta']['statuscode'] == 100: return result['ocs']['data'] if result["ocs"]["meta"]["statuscode"] == 100:
return result["ocs"]["data"]
raise ProviderItemNotExists raise ProviderItemNotExists
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise
# 100 - successful # 100 - successful
# q = """select u.uid as username, adn.value as displayname, ade.value as email, json_agg(gg.displayname) as admin_groups,json_agg(g.displayname) as groups # q = """select u.uid as username, adn.value as displayname, ade.value as email, json_agg(gg.displayname) as admin_groups,json_agg(g.displayname) as groups
# from oc_users as u # from oc_users as u
# left join oc_group_user as gu on gu.uid = u.uid # left join oc_group_user as gu on gu.uid = u.uid
# left join oc_groups as g on gu.gid = g.gid # left join oc_groups as g on gu.gid = g.gid
# left join oc_group_admin as ga on ga.uid = u.uid # left join oc_group_admin as ga on ga.uid = u.uid
# left join oc_groups as gg on gg.gid = ga.gid # left join oc_groups as gg on gg.gid = ga.gid
# left join oc_accounts_data as adn on adn.uid = u.uid and adn.name = 'displayname' # left join oc_accounts_data as adn on adn.uid = u.uid and adn.name = 'displayname'
# left join oc_accounts_data as ade on ade.uid = u.uid and ade.name = 'email' # left join oc_accounts_data as ade on ade.uid = u.uid and ade.name = 'email'
# group by u.uid, adn.value, ade.value""" # group by u.uid, adn.value, ade.value"""
# cur.execute(q) # cur.execute(q)
# users = cur.fetchall() # users = cur.fetchall()
# fields = [a.name for a in cur.description] # fields = [a.name for a in cur.description]
# cur.close() # cur.close()
# conn.close() # conn.close()
# users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
# users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users] # users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
# users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists] # list_dict_users = [dict(zip(fields, r)) for r in users_with_lists]
# list_dict_users = [dict(zip(fields, r)) for r in users_with_lists]
def get_users_list(self): def get_users_list(self):
# q = """select u.uid as username, adn.value as displayname, ade.value as email, json_agg(gg.displayname) as admin_groups,json_agg(g.displayname) as groups # q = """select u.uid as username, adn.value as displayname, ade.value as email, json_agg(gg.displayname) as admin_groups,json_agg(g.displayname) as groups
# from oc_users as u # from oc_users as u
# left join oc_group_user as gu on gu.uid = u.uid # left join oc_group_user as gu on gu.uid = u.uid
# left join oc_groups as g on gu.gid = g.gid # left join oc_groups as g on gu.gid = g.gid
# left join oc_group_admin as ga on ga.uid = u.uid # left join oc_group_admin as ga on ga.uid = u.uid
@ -127,9 +153,19 @@ class Nextcloud():
left join oc_storages as s on s.id=CONCAT('home::',u.uid) left join oc_storages as s on s.id=CONCAT('home::',u.uid)
left join oc_filecache as fc on fc.storage = numeric_id left join oc_filecache as fc on fc.storage = numeric_id
group by u.uid, adn.value, ade.value, pref.configvalue""" group by u.uid, adn.value, ade.value, pref.configvalue"""
(headers,users)=self.nextcloud_pg.select_with_headers(q) (headers, users) = self.nextcloud_pg.select_with_headers(q)
users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users] users_with_lists = [
users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists] list(l[:-2])
+ ([[]] if l[-2] == [None] else [list(set(l[-2]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users
]
users_with_lists = [
list(l[:-2])
+ ([[]] if l[-2] == [None] else [list(set(l[-2]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users_with_lists
]
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists] list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
return list_dict_users return list_dict_users
@ -143,33 +179,46 @@ class Nextcloud():
# raise ProviderOpError # raise ProviderOpError
# except: # except:
# log.error(traceback.format_exc()) # log.error(traceback.format_exc())
# raise # raise
def add_user(self,userid,userpassword,quota=False,group=False,email='',displayname=''): def add_user(
data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname} self, userid, userpassword, quota=False, group=False, email="", displayname=""
if not group: del data['groups[]'] ):
if not quota: del data['quota'] data = {
"userid": userid,
"password": userpassword,
"quota": quota,
"groups[]": group,
"email": email,
"displayname": displayname,
}
if not group:
del data["groups[]"]
if not quota:
del data["quota"]
# if group: # 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: # else:
# data={'userid':userid,'password':userpassword,'quota':quota,'email':email,'displayname':displayname} # 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",
'OCS-APIRequest': 'true', "OCS-APIRequest": "true",
} }
try: try:
result = json.loads(self._request('POST',url,data=data,headers=headers)) result = json.loads(self._request("POST", url, data=data, headers=headers))
if result['ocs']['meta']['statuscode'] == 100: return True if result["ocs"]["meta"]["statuscode"] == 100:
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists return True
if result['ocs']['meta']['statuscode'] == 104: if result["ocs"]["meta"]["statuscode"] == 102:
raise ProviderItemExists
if result["ocs"]["meta"]["statuscode"] == 104:
self.add_group(group) self.add_group(group)
# raise ProviderGroupNotExists # raise ProviderGroupNotExists
log.error('Get Nextcloud provider user add error: '+str(result)) log.error("Get Nextcloud provider user add error: " + str(result))
raise ProviderOpError raise ProviderOpError
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise
# 100 - successful # 100 - successful
# 101 - invalid input data # 101 - invalid input data
# 102 - username already exists # 102 - username already exists
@ -184,89 +233,113 @@ class Nextcloud():
url = self.apiurl + "users/" + userid + "?format=json" url = self.apiurl + "users/" + userid + "?format=json"
headers = { headers = {
'Content-Type': 'application/x-www-form-urlencoded', "Content-Type": "application/x-www-form-urlencoded",
'OCS-APIRequest': 'true', "OCS-APIRequest": "true",
} }
for k,v in key_values.items(): for k, v in key_values.items():
data={"key":k,"value":v} data = {"key": k, "value": v}
try: try:
result = json.loads(self._request('PUT',url,data=data,headers=headers)) result = json.loads(
if result['ocs']['meta']['statuscode'] == 100: return True self._request("PUT", url, data=data, headers=headers)
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists )
if result['ocs']['meta']['statuscode'] == 104: raise ProviderGroupNotExists if result["ocs"]["meta"]["statuscode"] == 100:
log.error('Get Nextcloud provider user add error: '+str(result)) return True
if result["ocs"]["meta"]["statuscode"] == 102:
raise ProviderItemExists
if result["ocs"]["meta"]["statuscode"] == 104:
raise ProviderGroupNotExists
log.error("Get Nextcloud provider user add error: " + str(result))
raise ProviderOpError raise ProviderOpError
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise
def add_user_to_group(self, userid, group_id): def add_user_to_group(self, userid, group_id):
data={'groupid':group_id} data = {"groupid": group_id}
url = self.apiurl + "users/" + userid + "/groups?format=json" url = self.apiurl + "users/" + userid + "/groups?format=json"
headers = { headers = {
'Content-Type': 'application/x-www-form-urlencoded', "Content-Type": "application/x-www-form-urlencoded",
'OCS-APIRequest': 'true', "OCS-APIRequest": "true",
} }
try: try:
result = json.loads(self._request('POST',url,data=data,headers=headers)) result = json.loads(self._request("POST", url, data=data, headers=headers))
if result['ocs']['meta']['statuscode'] == 100: return True if result["ocs"]["meta"]["statuscode"] == 100:
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists return True
if result['ocs']['meta']['statuscode'] == 104: raise ProviderGroupNotExists if result["ocs"]["meta"]["statuscode"] == 102:
log.error('Get Nextcloud provider user add error: '+str(result)) raise ProviderItemExists
if result["ocs"]["meta"]["statuscode"] == 104:
raise ProviderGroupNotExists
log.error("Get Nextcloud provider user add error: " + str(result))
raise ProviderOpError raise ProviderOpError
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise
def remove_user_from_group(self, userid, group_id): def remove_user_from_group(self, userid, group_id):
data={'groupid':group_id} data = {"groupid": group_id}
url = self.apiurl + "users/" + userid + "/groups?format=json" url = self.apiurl + "users/" + userid + "/groups?format=json"
headers = { headers = {
'Content-Type': 'application/x-www-form-urlencoded', "Content-Type": "application/x-www-form-urlencoded",
'OCS-APIRequest': 'true', "OCS-APIRequest": "true",
} }
try: try:
result = json.loads(self._request('DELETE',url,data=data,headers=headers)) result = json.loads(
if result['ocs']['meta']['statuscode'] == 100: return True self._request("DELETE", url, data=data, headers=headers)
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists )
if result['ocs']['meta']['statuscode'] == 104: if result["ocs"]["meta"]["statuscode"] == 100:
return True
if result["ocs"]["meta"]["statuscode"] == 102:
raise ProviderItemExists
if result["ocs"]["meta"]["statuscode"] == 104:
self.add_group(group) self.add_group(group)
# raise ProviderGroupNotExists # raise ProviderGroupNotExists
log.error('Get Nextcloud provider user add error: '+str(result)) log.error("Get Nextcloud provider user add error: " + str(result))
raise ProviderOpError raise ProviderOpError
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise
def add_user_with_groups(self,userid,userpassword,quota=False,groups=[],email='',displayname=''): def add_user_with_groups(
data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':groups,'email':email,'displayname':displayname} self, userid, userpassword, quota=False, groups=[], email="", displayname=""
):
data = {
"userid": userid,
"password": userpassword,
"quota": quota,
"groups[]": groups,
"email": email,
"displayname": displayname,
}
# if not group: del data['groups[]'] # if not group: del data['groups[]']
if not quota: del data['quota'] if not quota:
del data["quota"]
# if group: # 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: # else:
# data={'userid':userid,'password':userpassword,'quota':quota,'email':email,'displayname':displayname} # 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",
'OCS-APIRequest': 'true', "OCS-APIRequest": "true",
} }
try: try:
result = json.loads(self._request('POST',url,data=data,headers=headers)) result = json.loads(self._request("POST", url, data=data, headers=headers))
if result['ocs']['meta']['statuscode'] == 100: return True if result["ocs"]["meta"]["statuscode"] == 100:
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists return True
if result['ocs']['meta']['statuscode'] == 104: if result["ocs"]["meta"]["statuscode"] == 102:
raise ProviderItemExists
if result["ocs"]["meta"]["statuscode"] == 104:
# self.add_group(group) # self.add_group(group)
None None
# raise ProviderGroupNotExists # raise ProviderGroupNotExists
log.error('Get Nextcloud provider user add error: '+str(result)) log.error("Get Nextcloud provider user add error: " + str(result))
raise ProviderOpError raise ProviderOpError
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise
# 100 - successful # 100 - successful
# 101 - invalid input data # 101 - invalid input data
# 102 - username already exists # 102 - username already exists
@ -276,149 +349,172 @@ class Nextcloud():
# 106 - no group specified (required for subadmins) # 106 - no group specified (required for subadmins)
# 107 - all errors that contain a hint - for example “Password is among the 1,000,000 most common ones. Please make it unique.” (this code was added in 12.0.6 & 13.0.1) # 107 - all errors that contain a hint - for example “Password is among the 1,000,000 most common ones. Please make it unique.” (this code was added in 12.0.6 & 13.0.1)
def delete_user(self,userid): def delete_user(self, userid):
url = self.apiurl + "users/"+userid+"?format=json" url = self.apiurl + "users/" + userid + "?format=json"
try: try:
result = json.loads(self._request('DELETE',url)) result = json.loads(self._request("DELETE", url))
if result['ocs']['meta']['statuscode'] == 100: return True if result["ocs"]["meta"]["statuscode"] == 100:
if result['ocs']['meta']['statuscode'] == 101: raise ProviderUserNotExists return True
if result["ocs"]["meta"]["statuscode"] == 101:
raise ProviderUserNotExists
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise ProviderOpError raise ProviderOpError
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise
# 100 - successful # 100 - successful
# 101 - failure # 101 - failure
def enable_user(self,userid): def enable_user(self, userid):
None None
def disable_user(self,userid): def disable_user(self, userid):
None None
def exists_user_folder(self,userid,userpassword,folder='IsardVDI'): def exists_user_folder(self, userid, userpassword, folder="IsardVDI"):
auth=(userid,userpassword) auth = (userid, userpassword)
url = self.davurl + userid +"/" + folder+"?format=json" url = self.davurl + userid + "/" + folder + "?format=json"
headers = { headers = {
'Depth': '0', "Depth": "0",
'Content-Type': 'application/x-www-form-urlencoded', "Content-Type": "application/x-www-form-urlencoded",
'OCS-APIRequest': 'true', "OCS-APIRequest": "true",
} }
try: try:
result = self._request('PROPFIND',url,auth=auth,headers=headers) result = self._request("PROPFIND", url, auth=auth, headers=headers)
if '<d:status>HTTP/1.1 200 OK</d:status>' in result: return True if "<d:status>HTTP/1.1 200 OK</d:status>" in result:
return True
return False return False
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise
def add_user_folder(self,userid,userpassword,folder='IsardVDI'): def add_user_folder(self, userid, userpassword, folder="IsardVDI"):
auth=(userid,userpassword) auth = (userid, userpassword)
url = self.davurl + userid +"/" + folder+"?format=json" url = self.davurl + userid + "/" + folder + "?format=json"
headers = { headers = {
'Content-Type': 'application/x-www-form-urlencoded', "Content-Type": "application/x-www-form-urlencoded",
'OCS-APIRequest': 'true', "OCS-APIRequest": "true",
} }
try: try:
result = self._request('MKCOL',url,auth=auth,headers=headers) result = self._request("MKCOL", url, auth=auth, headers=headers)
if result=='': return True if result == "":
if '<s:message>The resource you tried to create already exists</s:message>' in result: raise ProviderItemExists return True
log.error(result.split('message>')[1].split('<')[0]) if (
"<s:message>The resource you tried to create already exists</s:message>"
in result
):
raise ProviderItemExists
log.error(result.split("message>")[1].split("<")[0])
raise ProviderOpError raise ProviderOpError
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise
def exists_user_share_folder(self,userid,userpassword,folder='IsardVDI'): def exists_user_share_folder(self, userid, userpassword, folder="IsardVDI"):
auth=(userid,userpassword) auth = (userid, userpassword)
url = self.shareurl + "shares?format=json" url = self.shareurl + "shares?format=json"
headers = { headers = {
'Content-Type': 'application/x-www-form-urlencoded', "Content-Type": "application/x-www-form-urlencoded",
'OCS-APIRequest': 'true', "OCS-APIRequest": "true",
} }
try: try:
result = json.loads(self._request('GET', url, auth=auth, headers=headers)) result = json.loads(self._request("GET", url, auth=auth, headers=headers))
if result['ocs']['meta']['statuscode']==200: if result["ocs"]["meta"]["statuscode"] == 200:
share=[s for s in result['ocs']['data'] if s['path'] == '/'+folder] share = [s for s in result["ocs"]["data"] if s["path"] == "/" + folder]
if len(share) >= 1: if len(share) >= 1:
# Should we delete all but the first (0) one? # Should we delete all but the first (0) one?
return {'token': share[0]['token'], return {"token": share[0]["token"], "url": share[0]["url"]}
'url': share[0]['url']}
raise ProviderItemNotExists raise ProviderItemNotExists
raise ProviderOpError raise ProviderOpError
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise
def add_user_share_folder(self,userid,userpassword,folder='IsardVDI'): def add_user_share_folder(self, userid, userpassword, folder="IsardVDI"):
auth=(userid,userpassword) auth = (userid, userpassword)
data={'path':'/'+folder,'shareType':3} data = {"path": "/" + folder, "shareType": 3}
url = self.shareurl + "shares?format=json" url = self.shareurl + "shares?format=json"
headers = { headers = {
'Content-Type': 'application/x-www-form-urlencoded', "Content-Type": "application/x-www-form-urlencoded",
'OCS-APIRequest': 'true', "OCS-APIRequest": "true",
} }
try: try:
result = json.loads(self._request('POST',url, data=data, auth=auth, headers=headers)) result = json.loads(
if result['ocs']['meta']['statuscode'] == 100 or result['ocs']['meta']['statuscode'] == 200: self._request("POST", url, data=data, auth=auth, headers=headers)
return {'token': result['ocs']['data']['token'], )
'url': result['ocs']['data']['url']} if (
log.error('Add user share folder error: '+result['ocs']['meta']['message']) result["ocs"]["meta"]["statuscode"] == 100
or result["ocs"]["meta"]["statuscode"] == 200
):
return {
"token": result["ocs"]["data"]["token"],
"url": result["ocs"]["data"]["url"],
}
log.error(
"Add user share folder error: " + result["ocs"]["meta"]["message"]
)
raise ProviderFolderNotExists raise ProviderFolderNotExists
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise
def get_group(self,userid): def get_group(self, userid):
None None
def get_groups_list(self): def get_groups_list(self):
url = self.apiurl + "groups?format=json" url = self.apiurl + "groups?format=json"
try: try:
result = json.loads(self._request('GET',url)) result = json.loads(self._request("GET", url))
if result['ocs']['meta']['statuscode'] == 100: return [g for g in result['ocs']['data']['groups']] if result["ocs"]["meta"]["statuscode"] == 100:
return [g for g in result["ocs"]["data"]["groups"]]
raise ProviderOpError raise ProviderOpError
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise
def add_group(self,groupid): def add_group(self, groupid):
data={'groupid':groupid} data = {"groupid": groupid}
url = self.apiurl + "groups?format=json" url = self.apiurl + "groups?format=json"
headers = { headers = {
'Content-Type': 'application/x-www-form-urlencoded', "Content-Type": "application/x-www-form-urlencoded",
'OCS-APIRequest': 'true', "OCS-APIRequest": "true",
} }
try: try:
result = json.loads(self._request('POST',url, data=data, auth=self.auth, headers=headers)) result = json.loads(
if result['ocs']['meta']['statuscode'] == 100: return True self._request("POST", url, data=data, auth=self.auth, headers=headers)
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists )
if result["ocs"]["meta"]["statuscode"] == 100:
return True
if result["ocs"]["meta"]["statuscode"] == 102:
raise ProviderItemExists
raise ProviderOpError raise ProviderOpError
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise
# 100 - successful # 100 - successful
# 101 - invalid input data # 101 - invalid input data
# 102 - group already exists # 102 - group already exists
# 103 - failed to add the group # 103 - failed to add the group
def delete_group(self,groupid): def delete_group(self, groupid):
group = urllib.parse.quote(groupid, safe='') group = urllib.parse.quote(groupid, safe="")
url = self.apiurl + "groups/"+group+"?format=json" url = self.apiurl + "groups/" + group + "?format=json"
headers = { headers = {
'Content-Type': 'application/x-www-form-urlencoded', "Content-Type": "application/x-www-form-urlencoded",
'OCS-APIRequest': 'true', "OCS-APIRequest": "true",
} }
try: try:
result = json.loads(self._request('DELETE',url, auth=self.auth, headers=headers)) result = json.loads(
if result['ocs']['meta']['statuscode'] == 100: return True self._request("DELETE", url, auth=self.auth, headers=headers)
)
if result["ocs"]["meta"]["statuscode"] == 100:
return True
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise ProviderOpError raise ProviderOpError
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise
# 100 - successful # 100 - successful
# 101 - invalid input data # 101 - invalid input data
# 102 - group already exists # 102 - group already exists
# 103 - failed to add the group # 103 - failed to add the group

View File

@ -3,30 +3,38 @@
class ProviderUnauthorized(Exception): class ProviderUnauthorized(Exception):
pass pass
class ProviderConnError(Exception): class ProviderConnError(Exception):
pass pass
class ProviderSslError(Exception): class ProviderSslError(Exception):
pass pass
class ProviderConnTimeout(Exception): class ProviderConnTimeout(Exception):
pass pass
class ProviderError(Exception): class ProviderError(Exception):
pass pass
class ProviderItemExists(Exception): class ProviderItemExists(Exception):
pass pass
class ProviderItemNotExists(Exception): class ProviderItemNotExists(Exception):
pass pass
class ProviderGroupNotExists(Exception): class ProviderGroupNotExists(Exception):
pass pass
class ProviderFolderNotExists(Exception): class ProviderFolderNotExists(Exception):
pass pass
class ProviderOpError(Exception): class ProviderOpError(Exception):
pass pass

View File

@ -1,50 +1,48 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf-8 # coding=utf-8
import json
import logging as log
import time import time
from admin import app import traceback
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging as log
import traceback
import yaml, json
import psycopg2 import psycopg2
import yaml
class Postgres(): from admin import app
def __init__(self,host,database,user,password):
class Postgres:
def __init__(self, host, database, user, password):
self.conn = psycopg2.connect( self.conn = psycopg2.connect(
host=host, host=host, database=database, user=user, password=password
database=database, )
user=user,
password=password)
# def __del__(self): # def __del__(self):
# self.cur.close() # self.cur.close()
# self.conn.close() # self.conn.close()
def select(self,sql): def select(self, sql):
self.cur = self.conn.cursor() self.cur = self.conn.cursor()
self.cur.execute(sql) self.cur.execute(sql)
data=self.cur.fetchall() data = self.cur.fetchall()
self.cur.close() self.cur.close()
return data return data
def update(self,sql): def update(self, sql):
self.cur = self.conn.cursor() self.cur = self.conn.cursor()
self.cur.execute(sql) self.cur.execute(sql)
self.conn.commit() self.conn.commit()
self.cur.close() self.cur.close()
# return self.cur.fetchall() # return self.cur.fetchall()
def select_with_headers(self,sql): def select_with_headers(self, sql):
self.cur = self.conn.cursor() self.cur = self.conn.cursor()
self.cur.execute(sql) self.cur.execute(sql)
data=self.cur.fetchall() data = self.cur.fetchall()
fields = [a.name for a in self.cur.description] fields = [a.name for a in self.cur.description]
self.cur.close() self.cur.close()
return (fields,data) return (fields, data)
# def update_moodle_saml_plugin(self): # def update_moodle_saml_plugin(self):
# plugin[('idpmetadata', '<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.'+app.config['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>NrtA5ynG0htowP3SXw7dBJRIAMxn-1PwuuXwOwNhlRw</ds:KeyName><ds:X509Data><ds:X509Certificate>MIICmzCCAYMCBgF5jb0RCTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwNTIxMDcwMjI4WhcNMzEwNTIxMDcwNDA4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCI8xh/C0+frz3kgWiUbziTDls71R2YiXLSVE+bw7gbEgZUGCLhoEI679azMtIxmnzM/snIX+yTb12+XoYkgbiLTMPQfnH+Kiab6g3HL3KPfhqS+yWkFxOoCp6Ibmp7yPlVWuHH+MBfO8OBr/r8Ao7heFbuzjiLd1KG67rcoaxfDgMuBoEomg1bgEjFgHaQIrSC6OZzH0h987/arqufZXeXlfyiqScMPUi+u5IpDWSwz06UKP0k8mxzNSlpZ93CKOUSsV0SMLxqg7FQ3SGiOk577bGW9o9BDTkkmSo3Up6smc0LzwvvUwuNd0B1irGkWZFQN9OXJnJYf1InEebIMtmPAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADM34+qEGeBQ22luphVTuVJtGxcbxLx7DfsT0QfJD/OuxTTbNAa1VRyarb5juIAkqdj4y2quZna9ZXLecVo4RkwpzPoKoAkYA8b+kHnWqEwJi9iPrDvKb+GR0bBkLPN49YxIZ8IdKX/PRa3yuLHe+loiNsCaS/2ZK2KO46COsqU4QX1iVhF9kWphNLybjNAX45B6cJLsa1g0vXLdm3kv3SB4I2fErFVaOoDtFIjttoYlXdpUiThkPXBfr7N67P3dZHaS4tjJh+IZ8I6TINpcsH8dBkUhzYEIPHCePwSiC1w6WDBLNDuKt1mj1CZrLq+1x+Yhrs+QNRheEKGi89HZ8N0=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml/resolve" index="0"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.mydomain.duckdns.org/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.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/></md:IDPSSODescriptor></md:EntityDescriptor></md:EntitiesDescriptor>')] # plugin[('idpmetadata', '<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.'+app.config['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>NrtA5ynG0htowP3SXw7dBJRIAMxn-1PwuuXwOwNhlRw</ds:KeyName><ds:X509Data><ds:X509Certificate>MIICmzCCAYMCBgF5jb0RCTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwNTIxMDcwMjI4WhcNMzEwNTIxMDcwNDA4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCI8xh/C0+frz3kgWiUbziTDls71R2YiXLSVE+bw7gbEgZUGCLhoEI679azMtIxmnzM/snIX+yTb12+XoYkgbiLTMPQfnH+Kiab6g3HL3KPfhqS+yWkFxOoCp6Ibmp7yPlVWuHH+MBfO8OBr/r8Ao7heFbuzjiLd1KG67rcoaxfDgMuBoEomg1bgEjFgHaQIrSC6OZzH0h987/arqufZXeXlfyiqScMPUi+u5IpDWSwz06UKP0k8mxzNSlpZ93CKOUSsV0SMLxqg7FQ3SGiOk577bGW9o9BDTkkmSo3Up6smc0LzwvvUwuNd0B1irGkWZFQN9OXJnJYf1InEebIMtmPAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADM34+qEGeBQ22luphVTuVJtGxcbxLx7DfsT0QfJD/OuxTTbNAa1VRyarb5juIAkqdj4y2quZna9ZXLecVo4RkwpzPoKoAkYA8b+kHnWqEwJi9iPrDvKb+GR0bBkLPN49YxIZ8IdKX/PRa3yuLHe+loiNsCaS/2ZK2KO46COsqU4QX1iVhF9kWphNLybjNAX45B6cJLsa1g0vXLdm3kv3SB4I2fErFVaOoDtFIjttoYlXdpUiThkPXBfr7N67P3dZHaS4tjJh+IZ8I6TINpcsH8dBkUhzYEIPHCePwSiC1w6WDBLNDuKt1mj1CZrLq+1x+Yhrs+QNRheEKGi89HZ8N0=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml/resolve" index="0"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.mydomain.duckdns.org/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.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/></md:IDPSSODescriptor></md:EntityDescriptor></md:EntitiesDescriptor>')]
@ -52,4 +50,4 @@ class Postgres():
# cursor.execute(pg_update, (title, bookid)) # cursor.execute(pg_update, (title, bookid))
# connection.commit() # connection.commit()
# count = cursor.rowcount # count = cursor.rowcount
# print(count, "Successfully Updated!") # print(count, "Successfully Updated!")

View File

@ -1,74 +1,111 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf-8 # coding=utf-8
import time, os import json
from admin import app
from datetime import datetime, timedelta
import logging as log import logging as log
import traceback import os
import yaml, json import random
import psycopg2
from .postgres import Postgres
# from .keycloak import Keycloak # from .keycloak import Keycloak
# from .moodle import Moodle # from .moodle import Moodle
import string, random import string
import time
import traceback
from datetime import datetime, timedelta
class Postup(): import psycopg2
import yaml
from admin import app
from .postgres import Postgres
class Postup:
def __init__(self): def __init__(self):
ready=False ready = False
while not ready: while not ready:
try: try:
self.pg=Postgres('isard-apps-postgresql','moodle',app.config['MOODLE_POSTGRES_USER'],app.config['MOODLE_POSTGRES_PASSWORD']) self.pg = Postgres(
ready=True "isard-apps-postgresql",
"moodle",
app.config["MOODLE_POSTGRES_USER"],
app.config["MOODLE_POSTGRES_PASSWORD"],
)
ready = True
except: except:
log.warning('Could not connect to moodle database. Retrying...') log.warning("Could not connect to moodle database. Retrying...")
time.sleep(2) time.sleep(2)
log.info('Connected to moodle database.') log.info("Connected to moodle database.")
ready=False ready = False
while not ready: while not ready:
try: try:
with open(os.path.join(app.root_path, "../moodledata/saml2/moodle."+app.config['DOMAIN']+".crt"),"r") as crt: with open(
app.config.setdefault('SP_CRT', crt.read()) os.path.join(
ready=True app.root_path,
"../moodledata/saml2/moodle." + app.config["DOMAIN"] + ".crt",
),
"r",
) as crt:
app.config.setdefault("SP_CRT", crt.read())
ready = True
except IOError: except IOError:
log.warning('Could not get moodle SAML2 crt certificate. Retrying...') log.warning("Could not get moodle SAML2 crt certificate. Retrying...")
time.sleep(2) time.sleep(2)
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
log.info('Got moodle srt certificate.') log.info("Got moodle srt certificate.")
ready=False ready = False
while not ready: while not ready:
try: try:
with open(os.path.join(app.root_path, "../moodledata/saml2/moodle."+app.config['DOMAIN']+".pem"),"r") as pem: with open(
app.config.setdefault('SP_PEM', pem.read()) os.path.join(
ready=True app.root_path,
"../moodledata/saml2/moodle." + app.config["DOMAIN"] + ".pem",
),
"r",
) as pem:
app.config.setdefault("SP_PEM", pem.read())
ready = True
except IOError: except IOError:
log.warning('Could not get moodle SAML2 pem certificate. Retrying...') log.warning("Could not get moodle SAML2 pem certificate. Retrying...")
time.sleep(2) time.sleep(2)
log.info('Got moodle pem certificate.') log.info("Got moodle pem certificate.")
self.select_and_configure_theme() self.select_and_configure_theme()
self.configure_tipnc() self.configure_tipnc()
self.add_moodle_ws_token() self.add_moodle_ws_token()
def select_and_configure_theme(self,theme='cbe'): def select_and_configure_theme(self, theme="cbe"):
try: try:
self.pg.update("""UPDATE "mdl_config" SET value = '%s' WHERE "name" = 'theme';""" % (theme)) self.pg.update(
"""UPDATE "mdl_config" SET value = '%s' WHERE "name" = 'theme';"""
% (theme)
)
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
exit(1) exit(1)
None None
try: try:
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'theme_cbe' AND "name" = 'host';""" % (os.environ['DOMAIN'])) self.pg.update(
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'theme_cbe' AND "name" = 'logourl';""" % ("https://api."+os.environ['DOMAIN']+"/img/logo.png")) """UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'theme_cbe' AND "name" = 'host';"""
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '1' WHERE "plugin" = 'theme_cbe' AND "name" = 'header_api';""") % (os.environ["DOMAIN"])
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '1' WHERE "plugin" = 'theme_cbe' AND "name" = 'vclasses_direct';""") )
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '1' WHERE "plugin" = 'theme_cbe' AND "name" = 'uniquenamecourse';""") self.pg.update(
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'theme_cbe' AND "name" = 'logourl';"""
% ("https://api." + os.environ["DOMAIN"] + "/img/logo.png")
)
self.pg.update(
"""UPDATE "mdl_config_plugins" SET value = '1' WHERE "plugin" = 'theme_cbe' AND "name" = 'header_api';"""
)
self.pg.update(
"""UPDATE "mdl_config_plugins" SET value = '1' WHERE "plugin" = 'theme_cbe' AND "name" = 'vclasses_direct';"""
)
self.pg.update(
"""UPDATE "mdl_config_plugins" SET value = '1' WHERE "plugin" = 'theme_cbe' AND "name" = 'uniquenamecourse';"""
)
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
exit(1) exit(1)
@ -76,12 +113,27 @@ class Postup():
def configure_tipnc(self): def configure_tipnc(self):
try: try:
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'host';""" % ("https://nextcloud."+os.environ['DOMAIN']+"/")) self.pg.update(
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'password';""" % (os.environ['NEXTCLOUD_ADMIN_PASSWORD'])) """UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'host';"""
self.pg.update("""UPDATE "mdl_config_plugins" SET value = 'template.docx' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'template';""") % ("https://nextcloud." + os.environ["DOMAIN"] + "/")
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '/apps/onlyoffice/' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'location';""") )
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'user';""" % (os.environ['NEXTCLOUD_ADMIN_USER'])) self.pg.update(
self.pg.update("""UPDATE "mdl_config_plugins" SET value = 'tasks' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'folder';""") """UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'password';"""
% (os.environ["NEXTCLOUD_ADMIN_PASSWORD"])
)
self.pg.update(
"""UPDATE "mdl_config_plugins" SET value = 'template.docx' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'template';"""
)
self.pg.update(
"""UPDATE "mdl_config_plugins" SET value = '/apps/onlyoffice/' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'location';"""
)
self.pg.update(
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'user';"""
% (os.environ["NEXTCLOUD_ADMIN_USER"])
)
self.pg.update(
"""UPDATE "mdl_config_plugins" SET value = 'tasks' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'folder';"""
)
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
exit(1) exit(1)
@ -89,18 +141,23 @@ class Postup():
def add_moodle_ws_token(self): def add_moodle_ws_token(self):
try: try:
token=self.pg.select("""SELECT * FROM "mdl_external_tokens" WHERE "externalserviceid" = 3""")[0][1] token = self.pg.select(
app.config.setdefault('MOODLE_WS_TOKEN',token) """SELECT * FROM "mdl_external_tokens" WHERE "externalserviceid" = 3"""
)[0][1]
app.config.setdefault("MOODLE_WS_TOKEN", token)
return return
except: except:
# log.error(traceback.format_exc()) # log.error(traceback.format_exc())
None None
try: try:
self.pg.update("""INSERT INTO "mdl_external_services" ("name", "enabled", "requiredcapability", "restrictedusers", "component", "timecreated", "timemodified", "shortname", "downloadfiles", "uploadfiles") VALUES self.pg.update(
('dd admin', 1, '', 1, NULL, 1621719763, 1621719850, 'dd_admin', 0, 0);""") """INSERT INTO "mdl_external_services" ("name", "enabled", "requiredcapability", "restrictedusers", "component", "timecreated", "timemodified", "shortname", "downloadfiles", "uploadfiles") VALUES
('dd admin', 1, '', 1, NULL, 1621719763, 1621719850, 'dd_admin', 0, 0);"""
)
self.pg.update("""INSERT INTO "mdl_external_services_functions" ("externalserviceid", "functionname") VALUES self.pg.update(
"""INSERT INTO "mdl_external_services_functions" ("externalserviceid", "functionname") VALUES
(3, 'core_course_update_courses'), (3, 'core_course_update_courses'),
(3, 'core_user_get_users'), (3, 'core_user_get_users'),
(3, 'core_user_get_users_by_field'), (3, 'core_user_get_users_by_field'),
@ -116,17 +173,37 @@ class Postup():
(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_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(
(3, 2, NULL, NULL, 1621719871);""") """INSERT INTO "mdl_external_services_users" ("externalserviceid", "userid", "iprestriction", "validuntil", "timecreated") VALUES
(3, 2, NULL, NULL, 1621719871);"""
)
b32=''.join(random.choices(string.ascii_uppercase + string.ascii_uppercase + string.ascii_lowercase, k = 32)) b32 = "".join(
b64=''.join(random.choices(string.ascii_uppercase + string.ascii_uppercase + string.ascii_lowercase, k = 64)) random.choices(
self.pg.update("""INSERT INTO "mdl_external_tokens" ("token", "privatetoken", "tokentype", "userid", "externalserviceid", "sid", "contextid", "creatorid", "iprestriction", "validuntil", "timecreated", "lastaccess") VALUES string.ascii_uppercase
('%s', '%s', 0, 2, 3, NULL, 1, 2, NULL, 0, 1621831206, NULL);""" % (b32,b64)) + string.ascii_uppercase
+ string.ascii_lowercase,
app.config.setdefault('MOODLE_WS_TOKEN',b32) k=32,
)
)
b64 = "".join(
random.choices(
string.ascii_uppercase
+ string.ascii_uppercase
+ string.ascii_lowercase,
k=64,
)
)
self.pg.update(
"""INSERT INTO "mdl_external_tokens" ("token", "privatetoken", "tokentype", "userid", "externalserviceid", "sid", "contextid", "creatorid", "iprestriction", "validuntil", "timecreated", "lastaccess") VALUES
('%s', '%s', 0, 2, 3, NULL, 1, 2, NULL, 0, 1621831206, NULL);"""
% (b32, b64)
)
app.config.setdefault("MOODLE_WS_TOKEN", b32)
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
exit(1) exit(1)

View File

@ -1,315 +1,488 @@
#!flask/bin/python #!flask/bin/python
# coding=utf-8 # coding=utf-8
from admin import app
import logging as log
import traceback
from uuid import uuid4
import time,json
import sys,os,re
from flask import render_template, Response, request, redirect, url_for, jsonify
import concurrent.futures import concurrent.futures
from flask_login import current_user, login_required import json
from .decorators import is_admin import logging as log
import os
from ..lib.helpers import system_group import re
import sys
# import Queue # import Queue
import threading import threading
threads={'external':None} import time
import traceback
from uuid import uuid4
from flask import Response, jsonify, redirect, render_template, request, url_for
from flask_login import current_user, login_required
from admin import app
from ..lib.helpers import system_group
from .decorators import is_admin
threads = {"external": None}
# q = Queue.Queue() # q = Queue.Queue()
from keycloak.exceptions import KeycloakGetError from keycloak.exceptions import KeycloakGetError
from ..lib.exceptions import UserExists, UserNotFound from ..lib.exceptions import UserExists, UserNotFound
@app.route('/api/resync')
@app.route("/api/resync")
@login_required @login_required
def resync(): def resync():
return json.dumps(app.admin.resync_data()), 200, {'Content-Type': 'application/json'} return (
json.dumps(app.admin.resync_data()),
200,
{"Content-Type": "application/json"},
)
@app.route('/api/users', methods=['GET','PUT'])
@app.route('/api/users/<provider>', methods=['POST', 'PUT', 'GET', 'DELETE']) @app.route("/api/users", methods=["GET", "PUT"])
@app.route("/api/users/<provider>", methods=["POST", "PUT", "GET", "DELETE"])
@login_required @login_required
def users(provider=False): def users(provider=False):
if request.method == 'DELETE': if request.method == "DELETE":
if current_user.role != 'admin': return json.dumps({}), 301, {'Content-Type': 'application/json'} if current_user.role != "admin":
if provider == 'keycloak': return json.dumps({}), 301, {"Content-Type": "application/json"}
return json.dumps(app.admin.delete_keycloak_users()), 200, {'Content-Type': 'application/json'} if provider == "keycloak":
if provider == 'nextcloud': return (
return json.dumps(app.admin.delete_nextcloud_users()), 200, {'Content-Type': 'application/json'} json.dumps(app.admin.delete_keycloak_users()),
if provider == 'moodle': 200,
return json.dumps(app.admin.delete_moodle_users()), 200, {'Content-Type': 'application/json'} {"Content-Type": "application/json"},
if request.method == 'POST': )
if current_user.role != 'admin': return json.dumps({}), 301, {'Content-Type': 'application/json'} if provider == "nextcloud":
if provider == 'moodle': return (
return json.dumps(app.admin.sync_to_moodle()), 200, {'Content-Type': 'application/json'} json.dumps(app.admin.delete_nextcloud_users()),
if provider == 'nextcloud': 200,
return json.dumps(app.admin.sync_to_nextcloud()), 200, {'Content-Type': 'application/json'} {"Content-Type": "application/json"},
if request.method == 'PUT' and not provider: )
if current_user.role != 'admin': return json.dumps({}), 301, {'Content-Type': 'application/json'} if provider == "moodle":
return (
json.dumps(app.admin.delete_moodle_users()),
200,
{"Content-Type": "application/json"},
)
if request.method == "POST":
if current_user.role != "admin":
return json.dumps({}), 301, {"Content-Type": "application/json"}
if provider == "moodle":
return (
json.dumps(app.admin.sync_to_moodle()),
200,
{"Content-Type": "application/json"},
)
if provider == "nextcloud":
return (
json.dumps(app.admin.sync_to_nextcloud()),
200,
{"Content-Type": "application/json"},
)
if request.method == "PUT" and not provider:
if current_user.role != "admin":
return json.dumps({}), 301, {"Content-Type": "application/json"}
if 'external' in threads.keys(): if "external" in threads.keys():
if threads['external'] is not None and threads['external'].is_alive(): if threads["external"] is not None and threads["external"].is_alive():
return json.dumps({'msg':'Precondition failed: already working with users'}), 412, {'Content-Type': 'application/json'} return (
json.dumps(
{"msg": "Precondition failed: already working with users"}
),
412,
{"Content-Type": "application/json"},
)
else: else:
threads['external']=None threads["external"] = None
try: try:
threads['external'] = threading.Thread(target=app.admin.update_users_from_keycloak, args=()) threads["external"] = threading.Thread(
threads['external'].start() target=app.admin.update_users_from_keycloak, args=()
return json.dumps({}), 200, {'Content-Type': 'application/json'} )
threads["external"].start()
return json.dumps({}), 200, {"Content-Type": "application/json"}
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
return json.dumps({'msg':'Add user error.'}), 500, {'Content-Type': 'application/json'} return (
json.dumps({"msg": "Add user error."}),
500,
{"Content-Type": "application/json"},
)
# return json.dumps(app.admin.update_users_from_keycloak()), 200, {'Content-Type': 'application/json'} # return json.dumps(app.admin.update_users_from_keycloak()), 200, {'Content-Type': 'application/json'}
users = app.admin.get_mix_users() users = app.admin.get_mix_users()
if current_user.role != 'admin': if current_user.role != "admin":
for user in users: for user in users:
user['keycloak_groups'] = [g for g in user['keycloak_groups'] if not system_group(g) ] user["keycloak_groups"] = [
return json.dumps(users), 200, {'Content-Type': 'application/json'} g for g in user["keycloak_groups"] if not system_group(g)
]
@app.route('/api/users_bulk/<action>', methods=['PUT']) return json.dumps(users), 200, {"Content-Type": "application/json"}
@app.route("/api/users_bulk/<action>", methods=["PUT"])
@login_required @login_required
def users_bulk(action): def users_bulk(action):
data=request.get_json(force=True) data = request.get_json(force=True)
if request.method == 'PUT': if request.method == "PUT":
if action == 'enable': if action == "enable":
if 'external' in threads.keys(): if "external" in threads.keys():
if threads['external'] is not None and threads['external'].is_alive(): if threads["external"] is not None and threads["external"].is_alive():
return json.dumps({'msg':'Precondition failed: already operating users'}), 412, {'Content-Type': 'application/json'} return (
json.dumps(
{"msg": "Precondition failed: already operating users"}
),
412,
{"Content-Type": "application/json"},
)
else: else:
threads['external']=None threads["external"] = None
try: try:
threads['external'] = threading.Thread(target=app.admin.enable_users, args=(data,)) threads["external"] = threading.Thread(
threads['external'].start() target=app.admin.enable_users, args=(data,)
return json.dumps({}), 200, {'Content-Type': 'application/json'} )
threads["external"].start()
return json.dumps({}), 200, {"Content-Type": "application/json"}
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
return json.dumps({'msg':'Enable users error.'}), 500, {'Content-Type': 'application/json'} return (
if action == 'disable': json.dumps({"msg": "Enable users error."}),
if 'external' in threads.keys(): 500,
if threads['external'] is not None and threads['external'].is_alive(): {"Content-Type": "application/json"},
return json.dumps({'msg':'Precondition failed: already operating users'}), 412, {'Content-Type': 'application/json'} )
if action == "disable":
if "external" in threads.keys():
if threads["external"] is not None and threads["external"].is_alive():
return (
json.dumps(
{"msg": "Precondition failed: already operating users"}
),
412,
{"Content-Type": "application/json"},
)
else: else:
threads['external']=None threads["external"] = None
try: try:
threads['external'] = threading.Thread(target=app.admin.disable_users, args=(data,)) threads["external"] = threading.Thread(
threads['external'].start() target=app.admin.disable_users, args=(data,)
return json.dumps({}), 200, {'Content-Type': 'application/json'} )
threads["external"].start()
return json.dumps({}), 200, {"Content-Type": "application/json"}
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
return json.dumps({'msg':'Disabling users error.'}), 500, {'Content-Type': 'application/json'} return (
if action == 'delete': json.dumps({"msg": "Disabling users error."}),
if 'external' in threads.keys(): 500,
if threads['external'] is not None and threads['external'].is_alive(): {"Content-Type": "application/json"},
return json.dumps({'msg':'Precondition failed: already operating users'}), 412, {'Content-Type': 'application/json'} )
if action == "delete":
if "external" in threads.keys():
if threads["external"] is not None and threads["external"].is_alive():
return (
json.dumps(
{"msg": "Precondition failed: already operating users"}
),
412,
{"Content-Type": "application/json"},
)
else: else:
threads['external']=None threads["external"] = None
try: try:
threads['external'] = threading.Thread(target=app.admin.delete_users, args=(data,)) threads["external"] = threading.Thread(
threads['external'].start() target=app.admin.delete_users, args=(data,)
return json.dumps({}), 200, {'Content-Type': 'application/json'} )
threads["external"].start()
return json.dumps({}), 200, {"Content-Type": "application/json"}
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
return json.dumps({'msg':'Deleting users error.'}), 500, {'Content-Type': 'application/json'} return (
return json.dumps({}), 405, {'Content-Type': 'application/json'} json.dumps({"msg": "Deleting users error."}),
500,
{"Content-Type": "application/json"},
)
return json.dumps({}), 405, {"Content-Type": "application/json"}
# Update pwd # Update pwd
@app.route('/api/user_password', methods=['GET']) @app.route("/api/user_password", methods=["GET"])
@app.route('/api/user_password/<userid>', methods=['PUT']) @app.route("/api/user_password/<userid>", methods=["PUT"])
@login_required @login_required
def user_password(userid=False): def user_password(userid=False):
if request.method == 'GET': if request.method == "GET":
return json.dumps(app.admin.get_dice_pwd()), 200, {'Content-Type': 'application/json'} return (
if request.method == 'PUT': json.dumps(app.admin.get_dice_pwd()),
data=request.get_json(force=True) 200,
password=data['password'] {"Content-Type": "application/json"},
temporary=data.get('temporary',True) )
if request.method == "PUT":
data = request.get_json(force=True)
password = data["password"]
temporary = data.get("temporary", True)
try: try:
res = app.admin.user_update_password(userid,password,temporary) res = app.admin.user_update_password(userid, password, temporary)
return json.dumps({}), 200, {'Content-Type': 'application/json'} return json.dumps({}), 200, {"Content-Type": "application/json"}
except KeycloakGetError as e: except KeycloakGetError as e:
log.error(e.error_message.decode("utf-8")) log.error(e.error_message.decode("utf-8"))
return json.dumps({'msg':'Update password error.'}), 500, {'Content-Type': 'application/json'} return (
json.dumps({"msg": "Update password error."}),
500,
{"Content-Type": "application/json"},
)
return json.dumps({}), 405, {"Content-Type": "application/json"}
return json.dumps({}), 405, {'Content-Type': 'application/json'}
# User # User
@app.route('/api/user', methods=['POST']) @app.route("/api/user", methods=["POST"])
@app.route('/api/user/<userid>', methods=['PUT', 'GET', 'DELETE']) @app.route("/api/user/<userid>", methods=["PUT", "GET", "DELETE"])
@login_required @login_required
def user(userid=None): def user(userid=None):
if request.method == 'DELETE': if request.method == "DELETE":
app.admin.delete_user(userid) app.admin.delete_user(userid)
return json.dumps({}), 200, {'Content-Type': 'application/json'} return json.dumps({}), 200, {"Content-Type": "application/json"}
if request.method == 'POST': if request.method == "POST":
data=request.get_json(force=True) data = request.get_json(force=True)
if app.admin.get_user_username(data['username']): if app.admin.get_user_username(data["username"]):
return json.dumps({'msg':'Add user error: already exists.'}), 409, {'Content-Type': 'application/json'} return (
data['enabled']=True if data.get('enabled',False) else False json.dumps({"msg": "Add user error: already exists."}),
data['quota']=data['quota'] if data['quota'] != 'false' else False 409,
data['groups']=data['groups'] if data.get('groups',False) else [] {"Content-Type": "application/json"},
if 'external' in threads.keys(): )
if threads['external'] is not None and threads['external'].is_alive(): data["enabled"] = True if data.get("enabled", False) else False
return json.dumps({'msg':'Precondition failed: already adding users'}), 412, {'Content-Type': 'application/json'} data["quota"] = data["quota"] if data["quota"] != "false" else False
data["groups"] = data["groups"] if data.get("groups", False) else []
if "external" in threads.keys():
if threads["external"] is not None and threads["external"].is_alive():
return (
json.dumps({"msg": "Precondition failed: already adding users"}),
412,
{"Content-Type": "application/json"},
)
else: else:
threads['external']=None threads["external"] = None
try: try:
threads['external'] = threading.Thread(target=app.admin.add_user, args=(data,)) threads["external"] = threading.Thread(
threads['external'].start() target=app.admin.add_user, args=(data,)
return json.dumps({}), 200, {'Content-Type': 'application/json'} )
threads["external"].start()
return json.dumps({}), 200, {"Content-Type": "application/json"}
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
return json.dumps({'msg':'Add user error.'}), 500, {'Content-Type': 'application/json'} return (
json.dumps({"msg": "Add user error."}),
500,
{"Content-Type": "application/json"},
)
if request.method == 'PUT': if request.method == "PUT":
data=request.get_json(force=True) data = request.get_json(force=True)
data['enabled']=True if data.get('enabled',False) else False data["enabled"] = True if data.get("enabled", False) else False
data['groups']=data['groups'] if data.get('groups',False) else [] data["groups"] = data["groups"] if data.get("groups", False) else []
data['roles']=[data.pop('role-keycloak')] data["roles"] = [data.pop("role-keycloak")]
try: try:
app.admin.user_update(data) app.admin.user_update(data)
return json.dumps({}), 200, {'Content-Type': 'application/json'} return json.dumps({}), 200, {"Content-Type": "application/json"}
except UserNotFound: except UserNotFound:
return json.dumps({'msg':'User not found.'}), 404, {'Content-Type': 'application/json'} return (
if request.method == 'DELETE': json.dumps({"msg": "User not found."}),
404,
{"Content-Type": "application/json"},
)
if request.method == "DELETE":
pass pass
if request.method == 'GET': if request.method == "GET":
user = app.admin.get_user(userid) user = app.admin.get_user(userid)
if not user: return json.dumps({'msg':'User not found.'}), 404, {'Content-Type': 'application/json'} if not user:
return json.dumps(user), 200, {'Content-Type': 'application/json'} return (
json.dumps({"msg": "User not found."}),
404,
{"Content-Type": "application/json"},
)
return json.dumps(user), 200, {"Content-Type": "application/json"}
@app.route('/api/roles')
@app.route("/api/roles")
@login_required @login_required
def roles(): def roles():
sorted_roles = sorted(app.admin.get_roles(), key=lambda k: k['name']) sorted_roles = sorted(app.admin.get_roles(), key=lambda k: k["name"])
if current_user.role != "admin": if current_user.role != "admin":
sorted_roles = [sr for sr in sorted_roles if sr['name'] != 'admin'] sorted_roles = [sr for sr in sorted_roles if sr["name"] != "admin"]
return json.dumps(sorted_roles), 200, {'Content-Type': 'application/json'} return json.dumps(sorted_roles), 200, {"Content-Type": "application/json"}
@app.route('/api/group', methods=['POST','DELETE'])
@app.route('/api/group/<group_id>', methods=['PUT', 'GET', 'DELETE']) @app.route("/api/group", methods=["POST", "DELETE"])
@app.route("/api/group/<group_id>", methods=["PUT", "GET", "DELETE"])
@login_required @login_required
def group(group_id=False): def group(group_id=False):
if request.method == 'POST': if request.method == "POST":
data=request.get_json(force=True) data = request.get_json(force=True)
data['parent']=data['parent'] if data['parent'] != '' else None data["parent"] = data["parent"] if data["parent"] != "" else None
return json.dumps(app.admin.add_group(data)), 200, {'Content-Type': 'application/json'} return (
if request.method == 'DELETE': json.dumps(app.admin.add_group(data)),
200,
{"Content-Type": "application/json"},
)
if request.method == "DELETE":
try: try:
data=request.get_json(force=True) data = request.get_json(force=True)
except: except:
data=False data = False
if data: if data:
res = app.admin.delete_group_by_path(data['path']) res = app.admin.delete_group_by_path(data["path"])
else: else:
res = app.admin.delete_group_by_id(group_id) res = app.admin.delete_group_by_id(group_id)
return json.dumps(res), 200, {'Content-Type': 'application/json'} return json.dumps(res), 200, {"Content-Type": "application/json"}
@app.route('/api/groups')
@app.route('/api/groups/<provider>', methods=['POST', 'PUT', 'GET', 'DELETE']) @app.route("/api/groups")
@app.route("/api/groups/<provider>", methods=["POST", "PUT", "GET", "DELETE"])
@login_required @login_required
def groups(provider=False): def groups(provider=False):
if request.method == 'GET': if request.method == "GET":
sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k['name']) sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k["name"])
if current_user.role != "admin": if current_user.role != "admin":
## internal groups should be avoided as are assigned with the role ## internal groups should be avoided as are assigned with the role
sorted_groups = [sg for sg in sorted_groups if not system_group(sg['name'])] sorted_groups = [sg for sg in sorted_groups if not system_group(sg["name"])]
else: else:
sorted_groups = [sg for sg in sorted_groups] sorted_groups = [sg for sg in sorted_groups]
return json.dumps(sorted_groups), 200, {'Content-Type': 'application/json'} return json.dumps(sorted_groups), 200, {"Content-Type": "application/json"}
if request.method == 'DELETE': if request.method == "DELETE":
if provider == 'keycloak': if provider == "keycloak":
return json.dumps(app.admin.delete_keycloak_groups()), 200, {'Content-Type': 'application/json'} return (
json.dumps(app.admin.delete_keycloak_groups()),
200,
{"Content-Type": "application/json"},
)
### SYSADM USERS ONLY ### SYSADM USERS ONLY
@app.route('/api/external', methods=['POST', 'PUT', 'GET','DELETE'])
@app.route("/api/external", methods=["POST", "PUT", "GET", "DELETE"])
@login_required @login_required
def external(): def external():
if 'external' in threads.keys(): if "external" in threads.keys():
if threads['external'] is not None and threads['external'].is_alive(): if threads["external"] is not None and threads["external"].is_alive():
return json.dumps({}), 301, {'Content-Type': 'application/json'} return json.dumps({}), 301, {"Content-Type": "application/json"}
else: else:
threads['external']=None threads["external"] = None
if request.method == 'POST': if request.method == "POST":
data=request.get_json(force=True) data = request.get_json(force=True)
if data['format']=='json-ga': if data["format"] == "json-ga":
threads['external'] = threading.Thread(target=app.admin.upload_json_ga, args=(data,)) threads["external"] = threading.Thread(
threads['external'].start() target=app.admin.upload_json_ga, args=(data,)
return json.dumps({}), 200, {'Content-Type': 'application/json'} )
if data['format']=='csv-ug': threads["external"].start()
return json.dumps({}), 200, {"Content-Type": "application/json"}
if data["format"] == "csv-ug":
valid = check_upload_errors(data) valid = check_upload_errors(data)
if valid['pass']: if valid["pass"]:
threads['external'] = threading.Thread(target=app.admin.upload_csv_ug, args=(data,)) threads["external"] = threading.Thread(
threads['external'].start() target=app.admin.upload_csv_ug, args=(data,)
return json.dumps({}), 200, {'Content-Type': 'application/json'} )
threads["external"].start()
return json.dumps({}), 200, {"Content-Type": "application/json"}
else: else:
return json.dumps(valid), 422, {'Content-Type': 'application/json'} return json.dumps(valid), 422, {"Content-Type": "application/json"}
if request.method == 'PUT': if request.method == "PUT":
data=request.get_json(force=True) data = request.get_json(force=True)
threads['external'] = threading.Thread(target=app.admin.sync_external, args=(data,)) threads["external"] = threading.Thread(
threads['external'].start() target=app.admin.sync_external, args=(data,)
return json.dumps({}), 200, {'Content-Type': 'application/json'} )
if request.method == 'DELETE': threads["external"].start()
print('RESET') return json.dumps({}), 200, {"Content-Type": "application/json"}
if request.method == "DELETE":
print("RESET")
app.admin.reset_external() app.admin.reset_external()
return json.dumps({}), 200, {'Content-Type': 'application/json'} return json.dumps({}), 200, {"Content-Type": "application/json"}
return json.dumps({}), 500, {'Content-Type': 'application/json'} return json.dumps({}), 500, {"Content-Type": "application/json"}
@app.route('/api/external/users')
@app.route("/api/external/users")
@login_required @login_required
def external_users_list(): def external_users_list():
while threads['external'] is not None and threads['external'].is_alive(): while threads["external"] is not None and threads["external"].is_alive():
time.sleep(.5) time.sleep(0.5)
return json.dumps(app.admin.get_external_users()), 200, {'Content-Type': 'application/json'} return (
json.dumps(app.admin.get_external_users()),
200,
{"Content-Type": "application/json"},
)
@app.route('/api/external/groups')
@app.route("/api/external/groups")
@login_required @login_required
def external_groups_list(): def external_groups_list():
while threads['external'] is not None and threads['external'].is_alive(): while threads["external"] is not None and threads["external"].is_alive():
time.sleep(.5) time.sleep(0.5)
return json.dumps(app.admin.get_external_groups()), 200, {'Content-Type': 'application/json'} return (
json.dumps(app.admin.get_external_groups()),
200,
{"Content-Type": "application/json"},
)
@app.route('/api/external/roles', methods=['PUT'])
@app.route("/api/external/roles", methods=["PUT"])
@login_required @login_required
def external_roles(): def external_roles():
if request.method == 'PUT': if request.method == "PUT":
return json.dumps(app.admin.external_roleassign(request.get_json(force=True))), 200, {'Content-Type': 'application/json'} return (
json.dumps(app.admin.external_roleassign(request.get_json(force=True))),
200,
{"Content-Type": "application/json"},
)
{'groups': '/alumnes/3er',
'firstname': 'Andreu', {
'lastname': 'B', "groups": "/alumnes/3er",
'email': '12andreub@escolamontseny.cat', "firstname": "Andreu",
'username': '12andreub', "lastname": "B",
'password': 'pepinillo', "email": "12andreub@escolamontseny.cat",
'password_temporal': 'yes', "username": "12andreub",
'role': 'student', "password": "pepinillo",
'quota': '500 MB', "password_temporal": "yes",
'': '', "role": "student",
'id': '12andreub'} "quota": "500 MB",
"": "",
"id": "12andreub",
}
def check_upload_errors(data): def check_upload_errors(data):
email_regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' email_regex = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
for u in data['data']: for u in data["data"]:
try: try:
user_groups=[g.strip() for g in u['groups'].split(',')] user_groups = [g.strip() for g in u["groups"].split(",")]
except: except:
return {'pass':False,'msg':'User '+u['username']+' has invalid groups: '+u['groups']} return {
"pass": False,
"msg": "User " + u["username"] + " has invalid groups: " + u["groups"],
}
if not re.fullmatch(email_regex, u['email']): if not re.fullmatch(email_regex, u["email"]):
return {'pass':False,'msg':'User '+u['username']+' has invalid email: '+u['email']} return {
"pass": False,
if u['role'] not in ['admin','manager','teacher','student']: "msg": "User " + u["username"] + " has invalid email: " + u["email"],
if u['role'] == '': }
return {'pass':False,'msg':'User '+u['username']+' has no role assigned!'}
return {'pass':False,'msg':'User '+u['username']+' has invalid role: '+u['role']} if u["role"] not in ["admin", "manager", "teacher", "student"]:
if u["role"] == "":
if u['password_temporal'].lower() not in ['yes','no']: return {
return {'pass':False,'msg':'User '+u['username']+' has invalid password_temporal value (yes/no): '+u['password_temporal']} "pass": False,
return {'pass':True,'msg':''} "msg": "User " + u["username"] + " has no role assigned!",
}
return {
"pass": False,
"msg": "User " + u["username"] + " has invalid role: " + u["role"],
}
if u["password_temporal"].lower() not in ["yes", "no"]:
return {
"pass": False,
"msg": "User "
+ u["username"]
+ " has invalid password_temporal value (yes/no): "
+ u["password_temporal"],
}
return {"pass": True, "msg": ""}

View File

@ -1,107 +1,136 @@
#!flask/bin/python #!flask/bin/python
# coding=utf-8 # coding=utf-8
from admin import app import json
import logging as log import logging as log
import os
import sys
import time
import traceback import traceback
import time,json
import sys,os
from flask import request from flask import request
from admin import app
from .decorators import is_internal from .decorators import is_internal
@app.route('/api/internal/users', methods=['GET'])
@app.route("/api/internal/users", methods=["GET"])
@is_internal @is_internal
def internal_users(): def internal_users():
if request.method == 'GET': if request.method == "GET":
sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k['username']) sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k["username"])
# group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']] # group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']]
users=[] users = []
for user in sorted_users: for user in sorted_users:
if not user['enabled']: continue if not user["enabled"]:
continue
users.append(user_parser(user)) users.append(user_parser(user))
return json.dumps(users), 200, {'Content-Type': 'application/json'} return json.dumps(users), 200, {"Content-Type": "application/json"}
@app.route('/api/internal/users/filter', methods=['POST'])
@app.route("/api/internal/users/filter", methods=["POST"])
@is_internal @is_internal
def internal_users_search(): def internal_users_search():
if request.method == 'POST': if request.method == "POST":
data=request.get_json(force=True) data = request.get_json(force=True)
users = app.admin.get_mix_users() users = app.admin.get_mix_users()
result = [user_parser(user) for user in filter_users(users, data['text'])] result = [user_parser(user) for user in filter_users(users, data["text"])]
sorted_result = sorted(result, key=lambda k: k['id']) sorted_result = sorted(result, key=lambda k: k["id"])
return json.dumps(sorted_result), 200, {'Content-Type': 'application/json'} return json.dumps(sorted_result), 200, {"Content-Type": "application/json"}
@app.route('/api/internal/groups', methods=['GET'])
@app.route("/api/internal/groups", methods=["GET"])
@is_internal @is_internal
def internal_groups(): def internal_groups():
if request.method == 'GET': if request.method == "GET":
sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k['name']) sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k["name"])
groups=[] groups = []
for group in sorted_groups: for group in sorted_groups:
if not group['path'].startswith('/'): continue if not group["path"].startswith("/"):
groups.append({'id':group['path'], continue
'name':group['name'], groups.append(
'description':group.get('description','')}) {
return json.dumps(groups), 200, {'Content-Type': 'application/json'} "id": group["path"],
"name": group["name"],
"description": group.get("description", ""),
}
)
return json.dumps(groups), 200, {"Content-Type": "application/json"}
@app.route('/api/internal/group/users', methods=['POST'])
@app.route("/api/internal/group/users", methods=["POST"])
@is_internal @is_internal
def internal_group_users(): def internal_group_users():
if request.method == 'POST': if request.method == "POST":
data=request.get_json(force=True) data = request.get_json(force=True)
sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k['username']) sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k["username"])
# group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']] # group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']]
users=[] users = []
for user in sorted_users: for user in sorted_users:
if data['path'] not in user['keycloak_groups'] or not user['enabled']: continue if data["path"] not in user["keycloak_groups"] or not user["enabled"]:
continue
users.append(user) users.append(user)
if data.get('text',False) and data['text'] != '': if data.get("text", False) and data["text"] != "":
result = [user_parser(user) for user in filter_users(users, data['text'])] result = [user_parser(user) for user in filter_users(users, data["text"])]
else: else:
result = [user_parser(user) for user in users] result = [user_parser(user) for user in users]
return json.dumps(result), 200, {'Content-Type': 'application/json'} return json.dumps(result), 200, {"Content-Type": "application/json"}
@app.route('/api/internal/roles', methods=['GET'])
@app.route("/api/internal/roles", methods=["GET"])
@is_internal @is_internal
def internal_roles(): def internal_roles():
if request.method == 'GET': if request.method == "GET":
roles=[] roles = []
for role in sorted(app.admin.get_roles(), key=lambda k: k['name']): for role in sorted(app.admin.get_roles(), key=lambda k: k["name"]):
if role['name'] == 'admin': continue if role["name"] == "admin":
roles.append({'id':role['id'], continue
'name':role['name'], roles.append(
'description':role.get('description','')}) {
return json.dumps(roles), 200, {'Content-Type': 'application/json'} "id": role["id"],
"name": role["name"],
"description": role.get("description", ""),
}
)
return json.dumps(roles), 200, {"Content-Type": "application/json"}
@app.route('/api/internal/role/users', methods=['POST'])
@app.route("/api/internal/role/users", methods=["POST"])
@is_internal @is_internal
def internal_role_users(): def internal_role_users():
if request.method == 'POST': if request.method == "POST":
data=request.get_json(force=True) data = request.get_json(force=True)
sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k['username']) sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k["username"])
# group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']] # group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']]
users=[] users = []
for user in sorted_users: for user in sorted_users:
if data['role'] not in user['roles'] or not user['enabled']: continue if data["role"] not in user["roles"] or not user["enabled"]:
continue
users.append(user) users.append(user)
if data.get('text',False) and data['text'] != '': if data.get("text", False) and data["text"] != "":
result = [user_parser(user) for user in filter_users(users, data['text'])] result = [user_parser(user) for user in filter_users(users, data["text"])]
else: else:
result = [user_parser(user) for user in users] result = [user_parser(user) for user in users]
return json.dumps(result), 200, {'Content-Type': 'application/json'} return json.dumps(result), 200, {"Content-Type": "application/json"}
def user_parser(user): def user_parser(user):
return {'id':user['username'], return {
'first':user['first'], "id": user["username"],
'last':user['last'], "first": user["first"],
'role':user['roles'][0] if len(user['roles']) else None, "last": user["last"],
'email':user['email'], "role": user["roles"][0] if len(user["roles"]) else None,
'groups':user['keycloak_groups']} "email": user["email"],
"groups": user["keycloak_groups"],
}
def filter_users(users, text): def filter_users(users, text):
return [user for user in users return [
if text in user['username'] or user
text in user['first'] or for user in users
text in user['last'] or if text in user["username"]
text in user['email']] or text in user["first"]
or text in user["last"]
or text in user["email"]
]

View File

@ -1,31 +1,42 @@
import os import os
from flask import flash, redirect, render_template, request, url_for
from flask_login import current_user, login_required, login_user, logout_user
from admin import app from admin import app
from flask import render_template, flash, request, redirect, url_for
from ..auth.authentication import *
from flask_login import login_required, current_user, login_user, logout_user
@app.route('/', methods=['GET', 'POST']) from ..auth.authentication import *
@app.route('/login', methods=['GET', 'POST'])
@app.route("/", methods=["GET", "POST"])
@app.route("/login", methods=["GET", "POST"])
def login(): def login():
if request.method == 'POST': if request.method == "POST":
if request.form['user'] == '' or request.form['password'] == '': if request.form["user"] == "" or request.form["password"] == "":
flash("Can't leave it blank",'danger') flash("Can't leave it blank", "danger")
elif request.form['user'].startswith(' '): elif request.form["user"].startswith(" "):
flash('Username not found or incorrect password.','warning') flash("Username not found or incorrect password.", "warning")
else: else:
ram_user=ram_users.get(request.form['user']) ram_user = ram_users.get(request.form["user"])
if ram_user and request.form['password'] == ram_user['password']: if ram_user and request.form["password"] == ram_user["password"]:
user=User({'id': ram_user['id'], 'password': ram_user['password'], 'role': ram_user['role'], 'active': True}) user = User(
login_user(user) {
flash('Logged in successfully.','success') "id": ram_user["id"],
return redirect(url_for('web_users')) "password": ram_user["password"],
else: "role": ram_user["role"],
flash('Username not found or incorrect password.','warning') "active": True,
return render_template('login.html') }
)
login_user(user)
flash("Logged in successfully.", "success")
return redirect(url_for("web_users"))
else:
flash("Username not found or incorrect password.", "warning")
return render_template("login.html")
@app.route('/logout', methods=['GET'])
@app.route("/logout", methods=["GET"])
@login_required @login_required
def logout(): def logout():
logout_user() logout_user()
return redirect(url_for('login')) return redirect(url_for("login"))

View File

@ -9,11 +9,11 @@
# @socketio.on('connect', namespace='//sio') # @socketio.on('connect', namespace='//sio')
# def socketio_connect(): # def socketio_connect():
# join_room('admin') # join_room('admin')
# socketio.emit('update', # socketio.emit('update',
# json.dumps('Joined'), # json.dumps('Joined'),
# namespace='//sio', # namespace='//sio',
# room='admin') # room='admin')
# @socketio.on('disconnect', namespace='//sio') # @socketio.on('disconnect', namespace='//sio')
# def socketio_domains_disconnect(): # def socketio_domains_disconnect():
# None # None

View File

@ -1,23 +1,34 @@
#!flask/bin/python #!flask/bin/python
# coding=utf-8 # coding=utf-8
from admin import app
import logging as log
import traceback
from uuid import uuid4
import time,json
import sys,os
from flask import render_template, Response, request, redirect, url_for, jsonify, send_file
import concurrent.futures import concurrent.futures
import json
import logging as log
import os
import sys
import time
import traceback
from pprint import pprint
from uuid import uuid4
from flask import (
Response,
jsonify,
redirect,
render_template,
request,
send_file,
url_for,
)
from flask_login import login_required from flask_login import login_required
from admin import app
from ..lib.avatars import Avatars
from .decorators import is_admin from .decorators import is_admin
from pprint import pprint avatars = Avatars()
from ..lib.avatars import Avatars
avatars=Avatars() """ OIDC TESTS """
''' OIDC TESTS '''
# from ..auth.authentication import oidc # from ..auth.authentication import oidc
# @app.route('/custom_callback') # @app.route('/custom_callback')
@ -43,47 +54,60 @@ avatars=Avatars()
# def logoutoidc(): # def logoutoidc():
# oidc.logout() # oidc.logout()
# return 'Hi, you have been logged out! <a href="/">Return</a>' # return 'Hi, you have been logged out! <a href="/">Return</a>'
''' OIDC TESTS ''' """ OIDC TESTS """
@app.route('/users')
@app.route("/users")
@login_required @login_required
def web_users(): def web_users():
return render_template('pages/users.html', title="Users", nav="Users") return render_template("pages/users.html", title="Users", nav="Users")
@app.route('/roles')
@app.route("/roles")
@login_required @login_required
def web_roles(): def web_roles():
return render_template('pages/roles.html', title="Roles", nav="Roles") return render_template("pages/roles.html", title="Roles", nav="Roles")
@app.route('/groups')
@app.route("/groups")
@login_required @login_required
def web_groups(provider=False): def web_groups(provider=False):
return render_template('pages/groups.html', title="Groups", nav="Groups") return render_template("pages/groups.html", title="Groups", nav="Groups")
@app.route('/avatar/<userid>', methods=['GET'])
@app.route("/avatar/<userid>", methods=["GET"])
@login_required @login_required
def avatar(userid): def avatar(userid):
if userid != 'false': if userid != "false":
return send_file('../avatars/master-avatars/'+userid, mimetype='image/jpeg') return send_file("../avatars/master-avatars/" + userid, mimetype="image/jpeg")
return send_file('static/img/missing.jpg', mimetype='image/jpeg') return send_file("static/img/missing.jpg", mimetype="image/jpeg")
### SYS ADMIN ### SYS ADMIN
@app.route('/sysadmin/users')
@app.route("/sysadmin/users")
@login_required @login_required
@is_admin @is_admin
def web_sysadmin_users(): def web_sysadmin_users():
return render_template('pages/sysadmin/users.html', title="SysAdmin Users", nav="SysAdminUsers") return render_template(
"pages/sysadmin/users.html", title="SysAdmin Users", nav="SysAdminUsers"
)
@app.route('/sysadmin/groups')
@app.route("/sysadmin/groups")
@login_required @login_required
@is_admin @is_admin
def web_sysadmin_groups(): def web_sysadmin_groups():
return render_template('pages/sysadmin/groups.html', title="SysAdmin Groups", nav="SysAdminGroups") return render_template(
"pages/sysadmin/groups.html", title="SysAdmin Groups", nav="SysAdminGroups"
)
@app.route('/sysadmin/external')
@app.route("/sysadmin/external")
@login_required @login_required
## SysAdmin role ## SysAdmin role
def web_sysadmin_external(): def web_sysadmin_external():
return render_template('pages/sysadmin/external.html', title="External", nav="External") return render_template(
"pages/sysadmin/external.html", title="External", nav="External"
)

View File

@ -1,25 +1,36 @@
#!flask/bin/python #!flask/bin/python
# coding=utf-8 # coding=utf-8
from functools import wraps
from flask import request, redirect, url_for
from flask_login import current_user, logout_user
import socket import socket
from functools import wraps
from flask import redirect, request, url_for
from flask_login import current_user, logout_user
def is_admin(fn): def is_admin(fn):
@wraps(fn) @wraps(fn)
def decorated_view(*args, **kwargs): def decorated_view(*args, **kwargs):
if current_user.role == 'admin': return fn(*args, **kwargs) if current_user.role == "admin":
return redirect(url_for('login')) return fn(*args, **kwargs)
return redirect(url_for("login"))
return decorated_view return decorated_view
def is_internal(fn): def is_internal(fn):
@wraps(fn) @wraps(fn)
def decorated_view(*args, **kwargs): def decorated_view(*args, **kwargs):
remote_addr=request.headers['X-Forwarded-For'].split(',')[0] if 'X-Forwarded-For' in request.headers else request.remote_addr.split(',')[0] remote_addr = (
request.headers["X-Forwarded-For"].split(",")[0]
if "X-Forwarded-For" in request.headers
else request.remote_addr.split(",")[0]
)
## Now only checks if it is wordpress container, ## Now only checks if it is wordpress container,
## but we should check if it is internal net and not haproxy ## but we should check if it is internal net and not haproxy
if socket.gethostbyname('isard-apps-wordpress') == remote_addr: return fn(*args, **kwargs) if socket.gethostbyname("isard-apps-wordpress") == remote_addr:
return fn(*args, **kwargs)
logout_user() logout_user()
return redirect(url_for('login')) return redirect(url_for("login"))
return decorated_view
return decorated_view

View File

@ -1,72 +1,89 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf-8 # coding=utf-8
import time, os import json
from datetime import datetime, timedelta
import pprint
import logging as log import logging as log
import os
import pprint
import random
import string
import time
import traceback import traceback
import yaml, json from datetime import datetime, timedelta
import psycopg2 import psycopg2
import yaml
from admin.lib.postgres import Postgres
from admin.lib.keycloak_client import KeycloakClient from admin.lib.keycloak_client import KeycloakClient
from admin.lib.postgres import Postgres
import string, random app = {}
app["config"] = {}
app={}
app['config']={}
class MoodleSaml(): class MoodleSaml:
def __init__(self): def __init__(self):
ready=False ready = False
while not ready: while not ready:
try: try:
self.pg=Postgres('isard-apps-postgresql','moodle',os.environ['MOODLE_POSTGRES_USER'],os.environ['MOODLE_POSTGRES_PASSWORD']) self.pg = Postgres(
ready=True "isard-apps-postgresql",
"moodle",
os.environ["MOODLE_POSTGRES_USER"],
os.environ["MOODLE_POSTGRES_PASSWORD"],
)
ready = True
except: except:
log.warning('Could not connect to moodle database. Retrying...') log.warning("Could not connect to moodle database. Retrying...")
time.sleep(2) time.sleep(2)
log.info('Connected to moodle database.') log.info("Connected to moodle database.")
ready=False ready = False
while not ready: while not ready:
try: try:
privatekey_pass=self.get_privatekey_pass() privatekey_pass = self.get_privatekey_pass()
log.warning("The key: "+str(privatekey_pass)) log.warning("The key: " + str(privatekey_pass))
if privatekey_pass.endswith(os.environ['DOMAIN']): if privatekey_pass.endswith(os.environ["DOMAIN"]):
app['config']['MOODLE_SAML_PRIVATEKEYPASS']=privatekey_pass app["config"]["MOODLE_SAML_PRIVATEKEYPASS"] = privatekey_pass
ready=True ready = True
except: except:
# print(traceback.format_exc()) # print(traceback.format_exc())
log.warning('Could not get moodle site identifier. Retrying...') log.warning("Could not get moodle site identifier. Retrying...")
time.sleep(2) time.sleep(2)
log.info('Got moodle site identifier.') log.info("Got moodle site identifier.")
ready=False ready = False
while not ready: while not ready:
try: try:
with open(os.path.join("./moodledata/saml2/moodle."+os.environ['DOMAIN']+".crt"),"r") as crt: with open(
app['config']['SP_CRT']=crt.read() os.path.join(
ready=True "./moodledata/saml2/moodle." + os.environ["DOMAIN"] + ".crt"
),
"r",
) as crt:
app["config"]["SP_CRT"] = crt.read()
ready = True
except IOError: except IOError:
log.warning('Could not get moodle SAML2 crt certificate. Retrying...') log.warning("Could not get moodle SAML2 crt certificate. Retrying...")
time.sleep(2) time.sleep(2)
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
log.info('Got moodle srt certificate.') log.info("Got moodle srt certificate.")
ready=False ready = False
while not ready: while not ready:
try: try:
with open(os.path.join("./moodledata/saml2/moodle."+os.environ['DOMAIN']+".pem"),"r") as pem: with open(
app['config']['SP_PEM']=pem.read() os.path.join(
ready=True "./moodledata/saml2/moodle." + os.environ["DOMAIN"] + ".pem"
),
"r",
) as pem:
app["config"]["SP_PEM"] = pem.read()
ready = True
except IOError: except IOError:
log.warning('Could not get moodle SAML2 pem certificate. Retrying...') log.warning("Could not get moodle SAML2 pem certificate. Retrying...")
time.sleep(2) time.sleep(2)
log.info('Got moodle pem certificate.') log.info("Got moodle pem certificate.")
## This seems related to the fact that the certificate generated the first time does'nt work. ## 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 ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
@ -77,183 +94,259 @@ class MoodleSaml():
## 3.- Cleanup all caches in moodle (Development tab) ## 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: # 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()) # xml.write(self.parse_idp_metadata())
with open(os.path.join("./moodledata/saml2/0f635d0e0f3874fff8b581c132e6c7a7.idp.xml"),"w") as xml: with open(
os.path.join("./moodledata/saml2/0f635d0e0f3874fff8b581c132e6c7a7.idp.xml"),
"w",
) as xml:
xml.write(self.parse_idp_metadata()) xml.write(self.parse_idp_metadata())
log.info('Written SP file on moodledata.') log.info("Written SP file on moodledata.")
try: try:
self.activate_saml_plugin() self.activate_saml_plugin()
except: except:
print('Error activating saml on moodle') print("Error activating saml on moodle")
try: try:
self.set_moodle_saml_plugin() self.set_moodle_saml_plugin()
except: except:
print('Error setting saml on moodle') print("Error setting saml on moodle")
try: try:
self.delete_keycloak_moodle_saml_plugin() self.delete_keycloak_moodle_saml_plugin()
except: except:
print('Error deleting saml on keycloak') print("Error deleting saml on keycloak")
try: try:
self.add_keycloak_moodle_saml() self.add_keycloak_moodle_saml()
except: except:
print('Error adding saml on keycloak') print("Error adding saml on keycloak")
# SAML clients don't work well with composite roles so disabling and adding on realm # SAML clients don't work well with composite roles so disabling and adding on realm
# self.add_client_roles() # 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'"""
)
def get_privatekey_pass(self): def get_privatekey_pass(self):
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=KeycloakClient() 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=KeycloakClient() 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=KeycloakClient() keycloak = KeycloakClient()
keycloak.delete_client('a92d5417-92b6-4678-9cb9-51bc0edcee8c') keycloak.delete_client("a92d5417-92b6-4678-9cb9-51bc0edcee8c")
keycloak=None keycloak = None
def set_moodle_saml_plugin(self): def set_moodle_saml_plugin(self):
config={'idpmetadata': self.parse_idp_metadata(), config = {
'certs_locked': '1', "idpmetadata": self.parse_idp_metadata(),
'duallogin': '1', "certs_locked": "1",
'idpattr': 'username', "duallogin": "1",
'autocreate': '1', "idpattr": "username",
'anyauth': '1', "autocreate": "1",
'saml_role_siteadmin_map': 'admin', "anyauth": "1",
'saml_role_coursecreator_map': 'teacher', "saml_role_siteadmin_map": "admin",
'saml_role_manager_map': 'manager', "saml_role_coursecreator_map": "teacher",
'field_map_email': 'email', "saml_role_manager_map": "manager",
'field_map_firstname': 'givenName', "field_map_email": "email",
'field_map_lastname': 'sn'} "field_map_firstname": "givenName",
"field_map_lastname": "sn",
}
for name in config.keys(): 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(
self.pg.update("""INSERT INTO "mdl_auth_saml2_idps" ("metadataurl", "entityid", "activeidp", "defaultidp", "adminidp", "defaultname", "displayname", "logo", "alias", "whitelist") VALUES """UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'auth_saml2' AND "name" = '%s'"""
('xml', 'https://sso.%s/auth/realms/master', 1, 0, 0, 'Login via SAML2', '', NULL, NULL, NULL);""" % (os.environ['DOMAIN'])) % (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): def add_keycloak_moodle_saml(self):
client={ client = {
"id" : "a92d5417-92b6-4678-9cb9-51bc0edcee8c", "id": "a92d5417-92b6-4678-9cb9-51bc0edcee8c",
"name": "moodle", "name": "moodle",
"description": "moodle", "description": "moodle",
"clientId" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/metadata.php", "clientId": "https://moodle."
"surrogateAuthRequired" : False, + os.environ["DOMAIN"]
"enabled" : True, + "/auth/saml2/sp/metadata.php",
"alwaysDisplayInConsole" : False, "surrogateAuthRequired": False,
"clientAuthenticatorType" : "client-secret", "enabled": True,
"redirectUris" : [ "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+os.environ['DOMAIN']+"" ], "alwaysDisplayInConsole": False,
"webOrigins" : [ "https://moodle."+os.environ['DOMAIN']+"" ], "clientAuthenticatorType": "client-secret",
"notBefore" : 0, "redirectUris": [
"bearerOnly" : False, "https://moodle."
"consentRequired" : False, + os.environ["DOMAIN"]
"standardFlowEnabled" : True, + "/auth/saml2/sp/saml2-acs.php/moodle."
"implicitFlowEnabled" : False, + os.environ["DOMAIN"]
"directAccessGrantsEnabled" : False, + ""
"serviceAccountsEnabled" : False, ],
"publicClient" : False, "webOrigins": ["https://moodle." + os.environ["DOMAIN"] + ""],
"frontchannelLogout" : True, "notBefore": 0,
"protocol" : "saml", "bearerOnly": False,
"attributes" : { "consentRequired": False,
"saml.force.post.binding" : True, "standardFlowEnabled": True,
"saml.encrypt" : False, "implicitFlowEnabled": False,
"saml_assertion_consumer_url_post" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+os.environ['DOMAIN']+"", "directAccessGrantsEnabled": False,
"saml.server.signature" : True, "serviceAccountsEnabled": False,
"saml.server.signature.keyinfo.ext" : False, "publicClient": False,
"saml.signing.certificate" : app['config']['SP_CRT'], "frontchannelLogout": True,
"saml_single_logout_service_url_redirect" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-logout.php/moodle."+os.environ['DOMAIN']+"", "protocol": "saml",
"saml.signature.algorithm" : "RSA_SHA256", "attributes": {
"saml_force_name_id_format" : False, "saml.force.post.binding": True,
"saml.client.signature" : True, "saml.encrypt": False,
"saml.encryption.certificate" : app['config']['SP_PEM'], "saml_assertion_consumer_url_post": "https://moodle."
"saml.authnstatement" : True, + os.environ["DOMAIN"]
"saml_name_id_format" : "username", + "/auth/saml2/sp/saml2-acs.php/moodle."
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#" + 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" : { }, "authenticationFlowBindingOverrides": {},
"fullScopeAllowed" : True, "fullScopeAllowed": True,
"nodeReRegistrationTimeout" : -1, "nodeReRegistrationTimeout": -1,
"protocolMappers" : [ { "protocolMappers": [
"id" : "9296daa3-4fc4-4b80-b007-5070f546ae13", {
"name" : "X500 sn", "id": "9296daa3-4fc4-4b80-b007-5070f546ae13",
"protocol" : "saml", "name": "X500 sn",
"protocolMapper" : "saml-user-property-mapper", "protocol": "saml",
"consentRequired" : False, "protocolMapper": "saml-user-property-mapper",
"config" : { "consentRequired": False,
"attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "config": {
"user.attribute" : "lastName", "attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"friendly.name" : "sn", "user.attribute": "lastName",
"attribute.name" : "urn:oid:2.5.4.4" "friendly.name": "sn",
} "attribute.name": "urn:oid:2.5.4.4",
}, { },
"id" : "ccecf6e4-d20a-4211-b67c-40200a6b2c5d", },
"name" : "username", {
"protocol" : "saml", "id": "ccecf6e4-d20a-4211-b67c-40200a6b2c5d",
"protocolMapper" : "saml-user-property-mapper", "name": "username",
"consentRequired" : False, "protocol": "saml",
"config" : { "protocolMapper": "saml-user-property-mapper",
"attribute.nameformat" : "Basic", "consentRequired": False,
"user.attribute" : "username", "config": {
"friendly.name" : "username", "attribute.nameformat": "Basic",
"attribute.name" : "username" "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", "id": "53858403-eba2-4f6d-81d0-cced700b5719",
"consentRequired" : False, "name": "X500 givenName",
"config" : { "protocol": "saml",
"attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "protocolMapper": "saml-user-property-mapper",
"user.attribute" : "firstName", "consentRequired": False,
"friendly.name" : "givenName", "config": {
"attribute.name" : "urn:oid:2.5.4.42" "attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
} "user.attribute": "firstName",
}, { "friendly.name": "givenName",
"id" : "20034db5-1d0e-4e66-b815-fb0440c6d1e2", "attribute.name": "urn:oid:2.5.4.42",
"name" : "X500 email", },
"protocol" : "saml", },
"protocolMapper" : "saml-user-property-mapper", {
"consentRequired" : False, "id": "20034db5-1d0e-4e66-b815-fb0440c6d1e2",
"config" : { "name": "X500 email",
"attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "protocol": "saml",
"user.attribute" : "email", "protocolMapper": "saml-user-property-mapper",
"friendly.name" : "email", "consentRequired": False,
"attribute.name" : "urn:oid:1.2.840.113549.1.9.1" "config": {
} "attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
} ], "user.attribute": "email",
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], "friendly.name": "email",
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ], "attribute.name": "urn:oid:1.2.840.113549.1.9.1",
"access" : { },
"view" : True, },
"configure" : True, ],
"manage" : True "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 = KeycloakClient()
keycloak.add_client(client) keycloak.add_client(client)
keycloak=None keycloak = None
def add_client_roles(self): def add_client_roles(self):
keycloak=KeycloakClient() keycloak = KeycloakClient()
keycloak.add_client_role('a92d5417-92b6-4678-9cb9-51bc0edcee8c','admin','Moodle admins') keycloak.add_client_role(
keycloak.add_client_role('a92d5417-92b6-4678-9cb9-51bc0edcee8c','manager','Moodle managers') "a92d5417-92b6-4678-9cb9-51bc0edcee8c", "admin", "Moodle admins"
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.add_client_role(
keycloak=None "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

@ -1,66 +1,81 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf-8 # coding=utf-8
import time, os import json
from datetime import datetime, timedelta
import pprint
import logging as log import logging as log
import os
import pprint
import random
import string
import time
import traceback import traceback
import yaml, json from datetime import datetime, timedelta
import psycopg2 import psycopg2
import yaml
from admin.lib.postgres import Postgres
from admin.lib.keycloak_client import KeycloakClient from admin.lib.keycloak_client import KeycloakClient
from admin.lib.postgres import Postgres
import string, random app = {}
app["config"] = {}
app={}
app['config']={}
class NextcloudSaml(): class NextcloudSaml:
def __init__(self): def __init__(self):
self.url="http://isard-sso-keycloak:8080/auth/" self.url = "http://isard-sso-keycloak:8080/auth/"
self.username=os.environ['KEYCLOAK_USER'] self.username = os.environ["KEYCLOAK_USER"]
self.password=os.environ['KEYCLOAK_PASSWORD'] self.password = os.environ["KEYCLOAK_PASSWORD"]
self.realm='master' self.realm = "master"
self.verify=True self.verify = True
ready=False ready = False
while not ready: while not ready:
try: try:
self.pg=Postgres('isard-apps-postgresql','nextcloud',os.environ['NEXTCLOUD_POSTGRES_USER'],os.environ['NEXTCLOUD_POSTGRES_PASSWORD']) self.pg = Postgres(
ready=True "isard-apps-postgresql",
"nextcloud",
os.environ["NEXTCLOUD_POSTGRES_USER"],
os.environ["NEXTCLOUD_POSTGRES_PASSWORD"],
)
ready = True
except: except:
log.warning('Could not connect to nextcloud database. Retrying...') log.warning("Could not connect to nextcloud database. Retrying...")
time.sleep(2) time.sleep(2)
log.info('Connected to nextcloud database.') log.info("Connected to nextcloud database.")
ready=False ready = False
while not ready: while not ready:
try: try:
with open(os.path.join("./saml_certs/public.cert"),"r") as crt: with open(os.path.join("./saml_certs/public.cert"), "r") as crt:
app['config']['PUBLIC_CERT']=crt.read() app["config"]["PUBLIC_CERT"] = crt.read()
ready=True ready = True
except IOError: except IOError:
log.warning('Could not get public certificate to be used in nextcloud. Retrying...') log.warning(
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert') "Could not get public certificate to be used in nextcloud. Retrying..."
)
log.warning(
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
)
time.sleep(2) time.sleep(2)
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
log.info('Got moodle srt certificate to be used in nextcloud.') log.info("Got moodle srt certificate to be used in nextcloud.")
ready=False ready = False
while not ready: while not ready:
try: try:
with open(os.path.join("./saml_certs/private.key"),"r") as pem: with open(os.path.join("./saml_certs/private.key"), "r") as pem:
app['config']['PRIVATE_KEY']=pem.read() app["config"]["PRIVATE_KEY"] = pem.read()
ready=True ready = True
except IOError: except IOError:
log.warning('Could not get private key to be used in nextcloud. Retrying...') log.warning(
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert') "Could not get private key to be used in nextcloud. Retrying..."
)
log.warning(
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
)
time.sleep(2) time.sleep(2)
log.info('Got moodle pem certificate to be used in nextcloud.') log.info("Got moodle pem certificate to be used in nextcloud.")
# ## This seems related to the fact that the certificate generated the first time does'nt work. # ## 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 # ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
@ -77,30 +92,32 @@ class NextcloudSaml():
try: try:
self.reset_saml() self.reset_saml()
except: except:
print('Error resetting saml on nextcloud') print("Error resetting saml on nextcloud")
try: try:
self.delete_keycloak_nextcloud_saml_plugin() self.delete_keycloak_nextcloud_saml_plugin()
except: except:
print('Error resetting saml on keycloak') print("Error resetting saml on keycloak")
try: try:
self.set_nextcloud_saml_plugin() self.set_nextcloud_saml_plugin()
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
print('Error adding saml on nextcloud') print("Error adding saml on nextcloud")
try: try:
self.add_keycloak_nextcloud_saml() self.add_keycloak_nextcloud_saml()
except: except:
print('Error adding saml on keycloak') print("Error adding saml on keycloak")
def connect(self): def connect(self):
self.keycloak= KeycloakClient(url=self.url, self.keycloak = KeycloakClient(
username=self.username, url=self.url,
password=self.password, username=self.username,
realm=self.realm, password=self.password,
verify=self.verify) 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
@ -111,22 +128,23 @@ class NextcloudSaml():
def parse_idp_cert(self): def parse_idp_cert(self):
self.connect() self.connect()
rsa=self.keycloak.get_server_rsa_key() rsa = self.keycloak.get_server_rsa_key()
self.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):
self.connect() self.connect()
self.keycloak.add_nextcloud_client() self.keycloak.add_nextcloud_client()
self.keycloak=None self.keycloak = None
def delete_keycloak_nextcloud_saml_plugin(self): def delete_keycloak_nextcloud_saml_plugin(self):
self.connect() self.connect()
self.keycloak.delete_client('bef873f0-2079-4876-8657-067de27d01b7') self.keycloak.delete_client("bef873f0-2079-4876-8657-067de27d01b7")
self.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
('user_saml', 'general-uid_mapping', 'username'), ('user_saml', 'general-uid_mapping', 'username'),
('user_saml', 'type', 'saml'), ('user_saml', 'type', 'saml'),
('user_saml', 'sp-privateKey', '%s'), ('user_saml', 'sp-privateKey', '%s'),
@ -144,155 +162,192 @@ class NextcloudSaml():
('user_saml', 'security-wantAssertionsSigned', '1'), ('user_saml', 'security-wantAssertionsSigned', '1'),
('user_saml', 'general-idp0_display_name', 'SAML Login'), ('user_saml', 'general-idp0_display_name', 'SAML Login'),
('user_saml', 'sp-x509cert', '%s'), ('user_saml', 'sp-x509cert', '%s'),
('user_saml', 'idp-singleLogoutService.url', 'https://sso.%s/auth/realms/master/protocol/saml');""" % (app['config']['PRIVATE_KEY'],os.environ['DOMAIN'],os.environ['DOMAIN'],self.parse_idp_cert(),app['config']['PUBLIC_CERT'],os.environ['DOMAIN'])) ('user_saml', 'idp-singleLogoutService.url', 'https://sso.%s/auth/realms/master/protocol/saml');"""
% (
app["config"]["PRIVATE_KEY"],
os.environ["DOMAIN"],
os.environ["DOMAIN"],
self.parse_idp_cert(),
app["config"]["PUBLIC_CERT"],
os.environ["DOMAIN"],
)
)
def reset_saml(self): def reset_saml(self):
cfg_list=['general-uid_mapping', cfg_list = [
'sp-privateKey', "general-uid_mapping",
'saml-attribute-mapping-email_mapping', "sp-privateKey",
'saml-attribute-mapping-quota_mapping', "saml-attribute-mapping-email_mapping",
'saml-attribute-mapping-displayName_mapping', "saml-attribute-mapping-quota_mapping",
'saml-attribute-mapping-group_mapping', "saml-attribute-mapping-displayName_mapping",
'idp-entityId', "saml-attribute-mapping-group_mapping",
'idp-singleSignOnService.url', "idp-entityId",
'idp-x509cert', "idp-singleSignOnService.url",
'security-authnRequestsSigned', "idp-x509cert",
'security-logoutRequestSigned', "security-authnRequestsSigned",
'security-logoutResponseSigned', "security-logoutRequestSigned",
'security-wantMessagesSigned', "security-logoutResponseSigned",
'security-wantAssertionsSigned', "security-wantMessagesSigned",
'general-idp0_display_name', "security-wantAssertionsSigned",
'type', "general-idp0_display_name",
'sp-x509cert', "type",
'idp-singleLogoutService.url'] "sp-x509cert",
"idp-singleLogoutService.url",
]
for cfg in cfg_list: for cfg in cfg_list:
self.pg.update("""DELETE FROM "oc_appconfig" WHERE appid = 'user_saml' AND configkey = '%s'""" % (cfg)) self.pg.update(
"""DELETE FROM "oc_appconfig" WHERE appid = 'user_saml' AND configkey = '%s'"""
% (cfg)
)
def add_keycloak_nextcloud_saml(self): def add_keycloak_nextcloud_saml(self):
client={"id" : "bef873f0-2079-4876-8657-067de27d01b7", client = {
"id": "bef873f0-2079-4876-8657-067de27d01b7",
"name": "nextcloud", "name": "nextcloud",
"description": "nextcloud", "description": "nextcloud",
"clientId" : "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/metadata", "clientId": "https://nextcloud."
"surrogateAuthRequired" : False, + os.environ["DOMAIN"]
"enabled" : True, + "/apps/user_saml/saml/metadata",
"alwaysDisplayInConsole" : False, "surrogateAuthRequired": False,
"clientAuthenticatorType" : "client-secret", "enabled": True,
"redirectUris" : [ "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/acs" ], "alwaysDisplayInConsole": False,
"webOrigins" : [ "https://nextcloud."+os.environ['DOMAIN'] ], "clientAuthenticatorType": "client-secret",
"notBefore" : 0, "redirectUris": [
"bearerOnly" : False, "https://nextcloud." + os.environ["DOMAIN"] + "/apps/user_saml/saml/acs"
"consentRequired" : False, ],
"standardFlowEnabled" : True, "webOrigins": ["https://nextcloud." + os.environ["DOMAIN"]],
"implicitFlowEnabled" : False, "notBefore": 0,
"directAccessGrantsEnabled" : False, "bearerOnly": False,
"serviceAccountsEnabled" : False, "consentRequired": False,
"publicClient" : False, "standardFlowEnabled": True,
"frontchannelLogout" : True, "implicitFlowEnabled": False,
"protocol" : "saml", "directAccessGrantsEnabled": False,
"attributes" : { "serviceAccountsEnabled": False,
"saml.assertion.signature" : True, "publicClient": False,
"saml.force.post.binding" : True, "frontchannelLogout": True,
"saml_assertion_consumer_url_post" : "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/acs", "protocol": "saml",
"saml.server.signature" : True, "attributes": {
"saml.server.signature.keyinfo.ext" : False, "saml.assertion.signature": True,
"saml.signing.certificate" : app['config']['PUBLIC_CERT'], "saml.force.post.binding": True,
"saml_single_logout_service_url_redirect" : "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/sls", "saml_assertion_consumer_url_post": "https://nextcloud."
"saml.signature.algorithm" : "RSA_SHA256", + os.environ["DOMAIN"]
"saml_force_name_id_format" : False, + "/apps/user_saml/saml/acs",
"saml.client.signature" : False, "saml.server.signature": True,
"saml.authnstatement" : True, "saml.server.signature.keyinfo.ext": False,
"saml_name_id_format" : "username", "saml.signing.certificate": app["config"]["PUBLIC_CERT"],
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#" "saml_single_logout_service_url_redirect": "https://nextcloud."
+ os.environ["DOMAIN"]
+ "/apps/user_saml/saml/sls",
"saml.signature.algorithm": "RSA_SHA256",
"saml_force_name_id_format": False,
"saml.client.signature": False,
"saml.authnstatement": True,
"saml_name_id_format": "username",
"saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#",
}, },
"authenticationFlowBindingOverrides" : { }, "authenticationFlowBindingOverrides": {},
"fullScopeAllowed" : True, "fullScopeAllowed": True,
"nodeReRegistrationTimeout" : -1, "nodeReRegistrationTimeout": -1,
"protocolMappers" : [ { "protocolMappers": [
"id" : "e8e4acff-da2b-46aa-8bdb-ba42171671d6", {
"name" : "username", "id": "e8e4acff-da2b-46aa-8bdb-ba42171671d6",
"protocol" : "saml", "name": "username",
"protocolMapper" : "saml-user-attribute-mapper", "protocol": "saml",
"consentRequired" : False, "protocolMapper": "saml-user-attribute-mapper",
"config" : { "consentRequired": False,
"attribute.nameformat" : "Basic", "config": {
"user.attribute" : "username", "attribute.nameformat": "Basic",
"friendly.name" : "username", "user.attribute": "username",
"attribute.name" : "username" "friendly.name": "username",
} "attribute.name": "username",
}, { },
"id" : "8ab13cd7-822a-40d5-a1e1-9f556aed2332", },
"name" : "quota", {
"protocol" : "saml", "id": "8ab13cd7-822a-40d5-a1e1-9f556aed2332",
"protocolMapper" : "saml-user-attribute-mapper", "name": "quota",
"consentRequired" : False, "protocol": "saml",
"config" : { "protocolMapper": "saml-user-attribute-mapper",
"attribute.nameformat" : "Basic", "consentRequired": False,
"user.attribute" : "quota", "config": {
"friendly.name" : "quota", "attribute.nameformat": "Basic",
"attribute.name" : "quota" "user.attribute": "quota",
} "friendly.name": "quota",
}, { "attribute.name": "quota",
"id" : "28206b59-757b-4e3c-81cb-0b6053b1fd3d", },
"name" : "email", },
"protocol" : "saml", {
"protocolMapper" : "saml-user-property-mapper", "id": "28206b59-757b-4e3c-81cb-0b6053b1fd3d",
"consentRequired" : False, "name": "email",
"config" : { "protocol": "saml",
"attribute.nameformat" : "Basic", "protocolMapper": "saml-user-property-mapper",
"user.attribute" : "email", "consentRequired": False,
"friendly.name" : "email", "config": {
"attribute.name" : "email" "attribute.nameformat": "Basic",
} "user.attribute": "email",
}, { "friendly.name": "email",
"id" : "5176a593-180f-4924-b294-b83a0d8d5972", "attribute.name": "email",
"name" : "displayname", },
"protocol" : "saml", },
"protocolMapper" : "saml-javascript-mapper", {
"consentRequired" : False, "id": "5176a593-180f-4924-b294-b83a0d8d5972",
"config" : { "name": "displayname",
"single" : False, "protocol": "saml",
"Script" : "/**\n * Available variables: \n * user - the current user\n * realm - the current realm\n * clientSession - the current clientSession\n * userSession - the current userSession\n * keycloakSession - the current keycloakSession\n */\n\n\n//insert your code here...\nvar Output = user.getFirstName()+\" \"+user.getLastName();\nOutput;\n", "protocolMapper": "saml-javascript-mapper",
"attribute.nameformat" : "Basic", "consentRequired": False,
"friendly.name" : "displayname", "config": {
"attribute.name" : "displayname" "single": False,
} "Script": '/**\n * Available variables: \n * user - the current user\n * realm - the current realm\n * clientSession - the current clientSession\n * userSession - the current userSession\n * keycloakSession - the current keycloakSession\n */\n\n\n//insert your code here...\nvar Output = user.getFirstName()+" "+user.getLastName();\nOutput;\n',
}, { "attribute.nameformat": "Basic",
"id" : "e51e04b9-f71a-42de-819e-dd9285246ada", "friendly.name": "displayname",
"name" : "Roles", "attribute.name": "displayname",
"protocol" : "saml", },
"protocolMapper" : "saml-role-list-mapper", },
"consentRequired" : False, {
"config" : { "id": "e51e04b9-f71a-42de-819e-dd9285246ada",
"single" : True, "name": "Roles",
"attribute.nameformat" : "Basic", "protocol": "saml",
"friendly.name" : "Roles", "protocolMapper": "saml-role-list-mapper",
"attribute.name" : "Roles" "consentRequired": False,
} "config": {
}, { "single": True,
"id" : "9c101249-bb09-4cc8-8f75-5a18fcb307e6", "attribute.nameformat": "Basic",
"name" : "group_list", "friendly.name": "Roles",
"protocol" : "saml", "attribute.name": "Roles",
"protocolMapper" : "saml-group-membership-mapper", },
"consentRequired" : False, },
"config" : { {
"single" : True, "id": "9c101249-bb09-4cc8-8f75-5a18fcb307e6",
"attribute.nameformat" : "Basic", "name": "group_list",
"full.path" : False, "protocol": "saml",
"friendly.name" : "member", "protocolMapper": "saml-group-membership-mapper",
"attribute.name" : "member" "consentRequired": False,
} "config": {
} ], "single": True,
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], "attribute.nameformat": "Basic",
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ], "full.path": False,
"access" : { "friendly.name": "member",
"view" : True, "attribute.name": "member",
"configure" : True, },
"manage" : True },
} ],
"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.connect()
self.keycloak.add_client(client) self.keycloak.add_client(client)
self.keycloak=None self.keycloak = None
n=NextcloudSaml()
n = NextcloudSaml()

View File

@ -1,66 +1,81 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf-8 # coding=utf-8
import time, os import json
from datetime import datetime, timedelta
import pprint
import logging as log import logging as log
import os
import pprint
import random
import string
import time
import traceback import traceback
import yaml, json from datetime import datetime, timedelta
import psycopg2 import psycopg2
import yaml
from admin.lib.postgres import Postgres
from admin.lib.keycloak_client import KeycloakClient from admin.lib.keycloak_client import KeycloakClient
from admin.lib.postgres import Postgres
import string, random app = {}
app["config"] = {}
app={}
app['config']={}
class NextcloudSaml(): class NextcloudSaml:
def __init__(self): def __init__(self):
self.url="http://isard-sso-keycloak:8080/auth/" self.url = "http://isard-sso-keycloak:8080/auth/"
self.username=os.environ['KEYCLOAK_USER'] self.username = os.environ["KEYCLOAK_USER"]
self.password=os.environ['KEYCLOAK_PASSWORD'] self.password = os.environ["KEYCLOAK_PASSWORD"]
self.realm='master' self.realm = "master"
self.verify=True self.verify = True
ready=False ready = False
while not ready: while not ready:
try: try:
self.pg=Postgres('isard-apps-postgresql','nextcloud',os.environ['NEXTCLOUD_POSTGRES_USER'],os.environ['NEXTCLOUD_POSTGRES_PASSWORD']) self.pg = Postgres(
ready=True "isard-apps-postgresql",
"nextcloud",
os.environ["NEXTCLOUD_POSTGRES_USER"],
os.environ["NEXTCLOUD_POSTGRES_PASSWORD"],
)
ready = True
except: except:
log.warning('Could not connect to nextcloud database. Retrying...') log.warning("Could not connect to nextcloud database. Retrying...")
time.sleep(2) time.sleep(2)
log.info('Connected to nextcloud database.') log.info("Connected to nextcloud database.")
ready=False ready = False
while not ready: while not ready:
try: try:
with open(os.path.join("./saml_certs/public.cert"),"r") as crt: with open(os.path.join("./saml_certs/public.cert"), "r") as crt:
app['config']['PUBLIC_CERT']=crt.read() app["config"]["PUBLIC_CERT"] = crt.read()
ready=True ready = True
except IOError: except IOError:
log.warning('Could not get public certificate to be used in nextcloud. Retrying...') log.warning(
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert') "Could not get public certificate to be used in nextcloud. Retrying..."
)
log.warning(
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
)
time.sleep(2) time.sleep(2)
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
log.info('Got moodle srt certificate to be used in nextcloud.') log.info("Got moodle srt certificate to be used in nextcloud.")
ready=False ready = False
while not ready: while not ready:
try: try:
with open(os.path.join("./saml_certs/private.key"),"r") as pem: with open(os.path.join("./saml_certs/private.key"), "r") as pem:
app['config']['PRIVATE_KEY']=pem.read() app["config"]["PRIVATE_KEY"] = pem.read()
ready=True ready = True
except IOError: except IOError:
log.warning('Could not get private key to be used in nextcloud. Retrying...') log.warning(
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert') "Could not get private key to be used in nextcloud. Retrying..."
)
log.warning(
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
)
time.sleep(2) time.sleep(2)
log.info('Got moodle pem certificate to be used in nextcloud.') log.info("Got moodle pem certificate to be used in nextcloud.")
# ## This seems related to the fact that the certificate generated the first time does'nt work. # ## 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 # ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
@ -77,20 +92,22 @@ class NextcloudSaml():
try: try:
self.reset_saml_certs() self.reset_saml_certs()
except: except:
print('Error resetting saml on nextcloud') print("Error resetting saml on nextcloud")
try: try:
self.set_nextcloud_saml_plugin_certs() self.set_nextcloud_saml_plugin_certs()
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
print('Error adding saml on nextcloud') print("Error adding saml on nextcloud")
def connect(self): def connect(self):
self.keycloak= KeycloakClient(url=self.url, self.keycloak = KeycloakClient(
username=self.username, url=self.url,
password=self.password, username=self.username,
realm=self.realm, password=self.password,
verify=self.verify) 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
@ -101,22 +118,30 @@ class NextcloudSaml():
def parse_idp_cert(self): def parse_idp_cert(self):
self.connect() self.connect()
rsa=self.keycloak.get_server_rsa_key() rsa = self.keycloak.get_server_rsa_key()
self.keycloak=None self.keycloak = None
return rsa['certificate'] return rsa["certificate"]
def set_nextcloud_saml_plugin_certs(self): def set_nextcloud_saml_plugin_certs(self):
self.pg.update("""INSERT INTO "oc_appconfig" ("appid", "configkey", "configvalue") VALUES self.pg.update(
"""INSERT INTO "oc_appconfig" ("appid", "configkey", "configvalue") VALUES
('user_saml', 'sp-privateKey', '%s'), ('user_saml', 'sp-privateKey', '%s'),
('user_saml', 'idp-x509cert', '%s'), ('user_saml', 'idp-x509cert', '%s'),
('user_saml', 'sp-x509cert', '%s');""" % (app['config']['PRIVATE_KEY'],self.parse_idp_cert(),app['config']['PUBLIC_CERT'])) ('user_saml', 'sp-x509cert', '%s');"""
% (
app["config"]["PRIVATE_KEY"],
self.parse_idp_cert(),
app["config"]["PUBLIC_CERT"],
)
)
def reset_saml_certs(self): def reset_saml_certs(self):
cfg_list=['sp-privateKey', cfg_list = ["sp-privateKey", "idp-x509cert", "sp-x509cert"]
'idp-x509cert',
'sp-x509cert']
for cfg in cfg_list: for cfg in cfg_list:
self.pg.update("""DELETE FROM "oc_appconfig" WHERE appid = 'user_saml' AND configkey = '%s'""" % (cfg)) self.pg.update(
"""DELETE FROM "oc_appconfig" WHERE appid = 'user_saml' AND configkey = '%s'"""
% (cfg)
)
n=NextcloudSaml()
n = NextcloudSaml()

View File

@ -1,50 +1,47 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf-8 # coding=utf-8
import time import json
from datetime import datetime, timedelta
import pprint
import logging as log import logging as log
import pprint
import time
import traceback import traceback
import yaml, json from datetime import datetime, timedelta
import psycopg2 import psycopg2
import yaml
class Postgres():
def __init__(self,host,database,user,password): class Postgres:
def __init__(self, host, database, user, password):
self.conn = psycopg2.connect( self.conn = psycopg2.connect(
host=host, host=host, database=database, user=user, password=password
database=database, )
user=user,
password=password)
# def __del__(self): # def __del__(self):
# self.cur.close() # self.cur.close()
# self.conn.close() # self.conn.close()
def select(self,sql): def select(self, sql):
self.cur = self.conn.cursor() self.cur = self.conn.cursor()
self.cur.execute(sql) self.cur.execute(sql)
data=self.cur.fetchall() data = self.cur.fetchall()
self.cur.close() self.cur.close()
return data return data
def update(self,sql): def update(self, sql):
self.cur = self.conn.cursor() self.cur = self.conn.cursor()
self.cur.execute(sql) self.cur.execute(sql)
self.conn.commit() self.conn.commit()
self.cur.close() self.cur.close()
# return self.cur.fetchall() # return self.cur.fetchall()
def select_with_headers(self,sql): def select_with_headers(self, sql):
self.cur = self.conn.cursor() self.cur = self.conn.cursor()
self.cur.execute(sql) self.cur.execute(sql)
data=self.cur.fetchall() data = self.cur.fetchall()
fields = [a.name for a in self.cur.description] fields = [a.name for a in self.cur.description]
self.cur.close() self.cur.close()
return (fields,data) return (fields, data)
# def update_moodle_saml_plugin(self): # def update_moodle_saml_plugin(self):
# plugin[('idpmetadata', '<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.'+app.config['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>NrtA5ynG0htowP3SXw7dBJRIAMxn-1PwuuXwOwNhlRw</ds:KeyName><ds:X509Data><ds:X509Certificate>MIICmzCCAYMCBgF5jb0RCTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwNTIxMDcwMjI4WhcNMzEwNTIxMDcwNDA4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCI8xh/C0+frz3kgWiUbziTDls71R2YiXLSVE+bw7gbEgZUGCLhoEI679azMtIxmnzM/snIX+yTb12+XoYkgbiLTMPQfnH+Kiab6g3HL3KPfhqS+yWkFxOoCp6Ibmp7yPlVWuHH+MBfO8OBr/r8Ao7heFbuzjiLd1KG67rcoaxfDgMuBoEomg1bgEjFgHaQIrSC6OZzH0h987/arqufZXeXlfyiqScMPUi+u5IpDWSwz06UKP0k8mxzNSlpZ93CKOUSsV0SMLxqg7FQ3SGiOk577bGW9o9BDTkkmSo3Up6smc0LzwvvUwuNd0B1irGkWZFQN9OXJnJYf1InEebIMtmPAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADM34+qEGeBQ22luphVTuVJtGxcbxLx7DfsT0QfJD/OuxTTbNAa1VRyarb5juIAkqdj4y2quZna9ZXLecVo4RkwpzPoKoAkYA8b+kHnWqEwJi9iPrDvKb+GR0bBkLPN49YxIZ8IdKX/PRa3yuLHe+loiNsCaS/2ZK2KO46COsqU4QX1iVhF9kWphNLybjNAX45B6cJLsa1g0vXLdm3kv3SB4I2fErFVaOoDtFIjttoYlXdpUiThkPXBfr7N67P3dZHaS4tjJh+IZ8I6TINpcsH8dBkUhzYEIPHCePwSiC1w6WDBLNDuKt1mj1CZrLq+1x+Yhrs+QNRheEKGi89HZ8N0=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml/resolve" index="0"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.mydomain.duckdns.org/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.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/></md:IDPSSODescriptor></md:EntityDescriptor></md:EntitiesDescriptor>')] # plugin[('idpmetadata', '<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.'+app.config['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>NrtA5ynG0htowP3SXw7dBJRIAMxn-1PwuuXwOwNhlRw</ds:KeyName><ds:X509Data><ds:X509Certificate>MIICmzCCAYMCBgF5jb0RCTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwNTIxMDcwMjI4WhcNMzEwNTIxMDcwNDA4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCI8xh/C0+frz3kgWiUbziTDls71R2YiXLSVE+bw7gbEgZUGCLhoEI679azMtIxmnzM/snIX+yTb12+XoYkgbiLTMPQfnH+Kiab6g3HL3KPfhqS+yWkFxOoCp6Ibmp7yPlVWuHH+MBfO8OBr/r8Ao7heFbuzjiLd1KG67rcoaxfDgMuBoEomg1bgEjFgHaQIrSC6OZzH0h987/arqufZXeXlfyiqScMPUi+u5IpDWSwz06UKP0k8mxzNSlpZ93CKOUSsV0SMLxqg7FQ3SGiOk577bGW9o9BDTkkmSo3Up6smc0LzwvvUwuNd0B1irGkWZFQN9OXJnJYf1InEebIMtmPAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADM34+qEGeBQ22luphVTuVJtGxcbxLx7DfsT0QfJD/OuxTTbNAa1VRyarb5juIAkqdj4y2quZna9ZXLecVo4RkwpzPoKoAkYA8b+kHnWqEwJi9iPrDvKb+GR0bBkLPN49YxIZ8IdKX/PRa3yuLHe+loiNsCaS/2ZK2KO46COsqU4QX1iVhF9kWphNLybjNAX45B6cJLsa1g0vXLdm3kv3SB4I2fErFVaOoDtFIjttoYlXdpUiThkPXBfr7N67P3dZHaS4tjJh+IZ8I6TINpcsH8dBkUhzYEIPHCePwSiC1w6WDBLNDuKt1mj1CZrLq+1x+Yhrs+QNRheEKGi89HZ8N0=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml/resolve" index="0"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.mydomain.duckdns.org/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.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/></md:IDPSSODescriptor></md:EntityDescriptor></md:EntitiesDescriptor>')]
@ -52,4 +49,4 @@ class Postgres():
# cursor.execute(pg_update, (title, bookid)) # cursor.execute(pg_update, (title, bookid))
# connection.commit() # connection.commit()
# count = cursor.rowcount # count = cursor.rowcount
# print(count, "Successfully Updated!") # print(count, "Successfully Updated!")

View File

@ -1,64 +1,82 @@
#!/usr/bin/env python #!/usr/bin/env python
import time ,os import json
from datetime import datetime, timedelta
import logging as log import logging as log
import os
import time
import traceback import traceback
import yaml, json from datetime import datetime, timedelta
from pprint import pprint from pprint import pprint
import yaml
from keycloak import KeycloakAdmin from keycloak import KeycloakAdmin
from postgres import Postgres
from minio import Minio from minio import Minio
from minio.commonconfig import REPLACE, CopySource from minio.commonconfig import REPLACE, CopySource
from minio.deleteobjects import DeleteObject from minio.deleteobjects import DeleteObject
class DefaultAvatars(): from postgres import Postgres
def __init__(self,
url="http://isard-sso-keycloak:8080/auth/",
username=os.environ['KEYCLOAK_USER'],
password=os.environ['KEYCLOAK_PASSWORD'],
realm='master',
verify=True):
self.url=url
self.username=username
self.password=password
self.realm=realm
self.verify=verify
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD'])
class DefaultAvatars:
def __init__(
self,
url="http://isard-sso-keycloak:8080/auth/",
username=os.environ["KEYCLOAK_USER"],
password=os.environ["KEYCLOAK_PASSWORD"],
realm="master",
verify=True,
):
self.url = url
self.username = username
self.password = password
self.realm = realm
self.verify = verify
self.keycloak_pg = Postgres(
"isard-apps-postgresql",
"keycloak",
os.environ["KEYCLOAK_DB_USER"],
os.environ["KEYCLOAK_DB_PASSWORD"],
)
self.mclient = Minio( self.mclient = Minio(
"isard-sso-avatars:9000", "isard-sso-avatars:9000",
access_key="AKIAIOSFODNN7EXAMPLE", access_key="AKIAIOSFODNN7EXAMPLE",
secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
secure=False secure=False,
) )
self.bucket='master-avatars' self.bucket = "master-avatars"
self._minio_set_realm() self._minio_set_realm()
self.update_missing_avatars() self.update_missing_avatars()
def connect(self): def connect(self):
self.keycloak_admin = KeycloakAdmin(server_url=self.url, self.keycloak_admin = KeycloakAdmin(
username=self.username, server_url=self.url,
password=self.password, username=self.username,
realm_name=self.realm, password=self.password,
verify=self.verify) realm_name=self.realm,
verify=self.verify,
)
def update_missing_avatars(self): def update_missing_avatars(self):
sys_roles=['admin','manager','teacher','student'] sys_roles = ["admin", "manager", "teacher", "student"]
for u in self.get_users_without_image(): for u in self.get_users_without_image():
try: try:
img=[r+'.jpg' for r in sys_roles if r in u['role']][0] img = [r + ".jpg" for r in sys_roles if r in u["role"]][0]
except: except:
img='unknown.jpg' img = "unknown.jpg"
self.mclient.fput_object( self.mclient.fput_object(
self.bucket, u['id'], "custom/avatars/"+img, self.bucket,
content_type="image/jpeg ", u["id"],
"custom/avatars/" + img,
content_type="image/jpeg ",
)
log.warning(
" AVATARS: Updated avatar for user "
+ u["username"]
+ " with role "
+ img.split(".")[0]
) )
log.warning(' AVATARS: Updated avatar for user '+u['username']+' with role '+img.split('.')[0])
def _minio_set_realm(self): def _minio_set_realm(self):
if not self.mclient.bucket_exists(self.bucket): if not self.mclient.bucket_exists(self.bucket):
@ -72,17 +90,17 @@ class DefaultAvatars():
lambda x: DeleteObject(x.object_name), lambda x: DeleteObject(x.object_name),
self.mclient.list_objects(self.bucket), self.mclient.list_objects(self.bucket),
) )
errors=self.mclient.remove_objects(self.bucket, delete_object_list) errors = self.mclient.remove_objects(self.bucket, delete_object_list)
for error in errors: for error in errors:
log.error(" AVATARS: Error occured when deleting avatar object", error) log.error(" AVATARS: Error occured when deleting avatar object", error)
def get_users(self): def get_users(self):
self.connect() self.connect()
users=self.get_users_with_groups_and_roles() users = self.get_users_with_groups_and_roles()
return users return users
def get_users_without_image(self): def get_users_without_image(self):
return [u for u in self.get_users() if u['id'] not in self.minio_get_objects()] return [u for u in self.get_users() if u["id"] not in self.minio_get_objects()]
def get_users_with_groups_and_roles(self): def get_users_with_groups_and_roles(self):
q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota
@ -99,20 +117,28 @@ class DefaultAvatars():
group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value
order by u.username""" order by u.username"""
(headers,users)=self.keycloak_pg.select_with_headers(q) (headers, users) = self.keycloak_pg.select_with_headers(q)
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\ users_with_lists = [
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\ list(l[:-4])
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\ + ([[]] if l[-4] == [None] else [list(set(l[-4]))])
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users] + ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\ + ([[]] if l[-1] == [None] else [list(set(l[-1]))])
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\ for l in users
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\ ]
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
users_with_lists = [
list(l[:-4])
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users_with_lists
]
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists] list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
return list_dict_users return list_dict_users
da=DefaultAvatars()
da = DefaultAvatars()

View File

@ -1,77 +1,97 @@
#!/usr/bin/env python #!/usr/bin/env python
import time ,os import json
from datetime import datetime, timedelta
import logging as log import logging as log
import os
import time
import traceback import traceback
import yaml, json from datetime import datetime, timedelta
from pprint import pprint from pprint import pprint
import diceware
import yaml
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from keycloak import KeycloakAdmin from keycloak import KeycloakAdmin
from postgres import Postgres from postgres import Postgres
import diceware
options = diceware.handle_options(None) options = diceware.handle_options(None)
options.wordlist = 'cat_ascii' options.wordlist = "cat_ascii"
options.num = 3 options.num = 3
class KeycloakClient():
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
https://github.com/marcospereirampj/python-keycloak
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
"""
def __init__(self,
url="http://isard-sso-keycloak:8080/auth/",
username=os.environ['KEYCLOAK_USER'],
password=os.environ['KEYCLOAK_PASSWORD'],
realm='master',
verify=True):
self.url=url
self.username=username
self.password=password
self.realm=realm
self.verify=verify
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD']) class KeycloakClient:
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
https://github.com/marcospereirampj/python-keycloak
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
"""
def __init__(
self,
url="http://isard-sso-keycloak:8080/auth/",
username=os.environ["KEYCLOAK_USER"],
password=os.environ["KEYCLOAK_PASSWORD"],
realm="master",
verify=True,
):
self.url = url
self.username = username
self.password = password
self.realm = realm
self.verify = verify
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(
username=self.username, server_url=self.url,
password=self.password, username=self.username,
realm_name=self.realm, password=self.password,
verify=self.verify) realm_name=self.realm,
verify=self.verify,
)
def update_pwds(self): def update_pwds(self):
self.get_users() self.get_users()
def get_users(self): def get_users(self):
self.connect() self.connect()
users=self.get_users_with_groups_and_roles() users = self.get_users_with_groups_and_roles()
userupdate=[] userupdate = []
for u in users: for u in users:
if u['username'] not in ['admin','ddadmin'] and not u['username'].startswith('system_'): if u["username"] not in ["admin", "ddadmin"] and not u[
print('Generating password for user '+u['username']) "username"
userupdate.append({'id':u['id'], ].startswith("system_"):
'username':u['username'], print("Generating password for user " + u["username"])
'password': diceware.get_passphrase(options=options)}) userupdate.append(
with open("user_temp_passwd.csv","w") as csv: {
"id": u["id"],
"username": u["username"],
"password": diceware.get_passphrase(options=options),
}
)
with open("user_temp_passwd.csv", "w") as csv:
for user in userupdate: for user in userupdate:
csv.write("%s,%s,%s\n"%(user['id'],user['username'],user['password'])) csv.write(
"%s,%s,%s\n" % (user["id"], user["username"], user["password"])
)
for u in userupdate: for u in userupdate:
print('Updating keycloak password for user '+u['username']) print("Updating keycloak password for user " + u["username"])
self.update_user_pwd(u['id'],u['password']) self.update_user_pwd(u["id"], u["password"])
def update_user_pwd(self,user_id,password,temporary=True): def update_user_pwd(self, user_id, password, temporary=True):
payload={"credentials":[{"type":"password", payload = {
"value":password, "credentials": [
"temporary":temporary}]} {"type": "password", "value": password, "temporary": temporary}
]
}
self.connect() self.connect()
self.keycloak_admin.update_user( user_id, payload) self.keycloak_admin.update_user(user_id, payload)
def get_users_with_groups_and_roles(self): def get_users_with_groups_and_roles(self):
q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota
@ -88,7 +108,7 @@ class KeycloakClient():
group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value
order by u.username""" order by u.username"""
# q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota, g.id, g.path, g.name, # q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota, g.id, g.path, g.name,
# --,json_agg(g."name") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2 # --,json_agg(g."name") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2
# --,json_agg(r.name) as role # --,json_agg(r.name) as role
# from user_entity as u # from user_entity as u
@ -96,7 +116,7 @@ class KeycloakClient():
# left join user_group_membership as ugm on ugm.user_id = u.id # left join user_group_membership as ugm on ugm.user_id = u.id
# left join keycloak_group as g on g.id = ugm.group_id # left join keycloak_group as g on g.id = ugm.group_id
# --left join keycloak_group as g_parent on g.parent_group = g_parent.id # --left join keycloak_group as g_parent on g.parent_group = g_parent.id
# --left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id # --left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id
# left join user_role_mapping as rm on rm.user_id = u.id # left join user_role_mapping as rm on rm.user_id = u.id
# left join keycloak_role as r on r.id = rm.role_id # left join keycloak_role as r on r.id = rm.role_id
# --group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value # --group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value
@ -111,25 +131,34 @@ class KeycloakClient():
# left join user_group_membership as ugm on ugm.user_id = u.id # left join user_group_membership as ugm on ugm.user_id = u.id
# left join keycloak_group as g on g.id = ugm.group_id # left join keycloak_group as g on g.id = ugm.group_id
# left join keycloak_group as g_parent on g.parent_group = g_parent.id # left join keycloak_group as g_parent on g.parent_group = g_parent.id
# left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id # left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id
# left join user_role_mapping as rm on rm.user_id = u.id # left join user_role_mapping as rm on rm.user_id = u.id
# left join keycloak_role as r on r.id = rm.role_id # left join keycloak_role as r on r.id = rm.role_id
# group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value # group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value
# order by u.username""" # order by u.username"""
(headers,users)=self.keycloak_pg.select_with_headers(q) (headers, users) = self.keycloak_pg.select_with_headers(q)
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\ users_with_lists = [
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\ list(l[:-4])
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\ + ([[]] if l[-4] == [None] else [list(set(l[-4]))])
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users] + ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\ + ([[]] if l[-1] == [None] else [list(set(l[-1]))])
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\ for l in users
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\ ]
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
users_with_lists = [
list(l[:-4])
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users_with_lists
]
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists] list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
return list_dict_users return list_dict_users
k=KeycloakClient()
k = KeycloakClient()
k.update_pwds() k.update_pwds()

View File

@ -1,70 +1,88 @@
#!/usr/bin/env python #!/usr/bin/env python
import time ,os import json
from datetime import datetime, timedelta
import logging as log import logging as log
import os
import time
import traceback import traceback
import yaml, json from datetime import datetime, timedelta
from pprint import pprint from pprint import pprint
import yaml
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from keycloak import KeycloakAdmin from keycloak import KeycloakAdmin
from postgres import Postgres from postgres import Postgres
class KeycloakClient(): 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
""" """
def __init__(self,
url="http://isard-sso-keycloak:8080/auth/",
username=os.environ['KEYCLOAK_USER'],
password=os.environ['KEYCLOAK_PASSWORD'],
realm='master',
verify=True):
self.url=url
self.username=username
self.password=password
self.realm=realm
self.verify=verify
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD']) def __init__(
self,
url="http://isard-sso-keycloak:8080/auth/",
username=os.environ["KEYCLOAK_USER"],
password=os.environ["KEYCLOAK_PASSWORD"],
realm="master",
verify=True,
):
self.url = url
self.username = username
self.password = password
self.realm = realm
self.verify = verify
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(
username=self.username, server_url=self.url,
password=self.password, username=self.username,
realm_name=self.realm, password=self.password,
verify=self.verify) realm_name=self.realm,
verify=self.verify,
)
def update_pwds(self): def update_pwds(self):
self.get_users() self.get_users()
def get_users(self): def get_users(self):
self.connect() self.connect()
users=self.get_users_with_groups_and_roles() users = self.get_users_with_groups_and_roles()
userupdate=[] userupdate = []
for u in users: for u in users:
if u['username'] not in ['admin','ddadmin'] and not u['username'].startswith('system_'): if u["username"] not in ["admin", "ddadmin"] and not u[
print('Generating password for user '+u['username']) "username"
userupdate.append({'id':u['id'], ].startswith("system_"):
'username':u['username'], print("Generating password for user " + u["username"])
'password': diceware.get_passphrase(options=options)}) userupdate.append(
with open("user_temp_passwd.csv","w") as csv: {
"id": u["id"],
"username": u["username"],
"password": diceware.get_passphrase(options=options),
}
)
with open("user_temp_passwd.csv", "w") as csv:
for user in userupdate: for user in userupdate:
csv.write("%s,%s,%s\n"%(user['id'],user['username'],user['password'])) csv.write(
"%s,%s,%s\n" % (user["id"], user["username"], user["password"])
)
for u in userupdate: for u in userupdate:
print('Updating keycloak password for user '+u['username']) print("Updating keycloak password for user " + u["username"])
self.update_user_pwd(u['id'],u['password']) self.update_user_pwd(u["id"], u["password"])
def update_user_pwd_temporary(self,temporary=False): def update_user_pwd_temporary(self, temporary=False):
payload={"credentials":[{"temporary":temporary}]} payload = {"credentials": [{"temporary": temporary}]}
self.connect() self.connect()
self.keycloak_admin.update_user( user_id, payload) self.keycloak_admin.update_user(user_id, payload)
def get_users_with_groups_and_roles(self): def get_users_with_groups_and_roles(self):
q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota
@ -81,7 +99,7 @@ class KeycloakClient():
group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value
order by u.username""" order by u.username"""
# q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota, g.id, g.path, g.name, # q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota, g.id, g.path, g.name,
# --,json_agg(g."name") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2 # --,json_agg(g."name") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2
# --,json_agg(r.name) as role # --,json_agg(r.name) as role
# from user_entity as u # from user_entity as u
@ -89,7 +107,7 @@ class KeycloakClient():
# left join user_group_membership as ugm on ugm.user_id = u.id # left join user_group_membership as ugm on ugm.user_id = u.id
# left join keycloak_group as g on g.id = ugm.group_id # left join keycloak_group as g on g.id = ugm.group_id
# --left join keycloak_group as g_parent on g.parent_group = g_parent.id # --left join keycloak_group as g_parent on g.parent_group = g_parent.id
# --left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id # --left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id
# left join user_role_mapping as rm on rm.user_id = u.id # left join user_role_mapping as rm on rm.user_id = u.id
# left join keycloak_role as r on r.id = rm.role_id # left join keycloak_role as r on r.id = rm.role_id
# --group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value # --group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value
@ -104,25 +122,34 @@ class KeycloakClient():
# left join user_group_membership as ugm on ugm.user_id = u.id # left join user_group_membership as ugm on ugm.user_id = u.id
# left join keycloak_group as g on g.id = ugm.group_id # left join keycloak_group as g on g.id = ugm.group_id
# left join keycloak_group as g_parent on g.parent_group = g_parent.id # left join keycloak_group as g_parent on g.parent_group = g_parent.id
# left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id # left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id
# left join user_role_mapping as rm on rm.user_id = u.id # left join user_role_mapping as rm on rm.user_id = u.id
# left join keycloak_role as r on r.id = rm.role_id # left join keycloak_role as r on r.id = rm.role_id
# group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value # group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value
# order by u.username""" # order by u.username"""
(headers,users)=self.keycloak_pg.select_with_headers(q) (headers, users) = self.keycloak_pg.select_with_headers(q)
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\ users_with_lists = [
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\ list(l[:-4])
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\ + ([[]] if l[-4] == [None] else [list(set(l[-4]))])
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users] + ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\ + ([[]] if l[-1] == [None] else [list(set(l[-1]))])
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\ for l in users
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\ ]
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
users_with_lists = [
list(l[:-4])
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users_with_lists
]
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists] list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
return list_dict_users return list_dict_users
k=KeycloakClient()
k = KeycloakClient()
k.update_user_pwd_temporary() k.update_user_pwd_temporary()

View File

@ -1,15 +1,16 @@
#!/usr/bin/env python #!/usr/bin/env python
import time ,os import json
from datetime import datetime, timedelta
import logging as log import logging as log
import os
import time
import traceback import traceback
import yaml, json from datetime import datetime, timedelta
from pprint import pprint from pprint import pprint
import yaml
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from keycloak import KeycloakAdmin from keycloak import KeycloakAdmin
from postgres import Postgres from postgres import Postgres

View File

@ -1,28 +1,46 @@
#!flask/bin/python #!flask/bin/python
# coding=utf-8 # coding=utf-8
from gevent import monkey from gevent import monkey
monkey.patch_all() monkey.patch_all()
from flask_socketio import SocketIO, emit, join_room, leave_room, \
close_room, rooms, disconnect, send
import json import json
from flask_socketio import (
SocketIO,
close_room,
disconnect,
emit,
join_room,
leave_room,
rooms,
send,
)
from admin import app from admin import app
app.socketio = SocketIO(app) app.socketio = SocketIO(app)
@app.socketio.on('connect', namespace='/sio')
@app.socketio.on("connect", namespace="/sio")
def socketio_connect(): def socketio_connect():
join_room('admin') join_room("admin")
app.socketio.emit('update', app.socketio.emit("update", json.dumps("Joined"), namespace="/sio", room="admin")
json.dumps('Joined'),
namespace='/sio',
room='admin') @app.socketio.on("disconnect", namespace="/sio")
@app.socketio.on('disconnect', namespace='/sio')
def socketio_disconnect(): def socketio_disconnect():
None None
if __name__ == '__main__':
app.socketio.run(app,host='0.0.0.0', port=9000, debug=False, ssl_context='adhoc', async_mode="threading") #, logger=logger, engineio_logger=engineio_logger) if __name__ == "__main__":
app.socketio.run(
app,
host="0.0.0.0",
port=9000,
debug=False,
ssl_context="adhoc",
async_mode="threading",
) # , logger=logger, engineio_logger=engineio_logger)
# , cors_allowed_origins="*" # , cors_allowed_origins="*"
# /usr/lib/python3.8/site-packages/certifi # /usr/lib/python3.8/site-packages/certifi

View File

@ -1,67 +1,84 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf-8 # coding=utf-8
import time, os import json
from datetime import datetime, timedelta
import pprint
import logging as log import logging as log
import os
import pprint
import random
import string
import time
import traceback import traceback
import yaml, json from datetime import datetime, timedelta
import psycopg2 import psycopg2
import yaml
from admin.lib.mysql import Mysql
from admin.lib.keycloak_client import KeycloakClient from admin.lib.keycloak_client import KeycloakClient
from admin.lib.mysql import Mysql
import string, random app = {}
app["config"] = {}
app={}
app['config']={}
class WordpressSaml(): class WordpressSaml:
def __init__(self): def __init__(self):
self.url="http://isard-sso-keycloak:8080/auth/" self.url = "http://isard-sso-keycloak:8080/auth/"
self.username=os.environ['KEYCLOAK_USER'] self.username = os.environ["KEYCLOAK_USER"]
self.password=os.environ['KEYCLOAK_PASSWORD'] self.password = os.environ["KEYCLOAK_PASSWORD"]
self.realm='master' self.realm = "master"
self.verify=True self.verify = True
ready=False ready = False
while not ready: while not ready:
try: try:
self.db=Mysql('isard-apps-mariadb','wordpress','root',os.environ['MARIADB_PASSWORD']) self.db = Mysql(
ready=True "isard-apps-mariadb",
"wordpress",
"root",
os.environ["MARIADB_PASSWORD"],
)
ready = True
except: except:
log.warning('Could not connect to wordpress database. Retrying...') log.warning("Could not connect to wordpress database. Retrying...")
time.sleep(2) time.sleep(2)
log.info('Connected to wordpress database.') log.info("Connected to wordpress database.")
ready=False ready = False
while not ready: while not ready:
try: try:
with open(os.path.join("./saml_certs/public.cert"),"r") as crt: with open(os.path.join("./saml_certs/public.cert"), "r") as crt:
app['config']['PUBLIC_CERT_RAW']=crt.read() app["config"]["PUBLIC_CERT_RAW"] = crt.read()
app['config']['PUBLIC_CERT']=self.cert_prepare(app['config']['PUBLIC_CERT_RAW']) app["config"]["PUBLIC_CERT"] = self.cert_prepare(
ready=True app["config"]["PUBLIC_CERT_RAW"]
)
ready = True
except IOError: except IOError:
log.warning('Could not get public certificate to be used in wordpress. Retrying...') log.warning(
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert') "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) time.sleep(2)
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
log.info('Got moodle srt certificate to be used in wordpress.') log.info("Got moodle srt certificate to be used in wordpress.")
ready=False ready = False
while not ready: while not ready:
try: try:
with open(os.path.join("./saml_certs/private.key"),"r") as pem: with open(os.path.join("./saml_certs/private.key"), "r") as pem:
app['config']['PRIVATE_KEY']=self.cert_prepare(pem.read()) app["config"]["PRIVATE_KEY"] = self.cert_prepare(pem.read())
ready=True ready = True
except IOError: except IOError:
log.warning('Could not get private key to be used in wordpress. Retrying...') log.warning(
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert') "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) time.sleep(2)
log.info('Got moodle pem certificate to be used in wordpress.') 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. # ## 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 # ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
@ -79,32 +96,34 @@ class WordpressSaml():
self.reset_saml() self.reset_saml()
except: except:
print(traceback.format_exc()) print(traceback.format_exc())
print('Error resetting saml on wordpress') print("Error resetting saml on wordpress")
try: try:
self.delete_keycloak_wordpress_saml_plugin() self.delete_keycloak_wordpress_saml_plugin()
except: except:
print('Error resetting saml on keycloak') print("Error resetting saml on keycloak")
try: try:
self.set_wordpress_saml_plugin() self.set_wordpress_saml_plugin()
except: except:
print(traceback.format_exc()) print(traceback.format_exc())
print('Error adding saml on wordpress') print("Error adding saml on wordpress")
try: try:
self.add_keycloak_wordpress_saml() self.add_keycloak_wordpress_saml()
except: except:
print('Error adding saml on keycloak') print("Error adding saml on keycloak")
# SAML clients don't work well with composite roles so disabling and adding on realm # SAML clients don't work well with composite roles so disabling and adding on realm
# self.add_client_roles() # self.add_client_roles()
def connect(self): def connect(self):
self.keycloak= KeycloakClient(url=self.url, self.keycloak = KeycloakClient(
username=self.username, url=self.url,
password=self.password, username=self.username,
realm=self.realm, password=self.password,
verify=self.verify) 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
@ -113,28 +132,29 @@ class WordpressSaml():
# def get_privatekey_pass(self): # def get_privatekey_pass(self):
# return self.db.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2] # return self.db.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2]
def cert_prepare(self,cert): def cert_prepare(self, cert):
return ''.join(cert.split('-----')[2].splitlines()) return "".join(cert.split("-----")[2].splitlines())
def parse_idp_cert(self): def parse_idp_cert(self):
self.connect() self.connect()
rsa=self.keycloak.get_server_rsa_key() rsa = self.keycloak.get_server_rsa_key()
self.keycloak=None self.keycloak = None
return rsa['certificate'] return rsa["certificate"]
def set_keycloak_wordpress_saml_plugin(self): def set_keycloak_wordpress_saml_plugin(self):
self.connect() self.connect()
self.keycloak.add_wordpress_client() self.keycloak.add_wordpress_client()
self.keycloak=None self.keycloak = None
def delete_keycloak_wordpress_saml_plugin(self): def delete_keycloak_wordpress_saml_plugin(self):
self.connect() self.connect()
self.keycloak.delete_client('630601f8-25d1-4822-8741-c93affd2cd84') self.keycloak.delete_client("630601f8-25d1-4822-8741-c93affd2cd84")
self.keycloak=None self.keycloak = None
def set_wordpress_saml_plugin(self): 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'), # ('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 self.db.update(
"""INSERT INTO wp_options (option_name, option_value, autoload) VALUES
('onelogin_saml_enabled', 'on', 'yes'), ('onelogin_saml_enabled', 'on', 'yes'),
('onelogin_saml_idp_entityid', 'Saml Login', 'yes'), ('onelogin_saml_idp_entityid', 'Saml Login', 'yes'),
('onelogin_saml_idp_sso', 'https://sso.%s/auth/realms/master/protocol/saml', 'yes'), ('onelogin_saml_idp_sso', 'https://sso.%s/auth/realms/master/protocol/saml', 'yes'),
@ -194,103 +214,139 @@ class WordpressSaml():
('onelogin_saml_advanced_settings_sp_x509cert', '%s', 'yes'), ('onelogin_saml_advanced_settings_sp_x509cert', '%s', 'yes'),
('onelogin_saml_advanced_settings_sp_privatekey', '%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_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'])) ('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): def reset_saml(self):
self.db.update("""DELETE FROM wp_options WHERE option_name LIKE 'onelogin_saml_%'""") self.db.update(
"""DELETE FROM wp_options WHERE option_name LIKE 'onelogin_saml_%'"""
)
def add_keycloak_wordpress_saml(self): def add_keycloak_wordpress_saml(self):
client={"id" : "630601f8-25d1-4822-8741-c93affd2cd84", client = {
"clientId" : "php-saml", "id": "630601f8-25d1-4822-8741-c93affd2cd84",
"surrogateAuthRequired" : False, "clientId": "php-saml",
"enabled" : True, "surrogateAuthRequired": False,
"alwaysDisplayInConsole" : False, "enabled": True,
"clientAuthenticatorType" : "client-secret", "alwaysDisplayInConsole": False,
"redirectUris" : [ "https://wp."+os.environ['DOMAIN']+"/wp-login.php?saml_acs" ], "clientAuthenticatorType": "client-secret",
"webOrigins" : [ "https://wp."+os.environ['DOMAIN'] ], "redirectUris": [
"notBefore" : 0, "https://wp." + os.environ["DOMAIN"] + "/wp-login.php?saml_acs"
"bearerOnly" : False, ],
"consentRequired" : False, "webOrigins": ["https://wp." + os.environ["DOMAIN"]],
"standardFlowEnabled" : True, "notBefore": 0,
"implicitFlowEnabled" : False, "bearerOnly": False,
"directAccessGrantsEnabled" : False, "consentRequired": False,
"serviceAccountsEnabled" : False, "standardFlowEnabled": True,
"publicClient" : False, "implicitFlowEnabled": False,
"frontchannelLogout" : True, "directAccessGrantsEnabled": False,
"protocol" : "saml", "serviceAccountsEnabled": False,
"attributes" : { "publicClient": False,
"saml.force.post.binding" : True, "frontchannelLogout": True,
"saml_assertion_consumer_url_post" : "https://wp."+os.environ['DOMAIN']+"/wp-login.php?saml_acs", "protocol": "saml",
"saml.server.signature" : True, "attributes": {
"saml.server.signature.keyinfo.ext" : False, "saml.force.post.binding": True,
"saml.signing.certificate" : app['config']['PUBLIC_CERT_RAW'], "saml_assertion_consumer_url_post": "https://wp."
"saml_single_logout_service_url_redirect" : "https://wp."+os.environ['DOMAIN']+"/wp-login.php?saml_sls", + os.environ["DOMAIN"]
"saml.signature.algorithm" : "RSA_SHA256", + "/wp-login.php?saml_acs",
"saml_force_name_id_format" : False, "saml.server.signature": True,
"saml.client.signature" : True, "saml.server.signature.keyinfo.ext": False,
"saml.authnstatement" : True, "saml.signing.certificate": app["config"]["PUBLIC_CERT_RAW"],
"saml_name_id_format" : "username", "saml_single_logout_service_url_redirect": "https://wp."
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#" + 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",
},
}, },
"authenticationFlowBindingOverrides" : { }, {
"fullScopeAllowed" : True, "id": "abd6562f-4732-4da9-987f-b1a6ad6605fa",
"nodeReRegistrationTimeout" : -1, "name": "roles",
"protocolMappers" : [ { "protocol": "saml",
"id" : "72c6175e-bd07-4c27-abd6-4e4ae38d834b", "protocolMapper": "saml-role-list-mapper",
"name" : "username", "consentRequired": False,
"protocol" : "saml", "config": {
"protocolMapper" : "saml-user-attribute-mapper", "single": True,
"consentRequired" : False, "attribute.nameformat": "Basic",
"config" : { "friendly.name": "Roles",
"attribute.nameformat" : "Basic", "attribute.name": "Role",
"user.attribute" : "username", },
"friendly.name" : "username", },
"attribute.name" : "username" {
} "id": "50aafb71-d91c-4bc7-bb60-e1ae0222aab3",
}, { "name": "email",
"id" : "abd6562f-4732-4da9-987f-b1a6ad6605fa", "protocol": "saml",
"name" : "roles", "protocolMapper": "saml-user-property-mapper",
"protocol" : "saml", "consentRequired": False,
"protocolMapper" : "saml-role-list-mapper", "config": {
"consentRequired" : False, "attribute.nameformat": "Basic",
"config" : { "user.attribute": "email",
"single" : True, "friendly.name": "email",
"attribute.nameformat" : "Basic", "attribute.name": "email",
"friendly.name" : "Roles", },
"attribute.name" : "Role" },
} ],
}, { "defaultClientScopes": [
"id" : "50aafb71-d91c-4bc7-bb60-e1ae0222aab3", "web-origins",
"name" : "email", "role_list",
"protocol" : "saml", "roles",
"protocolMapper" : "saml-user-property-mapper", "profile",
"consentRequired" : False, "email",
"config" : { ],
"attribute.nameformat" : "Basic", "optionalClientScopes": [
"user.attribute" : "email", "address",
"friendly.name" : "email", "phone",
"attribute.name" : "email" "offline_access",
} "microprofile-jwt",
} ], ],
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], "access": {"view": True, "configure": True, "manage": True},
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ], }
"access" : {
"view" : True,
"configure" : True,
"manage" : True
}
}
self.connect() self.connect()
self.keycloak.add_client(client) self.keycloak.add_client(client)
self.keycloak=None self.keycloak = None
def add_client_roles(self): def add_client_roles(self):
self.connect() self.connect()
self.keycloak.add_client_role('630601f8-25d1-4822-8741-c93affd2cd84','admin','Wordpress admins') self.keycloak.add_client_role(
self.keycloak.add_client_role('630601f8-25d1-4822-8741-c93affd2cd84','manager','Wordpress managers') "630601f8-25d1-4822-8741-c93affd2cd84", "admin", "Wordpress admins"
self.keycloak.add_client_role('630601f8-25d1-4822-8741-c93affd2cd84','teacher','Wordpress teachers') )
self.keycloak.add_client_role('630601f8-25d1-4822-8741-c93affd2cd84','student','Wordpress students') self.keycloak.add_client_role(
self.keycloak=None "630601f8-25d1-4822-8741-c93affd2cd84", "manager", "Wordpress managers"
)
self.keycloak.add_client_role(
"630601f8-25d1-4822-8741-c93affd2cd84", "teacher", "Wordpress teachers"
)
self.keycloak.add_client_role(
"630601f8-25d1-4822-8741-c93affd2cd84", "student", "Wordpress students"
)
self.keycloak = None
nw=WordpressSaml()
nw = WordpressSaml()

View File

@ -1,67 +1,84 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf-8 # coding=utf-8
import time, os import json
from datetime import datetime, timedelta
import pprint
import logging as log import logging as log
import os
import pprint
import random
import string
import time
import traceback import traceback
import yaml, json from datetime import datetime, timedelta
import psycopg2 import psycopg2
import yaml
from admin.lib.mysql import Mysql
from admin.lib.keycloak_client import KeycloakClient from admin.lib.keycloak_client import KeycloakClient
from admin.lib.mysql import Mysql
import string, random app = {}
app["config"] = {}
app={}
app['config']={}
class WordpressSaml(): class WordpressSaml:
def __init__(self): def __init__(self):
self.url="http://isard-sso-keycloak:8080/auth/" self.url = "http://isard-sso-keycloak:8080/auth/"
self.username=os.environ['KEYCLOAK_USER'] self.username = os.environ["KEYCLOAK_USER"]
self.password=os.environ['KEYCLOAK_PASSWORD'] self.password = os.environ["KEYCLOAK_PASSWORD"]
self.realm='master' self.realm = "master"
self.verify=True self.verify = True
ready=False ready = False
while not ready: while not ready:
try: try:
self.db=Mysql('isard-apps-mariadb','wordpress','root',os.environ['MARIADB_PASSWORD']) self.db = Mysql(
ready=True "isard-apps-mariadb",
"wordpress",
"root",
os.environ["MARIADB_PASSWORD"],
)
ready = True
except: except:
log.warning('Could not connect to wordpress database. Retrying...') log.warning("Could not connect to wordpress database. Retrying...")
time.sleep(2) time.sleep(2)
log.info('Connected to wordpress database.') log.info("Connected to wordpress database.")
ready=False ready = False
while not ready: while not ready:
try: try:
with open(os.path.join("./saml_certs/public.cert"),"r") as crt: with open(os.path.join("./saml_certs/public.cert"), "r") as crt:
app['config']['PUBLIC_CERT_RAW']=crt.read() app["config"]["PUBLIC_CERT_RAW"] = crt.read()
app['config']['PUBLIC_CERT']=self.cert_prepare(app['config']['PUBLIC_CERT_RAW']) app["config"]["PUBLIC_CERT"] = self.cert_prepare(
ready=True app["config"]["PUBLIC_CERT_RAW"]
)
ready = True
except IOError: except IOError:
log.warning('Could not get public certificate to be used in wordpress. Retrying...') log.warning(
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert') "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) time.sleep(2)
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
log.info('Got moodle srt certificate to be used in wordpress.') log.info("Got moodle srt certificate to be used in wordpress.")
ready=False ready = False
while not ready: while not ready:
try: try:
with open(os.path.join("./saml_certs/private.key"),"r") as pem: with open(os.path.join("./saml_certs/private.key"), "r") as pem:
app['config']['PRIVATE_KEY']=self.cert_prepare(pem.read()) app["config"]["PRIVATE_KEY"] = self.cert_prepare(pem.read())
ready=True ready = True
except IOError: except IOError:
log.warning('Could not get private key to be used in wordpress. Retrying...') log.warning(
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert') "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) time.sleep(2)
log.info('Got moodle pem certificate to be used in wordpress.') 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. # ## 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 # ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
@ -79,14 +96,14 @@ class WordpressSaml():
self.reset_saml_certs() self.reset_saml_certs()
except: except:
print(traceback.format_exc()) print(traceback.format_exc())
print('Error resetting saml certs on wordpress') print("Error resetting saml certs on wordpress")
try: try:
self.set_wordpress_saml_plugin_certs() self.set_wordpress_saml_plugin_certs()
except: except:
print(traceback.format_exc()) print(traceback.format_exc())
print('Error adding saml on wordpress') print("Error adding saml on wordpress")
# SAML clients don't work well with composite roles so disabling and adding on realm # SAML clients don't work well with composite roles so disabling and adding on realm
# self.add_client_roles() # self.add_client_roles()
@ -98,32 +115,47 @@ class WordpressSaml():
# return self.db.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2] # return self.db.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2]
def connect(self): def connect(self):
self.keycloak= KeycloakClient(url=self.url, self.keycloak = KeycloakClient(
username=self.username, url=self.url,
password=self.password, username=self.username,
realm=self.realm, password=self.password,
verify=self.verify) realm=self.realm,
verify=self.verify,
)
def cert_prepare(self,cert): def cert_prepare(self, cert):
return ''.join(cert.split('-----')[2].splitlines()) return "".join(cert.split("-----")[2].splitlines())
def parse_idp_cert(self): def parse_idp_cert(self):
self.connect() self.connect()
rsa=self.keycloak.get_server_rsa_key() rsa = self.keycloak.get_server_rsa_key()
self.keycloak=None self.keycloak = None
return rsa['certificate'] return rsa["certificate"]
def set_wordpress_saml_plugin_certs(self): def set_wordpress_saml_plugin_certs(self):
# ('active_plugins', 'a:2:{i:0;s:33:\"edwiser-bridge/edwiser-bridge.php\";i:1;s:17:\"onelogin_saml.php\";}', 'yes'), # ('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 self.db.update(
"""INSERT INTO wp_options (option_name, option_value, autoload) VALUES
('onelogin_saml_idp_x509cert', '%s', 'yes'), ('onelogin_saml_idp_x509cert', '%s', 'yes'),
('onelogin_saml_advanced_settings_sp_x509cert', '%s', 'yes'), ('onelogin_saml_advanced_settings_sp_x509cert', '%s', 'yes'),
('onelogin_saml_advanced_settings_sp_privatekey', '%s', 'yes');""" % (self.parse_idp_cert(),app['config']['PUBLIC_CERT'],app['config']['PRIVATE_KEY'])) ('onelogin_saml_advanced_settings_sp_privatekey', '%s', 'yes');"""
% (
self.parse_idp_cert(),
app["config"]["PUBLIC_CERT"],
app["config"]["PRIVATE_KEY"],
)
)
def reset_saml_certs(self): def reset_saml_certs(self):
self.db.update("""DELETE FROM wp_options WHERE option_name = 'onelogin_saml_idp_x509cert'""") self.db.update(
self.db.update("""DELETE FROM wp_options WHERE option_name = 'onelogin_saml_advanced_settings_sp_x509cert'""") """DELETE FROM wp_options WHERE option_name = 'onelogin_saml_idp_x509cert'"""
self.db.update("""DELETE FROM wp_options WHERE option_name = 'onelogin_saml_advanced_settings_sp_privatekey'""") )
self.db.update(
"""DELETE FROM wp_options WHERE option_name = 'onelogin_saml_advanced_settings_sp_x509cert'"""
)
self.db.update(
"""DELETE FROM wp_options WHERE option_name = 'onelogin_saml_advanced_settings_sp_privatekey'"""
)
nw=WordpressSaml()
nw = WordpressSaml()