[sso-admin] Disentangle module and add type hints
With this commit, code from the admin module can be re-used and thanks to adding type-hints in most places we are able to discover some bugs. This commit attempts to fix only that which was necessary to: - Add a reasonable amount of type hints - Disentangle the module There are already some issues that have been discovered by mypy.Xnet-DigitalDemocratic-main-patch-41273
parent
e98323913d
commit
81fff214d5
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -20,40 +21,12 @@
|
||||||
|
|
||||||
import logging as log
|
import logging as log
|
||||||
import os
|
import os
|
||||||
|
import os.path
|
||||||
|
|
||||||
from flask import Flask, render_template, send_from_directory
|
from admin.flaskapp import AdminFlaskApp
|
||||||
|
|
||||||
app = Flask(__name__, static_url_path="")
|
def get_app() -> AdminFlaskApp:
|
||||||
app = Flask(__name__, template_folder="static/templates")
|
app = AdminFlaskApp(__name__, template_folder="static/templates")
|
||||||
app.url_map.strict_slashes = False
|
|
||||||
|
|
||||||
"""
|
|
||||||
App secret key for encrypting cookies
|
|
||||||
You can generate one with:
|
|
||||||
import os
|
|
||||||
os.urandom(24)
|
|
||||||
And paste it here.
|
|
||||||
"""
|
|
||||||
app.secret_key = "Change this key!/\xf7\x83\xbe\x17\xfa\xa3zT\n\\]m\xa6\x8bF\xdd\r\xf7\x9e\x1d\x1f\x14'"
|
|
||||||
|
|
||||||
print("Starting dd-sso api...")
|
|
||||||
|
|
||||||
from admin.lib.load_config import loadConfig
|
|
||||||
|
|
||||||
try:
|
|
||||||
loadConfig(app)
|
|
||||||
except:
|
|
||||||
print("Could not get environment variables...")
|
|
||||||
|
|
||||||
from admin.lib.postup import Postup
|
|
||||||
|
|
||||||
Postup()
|
|
||||||
|
|
||||||
from admin.lib.admin import Admin
|
|
||||||
|
|
||||||
app.admin = Admin()
|
|
||||||
|
|
||||||
app.ready = False
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Debug should be removed on production!
|
Debug should be removed on production!
|
||||||
|
@ -63,65 +36,9 @@ if app.debug:
|
||||||
else:
|
else:
|
||||||
log.info("Debug mode: {}".format(app.debug))
|
log.info("Debug mode: {}".format(app.debug))
|
||||||
|
|
||||||
"""
|
return app
|
||||||
Serve static files
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/build/<path:path>")
|
|
||||||
def send_build(path):
|
|
||||||
return send_from_directory(
|
|
||||||
os.path.join(app.root_path, "node_modules/gentelella/build"), path
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/vendors/<path:path>")
|
|
||||||
def send_vendors(path):
|
|
||||||
return send_from_directory(
|
|
||||||
os.path.join(app.root_path, "node_modules/gentelella/vendors"), path
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/node_modules/<path:path>")
|
|
||||||
def send_nodes(path):
|
|
||||||
return send_from_directory(os.path.join(app.root_path, "node_modules"), path)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/templates/<path:path>")
|
|
||||||
def send_templates(path):
|
|
||||||
return send_from_directory(os.path.join(app.root_path, "templates"), path)
|
|
||||||
|
|
||||||
|
|
||||||
# @app.route('/templates/<path:path>')
|
|
||||||
# def send_templates(path):
|
|
||||||
# return send_from_directory(os.path.join(app.root_path, 'static/templates'), path)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/static/<path:path>")
|
|
||||||
def send_static_js(path):
|
|
||||||
return send_from_directory(os.path.join(app.root_path, "static"), path)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/avatars/<path:path>")
|
|
||||||
def send_avatars_img(path):
|
|
||||||
return send_from_directory(
|
|
||||||
os.path.join(app.root_path, "../avatars/master-avatars"), path
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/custom/<path:path>")
|
|
||||||
def send_custom(path):
|
|
||||||
return send_from_directory(os.path.join(app.root_path, "../custom"), path)
|
|
||||||
|
|
||||||
|
|
||||||
# @app.errorhandler(404)
|
|
||||||
# def not_found_error(error):
|
|
||||||
# return render_template('page_404.html'), 404
|
|
||||||
|
|
||||||
# @app.errorhandler(500)
|
|
||||||
# def internal_error(error):
|
|
||||||
# return render_template('page_500.html'), 500
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Import all views
|
Import all views
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -21,13 +22,9 @@ import os
|
||||||
|
|
||||||
from flask_login import LoginManager, UserMixin
|
from flask_login import LoginManager, UserMixin
|
||||||
|
|
||||||
from admin import app
|
from typing import TYPE_CHECKING, Dict
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from admin.flaskapp import AdminFlaskApp
|
||||||
login_manager = LoginManager()
|
|
||||||
login_manager.init_app(app)
|
|
||||||
login_manager.login_view = "login"
|
|
||||||
|
|
||||||
|
|
||||||
ram_users = {
|
ram_users = {
|
||||||
os.environ["ADMINAPP_USER"]: {
|
os.environ["ADMINAPP_USER"]: {
|
||||||
|
@ -49,13 +46,19 @@ ram_users = {
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin):
|
class User(UserMixin):
|
||||||
def __init__(self, dict):
|
def __init__(self, id : str, password : str, role : str, active : bool = True) -> None:
|
||||||
self.id = dict["id"]
|
self.id = id
|
||||||
self.username = dict["id"]
|
self.username = id
|
||||||
self.password = dict["password"]
|
self.password = password
|
||||||
self.role = dict["role"]
|
self.role = role
|
||||||
|
self.active = active
|
||||||
|
|
||||||
|
def setup_auth(app : "AdminFlaskApp") -> None:
|
||||||
|
login_manager = LoginManager()
|
||||||
|
login_manager.init_app(app)
|
||||||
|
login_manager.login_view = "login"
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def user_loader(username):
|
def user_loader(username : str) -> User:
|
||||||
return User(ram_users[username])
|
u = ram_users[username]
|
||||||
|
return User(id = u["id"], password = u["password"], role = u["role"])
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -30,17 +31,18 @@ from functools import wraps
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from jose import jwt
|
from jose import jwt
|
||||||
|
import jose.exceptions
|
||||||
|
|
||||||
from admin import app
|
from typing import Any
|
||||||
|
|
||||||
from ..lib.api_exceptions import Error
|
from admin.lib.api_exceptions import Error
|
||||||
|
|
||||||
|
|
||||||
def get_header_jwt_payload():
|
def get_header_jwt_payload() -> Any:
|
||||||
return get_token_payload(get_token_auth_header())
|
return get_token_payload(get_token_auth_header())
|
||||||
|
|
||||||
|
|
||||||
def get_token_header(header):
|
def get_token_header(header : str) -> str:
|
||||||
"""Obtains the Access Token from the a Header"""
|
"""Obtains the Access Token from the a Header"""
|
||||||
auth = request.headers.get(header, None)
|
auth = request.headers.get(header, None)
|
||||||
if not auth:
|
if not auth:
|
||||||
|
@ -70,15 +72,15 @@ def get_token_header(header):
|
||||||
return parts[1] # Token
|
return parts[1] # Token
|
||||||
|
|
||||||
|
|
||||||
def get_token_auth_header():
|
def get_token_auth_header() -> str:
|
||||||
return get_token_header("Authorization")
|
return get_token_header("Authorization")
|
||||||
|
|
||||||
|
|
||||||
def get_token_payload(token):
|
def get_token_payload(token : str) -> Any:
|
||||||
# log.warning("The received token in get_token_payload is: " + str(token))
|
# log.warning("The received token in get_token_payload is: " + str(token))
|
||||||
try:
|
try:
|
||||||
claims = jwt.get_unverified_claims(token)
|
claims = jwt.get_unverified_claims(token)
|
||||||
secret = app.config["API_SECRET"]
|
secret = os.environ["API_SECRET"]
|
||||||
|
|
||||||
except:
|
except:
|
||||||
log.warning(
|
log.warning(
|
||||||
|
@ -97,11 +99,11 @@ def get_token_payload(token):
|
||||||
algorithms=["HS256"],
|
algorithms=["HS256"],
|
||||||
options=dict(verify_aud=False, verify_sub=False, verify_exp=True),
|
options=dict(verify_aud=False, verify_sub=False, verify_exp=True),
|
||||||
)
|
)
|
||||||
except jwt.ExpiredSignatureError:
|
except jose.exceptions.ExpiredSignatureError:
|
||||||
log.warning("Token expired")
|
log.warning("Token expired")
|
||||||
raise Error("unauthorized", "Token is expired", traceback.format_stack())
|
raise Error("unauthorized", "Token is expired", traceback.format_stack())
|
||||||
|
|
||||||
except jwt.JWTClaimsError:
|
except jose.exceptions.JWTClaimsError:
|
||||||
raise Error(
|
raise Error(
|
||||||
"unauthorized",
|
"unauthorized",
|
||||||
"Incorrect claims, please check the audience and issuer",
|
"Incorrect claims, please check the audience and issuer",
|
||||||
|
|
|
@ -0,0 +1,225 @@
|
||||||
|
#
|
||||||
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
|
#
|
||||||
|
# This file is part of DD
|
||||||
|
#
|
||||||
|
# DD is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
# option) any later version.
|
||||||
|
#
|
||||||
|
# DD is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with DD. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import logging as log
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import secrets
|
||||||
|
import traceback
|
||||||
|
from typing import TYPE_CHECKING, Any, Callable, Dict
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from cerberus import Validator
|
||||||
|
from flask import Flask, Response, jsonify, render_template, send_from_directory
|
||||||
|
|
||||||
|
from admin.lib.api_exceptions import Error
|
||||||
|
from admin.views.decorators import OptionalJsonResponse
|
||||||
|
from admin.views.ApiViews import setup_api_views
|
||||||
|
from admin.views.AppViews import setup_app_views
|
||||||
|
from admin.views.LoginViews import setup_login_views
|
||||||
|
from admin.views.WebViews import setup_web_views
|
||||||
|
from admin.views.WpViews import setup_wp_views
|
||||||
|
from admin.auth.authentication import setup_auth
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from admin.lib.admin import Admin
|
||||||
|
from admin.lib.postup import Postup
|
||||||
|
|
||||||
|
|
||||||
|
class AdminValidator(Validator): # type: ignore # cerberus type hints MIA
|
||||||
|
# TODO: What's the point of this class?
|
||||||
|
None
|
||||||
|
# def _normalize_default_setter_genid(self, document):
|
||||||
|
# return _parse_string(document["name"])
|
||||||
|
|
||||||
|
# def _normalize_default_setter_genidlower(self, document):
|
||||||
|
# return _parse_string(document["name"]).lower()
|
||||||
|
|
||||||
|
# def _normalize_default_setter_gengroupid(self, document):
|
||||||
|
# return _parse_string(
|
||||||
|
# document["parent_category"] + "-" + document["uid"]
|
||||||
|
# ).lower()
|
||||||
|
|
||||||
|
|
||||||
|
class AdminFlaskApp(Flask):
|
||||||
|
"""
|
||||||
|
Subclass Flask app to ease customisation and type checking.
|
||||||
|
|
||||||
|
In order for an instance of this class to be useful,
|
||||||
|
the setup method should be called after instantiating.
|
||||||
|
"""
|
||||||
|
|
||||||
|
admin: "Admin"
|
||||||
|
secrets_dir: str
|
||||||
|
ready: bool = False
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.url_map.strict_slashes = False
|
||||||
|
from admin.lib.admin import Admin
|
||||||
|
self.admin = Admin(self)
|
||||||
|
# Minor setup tasks
|
||||||
|
self._load_validators()
|
||||||
|
self._load_config()
|
||||||
|
self._setup_routes()
|
||||||
|
setup_api_views(self)
|
||||||
|
setup_app_views(self)
|
||||||
|
setup_login_views(self)
|
||||||
|
setup_web_views(self)
|
||||||
|
setup_wp_views(self)
|
||||||
|
setup_auth(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def legal_path(self) -> str:
|
||||||
|
return os.path.join(self.root_path, "static/templates/pages/legal/")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def avatars_path(self) -> str:
|
||||||
|
return os.path.join(self.root_path, "../custom/avatars/")
|
||||||
|
|
||||||
|
def setup(self) -> None:
|
||||||
|
"""
|
||||||
|
Perform setup tasks that might do network
|
||||||
|
"""
|
||||||
|
from admin.lib.postup import Postup
|
||||||
|
Postup(self)
|
||||||
|
|
||||||
|
def json_route(self, rule: str, **options: Any) -> Callable[..., OptionalJsonResponse]:
|
||||||
|
return self.route(rule, **options) # type: ignore # mypy issue #7187
|
||||||
|
|
||||||
|
def _load_validators(self, purge_unknown: bool = True) -> Dict[str, Validator]:
|
||||||
|
validators = {}
|
||||||
|
schema_path = os.path.join(self.root_path, "schemas")
|
||||||
|
for schema_filename in os.listdir(schema_path):
|
||||||
|
try:
|
||||||
|
with open(os.path.join(schema_path, schema_filename)) as file:
|
||||||
|
schema_yml = file.read()
|
||||||
|
schema = yaml.load(schema_yml, Loader=yaml.FullLoader)
|
||||||
|
validators[schema_filename.split(".")[0]] = AdminValidator(
|
||||||
|
schema, purge_unknown=purge_unknown
|
||||||
|
)
|
||||||
|
except IsADirectoryError:
|
||||||
|
None
|
||||||
|
return validators
|
||||||
|
|
||||||
|
def _load_config(self) -> None:
|
||||||
|
try:
|
||||||
|
# Handle secrets like Flask's session key
|
||||||
|
self.secrets_dir = os.environ.get("SECRETS", "secret")
|
||||||
|
secret_key_file = os.path.join(self.secrets_dir, "secret_key")
|
||||||
|
if not os.path.exists(self.secrets_dir):
|
||||||
|
os.mkdir(self.secrets_dir)
|
||||||
|
if not os.path.exists(secret_key_file):
|
||||||
|
# Generate as needed
|
||||||
|
# https://flask.palletsprojects.com/en/2.1.x/config/#SECRET_KEY
|
||||||
|
with os.fdopen(
|
||||||
|
os.open(secret_key_file, os.O_WRONLY | os.O_CREAT, 0o600), "w"
|
||||||
|
) as f:
|
||||||
|
f.write(secrets.token_hex())
|
||||||
|
self.secret_key = open(secret_key_file, "r").read()
|
||||||
|
|
||||||
|
# Move on with ISARD's settings
|
||||||
|
self.config.setdefault("DOMAIN", os.environ["DOMAIN"])
|
||||||
|
self.config.setdefault(
|
||||||
|
"KEYCLOAK_POSTGRES_USER", os.environ["KEYCLOAK_DB_USER"]
|
||||||
|
)
|
||||||
|
self.config.setdefault(
|
||||||
|
"KEYCLOAK_POSTGRES_PASSWORD", os.environ["KEYCLOAK_DB_PASSWORD"]
|
||||||
|
)
|
||||||
|
self.config.setdefault(
|
||||||
|
"MOODLE_POSTGRES_USER", os.environ["MOODLE_POSTGRES_USER"]
|
||||||
|
)
|
||||||
|
self.config.setdefault(
|
||||||
|
"MOODLE_POSTGRES_PASSWORD", os.environ["MOODLE_POSTGRES_PASSWORD"]
|
||||||
|
)
|
||||||
|
self.config.setdefault(
|
||||||
|
"NEXTCLOUD_POSTGRES_USER", os.environ["NEXTCLOUD_POSTGRES_USER"]
|
||||||
|
)
|
||||||
|
self.config.setdefault(
|
||||||
|
"NEXTCLOUD_POSTGRES_PASSWORD", os.environ["NEXTCLOUD_POSTGRES_PASSWORD"]
|
||||||
|
)
|
||||||
|
self.config.setdefault(
|
||||||
|
"VERIFY", True if os.environ["VERIFY"] == "true" else False
|
||||||
|
)
|
||||||
|
self.config.setdefault("API_SECRET", os.environ.get("API_SECRET"))
|
||||||
|
except Exception as e:
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _setup_routes(self) -> None:
|
||||||
|
"""
|
||||||
|
Setup routes to Serve static files
|
||||||
|
"""
|
||||||
|
|
||||||
|
@self.route("/build/<path:path>")
|
||||||
|
def send_build(path: str) -> Response:
|
||||||
|
return send_from_directory(
|
||||||
|
os.path.join(self.root_path, "node_modules/gentelella/build"), path
|
||||||
|
)
|
||||||
|
|
||||||
|
@self.route("/vendors/<path:path>")
|
||||||
|
def send_vendors(path: str) -> Response:
|
||||||
|
return send_from_directory(
|
||||||
|
os.path.join(self.root_path, "node_modules/gentelella/vendors"), path
|
||||||
|
)
|
||||||
|
|
||||||
|
@self.route("/node_modules/<path:path>")
|
||||||
|
def send_nodes(path: str) -> Response:
|
||||||
|
return send_from_directory(
|
||||||
|
os.path.join(self.root_path, "node_modules"), path
|
||||||
|
)
|
||||||
|
|
||||||
|
@self.route("/templates/<path:path>")
|
||||||
|
def send_templates(path: str) -> Response:
|
||||||
|
return send_from_directory(os.path.join(self.root_path, "templates"), path)
|
||||||
|
|
||||||
|
# @self.route('/templates/<path:path>')
|
||||||
|
# def send_templates(path):
|
||||||
|
# return send_from_directory(os.path.join(self.root_path, 'static/templates'), path)
|
||||||
|
|
||||||
|
@self.route("/static/<path:path>")
|
||||||
|
def send_static_js(path: str) -> Response:
|
||||||
|
return send_from_directory(os.path.join(self.root_path, "static"), path)
|
||||||
|
|
||||||
|
@self.route("/avatars/<path:path>")
|
||||||
|
def send_avatars_img(path: str) -> Response:
|
||||||
|
return send_from_directory(
|
||||||
|
os.path.join(self.root_path, "../avatars/master-avatars"), path
|
||||||
|
)
|
||||||
|
|
||||||
|
@self.route("/custom/<path:path>")
|
||||||
|
def send_custom(path: str) -> Response:
|
||||||
|
return send_from_directory(os.path.join(self.root_path, "../custom"), path)
|
||||||
|
|
||||||
|
# @self.errorhandler(404)
|
||||||
|
# def not_found_error(error):
|
||||||
|
# return render_template('page_404.html'), 404
|
||||||
|
|
||||||
|
# @self.errorhandler(500)
|
||||||
|
# def internal_error(error):
|
||||||
|
# return render_template('page_500.html'), 500
|
||||||
|
|
||||||
|
@self.errorhandler(Error)
|
||||||
|
def handle_user_error(ex : Error) -> Response:
|
||||||
|
response : Response = jsonify(ex.error)
|
||||||
|
response.status_code = ex.status_code
|
||||||
|
response.headers.extend(ex.content_type) # type: ignore # werkzeug type hint MIA
|
||||||
|
return response
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -26,8 +27,6 @@ from time import sleep
|
||||||
|
|
||||||
import diceware
|
import diceware
|
||||||
|
|
||||||
from admin import app
|
|
||||||
|
|
||||||
from .avatars import Avatars
|
from .avatars import Avatars
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
filter_roles_list,
|
filter_roles_list,
|
||||||
|
@ -61,14 +60,27 @@ from .helpers import (
|
||||||
rand_password,
|
rand_password,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, cast, Any, Dict, Iterable, List, Optional
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from admin.flaskapp import AdminFlaskApp
|
||||||
|
|
||||||
MANAGER = os.environ["CUSTOM_ROLE_MANAGER"]
|
MANAGER = os.environ["CUSTOM_ROLE_MANAGER"]
|
||||||
TEACHER = os.environ["CUSTOM_ROLE_TEACHER"]
|
TEACHER = os.environ["CUSTOM_ROLE_TEACHER"]
|
||||||
STUDENT = os.environ["CUSTOM_ROLE_STUDENT"]
|
STUDENT = os.environ["CUSTOM_ROLE_STUDENT"]
|
||||||
|
|
||||||
|
DDUser = Dict[str, Any]
|
||||||
|
DDGroup = Dict[str, Any]
|
||||||
|
DDRole = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
class Admin:
|
class Admin:
|
||||||
def __init__(self):
|
app : "AdminFlaskApp"
|
||||||
self.check_connections()
|
internal : Dict[str, Any]
|
||||||
|
external : Dict[str, Any]
|
||||||
|
def __init__(self, app : "AdminFlaskApp") -> None:
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
self.check_connections(app)
|
||||||
|
|
||||||
self.set_custom_roles()
|
self.set_custom_roles()
|
||||||
self.overwrite_admins()
|
self.overwrite_admins()
|
||||||
|
@ -90,13 +102,13 @@ class Admin:
|
||||||
self.external = {"users": [], "groups": [], "roles": []}
|
self.external = {"users": [], "groups": [], "roles": []}
|
||||||
|
|
||||||
log.warning(" Updating missing user avatars with defaults")
|
log.warning(" Updating missing user avatars with defaults")
|
||||||
self.av = Avatars()
|
self.av = Avatars(app.avatars_path)
|
||||||
# av.minio_delete_all_objects() # This will reset all avatars on usres
|
# av.minio_delete_all_objects() # This will reset all avatars on usres
|
||||||
self.av.update_missing_avatars(self.internal["users"])
|
self.av.update_missing_avatars(self.internal["users"])
|
||||||
|
|
||||||
log.warning(" SYSTEM READY TO HANDLE CONNECTIONS")
|
log.warning(" SYSTEM READY TO HANDLE CONNECTIONS")
|
||||||
|
|
||||||
def check_connections(self):
|
def check_connections(self, app : "AdminFlaskApp") -> None:
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
|
@ -111,7 +123,7 @@ class Admin:
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
self.moodle = Moodle(verify=app.config["VERIFY"])
|
self.moodle = Moodle(app)
|
||||||
ready = True
|
ready = True
|
||||||
except:
|
except:
|
||||||
log.error("Could not connect to moodle, waiting to be online...")
|
log.error("Could not connect to moodle, waiting to be online...")
|
||||||
|
@ -136,18 +148,18 @@ class Admin:
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
self.nextcloud = Nextcloud(verify=app.config["VERIFY"])
|
self.nextcloud = Nextcloud(verify=app.config["VERIFY"], app=app)
|
||||||
ready = True
|
ready = True
|
||||||
except:
|
except:
|
||||||
log.error("Could not connect to nextcloud, waiting to be online...")
|
log.error("Could not connect to nextcloud, waiting to be online...")
|
||||||
sleep(2)
|
sleep(2)
|
||||||
log.warning("Nextcloud connected.")
|
log.warning("Nextcloud connected.")
|
||||||
|
|
||||||
def set_custom_roles(self):
|
def set_custom_roles(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
## This function should be moved to postup.py
|
## This function should be moved to postup.py
|
||||||
def overwrite_admins(self):
|
def overwrite_admins(self) -> None:
|
||||||
log.warning("Setting defaults...")
|
log.warning("Setting defaults...")
|
||||||
dduser = os.environ["DDADMIN_USER"]
|
dduser = os.environ["DDADMIN_USER"]
|
||||||
ddpassword = os.environ["DDADMIN_PASSWORD"]
|
ddpassword = os.environ["DDADMIN_PASSWORD"]
|
||||||
|
@ -223,7 +235,7 @@ class Admin:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
def default_setup(self):
|
def default_setup(self) -> None:
|
||||||
### Add default roles
|
### Add default roles
|
||||||
try:
|
try:
|
||||||
log.warning("KEYCLOAK: Adding default roles")
|
log.warning("KEYCLOAK: Adding default roles")
|
||||||
|
@ -324,7 +336,7 @@ class Admin:
|
||||||
# except:
|
# except:
|
||||||
# log.warning("KEYCLOAK: Seems to be there already")
|
# log.warning("KEYCLOAK: Seems to be there already")
|
||||||
|
|
||||||
def resync_data(self):
|
def resync_data(self) -> bool:
|
||||||
self.internal = {
|
self.internal = {
|
||||||
"users": self._get_mix_users(),
|
"users": self._get_mix_users(),
|
||||||
"groups": self._get_mix_groups(),
|
"groups": self._get_mix_groups(),
|
||||||
|
@ -332,7 +344,7 @@ class Admin:
|
||||||
}
|
}
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_moodle_users(self):
|
def get_moodle_users(self) -> List[Any]:
|
||||||
return [
|
return [
|
||||||
u
|
u
|
||||||
for u in self.moodle.get_users_with_groups_and_roles()
|
for u in self.moodle.get_users_with_groups_and_roles()
|
||||||
|
@ -353,7 +365,7 @@ class Admin:
|
||||||
# "roles": u['roles']}
|
# "roles": u['roles']}
|
||||||
# for u in users]
|
# for u in users]
|
||||||
|
|
||||||
def get_keycloak_users(self):
|
def get_keycloak_users(self) -> List[DDUser]:
|
||||||
# log.warning('Loading keycloak users... can take a long time...')
|
# log.warning('Loading keycloak users... can take a long time...')
|
||||||
|
|
||||||
users = self.keycloak.get_users_with_groups_and_roles()
|
users = self.keycloak.get_users_with_groups_and_roles()
|
||||||
|
@ -372,7 +384,7 @@ class Admin:
|
||||||
if not system_username(u["username"])
|
if not system_username(u["username"])
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_nextcloud_users(self):
|
def get_nextcloud_users(self) -> List[DDUser]:
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"id": u["username"],
|
"id": u["username"],
|
||||||
|
@ -414,11 +426,11 @@ class Admin:
|
||||||
# "roles": []})
|
# "roles": []})
|
||||||
# return users_list
|
# return users_list
|
||||||
|
|
||||||
def get_mix_users(self):
|
def get_mix_users(self) -> Any:
|
||||||
sio_event_send("get_users", {"you_win": "you got the users!"})
|
sio_event_send(self.app, "get_users", {"you_win": "you got the users!"})
|
||||||
return self.internal["users"]
|
return self.internal["users"]
|
||||||
|
|
||||||
def _get_mix_users(self):
|
def _get_mix_users(self) -> List[DDUser]:
|
||||||
kgroups = self.keycloak.get_groups()
|
kgroups = self.keycloak.get_groups()
|
||||||
|
|
||||||
kusers = self.get_keycloak_users()
|
kusers = self.get_keycloak_users()
|
||||||
|
@ -481,32 +493,32 @@ class Admin:
|
||||||
users.append(theuser)
|
users.append(theuser)
|
||||||
return users
|
return users
|
||||||
|
|
||||||
def get_roles(self):
|
def get_roles(self) -> Any:
|
||||||
return self.internal["roles"]
|
return self.internal["roles"]
|
||||||
|
|
||||||
def _get_roles(self):
|
def _get_roles(self) -> List[DDRole]:
|
||||||
return filter_roles_listofdicts(self.keycloak.get_roles())
|
return filter_roles_listofdicts(self.keycloak.get_roles())
|
||||||
|
|
||||||
def get_group_by_name(self, group_name):
|
def get_group_by_name(self, group_name : str) -> Any:
|
||||||
group = [g for g in self.internal["groups"] if g["name"] == group_name]
|
group = [g for g in self.internal["groups"] if g["name"] == group_name]
|
||||||
return group[0] if len(group) else False
|
return group[0] if len(group) else False
|
||||||
|
|
||||||
def get_keycloak_groups(self):
|
def get_keycloak_groups(self) -> Any:
|
||||||
log.warning("Loading keycloak groups...")
|
log.warning("Loading keycloak groups...")
|
||||||
return self.keycloak.get_groups()
|
return self.keycloak.get_groups()
|
||||||
|
|
||||||
def get_moodle_groups(self):
|
def get_moodle_groups(self) -> Any:
|
||||||
log.warning("Loading moodle groups...")
|
log.warning("Loading moodle groups...")
|
||||||
return self.moodle.get_cohorts()
|
return self.moodle.get_cohorts()
|
||||||
|
|
||||||
def get_nextcloud_groups(self):
|
def get_nextcloud_groups(self) -> Any:
|
||||||
log.warning("Loading nextcloud groups...")
|
log.warning("Loading nextcloud groups...")
|
||||||
return self.nextcloud.get_groups_list()
|
return self.nextcloud.get_groups_list()
|
||||||
|
|
||||||
def get_mix_groups(self):
|
def get_mix_groups(self) -> Any:
|
||||||
return self.internal["groups"]
|
return self.internal["groups"]
|
||||||
|
|
||||||
def _get_mix_groups(self):
|
def _get_mix_groups(self) -> List[Dict[str, Any]]:
|
||||||
kgroups = self.get_keycloak_groups()
|
kgroups = self.get_keycloak_groups()
|
||||||
mgroups = self.get_moodle_groups()
|
mgroups = self.get_moodle_groups()
|
||||||
ngroups = self.get_nextcloud_groups()
|
ngroups = self.get_nextcloud_groups()
|
||||||
|
@ -564,7 +576,7 @@ class Admin:
|
||||||
groups.append(thegroup)
|
groups.append(thegroup)
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
def sync_groups_from_keycloak(self):
|
def sync_groups_from_keycloak(self) -> None:
|
||||||
self.resync_data()
|
self.resync_data()
|
||||||
for group in self.internal["groups"]:
|
for group in self.internal["groups"]:
|
||||||
if not group["keycloak"]:
|
if not group["keycloak"]:
|
||||||
|
@ -586,22 +598,22 @@ class Admin:
|
||||||
self.nextcloud.add_group(group["name"])
|
self.nextcloud.add_group(group["name"])
|
||||||
self.resync_data()
|
self.resync_data()
|
||||||
|
|
||||||
def get_external_users(self):
|
def get_external_users(self) -> Any:
|
||||||
return self.external["users"]
|
return self.external["users"]
|
||||||
|
|
||||||
def get_external_groups(self):
|
def get_external_groups(self) -> Any:
|
||||||
return self.external["groups"]
|
return self.external["groups"]
|
||||||
|
|
||||||
def get_external_roles(self):
|
def get_external_roles(self) -> Any:
|
||||||
return self.external["roles"]
|
return self.external["roles"]
|
||||||
|
|
||||||
def upload_csv_ug(self, data):
|
def upload_csv_ug(self, data : Dict[str, Any]) -> bool:
|
||||||
log.warning("Processing uploaded users...")
|
log.warning("Processing uploaded users...")
|
||||||
users = []
|
users = []
|
||||||
total = len(data["data"])
|
total = len(data["data"])
|
||||||
item = 1
|
item = 1
|
||||||
ev = Events("Processing uploaded users", total=len(data["data"]))
|
ev = Events(self.app, "Processing uploaded users", total=len(data["data"]))
|
||||||
groups = []
|
groups : List[str] = []
|
||||||
for u in data["data"]:
|
for u in data["data"]:
|
||||||
log.warning(
|
log.warning(
|
||||||
"Processing ("
|
"Processing ("
|
||||||
|
@ -680,18 +692,18 @@ class Admin:
|
||||||
self.external["groups"] = sysgroups
|
self.external["groups"] = sysgroups
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_dice_pwd(self):
|
def get_dice_pwd(self) -> str:
|
||||||
return diceware.get_passphrase(options=options)
|
return cast(str, diceware.get_passphrase(options=options))
|
||||||
|
|
||||||
def reset_external(self):
|
def reset_external(self) -> bool:
|
||||||
self.external = {"users": [], "groups": [], "roles": []}
|
self.external = {"users": [], "groups": [], "roles": []}
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def upload_json_ga(self, data):
|
def upload_json_ga(self, data : Dict[str, Any]) -> bool:
|
||||||
groups = []
|
groups = []
|
||||||
log.warning("Processing uploaded groups...")
|
log.warning("Processing uploaded groups...")
|
||||||
try:
|
try:
|
||||||
ev = Events(
|
ev = Events(self.app,
|
||||||
"Processing uploaded groups",
|
"Processing uploaded groups",
|
||||||
"Group:",
|
"Group:",
|
||||||
total=len(data["data"]["groups"]),
|
total=len(data["data"]["groups"]),
|
||||||
|
@ -718,7 +730,7 @@ class Admin:
|
||||||
users = []
|
users = []
|
||||||
total = len(data["data"]["users"])
|
total = len(data["data"]["users"])
|
||||||
item = 1
|
item = 1
|
||||||
ev = Events(
|
ev = Events(self.app,
|
||||||
"Processing uploaded users",
|
"Processing uploaded users",
|
||||||
"User:",
|
"User:",
|
||||||
total=len(data["data"]["users"]),
|
total=len(data["data"]["users"]),
|
||||||
|
@ -757,7 +769,7 @@ class Admin:
|
||||||
u["groups"] = u["groups"] + [g["name"]]
|
u["groups"] = u["groups"] + [g["name"]]
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def sync_external(self, ids):
|
def sync_external(self, ids : Any) -> None:
|
||||||
# self.resync_data()
|
# self.resync_data()
|
||||||
log.warning("Starting sync to keycloak")
|
log.warning("Starting sync to keycloak")
|
||||||
self.sync_to_keycloak_external()
|
self.sync_to_keycloak_external()
|
||||||
|
@ -769,10 +781,10 @@ class Admin:
|
||||||
log.warning("All syncs finished. Resyncing from apps...")
|
log.warning("All syncs finished. Resyncing from apps...")
|
||||||
self.resync_data()
|
self.resync_data()
|
||||||
|
|
||||||
def add_keycloak_groups(self, groups):
|
def add_keycloak_groups(self, groups : List[Any]) -> None:
|
||||||
total = len(groups)
|
total = len(groups)
|
||||||
i = 0
|
i = 0
|
||||||
ev = Events(
|
ev = Events(self.app,
|
||||||
"Syncing import groups to keycloak", "Adding group:", total=len(groups)
|
"Syncing import groups to keycloak", "Adding group:", total=len(groups)
|
||||||
)
|
)
|
||||||
for g in groups:
|
for g in groups:
|
||||||
|
@ -790,8 +802,8 @@ class Admin:
|
||||||
|
|
||||||
def sync_to_keycloak_external(
|
def sync_to_keycloak_external(
|
||||||
self,
|
self,
|
||||||
): ### This one works from the external, moodle and nextcloud from the internal
|
) -> None: ### This one works from the external, moodle and nextcloud from the internal
|
||||||
groups = []
|
groups : List[DDGroup] = []
|
||||||
for u in self.external["users"]:
|
for u in self.external["users"]:
|
||||||
groups = groups + u["groups"]
|
groups = groups + u["groups"]
|
||||||
groups = list(dict.fromkeys(groups))
|
groups = list(dict.fromkeys(groups))
|
||||||
|
@ -800,7 +812,7 @@ class Admin:
|
||||||
|
|
||||||
total = len(self.external["users"])
|
total = len(self.external["users"])
|
||||||
index = 0
|
index = 0
|
||||||
ev = Events(
|
ev = Events(self.app,
|
||||||
"Syncing import users to keycloak",
|
"Syncing import users to keycloak",
|
||||||
"Adding user:",
|
"Adding user:",
|
||||||
total=len(self.external["users"]),
|
total=len(self.external["users"]),
|
||||||
|
@ -855,11 +867,11 @@ class Admin:
|
||||||
u["groups"].append(u["roles"][0])
|
u["groups"].append(u["roles"][0])
|
||||||
self.resync_data()
|
self.resync_data()
|
||||||
|
|
||||||
def add_moodle_groups(self, groups):
|
def add_moodle_groups(self, groups : List[Any]) -> None:
|
||||||
### Create all groups. Skip / in system groups
|
### Create all groups. Skip / in system groups
|
||||||
total = len(groups)
|
total = len(groups)
|
||||||
log.warning(groups)
|
log.warning(groups)
|
||||||
ev = Events("Syncing groups from external to moodle", total=len(groups))
|
ev = Events(self.app, "Syncing groups from external to moodle", total=len(groups))
|
||||||
i = 1
|
i = 1
|
||||||
for g in groups:
|
for g in groups:
|
||||||
moodle_groups = kpath2gids(g)
|
moodle_groups = kpath2gids(g)
|
||||||
|
@ -880,9 +892,9 @@ class Admin:
|
||||||
)
|
)
|
||||||
i = i + 1
|
i = i + 1
|
||||||
|
|
||||||
def sync_to_moodle_external(self): # works from the internal (keycloak)
|
def sync_to_moodle_external(self) -> None: # works from the internal (keycloak)
|
||||||
### Process all groups from the users keycloak_groups key
|
### Process all groups from the users keycloak_groups key
|
||||||
groups = []
|
groups : List[DDGroup] = []
|
||||||
for u in self.external["users"]:
|
for u in self.external["users"]:
|
||||||
groups = groups + u["groups"]
|
groups = groups + u["groups"]
|
||||||
groups = list(dict.fromkeys(groups))
|
groups = list(dict.fromkeys(groups))
|
||||||
|
@ -893,7 +905,7 @@ class Admin:
|
||||||
cohorts = self.moodle.get_cohorts()
|
cohorts = self.moodle.get_cohorts()
|
||||||
|
|
||||||
### Create users in moodle
|
### Create users in moodle
|
||||||
ev = Events(
|
ev = Events(self.app,
|
||||||
"Syncing users from external to moodle", total=len(self.internal["users"])
|
"Syncing users from external to moodle", total=len(self.internal["users"])
|
||||||
)
|
)
|
||||||
for u in self.external["users"]:
|
for u in self.external["users"]:
|
||||||
|
@ -920,7 +932,7 @@ class Admin:
|
||||||
|
|
||||||
# self.resync_data()
|
# self.resync_data()
|
||||||
### Add user to their cohorts (groups)
|
### Add user to their cohorts (groups)
|
||||||
ev = Events(
|
ev = Events(self.app,
|
||||||
"Syncing users groups from external to moodle cohorts",
|
"Syncing users groups from external to moodle cohorts",
|
||||||
total=len(self.internal["users"]),
|
total=len(self.internal["users"]),
|
||||||
)
|
)
|
||||||
|
@ -938,16 +950,16 @@ class Admin:
|
||||||
log.error(self.moodle.get_user_by("username", u["username"]))
|
log.error(self.moodle.get_user_by("username", u["username"]))
|
||||||
# self.resync_data()
|
# self.resync_data()
|
||||||
|
|
||||||
def delete_all_moodle_cohorts(self):
|
def delete_all_moodle_cohorts(self) -> None:
|
||||||
cohorts = self.moodle.get_cohorts()
|
cohorts = self.moodle.get_cohorts()
|
||||||
ids = [c["id"] for c in cohorts]
|
ids = [c["id"] for c in cohorts]
|
||||||
self.moodle.delete_cohorts(ids)
|
self.moodle.delete_cohorts(ids)
|
||||||
|
|
||||||
def add_nextcloud_groups(self, groups):
|
def add_nextcloud_groups(self, groups : List[Any]) -> None:
|
||||||
### Create all groups. Skip / in system groups
|
### Create all groups. Skip / in system groups
|
||||||
total = len(groups)
|
total = len(groups)
|
||||||
log.warning(groups)
|
log.warning(groups)
|
||||||
ev = Events("Syncing groups from external to nextcloud", total=len(groups))
|
ev = Events(self.app, "Syncing groups from external to nextcloud", total=len(groups))
|
||||||
i = 1
|
i = 1
|
||||||
for g in groups:
|
for g in groups:
|
||||||
nextcloud_groups = kpath2gids(g)
|
nextcloud_groups = kpath2gids(g)
|
||||||
|
@ -968,15 +980,15 @@ class Admin:
|
||||||
)
|
)
|
||||||
i = i + 1
|
i = i + 1
|
||||||
|
|
||||||
def sync_to_nextcloud_external(self):
|
def sync_to_nextcloud_external(self) -> None:
|
||||||
groups = []
|
groups : List[DDGroup] = []
|
||||||
for u in self.external["users"]:
|
for u in self.external["users"]:
|
||||||
groups = groups + u["gids"]
|
groups = groups + u["gids"]
|
||||||
groups = list(dict.fromkeys(groups))
|
groups = list(dict.fromkeys(groups))
|
||||||
|
|
||||||
self.add_nextcloud_groups(groups)
|
self.add_nextcloud_groups(groups)
|
||||||
|
|
||||||
ev = Events(
|
ev = Events(self.app,
|
||||||
"Syncing users from external to nextcloud",
|
"Syncing users from external to nextcloud",
|
||||||
total=len(self.internal["users"]),
|
total=len(self.internal["users"]),
|
||||||
)
|
)
|
||||||
|
@ -1009,14 +1021,14 @@ class Admin:
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
|
|
||||||
def sync_to_moodle(self): # works from the internal (keycloak)
|
def sync_to_moodle(self) -> None: # works from the internal (keycloak)
|
||||||
### Process all groups from the users keycloak_groups key
|
### Process all groups from the users keycloak_groups key
|
||||||
groups = []
|
groups : List[str] = []
|
||||||
for u in self.internal["users"]:
|
for u in self.internal["users"]:
|
||||||
groups = groups + u["keycloak_groups"]
|
groups = groups + u["keycloak_groups"]
|
||||||
groups = list(dict.fromkeys(groups))
|
groups = list(dict.fromkeys(groups))
|
||||||
|
|
||||||
ev = Events("Syncing groups from keycloak to moodle", total=len(groups))
|
ev = Events(self.app, "Syncing groups from keycloak to moodle", total=len(groups))
|
||||||
pathslist = []
|
pathslist = []
|
||||||
for group in groups:
|
for group in groups:
|
||||||
pathpart = ""
|
pathpart = ""
|
||||||
|
@ -1040,7 +1052,7 @@ class Admin:
|
||||||
cohorts = self.moodle.get_cohorts()
|
cohorts = self.moodle.get_cohorts()
|
||||||
|
|
||||||
### Create users in moodle
|
### Create users in moodle
|
||||||
ev = Events(
|
ev = Events(self.app,
|
||||||
"Syncing users from keycloak to moodle", total=len(self.internal["users"])
|
"Syncing users from keycloak to moodle", total=len(self.internal["users"])
|
||||||
)
|
)
|
||||||
for u in self.internal["users"]:
|
for u in self.internal["users"]:
|
||||||
|
@ -1067,7 +1079,7 @@ class Admin:
|
||||||
|
|
||||||
self.resync_data()
|
self.resync_data()
|
||||||
|
|
||||||
ev = Events(
|
ev = Events(self.app,
|
||||||
"Syncing users with moodle cohorts", total=len(self.internal["users"])
|
"Syncing users with moodle cohorts", total=len(self.internal["users"])
|
||||||
)
|
)
|
||||||
cohorts = self.moodle.get_cohorts()
|
cohorts = self.moodle.get_cohorts()
|
||||||
|
@ -1106,15 +1118,15 @@ class Admin:
|
||||||
|
|
||||||
self.resync_data()
|
self.resync_data()
|
||||||
|
|
||||||
def sync_to_nextcloud(self):
|
def sync_to_nextcloud(self) -> None:
|
||||||
groups = []
|
groups : List[str] = []
|
||||||
for u in self.internal["users"]:
|
for u in self.internal["users"]:
|
||||||
groups = groups + u["keycloak_groups"]
|
groups = groups + u["keycloak_groups"]
|
||||||
groups = list(dict.fromkeys(groups))
|
groups = list(dict.fromkeys(groups))
|
||||||
|
|
||||||
total = len(groups)
|
total = len(groups)
|
||||||
i = 0
|
i = 0
|
||||||
ev = Events("Syncing groups from keycloak to nextcloud", total=len(groups))
|
ev = Events(self.app, "Syncing groups from keycloak to nextcloud", total=len(groups))
|
||||||
for g in groups:
|
for g in groups:
|
||||||
parts = g.split("/")
|
parts = g.split("/")
|
||||||
subpath = ""
|
subpath = ""
|
||||||
|
@ -1137,7 +1149,7 @@ class Admin:
|
||||||
)
|
)
|
||||||
i = i + 1
|
i = i + 1
|
||||||
|
|
||||||
ev = Events(
|
ev = Events(self.app,
|
||||||
"Syncing users from keycloak to nextcloud",
|
"Syncing users from keycloak to nextcloud",
|
||||||
total=len(self.internal["users"]),
|
total=len(self.internal["users"]),
|
||||||
)
|
)
|
||||||
|
@ -1167,13 +1179,13 @@ class Admin:
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
|
|
||||||
def delete_keycloak_user(self, userid):
|
def delete_keycloak_user(self, userid : str) -> None:
|
||||||
user = [u for u in self.internal["users"] if u["id"] == userid]
|
users : List[DDUser] = [u for u in self.internal["users"] if u["id"] == userid]
|
||||||
if len(user) and user[0]["keycloak"]:
|
if len(users) and users[0]["keycloak"]:
|
||||||
user = user[0]
|
user = users[0]
|
||||||
keycloak_id = user["id"]
|
keycloak_id = user["id"]
|
||||||
else:
|
else:
|
||||||
return False
|
return
|
||||||
log.warning("Removing keycloak user: " + user["username"])
|
log.warning("Removing keycloak user: " + user["username"])
|
||||||
try:
|
try:
|
||||||
self.keycloak.delete_user(keycloak_id)
|
self.keycloak.delete_user(keycloak_id)
|
||||||
|
@ -1183,10 +1195,10 @@ class Admin:
|
||||||
|
|
||||||
self.av.delete_user_avatar(userid)
|
self.av.delete_user_avatar(userid)
|
||||||
|
|
||||||
def delete_keycloak_users(self):
|
def delete_keycloak_users(self) -> None:
|
||||||
total = len(self.internal["users"])
|
total = len(self.internal["users"])
|
||||||
i = 0
|
i = 0
|
||||||
ev = Events(
|
ev = Events(self.app,
|
||||||
"Deleting users from keycloak",
|
"Deleting users from keycloak",
|
||||||
"Deleting user:",
|
"Deleting user:",
|
||||||
total=len(self.internal["users"]),
|
total=len(self.internal["users"]),
|
||||||
|
@ -1217,13 +1229,13 @@ class Admin:
|
||||||
)
|
)
|
||||||
self.av.minio_delete_all_objects()
|
self.av.minio_delete_all_objects()
|
||||||
|
|
||||||
def delete_nextcloud_user(self, userid):
|
def delete_nextcloud_user(self, userid : str) -> None:
|
||||||
user = [u for u in self.internal["users"] if u["id"] == userid]
|
users : List[DDUser] = [u for u in self.internal["users"] if u["id"] == userid]
|
||||||
if len(user) and user[0]["nextcloud"]:
|
if len(users) and users[0]["nextcloud"]:
|
||||||
user = user[0]
|
user = users[0]
|
||||||
nextcloud_id = user["nextcloud_id"]
|
nextcloud_id = user["nextcloud_id"]
|
||||||
else:
|
else:
|
||||||
return False
|
return
|
||||||
log.warning("Removing nextcloud user: " + user["username"])
|
log.warning("Removing nextcloud user: " + user["username"])
|
||||||
try:
|
try:
|
||||||
self.nextcloud.delete_user(nextcloud_id)
|
self.nextcloud.delete_user(nextcloud_id)
|
||||||
|
@ -1231,8 +1243,8 @@ class Admin:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
log.warning("Could not remove users: " + user["username"])
|
log.warning("Could not remove users: " + user["username"])
|
||||||
|
|
||||||
def delete_nextcloud_users(self):
|
def delete_nextcloud_users(self) -> None:
|
||||||
ev = Events("Deleting users from nextcloud", total=len(self.internal["users"]))
|
ev = Events(self.app, "Deleting users from nextcloud", total=len(self.internal["users"]))
|
||||||
for u in self.internal["users"]:
|
for u in self.internal["users"]:
|
||||||
|
|
||||||
if u["nextcloud"] and not u["keycloak"]:
|
if u["nextcloud"] and not u["keycloak"]:
|
||||||
|
@ -1246,13 +1258,13 @@ class Admin:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
log.warning("Could not remove user: " + u["username"])
|
log.warning("Could not remove user: " + u["username"])
|
||||||
|
|
||||||
def delete_moodle_user(self, userid):
|
def delete_moodle_user(self, userid : str) -> None:
|
||||||
user = [u for u in self.internal["users"] if u["id"] == userid]
|
users : List[DDUser] = [u for u in self.internal["users"] if u["id"] == userid]
|
||||||
if len(user) and user[0]["moodle"]:
|
if len(users) and users[0]["moodle"]:
|
||||||
user = user[0]
|
user = users[0]
|
||||||
moodle_id = user["moodle_id"]
|
moodle_id = user["moodle_id"]
|
||||||
else:
|
else:
|
||||||
return False
|
return
|
||||||
log.warning("Removing moodle user: " + user["username"])
|
log.warning("Removing moodle user: " + user["username"])
|
||||||
try:
|
try:
|
||||||
self.moodle.delete_users([moodle_id])
|
self.moodle.delete_users([moodle_id])
|
||||||
|
@ -1260,7 +1272,7 @@ class Admin:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
log.warning("Could not remove users: " + user["username"])
|
log.warning("Could not remove users: " + user["username"])
|
||||||
|
|
||||||
def delete_moodle_users(self):
|
def delete_moodle_users(self, app : "AdminFlaskApp") -> None:
|
||||||
userids = []
|
userids = []
|
||||||
usernames = []
|
usernames = []
|
||||||
for u in self.internal["users"]:
|
for u in self.internal["users"]:
|
||||||
|
@ -1288,7 +1300,7 @@ class Admin:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
log.warning("Could not remove users: " + ",".join(usernames))
|
log.warning("Could not remove users: " + ",".join(usernames))
|
||||||
|
|
||||||
def delete_keycloak_groups(self):
|
def delete_keycloak_groups(self) -> None:
|
||||||
for g in self.internal["groups"]:
|
for g in self.internal["groups"]:
|
||||||
if not g["keycloak"]:
|
if not g["keycloak"]:
|
||||||
continue
|
continue
|
||||||
|
@ -1302,7 +1314,7 @@ class Admin:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
log.warning("Could not remove group: " + g["name"])
|
log.warning("Could not remove group: " + g["name"])
|
||||||
|
|
||||||
def external_roleassign(self, data):
|
def external_roleassign(self, data : Dict[str, Any]) -> bool:
|
||||||
for newuserid in data["ids"]:
|
for newuserid in data["ids"]:
|
||||||
for externaluser in self.external["users"]:
|
for externaluser in self.external["users"]:
|
||||||
if externaluser["id"] == newuserid:
|
if externaluser["id"] == newuserid:
|
||||||
|
@ -1316,10 +1328,10 @@ class Admin:
|
||||||
externaluser["gids"].append(data["action"])
|
externaluser["gids"].append(data["action"])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def user_update_password(self, userid, password, password_temporary):
|
def user_update_password(self, userid : str, password : str, password_temporary : bool) -> Any:
|
||||||
return self.keycloak.update_user_pwd(userid, password, password_temporary)
|
return self.keycloak.update_user_pwd(userid, password, password_temporary)
|
||||||
|
|
||||||
def update_users_from_keycloak(self):
|
def update_users_from_keycloak(self) -> None:
|
||||||
kgroups = self.keycloak.get_groups()
|
kgroups = self.keycloak.get_groups()
|
||||||
users = [
|
users = [
|
||||||
{
|
{
|
||||||
|
@ -1339,15 +1351,15 @@ class Admin:
|
||||||
]
|
]
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
ev = Events(
|
ev = Events(self.app,
|
||||||
"Updating users from keycloak", "User:", total=len(users), table="users"
|
"Updating users from keycloak", "User:", total=len(users), table="users"
|
||||||
)
|
)
|
||||||
self.user_update(user)
|
self.user_update(user)
|
||||||
ev.increment({"name": user["username"], "data": user["groups"]})
|
ev.increment({"name": user["username"], "data": user["groups"]})
|
||||||
|
|
||||||
def user_update(self, user):
|
def user_update(self, user : DDUser) -> bool:
|
||||||
log.warning("Updating user moodle, nextcloud keycloak")
|
log.warning("Updating user moodle, nextcloud keycloak")
|
||||||
ev = Events("Updating user", "Updating user in keycloak")
|
ev = Events(self.app, "Updating user", "Updating user in keycloak")
|
||||||
|
|
||||||
## Get actual user role
|
## Get actual user role
|
||||||
try:
|
try:
|
||||||
|
@ -1505,7 +1517,7 @@ class Admin:
|
||||||
ev.update_text("User updated")
|
ev.update_text("User updated")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def update_keycloak_user(self, user_id, user, kdelete, kadd):
|
def update_keycloak_user(self, user_id : str, user : DDUser, kdelete : List[Any], kadd : List[Any]) -> bool:
|
||||||
# pprint(self.keycloak.get_user_realm_roles(user_id))
|
# pprint(self.keycloak.get_user_realm_roles(user_id))
|
||||||
self.keycloak.remove_user_realm_roles(user_id, "student")
|
self.keycloak.remove_user_realm_roles(user_id, "student")
|
||||||
self.keycloak.assign_realm_roles(user_id, user["roles"][0])
|
self.keycloak.assign_realm_roles(user_id, user["roles"][0])
|
||||||
|
@ -1521,24 +1533,24 @@ class Admin:
|
||||||
self.resync_data()
|
self.resync_data()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def enable_users(self, data):
|
def enable_users(self, data : List[DDUser]) -> None:
|
||||||
# data={'id':'','username':''}
|
# data={'id':'','username':''}
|
||||||
ev = Events("Bulk actions", "Enabling user:", total=len(data))
|
ev = Events(self.app, "Bulk actions", "Enabling user:", total=len(data))
|
||||||
for user in data:
|
for user in data:
|
||||||
ev.increment({"name": user["username"], "data": user["username"]})
|
ev.increment({"name": user["username"], "data": user["username"]})
|
||||||
self.keycloak.user_enable(user["id"])
|
self.keycloak.user_enable(user["id"])
|
||||||
self.resync_data()
|
self.resync_data()
|
||||||
|
|
||||||
def disable_users(self, data):
|
def disable_users(self, data : List[DDUser]) -> None:
|
||||||
# data={'id':'','username':''}
|
# data={'id':'','username':''}
|
||||||
ev = Events("Bulk actions", "Disabling user:", total=len(data))
|
ev = Events(self.app, "Bulk actions", "Disabling user:", total=len(data))
|
||||||
for user in data:
|
for user in data:
|
||||||
ev.increment({"name": user["username"], "data": user["username"]})
|
ev.increment({"name": user["username"], "data": user["username"]})
|
||||||
self.keycloak.user_disable(user["id"])
|
self.keycloak.user_disable(user["id"])
|
||||||
self.resync_data()
|
self.resync_data()
|
||||||
|
|
||||||
def update_moodle_user(self, user_id, user, mdelete, madd):
|
def update_moodle_user(self, user_id : str, user : DDUser, mdelete : Iterable[Any], madd : Iterable[Any]) -> bool:
|
||||||
internaluser = [u for u in self.internal["users"] if u["id"] == user_id][0]
|
internaluser : DDUser = [u for u in self.internal["users"] if u["id"] == user_id][0]
|
||||||
cohorts = self.moodle.get_cohorts()
|
cohorts = self.moodle.get_cohorts()
|
||||||
for group in mdelete:
|
for group in mdelete:
|
||||||
cohort = [c for c in cohorts if c["name"] == group[0]]
|
cohort = [c for c in cohorts if c["name"] == group[0]]
|
||||||
|
@ -1576,29 +1588,29 @@ class Admin:
|
||||||
|
|
||||||
def add_moodle_user(
|
def add_moodle_user(
|
||||||
self,
|
self,
|
||||||
username,
|
username : str,
|
||||||
email,
|
email : str,
|
||||||
first_name,
|
first_name : str,
|
||||||
last_name,
|
last_name : str,
|
||||||
password="*12" + secrets.token_urlsafe(16),
|
password : str="*12" + secrets.token_urlsafe(16),
|
||||||
):
|
) -> None:
|
||||||
log.warning("Creating moodle user: " + username)
|
log.warning("Creating moodle user: " + username)
|
||||||
ev = Events("Add user", username)
|
ev = Events(self.app, "Add user", username)
|
||||||
try:
|
try:
|
||||||
self.moodle.create_user(email, username, password, first_name, last_name)
|
self.moodle.create_user(email, username, password, first_name, last_name)
|
||||||
ev.update_text({"name": "Added to moodle", "data": []})
|
ev.update_text(str({"name": "Added to moodle", "data": []}))
|
||||||
except UserExists:
|
except UserExists:
|
||||||
log.error(" -->> User already exists")
|
log.error(" -->> User already exists")
|
||||||
error = Events("User already exists.", str(se), type="error")
|
error = Events(self.app, "User already exists.", str(se), type="error")
|
||||||
except SystemError as se:
|
except SystemError as se:
|
||||||
log.error("Moodle create user error: " + str(se))
|
log.error("Moodle create user error: " + str(se))
|
||||||
error = Events("Moodle create user error", str(se), type="error")
|
error = Events(self.app, "Moodle create user error", str(se), type="error")
|
||||||
except:
|
except:
|
||||||
log.error(" -->> Error creating on moodle the user: " + username)
|
log.error(" -->> Error creating on moodle the user: " + username)
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
error = Events("Internal error", "Check logs", type="error")
|
error = Events(self.app, "Internal error", "Check logs", type="error")
|
||||||
|
|
||||||
def update_nextcloud_user(self, user_id, user, ndelete, nadd):
|
def update_nextcloud_user(self, user_id : str, user : DDUser, ndelete : Iterable[Any], nadd : Iterable[Any]) -> None:
|
||||||
|
|
||||||
## TODO: Disable de user? Is really needed? it is disabled in keycloak, so can't login again
|
## TODO: Disable de user? Is really needed? it is disabled in keycloak, so can't login again
|
||||||
## ocs/v1.php/cloud/users/{userid}/disable
|
## ocs/v1.php/cloud/users/{userid}/disable
|
||||||
|
@ -1648,21 +1660,21 @@ class Admin:
|
||||||
|
|
||||||
def add_nextcloud_user(
|
def add_nextcloud_user(
|
||||||
self,
|
self,
|
||||||
username,
|
username : str,
|
||||||
email,
|
email : str,
|
||||||
quota,
|
quota : Any,
|
||||||
first_name,
|
first_name : str,
|
||||||
last_name,
|
last_name : str,
|
||||||
groups,
|
groups : str,
|
||||||
password="*12" + secrets.token_urlsafe(16),
|
password : str = "*12" + secrets.token_urlsafe(16),
|
||||||
):
|
) -> None:
|
||||||
log.warning(
|
log.warning(
|
||||||
" NEXTCLOUD USERS: Creating nextcloud user: "
|
" NEXTCLOUD USERS: Creating nextcloud user: "
|
||||||
+ username
|
+ username
|
||||||
+ " in groups "
|
+ " in groups "
|
||||||
+ str(groups)
|
+ str(groups)
|
||||||
)
|
)
|
||||||
ev = Events("Add user", username)
|
ev = Events(self.app, "Add user", username)
|
||||||
try:
|
try:
|
||||||
# Quota is "1 GB", "500 MB"
|
# Quota is "1 GB", "500 MB"
|
||||||
self.nextcloud.add_user_with_groups(
|
self.nextcloud.add_user_with_groups(
|
||||||
|
@ -1676,16 +1688,16 @@ class Admin:
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
|
|
||||||
def delete_users(self, data):
|
def delete_users(self, data : List[DDUser]) -> None:
|
||||||
ev = Events("Bulk actions", "Deleting users:", total=len(data))
|
ev = Events(self.app, "Bulk actions", "Deleting users:", total=len(data))
|
||||||
for user in data:
|
for user in data:
|
||||||
ev.increment({"name": user["username"], "data": user["username"]})
|
ev.increment({"name": user["username"], "data": user["username"]})
|
||||||
self.delete_user(user["id"])
|
self.delete_user(user["id"])
|
||||||
self.resync_data()
|
self.resync_data()
|
||||||
|
|
||||||
def delete_user(self, userid):
|
def delete_user(self, userid : str) -> bool:
|
||||||
log.warning("Deleting user moodle, nextcloud keycloak")
|
log.warning("Deleting user moodle, nextcloud keycloak")
|
||||||
ev = Events("Deleting user", "Deleting from moodle")
|
ev = Events(self.app, "Deleting user", "Deleting from moodle")
|
||||||
self.delete_moodle_user(userid)
|
self.delete_moodle_user(userid)
|
||||||
ev.update_text("Deleting from nextcloud")
|
ev.update_text("Deleting from nextcloud")
|
||||||
self.delete_nextcloud_user(userid)
|
self.delete_nextcloud_user(userid)
|
||||||
|
@ -1694,23 +1706,22 @@ class Admin:
|
||||||
ev.update_text("Syncing data from applications...")
|
ev.update_text("Syncing data from applications...")
|
||||||
self.resync_data()
|
self.resync_data()
|
||||||
ev.update_text("User deleted")
|
ev.update_text("User deleted")
|
||||||
sio_event_send("delete_user", {"userid": userid})
|
sio_event_send(self.app, "delete_user", {"userid": userid})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_user(self, userid):
|
def get_user(self, userid : str) -> Optional[DDUser]:
|
||||||
user = [u for u in self.internal["users"] if u["id"] == userid]
|
user : List[DDUser] = [u for u in self.internal["users"] if u["id"] == userid]
|
||||||
if not len(user):
|
if not len(user):
|
||||||
return False
|
return None
|
||||||
return user[0]
|
return user[0]
|
||||||
|
|
||||||
def get_user_username(self, username):
|
def get_user_username(self, username : str) -> Optional[DDUser]:
|
||||||
user = [u for u in self.internal["users"] if u["username"] == username]
|
user : List[DDUser] = [u for u in self.internal["users"] if u["username"] == username]
|
||||||
if not len(user):
|
if not len(user):
|
||||||
return False
|
return None
|
||||||
return user[0]
|
return user[0]
|
||||||
|
|
||||||
def add_user(self, u):
|
def add_user(self, u : DDUser) -> Any:
|
||||||
|
|
||||||
pathslist = []
|
pathslist = []
|
||||||
for group in u["groups"]:
|
for group in u["groups"]:
|
||||||
pathpart = ""
|
pathpart = ""
|
||||||
|
@ -1739,7 +1750,7 @@ class Admin:
|
||||||
|
|
||||||
### KEYCLOAK
|
### KEYCLOAK
|
||||||
#######################
|
#######################
|
||||||
ev = Events("Add user", u["username"], total=5)
|
ev = Events(self.app, "Add user", u["username"], total=5)
|
||||||
log.warning(" KEYCLOAK USERS: Adding user: " + u["username"])
|
log.warning(" KEYCLOAK USERS: Adding user: " + u["username"])
|
||||||
uid = self.keycloak.add_user(
|
uid = self.keycloak.add_user(
|
||||||
u["username"],
|
u["username"],
|
||||||
|
@ -1784,14 +1795,14 @@ class Admin:
|
||||||
ev.increment({"name": "Added to moodle", "data": []})
|
ev.increment({"name": "Added to moodle", "data": []})
|
||||||
except UserExists:
|
except UserExists:
|
||||||
log.error(" -->> User already exists")
|
log.error(" -->> User already exists")
|
||||||
error = Events("User already exists.", str(se), type="error")
|
error = Events(self.app, "User already exists.", str(se), type="error")
|
||||||
except SystemError as se:
|
except SystemError as se:
|
||||||
log.error("Moodle create user error: " + str(se))
|
log.error("Moodle create user error: " + str(se))
|
||||||
error = Events("Moodle create user error", str(se), type="error")
|
error = Events(self.app, "Moodle create user error", str(se), type="error")
|
||||||
except:
|
except:
|
||||||
log.error(" -->> Error creating on moodle the user: " + u["username"])
|
log.error(" -->> Error creating on moodle the user: " + u["username"])
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
error = Events("Internal error", "Check logs", type="error")
|
error = Events(self.app, "Internal error", "Check logs", type="error")
|
||||||
|
|
||||||
# Add user to cohort
|
# Add user to cohort
|
||||||
## Get all existing moodle cohorts
|
## Get all existing moodle cohorts
|
||||||
|
@ -1847,30 +1858,29 @@ class Admin:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
|
|
||||||
self.resync_data()
|
self.resync_data()
|
||||||
sio_event_send("new_user", u)
|
sio_event_send(self.app, "new_user", u)
|
||||||
return uid
|
return uid
|
||||||
|
|
||||||
def add_group(self, g):
|
def add_group(self, g : DDGroup) -> str:
|
||||||
# TODO: Check if exists
|
# TODO: Check if exists
|
||||||
|
|
||||||
# We add in keycloak with his name, will be shown in app with full path with dots
|
# We add in keycloak with his name, will be shown in app with full path with dots
|
||||||
if g["parent"] != None:
|
if g["parent"] != None:
|
||||||
g["parent"] = gid2kpath(g["parent"])
|
g["parent"] = gid2kpath(g["parent"])
|
||||||
|
|
||||||
new_path = self.keycloak.add_group(g["name"], g["parent"])
|
new_path_kc = self.keycloak.add_group(g["name"], g["parent"])
|
||||||
|
|
||||||
|
new_path : str = g["name"]
|
||||||
if g["parent"] != None:
|
if g["parent"] != None:
|
||||||
new_path = kpath2gid(new_path["path"])
|
new_path = kpath2gid(new_path_kc["path"])
|
||||||
else:
|
|
||||||
new_path = g["name"]
|
|
||||||
|
|
||||||
self.moodle.add_system_cohort(new_path, description=g["description"])
|
self.moodle.add_system_cohort(new_path, description=g["description"])
|
||||||
self.nextcloud.add_group(new_path)
|
self.nextcloud.add_group(new_path)
|
||||||
self.resync_data()
|
self.resync_data()
|
||||||
return new_path
|
return new_path
|
||||||
|
|
||||||
def delete_group_by_id(self, group_id):
|
def delete_group_by_id(self, group_id : str) -> None:
|
||||||
ev = Events("Deleting group", "Deleting from keycloak")
|
ev = Events(self.app, "Deleting group", "Deleting from keycloak")
|
||||||
try:
|
try:
|
||||||
keycloak_group = self.keycloak.get_group_by_id(group_id)
|
keycloak_group = self.keycloak.get_group_by_id(group_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -1904,7 +1914,7 @@ class Admin:
|
||||||
self.nextcloud.delete_group(sg_gid)
|
self.nextcloud.delete_group(sg_gid)
|
||||||
self.resync_data()
|
self.resync_data()
|
||||||
|
|
||||||
def delete_group_by_path(self, path):
|
def delete_group_by_path(self, path : str) -> None:
|
||||||
group = self.keycloak.get_group_by_path(path)
|
group = self.keycloak.get_group_by_path(path)
|
||||||
|
|
||||||
to_be_deleted = []
|
to_be_deleted = []
|
||||||
|
@ -1926,5 +1936,5 @@ class Admin:
|
||||||
self.nextcloud.delete_group(gid)
|
self.nextcloud.delete_group(gid)
|
||||||
self.resync_data()
|
self.resync_data()
|
||||||
|
|
||||||
def set_nextcloud_user_mail(self, data):
|
def set_nextcloud_user_mail(self, data : Any) -> None:
|
||||||
self.nextcloud.set_user_mail(data)
|
self.nextcloud.set_user_mail(data)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -23,10 +24,11 @@ import logging as log
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from flask import jsonify, request
|
from typing import Any, Dict, Union, List
|
||||||
|
|
||||||
from admin import app
|
from flask import request
|
||||||
|
|
||||||
|
# TODO: Improve these constants' structure
|
||||||
content_type = {"Content-Type": "application/json"}
|
content_type = {"Content-Type": "application/json"}
|
||||||
ex = {
|
ex = {
|
||||||
"bad_request": {
|
"bad_request": {
|
||||||
|
@ -96,8 +98,10 @@ ex = {
|
||||||
|
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
def __init__(self, error="bad_request", description="", debug="", data=None):
|
status_code : int
|
||||||
self.error = ex[error]["error"].copy()
|
content_type : Dict[str, str]
|
||||||
|
def __init__(self, error : str ="bad_request", description : str="", debug : Union[str, List[str]]="", data : Any =None):
|
||||||
|
self.error : Dict[str, str] = (ex[error]["error"]).copy() # type: ignore # bad struct
|
||||||
self.error["function"] = (
|
self.error["function"] = (
|
||||||
inspect.stack()[1][1].split(os.sep)[-1]
|
inspect.stack()[1][1].split(os.sep)[-1]
|
||||||
+ ":"
|
+ ":"
|
||||||
|
@ -123,7 +127,7 @@ class Error(Exception):
|
||||||
"----------- REQUEST START -----------",
|
"----------- REQUEST START -----------",
|
||||||
request.method + " " + request.url,
|
request.method + " " + request.url,
|
||||||
"\r\n".join("{}: {}".format(k, v) for k, v in request.headers.items()),
|
"\r\n".join("{}: {}".format(k, v) for k, v in request.headers.items()),
|
||||||
request.body if hasattr(request, "body") else "",
|
getattr(request, "body", ""),
|
||||||
"----------- REQUEST STOP -----------",
|
"----------- REQUEST STOP -----------",
|
||||||
)
|
)
|
||||||
if request
|
if request
|
||||||
|
@ -138,7 +142,7 @@ class Error(Exception):
|
||||||
if data
|
if data
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
self.status_code = ex[error]["status_code"]
|
self.status_code = ex[error]["status_code"] # type: ignore # bad struct
|
||||||
self.content_type = content_type
|
self.content_type = content_type
|
||||||
log.debug(
|
log.debug(
|
||||||
"%s - %s - [%s -> %s]\r\n%s\r\n%s\r\n%s"
|
"%s - %s - [%s -> %s]\r\n%s\r\n%s\r\n%s"
|
||||||
|
@ -152,11 +156,3 @@ class Error(Exception):
|
||||||
self.error["data"],
|
self.error["data"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(Error)
|
|
||||||
def handle_user_error(ex):
|
|
||||||
response = jsonify(ex.error)
|
|
||||||
response.status_code = ex.status_code
|
|
||||||
response.headers = {"content-type": content_type}
|
|
||||||
return response
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -26,11 +27,13 @@ from minio.commonconfig import REPLACE, CopySource
|
||||||
from minio.deleteobjects import DeleteObject
|
from minio.deleteobjects import DeleteObject
|
||||||
from requests import get, post
|
from requests import get, post
|
||||||
|
|
||||||
from admin import app
|
from typing import Any, Callable, Dict, Iterable, List
|
||||||
|
|
||||||
|
|
||||||
class Avatars:
|
class Avatars:
|
||||||
def __init__(self):
|
avatars_path : str
|
||||||
|
def __init__(self, avatars_path : str):
|
||||||
|
self.avatars_path = avatars_path
|
||||||
self.mclient = Minio(
|
self.mclient = Minio(
|
||||||
"dd-sso-avatars:9000",
|
"dd-sso-avatars:9000",
|
||||||
access_key="AKIAIOSFODNN7EXAMPLE",
|
access_key="AKIAIOSFODNN7EXAMPLE",
|
||||||
|
@ -41,21 +44,22 @@ class 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 : str, role : str="unknown") -> None:
|
||||||
|
path = os.path.join(self.avatars_path, role) + ".jpg",
|
||||||
self.mclient.fput_object(
|
self.mclient.fput_object(
|
||||||
self.bucket,
|
self.bucket,
|
||||||
userid,
|
userid,
|
||||||
os.path.join(app.root_path, "../custom/avatars/" + role + ".jpg"),
|
path,
|
||||||
content_type="image/jpeg ",
|
content_type="image/jpeg ",
|
||||||
)
|
)
|
||||||
log.warning(
|
log.warning(
|
||||||
" AVATARS: Updated avatar for user " + userid + " with role " + role
|
" AVATARS: Updated avatar for user " + userid + " with role " + role
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete_user_avatar(self, userid):
|
def delete_user_avatar(self, userid : str) -> None:
|
||||||
self.minio_delete_object(userid)
|
self.minio_delete_object(userid)
|
||||||
|
|
||||||
def update_missing_avatars(self, users):
|
def update_missing_avatars(self, users : Iterable[Dict[str, Any]]) -> None:
|
||||||
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:
|
||||||
|
@ -63,10 +67,11 @@ class Avatars:
|
||||||
except:
|
except:
|
||||||
img = "unknown.jpg"
|
img = "unknown.jpg"
|
||||||
|
|
||||||
|
path = os.path.join(self.avatars_path, img)
|
||||||
self.mclient.fput_object(
|
self.mclient.fput_object(
|
||||||
self.bucket,
|
self.bucket,
|
||||||
u["id"],
|
u["id"],
|
||||||
os.path.join(app.root_path, "../custom/avatars/" + img),
|
path,
|
||||||
content_type="image/jpeg ",
|
content_type="image/jpeg ",
|
||||||
)
|
)
|
||||||
log.warning(
|
log.warning(
|
||||||
|
@ -76,26 +81,24 @@ class Avatars:
|
||||||
+ img.split(".")[0]
|
+ img.split(".")[0]
|
||||||
)
|
)
|
||||||
|
|
||||||
def _minio_set_realm(self):
|
def _minio_set_realm(self) -> None:
|
||||||
if not self.mclient.bucket_exists(self.bucket):
|
if not self.mclient.bucket_exists(self.bucket):
|
||||||
self.mclient.make_bucket(self.bucket)
|
self.mclient.make_bucket(self.bucket)
|
||||||
|
|
||||||
def minio_get_objects(self):
|
def minio_get_objects(self) -> List[Any]:
|
||||||
return [o.object_name for o in self.mclient.list_objects(self.bucket)]
|
return [o.object_name for o in self.mclient.list_objects(self.bucket)]
|
||||||
|
|
||||||
def minio_delete_all_objects(self):
|
def minio_delete_all_objects(self) -> None:
|
||||||
delete_object_list = map(
|
f : Callable[[Any], Any] = lambda x: DeleteObject(x.object_name)
|
||||||
lambda x: DeleteObject(x.object_name),
|
delete_object_list = map(f, self.mclient.list_objects(self.bucket))
|
||||||
self.mclient.list_objects(self.bucket),
|
|
||||||
)
|
|
||||||
errors = self.mclient.remove_objects(self.bucket, delete_object_list)
|
errors = self.mclient.remove_objects(self.bucket, delete_object_list)
|
||||||
for error in errors:
|
for error in errors:
|
||||||
log.error(" AVATARS: Error occured when deleting avatar object: " + error)
|
log.error(" AVATARS: Error occured when deleting avatar object: " + error)
|
||||||
|
|
||||||
def minio_delete_object(self, oid):
|
def minio_delete_object(self, oid : str) -> None:
|
||||||
errors = self.mclient.remove_objects(self.bucket, [DeleteObject(oid)])
|
errors = self.mclient.remove_objects(self.bucket, [DeleteObject(oid)])
|
||||||
for error in errors:
|
for error in errors:
|
||||||
log.error(" AVATARS: Error occured when deleting avatar object: " + error)
|
log.error(" AVATARS: Error occured when deleting avatar object: " + error)
|
||||||
|
|
||||||
def get_users_without_image(self, users):
|
def get_users_without_image(self, users : Iterable[Dict[str, Any]]) -> Iterable[Dict[str, Any]]:
|
||||||
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,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -29,16 +30,22 @@ import yaml
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from schema import And, Optional, Schema, SchemaError, Use
|
from schema import And, Optional, Schema, SchemaError, Use
|
||||||
|
|
||||||
from admin import app
|
from typing import TYPE_CHECKING, Any, Dict
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from admin.flaskapp import AdminFlaskApp
|
||||||
|
|
||||||
|
from werkzeug import FileStorage
|
||||||
|
|
||||||
class Dashboard:
|
class Dashboard:
|
||||||
|
app : "AdminFlaskApp"
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
):
|
app : "AdminFlaskApp",
|
||||||
|
) -> None:
|
||||||
|
self.app = app
|
||||||
self.custom_menu = os.path.join(app.root_path, "../custom/menu/custom.yaml")
|
self.custom_menu = os.path.join(app.root_path, "../custom/menu/custom.yaml")
|
||||||
|
|
||||||
def _update_custom_menu(self, custom_menu_part):
|
def _update_custom_menu(self, custom_menu_part : Dict[str, Any]) -> bool:
|
||||||
with open(self.custom_menu) as yml:
|
with open(self.custom_menu) as yml:
|
||||||
menu = yaml.load(yml, Loader=yaml.FullLoader)
|
menu = yaml.load(yml, Loader=yaml.FullLoader)
|
||||||
menu = {**menu, **custom_menu_part}
|
menu = {**menu, **custom_menu_part}
|
||||||
|
@ -46,7 +53,7 @@ class Dashboard:
|
||||||
yml.write(yaml.dump(menu, default_flow_style=False))
|
yml.write(yaml.dump(menu, default_flow_style=False))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def update_colours(self, colours):
|
def update_colours(self, colours : Dict[str, Any]) -> bool:
|
||||||
schema_template = Schema(
|
schema_template = Schema(
|
||||||
{
|
{
|
||||||
"background": And(Use(str)),
|
"background": And(Use(str)),
|
||||||
|
@ -63,7 +70,7 @@ class Dashboard:
|
||||||
self._update_custom_menu({"colours": colours})
|
self._update_custom_menu({"colours": colours})
|
||||||
return self.apply_updates()
|
return self.apply_updates()
|
||||||
|
|
||||||
def update_menu(self, menu):
|
def update_menu(self, menu : Dict[str, Any]) -> bool:
|
||||||
items = []
|
items = []
|
||||||
for menu_item in menu.keys():
|
for menu_item in menu.keys():
|
||||||
for mustexist_key in ["href", "icon", "name", "shortname"]:
|
for mustexist_key in ["href", "icon", "name", "shortname"]:
|
||||||
|
@ -73,16 +80,16 @@ class Dashboard:
|
||||||
self._update_custom_menu({"apps_external": items})
|
self._update_custom_menu({"apps_external": items})
|
||||||
return self.apply_updates()
|
return self.apply_updates()
|
||||||
|
|
||||||
def update_logo(self, logo):
|
def update_logo(self, logo : FileStorage) -> bool:
|
||||||
img = Image.open(logo.stream)
|
img = Image.open(logo.stream)
|
||||||
img.save(os.path.join(app.root_path, "../custom/img/logo.png"))
|
img.save(os.path.join(self.app.root_path, "../custom/img/logo.png"))
|
||||||
return self.apply_updates()
|
return self.apply_updates()
|
||||||
|
|
||||||
def update_background(self, background):
|
def update_background(self, background : FileStorage) -> bool:
|
||||||
img = Image.open(background.stream)
|
img = Image.open(background.stream)
|
||||||
img.save(os.path.join(app.root_path, "../custom/img/background.png"))
|
img.save(os.path.join(self.app.root_path, "../custom/img/background.png"))
|
||||||
return self.apply_updates()
|
return self.apply_updates()
|
||||||
|
|
||||||
def apply_updates(self):
|
def apply_updates(self) -> bool:
|
||||||
resp = requests.get("http://dd-sso-api:7039/restart")
|
resp = requests.get("http://dd-sso-api:7039/restart")
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -38,34 +39,46 @@ from flask_socketio import (
|
||||||
send,
|
send,
|
||||||
)
|
)
|
||||||
|
|
||||||
from admin import app
|
from typing import TYPE_CHECKING, Any, Dict
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from admin.flaskapp import AdminFlaskApp
|
||||||
|
|
||||||
|
|
||||||
def sio_event_send(event, data):
|
def sio_event_send(app : "AdminFlaskApp", event : str, data : Dict[str, Any]) -> None:
|
||||||
app.socketio.emit(
|
app.socketio.emit(
|
||||||
event,
|
event,
|
||||||
json.dumps(data),
|
json.dumps(data),
|
||||||
namespace="/sio/events",
|
namespace="/sio/events",
|
||||||
room="events",
|
room="events",
|
||||||
)
|
)
|
||||||
|
# TODO: Why on earth do we find these all over the place?
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
|
|
||||||
class Events:
|
class Events:
|
||||||
def __init__(self, title, text="", total=0, table=False, type="info"):
|
app : "AdminFlaskApp"
|
||||||
|
eid : str
|
||||||
|
title : str
|
||||||
|
text : str
|
||||||
|
total : int
|
||||||
|
table : bool
|
||||||
|
type : str
|
||||||
|
def __init__(self, app : "AdminFlaskApp", title : str, text : str="", total : int=0, table : bool=False, type : str="info") -> None:
|
||||||
|
self.app = app
|
||||||
# notice, info, success, and error
|
# notice, info, success, and error
|
||||||
self.eid = str(base64.b64encode(os.urandom(32))[:8])
|
self.eid = str(base64.b64encode(os.urandom(32))[:8])
|
||||||
self.title = title
|
self.title = title
|
||||||
self.text = text
|
self.text = text
|
||||||
self.total = total
|
self.total = total
|
||||||
|
# TODO: this is probably replacing the .table method????
|
||||||
self.table = table
|
self.table = table
|
||||||
self.item = 0
|
self.item = 0
|
||||||
self.type = type
|
self.type = type
|
||||||
self.create()
|
self.create()
|
||||||
|
|
||||||
def create(self):
|
def create(self) -> None:
|
||||||
log.info("START " + self.eid + ": " + self.text)
|
log.info("START " + self.eid + ": " + self.text)
|
||||||
app.socketio.emit(
|
self.app.socketio.emit(
|
||||||
"notify-create",
|
"notify-create",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
|
@ -80,9 +93,9 @@ class Events:
|
||||||
)
|
)
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self) -> None:
|
||||||
log.info("END " + self.eid + ": " + self.text)
|
log.info("END " + self.eid + ": " + self.text)
|
||||||
app.socketio.emit(
|
self.app.socketio.emit(
|
||||||
"notify-destroy",
|
"notify-destroy",
|
||||||
json.dumps({"id": self.eid}),
|
json.dumps({"id": self.eid}),
|
||||||
namespace="/sio",
|
namespace="/sio",
|
||||||
|
@ -90,9 +103,9 @@ class Events:
|
||||||
)
|
)
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
def update_text(self, text):
|
def update_text(self, text : str) -> None:
|
||||||
self.text = text
|
self.text = text
|
||||||
app.socketio.emit(
|
self.app.socketio.emit(
|
||||||
"notify-update",
|
"notify-update",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
|
@ -105,9 +118,9 @@ class Events:
|
||||||
)
|
)
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
def append_text(self, text):
|
def append_text(self, text : str) -> None:
|
||||||
self.text = self.text + "<br>" + text
|
self.text = self.text + "<br>" + text
|
||||||
app.socketio.emit(
|
self.app.socketio.emit(
|
||||||
"notify-update",
|
"notify-update",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
|
@ -120,10 +133,10 @@ class Events:
|
||||||
)
|
)
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
def increment(self, data={"name": "", "data": []}):
|
def increment(self, data : Dict[str, Any]={"name": "", "data": []}) -> None:
|
||||||
self.item += 1
|
self.item += 1
|
||||||
log.info("INCREMENT " + self.eid + ": " + self.text)
|
log.info("INCREMENT " + self.eid + ": " + self.text)
|
||||||
app.socketio.emit(
|
self.app.socketio.emit(
|
||||||
"notify-increment",
|
"notify-increment",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
|
@ -149,10 +162,10 @@ class Events:
|
||||||
)
|
)
|
||||||
sleep(0.0001)
|
sleep(0.0001)
|
||||||
|
|
||||||
def decrement(self, data={"name": "", "data": []}):
|
def decrement(self, data : Dict[str, Any]={"name": "", "data": []}) -> None:
|
||||||
self.item -= 1
|
self.item -= 1
|
||||||
log.info("DECREMENT " + self.eid + ": " + self.text)
|
log.info("DECREMENT " + self.eid + ": " + self.text)
|
||||||
app.socketio.emit(
|
self.app.socketio.emit(
|
||||||
"notify-decrement",
|
"notify-decrement",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
|
@ -178,13 +191,13 @@ class Events:
|
||||||
)
|
)
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
|
|
||||||
def reload(self):
|
def reload(self) -> None:
|
||||||
app.socketio.emit("reload", json.dumps({}), namespace="/sio", room="admin")
|
self.app.socketio.emit("reload", json.dumps({}), namespace="/sio", room="admin")
|
||||||
sleep(0.0001)
|
sleep(0.0001)
|
||||||
|
|
||||||
def table(self, event, table, data={}):
|
def table(self, event : str, table : bool, data : Dict[str, Any]={}) -> None:
|
||||||
# refresh, add, delete, update
|
# refresh, add, delete, update
|
||||||
app.socketio.emit(
|
self.app.socketio.emit(
|
||||||
"table_" + event,
|
"table_" + event,
|
||||||
json.dumps({"table": table, "data": data}),
|
json.dumps({"table": table, "data": data}),
|
||||||
namespace="/sio",
|
namespace="/sio",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -22,8 +23,11 @@ import string
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
|
from typing import Any, Dict, Generator, Iterable, Optional, List
|
||||||
|
|
||||||
def get_recursive_groups(l_groups, l):
|
DDGroup = Dict[str, Any]
|
||||||
|
|
||||||
|
def get_recursive_groups(l_groups : Iterable[DDGroup], l : List[DDGroup]) -> List[DDGroup]:
|
||||||
for d_group in l_groups:
|
for d_group in l_groups:
|
||||||
data = {}
|
data = {}
|
||||||
for key, value in d_group.items():
|
for key, value in d_group.items():
|
||||||
|
@ -35,11 +39,11 @@ def get_recursive_groups(l_groups, l):
|
||||||
return l
|
return l
|
||||||
|
|
||||||
|
|
||||||
def get_group_with_childs(keycloak_group):
|
def get_group_with_childs(keycloak_group : DDGroup) -> List[str]:
|
||||||
return [g["path"] for g in get_recursive_groups([keycloak_group], [])]
|
return [g["path"] for g in get_recursive_groups([keycloak_group], [])]
|
||||||
|
|
||||||
|
|
||||||
def system_username(username):
|
def system_username(username : str) -> bool:
|
||||||
return (
|
return (
|
||||||
True
|
True
|
||||||
if username in ["guest", "ddadmin", "admin"] or username.startswith("system_")
|
if username in ["guest", "ddadmin", "admin"] or username.startswith("system_")
|
||||||
|
@ -47,41 +51,43 @@ def system_username(username):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def system_group(groupname):
|
def system_group(groupname : str) -> bool:
|
||||||
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 : str, groups : Iterable[DDGroup]) -> Optional[DDGroup]:
|
||||||
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_kid_from_kpath(kpath, groups):
|
def get_kid_from_kpath(kpath : str, groups : Iterable[DDGroup]) -> Optional[str]:
|
||||||
ids = [g["id"] for g in groups if g["path"] == kpath]
|
ids : List[str] = [g["id"] for g in groups if g["path"] == kpath]
|
||||||
if not len(ids) or len(ids) > 1:
|
if len(ids) != 1:
|
||||||
return False
|
return None
|
||||||
return ids[0]
|
return ids[0]
|
||||||
|
|
||||||
|
|
||||||
def get_gid_from_kgroup_id(kgroup_id, groups):
|
def get_gid_from_kgroup_id(kgroup_id : str, groups : Iterable[DDGroup]) -> str:
|
||||||
return [
|
# TODO: Why is this interface different from get_kid_from_kpath?
|
||||||
|
o : List[str] = [
|
||||||
g["path"].replace("/", ".")[1:] if len(g["path"].split("/")) else g["path"][1:]
|
g["path"].replace("/", ".")[1:] if len(g["path"].split("/")) else g["path"][1:]
|
||||||
for g in groups
|
for g in groups
|
||||||
if g["id"] == kgroup_id
|
if g["id"] == kgroup_id
|
||||||
][0]
|
]
|
||||||
|
return o[0]
|
||||||
|
|
||||||
|
|
||||||
def get_gids_from_kgroup_ids(kgroup_ids, groups):
|
def get_gids_from_kgroup_ids(kgroup_ids : Iterable[str], groups : Iterable[DDGroup]) -> List[str]:
|
||||||
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 : str) -> str:
|
||||||
# print(path.replace('/','.')[1:])
|
# print(path.replace('/','.')[1:])
|
||||||
if path.startswith("/"):
|
if path.startswith("/"):
|
||||||
return path.replace("/", ".")[1:]
|
return path.replace("/", ".")[1:]
|
||||||
return path.replace("/", ".")
|
return path.replace("/", ".")
|
||||||
|
|
||||||
|
|
||||||
def kpath2gids(path):
|
def kpath2gids(path : str) -> List[str]:
|
||||||
path = kpath2gid(path)
|
path = kpath2gid(path)
|
||||||
l = []
|
l = []
|
||||||
for i in range(len(path.split("."))):
|
for i in range(len(path.split("."))):
|
||||||
|
@ -89,44 +95,45 @@ def kpath2gids(path):
|
||||||
return l
|
return l
|
||||||
|
|
||||||
|
|
||||||
def kpath2kpaths(path):
|
def kpath2kpaths(path : str) -> List[str]:
|
||||||
l = []
|
l = []
|
||||||
for i in range(len(path.split("/"))):
|
for i in range(len(path.split("/"))):
|
||||||
l.append("/".join(path.split("/")[: i + 1]))
|
l.append("/".join(path.split("/")[: i + 1]))
|
||||||
return l[1:]
|
return l[1:]
|
||||||
|
|
||||||
|
|
||||||
def gid2kpath(gid):
|
def gid2kpath(gid : str) -> str:
|
||||||
return "/" + gid.replace(".", "/")
|
return "/" + gid.replace(".", "/")
|
||||||
|
|
||||||
|
|
||||||
def count_repeated(itemslist):
|
def count_repeated(itemslist : Iterable[Any]) -> None:
|
||||||
print(Counter(itemslist))
|
print(Counter(itemslist))
|
||||||
|
|
||||||
|
|
||||||
def groups_kname2gid(groups):
|
def groups_kname2gid(groups : Iterable[str]) -> List[str]:
|
||||||
return [name.replace(".", "/") for name in groups]
|
return [name.replace(".", "/") for name in groups]
|
||||||
|
|
||||||
|
|
||||||
def groups_path2id(groups):
|
def groups_path2id(groups : Iterable[str]) -> List[str]:
|
||||||
return [g.replace("/", ".")[1:] for g in groups]
|
return [g.replace("/", ".")[1:] for g in groups]
|
||||||
|
|
||||||
|
|
||||||
def groups_id2path(groups):
|
def groups_id2path(groups : Iterable[str]) -> List[str]:
|
||||||
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 : Iterable[str]) -> List[str]:
|
||||||
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 : Iterable[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
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 : int) -> str:
|
||||||
|
# TODO: why is this not using py3's secrets?
|
||||||
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):
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -34,23 +35,33 @@ from .api_exceptions import Error
|
||||||
from .helpers import get_recursive_groups, kpath2kpaths
|
from .helpers import get_recursive_groups, kpath2kpaths
|
||||||
from .postgres import Postgres
|
from .postgres import Postgres
|
||||||
|
|
||||||
# from admin import app
|
from typing import cast, Any, Dict, Iterable, List, Optional
|
||||||
|
|
||||||
|
DDUser = Dict[str, Any]
|
||||||
|
|
||||||
|
# TODO: Improve typing of these class and simplify it
|
||||||
|
|
||||||
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
|
||||||
"""
|
"""
|
||||||
|
url : str
|
||||||
|
username : str
|
||||||
|
password : str
|
||||||
|
realm : str
|
||||||
|
verify : bool
|
||||||
|
keycloak_pg : Postgres
|
||||||
|
keycloak_admin : KeycloakAdmin
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
url="http://dd-sso-keycloak:8080/auth/",
|
url : str="http://dd-sso-keycloak:8080/auth/",
|
||||||
username=os.environ["KEYCLOAK_USER"],
|
username : str=os.environ["KEYCLOAK_USER"],
|
||||||
password=os.environ["KEYCLOAK_PASSWORD"],
|
password : str=os.environ["KEYCLOAK_PASSWORD"],
|
||||||
realm="master",
|
realm : str="master",
|
||||||
verify=True,
|
verify : bool=True,
|
||||||
):
|
) -> None:
|
||||||
self.url = url
|
self.url = url
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
|
@ -64,7 +75,7 @@ class KeycloakClient:
|
||||||
os.environ["KEYCLOAK_DB_PASSWORD"],
|
os.environ["KEYCLOAK_DB_PASSWORD"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def connect(self):
|
def connect(self) -> None:
|
||||||
self.keycloak_admin = KeycloakAdmin(
|
self.keycloak_admin = KeycloakAdmin(
|
||||||
server_url=self.url,
|
server_url=self.url,
|
||||||
username=self.username,
|
username=self.username,
|
||||||
|
@ -78,15 +89,19 @@ class KeycloakClient:
|
||||||
|
|
||||||
""" USERS """
|
""" USERS """
|
||||||
|
|
||||||
def get_user_id(self, username):
|
def get_user_id(self, username : str) -> str:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.get_user_id(username)
|
uid : str = self.keycloak_admin.get_user_id(username)
|
||||||
|
return uid
|
||||||
|
|
||||||
def get_users(self):
|
def get_users(self) -> Iterable[Dict[str, Any]]:
|
||||||
|
# https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userrepresentation
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.get_users({})
|
o : Iterable[Dict[str, Any]] = self.keycloak_admin.get_users({})
|
||||||
|
return o
|
||||||
|
|
||||||
def get_users_with_groups_and_roles(self):
|
# TODO: what is this actually doing?
|
||||||
|
def get_users_with_groups_and_roles(self) -> List[DDUser]:
|
||||||
q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, u.enabled, ua.value as quota
|
q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, u.enabled, ua.value as quota
|
||||||
,json_agg(g."id") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2
|
,json_agg(g."id") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2
|
||||||
,json_agg(r.name) as role
|
,json_agg(r.name) as role
|
||||||
|
@ -125,7 +140,7 @@ class KeycloakClient:
|
||||||
|
|
||||||
return list_dict_users
|
return list_dict_users
|
||||||
|
|
||||||
def getparent(self, group_id, data):
|
def getparent(self, group_id : str, data : Iterable[Any]) -> str:
|
||||||
# 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:
|
||||||
|
@ -134,14 +149,14 @@ class KeycloakClient:
|
||||||
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 : str) -> str:
|
||||||
# Get full path using getparent recursive func
|
# Get full path using getparent recursive func
|
||||||
# RETURNS: String with full path
|
# RETURNS: String with full path
|
||||||
q = """SELECT * FROM keycloak_group"""
|
q = """SELECT * FROM keycloak_group"""
|
||||||
groups = self.keycloak_pg.select(q)
|
groups = self.keycloak_pg.select(q)
|
||||||
return self.getparent(group_id, groups)
|
return self.getparent(group_id, groups)
|
||||||
|
|
||||||
def get_user_groups_paths(self, user_id):
|
def get_user_groups_paths(self, user_id : str) -> List[str]:
|
||||||
# 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'""" % (
|
q = """SELECT group_id FROM user_group_membership WHERE user_id = '%s'""" % (
|
||||||
|
@ -165,20 +180,20 @@ class KeycloakClient:
|
||||||
|
|
||||||
def add_user(
|
def add_user(
|
||||||
self,
|
self,
|
||||||
username,
|
username : str,
|
||||||
first,
|
first : str,
|
||||||
last,
|
last : str,
|
||||||
email,
|
email : str,
|
||||||
password,
|
password : str,
|
||||||
group=False,
|
group : Any=False,
|
||||||
password_temporary=True,
|
password_temporary : bool=True,
|
||||||
enabled=True,
|
enabled : bool=True,
|
||||||
):
|
) -> Any:
|
||||||
# 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(
|
uid : Any = self.keycloak_admin.create_user(
|
||||||
{
|
{
|
||||||
"email": email,
|
"email": email,
|
||||||
"username": username,
|
"username": username,
|
||||||
|
@ -213,7 +228,7 @@ class KeycloakClient:
|
||||||
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, password_temporary=True):
|
def update_user_pwd(self, user_id : str, password : str, password_temporary : bool=True) -> Any:
|
||||||
# Updates
|
# Updates
|
||||||
payload = {
|
payload = {
|
||||||
"credentials": [
|
"credentials": [
|
||||||
|
@ -223,7 +238,7 @@ class KeycloakClient:
|
||||||
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 : str, enabled : bool, email : str, first : str, last : str, groups : Iterable[str]=[], roles : Iterable[str]=[]) -> Any:
|
||||||
## 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 = {
|
payload = {
|
||||||
|
@ -237,17 +252,17 @@ class KeycloakClient:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.update_user(user_id, payload)
|
return self.keycloak_admin.update_user(user_id, payload)
|
||||||
|
|
||||||
def user_enable(self, user_id):
|
def user_enable(self, user_id : str) -> Any:
|
||||||
payload = {"enabled": True}
|
payload = {"enabled": True}
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.update_user(user_id, payload)
|
return self.keycloak_admin.update_user(user_id, payload)
|
||||||
|
|
||||||
def user_disable(self, user_id):
|
def user_disable(self, user_id : str) -> Any:
|
||||||
payload = {"enabled": False}
|
payload = {"enabled": False}
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.update_user(user_id, payload)
|
return self.keycloak_admin.update_user(user_id, payload)
|
||||||
|
|
||||||
def group_user_remove(self, user_id, group_id):
|
def group_user_remove(self, user_id : str, group_id : str) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.group_user_remove(user_id, group_id)
|
return self.keycloak_admin.group_user_remove(user_id, group_id)
|
||||||
|
|
||||||
|
@ -255,7 +270,7 @@ class KeycloakClient:
|
||||||
# self.connect()
|
# self.connect()
|
||||||
# return self.keycloak_admin.assign_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test")
|
# return self.keycloak_admin.assign_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test")
|
||||||
|
|
||||||
def remove_user_realm_roles(self, user_id, roles):
|
def remove_user_realm_roles(self, user_id : str, roles : Iterable[str]) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
roles = [
|
roles = [
|
||||||
r
|
r
|
||||||
|
@ -264,66 +279,66 @@ class KeycloakClient:
|
||||||
]
|
]
|
||||||
return self.keycloak_admin.delete_user_realm_role(user_id, roles)
|
return self.keycloak_admin.delete_user_realm_role(user_id, roles)
|
||||||
|
|
||||||
def delete_user(self, userid):
|
def delete_user(self, userid : str) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.delete_user(user_id=userid)
|
return self.keycloak_admin.delete_user(user_id=userid)
|
||||||
|
|
||||||
def get_user_groups(self, userid):
|
def get_user_groups(self, userid : str) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.get_user_groups(user_id=userid)
|
return self.keycloak_admin.get_user_groups(user_id=userid)
|
||||||
|
|
||||||
def get_user_realm_roles(self, userid):
|
def get_user_realm_roles(self, userid : str) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.get_realm_roles_of_user(user_id=userid)
|
return self.keycloak_admin.get_realm_roles_of_user(user_id=userid)
|
||||||
|
|
||||||
def add_user_client_role(self, client_id, user_id, role_id, role_name):
|
def add_user_client_role(self, client_id : str, user_id : str, role_id : str, role_name : str) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.assign_client_role(
|
return self.keycloak_admin.assign_client_role(
|
||||||
client_id=client_id, user_id=user_id, role_id=role_id, role_name="test"
|
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) -> Iterable[Any]:
|
||||||
## RETURNS ONLY MAIN GROUPS WITH NESTED subGroups list
|
## RETURNS ONLY MAIN GROUPS WITH NESTED subGroups list
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.get_groups()
|
return cast(Iterable[Any], self.keycloak_admin.get_groups())
|
||||||
|
|
||||||
def get_groups(self, with_subgroups=True):
|
def get_groups(self, with_subgroups : bool=True) -> Iterable[Any]:
|
||||||
## RETURNS ALL GROUPS in root list
|
## RETURNS ALL GROUPS in root list
|
||||||
self.connect()
|
self.connect()
|
||||||
groups = self.keycloak_admin.get_groups()
|
groups = self.keycloak_admin.get_groups()
|
||||||
return get_recursive_groups(groups, [])
|
return get_recursive_groups(groups, [])
|
||||||
|
|
||||||
def get_group_by_id(self, group_id):
|
def get_group_by_id(self, group_id : str) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.get_group(group_id=group_id)
|
return self.keycloak_admin.get_group(group_id=group_id)
|
||||||
|
|
||||||
def get_group_by_path(self, path, recursive=True):
|
def get_group_by_path(self, path : str, recursive : bool=True) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.get_group_by_path(
|
return self.keycloak_admin.get_group_by_path(
|
||||||
path=path, search_in_subgroups=recursive
|
path=path, search_in_subgroups=recursive
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_group(self, name, parent=None, skip_exists=False):
|
def add_group(self, name : str, parent : str="", skip_exists : bool=False) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
if parent != None:
|
if parent:
|
||||||
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 : str) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.delete_group(group_id=group_id)
|
return self.keycloak_admin.delete_group(group_id=group_id)
|
||||||
|
|
||||||
def group_user_add(self, user_id, group_id):
|
def group_user_add(self, user_id : str, group_id : str) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.group_user_add(user_id, group_id)
|
return self.keycloak_admin.group_user_add(user_id, group_id)
|
||||||
|
|
||||||
def add_group_tree(self, path):
|
def add_group_tree(self, path : str) -> None:
|
||||||
paths = kpath2kpaths(path)
|
paths = kpath2kpaths(path)
|
||||||
parent = "/"
|
parent = "/"
|
||||||
for path in paths:
|
for path in paths:
|
||||||
try:
|
try:
|
||||||
parent_path = None if parent == "/" else parent
|
parent_path = "" if parent == "/" else parent
|
||||||
# print("parent: "+str(parent_path)+" path: "+path.split("/")[-1])
|
# print("parent: "+str(parent_path)+" path: "+path.split("/")[-1])
|
||||||
self.add_group(path.split("/")[-1], parent_path, skip_exists=True)
|
self.add_group(path.split("/")[-1], parent_path, skip_exists=True)
|
||||||
parent = path
|
parent = path
|
||||||
|
@ -333,8 +348,8 @@ class KeycloakClient:
|
||||||
parent = path
|
parent = path
|
||||||
|
|
||||||
def add_user_with_groups_and_role(
|
def add_user_with_groups_and_role(
|
||||||
self, username, first, last, email, password, role, groups
|
self, username : str, first : str, last : str, email : str, password : str, role : str, groups : Iterable[str]
|
||||||
):
|
) -> None:
|
||||||
## 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
|
||||||
|
@ -348,7 +363,7 @@ class KeycloakClient:
|
||||||
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 = ""
|
||||||
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:
|
||||||
|
@ -360,9 +375,6 @@ class KeycloakClient:
|
||||||
+ " already exists. Skipping creation"
|
+ " already exists. Skipping creation"
|
||||||
)
|
)
|
||||||
pass
|
pass
|
||||||
if parent_path is None:
|
|
||||||
thepath = "/" + parts[i]
|
|
||||||
else:
|
|
||||||
thepath = parent_path + "/" + parts[i]
|
thepath = parent_path + "/" + parts[i]
|
||||||
if thepath == "/":
|
if thepath == "/":
|
||||||
log.warning(
|
log.warning(
|
||||||
|
@ -385,53 +397,51 @@ class KeycloakClient:
|
||||||
)
|
)
|
||||||
self.keycloak_admin.group_user_add(uid, gid)
|
self.keycloak_admin.group_user_add(uid, gid)
|
||||||
|
|
||||||
if parent_path == None:
|
parent_path += "/" + parts[i]
|
||||||
parent_path = ""
|
|
||||||
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) -> Iterable[Any]:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.get_realm_roles()
|
return cast(Iterable[Any], self.keycloak_admin.get_realm_roles())
|
||||||
|
|
||||||
def get_role(self, name):
|
def get_role(self, name : str) -> Any:
|
||||||
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 : str, description : str="") -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.create_realm_role(
|
return self.keycloak_admin.create_realm_role(
|
||||||
{"name": name, "description": description}
|
{"name": name, "description": description}
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete_role(self, name):
|
def delete_role(self, name : str) -> Any:
|
||||||
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 : str) -> Any:
|
||||||
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 : str, name : str, description : str="") -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.create_client_role(
|
return self.keycloak_admin.create_client_role(
|
||||||
client_id, {"name": name, "description": description, "clientRole": True}
|
client_id, {"name": name, "description": description, "clientRole": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
## SYSTEM
|
## SYSTEM
|
||||||
def get_server_info(self):
|
def get_server_info(self) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.get_server_info()
|
return self.keycloak_admin.get_server_info()
|
||||||
|
|
||||||
def get_server_clients(self):
|
def get_server_clients(self) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.get_clients()
|
return self.keycloak_admin.get_clients()
|
||||||
|
|
||||||
def get_server_rsa_key(self):
|
def get_server_rsa_key(self) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
rsa_key = [
|
rsa_key = [
|
||||||
k for k in self.keycloak_admin.get_keys()["keys"] if k["type"] == "RSA"
|
k for k in self.keycloak_admin.get_keys()["keys"] if k["type"] == "RSA"
|
||||||
|
@ -439,22 +449,21 @@ class KeycloakClient:
|
||||||
return {"name": rsa_key["kid"], "certificate": rsa_key["certificate"]}
|
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 : str, role : str) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
try:
|
try:
|
||||||
role = [
|
kcroles = [
|
||||||
r for r in self.keycloak_admin.get_realm_roles() if r["name"] == 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=kcroles)
|
||||||
# return self.keycloak_admin.assign_realm_roles(user_id=user_id, client_id=None, roles=role)
|
|
||||||
|
|
||||||
## CLIENTS
|
## CLIENTS
|
||||||
def delete_client(self, clientid):
|
def delete_client(self, clientid : str) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.delete_client(clientid)
|
return self.keycloak_admin.delete_client(clientid)
|
||||||
|
|
||||||
def add_client(self, client):
|
def add_client(self, client : str) -> Any:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self.keycloak_admin.create_client(client)
|
return self.keycloak_admin.create_client(client)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -21,7 +22,6 @@ import logging as log
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from admin import app
|
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
from minio import Minio
|
from minio import Minio
|
||||||
|
@ -29,18 +29,22 @@ from minio.commonconfig import REPLACE, CopySource
|
||||||
from minio.deleteobjects import DeleteObject
|
from minio.deleteobjects import DeleteObject
|
||||||
from requests import get, post
|
from requests import get, post
|
||||||
|
|
||||||
legal_path= os.path.join(app.root_path, "static/templates/pages/legal/")
|
from typing import TYPE_CHECKING
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from admin.flaskapp import AdminFlaskApp
|
||||||
|
|
||||||
def get_legal(lang):
|
|
||||||
with open(legal_path+lang, "r") as languagefile:
|
# TODO: Fix all this
|
||||||
|
def get_legal(app : "AdminFlaskApp", lang : str) -> str:
|
||||||
|
with open(app.legal_path+lang, "r") as languagefile:
|
||||||
return languagefile.read()
|
return languagefile.read()
|
||||||
|
|
||||||
def gen_legal_if_not_exists(lang):
|
def gen_legal_if_not_exists(app : "AdminFlaskApp", lang : str) -> None:
|
||||||
if not os.path.isfile(legal_path+lang):
|
if not os.path.isfile(app.legal_path+lang):
|
||||||
log.debug("Creating new language file")
|
log.debug("Creating new language file")
|
||||||
with open(legal_path+lang, "w") as languagefile:
|
with open(app.legal_path+lang, "w") as languagefile:
|
||||||
languagefile.write("<b>Legal</b><br>This is the default legal page for language " + lang)
|
languagefile.write("<b>Legal</b><br>This is the default legal page for language " + lang)
|
||||||
|
|
||||||
def new_legal(lang,html):
|
def new_legal(app : "AdminFlaskApp", lang : str, html : str) -> None:
|
||||||
with open(legal_path+lang, "w") as languagefile:
|
with open(app.legal_path+lang, "w") as languagefile:
|
||||||
languagefile.write(html)
|
languagefile.write(html)
|
|
@ -1,94 +0,0 @@
|
||||||
#
|
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
|
||||||
#
|
|
||||||
# This file is part of DD
|
|
||||||
#
|
|
||||||
# DD is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
# option) any later version.
|
|
||||||
#
|
|
||||||
# DD is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with DD. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
|
|
||||||
import logging as log
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
from cerberus import Validator, rules_set_registry, schema_registry
|
|
||||||
|
|
||||||
from admin import app
|
|
||||||
|
|
||||||
|
|
||||||
class AdminValidator(Validator):
|
|
||||||
None
|
|
||||||
# def _normalize_default_setter_genid(self, document):
|
|
||||||
# return _parse_string(document["name"])
|
|
||||||
|
|
||||||
# def _normalize_default_setter_genidlower(self, document):
|
|
||||||
# return _parse_string(document["name"]).lower()
|
|
||||||
|
|
||||||
# def _normalize_default_setter_gengroupid(self, document):
|
|
||||||
# return _parse_string(
|
|
||||||
# document["parent_category"] + "-" + document["uid"]
|
|
||||||
# ).lower()
|
|
||||||
|
|
||||||
|
|
||||||
def load_validators(purge_unknown=True):
|
|
||||||
validators = {}
|
|
||||||
schema_path = os.path.join(app.root_path, "schemas")
|
|
||||||
for schema_filename in os.listdir(schema_path):
|
|
||||||
try:
|
|
||||||
with open(os.path.join(schema_path, schema_filename)) as file:
|
|
||||||
schema_yml = file.read()
|
|
||||||
schema = yaml.load(schema_yml, Loader=yaml.FullLoader)
|
|
||||||
validators[schema_filename.split(".")[0]] = AdminValidator(
|
|
||||||
schema, purge_unknown=purge_unknown
|
|
||||||
)
|
|
||||||
except IsADirectoryError:
|
|
||||||
None
|
|
||||||
return validators
|
|
||||||
|
|
||||||
|
|
||||||
app.validators = load_validators()
|
|
||||||
|
|
||||||
|
|
||||||
class loadConfig:
|
|
||||||
def __init__(self, app=None):
|
|
||||||
try:
|
|
||||||
app.config.setdefault("DOMAIN", os.environ["DOMAIN"])
|
|
||||||
app.config.setdefault(
|
|
||||||
"KEYCLOAK_POSTGRES_USER", os.environ["KEYCLOAK_DB_USER"]
|
|
||||||
)
|
|
||||||
app.config.setdefault(
|
|
||||||
"KEYCLOAK_POSTGRES_PASSWORD", os.environ["KEYCLOAK_DB_PASSWORD"]
|
|
||||||
)
|
|
||||||
app.config.setdefault(
|
|
||||||
"MOODLE_POSTGRES_USER", os.environ["MOODLE_POSTGRES_USER"]
|
|
||||||
)
|
|
||||||
app.config.setdefault(
|
|
||||||
"MOODLE_POSTGRES_PASSWORD", os.environ["MOODLE_POSTGRES_PASSWORD"]
|
|
||||||
)
|
|
||||||
app.config.setdefault(
|
|
||||||
"NEXTCLOUD_POSTGRES_USER", os.environ["NEXTCLOUD_POSTGRES_USER"]
|
|
||||||
)
|
|
||||||
app.config.setdefault(
|
|
||||||
"NEXTCLOUD_POSTGRES_PASSWORD", os.environ["NEXTCLOUD_POSTGRES_PASSWORD"]
|
|
||||||
)
|
|
||||||
app.config.setdefault(
|
|
||||||
"VERIFY", True if os.environ["VERIFY"] == "true" else False
|
|
||||||
)
|
|
||||||
app.config.setdefault("API_SECRET", os.environ.get("API_SECRET"))
|
|
||||||
except Exception as e:
|
|
||||||
log.error(traceback.format_exc())
|
|
||||||
raise
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -23,11 +24,15 @@ from pprint import pprint
|
||||||
|
|
||||||
from requests import get, post
|
from requests import get, post
|
||||||
|
|
||||||
from admin import app
|
|
||||||
|
|
||||||
from .exceptions import UserExists, UserNotFound
|
from .exceptions import UserExists, UserNotFound
|
||||||
from .postgres import Postgres
|
from .postgres import Postgres
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, cast, Any, Dict, Iterable, List, Optional
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from admin.flaskapp import AdminFlaskApp
|
||||||
|
|
||||||
# Module variables to connect to moodle api
|
# Module variables to connect to moodle api
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,18 +41,20 @@ class Moodle:
|
||||||
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
|
||||||
"""
|
"""
|
||||||
|
key: str
|
||||||
|
url : str
|
||||||
|
endpoint : str
|
||||||
|
verify : bool
|
||||||
|
moodle_pg : Postgres
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
key=app.config["MOODLE_WS_TOKEN"],
|
app : "AdminFlaskApp",
|
||||||
url="https://moodle." + app.config["DOMAIN"],
|
endpoint : str="/webservice/rest/server.php",
|
||||||
endpoint="/webservice/rest/server.php",
|
) -> None:
|
||||||
verify=app.config["VERIFY"],
|
self.key = app.config["MOODLE_WS_TOKEN"]
|
||||||
):
|
self.url = f"https://moodle.{ app.config['DOMAIN'] }"
|
||||||
self.key = key
|
|
||||||
self.url = url
|
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
self.verify = verify
|
self.verify = cast(bool, app.config["VERIFY"])
|
||||||
|
|
||||||
self.moodle_pg = Postgres(
|
self.moodle_pg = Postgres(
|
||||||
"dd-apps-postgresql",
|
"dd-apps-postgresql",
|
||||||
|
@ -56,7 +63,7 @@ class Moodle:
|
||||||
app.config["MOODLE_POSTGRES_PASSWORD"],
|
app.config["MOODLE_POSTGRES_PASSWORD"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def rest_api_parameters(self, in_args, prefix="", out_dict=None):
|
def rest_api_parameters(self, in_args : Any, prefix : str="", out_dict : Optional[Dict]=None) -> Dict[Any, Any]:
|
||||||
"""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:
|
||||||
|
@ -64,24 +71,23 @@ class Moodle:
|
||||||
{'courses[0][id]':1,
|
{'courses[0][id]':1,
|
||||||
'courses[0][name]':'course1'}
|
'courses[0][name]':'course1'}
|
||||||
"""
|
"""
|
||||||
if out_dict == None:
|
o : Dict[Any, Any] = {} if out_dict is None else out_dict
|
||||||
out_dict = {}
|
|
||||||
if not type(in_args) in (list, dict):
|
if not type(in_args) in (list, dict):
|
||||||
out_dict[prefix] = in_args
|
o[prefix] = in_args
|
||||||
return out_dict
|
return o
|
||||||
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), o)
|
||||||
elif type(in_args) == dict:
|
elif type(in_args) == dict:
|
||||||
for key, item in in_args.items():
|
for key, item in in_args.items():
|
||||||
self.rest_api_parameters(item, prefix.format(key), out_dict)
|
self.rest_api_parameters(item, prefix.format(key), o)
|
||||||
return out_dict
|
return o
|
||||||
|
|
||||||
def call(self, fname, **kwargs):
|
def call(self, fname : str, **kwargs : Any) -> Any:
|
||||||
"""Calls moodle API function with function name fname and keyword arguments.
|
"""Calls moodle API function with function name fname and keyword arguments.
|
||||||
Example:
|
Example:
|
||||||
>>> call_mdl_function('core_course_update_courses',
|
>>> call_mdl_function('core_course_update_courses',
|
||||||
|
@ -97,7 +103,7 @@ class Moodle:
|
||||||
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 : str, username : str, password : str, first_name : str="-", last_name : str="-") -> Any:
|
||||||
if len(self.get_user_by("username", username)["users"]):
|
if len(self.get_user_by("username", username)["users"]):
|
||||||
raise UserExists
|
raise UserExists
|
||||||
try:
|
try:
|
||||||
|
@ -115,7 +121,7 @@ class Moodle:
|
||||||
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 : str, email : str, first_name : str, last_name : str, enabled : bool=True) -> Any:
|
||||||
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
|
||||||
|
@ -135,15 +141,15 @@ class Moodle:
|
||||||
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 : str) -> Any:
|
||||||
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 : List[str]) -> Any:
|
||||||
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 : str, value : str) -> Any:
|
||||||
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)
|
||||||
|
@ -152,7 +158,7 @@ class Moodle:
|
||||||
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) -> List[Dict[Any, Any]]:
|
||||||
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
|
||||||
LEFT JOIN mdl_cohort_members AS hm on hm.userid = u.id
|
LEFT JOIN mdl_cohort_members AS hm on hm.userid = u.id
|
||||||
|
@ -179,31 +185,31 @@ class Moodle:
|
||||||
# user['roles']=[]
|
# user['roles']=[]
|
||||||
# return users
|
# return users
|
||||||
|
|
||||||
def enroll_user_to_course(self, user_id, course_id, role_id=5):
|
def enroll_user_to_course(self, user_id : str, course_id : str, role_id : int=5) -> Any:
|
||||||
# 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 : str, user_id : str) -> Any:
|
||||||
attempts = self.call(
|
attempts = self.call(
|
||||||
"mod_quiz_get_user_attempts", quizid=quiz_id, userid=user_id
|
"mod_quiz_get_user_attempts", quizid=quiz_id, userid=user_id
|
||||||
)
|
)
|
||||||
return attempts
|
return attempts
|
||||||
|
|
||||||
def get_cohorts(self):
|
def get_cohorts(self) -> Any:
|
||||||
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 : str, description : str ="", visible : bool=True) -> Any:
|
||||||
visible = 1 if visible else 0
|
bit_visible = 1 if visible else 0
|
||||||
data = [
|
data = [
|
||||||
{
|
{
|
||||||
"categorytype": {"type": "system", "value": ""},
|
"categorytype": {"type": "system", "value": ""},
|
||||||
"name": name,
|
"name": name,
|
||||||
"idnumber": name,
|
"idnumber": name,
|
||||||
"description": description,
|
"description": description,
|
||||||
"visible": visible,
|
"visible": bit_visible,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
cohort = self.call("core_cohort_create_cohorts", cohorts=data)
|
cohort = self.call("core_cohort_create_cohorts", cohorts=data)
|
||||||
|
@ -214,7 +220,7 @@ class Moodle:
|
||||||
# user = self.call('core_cohort_add_cohort_members', criteria=criteria)
|
# user = self.call('core_cohort_add_cohort_members', criteria=criteria)
|
||||||
# return user
|
# return user
|
||||||
|
|
||||||
def add_user_to_cohort(self, userid, cohortid):
|
def add_user_to_cohort(self, userid : str, cohortid : str) -> Any:
|
||||||
members = [
|
members = [
|
||||||
{
|
{
|
||||||
"cohorttype": {"type": "id", "value": cohortid},
|
"cohorttype": {"type": "id", "value": cohortid},
|
||||||
|
@ -224,21 +230,21 @@ class Moodle:
|
||||||
user = self.call("core_cohort_add_cohort_members", members=members)
|
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 : str, cohortid : str) -> Any:
|
||||||
members = [{"cohortid": cohortid, "userid": userid}]
|
members = [{"cohortid": cohortid, "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 : str) -> Any:
|
||||||
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 : Iterable[str]) -> Any:
|
||||||
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 : str) -> Any:
|
||||||
user_cohorts = []
|
user_cohorts = []
|
||||||
cohorts = self.get_cohorts()
|
cohorts = self.get_cohorts()
|
||||||
for cohort in cohorts:
|
for cohort in cohorts:
|
||||||
|
@ -246,7 +252,7 @@ class Moodle:
|
||||||
user_cohorts.append(cohort)
|
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 : str) -> Any:
|
||||||
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:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -18,32 +19,29 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import json
|
|
||||||
import logging as log
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
import yaml
|
|
||||||
|
|
||||||
# from admin import app
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
|
||||||
class Mysql:
|
class Mysql:
|
||||||
def __init__(self, host, database, user, password):
|
# TODO: Fix this whole class
|
||||||
|
cur : mysql.connector.MySQLCursor
|
||||||
|
conn : mysql.connector.MySQLConnection
|
||||||
|
def __init__(self, host : str, database : str, user : str, password : str) -> None:
|
||||||
self.conn = mysql.connector.connect(
|
self.conn = mysql.connector.connect(
|
||||||
host=host, database=database, user=user, password=password
|
host=host, database=database, user=user, password=password
|
||||||
)
|
)
|
||||||
|
|
||||||
def select(self, sql):
|
def select(self, sql : str) -> List[Tuple]:
|
||||||
self.cur = self.conn.cursor()
|
self.cur = self.conn.cursor()
|
||||||
self.cur.execute(sql)
|
self.cur.execute(sql)
|
||||||
data = self.cur.fetchall()
|
data : List[Tuple] = self.cur.fetchall()
|
||||||
self.cur.close()
|
self.cur.close()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def update(self, sql):
|
def update(self, sql : str) -> None:
|
||||||
|
# TODO: Fix this whole method
|
||||||
self.cur = self.conn.cursor()
|
self.cur = self.conn.cursor()
|
||||||
self.cur.execute(sql)
|
self.cur.execute(sql)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -30,21 +31,31 @@ import urllib
|
||||||
import requests
|
import requests
|
||||||
from psycopg2 import sql
|
from psycopg2 import sql
|
||||||
|
|
||||||
# from ..lib.log import *
|
|
||||||
from admin import app
|
|
||||||
|
|
||||||
from .nextcloud_exc import *
|
from .nextcloud_exc import *
|
||||||
from .postgres import Postgres
|
from .postgres import Postgres
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from admin.flaskapp import AdminFlaskApp
|
||||||
|
|
||||||
|
DDUser = Dict[Any, Any]
|
||||||
|
|
||||||
class Nextcloud:
|
class Nextcloud:
|
||||||
|
verify_cert : bool
|
||||||
|
apiurl : str
|
||||||
|
shareurl : str
|
||||||
|
davurl : str
|
||||||
|
auth : Tuple[str, str]
|
||||||
|
user : str
|
||||||
|
nextcloud_pg : Postgres
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
url="https://nextcloud." + app.config["DOMAIN"],
|
app : "AdminFlaskApp",
|
||||||
username=os.environ["NEXTCLOUD_ADMIN_USER"],
|
username : str=os.environ["NEXTCLOUD_ADMIN_USER"],
|
||||||
password=os.environ["NEXTCLOUD_ADMIN_PASSWORD"],
|
password : str=os.environ["NEXTCLOUD_ADMIN_PASSWORD"],
|
||||||
verify=True,
|
verify : bool=True,
|
||||||
):
|
) -> None:
|
||||||
|
url = "https://nextcloud." + app.config["DOMAIN"]
|
||||||
|
|
||||||
self.verify_cert = verify
|
self.verify_cert = verify
|
||||||
self.apiurl = url + "/ocs/v1.php/cloud/"
|
self.apiurl = url + "/ocs/v1.php/cloud/"
|
||||||
|
@ -61,9 +72,9 @@ class Nextcloud:
|
||||||
)
|
)
|
||||||
|
|
||||||
def _request(
|
def _request(
|
||||||
self, method, url, data={}, headers={"OCS-APIRequest": "true"}, auth=False
|
self, method : str, url : str, data : Any={}, headers : Dict[str, str]={"OCS-APIRequest": "true"}, auth : Optional[Tuple[str, str]]=None
|
||||||
):
|
) -> str:
|
||||||
if auth == False:
|
if auth is None:
|
||||||
auth = self.auth
|
auth = self.auth
|
||||||
try:
|
try:
|
||||||
response = requests.request(
|
response = requests.request(
|
||||||
|
@ -96,7 +107,7 @@ class Nextcloud:
|
||||||
raise ProviderConnError
|
raise ProviderConnError
|
||||||
raise ProviderError
|
raise ProviderError
|
||||||
|
|
||||||
def check_connection(self):
|
def check_connection(self) -> bool:
|
||||||
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)
|
||||||
|
@ -118,7 +129,7 @@ class Nextcloud:
|
||||||
raise ProviderConnError
|
raise ProviderConnError
|
||||||
raise ProviderError
|
raise ProviderError
|
||||||
|
|
||||||
def get_user(self, userid):
|
def get_user(self, userid : str) -> Any:
|
||||||
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))
|
||||||
|
@ -148,7 +159,7 @@ class Nextcloud:
|
||||||
# users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
|
# users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
|
||||||
# users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
|
# users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
|
||||||
# list_dict_users = [dict(zip(fields, r)) for r in users_with_lists]
|
# list_dict_users = [dict(zip(fields, r)) for r in users_with_lists]
|
||||||
def get_users_list(self):
|
def get_users_list(self) -> List[DDUser]:
|
||||||
# q = """select u.uid as username, adn.value as displayname, ade.value as email, json_agg(gg.displayname) as admin_groups,json_agg(g.displayname) as groups
|
# q = """select u.uid as username, adn.value as displayname, ade.value as email, json_agg(gg.displayname) as admin_groups,json_agg(g.displayname) as groups
|
||||||
# from oc_users as u
|
# from oc_users as u
|
||||||
# left join oc_group_user as gu on gu.uid = u.uid
|
# left join oc_group_user as gu on gu.uid = u.uid
|
||||||
|
@ -200,9 +211,10 @@ class Nextcloud:
|
||||||
# log.error(traceback.format_exc())
|
# log.error(traceback.format_exc())
|
||||||
# raise
|
# raise
|
||||||
|
|
||||||
|
# TODO: Improve typing of these functions...
|
||||||
def add_user(
|
def add_user(
|
||||||
self, userid, userpassword, quota=False, group=False, email="", displayname=""
|
self, userid : str, userpassword : str, quota : Any=False, group : Any=False, email : str="", displayname : str=""
|
||||||
):
|
) -> bool:
|
||||||
data = {
|
data = {
|
||||||
"userid": userid,
|
"userid": userid,
|
||||||
"password": userpassword,
|
"password": userpassword,
|
||||||
|
@ -247,7 +259,7 @@ class Nextcloud:
|
||||||
# 106 - no group specified (required for subadmins)
|
# 106 - no group specified (required for subadmins)
|
||||||
# 107 - all errors that contain a hint - for example “Password is among the 1,000,000 most common ones. Please make it unique.” (this code was added in 12.0.6 & 13.0.1)
|
# 107 - all errors that contain a hint - for example “Password is among the 1,000,000 most common ones. Please make it unique.” (this code was added in 12.0.6 & 13.0.1)
|
||||||
|
|
||||||
def update_user(self, userid, key_values):
|
def update_user(self, userid : str, key_values : Dict[str, Any]) -> bool:
|
||||||
# key_values={'quota':quota,'email':email,'displayname':displayname}
|
# key_values={'quota':quota,'email':email,'displayname':displayname}
|
||||||
|
|
||||||
url = self.apiurl + "users/" + userid + "?format=json"
|
url = self.apiurl + "users/" + userid + "?format=json"
|
||||||
|
@ -262,6 +274,8 @@ class Nextcloud:
|
||||||
result = json.loads(
|
result = json.loads(
|
||||||
self._request("PUT", url, data=data, headers=headers)
|
self._request("PUT", url, data=data, headers=headers)
|
||||||
)
|
)
|
||||||
|
# TODO: It seems like this only sets the first item in key_values
|
||||||
|
# This function probably doesn't do what it is supposed to
|
||||||
if result["ocs"]["meta"]["statuscode"] == 100:
|
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||||
return True
|
return True
|
||||||
if result["ocs"]["meta"]["statuscode"] == 102:
|
if result["ocs"]["meta"]["statuscode"] == 102:
|
||||||
|
@ -273,8 +287,9 @@ class Nextcloud:
|
||||||
except:
|
except:
|
||||||
# log.error(traceback.format_exc())
|
# log.error(traceback.format_exc())
|
||||||
raise
|
raise
|
||||||
|
return False
|
||||||
|
|
||||||
def add_user_to_group(self, userid, group_id):
|
def add_user_to_group(self, userid : str, group_id : str) -> bool:
|
||||||
data = {"groupid": group_id}
|
data = {"groupid": group_id}
|
||||||
|
|
||||||
url = self.apiurl + "users/" + userid + "/groups?format=json"
|
url = self.apiurl + "users/" + userid + "/groups?format=json"
|
||||||
|
@ -296,7 +311,7 @@ class Nextcloud:
|
||||||
# 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 : str, group_id : str) -> bool:
|
||||||
data = {"groupid": group_id}
|
data = {"groupid": group_id}
|
||||||
|
|
||||||
url = self.apiurl + "users/" + userid + "/groups?format=json"
|
url = self.apiurl + "users/" + userid + "/groups?format=json"
|
||||||
|
@ -321,9 +336,10 @@ class Nextcloud:
|
||||||
# log.error(traceback.format_exc())
|
# log.error(traceback.format_exc())
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
# TODO: Improve typing of these functions...
|
||||||
def add_user_with_groups(
|
def add_user_with_groups(
|
||||||
self, userid, userpassword, quota=False, groups=[], email="", displayname=""
|
self, userid : str, userpassword : str, quota : Any=False, groups : Any=[], email : str="", displayname : str=""
|
||||||
):
|
) -> bool:
|
||||||
data = {
|
data = {
|
||||||
"userid": userid,
|
"userid": userid,
|
||||||
"password": userpassword,
|
"password": userpassword,
|
||||||
|
@ -352,7 +368,7 @@ class Nextcloud:
|
||||||
raise ProviderItemExists
|
raise ProviderItemExists
|
||||||
if result["ocs"]["meta"]["statuscode"] == 104:
|
if result["ocs"]["meta"]["statuscode"] == 104:
|
||||||
# self.add_group(group)
|
# self.add_group(group)
|
||||||
None
|
pass
|
||||||
# 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
|
||||||
|
@ -368,7 +384,7 @@ class Nextcloud:
|
||||||
# 106 - no group specified (required for subadmins)
|
# 106 - no group specified (required for subadmins)
|
||||||
# 107 - all errors that contain a hint - for example “Password is among the 1,000,000 most common ones. Please make it unique.” (this code was added in 12.0.6 & 13.0.1)
|
# 107 - all errors that contain a hint - for example “Password is among the 1,000,000 most common ones. Please make it unique.” (this code was added in 12.0.6 & 13.0.1)
|
||||||
|
|
||||||
def delete_user(self, userid):
|
def delete_user(self, userid : str) -> bool:
|
||||||
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))
|
||||||
|
@ -384,13 +400,13 @@ class Nextcloud:
|
||||||
# 100 - successful
|
# 100 - successful
|
||||||
# 101 - failure
|
# 101 - failure
|
||||||
|
|
||||||
def enable_user(self, userid):
|
def enable_user(self, userid : str) -> None:
|
||||||
None
|
pass
|
||||||
|
|
||||||
def disable_user(self, userid):
|
def disable_user(self, userid : str) -> None:
|
||||||
None
|
pass
|
||||||
|
|
||||||
def exists_user_folder(self, userid, userpassword, folder="IsardVDI"):
|
def exists_user_folder(self, userid : str, userpassword : str, folder : str="IsardVDI") -> bool:
|
||||||
auth = (userid, userpassword)
|
auth = (userid, userpassword)
|
||||||
url = self.davurl + userid + "/" + folder + "?format=json"
|
url = self.davurl + userid + "/" + folder + "?format=json"
|
||||||
headers = {
|
headers = {
|
||||||
|
@ -407,7 +423,7 @@ class Nextcloud:
|
||||||
# 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 : str, userpassword : str, folder : str="IsardVDI") -> bool:
|
||||||
auth = (userid, userpassword)
|
auth = (userid, userpassword)
|
||||||
url = self.davurl + userid + "/" + folder + "?format=json"
|
url = self.davurl + userid + "/" + folder + "?format=json"
|
||||||
headers = {
|
headers = {
|
||||||
|
@ -429,7 +445,7 @@ class Nextcloud:
|
||||||
# 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 : str, userpassword : str, folder : str="IsardVDI") -> Dict[str, str]:
|
||||||
auth = (userid, userpassword)
|
auth = (userid, userpassword)
|
||||||
url = self.shareurl + "shares?format=json"
|
url = self.shareurl + "shares?format=json"
|
||||||
headers = {
|
headers = {
|
||||||
|
@ -449,7 +465,7 @@ class Nextcloud:
|
||||||
# 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 : str, userpassword : str, folder : str="IsardVDI") -> Dict[str, str]:
|
||||||
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"
|
||||||
|
@ -477,10 +493,10 @@ class Nextcloud:
|
||||||
# log.error(traceback.format_exc())
|
# log.error(traceback.format_exc())
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def get_group(self, userid):
|
def get_group(self, userid : str) -> None:
|
||||||
None
|
pass
|
||||||
|
|
||||||
def get_groups_list(self):
|
def get_groups_list(self) -> List[Any]:
|
||||||
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))
|
||||||
|
@ -491,7 +507,7 @@ class Nextcloud:
|
||||||
# log.error(traceback.format_exc())
|
# log.error(traceback.format_exc())
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def add_group(self, groupid):
|
def add_group(self, groupid : str) -> bool:
|
||||||
data = {"groupid": groupid}
|
data = {"groupid": groupid}
|
||||||
url = self.apiurl + "groups?format=json"
|
url = self.apiurl + "groups?format=json"
|
||||||
headers = {
|
headers = {
|
||||||
|
@ -515,7 +531,7 @@ class Nextcloud:
|
||||||
# 102 - group already exists
|
# 102 - group already exists
|
||||||
# 103 - failed to add the group
|
# 103 - failed to add the group
|
||||||
|
|
||||||
def delete_group(self, groupid):
|
def delete_group(self, groupid : str) -> bool:
|
||||||
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 = {
|
||||||
|
@ -538,7 +554,7 @@ class Nextcloud:
|
||||||
# 102 - group already exists
|
# 102 - group already exists
|
||||||
# 103 - failed to add the group
|
# 103 - failed to add the group
|
||||||
|
|
||||||
def set_user_mail(self, data):
|
def set_user_mail(self, data : DDUser) -> None:
|
||||||
query = """SELECT * FROM "oc_mail_accounts" WHERE "email" = '%s'"""
|
query = """SELECT * FROM "oc_mail_accounts" WHERE "email" = '%s'"""
|
||||||
sql_query = sql.SQL(query.format(data["email"]))
|
sql_query = sql.SQL(query.format(data["email"]))
|
||||||
if not len(self.nextcloud_pg.select(sql_query)):
|
if not len(self.nextcloud_pg.select(sql_query)):
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -18,54 +19,41 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import json
|
|
||||||
import logging as log
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
import yaml
|
import psycopg2.sql
|
||||||
|
from psycopg2.extensions import connection, cursor
|
||||||
|
|
||||||
# from admin import app
|
from typing import Any, List, Tuple, Union
|
||||||
|
|
||||||
|
query = Union[str, psycopg2.sql.SQL]
|
||||||
|
|
||||||
class Postgres:
|
class Postgres:
|
||||||
def __init__(self, host, database, user, password):
|
# TODO: Fix this whole class
|
||||||
|
cur : cursor
|
||||||
|
conn : connection
|
||||||
|
def __init__(self, host : str, database : str, user : str, password : str) -> None:
|
||||||
self.conn = psycopg2.connect(
|
self.conn = psycopg2.connect(
|
||||||
host=host, database=database, user=user, password=password
|
host=host, database=database, user=user, password=password
|
||||||
)
|
)
|
||||||
|
|
||||||
# def __del__(self):
|
def select(self, sql: query) -> List[Tuple[Any, ...]]:
|
||||||
# self.cur.close()
|
|
||||||
# self.conn.close()
|
|
||||||
|
|
||||||
def select(self, sql):
|
|
||||||
self.cur = self.conn.cursor()
|
self.cur = self.conn.cursor()
|
||||||
self.cur.execute(sql)
|
self.cur.execute(sql)
|
||||||
data = self.cur.fetchall()
|
data = self.cur.fetchall()
|
||||||
self.cur.close()
|
self.cur.close() # type: ignore # psycopg2 type hint missing
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def update(self, sql):
|
def update(self, sql : query) -> None:
|
||||||
self.cur = self.conn.cursor()
|
self.cur = self.conn.cursor()
|
||||||
self.cur.execute(sql)
|
self.cur.execute(sql)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
self.cur.close()
|
self.cur.close() # type: ignore # psycopg2 type hint missing
|
||||||
# return self.cur.fetchall()
|
# return self.cur.fetchall()
|
||||||
|
|
||||||
def select_with_headers(self, sql):
|
def select_with_headers(self, sql : query) -> Tuple[List[Any], List[Tuple[Any, ...]]]:
|
||||||
self.cur = self.conn.cursor()
|
self.cur = self.conn.cursor()
|
||||||
self.cur.execute(sql)
|
self.cur.execute(sql)
|
||||||
data = self.cur.fetchall()
|
data = self.cur.fetchall()
|
||||||
fields = [a.name for a in self.cur.description]
|
fields = [a.name for a in self.cur.description]
|
||||||
self.cur.close()
|
self.cur.close() # type: ignore # psycopg2 type hint missing
|
||||||
return (fields, data)
|
return (fields, data)
|
||||||
|
|
||||||
# def update_moodle_saml_plugin(self):
|
|
||||||
# plugin[('idpmetadata', '<md:EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Name="urn:keycloak"><md:EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://sso.'+app.config['DOMAIN']+'/auth/realms/master"><md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor use="signing"><ds:KeyInfo><ds:KeyName>NrtA5ynG0htowP3SXw7dBJRIAMxn-1PwuuXwOwNhlRw</ds:KeyName><ds:X509Data><ds:X509Certificate>MIICmzCCAYMCBgF5jb0RCTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwNTIxMDcwMjI4WhcNMzEwNTIxMDcwNDA4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCI8xh/C0+frz3kgWiUbziTDls71R2YiXLSVE+bw7gbEgZUGCLhoEI679azMtIxmnzM/snIX+yTb12+XoYkgbiLTMPQfnH+Kiab6g3HL3KPfhqS+yWkFxOoCp6Ibmp7yPlVWuHH+MBfO8OBr/r8Ao7heFbuzjiLd1KG67rcoaxfDgMuBoEomg1bgEjFgHaQIrSC6OZzH0h987/arqufZXeXlfyiqScMPUi+u5IpDWSwz06UKP0k8mxzNSlpZ93CKOUSsV0SMLxqg7FQ3SGiOk577bGW9o9BDTkkmSo3Up6smc0LzwvvUwuNd0B1irGkWZFQN9OXJnJYf1InEebIMtmPAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADM34+qEGeBQ22luphVTuVJtGxcbxLx7DfsT0QfJD/OuxTTbNAa1VRyarb5juIAkqdj4y2quZna9ZXLecVo4RkwpzPoKoAkYA8b+kHnWqEwJi9iPrDvKb+GR0bBkLPN49YxIZ8IdKX/PRa3yuLHe+loiNsCaS/2ZK2KO46COsqU4QX1iVhF9kWphNLybjNAX45B6cJLsa1g0vXLdm3kv3SB4I2fErFVaOoDtFIjttoYlXdpUiThkPXBfr7N67P3dZHaS4tjJh+IZ8I6TINpcsH8dBkUhzYEIPHCePwSiC1w6WDBLNDuKt1mj1CZrLq+1x+Yhrs+QNRheEKGi89HZ8N0=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml/resolve" index="0"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/></md:IDPSSODescriptor></md:EntityDescriptor></md:EntitiesDescriptor>')]
|
|
||||||
# pg_update = """UPDATE mdl_config_plugins set title = %s where plugin = auth_saml2 and name ="""
|
|
||||||
# cursor.execute(pg_update, (title, bookid))
|
|
||||||
# connection.commit()
|
|
||||||
# count = cursor.rowcount
|
|
||||||
# print(count, "Successfully Updated!")
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -23,8 +24,6 @@ import logging as log
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
|
||||||
# from .keycloak import Keycloak
|
|
||||||
# from .moodle import Moodle
|
|
||||||
import string
|
import string
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
@ -33,13 +32,16 @@ from datetime import datetime, timedelta
|
||||||
import psycopg2
|
import psycopg2
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from admin import app
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from admin.flaskapp import AdminFlaskApp
|
||||||
|
|
||||||
from .postgres import Postgres
|
from .postgres import Postgres
|
||||||
|
|
||||||
|
|
||||||
class Postup:
|
class Postup:
|
||||||
def __init__(self):
|
def __init__(self, app: "AdminFlaskApp") -> None:
|
||||||
ready = False
|
ready = False
|
||||||
while not ready:
|
while not ready:
|
||||||
try:
|
try:
|
||||||
|
@ -93,9 +95,9 @@ class Postup:
|
||||||
|
|
||||||
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(app)
|
||||||
|
|
||||||
def select_and_configure_theme(self, theme="cbe"):
|
def select_and_configure_theme(self, theme : str="cbe") -> None:
|
||||||
try:
|
try:
|
||||||
self.pg.update(
|
self.pg.update(
|
||||||
"""UPDATE "mdl_config" SET value = '%s' WHERE "name" = 'theme';"""
|
"""UPDATE "mdl_config" SET value = '%s' WHERE "name" = 'theme';"""
|
||||||
|
@ -104,7 +106,6 @@ class Postup:
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
exit(1)
|
exit(1)
|
||||||
None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.pg.update(
|
self.pg.update(
|
||||||
|
@ -127,9 +128,8 @@ class Postup:
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
exit(1)
|
exit(1)
|
||||||
None
|
|
||||||
|
|
||||||
def configure_tipnc(self):
|
def configure_tipnc(self) -> None:
|
||||||
try:
|
try:
|
||||||
self.pg.update(
|
self.pg.update(
|
||||||
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'host';"""
|
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'host';"""
|
||||||
|
@ -155,9 +155,8 @@ class Postup:
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
exit(1)
|
exit(1)
|
||||||
None
|
|
||||||
|
|
||||||
def add_moodle_ws_token(self):
|
def add_moodle_ws_token(self, app: "AdminFlaskApp") -> None:
|
||||||
try:
|
try:
|
||||||
token = self.pg.select(
|
token = self.pg.select(
|
||||||
"""SELECT * FROM "mdl_external_tokens" WHERE "externalserviceid" = 3"""
|
"""SELECT * FROM "mdl_external_tokens" WHERE "externalserviceid" = 3"""
|
||||||
|
@ -166,7 +165,7 @@ class Postup:
|
||||||
return
|
return
|
||||||
except:
|
except:
|
||||||
# log.error(traceback.format_exc())
|
# log.error(traceback.format_exc())
|
||||||
None
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.pg.update(
|
self.pg.update(
|
||||||
|
@ -225,4 +224,3 @@ class Postup:
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
exit(1)
|
exit(1)
|
||||||
None
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -19,6 +20,7 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
import json
|
import json
|
||||||
import logging as log
|
import logging as log
|
||||||
|
from operator import itemgetter
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
|
@ -27,54 +29,57 @@ import traceback
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from admin import app
|
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from admin.flaskapp import AdminFlaskApp
|
||||||
|
|
||||||
from ..lib.api_exceptions import Error
|
from ..lib.api_exceptions import Error
|
||||||
from .decorators import has_token
|
from .decorators import has_token, OptionalJsonResponse
|
||||||
|
|
||||||
|
|
||||||
|
def setup_api_views(app : "AdminFlaskApp") -> None:
|
||||||
## LISTS
|
## LISTS
|
||||||
@app.route("/ddapi/users", methods=["GET"])
|
@app.json_route("/ddapi/users", methods=["GET"])
|
||||||
@has_token
|
@has_token
|
||||||
def ddapi_users():
|
def ddapi_users() -> OptionalJsonResponse:
|
||||||
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=itemgetter("username"))
|
||||||
users = []
|
users = []
|
||||||
for user in sorted_users:
|
for user in sorted_users:
|
||||||
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"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.json_route("/ddapi/users/filter", methods=["POST"])
|
||||||
@app.route("/ddapi/users/filter", methods=["POST"])
|
|
||||||
@has_token
|
@has_token
|
||||||
def ddapi_users_search():
|
def ddapi_users_search() -> OptionalJsonResponse:
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
if not data.get("text"):
|
if not data.get("text"):
|
||||||
raise Error("bad_request", "Incorrect data requested.")
|
raise Error("bad_request", "Incorrect data requested.")
|
||||||
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=itemgetter("id"))
|
||||||
return json.dumps(sorted_result), 200, {"Content-Type": "application/json"}
|
return json.dumps(sorted_result), 200, {"Content-Type": "application/json"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.json_route("/ddapi/groups", methods=["GET"])
|
||||||
@app.route("/ddapi/groups", methods=["GET"])
|
|
||||||
@has_token
|
@has_token
|
||||||
def ddapi_groups():
|
def ddapi_groups() -> OptionalJsonResponse:
|
||||||
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=itemgetter("name"))
|
||||||
groups = []
|
groups = []
|
||||||
for group in sorted_groups:
|
for group in sorted_groups:
|
||||||
groups.append(group_parser(group))
|
groups.append(group_parser(group))
|
||||||
return json.dumps(groups), 200, {"Content-Type": "application/json"}
|
return json.dumps(groups), 200, {"Content-Type": "application/json"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.json_route("/ddapi/group/users", methods=["POST"])
|
||||||
@app.route("/ddapi/group/users", methods=["POST"])
|
|
||||||
@has_token
|
@has_token
|
||||||
def ddapi_group_users():
|
def ddapi_group_users() -> OptionalJsonResponse:
|
||||||
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=itemgetter("username"))
|
||||||
if data.get("id"):
|
if data.get("id"):
|
||||||
group_users = [
|
group_users = [
|
||||||
user_parser(user)
|
user_parser(user)
|
||||||
|
@ -112,14 +117,14 @@ def ddapi_group_users():
|
||||||
else:
|
else:
|
||||||
raise Error("bad_request", "Incorrect data requested.")
|
raise Error("bad_request", "Incorrect data requested.")
|
||||||
return json.dumps(group_users), 200, {"Content-Type": "application/json"}
|
return json.dumps(group_users), 200, {"Content-Type": "application/json"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.json_route("/ddapi/roles", methods=["GET"])
|
||||||
@app.route("/ddapi/roles", methods=["GET"])
|
|
||||||
@has_token
|
@has_token
|
||||||
def ddapi_roles():
|
def ddapi_roles() -> OptionalJsonResponse:
|
||||||
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=itemgetter("name")):
|
||||||
log.error(role)
|
log.error(role)
|
||||||
roles.append(
|
roles.append(
|
||||||
{
|
{
|
||||||
|
@ -130,14 +135,14 @@ def ddapi_roles():
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return json.dumps(roles), 200, {"Content-Type": "application/json"}
|
return json.dumps(roles), 200, {"Content-Type": "application/json"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.json_route("/ddapi/role/users", methods=["POST"])
|
||||||
@app.route("/ddapi/role/users", methods=["POST"])
|
|
||||||
@has_token
|
@has_token
|
||||||
def ddapi_role_users():
|
def ddapi_role_users() -> OptionalJsonResponse:
|
||||||
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=itemgetter("username"))
|
||||||
if data.get("id", data.get("name")):
|
if data.get("id", data.get("name")):
|
||||||
role_users = [
|
role_users = [
|
||||||
user_parser(user)
|
user_parser(user)
|
||||||
|
@ -159,20 +164,21 @@ def ddapi_role_users():
|
||||||
else:
|
else:
|
||||||
raise Error("bad_request", "Incorrect data requested.")
|
raise Error("bad_request", "Incorrect data requested.")
|
||||||
return json.dumps(role_users), 200, {"Content-Type": "application/json"}
|
return json.dumps(role_users), 200, {"Content-Type": "application/json"}
|
||||||
|
return None
|
||||||
|
|
||||||
## INDIVIDUAL ACTIONS
|
## INDIVIDUAL ACTIONS
|
||||||
@app.route("/ddapi/user", methods=["POST"])
|
@app.json_route("/ddapi/user", methods=["POST"])
|
||||||
@app.route("/ddapi/user/<user_ddid>", methods=["PUT", "GET", "DELETE"])
|
@app.json_route("/ddapi/user/<user_ddid>", methods=["PUT", "GET", "DELETE"])
|
||||||
@has_token
|
@has_token
|
||||||
def ddapi_user(user_ddid=None):
|
def ddapi_user(user_ddid : Optional[str]=None) -> OptionalJsonResponse:
|
||||||
|
uid : str = user_ddid if user_ddid else ''
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
user = app.admin.get_user_username(user_ddid)
|
user = app.admin.get_user_username(uid)
|
||||||
if not user:
|
if not user:
|
||||||
raise Error("not_found", "User id not found")
|
raise Error("not_found", "User id not found")
|
||||||
return json.dumps(user_parser(user)), 200, {"Content-Type": "application/json"}
|
return json.dumps(user_parser(user)), 200, {"Content-Type": "application/json"}
|
||||||
if request.method == "DELETE":
|
if request.method == "DELETE":
|
||||||
user = app.admin.get_user_username(user_ddid)
|
user = app.admin.get_user_username(uid)
|
||||||
if not user:
|
if not user:
|
||||||
raise Error("not_found", "User id not found")
|
raise Error("not_found", "User id not found")
|
||||||
app.admin.delete_user(user["id"])
|
app.admin.delete_user(user["id"])
|
||||||
|
@ -203,7 +209,7 @@ def ddapi_user(user_ddid=None):
|
||||||
)
|
)
|
||||||
|
|
||||||
if request.method == "PUT":
|
if request.method == "PUT":
|
||||||
user = app.admin.get_user_username(user_ddid)
|
user = app.admin.get_user_username(uid)
|
||||||
if not user:
|
if not user:
|
||||||
raise Error("not_found", "User id not found")
|
raise Error("not_found", "User id not found")
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
|
@ -216,7 +222,7 @@ def ddapi_user(user_ddid=None):
|
||||||
)
|
)
|
||||||
data = {**user, **data}
|
data = {**user, **data}
|
||||||
data = app.validators["user_update"].normalized(data)
|
data = app.validators["user_update"].normalized(data)
|
||||||
data = {**data, **{"username": user_ddid}}
|
data = {**data, **{"username": uid}}
|
||||||
data["roles"] = [data.pop("role")]
|
data["roles"] = [data.pop("role")]
|
||||||
data["firstname"] = data.pop("first")
|
data["firstname"] = data.pop("first")
|
||||||
data["lastname"] = data.pop("last")
|
data["lastname"] = data.pop("last")
|
||||||
|
@ -226,25 +232,25 @@ def ddapi_user(user_ddid=None):
|
||||||
user["id"], data["password"], data["password_temporary"]
|
user["id"], data["password"], data["password_temporary"]
|
||||||
)
|
)
|
||||||
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.json_route("/ddapi/username/<old_user_ddid>/<new_user_did>", methods=["PUT"])
|
||||||
@app.route("/ddapi/username/<old_user_ddid>/<new_user_did>", methods=["PUT"])
|
|
||||||
@has_token
|
@has_token
|
||||||
def ddapi_username(old_user_ddid, new_user_did):
|
def ddapi_username(old_user_ddid : str, new_user_did : str) -> OptionalJsonResponse:
|
||||||
user = app.admin.get_user_username(user_ddid)
|
user = app.admin.get_user_username(user_ddid)
|
||||||
if not user:
|
if not user:
|
||||||
raise Error("not_found", "User id not found")
|
raise Error("not_found", "User id not found")
|
||||||
# user = app.admin.update_user_username(old_user_ddid,new_user_did)
|
# user = app.admin.update_user_username(old_user_ddid,new_user_did)
|
||||||
return json.dumps("Not implemented yet!"), 419, {"Content-Type": "application/json"}
|
return json.dumps("Not implemented yet!"), 419, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
@app.json_route("/ddapi/group", methods=["POST"])
|
||||||
@app.route("/ddapi/group", methods=["POST"])
|
@app.json_route("/ddapi/group/<group_id>", methods=["GET", "POST", "DELETE"])
|
||||||
@app.route("/ddapi/group/<id>", methods=["GET", "POST", "DELETE"])
|
# @app.json_route("/api/group/<group_id>", methods=["PUT", "GET", "DELETE"])
|
||||||
# @app.route("/api/group/<group_id>", methods=["PUT", "GET", "DELETE"])
|
|
||||||
@has_token
|
@has_token
|
||||||
def ddapi_group(id=None):
|
def ddapi_group(group_id : Optional[str]=None) -> OptionalJsonResponse:
|
||||||
|
uid : str = group_id if group_id else ''
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
group = app.admin.get_group_by_name(id)
|
group = app.admin.get_group_by_name(uid)
|
||||||
if not group:
|
if not group:
|
||||||
Error("not found", "Group id not found")
|
Error("not found", "Group id not found")
|
||||||
return (
|
return (
|
||||||
|
@ -264,7 +270,7 @@ def ddapi_group(id=None):
|
||||||
data = app.validators["group"].normalized(data)
|
data = app.validators["group"].normalized(data)
|
||||||
data["parent"] = data["parent"] if data["parent"] != "" else None
|
data["parent"] = data["parent"] if data["parent"] != "" else None
|
||||||
|
|
||||||
if app.admin.get_group_by_name(id):
|
if app.admin.get_group_by_name(uid):
|
||||||
raise Error("conflict", "Group id already exists")
|
raise Error("conflict", "Group id already exists")
|
||||||
|
|
||||||
path = app.admin.add_group(data)
|
path = app.admin.add_group(data)
|
||||||
|
@ -277,17 +283,17 @@ def ddapi_group(id=None):
|
||||||
{"Content-Type": "application/json"},
|
{"Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
if request.method == "DELETE":
|
if request.method == "DELETE":
|
||||||
group = app.admin.get_group_by_name(id)
|
group = app.admin.get_group_by_name(uid)
|
||||||
if not group:
|
if not group:
|
||||||
raise Error("not_found", "Group id not found")
|
raise Error("not_found", "Group id not found")
|
||||||
app.admin.delete_group_by_id(group["id"])
|
app.admin.delete_group_by_id(group["id"])
|
||||||
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
return json.dumps({}), 200, {"Content-Type": "application/json"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.json_route("/ddapi/user_mail", methods=["POST"])
|
||||||
@app.route("/ddapi/user_mail", methods=["POST"])
|
@app.json_route("/ddapi/user_mail/<id>", methods=["GET", "DELETE"])
|
||||||
@app.route("/ddapi/user_mail/<id>", methods=["GET", "DELETE"])
|
|
||||||
@has_token
|
@has_token
|
||||||
def ddapi_user_mail(id=None):
|
def ddapi_user_mail(id : Optional[str]=None) -> OptionalJsonResponse:
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return (
|
return (
|
||||||
json.dumps("Not implemented yet"),
|
json.dumps("Not implemented yet"),
|
||||||
|
@ -320,9 +326,10 @@ def ddapi_user_mail(id=None):
|
||||||
200,
|
200,
|
||||||
{"Content-Type": "application/json"},
|
{"Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# TODO: After this line, this is all mostly duplicated from other places...
|
||||||
def user_parser(user):
|
def user_parser(user : Dict[str, Any]) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"keycloak_id": user["id"],
|
"keycloak_id": user["id"],
|
||||||
"id": user["username"],
|
"id": user["username"],
|
||||||
|
@ -338,7 +345,7 @@ def user_parser(user):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def group_parser(group):
|
def group_parser(group : Dict[str, str]) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"keycloak_id": group["id"],
|
"keycloak_id": group["id"],
|
||||||
"id": group["name"],
|
"id": group["name"],
|
||||||
|
@ -348,7 +355,7 @@ def group_parser(group):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def filter_users(users, text):
|
def filter_users(users : Iterable[Dict[str, Any]], text : str) -> List[Dict[str, Any]]:
|
||||||
return [
|
return [
|
||||||
user
|
user
|
||||||
for user in users
|
for user in users
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -33,10 +34,12 @@ from uuid import uuid4
|
||||||
from flask import Response, jsonify, redirect, render_template, request, url_for
|
from flask import Response, jsonify, redirect, render_template, request, url_for
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
|
||||||
from admin import app
|
from typing import TYPE_CHECKING, cast, Any, Dict, Optional
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from admin.flaskapp import AdminFlaskApp
|
||||||
|
|
||||||
from ..lib.helpers import system_group
|
from ..lib.helpers import system_group
|
||||||
from .decorators import login_or_token
|
from .decorators import login_or_token, OptionalJsonResponse
|
||||||
|
|
||||||
threads = {"external": None}
|
threads = {"external": None}
|
||||||
# q = Queue.Queue()
|
# q = Queue.Queue()
|
||||||
|
@ -46,14 +49,15 @@ from keycloak.exceptions import KeycloakGetError
|
||||||
from ..lib.dashboard import Dashboard
|
from ..lib.dashboard import Dashboard
|
||||||
from ..lib.exceptions import UserExists, UserNotFound
|
from ..lib.exceptions import UserExists, UserNotFound
|
||||||
|
|
||||||
dashboard = Dashboard()
|
|
||||||
|
|
||||||
from ..lib.legal import get_legal, gen_legal_if_not_exists, new_legal
|
from ..lib.legal import get_legal, gen_legal_if_not_exists, new_legal
|
||||||
|
|
||||||
@app.route("/sysadmin/api/resync")
|
def setup_app_views(app : "AdminFlaskApp") -> None:
|
||||||
@app.route("/api/resync")
|
dashboard = Dashboard(app)
|
||||||
|
@app.json_route("/sysadmin/api/resync")
|
||||||
|
@app.json_route("/api/resync")
|
||||||
@login_required
|
@login_required
|
||||||
def resync():
|
def resync() -> OptionalJsonResponse:
|
||||||
return (
|
return (
|
||||||
json.dumps(app.admin.resync_data()),
|
json.dumps(app.admin.resync_data()),
|
||||||
200,
|
200,
|
||||||
|
@ -61,10 +65,10 @@ def resync():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/users", methods=["GET", "PUT"])
|
@app.json_route("/api/users", methods=["GET", "PUT"])
|
||||||
@app.route("/api/users/<provider>", methods=["POST", "PUT", "GET", "DELETE"])
|
@app.json_route("/api/users/<provider>", methods=["POST", "PUT", "GET", "DELETE"])
|
||||||
@login_or_token
|
@login_or_token
|
||||||
def users(provider=False):
|
def users(provider : bool=False) -> OptionalJsonResponse:
|
||||||
if request.method == "DELETE":
|
if request.method == "DELETE":
|
||||||
if current_user.role != "admin":
|
if current_user.role != "admin":
|
||||||
return json.dumps({}), 301, {"Content-Type": "application/json"}
|
return json.dumps({}), 301, {"Content-Type": "application/json"}
|
||||||
|
@ -82,7 +86,7 @@ def users(provider=False):
|
||||||
)
|
)
|
||||||
if provider == "moodle":
|
if provider == "moodle":
|
||||||
return (
|
return (
|
||||||
json.dumps(app.admin.delete_moodle_users()),
|
json.dumps(app.admin.delete_moodle_users(app)),
|
||||||
200,
|
200,
|
||||||
{"Content-Type": "application/json"},
|
{"Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
|
@ -141,9 +145,9 @@ def users(provider=False):
|
||||||
return json.dumps(users), 200, {"Content-Type": "application/json"}
|
return json.dumps(users), 200, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/users_bulk/<action>", methods=["PUT"])
|
@app.json_route("/api/users_bulk/<action>", methods=["PUT"])
|
||||||
@login_required
|
@login_required
|
||||||
def users_bulk(action):
|
def users_bulk(action : str) -> OptionalJsonResponse:
|
||||||
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":
|
||||||
|
@ -225,10 +229,10 @@ def users_bulk(action):
|
||||||
|
|
||||||
|
|
||||||
# Update pwd
|
# Update pwd
|
||||||
@app.route("/api/user_password", methods=["GET"])
|
@app.json_route("/api/user_password", methods=["GET"])
|
||||||
@app.route("/api/user_password/<userid>", methods=["PUT"])
|
@app.json_route("/api/user_password/<userid>", methods=["PUT"])
|
||||||
@login_required
|
@login_required
|
||||||
def user_password(userid=False):
|
def user_password(userid : Optional[str]=None) -> OptionalJsonResponse:
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return (
|
return (
|
||||||
json.dumps(app.admin.get_dice_pwd()),
|
json.dumps(app.admin.get_dice_pwd()),
|
||||||
|
@ -239,8 +243,9 @@ def user_password(userid=False):
|
||||||
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)
|
||||||
|
uid = cast(str, userid)
|
||||||
try:
|
try:
|
||||||
res = app.admin.user_update_password(userid, password, temporary)
|
res = app.admin.user_update_password(uid, 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"))
|
||||||
|
@ -254,12 +259,13 @@ def user_password(userid=False):
|
||||||
|
|
||||||
|
|
||||||
# User
|
# User
|
||||||
@app.route("/api/user", methods=["POST"])
|
@app.json_route("/api/user", methods=["POST"])
|
||||||
@app.route("/api/user/<userid>", methods=["PUT", "GET", "DELETE"])
|
@app.json_route("/api/user/<userid>", methods=["PUT", "GET", "DELETE"])
|
||||||
@login_required
|
@login_required
|
||||||
def user(userid=None):
|
def user(userid : Optional[str]=None) -> OptionalJsonResponse:
|
||||||
|
uid : str = userid if userid else ''
|
||||||
if request.method == "DELETE":
|
if request.method == "DELETE":
|
||||||
app.admin.delete_user(userid)
|
app.admin.delete_user(uid)
|
||||||
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)
|
||||||
|
@ -312,7 +318,7 @@ def user(userid=None):
|
||||||
if request.method == "DELETE":
|
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(uid)
|
||||||
if not user:
|
if not user:
|
||||||
return (
|
return (
|
||||||
json.dumps({"msg": "User not found."}),
|
json.dumps({"msg": "User not found."}),
|
||||||
|
@ -320,21 +326,21 @@ def user(userid=None):
|
||||||
{"Content-Type": "application/json"},
|
{"Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
return json.dumps(user), 200, {"Content-Type": "application/json"}
|
return json.dumps(user), 200, {"Content-Type": "application/json"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.json_route("/api/roles")
|
||||||
@app.route("/api/roles")
|
|
||||||
@login_required
|
@login_required
|
||||||
def roles():
|
def roles() -> OptionalJsonResponse:
|
||||||
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.json_route("/api/group", methods=["POST", "DELETE"])
|
||||||
@app.route("/api/group/<group_id>", methods=["PUT", "GET", "DELETE"])
|
@app.json_route("/api/group/<group_id>", methods=["PUT", "GET", "DELETE"])
|
||||||
@login_required
|
@login_required
|
||||||
def group(group_id=False):
|
def group(group_id : Optional[str]=None) -> OptionalJsonResponse:
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
log.error(data)
|
log.error(data)
|
||||||
|
@ -361,12 +367,12 @@ def group(group_id=False):
|
||||||
)
|
)
|
||||||
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"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.json_route("/api/groups")
|
||||||
@app.route("/api/groups")
|
@app.json_route("/api/groups/<provider>", methods=["POST", "PUT", "GET", "DELETE"])
|
||||||
@app.route("/api/groups/<provider>", methods=["POST", "PUT", "GET", "DELETE"])
|
|
||||||
@login_required
|
@login_required
|
||||||
def groups(provider=False):
|
def groups(provider : Optional[str] = None) -> OptionalJsonResponse:
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: str(k["name"]))
|
sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: str(k["name"]))
|
||||||
if current_user.role != "admin":
|
if current_user.role != "admin":
|
||||||
|
@ -382,14 +388,14 @@ def groups(provider=False):
|
||||||
200,
|
200,
|
||||||
{"Content-Type": "application/json"},
|
{"Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
### SYSADM USERS ONLY
|
### SYSADM USERS ONLY
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/external", methods=["POST", "PUT", "GET", "DELETE"])
|
@app.json_route("/api/external", methods=["POST", "PUT", "GET", "DELETE"])
|
||||||
@login_required
|
@login_required
|
||||||
def external():
|
def external() -> OptionalJsonResponse:
|
||||||
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"}
|
||||||
|
@ -428,9 +434,9 @@ def external():
|
||||||
return json.dumps({}), 500, {"Content-Type": "application/json"}
|
return json.dumps({}), 500, {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/external/users")
|
@app.json_route("/api/external/users")
|
||||||
@login_required
|
@login_required
|
||||||
def external_users_list():
|
def external_users_list() -> OptionalJsonResponse:
|
||||||
while threads["external"] is not None and threads["external"].is_alive():
|
while threads["external"] is not None and threads["external"].is_alive():
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
return (
|
return (
|
||||||
|
@ -440,9 +446,9 @@ def external_users_list():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/external/groups")
|
@app.json_route("/api/external/groups")
|
||||||
@login_required
|
@login_required
|
||||||
def external_groups_list():
|
def external_groups_list() -> OptionalJsonResponse:
|
||||||
while threads["external"] is not None and threads["external"].is_alive():
|
while threads["external"] is not None and threads["external"].is_alive():
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
return (
|
return (
|
||||||
|
@ -452,18 +458,19 @@ def external_groups_list():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/external/roles", methods=["PUT"])
|
@app.json_route("/api/external/roles", methods=["PUT"])
|
||||||
@login_required
|
@login_required
|
||||||
def external_roles():
|
def external_roles() -> OptionalJsonResponse:
|
||||||
if request.method == "PUT":
|
if request.method == "PUT":
|
||||||
return (
|
return (
|
||||||
json.dumps(app.admin.external_roleassign(request.get_json(force=True))),
|
json.dumps(app.admin.external_roleassign(request.get_json(force=True))),
|
||||||
200,
|
200,
|
||||||
{"Content-Type": "application/json"},
|
{"Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def check_upload_errors(data):
|
def check_upload_errors(data : Dict[Any, Any]) -> Dict[Any, Any]:
|
||||||
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:
|
||||||
|
@ -501,9 +508,9 @@ def check_upload_errors(data):
|
||||||
return {"pass": True, "msg": ""}
|
return {"pass": True, "msg": ""}
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/dashboard/<item>", methods=["PUT"])
|
@app.json_route("/api/dashboard/<item>", methods=["PUT"])
|
||||||
@login_required
|
@login_required
|
||||||
def dashboard_put(item):
|
def dashboard_put(item : str) -> OptionalJsonResponse:
|
||||||
if item == "colours":
|
if item == "colours":
|
||||||
try:
|
try:
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
|
@ -536,27 +543,28 @@ def dashboard_put(item):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/legal/<item>", methods=["GET"])
|
@app.json_route("/api/legal/<item>", methods=["GET"])
|
||||||
# @login_required
|
# @login_required
|
||||||
def legal_get(item):
|
def legal_get(item : str) -> OptionalJsonResponse:
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
if item == "legal":
|
if item == "legal":
|
||||||
lang = request.args.get("lang")
|
lang = request.args.get("lang")
|
||||||
if not lang or lang not in ["ca","es","en","fr"]:
|
if not lang or lang not in ["ca","es","en","fr"]:
|
||||||
lang="ca"
|
lang="ca"
|
||||||
gen_legal_if_not_exists(lang)
|
gen_legal_if_not_exists(app, lang)
|
||||||
return (
|
return (
|
||||||
json.dumps({"html": get_legal(lang)}),
|
json.dumps({"html": get_legal(app, lang)}),
|
||||||
200,
|
200,
|
||||||
{"Content-Type": "application/json"},
|
{"Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
# if item == "privacy":
|
# if item == "privacy":
|
||||||
# return json.dumps({ "html": "<b>Privacy policy</b><br>This works!"}), 200, {'Content-Type': 'application/json'}
|
# return json.dumps({ "html": "<b>Privacy policy</b><br>This works!"}), 200, {'Content-Type': 'application/json'}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/legal/<item>", methods=["POST"])
|
@app.json_route("/api/legal/<item>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def legal_put(item):
|
def legal_put(item : str) -> OptionalJsonResponse:
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if item == "legal":
|
if item == "legal":
|
||||||
data = None
|
data = None
|
||||||
|
@ -566,10 +574,11 @@ def legal_put(item):
|
||||||
lang = data["lang"]
|
lang = data["lang"]
|
||||||
if not lang or lang not in ["ca","es","en","fr"]:
|
if not lang or lang not in ["ca","es","en","fr"]:
|
||||||
lang="ca"
|
lang="ca"
|
||||||
new_legal(lang,html)
|
new_legal(app, lang, html)
|
||||||
except:
|
except:
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
return json.dumps(data), 200, {"Content-Type": "application/json"}
|
return json.dumps(data), 200, {"Content-Type": "application/json"}
|
||||||
|
return None
|
||||||
# if item == "privacy":
|
# if item == "privacy":
|
||||||
# data = None
|
# data = None
|
||||||
# try:
|
# try:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -21,15 +22,19 @@ import os
|
||||||
|
|
||||||
from flask import flash, redirect, render_template, request, url_for
|
from flask import flash, redirect, render_template, request, url_for
|
||||||
from flask_login import current_user, login_required, login_user, logout_user
|
from flask_login import current_user, login_required, login_user, logout_user
|
||||||
|
from werkzeug.wrappers import Response
|
||||||
|
|
||||||
from admin import app
|
from typing import TYPE_CHECKING
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from admin.flaskapp import AdminFlaskApp
|
||||||
|
|
||||||
from ..auth.authentication import *
|
from ..auth.authentication import *
|
||||||
|
|
||||||
|
|
||||||
|
def setup_login_views(app : "AdminFlaskApp") -> None:
|
||||||
@app.route("/", methods=["GET", "POST"])
|
@app.route("/", methods=["GET", "POST"])
|
||||||
@app.route("/login", methods=["GET", "POST"])
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login() -> Response:
|
||||||
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")
|
||||||
|
@ -39,23 +44,22 @@ def login():
|
||||||
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(
|
user = User(
|
||||||
{
|
id = ram_user["id"],
|
||||||
"id": ram_user["id"],
|
password = ram_user["password"],
|
||||||
"password": ram_user["password"],
|
role = ram_user["role"],
|
||||||
"role": ram_user["role"],
|
active = True,
|
||||||
"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")
|
o : Response = app.make_response(render_template("login.html"))
|
||||||
|
return o
|
||||||
|
|
||||||
|
|
||||||
@app.route("/logout", methods=["GET"])
|
@app.route("/logout", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def logout():
|
def logout() -> Response:
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect(url_for("login"))
|
return redirect(url_for("login"))
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
#
|
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
|
||||||
#
|
|
||||||
# This file is part of DD
|
|
||||||
#
|
|
||||||
# DD is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
# option) any later version.
|
|
||||||
#
|
|
||||||
# DD is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with DD. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
# from flask_socketio import SocketIO, emit, join_room, leave_room, \
|
|
||||||
# close_room, rooms, disconnect, send
|
|
||||||
# from admin import app
|
|
||||||
# import json
|
|
||||||
|
|
||||||
# # socketio = SocketIO(app)
|
|
||||||
# # from ...start import socketio
|
|
||||||
|
|
||||||
# @socketio.on('connect', namespace='//sio')
|
|
||||||
# def socketio_connect():
|
|
||||||
# join_room('admin')
|
|
||||||
# socketio.emit('update',
|
|
||||||
# json.dumps('Joined'),
|
|
||||||
# namespace='//sio',
|
|
||||||
# room='admin')
|
|
||||||
|
|
||||||
# @socketio.on('disconnect', namespace='//sio')
|
|
||||||
# def socketio_domains_disconnect():
|
|
||||||
# None
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -34,47 +35,48 @@ from flask import (
|
||||||
jsonify,
|
jsonify,
|
||||||
redirect,
|
redirect,
|
||||||
request,
|
request,
|
||||||
|
Response,
|
||||||
send_file,
|
send_file,
|
||||||
url_for,
|
url_for,
|
||||||
)
|
)
|
||||||
from flask import render_template as render_template_flask
|
from flask import render_template as render_template_flask
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
from admin import app
|
from typing import TYPE_CHECKING
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from admin.flaskapp import AdminFlaskApp
|
||||||
|
|
||||||
from ..lib.avatars import Avatars
|
|
||||||
from .decorators import is_admin
|
from .decorators import is_admin
|
||||||
|
|
||||||
avatars = Avatars()
|
|
||||||
|
|
||||||
from ..lib.legal import gen_legal_if_not_exists
|
from ..lib.legal import gen_legal_if_not_exists
|
||||||
|
|
||||||
|
|
||||||
def render_template(*args, **kwargs):
|
def render_template(*args : str, **kwargs : str) -> str:
|
||||||
kwargs["DOMAIN"] = os.environ["DOMAIN"]
|
kwargs["DOMAIN"] = os.environ["DOMAIN"]
|
||||||
return render_template_flask(*args, **kwargs)
|
return render_template_flask(*args, **kwargs)
|
||||||
|
|
||||||
|
def setup_web_views(app : "AdminFlaskApp") -> None:
|
||||||
@app.route("/users")
|
@app.route("/users")
|
||||||
@login_required
|
@login_required
|
||||||
def web_users():
|
def web_users() -> str:
|
||||||
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() -> str:
|
||||||
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 : bool=False) -> str:
|
||||||
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 : str) -> Response:
|
||||||
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")
|
||||||
|
@ -82,7 +84,7 @@ def avatar(userid):
|
||||||
|
|
||||||
@app.route("/dashboard")
|
@app.route("/dashboard")
|
||||||
@login_required
|
@login_required
|
||||||
def dashboard(provider=False):
|
def dashboard(provider : bool=False) -> str:
|
||||||
data = json.loads(requests.get("http://dd-sso-api/json").text)
|
data = json.loads(requests.get("http://dd-sso-api/json").text)
|
||||||
return render_template(
|
return render_template(
|
||||||
"pages/dashboard.html", title="Customization", nav="Customization", data=data
|
"pages/dashboard.html", title="Customization", nav="Customization", data=data
|
||||||
|
@ -91,16 +93,16 @@ def dashboard(provider=False):
|
||||||
|
|
||||||
@app.route("/legal")
|
@app.route("/legal")
|
||||||
@login_required
|
@login_required
|
||||||
def legal():
|
def legal() -> str:
|
||||||
# data = json.loads(requests.get("http://dd-sso-api/json").text)
|
# data = json.loads(requests.get("http://dd-sso-api/json").text)
|
||||||
return render_template("pages/legal.html", title="Legal", nav="Legal", data={})
|
return render_template("pages/legal.html", title="Legal", nav="Legal", data="")
|
||||||
|
|
||||||
@app.route("/legal_text")
|
@app.route("/legal_text")
|
||||||
def legal_text():
|
def legal_text() -> str:
|
||||||
lang = request.args.get("lang")
|
lang = request.args.get("lang")
|
||||||
if not lang or lang not in ["ca","es","en","fr"]:
|
if not lang or lang not in ["ca","es","en","fr"]:
|
||||||
lang="ca"
|
lang="ca"
|
||||||
gen_legal_if_not_exists(lang)
|
gen_legal_if_not_exists(app, lang)
|
||||||
return render_template("pages/legal/"+lang)
|
return render_template("pages/legal/"+lang)
|
||||||
|
|
||||||
### SYS ADMIN
|
### SYS ADMIN
|
||||||
|
@ -109,32 +111,34 @@ def legal_text():
|
||||||
@app.route("/sysadmin/users")
|
@app.route("/sysadmin/users")
|
||||||
@login_required
|
@login_required
|
||||||
@is_admin
|
@is_admin
|
||||||
def web_sysadmin_users():
|
def web_sysadmin_users() -> Response:
|
||||||
return render_template(
|
o : Response = app.make_response(render_template(
|
||||||
"pages/sysadmin/users.html", title="SysAdmin Users", nav="SysAdminUsers"
|
"pages/sysadmin/users.html", title="SysAdmin Users", nav="SysAdminUsers"
|
||||||
)
|
))
|
||||||
|
return o
|
||||||
|
|
||||||
|
|
||||||
@app.route("/sysadmin/groups")
|
@app.route("/sysadmin/groups")
|
||||||
@login_required
|
@login_required
|
||||||
@is_admin
|
@is_admin
|
||||||
def web_sysadmin_groups():
|
def web_sysadmin_groups() -> Response:
|
||||||
return render_template(
|
o : Response = app.make_response(render_template(
|
||||||
"pages/sysadmin/groups.html", title="SysAdmin Groups", nav="SysAdminGroups"
|
"pages/sysadmin/groups.html", title="SysAdmin Groups", nav="SysAdminGroups"
|
||||||
)
|
))
|
||||||
|
return o
|
||||||
|
|
||||||
|
|
||||||
@app.route("/sysadmin/external")
|
@app.route("/sysadmin/external")
|
||||||
@login_required
|
@login_required
|
||||||
## SysAdmin role
|
## SysAdmin role
|
||||||
def web_sysadmin_external():
|
def web_sysadmin_external() -> str:
|
||||||
return render_template(
|
return render_template(
|
||||||
"pages/sysadmin/external.html", title="External", nav="External"
|
"pages/sysadmin/external.html", title="External", nav="External"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/sockettest")
|
@app.route("/sockettest")
|
||||||
def web_sockettest():
|
def web_sockettest() -> str:
|
||||||
return render_template(
|
return render_template(
|
||||||
"pages/sockettest.html", title="Sockettest Users", nav="SysAdminUsers"
|
"pages/sockettest.html", title="Sockettest Users", nav="SysAdminUsers"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -20,6 +21,7 @@
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging as log
|
import logging as log
|
||||||
|
from operator import itemgetter
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
|
@ -28,17 +30,21 @@ import traceback
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from admin import app
|
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from admin.flaskapp import AdminFlaskApp
|
||||||
|
|
||||||
from .decorators import is_internal
|
from admin.views.decorators import OptionalJsonResponse, is_internal
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/internal/users", methods=["GET"])
|
|
||||||
|
def setup_wp_views(app : "AdminFlaskApp") -> None:
|
||||||
|
@app.json_route("/api/internal/users", methods=["GET"])
|
||||||
@is_internal
|
@is_internal
|
||||||
def internal_users():
|
def internal_users() -> OptionalJsonResponse:
|
||||||
log.error(socket.gethostbyname("dd-apps-wordpress"))
|
log.error(socket.gethostbyname("dd-apps-wordpress"))
|
||||||
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=itemgetter("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:
|
||||||
|
@ -46,24 +52,24 @@ def internal_users():
|
||||||
continue
|
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"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.json_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() -> OptionalJsonResponse:
|
||||||
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=itemgetter("id"))
|
||||||
return json.dumps(sorted_result), 200, {"Content-Type": "application/json"}
|
return json.dumps(sorted_result), 200, {"Content-Type": "application/json"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.json_route("/api/internal/groups", methods=["GET"])
|
||||||
@app.route("/api/internal/groups", methods=["GET"])
|
|
||||||
@is_internal
|
@is_internal
|
||||||
def internal_groups():
|
def internal_groups() -> OptionalJsonResponse:
|
||||||
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=itemgetter("name"))
|
||||||
groups = []
|
groups = []
|
||||||
for group in sorted_groups:
|
for group in sorted_groups:
|
||||||
if not group["path"].startswith("/"):
|
if not group["path"].startswith("/"):
|
||||||
|
@ -76,14 +82,14 @@ def internal_groups():
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return json.dumps(groups), 200, {"Content-Type": "application/json"}
|
return json.dumps(groups), 200, {"Content-Type": "application/json"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.json_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() -> OptionalJsonResponse:
|
||||||
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=itemgetter("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:
|
||||||
|
@ -95,14 +101,14 @@ def internal_group_users():
|
||||||
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"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.json_route("/api/internal/roles", methods=["GET"])
|
||||||
@app.route("/api/internal/roles", methods=["GET"])
|
|
||||||
@is_internal
|
@is_internal
|
||||||
def internal_roles():
|
def internal_roles() -> OptionalJsonResponse:
|
||||||
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=itemgetter("name")):
|
||||||
if role["name"] == "admin":
|
if role["name"] == "admin":
|
||||||
continue
|
continue
|
||||||
roles.append(
|
roles.append(
|
||||||
|
@ -113,14 +119,14 @@ def internal_roles():
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return json.dumps(roles), 200, {"Content-Type": "application/json"}
|
return json.dumps(roles), 200, {"Content-Type": "application/json"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.json_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() -> OptionalJsonResponse:
|
||||||
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=itemgetter("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:
|
||||||
|
@ -132,9 +138,9 @@ def internal_role_users():
|
||||||
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"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
def user_parser(user : Dict[str, Any]) -> Dict[str, Any]:
|
||||||
def user_parser(user):
|
|
||||||
return {
|
return {
|
||||||
"id": user["username"],
|
"id": user["username"],
|
||||||
"first": user["first"],
|
"first": user["first"],
|
||||||
|
@ -145,7 +151,7 @@ def user_parser(user):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def filter_users(users, text):
|
def filter_users(users : Iterable[Dict[str, Any]], text : str) -> List[Dict[str, Any]]:
|
||||||
return [
|
return [
|
||||||
user
|
user
|
||||||
for user in users
|
for user in users
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -25,25 +26,28 @@ import socket
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import redirect, request, url_for
|
from flask import redirect, request, url_for
|
||||||
|
from werkzeug.wrappers import Response
|
||||||
from flask_login import current_user, logout_user
|
from flask_login import current_user, logout_user
|
||||||
from jose import jwt
|
from jose import jwt
|
||||||
|
|
||||||
from ..auth.tokens import get_header_jwt_payload
|
from ..auth.tokens import get_header_jwt_payload
|
||||||
|
|
||||||
|
from typing import Any, Callable, Dict, Optional, Tuple
|
||||||
|
JsonResponse = Tuple[str, int, Dict[str, str]]
|
||||||
|
OptionalJsonResponse = Optional[JsonResponse]
|
||||||
|
|
||||||
def is_admin(fn):
|
def is_admin(fn : Callable[..., Response]) -> Callable[..., Response]:
|
||||||
@wraps(fn)
|
@wraps(fn)
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args : Any, **kwargs : Any) -> Response:
|
||||||
if current_user.role == "admin":
|
if current_user.role == "admin":
|
||||||
return fn(*args, **kwargs)
|
return fn(*args, **kwargs)
|
||||||
return redirect(url_for("login"))
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
return decorated_view
|
return decorated_view
|
||||||
|
|
||||||
|
def is_internal(fn : Callable[..., OptionalJsonResponse]) -> Callable[..., OptionalJsonResponse]:
|
||||||
def is_internal(fn):
|
|
||||||
@wraps(fn)
|
@wraps(fn)
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args : Any, **kwargs : Any) -> OptionalJsonResponse:
|
||||||
remote_addr = (
|
remote_addr = (
|
||||||
request.headers["X-Forwarded-For"].split(",")[0]
|
request.headers["X-Forwarded-For"].split(",")[0]
|
||||||
if "X-Forwarded-For" in request.headers
|
if "X-Forwarded-For" in request.headers
|
||||||
|
@ -67,18 +71,18 @@ def is_internal(fn):
|
||||||
return decorated_view
|
return decorated_view
|
||||||
|
|
||||||
|
|
||||||
def has_token(fn):
|
def has_token(fn : Callable[..., Any]) -> Callable[..., Any]:
|
||||||
@wraps(fn)
|
@wraps(fn)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args : Any, **kwargs : Any) -> Any:
|
||||||
payload = get_header_jwt_payload()
|
payload = get_header_jwt_payload()
|
||||||
return fn(*args, **kwargs)
|
return fn(*args, **kwargs)
|
||||||
|
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
def is_internal_or_has_token(fn):
|
def is_internal_or_has_token(fn : Callable[..., Any]) -> Callable[..., Any]:
|
||||||
@wraps(fn)
|
@wraps(fn)
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args : Any, **kwargs : Any) -> Any:
|
||||||
remote_addr = (
|
remote_addr = (
|
||||||
request.headers["X-Forwarded-For"].split(",")[0]
|
request.headers["X-Forwarded-For"].split(",")[0]
|
||||||
if "X-Forwarded-For" in request.headers
|
if "X-Forwarded-For" in request.headers
|
||||||
|
@ -94,9 +98,9 @@ def is_internal_or_has_token(fn):
|
||||||
return decorated_view
|
return decorated_view
|
||||||
|
|
||||||
|
|
||||||
def login_or_token(fn):
|
def login_or_token(fn : Callable[..., Any]) -> Callable[..., Any]:
|
||||||
@wraps(fn)
|
@wraps(fn)
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args : Any, **kwargs : Any) -> Any:
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return fn(*args, **kwargs)
|
return fn(*args, **kwargs)
|
||||||
payload = get_header_jwt_payload()
|
payload = get_header_jwt_payload()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#
|
#
|
||||||
# Copyright © 2021,2022 IsardVDI S.L.
|
# Copyright © 2021,2022 IsardVDI S.L.
|
||||||
|
# Copyright © 2022 Evilham <contact@evilham.com>
|
||||||
#
|
#
|
||||||
# This file is part of DD
|
# This file is part of DD
|
||||||
#
|
#
|
||||||
|
@ -39,14 +40,17 @@ from flask_socketio import (
|
||||||
send,
|
send,
|
||||||
)
|
)
|
||||||
|
|
||||||
from admin import app
|
from admin import get_app
|
||||||
|
# Set up the app
|
||||||
|
app = get_app()
|
||||||
|
app.setup()
|
||||||
|
|
||||||
app.socketio = SocketIO(app)
|
app.socketio = SocketIO(app)
|
||||||
|
|
||||||
|
|
||||||
@app.socketio.on("connect", namespace="/sio")
|
@app.socketio.on("connect", namespace="/sio")
|
||||||
@login_required
|
@login_required
|
||||||
def socketio_connect():
|
def socketio_connect() -> None:
|
||||||
if current_user.id:
|
if current_user.id:
|
||||||
join_room("admin")
|
join_room("admin")
|
||||||
app.socketio.emit(
|
app.socketio.emit(
|
||||||
|
@ -57,12 +61,12 @@ def socketio_connect():
|
||||||
|
|
||||||
|
|
||||||
@app.socketio.on("disconnect", namespace="/sio")
|
@app.socketio.on("disconnect", namespace="/sio")
|
||||||
def socketio_disconnect():
|
def socketio_disconnect() -> None:
|
||||||
leave_room("admin")
|
leave_room("admin")
|
||||||
|
|
||||||
|
|
||||||
@app.socketio.on("connect", namespace="/sio/events")
|
@app.socketio.on("connect", namespace="/sio/events")
|
||||||
def socketio_connect():
|
def socketio_connect() -> None:
|
||||||
jwt = get_token_payload(request.args.get("jwt"))
|
jwt = get_token_payload(request.args.get("jwt"))
|
||||||
|
|
||||||
join_room("events")
|
join_room("events")
|
||||||
|
@ -75,7 +79,7 @@ def socketio_connect():
|
||||||
|
|
||||||
|
|
||||||
@app.socketio.on("disconnect", namespace="/sio/events")
|
@app.socketio.on("disconnect", namespace="/sio/events")
|
||||||
def socketio_events_disconnect():
|
def socketio_events_disconnect() -> None:
|
||||||
leave_room("events")
|
leave_room("events")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -43,9 +43,11 @@ services:
|
||||||
- ${DATA_FOLDER}/moodle/saml2:/admin/moodledata/saml2:rw
|
- ${DATA_FOLDER}/moodle/saml2:/admin/moodledata/saml2:rw
|
||||||
- ${DATA_FOLDER}/saml_certs:/admin/saml_certs:rw
|
- ${DATA_FOLDER}/saml_certs:/admin/saml_certs:rw
|
||||||
- ${DATA_FOLDER}/legal:/admin/admin/static/templates/pages/legal:rw
|
- ${DATA_FOLDER}/legal:/admin/admin/static/templates/pages/legal:rw
|
||||||
|
- ${DATA_FOLDER}/dd-admin:/data:rw
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- VERIFY="false" # In development do not verify certificates
|
- VERIFY="false" # In development do not verify certificates
|
||||||
- DOMAIN=${DOMAIN}
|
- DOMAIN=${DOMAIN}
|
||||||
- MANAGED_EMAIL_DOMAIN=${MANAGED_EMAIL_DOMAIN}
|
- MANAGED_EMAIL_DOMAIN=${MANAGED_EMAIL_DOMAIN}
|
||||||
|
- SECRETS=/data/secret
|
||||||
|
|
Loading…
Reference in New Issue