refator(admin): black

darta 2021-12-28 22:40:32 +01:00
parent 53fbbc123a
commit f87a6c6b2f
32 changed files with 4156 additions and 2695 deletions

View File

@ -1,77 +1,96 @@
#!flask/bin/python
# coding=utf-8
import os
import logging as log
import os
from flask import Flask, send_from_directory, render_template
app = Flask(__name__, static_url_path='')
app = Flask(__name__, template_folder='static/templates')
from flask import Flask, render_template, send_from_directory
app = Flask(__name__, static_url_path="")
app = Flask(__name__, template_folder="static/templates")
app.url_map.strict_slashes = False
'''
"""
App secret key for encrypting cookies
You can generate one with:
import os
os.urandom(24)
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'"
print('Starting isard-sso api...')
print("Starting isard-sso api...")
from admin.lib.load_config import loadConfig
try:
loadConfig(app)
except:
print('Could not get environment variables...')
print("Could not get environment variables...")
from admin.lib.postup import Postup
Postup()
from admin.lib.admin import Admin
app.admin = Admin()
app.ready = False
'''
"""
Debug should be removed on production!
'''
"""
if app.debug:
log.warning('Debug mode: {}'.format(app.debug))
log.warning("Debug mode: {}".format(app.debug))
else:
log.info('Debug mode: {}'.format(app.debug))
log.info("Debug mode: {}".format(app.debug))
'''
"""
Serve static files
'''
@app.route('/build/<path:path>')
"""
@app.route("/build/<path:path>")
def send_build(path):
return send_from_directory(os.path.join(app.root_path, 'node_modules/gentelella/build'), path)
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):
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):
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>')
# def send_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):
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):
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):
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)
# def not_found_error(error):
@ -81,15 +100,7 @@ def send_custom(path):
# def internal_error(error):
# return render_template('page_500.html'), 500
'''
"""
Import all views
'''
from .views import LoginViews
from .views import WebViews
from .views import ApiViews
from .views import InternalViews
"""
from .views import ApiViews, InternalViews, LoginViews, WebViews

View File

@ -1,8 +1,10 @@
from admin import app
from flask_login import LoginManager, UserMixin
import os
''' OIDC TESTS '''
from flask_login import LoginManager, UserMixin
from admin import app
""" OIDC TESTS """
# from flask_oidc import OpenIDConnect
# 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',
@ -18,7 +20,7 @@ import os
# # 'OVERWRITE_REDIRECT_URI': 'https://sso.mydomain.duckdns.org//custom_callback',
# # 'OIDC_CALLBACK_ROUTE': '//custom_callback'
# oidc = OpenIDConnect(app)
''' OIDC TESTS '''
""" OIDC TESTS """
login_manager = LoginManager()
@ -28,28 +30,30 @@ login_manager.login_view = "login"
ram_users = {
os.environ["ADMINAPP_USER"]: {
'id': os.environ["ADMINAPP_USER"],
'password': os.environ["ADMINAPP_PASSWORD"],
'role': 'manager'
"id": os.environ["ADMINAPP_USER"],
"password": os.environ["ADMINAPP_PASSWORD"],
"role": "manager",
},
os.environ["KEYCLOAK_USER"]: {
'id': os.environ["KEYCLOAK_USER"],
'password': os.environ["KEYCLOAK_PASSWORD"],
'role': 'admin',
"id": os.environ["KEYCLOAK_USER"],
"password": os.environ["KEYCLOAK_PASSWORD"],
"role": "admin",
},
os.environ["WORDPRESS_MARIADB_USER"]: {
'id': os.environ["WORDPRESS_MARIADB_USER"],
'password': os.environ["WORDPRESS_MARIADB_PASSWORD"],
'role': 'manager',
}
"id": os.environ["WORDPRESS_MARIADB_USER"],
"password": os.environ["WORDPRESS_MARIADB_PASSWORD"],
"role": "manager",
},
}
class User(UserMixin):
def __init__(self, dict):
self.id = dict['id']
self.username = dict['id']
self.password = dict['password']
self.role = dict['role']
self.id = dict["id"]
self.username = dict["id"]
self.password = dict["password"]
self.role = dict["role"]
@login_manager.user_loader
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
from pprint import pprint
import os
from pprint import pprint
from minio import Minio
from minio.commonconfig import REPLACE, CopySource
from minio.deleteobjects import DeleteObject
from requests import get, post
class Avatars():
from admin import app
class Avatars:
def __init__(self):
self.mclient = Minio(
"isard-sso-avatars:9000",
access_key="AKIAIOSFODNN7EXAMPLE",
secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
secure=False
secure=False,
)
self.bucket='master-avatars'
self.bucket = "master-avatars"
self._minio_set_realm()
# 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.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 ",
)
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):
self.minio_delete_object(userid)
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):
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:
img='unknown.jpg'
img = "unknown.jpg"
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 ",
)
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):
if not self.mclient.bucket_exists(self.bucket):
@ -67,4 +79,4 @@ class Avatars():
log.error(" AVATARS: Error occured when deleting avatar object: " + error)
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
# 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 json
import logging as log
import os
import sys
import traceback
from time import sleep
from uuid import uuid4
class Events():
def __init__(self,title,text='',total=0,table=False,type='info'):
from flask import Response, jsonify, redirect, render_template, request, url_for
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
self.eid = str(base64.b64encode(os.urandom(32))[:8])
self.title = title
@ -26,85 +37,130 @@ class Events():
self.create()
def create(self):
log.info('START '+self.eid+': '+self.text)
app.socketio.emit('notify-create',
json.dumps({'id':self.eid,
'title':self.title,
'text':self.text,
'type':self.type}),
namespace='/sio',
room='admin')
log.info("START " + self.eid + ": " + self.text)
app.socketio.emit(
"notify-create",
json.dumps(
{
"id": self.eid,
"title": self.title,
"text": self.text,
"type": self.type,
}
),
namespace="/sio",
room="admin",
)
sleep(0.001)
def __del__(self):
log.info('END '+self.eid+': '+self.text)
app.socketio.emit('notify-destroy',
json.dumps({'id':self.eid}),
namespace='/sio',
room='admin')
log.info("END " + self.eid + ": " + self.text)
app.socketio.emit(
"notify-destroy",
json.dumps({"id": self.eid}),
namespace="/sio",
room="admin",
)
sleep(0.001)
def update_text(self, text):
self.text = text
app.socketio.emit('notify-update',
json.dumps({'id':self.eid,
'text':self.text,}),
namespace='/sio',
room='admin')
app.socketio.emit(
"notify-update",
json.dumps(
{
"id": self.eid,
"text": self.text,
}
),
namespace="/sio",
room="admin",
)
sleep(0.001)
def append_text(self, text):
self.text=self.text+'<br>'+text
app.socketio.emit('notify-update',
json.dumps({'id':self.eid,
'text':self.text,}),
namespace='/sio',
room='admin')
self.text = self.text + "<br>" + text
app.socketio.emit(
"notify-update",
json.dumps(
{
"id": self.eid,
"text": self.text,
}
),
namespace="/sio",
room="admin",
)
sleep(0.001)
def increment(self,data={'name':'','data':[]}):
def increment(self, data={"name": "", "data": []}):
self.item += 1
log.info('INCREMENT '+self.eid+': '+self.text)
app.socketio.emit('notify-increment',
json.dumps({'id':self.eid,
'title':self.title,
'text': '['+str(self.item)+'/'+str(self.total)+'] '+self.text+' '+data['name'],
'item':self.item,
'total':self.total,
'table':self.table,
'type':self.type,
'data':data}),
namespace='/sio',
room='admin')
log.info("INCREMENT " + self.eid + ": " + self.text)
app.socketio.emit(
"notify-increment",
json.dumps(
{
"id": self.eid,
"title": self.title,
"text": "["
+ str(self.item)
+ "/"
+ str(self.total)
+ "] "
+ 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)
def decrement(self,data={'name':'','data':[]}):
def decrement(self, data={"name": "", "data": []}):
self.item -= 1
log.info('DECREMENT '+self.eid+': '+self.text)
app.socketio.emit('notify-decrement',
json.dumps({'id':self.eid,
'title':self.title,
'text': '['+str(self.item)+'/'+str(self.total)+'] '+self.text+' '+data['name'],
'item':self.item,
'total':self.total,
'table':self.table,
'type':self.type,
'data':data}),
namespace='/sio',
room='admin')
log.info("DECREMENT " + self.eid + ": " + self.text)
app.socketio.emit(
"notify-decrement",
json.dumps(
{
"id": self.eid,
"title": self.title,
"text": "["
+ str(self.item)
+ "/"
+ str(self.total)
+ "] "
+ 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)
def reload(self):
app.socketio.emit('reload',
json.dumps({}),
namespace='/sio',
room='admin')
app.socketio.emit("reload", json.dumps({}), namespace="/sio", room="admin")
sleep(0.0001)
def table(self, event, table, data={}):
# refresh, add, delete, update
app.socketio.emit('table_'+event,
json.dumps({'table':table,'data':data}),
namespace='/sio',
room='admin')
app.socketio.emit(
"table_" + event,
json.dumps({"table": table, "data": data}),
namespace="/sio",
room="admin",
)
sleep(0.0001)

View File

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

View File

@ -1,55 +1,74 @@
import random, string
from pprint import pprint
import random
import string
from collections import Counter
from pprint import pprint
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):
return True if groupname in ['admin','manager','teacher','student'] else False
return True if groupname in ["admin", "manager", "teacher", "student"] else False
def get_group_from_group_id(group_id, groups):
return next((d for d in groups if d.get('id') == group_id), None)
return next((d for d in groups if d.get("id") == group_id), None)
def get_gid_from_kgroup_id(kgroup_id, groups):
# print(kgroup_id)
# pprint(groups)
# return get_group_from_group_id(kgroup_id,groups)['path'].replace('/','.')[1:]
return [g['path'].replace('/','.')[1:] for g in groups if g['id'] == kgroup_id][0]
return [g["path"].replace("/", ".")[1:] for g in groups if g["id"] == kgroup_id][0]
def get_gids_from_kgroup_ids(kgroup_ids, groups):
return [get_gid_from_kgroup_id(kgroup_id, groups) for kgroup_id in kgroup_ids]
def kpath2gid(path):
# print(path.replace('/','.')[1:])
return path.replace('/','.')[1:]
return path.replace("/", ".")[1:]
def gid2kpath(gid):
return '/'+gid.replace('.','/')
return "/" + gid.replace(".", "/")
def count_repeated(itemslist):
print(Counter(itemslist))
def groups_kname2gid(groups):
return [name.replace('.','/') for name in groups]
return [name.replace(".", "/") for name in 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):
return ['/'+g.replace('.','/') for g in groups]
return ["/" + g.replace(".", "/") for g in groups]
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]
def filter_roles_listofdicts(role_listofdicts):
client_roles=['admin','manager','teacher','student']
return [r for r in role_listofdicts if r['name'] in client_roles]
client_roles = ["admin", "manager", "teacher", "student"]
return [r for r in role_listofdicts if r["name"] in client_roles]
def rand_password(lenght):
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):
passwd = ''.join(random.choice(characters) for i in range(lenght))
passwd = "".join(random.choice(characters) for i in range(lenght))
return passwd

View File

@ -1,44 +1,58 @@
#!/usr/bin/env python
# coding=utf-8
import time ,os
from admin import app
from datetime import datetime, timedelta
import json
import logging as log
import os
import time
import traceback
import yaml, json
from datetime import datetime, timedelta
from pprint import pprint
import yaml
from jinja2 import Environment, FileSystemLoader
from keycloak import KeycloakAdmin
from admin import app
from .postgres import Postgres
class KeycloakClient():
class KeycloakClient:
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
https://github.com/marcospereirampj/python-keycloak
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
"""
def __init__(self,
def __init__(
self,
url="http://isard-sso-keycloak:8080/auth/",
username=os.environ['KEYCLOAK_USER'],
password=os.environ['KEYCLOAK_PASSWORD'],
realm='master',
verify=True):
username=os.environ["KEYCLOAK_USER"],
password=os.environ["KEYCLOAK_PASSWORD"],
realm="master",
verify=True,
):
self.url = url
self.username = username
self.password = password
self.realm = realm
self.verify = verify
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD'])
self.keycloak_pg = Postgres(
"isard-apps-postgresql",
"keycloak",
os.environ["KEYCLOAK_DB_USER"],
os.environ["KEYCLOAK_DB_PASSWORD"],
)
def connect(self):
self.keycloak_admin = KeycloakAdmin(server_url=self.url,
self.keycloak_admin = KeycloakAdmin(
server_url=self.url,
username=self.username,
password=self.password,
realm_name=self.realm,
verify=self.verify)
verify=self.verify,
)
# from keycloak import KeycloakAdmin
# keycloak_admin = KeycloakAdmin(server_url="http://isard-sso-keycloak:8080/auth/",username="admin",password="keycloakkeycloak",realm_name="master",verify=False)
@ -60,7 +74,7 @@ class KeycloakClient():
# self.add_role('superman')
# pprint(self.get_roles())
''' USERS '''
""" USERS """
def get_user_id(self, username):
self.connect()
@ -87,22 +101,29 @@ class KeycloakClient():
(headers, users) = self.keycloak_pg.select_with_headers(q)
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
users_with_lists = [
list(l[:-4])
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users
]
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
users_with_lists = [
list(l[:-4])
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users_with_lists
]
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
# self.connect()
# groups = self.keycloak_admin.get_groups()
# for user in list_dict_users:
# new_user_groups = []
# for group_id in user['group']:
@ -113,15 +134,13 @@ class KeycloakClient():
# user['group']=new_user_groups
return list_dict_users
def getparent(self, group_id, data):
# Recursively get full path from any group_id in the tree
path = ""
for item in data:
if group_id == item[0]:
path = self.getparent(item[2], data)
path = f'{path}/{item[1]}'
path = f"{path}/{item[1]}"
return path
def get_group_path(self, group_id):
@ -134,7 +153,9 @@ class KeycloakClient():
def get_user_groups_paths(self, user_id):
# Get full paths for user grups
# 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)
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'])]
# 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)
self.connect()
username = username.lower()
try:
uid=self.keycloak_admin.create_user({"email": email,
uid = self.keycloak_admin.create_user(
{
"email": email,
"username": username,
"enabled": enabled,
"firstName": first,
"lastName": last,
"credentials":[{"type":"password",
"value":password,
"temporary":temporary}]})
"credentials": [
{"type": "password", "value": password, "temporary": temporary}
],
}
)
except:
log.error(traceback.format_exc())
if group:
path = '/'+group if group[1:] != '/' else group
path = "/" + group if group[1:] != "/" else group
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:
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)
return uid
def update_user_pwd(self, user_id, password, temporary=True):
# Updates
payload={"credentials":[{"type":"password",
"value":password,
"temporary":temporary}]}
payload = {
"credentials": [
{"type": "password", "value": password, "temporary": temporary}
]
}
self.connect()
return self.keycloak_admin.update_user(user_id, payload)
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
# Updates
payload={"enabled":enabled,
payload = {
"enabled": enabled,
"email": email,
"firstName": first,
"lastName": last,
"groups": groups,
"realmRoles":roles}
"realmRoles": roles,
}
self.connect()
return self.keycloak_admin.update_user(user_id, payload)
@ -217,7 +258,11 @@ class KeycloakClient():
def remove_user_realm_roles(self, user_id, roles):
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)
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):
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
def get_all_groups(self):
@ -246,7 +293,7 @@ class KeycloakClient():
for d_group in l_groups:
d = {}
for key, value in d_group.items():
if key == 'subGroups':
if key == "subGroups":
self.get_recursive_groups(value, l)
else:
d[key] = value
@ -263,8 +310,8 @@ class KeycloakClient():
# This needs to be recursive function
if with_subgroups:
for group in groups:
if len(group['subGroups']):
for sg in group['subGroups']:
if len(group["subGroups"]):
for sg in group["subGroups"]:
subgroups.append(sg)
# for sgroup in subgroups:
# if len(sgroup['subGroups']):
@ -279,12 +326,14 @@ class KeycloakClient():
def get_group_by_path(self, path, recursive=True):
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):
self.connect()
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)
def delete_group(self, group_id):
@ -296,20 +345,20 @@ class KeycloakClient():
return self.keycloak_admin.group_user_add(user_id, group_id)
def add_group_tree(self, path):
parts=path.split('/')
parent_path='/'
parts = path.split("/")
parent_path = "/"
for i in range(1, len(parts)):
if i == 1:
try:
self.add_group(parts[i], None, skip_exists=True)
except:
log.warning('KEYCLOAK: Group :'+parts[i]+ ' already exists.')
log.warning("KEYCLOAK: Group :" + parts[i] + " already exists.")
parent_path = parent_path + parts[i]
else:
try:
self.add_group(parts[i], parent_path, skip_exists=True)
except:
log.warning('KEYCLOAK: Group :'+parts[i]+ ' already exists.')
log.warning("KEYCLOAK: Group :" + parts[i] + " already exists.")
parent_path = parent_path + parts[i]
# parts=path.split('/')
@ -339,48 +388,65 @@ class KeycloakClient():
# continue
# 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
uid = self.add_user(username, first, last, email, password)
## Add user to role
log.info('User uid: '+str(uid)+ ' role: '+str(role))
log.info("User uid: " + str(uid) + " role: " + str(role))
try:
therole = role[0]
except:
therole=''
therole = ""
log.info(self.assign_realm_roles(uid, role))
## Create groups in user
for g in groups:
log.warning('Creating keycloak group: '+g)
parts=g.split('/')
log.warning("Creating keycloak group: " + g)
parts = g.split("/")
parent_path = None
for i in range(1, len(parts)):
# parent_id=None if parent_path==None else self.get_group(parent_path)['id']
try:
self.add_group(parts[i], parent_path, skip_exists=True)
except:
log.warning('Group '+str(parent_path)+ ' already exists. Skipping creation')
log.warning(
"Group "
+ str(parent_path)
+ " already exists. Skipping creation"
)
pass
if parent_path is None:
thepath='/'+parts[i]
thepath = "/" + parts[i]
else:
thepath=parent_path+'/'+parts[i]
if thepath=='/':
log.warning('Not adding the user '+username+' to any group as does not have any...')
thepath = parent_path + "/" + parts[i]
if thepath == "/":
log.warning(
"Not adding the user "
+ username
+ " to any group as does not have any..."
)
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)
if parent_path==None: parent_path=''
parent_path=parent_path+'/'+parts[i]
if parent_path == None:
parent_path = ""
parent_path = parent_path + "/" + parts[i]
# self.group_user_add(uid,gid)
## ROLES
def get_roles(self):
self.connect()
@ -390,25 +456,27 @@ class KeycloakClient():
self.connect()
return self.keycloak_admin.get_realm_role(name)
def add_role(self,name,description=''):
def add_role(self, name, description=""):
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):
self.connect()
return self.keycloak_admin.delete_realm_role(name)
## CLIENTS
def get_client_roles(self, client_id):
self.connect()
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()
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
def get_server_info(self):
@ -421,14 +489,18 @@ class KeycloakClient():
def get_server_rsa_key(self):
self.connect()
rsa_key = [k for k in self.keycloak_admin.get_keys()['keys'] if k['type']=='RSA'][0]
return {'name':rsa_key['kid'],'certificate':rsa_key['certificate']}
rsa_key = [
k for k in self.keycloak_admin.get_keys()["keys"] if k["type"] == "RSA"
][0]
return {"name": rsa_key["kid"], "certificate": rsa_key["certificate"]}
## REALM
def assign_realm_roles(self, user_id, role):
self.connect()
try:
role=[r for r in self.keycloak_admin.get_realm_roles() if r['name']==role]
role = [
r for r in self.keycloak_admin.get_realm_roles() if r["name"] == role
]
except:
return False
return self.keycloak_admin.assign_realm_roles(user_id=user_id, roles=role)

View File

@ -1,24 +1,39 @@
#!/usr/bin/env python
# coding=utf-8
from admin import app
import os, sys
import logging as log
import os
import sys
import traceback
class loadConfig():
from admin import app
class loadConfig:
def __init__(self, app=None):
try:
app.config.setdefault('DOMAIN', os.environ['DOMAIN'])
app.config.setdefault('KEYCLOAK_POSTGRES_USER', os.environ['KEYCLOAK_DB_USER'])
app.config.setdefault('KEYCLOAK_POSTGRES_PASSWORD', os.environ['KEYCLOAK_DB_PASSWORD'])
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)
app.config.setdefault("DOMAIN", os.environ["DOMAIN"])
app.config.setdefault(
"KEYCLOAK_POSTGRES_USER", os.environ["KEYCLOAK_DB_USER"]
)
app.config.setdefault(
"KEYCLOAK_POSTGRES_PASSWORD", os.environ["KEYCLOAK_DB_PASSWORD"]
)
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:
log.error(traceback.format_exc())
raise

View File

@ -1,35 +1,43 @@
import logging as log
import traceback
from pprint import pprint
from requests import get, post
from admin import app
import logging as log
from pprint import pprint
import traceback
from .postgres import Postgres
from .exceptions import UserExists, UserNotFound
from .postgres import Postgres
# Module variables to connect to moodle api
class Moodle():
class Moodle:
"""https://github.com/mrcinv/moodle_api.py
https://docs.moodle.org/dev/Web_service_API_functions
https://docs.moodle.org/311/en/Using_web_services
"""
def __init__(self,
def __init__(
self,
key=app.config["MOODLE_WS_TOKEN"],
url="https://moodle." + app.config["DOMAIN"],
endpoint="/webservice/rest/server.php",
verify=app.config["VERIFY"]):
verify=app.config["VERIFY"],
):
self.key = key
self.url = url
self.endpoint = endpoint
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
defining the structure.
Example usage:
@ -42,10 +50,10 @@ class Moodle():
if not type(in_args) in (list, dict):
out_dict[prefix] = in_args
return out_dict
if prefix == '':
prefix = prefix + '{0}'
if prefix == "":
prefix = prefix + "{0}"
else:
prefix = prefix + '[{0}]'
prefix = prefix + "[{0}]"
if type(in_args) == list:
for idx, item in enumerate(in_args):
self.rest_api_parameters(item, prefix.format(idx), out_dict)
@ -61,55 +69,70 @@ class Moodle():
courses = [{'id': 1, 'fullname': 'My favorite course'}])
"""
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 = response.json()
if type(response) == dict and response.get('exception'):
if type(response) == dict and response.get("exception"):
raise SystemError(response)
return response
def create_user(self, email, username, password, first_name='-', last_name='-'):
if len(self.get_user_by('username',username)['users']):
def create_user(self, email, username, password, first_name="-", last_name="-"):
if len(self.get_user_by("username", username)["users"]):
raise UserExists
try:
data = [{'username': username, 'email':email,
'password': password, 'firstname':first_name, 'lastname':last_name}]
user = self.call('core_user_create_users', users=data)
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'}]
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):
user = self.get_user_by('username',username)['users'][0]
user = self.get_user_by("username", username)["users"][0]
if not len(user):
raise UserNotFound
try:
data = [{'id':user['id'],'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)
data = [
{
"id": user["id"],
"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
except SystemError as se:
raise SystemError(se.args[0]['message'])
raise SystemError(se.args[0]["message"])
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
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
def get_user_by(self, key, value):
criteria = [{'key': key, 'value': value}]
criteria = [{"key": key, "value": value}]
try:
user = self.call('core_user_get_users', criteria=criteria)
user = self.call("core_user_get_users", criteria=criteria)
except:
raise SystemError("Error calling Moodle API\n", traceback.format_exc())
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': []}
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
from mdl_user as u
@ -120,7 +143,12 @@ class Moodle():
where u.deleted = 0
group by u.id , username, first, last, email"""
(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]
return list_dict_users
@ -134,26 +162,32 @@ class Moodle():
def enroll_user_to_course(self, user_id, course_id, role_id=5):
# 5 is student
data = [{'roleid': role_id, 'userid': user_id, 'courseid': course_id}]
enrolment = self.call('enrol_manual_enrol_users', enrolments=data)
data = [{"roleid": role_id, "userid": user_id, "courseid": course_id}]
enrolment = self.call("enrol_manual_enrol_users", enrolments=data)
return enrolment
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
def get_cohorts(self):
cohorts = self.call('core_cohort_get_cohorts')
cohorts = self.call("core_cohort_get_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
data = [{'categorytype': {'type': 'system', 'value': ''},
'name': name,
'idnumber': name,
'description': description,
'visible': visible}]
cohort = self.call('core_cohort_create_cohorts', cohorts=data)
data = [
{
"categorytype": {"type": "system", "value": ""},
"name": name,
"idnumber": name,
"description": description,
"visible": visible,
}
]
cohort = self.call("core_cohort_create_cohorts", cohorts=data)
return cohort
# def add_users_to_cohort(self,users,cohort):
@ -162,41 +196,49 @@ class Moodle():
# return user
def add_user_to_cohort(self, userid, cohortid):
members=[{'cohorttype':{'type':'id','value':cohortid},
'usertype':{'type':'id','value':userid}}]
user = self.call('core_cohort_add_cohort_members', 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
def delete_user_in_cohort(self, userid, cohortid):
members=[{'cohortid':cohortid,
'userid':userid}]
user = self.call('core_cohort_delete_cohort_members', members=members)
members = [{"cohortid": cohortid, "userid": userid}]
user = self.call("core_cohort_delete_cohort_members", members=members)
return user
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']
return members
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
def get_user_cohorts(self, user_id):
user_cohorts = []
cohorts = self.get_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
def add_user_to_siteadmin(self, user_id):
q = """SELECT value FROM mdl_config WHERE name='siteadmins'"""
value = self.moodle_pg.select(q)[0][0]
if str(user_id) not in value:
value=value+','+str(user_id)
q = """UPDATE mdl_config SET value = '%s' WHERE name='siteadmins'""" % (value)
value = value + "," + str(user_id)
q = """UPDATE mdl_config SET value = '%s' WHERE name='siteadmins'""" % (
value
)
self.moodle_pg.update(q)
log.warning('MOODLE:ADDING THE USER TO ADMINS: This needs a purge cache in moodle!')
log.warning(
"MOODLE:ADDING THE USER TO ADMINS: This needs a purge cache in moodle!"
)
# def add_role_to_user(self, user_id, role='admin', context='missing'):
# if role=='admin':
@ -208,6 +250,7 @@ class Moodle():
# userid=user_id, role_id=role_id)
# 'contextlevel': 1,
# define('CONTEXT_SYSTEM', 10);
# define('CONTEXT_USER', 30);
# define('CONTEXT_COURSECAT', 40);

View File

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

View File

@ -1,38 +1,63 @@
#!/usr/bin/env python
# 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 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
class Nextcloud():
def __init__(self,
url="https://nextcloud."+app.config['DOMAIN'],
username=os.environ['NEXTCLOUD_ADMIN_USER'],
password=os.environ['NEXTCLOUD_ADMIN_PASSWORD'],
verify=True):
class Nextcloud:
def __init__(
self,
url="https://nextcloud." + app.config["DOMAIN"],
username=os.environ["NEXTCLOUD_ADMIN_USER"],
password=os.environ["NEXTCLOUD_ADMIN_PASSWORD"],
verify=True,
):
self.verify_cert = verify
self.apiurl=url+'/ocs/v1.php/cloud/'
self.shareurl=url+'/ocs/v2.php/apps/files_sharing/api/v1/'
self.davurl=url+'/remote.php/dav/files/'
self.apiurl = url + "/ocs/v1.php/cloud/"
self.shareurl = url + "/ocs/v2.php/apps/files_sharing/api/v1/"
self.davurl = url + "/remote.php/dav/files/"
self.auth = (username, password)
self.user = username
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):
if auth == False: auth=self.auth
def _request(
self, method, url, data={}, headers={"OCS-APIRequest": "true"}, auth=False
):
if auth == False:
auth = self.auth
try:
response = requests.request(method, 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
response = requests.request(
method,
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
return response.text
@ -48,15 +73,16 @@ class Nextcloud():
# except requests.exceptions.RequestException as err:
# raise ProviderError
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 ProviderError
def check_connection(self):
url = self.apiurl + "users/" + self.user + "?format=json"
try:
result = self._request('GET',url)
if json.loads(result)['ocs']['meta']['statuscode'] == 100: return True
result = self._request("GET", url)
if json.loads(result)["ocs"]["meta"]["statuscode"] == 100:
return True
raise ProviderError
except requests.exceptions.HTTPError as errh:
raise ProviderConnError
@ -69,15 +95,16 @@ class Nextcloud():
except requests.exceptions.RequestException as err:
raise ProviderError
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 ProviderError
def get_user(self, userid):
url = self.apiurl + "users/" + userid + "?format=json"
try:
result = json.loads(self._request('GET',url))
if result['ocs']['meta']['statuscode'] == 100: return result['ocs']['data']
result = json.loads(self._request("GET", url))
if result["ocs"]["meta"]["statuscode"] == 100:
return result["ocs"]["data"]
raise ProviderItemNotExists
except:
log.error(traceback.format_exc())
@ -99,7 +126,6 @@ class Nextcloud():
# cur.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_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
group by u.uid, adn.value, ade.value, pref.configvalue"""
(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 = [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
]
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]
return list_dict_users
@ -145,27 +181,40 @@ class Nextcloud():
# log.error(traceback.format_exc())
# raise
def add_user(self,userid,userpassword,quota=False,group=False,email='',displayname=''):
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']
def add_user(
self, userid, userpassword, quota=False, group=False, email="", displayname=""
):
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:
# data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname}
# else:
# data={'userid':userid,'password':userpassword,'quota':quota,'email':email,'displayname':displayname}
url = self.apiurl + "users?format=json"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
"Content-Type": "application/x-www-form-urlencoded",
"OCS-APIRequest": "true",
}
try:
result = json.loads(self._request('POST',url,data=data,headers=headers))
if result['ocs']['meta']['statuscode'] == 100: return True
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists
if result['ocs']['meta']['statuscode'] == 104:
result = json.loads(self._request("POST", url, data=data, headers=headers))
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)
# raise ProviderGroupNotExists
log.error('Get Nextcloud provider user add error: '+str(result))
log.error("Get Nextcloud provider user add error: " + str(result))
raise ProviderOpError
except:
log.error(traceback.format_exc())
@ -184,85 +233,109 @@ class Nextcloud():
url = self.apiurl + "users/" + userid + "?format=json"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
"Content-Type": "application/x-www-form-urlencoded",
"OCS-APIRequest": "true",
}
for k, v in key_values.items():
data = {"key": k, "value": v}
try:
result = json.loads(self._request('PUT',url,data=data,headers=headers))
if result['ocs']['meta']['statuscode'] == 100: 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))
result = json.loads(
self._request("PUT", url, data=data, headers=headers)
)
if result["ocs"]["meta"]["statuscode"] == 100:
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
except:
log.error(traceback.format_exc())
raise
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"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
"Content-Type": "application/x-www-form-urlencoded",
"OCS-APIRequest": "true",
}
try:
result = json.loads(self._request('POST',url,data=data,headers=headers))
if result['ocs']['meta']['statuscode'] == 100: 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))
result = json.loads(self._request("POST", url, data=data, headers=headers))
if result["ocs"]["meta"]["statuscode"] == 100:
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
except:
log.error(traceback.format_exc())
raise
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"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
"Content-Type": "application/x-www-form-urlencoded",
"OCS-APIRequest": "true",
}
try:
result = json.loads(self._request('DELETE',url,data=data,headers=headers))
if result['ocs']['meta']['statuscode'] == 100: return True
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists
if result['ocs']['meta']['statuscode'] == 104:
result = json.loads(
self._request("DELETE", url, data=data, headers=headers)
)
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)
# raise ProviderGroupNotExists
log.error('Get Nextcloud provider user add error: '+str(result))
log.error("Get Nextcloud provider user add error: " + str(result))
raise ProviderOpError
except:
log.error(traceback.format_exc())
raise
def add_user_with_groups(self,userid,userpassword,quota=False,groups=[],email='',displayname=''):
data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':groups,'email':email,'displayname':displayname}
def add_user_with_groups(
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 quota: del data['quota']
if not quota:
del data["quota"]
# if group:
# data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname}
# else:
# data={'userid':userid,'password':userpassword,'quota':quota,'email':email,'displayname':displayname}
url = self.apiurl + "users?format=json"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
"Content-Type": "application/x-www-form-urlencoded",
"OCS-APIRequest": "true",
}
try:
result = json.loads(self._request('POST',url,data=data,headers=headers))
if result['ocs']['meta']['statuscode'] == 100: return True
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists
if result['ocs']['meta']['statuscode'] == 104:
result = json.loads(self._request("POST", url, data=data, headers=headers))
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)
None
# raise ProviderGroupNotExists
log.error('Get Nextcloud provider user add error: '+str(result))
log.error("Get Nextcloud provider user add error: " + str(result))
raise ProviderOpError
except:
log.error(traceback.format_exc())
@ -279,9 +352,11 @@ class Nextcloud():
def delete_user(self, userid):
url = self.apiurl + "users/" + userid + "?format=json"
try:
result = json.loads(self._request('DELETE',url))
if result['ocs']['meta']['statuscode'] == 100: return True
if result['ocs']['meta']['statuscode'] == 101: raise ProviderUserNotExists
result = json.loads(self._request("DELETE", url))
if result["ocs"]["meta"]["statuscode"] == 100:
return True
if result["ocs"]["meta"]["statuscode"] == 101:
raise ProviderUserNotExists
log.error(traceback.format_exc())
raise ProviderOpError
except:
@ -296,74 +371,88 @@ class Nextcloud():
def disable_user(self, userid):
None
def exists_user_folder(self,userid,userpassword,folder='IsardVDI'):
def exists_user_folder(self, userid, userpassword, folder="IsardVDI"):
auth = (userid, userpassword)
url = self.davurl + userid + "/" + folder + "?format=json"
headers = {
'Depth': '0',
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
"Depth": "0",
"Content-Type": "application/x-www-form-urlencoded",
"OCS-APIRequest": "true",
}
try:
result = self._request('PROPFIND',url,auth=auth,headers=headers)
if '<d:status>HTTP/1.1 200 OK</d:status>' in result: return True
result = self._request("PROPFIND", url, auth=auth, headers=headers)
if "<d:status>HTTP/1.1 200 OK</d:status>" in result:
return True
return False
except:
log.error(traceback.format_exc())
raise
def add_user_folder(self,userid,userpassword,folder='IsardVDI'):
def add_user_folder(self, userid, userpassword, folder="IsardVDI"):
auth = (userid, userpassword)
url = self.davurl + userid + "/" + folder + "?format=json"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
"Content-Type": "application/x-www-form-urlencoded",
"OCS-APIRequest": "true",
}
try:
result = self._request('MKCOL',url,auth=auth,headers=headers)
if result=='': return True
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])
result = self._request("MKCOL", url, auth=auth, headers=headers)
if result == "":
return True
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
except:
log.error(traceback.format_exc())
raise
def exists_user_share_folder(self,userid,userpassword,folder='IsardVDI'):
def exists_user_share_folder(self, userid, userpassword, folder="IsardVDI"):
auth = (userid, userpassword)
url = self.shareurl + "shares?format=json"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
"Content-Type": "application/x-www-form-urlencoded",
"OCS-APIRequest": "true",
}
try:
result = json.loads(self._request('GET', url, auth=auth, headers=headers))
if result['ocs']['meta']['statuscode']==200:
share=[s for s in result['ocs']['data'] if s['path'] == '/'+folder]
result = json.loads(self._request("GET", url, auth=auth, headers=headers))
if result["ocs"]["meta"]["statuscode"] == 200:
share = [s for s in result["ocs"]["data"] if s["path"] == "/" + folder]
if len(share) >= 1:
# Should we delete all but the first (0) one?
return {'token': share[0]['token'],
'url': share[0]['url']}
return {"token": share[0]["token"], "url": share[0]["url"]}
raise ProviderItemNotExists
raise ProviderOpError
except:
log.error(traceback.format_exc())
raise
def add_user_share_folder(self,userid,userpassword,folder='IsardVDI'):
def add_user_share_folder(self, userid, userpassword, folder="IsardVDI"):
auth = (userid, userpassword)
data={'path':'/'+folder,'shareType':3}
data = {"path": "/" + folder, "shareType": 3}
url = self.shareurl + "shares?format=json"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
"Content-Type": "application/x-www-form-urlencoded",
"OCS-APIRequest": "true",
}
try:
result = json.loads(self._request('POST',url, data=data, auth=auth, headers=headers))
if 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'])
result = json.loads(
self._request("POST", url, data=data, auth=auth, headers=headers)
)
if (
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
except:
log.error(traceback.format_exc())
@ -375,24 +464,29 @@ class Nextcloud():
def get_groups_list(self):
url = self.apiurl + "groups?format=json"
try:
result = json.loads(self._request('GET',url))
if result['ocs']['meta']['statuscode'] == 100: return [g for g in result['ocs']['data']['groups']]
result = json.loads(self._request("GET", url))
if result["ocs"]["meta"]["statuscode"] == 100:
return [g for g in result["ocs"]["data"]["groups"]]
raise ProviderOpError
except:
log.error(traceback.format_exc())
raise
def add_group(self, groupid):
data={'groupid':groupid}
data = {"groupid": groupid}
url = self.apiurl + "groups?format=json"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
"Content-Type": "application/x-www-form-urlencoded",
"OCS-APIRequest": "true",
}
try:
result = json.loads(self._request('POST',url, data=data, auth=self.auth, headers=headers))
if result['ocs']['meta']['statuscode'] == 100: return True
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists
result = json.loads(
self._request("POST", url, data=data, auth=self.auth, headers=headers)
)
if result["ocs"]["meta"]["statuscode"] == 100:
return True
if result["ocs"]["meta"]["statuscode"] == 102:
raise ProviderItemExists
raise ProviderOpError
except:
log.error(traceback.format_exc())
@ -403,15 +497,18 @@ class Nextcloud():
# 103 - failed to add the group
def delete_group(self, groupid):
group = urllib.parse.quote(groupid, safe='')
group = urllib.parse.quote(groupid, safe="")
url = self.apiurl + "groups/" + group + "?format=json"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
"Content-Type": "application/x-www-form-urlencoded",
"OCS-APIRequest": "true",
}
try:
result = json.loads(self._request('DELETE',url, auth=self.auth, headers=headers))
if result['ocs']['meta']['statuscode'] == 100: return True
result = json.loads(
self._request("DELETE", url, auth=self.auth, headers=headers)
)
if result["ocs"]["meta"]["statuscode"] == 100:
return True
log.error(traceback.format_exc())
raise ProviderOpError
except:
@ -421,4 +518,3 @@ class Nextcloud():
# 101 - invalid input data
# 102 - group already exists
# 103 - failed to add the group

View File

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

View File

@ -1,24 +1,22 @@
#!/usr/bin/env python
# coding=utf-8
import json
import logging as log
import time
from admin import app
import traceback
from datetime import datetime, timedelta
import logging as log
import traceback
import yaml, json
import psycopg2
import yaml
class Postgres():
from admin import app
class Postgres:
def __init__(self, host, database, user, password):
self.conn = psycopg2.connect(
host=host,
database=database,
user=user,
password=password)
host=host, database=database, user=user, password=password
)
# def __del__(self):
# self.cur.close()

View File

@ -1,74 +1,111 @@
#!/usr/bin/env python
# coding=utf-8
import time, os
from admin import app
from datetime import datetime, timedelta
import json
import logging as log
import traceback
import yaml, json
import os
import random
import psycopg2
from .postgres import Postgres
# from .keycloak import Keycloak
# 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):
ready = False
while not ready:
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
except:
log.warning('Could not connect to moodle database. Retrying...')
log.warning("Could not connect to moodle database. Retrying...")
time.sleep(2)
log.info('Connected to moodle database.')
log.info("Connected to moodle database.")
ready = False
while not ready:
try:
with open(os.path.join(app.root_path, "../moodledata/saml2/moodle."+app.config['DOMAIN']+".crt"),"r") as crt:
app.config.setdefault('SP_CRT', crt.read())
with open(
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
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)
except:
log.error(traceback.format_exc())
log.info('Got moodle srt certificate.')
log.info("Got moodle srt certificate.")
ready = False
while not ready:
try:
with open(os.path.join(app.root_path, "../moodledata/saml2/moodle."+app.config['DOMAIN']+".pem"),"r") as pem:
app.config.setdefault('SP_PEM', pem.read())
with open(
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
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)
log.info('Got moodle pem certificate.')
log.info("Got moodle pem certificate.")
self.select_and_configure_theme()
self.configure_tipnc()
self.add_moodle_ws_token()
def select_and_configure_theme(self,theme='cbe'):
def select_and_configure_theme(self, theme="cbe"):
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:
log.error(traceback.format_exc())
exit(1)
None
try:
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'theme_cbe' AND "name" = 'host';""" % (os.environ['DOMAIN']))
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';""")
self.pg.update(
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'theme_cbe' AND "name" = 'host';"""
% (os.environ["DOMAIN"])
)
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:
log.error(traceback.format_exc())
exit(1)
@ -76,12 +113,27 @@ class Postup():
def configure_tipnc(self):
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("""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';""")
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(
"""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:
log.error(traceback.format_exc())
exit(1)
@ -89,18 +141,23 @@ class Postup():
def add_moodle_ws_token(self):
try:
token=self.pg.select("""SELECT * FROM "mdl_external_tokens" WHERE "externalserviceid" = 3""")[0][1]
app.config.setdefault('MOODLE_WS_TOKEN',token)
token = self.pg.select(
"""SELECT * FROM "mdl_external_tokens" WHERE "externalserviceid" = 3"""
)[0][1]
app.config.setdefault("MOODLE_WS_TOKEN", token)
return
except:
# log.error(traceback.format_exc())
None
try:
self.pg.update("""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" ("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_user_get_users'),
(3, 'core_user_get_users_by_field'),
@ -116,17 +173,37 @@ class Postup():
(3, 'core_cohort_search_cohorts'),
(3, 'core_cohort_update_cohorts'),
(3, 'core_role_assign_roles'),
(3, 'core_cohort_get_cohorts');""")
(3, 'core_cohort_get_cohorts');"""
)
self.pg.update("""INSERT INTO "mdl_external_services_users" ("externalserviceid", "userid", "iprestriction", "validuntil", "timecreated") VALUES
(3, 2, NULL, NULL, 1621719871);""")
self.pg.update(
"""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))
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))
b32 = "".join(
random.choices(
string.ascii_uppercase
+ 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:
log.error(traceback.format_exc())
exit(1)

View File

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

View File

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

View File

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

View File

@ -1,23 +1,34 @@
#!flask/bin/python
# 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
from flask_login import login_required
from .decorators import is_admin
import json
import logging as log
import os
import sys
import time
import traceback
from pprint import pprint
from uuid import uuid4
from flask import (
Response,
jsonify,
redirect,
render_template,
request,
send_file,
url_for,
)
from flask_login import login_required
from admin import app
from ..lib.avatars import Avatars
from .decorators import is_admin
avatars = Avatars()
''' OIDC TESTS '''
""" OIDC TESTS """
# from ..auth.authentication import oidc
# @app.route('/custom_callback')
@ -43,47 +54,60 @@ avatars=Avatars()
# def logoutoidc():
# oidc.logout()
# return 'Hi, you have been logged out! <a href="/">Return</a>'
''' OIDC TESTS '''
""" OIDC TESTS """
@app.route('/users')
@app.route("/users")
@login_required
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
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
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
def avatar(userid):
if userid != 'false':
return send_file('../avatars/master-avatars/'+userid, mimetype='image/jpeg')
return send_file('static/img/missing.jpg', mimetype='image/jpeg')
if userid != "false":
return send_file("../avatars/master-avatars/" + userid, mimetype="image/jpeg")
return send_file("static/img/missing.jpg", mimetype="image/jpeg")
### SYS ADMIN
@app.route('/sysadmin/users')
@app.route("/sysadmin/users")
@login_required
@is_admin
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
@is_admin
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
## SysAdmin role
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
# coding=utf-8
from functools import wraps
from flask import request, redirect, url_for
from flask_login import current_user, logout_user
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):
@wraps(fn)
def decorated_view(*args, **kwargs):
if current_user.role == 'admin': return fn(*args, **kwargs)
return redirect(url_for('login'))
if current_user.role == "admin":
return fn(*args, **kwargs)
return redirect(url_for("login"))
return decorated_view
def is_internal(fn):
@wraps(fn)
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,
## 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()
return redirect(url_for('login'))
return redirect(url_for("login"))
return decorated_view

View File

@ -1,72 +1,89 @@
#!/usr/bin/env python
# coding=utf-8
import time, os
from datetime import datetime, timedelta
import pprint
import json
import logging as log
import os
import pprint
import random
import string
import time
import traceback
import yaml, json
from datetime import datetime, timedelta
import psycopg2
import yaml
from admin.lib.postgres import Postgres
from admin.lib.keycloak_client import KeycloakClient
import string, random
from admin.lib.postgres import Postgres
app = {}
app['config']={}
app["config"] = {}
class MoodleSaml():
class MoodleSaml:
def __init__(self):
ready = False
while not ready:
try:
self.pg=Postgres('isard-apps-postgresql','moodle',os.environ['MOODLE_POSTGRES_USER'],os.environ['MOODLE_POSTGRES_PASSWORD'])
self.pg = Postgres(
"isard-apps-postgresql",
"moodle",
os.environ["MOODLE_POSTGRES_USER"],
os.environ["MOODLE_POSTGRES_PASSWORD"],
)
ready = True
except:
log.warning('Could not connect to moodle database. Retrying...')
log.warning("Could not connect to moodle database. Retrying...")
time.sleep(2)
log.info('Connected to moodle database.')
log.info("Connected to moodle database.")
ready = False
while not ready:
try:
privatekey_pass = self.get_privatekey_pass()
log.warning("The key: " + str(privatekey_pass))
if privatekey_pass.endswith(os.environ['DOMAIN']):
app['config']['MOODLE_SAML_PRIVATEKEYPASS']=privatekey_pass
if privatekey_pass.endswith(os.environ["DOMAIN"]):
app["config"]["MOODLE_SAML_PRIVATEKEYPASS"] = privatekey_pass
ready = True
except:
# print(traceback.format_exc())
log.warning('Could not get moodle site identifier. Retrying...')
log.warning("Could not get moodle site identifier. Retrying...")
time.sleep(2)
log.info('Got moodle site identifier.')
log.info("Got moodle site identifier.")
ready = False
while not ready:
try:
with open(os.path.join("./moodledata/saml2/moodle."+os.environ['DOMAIN']+".crt"),"r") as crt:
app['config']['SP_CRT']=crt.read()
with open(
os.path.join(
"./moodledata/saml2/moodle." + os.environ["DOMAIN"] + ".crt"
),
"r",
) as crt:
app["config"]["SP_CRT"] = crt.read()
ready = True
except IOError:
log.warning('Could not get moodle SAML2 crt certificate. Retrying...')
log.warning("Could not get moodle SAML2 crt certificate. Retrying...")
time.sleep(2)
except:
log.error(traceback.format_exc())
log.info('Got moodle srt certificate.')
log.info("Got moodle srt certificate.")
ready = False
while not ready:
try:
with open(os.path.join("./moodledata/saml2/moodle."+os.environ['DOMAIN']+".pem"),"r") as pem:
app['config']['SP_PEM']=pem.read()
with open(
os.path.join(
"./moodledata/saml2/moodle." + os.environ["DOMAIN"] + ".pem"
),
"r",
) as pem:
app["config"]["SP_PEM"] = pem.read()
ready = True
except IOError:
log.warning('Could not get moodle SAML2 pem certificate. Retrying...')
log.warning("Could not get moodle SAML2 pem certificate. Retrying...")
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.
## 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)
# with open(os.path.join("./moodledata/saml2/"+os.environ['MOODLE_SAML_PRIVATEKEYPASS'].replace("moodle."+os.environ['DOMAIN'],'')+'.idp.xml'),"w") as xml:
# xml.write(self.parse_idp_metadata())
with open(os.path.join("./moodledata/saml2/0f635d0e0f3874fff8b581c132e6c7a7.idp.xml"),"w") as xml:
with open(
os.path.join("./moodledata/saml2/0f635d0e0f3874fff8b581c132e6c7a7.idp.xml"),
"w",
) as xml:
xml.write(self.parse_idp_metadata())
log.info('Written SP file on moodledata.')
log.info("Written SP file on moodledata.")
try:
self.activate_saml_plugin()
except:
print('Error activating saml on moodle')
print("Error activating saml on moodle")
try:
self.set_moodle_saml_plugin()
except:
print('Error setting saml on moodle')
print("Error setting saml on moodle")
try:
self.delete_keycloak_moodle_saml_plugin()
except:
print('Error deleting saml on keycloak')
print("Error deleting saml on keycloak")
try:
self.add_keycloak_moodle_saml()
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
# self.add_client_roles()
def activate_saml_plugin(self):
## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php
return self.pg.update("""UPDATE "mdl_config" SET value = 'email,saml2' WHERE "name" = 'auth'""")
return self.pg.update(
"""UPDATE "mdl_config" SET value = 'email,saml2' WHERE "name" = 'auth'"""
)
def get_privatekey_pass(self):
return self.pg.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2]
return self.pg.select(
"""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'"""
)[0][2]
def parse_idp_metadata(self):
keycloak = KeycloakClient()
rsa = keycloak.get_server_rsa_key()
keycloak = None
return '<md:EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Name="urn:keycloak"><md:EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master"><md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor use="signing"><ds:KeyInfo><ds:KeyName>'+rsa['name']+'</ds:KeyName><ds:X509Data><ds:X509Certificate>'+rsa['certificate']+'</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml/resolve" index="0"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/></md:IDPSSODescriptor></md:EntityDescriptor></md:EntitiesDescriptor>'
return (
'<md:EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Name="urn:keycloak"><md:EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://sso.'
+ os.environ["DOMAIN"]
+ '/auth/realms/master"><md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor use="signing"><ds:KeyInfo><ds:KeyName>'
+ rsa["name"]
+ "</ds:KeyName><ds:X509Data><ds:X509Certificate>"
+ rsa["certificate"]
+ '</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.'
+ os.environ["DOMAIN"]
+ '/auth/realms/master/protocol/saml/resolve" index="0"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.'
+ os.environ["DOMAIN"]
+ '/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.'
+ os.environ["DOMAIN"]
+ '/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.'
+ os.environ["DOMAIN"]
+ '/auth/realms/master/protocol/saml"/><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.'
+ os.environ["DOMAIN"]
+ '/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.'
+ os.environ["DOMAIN"]
+ '/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.'
+ os.environ["DOMAIN"]
+ '/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.'
+ os.environ["DOMAIN"]
+ '/auth/realms/master/protocol/saml"/></md:IDPSSODescriptor></md:EntityDescriptor></md:EntitiesDescriptor>'
)
def set_keycloak_moodle_saml_plugin(self):
keycloak = KeycloakClient()
@ -125,39 +173,55 @@ class MoodleSaml():
def delete_keycloak_moodle_saml_plugin(self):
keycloak = KeycloakClient()
keycloak.delete_client('a92d5417-92b6-4678-9cb9-51bc0edcee8c')
keycloak.delete_client("a92d5417-92b6-4678-9cb9-51bc0edcee8c")
keycloak = None
def set_moodle_saml_plugin(self):
config={'idpmetadata': self.parse_idp_metadata(),
'certs_locked': '1',
'duallogin': '1',
'idpattr': 'username',
'autocreate': '1',
'anyauth': '1',
'saml_role_siteadmin_map': 'admin',
'saml_role_coursecreator_map': 'teacher',
'saml_role_manager_map': 'manager',
'field_map_email': 'email',
'field_map_firstname': 'givenName',
'field_map_lastname': 'sn'}
config = {
"idpmetadata": self.parse_idp_metadata(),
"certs_locked": "1",
"duallogin": "1",
"idpattr": "username",
"autocreate": "1",
"anyauth": "1",
"saml_role_siteadmin_map": "admin",
"saml_role_coursecreator_map": "teacher",
"saml_role_manager_map": "manager",
"field_map_email": "email",
"field_map_firstname": "givenName",
"field_map_lastname": "sn",
}
for name in config.keys():
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'auth_saml2' AND "name" = '%s'""" % (config[name],name))
self.pg.update("""INSERT INTO "mdl_auth_saml2_idps" ("metadataurl", "entityid", "activeidp", "defaultidp", "adminidp", "defaultname", "displayname", "logo", "alias", "whitelist") VALUES
('xml', 'https://sso.%s/auth/realms/master', 1, 0, 0, 'Login via SAML2', '', NULL, NULL, NULL);""" % (os.environ['DOMAIN']))
self.pg.update(
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'auth_saml2' AND "name" = '%s'"""
% (config[name], name)
)
self.pg.update(
"""INSERT INTO "mdl_auth_saml2_idps" ("metadataurl", "entityid", "activeidp", "defaultidp", "adminidp", "defaultname", "displayname", "logo", "alias", "whitelist") VALUES
('xml', 'https://sso.%s/auth/realms/master', 1, 0, 0, 'Login via SAML2', '', NULL, NULL, NULL);"""
% (os.environ["DOMAIN"])
)
def add_keycloak_moodle_saml(self):
client = {
"id": "a92d5417-92b6-4678-9cb9-51bc0edcee8c",
"name": "moodle",
"description": "moodle",
"clientId" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/metadata.php",
"clientId": "https://moodle."
+ os.environ["DOMAIN"]
+ "/auth/saml2/sp/metadata.php",
"surrogateAuthRequired": False,
"enabled": True,
"alwaysDisplayInConsole": False,
"clientAuthenticatorType": "client-secret",
"redirectUris" : [ "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+os.environ['DOMAIN']+"" ],
"webOrigins" : [ "https://moodle."+os.environ['DOMAIN']+"" ],
"redirectUris": [
"https://moodle."
+ os.environ["DOMAIN"]
+ "/auth/saml2/sp/saml2-acs.php/moodle."
+ os.environ["DOMAIN"]
+ ""
],
"webOrigins": ["https://moodle." + os.environ["DOMAIN"] + ""],
"notBefore": 0,
"bearerOnly": False,
"consentRequired": False,
@ -171,23 +235,32 @@ class MoodleSaml():
"attributes": {
"saml.force.post.binding": True,
"saml.encrypt": False,
"saml_assertion_consumer_url_post" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+os.environ['DOMAIN']+"",
"saml_assertion_consumer_url_post": "https://moodle."
+ os.environ["DOMAIN"]
+ "/auth/saml2/sp/saml2-acs.php/moodle."
+ os.environ["DOMAIN"]
+ "",
"saml.server.signature": True,
"saml.server.signature.keyinfo.ext": False,
"saml.signing.certificate" : app['config']['SP_CRT'],
"saml_single_logout_service_url_redirect" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-logout.php/moodle."+os.environ['DOMAIN']+"",
"saml.signing.certificate": app["config"]["SP_CRT"],
"saml_single_logout_service_url_redirect": "https://moodle."
+ os.environ["DOMAIN"]
+ "/auth/saml2/sp/saml2-logout.php/moodle."
+ os.environ["DOMAIN"]
+ "",
"saml.signature.algorithm": "RSA_SHA256",
"saml_force_name_id_format": False,
"saml.client.signature": True,
"saml.encryption.certificate" : app['config']['SP_PEM'],
"saml.encryption.certificate": app["config"]["SP_PEM"],
"saml.authnstatement": True,
"saml_name_id_format": "username",
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#"
"saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#",
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": True,
"nodeReRegistrationTimeout": -1,
"protocolMappers" : [ {
"protocolMappers": [
{
"id": "9296daa3-4fc4-4b80-b007-5070f546ae13",
"name": "X500 sn",
"protocol": "saml",
@ -197,9 +270,10 @@ class MoodleSaml():
"attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"user.attribute": "lastName",
"friendly.name": "sn",
"attribute.name" : "urn:oid:2.5.4.4"
}
}, {
"attribute.name": "urn:oid:2.5.4.4",
},
},
{
"id": "ccecf6e4-d20a-4211-b67c-40200a6b2c5d",
"name": "username",
"protocol": "saml",
@ -209,9 +283,10 @@ class MoodleSaml():
"attribute.nameformat": "Basic",
"user.attribute": "username",
"friendly.name": "username",
"attribute.name" : "username"
}
}, {
"attribute.name": "username",
},
},
{
"id": "53858403-eba2-4f6d-81d0-cced700b5719",
"name": "X500 givenName",
"protocol": "saml",
@ -221,9 +296,10 @@ class MoodleSaml():
"attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"user.attribute": "firstName",
"friendly.name": "givenName",
"attribute.name" : "urn:oid:2.5.4.42"
}
}, {
"attribute.name": "urn:oid:2.5.4.42",
},
},
{
"id": "20034db5-1d0e-4e66-b815-fb0440c6d1e2",
"name": "X500 email",
"protocol": "saml",
@ -233,16 +309,24 @@ class MoodleSaml():
"attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"user.attribute": "email",
"friendly.name": "email",
"attribute.name" : "urn:oid:1.2.840.113549.1.9.1"
}
} ],
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ],
"access" : {
"view" : True,
"configure" : True,
"manage" : True
}
"attribute.name": "urn:oid:1.2.840.113549.1.9.1",
},
},
],
"defaultClientScopes": [
"web-origins",
"role_list",
"roles",
"profile",
"email",
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt",
],
"access": {"view": True, "configure": True, "manage": True},
}
keycloak = KeycloakClient()
keycloak.add_client(client)
@ -250,10 +334,19 @@ class MoodleSaml():
def add_client_roles(self):
keycloak = KeycloakClient()
keycloak.add_client_role('a92d5417-92b6-4678-9cb9-51bc0edcee8c','admin','Moodle admins')
keycloak.add_client_role('a92d5417-92b6-4678-9cb9-51bc0edcee8c','manager','Moodle managers')
keycloak.add_client_role('a92d5417-92b6-4678-9cb9-51bc0edcee8c','teacher','Moodle teachers')
keycloak.add_client_role('a92d5417-92b6-4678-9cb9-51bc0edcee8c','student','Moodle students')
keycloak.add_client_role(
"a92d5417-92b6-4678-9cb9-51bc0edcee8c", "admin", "Moodle admins"
)
keycloak.add_client_role(
"a92d5417-92b6-4678-9cb9-51bc0edcee8c", "manager", "Moodle managers"
)
keycloak.add_client_role(
"a92d5417-92b6-4678-9cb9-51bc0edcee8c", "teacher", "Moodle teachers"
)
keycloak.add_client_role(
"a92d5417-92b6-4678-9cb9-51bc0edcee8c", "student", "Moodle students"
)
keycloak = None
m = MoodleSaml()

View File

@ -1,66 +1,81 @@
#!/usr/bin/env python
# coding=utf-8
import time, os
from datetime import datetime, timedelta
import pprint
import json
import logging as log
import os
import pprint
import random
import string
import time
import traceback
import yaml, json
from datetime import datetime, timedelta
import psycopg2
import yaml
from admin.lib.postgres import Postgres
from admin.lib.keycloak_client import KeycloakClient
import string, random
from admin.lib.postgres import Postgres
app = {}
app['config']={}
app["config"] = {}
class NextcloudSaml():
class NextcloudSaml:
def __init__(self):
self.url = "http://isard-sso-keycloak:8080/auth/"
self.username=os.environ['KEYCLOAK_USER']
self.password=os.environ['KEYCLOAK_PASSWORD']
self.realm='master'
self.username = os.environ["KEYCLOAK_USER"]
self.password = os.environ["KEYCLOAK_PASSWORD"]
self.realm = "master"
self.verify = True
ready = False
while not ready:
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
except:
log.warning('Could not connect to nextcloud database. Retrying...')
log.warning("Could not connect to nextcloud database. Retrying...")
time.sleep(2)
log.info('Connected to nextcloud database.')
log.info("Connected to nextcloud database.")
ready = False
while not ready:
try:
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
except IOError:
log.warning('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')
log.warning(
"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)
except:
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
while not ready:
try:
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
except IOError:
log.warning('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')
log.warning(
"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)
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.
# ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
@ -77,30 +92,32 @@ class NextcloudSaml():
try:
self.reset_saml()
except:
print('Error resetting saml on nextcloud')
print("Error resetting saml on nextcloud")
try:
self.delete_keycloak_nextcloud_saml_plugin()
except:
print('Error resetting saml on keycloak')
print("Error resetting saml on keycloak")
try:
self.set_nextcloud_saml_plugin()
except:
log.error(traceback.format_exc())
print('Error adding saml on nextcloud')
print("Error adding saml on nextcloud")
try:
self.add_keycloak_nextcloud_saml()
except:
print('Error adding saml on keycloak')
print("Error adding saml on keycloak")
def connect(self):
self.keycloak= KeycloakClient(url=self.url,
self.keycloak = KeycloakClient(
url=self.url,
username=self.username,
password=self.password,
realm=self.realm,
verify=self.verify)
verify=self.verify,
)
# def activate_saml_plugin(self):
# ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php
@ -113,7 +130,7 @@ class NextcloudSaml():
self.connect()
rsa = self.keycloak.get_server_rsa_key()
self.keycloak = None
return rsa['certificate']
return rsa["certificate"]
def set_keycloak_nextcloud_saml_plugin(self):
self.connect()
@ -122,11 +139,12 @@ class NextcloudSaml():
def delete_keycloak_nextcloud_saml_plugin(self):
self.connect()
self.keycloak.delete_client('bef873f0-2079-4876-8657-067de27d01b7')
self.keycloak.delete_client("bef873f0-2079-4876-8657-067de27d01b7")
self.keycloak = None
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', 'type', 'saml'),
('user_saml', 'sp-privateKey', '%s'),
@ -144,42 +162,60 @@ class NextcloudSaml():
('user_saml', 'security-wantAssertionsSigned', '1'),
('user_saml', 'general-idp0_display_name', 'SAML Login'),
('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):
cfg_list=['general-uid_mapping',
'sp-privateKey',
'saml-attribute-mapping-email_mapping',
'saml-attribute-mapping-quota_mapping',
'saml-attribute-mapping-displayName_mapping',
'saml-attribute-mapping-group_mapping',
'idp-entityId',
'idp-singleSignOnService.url',
'idp-x509cert',
'security-authnRequestsSigned',
'security-logoutRequestSigned',
'security-logoutResponseSigned',
'security-wantMessagesSigned',
'security-wantAssertionsSigned',
'general-idp0_display_name',
'type',
'sp-x509cert',
'idp-singleLogoutService.url']
cfg_list = [
"general-uid_mapping",
"sp-privateKey",
"saml-attribute-mapping-email_mapping",
"saml-attribute-mapping-quota_mapping",
"saml-attribute-mapping-displayName_mapping",
"saml-attribute-mapping-group_mapping",
"idp-entityId",
"idp-singleSignOnService.url",
"idp-x509cert",
"security-authnRequestsSigned",
"security-logoutRequestSigned",
"security-logoutResponseSigned",
"security-wantMessagesSigned",
"security-wantAssertionsSigned",
"general-idp0_display_name",
"type",
"sp-x509cert",
"idp-singleLogoutService.url",
]
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):
client={"id" : "bef873f0-2079-4876-8657-067de27d01b7",
client = {
"id": "bef873f0-2079-4876-8657-067de27d01b7",
"name": "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,
"enabled": True,
"alwaysDisplayInConsole": False,
"clientAuthenticatorType": "client-secret",
"redirectUris" : [ "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/acs" ],
"webOrigins" : [ "https://nextcloud."+os.environ['DOMAIN'] ],
"redirectUris": [
"https://nextcloud." + os.environ["DOMAIN"] + "/apps/user_saml/saml/acs"
],
"webOrigins": ["https://nextcloud." + os.environ["DOMAIN"]],
"notBefore": 0,
"bearerOnly": False,
"consentRequired": False,
@ -193,22 +229,27 @@ class NextcloudSaml():
"attributes": {
"saml.assertion.signature": 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.keyinfo.ext": False,
"saml.signing.certificate" : app['config']['PUBLIC_CERT'],
"saml_single_logout_service_url_redirect" : "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/sls",
"saml.signing.certificate": app["config"]["PUBLIC_CERT"],
"saml_single_logout_service_url_redirect": "https://nextcloud."
+ os.environ["DOMAIN"]
+ "/apps/user_saml/saml/sls",
"saml.signature.algorithm": "RSA_SHA256",
"saml_force_name_id_format": False,
"saml.client.signature": False,
"saml.authnstatement": True,
"saml_name_id_format": "username",
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#"
"saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#",
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": True,
"nodeReRegistrationTimeout": -1,
"protocolMappers" : [ {
"protocolMappers": [
{
"id": "e8e4acff-da2b-46aa-8bdb-ba42171671d6",
"name": "username",
"protocol": "saml",
@ -218,9 +259,10 @@ class NextcloudSaml():
"attribute.nameformat": "Basic",
"user.attribute": "username",
"friendly.name": "username",
"attribute.name" : "username"
}
}, {
"attribute.name": "username",
},
},
{
"id": "8ab13cd7-822a-40d5-a1e1-9f556aed2332",
"name": "quota",
"protocol": "saml",
@ -230,9 +272,10 @@ class NextcloudSaml():
"attribute.nameformat": "Basic",
"user.attribute": "quota",
"friendly.name": "quota",
"attribute.name" : "quota"
}
}, {
"attribute.name": "quota",
},
},
{
"id": "28206b59-757b-4e3c-81cb-0b6053b1fd3d",
"name": "email",
"protocol": "saml",
@ -242,9 +285,10 @@ class NextcloudSaml():
"attribute.nameformat": "Basic",
"user.attribute": "email",
"friendly.name": "email",
"attribute.name" : "email"
}
}, {
"attribute.name": "email",
},
},
{
"id": "5176a593-180f-4924-b294-b83a0d8d5972",
"name": "displayname",
"protocol": "saml",
@ -252,12 +296,13 @@ class NextcloudSaml():
"consentRequired": False,
"config": {
"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",
"friendly.name": "displayname",
"attribute.name" : "displayname"
}
}, {
"attribute.name": "displayname",
},
},
{
"id": "e51e04b9-f71a-42de-819e-dd9285246ada",
"name": "Roles",
"protocol": "saml",
@ -267,9 +312,10 @@ class NextcloudSaml():
"single": True,
"attribute.nameformat": "Basic",
"friendly.name": "Roles",
"attribute.name" : "Roles"
}
}, {
"attribute.name": "Roles",
},
},
{
"id": "9c101249-bb09-4cc8-8f75-5a18fcb307e6",
"name": "group_list",
"protocol": "saml",
@ -280,19 +326,28 @@ class NextcloudSaml():
"attribute.nameformat": "Basic",
"full.path": False,
"friendly.name": "member",
"attribute.name" : "member"
}
} ],
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ],
"access" : {
"view" : True,
"configure" : True,
"manage" : True
}
"attribute.name": "member",
},
},
],
"defaultClientScopes": [
"web-origins",
"role_list",
"roles",
"profile",
"email",
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt",
],
"access": {"view": True, "configure": True, "manage": True},
}
self.connect()
self.keycloak.add_client(client)
self.keycloak = None
n = NextcloudSaml()

View File

@ -1,66 +1,81 @@
#!/usr/bin/env python
# coding=utf-8
import time, os
from datetime import datetime, timedelta
import pprint
import json
import logging as log
import os
import pprint
import random
import string
import time
import traceback
import yaml, json
from datetime import datetime, timedelta
import psycopg2
import yaml
from admin.lib.postgres import Postgres
from admin.lib.keycloak_client import KeycloakClient
import string, random
from admin.lib.postgres import Postgres
app = {}
app['config']={}
app["config"] = {}
class NextcloudSaml():
class NextcloudSaml:
def __init__(self):
self.url = "http://isard-sso-keycloak:8080/auth/"
self.username=os.environ['KEYCLOAK_USER']
self.password=os.environ['KEYCLOAK_PASSWORD']
self.realm='master'
self.username = os.environ["KEYCLOAK_USER"]
self.password = os.environ["KEYCLOAK_PASSWORD"]
self.realm = "master"
self.verify = True
ready = False
while not ready:
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
except:
log.warning('Could not connect to nextcloud database. Retrying...')
log.warning("Could not connect to nextcloud database. Retrying...")
time.sleep(2)
log.info('Connected to nextcloud database.')
log.info("Connected to nextcloud database.")
ready = False
while not ready:
try:
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
except IOError:
log.warning('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')
log.warning(
"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)
except:
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
while not ready:
try:
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
except IOError:
log.warning('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')
log.warning(
"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)
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.
# ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
@ -77,20 +92,22 @@ class NextcloudSaml():
try:
self.reset_saml_certs()
except:
print('Error resetting saml on nextcloud')
print("Error resetting saml on nextcloud")
try:
self.set_nextcloud_saml_plugin_certs()
except:
log.error(traceback.format_exc())
print('Error adding saml on nextcloud')
print("Error adding saml on nextcloud")
def connect(self):
self.keycloak= KeycloakClient(url=self.url,
self.keycloak = KeycloakClient(
url=self.url,
username=self.username,
password=self.password,
realm=self.realm,
verify=self.verify)
verify=self.verify,
)
# def activate_saml_plugin(self):
# ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php
@ -103,20 +120,28 @@ class NextcloudSaml():
self.connect()
rsa = self.keycloak.get_server_rsa_key()
self.keycloak = None
return rsa['certificate']
return rsa["certificate"]
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', '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):
cfg_list=['sp-privateKey',
'idp-x509cert',
'sp-x509cert']
cfg_list = ["sp-privateKey", "idp-x509cert", "sp-x509cert"]
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()

View File

@ -1,24 +1,21 @@
#!/usr/bin/env python
# coding=utf-8
import time
from datetime import datetime, timedelta
import pprint
import json
import logging as log
import pprint
import time
import traceback
import yaml, json
from datetime import datetime, timedelta
import psycopg2
import yaml
class Postgres():
class Postgres:
def __init__(self, host, database, user, password):
self.conn = psycopg2.connect(
host=host,
database=database,
user=user,
password=password)
host=host, database=database, user=user, password=password
)
# def __del__(self):
# self.cur.close()

View File

@ -1,64 +1,82 @@
#!/usr/bin/env python
import time ,os
from datetime import datetime, timedelta
import json
import logging as log
import os
import time
import traceback
import yaml, json
from datetime import datetime, timedelta
from pprint import pprint
import yaml
from keycloak import KeycloakAdmin
from postgres import Postgres
from minio import Minio
from minio.commonconfig import REPLACE, CopySource
from minio.deleteobjects import DeleteObject
class DefaultAvatars():
def __init__(self,
from postgres import Postgres
class DefaultAvatars:
def __init__(
self,
url="http://isard-sso-keycloak:8080/auth/",
username=os.environ['KEYCLOAK_USER'],
password=os.environ['KEYCLOAK_PASSWORD'],
realm='master',
verify=True):
username=os.environ["KEYCLOAK_USER"],
password=os.environ["KEYCLOAK_PASSWORD"],
realm="master",
verify=True,
):
self.url = url
self.username = username
self.password = password
self.realm = realm
self.verify = verify
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD'])
self.keycloak_pg = Postgres(
"isard-apps-postgresql",
"keycloak",
os.environ["KEYCLOAK_DB_USER"],
os.environ["KEYCLOAK_DB_PASSWORD"],
)
self.mclient = Minio(
"isard-sso-avatars:9000",
access_key="AKIAIOSFODNN7EXAMPLE",
secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
secure=False
secure=False,
)
self.bucket='master-avatars'
self.bucket = "master-avatars"
self._minio_set_realm()
self.update_missing_avatars()
def connect(self):
self.keycloak_admin = KeycloakAdmin(server_url=self.url,
self.keycloak_admin = KeycloakAdmin(
server_url=self.url,
username=self.username,
password=self.password,
realm_name=self.realm,
verify=self.verify)
verify=self.verify,
)
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():
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:
img='unknown.jpg'
img = "unknown.jpg"
self.mclient.fput_object(
self.bucket, u['id'], "custom/avatars/"+img,
self.bucket,
u["id"],
"custom/avatars/" + img,
content_type="image/jpeg ",
)
log.warning(' AVATARS: Updated avatar for user '+u['username']+' with role '+img.split('.')[0])
log.warning(
" AVATARS: Updated avatar for user "
+ u["username"]
+ " with role "
+ img.split(".")[0]
)
def _minio_set_realm(self):
if not self.mclient.bucket_exists(self.bucket):
@ -82,7 +100,7 @@ class DefaultAvatars():
return users
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):
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)
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
users_with_lists = [
list(l[:-4])
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users
]
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
users_with_lists = [
list(l[:-4])
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users_with_lists
]
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
return list_dict_users
da=DefaultAvatars()
da = DefaultAvatars()

View File

@ -1,48 +1,59 @@
#!/usr/bin/env python
import time ,os
from datetime import datetime, timedelta
import json
import logging as log
import os
import time
import traceback
import yaml, json
from datetime import datetime, timedelta
from pprint import pprint
import diceware
import yaml
from jinja2 import Environment, FileSystemLoader
from keycloak import KeycloakAdmin
from postgres import Postgres
import diceware
options = diceware.handle_options(None)
options.wordlist = 'cat_ascii'
options.wordlist = "cat_ascii"
options.num = 3
class KeycloakClient():
class KeycloakClient:
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
https://github.com/marcospereirampj/python-keycloak
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
"""
def __init__(self,
def __init__(
self,
url="http://isard-sso-keycloak:8080/auth/",
username=os.environ['KEYCLOAK_USER'],
password=os.environ['KEYCLOAK_PASSWORD'],
realm='master',
verify=True):
username=os.environ["KEYCLOAK_USER"],
password=os.environ["KEYCLOAK_PASSWORD"],
realm="master",
verify=True,
):
self.url = url
self.username = username
self.password = password
self.realm = realm
self.verify = verify
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD'])
self.keycloak_pg = Postgres(
"isard-apps-postgresql",
"keycloak",
os.environ["KEYCLOAK_DB_USER"],
os.environ["KEYCLOAK_DB_PASSWORD"],
)
def connect(self):
self.keycloak_admin = KeycloakAdmin(server_url=self.url,
self.keycloak_admin = KeycloakAdmin(
server_url=self.url,
username=self.username,
password=self.password,
realm_name=self.realm,
verify=self.verify)
verify=self.verify,
)
def update_pwds(self):
self.get_users()
@ -52,27 +63,36 @@ class KeycloakClient():
users = self.get_users_with_groups_and_roles()
userupdate = []
for u in users:
if u['username'] not in ['admin','ddadmin'] and not u['username'].startswith('system_'):
print('Generating password for user '+u['username'])
userupdate.append({'id':u['id'],
'username':u['username'],
'password': diceware.get_passphrase(options=options)})
if u["username"] not in ["admin", "ddadmin"] and not u[
"username"
].startswith("system_"):
print("Generating password for user " + u["username"])
userupdate.append(
{
"id": u["id"],
"username": u["username"],
"password": diceware.get_passphrase(options=options),
}
)
with open("user_temp_passwd.csv", "w") as csv:
for user in userupdate:
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:
print('Updating keycloak password for user '+u['username'])
self.update_user_pwd(u['id'],u['password'])
print("Updating keycloak password for user " + u["username"])
self.update_user_pwd(u["id"], u["password"])
def update_user_pwd(self, user_id, password, temporary=True):
payload={"credentials":[{"type":"password",
"value":password,
"temporary":temporary}]}
payload = {
"credentials": [
{"type": "password", "value": password, "temporary": temporary}
]
}
self.connect()
self.keycloak_admin.update_user(user_id, payload)
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
,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"""
(headers, users) = self.keycloak_pg.select_with_headers(q)
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
users_with_lists = [
list(l[:-4])
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users
]
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
users_with_lists = [
list(l[:-4])
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users_with_lists
]
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
return list_dict_users
k = KeycloakClient()
k.update_pwds()

View File

@ -1,44 +1,54 @@
#!/usr/bin/env python
import time ,os
from datetime import datetime, timedelta
import json
import logging as log
import os
import time
import traceback
import yaml, json
from datetime import datetime, timedelta
from pprint import pprint
import yaml
from jinja2 import Environment, FileSystemLoader
from keycloak import KeycloakAdmin
from postgres import Postgres
class KeycloakClient():
class KeycloakClient:
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
https://github.com/marcospereirampj/python-keycloak
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
"""
def __init__(self,
def __init__(
self,
url="http://isard-sso-keycloak:8080/auth/",
username=os.environ['KEYCLOAK_USER'],
password=os.environ['KEYCLOAK_PASSWORD'],
realm='master',
verify=True):
username=os.environ["KEYCLOAK_USER"],
password=os.environ["KEYCLOAK_PASSWORD"],
realm="master",
verify=True,
):
self.url = url
self.username = username
self.password = password
self.realm = realm
self.verify = verify
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD'])
self.keycloak_pg = Postgres(
"isard-apps-postgresql",
"keycloak",
os.environ["KEYCLOAK_DB_USER"],
os.environ["KEYCLOAK_DB_PASSWORD"],
)
def connect(self):
self.keycloak_admin = KeycloakAdmin(server_url=self.url,
self.keycloak_admin = KeycloakAdmin(
server_url=self.url,
username=self.username,
password=self.password,
realm_name=self.realm,
verify=self.verify)
verify=self.verify,
)
def update_pwds(self):
self.get_users()
@ -48,18 +58,26 @@ class KeycloakClient():
users = self.get_users_with_groups_and_roles()
userupdate = []
for u in users:
if u['username'] not in ['admin','ddadmin'] and not u['username'].startswith('system_'):
print('Generating password for user '+u['username'])
userupdate.append({'id':u['id'],
'username':u['username'],
'password': diceware.get_passphrase(options=options)})
if u["username"] not in ["admin", "ddadmin"] and not u[
"username"
].startswith("system_"):
print("Generating password for user " + u["username"])
userupdate.append(
{
"id": u["id"],
"username": u["username"],
"password": diceware.get_passphrase(options=options),
}
)
with open("user_temp_passwd.csv", "w") as csv:
for user in userupdate:
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:
print('Updating keycloak password for user '+u['username'])
self.update_user_pwd(u['id'],u['password'])
print("Updating keycloak password for user " + u["username"])
self.update_user_pwd(u["id"], u["password"])
def update_user_pwd_temporary(self, temporary=False):
payload = {"credentials": [{"temporary": temporary}]}
@ -111,18 +129,27 @@ class KeycloakClient():
# order by u.username"""
(headers, users) = self.keycloak_pg.select_with_headers(q)
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
users_with_lists = [
list(l[:-4])
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users
]
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
users_with_lists = [
list(l[:-4])
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
for l in users_with_lists
]
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
return list_dict_users
k = KeycloakClient()
k.update_user_pwd_temporary()

View File

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

View File

@ -1,28 +1,46 @@
#!flask/bin/python
# coding=utf-8
from gevent import monkey
monkey.patch_all()
from flask_socketio import SocketIO, emit, join_room, leave_room, \
close_room, rooms, disconnect, send
import json
from flask_socketio import (
SocketIO,
close_room,
disconnect,
emit,
join_room,
leave_room,
rooms,
send,
)
from admin import 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():
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="*"
# /usr/lib/python3.8/site-packages/certifi

View File

@ -1,67 +1,84 @@
#!/usr/bin/env python
# coding=utf-8
import time, os
from datetime import datetime, timedelta
import pprint
import json
import logging as log
import os
import pprint
import random
import string
import time
import traceback
import yaml, json
from datetime import datetime, timedelta
import psycopg2
import yaml
from admin.lib.mysql import Mysql
from admin.lib.keycloak_client import KeycloakClient
import string, random
from admin.lib.mysql import Mysql
app = {}
app['config']={}
app["config"] = {}
class WordpressSaml():
class WordpressSaml:
def __init__(self):
self.url = "http://isard-sso-keycloak:8080/auth/"
self.username=os.environ['KEYCLOAK_USER']
self.password=os.environ['KEYCLOAK_PASSWORD']
self.realm='master'
self.username = os.environ["KEYCLOAK_USER"]
self.password = os.environ["KEYCLOAK_PASSWORD"]
self.realm = "master"
self.verify = True
ready = False
while not ready:
try:
self.db=Mysql('isard-apps-mariadb','wordpress','root',os.environ['MARIADB_PASSWORD'])
self.db = Mysql(
"isard-apps-mariadb",
"wordpress",
"root",
os.environ["MARIADB_PASSWORD"],
)
ready = True
except:
log.warning('Could not connect to wordpress database. Retrying...')
log.warning("Could not connect to wordpress database. Retrying...")
time.sleep(2)
log.info('Connected to wordpress database.')
log.info("Connected to wordpress database.")
ready = False
while not ready:
try:
with open(os.path.join("./saml_certs/public.cert"), "r") as crt:
app['config']['PUBLIC_CERT_RAW']=crt.read()
app['config']['PUBLIC_CERT']=self.cert_prepare(app['config']['PUBLIC_CERT_RAW'])
app["config"]["PUBLIC_CERT_RAW"] = crt.read()
app["config"]["PUBLIC_CERT"] = self.cert_prepare(
app["config"]["PUBLIC_CERT_RAW"]
)
ready = True
except IOError:
log.warning('Could not get public certificate to be used in wordpress. Retrying...')
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert')
log.warning(
"Could not get public certificate to be used in wordpress. Retrying..."
)
log.warning(
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
)
time.sleep(2)
except:
log.error(traceback.format_exc())
log.info('Got moodle srt certificate to be used in wordpress.')
log.info("Got moodle srt certificate to be used in wordpress.")
ready = False
while not ready:
try:
with open(os.path.join("./saml_certs/private.key"), "r") as pem:
app['config']['PRIVATE_KEY']=self.cert_prepare(pem.read())
app["config"]["PRIVATE_KEY"] = self.cert_prepare(pem.read())
ready = True
except IOError:
log.warning('Could not get private key to be used in wordpress. Retrying...')
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert')
log.warning(
"Could not get private key to be used in wordpress. Retrying..."
)
log.warning(
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
)
time.sleep(2)
log.info('Got moodle pem certificate to be used in wordpress.')
log.info("Got moodle pem certificate to be used in wordpress.")
# ## This seems related to the fact that the certificate generated the first time does'nt work.
# ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
@ -79,32 +96,34 @@ class WordpressSaml():
self.reset_saml()
except:
print(traceback.format_exc())
print('Error resetting saml on wordpress')
print("Error resetting saml on wordpress")
try:
self.delete_keycloak_wordpress_saml_plugin()
except:
print('Error resetting saml on keycloak')
print("Error resetting saml on keycloak")
try:
self.set_wordpress_saml_plugin()
except:
print(traceback.format_exc())
print('Error adding saml on wordpress')
print("Error adding saml on wordpress")
try:
self.add_keycloak_wordpress_saml()
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
# self.add_client_roles()
def connect(self):
self.keycloak= KeycloakClient(url=self.url,
self.keycloak = KeycloakClient(
url=self.url,
username=self.username,
password=self.password,
realm=self.realm,
verify=self.verify)
verify=self.verify,
)
# def activate_saml_plugin(self):
# ## 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]
def cert_prepare(self, cert):
return ''.join(cert.split('-----')[2].splitlines())
return "".join(cert.split("-----")[2].splitlines())
def parse_idp_cert(self):
self.connect()
rsa = self.keycloak.get_server_rsa_key()
self.keycloak = None
return rsa['certificate']
return rsa["certificate"]
def set_keycloak_wordpress_saml_plugin(self):
self.connect()
@ -129,12 +148,13 @@ class WordpressSaml():
def delete_keycloak_wordpress_saml_plugin(self):
self.connect()
self.keycloak.delete_client('630601f8-25d1-4822-8741-c93affd2cd84')
self.keycloak.delete_client("630601f8-25d1-4822-8741-c93affd2cd84")
self.keycloak = None
def set_wordpress_saml_plugin(self):
# ('active_plugins', 'a:2:{i:0;s:33:\"edwiser-bridge/edwiser-bridge.php\";i:1;s:17:\"onelogin_saml.php\";}', 'yes'),
self.db.update("""INSERT INTO wp_options (option_name, option_value, autoload) VALUES
self.db.update(
"""INSERT INTO wp_options (option_name, option_value, autoload) VALUES
('onelogin_saml_enabled', 'on', 'yes'),
('onelogin_saml_idp_entityid', 'Saml Login', 'yes'),
('onelogin_saml_idp_sso', 'https://sso.%s/auth/realms/master/protocol/saml', 'yes'),
@ -194,21 +214,33 @@ class WordpressSaml():
('onelogin_saml_advanced_settings_sp_x509cert', '%s', 'yes'),
('onelogin_saml_advanced_settings_sp_privatekey', '%s', 'yes'),
('onelogin_saml_advanced_signaturealgorithm', 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', 'yes'),
('onelogin_saml_advanced_digestalgorithm', 'http://www.w3.org/2000/09/xmldsig#sha1', 'yes');""" % (os.environ['DOMAIN'],os.environ['DOMAIN'],self.parse_idp_cert(),app['config']['PUBLIC_CERT'],app['config']['PRIVATE_KEY']))
('onelogin_saml_advanced_digestalgorithm', 'http://www.w3.org/2000/09/xmldsig#sha1', 'yes');"""
% (
os.environ["DOMAIN"],
os.environ["DOMAIN"],
self.parse_idp_cert(),
app["config"]["PUBLIC_CERT"],
app["config"]["PRIVATE_KEY"],
)
)
def reset_saml(self):
self.db.update("""DELETE FROM wp_options WHERE option_name LIKE 'onelogin_saml_%'""")
self.db.update(
"""DELETE FROM wp_options WHERE option_name LIKE 'onelogin_saml_%'"""
)
def add_keycloak_wordpress_saml(self):
client={"id" : "630601f8-25d1-4822-8741-c93affd2cd84",
client = {
"id": "630601f8-25d1-4822-8741-c93affd2cd84",
"clientId": "php-saml",
"surrogateAuthRequired": False,
"enabled": True,
"alwaysDisplayInConsole": False,
"clientAuthenticatorType": "client-secret",
"redirectUris" : [ "https://wp."+os.environ['DOMAIN']+"/wp-login.php?saml_acs" ],
"webOrigins" : [ "https://wp."+os.environ['DOMAIN'] ],
"redirectUris": [
"https://wp." + os.environ["DOMAIN"] + "/wp-login.php?saml_acs"
],
"webOrigins": ["https://wp." + os.environ["DOMAIN"]],
"notBefore": 0,
"bearerOnly": False,
"consentRequired": False,
@ -221,22 +253,27 @@ class WordpressSaml():
"protocol": "saml",
"attributes": {
"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.keyinfo.ext": False,
"saml.signing.certificate" : app['config']['PUBLIC_CERT_RAW'],
"saml_single_logout_service_url_redirect" : "https://wp."+os.environ['DOMAIN']+"/wp-login.php?saml_sls",
"saml.signing.certificate": app["config"]["PUBLIC_CERT_RAW"],
"saml_single_logout_service_url_redirect": "https://wp."
+ os.environ["DOMAIN"]
+ "/wp-login.php?saml_sls",
"saml.signature.algorithm": "RSA_SHA256",
"saml_force_name_id_format": False,
"saml.client.signature": True,
"saml.authnstatement": True,
"saml_name_id_format": "username",
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#"
"saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#",
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": True,
"nodeReRegistrationTimeout": -1,
"protocolMappers" : [ {
"protocolMappers": [
{
"id": "72c6175e-bd07-4c27-abd6-4e4ae38d834b",
"name": "username",
"protocol": "saml",
@ -246,9 +283,10 @@ class WordpressSaml():
"attribute.nameformat": "Basic",
"user.attribute": "username",
"friendly.name": "username",
"attribute.name" : "username"
}
}, {
"attribute.name": "username",
},
},
{
"id": "abd6562f-4732-4da9-987f-b1a6ad6605fa",
"name": "roles",
"protocol": "saml",
@ -258,9 +296,10 @@ class WordpressSaml():
"single": True,
"attribute.nameformat": "Basic",
"friendly.name": "Roles",
"attribute.name" : "Role"
}
}, {
"attribute.name": "Role",
},
},
{
"id": "50aafb71-d91c-4bc7-bb60-e1ae0222aab3",
"name": "email",
"protocol": "saml",
@ -270,16 +309,24 @@ class WordpressSaml():
"attribute.nameformat": "Basic",
"user.attribute": "email",
"friendly.name": "email",
"attribute.name" : "email"
}
} ],
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ],
"access" : {
"view" : True,
"configure" : True,
"manage" : True
}
"attribute.name": "email",
},
},
],
"defaultClientScopes": [
"web-origins",
"role_list",
"roles",
"profile",
"email",
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt",
],
"access": {"view": True, "configure": True, "manage": True},
}
self.connect()
self.keycloak.add_client(client)
@ -287,10 +334,19 @@ class WordpressSaml():
def add_client_roles(self):
self.connect()
self.keycloak.add_client_role('630601f8-25d1-4822-8741-c93affd2cd84','admin','Wordpress admins')
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.add_client_role(
"630601f8-25d1-4822-8741-c93affd2cd84", "admin", "Wordpress admins"
)
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
nw = WordpressSaml()

View File

@ -1,67 +1,84 @@
#!/usr/bin/env python
# coding=utf-8
import time, os
from datetime import datetime, timedelta
import pprint
import json
import logging as log
import os
import pprint
import random
import string
import time
import traceback
import yaml, json
from datetime import datetime, timedelta
import psycopg2
import yaml
from admin.lib.mysql import Mysql
from admin.lib.keycloak_client import KeycloakClient
import string, random
from admin.lib.mysql import Mysql
app = {}
app['config']={}
app["config"] = {}
class WordpressSaml():
class WordpressSaml:
def __init__(self):
self.url = "http://isard-sso-keycloak:8080/auth/"
self.username=os.environ['KEYCLOAK_USER']
self.password=os.environ['KEYCLOAK_PASSWORD']
self.realm='master'
self.username = os.environ["KEYCLOAK_USER"]
self.password = os.environ["KEYCLOAK_PASSWORD"]
self.realm = "master"
self.verify = True
ready = False
while not ready:
try:
self.db=Mysql('isard-apps-mariadb','wordpress','root',os.environ['MARIADB_PASSWORD'])
self.db = Mysql(
"isard-apps-mariadb",
"wordpress",
"root",
os.environ["MARIADB_PASSWORD"],
)
ready = True
except:
log.warning('Could not connect to wordpress database. Retrying...')
log.warning("Could not connect to wordpress database. Retrying...")
time.sleep(2)
log.info('Connected to wordpress database.')
log.info("Connected to wordpress database.")
ready = False
while not ready:
try:
with open(os.path.join("./saml_certs/public.cert"), "r") as crt:
app['config']['PUBLIC_CERT_RAW']=crt.read()
app['config']['PUBLIC_CERT']=self.cert_prepare(app['config']['PUBLIC_CERT_RAW'])
app["config"]["PUBLIC_CERT_RAW"] = crt.read()
app["config"]["PUBLIC_CERT"] = self.cert_prepare(
app["config"]["PUBLIC_CERT_RAW"]
)
ready = True
except IOError:
log.warning('Could not get public certificate to be used in wordpress. Retrying...')
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert')
log.warning(
"Could not get public certificate to be used in wordpress. Retrying..."
)
log.warning(
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
)
time.sleep(2)
except:
log.error(traceback.format_exc())
log.info('Got moodle srt certificate to be used in wordpress.')
log.info("Got moodle srt certificate to be used in wordpress.")
ready = False
while not ready:
try:
with open(os.path.join("./saml_certs/private.key"), "r") as pem:
app['config']['PRIVATE_KEY']=self.cert_prepare(pem.read())
app["config"]["PRIVATE_KEY"] = self.cert_prepare(pem.read())
ready = True
except IOError:
log.warning('Could not get private key to be used in wordpress. Retrying...')
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert')
log.warning(
"Could not get private key to be used in wordpress. Retrying..."
)
log.warning(
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
)
time.sleep(2)
log.info('Got moodle pem certificate to be used in wordpress.')
log.info("Got moodle pem certificate to be used in wordpress.")
# ## This seems related to the fact that the certificate generated the first time does'nt work.
# ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
@ -79,13 +96,13 @@ class WordpressSaml():
self.reset_saml_certs()
except:
print(traceback.format_exc())
print('Error resetting saml certs on wordpress')
print("Error resetting saml certs on wordpress")
try:
self.set_wordpress_saml_plugin_certs()
except:
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
# self.add_client_roles()
@ -98,32 +115,47 @@ class WordpressSaml():
# return self.db.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2]
def connect(self):
self.keycloak= KeycloakClient(url=self.url,
self.keycloak = KeycloakClient(
url=self.url,
username=self.username,
password=self.password,
realm=self.realm,
verify=self.verify)
verify=self.verify,
)
def cert_prepare(self, cert):
return ''.join(cert.split('-----')[2].splitlines())
return "".join(cert.split("-----")[2].splitlines())
def parse_idp_cert(self):
self.connect()
rsa = self.keycloak.get_server_rsa_key()
self.keycloak = None
return rsa['certificate']
return rsa["certificate"]
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'),
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_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):
self.db.update("""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_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_idp_x509cert'"""
)
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()