From c2fc854f165f3dc5126b5c88971e2a05d7c63477 Mon Sep 17 00:00:00 2001 From: darta Date: Fri, 15 Apr 2022 19:22:34 +0200 Subject: [PATCH 01/14] feat(admin): opened basic api with jwt auth --- admin/docker/requirements.pip3 | 1 + admin/src/admin/lib/load_config.py | 3 ++ admin/src/admin/views/InternalViews.py | 16 ++++---- admin/src/admin/views/decorators.py | 53 ++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/admin/docker/requirements.pip3 b/admin/docker/requirements.pip3 index ffaf19a..5d387f6 100644 --- a/admin/docker/requirements.pip3 +++ b/admin/docker/requirements.pip3 @@ -12,5 +12,6 @@ minio==7.0.3 urllib3==1.26.6 schema==0.7.5 Werkzeug~=2.0.0 +python-jose==3.3.0 # Unused yet #flask-oidc==1.4.0 diff --git a/admin/src/admin/lib/load_config.py b/admin/src/admin/lib/load_config.py index 58b8d9a..cb66a37 100644 --- a/admin/src/admin/lib/load_config.py +++ b/admin/src/admin/lib/load_config.py @@ -34,6 +34,9 @@ class loadConfig: 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 diff --git a/admin/src/admin/views/InternalViews.py b/admin/src/admin/views/InternalViews.py index ee4c4c5..b99e00e 100644 --- a/admin/src/admin/views/InternalViews.py +++ b/admin/src/admin/views/InternalViews.py @@ -11,12 +11,14 @@ from flask import request from admin import app -from .decorators import is_internal +from .decorators import is_internal, is_internal_or_has_token +import socket @app.route("/api/internal/users", methods=["GET"]) -@is_internal +@is_internal_or_has_token def internal_users(): + log.error(socket.gethostbyname("isard-apps-wordpress")) if request.method == "GET": sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k["username"]) # group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']] @@ -29,7 +31,7 @@ def internal_users(): @app.route("/api/internal/users/filter", methods=["POST"]) -@is_internal +@is_internal_or_has_token def internal_users_search(): if request.method == "POST": data = request.get_json(force=True) @@ -40,7 +42,7 @@ def internal_users_search(): @app.route("/api/internal/groups", methods=["GET"]) -@is_internal +@is_internal_or_has_token def internal_groups(): if request.method == "GET": sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k["name"]) @@ -59,7 +61,7 @@ def internal_groups(): @app.route("/api/internal/group/users", methods=["POST"]) -@is_internal +@is_internal_or_has_token def internal_group_users(): if request.method == "POST": data = request.get_json(force=True) @@ -78,7 +80,7 @@ def internal_group_users(): @app.route("/api/internal/roles", methods=["GET"]) -@is_internal +@is_internal_or_has_token def internal_roles(): if request.method == "GET": roles = [] @@ -96,7 +98,7 @@ def internal_roles(): @app.route("/api/internal/role/users", methods=["POST"]) -@is_internal +@is_internal_or_has_token def internal_role_users(): if request.method == "POST": data = request.get_json(force=True) diff --git a/admin/src/admin/views/decorators.py b/admin/src/admin/views/decorators.py index 55ef0fe..441fde8 100644 --- a/admin/src/admin/views/decorators.py +++ b/admin/src/admin/views/decorators.py @@ -3,6 +3,10 @@ import socket from functools import wraps +import json +import os +from jose import jwt +from ..auth.tokens import get_header_jwt_payload from flask import redirect, request, url_for from flask_login import current_user, logout_user @@ -34,3 +38,52 @@ def is_internal(fn): return redirect(url_for("login")) return decorated_view + +def has_token(fn): + @wraps(fn) + def decorated(*args, **kwargs): + payload = get_header_jwt_payload() + # if payload.get("role_id") != "admin": + # maintenance() + kwargs["payload"] = payload + return fn(*args, **kwargs) + + return decorated + +def is_internal_or_has_token(fn): + @wraps(fn) + def decorated_view(*args, **kwargs): + remote_addr = ( + request.headers["X-Forwarded-For"].split(",")[0] + if "X-Forwarded-For" in request.headers + else request.remote_addr.split(",")[0] + ) + ## Now only checks if it is wordpress container, + ## but we should check if it is internal net and not haproxy + valid_jwt = False + try: + payload = get_header_jwt_payload() + valid_jwt = True + except: + valid_jwt = False + if valid_jwt: + return fn(*args, **kwargs) + else: + return ( + json.dumps( + { + "error": "unauthorized", + "msg": "Unauthorized access", + } + ), + 401, + {"Content-Type": "application/json"}, + ) + + if socket.gethostbyname("isard-apps-wordpress") == remote_addr: + return fn(*args, **kwargs) + else: + logout_user() + return redirect(url_for("login")) + + return decorated_view \ No newline at end of file From e9a6c7108d87cc698fa62684b57be1f138056f43 Mon Sep 17 00:00:00 2001 From: darta Date: Sun, 17 Apr 2022 00:12:46 +0200 Subject: [PATCH 02/14] feat(api): users and groups actions --- admin/docker/requirements.pip3 | 5 +- admin/src/admin/__init__.py | 2 +- admin/src/admin/auth/tokens.py | 96 +++ admin/src/admin/lib/admin.py | 8 + admin/src/admin/lib/api_exceptions.py | 143 ++++ admin/src/admin/lib/load_config.py | 40 +- admin/src/admin/schemas/group.yml | 11 + admin/src/admin/schemas/user.yml | 29 + admin/src/admin/schemas/user_update.yml | 30 + admin/src/admin/views/ApiViews.py | 775 ++++++------------ .../views/{InternalViews.py => WpViews.py} | 16 +- admin/src/admin/views/decorators.py | 67 +- admin/src/start.py | 10 +- 13 files changed, 667 insertions(+), 565 deletions(-) create mode 100644 admin/src/admin/auth/tokens.py create mode 100644 admin/src/admin/lib/api_exceptions.py create mode 100644 admin/src/admin/schemas/group.yml create mode 100644 admin/src/admin/schemas/user.yml create mode 100644 admin/src/admin/schemas/user_update.yml rename admin/src/admin/views/{InternalViews.py => WpViews.py} (95%) diff --git a/admin/docker/requirements.pip3 b/admin/docker/requirements.pip3 index 5d387f6..5d6ee2c 100644 --- a/admin/docker/requirements.pip3 +++ b/admin/docker/requirements.pip3 @@ -3,7 +3,6 @@ Flask-Login==0.5.0 eventlet==0.33.0 Flask-SocketIO==5.1.0 bcrypt==3.2.0 - diceware==0.9.6 mysql-connector-python==8.0.25 psycopg2==2.8.6 @@ -13,5 +12,5 @@ urllib3==1.26.6 schema==0.7.5 Werkzeug~=2.0.0 python-jose==3.3.0 -# Unused yet -#flask-oidc==1.4.0 +Cerberus==1.3.4 +PyYAML==6.0 diff --git a/admin/src/admin/__init__.py b/admin/src/admin/__init__.py index a5c52cf..2fc10bd 100644 --- a/admin/src/admin/__init__.py +++ b/admin/src/admin/__init__.py @@ -108,4 +108,4 @@ def send_custom(path): """ Import all views """ -from .views import ApiViews, InternalViews, LoginViews, WebViews +from .views import ApiViews, AppViews, LoginViews, WebViews, WpViews diff --git a/admin/src/admin/auth/tokens.py b/admin/src/admin/auth/tokens.py new file mode 100644 index 0000000..3f55578 --- /dev/null +++ b/admin/src/admin/auth/tokens.py @@ -0,0 +1,96 @@ +# Copyright 2017 the Isard-vdi project authors: +# Josep Maria ViƱolas Auquer +# Alberto Larraz Dalmases +# License: AGPLv3 + +import json +import logging as log +import os +import traceback +from functools import wraps + +from flask import request +from jose import jwt + +from admin import app + +from ..lib.api_exceptions import Error + + +def get_header_jwt_payload(): + return get_token_payload(get_token_auth_header()) + + +def get_token_header(header): + """Obtains the Access Token from the a Header""" + auth = request.headers.get(header, None) + if not auth: + raise Error( + "unauthorized", + "Authorization header is expected", + traceback.format_stack(), + ) + + parts = auth.split() + if parts[0].lower() != "bearer": + raise Error( + "unauthorized", + "Authorization header must start with Bearer", + traceback.format_stack(), + ) + + elif len(parts) == 1: + raise Error("bad_request", "Token not found") + elif len(parts) > 2: + raise Error( + "unauthorized", + "Authorization header must be Bearer token", + traceback.format_stack(), + ) + + return parts[1] # Token + + +def get_token_auth_header(): + return get_token_header("Authorization") + + +def get_token_payload(token): + try: + claims = jwt.get_unverified_claims(token) + secret = app.config["API_SECRET"] + + except: + log.warning("JWT token with invalid parameters. Can not parse it.") + raise Error( + "unauthorized", + "Unable to parse authentication parameters token.", + traceback.format_stack(), + ) + + try: + payload = jwt.decode( + token, + secret, + algorithms=["HS256"], + options=dict(verify_aud=False, verify_sub=False, verify_exp=True), + ) + except jwt.ExpiredSignatureError: + log.info("Token expired") + raise Error("unauthorized", "Token is expired", traceback.format_stack()) + + except jwt.JWTClaimsError: + raise Error( + "unauthorized", + "Incorrect claims, please check the audience and issuer", + traceback.format_stack(), + ) + except Exception: + raise Error( + "unauthorized", + "Unable to parse authentication token.", + traceback.format_stack(), + ) + if payload.get("data", False): + return payload["data"] + return payload diff --git a/admin/src/admin/lib/admin.py b/admin/src/admin/lib/admin.py index 641c2cd..1286cfb 100644 --- a/admin/src/admin/lib/admin.py +++ b/admin/src/admin/lib/admin.py @@ -30,6 +30,7 @@ options.num = 3 import secrets +from .api_exceptions import Error from .events import Events from .exceptions import UserExists, UserNotFound from .helpers import ( @@ -466,6 +467,10 @@ class Admin: def _get_roles(self): return filter_roles_listofdicts(self.keycloak.get_roles()) + def get_group_by_name(self, group_name): + group = [g for g in self.internal["groups"] if g["name"] == group_name] + return group[0] if len(group) else False + def get_keycloak_groups(self): log.warning("Loading keycloak groups...") return self.keycloak.get_groups() @@ -1812,6 +1817,7 @@ class Admin: log.error(traceback.format_exc()) self.resync_data() + return uid def add_group(self, g): # TODO: Check if exists @@ -1830,6 +1836,7 @@ class Admin: self.moodle.add_system_cohort(new_path, description=g["description"]) self.nextcloud.add_group(new_path) self.resync_data() + return new_path def delete_group_by_id(self, group_id): ev = Events("Deleting group", "Deleting from keycloak") @@ -1843,6 +1850,7 @@ class Admin: + str(group_id) + " as it does not exist!" ) + raise Error("not_found", "Group " + group_id + " not found.") # {'id': '966ad67c-499a-4f56-bd1d-283691cde0e7', 'name': 'asdgfewfwe', 'path': '/asdgfewfwe', 'attributes': {}, 'realmRoles': [], 'clientRoles': {}, 'subGroups': [], 'access': {'view': True, 'manage': True, 'manageMembership': True}} diff --git a/admin/src/admin/lib/api_exceptions.py b/admin/src/admin/lib/api_exceptions.py new file mode 100644 index 0000000..3873f22 --- /dev/null +++ b/admin/src/admin/lib/api_exceptions.py @@ -0,0 +1,143 @@ +import inspect +import json +import logging as log +import os +import traceback + +from flask import jsonify, request + +from admin import app + +content_type = {"Content-Type": "application/json"} +ex = { + "bad_request": { + "error": { + "error": "bad_request", + "msg": "Bad request", + }, + "status_code": 400, + }, + "unauthorized": { + "error": { + "error": "unauthorized", + "msg": "Unauthorized", + }, + "status_code": 401, + }, + "forbidden": { + "error": { + "error": "forbidden", + "msg": "Forbidden", + }, + "status_code": 403, + }, + "not_found": { + "error": { + "error": "not_found", + "msg": "Not found", + }, + "status_code": 404, + }, + "conflict": { + "error": { + "error": "conflict", + "msg": "Conflict", + }, + "status_code": 409, + }, + "internal_server": { + "error": { + "error": "internal_server", + "msg": "Internal server error", + }, + "status_code": 500, + }, + "gateway_timeout": { + "error": { + "error": "gateway_timeout", + "msg": "Gateway timeout", + }, + "status_code": 504, + }, + "precondition_required": { + "error": { + "error": "precondition_required", + "msg": "Precondition required", + }, + "status_code": 428, + }, + "insufficient_storage": { + "error": { + "error": "insufficient_storage", + "msg": "Insufficient storage", + }, + "status_code": 507, + }, +} + + +class Error(Exception): + def __init__(self, error="bad_request", description="", debug="", data=None): + self.error = ex[error]["error"].copy() + self.error["function"] = ( + inspect.stack()[1][1].split(os.sep)[-1] + + ":" + + str(inspect.stack()[1][2]) + + ":" + + inspect.stack()[1][3] + ) + self.error["function_call"] = ( + inspect.stack()[2][1].split(os.sep)[-1] + + ":" + + str(inspect.stack()[2][2]) + + ":" + + inspect.stack()[2][3] + ) + self.error["description"] = str(description) + self.error["debug"] = "{}\n\r{}{}".format( + "----------- DEBUG START -------------", + debug, + "----------- DEBUG STOP -------------", + ) + self.error["request"] = ( + "{}\n{}\r\n{}\r\n\r\n{}{}".format( + "----------- REQUEST START -----------", + request.method + " " + request.url, + "\r\n".join("{}: {}".format(k, v) for k, v in request.headers.items()), + request.body if hasattr(request, "body") else "", + "----------- REQUEST STOP -----------", + ) + if request + else "" + ) + self.error["data"] = ( + "{}\n{}\n{}".format( + "----------- DATA START -----------", + json.dumps(data, indent=2), + "----------- DATA STOP -----------", + ) + if data + else "" + ) + self.status_code = ex[error]["status_code"] + self.content_type = content_type + log.debug( + "%s - %s - [%s -> %s]\r\n%s\r\n%s\r\n%s" + % ( + error, + str(description), + self.error["function_call"], + self.error["function"], + self.error["debug"], + self.error["request"], + 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 diff --git a/admin/src/admin/lib/load_config.py b/admin/src/admin/lib/load_config.py index cb66a37..ba193d8 100644 --- a/admin/src/admin/lib/load_config.py +++ b/admin/src/admin/lib/load_config.py @@ -6,9 +6,45 @@ 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: @@ -34,9 +70,7 @@ class loadConfig: app.config.setdefault( "VERIFY", True if os.environ["VERIFY"] == "true" else False ) - app.config.setdefault( - "API_SECRET", os.environ.get("API_SECRET") - ) + app.config.setdefault("API_SECRET", os.environ.get("API_SECRET")) except Exception as e: log.error(traceback.format_exc()) raise diff --git a/admin/src/admin/schemas/group.yml b/admin/src/admin/schemas/group.yml new file mode 100644 index 0000000..fb1e631 --- /dev/null +++ b/admin/src/admin/schemas/group.yml @@ -0,0 +1,11 @@ +name: + required: true + type: string +description: + required: false + type: string + default: "Api created" +parent: + required: false + type: string + default: "" \ No newline at end of file diff --git a/admin/src/admin/schemas/user.yml b/admin/src/admin/schemas/user.yml new file mode 100644 index 0000000..b94083d --- /dev/null +++ b/admin/src/admin/schemas/user.yml @@ -0,0 +1,29 @@ +username: + required: true + type: string +first: + required: true + type: string +last: + required: true + type: string +email: + required: true + type: string +password: + required: true + type: string +quota: + required: true + type: string +enabled: + required: true + type: boolean +role: + required: true + type: string + empty: false +groups: + required: true + type: list + diff --git a/admin/src/admin/schemas/user_update.yml b/admin/src/admin/schemas/user_update.yml new file mode 100644 index 0000000..6bc8c56 --- /dev/null +++ b/admin/src/admin/schemas/user_update.yml @@ -0,0 +1,30 @@ +first: + required: false + type: string +last: + required: false + type: string +email: + required: false + type: string +password: + required: false + type: string +password_temporary: + required: false + type: boolean + default: true +quota: + required: false + type: string +enabled: + required: false + type: boolean +role: + required: false + type: string + empty: false +groups: + required: false + type: list + diff --git a/admin/src/admin/views/ApiViews.py b/admin/src/admin/views/ApiViews.py index 1fdc6dd..91d152e 100644 --- a/admin/src/admin/views/ApiViews.py +++ b/admin/src/admin/views/ApiViews.py @@ -1,545 +1,298 @@ #!flask/bin/python # coding=utf-8 -import concurrent.futures import json import logging as log import os -import re +import socket import sys - -# import Queue -import threading import time import traceback -from uuid import uuid4 -from flask import Response, jsonify, redirect, render_template, request, url_for -from flask_login import current_user, login_required +from flask import request from admin import app -from ..lib.helpers import system_group -from .decorators import is_admin - -threads = {"external": None} -# q = Queue.Queue() - -from keycloak.exceptions import KeycloakGetError - -from ..lib.dashboard import Dashboard -from ..lib.exceptions import UserExists, UserNotFound - -dashboard = Dashboard() +from ..lib.api_exceptions import Error +from .decorators import has_token -@app.route("/sysadmin/api/resync") -@app.route("/api/resync") -@login_required -def resync(): - return ( - json.dumps(app.admin.resync_data()), - 200, - {"Content-Type": "application/json"}, - ) +## LISTS +@app.route("/ddapi/users", methods=["GET"]) +@has_token +def ddapi_users(): + if request.method == "GET": + sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k["username"]) + users = [] + for user in sorted_users: + users.append(user_parser(user)) + return json.dumps(users), 200, {"Content-Type": "application/json"} -@app.route("/api/users", methods=["GET", "PUT"]) -@app.route("/api/users/", methods=["POST", "PUT", "GET", "DELETE"]) -@login_required -def users(provider=False): - if request.method == "DELETE": - if current_user.role != "admin": - return json.dumps({}), 301, {"Content-Type": "application/json"} - if provider == "keycloak": - return ( - json.dumps(app.admin.delete_keycloak_users()), - 200, - {"Content-Type": "application/json"}, - ) - if provider == "nextcloud": - return ( - json.dumps(app.admin.delete_nextcloud_users()), - 200, - {"Content-Type": "application/json"}, - ) - if provider == "moodle": - return ( - json.dumps(app.admin.delete_moodle_users()), - 200, - {"Content-Type": "application/json"}, - ) +@app.route("/ddapi/users/filter", methods=["POST"]) +@has_token +def ddapi_users_search(): if request.method == "POST": - if current_user.role != "admin": - return json.dumps({}), 301, {"Content-Type": "application/json"} - if provider == "moodle": - return ( - json.dumps(app.admin.sync_to_moodle()), - 200, - {"Content-Type": "application/json"}, - ) - if provider == "nextcloud": - return ( - json.dumps(app.admin.sync_to_nextcloud()), - 200, - {"Content-Type": "application/json"}, - ) - if request.method == "PUT" and not provider: - if current_user.role != "admin": - return json.dumps({}), 301, {"Content-Type": "application/json"} + data = request.get_json(force=True) + if not data.get("text"): + raise Error("bad_request", "Incorrect data requested.") + users = app.admin.get_mix_users() + result = [user_parser(user) for user in filter_users(users, data["text"])] + sorted_result = sorted(result, key=lambda k: k["id"]) + return json.dumps(sorted_result), 200, {"Content-Type": "application/json"} - if "external" in threads.keys(): - if threads["external"] is not None and threads["external"].is_alive(): - return ( - json.dumps( - {"msg": "Precondition failed: already working with users"} - ), - 412, - {"Content-Type": "application/json"}, - ) - else: - threads["external"] = None - try: - threads["external"] = threading.Thread( - target=app.admin.update_users_from_keycloak, args=() - ) - threads["external"].start() - return json.dumps({}), 200, {"Content-Type": "application/json"} - except: - log.error(traceback.format_exc()) - return ( - json.dumps({"msg": "Add user error."}), - 500, - {"Content-Type": "application/json"}, - ) - # return json.dumps(app.admin.update_users_from_keycloak()), 200, {'Content-Type': 'application/json'} +@app.route("/ddapi/groups", methods=["GET"]) +@has_token +def ddapi_groups(): + if request.method == "GET": + sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k["name"]) + groups = [] + for group in sorted_groups: + groups.append(group_parser(group)) + return json.dumps(groups), 200, {"Content-Type": "application/json"} - users = app.admin.get_mix_users() - if current_user.role != "admin": - for user in users: - user["keycloak_groups"] = [ - g for g in user["keycloak_groups"] if not system_group(g) + +@app.route("/ddapi/group/users", methods=["POST"]) +@has_token +def ddapi_group_users(): + if request.method == "POST": + data = request.get_json(force=True) + sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k["username"]) + if data.get("id"): + group_users = [ + user_parser(user) + for user in sorted_users + if data.get("id") in user["keycloak_groups"] ] - return json.dumps(users), 200, {"Content-Type": "application/json"} - - -@app.route("/api/users_bulk/", methods=["PUT"]) -@login_required -def users_bulk(action): - data = request.get_json(force=True) - if request.method == "PUT": - if action == "enable": - if "external" in threads.keys(): - if threads["external"] is not None and threads["external"].is_alive(): - return ( - json.dumps( - {"msg": "Precondition failed: already operating users"} - ), - 412, - {"Content-Type": "application/json"}, - ) - else: - threads["external"] = None + elif data.get("path"): try: - threads["external"] = threading.Thread( - target=app.admin.enable_users, args=(data,) - ) - threads["external"].start() - return json.dumps({}), 200, {"Content-Type": "application/json"} + name = [ + g["name"] + for g in app.admin.get_mix_groups() + if g["path"] == data.get("path") + ][0] + group_users = [ + user_parser(user) + for user in sorted_users + if name in user["keycloak_groups"] + ] except: - log.error(traceback.format_exc()) - return ( - json.dumps({"msg": "Enable users error."}), - 500, - {"Content-Type": "application/json"}, - ) - if action == "disable": - if "external" in threads.keys(): - if threads["external"] is not None and threads["external"].is_alive(): - return ( - json.dumps( - {"msg": "Precondition failed: already operating users"} - ), - 412, - {"Content-Type": "application/json"}, - ) - else: - threads["external"] = None + raise Error("not_found", "Group path not found in system") + elif data.get("keycloak_id"): try: - threads["external"] = threading.Thread( - target=app.admin.disable_users, args=(data,) - ) - threads["external"].start() - return json.dumps({}), 200, {"Content-Type": "application/json"} + name = [ + g["name"] + for g in app.admin.get_mix_groups() + if g["id"] == data.get("keycloak_id") + ][0] + group_users = [ + user_parser(user) + for user in sorted_users + if name in user["keycloak_groups"] + ] except: - log.error(traceback.format_exc()) - return ( - json.dumps({"msg": "Disabling users error."}), - 500, - {"Content-Type": "application/json"}, - ) - if action == "delete": - if "external" in threads.keys(): - if threads["external"] is not None and threads["external"].is_alive(): - return ( - json.dumps( - {"msg": "Precondition failed: already operating users"} - ), - 412, - {"Content-Type": "application/json"}, - ) - else: - threads["external"] = None - try: - threads["external"] = threading.Thread( - target=app.admin.delete_users, args=(data,) - ) - threads["external"].start() - return json.dumps({}), 200, {"Content-Type": "application/json"} - except: - log.error(traceback.format_exc()) - return ( - json.dumps({"msg": "Deleting users error."}), - 500, - {"Content-Type": "application/json"}, - ) - return json.dumps({}), 405, {"Content-Type": "application/json"} - - -# Update pwd -@app.route("/api/user_password", methods=["GET"]) -@app.route("/api/user_password/", methods=["PUT"]) -@login_required -def user_password(userid=False): - if request.method == "GET": - return ( - json.dumps(app.admin.get_dice_pwd()), - 200, - {"Content-Type": "application/json"}, - ) - if request.method == "PUT": - data = request.get_json(force=True) - password = data["password"] - temporary = data.get("temporary", True) - try: - res = app.admin.user_update_password(userid, password, temporary) - return json.dumps({}), 200, {"Content-Type": "application/json"} - except KeycloakGetError as e: - log.error(e.error_message.decode("utf-8")) - return ( - json.dumps({"msg": "Update password error."}), - 500, - {"Content-Type": "application/json"}, - ) - - return json.dumps({}), 405, {"Content-Type": "application/json"} - - -# User -@app.route("/api/user", methods=["POST"]) -@app.route("/api/user/", methods=["PUT", "GET", "DELETE"]) -@login_required -def user(userid=None): - if request.method == "DELETE": - app.admin.delete_user(userid) - return json.dumps({}), 200, {"Content-Type": "application/json"} - if request.method == "POST": - data = request.get_json(force=True) - if app.admin.get_user_username(data["username"]): - return ( - json.dumps({"msg": "Add user error: already exists."}), - 409, - {"Content-Type": "application/json"}, - ) - data["enabled"] = True if data.get("enabled", False) else False - data["quota"] = data["quota"] if data["quota"] != "false" else False - data["groups"] = data["groups"] if data.get("groups", False) else [] - if "external" in threads.keys(): - if threads["external"] is not None and threads["external"].is_alive(): - return ( - json.dumps({"msg": "Precondition failed: already adding users"}), - 412, - {"Content-Type": "application/json"}, - ) - else: - threads["external"] = None - try: - threads["external"] = threading.Thread( - target=app.admin.add_user, args=(data,) - ) - threads["external"].start() - return json.dumps({}), 200, {"Content-Type": "application/json"} - except: - log.error(traceback.format_exc()) - return ( - json.dumps({"msg": "Add user error."}), - 500, - {"Content-Type": "application/json"}, - ) - - if request.method == "PUT": - data = request.get_json(force=True) - data["enabled"] = True if data.get("enabled", False) else False - data["groups"] = data["groups"] if data.get("groups", False) else [] - data["roles"] = [data.pop("role-keycloak")] - try: - app.admin.user_update(data) - return json.dumps({}), 200, {"Content-Type": "application/json"} - except UserNotFound: - return ( - json.dumps({"msg": "User not found."}), - 404, - {"Content-Type": "application/json"}, - ) - if request.method == "DELETE": - pass - if request.method == "GET": - user = app.admin.get_user(userid) - if not user: - return ( - json.dumps({"msg": "User not found."}), - 404, - {"Content-Type": "application/json"}, - ) - return json.dumps(user), 200, {"Content-Type": "application/json"} - - -@app.route("/api/roles") -@login_required -def roles(): - sorted_roles = sorted(app.admin.get_roles(), key=lambda k: k["name"]) - if current_user.role != "admin": - sorted_roles = [sr for sr in sorted_roles if sr["name"] != "admin"] - return json.dumps(sorted_roles), 200, {"Content-Type": "application/json"} - - -@app.route("/api/group", methods=["POST", "DELETE"]) -@app.route("/api/group/", methods=["PUT", "GET", "DELETE"]) -@login_required -def group(group_id=False): - if request.method == "POST": - data = request.get_json(force=True) - data["parent"] = data["parent"] if data["parent"] != "" else None - return ( - json.dumps(app.admin.add_group(data)), - 200, - {"Content-Type": "application/json"}, - ) - if request.method == "DELETE": - try: - data = request.get_json(force=True) - except: - data = False - - if data: - res = app.admin.delete_group_by_path(data["path"]) + raise Error("not_found", "Group keycloak_id not found in system") else: - res = app.admin.delete_group_by_id(group_id) - return json.dumps(res), 200, {"Content-Type": "application/json"} + raise Error("bad_request", "Incorrect data requested.") + return json.dumps(group_users), 200, {"Content-Type": "application/json"} -@app.route("/api/groups") -@app.route("/api/groups/", methods=["POST", "PUT", "GET", "DELETE"]) -@login_required -def groups(provider=False): +@app.route("/ddapi/roles", methods=["GET"]) +@has_token +def ddapi_roles(): if request.method == "GET": - sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: str(k["name"])) - if current_user.role != "admin": - ## internal groups should be avoided as are assigned with the role - sorted_groups = [sg for sg in sorted_groups if not system_group(sg["name"])] - else: - sorted_groups = [sg for sg in sorted_groups] - return json.dumps(sorted_groups), 200, {"Content-Type": "application/json"} - if request.method == "DELETE": - if provider == "keycloak": - return ( - json.dumps(app.admin.delete_keycloak_groups()), - 200, - {"Content-Type": "application/json"}, - ) - - -### SYSADM USERS ONLY - - -@app.route("/api/external", methods=["POST", "PUT", "GET", "DELETE"]) -@login_required -def external(): - if "external" in threads.keys(): - if threads["external"] is not None and threads["external"].is_alive(): - return json.dumps({}), 301, {"Content-Type": "application/json"} - else: - threads["external"] = None - - if request.method == "POST": - data = request.get_json(force=True) - if data["format"] == "json-ga": - threads["external"] = threading.Thread( - target=app.admin.upload_json_ga, args=(data,) - ) - threads["external"].start() - return json.dumps({}), 200, {"Content-Type": "application/json"} - if data["format"] == "csv-ug": - valid = check_upload_errors(data) - if valid["pass"]: - threads["external"] = threading.Thread( - target=app.admin.upload_csv_ug, args=(data,) - ) - threads["external"].start() - return json.dumps({}), 200, {"Content-Type": "application/json"} - else: - return json.dumps(valid), 422, {"Content-Type": "application/json"} - if request.method == "PUT": - data = request.get_json(force=True) - threads["external"] = threading.Thread( - target=app.admin.sync_external, args=(data,) - ) - threads["external"].start() - return json.dumps({}), 200, {"Content-Type": "application/json"} - if request.method == "DELETE": - print("RESET") - app.admin.reset_external() - return json.dumps({}), 200, {"Content-Type": "application/json"} - return json.dumps({}), 500, {"Content-Type": "application/json"} - - -@app.route("/api/external/users") -@login_required -def external_users_list(): - while threads["external"] is not None and threads["external"].is_alive(): - time.sleep(0.5) - return ( - json.dumps(app.admin.get_external_users()), - 200, - {"Content-Type": "application/json"}, - ) - - -@app.route("/api/external/groups") -@login_required -def external_groups_list(): - while threads["external"] is not None and threads["external"].is_alive(): - time.sleep(0.5) - return ( - json.dumps(app.admin.get_external_groups()), - 200, - {"Content-Type": "application/json"}, - ) - - -@app.route("/api/external/roles", methods=["PUT"]) -@login_required -def external_roles(): - if request.method == "PUT": - return ( - json.dumps(app.admin.external_roleassign(request.get_json(force=True))), - 200, - {"Content-Type": "application/json"}, - ) - - -def check_upload_errors(data): - email_regex = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b" - for u in data["data"]: - try: - user_groups = [g.strip() for g in u["groups"].split(",")] - except: - resp = { - "pass": False, - "msg": "User " + u["username"] + " has invalid groups: " + u["groups"], - } - log.error(resp) - return resp - - if not re.fullmatch(email_regex, u["email"]): - resp = { - "pass": False, - "msg": "User " + u["username"] + " has invalid email: " + u["email"], - } - log.error(resp) - return resp - - if u["role"] not in ["admin", "manager", "teacher", "student"]: - if u["role"] == "": - resp = { - "pass": False, - "msg": "User " + u["username"] + " has no role assigned!", + roles = [] + for role in sorted(app.admin.get_roles(), key=lambda k: k["name"]): + log.error(role) + roles.append( + { + "keycloak_id": role["id"], + "id": role["name"], + "name": role["name"], + "description": role.get("description", ""), } - log.error(resp) - return resp - resp = { - "pass": False, - "msg": "User " + u["username"] + " has invalid role: " + u["role"], - } - log.error(resp) - return resp - return {"pass": True, "msg": ""} - - -@app.route("/api/dashboard/", methods=["PUT"]) -# @login_required -def dashboard_put(item): - if item == "colours": - try: - data = request.get_json(force=True) - dashboard.update_colours(data) - except: - log.error(traceback.format_exc()) - return json.dumps({"colours": data}), 200, {"Content-Type": "application/json"} - if item == "menu": - try: - data = request.get_json(force=True) - dashboard.update_menu(data) - except: - log.error(traceback.format_exc()) - return json.dumps(data), 200, {"Content-Type": "application/json"} - if item == "logo": - dashboard.update_logo(request.files["croppedImage"]) - return json.dumps({}), 200, {"Content-Type": "application/json"} - if item == "background": - dashboard.update_background(request.files["croppedImage"]) - return json.dumps({}), 200, {"Content-Type": "application/json"} - return ( - json.dumps( - { - "error": "update_error", - "msg": "Error updating item " + item + "\n" + traceback.format_exc(), - } - ), - 500, - {"Content-Type": "application/json"}, - ) - - -@app.route("/api/legal/", methods=["GET", "POST"]) -# @login_required -def legal_put(item): - if request.method == "GET": - if item == "legal": - lang = request.args.get("lang") - return ( - json.dumps({"html": "Legal
This works! in lang: " + lang}), - 200, - {"Content-Type": "application/json"}, ) - # if item == "privacy": - # return json.dumps({ "html": "Privacy policy
This works!"}), 200, {'Content-Type': 'application/json'} + return json.dumps(roles), 200, {"Content-Type": "application/json"} + + +@app.route("/ddapi/role/users", methods=["POST"]) +@has_token +def ddapi_role_users(): if request.method == "POST": - if item == "legal": - data = None + data = request.get_json(force=True) + sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k["username"]) + if data.get("id", data.get("name")): + role_users = [ + user_parser(user) + for user in sorted_users + if data.get("id", data.get("name")) in user["roles"] + ] + elif data.get("keycloak_id"): try: - data = request.json - html = data["html"] - lang = data["lang"] + id = [ + r["id"] + for r in app.admin.get_roles() + if r["id"] == data.get("keycloak_id") + ][0] + role_users = [ + user_parser(user) for user in sorted_users if id in user["roles"] + ] except: - log.error(traceback.format_exc()) - return json.dumps(data), 200, {"Content-Type": "application/json"} - # if item == "privacy": - # data = None - # try: - # data = request.json - # html = data["html"] - # lang = data["lang"] - # except: - # log.error(traceback.format_exc()) - # return json.dumps(data), 200, {'Content-Type': 'application/json'} + raise Error("not_found", "Role keycloak_id not found in system") + else: + raise Error("bad_request", "Incorrect data requested.") + return json.dumps(role_users), 200, {"Content-Type": "application/json"} + + +## INDIVIDUAL ACTIONS +@app.route("/ddapi/user", methods=["POST"]) +@app.route("/ddapi/user/", methods=["PUT", "GET", "DELETE"]) +@has_token +def ddapi_user(user_ddid=None): + if request.method == "GET": + user = app.admin.get_user_username(user_ddid) + if not user: + raise Error("not_found", "User id not found") + return json.dumps(user_parser(user)), 200, {"Content-Type": "application/json"} + if request.method == "DELETE": + user = app.admin.get_user_username(user_ddid) + if not user: + raise Error("not_found", "User id not found") + app.admin.delete_user(user["id"]) + return json.dumps({}), 200, {"Content-Type": "application/json"} + if request.method == "POST": + data = request.get_json(force=True) + if not app.validators["user"].validate(data): + raise Error( + "bad_request", + "Data validation for user failed: ", + +str(app.validators["user"].errors), + traceback.format_exc(), + ) + + if app.admin.get_user_username(data["username"]): + raise Error("conflict", "User id already exists") + keycloak_id = app.admin.add_user(data) + return ( + json.dumps({"keycloak_id": keycloak_id}), + 200, + {"Content-Type": "application/json"}, + ) + + if request.method == "PUT": + user = app.admin.get_user_username(user_ddid) + if not user: + raise Error("not_found", "User id not found") + data = request.get_json(force=True) + if not app.validators["user_update"].validate(data): + raise Error( + "bad_request", + "Data validation for user failed: " + + str(app.validators["user_update"].errors), + traceback.format_exc(), + ) + data = {**user, **data} + data = app.validators["user_update"].normalized(data) + data = {**data, **{"username": user_ddid}} + data["roles"] = [data.pop("role")] + data["firstname"] = data.pop("first") + data["lastname"] = data.pop("last") + app.admin.user_update(data) + if data.get("password"): + app.admin.user_update_password( + user["id"], data["password"], data["password_temporary"] + ) + return json.dumps({}), 200, {"Content-Type": "application/json"} + + +@app.route("/ddapi/username//", methods=["PUT"]) +@has_token +def ddapi_username(old_user_ddid, new_user_did): + user = app.admin.get_user_username(user_ddid) + if not user: + raise Error("not_found", "User id not found") + # user = app.admin.update_user_username(old_user_ddid,new_user_did) + return json.dumps("Not implemented yet!"), 419, {"Content-Type": "application/json"} + + +@app.route("/ddapi/group", methods=["POST"]) +@app.route("/ddapi/group/", methods=["GET", "POST", "DELETE"]) +# @app.route("/api/group/", methods=["PUT", "GET", "DELETE"]) +@has_token +def ddapi_group(id=None): + if request.method == "GET": + group = app.admin.get_group_by_name(id) + if not group: + Error("not found", "Group id not found") + return ( + json.dumps(group_parser(group)), + 200, + {"Content-Type": "application/json"}, + ) + if request.method == "POST": + data = request.get_json(force=True) + if not app.validators["group"].validate(data): + raise Error( + "bad_request", + "Data validation for group failed: " + + str(app.validators["group"].errors), + traceback.format_exc(), + ) + data = app.validators["group"].normalized(data) + data["parent"] = data["parent"] if data["parent"] != "" else None + + if app.admin.get_group_by_name(id): + raise Error("conflict", "Group id already exists") + + path = app.admin.add_group(data) + # log.error(path) + # keycloak_id = app.admin.get_group_by_name(id)["id"] + # log.error() + return ( + json.dumps({"keycloak_id": None}), + 200, + {"Content-Type": "application/json"}, + ) + if request.method == "DELETE": + group = app.admin.get_group_by_name(id) + if not group: + raise Error("not_found", "Group id not found") + app.admin.delete_group_by_id(group["id"]) + return json.dumps({}), 200, {"Content-Type": "application/json"} + + +def user_parser(user): + return { + "keycloak_id": user["id"], + "id": user["username"], + "username": user["username"], + "enabled": user["enabled"], + "first": user["first"], + "last": user["last"], + "role": user["roles"][0] if len(user["roles"]) else None, + "email": user["email"], + "groups": user.get("groups", user["keycloak_groups"]), + "quota": user["quota"], + "quota_used_bytes": user["quota_used_bytes"], + } + + +def group_parser(group): + return { + "keycloak_id": group["id"], + "id": group["name"], + "name": group["name"].split(".")[-1], + "path": group["path"], + "description": group.get("description", ""), + } + + +def filter_users(users, text): + return [ + user + for user in users + if text in user["username"] + or text in user["first"] + or text in user["last"] + or text in user["email"] + ] diff --git a/admin/src/admin/views/InternalViews.py b/admin/src/admin/views/WpViews.py similarity index 95% rename from admin/src/admin/views/InternalViews.py rename to admin/src/admin/views/WpViews.py index b99e00e..0c829dd 100644 --- a/admin/src/admin/views/InternalViews.py +++ b/admin/src/admin/views/WpViews.py @@ -3,6 +3,7 @@ import json import logging as log import os +import socket import sys import time import traceback @@ -11,12 +12,11 @@ from flask import request from admin import app -from .decorators import is_internal, is_internal_or_has_token +from .decorators import is_internal -import socket @app.route("/api/internal/users", methods=["GET"]) -@is_internal_or_has_token +@is_internal def internal_users(): log.error(socket.gethostbyname("isard-apps-wordpress")) if request.method == "GET": @@ -31,7 +31,7 @@ def internal_users(): @app.route("/api/internal/users/filter", methods=["POST"]) -@is_internal_or_has_token +@is_internal def internal_users_search(): if request.method == "POST": data = request.get_json(force=True) @@ -42,7 +42,7 @@ def internal_users_search(): @app.route("/api/internal/groups", methods=["GET"]) -@is_internal_or_has_token +@is_internal def internal_groups(): if request.method == "GET": sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k["name"]) @@ -61,7 +61,7 @@ def internal_groups(): @app.route("/api/internal/group/users", methods=["POST"]) -@is_internal_or_has_token +@is_internal def internal_group_users(): if request.method == "POST": data = request.get_json(force=True) @@ -80,7 +80,7 @@ def internal_group_users(): @app.route("/api/internal/roles", methods=["GET"]) -@is_internal_or_has_token +@is_internal def internal_roles(): if request.method == "GET": roles = [] @@ -98,7 +98,7 @@ def internal_roles(): @app.route("/api/internal/role/users", methods=["POST"]) -@is_internal_or_has_token +@is_internal def internal_role_users(): if request.method == "POST": data = request.get_json(force=True) diff --git a/admin/src/admin/views/decorators.py b/admin/src/admin/views/decorators.py index 441fde8..1497fd4 100644 --- a/admin/src/admin/views/decorators.py +++ b/admin/src/admin/views/decorators.py @@ -1,15 +1,17 @@ #!flask/bin/python # coding=utf-8 +import json +import logging as log +import os import socket from functools import wraps -import json -import os -from jose import jwt -from ..auth.tokens import get_header_jwt_payload from flask import redirect, request, url_for from flask_login import current_user, logout_user +from jose import jwt + +from ..auth.tokens import get_header_jwt_payload def is_admin(fn): @@ -34,22 +36,29 @@ def is_internal(fn): ## but we should check if it is internal net and not haproxy if socket.gethostbyname("isard-apps-wordpress") == remote_addr: return fn(*args, **kwargs) - logout_user() - return redirect(url_for("login")) + return ( + json.dumps( + { + "error": "unauthorized", + "msg": "Unauthorized access", + } + ), + 401, + {"Content-Type": "application/json"}, + ) return decorated_view + def has_token(fn): @wraps(fn) def decorated(*args, **kwargs): payload = get_header_jwt_payload() - # if payload.get("role_id") != "admin": - # maintenance() - kwargs["payload"] = payload return fn(*args, **kwargs) return decorated + def is_internal_or_has_token(fn): @wraps(fn) def decorated_view(*args, **kwargs): @@ -58,32 +67,22 @@ def is_internal_or_has_token(fn): if "X-Forwarded-For" in request.headers else request.remote_addr.split(",")[0] ) - ## Now only checks if it is wordpress container, - ## but we should check if it is internal net and not haproxy - valid_jwt = False - try: - payload = get_header_jwt_payload() - valid_jwt = True - except: - valid_jwt = False - if valid_jwt: - return fn(*args, **kwargs) - else: - return ( - json.dumps( - { - "error": "unauthorized", - "msg": "Unauthorized access", - } - ), - 401, - {"Content-Type": "application/json"}, - ) + payload = get_header_jwt_payload() if socket.gethostbyname("isard-apps-wordpress") == remote_addr: return fn(*args, **kwargs) - else: - logout_user() - return redirect(url_for("login")) + payload = get_header_jwt_payload() + return fn(*args, **kwargs) - return decorated_view \ No newline at end of file + return decorated_view + + +def login_or_token(fn): + @wraps(fn) + def decorated_view(*args, **kwargs): + if current_user.is_authenticated: + return fn(*args, **kwargs) + payload = get_header_jwt_payload() + return fn(*args, **kwargs) + + return decorated_view diff --git a/admin/src/start.py b/admin/src/start.py index 9111d3d..df970d5 100644 --- a/admin/src/start.py +++ b/admin/src/start.py @@ -41,9 +41,9 @@ if __name__ == "__main__": app, host="0.0.0.0", port=9000, - debug=False, - # ssl_context="adhoc", - # async_mode="threading", - ) # , logger=logger, engineio_logger=engineio_logger) + debug=True, + ) + # ssl_context="adhoc", + # async_mode="threading", + # ) # , logger=logger, engineio_logger=engineio_logger) # , cors_allowed_origins="*" -# /usr/lib/python3.8/site-packages/certifi From b82cf7922428de77a5396b9c0daaeecf263bf055 Mon Sep 17 00:00:00 2001 From: darta Date: Sun, 17 Apr 2022 00:45:24 +0200 Subject: [PATCH 03/14] feat(api): added basic api endpoint queries --- admin/src/tests/api.py | 425 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 425 insertions(+) create mode 100644 admin/src/tests/api.py diff --git a/admin/src/tests/api.py b/admin/src/tests/api.py new file mode 100644 index 0000000..422c897 --- /dev/null +++ b/admin/src/tests/api.py @@ -0,0 +1,425 @@ +import json +import os +import secrets +import time +import traceback +from pprint import pprint +from datetime import datetime +from datetime import timedelta + +from jose import jwt +import requests + +## SETUP +domain = "admin.[YOURDOMAIN]" +secret = "[your API_SECRET]" +## END SETUP + + +auths = {} +dbconn = None +base = "https://"+domain+"/ddapi" + +raw_jwt_data = { + "exp": datetime.utcnow() + timedelta(minutes=5), + "kid": "test", +} +admin_jwt = jwt.encode(raw_jwt_data, secret, algorithm="HS256") +jwt = {"Authorization": "Bearer " + admin_jwt} + + +####################################################################################################################### +print(" ----- USUARIS AL SISTEMA") +response = requests.get( + base + "/users", + headers=jwt, + verify=True, +) +print("METHOD: GET, URL: " + base + "/users, STATUS_CODE:" + str(response.status_code)) +if response.status_code == 200: + print("RESPONSE:") + pprint(json.loads(response.text)[:2]) +else: + print( + "ERROR: " + + json.loads(response.text)["error"] + + " DESCRIPTION: " + + json.loads(response.text)["description"] + ) + +####################################################################################################################### +print(" ----- USUARIS AL SISTEMA QUE CONTENEN UN TEXT") +data = {"text": "alu"} +response = requests.post( + base + "/users/filter", + json=data, + headers=jwt, + verify=True, +) +print( + "METHOD: POST, URL: " + + base + + "/users/filter, STATUS_CODE:" + + str(response.status_code) + + ", POST DATA:" +) +pprint(data) +if response.status_code == 200: + print("RESPONSE:") + pprint(json.loads(response.text)[:2]) +else: + print( + "ERROR: " + + json.loads(response.text)["error"] + + " DESCRIPTION: " + + json.loads(response.text)["description"] + ) + +####################################################################################################################### +print(" ----- GRUPS AL SISTEMA") +response = requests.get( + base + "/groups", + headers=jwt, + verify=True, +) +print("METHOD: GET, URL: " + base + "/groups, STATUS_CODE:" + str(response.status_code)) +if response.status_code == 200: + print("RESPONSE:") + pprint(json.loads(response.text)[:2]) +else: + print( + "ERROR: " + + json.loads(response.text)["error"] + + " DESCRIPTION: " + + json.loads(response.text)["description"] + ) + +####################################################################################################################### +print(" ----- USUARIS DEL GRUP") +data = {"id": "test00.classeB"} +response = requests.post( + base + "/group/users", + json=data, + headers=jwt, + verify=True, +) +print( + "METHOD: POST, URL: " + + base + + "/group/users, STATUS_CODE:" + + str(response.status_code) + + ", POST DATA:" +) +pprint(data) +if response.status_code == 200: + print("RESPONSE:") + pprint(json.loads(response.text)[:2]) +else: + print( + "ERROR: " + + json.loads(response.text)["error"] + + " DESCRIPTION: " + + json.loads(response.text)["description"] + ) + +####################################################################################################################### +print(" ----- ROLS AL SISTEMA") +response = requests.get( + base + "/roles", + headers=jwt, + verify=True, +) +print("METHOD: GET, URL: " + base + "/roles, STATUS_CODE:" + str(response.status_code)) +if response.status_code == 200: + print("RESPONSE:") + pprint(json.loads(response.text)[:2]) +else: + print( + "ERROR: " + + json.loads(response.text)["error"] + + " DESCRIPTION: " + + json.loads(response.text)["description"] + ) + +####################################################################################################################### +print(" ----- USUARIS DEL ROL") +data = {"id": "teacher"} +response = requests.post( + base + "/role/users", + json=data, + headers=jwt, + verify=True, +) +print( + "METHOD: POST, URL: " + + base + + "/role/users, STATUS_CODE:" + + str(response.status_code) + + ", POST DATA:" +) +pprint(data) +if response.status_code == 200: + print("RESPONSE:") + pprint(json.loads(response.text)[:2]) +else: + print( + "ERROR: " + + json.loads(response.text)["error"] + + " DESCRIPTION: " + + json.loads(response.text)["description"] + ) + + +print("\nXXXXXXXXXXXXXXXXX ACTIONS ON USER XXXXXXXXXXXXXXXXXXXXXX\n") +####################################################################################################################### +print(" ----- GET USER") +response = requests.get( + base + "/user/nou.usuari", + headers=jwt, + verify=True, +) +print( + "METHOD: GET, URL: " + + base + + "/user/nou.usuari, STATUS_CODE:" + + str(response.status_code) +) +if response.status_code == 200: + print("RESPONSE:") + pprint(json.loads(response.text)) +else: + print( + "ERROR: " + + json.loads(response.text)["error"] + + " DESCRIPTION: " + + json.loads(response.text)["description"] + ) + +####################################################################################################################### +print(" ----- DELETE USER") +response = requests.delete( + base + "/user/nou.usuari", + headers=jwt, + verify=True, +) +print( + "METHOD: DELETE, URL: " + + base + + "/user/nou.usuari, STATUS_CODE:" + + str(response.status_code) +) +if response.status_code == 200: + print("RESPONSE:") + pprint(json.loads(response.text)) +else: + print( + "ERROR: " + + json.loads(response.text)["error"] + + " DESCRIPTION: " + + json.loads(response.text)["description"] + ) + +####################################################################################################################### +print(" ----- POST NEW USER") +user = { + "username": "nou.usuari", + "first": "Nou", + "last": "Usuari", + "email": "nou.usuari@nodns.com", + "password": "1n2n3n4n5n6", + "quota": "default", + "enabled": True, + "role": "student", + "groups": ["test00.classeB"], +} +response = requests.post( + base + "/user", + json=user, + headers=jwt, + verify=True, +) +print( + "METHOD: POST, URL: " + + base + + "/user, STATUS_CODE:" + + str(response.status_code) + + ", POST DATA:" +) +pprint(user) +if response.status_code == 200: + print("RESPONSE:") + pprint(json.loads(response.text)) +else: + print( + "ERROR: " + + json.loads(response.text)["error"] + + " DESCRIPTION: " + + json.loads(response.text)["description"] + ) + +####################################################################################################################### +print(" ----- UPDATE USER") +update_user = { + "id": "nou.usuari", + "email": "nou.usuari@nodns.com", + "enabled": True, + "first": "Antic", + "groups": ["test00.classeB"], + "last": "Usuari", + "quota": "default", + "quota_used_bytes": "0 MB", + "role": "teacher", +} +response = requests.put( + base + "/user/nou.usuari", + json=update_user, + headers=jwt, + verify=True, +) +print( + "METHOD: PUT, URL: " + + base + + "/user/nou.usuari, STATUS_CODE:" + + str(response.status_code) + + ", PUT DATA:" +) +pprint(update_user) +if response.status_code == 200: + print("RESPONSE:") + pprint(json.loads(response.text)) +else: + print( + "ERROR: " + + json.loads(response.text)["error"] + + " DESCRIPTION: " + + json.loads(response.text)["description"] + ) + +####################################################################################################################### +print(" ----- GET USER") +response = requests.get( + base + "/user/nou.usuari", + headers=jwt, + verify=True, +) +print( + "METHOD: GET, URL: " + + base + + "/user/nou.usuari, STATUS_CODE:" + + str(response.status_code) +) +if response.status_code == 200: + print("RESPONSE:") + pprint(json.loads(response.text)) +else: + print( + "ERROR: " + + json.loads(response.text)["error"] + + " DESCRIPTION: " + + json.loads(response.text)["description"] + ) + +####################################################################################################################### +print(" ----- DELETE USER") +response = requests.delete( + base + "/user/nou.usuari", + headers=jwt, + verify=True, +) +print( + "METHOD: DELETE, URL: " + + base + + "/user/nou.usuari, STATUS_CODE:" + + str(response.status_code) +) +if response.status_code == 200: + print("RESPONSE:") + pprint(json.loads(response.text)) +else: + print( + "ERROR: " + + json.loads(response.text)["error"] + + " DESCRIPTION: " + + json.loads(response.text)["description"] + ) + + +print("\nXXXXXXXXXXXXXXXXX ACTIONS ON GROUP XXXXXXXXXXXXXXXXXXXXXX\n") + +####################################################################################################################### +print(" ----- GET GROUP") +response = requests.get( + base + "/group/teacher", + headers=jwt, + verify=True, +) +print( + "METHOD: GET, URL: " + + base + + "/group/teacher, STATUS_CODE:" + + str(response.status_code) +) +if response.status_code == 200: + print("RESPONSE:") + pprint(json.loads(response.text)) +else: + print( + "ERROR: " + + json.loads(response.text)["error"] + + " DESCRIPTION: " + + json.loads(response.text)["description"] + ) + +####################################################################################################################### +print(" ----- POST NEW GROUP") +group = {"name": "test"} +response = requests.post( + base + "/group", + json=group, + headers=jwt, + verify=True, +) +print( + "METHOD: POST, URL: " + + base + + "/group, STATUS_CODE:" + + str(response.status_code) + + ", POST DATA:" +) +pprint(group) +if response.status_code == 200: + print("RESPONSE:") + pprint(json.loads(response.text)) +else: + print( + "ERROR: " + + json.loads(response.text)["error"] + + " DESCRIPTION: " + + json.loads(response.text)["description"] + ) + + +####################################################################################################################### +print(" ----- DELETE GROUP") +response = requests.delete( + base + "/group/test", + headers=jwt, + verify=True, +) +print( + "METHOD: DELETE, URL: " + + base + + "/group/test, STATUS_CODE:" + + str(response.status_code) +) +if response.status_code == 200: + print("RESPONSE:") + pprint(json.loads(response.text)) +else: + print( + "ERROR: " + + json.loads(response.text)["error"] + + " DESCRIPTION: " + + json.loads(response.text)["description"] + ) + From 5d9231d7246f80630d4814538314d17b029e2841 Mon Sep 17 00:00:00 2001 From: darta Date: Mon, 16 May 2022 10:12:11 +0200 Subject: [PATCH 04/14] fix(admin): jwt api checks if new users groups exist --- admin/src/admin/lib/admin.py | 39 ++++++++++++++++---------- admin/src/admin/lib/keycloak_client.py | 12 +++++--- admin/src/admin/schemas/user.yml | 4 +++ admin/src/admin/views/ApiViews.py | 6 ++++ 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/admin/src/admin/lib/admin.py b/admin/src/admin/lib/admin.py index 1286cfb..4bd75f9 100644 --- a/admin/src/admin/lib/admin.py +++ b/admin/src/admin/lib/admin.py @@ -162,7 +162,7 @@ class Admin: ddmail, ddpassword, group="admin", - temporary=False, + password_temporary=False, ) self.keycloak.assign_realm_roles(uid, "admin") log.warning("KEYCLOAK: OK") @@ -632,7 +632,7 @@ class Admin: "gids": pathslist, "quota": u["quota"], "roles": [u["role"].strip()], - "temporary": True + "password_temporary": True if u["password_temporal"].lower() == "yes" else False, "password": self.get_dice_pwd() @@ -803,7 +803,7 @@ class Admin: u["last"], u["email"], u["password"], - temporary=u["temporary"], + password_temporary=u["password_temporary"], ) self.av.add_user_default_avatar(uid, u["roles"][0]) # Add user to role and group rolename @@ -1296,8 +1296,8 @@ class Admin: externaluser["gids"].append(data["action"]) return True - def user_update_password(self, userid, password, temporary): - return self.keycloak.update_user_pwd(userid, password, temporary) + def user_update_password(self, userid, password, password_temporary): + return self.keycloak.update_user_pwd(userid, password, password_temporary) def update_users_from_keycloak(self): kgroups = self.keycloak.get_groups() @@ -1700,6 +1700,22 @@ class Admin: pathpart = pathpart + "." + part pathslist.append(pathpart) + for path in pathslist: + path = "/" + path.replace(".", "/") + log.warning( + " KEYCLOAK USERS: Assign user " + u["username"] + " to group " + path + ) + try: + gid = self.keycloak.get_group_by_path(path=path)["id"] + except: + return False + # gid = self.keycloak.add_group_tree(path) + # log.warning("THE PATH "+str(path)+" HAS GID "+str(gid)) + # self.moodle.add_system_cohort(path) + # self.nextcloud.add_group(path) + # self.resync_data() + # gid = self.keycloak.get_group_by_path(path=path)["id"] + ### KEYCLOAK ####################### ev = Events("Add user", u["username"], total=5) @@ -1711,18 +1727,14 @@ class Admin: u["email"], u["password"], enabled=u["enabled"], + password_temporary=u.get("password_temporary", True), ) self.av.add_user_default_avatar(uid, u["role"]) # Add user to role and group rolename log.warning( - " KEYCLOAK USERS: Assign user " - + u["username"] - + " with initial pwd " - + u["password"] - + " to role " - + u["role"] + " KEYCLOAK USERS: Assign user " + u["username"] + " to role " + u["role"] ) self.keycloak.assign_realm_roles(uid, u["role"]) gid = self.keycloak.get_group_by_path(path="/" + u["role"])["id"] @@ -1731,12 +1743,9 @@ class Admin: # Add user to groups for path in pathslist: path = "/" + path.replace(".", "/") - log.warning( - " KEYCLOAK USERS: Assign user " + u["username"] + " to group " + path - ) gid = self.keycloak.get_group_by_path(path=path)["id"] self.keycloak.group_user_add(uid, gid) - ev.increment({"name": "Added to system groups", "data": []}) + ev.increment({"name": "Added to system groups", "data": []}) pathslist.append(u["role"]) ### MOODLE diff --git a/admin/src/admin/lib/keycloak_client.py b/admin/src/admin/lib/keycloak_client.py index 6aa3ba9..da316cb 100644 --- a/admin/src/admin/lib/keycloak_client.py +++ b/admin/src/admin/lib/keycloak_client.py @@ -152,7 +152,7 @@ class KeycloakClient: email, password, group=False, - temporary=True, + password_temporary=True, enabled=True, ): # RETURNS string with keycloak user id (the main id in this app) @@ -167,7 +167,11 @@ class KeycloakClient: "firstName": first, "lastName": last, "credentials": [ - {"type": "password", "value": password, "temporary": temporary} + { + "type": "password", + "value": password, + "temporary": password_temporary, + } ], } ) @@ -186,11 +190,11 @@ class KeycloakClient: self.keycloak_admin.group_user_add(uid, gid) return uid - def update_user_pwd(self, user_id, password, temporary=True): + def update_user_pwd(self, user_id, password, password_temporary=True): # Updates payload = { "credentials": [ - {"type": "password", "value": password, "temporary": temporary} + {"type": "password", "value": password, "temporary": password_temporary} ] } self.connect() diff --git a/admin/src/admin/schemas/user.yml b/admin/src/admin/schemas/user.yml index b94083d..6cc9770 100644 --- a/admin/src/admin/schemas/user.yml +++ b/admin/src/admin/schemas/user.yml @@ -13,6 +13,10 @@ email: password: required: true type: string +password_temporary: + required: false + type: boolean + default: true quota: required: true type: string diff --git a/admin/src/admin/views/ApiViews.py b/admin/src/admin/views/ApiViews.py index 91d152e..f095d18 100644 --- a/admin/src/admin/views/ApiViews.py +++ b/admin/src/admin/views/ApiViews.py @@ -172,7 +172,13 @@ def ddapi_user(user_ddid=None): if app.admin.get_user_username(data["username"]): raise Error("conflict", "User id already exists") + data = app.validators["user"].normalized(data) keycloak_id = app.admin.add_user(data) + if not keycloak_id: + raise Error( + "precondition_required", + "Not all user groups already in system. Please create user groups before adding user.", + ) return ( json.dumps({"keycloak_id": keycloak_id}), 200, From 084c4cd438a0482321d13bf26793b66a0c33ab12 Mon Sep 17 00:00:00 2001 From: darta Date: Mon, 16 May 2022 12:12:52 +0200 Subject: [PATCH 05/14] fix(api): raise error when user exists --- admin/src/admin/lib/keycloak_client.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/admin/src/admin/lib/keycloak_client.py b/admin/src/admin/lib/keycloak_client.py index da316cb..928f252 100644 --- a/admin/src/admin/lib/keycloak_client.py +++ b/admin/src/admin/lib/keycloak_client.py @@ -12,6 +12,7 @@ import yaml from jinja2 import Environment, FileSystemLoader from keycloak import KeycloakAdmin +from .api_exceptions import Error from .helpers import get_recursive_groups, kpath2kpaths from .postgres import Postgres @@ -175,8 +176,12 @@ class KeycloakClient: ], } ) - except: + except Exception as e: log.error(traceback.format_exc()) + raise Error( + "conflict", + "user/email already exists: " + str(username) + "/" + str(email), + ) if group: path = "/" + group if group[1:] != "/" else group From 9097f69273ecfa5fc3bced3d7fc9adf1dcf83c60 Mon Sep 17 00:00:00 2001 From: darta Date: Tue, 24 May 2022 08:45:25 +0200 Subject: [PATCH 06/14] fix(admin): post legal endpoint with login required --- admin/src/admin/views/AppViews.py | 1 + 1 file changed, 1 insertion(+) diff --git a/admin/src/admin/views/AppViews.py b/admin/src/admin/views/AppViews.py index 3c1feeb..2c13eae 100644 --- a/admin/src/admin/views/AppViews.py +++ b/admin/src/admin/views/AppViews.py @@ -518,6 +518,7 @@ def dashboard_put(item): {"Content-Type": "application/json"}, ) + @app.route("/api/legal/", methods=["GET"]) # @login_required def legal_get(item): From e0eee8737094c4a3fa303d522df9cf5f7061b7dc Mon Sep 17 00:00:00 2001 From: darta Date: Tue, 24 May 2022 08:52:42 +0200 Subject: [PATCH 07/14] fix(admin): applied jwt token verification at ws and black/isort --- admin/src/admin/auth/authentication.py | 3 +- admin/src/admin/auth/tokens.py | 3 +- admin/src/admin/lib/admin.py | 29 +++++++------------ admin/src/admin/lib/api_exceptions.py | 3 +- admin/src/admin/lib/avatars.py | 3 +- admin/src/admin/lib/dashboard.py | 3 +- admin/src/admin/lib/events.py | 26 +++++++++-------- admin/src/admin/lib/legal.py | 7 +++++ admin/src/admin/lib/load_config.py | 3 +- admin/src/admin/lib/moodle.py | 3 +- admin/src/admin/lib/nextcloud.py | 1 - admin/src/admin/lib/postup.py | 2 -- admin/src/admin/views/ApiViews.py | 3 +- admin/src/admin/views/AppViews.py | 7 ++--- admin/src/admin/views/LoginViews.py | 3 +- admin/src/admin/views/WebViews.py | 21 +++++++------- admin/src/admin/views/WpViews.py | 3 +- admin/src/start.py | 34 ++++++++++++++++++++++- admin/src/tests/api.py | 8 ++---- docker/api/src/api/views/AvatarsViews.py | 11 ++------ docker/api/src/api/views/InternalViews.py | 3 +- docker/api/src/api/views/MenuViews.py | 3 +- 22 files changed, 96 insertions(+), 86 deletions(-) diff --git a/admin/src/admin/auth/authentication.py b/admin/src/admin/auth/authentication.py index cc2d632..729381e 100644 --- a/admin/src/admin/auth/authentication.py +++ b/admin/src/admin/auth/authentication.py @@ -1,8 +1,7 @@ import os -from flask_login import LoginManager, UserMixin - from admin import app +from flask_login import LoginManager, UserMixin """ OIDC TESTS """ # from flask_oidc import OpenIDConnect diff --git a/admin/src/admin/auth/tokens.py b/admin/src/admin/auth/tokens.py index 3f55578..a3c7b01 100644 --- a/admin/src/admin/auth/tokens.py +++ b/admin/src/admin/auth/tokens.py @@ -9,11 +9,10 @@ import os import traceback from functools import wraps +from admin import app from flask import request from jose import jwt -from admin import app - from ..lib.api_exceptions import Error diff --git a/admin/src/admin/lib/admin.py b/admin/src/admin/lib/admin.py index 4bd75f9..086eaea 100644 --- a/admin/src/admin/lib/admin.py +++ b/admin/src/admin/lib/admin.py @@ -6,19 +6,12 @@ from pprint import pprint from time import sleep import diceware - from admin import app from .avatars import Avatars -from .helpers import ( - filter_roles_list, - filter_roles_listofdicts, - get_gids_from_kgroup_ids, - get_group_from_group_id, - gid2kpath, - kpath2gid, - system_username, -) +from .helpers import (filter_roles_list, filter_roles_listofdicts, + get_gids_from_kgroup_ids, get_group_from_group_id, + gid2kpath, kpath2gid, system_username) from .keycloak_client import KeycloakClient from .moodle import Moodle from .nextcloud import Nextcloud @@ -31,16 +24,11 @@ options.num = 3 import secrets from .api_exceptions import Error -from .events import Events +from .events import Events, sio_event_send from .exceptions import UserExists, UserNotFound -from .helpers import ( - count_repeated, - get_group_with_childs, - get_kid_from_kpath, - kpath2gids, - kpath2kpaths, - rand_password, -) +from .helpers import (count_repeated, get_group_with_childs, + get_kid_from_kpath, kpath2gids, kpath2kpaths, + rand_password) MANAGER = os.environ["CUSTOM_ROLE_MANAGER"] TEACHER = os.environ["CUSTOM_ROLE_TEACHER"] @@ -396,6 +384,7 @@ class Admin: # return users_list def get_mix_users(self): + sio_event_send("get_users", {"you_win": "you got the users!"}) return self.internal["users"] def _get_mix_users(self): @@ -1674,6 +1663,7 @@ class Admin: ev.update_text("Syncing data from applications...") self.resync_data() ev.update_text("User deleted") + sio_event_send("delete_user", {"userid": userid}) return True def get_user(self, userid): @@ -1826,6 +1816,7 @@ class Admin: log.error(traceback.format_exc()) self.resync_data() + sio_event_send("new_user", u) return uid def add_group(self, g): diff --git a/admin/src/admin/lib/api_exceptions.py b/admin/src/admin/lib/api_exceptions.py index 3873f22..e332ac7 100644 --- a/admin/src/admin/lib/api_exceptions.py +++ b/admin/src/admin/lib/api_exceptions.py @@ -4,9 +4,8 @@ import logging as log import os import traceback -from flask import jsonify, request - from admin import app +from flask import jsonify, request content_type = {"Content-Type": "application/json"} ex = { diff --git a/admin/src/admin/lib/avatars.py b/admin/src/admin/lib/avatars.py index 65caab3..d12b08b 100644 --- a/admin/src/admin/lib/avatars.py +++ b/admin/src/admin/lib/avatars.py @@ -2,13 +2,12 @@ import logging as log import os from pprint import pprint +from admin import app from minio import Minio from minio.commonconfig import REPLACE, CopySource from minio.deleteobjects import DeleteObject from requests import get, post -from admin import app - class Avatars: def __init__(self): diff --git a/admin/src/admin/lib/dashboard.py b/admin/src/admin/lib/dashboard.py index cb5699d..89a2cff 100644 --- a/admin/src/admin/lib/dashboard.py +++ b/admin/src/admin/lib/dashboard.py @@ -7,11 +7,10 @@ from pprint import pprint import requests import yaml +from admin import app from PIL import Image from schema import And, Optional, Schema, SchemaError, Use -from admin import app - class Dashboard: def __init__( diff --git a/admin/src/admin/lib/events.py b/admin/src/admin/lib/events.py index ddcffe5..65e01b5 100644 --- a/admin/src/admin/lib/events.py +++ b/admin/src/admin/lib/events.py @@ -9,19 +9,21 @@ import traceback from time import sleep from uuid import uuid4 -from flask import Response, jsonify, redirect, render_template, request, url_for -from flask_socketio import ( - SocketIO, - close_room, - disconnect, - emit, - join_room, - leave_room, - rooms, - send, -) - from admin import app +from flask import (Response, jsonify, redirect, render_template, request, + url_for) +from flask_socketio import (SocketIO, close_room, disconnect, emit, join_room, + leave_room, rooms, send) + + +def sio_event_send(event, data): + app.socketio.emit( + event, + json.dumps(data), + namespace="/sio/events", + room="events", + ) + sleep(0.001) class Events: diff --git a/admin/src/admin/lib/legal.py b/admin/src/admin/lib/legal.py index ff02087..5595199 100644 --- a/admin/src/admin/lib/legal.py +++ b/admin/src/admin/lib/legal.py @@ -3,6 +3,13 @@ import os import traceback from admin import app +from pprint import pprint + +from admin import app +from minio import Minio +from minio.commonconfig import REPLACE, CopySource +from minio.deleteobjects import DeleteObject +from requests import get, post legal_path= os.path.join(app.root_path, "static/templates/pages/legal/") diff --git a/admin/src/admin/lib/load_config.py b/admin/src/admin/lib/load_config.py index ba193d8..a004004 100644 --- a/admin/src/admin/lib/load_config.py +++ b/admin/src/admin/lib/load_config.py @@ -7,9 +7,8 @@ import sys import traceback import yaml -from cerberus import Validator, rules_set_registry, schema_registry - from admin import app +from cerberus import Validator, rules_set_registry, schema_registry class AdminValidator(Validator): diff --git a/admin/src/admin/lib/moodle.py b/admin/src/admin/lib/moodle.py index 25190f7..3f062ef 100644 --- a/admin/src/admin/lib/moodle.py +++ b/admin/src/admin/lib/moodle.py @@ -2,9 +2,8 @@ import logging as log import traceback from pprint import pprint -from requests import get, post - from admin import app +from requests import get, post from .exceptions import UserExists, UserNotFound from .postgres import Postgres diff --git a/admin/src/admin/lib/nextcloud.py b/admin/src/admin/lib/nextcloud.py index e3d9d2e..6a8b573 100644 --- a/admin/src/admin/lib/nextcloud.py +++ b/admin/src/admin/lib/nextcloud.py @@ -10,7 +10,6 @@ import traceback import urllib import requests - # from ..lib.log import * from admin import app diff --git a/admin/src/admin/lib/postup.py b/admin/src/admin/lib/postup.py index f8b048b..db9e85d 100644 --- a/admin/src/admin/lib/postup.py +++ b/admin/src/admin/lib/postup.py @@ -4,7 +4,6 @@ import json import logging as log import os import random - # from .keycloak import Keycloak # from .moodle import Moodle import string @@ -14,7 +13,6 @@ from datetime import datetime, timedelta import psycopg2 import yaml - from admin import app from .postgres import Postgres diff --git a/admin/src/admin/views/ApiViews.py b/admin/src/admin/views/ApiViews.py index f095d18..cf76974 100644 --- a/admin/src/admin/views/ApiViews.py +++ b/admin/src/admin/views/ApiViews.py @@ -8,9 +8,8 @@ import sys import time import traceback -from flask import request - from admin import app +from flask import request from ..lib.api_exceptions import Error from .decorators import has_token diff --git a/admin/src/admin/views/AppViews.py b/admin/src/admin/views/AppViews.py index 2c13eae..447e612 100644 --- a/admin/src/admin/views/AppViews.py +++ b/admin/src/admin/views/AppViews.py @@ -6,17 +6,16 @@ import logging as log import os import re import sys - # import Queue import threading import time import traceback from uuid import uuid4 -from flask import Response, jsonify, redirect, render_template, request, url_for -from flask_login import current_user, login_required - from admin import app +from flask import (Response, jsonify, redirect, render_template, request, + url_for) +from flask_login import current_user, login_required from ..lib.helpers import system_group from .decorators import login_or_token diff --git a/admin/src/admin/views/LoginViews.py b/admin/src/admin/views/LoginViews.py index 61a9a9d..2194df8 100644 --- a/admin/src/admin/views/LoginViews.py +++ b/admin/src/admin/views/LoginViews.py @@ -1,10 +1,9 @@ import os +from admin import app from flask import flash, redirect, render_template, request, url_for from flask_login import current_user, login_required, login_user, logout_user -from admin import app - from ..auth.authentication import * diff --git a/admin/src/admin/views/WebViews.py b/admin/src/admin/views/WebViews.py index 1fbc8a4..d570195 100644 --- a/admin/src/admin/views/WebViews.py +++ b/admin/src/admin/views/WebViews.py @@ -11,18 +11,10 @@ from pprint import pprint from uuid import uuid4 import requests -from flask import ( - Response, - jsonify, - redirect, - render_template, - request, - send_file, - url_for, -) -from flask_login import login_required - from admin import app +from flask import (Response, jsonify, redirect, render_template, request, + send_file, url_for) +from flask_login import login_required from ..lib.avatars import Avatars from .decorators import is_admin @@ -137,3 +129,10 @@ def web_sysadmin_external(): return render_template( "pages/sysadmin/external.html", title="External", nav="External" ) + + +@app.route("/sockettest") +def web_sockettest(): + return render_template( + "pages/sockettest.html", title="Sockettest Users", nav="SysAdminUsers" + ) diff --git a/admin/src/admin/views/WpViews.py b/admin/src/admin/views/WpViews.py index 0c829dd..43e3d7a 100644 --- a/admin/src/admin/views/WpViews.py +++ b/admin/src/admin/views/WpViews.py @@ -8,9 +8,8 @@ import sys import time import traceback -from flask import request - from admin import app +from flask import request from .decorators import is_internal diff --git a/admin/src/start.py b/admin/src/start.py index df970d5..dedd1ef 100644 --- a/admin/src/start.py +++ b/admin/src/start.py @@ -6,6 +6,7 @@ monkey_patch() import json +<<<<<<< HEAD from flask_login import login_required from flask_socketio import ( SocketIO, @@ -18,7 +19,14 @@ from flask_socketio import ( send, ) +======= +>>>>>>> fix(admin): applied jwt token verification at ws and black/isort from admin import app +from admin.auth.tokens import get_token_payload +from admin.lib.api_exceptions import Error +from flask import request +from flask_socketio import (SocketIO, close_room, disconnect, emit, join_room, + leave_room, rooms, send) app.socketio = SocketIO(app) @@ -31,17 +39,41 @@ def socketio_connect(): "update", json.dumps("Joined admins room"), namespace="/sio", room="admin" ) + @app.socketio.on("disconnect", namespace="/sio") def socketio_disconnect(): None +@app.socketio.on("connect", namespace="/sio/events") +def socketio_connect(): + try: + jwt = get_token_payload(request.args.get("jwt")) + except: + return Error("bad_request", "Missing websocket jwt authorization bearer token") + + payload = get_token_payload(jwt) + + join_room("events") + app.socketio.emit( + "update", + json.dumps("Joined events room"), + namespace="/sio/events", + room="events", + ) + + +@app.socketio.on("disconnect", namespace="/sio/events") +def socketio_events_disconnect(): + None + + if __name__ == "__main__": app.socketio.run( app, host="0.0.0.0", port=9000, - debug=True, + debug=False, ) # ssl_context="adhoc", # async_mode="threading", diff --git a/admin/src/tests/api.py b/admin/src/tests/api.py index 422c897..6ec7ac3 100644 --- a/admin/src/tests/api.py +++ b/admin/src/tests/api.py @@ -3,12 +3,11 @@ import os import secrets import time import traceback +from datetime import datetime, timedelta from pprint import pprint -from datetime import datetime -from datetime import timedelta -from jose import jwt import requests +from jose import jwt ## SETUP domain = "admin.[YOURDOMAIN]" @@ -18,7 +17,7 @@ secret = "[your API_SECRET]" auths = {} dbconn = None -base = "https://"+domain+"/ddapi" +base = "https://" + domain + "/ddapi" raw_jwt_data = { "exp": datetime.utcnow() + timedelta(minutes=5), @@ -422,4 +421,3 @@ else: + " DESCRIPTION: " + json.loads(response.text)["description"] ) - diff --git a/docker/api/src/api/views/AvatarsViews.py b/docker/api/src/api/views/AvatarsViews.py index 56e1654..a0c481b 100644 --- a/docker/api/src/api/views/AvatarsViews.py +++ b/docker/api/src/api/views/AvatarsViews.py @@ -9,15 +9,8 @@ import traceback from uuid import uuid4 from api import app -from flask import ( - Response, - jsonify, - redirect, - render_template, - request, - send_from_directory, - url_for, -) +from flask import (Response, jsonify, redirect, render_template, request, + send_from_directory, url_for) from ..lib.avatars import Avatars diff --git a/docker/api/src/api/views/InternalViews.py b/docker/api/src/api/views/InternalViews.py index 6a2ad46..5a63843 100644 --- a/docker/api/src/api/views/InternalViews.py +++ b/docker/api/src/api/views/InternalViews.py @@ -3,7 +3,8 @@ import os from api import app -from flask import Response, jsonify, redirect, render_template, request, url_for +from flask import (Response, jsonify, redirect, render_template, request, + url_for) from .decorators import is_internal diff --git a/docker/api/src/api/views/MenuViews.py b/docker/api/src/api/views/MenuViews.py index 2ba9a7f..30eb261 100644 --- a/docker/api/src/api/views/MenuViews.py +++ b/docker/api/src/api/views/MenuViews.py @@ -9,7 +9,8 @@ import traceback from uuid import uuid4 from api import app -from flask import Response, jsonify, redirect, render_template, request, url_for +from flask import (Response, jsonify, redirect, render_template, request, + url_for) from ..lib.menu import Menu From 267d1e26a12ce2a93e6ae74be8a7f3b9ce9c9d02 Mon Sep 17 00:00:00 2001 From: darta Date: Tue, 24 May 2022 20:16:51 +0200 Subject: [PATCH 08/14] fix(api): fixed websocket /sio/events namespace with jwt --- admin/src/admin/auth/authentication.py | 3 ++- admin/src/admin/auth/tokens.py | 10 +++++++--- admin/src/admin/lib/admin.py | 24 ++++++++++++++++++------ admin/src/admin/lib/api_exceptions.py | 3 ++- admin/src/admin/lib/avatars.py | 3 ++- admin/src/admin/lib/dashboard.py | 3 ++- admin/src/admin/lib/events.py | 16 ++++++++++++---- admin/src/admin/lib/legal.py | 1 - admin/src/admin/lib/load_config.py | 3 ++- admin/src/admin/lib/moodle.py | 3 ++- admin/src/admin/lib/nextcloud.py | 1 + admin/src/admin/lib/postup.py | 2 ++ admin/src/admin/views/ApiViews.py | 3 ++- admin/src/admin/views/AppViews.py | 7 ++++--- admin/src/admin/views/LoginViews.py | 3 ++- admin/src/admin/views/WebViews.py | 14 +++++++++++--- admin/src/admin/views/WpViews.py | 3 ++- admin/src/start.py | 5 ----- 18 files changed, 73 insertions(+), 34 deletions(-) diff --git a/admin/src/admin/auth/authentication.py b/admin/src/admin/auth/authentication.py index 729381e..cc2d632 100644 --- a/admin/src/admin/auth/authentication.py +++ b/admin/src/admin/auth/authentication.py @@ -1,8 +1,9 @@ import os -from admin import app from flask_login import LoginManager, UserMixin +from admin import app + """ OIDC TESTS """ # from flask_oidc import OpenIDConnect # app.config.update({ diff --git a/admin/src/admin/auth/tokens.py b/admin/src/admin/auth/tokens.py index a3c7b01..a220b1f 100644 --- a/admin/src/admin/auth/tokens.py +++ b/admin/src/admin/auth/tokens.py @@ -9,10 +9,11 @@ import os import traceback from functools import wraps -from admin import app from flask import request from jose import jwt +from admin import app + from ..lib.api_exceptions import Error @@ -55,12 +56,15 @@ def get_token_auth_header(): def get_token_payload(token): + log.warning("The received token in get_token_payload is: " + str(token)) try: claims = jwt.get_unverified_claims(token) secret = app.config["API_SECRET"] except: - log.warning("JWT token with invalid parameters. Can not parse it.") + log.warning( + "JWT token with invalid parameters. Can not parse it.: " + str(token) + ) raise Error( "unauthorized", "Unable to parse authentication parameters token.", @@ -75,7 +79,7 @@ def get_token_payload(token): options=dict(verify_aud=False, verify_sub=False, verify_exp=True), ) except jwt.ExpiredSignatureError: - log.info("Token expired") + log.warning("Token expired") raise Error("unauthorized", "Token is expired", traceback.format_stack()) except jwt.JWTClaimsError: diff --git a/admin/src/admin/lib/admin.py b/admin/src/admin/lib/admin.py index 086eaea..990faa0 100644 --- a/admin/src/admin/lib/admin.py +++ b/admin/src/admin/lib/admin.py @@ -6,12 +6,19 @@ from pprint import pprint from time import sleep import diceware + from admin import app from .avatars import Avatars -from .helpers import (filter_roles_list, filter_roles_listofdicts, - get_gids_from_kgroup_ids, get_group_from_group_id, - gid2kpath, kpath2gid, system_username) +from .helpers import ( + filter_roles_list, + filter_roles_listofdicts, + get_gids_from_kgroup_ids, + get_group_from_group_id, + gid2kpath, + kpath2gid, + system_username, +) from .keycloak_client import KeycloakClient from .moodle import Moodle from .nextcloud import Nextcloud @@ -26,9 +33,14 @@ import secrets from .api_exceptions import Error from .events import Events, sio_event_send from .exceptions import UserExists, UserNotFound -from .helpers import (count_repeated, get_group_with_childs, - get_kid_from_kpath, kpath2gids, kpath2kpaths, - rand_password) +from .helpers import ( + count_repeated, + get_group_with_childs, + get_kid_from_kpath, + kpath2gids, + kpath2kpaths, + rand_password, +) MANAGER = os.environ["CUSTOM_ROLE_MANAGER"] TEACHER = os.environ["CUSTOM_ROLE_TEACHER"] diff --git a/admin/src/admin/lib/api_exceptions.py b/admin/src/admin/lib/api_exceptions.py index e332ac7..3873f22 100644 --- a/admin/src/admin/lib/api_exceptions.py +++ b/admin/src/admin/lib/api_exceptions.py @@ -4,9 +4,10 @@ import logging as log import os import traceback -from admin import app from flask import jsonify, request +from admin import app + content_type = {"Content-Type": "application/json"} ex = { "bad_request": { diff --git a/admin/src/admin/lib/avatars.py b/admin/src/admin/lib/avatars.py index d12b08b..65caab3 100644 --- a/admin/src/admin/lib/avatars.py +++ b/admin/src/admin/lib/avatars.py @@ -2,12 +2,13 @@ import logging as log import os from pprint import pprint -from admin import app from minio import Minio from minio.commonconfig import REPLACE, CopySource from minio.deleteobjects import DeleteObject from requests import get, post +from admin import app + class Avatars: def __init__(self): diff --git a/admin/src/admin/lib/dashboard.py b/admin/src/admin/lib/dashboard.py index 89a2cff..cb5699d 100644 --- a/admin/src/admin/lib/dashboard.py +++ b/admin/src/admin/lib/dashboard.py @@ -7,10 +7,11 @@ from pprint import pprint import requests import yaml -from admin import app from PIL import Image from schema import And, Optional, Schema, SchemaError, Use +from admin import app + class Dashboard: def __init__( diff --git a/admin/src/admin/lib/events.py b/admin/src/admin/lib/events.py index 65e01b5..534ed07 100644 --- a/admin/src/admin/lib/events.py +++ b/admin/src/admin/lib/events.py @@ -9,11 +9,19 @@ import traceback from time import sleep from uuid import uuid4 +from flask import Response, jsonify, redirect, render_template, request, url_for +from flask_socketio import ( + SocketIO, + close_room, + disconnect, + emit, + join_room, + leave_room, + rooms, + send, +) + from admin import app -from flask import (Response, jsonify, redirect, render_template, request, - url_for) -from flask_socketio import (SocketIO, close_room, disconnect, emit, join_room, - leave_room, rooms, send) def sio_event_send(event, data): diff --git a/admin/src/admin/lib/legal.py b/admin/src/admin/lib/legal.py index 5595199..055bdbd 100644 --- a/admin/src/admin/lib/legal.py +++ b/admin/src/admin/lib/legal.py @@ -5,7 +5,6 @@ import traceback from admin import app from pprint import pprint -from admin import app from minio import Minio from minio.commonconfig import REPLACE, CopySource from minio.deleteobjects import DeleteObject diff --git a/admin/src/admin/lib/load_config.py b/admin/src/admin/lib/load_config.py index a004004..ba193d8 100644 --- a/admin/src/admin/lib/load_config.py +++ b/admin/src/admin/lib/load_config.py @@ -7,9 +7,10 @@ import sys import traceback import yaml -from admin import app from cerberus import Validator, rules_set_registry, schema_registry +from admin import app + class AdminValidator(Validator): None diff --git a/admin/src/admin/lib/moodle.py b/admin/src/admin/lib/moodle.py index 3f062ef..25190f7 100644 --- a/admin/src/admin/lib/moodle.py +++ b/admin/src/admin/lib/moodle.py @@ -2,9 +2,10 @@ import logging as log import traceback from pprint import pprint -from admin import app from requests import get, post +from admin import app + from .exceptions import UserExists, UserNotFound from .postgres import Postgres diff --git a/admin/src/admin/lib/nextcloud.py b/admin/src/admin/lib/nextcloud.py index 6a8b573..e3d9d2e 100644 --- a/admin/src/admin/lib/nextcloud.py +++ b/admin/src/admin/lib/nextcloud.py @@ -10,6 +10,7 @@ import traceback import urllib import requests + # from ..lib.log import * from admin import app diff --git a/admin/src/admin/lib/postup.py b/admin/src/admin/lib/postup.py index db9e85d..f8b048b 100644 --- a/admin/src/admin/lib/postup.py +++ b/admin/src/admin/lib/postup.py @@ -4,6 +4,7 @@ import json import logging as log import os import random + # from .keycloak import Keycloak # from .moodle import Moodle import string @@ -13,6 +14,7 @@ from datetime import datetime, timedelta import psycopg2 import yaml + from admin import app from .postgres import Postgres diff --git a/admin/src/admin/views/ApiViews.py b/admin/src/admin/views/ApiViews.py index cf76974..f095d18 100644 --- a/admin/src/admin/views/ApiViews.py +++ b/admin/src/admin/views/ApiViews.py @@ -8,9 +8,10 @@ import sys import time import traceback -from admin import app from flask import request +from admin import app + from ..lib.api_exceptions import Error from .decorators import has_token diff --git a/admin/src/admin/views/AppViews.py b/admin/src/admin/views/AppViews.py index 447e612..2c13eae 100644 --- a/admin/src/admin/views/AppViews.py +++ b/admin/src/admin/views/AppViews.py @@ -6,17 +6,18 @@ import logging as log import os import re import sys + # import Queue import threading import time import traceback from uuid import uuid4 -from admin import app -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 admin import app + from ..lib.helpers import system_group from .decorators import login_or_token diff --git a/admin/src/admin/views/LoginViews.py b/admin/src/admin/views/LoginViews.py index 2194df8..61a9a9d 100644 --- a/admin/src/admin/views/LoginViews.py +++ b/admin/src/admin/views/LoginViews.py @@ -1,9 +1,10 @@ import os -from admin import app from flask import flash, redirect, render_template, request, url_for from flask_login import current_user, login_required, login_user, logout_user +from admin import app + from ..auth.authentication import * diff --git a/admin/src/admin/views/WebViews.py b/admin/src/admin/views/WebViews.py index d570195..e162630 100644 --- a/admin/src/admin/views/WebViews.py +++ b/admin/src/admin/views/WebViews.py @@ -11,11 +11,19 @@ from pprint import pprint from uuid import uuid4 import requests -from admin import app -from flask import (Response, jsonify, redirect, render_template, request, - send_file, url_for) +from flask import ( + Response, + jsonify, + redirect, + render_template, + request, + send_file, + url_for, +) from flask_login import login_required +from admin import app + from ..lib.avatars import Avatars from .decorators import is_admin diff --git a/admin/src/admin/views/WpViews.py b/admin/src/admin/views/WpViews.py index 43e3d7a..0c829dd 100644 --- a/admin/src/admin/views/WpViews.py +++ b/admin/src/admin/views/WpViews.py @@ -8,9 +8,10 @@ import sys import time import traceback -from admin import app from flask import request +from admin import app + from .decorators import is_internal diff --git a/admin/src/start.py b/admin/src/start.py index dedd1ef..30532ad 100644 --- a/admin/src/start.py +++ b/admin/src/start.py @@ -6,7 +6,6 @@ monkey_patch() import json -<<<<<<< HEAD from flask_login import login_required from flask_socketio import ( SocketIO, @@ -19,14 +18,10 @@ from flask_socketio import ( send, ) -======= ->>>>>>> fix(admin): applied jwt token verification at ws and black/isort from admin import app from admin.auth.tokens import get_token_payload from admin.lib.api_exceptions import Error from flask import request -from flask_socketio import (SocketIO, close_room, disconnect, emit, join_room, - leave_room, rooms, send) app.socketio = SocketIO(app) From 897da17dbd45b42393ea39438fc674b3dd90c7ce Mon Sep 17 00:00:00 2001 From: darta Date: Tue, 24 May 2022 20:26:39 +0200 Subject: [PATCH 09/14] fix(api): added mail cerberus validator to user_mail endpoint --- admin/src/admin/schemas/mail.yml | 34 +++++++++++++++++++++++++++++++ admin/src/admin/views/ApiViews.py | 27 ++++++++++++++++++++++++ admin/src/start.py | 7 +------ 3 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 admin/src/admin/schemas/mail.yml diff --git a/admin/src/admin/schemas/mail.yml b/admin/src/admin/schemas/mail.yml new file mode 100644 index 0000000..a264378 --- /dev/null +++ b/admin/src/admin/schemas/mail.yml @@ -0,0 +1,34 @@ +user_id: + type: string + required: true +name: + type: string + required: false +email: + type: string + required: true + regex: ^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$ +inbound_host: + type: string + required: true +inbound_port: + type: integer + required: true +inbound_ssl_mode: + type: string + default: ssl +inbound_user: + type: string + required: true +outbound_host: + type: string + required: true +outbound_port: + type: integer + required: true +outbound_ssl_mode: + type: string + default: ssl +outbound_user: + type: string + required: true \ No newline at end of file diff --git a/admin/src/admin/views/ApiViews.py b/admin/src/admin/views/ApiViews.py index f095d18..e31a899 100644 --- a/admin/src/admin/views/ApiViews.py +++ b/admin/src/admin/views/ApiViews.py @@ -267,6 +267,33 @@ def ddapi_group(id=None): return json.dumps({}), 200, {"Content-Type": "application/json"} +@app.route("/ddapi/user_mail", methods=["POST"]) +@app.route("/ddapi/user_mail/", methods=["GET", "POST", "DELETE"]) +@has_token +def ddapi_user_mail(id=None): + if request.method == "GET": + return ( + json.dumps("Not implemented yet"), + 200, + {"Content-Type": "application/json"}, + ) + if request.method == "POST": + data = request.get_json(force=True) + + if not app.validators["mail"].validate(data): + raise Error( + "bad_request", + "Data validation for mail failed: " + + str(app.validators["mail"].errors), + traceback.format_exc(), + ) + return ( + json.dumps("Not implemented yet"), + 200, + {"Content-Type": "application/json"}, + ) + + def user_parser(user): return { "keycloak_id": user["id"], diff --git a/admin/src/start.py b/admin/src/start.py index 30532ad..2651b92 100644 --- a/admin/src/start.py +++ b/admin/src/start.py @@ -42,12 +42,7 @@ def socketio_disconnect(): @app.socketio.on("connect", namespace="/sio/events") def socketio_connect(): - try: - jwt = get_token_payload(request.args.get("jwt")) - except: - return Error("bad_request", "Missing websocket jwt authorization bearer token") - - payload = get_token_payload(jwt) + jwt = get_token_payload(request.args.get("jwt")) join_room("events") app.socketio.emit( From 02e1bcf33139c37c1d78aa6547b62d434aa8cb85 Mon Sep 17 00:00:00 2001 From: darta Date: Fri, 27 May 2022 16:52:35 +0200 Subject: [PATCH 10/14] fix(api,admin): refactor and events websocket fixed --- admin/src/admin/auth/tokens.py | 2 +- admin/src/admin/lib/admin.py | 19 ++++++++++++ admin/src/admin/schemas/mails.yml | 37 +++++++++++++++++++++++ admin/src/admin/views/ApiViews.py | 5 ++- admin/src/start.py | 22 ++++++++------ docker/api/src/api/views/AvatarsViews.py | 11 +++++-- docker/api/src/api/views/InternalViews.py | 3 +- docker/api/src/api/views/MenuViews.py | 3 +- 8 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 admin/src/admin/schemas/mails.yml diff --git a/admin/src/admin/auth/tokens.py b/admin/src/admin/auth/tokens.py index a220b1f..bb8c345 100644 --- a/admin/src/admin/auth/tokens.py +++ b/admin/src/admin/auth/tokens.py @@ -56,7 +56,7 @@ def get_token_auth_header(): def get_token_payload(token): - 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: claims = jwt.get_unverified_claims(token) secret = app.config["API_SECRET"] diff --git a/admin/src/admin/lib/admin.py b/admin/src/admin/lib/admin.py index 990faa0..64cbaa7 100644 --- a/admin/src/admin/lib/admin.py +++ b/admin/src/admin/lib/admin.py @@ -1906,3 +1906,22 @@ class Admin: self.moodle.delete_cohorts(cohort) self.nextcloud.delete_group(gid) self.resync_data() + + def set_nextcloud_user_mail(self, data): + self.pg.update( + """INSERT INTO "oc_appconfig" ("user_id","name","email","inbound_host","inbound_port","inbound_ssl_mode","inbound_user","outbound_host","outbound_port","outbound_ssl_mode","outbound_user") VALUES +(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s),""" + % ( + data["user_id"], + data["name"], + data["email"], + data["inbound_host"], + data["inbound_port"], + data["inbound_ssl_mode"], + data["inbound_user"], + data["outbound_host"], + data["outbound_port"], + data["outbound_ssl_mode"], + data["outbound_user"], + ) + ) diff --git a/admin/src/admin/schemas/mails.yml b/admin/src/admin/schemas/mails.yml new file mode 100644 index 0000000..779ccf0 --- /dev/null +++ b/admin/src/admin/schemas/mails.yml @@ -0,0 +1,37 @@ +mails: + type: list + schema: + user_id: + type: string + required: true + name: + type: string + required: false + email: + type: string + required: true + regex: ^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$ + inbound_host: + type: string + required: true + inbound_port: + type: integer + required: true + inbound_ssl_mode: + type: string + default: ssl + inbound_user: + type: string + required: true + outbound_host: + type: string + required: true + outbound_port: + type: integer + required: true + outbound_ssl_mode: + type: string + default: ssl + outbound_user: + type: string + required: true \ No newline at end of file diff --git a/admin/src/admin/views/ApiViews.py b/admin/src/admin/views/ApiViews.py index e31a899..624cd90 100644 --- a/admin/src/admin/views/ApiViews.py +++ b/admin/src/admin/views/ApiViews.py @@ -280,13 +280,16 @@ def ddapi_user_mail(id=None): if request.method == "POST": data = request.get_json(force=True) - if not app.validators["mail"].validate(data): + if not app.validators["mails"].validate(data): raise Error( "bad_request", "Data validation for mail failed: " + str(app.validators["mail"].errors), traceback.format_exc(), ) + for user in data: + log.info("Added user email") + app.admin.set_nextcloud_user_mail(user) return ( json.dumps("Not implemented yet"), 200, diff --git a/admin/src/start.py b/admin/src/start.py index 2651b92..78219df 100644 --- a/admin/src/start.py +++ b/admin/src/start.py @@ -7,6 +7,11 @@ monkey_patch() import json from flask_login import login_required +from admin import app +from admin.auth.tokens import get_token_payload +from admin.lib.api_exceptions import Error +from flask import request +from flask_login import current_user from flask_socketio import ( SocketIO, close_room, @@ -18,10 +23,6 @@ from flask_socketio import ( send, ) -from admin import app -from admin.auth.tokens import get_token_payload -from admin.lib.api_exceptions import Error -from flask import request app.socketio = SocketIO(app) @@ -29,15 +30,18 @@ app.socketio = SocketIO(app) @app.socketio.on("connect", namespace="/sio") @login_required def socketio_connect(): - join_room("admin") - app.socketio.emit( - "update", json.dumps("Joined admins room"), namespace="/sio", room="admin" - ) + if current_user.id: + join_room("admin") + app.socketio.emit( + "update", json.dumps("Joined admins room"), namespace="/sio", room="admin" + ) + else: + None @app.socketio.on("disconnect", namespace="/sio") def socketio_disconnect(): - None + leave_room("admin") @app.socketio.on("connect", namespace="/sio/events") diff --git a/docker/api/src/api/views/AvatarsViews.py b/docker/api/src/api/views/AvatarsViews.py index a0c481b..56e1654 100644 --- a/docker/api/src/api/views/AvatarsViews.py +++ b/docker/api/src/api/views/AvatarsViews.py @@ -9,8 +9,15 @@ import traceback from uuid import uuid4 from api import app -from flask import (Response, jsonify, redirect, render_template, request, - send_from_directory, url_for) +from flask import ( + Response, + jsonify, + redirect, + render_template, + request, + send_from_directory, + url_for, +) from ..lib.avatars import Avatars diff --git a/docker/api/src/api/views/InternalViews.py b/docker/api/src/api/views/InternalViews.py index 5a63843..6a2ad46 100644 --- a/docker/api/src/api/views/InternalViews.py +++ b/docker/api/src/api/views/InternalViews.py @@ -3,8 +3,7 @@ import os from api import app -from flask import (Response, jsonify, redirect, render_template, request, - url_for) +from flask import Response, jsonify, redirect, render_template, request, url_for from .decorators import is_internal diff --git a/docker/api/src/api/views/MenuViews.py b/docker/api/src/api/views/MenuViews.py index 30eb261..2ba9a7f 100644 --- a/docker/api/src/api/views/MenuViews.py +++ b/docker/api/src/api/views/MenuViews.py @@ -9,8 +9,7 @@ import traceback from uuid import uuid4 from api import app -from flask import (Response, jsonify, redirect, render_template, request, - url_for) +from flask import Response, jsonify, redirect, render_template, request, url_for from ..lib.menu import Menu From 5527699e30189b03b39c3bc3061b39f73f6c8c12 Mon Sep 17 00:00:00 2001 From: darta Date: Fri, 27 May 2022 18:25:48 +0200 Subject: [PATCH 11/14] fix(admin): insert mail into oc_mail_accounts --- admin/src/admin/lib/admin.py | 18 +----------- admin/src/admin/lib/nextcloud.py | 48 +++++++++++++++++++++++++++++++ admin/src/admin/views/ApiViews.py | 26 +++++++++++------ admin/src/start.py | 2 +- 4 files changed, 67 insertions(+), 27 deletions(-) diff --git a/admin/src/admin/lib/admin.py b/admin/src/admin/lib/admin.py index 64cbaa7..8d193f8 100644 --- a/admin/src/admin/lib/admin.py +++ b/admin/src/admin/lib/admin.py @@ -1908,20 +1908,4 @@ class Admin: self.resync_data() def set_nextcloud_user_mail(self, data): - self.pg.update( - """INSERT INTO "oc_appconfig" ("user_id","name","email","inbound_host","inbound_port","inbound_ssl_mode","inbound_user","outbound_host","outbound_port","outbound_ssl_mode","outbound_user") VALUES -(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s),""" - % ( - data["user_id"], - data["name"], - data["email"], - data["inbound_host"], - data["inbound_port"], - data["inbound_ssl_mode"], - data["inbound_user"], - data["outbound_host"], - data["outbound_port"], - data["outbound_ssl_mode"], - data["outbound_user"], - ) - ) + self.nextcloud.set_user_mail(data) diff --git a/admin/src/admin/lib/nextcloud.py b/admin/src/admin/lib/nextcloud.py index e3d9d2e..cfba60a 100644 --- a/admin/src/admin/lib/nextcloud.py +++ b/admin/src/admin/lib/nextcloud.py @@ -518,3 +518,51 @@ class Nextcloud: # 101 - invalid input data # 102 - group already exists # 103 - failed to add the group + + def set_user_mail(self, data): + if not len( + self.nextcloud_pg.select( + """SELECT * FROM "oc_mail_accounts" WHERE "email" = '%s'""" + % (data["email"]) + ) + ): + self.nextcloud_pg.update( + """INSERT INTO "oc_mail_accounts" ("user_id","name","email","inbound_host","inbound_port","inbound_ssl_mode","inbound_user","inbound_password","outbound_host","outbound_port","outbound_ssl_mode","outbound_user","outbound_password") VALUES + ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s');""" + % ( + data["user_id"], + data["name"], + data["email"], + data["inbound_host"], + data["inbound_port"], + data["inbound_ssl_mode"], + data["inbound_user"], + data["inbound_password"], + data["outbound_host"], + data["outbound_port"], + data["outbound_ssl_mode"], + data["outbound_user"], + data["outbound_password"], + ) + ) + else: + self.nextcloud_pg.update( + """UPDATE "oc_mail_accounts" SET ("user_id","name","email","inbound_host","inbound_port","inbound_ssl_mode","inbound_user","inbound_password","outbound_host","outbound_port","outbound_ssl_mode","outbound_user","outbound_password") = + ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s') WHERE email = '%s';""" + % ( + data["user_id"], + data["name"], + data["email"], + data["inbound_host"], + data["inbound_port"], + data["inbound_ssl_mode"], + data["inbound_user"], + data["inbound_password"], + data["outbound_host"], + data["outbound_port"], + data["outbound_ssl_mode"], + data["outbound_user"], + data["outbound_password"], + data["email"], + ) + ) diff --git a/admin/src/admin/views/ApiViews.py b/admin/src/admin/views/ApiViews.py index 624cd90..ad6c80a 100644 --- a/admin/src/admin/views/ApiViews.py +++ b/admin/src/admin/views/ApiViews.py @@ -268,7 +268,7 @@ def ddapi_group(id=None): @app.route("/ddapi/user_mail", methods=["POST"]) -@app.route("/ddapi/user_mail/", methods=["GET", "POST", "DELETE"]) +@app.route("/ddapi/user_mail/", methods=["GET", "DELETE"]) @has_token def ddapi_user_mail(id=None): if request.method == "GET": @@ -280,18 +280,26 @@ def ddapi_user_mail(id=None): if request.method == "POST": data = request.get_json(force=True) - if not app.validators["mails"].validate(data): - raise Error( - "bad_request", - "Data validation for mail failed: " - + str(app.validators["mail"].errors), - traceback.format_exc(), - ) + # if not app.validators["mails"].validate(data): + # raise Error( + # "bad_request", + # "Data validation for mail failed: " + # + str(app.validators["mail"].errors), + # traceback.format_exc(), + # ) + for user in data: + if not app.validators["mail"].validate(user): + raise Error( + "bad_request", + "Data validation for mail failed: " + + str(app.validators["mail"].errors), + traceback.format_exc(), + ) for user in data: log.info("Added user email") app.admin.set_nextcloud_user_mail(user) return ( - json.dumps("Not implemented yet"), + json.dumps("Users emails updated"), 200, {"Content-Type": "application/json"}, ) diff --git a/admin/src/start.py b/admin/src/start.py index 78219df..cf1a2bc 100644 --- a/admin/src/start.py +++ b/admin/src/start.py @@ -59,7 +59,7 @@ def socketio_connect(): @app.socketio.on("disconnect", namespace="/sio/events") def socketio_events_disconnect(): - None + leave_room("events") if __name__ == "__main__": From e299004aaeaff76e86f9176193ccd9591753de80 Mon Sep 17 00:00:00 2001 From: darta Date: Wed, 1 Jun 2022 20:37:35 +0200 Subject: [PATCH 12/14] fix(admin): regex check for new user email field --- admin/src/admin/schemas/user.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/admin/src/admin/schemas/user.yml b/admin/src/admin/schemas/user.yml index 6cc9770..7bb7c54 100644 --- a/admin/src/admin/schemas/user.yml +++ b/admin/src/admin/schemas/user.yml @@ -10,6 +10,7 @@ last: email: required: true type: string + regex: ^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$ password: required: true type: string From 9fb3c8a079429947c2a59179e4ec0e55defe43e1 Mon Sep 17 00:00:00 2001 From: darta Date: Wed, 1 Jun 2022 21:25:18 +0200 Subject: [PATCH 13/14] fix(admin): added regex to email field on user update --- admin/src/admin/schemas/user_update.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/admin/src/admin/schemas/user_update.yml b/admin/src/admin/schemas/user_update.yml index 6bc8c56..eb2118f 100644 --- a/admin/src/admin/schemas/user_update.yml +++ b/admin/src/admin/schemas/user_update.yml @@ -7,6 +7,7 @@ last: email: required: false type: string + regex: ^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$ password: required: false type: string From 0019637a6537f0c0a1e01cec904aedd61a83d068 Mon Sep 17 00:00:00 2001 From: darta Date: Wed, 1 Jun 2022 22:50:27 +0200 Subject: [PATCH 14/14] fix(admin): applied sql sanitizer --- admin/src/admin/lib/nextcloud.py | 83 +++++++++++++++----------------- admin/src/start.py | 5 +- 2 files changed, 42 insertions(+), 46 deletions(-) diff --git a/admin/src/admin/lib/nextcloud.py b/admin/src/admin/lib/nextcloud.py index cfba60a..9b5524d 100644 --- a/admin/src/admin/lib/nextcloud.py +++ b/admin/src/admin/lib/nextcloud.py @@ -10,6 +10,7 @@ import traceback import urllib import requests +from psycopg2 import sql # from ..lib.log import * from admin import app @@ -520,49 +521,45 @@ class Nextcloud: # 103 - failed to add the group def set_user_mail(self, data): - if not len( - self.nextcloud_pg.select( - """SELECT * FROM "oc_mail_accounts" WHERE "email" = '%s'""" - % (data["email"]) - ) - ): - self.nextcloud_pg.update( - """INSERT INTO "oc_mail_accounts" ("user_id","name","email","inbound_host","inbound_port","inbound_ssl_mode","inbound_user","inbound_password","outbound_host","outbound_port","outbound_ssl_mode","outbound_user","outbound_password") VALUES + query = """SELECT * FROM "oc_mail_accounts" WHERE "email" = '%s'""" + sql_query = sql.SQL(query.format(data["email"])) + if not len(self.nextcloud_pg.select(sql_query)): + query = """INSERT INTO "oc_mail_accounts" ("user_id","name","email","inbound_host","inbound_port","inbound_ssl_mode","inbound_user","inbound_password","outbound_host","outbound_port","outbound_ssl_mode","outbound_user","outbound_password") VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s');""" - % ( - data["user_id"], - data["name"], - data["email"], - data["inbound_host"], - data["inbound_port"], - data["inbound_ssl_mode"], - data["inbound_user"], - data["inbound_password"], - data["outbound_host"], - data["outbound_port"], - data["outbound_ssl_mode"], - data["outbound_user"], - data["outbound_password"], - ) - ) + account = [ + data["user_id"], + data["name"], + data["email"], + data["inbound_host"], + data["inbound_port"], + data["inbound_ssl_mode"], + data["inbound_user"], + data["inbound_password"], + data["outbound_host"], + data["outbound_port"], + data["outbound_ssl_mode"], + data["outbound_user"], + data["outbound_password"], + ] else: - self.nextcloud_pg.update( - """UPDATE "oc_mail_accounts" SET ("user_id","name","email","inbound_host","inbound_port","inbound_ssl_mode","inbound_user","inbound_password","outbound_host","outbound_port","outbound_ssl_mode","outbound_user","outbound_password") = + query = """UPDATE "oc_mail_accounts" SET ("user_id","name","email","inbound_host","inbound_port","inbound_ssl_mode","inbound_user","inbound_password","outbound_host","outbound_port","outbound_ssl_mode","outbound_user","outbound_password") = ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s') WHERE email = '%s';""" - % ( - data["user_id"], - data["name"], - data["email"], - data["inbound_host"], - data["inbound_port"], - data["inbound_ssl_mode"], - data["inbound_user"], - data["inbound_password"], - data["outbound_host"], - data["outbound_port"], - data["outbound_ssl_mode"], - data["outbound_user"], - data["outbound_password"], - data["email"], - ) - ) + + account = [ + data["user_id"], + data["name"], + data["email"], + data["inbound_host"], + data["inbound_port"], + data["inbound_ssl_mode"], + data["inbound_user"], + data["inbound_password"], + data["outbound_host"], + data["outbound_port"], + data["outbound_ssl_mode"], + data["outbound_user"], + data["outbound_password"], + data["email"], + ] + sql_query = sql.SQL(query.format(",".join([str(acc) for acc in account]))) + self.nextcloud_pg.update(sql_query) diff --git a/admin/src/start.py b/admin/src/start.py index cf1a2bc..d778b97 100644 --- a/admin/src/start.py +++ b/admin/src/start.py @@ -6,12 +6,10 @@ monkey_patch() import json -from flask_login import login_required -from admin import app from admin.auth.tokens import get_token_payload from admin.lib.api_exceptions import Error from flask import request -from flask_login import current_user +from flask_login import current_user, login_required from flask_socketio import ( SocketIO, close_room, @@ -23,6 +21,7 @@ from flask_socketio import ( send, ) +from admin import app app.socketio = SocketIO(app)