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.admin = Admin()
app.ready = False 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>')
@app.route("/build/<path:path>")
def send_build(path): def send_build(path):
return send_from_directory(os.path.join(app.root_path, 'node_modules/gentelella/build'), path) return send_from_directory(
os.path.join(app.root_path, "node_modules/gentelella/build"), path
)
@app.route('/vendors/<path:path>')
@app.route("/vendors/<path:path>")
def send_vendors(path): def send_vendors(path):
return send_from_directory(os.path.join(app.root_path, 'node_modules/gentelella/vendors'), path) return send_from_directory(
os.path.join(app.root_path, "node_modules/gentelella/vendors"), 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, '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()
@ -28,28 +30,30 @@ 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):

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,
userid,
os.path.join(app.root_path, "../custom/avatars/" + role + ".jpg"),
content_type="image/jpeg ", 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,
u["id"],
os.path.join(app.root_path, "../custom/avatars/" + img),
content_type="image/jpeg ", 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):
@ -67,4 +79,4 @@ class Avatars():
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,20 +1,31 @@
#!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
@ -26,85 +37,130 @@ class Events():
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",
room="admin",
)
sleep(0.0001) 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): def get_group_from_group_id(group_id, groups):
return next((d for d in groups if d.get('id') == group_id), None) 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_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): def get_gids_from_kgroup_ids(kgroup_ids, groups):
return [get_gid_from_kgroup_id(kgroup_id, groups) for kgroup_id in kgroup_ids] 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,44 +1,58 @@
#!/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():
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,
def __init__(
self,
url="http://isard-sso-keycloak:8080/auth/", url="http://isard-sso-keycloak:8080/auth/",
username=os.environ['KEYCLOAK_USER'], username=os.environ["KEYCLOAK_USER"],
password=os.environ['KEYCLOAK_PASSWORD'], password=os.environ["KEYCLOAK_PASSWORD"],
realm='master', realm="master",
verify=True): verify=True,
):
self.url = url self.url = url
self.username = username self.username = username
self.password = password self.password = password
self.realm = realm self.realm = realm
self.verify = verify self.verify = verify
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD']) self.keycloak_pg = Postgres(
"isard-apps-postgresql",
"keycloak",
os.environ["KEYCLOAK_DB_USER"],
os.environ["KEYCLOAK_DB_PASSWORD"],
)
def connect(self): def connect(self):
self.keycloak_admin = KeycloakAdmin(server_url=self.url, self.keycloak_admin = KeycloakAdmin(
server_url=self.url,
username=self.username, username=self.username,
password=self.password, password=self.password,
realm_name=self.realm, realm_name=self.realm,
verify=self.verify) verify=self.verify,
)
# from keycloak import KeycloakAdmin # from keycloak import KeycloakAdmin
# keycloak_admin = KeycloakAdmin(server_url="http://isard-sso-keycloak:8080/auth/",username="admin",password="keycloakkeycloak",realm_name="master",verify=False) # keycloak_admin = KeycloakAdmin(server_url="http://isard-sso-keycloak:8080/auth/",username="admin",password="keycloakkeycloak",realm_name="master",verify=False)
@ -60,7 +74,7 @@ class KeycloakClient():
# self.add_role('superman') # self.add_role('superman')
# pprint(self.get_roles()) # pprint(self.get_roles())
''' USERS ''' """ USERS """
def get_user_id(self, username): def get_user_id(self, username):
self.connect() self.connect()
@ -87,22 +101,29 @@ class KeycloakClient():
(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]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users
]
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_with_lists] + ([[]] 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,15 +134,13 @@ 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):
@ -134,7 +153,9 @@ class KeycloakClient():
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_id
)
user_group_ids = self.keycloak_pg.select(q) user_group_ids = self.keycloak_pg.select(q)
paths = [] paths = []
@ -151,49 +172,69 @@ 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(
{
"email": email,
"username": username, "username": username,
"enabled": enabled, "enabled": enabled,
"firstName": first, "firstName": first,
"lastName": last, "lastName": last,
"credentials":[{"type":"password", "credentials": [
"value":password, {"type": "password", "value": password, "temporary": temporary}
"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 = {
"enabled": enabled,
"email": email, "email": email,
"firstName": first, "firstName": first,
"lastName": last, "lastName": last,
"groups": groups, "groups": groups,
"realmRoles":roles} "realmRoles": roles,
}
self.connect() self.connect()
return self.keycloak_admin.update_user(user_id, payload) return self.keycloak_admin.update_user(user_id, payload)
@ -217,7 +258,11 @@ class KeycloakClient():
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 = [
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) return self.keycloak_admin.delete_realm_roles_of_user(user_id, roles)
def delete_user(self, userid): def delete_user(self, userid):
@ -234,7 +279,9 @@ class KeycloakClient():
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):
@ -246,7 +293,7 @@ class KeycloakClient():
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
@ -263,8 +310,8 @@ class KeycloakClient():
# 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']):
@ -279,12 +326,14 @@ class KeycloakClient():
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):
@ -296,20 +345,20 @@ class KeycloakClient():
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('/')
@ -339,48 +388,65 @@ class KeycloakClient():
# continue # continue
# self.add_group(parts[i],parent_id) # self.add_group(parts[i],parent_id)
def add_user_with_groups_and_role(self,username,first,last,email,password,role,groups): 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(
"Adding "
+ username
+ " with uuid: "
+ uid
+ " to group "
+ g
+ " with uuid: "
+ gid
)
self.keycloak_admin.group_user_add(uid, 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()
@ -390,25 +456,27 @@ class KeycloakClient():
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):
@ -421,14 +489,18 @@ class KeycloakClient():
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)

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
class loadConfig:
def __init__(self, app=None): 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__(
self,
key=app.config["MOODLE_WS_TOKEN"], key=app.config["MOODLE_WS_TOKEN"],
url="https://moodle." + app.config["DOMAIN"], url="https://moodle." + app.config["DOMAIN"],
endpoint="/webservice/rest/server.php", endpoint="/webservice/rest/server.php",
verify=app.config["VERIFY"]): 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:
@ -42,10 +50,10 @@ class Moodle():
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)
@ -61,55 +69,70 @@ 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(
{"wstoken": self.key, "moodlewsrestformat": "json", "wsfunction": fname}
)
response = post(self.url + self.endpoint, parameters, verify=self.verify) 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,
"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'}] 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
from mdl_user as u from mdl_user as u
@ -120,7 +143,12 @@ class Moodle():
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):
@ -162,41 +196,49 @@ class Moodle():
# 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':
@ -208,6 +250,7 @@ class Moodle():
# 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);
# define('CONTEXT_COURSECAT', 40); # define('CONTEXT_COURSECAT', 40);

View File

@ -1,23 +1,22 @@
#!/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
class Mysql:
def __init__(self, host, database, user, password): 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()

View File

@ -1,38 +1,63 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf-8 # coding=utf-8
import json
import logging as log
import os
import pprint
import time
import traceback
import urllib
import requests
# from ..lib.log import * # from ..lib.log import *
from admin import app from admin import app
import time,requests,json,pprint,os
import urllib
import traceback
import logging as log
from .nextcloud_exc import *
from .nextcloud_exc import *
from .postgres import Postgres from .postgres import Postgres
class Nextcloud():
def __init__(self, class Nextcloud:
url="https://nextcloud."+app.config['DOMAIN'], def __init__(
username=os.environ['NEXTCLOUD_ADMIN_USER'], self,
password=os.environ['NEXTCLOUD_ADMIN_PASSWORD'], url="https://nextcloud." + app.config["DOMAIN"],
verify=True): username=os.environ["NEXTCLOUD_ADMIN_USER"],
password=os.environ["NEXTCLOUD_ADMIN_PASSWORD"],
verify=True,
):
self.verify_cert = verify self.verify_cert = verify
self.apiurl=url+'/ocs/v1.php/cloud/' self.apiurl = url + "/ocs/v1.php/cloud/"
self.shareurl=url+'/ocs/v2.php/apps/files_sharing/api/v1/' self.shareurl = url + "/ocs/v2.php/apps/files_sharing/api/v1/"
self.davurl=url+'/remote.php/dav/files/' self.davurl = url + "/remote.php/dav/files/"
self.auth = (username, password) self.auth = (username, password)
self.user = username self.user = username
self.nextcloud_pg=Postgres('isard-apps-postgresql','nextcloud',app.config['NEXTCLOUD_POSTGRES_USER'],app.config['NEXTCLOUD_POSTGRES_PASSWORD']) self.nextcloud_pg = Postgres(
"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): def _request(
if auth == False: auth=self.auth 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,15 +95,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 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())
@ -99,7 +126,6 @@ class Nextcloud():
# 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]
@ -128,8 +154,18 @@ class Nextcloud():
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
@ -145,27 +181,40 @@ class Nextcloud():
# 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())
@ -184,85 +233,109 @@ 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())
@ -279,9 +352,11 @@ class Nextcloud():
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:
@ -296,74 +371,88 @@ class Nextcloud():
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())
@ -375,24 +464,29 @@ class Nextcloud():
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())
@ -403,15 +497,18 @@ class Nextcloud():
# 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:
@ -421,4 +518,3 @@ class Nextcloud():
# 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,27 +3,35 @@
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

View File

@ -1,24 +1,22 @@
#!/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
class Postgres:
def __init__(self, host, database, user, password): 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()

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(
"isard-apps-postgresql",
"moodle",
app.config["MOODLE_POSTGRES_USER"],
app.config["MOODLE_POSTGRES_PASSWORD"],
)
ready = True 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(
app.root_path,
"../moodledata/saml2/moodle." + app.config["DOMAIN"] + ".crt",
),
"r",
) as crt:
app.config.setdefault("SP_CRT", crt.read())
ready = True 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(
app.root_path,
"../moodledata/saml2/moodle." + app.config["DOMAIN"] + ".pem",
),
"r",
) as pem:
app.config.setdefault("SP_PEM", pem.read())
ready = True 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,
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) 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)
]
return json.dumps(users), 200, {"Content-Type": "application/json"}
@app.route('/api/users_bulk/<action>', methods=['PUT'])
@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()),
200,
{"Content-Type": "application/json"},
)
if request.method == "PUT":
data = request.get_json(force=True) data = request.get_json(force=True)
password=data['password'] password = data["password"]
temporary=data.get('temporary',True) 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,
"msg": "User " + u["username"] + " has invalid email: " + u["email"],
}
if u['role'] not in ['admin','manager','teacher','student']: if u["role"] not in ["admin", "manager", "teacher", "student"]:
if u['role'] == '': if u["role"] == "":
return {'pass':False,'msg':'User '+u['username']+' has no role assigned!'} return {
return {'pass':False,'msg':'User '+u['username']+' has invalid role: '+u['role']} "pass": False,
"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']: 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 {
return {'pass':True,'msg':''} "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 ..auth.authentication import *
from flask_login import login_required, current_user, login_user, logout_user
@app.route('/', methods=['GET', 'POST'])
@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(
{
"id": ram_user["id"],
"password": ram_user["password"],
"role": ram_user["role"],
"active": True,
}
)
login_user(user) login_user(user)
flash('Logged in successfully.','success') flash("Logged in successfully.", "success")
return redirect(url_for('web_users')) return redirect(url_for("web_users"))
else: else:
flash('Username not found or incorrect password.','warning') flash("Username not found or incorrect password.", "warning")
return render_template('login.html') 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

@ -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
from flask_login import login_required import json
from .decorators import is_admin import logging as log
import os
import sys
import time
import traceback
from pprint import pprint 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 admin import app
from ..lib.avatars import Avatars from ..lib.avatars import Avatars
from .decorators import is_admin
avatars = 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 = {}
app['config']={} 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(
"isard-apps-postgresql",
"moodle",
os.environ["MOODLE_POSTGRES_USER"],
os.environ["MOODLE_POSTGRES_PASSWORD"],
)
ready = True 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(
"./moodledata/saml2/moodle." + os.environ["DOMAIN"] + ".crt"
),
"r",
) as crt:
app["config"]["SP_CRT"] = crt.read()
ready = True 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(
"./moodledata/saml2/moodle." + os.environ["DOMAIN"] + ".pem"
),
"r",
) as pem:
app["config"]["SP_PEM"] = pem.read()
ready = True 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,46 +94,77 @@ 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()
@ -125,39 +173,55 @@ class MoodleSaml():
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."
+ os.environ["DOMAIN"]
+ "/auth/saml2/sp/metadata.php",
"surrogateAuthRequired": False, "surrogateAuthRequired": False,
"enabled": True, "enabled": True,
"alwaysDisplayInConsole": False, "alwaysDisplayInConsole": False,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris" : [ "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+os.environ['DOMAIN']+"" ], "redirectUris": [
"webOrigins" : [ "https://moodle."+os.environ['DOMAIN']+"" ], "https://moodle."
+ os.environ["DOMAIN"]
+ "/auth/saml2/sp/saml2-acs.php/moodle."
+ os.environ["DOMAIN"]
+ ""
],
"webOrigins": ["https://moodle." + os.environ["DOMAIN"] + ""],
"notBefore": 0, "notBefore": 0,
"bearerOnly": False, "bearerOnly": False,
"consentRequired": False, "consentRequired": False,
@ -171,23 +235,32 @@ class MoodleSaml():
"attributes": { "attributes": {
"saml.force.post.binding": True, "saml.force.post.binding": True,
"saml.encrypt": False, "saml.encrypt": False,
"saml_assertion_consumer_url_post" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+os.environ['DOMAIN']+"", "saml_assertion_consumer_url_post": "https://moodle."
+ os.environ["DOMAIN"]
+ "/auth/saml2/sp/saml2-acs.php/moodle."
+ os.environ["DOMAIN"]
+ "",
"saml.server.signature": True, "saml.server.signature": True,
"saml.server.signature.keyinfo.ext": False, "saml.server.signature.keyinfo.ext": False,
"saml.signing.certificate" : app['config']['SP_CRT'], "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_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.signature.algorithm": "RSA_SHA256",
"saml_force_name_id_format": False, "saml_force_name_id_format": False,
"saml.client.signature": True, "saml.client.signature": True,
"saml.encryption.certificate" : app['config']['SP_PEM'], "saml.encryption.certificate": app["config"]["SP_PEM"],
"saml.authnstatement": True, "saml.authnstatement": True,
"saml_name_id_format": "username", "saml_name_id_format": "username",
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#" "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", "id": "9296daa3-4fc4-4b80-b007-5070f546ae13",
"name": "X500 sn", "name": "X500 sn",
"protocol": "saml", "protocol": "saml",
@ -197,9 +270,10 @@ class MoodleSaml():
"attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"user.attribute": "lastName", "user.attribute": "lastName",
"friendly.name": "sn", "friendly.name": "sn",
"attribute.name" : "urn:oid:2.5.4.4" "attribute.name": "urn:oid:2.5.4.4",
} },
}, { },
{
"id": "ccecf6e4-d20a-4211-b67c-40200a6b2c5d", "id": "ccecf6e4-d20a-4211-b67c-40200a6b2c5d",
"name": "username", "name": "username",
"protocol": "saml", "protocol": "saml",
@ -209,9 +283,10 @@ class MoodleSaml():
"attribute.nameformat": "Basic", "attribute.nameformat": "Basic",
"user.attribute": "username", "user.attribute": "username",
"friendly.name": "username", "friendly.name": "username",
"attribute.name" : "username" "attribute.name": "username",
} },
}, { },
{
"id": "53858403-eba2-4f6d-81d0-cced700b5719", "id": "53858403-eba2-4f6d-81d0-cced700b5719",
"name": "X500 givenName", "name": "X500 givenName",
"protocol": "saml", "protocol": "saml",
@ -221,9 +296,10 @@ class MoodleSaml():
"attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"user.attribute": "firstName", "user.attribute": "firstName",
"friendly.name": "givenName", "friendly.name": "givenName",
"attribute.name" : "urn:oid:2.5.4.42" "attribute.name": "urn:oid:2.5.4.42",
} },
}, { },
{
"id": "20034db5-1d0e-4e66-b815-fb0440c6d1e2", "id": "20034db5-1d0e-4e66-b815-fb0440c6d1e2",
"name": "X500 email", "name": "X500 email",
"protocol": "saml", "protocol": "saml",
@ -233,16 +309,24 @@ class MoodleSaml():
"attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"user.attribute": "email", "user.attribute": "email",
"friendly.name": "email", "friendly.name": "email",
"attribute.name" : "urn:oid:1.2.840.113549.1.9.1" "attribute.name": "urn:oid:1.2.840.113549.1.9.1",
} },
} ], },
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ], "defaultClientScopes": [
"access" : { "web-origins",
"view" : True, "role_list",
"configure" : True, "roles",
"manage" : True "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)
@ -250,10 +334,19 @@ class MoodleSaml():
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(
"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 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 = {}
app['config']={} 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(
"isard-apps-postgresql",
"nextcloud",
os.environ["NEXTCLOUD_POSTGRES_USER"],
os.environ["NEXTCLOUD_POSTGRES_PASSWORD"],
)
ready = True 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(
url=self.url,
username=self.username, username=self.username,
password=self.password, password=self.password,
realm=self.realm, realm=self.realm,
verify=self.verify) 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,7 +130,7 @@ class NextcloudSaml():
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()
@ -122,11 +139,12 @@ class NextcloudSaml():
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,42 +162,60 @@ 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."
+ os.environ["DOMAIN"]
+ "/apps/user_saml/saml/metadata",
"surrogateAuthRequired": False, "surrogateAuthRequired": False,
"enabled": True, "enabled": True,
"alwaysDisplayInConsole": False, "alwaysDisplayInConsole": False,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris" : [ "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/acs" ], "redirectUris": [
"webOrigins" : [ "https://nextcloud."+os.environ['DOMAIN'] ], "https://nextcloud." + os.environ["DOMAIN"] + "/apps/user_saml/saml/acs"
],
"webOrigins": ["https://nextcloud." + os.environ["DOMAIN"]],
"notBefore": 0, "notBefore": 0,
"bearerOnly": False, "bearerOnly": False,
"consentRequired": False, "consentRequired": False,
@ -193,22 +229,27 @@ class NextcloudSaml():
"attributes": { "attributes": {
"saml.assertion.signature": True, "saml.assertion.signature": True,
"saml.force.post.binding": True, "saml.force.post.binding": True,
"saml_assertion_consumer_url_post" : "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/acs", "saml_assertion_consumer_url_post": "https://nextcloud."
+ os.environ["DOMAIN"]
+ "/apps/user_saml/saml/acs",
"saml.server.signature": True, "saml.server.signature": True,
"saml.server.signature.keyinfo.ext": False, "saml.server.signature.keyinfo.ext": False,
"saml.signing.certificate" : app['config']['PUBLIC_CERT'], "saml.signing.certificate": app["config"]["PUBLIC_CERT"],
"saml_single_logout_service_url_redirect" : "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/sls", "saml_single_logout_service_url_redirect": "https://nextcloud."
+ os.environ["DOMAIN"]
+ "/apps/user_saml/saml/sls",
"saml.signature.algorithm": "RSA_SHA256", "saml.signature.algorithm": "RSA_SHA256",
"saml_force_name_id_format": False, "saml_force_name_id_format": False,
"saml.client.signature": False, "saml.client.signature": False,
"saml.authnstatement": True, "saml.authnstatement": True,
"saml_name_id_format": "username", "saml_name_id_format": "username",
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#" "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", "id": "e8e4acff-da2b-46aa-8bdb-ba42171671d6",
"name": "username", "name": "username",
"protocol": "saml", "protocol": "saml",
@ -218,9 +259,10 @@ class NextcloudSaml():
"attribute.nameformat": "Basic", "attribute.nameformat": "Basic",
"user.attribute": "username", "user.attribute": "username",
"friendly.name": "username", "friendly.name": "username",
"attribute.name" : "username" "attribute.name": "username",
} },
}, { },
{
"id": "8ab13cd7-822a-40d5-a1e1-9f556aed2332", "id": "8ab13cd7-822a-40d5-a1e1-9f556aed2332",
"name": "quota", "name": "quota",
"protocol": "saml", "protocol": "saml",
@ -230,9 +272,10 @@ class NextcloudSaml():
"attribute.nameformat": "Basic", "attribute.nameformat": "Basic",
"user.attribute": "quota", "user.attribute": "quota",
"friendly.name": "quota", "friendly.name": "quota",
"attribute.name" : "quota" "attribute.name": "quota",
} },
}, { },
{
"id": "28206b59-757b-4e3c-81cb-0b6053b1fd3d", "id": "28206b59-757b-4e3c-81cb-0b6053b1fd3d",
"name": "email", "name": "email",
"protocol": "saml", "protocol": "saml",
@ -242,9 +285,10 @@ class NextcloudSaml():
"attribute.nameformat": "Basic", "attribute.nameformat": "Basic",
"user.attribute": "email", "user.attribute": "email",
"friendly.name": "email", "friendly.name": "email",
"attribute.name" : "email" "attribute.name": "email",
} },
}, { },
{
"id": "5176a593-180f-4924-b294-b83a0d8d5972", "id": "5176a593-180f-4924-b294-b83a0d8d5972",
"name": "displayname", "name": "displayname",
"protocol": "saml", "protocol": "saml",
@ -252,12 +296,13 @@ class NextcloudSaml():
"consentRequired": False, "consentRequired": False,
"config": { "config": {
"single": False, "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", "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", "attribute.nameformat": "Basic",
"friendly.name": "displayname", "friendly.name": "displayname",
"attribute.name" : "displayname" "attribute.name": "displayname",
} },
}, { },
{
"id": "e51e04b9-f71a-42de-819e-dd9285246ada", "id": "e51e04b9-f71a-42de-819e-dd9285246ada",
"name": "Roles", "name": "Roles",
"protocol": "saml", "protocol": "saml",
@ -267,9 +312,10 @@ class NextcloudSaml():
"single": True, "single": True,
"attribute.nameformat": "Basic", "attribute.nameformat": "Basic",
"friendly.name": "Roles", "friendly.name": "Roles",
"attribute.name" : "Roles" "attribute.name": "Roles",
} },
}, { },
{
"id": "9c101249-bb09-4cc8-8f75-5a18fcb307e6", "id": "9c101249-bb09-4cc8-8f75-5a18fcb307e6",
"name": "group_list", "name": "group_list",
"protocol": "saml", "protocol": "saml",
@ -280,19 +326,28 @@ class NextcloudSaml():
"attribute.nameformat": "Basic", "attribute.nameformat": "Basic",
"full.path": False, "full.path": False,
"friendly.name": "member", "friendly.name": "member",
"attribute.name" : "member" "attribute.name": "member",
} },
} ], },
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ], "defaultClientScopes": [
"access" : { "web-origins",
"view" : True, "role_list",
"configure" : True, "roles",
"manage" : True "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 = {}
app['config']={} 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(
"isard-apps-postgresql",
"nextcloud",
os.environ["NEXTCLOUD_POSTGRES_USER"],
os.environ["NEXTCLOUD_POSTGRES_PASSWORD"],
)
ready = True 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(
url=self.url,
username=self.username, username=self.username,
password=self.password, password=self.password,
realm=self.realm, realm=self.realm,
verify=self.verify) 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
@ -103,20 +120,28 @@ class NextcloudSaml():
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,24 +1,21 @@
#!/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():
class Postgres:
def __init__(self, host, database, user, password): 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()

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,
class DefaultAvatars:
def __init__(
self,
url="http://isard-sso-keycloak:8080/auth/", url="http://isard-sso-keycloak:8080/auth/",
username=os.environ['KEYCLOAK_USER'], username=os.environ["KEYCLOAK_USER"],
password=os.environ['KEYCLOAK_PASSWORD'], password=os.environ["KEYCLOAK_PASSWORD"],
realm='master', realm="master",
verify=True): verify=True,
):
self.url = url self.url = url
self.username = username self.username = username
self.password = password self.password = password
self.realm = realm self.realm = realm
self.verify = verify self.verify = verify
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD']) 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(
server_url=self.url,
username=self.username, username=self.username,
password=self.password, password=self.password,
realm_name=self.realm, realm_name=self.realm,
verify=self.verify) 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,
u["id"],
"custom/avatars/" + img,
content_type="image/jpeg ", 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):
@ -82,7 +100,7 @@ class DefaultAvatars():
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
@ -101,18 +119,26 @@ class DefaultAvatars():
(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]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users
]
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_with_lists] + ([[]] 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,48 +1,59 @@
#!/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():
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,
def __init__(
self,
url="http://isard-sso-keycloak:8080/auth/", url="http://isard-sso-keycloak:8080/auth/",
username=os.environ['KEYCLOAK_USER'], username=os.environ["KEYCLOAK_USER"],
password=os.environ['KEYCLOAK_PASSWORD'], password=os.environ["KEYCLOAK_PASSWORD"],
realm='master', realm="master",
verify=True): verify=True,
):
self.url = url self.url = url
self.username = username self.username = username
self.password = password self.password = password
self.realm = realm self.realm = realm
self.verify = verify self.verify = verify
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD']) self.keycloak_pg = Postgres(
"isard-apps-postgresql",
"keycloak",
os.environ["KEYCLOAK_DB_USER"],
os.environ["KEYCLOAK_DB_PASSWORD"],
)
def connect(self): def connect(self):
self.keycloak_admin = KeycloakAdmin(server_url=self.url, self.keycloak_admin = KeycloakAdmin(
server_url=self.url,
username=self.username, username=self.username,
password=self.password, password=self.password,
realm_name=self.realm, realm_name=self.realm,
verify=self.verify) verify=self.verify,
)
def update_pwds(self): def update_pwds(self):
self.get_users() self.get_users()
@ -52,27 +63,36 @@ class KeycloakClient():
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(
{
"id": u["id"],
"username": u["username"],
"password": diceware.get_passphrase(options=options),
}
)
with open("user_temp_passwd.csv", "w") as csv: 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
,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
@ -118,18 +138,27 @@ class KeycloakClient():
# 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]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users
]
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_with_lists] + ([[]] 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,44 +1,54 @@
#!/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,
def __init__(
self,
url="http://isard-sso-keycloak:8080/auth/", url="http://isard-sso-keycloak:8080/auth/",
username=os.environ['KEYCLOAK_USER'], username=os.environ["KEYCLOAK_USER"],
password=os.environ['KEYCLOAK_PASSWORD'], password=os.environ["KEYCLOAK_PASSWORD"],
realm='master', realm="master",
verify=True): verify=True,
):
self.url = url self.url = url
self.username = username self.username = username
self.password = password self.password = password
self.realm = realm self.realm = realm
self.verify = verify self.verify = verify
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD']) self.keycloak_pg = Postgres(
"isard-apps-postgresql",
"keycloak",
os.environ["KEYCLOAK_DB_USER"],
os.environ["KEYCLOAK_DB_PASSWORD"],
)
def connect(self): def connect(self):
self.keycloak_admin = KeycloakAdmin(server_url=self.url, self.keycloak_admin = KeycloakAdmin(
server_url=self.url,
username=self.username, username=self.username,
password=self.password, password=self.password,
realm_name=self.realm, realm_name=self.realm,
verify=self.verify) verify=self.verify,
)
def update_pwds(self): def update_pwds(self):
self.get_users() self.get_users()
@ -48,18 +58,26 @@ class KeycloakClient():
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(
{
"id": u["id"],
"username": u["username"],
"password": diceware.get_passphrase(options=options),
}
)
with open("user_temp_passwd.csv", "w") as csv: 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}]}
@ -111,18 +129,27 @@ class KeycloakClient():
# 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]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users
]
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_with_lists] + ([[]] 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')
def socketio_connect():
join_room('admin')
app.socketio.emit('update',
json.dumps('Joined'),
namespace='/sio',
room='admin')
@app.socketio.on('disconnect', namespace='/sio') @app.socketio.on("connect", namespace="/sio")
def socketio_connect():
join_room("admin")
app.socketio.emit("update", json.dumps("Joined"), namespace="/sio", room="admin")
@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 = {}
app['config']={} 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(
"isard-apps-mariadb",
"wordpress",
"root",
os.environ["MARIADB_PASSWORD"],
)
ready = True 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(
app["config"]["PUBLIC_CERT_RAW"]
)
ready = True 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(
url=self.url,
username=self.username, username=self.username,
password=self.password, password=self.password,
realm=self.realm, realm=self.realm,
verify=self.verify) 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
@ -114,13 +133,13 @@ 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 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()
@ -129,12 +148,13 @@ class WordpressSaml():
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,21 +214,33 @@ 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 = {
"id": "630601f8-25d1-4822-8741-c93affd2cd84",
"clientId": "php-saml", "clientId": "php-saml",
"surrogateAuthRequired": False, "surrogateAuthRequired": False,
"enabled": True, "enabled": True,
"alwaysDisplayInConsole": False, "alwaysDisplayInConsole": False,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris" : [ "https://wp."+os.environ['DOMAIN']+"/wp-login.php?saml_acs" ], "redirectUris": [
"webOrigins" : [ "https://wp."+os.environ['DOMAIN'] ], "https://wp." + os.environ["DOMAIN"] + "/wp-login.php?saml_acs"
],
"webOrigins": ["https://wp." + os.environ["DOMAIN"]],
"notBefore": 0, "notBefore": 0,
"bearerOnly": False, "bearerOnly": False,
"consentRequired": False, "consentRequired": False,
@ -221,22 +253,27 @@ class WordpressSaml():
"protocol": "saml", "protocol": "saml",
"attributes": { "attributes": {
"saml.force.post.binding": True, "saml.force.post.binding": True,
"saml_assertion_consumer_url_post" : "https://wp."+os.environ['DOMAIN']+"/wp-login.php?saml_acs", "saml_assertion_consumer_url_post": "https://wp."
+ os.environ["DOMAIN"]
+ "/wp-login.php?saml_acs",
"saml.server.signature": True, "saml.server.signature": True,
"saml.server.signature.keyinfo.ext": False, "saml.server.signature.keyinfo.ext": False,
"saml.signing.certificate" : app['config']['PUBLIC_CERT_RAW'], "saml.signing.certificate": app["config"]["PUBLIC_CERT_RAW"],
"saml_single_logout_service_url_redirect" : "https://wp."+os.environ['DOMAIN']+"/wp-login.php?saml_sls", "saml_single_logout_service_url_redirect": "https://wp."
+ os.environ["DOMAIN"]
+ "/wp-login.php?saml_sls",
"saml.signature.algorithm": "RSA_SHA256", "saml.signature.algorithm": "RSA_SHA256",
"saml_force_name_id_format": False, "saml_force_name_id_format": False,
"saml.client.signature": True, "saml.client.signature": True,
"saml.authnstatement": True, "saml.authnstatement": True,
"saml_name_id_format": "username", "saml_name_id_format": "username",
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#" "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": "72c6175e-bd07-4c27-abd6-4e4ae38d834b", "id": "72c6175e-bd07-4c27-abd6-4e4ae38d834b",
"name": "username", "name": "username",
"protocol": "saml", "protocol": "saml",
@ -246,9 +283,10 @@ class WordpressSaml():
"attribute.nameformat": "Basic", "attribute.nameformat": "Basic",
"user.attribute": "username", "user.attribute": "username",
"friendly.name": "username", "friendly.name": "username",
"attribute.name" : "username" "attribute.name": "username",
} },
}, { },
{
"id": "abd6562f-4732-4da9-987f-b1a6ad6605fa", "id": "abd6562f-4732-4da9-987f-b1a6ad6605fa",
"name": "roles", "name": "roles",
"protocol": "saml", "protocol": "saml",
@ -258,9 +296,10 @@ class WordpressSaml():
"single": True, "single": True,
"attribute.nameformat": "Basic", "attribute.nameformat": "Basic",
"friendly.name": "Roles", "friendly.name": "Roles",
"attribute.name" : "Role" "attribute.name": "Role",
} },
}, { },
{
"id": "50aafb71-d91c-4bc7-bb60-e1ae0222aab3", "id": "50aafb71-d91c-4bc7-bb60-e1ae0222aab3",
"name": "email", "name": "email",
"protocol": "saml", "protocol": "saml",
@ -270,16 +309,24 @@ class WordpressSaml():
"attribute.nameformat": "Basic", "attribute.nameformat": "Basic",
"user.attribute": "email", "user.attribute": "email",
"friendly.name": "email", "friendly.name": "email",
"attribute.name" : "email" "attribute.name": "email",
} },
} ], },
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ], "defaultClientScopes": [
"access" : { "web-origins",
"view" : True, "role_list",
"configure" : True, "roles",
"manage" : True "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)
@ -287,10 +334,19 @@ class WordpressSaml():
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(
"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 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 = {}
app['config']={} 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(
"isard-apps-mariadb",
"wordpress",
"root",
os.environ["MARIADB_PASSWORD"],
)
ready = True 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(
app["config"]["PUBLIC_CERT_RAW"]
)
ready = True 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,13 +96,13 @@ 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(
url=self.url,
username=self.username, username=self.username,
password=self.password, password=self.password,
realm=self.realm, realm=self.realm,
verify=self.verify) 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()