refator(admin): black
parent
53fbbc123a
commit
f87a6c6b2f
|
@ -1,77 +1,96 @@
|
||||||
#!flask/bin/python
|
#!flask/bin/python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
|
|
||||||
import os
|
|
||||||
import logging as log
|
import logging as log
|
||||||
|
import os
|
||||||
|
|
||||||
from flask import Flask, send_from_directory, render_template
|
from flask import Flask, render_template, send_from_directory
|
||||||
app = Flask(__name__, static_url_path='')
|
|
||||||
app = Flask(__name__, template_folder='static/templates')
|
app = Flask(__name__, static_url_path="")
|
||||||
|
app = Flask(__name__, template_folder="static/templates")
|
||||||
app.url_map.strict_slashes = False
|
app.url_map.strict_slashes = False
|
||||||
|
|
||||||
'''
|
"""
|
||||||
App secret key for encrypting cookies
|
App secret key for encrypting cookies
|
||||||
You can generate one with:
|
You can generate one with:
|
||||||
import os
|
import os
|
||||||
os.urandom(24)
|
os.urandom(24)
|
||||||
And paste it here.
|
And paste it here.
|
||||||
'''
|
"""
|
||||||
app.secret_key = "Change this key!/\xf7\x83\xbe\x17\xfa\xa3zT\n\\]m\xa6\x8bF\xdd\r\xf7\x9e\x1d\x1f\x14'"
|
app.secret_key = "Change this key!/\xf7\x83\xbe\x17\xfa\xa3zT\n\\]m\xa6\x8bF\xdd\r\xf7\x9e\x1d\x1f\x14'"
|
||||||
|
|
||||||
print('Starting isard-sso api...')
|
print("Starting isard-sso api...")
|
||||||
|
|
||||||
from admin.lib.load_config import loadConfig
|
from admin.lib.load_config import loadConfig
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loadConfig(app)
|
loadConfig(app)
|
||||||
except:
|
except:
|
||||||
print('Could not get environment variables...')
|
print("Could not get environment variables...")
|
||||||
|
|
||||||
from admin.lib.postup import Postup
|
from admin.lib.postup import Postup
|
||||||
|
|
||||||
Postup()
|
Postup()
|
||||||
|
|
||||||
from admin.lib.admin import Admin
|
from admin.lib.admin import Admin
|
||||||
|
|
||||||
app.admin = Admin()
|
app.admin = Admin()
|
||||||
|
|
||||||
app.ready = False
|
app.ready = False
|
||||||
|
|
||||||
'''
|
"""
|
||||||
Debug should be removed on production!
|
Debug should be removed on production!
|
||||||
'''
|
"""
|
||||||
if app.debug:
|
if app.debug:
|
||||||
log.warning('Debug mode: {}'.format(app.debug))
|
log.warning("Debug mode: {}".format(app.debug))
|
||||||
else:
|
else:
|
||||||
log.info('Debug mode: {}'.format(app.debug))
|
log.info("Debug mode: {}".format(app.debug))
|
||||||
|
|
||||||
'''
|
"""
|
||||||
Serve static files
|
Serve static files
|
||||||
'''
|
"""
|
||||||
@app.route('/build/<path:path>')
|
|
||||||
|
|
||||||
|
@app.route("/build/<path:path>")
|
||||||
def send_build(path):
|
def send_build(path):
|
||||||
return send_from_directory(os.path.join(app.root_path, 'node_modules/gentelella/build'), path)
|
return send_from_directory(
|
||||||
|
os.path.join(app.root_path, "node_modules/gentelella/build"), path
|
||||||
|
)
|
||||||
|
|
||||||
@app.route('/vendors/<path:path>')
|
|
||||||
|
@app.route("/vendors/<path:path>")
|
||||||
def send_vendors(path):
|
def send_vendors(path):
|
||||||
return send_from_directory(os.path.join(app.root_path, 'node_modules/gentelella/vendors'), path)
|
return send_from_directory(
|
||||||
|
os.path.join(app.root_path, "node_modules/gentelella/vendors"), path
|
||||||
|
)
|
||||||
|
|
||||||
@app.route('/templates/<path:path>')
|
|
||||||
|
@app.route("/templates/<path:path>")
|
||||||
def send_templates(path):
|
def send_templates(path):
|
||||||
return send_from_directory(os.path.join(app.root_path, 'templates'), path)
|
return send_from_directory(os.path.join(app.root_path, "templates"), path)
|
||||||
|
|
||||||
|
|
||||||
# @app.route('/templates/<path:path>')
|
# @app.route('/templates/<path:path>')
|
||||||
# def send_templates(path):
|
# def send_templates(path):
|
||||||
# return send_from_directory(os.path.join(app.root_path, 'static/templates'), path)
|
# return send_from_directory(os.path.join(app.root_path, 'static/templates'), path)
|
||||||
|
|
||||||
@app.route('/static/<path:path>')
|
|
||||||
|
@app.route("/static/<path:path>")
|
||||||
def send_static_js(path):
|
def send_static_js(path):
|
||||||
return send_from_directory(os.path.join(app.root_path, 'static'), path)
|
return send_from_directory(os.path.join(app.root_path, "static"), path)
|
||||||
|
|
||||||
@app.route('/avatars/<path:path>')
|
|
||||||
|
@app.route("/avatars/<path:path>")
|
||||||
def send_avatars_img(path):
|
def send_avatars_img(path):
|
||||||
return send_from_directory(os.path.join(app.root_path, '../avatars/master-avatars'), path)
|
return send_from_directory(
|
||||||
|
os.path.join(app.root_path, "../avatars/master-avatars"), path
|
||||||
|
)
|
||||||
|
|
||||||
@app.route('/custom/<path:path>')
|
|
||||||
|
@app.route("/custom/<path:path>")
|
||||||
def send_custom(path):
|
def send_custom(path):
|
||||||
return send_from_directory(os.path.join(app.root_path, '../custom'), path)
|
return send_from_directory(os.path.join(app.root_path, "../custom"), path)
|
||||||
|
|
||||||
|
|
||||||
# @app.errorhandler(404)
|
# @app.errorhandler(404)
|
||||||
# def not_found_error(error):
|
# def not_found_error(error):
|
||||||
|
@ -81,15 +100,7 @@ def send_custom(path):
|
||||||
# def internal_error(error):
|
# def internal_error(error):
|
||||||
# return render_template('page_500.html'), 500
|
# return render_template('page_500.html'), 500
|
||||||
|
|
||||||
'''
|
"""
|
||||||
Import all views
|
Import all views
|
||||||
'''
|
"""
|
||||||
from .views import LoginViews
|
from .views import ApiViews, InternalViews, LoginViews, WebViews
|
||||||
from .views import WebViews
|
|
||||||
from .views import ApiViews
|
|
||||||
from .views import InternalViews
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from admin import app
|
|
||||||
from flask_login import LoginManager, UserMixin
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
''' OIDC TESTS '''
|
from flask_login import LoginManager, UserMixin
|
||||||
|
|
||||||
|
from admin import app
|
||||||
|
|
||||||
|
""" OIDC TESTS """
|
||||||
# from flask_oidc import OpenIDConnect
|
# from flask_oidc import OpenIDConnect
|
||||||
# app.config.update({
|
# app.config.update({
|
||||||
# 'SECRET_KEY': 'u\x91\xcf\xfa\x0c\xb9\x95\xe3t\xba2K\x7f\xfd\xca\xa3\x9f\x90\x88\xb8\xee\xa4\xd6\xe4',
|
# 'SECRET_KEY': 'u\x91\xcf\xfa\x0c\xb9\x95\xe3t\xba2K\x7f\xfd\xca\xa3\x9f\x90\x88\xb8\xee\xa4\xd6\xe4',
|
||||||
|
@ -18,7 +20,7 @@ import os
|
||||||
# # 'OVERWRITE_REDIRECT_URI': 'https://sso.mydomain.duckdns.org//custom_callback',
|
# # 'OVERWRITE_REDIRECT_URI': 'https://sso.mydomain.duckdns.org//custom_callback',
|
||||||
# # 'OIDC_CALLBACK_ROUTE': '//custom_callback'
|
# # 'OIDC_CALLBACK_ROUTE': '//custom_callback'
|
||||||
# oidc = OpenIDConnect(app)
|
# oidc = OpenIDConnect(app)
|
||||||
''' OIDC TESTS '''
|
""" OIDC TESTS """
|
||||||
|
|
||||||
|
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
|
@ -28,28 +30,30 @@ login_manager.login_view = "login"
|
||||||
|
|
||||||
ram_users = {
|
ram_users = {
|
||||||
os.environ["ADMINAPP_USER"]: {
|
os.environ["ADMINAPP_USER"]: {
|
||||||
'id': os.environ["ADMINAPP_USER"],
|
"id": os.environ["ADMINAPP_USER"],
|
||||||
'password': os.environ["ADMINAPP_PASSWORD"],
|
"password": os.environ["ADMINAPP_PASSWORD"],
|
||||||
'role': 'manager'
|
"role": "manager",
|
||||||
},
|
},
|
||||||
os.environ["KEYCLOAK_USER"]: {
|
os.environ["KEYCLOAK_USER"]: {
|
||||||
'id': os.environ["KEYCLOAK_USER"],
|
"id": os.environ["KEYCLOAK_USER"],
|
||||||
'password': os.environ["KEYCLOAK_PASSWORD"],
|
"password": os.environ["KEYCLOAK_PASSWORD"],
|
||||||
'role': 'admin',
|
"role": "admin",
|
||||||
},
|
},
|
||||||
os.environ["WORDPRESS_MARIADB_USER"]: {
|
os.environ["WORDPRESS_MARIADB_USER"]: {
|
||||||
'id': os.environ["WORDPRESS_MARIADB_USER"],
|
"id": os.environ["WORDPRESS_MARIADB_USER"],
|
||||||
'password': os.environ["WORDPRESS_MARIADB_PASSWORD"],
|
"password": os.environ["WORDPRESS_MARIADB_PASSWORD"],
|
||||||
'role': 'manager',
|
"role": "manager",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin):
|
class User(UserMixin):
|
||||||
def __init__(self, dict):
|
def __init__(self, dict):
|
||||||
self.id = dict['id']
|
self.id = dict["id"]
|
||||||
self.username = dict['id']
|
self.username = dict["id"]
|
||||||
self.password = dict['password']
|
self.password = dict["password"]
|
||||||
self.role = dict['role']
|
self.role = dict["role"]
|
||||||
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def user_loader(username):
|
def user_loader(username):
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,49 +1,61 @@
|
||||||
from requests import get, post
|
|
||||||
from admin import app
|
|
||||||
|
|
||||||
import logging as log
|
import logging as log
|
||||||
from pprint import pprint
|
|
||||||
import os
|
import os
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
from minio import Minio
|
from minio import Minio
|
||||||
from minio.commonconfig import REPLACE, CopySource
|
from minio.commonconfig import REPLACE, CopySource
|
||||||
from minio.deleteobjects import DeleteObject
|
from minio.deleteobjects import DeleteObject
|
||||||
|
from requests import get, post
|
||||||
|
|
||||||
class Avatars():
|
from admin import app
|
||||||
|
|
||||||
|
|
||||||
|
class Avatars:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.mclient = Minio(
|
self.mclient = Minio(
|
||||||
"isard-sso-avatars:9000",
|
"isard-sso-avatars:9000",
|
||||||
access_key="AKIAIOSFODNN7EXAMPLE",
|
access_key="AKIAIOSFODNN7EXAMPLE",
|
||||||
secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||||
secure=False
|
secure=False,
|
||||||
)
|
)
|
||||||
self.bucket='master-avatars'
|
self.bucket = "master-avatars"
|
||||||
self._minio_set_realm()
|
self._minio_set_realm()
|
||||||
# self.update_missing_avatars()
|
# self.update_missing_avatars()
|
||||||
|
|
||||||
def add_user_default_avatar(self,userid,role='unknown'):
|
def add_user_default_avatar(self, userid, role="unknown"):
|
||||||
self.mclient.fput_object(
|
self.mclient.fput_object(
|
||||||
self.bucket, userid, os.path.join(app.root_path,"../custom/avatars/"+role+'.jpg'),
|
self.bucket,
|
||||||
|
userid,
|
||||||
|
os.path.join(app.root_path, "../custom/avatars/" + role + ".jpg"),
|
||||||
content_type="image/jpeg ",
|
content_type="image/jpeg ",
|
||||||
)
|
)
|
||||||
log.warning(' AVATARS: Updated avatar for user '+userid+' with role '+role)
|
log.warning(
|
||||||
|
" AVATARS: Updated avatar for user " + userid + " with role " + role
|
||||||
|
)
|
||||||
|
|
||||||
def delete_user_avatar(self, userid):
|
def delete_user_avatar(self, userid):
|
||||||
self.minio_delete_object(userid)
|
self.minio_delete_object(userid)
|
||||||
|
|
||||||
def update_missing_avatars(self, users):
|
def update_missing_avatars(self, users):
|
||||||
sys_roles=['admin','manager','teacher','student']
|
sys_roles = ["admin", "manager", "teacher", "student"]
|
||||||
for u in self.get_users_without_image(users):
|
for u in self.get_users_without_image(users):
|
||||||
try:
|
try:
|
||||||
img=[r+'.jpg' for r in sys_roles if r in u['roles']][0]
|
img = [r + ".jpg" for r in sys_roles if r in u["roles"]][0]
|
||||||
except:
|
except:
|
||||||
img='unknown.jpg'
|
img = "unknown.jpg"
|
||||||
|
|
||||||
self.mclient.fput_object(
|
self.mclient.fput_object(
|
||||||
self.bucket, u['id'], os.path.join(app.root_path,"../custom/avatars/"+img),
|
self.bucket,
|
||||||
|
u["id"],
|
||||||
|
os.path.join(app.root_path, "../custom/avatars/" + img),
|
||||||
content_type="image/jpeg ",
|
content_type="image/jpeg ",
|
||||||
)
|
)
|
||||||
log.warning(' AVATARS: Updated avatar for user '+u['username']+' with role '+img.split('.')[0])
|
log.warning(
|
||||||
|
" AVATARS: Updated avatar for user "
|
||||||
|
+ u["username"]
|
||||||
|
+ " with role "
|
||||||
|
+ img.split(".")[0]
|
||||||
|
)
|
||||||
|
|
||||||
def _minio_set_realm(self):
|
def _minio_set_realm(self):
|
||||||
if not self.mclient.bucket_exists(self.bucket):
|
if not self.mclient.bucket_exists(self.bucket):
|
||||||
|
@ -67,4 +79,4 @@ class Avatars():
|
||||||
log.error(" AVATARS: Error occured when deleting avatar object: " + error)
|
log.error(" AVATARS: Error occured when deleting avatar object: " + error)
|
||||||
|
|
||||||
def get_users_without_image(self, users):
|
def get_users_without_image(self, users):
|
||||||
return [u for u in users if u['id'] and u['id'] not in self.minio_get_objects()]
|
return [u for u in users if u["id"] and u["id"] not in self.minio_get_objects()]
|
||||||
|
|
|
@ -1,20 +1,31 @@
|
||||||
#!flask/bin/python
|
#!flask/bin/python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
from admin import app
|
|
||||||
import logging as log
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from uuid import uuid4
|
|
||||||
import json
|
|
||||||
from time import sleep
|
|
||||||
import sys,os
|
|
||||||
from flask import render_template, Response, request, redirect, url_for, jsonify
|
|
||||||
from flask_socketio import SocketIO, emit, join_room, leave_room, \
|
|
||||||
close_room, rooms, disconnect, send
|
|
||||||
import base64
|
import base64
|
||||||
|
import json
|
||||||
|
import logging as log
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from time import sleep
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
class Events():
|
from flask import Response, jsonify, redirect, render_template, request, url_for
|
||||||
def __init__(self,title,text='',total=0,table=False,type='info'):
|
from flask_socketio import (
|
||||||
|
SocketIO,
|
||||||
|
close_room,
|
||||||
|
disconnect,
|
||||||
|
emit,
|
||||||
|
join_room,
|
||||||
|
leave_room,
|
||||||
|
rooms,
|
||||||
|
send,
|
||||||
|
)
|
||||||
|
|
||||||
|
from admin import app
|
||||||
|
|
||||||
|
|
||||||
|
class Events:
|
||||||
|
def __init__(self, title, text="", total=0, table=False, type="info"):
|
||||||
# notice, info, success, and error
|
# notice, info, success, and error
|
||||||
self.eid = str(base64.b64encode(os.urandom(32))[:8])
|
self.eid = str(base64.b64encode(os.urandom(32))[:8])
|
||||||
self.title = title
|
self.title = title
|
||||||
|
@ -26,85 +37,130 @@ class Events():
|
||||||
self.create()
|
self.create()
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
log.info('START '+self.eid+': '+self.text)
|
log.info("START " + self.eid + ": " + self.text)
|
||||||
app.socketio.emit('notify-create',
|
app.socketio.emit(
|
||||||
json.dumps({'id':self.eid,
|
"notify-create",
|
||||||
'title':self.title,
|
json.dumps(
|
||||||
'text':self.text,
|
{
|
||||||
'type':self.type}),
|
"id": self.eid,
|
||||||
namespace='/sio',
|
"title": self.title,
|
||||||
room='admin')
|
"text": self.text,
|
||||||
|
"type": self.type,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
namespace="/sio",
|
||||||
|
room="admin",
|
||||||
|
)
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
log.info('END '+self.eid+': '+self.text)
|
log.info("END " + self.eid + ": " + self.text)
|
||||||
app.socketio.emit('notify-destroy',
|
app.socketio.emit(
|
||||||
json.dumps({'id':self.eid}),
|
"notify-destroy",
|
||||||
namespace='/sio',
|
json.dumps({"id": self.eid}),
|
||||||
room='admin')
|
namespace="/sio",
|
||||||
|
room="admin",
|
||||||
|
)
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
def update_text(self, text):
|
def update_text(self, text):
|
||||||
self.text = text
|
self.text = text
|
||||||
app.socketio.emit('notify-update',
|
app.socketio.emit(
|
||||||
json.dumps({'id':self.eid,
|
"notify-update",
|
||||||
'text':self.text,}),
|
json.dumps(
|
||||||
namespace='/sio',
|
{
|
||||||
room='admin')
|
"id": self.eid,
|
||||||
|
"text": self.text,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
namespace="/sio",
|
||||||
|
room="admin",
|
||||||
|
)
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
def append_text(self, text):
|
def append_text(self, text):
|
||||||
self.text=self.text+'<br>'+text
|
self.text = self.text + "<br>" + text
|
||||||
app.socketio.emit('notify-update',
|
app.socketio.emit(
|
||||||
json.dumps({'id':self.eid,
|
"notify-update",
|
||||||
'text':self.text,}),
|
json.dumps(
|
||||||
namespace='/sio',
|
{
|
||||||
room='admin')
|
"id": self.eid,
|
||||||
|
"text": self.text,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
namespace="/sio",
|
||||||
|
room="admin",
|
||||||
|
)
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
def increment(self,data={'name':'','data':[]}):
|
def increment(self, data={"name": "", "data": []}):
|
||||||
self.item += 1
|
self.item += 1
|
||||||
log.info('INCREMENT '+self.eid+': '+self.text)
|
log.info("INCREMENT " + self.eid + ": " + self.text)
|
||||||
app.socketio.emit('notify-increment',
|
app.socketio.emit(
|
||||||
json.dumps({'id':self.eid,
|
"notify-increment",
|
||||||
'title':self.title,
|
json.dumps(
|
||||||
'text': '['+str(self.item)+'/'+str(self.total)+'] '+self.text+' '+data['name'],
|
{
|
||||||
'item':self.item,
|
"id": self.eid,
|
||||||
'total':self.total,
|
"title": self.title,
|
||||||
'table':self.table,
|
"text": "["
|
||||||
'type':self.type,
|
+ str(self.item)
|
||||||
'data':data}),
|
+ "/"
|
||||||
namespace='/sio',
|
+ str(self.total)
|
||||||
room='admin')
|
+ "] "
|
||||||
|
+ self.text
|
||||||
|
+ " "
|
||||||
|
+ data["name"],
|
||||||
|
"item": self.item,
|
||||||
|
"total": self.total,
|
||||||
|
"table": self.table,
|
||||||
|
"type": self.type,
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
namespace="/sio",
|
||||||
|
room="admin",
|
||||||
|
)
|
||||||
sleep(0.0001)
|
sleep(0.0001)
|
||||||
|
|
||||||
def decrement(self,data={'name':'','data':[]}):
|
def decrement(self, data={"name": "", "data": []}):
|
||||||
self.item -= 1
|
self.item -= 1
|
||||||
log.info('DECREMENT '+self.eid+': '+self.text)
|
log.info("DECREMENT " + self.eid + ": " + self.text)
|
||||||
app.socketio.emit('notify-decrement',
|
app.socketio.emit(
|
||||||
json.dumps({'id':self.eid,
|
"notify-decrement",
|
||||||
'title':self.title,
|
json.dumps(
|
||||||
'text': '['+str(self.item)+'/'+str(self.total)+'] '+self.text+' '+data['name'],
|
{
|
||||||
'item':self.item,
|
"id": self.eid,
|
||||||
'total':self.total,
|
"title": self.title,
|
||||||
'table':self.table,
|
"text": "["
|
||||||
'type':self.type,
|
+ str(self.item)
|
||||||
'data':data}),
|
+ "/"
|
||||||
namespace='/sio',
|
+ str(self.total)
|
||||||
room='admin')
|
+ "] "
|
||||||
|
+ self.text
|
||||||
|
+ " "
|
||||||
|
+ data["name"],
|
||||||
|
"item": self.item,
|
||||||
|
"total": self.total,
|
||||||
|
"table": self.table,
|
||||||
|
"type": self.type,
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
namespace="/sio",
|
||||||
|
room="admin",
|
||||||
|
)
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
def reload(self):
|
def reload(self):
|
||||||
app.socketio.emit('reload',
|
app.socketio.emit("reload", json.dumps({}), namespace="/sio", room="admin")
|
||||||
json.dumps({}),
|
|
||||||
namespace='/sio',
|
|
||||||
room='admin')
|
|
||||||
sleep(0.0001)
|
sleep(0.0001)
|
||||||
|
|
||||||
def table(self, event, table, data={}):
|
def table(self, event, table, data={}):
|
||||||
# refresh, add, delete, update
|
# refresh, add, delete, update
|
||||||
app.socketio.emit('table_'+event,
|
app.socketio.emit(
|
||||||
json.dumps({'table':table,'data':data}),
|
"table_" + event,
|
||||||
namespace='/sio',
|
json.dumps({"table": table, "data": data}),
|
||||||
room='admin')
|
namespace="/sio",
|
||||||
|
room="admin",
|
||||||
|
)
|
||||||
sleep(0.0001)
|
sleep(0.0001)
|
|
@ -3,5 +3,6 @@
|
||||||
class UserExists(Exception):
|
class UserExists(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UserNotFound(Exception):
|
class UserNotFound(Exception):
|
||||||
pass
|
pass
|
|
@ -1,55 +1,74 @@
|
||||||
import random, string
|
import random
|
||||||
from pprint import pprint
|
import string
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
|
||||||
def system_username(username):
|
def system_username(username):
|
||||||
return True if username in ['guest','ddadmin','admin'] or username.startswith('system_') else False
|
return (
|
||||||
|
True
|
||||||
|
if username in ["guest", "ddadmin", "admin"] or username.startswith("system_")
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def system_group(groupname):
|
def system_group(groupname):
|
||||||
return True if groupname in ['admin','manager','teacher','student'] else False
|
return True if groupname in ["admin", "manager", "teacher", "student"] else False
|
||||||
|
|
||||||
|
|
||||||
def get_group_from_group_id(group_id, groups):
|
def get_group_from_group_id(group_id, groups):
|
||||||
return next((d for d in groups if d.get('id') == group_id), None)
|
return next((d for d in groups if d.get("id") == group_id), None)
|
||||||
|
|
||||||
|
|
||||||
def get_gid_from_kgroup_id(kgroup_id, groups):
|
def get_gid_from_kgroup_id(kgroup_id, groups):
|
||||||
# print(kgroup_id)
|
# print(kgroup_id)
|
||||||
# pprint(groups)
|
# pprint(groups)
|
||||||
# return get_group_from_group_id(kgroup_id,groups)['path'].replace('/','.')[1:]
|
# return get_group_from_group_id(kgroup_id,groups)['path'].replace('/','.')[1:]
|
||||||
return [g['path'].replace('/','.')[1:] for g in groups if g['id'] == kgroup_id][0]
|
return [g["path"].replace("/", ".")[1:] for g in groups if g["id"] == kgroup_id][0]
|
||||||
|
|
||||||
|
|
||||||
def get_gids_from_kgroup_ids(kgroup_ids, groups):
|
def get_gids_from_kgroup_ids(kgroup_ids, groups):
|
||||||
return [get_gid_from_kgroup_id(kgroup_id, groups) for kgroup_id in kgroup_ids]
|
return [get_gid_from_kgroup_id(kgroup_id, groups) for kgroup_id in kgroup_ids]
|
||||||
|
|
||||||
|
|
||||||
def kpath2gid(path):
|
def kpath2gid(path):
|
||||||
# print(path.replace('/','.')[1:])
|
# print(path.replace('/','.')[1:])
|
||||||
return path.replace('/','.')[1:]
|
return path.replace("/", ".")[1:]
|
||||||
|
|
||||||
|
|
||||||
def gid2kpath(gid):
|
def gid2kpath(gid):
|
||||||
return '/'+gid.replace('.','/')
|
return "/" + gid.replace(".", "/")
|
||||||
|
|
||||||
|
|
||||||
def count_repeated(itemslist):
|
def count_repeated(itemslist):
|
||||||
print(Counter(itemslist))
|
print(Counter(itemslist))
|
||||||
|
|
||||||
|
|
||||||
def groups_kname2gid(groups):
|
def groups_kname2gid(groups):
|
||||||
return [name.replace('.','/') for name in groups]
|
return [name.replace(".", "/") for name in groups]
|
||||||
|
|
||||||
|
|
||||||
def groups_path2id(groups):
|
def groups_path2id(groups):
|
||||||
return [g.replace('/','.')[1:] for g in groups]
|
return [g.replace("/", ".")[1:] for g in groups]
|
||||||
|
|
||||||
|
|
||||||
def groups_id2path(groups):
|
def groups_id2path(groups):
|
||||||
return ['/'+g.replace('.','/') for g in groups]
|
return ["/" + g.replace(".", "/") for g in groups]
|
||||||
|
|
||||||
|
|
||||||
def filter_roles_list(role_list):
|
def filter_roles_list(role_list):
|
||||||
client_roles=['admin','manager','teacher','student']
|
client_roles = ["admin", "manager", "teacher", "student"]
|
||||||
return [r for r in role_list if r in client_roles]
|
return [r for r in role_list if r in client_roles]
|
||||||
|
|
||||||
|
|
||||||
def filter_roles_listofdicts(role_listofdicts):
|
def filter_roles_listofdicts(role_listofdicts):
|
||||||
client_roles=['admin','manager','teacher','student']
|
client_roles = ["admin", "manager", "teacher", "student"]
|
||||||
return [r for r in role_listofdicts if r['name'] in client_roles]
|
return [r for r in role_listofdicts if r["name"] in client_roles]
|
||||||
|
|
||||||
|
|
||||||
def rand_password(lenght):
|
def rand_password(lenght):
|
||||||
characters = string.ascii_letters + string.digits + string.punctuation
|
characters = string.ascii_letters + string.digits + string.punctuation
|
||||||
passwd = ''.join(random.choice(characters) for i in range(lenght))
|
passwd = "".join(random.choice(characters) for i in range(lenght))
|
||||||
while not any(ele.isupper() for ele in passwd):
|
while not any(ele.isupper() for ele in passwd):
|
||||||
passwd = ''.join(random.choice(characters) for i in range(lenght))
|
passwd = "".join(random.choice(characters) for i in range(lenght))
|
||||||
return passwd
|
return passwd
|
|
@ -1,44 +1,58 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
import time ,os
|
import json
|
||||||
from admin import app
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import logging as log
|
import logging as log
|
||||||
|
import os
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import yaml, json
|
from datetime import datetime, timedelta
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
|
import yaml
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
from keycloak import KeycloakAdmin
|
from keycloak import KeycloakAdmin
|
||||||
|
|
||||||
|
from admin import app
|
||||||
|
|
||||||
from .postgres import Postgres
|
from .postgres import Postgres
|
||||||
|
|
||||||
class KeycloakClient():
|
|
||||||
|
class KeycloakClient:
|
||||||
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
|
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
|
||||||
https://github.com/marcospereirampj/python-keycloak
|
https://github.com/marcospereirampj/python-keycloak
|
||||||
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
|
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
|
||||||
"""
|
"""
|
||||||
def __init__(self,
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
url="http://isard-sso-keycloak:8080/auth/",
|
url="http://isard-sso-keycloak:8080/auth/",
|
||||||
username=os.environ['KEYCLOAK_USER'],
|
username=os.environ["KEYCLOAK_USER"],
|
||||||
password=os.environ['KEYCLOAK_PASSWORD'],
|
password=os.environ["KEYCLOAK_PASSWORD"],
|
||||||
realm='master',
|
realm="master",
|
||||||
verify=True):
|
verify=True,
|
||||||
|
):
|
||||||
self.url = url
|
self.url = url
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.realm = realm
|
self.realm = realm
|
||||||
self.verify = verify
|
self.verify = verify
|
||||||
|
|
||||||
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD'])
|
self.keycloak_pg = Postgres(
|
||||||
|
"isard-apps-postgresql",
|
||||||
|
"keycloak",
|
||||||
|
os.environ["KEYCLOAK_DB_USER"],
|
||||||
|
os.environ["KEYCLOAK_DB_PASSWORD"],
|
||||||
|
)
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
self.keycloak_admin = KeycloakAdmin(server_url=self.url,
|
self.keycloak_admin = KeycloakAdmin(
|
||||||
|
server_url=self.url,
|
||||||
username=self.username,
|
username=self.username,
|
||||||
password=self.password,
|
password=self.password,
|
||||||
realm_name=self.realm,
|
realm_name=self.realm,
|
||||||
verify=self.verify)
|
verify=self.verify,
|
||||||
|
)
|
||||||
|
|
||||||
# from keycloak import KeycloakAdmin
|
# from keycloak import KeycloakAdmin
|
||||||
# keycloak_admin = KeycloakAdmin(server_url="http://isard-sso-keycloak:8080/auth/",username="admin",password="keycloakkeycloak",realm_name="master",verify=False)
|
# keycloak_admin = KeycloakAdmin(server_url="http://isard-sso-keycloak:8080/auth/",username="admin",password="keycloakkeycloak",realm_name="master",verify=False)
|
||||||
|
|
||||||
|
@ -60,7 +74,7 @@ class KeycloakClient():
|
||||||
# self.add_role('superman')
|
# self.add_role('superman')
|
||||||
# pprint(self.get_roles())
|
# pprint(self.get_roles())
|
||||||
|
|
||||||
''' USERS '''
|
""" USERS """
|
||||||
|
|
||||||
def get_user_id(self, username):
|
def get_user_id(self, username):
|
||||||
self.connect()
|
self.connect()
|
||||||
|
@ -87,22 +101,29 @@ class KeycloakClient():
|
||||||
|
|
||||||
(headers, users) = self.keycloak_pg.select_with_headers(q)
|
(headers, users) = self.keycloak_pg.select_with_headers(q)
|
||||||
|
|
||||||
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
|
users_with_lists = [
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
|
list(l[:-4])
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
|
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
|
||||||
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
|
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
|
||||||
|
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
|
||||||
|
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||||
|
for l in users
|
||||||
|
]
|
||||||
|
|
||||||
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
|
users_with_lists = [
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
|
list(l[:-4])
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
|
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
|
||||||
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
|
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
|
||||||
|
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
|
||||||
|
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||||
|
for l in users_with_lists
|
||||||
|
]
|
||||||
|
|
||||||
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
|
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
|
||||||
|
|
||||||
# self.connect()
|
# self.connect()
|
||||||
# groups = self.keycloak_admin.get_groups()
|
# groups = self.keycloak_admin.get_groups()
|
||||||
|
|
||||||
|
|
||||||
# for user in list_dict_users:
|
# for user in list_dict_users:
|
||||||
# new_user_groups = []
|
# new_user_groups = []
|
||||||
# for group_id in user['group']:
|
# for group_id in user['group']:
|
||||||
|
@ -113,15 +134,13 @@ class KeycloakClient():
|
||||||
# user['group']=new_user_groups
|
# user['group']=new_user_groups
|
||||||
return list_dict_users
|
return list_dict_users
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def getparent(self, group_id, data):
|
def getparent(self, group_id, data):
|
||||||
# Recursively get full path from any group_id in the tree
|
# Recursively get full path from any group_id in the tree
|
||||||
path = ""
|
path = ""
|
||||||
for item in data:
|
for item in data:
|
||||||
if group_id == item[0]:
|
if group_id == item[0]:
|
||||||
path = self.getparent(item[2], data)
|
path = self.getparent(item[2], data)
|
||||||
path = f'{path}/{item[1]}'
|
path = f"{path}/{item[1]}"
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def get_group_path(self, group_id):
|
def get_group_path(self, group_id):
|
||||||
|
@ -134,7 +153,9 @@ class KeycloakClient():
|
||||||
def get_user_groups_paths(self, user_id):
|
def get_user_groups_paths(self, user_id):
|
||||||
# Get full paths for user grups
|
# Get full paths for user grups
|
||||||
# RETURNS list of paths
|
# RETURNS list of paths
|
||||||
q = """SELECT group_id FROM user_group_membership WHERE user_id = '%s'""" % (user_id)
|
q = """SELECT group_id FROM user_group_membership WHERE user_id = '%s'""" % (
|
||||||
|
user_id
|
||||||
|
)
|
||||||
user_group_ids = self.keycloak_pg.select(q)
|
user_group_ids = self.keycloak_pg.select(q)
|
||||||
|
|
||||||
paths = []
|
paths = []
|
||||||
|
@ -151,49 +172,69 @@ class KeycloakClient():
|
||||||
# user['roles']= [r['name'] for r in self.keycloak_admin.get_realm_roles_of_user(user_id=user['id'])]
|
# user['roles']= [r['name'] for r in self.keycloak_admin.get_realm_roles_of_user(user_id=user['id'])]
|
||||||
# return users
|
# return users
|
||||||
|
|
||||||
def add_user(self,username,first,last,email,password,group=False,temporary=True,enabled=True):
|
def add_user(
|
||||||
|
self,
|
||||||
|
username,
|
||||||
|
first,
|
||||||
|
last,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
group=False,
|
||||||
|
temporary=True,
|
||||||
|
enabled=True,
|
||||||
|
):
|
||||||
# RETURNS string with keycloak user id (the main id in this app)
|
# RETURNS string with keycloak user id (the main id in this app)
|
||||||
self.connect()
|
self.connect()
|
||||||
username = username.lower()
|
username = username.lower()
|
||||||
try:
|
try:
|
||||||
uid=self.keycloak_admin.create_user({"email": email,
|
uid = self.keycloak_admin.create_user(
|
||||||
|
{
|
||||||
|
"email": email,
|
||||||
"username": username,
|
"username": username,
|
||||||
"enabled": enabled,
|
"enabled": enabled,
|
||||||
"firstName": first,
|
"firstName": first,
|
||||||
"lastName": last,
|
"lastName": last,
|
||||||
"credentials":[{"type":"password",
|
"credentials": [
|
||||||
"value":password,
|
{"type": "password", "value": password, "temporary": temporary}
|
||||||
"temporary":temporary}]})
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
|
|
||||||
if group:
|
if group:
|
||||||
path = '/'+group if group[1:] != '/' else group
|
path = "/" + group if group[1:] != "/" else group
|
||||||
try:
|
try:
|
||||||
gid=self.keycloak_admin.get_group_by_path(path=path,search_in_subgroups=False)['id']
|
gid = self.keycloak_admin.get_group_by_path(
|
||||||
|
path=path, search_in_subgroups=False
|
||||||
|
)["id"]
|
||||||
except:
|
except:
|
||||||
self.keycloak_admin.create_group({"name": group})
|
self.keycloak_admin.create_group({"name": group})
|
||||||
gid=self.keycloak_admin.get_group_by_path(path)['id']
|
gid = self.keycloak_admin.get_group_by_path(path)["id"]
|
||||||
self.keycloak_admin.group_user_add(uid, gid)
|
self.keycloak_admin.group_user_add(uid, gid)
|
||||||
return uid
|
return uid
|
||||||
|
|
||||||
def update_user_pwd(self, user_id, password, temporary=True):
|
def update_user_pwd(self, user_id, password, temporary=True):
|
||||||
# Updates
|
# Updates
|
||||||
payload={"credentials":[{"type":"password",
|
payload = {
|
||||||
"value":password,
|
"credentials": [
|
||||||
"temporary":temporary}]}
|
{"type": "password", "value": password, "temporary": temporary}
|
||||||
|
]
|
||||||
|
}
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.update_user(user_id, payload)
|
return self.keycloak_admin.update_user(user_id, payload)
|
||||||
|
|
||||||
def user_update(self, user_id, enabled, email, first, last, groups=[], roles=[]):
|
def user_update(self, user_id, enabled, email, first, last, groups=[], roles=[]):
|
||||||
## NOTE: Roles didn't seem to be updated/added. Also not confident with groups
|
## NOTE: Roles didn't seem to be updated/added. Also not confident with groups
|
||||||
# Updates
|
# Updates
|
||||||
payload={"enabled":enabled,
|
payload = {
|
||||||
|
"enabled": enabled,
|
||||||
"email": email,
|
"email": email,
|
||||||
"firstName": first,
|
"firstName": first,
|
||||||
"lastName": last,
|
"lastName": last,
|
||||||
"groups": groups,
|
"groups": groups,
|
||||||
"realmRoles":roles}
|
"realmRoles": roles,
|
||||||
|
}
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.update_user(user_id, payload)
|
return self.keycloak_admin.update_user(user_id, payload)
|
||||||
|
|
||||||
|
@ -217,7 +258,11 @@ class KeycloakClient():
|
||||||
|
|
||||||
def remove_user_realm_roles(self, user_id, roles):
|
def remove_user_realm_roles(self, user_id, roles):
|
||||||
self.connect()
|
self.connect()
|
||||||
roles = [r for r in self.get_user_realm_roles(user_id) if r['name'] in ['admin','manager','teacher','student']]
|
roles = [
|
||||||
|
r
|
||||||
|
for r in self.get_user_realm_roles(user_id)
|
||||||
|
if r["name"] in ["admin", "manager", "teacher", "student"]
|
||||||
|
]
|
||||||
return self.keycloak_admin.delete_realm_roles_of_user(user_id, roles)
|
return self.keycloak_admin.delete_realm_roles_of_user(user_id, roles)
|
||||||
|
|
||||||
def delete_user(self, userid):
|
def delete_user(self, userid):
|
||||||
|
@ -234,7 +279,9 @@ class KeycloakClient():
|
||||||
|
|
||||||
def add_user_client_role(self, client_id, user_id, role_id, role_name):
|
def add_user_client_role(self, client_id, user_id, role_id, role_name):
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.assign_client_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test")
|
return self.keycloak_admin.assign_client_role(
|
||||||
|
client_id=client_id, user_id=user_id, role_id=role_id, role_name="test"
|
||||||
|
)
|
||||||
|
|
||||||
## GROUPS
|
## GROUPS
|
||||||
def get_all_groups(self):
|
def get_all_groups(self):
|
||||||
|
@ -246,7 +293,7 @@ class KeycloakClient():
|
||||||
for d_group in l_groups:
|
for d_group in l_groups:
|
||||||
d = {}
|
d = {}
|
||||||
for key, value in d_group.items():
|
for key, value in d_group.items():
|
||||||
if key == 'subGroups':
|
if key == "subGroups":
|
||||||
self.get_recursive_groups(value, l)
|
self.get_recursive_groups(value, l)
|
||||||
else:
|
else:
|
||||||
d[key] = value
|
d[key] = value
|
||||||
|
@ -263,8 +310,8 @@ class KeycloakClient():
|
||||||
# This needs to be recursive function
|
# This needs to be recursive function
|
||||||
if with_subgroups:
|
if with_subgroups:
|
||||||
for group in groups:
|
for group in groups:
|
||||||
if len(group['subGroups']):
|
if len(group["subGroups"]):
|
||||||
for sg in group['subGroups']:
|
for sg in group["subGroups"]:
|
||||||
subgroups.append(sg)
|
subgroups.append(sg)
|
||||||
# for sgroup in subgroups:
|
# for sgroup in subgroups:
|
||||||
# if len(sgroup['subGroups']):
|
# if len(sgroup['subGroups']):
|
||||||
|
@ -279,12 +326,14 @@ class KeycloakClient():
|
||||||
|
|
||||||
def get_group_by_path(self, path, recursive=True):
|
def get_group_by_path(self, path, recursive=True):
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.get_group_by_path(path=path,search_in_subgroups=recursive)
|
return self.keycloak_admin.get_group_by_path(
|
||||||
|
path=path, search_in_subgroups=recursive
|
||||||
|
)
|
||||||
|
|
||||||
def add_group(self, name, parent=None, skip_exists=False):
|
def add_group(self, name, parent=None, skip_exists=False):
|
||||||
self.connect()
|
self.connect()
|
||||||
if parent != None:
|
if parent != None:
|
||||||
parent=self.get_group_by_path(parent)['id']
|
parent = self.get_group_by_path(parent)["id"]
|
||||||
return self.keycloak_admin.create_group({"name": name}, parent=parent)
|
return self.keycloak_admin.create_group({"name": name}, parent=parent)
|
||||||
|
|
||||||
def delete_group(self, group_id):
|
def delete_group(self, group_id):
|
||||||
|
@ -296,20 +345,20 @@ class KeycloakClient():
|
||||||
return self.keycloak_admin.group_user_add(user_id, group_id)
|
return self.keycloak_admin.group_user_add(user_id, group_id)
|
||||||
|
|
||||||
def add_group_tree(self, path):
|
def add_group_tree(self, path):
|
||||||
parts=path.split('/')
|
parts = path.split("/")
|
||||||
parent_path='/'
|
parent_path = "/"
|
||||||
for i in range(1, len(parts)):
|
for i in range(1, len(parts)):
|
||||||
if i == 1:
|
if i == 1:
|
||||||
try:
|
try:
|
||||||
self.add_group(parts[i], None, skip_exists=True)
|
self.add_group(parts[i], None, skip_exists=True)
|
||||||
except:
|
except:
|
||||||
log.warning('KEYCLOAK: Group :'+parts[i]+ ' already exists.')
|
log.warning("KEYCLOAK: Group :" + parts[i] + " already exists.")
|
||||||
parent_path = parent_path + parts[i]
|
parent_path = parent_path + parts[i]
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.add_group(parts[i], parent_path, skip_exists=True)
|
self.add_group(parts[i], parent_path, skip_exists=True)
|
||||||
except:
|
except:
|
||||||
log.warning('KEYCLOAK: Group :'+parts[i]+ ' already exists.')
|
log.warning("KEYCLOAK: Group :" + parts[i] + " already exists.")
|
||||||
parent_path = parent_path + parts[i]
|
parent_path = parent_path + parts[i]
|
||||||
|
|
||||||
# parts=path.split('/')
|
# parts=path.split('/')
|
||||||
|
@ -339,48 +388,65 @@ class KeycloakClient():
|
||||||
# continue
|
# continue
|
||||||
# self.add_group(parts[i],parent_id)
|
# self.add_group(parts[i],parent_id)
|
||||||
|
|
||||||
def add_user_with_groups_and_role(self,username,first,last,email,password,role,groups):
|
def add_user_with_groups_and_role(
|
||||||
|
self, username, first, last, email, password, role, groups
|
||||||
|
):
|
||||||
## Add user
|
## Add user
|
||||||
uid = self.add_user(username, first, last, email, password)
|
uid = self.add_user(username, first, last, email, password)
|
||||||
## Add user to role
|
## Add user to role
|
||||||
log.info('User uid: '+str(uid)+ ' role: '+str(role))
|
log.info("User uid: " + str(uid) + " role: " + str(role))
|
||||||
try:
|
try:
|
||||||
therole = role[0]
|
therole = role[0]
|
||||||
except:
|
except:
|
||||||
therole=''
|
therole = ""
|
||||||
log.info(self.assign_realm_roles(uid, role))
|
log.info(self.assign_realm_roles(uid, role))
|
||||||
## Create groups in user
|
## Create groups in user
|
||||||
for g in groups:
|
for g in groups:
|
||||||
log.warning('Creating keycloak group: '+g)
|
log.warning("Creating keycloak group: " + g)
|
||||||
parts=g.split('/')
|
parts = g.split("/")
|
||||||
parent_path = None
|
parent_path = None
|
||||||
for i in range(1, len(parts)):
|
for i in range(1, len(parts)):
|
||||||
# parent_id=None if parent_path==None else self.get_group(parent_path)['id']
|
# parent_id=None if parent_path==None else self.get_group(parent_path)['id']
|
||||||
try:
|
try:
|
||||||
self.add_group(parts[i], parent_path, skip_exists=True)
|
self.add_group(parts[i], parent_path, skip_exists=True)
|
||||||
except:
|
except:
|
||||||
log.warning('Group '+str(parent_path)+ ' already exists. Skipping creation')
|
log.warning(
|
||||||
|
"Group "
|
||||||
|
+ str(parent_path)
|
||||||
|
+ " already exists. Skipping creation"
|
||||||
|
)
|
||||||
pass
|
pass
|
||||||
if parent_path is None:
|
if parent_path is None:
|
||||||
thepath='/'+parts[i]
|
thepath = "/" + parts[i]
|
||||||
else:
|
else:
|
||||||
thepath=parent_path+'/'+parts[i]
|
thepath = parent_path + "/" + parts[i]
|
||||||
if thepath=='/':
|
if thepath == "/":
|
||||||
log.warning('Not adding the user '+username+' to any group as does not have any...')
|
log.warning(
|
||||||
|
"Not adding the user "
|
||||||
|
+ username
|
||||||
|
+ " to any group as does not have any..."
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
gid=self.get_group_by_path(path=thepath)['id']
|
gid = self.get_group_by_path(path=thepath)["id"]
|
||||||
|
|
||||||
log.warning('Adding '+username+' with uuid: '+uid+' to group '+g+' with uuid: '+gid)
|
log.warning(
|
||||||
|
"Adding "
|
||||||
|
+ username
|
||||||
|
+ " with uuid: "
|
||||||
|
+ uid
|
||||||
|
+ " to group "
|
||||||
|
+ g
|
||||||
|
+ " with uuid: "
|
||||||
|
+ gid
|
||||||
|
)
|
||||||
self.keycloak_admin.group_user_add(uid, gid)
|
self.keycloak_admin.group_user_add(uid, gid)
|
||||||
|
|
||||||
|
if parent_path == None:
|
||||||
if parent_path==None: parent_path=''
|
parent_path = ""
|
||||||
parent_path=parent_path+'/'+parts[i]
|
parent_path = parent_path + "/" + parts[i]
|
||||||
|
|
||||||
|
|
||||||
# self.group_user_add(uid,gid)
|
# self.group_user_add(uid,gid)
|
||||||
|
|
||||||
|
|
||||||
## ROLES
|
## ROLES
|
||||||
def get_roles(self):
|
def get_roles(self):
|
||||||
self.connect()
|
self.connect()
|
||||||
|
@ -390,25 +456,27 @@ class KeycloakClient():
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.get_realm_role(name)
|
return self.keycloak_admin.get_realm_role(name)
|
||||||
|
|
||||||
def add_role(self,name,description=''):
|
def add_role(self, name, description=""):
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.create_realm_role({"name":name, "description":description})
|
return self.keycloak_admin.create_realm_role(
|
||||||
|
{"name": name, "description": description}
|
||||||
|
)
|
||||||
|
|
||||||
def delete_role(self, name):
|
def delete_role(self, name):
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.delete_realm_role(name)
|
return self.keycloak_admin.delete_realm_role(name)
|
||||||
|
|
||||||
|
|
||||||
## CLIENTS
|
## CLIENTS
|
||||||
|
|
||||||
def get_client_roles(self, client_id):
|
def get_client_roles(self, client_id):
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.get_client_roles(client_id=client_id)
|
return self.keycloak_admin.get_client_roles(client_id=client_id)
|
||||||
|
|
||||||
def add_client_role(self,client_id,name,description=''):
|
def add_client_role(self, client_id, name, description=""):
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.create_client_role(client_id, {'name': name, 'description':description, 'clientRole': True})
|
return self.keycloak_admin.create_client_role(
|
||||||
|
client_id, {"name": name, "description": description, "clientRole": True}
|
||||||
|
)
|
||||||
|
|
||||||
## SYSTEM
|
## SYSTEM
|
||||||
def get_server_info(self):
|
def get_server_info(self):
|
||||||
|
@ -421,14 +489,18 @@ class KeycloakClient():
|
||||||
|
|
||||||
def get_server_rsa_key(self):
|
def get_server_rsa_key(self):
|
||||||
self.connect()
|
self.connect()
|
||||||
rsa_key = [k for k in self.keycloak_admin.get_keys()['keys'] if k['type']=='RSA'][0]
|
rsa_key = [
|
||||||
return {'name':rsa_key['kid'],'certificate':rsa_key['certificate']}
|
k for k in self.keycloak_admin.get_keys()["keys"] if k["type"] == "RSA"
|
||||||
|
][0]
|
||||||
|
return {"name": rsa_key["kid"], "certificate": rsa_key["certificate"]}
|
||||||
|
|
||||||
## REALM
|
## REALM
|
||||||
def assign_realm_roles(self, user_id, role):
|
def assign_realm_roles(self, user_id, role):
|
||||||
self.connect()
|
self.connect()
|
||||||
try:
|
try:
|
||||||
role=[r for r in self.keycloak_admin.get_realm_roles() if r['name']==role]
|
role = [
|
||||||
|
r for r in self.keycloak_admin.get_realm_roles() if r["name"] == role
|
||||||
|
]
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
return self.keycloak_admin.assign_realm_roles(user_id=user_id, roles=role)
|
return self.keycloak_admin.assign_realm_roles(user_id=user_id, roles=role)
|
||||||
|
|
|
@ -1,24 +1,39 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
|
|
||||||
from admin import app
|
|
||||||
|
|
||||||
import os, sys
|
|
||||||
import logging as log
|
import logging as log
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
class loadConfig():
|
from admin import app
|
||||||
|
|
||||||
|
|
||||||
|
class loadConfig:
|
||||||
def __init__(self, app=None):
|
def __init__(self, app=None):
|
||||||
try:
|
try:
|
||||||
app.config.setdefault('DOMAIN', os.environ['DOMAIN'])
|
app.config.setdefault("DOMAIN", os.environ["DOMAIN"])
|
||||||
app.config.setdefault('KEYCLOAK_POSTGRES_USER', os.environ['KEYCLOAK_DB_USER'])
|
app.config.setdefault(
|
||||||
app.config.setdefault('KEYCLOAK_POSTGRES_PASSWORD', os.environ['KEYCLOAK_DB_PASSWORD'])
|
"KEYCLOAK_POSTGRES_USER", os.environ["KEYCLOAK_DB_USER"]
|
||||||
app.config.setdefault('MOODLE_POSTGRES_USER', os.environ['MOODLE_POSTGRES_USER'])
|
)
|
||||||
app.config.setdefault('MOODLE_POSTGRES_PASSWORD', os.environ['MOODLE_POSTGRES_PASSWORD'])
|
app.config.setdefault(
|
||||||
app.config.setdefault('NEXTCLOUD_POSTGRES_USER', os.environ['NEXTCLOUD_POSTGRES_USER'])
|
"KEYCLOAK_POSTGRES_PASSWORD", os.environ["KEYCLOAK_DB_PASSWORD"]
|
||||||
app.config.setdefault('NEXTCLOUD_POSTGRES_PASSWORD', os.environ['NEXTCLOUD_POSTGRES_PASSWORD'])
|
)
|
||||||
app.config.setdefault('VERIFY', True if os.environ['VERIFY']=="true" else False)
|
app.config.setdefault(
|
||||||
|
"MOODLE_POSTGRES_USER", os.environ["MOODLE_POSTGRES_USER"]
|
||||||
|
)
|
||||||
|
app.config.setdefault(
|
||||||
|
"MOODLE_POSTGRES_PASSWORD", os.environ["MOODLE_POSTGRES_PASSWORD"]
|
||||||
|
)
|
||||||
|
app.config.setdefault(
|
||||||
|
"NEXTCLOUD_POSTGRES_USER", os.environ["NEXTCLOUD_POSTGRES_USER"]
|
||||||
|
)
|
||||||
|
app.config.setdefault(
|
||||||
|
"NEXTCLOUD_POSTGRES_PASSWORD", os.environ["NEXTCLOUD_POSTGRES_PASSWORD"]
|
||||||
|
)
|
||||||
|
app.config.setdefault(
|
||||||
|
"VERIFY", True if os.environ["VERIFY"] == "true" else False
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
raise
|
raise
|
||||||
|
|
|
@ -1,35 +1,43 @@
|
||||||
|
import logging as log
|
||||||
|
import traceback
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
from requests import get, post
|
from requests import get, post
|
||||||
|
|
||||||
from admin import app
|
from admin import app
|
||||||
|
|
||||||
import logging as log
|
|
||||||
from pprint import pprint
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from .postgres import Postgres
|
|
||||||
from .exceptions import UserExists, UserNotFound
|
from .exceptions import UserExists, UserNotFound
|
||||||
|
from .postgres import Postgres
|
||||||
|
|
||||||
# Module variables to connect to moodle api
|
# Module variables to connect to moodle api
|
||||||
|
|
||||||
class Moodle():
|
|
||||||
|
class Moodle:
|
||||||
"""https://github.com/mrcinv/moodle_api.py
|
"""https://github.com/mrcinv/moodle_api.py
|
||||||
https://docs.moodle.org/dev/Web_service_API_functions
|
https://docs.moodle.org/dev/Web_service_API_functions
|
||||||
https://docs.moodle.org/311/en/Using_web_services
|
https://docs.moodle.org/311/en/Using_web_services
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
|
self,
|
||||||
key=app.config["MOODLE_WS_TOKEN"],
|
key=app.config["MOODLE_WS_TOKEN"],
|
||||||
url="https://moodle." + app.config["DOMAIN"],
|
url="https://moodle." + app.config["DOMAIN"],
|
||||||
endpoint="/webservice/rest/server.php",
|
endpoint="/webservice/rest/server.php",
|
||||||
verify=app.config["VERIFY"]):
|
verify=app.config["VERIFY"],
|
||||||
|
):
|
||||||
self.key = key
|
self.key = key
|
||||||
self.url = url
|
self.url = url
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
self.verify = verify
|
self.verify = verify
|
||||||
|
|
||||||
self.moodle_pg=Postgres('isard-apps-postgresql','moodle',app.config['MOODLE_POSTGRES_USER'],app.config['MOODLE_POSTGRES_PASSWORD'])
|
self.moodle_pg = Postgres(
|
||||||
|
"isard-apps-postgresql",
|
||||||
|
"moodle",
|
||||||
|
app.config["MOODLE_POSTGRES_USER"],
|
||||||
|
app.config["MOODLE_POSTGRES_PASSWORD"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def rest_api_parameters(self, in_args, prefix="", out_dict=None):
|
||||||
def rest_api_parameters(self, in_args, prefix='', out_dict=None):
|
|
||||||
"""Transform dictionary/array structure to a flat dictionary, with key names
|
"""Transform dictionary/array structure to a flat dictionary, with key names
|
||||||
defining the structure.
|
defining the structure.
|
||||||
Example usage:
|
Example usage:
|
||||||
|
@ -42,10 +50,10 @@ class Moodle():
|
||||||
if not type(in_args) in (list, dict):
|
if not type(in_args) in (list, dict):
|
||||||
out_dict[prefix] = in_args
|
out_dict[prefix] = in_args
|
||||||
return out_dict
|
return out_dict
|
||||||
if prefix == '':
|
if prefix == "":
|
||||||
prefix = prefix + '{0}'
|
prefix = prefix + "{0}"
|
||||||
else:
|
else:
|
||||||
prefix = prefix + '[{0}]'
|
prefix = prefix + "[{0}]"
|
||||||
if type(in_args) == list:
|
if type(in_args) == list:
|
||||||
for idx, item in enumerate(in_args):
|
for idx, item in enumerate(in_args):
|
||||||
self.rest_api_parameters(item, prefix.format(idx), out_dict)
|
self.rest_api_parameters(item, prefix.format(idx), out_dict)
|
||||||
|
@ -61,55 +69,70 @@ class Moodle():
|
||||||
courses = [{'id': 1, 'fullname': 'My favorite course'}])
|
courses = [{'id': 1, 'fullname': 'My favorite course'}])
|
||||||
"""
|
"""
|
||||||
parameters = self.rest_api_parameters(kwargs)
|
parameters = self.rest_api_parameters(kwargs)
|
||||||
parameters.update({"wstoken": self.key, 'moodlewsrestformat': 'json', "wsfunction": fname})
|
parameters.update(
|
||||||
|
{"wstoken": self.key, "moodlewsrestformat": "json", "wsfunction": fname}
|
||||||
|
)
|
||||||
response = post(self.url + self.endpoint, parameters, verify=self.verify)
|
response = post(self.url + self.endpoint, parameters, verify=self.verify)
|
||||||
response = response.json()
|
response = response.json()
|
||||||
if type(response) == dict and response.get('exception'):
|
if type(response) == dict and response.get("exception"):
|
||||||
raise SystemError(response)
|
raise SystemError(response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def create_user(self, email, username, password, first_name='-', last_name='-'):
|
def create_user(self, email, username, password, first_name="-", last_name="-"):
|
||||||
if len(self.get_user_by('username',username)['users']):
|
if len(self.get_user_by("username", username)["users"]):
|
||||||
raise UserExists
|
raise UserExists
|
||||||
try:
|
try:
|
||||||
data = [{'username': username, 'email':email,
|
data = [
|
||||||
'password': password, 'firstname':first_name, 'lastname':last_name}]
|
{
|
||||||
user = self.call('core_user_create_users', users=data)
|
"username": username,
|
||||||
|
"email": email,
|
||||||
|
"password": password,
|
||||||
|
"firstname": first_name,
|
||||||
|
"lastname": last_name,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
user = self.call("core_user_create_users", users=data)
|
||||||
return user # [{'id': 8, 'username': 'asdfw'}]
|
return user # [{'id': 8, 'username': 'asdfw'}]
|
||||||
except SystemError as se:
|
except SystemError as se:
|
||||||
raise SystemError(se.args[0]['message'])
|
raise SystemError(se.args[0]["message"])
|
||||||
|
|
||||||
def update_user(self, username, email, first_name, last_name, enabled=True):
|
def update_user(self, username, email, first_name, last_name, enabled=True):
|
||||||
user = self.get_user_by('username',username)['users'][0]
|
user = self.get_user_by("username", username)["users"][0]
|
||||||
if not len(user):
|
if not len(user):
|
||||||
raise UserNotFound
|
raise UserNotFound
|
||||||
try:
|
try:
|
||||||
data = [{'id':user['id'],'username': username, 'email':email,
|
data = [
|
||||||
'firstname':first_name, 'lastname':last_name,
|
{
|
||||||
'suspended':0 if enabled else 1}]
|
"id": user["id"],
|
||||||
user = self.call('core_user_update_users', users=data)
|
"username": username,
|
||||||
|
"email": email,
|
||||||
|
"firstname": first_name,
|
||||||
|
"lastname": last_name,
|
||||||
|
"suspended": 0 if enabled else 1,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
user = self.call("core_user_update_users", users=data)
|
||||||
return user
|
return user
|
||||||
except SystemError as se:
|
except SystemError as se:
|
||||||
raise SystemError(se.args[0]['message'])
|
raise SystemError(se.args[0]["message"])
|
||||||
|
|
||||||
def delete_user(self, user_id):
|
def delete_user(self, user_id):
|
||||||
user = self.call('core_user_delete_users', userids=[user_id])
|
user = self.call("core_user_delete_users", userids=[user_id])
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def delete_users(self, userids):
|
def delete_users(self, userids):
|
||||||
user = self.call('core_user_delete_users', userids=userids)
|
user = self.call("core_user_delete_users", userids=userids)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def get_user_by(self, key, value):
|
def get_user_by(self, key, value):
|
||||||
criteria = [{'key': key, 'value': value}]
|
criteria = [{"key": key, "value": value}]
|
||||||
try:
|
try:
|
||||||
user = self.call('core_user_get_users', criteria=criteria)
|
user = self.call("core_user_get_users", criteria=criteria)
|
||||||
except:
|
except:
|
||||||
raise SystemError("Error calling Moodle API\n", traceback.format_exc())
|
raise SystemError("Error calling Moodle API\n", traceback.format_exc())
|
||||||
return user
|
return user
|
||||||
# {'users': [{'id': 8, 'username': 'asdfw', 'firstname': 'afowie', 'lastname': 'aokjdnfwe', 'fullname': 'afowie aokjdnfwe', 'email': 'awfewe@ads.com', 'department': '', 'firstaccess': 0, 'lastaccess': 0, 'auth': 'manual', 'suspended': False, 'confirmed': True, 'lang': 'ca', 'theme': '', 'timezone': '99', 'mailformat': 1, 'profileimageurlsmall': 'https://moodle.mydomain.duckdns.org/theme/image.php/cbe/core/1630941606/u/f2', 'profileimageurl': 'https://DOMAIN/theme/image.php/cbe/core/1630941606/u/f1'}], 'warnings': []}
|
# {'users': [{'id': 8, 'username': 'asdfw', 'firstname': 'afowie', 'lastname': 'aokjdnfwe', 'fullname': 'afowie aokjdnfwe', 'email': 'awfewe@ads.com', 'department': '', 'firstaccess': 0, 'lastaccess': 0, 'auth': 'manual', 'suspended': False, 'confirmed': True, 'lang': 'ca', 'theme': '', 'timezone': '99', 'mailformat': 1, 'profileimageurlsmall': 'https://moodle.mydomain.duckdns.org/theme/image.php/cbe/core/1630941606/u/f2', 'profileimageurl': 'https://DOMAIN/theme/image.php/cbe/core/1630941606/u/f1'}], 'warnings': []}
|
||||||
|
|
||||||
|
|
||||||
def get_users_with_groups_and_roles(self):
|
def get_users_with_groups_and_roles(self):
|
||||||
q = """select u.id as id, username, firstname as first, lastname as last, email, json_agg(h.name) as groups, json_agg(r.shortname) as roles
|
q = """select u.id as id, username, firstname as first, lastname as last, email, json_agg(h.name) as groups, json_agg(r.shortname) as roles
|
||||||
from mdl_user as u
|
from mdl_user as u
|
||||||
|
@ -120,7 +143,12 @@ class Moodle():
|
||||||
where u.deleted = 0
|
where u.deleted = 0
|
||||||
group by u.id , username, first, last, email"""
|
group by u.id , username, first, last, email"""
|
||||||
(headers, users) = self.moodle_pg.select_with_headers(q)
|
(headers, users) = self.moodle_pg.select_with_headers(q)
|
||||||
users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
|
users_with_lists = [
|
||||||
|
list(l[:-2])
|
||||||
|
+ ([[]] if l[-2] == [None] else [list(set(l[-2]))])
|
||||||
|
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||||
|
for l in users
|
||||||
|
]
|
||||||
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
|
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
|
||||||
return list_dict_users
|
return list_dict_users
|
||||||
|
|
||||||
|
@ -134,26 +162,32 @@ class Moodle():
|
||||||
|
|
||||||
def enroll_user_to_course(self, user_id, course_id, role_id=5):
|
def enroll_user_to_course(self, user_id, course_id, role_id=5):
|
||||||
# 5 is student
|
# 5 is student
|
||||||
data = [{'roleid': role_id, 'userid': user_id, 'courseid': course_id}]
|
data = [{"roleid": role_id, "userid": user_id, "courseid": course_id}]
|
||||||
enrolment = self.call('enrol_manual_enrol_users', enrolments=data)
|
enrolment = self.call("enrol_manual_enrol_users", enrolments=data)
|
||||||
return enrolment
|
return enrolment
|
||||||
|
|
||||||
def get_quiz_attempt(self, quiz_id, user_id):
|
def get_quiz_attempt(self, quiz_id, user_id):
|
||||||
attempts = self.call('mod_quiz_get_user_attempts', quizid=quiz_id, userid=user_id)
|
attempts = self.call(
|
||||||
|
"mod_quiz_get_user_attempts", quizid=quiz_id, userid=user_id
|
||||||
|
)
|
||||||
return attempts
|
return attempts
|
||||||
|
|
||||||
def get_cohorts(self):
|
def get_cohorts(self):
|
||||||
cohorts = self.call('core_cohort_get_cohorts')
|
cohorts = self.call("core_cohort_get_cohorts")
|
||||||
return cohorts
|
return cohorts
|
||||||
|
|
||||||
def add_system_cohort(self,name,description='',visible=True):
|
def add_system_cohort(self, name, description="", visible=True):
|
||||||
visible = 1 if visible else 0
|
visible = 1 if visible else 0
|
||||||
data = [{'categorytype': {'type': 'system', 'value': ''},
|
data = [
|
||||||
'name': name,
|
{
|
||||||
'idnumber': name,
|
"categorytype": {"type": "system", "value": ""},
|
||||||
'description': description,
|
"name": name,
|
||||||
'visible': visible}]
|
"idnumber": name,
|
||||||
cohort = self.call('core_cohort_create_cohorts', cohorts=data)
|
"description": description,
|
||||||
|
"visible": visible,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
cohort = self.call("core_cohort_create_cohorts", cohorts=data)
|
||||||
return cohort
|
return cohort
|
||||||
|
|
||||||
# def add_users_to_cohort(self,users,cohort):
|
# def add_users_to_cohort(self,users,cohort):
|
||||||
|
@ -162,41 +196,49 @@ class Moodle():
|
||||||
# return user
|
# return user
|
||||||
|
|
||||||
def add_user_to_cohort(self, userid, cohortid):
|
def add_user_to_cohort(self, userid, cohortid):
|
||||||
members=[{'cohorttype':{'type':'id','value':cohortid},
|
members = [
|
||||||
'usertype':{'type':'id','value':userid}}]
|
{
|
||||||
user = self.call('core_cohort_add_cohort_members', members=members)
|
"cohorttype": {"type": "id", "value": cohortid},
|
||||||
|
"usertype": {"type": "id", "value": userid},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
user = self.call("core_cohort_add_cohort_members", members=members)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def delete_user_in_cohort(self, userid, cohortid):
|
def delete_user_in_cohort(self, userid, cohortid):
|
||||||
members=[{'cohortid':cohortid,
|
members = [{"cohortid": cohortid, "userid": userid}]
|
||||||
'userid':userid}]
|
user = self.call("core_cohort_delete_cohort_members", members=members)
|
||||||
user = self.call('core_cohort_delete_cohort_members', members=members)
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def get_cohort_members(self, cohort_ids):
|
def get_cohort_members(self, cohort_ids):
|
||||||
members = self.call('core_cohort_get_cohort_members', cohortids=cohort_ids)
|
members = self.call("core_cohort_get_cohort_members", cohortids=cohort_ids)
|
||||||
# [0]['userids']
|
# [0]['userids']
|
||||||
return members
|
return members
|
||||||
|
|
||||||
def delete_cohorts(self, cohortids):
|
def delete_cohorts(self, cohortids):
|
||||||
deleted = self.call('core_cohort_delete_cohorts', cohortids=cohortids)
|
deleted = self.call("core_cohort_delete_cohorts", cohortids=cohortids)
|
||||||
return deleted
|
return deleted
|
||||||
|
|
||||||
def get_user_cohorts(self, user_id):
|
def get_user_cohorts(self, user_id):
|
||||||
user_cohorts = []
|
user_cohorts = []
|
||||||
cohorts = self.get_cohorts()
|
cohorts = self.get_cohorts()
|
||||||
for cohort in cohorts:
|
for cohort in cohorts:
|
||||||
if user_id in self.get_cohort_members(cohort['id']): user_cohorts.append(cohort)
|
if user_id in self.get_cohort_members(cohort["id"]):
|
||||||
|
user_cohorts.append(cohort)
|
||||||
return user_cohorts
|
return user_cohorts
|
||||||
|
|
||||||
def add_user_to_siteadmin(self, user_id):
|
def add_user_to_siteadmin(self, user_id):
|
||||||
q = """SELECT value FROM mdl_config WHERE name='siteadmins'"""
|
q = """SELECT value FROM mdl_config WHERE name='siteadmins'"""
|
||||||
value = self.moodle_pg.select(q)[0][0]
|
value = self.moodle_pg.select(q)[0][0]
|
||||||
if str(user_id) not in value:
|
if str(user_id) not in value:
|
||||||
value=value+','+str(user_id)
|
value = value + "," + str(user_id)
|
||||||
q = """UPDATE mdl_config SET value = '%s' WHERE name='siteadmins'""" % (value)
|
q = """UPDATE mdl_config SET value = '%s' WHERE name='siteadmins'""" % (
|
||||||
|
value
|
||||||
|
)
|
||||||
self.moodle_pg.update(q)
|
self.moodle_pg.update(q)
|
||||||
log.warning('MOODLE:ADDING THE USER TO ADMINS: This needs a purge cache in moodle!')
|
log.warning(
|
||||||
|
"MOODLE:ADDING THE USER TO ADMINS: This needs a purge cache in moodle!"
|
||||||
|
)
|
||||||
|
|
||||||
# def add_role_to_user(self, user_id, role='admin', context='missing'):
|
# def add_role_to_user(self, user_id, role='admin', context='missing'):
|
||||||
# if role=='admin':
|
# if role=='admin':
|
||||||
|
@ -208,6 +250,7 @@ class Moodle():
|
||||||
# userid=user_id, role_id=role_id)
|
# userid=user_id, role_id=role_id)
|
||||||
# 'contextlevel': 1,
|
# 'contextlevel': 1,
|
||||||
|
|
||||||
|
|
||||||
# define('CONTEXT_SYSTEM', 10);
|
# define('CONTEXT_SYSTEM', 10);
|
||||||
# define('CONTEXT_USER', 30);
|
# define('CONTEXT_USER', 30);
|
||||||
# define('CONTEXT_COURSECAT', 40);
|
# define('CONTEXT_COURSECAT', 40);
|
||||||
|
|
|
@ -1,23 +1,22 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
|
import json
|
||||||
|
import logging as log
|
||||||
import time
|
import time
|
||||||
from admin import app
|
import traceback
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import logging as log
|
|
||||||
import traceback
|
|
||||||
import yaml, json
|
|
||||||
|
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
|
import yaml
|
||||||
|
|
||||||
class Mysql():
|
from admin import app
|
||||||
|
|
||||||
|
|
||||||
|
class Mysql:
|
||||||
def __init__(self, host, database, user, password):
|
def __init__(self, host, database, user, password):
|
||||||
self.conn = mysql.connector.connect(
|
self.conn = mysql.connector.connect(
|
||||||
host=host,
|
host=host, database=database, user=user, password=password
|
||||||
database=database,
|
)
|
||||||
user=user,
|
|
||||||
password=password)
|
|
||||||
|
|
||||||
def select(self, sql):
|
def select(self, sql):
|
||||||
self.cur = self.conn.cursor()
|
self.cur = self.conn.cursor()
|
||||||
|
|
|
@ -1,38 +1,63 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging as log
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
# from ..lib.log import *
|
# from ..lib.log import *
|
||||||
from admin import app
|
from admin import app
|
||||||
import time,requests,json,pprint,os
|
|
||||||
import urllib
|
|
||||||
import traceback
|
|
||||||
import logging as log
|
|
||||||
from .nextcloud_exc import *
|
|
||||||
|
|
||||||
|
from .nextcloud_exc import *
|
||||||
from .postgres import Postgres
|
from .postgres import Postgres
|
||||||
|
|
||||||
class Nextcloud():
|
|
||||||
def __init__(self,
|
class Nextcloud:
|
||||||
url="https://nextcloud."+app.config['DOMAIN'],
|
def __init__(
|
||||||
username=os.environ['NEXTCLOUD_ADMIN_USER'],
|
self,
|
||||||
password=os.environ['NEXTCLOUD_ADMIN_PASSWORD'],
|
url="https://nextcloud." + app.config["DOMAIN"],
|
||||||
verify=True):
|
username=os.environ["NEXTCLOUD_ADMIN_USER"],
|
||||||
|
password=os.environ["NEXTCLOUD_ADMIN_PASSWORD"],
|
||||||
|
verify=True,
|
||||||
|
):
|
||||||
|
|
||||||
self.verify_cert = verify
|
self.verify_cert = verify
|
||||||
self.apiurl=url+'/ocs/v1.php/cloud/'
|
self.apiurl = url + "/ocs/v1.php/cloud/"
|
||||||
self.shareurl=url+'/ocs/v2.php/apps/files_sharing/api/v1/'
|
self.shareurl = url + "/ocs/v2.php/apps/files_sharing/api/v1/"
|
||||||
self.davurl=url+'/remote.php/dav/files/'
|
self.davurl = url + "/remote.php/dav/files/"
|
||||||
self.auth = (username, password)
|
self.auth = (username, password)
|
||||||
self.user = username
|
self.user = username
|
||||||
|
|
||||||
self.nextcloud_pg=Postgres('isard-apps-postgresql','nextcloud',app.config['NEXTCLOUD_POSTGRES_USER'],app.config['NEXTCLOUD_POSTGRES_PASSWORD'])
|
self.nextcloud_pg = Postgres(
|
||||||
|
"isard-apps-postgresql",
|
||||||
|
"nextcloud",
|
||||||
|
app.config["NEXTCLOUD_POSTGRES_USER"],
|
||||||
|
app.config["NEXTCLOUD_POSTGRES_PASSWORD"],
|
||||||
|
)
|
||||||
|
|
||||||
def _request(self,method,url,data={},headers={'OCS-APIRequest':'true'},auth=False):
|
def _request(
|
||||||
if auth == False: auth=self.auth
|
self, method, url, data={}, headers={"OCS-APIRequest": "true"}, auth=False
|
||||||
|
):
|
||||||
|
if auth == False:
|
||||||
|
auth = self.auth
|
||||||
try:
|
try:
|
||||||
response = requests.request(method, url, data=data, auth=auth, verify=self.verify_cert, headers=headers)
|
response = requests.request(
|
||||||
if 'meta' in response.text:
|
method,
|
||||||
if '<statuscode>997</statuscode>' in response.text: raise ProviderUnauthorized
|
url,
|
||||||
|
data=data,
|
||||||
|
auth=auth,
|
||||||
|
verify=self.verify_cert,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
if "meta" in response.text:
|
||||||
|
if "<statuscode>997</statuscode>" in response.text:
|
||||||
|
raise ProviderUnauthorized
|
||||||
# if '<statuscode>998</statuscode>' in response.text: raise ProviderInvalidQuery
|
# if '<statuscode>998</statuscode>' in response.text: raise ProviderInvalidQuery
|
||||||
return response.text
|
return response.text
|
||||||
|
|
||||||
|
@ -48,15 +73,16 @@ class Nextcloud():
|
||||||
# except requests.exceptions.RequestException as err:
|
# except requests.exceptions.RequestException as err:
|
||||||
# raise ProviderError
|
# raise ProviderError
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if str(e) == 'an integer is required (got type bytes)':
|
if str(e) == "an integer is required (got type bytes)":
|
||||||
raise ProviderConnError
|
raise ProviderConnError
|
||||||
raise ProviderError
|
raise ProviderError
|
||||||
|
|
||||||
def check_connection(self):
|
def check_connection(self):
|
||||||
url = self.apiurl + "users/" + self.user + "?format=json"
|
url = self.apiurl + "users/" + self.user + "?format=json"
|
||||||
try:
|
try:
|
||||||
result = self._request('GET',url)
|
result = self._request("GET", url)
|
||||||
if json.loads(result)['ocs']['meta']['statuscode'] == 100: return True
|
if json.loads(result)["ocs"]["meta"]["statuscode"] == 100:
|
||||||
|
return True
|
||||||
raise ProviderError
|
raise ProviderError
|
||||||
except requests.exceptions.HTTPError as errh:
|
except requests.exceptions.HTTPError as errh:
|
||||||
raise ProviderConnError
|
raise ProviderConnError
|
||||||
|
@ -69,15 +95,16 @@ class Nextcloud():
|
||||||
except requests.exceptions.RequestException as err:
|
except requests.exceptions.RequestException as err:
|
||||||
raise ProviderError
|
raise ProviderError
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if str(e) == 'an integer is required (got type bytes)':
|
if str(e) == "an integer is required (got type bytes)":
|
||||||
raise ProviderConnError
|
raise ProviderConnError
|
||||||
raise ProviderError
|
raise ProviderError
|
||||||
|
|
||||||
def get_user(self, userid):
|
def get_user(self, userid):
|
||||||
url = self.apiurl + "users/" + userid + "?format=json"
|
url = self.apiurl + "users/" + userid + "?format=json"
|
||||||
try:
|
try:
|
||||||
result = json.loads(self._request('GET',url))
|
result = json.loads(self._request("GET", url))
|
||||||
if result['ocs']['meta']['statuscode'] == 100: return result['ocs']['data']
|
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||||
|
return result["ocs"]["data"]
|
||||||
raise ProviderItemNotExists
|
raise ProviderItemNotExists
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
|
@ -99,7 +126,6 @@ class Nextcloud():
|
||||||
# cur.close()
|
# cur.close()
|
||||||
# conn.close()
|
# conn.close()
|
||||||
|
|
||||||
|
|
||||||
# users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
|
# users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
|
||||||
# users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
|
# users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
|
||||||
# list_dict_users = [dict(zip(fields, r)) for r in users_with_lists]
|
# list_dict_users = [dict(zip(fields, r)) for r in users_with_lists]
|
||||||
|
@ -128,8 +154,18 @@ class Nextcloud():
|
||||||
left join oc_filecache as fc on fc.storage = numeric_id
|
left join oc_filecache as fc on fc.storage = numeric_id
|
||||||
group by u.uid, adn.value, ade.value, pref.configvalue"""
|
group by u.uid, adn.value, ade.value, pref.configvalue"""
|
||||||
(headers, users) = self.nextcloud_pg.select_with_headers(q)
|
(headers, users) = self.nextcloud_pg.select_with_headers(q)
|
||||||
users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
|
users_with_lists = [
|
||||||
users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
|
list(l[:-2])
|
||||||
|
+ ([[]] if l[-2] == [None] else [list(set(l[-2]))])
|
||||||
|
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||||
|
for l in users
|
||||||
|
]
|
||||||
|
users_with_lists = [
|
||||||
|
list(l[:-2])
|
||||||
|
+ ([[]] if l[-2] == [None] else [list(set(l[-2]))])
|
||||||
|
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||||
|
for l in users_with_lists
|
||||||
|
]
|
||||||
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
|
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
|
||||||
return list_dict_users
|
return list_dict_users
|
||||||
|
|
||||||
|
@ -145,27 +181,40 @@ class Nextcloud():
|
||||||
# log.error(traceback.format_exc())
|
# log.error(traceback.format_exc())
|
||||||
# raise
|
# raise
|
||||||
|
|
||||||
def add_user(self,userid,userpassword,quota=False,group=False,email='',displayname=''):
|
def add_user(
|
||||||
data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname}
|
self, userid, userpassword, quota=False, group=False, email="", displayname=""
|
||||||
if not group: del data['groups[]']
|
):
|
||||||
if not quota: del data['quota']
|
data = {
|
||||||
|
"userid": userid,
|
||||||
|
"password": userpassword,
|
||||||
|
"quota": quota,
|
||||||
|
"groups[]": group,
|
||||||
|
"email": email,
|
||||||
|
"displayname": displayname,
|
||||||
|
}
|
||||||
|
if not group:
|
||||||
|
del data["groups[]"]
|
||||||
|
if not quota:
|
||||||
|
del data["quota"]
|
||||||
# if group:
|
# if group:
|
||||||
# data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname}
|
# data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname}
|
||||||
# else:
|
# else:
|
||||||
# data={'userid':userid,'password':userpassword,'quota':quota,'email':email,'displayname':displayname}
|
# data={'userid':userid,'password':userpassword,'quota':quota,'email':email,'displayname':displayname}
|
||||||
url = self.apiurl + "users?format=json"
|
url = self.apiurl + "users?format=json"
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
'OCS-APIRequest': 'true',
|
"OCS-APIRequest": "true",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = json.loads(self._request('POST',url,data=data,headers=headers))
|
result = json.loads(self._request("POST", url, data=data, headers=headers))
|
||||||
if result['ocs']['meta']['statuscode'] == 100: return True
|
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||||
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists
|
return True
|
||||||
if result['ocs']['meta']['statuscode'] == 104:
|
if result["ocs"]["meta"]["statuscode"] == 102:
|
||||||
|
raise ProviderItemExists
|
||||||
|
if result["ocs"]["meta"]["statuscode"] == 104:
|
||||||
self.add_group(group)
|
self.add_group(group)
|
||||||
# raise ProviderGroupNotExists
|
# raise ProviderGroupNotExists
|
||||||
log.error('Get Nextcloud provider user add error: '+str(result))
|
log.error("Get Nextcloud provider user add error: " + str(result))
|
||||||
raise ProviderOpError
|
raise ProviderOpError
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
|
@ -184,85 +233,109 @@ class Nextcloud():
|
||||||
|
|
||||||
url = self.apiurl + "users/" + userid + "?format=json"
|
url = self.apiurl + "users/" + userid + "?format=json"
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
'OCS-APIRequest': 'true',
|
"OCS-APIRequest": "true",
|
||||||
}
|
}
|
||||||
for k, v in key_values.items():
|
for k, v in key_values.items():
|
||||||
data = {"key": k, "value": v}
|
data = {"key": k, "value": v}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = json.loads(self._request('PUT',url,data=data,headers=headers))
|
result = json.loads(
|
||||||
if result['ocs']['meta']['statuscode'] == 100: return True
|
self._request("PUT", url, data=data, headers=headers)
|
||||||
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists
|
)
|
||||||
if result['ocs']['meta']['statuscode'] == 104: raise ProviderGroupNotExists
|
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||||
log.error('Get Nextcloud provider user add error: '+str(result))
|
return True
|
||||||
|
if result["ocs"]["meta"]["statuscode"] == 102:
|
||||||
|
raise ProviderItemExists
|
||||||
|
if result["ocs"]["meta"]["statuscode"] == 104:
|
||||||
|
raise ProviderGroupNotExists
|
||||||
|
log.error("Get Nextcloud provider user add error: " + str(result))
|
||||||
raise ProviderOpError
|
raise ProviderOpError
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def add_user_to_group(self, userid, group_id):
|
def add_user_to_group(self, userid, group_id):
|
||||||
data={'groupid':group_id}
|
data = {"groupid": group_id}
|
||||||
|
|
||||||
url = self.apiurl + "users/" + userid + "/groups?format=json"
|
url = self.apiurl + "users/" + userid + "/groups?format=json"
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
'OCS-APIRequest': 'true',
|
"OCS-APIRequest": "true",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = json.loads(self._request('POST',url,data=data,headers=headers))
|
result = json.loads(self._request("POST", url, data=data, headers=headers))
|
||||||
if result['ocs']['meta']['statuscode'] == 100: return True
|
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||||
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists
|
return True
|
||||||
if result['ocs']['meta']['statuscode'] == 104: raise ProviderGroupNotExists
|
if result["ocs"]["meta"]["statuscode"] == 102:
|
||||||
log.error('Get Nextcloud provider user add error: '+str(result))
|
raise ProviderItemExists
|
||||||
|
if result["ocs"]["meta"]["statuscode"] == 104:
|
||||||
|
raise ProviderGroupNotExists
|
||||||
|
log.error("Get Nextcloud provider user add error: " + str(result))
|
||||||
raise ProviderOpError
|
raise ProviderOpError
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def remove_user_from_group(self, userid, group_id):
|
def remove_user_from_group(self, userid, group_id):
|
||||||
data={'groupid':group_id}
|
data = {"groupid": group_id}
|
||||||
|
|
||||||
url = self.apiurl + "users/" + userid + "/groups?format=json"
|
url = self.apiurl + "users/" + userid + "/groups?format=json"
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
'OCS-APIRequest': 'true',
|
"OCS-APIRequest": "true",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = json.loads(self._request('DELETE',url,data=data,headers=headers))
|
result = json.loads(
|
||||||
if result['ocs']['meta']['statuscode'] == 100: return True
|
self._request("DELETE", url, data=data, headers=headers)
|
||||||
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists
|
)
|
||||||
if result['ocs']['meta']['statuscode'] == 104:
|
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||||
|
return True
|
||||||
|
if result["ocs"]["meta"]["statuscode"] == 102:
|
||||||
|
raise ProviderItemExists
|
||||||
|
if result["ocs"]["meta"]["statuscode"] == 104:
|
||||||
self.add_group(group)
|
self.add_group(group)
|
||||||
# raise ProviderGroupNotExists
|
# raise ProviderGroupNotExists
|
||||||
log.error('Get Nextcloud provider user add error: '+str(result))
|
log.error("Get Nextcloud provider user add error: " + str(result))
|
||||||
raise ProviderOpError
|
raise ProviderOpError
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def add_user_with_groups(self,userid,userpassword,quota=False,groups=[],email='',displayname=''):
|
def add_user_with_groups(
|
||||||
data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':groups,'email':email,'displayname':displayname}
|
self, userid, userpassword, quota=False, groups=[], email="", displayname=""
|
||||||
|
):
|
||||||
|
data = {
|
||||||
|
"userid": userid,
|
||||||
|
"password": userpassword,
|
||||||
|
"quota": quota,
|
||||||
|
"groups[]": groups,
|
||||||
|
"email": email,
|
||||||
|
"displayname": displayname,
|
||||||
|
}
|
||||||
# if not group: del data['groups[]']
|
# if not group: del data['groups[]']
|
||||||
if not quota: del data['quota']
|
if not quota:
|
||||||
|
del data["quota"]
|
||||||
# if group:
|
# if group:
|
||||||
# data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname}
|
# data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname}
|
||||||
# else:
|
# else:
|
||||||
# data={'userid':userid,'password':userpassword,'quota':quota,'email':email,'displayname':displayname}
|
# data={'userid':userid,'password':userpassword,'quota':quota,'email':email,'displayname':displayname}
|
||||||
url = self.apiurl + "users?format=json"
|
url = self.apiurl + "users?format=json"
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
'OCS-APIRequest': 'true',
|
"OCS-APIRequest": "true",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = json.loads(self._request('POST',url,data=data,headers=headers))
|
result = json.loads(self._request("POST", url, data=data, headers=headers))
|
||||||
if result['ocs']['meta']['statuscode'] == 100: return True
|
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||||
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists
|
return True
|
||||||
if result['ocs']['meta']['statuscode'] == 104:
|
if result["ocs"]["meta"]["statuscode"] == 102:
|
||||||
|
raise ProviderItemExists
|
||||||
|
if result["ocs"]["meta"]["statuscode"] == 104:
|
||||||
# self.add_group(group)
|
# self.add_group(group)
|
||||||
None
|
None
|
||||||
# raise ProviderGroupNotExists
|
# raise ProviderGroupNotExists
|
||||||
log.error('Get Nextcloud provider user add error: '+str(result))
|
log.error("Get Nextcloud provider user add error: " + str(result))
|
||||||
raise ProviderOpError
|
raise ProviderOpError
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
|
@ -279,9 +352,11 @@ class Nextcloud():
|
||||||
def delete_user(self, userid):
|
def delete_user(self, userid):
|
||||||
url = self.apiurl + "users/" + userid + "?format=json"
|
url = self.apiurl + "users/" + userid + "?format=json"
|
||||||
try:
|
try:
|
||||||
result = json.loads(self._request('DELETE',url))
|
result = json.loads(self._request("DELETE", url))
|
||||||
if result['ocs']['meta']['statuscode'] == 100: return True
|
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||||
if result['ocs']['meta']['statuscode'] == 101: raise ProviderUserNotExists
|
return True
|
||||||
|
if result["ocs"]["meta"]["statuscode"] == 101:
|
||||||
|
raise ProviderUserNotExists
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
raise ProviderOpError
|
raise ProviderOpError
|
||||||
except:
|
except:
|
||||||
|
@ -296,74 +371,88 @@ class Nextcloud():
|
||||||
def disable_user(self, userid):
|
def disable_user(self, userid):
|
||||||
None
|
None
|
||||||
|
|
||||||
def exists_user_folder(self,userid,userpassword,folder='IsardVDI'):
|
def exists_user_folder(self, userid, userpassword, folder="IsardVDI"):
|
||||||
auth = (userid, userpassword)
|
auth = (userid, userpassword)
|
||||||
url = self.davurl + userid + "/" + folder + "?format=json"
|
url = self.davurl + userid + "/" + folder + "?format=json"
|
||||||
headers = {
|
headers = {
|
||||||
'Depth': '0',
|
"Depth": "0",
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
'OCS-APIRequest': 'true',
|
"OCS-APIRequest": "true",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = self._request('PROPFIND',url,auth=auth,headers=headers)
|
result = self._request("PROPFIND", url, auth=auth, headers=headers)
|
||||||
if '<d:status>HTTP/1.1 200 OK</d:status>' in result: return True
|
if "<d:status>HTTP/1.1 200 OK</d:status>" in result:
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def add_user_folder(self,userid,userpassword,folder='IsardVDI'):
|
def add_user_folder(self, userid, userpassword, folder="IsardVDI"):
|
||||||
auth = (userid, userpassword)
|
auth = (userid, userpassword)
|
||||||
url = self.davurl + userid + "/" + folder + "?format=json"
|
url = self.davurl + userid + "/" + folder + "?format=json"
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
'OCS-APIRequest': 'true',
|
"OCS-APIRequest": "true",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = self._request('MKCOL',url,auth=auth,headers=headers)
|
result = self._request("MKCOL", url, auth=auth, headers=headers)
|
||||||
if result=='': return True
|
if result == "":
|
||||||
if '<s:message>The resource you tried to create already exists</s:message>' in result: raise ProviderItemExists
|
return True
|
||||||
log.error(result.split('message>')[1].split('<')[0])
|
if (
|
||||||
|
"<s:message>The resource you tried to create already exists</s:message>"
|
||||||
|
in result
|
||||||
|
):
|
||||||
|
raise ProviderItemExists
|
||||||
|
log.error(result.split("message>")[1].split("<")[0])
|
||||||
raise ProviderOpError
|
raise ProviderOpError
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def exists_user_share_folder(self,userid,userpassword,folder='IsardVDI'):
|
def exists_user_share_folder(self, userid, userpassword, folder="IsardVDI"):
|
||||||
auth = (userid, userpassword)
|
auth = (userid, userpassword)
|
||||||
url = self.shareurl + "shares?format=json"
|
url = self.shareurl + "shares?format=json"
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
'OCS-APIRequest': 'true',
|
"OCS-APIRequest": "true",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = json.loads(self._request('GET', url, auth=auth, headers=headers))
|
result = json.loads(self._request("GET", url, auth=auth, headers=headers))
|
||||||
if result['ocs']['meta']['statuscode']==200:
|
if result["ocs"]["meta"]["statuscode"] == 200:
|
||||||
share=[s for s in result['ocs']['data'] if s['path'] == '/'+folder]
|
share = [s for s in result["ocs"]["data"] if s["path"] == "/" + folder]
|
||||||
if len(share) >= 1:
|
if len(share) >= 1:
|
||||||
# Should we delete all but the first (0) one?
|
# Should we delete all but the first (0) one?
|
||||||
return {'token': share[0]['token'],
|
return {"token": share[0]["token"], "url": share[0]["url"]}
|
||||||
'url': share[0]['url']}
|
|
||||||
raise ProviderItemNotExists
|
raise ProviderItemNotExists
|
||||||
raise ProviderOpError
|
raise ProviderOpError
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def add_user_share_folder(self,userid,userpassword,folder='IsardVDI'):
|
def add_user_share_folder(self, userid, userpassword, folder="IsardVDI"):
|
||||||
auth = (userid, userpassword)
|
auth = (userid, userpassword)
|
||||||
data={'path':'/'+folder,'shareType':3}
|
data = {"path": "/" + folder, "shareType": 3}
|
||||||
url = self.shareurl + "shares?format=json"
|
url = self.shareurl + "shares?format=json"
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
'OCS-APIRequest': 'true',
|
"OCS-APIRequest": "true",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = json.loads(self._request('POST',url, data=data, auth=auth, headers=headers))
|
result = json.loads(
|
||||||
if result['ocs']['meta']['statuscode'] == 100 or result['ocs']['meta']['statuscode'] == 200:
|
self._request("POST", url, data=data, auth=auth, headers=headers)
|
||||||
return {'token': result['ocs']['data']['token'],
|
)
|
||||||
'url': result['ocs']['data']['url']}
|
if (
|
||||||
log.error('Add user share folder error: '+result['ocs']['meta']['message'])
|
result["ocs"]["meta"]["statuscode"] == 100
|
||||||
|
or result["ocs"]["meta"]["statuscode"] == 200
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"token": result["ocs"]["data"]["token"],
|
||||||
|
"url": result["ocs"]["data"]["url"],
|
||||||
|
}
|
||||||
|
log.error(
|
||||||
|
"Add user share folder error: " + result["ocs"]["meta"]["message"]
|
||||||
|
)
|
||||||
raise ProviderFolderNotExists
|
raise ProviderFolderNotExists
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
|
@ -375,24 +464,29 @@ class Nextcloud():
|
||||||
def get_groups_list(self):
|
def get_groups_list(self):
|
||||||
url = self.apiurl + "groups?format=json"
|
url = self.apiurl + "groups?format=json"
|
||||||
try:
|
try:
|
||||||
result = json.loads(self._request('GET',url))
|
result = json.loads(self._request("GET", url))
|
||||||
if result['ocs']['meta']['statuscode'] == 100: return [g for g in result['ocs']['data']['groups']]
|
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||||
|
return [g for g in result["ocs"]["data"]["groups"]]
|
||||||
raise ProviderOpError
|
raise ProviderOpError
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def add_group(self, groupid):
|
def add_group(self, groupid):
|
||||||
data={'groupid':groupid}
|
data = {"groupid": groupid}
|
||||||
url = self.apiurl + "groups?format=json"
|
url = self.apiurl + "groups?format=json"
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
'OCS-APIRequest': 'true',
|
"OCS-APIRequest": "true",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = json.loads(self._request('POST',url, data=data, auth=self.auth, headers=headers))
|
result = json.loads(
|
||||||
if result['ocs']['meta']['statuscode'] == 100: return True
|
self._request("POST", url, data=data, auth=self.auth, headers=headers)
|
||||||
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists
|
)
|
||||||
|
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||||
|
return True
|
||||||
|
if result["ocs"]["meta"]["statuscode"] == 102:
|
||||||
|
raise ProviderItemExists
|
||||||
raise ProviderOpError
|
raise ProviderOpError
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
|
@ -403,15 +497,18 @@ class Nextcloud():
|
||||||
# 103 - failed to add the group
|
# 103 - failed to add the group
|
||||||
|
|
||||||
def delete_group(self, groupid):
|
def delete_group(self, groupid):
|
||||||
group = urllib.parse.quote(groupid, safe='')
|
group = urllib.parse.quote(groupid, safe="")
|
||||||
url = self.apiurl + "groups/" + group + "?format=json"
|
url = self.apiurl + "groups/" + group + "?format=json"
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
'OCS-APIRequest': 'true',
|
"OCS-APIRequest": "true",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = json.loads(self._request('DELETE',url, auth=self.auth, headers=headers))
|
result = json.loads(
|
||||||
if result['ocs']['meta']['statuscode'] == 100: return True
|
self._request("DELETE", url, auth=self.auth, headers=headers)
|
||||||
|
)
|
||||||
|
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||||
|
return True
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
raise ProviderOpError
|
raise ProviderOpError
|
||||||
except:
|
except:
|
||||||
|
@ -421,4 +518,3 @@ class Nextcloud():
|
||||||
# 101 - invalid input data
|
# 101 - invalid input data
|
||||||
# 102 - group already exists
|
# 102 - group already exists
|
||||||
# 103 - failed to add the group
|
# 103 - failed to add the group
|
||||||
|
|
||||||
|
|
|
@ -3,27 +3,35 @@
|
||||||
class ProviderUnauthorized(Exception):
|
class ProviderUnauthorized(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProviderConnError(Exception):
|
class ProviderConnError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProviderSslError(Exception):
|
class ProviderSslError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProviderConnTimeout(Exception):
|
class ProviderConnTimeout(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProviderError(Exception):
|
class ProviderError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProviderItemExists(Exception):
|
class ProviderItemExists(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProviderItemNotExists(Exception):
|
class ProviderItemNotExists(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProviderGroupNotExists(Exception):
|
class ProviderGroupNotExists(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProviderFolderNotExists(Exception):
|
class ProviderFolderNotExists(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
|
import json
|
||||||
|
import logging as log
|
||||||
import time
|
import time
|
||||||
from admin import app
|
import traceback
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import logging as log
|
|
||||||
import traceback
|
|
||||||
import yaml, json
|
|
||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
import yaml
|
||||||
|
|
||||||
class Postgres():
|
from admin import app
|
||||||
|
|
||||||
|
|
||||||
|
class Postgres:
|
||||||
def __init__(self, host, database, user, password):
|
def __init__(self, host, database, user, password):
|
||||||
self.conn = psycopg2.connect(
|
self.conn = psycopg2.connect(
|
||||||
host=host,
|
host=host, database=database, user=user, password=password
|
||||||
database=database,
|
)
|
||||||
user=user,
|
|
||||||
password=password)
|
|
||||||
|
|
||||||
|
|
||||||
# def __del__(self):
|
# def __del__(self):
|
||||||
# self.cur.close()
|
# self.cur.close()
|
||||||
|
|
|
@ -1,74 +1,111 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
import time, os
|
import json
|
||||||
from admin import app
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import logging as log
|
import logging as log
|
||||||
import traceback
|
import os
|
||||||
import yaml, json
|
import random
|
||||||
|
|
||||||
import psycopg2
|
|
||||||
|
|
||||||
from .postgres import Postgres
|
|
||||||
# from .keycloak import Keycloak
|
# from .keycloak import Keycloak
|
||||||
# from .moodle import Moodle
|
# from .moodle import Moodle
|
||||||
import string, random
|
import string
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
class Postup():
|
import psycopg2
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from admin import app
|
||||||
|
|
||||||
|
from .postgres import Postgres
|
||||||
|
|
||||||
|
|
||||||
|
class Postup:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
self.pg=Postgres('isard-apps-postgresql','moodle',app.config['MOODLE_POSTGRES_USER'],app.config['MOODLE_POSTGRES_PASSWORD'])
|
self.pg = Postgres(
|
||||||
|
"isard-apps-postgresql",
|
||||||
|
"moodle",
|
||||||
|
app.config["MOODLE_POSTGRES_USER"],
|
||||||
|
app.config["MOODLE_POSTGRES_PASSWORD"],
|
||||||
|
)
|
||||||
ready = True
|
ready = True
|
||||||
except:
|
except:
|
||||||
log.warning('Could not connect to moodle database. Retrying...')
|
log.warning("Could not connect to moodle database. Retrying...")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
log.info('Connected to moodle database.')
|
log.info("Connected to moodle database.")
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(app.root_path, "../moodledata/saml2/moodle."+app.config['DOMAIN']+".crt"),"r") as crt:
|
with open(
|
||||||
app.config.setdefault('SP_CRT', crt.read())
|
os.path.join(
|
||||||
|
app.root_path,
|
||||||
|
"../moodledata/saml2/moodle." + app.config["DOMAIN"] + ".crt",
|
||||||
|
),
|
||||||
|
"r",
|
||||||
|
) as crt:
|
||||||
|
app.config.setdefault("SP_CRT", crt.read())
|
||||||
ready = True
|
ready = True
|
||||||
except IOError:
|
except IOError:
|
||||||
log.warning('Could not get moodle SAML2 crt certificate. Retrying...')
|
log.warning("Could not get moodle SAML2 crt certificate. Retrying...")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
log.info('Got moodle srt certificate.')
|
log.info("Got moodle srt certificate.")
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(app.root_path, "../moodledata/saml2/moodle."+app.config['DOMAIN']+".pem"),"r") as pem:
|
with open(
|
||||||
app.config.setdefault('SP_PEM', pem.read())
|
os.path.join(
|
||||||
|
app.root_path,
|
||||||
|
"../moodledata/saml2/moodle." + app.config["DOMAIN"] + ".pem",
|
||||||
|
),
|
||||||
|
"r",
|
||||||
|
) as pem:
|
||||||
|
app.config.setdefault("SP_PEM", pem.read())
|
||||||
ready = True
|
ready = True
|
||||||
except IOError:
|
except IOError:
|
||||||
log.warning('Could not get moodle SAML2 pem certificate. Retrying...')
|
log.warning("Could not get moodle SAML2 pem certificate. Retrying...")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
log.info('Got moodle pem certificate.')
|
log.info("Got moodle pem certificate.")
|
||||||
|
|
||||||
self.select_and_configure_theme()
|
self.select_and_configure_theme()
|
||||||
self.configure_tipnc()
|
self.configure_tipnc()
|
||||||
self.add_moodle_ws_token()
|
self.add_moodle_ws_token()
|
||||||
|
|
||||||
def select_and_configure_theme(self,theme='cbe'):
|
def select_and_configure_theme(self, theme="cbe"):
|
||||||
try:
|
try:
|
||||||
self.pg.update("""UPDATE "mdl_config" SET value = '%s' WHERE "name" = 'theme';""" % (theme))
|
self.pg.update(
|
||||||
|
"""UPDATE "mdl_config" SET value = '%s' WHERE "name" = 'theme';"""
|
||||||
|
% (theme)
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
exit(1)
|
exit(1)
|
||||||
None
|
None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'theme_cbe' AND "name" = 'host';""" % (os.environ['DOMAIN']))
|
self.pg.update(
|
||||||
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'theme_cbe' AND "name" = 'logourl';""" % ("https://api."+os.environ['DOMAIN']+"/img/logo.png"))
|
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'theme_cbe' AND "name" = 'host';"""
|
||||||
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '1' WHERE "plugin" = 'theme_cbe' AND "name" = 'header_api';""")
|
% (os.environ["DOMAIN"])
|
||||||
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '1' WHERE "plugin" = 'theme_cbe' AND "name" = 'vclasses_direct';""")
|
)
|
||||||
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '1' WHERE "plugin" = 'theme_cbe' AND "name" = 'uniquenamecourse';""")
|
self.pg.update(
|
||||||
|
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'theme_cbe' AND "name" = 'logourl';"""
|
||||||
|
% ("https://api." + os.environ["DOMAIN"] + "/img/logo.png")
|
||||||
|
)
|
||||||
|
self.pg.update(
|
||||||
|
"""UPDATE "mdl_config_plugins" SET value = '1' WHERE "plugin" = 'theme_cbe' AND "name" = 'header_api';"""
|
||||||
|
)
|
||||||
|
self.pg.update(
|
||||||
|
"""UPDATE "mdl_config_plugins" SET value = '1' WHERE "plugin" = 'theme_cbe' AND "name" = 'vclasses_direct';"""
|
||||||
|
)
|
||||||
|
self.pg.update(
|
||||||
|
"""UPDATE "mdl_config_plugins" SET value = '1' WHERE "plugin" = 'theme_cbe' AND "name" = 'uniquenamecourse';"""
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
exit(1)
|
exit(1)
|
||||||
|
@ -76,12 +113,27 @@ class Postup():
|
||||||
|
|
||||||
def configure_tipnc(self):
|
def configure_tipnc(self):
|
||||||
try:
|
try:
|
||||||
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'host';""" % ("https://nextcloud."+os.environ['DOMAIN']+"/"))
|
self.pg.update(
|
||||||
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'password';""" % (os.environ['NEXTCLOUD_ADMIN_PASSWORD']))
|
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'host';"""
|
||||||
self.pg.update("""UPDATE "mdl_config_plugins" SET value = 'template.docx' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'template';""")
|
% ("https://nextcloud." + os.environ["DOMAIN"] + "/")
|
||||||
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '/apps/onlyoffice/' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'location';""")
|
)
|
||||||
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'user';""" % (os.environ['NEXTCLOUD_ADMIN_USER']))
|
self.pg.update(
|
||||||
self.pg.update("""UPDATE "mdl_config_plugins" SET value = 'tasks' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'folder';""")
|
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'password';"""
|
||||||
|
% (os.environ["NEXTCLOUD_ADMIN_PASSWORD"])
|
||||||
|
)
|
||||||
|
self.pg.update(
|
||||||
|
"""UPDATE "mdl_config_plugins" SET value = 'template.docx' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'template';"""
|
||||||
|
)
|
||||||
|
self.pg.update(
|
||||||
|
"""UPDATE "mdl_config_plugins" SET value = '/apps/onlyoffice/' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'location';"""
|
||||||
|
)
|
||||||
|
self.pg.update(
|
||||||
|
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'user';"""
|
||||||
|
% (os.environ["NEXTCLOUD_ADMIN_USER"])
|
||||||
|
)
|
||||||
|
self.pg.update(
|
||||||
|
"""UPDATE "mdl_config_plugins" SET value = 'tasks' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'folder';"""
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
exit(1)
|
exit(1)
|
||||||
|
@ -89,18 +141,23 @@ class Postup():
|
||||||
|
|
||||||
def add_moodle_ws_token(self):
|
def add_moodle_ws_token(self):
|
||||||
try:
|
try:
|
||||||
token=self.pg.select("""SELECT * FROM "mdl_external_tokens" WHERE "externalserviceid" = 3""")[0][1]
|
token = self.pg.select(
|
||||||
app.config.setdefault('MOODLE_WS_TOKEN',token)
|
"""SELECT * FROM "mdl_external_tokens" WHERE "externalserviceid" = 3"""
|
||||||
|
)[0][1]
|
||||||
|
app.config.setdefault("MOODLE_WS_TOKEN", token)
|
||||||
return
|
return
|
||||||
except:
|
except:
|
||||||
# log.error(traceback.format_exc())
|
# log.error(traceback.format_exc())
|
||||||
None
|
None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.pg.update("""INSERT INTO "mdl_external_services" ("name", "enabled", "requiredcapability", "restrictedusers", "component", "timecreated", "timemodified", "shortname", "downloadfiles", "uploadfiles") VALUES
|
self.pg.update(
|
||||||
('dd admin', 1, '', 1, NULL, 1621719763, 1621719850, 'dd_admin', 0, 0);""")
|
"""INSERT INTO "mdl_external_services" ("name", "enabled", "requiredcapability", "restrictedusers", "component", "timecreated", "timemodified", "shortname", "downloadfiles", "uploadfiles") VALUES
|
||||||
|
('dd admin', 1, '', 1, NULL, 1621719763, 1621719850, 'dd_admin', 0, 0);"""
|
||||||
|
)
|
||||||
|
|
||||||
self.pg.update("""INSERT INTO "mdl_external_services_functions" ("externalserviceid", "functionname") VALUES
|
self.pg.update(
|
||||||
|
"""INSERT INTO "mdl_external_services_functions" ("externalserviceid", "functionname") VALUES
|
||||||
(3, 'core_course_update_courses'),
|
(3, 'core_course_update_courses'),
|
||||||
(3, 'core_user_get_users'),
|
(3, 'core_user_get_users'),
|
||||||
(3, 'core_user_get_users_by_field'),
|
(3, 'core_user_get_users_by_field'),
|
||||||
|
@ -116,17 +173,37 @@ class Postup():
|
||||||
(3, 'core_cohort_search_cohorts'),
|
(3, 'core_cohort_search_cohorts'),
|
||||||
(3, 'core_cohort_update_cohorts'),
|
(3, 'core_cohort_update_cohorts'),
|
||||||
(3, 'core_role_assign_roles'),
|
(3, 'core_role_assign_roles'),
|
||||||
(3, 'core_cohort_get_cohorts');""")
|
(3, 'core_cohort_get_cohorts');"""
|
||||||
|
)
|
||||||
|
|
||||||
self.pg.update("""INSERT INTO "mdl_external_services_users" ("externalserviceid", "userid", "iprestriction", "validuntil", "timecreated") VALUES
|
self.pg.update(
|
||||||
(3, 2, NULL, NULL, 1621719871);""")
|
"""INSERT INTO "mdl_external_services_users" ("externalserviceid", "userid", "iprestriction", "validuntil", "timecreated") VALUES
|
||||||
|
(3, 2, NULL, NULL, 1621719871);"""
|
||||||
|
)
|
||||||
|
|
||||||
b32=''.join(random.choices(string.ascii_uppercase + string.ascii_uppercase + string.ascii_lowercase, k = 32))
|
b32 = "".join(
|
||||||
b64=''.join(random.choices(string.ascii_uppercase + string.ascii_uppercase + string.ascii_lowercase, k = 64))
|
random.choices(
|
||||||
self.pg.update("""INSERT INTO "mdl_external_tokens" ("token", "privatetoken", "tokentype", "userid", "externalserviceid", "sid", "contextid", "creatorid", "iprestriction", "validuntil", "timecreated", "lastaccess") VALUES
|
string.ascii_uppercase
|
||||||
('%s', '%s', 0, 2, 3, NULL, 1, 2, NULL, 0, 1621831206, NULL);""" % (b32,b64))
|
+ string.ascii_uppercase
|
||||||
|
+ string.ascii_lowercase,
|
||||||
|
k=32,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
b64 = "".join(
|
||||||
|
random.choices(
|
||||||
|
string.ascii_uppercase
|
||||||
|
+ string.ascii_uppercase
|
||||||
|
+ string.ascii_lowercase,
|
||||||
|
k=64,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.pg.update(
|
||||||
|
"""INSERT INTO "mdl_external_tokens" ("token", "privatetoken", "tokentype", "userid", "externalserviceid", "sid", "contextid", "creatorid", "iprestriction", "validuntil", "timecreated", "lastaccess") VALUES
|
||||||
|
('%s', '%s', 0, 2, 3, NULL, 1, 2, NULL, 0, 1621831206, NULL);"""
|
||||||
|
% (b32, b64)
|
||||||
|
)
|
||||||
|
|
||||||
app.config.setdefault('MOODLE_WS_TOKEN',b32)
|
app.config.setdefault("MOODLE_WS_TOKEN", b32)
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
|
@ -1,315 +1,488 @@
|
||||||
#!flask/bin/python
|
#!flask/bin/python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
from admin import app
|
|
||||||
import logging as log
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from uuid import uuid4
|
|
||||||
import time,json
|
|
||||||
import sys,os,re
|
|
||||||
from flask import render_template, Response, request, redirect, url_for, jsonify
|
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
from flask_login import current_user, login_required
|
import json
|
||||||
from .decorators import is_admin
|
import logging as log
|
||||||
|
import os
|
||||||
from ..lib.helpers import system_group
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
# import Queue
|
# import Queue
|
||||||
import threading
|
import threading
|
||||||
threads={'external':None}
|
import time
|
||||||
|
import traceback
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from flask import Response, jsonify, redirect, render_template, request, url_for
|
||||||
|
from flask_login import current_user, login_required
|
||||||
|
|
||||||
|
from admin import app
|
||||||
|
|
||||||
|
from ..lib.helpers import system_group
|
||||||
|
from .decorators import is_admin
|
||||||
|
|
||||||
|
threads = {"external": None}
|
||||||
# q = Queue.Queue()
|
# q = Queue.Queue()
|
||||||
|
|
||||||
from keycloak.exceptions import KeycloakGetError
|
from keycloak.exceptions import KeycloakGetError
|
||||||
|
|
||||||
from ..lib.exceptions import UserExists, UserNotFound
|
from ..lib.exceptions import UserExists, UserNotFound
|
||||||
|
|
||||||
@app.route('/api/resync')
|
|
||||||
|
@app.route("/api/resync")
|
||||||
@login_required
|
@login_required
|
||||||
def resync():
|
def resync():
|
||||||
return json.dumps(app.admin.resync_data()), 200, {'Content-Type': 'application/json'}
|
return (
|
||||||
|
json.dumps(app.admin.resync_data()),
|
||||||
|
200,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
@app.route('/api/users', methods=['GET','PUT'])
|
|
||||||
@app.route('/api/users/<provider>', methods=['POST', 'PUT', 'GET', 'DELETE'])
|
@app.route("/api/users", methods=["GET", "PUT"])
|
||||||
|
@app.route("/api/users/<provider>", methods=["POST", "PUT", "GET", "DELETE"])
|
||||||
@login_required
|
@login_required
|
||||||
def users(provider=False):
|
def users(provider=False):
|
||||||
if request.method == 'DELETE':
|
if request.method == "DELETE":
|
||||||
if current_user.role != 'admin': return json.dumps({}), 301, {'Content-Type': 'application/json'}
|
if current_user.role != "admin":
|
||||||
if provider == 'keycloak':
|
return json.dumps({}), 301, {"Content-Type": "application/json"}
|
||||||
return json.dumps(app.admin.delete_keycloak_users()), 200, {'Content-Type': 'application/json'}
|
if provider == "keycloak":
|
||||||
if provider == 'nextcloud':
|
return (
|
||||||
return json.dumps(app.admin.delete_nextcloud_users()), 200, {'Content-Type': 'application/json'}
|
json.dumps(app.admin.delete_keycloak_users()),
|
||||||
if provider == 'moodle':
|
200,
|
||||||
return json.dumps(app.admin.delete_moodle_users()), 200, {'Content-Type': 'application/json'}
|
{"Content-Type": "application/json"},
|
||||||
if request.method == 'POST':
|
)
|
||||||
if current_user.role != 'admin': return json.dumps({}), 301, {'Content-Type': 'application/json'}
|
if provider == "nextcloud":
|
||||||
if provider == 'moodle':
|
return (
|
||||||
return json.dumps(app.admin.sync_to_moodle()), 200, {'Content-Type': 'application/json'}
|
json.dumps(app.admin.delete_nextcloud_users()),
|
||||||
if provider == 'nextcloud':
|
200,
|
||||||
return json.dumps(app.admin.sync_to_nextcloud()), 200, {'Content-Type': 'application/json'}
|
{"Content-Type": "application/json"},
|
||||||
if request.method == 'PUT' and not provider:
|
)
|
||||||
if current_user.role != 'admin': return json.dumps({}), 301, {'Content-Type': 'application/json'}
|
if provider == "moodle":
|
||||||
|
return (
|
||||||
|
json.dumps(app.admin.delete_moodle_users()),
|
||||||
|
200,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
if request.method == "POST":
|
||||||
|
if current_user.role != "admin":
|
||||||
|
return json.dumps({}), 301, {"Content-Type": "application/json"}
|
||||||
|
if provider == "moodle":
|
||||||
|
return (
|
||||||
|
json.dumps(app.admin.sync_to_moodle()),
|
||||||
|
200,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
if provider == "nextcloud":
|
||||||
|
return (
|
||||||
|
json.dumps(app.admin.sync_to_nextcloud()),
|
||||||
|
200,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
if request.method == "PUT" and not provider:
|
||||||
|
if current_user.role != "admin":
|
||||||
|
return json.dumps({}), 301, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
if 'external' in threads.keys():
|
if "external" in threads.keys():
|
||||||
if threads['external'] is not None and threads['external'].is_alive():
|
if threads["external"] is not None and threads["external"].is_alive():
|
||||||
return json.dumps({'msg':'Precondition failed: already working with users'}), 412, {'Content-Type': 'application/json'}
|
return (
|
||||||
|
json.dumps(
|
||||||
|
{"msg": "Precondition failed: already working with users"}
|
||||||
|
),
|
||||||
|
412,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
threads['external']=None
|
threads["external"] = None
|
||||||
try:
|
try:
|
||||||
threads['external'] = threading.Thread(target=app.admin.update_users_from_keycloak, args=())
|
threads["external"] = threading.Thread(
|
||||||
threads['external'].start()
|
target=app.admin.update_users_from_keycloak, args=()
|
||||||
return json.dumps({}), 200, {'Content-Type': 'application/json'}
|
)
|
||||||
|
threads["external"].start()
|
||||||
|
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
return json.dumps({'msg':'Add user error.'}), 500, {'Content-Type': 'application/json'}
|
return (
|
||||||
|
json.dumps({"msg": "Add user error."}),
|
||||||
|
500,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
# return json.dumps(app.admin.update_users_from_keycloak()), 200, {'Content-Type': 'application/json'}
|
# return json.dumps(app.admin.update_users_from_keycloak()), 200, {'Content-Type': 'application/json'}
|
||||||
|
|
||||||
users = app.admin.get_mix_users()
|
users = app.admin.get_mix_users()
|
||||||
if current_user.role != 'admin':
|
if current_user.role != "admin":
|
||||||
for user in users:
|
for user in users:
|
||||||
user['keycloak_groups'] = [g for g in user['keycloak_groups'] if not system_group(g) ]
|
user["keycloak_groups"] = [
|
||||||
return json.dumps(users), 200, {'Content-Type': 'application/json'}
|
g for g in user["keycloak_groups"] if not system_group(g)
|
||||||
|
]
|
||||||
|
return json.dumps(users), 200, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
@app.route('/api/users_bulk/<action>', methods=['PUT'])
|
|
||||||
|
@app.route("/api/users_bulk/<action>", methods=["PUT"])
|
||||||
@login_required
|
@login_required
|
||||||
def users_bulk(action):
|
def users_bulk(action):
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
if request.method == 'PUT':
|
if request.method == "PUT":
|
||||||
if action == 'enable':
|
if action == "enable":
|
||||||
if 'external' in threads.keys():
|
if "external" in threads.keys():
|
||||||
if threads['external'] is not None and threads['external'].is_alive():
|
if threads["external"] is not None and threads["external"].is_alive():
|
||||||
return json.dumps({'msg':'Precondition failed: already operating users'}), 412, {'Content-Type': 'application/json'}
|
return (
|
||||||
|
json.dumps(
|
||||||
|
{"msg": "Precondition failed: already operating users"}
|
||||||
|
),
|
||||||
|
412,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
threads['external']=None
|
threads["external"] = None
|
||||||
try:
|
try:
|
||||||
threads['external'] = threading.Thread(target=app.admin.enable_users, args=(data,))
|
threads["external"] = threading.Thread(
|
||||||
threads['external'].start()
|
target=app.admin.enable_users, args=(data,)
|
||||||
return json.dumps({}), 200, {'Content-Type': 'application/json'}
|
)
|
||||||
|
threads["external"].start()
|
||||||
|
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
return json.dumps({'msg':'Enable users error.'}), 500, {'Content-Type': 'application/json'}
|
return (
|
||||||
if action == 'disable':
|
json.dumps({"msg": "Enable users error."}),
|
||||||
if 'external' in threads.keys():
|
500,
|
||||||
if threads['external'] is not None and threads['external'].is_alive():
|
{"Content-Type": "application/json"},
|
||||||
return json.dumps({'msg':'Precondition failed: already operating users'}), 412, {'Content-Type': 'application/json'}
|
)
|
||||||
|
if action == "disable":
|
||||||
|
if "external" in threads.keys():
|
||||||
|
if threads["external"] is not None and threads["external"].is_alive():
|
||||||
|
return (
|
||||||
|
json.dumps(
|
||||||
|
{"msg": "Precondition failed: already operating users"}
|
||||||
|
),
|
||||||
|
412,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
threads['external']=None
|
threads["external"] = None
|
||||||
try:
|
try:
|
||||||
threads['external'] = threading.Thread(target=app.admin.disable_users, args=(data,))
|
threads["external"] = threading.Thread(
|
||||||
threads['external'].start()
|
target=app.admin.disable_users, args=(data,)
|
||||||
return json.dumps({}), 200, {'Content-Type': 'application/json'}
|
)
|
||||||
|
threads["external"].start()
|
||||||
|
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
return json.dumps({'msg':'Disabling users error.'}), 500, {'Content-Type': 'application/json'}
|
return (
|
||||||
if action == 'delete':
|
json.dumps({"msg": "Disabling users error."}),
|
||||||
if 'external' in threads.keys():
|
500,
|
||||||
if threads['external'] is not None and threads['external'].is_alive():
|
{"Content-Type": "application/json"},
|
||||||
return json.dumps({'msg':'Precondition failed: already operating users'}), 412, {'Content-Type': 'application/json'}
|
)
|
||||||
|
if action == "delete":
|
||||||
|
if "external" in threads.keys():
|
||||||
|
if threads["external"] is not None and threads["external"].is_alive():
|
||||||
|
return (
|
||||||
|
json.dumps(
|
||||||
|
{"msg": "Precondition failed: already operating users"}
|
||||||
|
),
|
||||||
|
412,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
threads['external']=None
|
threads["external"] = None
|
||||||
try:
|
try:
|
||||||
threads['external'] = threading.Thread(target=app.admin.delete_users, args=(data,))
|
threads["external"] = threading.Thread(
|
||||||
threads['external'].start()
|
target=app.admin.delete_users, args=(data,)
|
||||||
return json.dumps({}), 200, {'Content-Type': 'application/json'}
|
)
|
||||||
|
threads["external"].start()
|
||||||
|
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
return json.dumps({'msg':'Deleting users error.'}), 500, {'Content-Type': 'application/json'}
|
return (
|
||||||
return json.dumps({}), 405, {'Content-Type': 'application/json'}
|
json.dumps({"msg": "Deleting users error."}),
|
||||||
|
500,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
return json.dumps({}), 405, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
|
||||||
# Update pwd
|
# Update pwd
|
||||||
@app.route('/api/user_password', methods=['GET'])
|
@app.route("/api/user_password", methods=["GET"])
|
||||||
@app.route('/api/user_password/<userid>', methods=['PUT'])
|
@app.route("/api/user_password/<userid>", methods=["PUT"])
|
||||||
@login_required
|
@login_required
|
||||||
def user_password(userid=False):
|
def user_password(userid=False):
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
return json.dumps(app.admin.get_dice_pwd()), 200, {'Content-Type': 'application/json'}
|
return (
|
||||||
if request.method == 'PUT':
|
json.dumps(app.admin.get_dice_pwd()),
|
||||||
|
200,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
if request.method == "PUT":
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
password=data['password']
|
password = data["password"]
|
||||||
temporary=data.get('temporary',True)
|
temporary = data.get("temporary", True)
|
||||||
try:
|
try:
|
||||||
res = app.admin.user_update_password(userid, password, temporary)
|
res = app.admin.user_update_password(userid, password, temporary)
|
||||||
return json.dumps({}), 200, {'Content-Type': 'application/json'}
|
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
||||||
except KeycloakGetError as e:
|
except KeycloakGetError as e:
|
||||||
log.error(e.error_message.decode("utf-8"))
|
log.error(e.error_message.decode("utf-8"))
|
||||||
return json.dumps({'msg':'Update password error.'}), 500, {'Content-Type': 'application/json'}
|
return (
|
||||||
|
json.dumps({"msg": "Update password error."}),
|
||||||
|
500,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
|
return json.dumps({}), 405, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
return json.dumps({}), 405, {'Content-Type': 'application/json'}
|
|
||||||
|
|
||||||
# User
|
# User
|
||||||
@app.route('/api/user', methods=['POST'])
|
@app.route("/api/user", methods=["POST"])
|
||||||
@app.route('/api/user/<userid>', methods=['PUT', 'GET', 'DELETE'])
|
@app.route("/api/user/<userid>", methods=["PUT", "GET", "DELETE"])
|
||||||
@login_required
|
@login_required
|
||||||
def user(userid=None):
|
def user(userid=None):
|
||||||
if request.method == 'DELETE':
|
if request.method == "DELETE":
|
||||||
app.admin.delete_user(userid)
|
app.admin.delete_user(userid)
|
||||||
return json.dumps({}), 200, {'Content-Type': 'application/json'}
|
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
if app.admin.get_user_username(data['username']):
|
if app.admin.get_user_username(data["username"]):
|
||||||
return json.dumps({'msg':'Add user error: already exists.'}), 409, {'Content-Type': 'application/json'}
|
return (
|
||||||
data['enabled']=True if data.get('enabled',False) else False
|
json.dumps({"msg": "Add user error: already exists."}),
|
||||||
data['quota']=data['quota'] if data['quota'] != 'false' else False
|
409,
|
||||||
data['groups']=data['groups'] if data.get('groups',False) else []
|
{"Content-Type": "application/json"},
|
||||||
if 'external' in threads.keys():
|
)
|
||||||
if threads['external'] is not None and threads['external'].is_alive():
|
data["enabled"] = True if data.get("enabled", False) else False
|
||||||
return json.dumps({'msg':'Precondition failed: already adding users'}), 412, {'Content-Type': 'application/json'}
|
data["quota"] = data["quota"] if data["quota"] != "false" else False
|
||||||
|
data["groups"] = data["groups"] if data.get("groups", False) else []
|
||||||
|
if "external" in threads.keys():
|
||||||
|
if threads["external"] is not None and threads["external"].is_alive():
|
||||||
|
return (
|
||||||
|
json.dumps({"msg": "Precondition failed: already adding users"}),
|
||||||
|
412,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
threads['external']=None
|
threads["external"] = None
|
||||||
try:
|
try:
|
||||||
threads['external'] = threading.Thread(target=app.admin.add_user, args=(data,))
|
threads["external"] = threading.Thread(
|
||||||
threads['external'].start()
|
target=app.admin.add_user, args=(data,)
|
||||||
return json.dumps({}), 200, {'Content-Type': 'application/json'}
|
)
|
||||||
|
threads["external"].start()
|
||||||
|
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
return json.dumps({'msg':'Add user error.'}), 500, {'Content-Type': 'application/json'}
|
return (
|
||||||
|
json.dumps({"msg": "Add user error."}),
|
||||||
|
500,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
if request.method == 'PUT':
|
if request.method == "PUT":
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
data['enabled']=True if data.get('enabled',False) else False
|
data["enabled"] = True if data.get("enabled", False) else False
|
||||||
data['groups']=data['groups'] if data.get('groups',False) else []
|
data["groups"] = data["groups"] if data.get("groups", False) else []
|
||||||
data['roles']=[data.pop('role-keycloak')]
|
data["roles"] = [data.pop("role-keycloak")]
|
||||||
try:
|
try:
|
||||||
app.admin.user_update(data)
|
app.admin.user_update(data)
|
||||||
return json.dumps({}), 200, {'Content-Type': 'application/json'}
|
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
||||||
except UserNotFound:
|
except UserNotFound:
|
||||||
return json.dumps({'msg':'User not found.'}), 404, {'Content-Type': 'application/json'}
|
return (
|
||||||
if request.method == 'DELETE':
|
json.dumps({"msg": "User not found."}),
|
||||||
|
404,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
if request.method == "DELETE":
|
||||||
pass
|
pass
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
user = app.admin.get_user(userid)
|
user = app.admin.get_user(userid)
|
||||||
if not user: return json.dumps({'msg':'User not found.'}), 404, {'Content-Type': 'application/json'}
|
if not user:
|
||||||
return json.dumps(user), 200, {'Content-Type': 'application/json'}
|
return (
|
||||||
|
json.dumps({"msg": "User not found."}),
|
||||||
|
404,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
return json.dumps(user), 200, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
@app.route('/api/roles')
|
|
||||||
|
@app.route("/api/roles")
|
||||||
@login_required
|
@login_required
|
||||||
def roles():
|
def roles():
|
||||||
sorted_roles = sorted(app.admin.get_roles(), key=lambda k: k['name'])
|
sorted_roles = sorted(app.admin.get_roles(), key=lambda k: k["name"])
|
||||||
if current_user.role != "admin":
|
if current_user.role != "admin":
|
||||||
sorted_roles = [sr for sr in sorted_roles if sr['name'] != 'admin']
|
sorted_roles = [sr for sr in sorted_roles if sr["name"] != "admin"]
|
||||||
return json.dumps(sorted_roles), 200, {'Content-Type': 'application/json'}
|
return json.dumps(sorted_roles), 200, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
@app.route('/api/group', methods=['POST','DELETE'])
|
|
||||||
@app.route('/api/group/<group_id>', methods=['PUT', 'GET', 'DELETE'])
|
@app.route("/api/group", methods=["POST", "DELETE"])
|
||||||
|
@app.route("/api/group/<group_id>", methods=["PUT", "GET", "DELETE"])
|
||||||
@login_required
|
@login_required
|
||||||
def group(group_id=False):
|
def group(group_id=False):
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
data['parent']=data['parent'] if data['parent'] != '' else None
|
data["parent"] = data["parent"] if data["parent"] != "" else None
|
||||||
return json.dumps(app.admin.add_group(data)), 200, {'Content-Type': 'application/json'}
|
return (
|
||||||
if request.method == 'DELETE':
|
json.dumps(app.admin.add_group(data)),
|
||||||
|
200,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
if request.method == "DELETE":
|
||||||
try:
|
try:
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
except:
|
except:
|
||||||
data = False
|
data = False
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
res = app.admin.delete_group_by_path(data['path'])
|
res = app.admin.delete_group_by_path(data["path"])
|
||||||
else:
|
else:
|
||||||
res = app.admin.delete_group_by_id(group_id)
|
res = app.admin.delete_group_by_id(group_id)
|
||||||
return json.dumps(res), 200, {'Content-Type': 'application/json'}
|
return json.dumps(res), 200, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
@app.route('/api/groups')
|
|
||||||
@app.route('/api/groups/<provider>', methods=['POST', 'PUT', 'GET', 'DELETE'])
|
@app.route("/api/groups")
|
||||||
|
@app.route("/api/groups/<provider>", methods=["POST", "PUT", "GET", "DELETE"])
|
||||||
@login_required
|
@login_required
|
||||||
def groups(provider=False):
|
def groups(provider=False):
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k['name'])
|
sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k["name"])
|
||||||
if current_user.role != "admin":
|
if current_user.role != "admin":
|
||||||
## internal groups should be avoided as are assigned with the role
|
## internal groups should be avoided as are assigned with the role
|
||||||
sorted_groups = [sg for sg in sorted_groups if not system_group(sg['name'])]
|
sorted_groups = [sg for sg in sorted_groups if not system_group(sg["name"])]
|
||||||
else:
|
else:
|
||||||
sorted_groups = [sg for sg in sorted_groups]
|
sorted_groups = [sg for sg in sorted_groups]
|
||||||
return json.dumps(sorted_groups), 200, {'Content-Type': 'application/json'}
|
return json.dumps(sorted_groups), 200, {"Content-Type": "application/json"}
|
||||||
if request.method == 'DELETE':
|
if request.method == "DELETE":
|
||||||
if provider == 'keycloak':
|
if provider == "keycloak":
|
||||||
return json.dumps(app.admin.delete_keycloak_groups()), 200, {'Content-Type': 'application/json'}
|
return (
|
||||||
|
json.dumps(app.admin.delete_keycloak_groups()),
|
||||||
|
200,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
### SYSADM USERS ONLY
|
### SYSADM USERS ONLY
|
||||||
|
|
||||||
@app.route('/api/external', methods=['POST', 'PUT', 'GET','DELETE'])
|
|
||||||
|
@app.route("/api/external", methods=["POST", "PUT", "GET", "DELETE"])
|
||||||
@login_required
|
@login_required
|
||||||
def external():
|
def external():
|
||||||
if 'external' in threads.keys():
|
if "external" in threads.keys():
|
||||||
if threads['external'] is not None and threads['external'].is_alive():
|
if threads["external"] is not None and threads["external"].is_alive():
|
||||||
return json.dumps({}), 301, {'Content-Type': 'application/json'}
|
return json.dumps({}), 301, {"Content-Type": "application/json"}
|
||||||
else:
|
else:
|
||||||
threads['external']=None
|
threads["external"] = None
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
if data['format']=='json-ga':
|
if data["format"] == "json-ga":
|
||||||
threads['external'] = threading.Thread(target=app.admin.upload_json_ga, args=(data,))
|
threads["external"] = threading.Thread(
|
||||||
threads['external'].start()
|
target=app.admin.upload_json_ga, args=(data,)
|
||||||
return json.dumps({}), 200, {'Content-Type': 'application/json'}
|
)
|
||||||
if data['format']=='csv-ug':
|
threads["external"].start()
|
||||||
|
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
||||||
|
if data["format"] == "csv-ug":
|
||||||
valid = check_upload_errors(data)
|
valid = check_upload_errors(data)
|
||||||
if valid['pass']:
|
if valid["pass"]:
|
||||||
threads['external'] = threading.Thread(target=app.admin.upload_csv_ug, args=(data,))
|
threads["external"] = threading.Thread(
|
||||||
threads['external'].start()
|
target=app.admin.upload_csv_ug, args=(data,)
|
||||||
return json.dumps({}), 200, {'Content-Type': 'application/json'}
|
)
|
||||||
|
threads["external"].start()
|
||||||
|
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
||||||
else:
|
else:
|
||||||
return json.dumps(valid), 422, {'Content-Type': 'application/json'}
|
return json.dumps(valid), 422, {"Content-Type": "application/json"}
|
||||||
if request.method == 'PUT':
|
if request.method == "PUT":
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
threads['external'] = threading.Thread(target=app.admin.sync_external, args=(data,))
|
threads["external"] = threading.Thread(
|
||||||
threads['external'].start()
|
target=app.admin.sync_external, args=(data,)
|
||||||
return json.dumps({}), 200, {'Content-Type': 'application/json'}
|
)
|
||||||
if request.method == 'DELETE':
|
threads["external"].start()
|
||||||
print('RESET')
|
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
||||||
|
if request.method == "DELETE":
|
||||||
|
print("RESET")
|
||||||
app.admin.reset_external()
|
app.admin.reset_external()
|
||||||
return json.dumps({}), 200, {'Content-Type': 'application/json'}
|
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
||||||
return json.dumps({}), 500, {'Content-Type': 'application/json'}
|
return json.dumps({}), 500, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
@app.route('/api/external/users')
|
|
||||||
|
@app.route("/api/external/users")
|
||||||
@login_required
|
@login_required
|
||||||
def external_users_list():
|
def external_users_list():
|
||||||
while threads['external'] is not None and threads['external'].is_alive():
|
while threads["external"] is not None and threads["external"].is_alive():
|
||||||
time.sleep(.5)
|
time.sleep(0.5)
|
||||||
return json.dumps(app.admin.get_external_users()), 200, {'Content-Type': 'application/json'}
|
return (
|
||||||
|
json.dumps(app.admin.get_external_users()),
|
||||||
|
200,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
@app.route('/api/external/groups')
|
|
||||||
|
@app.route("/api/external/groups")
|
||||||
@login_required
|
@login_required
|
||||||
def external_groups_list():
|
def external_groups_list():
|
||||||
while threads['external'] is not None and threads['external'].is_alive():
|
while threads["external"] is not None and threads["external"].is_alive():
|
||||||
time.sleep(.5)
|
time.sleep(0.5)
|
||||||
return json.dumps(app.admin.get_external_groups()), 200, {'Content-Type': 'application/json'}
|
return (
|
||||||
|
json.dumps(app.admin.get_external_groups()),
|
||||||
|
200,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
@app.route('/api/external/roles', methods=['PUT'])
|
|
||||||
|
@app.route("/api/external/roles", methods=["PUT"])
|
||||||
@login_required
|
@login_required
|
||||||
def external_roles():
|
def external_roles():
|
||||||
if request.method == 'PUT':
|
if request.method == "PUT":
|
||||||
return json.dumps(app.admin.external_roleassign(request.get_json(force=True))), 200, {'Content-Type': 'application/json'}
|
return (
|
||||||
|
json.dumps(app.admin.external_roleassign(request.get_json(force=True))),
|
||||||
|
200,
|
||||||
|
{"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
{'groups': '/alumnes/3er',
|
|
||||||
'firstname': 'Andreu',
|
{
|
||||||
'lastname': 'B',
|
"groups": "/alumnes/3er",
|
||||||
'email': '12andreub@escolamontseny.cat',
|
"firstname": "Andreu",
|
||||||
'username': '12andreub',
|
"lastname": "B",
|
||||||
'password': 'pepinillo',
|
"email": "12andreub@escolamontseny.cat",
|
||||||
'password_temporal': 'yes',
|
"username": "12andreub",
|
||||||
'role': 'student',
|
"password": "pepinillo",
|
||||||
'quota': '500 MB',
|
"password_temporal": "yes",
|
||||||
'': '',
|
"role": "student",
|
||||||
'id': '12andreub'}
|
"quota": "500 MB",
|
||||||
|
"": "",
|
||||||
|
"id": "12andreub",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def check_upload_errors(data):
|
def check_upload_errors(data):
|
||||||
email_regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
|
email_regex = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
|
||||||
for u in data['data']:
|
for u in data["data"]:
|
||||||
try:
|
try:
|
||||||
user_groups=[g.strip() for g in u['groups'].split(',')]
|
user_groups = [g.strip() for g in u["groups"].split(",")]
|
||||||
except:
|
except:
|
||||||
return {'pass':False,'msg':'User '+u['username']+' has invalid groups: '+u['groups']}
|
return {
|
||||||
|
"pass": False,
|
||||||
|
"msg": "User " + u["username"] + " has invalid groups: " + u["groups"],
|
||||||
|
}
|
||||||
|
|
||||||
if not re.fullmatch(email_regex, u['email']):
|
if not re.fullmatch(email_regex, u["email"]):
|
||||||
return {'pass':False,'msg':'User '+u['username']+' has invalid email: '+u['email']}
|
return {
|
||||||
|
"pass": False,
|
||||||
|
"msg": "User " + u["username"] + " has invalid email: " + u["email"],
|
||||||
|
}
|
||||||
|
|
||||||
if u['role'] not in ['admin','manager','teacher','student']:
|
if u["role"] not in ["admin", "manager", "teacher", "student"]:
|
||||||
if u['role'] == '':
|
if u["role"] == "":
|
||||||
return {'pass':False,'msg':'User '+u['username']+' has no role assigned!'}
|
return {
|
||||||
return {'pass':False,'msg':'User '+u['username']+' has invalid role: '+u['role']}
|
"pass": False,
|
||||||
|
"msg": "User " + u["username"] + " has no role assigned!",
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"pass": False,
|
||||||
|
"msg": "User " + u["username"] + " has invalid role: " + u["role"],
|
||||||
|
}
|
||||||
|
|
||||||
if u['password_temporal'].lower() not in ['yes','no']:
|
if u["password_temporal"].lower() not in ["yes", "no"]:
|
||||||
return {'pass':False,'msg':'User '+u['username']+' has invalid password_temporal value (yes/no): '+u['password_temporal']}
|
return {
|
||||||
return {'pass':True,'msg':''}
|
"pass": False,
|
||||||
|
"msg": "User "
|
||||||
|
+ u["username"]
|
||||||
|
+ " has invalid password_temporal value (yes/no): "
|
||||||
|
+ u["password_temporal"],
|
||||||
|
}
|
||||||
|
return {"pass": True, "msg": ""}
|
||||||
|
|
|
@ -1,107 +1,136 @@
|
||||||
#!flask/bin/python
|
#!flask/bin/python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
from admin import app
|
import json
|
||||||
import logging as log
|
import logging as log
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
import time,json
|
|
||||||
import sys,os
|
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
|
from admin import app
|
||||||
|
|
||||||
from .decorators import is_internal
|
from .decorators import is_internal
|
||||||
|
|
||||||
@app.route('/api/internal/users', methods=['GET'])
|
|
||||||
|
@app.route("/api/internal/users", methods=["GET"])
|
||||||
@is_internal
|
@is_internal
|
||||||
def internal_users():
|
def internal_users():
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k['username'])
|
sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k["username"])
|
||||||
# group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']]
|
# group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']]
|
||||||
users = []
|
users = []
|
||||||
for user in sorted_users:
|
for user in sorted_users:
|
||||||
if not user['enabled']: continue
|
if not user["enabled"]:
|
||||||
|
continue
|
||||||
users.append(user_parser(user))
|
users.append(user_parser(user))
|
||||||
return json.dumps(users), 200, {'Content-Type': 'application/json'}
|
return json.dumps(users), 200, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
@app.route('/api/internal/users/filter', methods=['POST'])
|
|
||||||
|
@app.route("/api/internal/users/filter", methods=["POST"])
|
||||||
@is_internal
|
@is_internal
|
||||||
def internal_users_search():
|
def internal_users_search():
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
users = app.admin.get_mix_users()
|
users = app.admin.get_mix_users()
|
||||||
result = [user_parser(user) for user in filter_users(users, data['text'])]
|
result = [user_parser(user) for user in filter_users(users, data["text"])]
|
||||||
sorted_result = sorted(result, key=lambda k: k['id'])
|
sorted_result = sorted(result, key=lambda k: k["id"])
|
||||||
return json.dumps(sorted_result), 200, {'Content-Type': 'application/json'}
|
return json.dumps(sorted_result), 200, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
@app.route('/api/internal/groups', methods=['GET'])
|
|
||||||
|
@app.route("/api/internal/groups", methods=["GET"])
|
||||||
@is_internal
|
@is_internal
|
||||||
def internal_groups():
|
def internal_groups():
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k['name'])
|
sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k["name"])
|
||||||
groups = []
|
groups = []
|
||||||
for group in sorted_groups:
|
for group in sorted_groups:
|
||||||
if not group['path'].startswith('/'): continue
|
if not group["path"].startswith("/"):
|
||||||
groups.append({'id':group['path'],
|
continue
|
||||||
'name':group['name'],
|
groups.append(
|
||||||
'description':group.get('description','')})
|
{
|
||||||
return json.dumps(groups), 200, {'Content-Type': 'application/json'}
|
"id": group["path"],
|
||||||
|
"name": group["name"],
|
||||||
|
"description": group.get("description", ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return json.dumps(groups), 200, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
@app.route('/api/internal/group/users', methods=['POST'])
|
|
||||||
|
@app.route("/api/internal/group/users", methods=["POST"])
|
||||||
@is_internal
|
@is_internal
|
||||||
def internal_group_users():
|
def internal_group_users():
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k['username'])
|
sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k["username"])
|
||||||
# group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']]
|
# group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']]
|
||||||
users = []
|
users = []
|
||||||
for user in sorted_users:
|
for user in sorted_users:
|
||||||
if data['path'] not in user['keycloak_groups'] or not user['enabled']: continue
|
if data["path"] not in user["keycloak_groups"] or not user["enabled"]:
|
||||||
|
continue
|
||||||
users.append(user)
|
users.append(user)
|
||||||
if data.get('text',False) and data['text'] != '':
|
if data.get("text", False) and data["text"] != "":
|
||||||
result = [user_parser(user) for user in filter_users(users, data['text'])]
|
result = [user_parser(user) for user in filter_users(users, data["text"])]
|
||||||
else:
|
else:
|
||||||
result = [user_parser(user) for user in users]
|
result = [user_parser(user) for user in users]
|
||||||
return json.dumps(result), 200, {'Content-Type': 'application/json'}
|
return json.dumps(result), 200, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
@app.route('/api/internal/roles', methods=['GET'])
|
|
||||||
|
@app.route("/api/internal/roles", methods=["GET"])
|
||||||
@is_internal
|
@is_internal
|
||||||
def internal_roles():
|
def internal_roles():
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
roles = []
|
roles = []
|
||||||
for role in sorted(app.admin.get_roles(), key=lambda k: k['name']):
|
for role in sorted(app.admin.get_roles(), key=lambda k: k["name"]):
|
||||||
if role['name'] == 'admin': continue
|
if role["name"] == "admin":
|
||||||
roles.append({'id':role['id'],
|
continue
|
||||||
'name':role['name'],
|
roles.append(
|
||||||
'description':role.get('description','')})
|
{
|
||||||
return json.dumps(roles), 200, {'Content-Type': 'application/json'}
|
"id": role["id"],
|
||||||
|
"name": role["name"],
|
||||||
|
"description": role.get("description", ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return json.dumps(roles), 200, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
@app.route('/api/internal/role/users', methods=['POST'])
|
|
||||||
|
@app.route("/api/internal/role/users", methods=["POST"])
|
||||||
@is_internal
|
@is_internal
|
||||||
def internal_role_users():
|
def internal_role_users():
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k['username'])
|
sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k["username"])
|
||||||
# group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']]
|
# group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']]
|
||||||
users = []
|
users = []
|
||||||
for user in sorted_users:
|
for user in sorted_users:
|
||||||
if data['role'] not in user['roles'] or not user['enabled']: continue
|
if data["role"] not in user["roles"] or not user["enabled"]:
|
||||||
|
continue
|
||||||
users.append(user)
|
users.append(user)
|
||||||
if data.get('text',False) and data['text'] != '':
|
if data.get("text", False) and data["text"] != "":
|
||||||
result = [user_parser(user) for user in filter_users(users, data['text'])]
|
result = [user_parser(user) for user in filter_users(users, data["text"])]
|
||||||
else:
|
else:
|
||||||
result = [user_parser(user) for user in users]
|
result = [user_parser(user) for user in users]
|
||||||
return json.dumps(result), 200, {'Content-Type': 'application/json'}
|
return json.dumps(result), 200, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
|
||||||
def user_parser(user):
|
def user_parser(user):
|
||||||
return {'id':user['username'],
|
return {
|
||||||
'first':user['first'],
|
"id": user["username"],
|
||||||
'last':user['last'],
|
"first": user["first"],
|
||||||
'role':user['roles'][0] if len(user['roles']) else None,
|
"last": user["last"],
|
||||||
'email':user['email'],
|
"role": user["roles"][0] if len(user["roles"]) else None,
|
||||||
'groups':user['keycloak_groups']}
|
"email": user["email"],
|
||||||
|
"groups": user["keycloak_groups"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def filter_users(users, text):
|
def filter_users(users, text):
|
||||||
return [user for user in users
|
return [
|
||||||
if text in user['username'] or
|
user
|
||||||
text in user['first'] or
|
for user in users
|
||||||
text in user['last'] or
|
if text in user["username"]
|
||||||
text in user['email']]
|
or text in user["first"]
|
||||||
|
or text in user["last"]
|
||||||
|
or text in user["email"]
|
||||||
|
]
|
||||||
|
|
|
@ -1,31 +1,42 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from flask import flash, redirect, render_template, request, url_for
|
||||||
|
from flask_login import current_user, login_required, login_user, logout_user
|
||||||
|
|
||||||
from admin import app
|
from admin import app
|
||||||
from flask import render_template, flash, request, redirect, url_for
|
|
||||||
from ..auth.authentication import *
|
from ..auth.authentication import *
|
||||||
from flask_login import login_required, current_user, login_user, logout_user
|
|
||||||
|
|
||||||
@app.route('/', methods=['GET', 'POST'])
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route("/", methods=["GET", "POST"])
|
||||||
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
if request.form['user'] == '' or request.form['password'] == '':
|
if request.form["user"] == "" or request.form["password"] == "":
|
||||||
flash("Can't leave it blank",'danger')
|
flash("Can't leave it blank", "danger")
|
||||||
elif request.form['user'].startswith(' '):
|
elif request.form["user"].startswith(" "):
|
||||||
flash('Username not found or incorrect password.','warning')
|
flash("Username not found or incorrect password.", "warning")
|
||||||
else:
|
else:
|
||||||
ram_user=ram_users.get(request.form['user'])
|
ram_user = ram_users.get(request.form["user"])
|
||||||
if ram_user and request.form['password'] == ram_user['password']:
|
if ram_user and request.form["password"] == ram_user["password"]:
|
||||||
user=User({'id': ram_user['id'], 'password': ram_user['password'], 'role': ram_user['role'], 'active': True})
|
user = User(
|
||||||
|
{
|
||||||
|
"id": ram_user["id"],
|
||||||
|
"password": ram_user["password"],
|
||||||
|
"role": ram_user["role"],
|
||||||
|
"active": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
login_user(user)
|
login_user(user)
|
||||||
flash('Logged in successfully.','success')
|
flash("Logged in successfully.", "success")
|
||||||
return redirect(url_for('web_users'))
|
return redirect(url_for("web_users"))
|
||||||
else:
|
else:
|
||||||
flash('Username not found or incorrect password.','warning')
|
flash("Username not found or incorrect password.", "warning")
|
||||||
return render_template('login.html')
|
return render_template("login.html")
|
||||||
|
|
||||||
@app.route('/logout', methods=['GET'])
|
|
||||||
|
@app.route("/logout", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def logout():
|
def logout():
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for("login"))
|
||||||
|
|
|
@ -1,23 +1,34 @@
|
||||||
#!flask/bin/python
|
#!flask/bin/python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
from admin import app
|
|
||||||
import logging as log
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from uuid import uuid4
|
|
||||||
import time,json
|
|
||||||
import sys,os
|
|
||||||
from flask import render_template, Response, request, redirect, url_for, jsonify, send_file
|
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
from flask_login import login_required
|
import json
|
||||||
from .decorators import is_admin
|
import logging as log
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Response,
|
||||||
|
jsonify,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
|
send_file,
|
||||||
|
url_for,
|
||||||
|
)
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
from admin import app
|
||||||
|
|
||||||
from ..lib.avatars import Avatars
|
from ..lib.avatars import Avatars
|
||||||
|
from .decorators import is_admin
|
||||||
|
|
||||||
avatars = Avatars()
|
avatars = Avatars()
|
||||||
|
|
||||||
''' OIDC TESTS '''
|
""" OIDC TESTS """
|
||||||
# from ..auth.authentication import oidc
|
# from ..auth.authentication import oidc
|
||||||
|
|
||||||
# @app.route('/custom_callback')
|
# @app.route('/custom_callback')
|
||||||
|
@ -43,47 +54,60 @@ avatars=Avatars()
|
||||||
# def logoutoidc():
|
# def logoutoidc():
|
||||||
# oidc.logout()
|
# oidc.logout()
|
||||||
# return 'Hi, you have been logged out! <a href="/">Return</a>'
|
# return 'Hi, you have been logged out! <a href="/">Return</a>'
|
||||||
''' OIDC TESTS '''
|
""" OIDC TESTS """
|
||||||
|
|
||||||
@app.route('/users')
|
|
||||||
|
@app.route("/users")
|
||||||
@login_required
|
@login_required
|
||||||
def web_users():
|
def web_users():
|
||||||
return render_template('pages/users.html', title="Users", nav="Users")
|
return render_template("pages/users.html", title="Users", nav="Users")
|
||||||
|
|
||||||
@app.route('/roles')
|
|
||||||
|
@app.route("/roles")
|
||||||
@login_required
|
@login_required
|
||||||
def web_roles():
|
def web_roles():
|
||||||
return render_template('pages/roles.html', title="Roles", nav="Roles")
|
return render_template("pages/roles.html", title="Roles", nav="Roles")
|
||||||
|
|
||||||
@app.route('/groups')
|
|
||||||
|
@app.route("/groups")
|
||||||
@login_required
|
@login_required
|
||||||
def web_groups(provider=False):
|
def web_groups(provider=False):
|
||||||
return render_template('pages/groups.html', title="Groups", nav="Groups")
|
return render_template("pages/groups.html", title="Groups", nav="Groups")
|
||||||
|
|
||||||
@app.route('/avatar/<userid>', methods=['GET'])
|
|
||||||
|
@app.route("/avatar/<userid>", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def avatar(userid):
|
def avatar(userid):
|
||||||
if userid != 'false':
|
if userid != "false":
|
||||||
return send_file('../avatars/master-avatars/'+userid, mimetype='image/jpeg')
|
return send_file("../avatars/master-avatars/" + userid, mimetype="image/jpeg")
|
||||||
return send_file('static/img/missing.jpg', mimetype='image/jpeg')
|
return send_file("static/img/missing.jpg", mimetype="image/jpeg")
|
||||||
|
|
||||||
|
|
||||||
### SYS ADMIN
|
### SYS ADMIN
|
||||||
|
|
||||||
@app.route('/sysadmin/users')
|
|
||||||
|
@app.route("/sysadmin/users")
|
||||||
@login_required
|
@login_required
|
||||||
@is_admin
|
@is_admin
|
||||||
def web_sysadmin_users():
|
def web_sysadmin_users():
|
||||||
return render_template('pages/sysadmin/users.html', title="SysAdmin Users", nav="SysAdminUsers")
|
return render_template(
|
||||||
|
"pages/sysadmin/users.html", title="SysAdmin Users", nav="SysAdminUsers"
|
||||||
|
)
|
||||||
|
|
||||||
@app.route('/sysadmin/groups')
|
|
||||||
|
@app.route("/sysadmin/groups")
|
||||||
@login_required
|
@login_required
|
||||||
@is_admin
|
@is_admin
|
||||||
def web_sysadmin_groups():
|
def web_sysadmin_groups():
|
||||||
return render_template('pages/sysadmin/groups.html', title="SysAdmin Groups", nav="SysAdminGroups")
|
return render_template(
|
||||||
|
"pages/sysadmin/groups.html", title="SysAdmin Groups", nav="SysAdminGroups"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/sysadmin/external')
|
@app.route("/sysadmin/external")
|
||||||
@login_required
|
@login_required
|
||||||
## SysAdmin role
|
## SysAdmin role
|
||||||
def web_sysadmin_external():
|
def web_sysadmin_external():
|
||||||
return render_template('pages/sysadmin/external.html', title="External", nav="External")
|
return render_template(
|
||||||
|
"pages/sysadmin/external.html", title="External", nav="External"
|
||||||
|
)
|
||||||
|
|
|
@ -1,25 +1,36 @@
|
||||||
#!flask/bin/python
|
#!flask/bin/python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
|
|
||||||
from functools import wraps
|
|
||||||
from flask import request, redirect, url_for
|
|
||||||
from flask_login import current_user, logout_user
|
|
||||||
import socket
|
import socket
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from flask import redirect, request, url_for
|
||||||
|
from flask_login import current_user, logout_user
|
||||||
|
|
||||||
|
|
||||||
def is_admin(fn):
|
def is_admin(fn):
|
||||||
@wraps(fn)
|
@wraps(fn)
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args, **kwargs):
|
||||||
if current_user.role == 'admin': return fn(*args, **kwargs)
|
if current_user.role == "admin":
|
||||||
return redirect(url_for('login'))
|
return fn(*args, **kwargs)
|
||||||
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
return decorated_view
|
return decorated_view
|
||||||
|
|
||||||
|
|
||||||
def is_internal(fn):
|
def is_internal(fn):
|
||||||
@wraps(fn)
|
@wraps(fn)
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args, **kwargs):
|
||||||
remote_addr=request.headers['X-Forwarded-For'].split(',')[0] if 'X-Forwarded-For' in request.headers else request.remote_addr.split(',')[0]
|
remote_addr = (
|
||||||
|
request.headers["X-Forwarded-For"].split(",")[0]
|
||||||
|
if "X-Forwarded-For" in request.headers
|
||||||
|
else request.remote_addr.split(",")[0]
|
||||||
|
)
|
||||||
## Now only checks if it is wordpress container,
|
## Now only checks if it is wordpress container,
|
||||||
## but we should check if it is internal net and not haproxy
|
## but we should check if it is internal net and not haproxy
|
||||||
if socket.gethostbyname('isard-apps-wordpress') == remote_addr: return fn(*args, **kwargs)
|
if socket.gethostbyname("isard-apps-wordpress") == remote_addr:
|
||||||
|
return fn(*args, **kwargs)
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
return decorated_view
|
return decorated_view
|
|
@ -1,72 +1,89 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
import time, os
|
import json
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import pprint
|
|
||||||
|
|
||||||
import logging as log
|
import logging as log
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import yaml, json
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
import yaml
|
||||||
|
|
||||||
from admin.lib.postgres import Postgres
|
|
||||||
from admin.lib.keycloak_client import KeycloakClient
|
from admin.lib.keycloak_client import KeycloakClient
|
||||||
|
from admin.lib.postgres import Postgres
|
||||||
import string, random
|
|
||||||
|
|
||||||
app = {}
|
app = {}
|
||||||
app['config']={}
|
app["config"] = {}
|
||||||
|
|
||||||
class MoodleSaml():
|
|
||||||
|
class MoodleSaml:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
self.pg=Postgres('isard-apps-postgresql','moodle',os.environ['MOODLE_POSTGRES_USER'],os.environ['MOODLE_POSTGRES_PASSWORD'])
|
self.pg = Postgres(
|
||||||
|
"isard-apps-postgresql",
|
||||||
|
"moodle",
|
||||||
|
os.environ["MOODLE_POSTGRES_USER"],
|
||||||
|
os.environ["MOODLE_POSTGRES_PASSWORD"],
|
||||||
|
)
|
||||||
ready = True
|
ready = True
|
||||||
except:
|
except:
|
||||||
log.warning('Could not connect to moodle database. Retrying...')
|
log.warning("Could not connect to moodle database. Retrying...")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
log.info('Connected to moodle database.')
|
log.info("Connected to moodle database.")
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
privatekey_pass = self.get_privatekey_pass()
|
privatekey_pass = self.get_privatekey_pass()
|
||||||
log.warning("The key: " + str(privatekey_pass))
|
log.warning("The key: " + str(privatekey_pass))
|
||||||
if privatekey_pass.endswith(os.environ['DOMAIN']):
|
if privatekey_pass.endswith(os.environ["DOMAIN"]):
|
||||||
app['config']['MOODLE_SAML_PRIVATEKEYPASS']=privatekey_pass
|
app["config"]["MOODLE_SAML_PRIVATEKEYPASS"] = privatekey_pass
|
||||||
ready = True
|
ready = True
|
||||||
except:
|
except:
|
||||||
# print(traceback.format_exc())
|
# print(traceback.format_exc())
|
||||||
log.warning('Could not get moodle site identifier. Retrying...')
|
log.warning("Could not get moodle site identifier. Retrying...")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
log.info('Got moodle site identifier.')
|
log.info("Got moodle site identifier.")
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join("./moodledata/saml2/moodle."+os.environ['DOMAIN']+".crt"),"r") as crt:
|
with open(
|
||||||
app['config']['SP_CRT']=crt.read()
|
os.path.join(
|
||||||
|
"./moodledata/saml2/moodle." + os.environ["DOMAIN"] + ".crt"
|
||||||
|
),
|
||||||
|
"r",
|
||||||
|
) as crt:
|
||||||
|
app["config"]["SP_CRT"] = crt.read()
|
||||||
ready = True
|
ready = True
|
||||||
except IOError:
|
except IOError:
|
||||||
log.warning('Could not get moodle SAML2 crt certificate. Retrying...')
|
log.warning("Could not get moodle SAML2 crt certificate. Retrying...")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
log.info('Got moodle srt certificate.')
|
log.info("Got moodle srt certificate.")
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join("./moodledata/saml2/moodle."+os.environ['DOMAIN']+".pem"),"r") as pem:
|
with open(
|
||||||
app['config']['SP_PEM']=pem.read()
|
os.path.join(
|
||||||
|
"./moodledata/saml2/moodle." + os.environ["DOMAIN"] + ".pem"
|
||||||
|
),
|
||||||
|
"r",
|
||||||
|
) as pem:
|
||||||
|
app["config"]["SP_PEM"] = pem.read()
|
||||||
ready = True
|
ready = True
|
||||||
except IOError:
|
except IOError:
|
||||||
log.warning('Could not get moodle SAML2 pem certificate. Retrying...')
|
log.warning("Could not get moodle SAML2 pem certificate. Retrying...")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
log.info('Got moodle pem certificate.')
|
log.info("Got moodle pem certificate.")
|
||||||
|
|
||||||
## This seems related to the fact that the certificate generated the first time does'nt work.
|
## This seems related to the fact that the certificate generated the first time does'nt work.
|
||||||
## And when regenerating the certificate de privatekeypass seems not to be used and instead it
|
## And when regenerating the certificate de privatekeypass seems not to be used and instead it
|
||||||
|
@ -77,46 +94,77 @@ class MoodleSaml():
|
||||||
## 3.- Cleanup all caches in moodle (Development tab)
|
## 3.- Cleanup all caches in moodle (Development tab)
|
||||||
# with open(os.path.join("./moodledata/saml2/"+os.environ['MOODLE_SAML_PRIVATEKEYPASS'].replace("moodle."+os.environ['DOMAIN'],'')+'.idp.xml'),"w") as xml:
|
# with open(os.path.join("./moodledata/saml2/"+os.environ['MOODLE_SAML_PRIVATEKEYPASS'].replace("moodle."+os.environ['DOMAIN'],'')+'.idp.xml'),"w") as xml:
|
||||||
# xml.write(self.parse_idp_metadata())
|
# xml.write(self.parse_idp_metadata())
|
||||||
with open(os.path.join("./moodledata/saml2/0f635d0e0f3874fff8b581c132e6c7a7.idp.xml"),"w") as xml:
|
with open(
|
||||||
|
os.path.join("./moodledata/saml2/0f635d0e0f3874fff8b581c132e6c7a7.idp.xml"),
|
||||||
|
"w",
|
||||||
|
) as xml:
|
||||||
xml.write(self.parse_idp_metadata())
|
xml.write(self.parse_idp_metadata())
|
||||||
|
|
||||||
log.info('Written SP file on moodledata.')
|
log.info("Written SP file on moodledata.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.activate_saml_plugin()
|
self.activate_saml_plugin()
|
||||||
except:
|
except:
|
||||||
print('Error activating saml on moodle')
|
print("Error activating saml on moodle")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.set_moodle_saml_plugin()
|
self.set_moodle_saml_plugin()
|
||||||
except:
|
except:
|
||||||
print('Error setting saml on moodle')
|
print("Error setting saml on moodle")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.delete_keycloak_moodle_saml_plugin()
|
self.delete_keycloak_moodle_saml_plugin()
|
||||||
except:
|
except:
|
||||||
print('Error deleting saml on keycloak')
|
print("Error deleting saml on keycloak")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.add_keycloak_moodle_saml()
|
self.add_keycloak_moodle_saml()
|
||||||
except:
|
except:
|
||||||
print('Error adding saml on keycloak')
|
print("Error adding saml on keycloak")
|
||||||
|
|
||||||
# SAML clients don't work well with composite roles so disabling and adding on realm
|
# SAML clients don't work well with composite roles so disabling and adding on realm
|
||||||
# self.add_client_roles()
|
# self.add_client_roles()
|
||||||
|
|
||||||
def activate_saml_plugin(self):
|
def activate_saml_plugin(self):
|
||||||
## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php
|
## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php
|
||||||
return self.pg.update("""UPDATE "mdl_config" SET value = 'email,saml2' WHERE "name" = 'auth'""")
|
return self.pg.update(
|
||||||
|
"""UPDATE "mdl_config" SET value = 'email,saml2' WHERE "name" = 'auth'"""
|
||||||
|
)
|
||||||
|
|
||||||
def get_privatekey_pass(self):
|
def get_privatekey_pass(self):
|
||||||
return self.pg.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2]
|
return self.pg.select(
|
||||||
|
"""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'"""
|
||||||
|
)[0][2]
|
||||||
|
|
||||||
def parse_idp_metadata(self):
|
def parse_idp_metadata(self):
|
||||||
keycloak = KeycloakClient()
|
keycloak = KeycloakClient()
|
||||||
rsa = keycloak.get_server_rsa_key()
|
rsa = keycloak.get_server_rsa_key()
|
||||||
keycloak = None
|
keycloak = None
|
||||||
return '<md:EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Name="urn:keycloak"><md:EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master"><md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor use="signing"><ds:KeyInfo><ds:KeyName>'+rsa['name']+'</ds:KeyName><ds:X509Data><ds:X509Certificate>'+rsa['certificate']+'</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml/resolve" index="0"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.'+os.environ['DOMAIN']+'/auth/realms/master/protocol/saml"/></md:IDPSSODescriptor></md:EntityDescriptor></md:EntitiesDescriptor>'
|
return (
|
||||||
|
'<md:EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Name="urn:keycloak"><md:EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://sso.'
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ '/auth/realms/master"><md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor use="signing"><ds:KeyInfo><ds:KeyName>'
|
||||||
|
+ rsa["name"]
|
||||||
|
+ "</ds:KeyName><ds:X509Data><ds:X509Certificate>"
|
||||||
|
+ rsa["certificate"]
|
||||||
|
+ '</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.'
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ '/auth/realms/master/protocol/saml/resolve" index="0"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.'
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ '/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.'
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ '/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.'
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ '/auth/realms/master/protocol/saml"/><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.'
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ '/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.'
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ '/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.'
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ '/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.'
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ '/auth/realms/master/protocol/saml"/></md:IDPSSODescriptor></md:EntityDescriptor></md:EntitiesDescriptor>'
|
||||||
|
)
|
||||||
|
|
||||||
def set_keycloak_moodle_saml_plugin(self):
|
def set_keycloak_moodle_saml_plugin(self):
|
||||||
keycloak = KeycloakClient()
|
keycloak = KeycloakClient()
|
||||||
|
@ -125,39 +173,55 @@ class MoodleSaml():
|
||||||
|
|
||||||
def delete_keycloak_moodle_saml_plugin(self):
|
def delete_keycloak_moodle_saml_plugin(self):
|
||||||
keycloak = KeycloakClient()
|
keycloak = KeycloakClient()
|
||||||
keycloak.delete_client('a92d5417-92b6-4678-9cb9-51bc0edcee8c')
|
keycloak.delete_client("a92d5417-92b6-4678-9cb9-51bc0edcee8c")
|
||||||
keycloak = None
|
keycloak = None
|
||||||
|
|
||||||
def set_moodle_saml_plugin(self):
|
def set_moodle_saml_plugin(self):
|
||||||
config={'idpmetadata': self.parse_idp_metadata(),
|
config = {
|
||||||
'certs_locked': '1',
|
"idpmetadata": self.parse_idp_metadata(),
|
||||||
'duallogin': '1',
|
"certs_locked": "1",
|
||||||
'idpattr': 'username',
|
"duallogin": "1",
|
||||||
'autocreate': '1',
|
"idpattr": "username",
|
||||||
'anyauth': '1',
|
"autocreate": "1",
|
||||||
'saml_role_siteadmin_map': 'admin',
|
"anyauth": "1",
|
||||||
'saml_role_coursecreator_map': 'teacher',
|
"saml_role_siteadmin_map": "admin",
|
||||||
'saml_role_manager_map': 'manager',
|
"saml_role_coursecreator_map": "teacher",
|
||||||
'field_map_email': 'email',
|
"saml_role_manager_map": "manager",
|
||||||
'field_map_firstname': 'givenName',
|
"field_map_email": "email",
|
||||||
'field_map_lastname': 'sn'}
|
"field_map_firstname": "givenName",
|
||||||
|
"field_map_lastname": "sn",
|
||||||
|
}
|
||||||
for name in config.keys():
|
for name in config.keys():
|
||||||
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'auth_saml2' AND "name" = '%s'""" % (config[name],name))
|
self.pg.update(
|
||||||
self.pg.update("""INSERT INTO "mdl_auth_saml2_idps" ("metadataurl", "entityid", "activeidp", "defaultidp", "adminidp", "defaultname", "displayname", "logo", "alias", "whitelist") VALUES
|
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'auth_saml2' AND "name" = '%s'"""
|
||||||
('xml', 'https://sso.%s/auth/realms/master', 1, 0, 0, 'Login via SAML2', '', NULL, NULL, NULL);""" % (os.environ['DOMAIN']))
|
% (config[name], name)
|
||||||
|
)
|
||||||
|
self.pg.update(
|
||||||
|
"""INSERT INTO "mdl_auth_saml2_idps" ("metadataurl", "entityid", "activeidp", "defaultidp", "adminidp", "defaultname", "displayname", "logo", "alias", "whitelist") VALUES
|
||||||
|
('xml', 'https://sso.%s/auth/realms/master', 1, 0, 0, 'Login via SAML2', '', NULL, NULL, NULL);"""
|
||||||
|
% (os.environ["DOMAIN"])
|
||||||
|
)
|
||||||
|
|
||||||
def add_keycloak_moodle_saml(self):
|
def add_keycloak_moodle_saml(self):
|
||||||
client = {
|
client = {
|
||||||
"id": "a92d5417-92b6-4678-9cb9-51bc0edcee8c",
|
"id": "a92d5417-92b6-4678-9cb9-51bc0edcee8c",
|
||||||
"name": "moodle",
|
"name": "moodle",
|
||||||
"description": "moodle",
|
"description": "moodle",
|
||||||
"clientId" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/metadata.php",
|
"clientId": "https://moodle."
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ "/auth/saml2/sp/metadata.php",
|
||||||
"surrogateAuthRequired": False,
|
"surrogateAuthRequired": False,
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"alwaysDisplayInConsole": False,
|
"alwaysDisplayInConsole": False,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris" : [ "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+os.environ['DOMAIN']+"" ],
|
"redirectUris": [
|
||||||
"webOrigins" : [ "https://moodle."+os.environ['DOMAIN']+"" ],
|
"https://moodle."
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ "/auth/saml2/sp/saml2-acs.php/moodle."
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ ""
|
||||||
|
],
|
||||||
|
"webOrigins": ["https://moodle." + os.environ["DOMAIN"] + ""],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": False,
|
"bearerOnly": False,
|
||||||
"consentRequired": False,
|
"consentRequired": False,
|
||||||
|
@ -171,23 +235,32 @@ class MoodleSaml():
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"saml.force.post.binding": True,
|
"saml.force.post.binding": True,
|
||||||
"saml.encrypt": False,
|
"saml.encrypt": False,
|
||||||
"saml_assertion_consumer_url_post" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+os.environ['DOMAIN']+"",
|
"saml_assertion_consumer_url_post": "https://moodle."
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ "/auth/saml2/sp/saml2-acs.php/moodle."
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ "",
|
||||||
"saml.server.signature": True,
|
"saml.server.signature": True,
|
||||||
"saml.server.signature.keyinfo.ext": False,
|
"saml.server.signature.keyinfo.ext": False,
|
||||||
"saml.signing.certificate" : app['config']['SP_CRT'],
|
"saml.signing.certificate": app["config"]["SP_CRT"],
|
||||||
"saml_single_logout_service_url_redirect" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-logout.php/moodle."+os.environ['DOMAIN']+"",
|
"saml_single_logout_service_url_redirect": "https://moodle."
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ "/auth/saml2/sp/saml2-logout.php/moodle."
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ "",
|
||||||
"saml.signature.algorithm": "RSA_SHA256",
|
"saml.signature.algorithm": "RSA_SHA256",
|
||||||
"saml_force_name_id_format": False,
|
"saml_force_name_id_format": False,
|
||||||
"saml.client.signature": True,
|
"saml.client.signature": True,
|
||||||
"saml.encryption.certificate" : app['config']['SP_PEM'],
|
"saml.encryption.certificate": app["config"]["SP_PEM"],
|
||||||
"saml.authnstatement": True,
|
"saml.authnstatement": True,
|
||||||
"saml_name_id_format": "username",
|
"saml_name_id_format": "username",
|
||||||
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#"
|
"saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#",
|
||||||
},
|
},
|
||||||
"authenticationFlowBindingOverrides": {},
|
"authenticationFlowBindingOverrides": {},
|
||||||
"fullScopeAllowed": True,
|
"fullScopeAllowed": True,
|
||||||
"nodeReRegistrationTimeout": -1,
|
"nodeReRegistrationTimeout": -1,
|
||||||
"protocolMappers" : [ {
|
"protocolMappers": [
|
||||||
|
{
|
||||||
"id": "9296daa3-4fc4-4b80-b007-5070f546ae13",
|
"id": "9296daa3-4fc4-4b80-b007-5070f546ae13",
|
||||||
"name": "X500 sn",
|
"name": "X500 sn",
|
||||||
"protocol": "saml",
|
"protocol": "saml",
|
||||||
|
@ -197,9 +270,10 @@ class MoodleSaml():
|
||||||
"attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
|
"attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
|
||||||
"user.attribute": "lastName",
|
"user.attribute": "lastName",
|
||||||
"friendly.name": "sn",
|
"friendly.name": "sn",
|
||||||
"attribute.name" : "urn:oid:2.5.4.4"
|
"attribute.name": "urn:oid:2.5.4.4",
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
"id": "ccecf6e4-d20a-4211-b67c-40200a6b2c5d",
|
"id": "ccecf6e4-d20a-4211-b67c-40200a6b2c5d",
|
||||||
"name": "username",
|
"name": "username",
|
||||||
"protocol": "saml",
|
"protocol": "saml",
|
||||||
|
@ -209,9 +283,10 @@ class MoodleSaml():
|
||||||
"attribute.nameformat": "Basic",
|
"attribute.nameformat": "Basic",
|
||||||
"user.attribute": "username",
|
"user.attribute": "username",
|
||||||
"friendly.name": "username",
|
"friendly.name": "username",
|
||||||
"attribute.name" : "username"
|
"attribute.name": "username",
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
"id": "53858403-eba2-4f6d-81d0-cced700b5719",
|
"id": "53858403-eba2-4f6d-81d0-cced700b5719",
|
||||||
"name": "X500 givenName",
|
"name": "X500 givenName",
|
||||||
"protocol": "saml",
|
"protocol": "saml",
|
||||||
|
@ -221,9 +296,10 @@ class MoodleSaml():
|
||||||
"attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
|
"attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
|
||||||
"user.attribute": "firstName",
|
"user.attribute": "firstName",
|
||||||
"friendly.name": "givenName",
|
"friendly.name": "givenName",
|
||||||
"attribute.name" : "urn:oid:2.5.4.42"
|
"attribute.name": "urn:oid:2.5.4.42",
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
"id": "20034db5-1d0e-4e66-b815-fb0440c6d1e2",
|
"id": "20034db5-1d0e-4e66-b815-fb0440c6d1e2",
|
||||||
"name": "X500 email",
|
"name": "X500 email",
|
||||||
"protocol": "saml",
|
"protocol": "saml",
|
||||||
|
@ -233,16 +309,24 @@ class MoodleSaml():
|
||||||
"attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
|
"attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
|
||||||
"user.attribute": "email",
|
"user.attribute": "email",
|
||||||
"friendly.name": "email",
|
"friendly.name": "email",
|
||||||
"attribute.name" : "urn:oid:1.2.840.113549.1.9.1"
|
"attribute.name": "urn:oid:1.2.840.113549.1.9.1",
|
||||||
}
|
},
|
||||||
} ],
|
},
|
||||||
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ],
|
],
|
||||||
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ],
|
"defaultClientScopes": [
|
||||||
"access" : {
|
"web-origins",
|
||||||
"view" : True,
|
"role_list",
|
||||||
"configure" : True,
|
"roles",
|
||||||
"manage" : True
|
"profile",
|
||||||
}
|
"email",
|
||||||
|
],
|
||||||
|
"optionalClientScopes": [
|
||||||
|
"address",
|
||||||
|
"phone",
|
||||||
|
"offline_access",
|
||||||
|
"microprofile-jwt",
|
||||||
|
],
|
||||||
|
"access": {"view": True, "configure": True, "manage": True},
|
||||||
}
|
}
|
||||||
keycloak = KeycloakClient()
|
keycloak = KeycloakClient()
|
||||||
keycloak.add_client(client)
|
keycloak.add_client(client)
|
||||||
|
@ -250,10 +334,19 @@ class MoodleSaml():
|
||||||
|
|
||||||
def add_client_roles(self):
|
def add_client_roles(self):
|
||||||
keycloak = KeycloakClient()
|
keycloak = KeycloakClient()
|
||||||
keycloak.add_client_role('a92d5417-92b6-4678-9cb9-51bc0edcee8c','admin','Moodle admins')
|
keycloak.add_client_role(
|
||||||
keycloak.add_client_role('a92d5417-92b6-4678-9cb9-51bc0edcee8c','manager','Moodle managers')
|
"a92d5417-92b6-4678-9cb9-51bc0edcee8c", "admin", "Moodle admins"
|
||||||
keycloak.add_client_role('a92d5417-92b6-4678-9cb9-51bc0edcee8c','teacher','Moodle teachers')
|
)
|
||||||
keycloak.add_client_role('a92d5417-92b6-4678-9cb9-51bc0edcee8c','student','Moodle students')
|
keycloak.add_client_role(
|
||||||
|
"a92d5417-92b6-4678-9cb9-51bc0edcee8c", "manager", "Moodle managers"
|
||||||
|
)
|
||||||
|
keycloak.add_client_role(
|
||||||
|
"a92d5417-92b6-4678-9cb9-51bc0edcee8c", "teacher", "Moodle teachers"
|
||||||
|
)
|
||||||
|
keycloak.add_client_role(
|
||||||
|
"a92d5417-92b6-4678-9cb9-51bc0edcee8c", "student", "Moodle students"
|
||||||
|
)
|
||||||
keycloak = None
|
keycloak = None
|
||||||
|
|
||||||
|
|
||||||
m = MoodleSaml()
|
m = MoodleSaml()
|
||||||
|
|
|
@ -1,66 +1,81 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
import time, os
|
import json
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import pprint
|
|
||||||
|
|
||||||
import logging as log
|
import logging as log
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import yaml, json
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
import yaml
|
||||||
|
|
||||||
from admin.lib.postgres import Postgres
|
|
||||||
from admin.lib.keycloak_client import KeycloakClient
|
from admin.lib.keycloak_client import KeycloakClient
|
||||||
|
from admin.lib.postgres import Postgres
|
||||||
import string, random
|
|
||||||
|
|
||||||
app = {}
|
app = {}
|
||||||
app['config']={}
|
app["config"] = {}
|
||||||
|
|
||||||
class NextcloudSaml():
|
|
||||||
|
class NextcloudSaml:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.url = "http://isard-sso-keycloak:8080/auth/"
|
self.url = "http://isard-sso-keycloak:8080/auth/"
|
||||||
self.username=os.environ['KEYCLOAK_USER']
|
self.username = os.environ["KEYCLOAK_USER"]
|
||||||
self.password=os.environ['KEYCLOAK_PASSWORD']
|
self.password = os.environ["KEYCLOAK_PASSWORD"]
|
||||||
self.realm='master'
|
self.realm = "master"
|
||||||
self.verify = True
|
self.verify = True
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
self.pg=Postgres('isard-apps-postgresql','nextcloud',os.environ['NEXTCLOUD_POSTGRES_USER'],os.environ['NEXTCLOUD_POSTGRES_PASSWORD'])
|
self.pg = Postgres(
|
||||||
|
"isard-apps-postgresql",
|
||||||
|
"nextcloud",
|
||||||
|
os.environ["NEXTCLOUD_POSTGRES_USER"],
|
||||||
|
os.environ["NEXTCLOUD_POSTGRES_PASSWORD"],
|
||||||
|
)
|
||||||
ready = True
|
ready = True
|
||||||
except:
|
except:
|
||||||
log.warning('Could not connect to nextcloud database. Retrying...')
|
log.warning("Could not connect to nextcloud database. Retrying...")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
log.info('Connected to nextcloud database.')
|
log.info("Connected to nextcloud database.")
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join("./saml_certs/public.cert"), "r") as crt:
|
with open(os.path.join("./saml_certs/public.cert"), "r") as crt:
|
||||||
app['config']['PUBLIC_CERT']=crt.read()
|
app["config"]["PUBLIC_CERT"] = crt.read()
|
||||||
ready = True
|
ready = True
|
||||||
except IOError:
|
except IOError:
|
||||||
log.warning('Could not get public certificate to be used in nextcloud. Retrying...')
|
log.warning(
|
||||||
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert')
|
"Could not get public certificate to be used in nextcloud. Retrying..."
|
||||||
|
)
|
||||||
|
log.warning(
|
||||||
|
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
|
||||||
|
)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
log.info('Got moodle srt certificate to be used in nextcloud.')
|
log.info("Got moodle srt certificate to be used in nextcloud.")
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join("./saml_certs/private.key"), "r") as pem:
|
with open(os.path.join("./saml_certs/private.key"), "r") as pem:
|
||||||
app['config']['PRIVATE_KEY']=pem.read()
|
app["config"]["PRIVATE_KEY"] = pem.read()
|
||||||
ready = True
|
ready = True
|
||||||
except IOError:
|
except IOError:
|
||||||
log.warning('Could not get private key to be used in nextcloud. Retrying...')
|
log.warning(
|
||||||
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert')
|
"Could not get private key to be used in nextcloud. Retrying..."
|
||||||
|
)
|
||||||
|
log.warning(
|
||||||
|
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
|
||||||
|
)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
log.info('Got moodle pem certificate to be used in nextcloud.')
|
log.info("Got moodle pem certificate to be used in nextcloud.")
|
||||||
|
|
||||||
# ## This seems related to the fact that the certificate generated the first time does'nt work.
|
# ## This seems related to the fact that the certificate generated the first time does'nt work.
|
||||||
# ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
|
# ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
|
||||||
|
@ -77,30 +92,32 @@ class NextcloudSaml():
|
||||||
try:
|
try:
|
||||||
self.reset_saml()
|
self.reset_saml()
|
||||||
except:
|
except:
|
||||||
print('Error resetting saml on nextcloud')
|
print("Error resetting saml on nextcloud")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.delete_keycloak_nextcloud_saml_plugin()
|
self.delete_keycloak_nextcloud_saml_plugin()
|
||||||
except:
|
except:
|
||||||
print('Error resetting saml on keycloak')
|
print("Error resetting saml on keycloak")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.set_nextcloud_saml_plugin()
|
self.set_nextcloud_saml_plugin()
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
print('Error adding saml on nextcloud')
|
print("Error adding saml on nextcloud")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.add_keycloak_nextcloud_saml()
|
self.add_keycloak_nextcloud_saml()
|
||||||
except:
|
except:
|
||||||
print('Error adding saml on keycloak')
|
print("Error adding saml on keycloak")
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
self.keycloak= KeycloakClient(url=self.url,
|
self.keycloak = KeycloakClient(
|
||||||
|
url=self.url,
|
||||||
username=self.username,
|
username=self.username,
|
||||||
password=self.password,
|
password=self.password,
|
||||||
realm=self.realm,
|
realm=self.realm,
|
||||||
verify=self.verify)
|
verify=self.verify,
|
||||||
|
)
|
||||||
|
|
||||||
# def activate_saml_plugin(self):
|
# def activate_saml_plugin(self):
|
||||||
# ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php
|
# ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php
|
||||||
|
@ -113,7 +130,7 @@ class NextcloudSaml():
|
||||||
self.connect()
|
self.connect()
|
||||||
rsa = self.keycloak.get_server_rsa_key()
|
rsa = self.keycloak.get_server_rsa_key()
|
||||||
self.keycloak = None
|
self.keycloak = None
|
||||||
return rsa['certificate']
|
return rsa["certificate"]
|
||||||
|
|
||||||
def set_keycloak_nextcloud_saml_plugin(self):
|
def set_keycloak_nextcloud_saml_plugin(self):
|
||||||
self.connect()
|
self.connect()
|
||||||
|
@ -122,11 +139,12 @@ class NextcloudSaml():
|
||||||
|
|
||||||
def delete_keycloak_nextcloud_saml_plugin(self):
|
def delete_keycloak_nextcloud_saml_plugin(self):
|
||||||
self.connect()
|
self.connect()
|
||||||
self.keycloak.delete_client('bef873f0-2079-4876-8657-067de27d01b7')
|
self.keycloak.delete_client("bef873f0-2079-4876-8657-067de27d01b7")
|
||||||
self.keycloak = None
|
self.keycloak = None
|
||||||
|
|
||||||
def set_nextcloud_saml_plugin(self):
|
def set_nextcloud_saml_plugin(self):
|
||||||
self.pg.update("""INSERT INTO "oc_appconfig" ("appid", "configkey", "configvalue") VALUES
|
self.pg.update(
|
||||||
|
"""INSERT INTO "oc_appconfig" ("appid", "configkey", "configvalue") VALUES
|
||||||
('user_saml', 'general-uid_mapping', 'username'),
|
('user_saml', 'general-uid_mapping', 'username'),
|
||||||
('user_saml', 'type', 'saml'),
|
('user_saml', 'type', 'saml'),
|
||||||
('user_saml', 'sp-privateKey', '%s'),
|
('user_saml', 'sp-privateKey', '%s'),
|
||||||
|
@ -144,42 +162,60 @@ class NextcloudSaml():
|
||||||
('user_saml', 'security-wantAssertionsSigned', '1'),
|
('user_saml', 'security-wantAssertionsSigned', '1'),
|
||||||
('user_saml', 'general-idp0_display_name', 'SAML Login'),
|
('user_saml', 'general-idp0_display_name', 'SAML Login'),
|
||||||
('user_saml', 'sp-x509cert', '%s'),
|
('user_saml', 'sp-x509cert', '%s'),
|
||||||
('user_saml', 'idp-singleLogoutService.url', 'https://sso.%s/auth/realms/master/protocol/saml');""" % (app['config']['PRIVATE_KEY'],os.environ['DOMAIN'],os.environ['DOMAIN'],self.parse_idp_cert(),app['config']['PUBLIC_CERT'],os.environ['DOMAIN']))
|
('user_saml', 'idp-singleLogoutService.url', 'https://sso.%s/auth/realms/master/protocol/saml');"""
|
||||||
|
% (
|
||||||
|
app["config"]["PRIVATE_KEY"],
|
||||||
|
os.environ["DOMAIN"],
|
||||||
|
os.environ["DOMAIN"],
|
||||||
|
self.parse_idp_cert(),
|
||||||
|
app["config"]["PUBLIC_CERT"],
|
||||||
|
os.environ["DOMAIN"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def reset_saml(self):
|
def reset_saml(self):
|
||||||
cfg_list=['general-uid_mapping',
|
cfg_list = [
|
||||||
'sp-privateKey',
|
"general-uid_mapping",
|
||||||
'saml-attribute-mapping-email_mapping',
|
"sp-privateKey",
|
||||||
'saml-attribute-mapping-quota_mapping',
|
"saml-attribute-mapping-email_mapping",
|
||||||
'saml-attribute-mapping-displayName_mapping',
|
"saml-attribute-mapping-quota_mapping",
|
||||||
'saml-attribute-mapping-group_mapping',
|
"saml-attribute-mapping-displayName_mapping",
|
||||||
'idp-entityId',
|
"saml-attribute-mapping-group_mapping",
|
||||||
'idp-singleSignOnService.url',
|
"idp-entityId",
|
||||||
'idp-x509cert',
|
"idp-singleSignOnService.url",
|
||||||
'security-authnRequestsSigned',
|
"idp-x509cert",
|
||||||
'security-logoutRequestSigned',
|
"security-authnRequestsSigned",
|
||||||
'security-logoutResponseSigned',
|
"security-logoutRequestSigned",
|
||||||
'security-wantMessagesSigned',
|
"security-logoutResponseSigned",
|
||||||
'security-wantAssertionsSigned',
|
"security-wantMessagesSigned",
|
||||||
'general-idp0_display_name',
|
"security-wantAssertionsSigned",
|
||||||
'type',
|
"general-idp0_display_name",
|
||||||
'sp-x509cert',
|
"type",
|
||||||
'idp-singleLogoutService.url']
|
"sp-x509cert",
|
||||||
|
"idp-singleLogoutService.url",
|
||||||
|
]
|
||||||
for cfg in cfg_list:
|
for cfg in cfg_list:
|
||||||
self.pg.update("""DELETE FROM "oc_appconfig" WHERE appid = 'user_saml' AND configkey = '%s'""" % (cfg))
|
self.pg.update(
|
||||||
|
"""DELETE FROM "oc_appconfig" WHERE appid = 'user_saml' AND configkey = '%s'"""
|
||||||
|
% (cfg)
|
||||||
|
)
|
||||||
|
|
||||||
def add_keycloak_nextcloud_saml(self):
|
def add_keycloak_nextcloud_saml(self):
|
||||||
client={"id" : "bef873f0-2079-4876-8657-067de27d01b7",
|
client = {
|
||||||
|
"id": "bef873f0-2079-4876-8657-067de27d01b7",
|
||||||
"name": "nextcloud",
|
"name": "nextcloud",
|
||||||
"description": "nextcloud",
|
"description": "nextcloud",
|
||||||
"clientId" : "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/metadata",
|
"clientId": "https://nextcloud."
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ "/apps/user_saml/saml/metadata",
|
||||||
"surrogateAuthRequired": False,
|
"surrogateAuthRequired": False,
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"alwaysDisplayInConsole": False,
|
"alwaysDisplayInConsole": False,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris" : [ "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/acs" ],
|
"redirectUris": [
|
||||||
"webOrigins" : [ "https://nextcloud."+os.environ['DOMAIN'] ],
|
"https://nextcloud." + os.environ["DOMAIN"] + "/apps/user_saml/saml/acs"
|
||||||
|
],
|
||||||
|
"webOrigins": ["https://nextcloud." + os.environ["DOMAIN"]],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": False,
|
"bearerOnly": False,
|
||||||
"consentRequired": False,
|
"consentRequired": False,
|
||||||
|
@ -193,22 +229,27 @@ class NextcloudSaml():
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"saml.assertion.signature": True,
|
"saml.assertion.signature": True,
|
||||||
"saml.force.post.binding": True,
|
"saml.force.post.binding": True,
|
||||||
"saml_assertion_consumer_url_post" : "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/acs",
|
"saml_assertion_consumer_url_post": "https://nextcloud."
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ "/apps/user_saml/saml/acs",
|
||||||
"saml.server.signature": True,
|
"saml.server.signature": True,
|
||||||
"saml.server.signature.keyinfo.ext": False,
|
"saml.server.signature.keyinfo.ext": False,
|
||||||
"saml.signing.certificate" : app['config']['PUBLIC_CERT'],
|
"saml.signing.certificate": app["config"]["PUBLIC_CERT"],
|
||||||
"saml_single_logout_service_url_redirect" : "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/sls",
|
"saml_single_logout_service_url_redirect": "https://nextcloud."
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ "/apps/user_saml/saml/sls",
|
||||||
"saml.signature.algorithm": "RSA_SHA256",
|
"saml.signature.algorithm": "RSA_SHA256",
|
||||||
"saml_force_name_id_format": False,
|
"saml_force_name_id_format": False,
|
||||||
"saml.client.signature": False,
|
"saml.client.signature": False,
|
||||||
"saml.authnstatement": True,
|
"saml.authnstatement": True,
|
||||||
"saml_name_id_format": "username",
|
"saml_name_id_format": "username",
|
||||||
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#"
|
"saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#",
|
||||||
},
|
},
|
||||||
"authenticationFlowBindingOverrides": {},
|
"authenticationFlowBindingOverrides": {},
|
||||||
"fullScopeAllowed": True,
|
"fullScopeAllowed": True,
|
||||||
"nodeReRegistrationTimeout": -1,
|
"nodeReRegistrationTimeout": -1,
|
||||||
"protocolMappers" : [ {
|
"protocolMappers": [
|
||||||
|
{
|
||||||
"id": "e8e4acff-da2b-46aa-8bdb-ba42171671d6",
|
"id": "e8e4acff-da2b-46aa-8bdb-ba42171671d6",
|
||||||
"name": "username",
|
"name": "username",
|
||||||
"protocol": "saml",
|
"protocol": "saml",
|
||||||
|
@ -218,9 +259,10 @@ class NextcloudSaml():
|
||||||
"attribute.nameformat": "Basic",
|
"attribute.nameformat": "Basic",
|
||||||
"user.attribute": "username",
|
"user.attribute": "username",
|
||||||
"friendly.name": "username",
|
"friendly.name": "username",
|
||||||
"attribute.name" : "username"
|
"attribute.name": "username",
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
"id": "8ab13cd7-822a-40d5-a1e1-9f556aed2332",
|
"id": "8ab13cd7-822a-40d5-a1e1-9f556aed2332",
|
||||||
"name": "quota",
|
"name": "quota",
|
||||||
"protocol": "saml",
|
"protocol": "saml",
|
||||||
|
@ -230,9 +272,10 @@ class NextcloudSaml():
|
||||||
"attribute.nameformat": "Basic",
|
"attribute.nameformat": "Basic",
|
||||||
"user.attribute": "quota",
|
"user.attribute": "quota",
|
||||||
"friendly.name": "quota",
|
"friendly.name": "quota",
|
||||||
"attribute.name" : "quota"
|
"attribute.name": "quota",
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
"id": "28206b59-757b-4e3c-81cb-0b6053b1fd3d",
|
"id": "28206b59-757b-4e3c-81cb-0b6053b1fd3d",
|
||||||
"name": "email",
|
"name": "email",
|
||||||
"protocol": "saml",
|
"protocol": "saml",
|
||||||
|
@ -242,9 +285,10 @@ class NextcloudSaml():
|
||||||
"attribute.nameformat": "Basic",
|
"attribute.nameformat": "Basic",
|
||||||
"user.attribute": "email",
|
"user.attribute": "email",
|
||||||
"friendly.name": "email",
|
"friendly.name": "email",
|
||||||
"attribute.name" : "email"
|
"attribute.name": "email",
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
"id": "5176a593-180f-4924-b294-b83a0d8d5972",
|
"id": "5176a593-180f-4924-b294-b83a0d8d5972",
|
||||||
"name": "displayname",
|
"name": "displayname",
|
||||||
"protocol": "saml",
|
"protocol": "saml",
|
||||||
|
@ -252,12 +296,13 @@ class NextcloudSaml():
|
||||||
"consentRequired": False,
|
"consentRequired": False,
|
||||||
"config": {
|
"config": {
|
||||||
"single": False,
|
"single": False,
|
||||||
"Script" : "/**\n * Available variables: \n * user - the current user\n * realm - the current realm\n * clientSession - the current clientSession\n * userSession - the current userSession\n * keycloakSession - the current keycloakSession\n */\n\n\n//insert your code here...\nvar Output = user.getFirstName()+\" \"+user.getLastName();\nOutput;\n",
|
"Script": '/**\n * Available variables: \n * user - the current user\n * realm - the current realm\n * clientSession - the current clientSession\n * userSession - the current userSession\n * keycloakSession - the current keycloakSession\n */\n\n\n//insert your code here...\nvar Output = user.getFirstName()+" "+user.getLastName();\nOutput;\n',
|
||||||
"attribute.nameformat": "Basic",
|
"attribute.nameformat": "Basic",
|
||||||
"friendly.name": "displayname",
|
"friendly.name": "displayname",
|
||||||
"attribute.name" : "displayname"
|
"attribute.name": "displayname",
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
"id": "e51e04b9-f71a-42de-819e-dd9285246ada",
|
"id": "e51e04b9-f71a-42de-819e-dd9285246ada",
|
||||||
"name": "Roles",
|
"name": "Roles",
|
||||||
"protocol": "saml",
|
"protocol": "saml",
|
||||||
|
@ -267,9 +312,10 @@ class NextcloudSaml():
|
||||||
"single": True,
|
"single": True,
|
||||||
"attribute.nameformat": "Basic",
|
"attribute.nameformat": "Basic",
|
||||||
"friendly.name": "Roles",
|
"friendly.name": "Roles",
|
||||||
"attribute.name" : "Roles"
|
"attribute.name": "Roles",
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
"id": "9c101249-bb09-4cc8-8f75-5a18fcb307e6",
|
"id": "9c101249-bb09-4cc8-8f75-5a18fcb307e6",
|
||||||
"name": "group_list",
|
"name": "group_list",
|
||||||
"protocol": "saml",
|
"protocol": "saml",
|
||||||
|
@ -280,19 +326,28 @@ class NextcloudSaml():
|
||||||
"attribute.nameformat": "Basic",
|
"attribute.nameformat": "Basic",
|
||||||
"full.path": False,
|
"full.path": False,
|
||||||
"friendly.name": "member",
|
"friendly.name": "member",
|
||||||
"attribute.name" : "member"
|
"attribute.name": "member",
|
||||||
}
|
},
|
||||||
} ],
|
},
|
||||||
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ],
|
],
|
||||||
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ],
|
"defaultClientScopes": [
|
||||||
"access" : {
|
"web-origins",
|
||||||
"view" : True,
|
"role_list",
|
||||||
"configure" : True,
|
"roles",
|
||||||
"manage" : True
|
"profile",
|
||||||
}
|
"email",
|
||||||
|
],
|
||||||
|
"optionalClientScopes": [
|
||||||
|
"address",
|
||||||
|
"phone",
|
||||||
|
"offline_access",
|
||||||
|
"microprofile-jwt",
|
||||||
|
],
|
||||||
|
"access": {"view": True, "configure": True, "manage": True},
|
||||||
}
|
}
|
||||||
self.connect()
|
self.connect()
|
||||||
self.keycloak.add_client(client)
|
self.keycloak.add_client(client)
|
||||||
self.keycloak = None
|
self.keycloak = None
|
||||||
|
|
||||||
|
|
||||||
n = NextcloudSaml()
|
n = NextcloudSaml()
|
|
@ -1,66 +1,81 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
import time, os
|
import json
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import pprint
|
|
||||||
|
|
||||||
import logging as log
|
import logging as log
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import yaml, json
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
import yaml
|
||||||
|
|
||||||
from admin.lib.postgres import Postgres
|
|
||||||
from admin.lib.keycloak_client import KeycloakClient
|
from admin.lib.keycloak_client import KeycloakClient
|
||||||
|
from admin.lib.postgres import Postgres
|
||||||
import string, random
|
|
||||||
|
|
||||||
app = {}
|
app = {}
|
||||||
app['config']={}
|
app["config"] = {}
|
||||||
|
|
||||||
class NextcloudSaml():
|
|
||||||
|
class NextcloudSaml:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.url = "http://isard-sso-keycloak:8080/auth/"
|
self.url = "http://isard-sso-keycloak:8080/auth/"
|
||||||
self.username=os.environ['KEYCLOAK_USER']
|
self.username = os.environ["KEYCLOAK_USER"]
|
||||||
self.password=os.environ['KEYCLOAK_PASSWORD']
|
self.password = os.environ["KEYCLOAK_PASSWORD"]
|
||||||
self.realm='master'
|
self.realm = "master"
|
||||||
self.verify = True
|
self.verify = True
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
self.pg=Postgres('isard-apps-postgresql','nextcloud',os.environ['NEXTCLOUD_POSTGRES_USER'],os.environ['NEXTCLOUD_POSTGRES_PASSWORD'])
|
self.pg = Postgres(
|
||||||
|
"isard-apps-postgresql",
|
||||||
|
"nextcloud",
|
||||||
|
os.environ["NEXTCLOUD_POSTGRES_USER"],
|
||||||
|
os.environ["NEXTCLOUD_POSTGRES_PASSWORD"],
|
||||||
|
)
|
||||||
ready = True
|
ready = True
|
||||||
except:
|
except:
|
||||||
log.warning('Could not connect to nextcloud database. Retrying...')
|
log.warning("Could not connect to nextcloud database. Retrying...")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
log.info('Connected to nextcloud database.')
|
log.info("Connected to nextcloud database.")
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join("./saml_certs/public.cert"), "r") as crt:
|
with open(os.path.join("./saml_certs/public.cert"), "r") as crt:
|
||||||
app['config']['PUBLIC_CERT']=crt.read()
|
app["config"]["PUBLIC_CERT"] = crt.read()
|
||||||
ready = True
|
ready = True
|
||||||
except IOError:
|
except IOError:
|
||||||
log.warning('Could not get public certificate to be used in nextcloud. Retrying...')
|
log.warning(
|
||||||
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert')
|
"Could not get public certificate to be used in nextcloud. Retrying..."
|
||||||
|
)
|
||||||
|
log.warning(
|
||||||
|
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
|
||||||
|
)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
log.info('Got moodle srt certificate to be used in nextcloud.')
|
log.info("Got moodle srt certificate to be used in nextcloud.")
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join("./saml_certs/private.key"), "r") as pem:
|
with open(os.path.join("./saml_certs/private.key"), "r") as pem:
|
||||||
app['config']['PRIVATE_KEY']=pem.read()
|
app["config"]["PRIVATE_KEY"] = pem.read()
|
||||||
ready = True
|
ready = True
|
||||||
except IOError:
|
except IOError:
|
||||||
log.warning('Could not get private key to be used in nextcloud. Retrying...')
|
log.warning(
|
||||||
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert')
|
"Could not get private key to be used in nextcloud. Retrying..."
|
||||||
|
)
|
||||||
|
log.warning(
|
||||||
|
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
|
||||||
|
)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
log.info('Got moodle pem certificate to be used in nextcloud.')
|
log.info("Got moodle pem certificate to be used in nextcloud.")
|
||||||
|
|
||||||
# ## This seems related to the fact that the certificate generated the first time does'nt work.
|
# ## This seems related to the fact that the certificate generated the first time does'nt work.
|
||||||
# ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
|
# ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
|
||||||
|
@ -77,20 +92,22 @@ class NextcloudSaml():
|
||||||
try:
|
try:
|
||||||
self.reset_saml_certs()
|
self.reset_saml_certs()
|
||||||
except:
|
except:
|
||||||
print('Error resetting saml on nextcloud')
|
print("Error resetting saml on nextcloud")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.set_nextcloud_saml_plugin_certs()
|
self.set_nextcloud_saml_plugin_certs()
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
print('Error adding saml on nextcloud')
|
print("Error adding saml on nextcloud")
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
self.keycloak= KeycloakClient(url=self.url,
|
self.keycloak = KeycloakClient(
|
||||||
|
url=self.url,
|
||||||
username=self.username,
|
username=self.username,
|
||||||
password=self.password,
|
password=self.password,
|
||||||
realm=self.realm,
|
realm=self.realm,
|
||||||
verify=self.verify)
|
verify=self.verify,
|
||||||
|
)
|
||||||
|
|
||||||
# def activate_saml_plugin(self):
|
# def activate_saml_plugin(self):
|
||||||
# ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php
|
# ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php
|
||||||
|
@ -103,20 +120,28 @@ class NextcloudSaml():
|
||||||
self.connect()
|
self.connect()
|
||||||
rsa = self.keycloak.get_server_rsa_key()
|
rsa = self.keycloak.get_server_rsa_key()
|
||||||
self.keycloak = None
|
self.keycloak = None
|
||||||
return rsa['certificate']
|
return rsa["certificate"]
|
||||||
|
|
||||||
def set_nextcloud_saml_plugin_certs(self):
|
def set_nextcloud_saml_plugin_certs(self):
|
||||||
self.pg.update("""INSERT INTO "oc_appconfig" ("appid", "configkey", "configvalue") VALUES
|
self.pg.update(
|
||||||
|
"""INSERT INTO "oc_appconfig" ("appid", "configkey", "configvalue") VALUES
|
||||||
('user_saml', 'sp-privateKey', '%s'),
|
('user_saml', 'sp-privateKey', '%s'),
|
||||||
('user_saml', 'idp-x509cert', '%s'),
|
('user_saml', 'idp-x509cert', '%s'),
|
||||||
('user_saml', 'sp-x509cert', '%s');""" % (app['config']['PRIVATE_KEY'],self.parse_idp_cert(),app['config']['PUBLIC_CERT']))
|
('user_saml', 'sp-x509cert', '%s');"""
|
||||||
|
% (
|
||||||
|
app["config"]["PRIVATE_KEY"],
|
||||||
|
self.parse_idp_cert(),
|
||||||
|
app["config"]["PUBLIC_CERT"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def reset_saml_certs(self):
|
def reset_saml_certs(self):
|
||||||
cfg_list=['sp-privateKey',
|
cfg_list = ["sp-privateKey", "idp-x509cert", "sp-x509cert"]
|
||||||
'idp-x509cert',
|
|
||||||
'sp-x509cert']
|
|
||||||
for cfg in cfg_list:
|
for cfg in cfg_list:
|
||||||
self.pg.update("""DELETE FROM "oc_appconfig" WHERE appid = 'user_saml' AND configkey = '%s'""" % (cfg))
|
self.pg.update(
|
||||||
|
"""DELETE FROM "oc_appconfig" WHERE appid = 'user_saml' AND configkey = '%s'"""
|
||||||
|
% (cfg)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
n = NextcloudSaml()
|
n = NextcloudSaml()
|
|
@ -1,24 +1,21 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
import time
|
import json
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import pprint
|
|
||||||
|
|
||||||
import logging as log
|
import logging as log
|
||||||
|
import pprint
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import yaml, json
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
import yaml
|
||||||
|
|
||||||
class Postgres():
|
|
||||||
|
|
||||||
|
class Postgres:
|
||||||
def __init__(self, host, database, user, password):
|
def __init__(self, host, database, user, password):
|
||||||
self.conn = psycopg2.connect(
|
self.conn = psycopg2.connect(
|
||||||
host=host,
|
host=host, database=database, user=user, password=password
|
||||||
database=database,
|
)
|
||||||
user=user,
|
|
||||||
password=password)
|
|
||||||
|
|
||||||
|
|
||||||
# def __del__(self):
|
# def __del__(self):
|
||||||
# self.cur.close()
|
# self.cur.close()
|
||||||
|
|
|
@ -1,64 +1,82 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import time ,os
|
import json
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import logging as log
|
import logging as log
|
||||||
|
import os
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import yaml, json
|
from datetime import datetime, timedelta
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
|
import yaml
|
||||||
from keycloak import KeycloakAdmin
|
from keycloak import KeycloakAdmin
|
||||||
from postgres import Postgres
|
|
||||||
|
|
||||||
from minio import Minio
|
from minio import Minio
|
||||||
from minio.commonconfig import REPLACE, CopySource
|
from minio.commonconfig import REPLACE, CopySource
|
||||||
from minio.deleteobjects import DeleteObject
|
from minio.deleteobjects import DeleteObject
|
||||||
|
|
||||||
class DefaultAvatars():
|
from postgres import Postgres
|
||||||
def __init__(self,
|
|
||||||
|
|
||||||
|
class DefaultAvatars:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
url="http://isard-sso-keycloak:8080/auth/",
|
url="http://isard-sso-keycloak:8080/auth/",
|
||||||
username=os.environ['KEYCLOAK_USER'],
|
username=os.environ["KEYCLOAK_USER"],
|
||||||
password=os.environ['KEYCLOAK_PASSWORD'],
|
password=os.environ["KEYCLOAK_PASSWORD"],
|
||||||
realm='master',
|
realm="master",
|
||||||
verify=True):
|
verify=True,
|
||||||
|
):
|
||||||
self.url = url
|
self.url = url
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.realm = realm
|
self.realm = realm
|
||||||
self.verify = verify
|
self.verify = verify
|
||||||
|
|
||||||
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD'])
|
self.keycloak_pg = Postgres(
|
||||||
|
"isard-apps-postgresql",
|
||||||
|
"keycloak",
|
||||||
|
os.environ["KEYCLOAK_DB_USER"],
|
||||||
|
os.environ["KEYCLOAK_DB_PASSWORD"],
|
||||||
|
)
|
||||||
|
|
||||||
self.mclient = Minio(
|
self.mclient = Minio(
|
||||||
"isard-sso-avatars:9000",
|
"isard-sso-avatars:9000",
|
||||||
access_key="AKIAIOSFODNN7EXAMPLE",
|
access_key="AKIAIOSFODNN7EXAMPLE",
|
||||||
secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||||
secure=False
|
secure=False,
|
||||||
)
|
)
|
||||||
self.bucket='master-avatars'
|
self.bucket = "master-avatars"
|
||||||
self._minio_set_realm()
|
self._minio_set_realm()
|
||||||
self.update_missing_avatars()
|
self.update_missing_avatars()
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
self.keycloak_admin = KeycloakAdmin(server_url=self.url,
|
self.keycloak_admin = KeycloakAdmin(
|
||||||
|
server_url=self.url,
|
||||||
username=self.username,
|
username=self.username,
|
||||||
password=self.password,
|
password=self.password,
|
||||||
realm_name=self.realm,
|
realm_name=self.realm,
|
||||||
verify=self.verify)
|
verify=self.verify,
|
||||||
|
)
|
||||||
|
|
||||||
def update_missing_avatars(self):
|
def update_missing_avatars(self):
|
||||||
sys_roles=['admin','manager','teacher','student']
|
sys_roles = ["admin", "manager", "teacher", "student"]
|
||||||
for u in self.get_users_without_image():
|
for u in self.get_users_without_image():
|
||||||
try:
|
try:
|
||||||
img=[r+'.jpg' for r in sys_roles if r in u['role']][0]
|
img = [r + ".jpg" for r in sys_roles if r in u["role"]][0]
|
||||||
except:
|
except:
|
||||||
img='unknown.jpg'
|
img = "unknown.jpg"
|
||||||
|
|
||||||
self.mclient.fput_object(
|
self.mclient.fput_object(
|
||||||
self.bucket, u['id'], "custom/avatars/"+img,
|
self.bucket,
|
||||||
|
u["id"],
|
||||||
|
"custom/avatars/" + img,
|
||||||
content_type="image/jpeg ",
|
content_type="image/jpeg ",
|
||||||
)
|
)
|
||||||
log.warning(' AVATARS: Updated avatar for user '+u['username']+' with role '+img.split('.')[0])
|
log.warning(
|
||||||
|
" AVATARS: Updated avatar for user "
|
||||||
|
+ u["username"]
|
||||||
|
+ " with role "
|
||||||
|
+ img.split(".")[0]
|
||||||
|
)
|
||||||
|
|
||||||
def _minio_set_realm(self):
|
def _minio_set_realm(self):
|
||||||
if not self.mclient.bucket_exists(self.bucket):
|
if not self.mclient.bucket_exists(self.bucket):
|
||||||
|
@ -82,7 +100,7 @@ class DefaultAvatars():
|
||||||
return users
|
return users
|
||||||
|
|
||||||
def get_users_without_image(self):
|
def get_users_without_image(self):
|
||||||
return [u for u in self.get_users() if u['id'] not in self.minio_get_objects()]
|
return [u for u in self.get_users() if u["id"] not in self.minio_get_objects()]
|
||||||
|
|
||||||
def get_users_with_groups_and_roles(self):
|
def get_users_with_groups_and_roles(self):
|
||||||
q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota
|
q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota
|
||||||
|
@ -101,18 +119,26 @@ class DefaultAvatars():
|
||||||
|
|
||||||
(headers, users) = self.keycloak_pg.select_with_headers(q)
|
(headers, users) = self.keycloak_pg.select_with_headers(q)
|
||||||
|
|
||||||
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
|
users_with_lists = [
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
|
list(l[:-4])
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
|
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
|
||||||
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
|
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
|
||||||
|
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
|
||||||
|
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||||
|
for l in users
|
||||||
|
]
|
||||||
|
|
||||||
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
|
users_with_lists = [
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
|
list(l[:-4])
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
|
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
|
||||||
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
|
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
|
||||||
|
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
|
||||||
|
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||||
|
for l in users_with_lists
|
||||||
|
]
|
||||||
|
|
||||||
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
|
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
|
||||||
return list_dict_users
|
return list_dict_users
|
||||||
|
|
||||||
da=DefaultAvatars()
|
|
||||||
|
|
||||||
|
da = DefaultAvatars()
|
||||||
|
|
|
@ -1,48 +1,59 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import time ,os
|
import json
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import logging as log
|
import logging as log
|
||||||
|
import os
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import yaml, json
|
from datetime import datetime, timedelta
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
|
import diceware
|
||||||
|
import yaml
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
from keycloak import KeycloakAdmin
|
from keycloak import KeycloakAdmin
|
||||||
|
|
||||||
from postgres import Postgres
|
from postgres import Postgres
|
||||||
|
|
||||||
import diceware
|
|
||||||
options = diceware.handle_options(None)
|
options = diceware.handle_options(None)
|
||||||
options.wordlist = 'cat_ascii'
|
options.wordlist = "cat_ascii"
|
||||||
options.num = 3
|
options.num = 3
|
||||||
|
|
||||||
class KeycloakClient():
|
|
||||||
|
class KeycloakClient:
|
||||||
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
|
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
|
||||||
https://github.com/marcospereirampj/python-keycloak
|
https://github.com/marcospereirampj/python-keycloak
|
||||||
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
|
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
|
||||||
"""
|
"""
|
||||||
def __init__(self,
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
url="http://isard-sso-keycloak:8080/auth/",
|
url="http://isard-sso-keycloak:8080/auth/",
|
||||||
username=os.environ['KEYCLOAK_USER'],
|
username=os.environ["KEYCLOAK_USER"],
|
||||||
password=os.environ['KEYCLOAK_PASSWORD'],
|
password=os.environ["KEYCLOAK_PASSWORD"],
|
||||||
realm='master',
|
realm="master",
|
||||||
verify=True):
|
verify=True,
|
||||||
|
):
|
||||||
self.url = url
|
self.url = url
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.realm = realm
|
self.realm = realm
|
||||||
self.verify = verify
|
self.verify = verify
|
||||||
|
|
||||||
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD'])
|
self.keycloak_pg = Postgres(
|
||||||
|
"isard-apps-postgresql",
|
||||||
|
"keycloak",
|
||||||
|
os.environ["KEYCLOAK_DB_USER"],
|
||||||
|
os.environ["KEYCLOAK_DB_PASSWORD"],
|
||||||
|
)
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
self.keycloak_admin = KeycloakAdmin(server_url=self.url,
|
self.keycloak_admin = KeycloakAdmin(
|
||||||
|
server_url=self.url,
|
||||||
username=self.username,
|
username=self.username,
|
||||||
password=self.password,
|
password=self.password,
|
||||||
realm_name=self.realm,
|
realm_name=self.realm,
|
||||||
verify=self.verify)
|
verify=self.verify,
|
||||||
|
)
|
||||||
|
|
||||||
def update_pwds(self):
|
def update_pwds(self):
|
||||||
self.get_users()
|
self.get_users()
|
||||||
|
@ -52,27 +63,36 @@ class KeycloakClient():
|
||||||
users = self.get_users_with_groups_and_roles()
|
users = self.get_users_with_groups_and_roles()
|
||||||
userupdate = []
|
userupdate = []
|
||||||
for u in users:
|
for u in users:
|
||||||
if u['username'] not in ['admin','ddadmin'] and not u['username'].startswith('system_'):
|
if u["username"] not in ["admin", "ddadmin"] and not u[
|
||||||
print('Generating password for user '+u['username'])
|
"username"
|
||||||
userupdate.append({'id':u['id'],
|
].startswith("system_"):
|
||||||
'username':u['username'],
|
print("Generating password for user " + u["username"])
|
||||||
'password': diceware.get_passphrase(options=options)})
|
userupdate.append(
|
||||||
|
{
|
||||||
|
"id": u["id"],
|
||||||
|
"username": u["username"],
|
||||||
|
"password": diceware.get_passphrase(options=options),
|
||||||
|
}
|
||||||
|
)
|
||||||
with open("user_temp_passwd.csv", "w") as csv:
|
with open("user_temp_passwd.csv", "w") as csv:
|
||||||
for user in userupdate:
|
for user in userupdate:
|
||||||
csv.write("%s,%s,%s\n"%(user['id'],user['username'],user['password']))
|
csv.write(
|
||||||
|
"%s,%s,%s\n" % (user["id"], user["username"], user["password"])
|
||||||
|
)
|
||||||
|
|
||||||
for u in userupdate:
|
for u in userupdate:
|
||||||
print('Updating keycloak password for user '+u['username'])
|
print("Updating keycloak password for user " + u["username"])
|
||||||
self.update_user_pwd(u['id'],u['password'])
|
self.update_user_pwd(u["id"], u["password"])
|
||||||
|
|
||||||
def update_user_pwd(self, user_id, password, temporary=True):
|
def update_user_pwd(self, user_id, password, temporary=True):
|
||||||
payload={"credentials":[{"type":"password",
|
payload = {
|
||||||
"value":password,
|
"credentials": [
|
||||||
"temporary":temporary}]}
|
{"type": "password", "value": password, "temporary": temporary}
|
||||||
|
]
|
||||||
|
}
|
||||||
self.connect()
|
self.connect()
|
||||||
self.keycloak_admin.update_user(user_id, payload)
|
self.keycloak_admin.update_user(user_id, payload)
|
||||||
|
|
||||||
|
|
||||||
def get_users_with_groups_and_roles(self):
|
def get_users_with_groups_and_roles(self):
|
||||||
q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota
|
q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota
|
||||||
,json_agg(g."name") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2
|
,json_agg(g."name") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2
|
||||||
|
@ -118,18 +138,27 @@ class KeycloakClient():
|
||||||
# order by u.username"""
|
# order by u.username"""
|
||||||
(headers, users) = self.keycloak_pg.select_with_headers(q)
|
(headers, users) = self.keycloak_pg.select_with_headers(q)
|
||||||
|
|
||||||
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
|
users_with_lists = [
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
|
list(l[:-4])
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
|
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
|
||||||
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
|
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
|
||||||
|
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
|
||||||
|
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||||
|
for l in users
|
||||||
|
]
|
||||||
|
|
||||||
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
|
users_with_lists = [
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
|
list(l[:-4])
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
|
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
|
||||||
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
|
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
|
||||||
|
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
|
||||||
|
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||||
|
for l in users_with_lists
|
||||||
|
]
|
||||||
|
|
||||||
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
|
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
|
||||||
return list_dict_users
|
return list_dict_users
|
||||||
|
|
||||||
|
|
||||||
k = KeycloakClient()
|
k = KeycloakClient()
|
||||||
k.update_pwds()
|
k.update_pwds()
|
||||||
|
|
|
@ -1,44 +1,54 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import time ,os
|
import json
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import logging as log
|
import logging as log
|
||||||
|
import os
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import yaml, json
|
from datetime import datetime, timedelta
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
|
import yaml
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
from keycloak import KeycloakAdmin
|
from keycloak import KeycloakAdmin
|
||||||
|
|
||||||
from postgres import Postgres
|
from postgres import Postgres
|
||||||
|
|
||||||
|
|
||||||
class KeycloakClient():
|
class KeycloakClient:
|
||||||
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
|
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
|
||||||
https://github.com/marcospereirampj/python-keycloak
|
https://github.com/marcospereirampj/python-keycloak
|
||||||
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
|
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
|
||||||
"""
|
"""
|
||||||
def __init__(self,
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
url="http://isard-sso-keycloak:8080/auth/",
|
url="http://isard-sso-keycloak:8080/auth/",
|
||||||
username=os.environ['KEYCLOAK_USER'],
|
username=os.environ["KEYCLOAK_USER"],
|
||||||
password=os.environ['KEYCLOAK_PASSWORD'],
|
password=os.environ["KEYCLOAK_PASSWORD"],
|
||||||
realm='master',
|
realm="master",
|
||||||
verify=True):
|
verify=True,
|
||||||
|
):
|
||||||
self.url = url
|
self.url = url
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.realm = realm
|
self.realm = realm
|
||||||
self.verify = verify
|
self.verify = verify
|
||||||
|
|
||||||
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD'])
|
self.keycloak_pg = Postgres(
|
||||||
|
"isard-apps-postgresql",
|
||||||
|
"keycloak",
|
||||||
|
os.environ["KEYCLOAK_DB_USER"],
|
||||||
|
os.environ["KEYCLOAK_DB_PASSWORD"],
|
||||||
|
)
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
self.keycloak_admin = KeycloakAdmin(server_url=self.url,
|
self.keycloak_admin = KeycloakAdmin(
|
||||||
|
server_url=self.url,
|
||||||
username=self.username,
|
username=self.username,
|
||||||
password=self.password,
|
password=self.password,
|
||||||
realm_name=self.realm,
|
realm_name=self.realm,
|
||||||
verify=self.verify)
|
verify=self.verify,
|
||||||
|
)
|
||||||
|
|
||||||
def update_pwds(self):
|
def update_pwds(self):
|
||||||
self.get_users()
|
self.get_users()
|
||||||
|
@ -48,18 +58,26 @@ class KeycloakClient():
|
||||||
users = self.get_users_with_groups_and_roles()
|
users = self.get_users_with_groups_and_roles()
|
||||||
userupdate = []
|
userupdate = []
|
||||||
for u in users:
|
for u in users:
|
||||||
if u['username'] not in ['admin','ddadmin'] and not u['username'].startswith('system_'):
|
if u["username"] not in ["admin", "ddadmin"] and not u[
|
||||||
print('Generating password for user '+u['username'])
|
"username"
|
||||||
userupdate.append({'id':u['id'],
|
].startswith("system_"):
|
||||||
'username':u['username'],
|
print("Generating password for user " + u["username"])
|
||||||
'password': diceware.get_passphrase(options=options)})
|
userupdate.append(
|
||||||
|
{
|
||||||
|
"id": u["id"],
|
||||||
|
"username": u["username"],
|
||||||
|
"password": diceware.get_passphrase(options=options),
|
||||||
|
}
|
||||||
|
)
|
||||||
with open("user_temp_passwd.csv", "w") as csv:
|
with open("user_temp_passwd.csv", "w") as csv:
|
||||||
for user in userupdate:
|
for user in userupdate:
|
||||||
csv.write("%s,%s,%s\n"%(user['id'],user['username'],user['password']))
|
csv.write(
|
||||||
|
"%s,%s,%s\n" % (user["id"], user["username"], user["password"])
|
||||||
|
)
|
||||||
|
|
||||||
for u in userupdate:
|
for u in userupdate:
|
||||||
print('Updating keycloak password for user '+u['username'])
|
print("Updating keycloak password for user " + u["username"])
|
||||||
self.update_user_pwd(u['id'],u['password'])
|
self.update_user_pwd(u["id"], u["password"])
|
||||||
|
|
||||||
def update_user_pwd_temporary(self, temporary=False):
|
def update_user_pwd_temporary(self, temporary=False):
|
||||||
payload = {"credentials": [{"temporary": temporary}]}
|
payload = {"credentials": [{"temporary": temporary}]}
|
||||||
|
@ -111,18 +129,27 @@ class KeycloakClient():
|
||||||
# order by u.username"""
|
# order by u.username"""
|
||||||
(headers, users) = self.keycloak_pg.select_with_headers(q)
|
(headers, users) = self.keycloak_pg.select_with_headers(q)
|
||||||
|
|
||||||
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
|
users_with_lists = [
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
|
list(l[:-4])
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
|
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
|
||||||
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
|
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
|
||||||
|
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
|
||||||
|
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||||
|
for l in users
|
||||||
|
]
|
||||||
|
|
||||||
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
|
users_with_lists = [
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
|
list(l[:-4])
|
||||||
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
|
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
|
||||||
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
|
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
|
||||||
|
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
|
||||||
|
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||||
|
for l in users_with_lists
|
||||||
|
]
|
||||||
|
|
||||||
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
|
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
|
||||||
return list_dict_users
|
return list_dict_users
|
||||||
|
|
||||||
|
|
||||||
k = KeycloakClient()
|
k = KeycloakClient()
|
||||||
k.update_user_pwd_temporary()
|
k.update_user_pwd_temporary()
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import time ,os
|
import json
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import logging as log
|
import logging as log
|
||||||
|
import os
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import yaml, json
|
from datetime import datetime, timedelta
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
|
import yaml
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
from keycloak import KeycloakAdmin
|
from keycloak import KeycloakAdmin
|
||||||
|
|
||||||
from postgres import Postgres
|
from postgres import Postgres
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,28 +1,46 @@
|
||||||
#!flask/bin/python
|
#!flask/bin/python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
from gevent import monkey
|
from gevent import monkey
|
||||||
|
|
||||||
monkey.patch_all()
|
monkey.patch_all()
|
||||||
|
|
||||||
from flask_socketio import SocketIO, emit, join_room, leave_room, \
|
|
||||||
close_room, rooms, disconnect, send
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from flask_socketio import (
|
||||||
|
SocketIO,
|
||||||
|
close_room,
|
||||||
|
disconnect,
|
||||||
|
emit,
|
||||||
|
join_room,
|
||||||
|
leave_room,
|
||||||
|
rooms,
|
||||||
|
send,
|
||||||
|
)
|
||||||
|
|
||||||
from admin import app
|
from admin import app
|
||||||
|
|
||||||
app.socketio = SocketIO(app)
|
app.socketio = SocketIO(app)
|
||||||
|
|
||||||
@app.socketio.on('connect', namespace='/sio')
|
|
||||||
def socketio_connect():
|
|
||||||
join_room('admin')
|
|
||||||
app.socketio.emit('update',
|
|
||||||
json.dumps('Joined'),
|
|
||||||
namespace='/sio',
|
|
||||||
room='admin')
|
|
||||||
|
|
||||||
@app.socketio.on('disconnect', namespace='/sio')
|
@app.socketio.on("connect", namespace="/sio")
|
||||||
|
def socketio_connect():
|
||||||
|
join_room("admin")
|
||||||
|
app.socketio.emit("update", json.dumps("Joined"), namespace="/sio", room="admin")
|
||||||
|
|
||||||
|
|
||||||
|
@app.socketio.on("disconnect", namespace="/sio")
|
||||||
def socketio_disconnect():
|
def socketio_disconnect():
|
||||||
None
|
None
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.socketio.run(app,host='0.0.0.0', port=9000, debug=False, ssl_context='adhoc', async_mode="threading") #, logger=logger, engineio_logger=engineio_logger)
|
if __name__ == "__main__":
|
||||||
|
app.socketio.run(
|
||||||
|
app,
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=9000,
|
||||||
|
debug=False,
|
||||||
|
ssl_context="adhoc",
|
||||||
|
async_mode="threading",
|
||||||
|
) # , logger=logger, engineio_logger=engineio_logger)
|
||||||
# , cors_allowed_origins="*"
|
# , cors_allowed_origins="*"
|
||||||
# /usr/lib/python3.8/site-packages/certifi
|
# /usr/lib/python3.8/site-packages/certifi
|
|
@ -1,67 +1,84 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
import time, os
|
import json
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import pprint
|
|
||||||
|
|
||||||
import logging as log
|
import logging as log
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import yaml, json
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
import yaml
|
||||||
|
|
||||||
from admin.lib.mysql import Mysql
|
|
||||||
from admin.lib.keycloak_client import KeycloakClient
|
from admin.lib.keycloak_client import KeycloakClient
|
||||||
|
from admin.lib.mysql import Mysql
|
||||||
import string, random
|
|
||||||
|
|
||||||
app = {}
|
app = {}
|
||||||
app['config']={}
|
app["config"] = {}
|
||||||
|
|
||||||
class WordpressSaml():
|
|
||||||
|
class WordpressSaml:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.url = "http://isard-sso-keycloak:8080/auth/"
|
self.url = "http://isard-sso-keycloak:8080/auth/"
|
||||||
self.username=os.environ['KEYCLOAK_USER']
|
self.username = os.environ["KEYCLOAK_USER"]
|
||||||
self.password=os.environ['KEYCLOAK_PASSWORD']
|
self.password = os.environ["KEYCLOAK_PASSWORD"]
|
||||||
self.realm='master'
|
self.realm = "master"
|
||||||
self.verify = True
|
self.verify = True
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
self.db=Mysql('isard-apps-mariadb','wordpress','root',os.environ['MARIADB_PASSWORD'])
|
self.db = Mysql(
|
||||||
|
"isard-apps-mariadb",
|
||||||
|
"wordpress",
|
||||||
|
"root",
|
||||||
|
os.environ["MARIADB_PASSWORD"],
|
||||||
|
)
|
||||||
ready = True
|
ready = True
|
||||||
except:
|
except:
|
||||||
log.warning('Could not connect to wordpress database. Retrying...')
|
log.warning("Could not connect to wordpress database. Retrying...")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
log.info('Connected to wordpress database.')
|
log.info("Connected to wordpress database.")
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join("./saml_certs/public.cert"), "r") as crt:
|
with open(os.path.join("./saml_certs/public.cert"), "r") as crt:
|
||||||
app['config']['PUBLIC_CERT_RAW']=crt.read()
|
app["config"]["PUBLIC_CERT_RAW"] = crt.read()
|
||||||
app['config']['PUBLIC_CERT']=self.cert_prepare(app['config']['PUBLIC_CERT_RAW'])
|
app["config"]["PUBLIC_CERT"] = self.cert_prepare(
|
||||||
|
app["config"]["PUBLIC_CERT_RAW"]
|
||||||
|
)
|
||||||
ready = True
|
ready = True
|
||||||
except IOError:
|
except IOError:
|
||||||
log.warning('Could not get public certificate to be used in wordpress. Retrying...')
|
log.warning(
|
||||||
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert')
|
"Could not get public certificate to be used in wordpress. Retrying..."
|
||||||
|
)
|
||||||
|
log.warning(
|
||||||
|
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
|
||||||
|
)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
log.info('Got moodle srt certificate to be used in wordpress.')
|
log.info("Got moodle srt certificate to be used in wordpress.")
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join("./saml_certs/private.key"), "r") as pem:
|
with open(os.path.join("./saml_certs/private.key"), "r") as pem:
|
||||||
app['config']['PRIVATE_KEY']=self.cert_prepare(pem.read())
|
app["config"]["PRIVATE_KEY"] = self.cert_prepare(pem.read())
|
||||||
ready = True
|
ready = True
|
||||||
except IOError:
|
except IOError:
|
||||||
log.warning('Could not get private key to be used in wordpress. Retrying...')
|
log.warning(
|
||||||
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert')
|
"Could not get private key to be used in wordpress. Retrying..."
|
||||||
|
)
|
||||||
|
log.warning(
|
||||||
|
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
|
||||||
|
)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
log.info('Got moodle pem certificate to be used in wordpress.')
|
log.info("Got moodle pem certificate to be used in wordpress.")
|
||||||
|
|
||||||
# ## This seems related to the fact that the certificate generated the first time does'nt work.
|
# ## This seems related to the fact that the certificate generated the first time does'nt work.
|
||||||
# ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
|
# ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
|
||||||
|
@ -79,32 +96,34 @@ class WordpressSaml():
|
||||||
self.reset_saml()
|
self.reset_saml()
|
||||||
except:
|
except:
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
print('Error resetting saml on wordpress')
|
print("Error resetting saml on wordpress")
|
||||||
try:
|
try:
|
||||||
self.delete_keycloak_wordpress_saml_plugin()
|
self.delete_keycloak_wordpress_saml_plugin()
|
||||||
except:
|
except:
|
||||||
print('Error resetting saml on keycloak')
|
print("Error resetting saml on keycloak")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.set_wordpress_saml_plugin()
|
self.set_wordpress_saml_plugin()
|
||||||
except:
|
except:
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
print('Error adding saml on wordpress')
|
print("Error adding saml on wordpress")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.add_keycloak_wordpress_saml()
|
self.add_keycloak_wordpress_saml()
|
||||||
except:
|
except:
|
||||||
print('Error adding saml on keycloak')
|
print("Error adding saml on keycloak")
|
||||||
|
|
||||||
# SAML clients don't work well with composite roles so disabling and adding on realm
|
# SAML clients don't work well with composite roles so disabling and adding on realm
|
||||||
# self.add_client_roles()
|
# self.add_client_roles()
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
self.keycloak= KeycloakClient(url=self.url,
|
self.keycloak = KeycloakClient(
|
||||||
|
url=self.url,
|
||||||
username=self.username,
|
username=self.username,
|
||||||
password=self.password,
|
password=self.password,
|
||||||
realm=self.realm,
|
realm=self.realm,
|
||||||
verify=self.verify)
|
verify=self.verify,
|
||||||
|
)
|
||||||
|
|
||||||
# def activate_saml_plugin(self):
|
# def activate_saml_plugin(self):
|
||||||
# ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php
|
# ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php
|
||||||
|
@ -114,13 +133,13 @@ class WordpressSaml():
|
||||||
# return self.db.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2]
|
# return self.db.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2]
|
||||||
|
|
||||||
def cert_prepare(self, cert):
|
def cert_prepare(self, cert):
|
||||||
return ''.join(cert.split('-----')[2].splitlines())
|
return "".join(cert.split("-----")[2].splitlines())
|
||||||
|
|
||||||
def parse_idp_cert(self):
|
def parse_idp_cert(self):
|
||||||
self.connect()
|
self.connect()
|
||||||
rsa = self.keycloak.get_server_rsa_key()
|
rsa = self.keycloak.get_server_rsa_key()
|
||||||
self.keycloak = None
|
self.keycloak = None
|
||||||
return rsa['certificate']
|
return rsa["certificate"]
|
||||||
|
|
||||||
def set_keycloak_wordpress_saml_plugin(self):
|
def set_keycloak_wordpress_saml_plugin(self):
|
||||||
self.connect()
|
self.connect()
|
||||||
|
@ -129,12 +148,13 @@ class WordpressSaml():
|
||||||
|
|
||||||
def delete_keycloak_wordpress_saml_plugin(self):
|
def delete_keycloak_wordpress_saml_plugin(self):
|
||||||
self.connect()
|
self.connect()
|
||||||
self.keycloak.delete_client('630601f8-25d1-4822-8741-c93affd2cd84')
|
self.keycloak.delete_client("630601f8-25d1-4822-8741-c93affd2cd84")
|
||||||
self.keycloak = None
|
self.keycloak = None
|
||||||
|
|
||||||
def set_wordpress_saml_plugin(self):
|
def set_wordpress_saml_plugin(self):
|
||||||
# ('active_plugins', 'a:2:{i:0;s:33:\"edwiser-bridge/edwiser-bridge.php\";i:1;s:17:\"onelogin_saml.php\";}', 'yes'),
|
# ('active_plugins', 'a:2:{i:0;s:33:\"edwiser-bridge/edwiser-bridge.php\";i:1;s:17:\"onelogin_saml.php\";}', 'yes'),
|
||||||
self.db.update("""INSERT INTO wp_options (option_name, option_value, autoload) VALUES
|
self.db.update(
|
||||||
|
"""INSERT INTO wp_options (option_name, option_value, autoload) VALUES
|
||||||
('onelogin_saml_enabled', 'on', 'yes'),
|
('onelogin_saml_enabled', 'on', 'yes'),
|
||||||
('onelogin_saml_idp_entityid', 'Saml Login', 'yes'),
|
('onelogin_saml_idp_entityid', 'Saml Login', 'yes'),
|
||||||
('onelogin_saml_idp_sso', 'https://sso.%s/auth/realms/master/protocol/saml', 'yes'),
|
('onelogin_saml_idp_sso', 'https://sso.%s/auth/realms/master/protocol/saml', 'yes'),
|
||||||
|
@ -194,21 +214,33 @@ class WordpressSaml():
|
||||||
('onelogin_saml_advanced_settings_sp_x509cert', '%s', 'yes'),
|
('onelogin_saml_advanced_settings_sp_x509cert', '%s', 'yes'),
|
||||||
('onelogin_saml_advanced_settings_sp_privatekey', '%s', 'yes'),
|
('onelogin_saml_advanced_settings_sp_privatekey', '%s', 'yes'),
|
||||||
('onelogin_saml_advanced_signaturealgorithm', 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', 'yes'),
|
('onelogin_saml_advanced_signaturealgorithm', 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', 'yes'),
|
||||||
('onelogin_saml_advanced_digestalgorithm', 'http://www.w3.org/2000/09/xmldsig#sha1', 'yes');""" % (os.environ['DOMAIN'],os.environ['DOMAIN'],self.parse_idp_cert(),app['config']['PUBLIC_CERT'],app['config']['PRIVATE_KEY']))
|
('onelogin_saml_advanced_digestalgorithm', 'http://www.w3.org/2000/09/xmldsig#sha1', 'yes');"""
|
||||||
|
% (
|
||||||
|
os.environ["DOMAIN"],
|
||||||
|
os.environ["DOMAIN"],
|
||||||
|
self.parse_idp_cert(),
|
||||||
|
app["config"]["PUBLIC_CERT"],
|
||||||
|
app["config"]["PRIVATE_KEY"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def reset_saml(self):
|
def reset_saml(self):
|
||||||
self.db.update("""DELETE FROM wp_options WHERE option_name LIKE 'onelogin_saml_%'""")
|
self.db.update(
|
||||||
|
"""DELETE FROM wp_options WHERE option_name LIKE 'onelogin_saml_%'"""
|
||||||
|
)
|
||||||
|
|
||||||
def add_keycloak_wordpress_saml(self):
|
def add_keycloak_wordpress_saml(self):
|
||||||
client={"id" : "630601f8-25d1-4822-8741-c93affd2cd84",
|
client = {
|
||||||
|
"id": "630601f8-25d1-4822-8741-c93affd2cd84",
|
||||||
"clientId": "php-saml",
|
"clientId": "php-saml",
|
||||||
"surrogateAuthRequired": False,
|
"surrogateAuthRequired": False,
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"alwaysDisplayInConsole": False,
|
"alwaysDisplayInConsole": False,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris" : [ "https://wp."+os.environ['DOMAIN']+"/wp-login.php?saml_acs" ],
|
"redirectUris": [
|
||||||
"webOrigins" : [ "https://wp."+os.environ['DOMAIN'] ],
|
"https://wp." + os.environ["DOMAIN"] + "/wp-login.php?saml_acs"
|
||||||
|
],
|
||||||
|
"webOrigins": ["https://wp." + os.environ["DOMAIN"]],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": False,
|
"bearerOnly": False,
|
||||||
"consentRequired": False,
|
"consentRequired": False,
|
||||||
|
@ -221,22 +253,27 @@ class WordpressSaml():
|
||||||
"protocol": "saml",
|
"protocol": "saml",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"saml.force.post.binding": True,
|
"saml.force.post.binding": True,
|
||||||
"saml_assertion_consumer_url_post" : "https://wp."+os.environ['DOMAIN']+"/wp-login.php?saml_acs",
|
"saml_assertion_consumer_url_post": "https://wp."
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ "/wp-login.php?saml_acs",
|
||||||
"saml.server.signature": True,
|
"saml.server.signature": True,
|
||||||
"saml.server.signature.keyinfo.ext": False,
|
"saml.server.signature.keyinfo.ext": False,
|
||||||
"saml.signing.certificate" : app['config']['PUBLIC_CERT_RAW'],
|
"saml.signing.certificate": app["config"]["PUBLIC_CERT_RAW"],
|
||||||
"saml_single_logout_service_url_redirect" : "https://wp."+os.environ['DOMAIN']+"/wp-login.php?saml_sls",
|
"saml_single_logout_service_url_redirect": "https://wp."
|
||||||
|
+ os.environ["DOMAIN"]
|
||||||
|
+ "/wp-login.php?saml_sls",
|
||||||
"saml.signature.algorithm": "RSA_SHA256",
|
"saml.signature.algorithm": "RSA_SHA256",
|
||||||
"saml_force_name_id_format": False,
|
"saml_force_name_id_format": False,
|
||||||
"saml.client.signature": True,
|
"saml.client.signature": True,
|
||||||
"saml.authnstatement": True,
|
"saml.authnstatement": True,
|
||||||
"saml_name_id_format": "username",
|
"saml_name_id_format": "username",
|
||||||
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#"
|
"saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#",
|
||||||
},
|
},
|
||||||
"authenticationFlowBindingOverrides": {},
|
"authenticationFlowBindingOverrides": {},
|
||||||
"fullScopeAllowed": True,
|
"fullScopeAllowed": True,
|
||||||
"nodeReRegistrationTimeout": -1,
|
"nodeReRegistrationTimeout": -1,
|
||||||
"protocolMappers" : [ {
|
"protocolMappers": [
|
||||||
|
{
|
||||||
"id": "72c6175e-bd07-4c27-abd6-4e4ae38d834b",
|
"id": "72c6175e-bd07-4c27-abd6-4e4ae38d834b",
|
||||||
"name": "username",
|
"name": "username",
|
||||||
"protocol": "saml",
|
"protocol": "saml",
|
||||||
|
@ -246,9 +283,10 @@ class WordpressSaml():
|
||||||
"attribute.nameformat": "Basic",
|
"attribute.nameformat": "Basic",
|
||||||
"user.attribute": "username",
|
"user.attribute": "username",
|
||||||
"friendly.name": "username",
|
"friendly.name": "username",
|
||||||
"attribute.name" : "username"
|
"attribute.name": "username",
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
"id": "abd6562f-4732-4da9-987f-b1a6ad6605fa",
|
"id": "abd6562f-4732-4da9-987f-b1a6ad6605fa",
|
||||||
"name": "roles",
|
"name": "roles",
|
||||||
"protocol": "saml",
|
"protocol": "saml",
|
||||||
|
@ -258,9 +296,10 @@ class WordpressSaml():
|
||||||
"single": True,
|
"single": True,
|
||||||
"attribute.nameformat": "Basic",
|
"attribute.nameformat": "Basic",
|
||||||
"friendly.name": "Roles",
|
"friendly.name": "Roles",
|
||||||
"attribute.name" : "Role"
|
"attribute.name": "Role",
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
"id": "50aafb71-d91c-4bc7-bb60-e1ae0222aab3",
|
"id": "50aafb71-d91c-4bc7-bb60-e1ae0222aab3",
|
||||||
"name": "email",
|
"name": "email",
|
||||||
"protocol": "saml",
|
"protocol": "saml",
|
||||||
|
@ -270,16 +309,24 @@ class WordpressSaml():
|
||||||
"attribute.nameformat": "Basic",
|
"attribute.nameformat": "Basic",
|
||||||
"user.attribute": "email",
|
"user.attribute": "email",
|
||||||
"friendly.name": "email",
|
"friendly.name": "email",
|
||||||
"attribute.name" : "email"
|
"attribute.name": "email",
|
||||||
}
|
},
|
||||||
} ],
|
},
|
||||||
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ],
|
],
|
||||||
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ],
|
"defaultClientScopes": [
|
||||||
"access" : {
|
"web-origins",
|
||||||
"view" : True,
|
"role_list",
|
||||||
"configure" : True,
|
"roles",
|
||||||
"manage" : True
|
"profile",
|
||||||
}
|
"email",
|
||||||
|
],
|
||||||
|
"optionalClientScopes": [
|
||||||
|
"address",
|
||||||
|
"phone",
|
||||||
|
"offline_access",
|
||||||
|
"microprofile-jwt",
|
||||||
|
],
|
||||||
|
"access": {"view": True, "configure": True, "manage": True},
|
||||||
}
|
}
|
||||||
self.connect()
|
self.connect()
|
||||||
self.keycloak.add_client(client)
|
self.keycloak.add_client(client)
|
||||||
|
@ -287,10 +334,19 @@ class WordpressSaml():
|
||||||
|
|
||||||
def add_client_roles(self):
|
def add_client_roles(self):
|
||||||
self.connect()
|
self.connect()
|
||||||
self.keycloak.add_client_role('630601f8-25d1-4822-8741-c93affd2cd84','admin','Wordpress admins')
|
self.keycloak.add_client_role(
|
||||||
self.keycloak.add_client_role('630601f8-25d1-4822-8741-c93affd2cd84','manager','Wordpress managers')
|
"630601f8-25d1-4822-8741-c93affd2cd84", "admin", "Wordpress admins"
|
||||||
self.keycloak.add_client_role('630601f8-25d1-4822-8741-c93affd2cd84','teacher','Wordpress teachers')
|
)
|
||||||
self.keycloak.add_client_role('630601f8-25d1-4822-8741-c93affd2cd84','student','Wordpress students')
|
self.keycloak.add_client_role(
|
||||||
|
"630601f8-25d1-4822-8741-c93affd2cd84", "manager", "Wordpress managers"
|
||||||
|
)
|
||||||
|
self.keycloak.add_client_role(
|
||||||
|
"630601f8-25d1-4822-8741-c93affd2cd84", "teacher", "Wordpress teachers"
|
||||||
|
)
|
||||||
|
self.keycloak.add_client_role(
|
||||||
|
"630601f8-25d1-4822-8741-c93affd2cd84", "student", "Wordpress students"
|
||||||
|
)
|
||||||
self.keycloak = None
|
self.keycloak = None
|
||||||
|
|
||||||
|
|
||||||
nw = WordpressSaml()
|
nw = WordpressSaml()
|
|
@ -1,67 +1,84 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
import time, os
|
import json
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import pprint
|
|
||||||
|
|
||||||
import logging as log
|
import logging as log
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import yaml, json
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
import yaml
|
||||||
|
|
||||||
from admin.lib.mysql import Mysql
|
|
||||||
from admin.lib.keycloak_client import KeycloakClient
|
from admin.lib.keycloak_client import KeycloakClient
|
||||||
|
from admin.lib.mysql import Mysql
|
||||||
import string, random
|
|
||||||
|
|
||||||
app = {}
|
app = {}
|
||||||
app['config']={}
|
app["config"] = {}
|
||||||
|
|
||||||
class WordpressSaml():
|
|
||||||
|
class WordpressSaml:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.url = "http://isard-sso-keycloak:8080/auth/"
|
self.url = "http://isard-sso-keycloak:8080/auth/"
|
||||||
self.username=os.environ['KEYCLOAK_USER']
|
self.username = os.environ["KEYCLOAK_USER"]
|
||||||
self.password=os.environ['KEYCLOAK_PASSWORD']
|
self.password = os.environ["KEYCLOAK_PASSWORD"]
|
||||||
self.realm='master'
|
self.realm = "master"
|
||||||
self.verify = True
|
self.verify = True
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
self.db=Mysql('isard-apps-mariadb','wordpress','root',os.environ['MARIADB_PASSWORD'])
|
self.db = Mysql(
|
||||||
|
"isard-apps-mariadb",
|
||||||
|
"wordpress",
|
||||||
|
"root",
|
||||||
|
os.environ["MARIADB_PASSWORD"],
|
||||||
|
)
|
||||||
ready = True
|
ready = True
|
||||||
except:
|
except:
|
||||||
log.warning('Could not connect to wordpress database. Retrying...')
|
log.warning("Could not connect to wordpress database. Retrying...")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
log.info('Connected to wordpress database.')
|
log.info("Connected to wordpress database.")
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join("./saml_certs/public.cert"), "r") as crt:
|
with open(os.path.join("./saml_certs/public.cert"), "r") as crt:
|
||||||
app['config']['PUBLIC_CERT_RAW']=crt.read()
|
app["config"]["PUBLIC_CERT_RAW"] = crt.read()
|
||||||
app['config']['PUBLIC_CERT']=self.cert_prepare(app['config']['PUBLIC_CERT_RAW'])
|
app["config"]["PUBLIC_CERT"] = self.cert_prepare(
|
||||||
|
app["config"]["PUBLIC_CERT_RAW"]
|
||||||
|
)
|
||||||
ready = True
|
ready = True
|
||||||
except IOError:
|
except IOError:
|
||||||
log.warning('Could not get public certificate to be used in wordpress. Retrying...')
|
log.warning(
|
||||||
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert')
|
"Could not get public certificate to be used in wordpress. Retrying..."
|
||||||
|
)
|
||||||
|
log.warning(
|
||||||
|
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
|
||||||
|
)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
log.info('Got moodle srt certificate to be used in wordpress.')
|
log.info("Got moodle srt certificate to be used in wordpress.")
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join("./saml_certs/private.key"), "r") as pem:
|
with open(os.path.join("./saml_certs/private.key"), "r") as pem:
|
||||||
app['config']['PRIVATE_KEY']=self.cert_prepare(pem.read())
|
app["config"]["PRIVATE_KEY"] = self.cert_prepare(pem.read())
|
||||||
ready = True
|
ready = True
|
||||||
except IOError:
|
except IOError:
|
||||||
log.warning('Could not get private key to be used in wordpress. Retrying...')
|
log.warning(
|
||||||
log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert')
|
"Could not get private key to be used in wordpress. Retrying..."
|
||||||
|
)
|
||||||
|
log.warning(
|
||||||
|
" You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert"
|
||||||
|
)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
log.info('Got moodle pem certificate to be used in wordpress.')
|
log.info("Got moodle pem certificate to be used in wordpress.")
|
||||||
|
|
||||||
# ## This seems related to the fact that the certificate generated the first time does'nt work.
|
# ## This seems related to the fact that the certificate generated the first time does'nt work.
|
||||||
# ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
|
# ## And when regenerating the certificate de privatekeypass seems not to be used and instead it
|
||||||
|
@ -79,13 +96,13 @@ class WordpressSaml():
|
||||||
self.reset_saml_certs()
|
self.reset_saml_certs()
|
||||||
except:
|
except:
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
print('Error resetting saml certs on wordpress')
|
print("Error resetting saml certs on wordpress")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.set_wordpress_saml_plugin_certs()
|
self.set_wordpress_saml_plugin_certs()
|
||||||
except:
|
except:
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
print('Error adding saml on wordpress')
|
print("Error adding saml on wordpress")
|
||||||
|
|
||||||
# SAML clients don't work well with composite roles so disabling and adding on realm
|
# SAML clients don't work well with composite roles so disabling and adding on realm
|
||||||
# self.add_client_roles()
|
# self.add_client_roles()
|
||||||
|
@ -98,32 +115,47 @@ class WordpressSaml():
|
||||||
# return self.db.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2]
|
# return self.db.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2]
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
self.keycloak= KeycloakClient(url=self.url,
|
self.keycloak = KeycloakClient(
|
||||||
|
url=self.url,
|
||||||
username=self.username,
|
username=self.username,
|
||||||
password=self.password,
|
password=self.password,
|
||||||
realm=self.realm,
|
realm=self.realm,
|
||||||
verify=self.verify)
|
verify=self.verify,
|
||||||
|
)
|
||||||
|
|
||||||
def cert_prepare(self, cert):
|
def cert_prepare(self, cert):
|
||||||
return ''.join(cert.split('-----')[2].splitlines())
|
return "".join(cert.split("-----")[2].splitlines())
|
||||||
|
|
||||||
def parse_idp_cert(self):
|
def parse_idp_cert(self):
|
||||||
self.connect()
|
self.connect()
|
||||||
rsa = self.keycloak.get_server_rsa_key()
|
rsa = self.keycloak.get_server_rsa_key()
|
||||||
self.keycloak = None
|
self.keycloak = None
|
||||||
return rsa['certificate']
|
return rsa["certificate"]
|
||||||
|
|
||||||
def set_wordpress_saml_plugin_certs(self):
|
def set_wordpress_saml_plugin_certs(self):
|
||||||
# ('active_plugins', 'a:2:{i:0;s:33:\"edwiser-bridge/edwiser-bridge.php\";i:1;s:17:\"onelogin_saml.php\";}', 'yes'),
|
# ('active_plugins', 'a:2:{i:0;s:33:\"edwiser-bridge/edwiser-bridge.php\";i:1;s:17:\"onelogin_saml.php\";}', 'yes'),
|
||||||
self.db.update("""INSERT INTO wp_options (option_name, option_value, autoload) VALUES
|
self.db.update(
|
||||||
|
"""INSERT INTO wp_options (option_name, option_value, autoload) VALUES
|
||||||
('onelogin_saml_idp_x509cert', '%s', 'yes'),
|
('onelogin_saml_idp_x509cert', '%s', 'yes'),
|
||||||
('onelogin_saml_advanced_settings_sp_x509cert', '%s', 'yes'),
|
('onelogin_saml_advanced_settings_sp_x509cert', '%s', 'yes'),
|
||||||
('onelogin_saml_advanced_settings_sp_privatekey', '%s', 'yes');""" % (self.parse_idp_cert(),app['config']['PUBLIC_CERT'],app['config']['PRIVATE_KEY']))
|
('onelogin_saml_advanced_settings_sp_privatekey', '%s', 'yes');"""
|
||||||
|
% (
|
||||||
|
self.parse_idp_cert(),
|
||||||
|
app["config"]["PUBLIC_CERT"],
|
||||||
|
app["config"]["PRIVATE_KEY"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def reset_saml_certs(self):
|
def reset_saml_certs(self):
|
||||||
self.db.update("""DELETE FROM wp_options WHERE option_name = 'onelogin_saml_idp_x509cert'""")
|
self.db.update(
|
||||||
self.db.update("""DELETE FROM wp_options WHERE option_name = 'onelogin_saml_advanced_settings_sp_x509cert'""")
|
"""DELETE FROM wp_options WHERE option_name = 'onelogin_saml_idp_x509cert'"""
|
||||||
self.db.update("""DELETE FROM wp_options WHERE option_name = 'onelogin_saml_advanced_settings_sp_privatekey'""")
|
)
|
||||||
|
self.db.update(
|
||||||
|
"""DELETE FROM wp_options WHERE option_name = 'onelogin_saml_advanced_settings_sp_x509cert'"""
|
||||||
|
)
|
||||||
|
self.db.update(
|
||||||
|
"""DELETE FROM wp_options WHERE option_name = 'onelogin_saml_advanced_settings_sp_privatekey'"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
nw = WordpressSaml()
|
nw = WordpressSaml()
|
||||||
|
|
Loading…
Reference in New Issue