feat(api): users and groups actions

darta 2022-04-17 00:12:46 +02:00
parent c2fc854f16
commit e9a6c7108d
13 changed files with 667 additions and 565 deletions

View File

@ -3,7 +3,6 @@ Flask-Login==0.5.0
eventlet==0.33.0 eventlet==0.33.0
Flask-SocketIO==5.1.0 Flask-SocketIO==5.1.0
bcrypt==3.2.0 bcrypt==3.2.0
diceware==0.9.6 diceware==0.9.6
mysql-connector-python==8.0.25 mysql-connector-python==8.0.25
psycopg2==2.8.6 psycopg2==2.8.6
@ -13,5 +12,5 @@ urllib3==1.26.6
schema==0.7.5 schema==0.7.5
Werkzeug~=2.0.0 Werkzeug~=2.0.0
python-jose==3.3.0 python-jose==3.3.0
# Unused yet Cerberus==1.3.4
#flask-oidc==1.4.0 PyYAML==6.0

View File

@ -108,4 +108,4 @@ def send_custom(path):
""" """
Import all views Import all views
""" """
from .views import ApiViews, InternalViews, LoginViews, WebViews from .views import ApiViews, AppViews, LoginViews, WebViews, WpViews

View File

@ -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

View File

@ -30,6 +30,7 @@ options.num = 3
import secrets import secrets
from .api_exceptions import Error
from .events import Events from .events import Events
from .exceptions import UserExists, UserNotFound from .exceptions import UserExists, UserNotFound
from .helpers import ( from .helpers import (
@ -466,6 +467,10 @@ class Admin:
def _get_roles(self): def _get_roles(self):
return filter_roles_listofdicts(self.keycloak.get_roles()) 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): def get_keycloak_groups(self):
log.warning("Loading keycloak groups...") log.warning("Loading keycloak groups...")
return self.keycloak.get_groups() return self.keycloak.get_groups()
@ -1812,6 +1817,7 @@ class Admin:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
self.resync_data() self.resync_data()
return uid
def add_group(self, g): def add_group(self, g):
# TODO: Check if exists # TODO: Check if exists
@ -1830,6 +1836,7 @@ class Admin:
self.moodle.add_system_cohort(new_path, description=g["description"]) self.moodle.add_system_cohort(new_path, description=g["description"])
self.nextcloud.add_group(new_path) self.nextcloud.add_group(new_path)
self.resync_data() self.resync_data()
return new_path
def delete_group_by_id(self, group_id): def delete_group_by_id(self, group_id):
ev = Events("Deleting group", "Deleting from keycloak") ev = Events("Deleting group", "Deleting from keycloak")
@ -1843,6 +1850,7 @@ class Admin:
+ str(group_id) + str(group_id)
+ " as it does not exist!" + " 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}} # {'id': '966ad67c-499a-4f56-bd1d-283691cde0e7', 'name': 'asdgfewfwe', 'path': '/asdgfewfwe', 'attributes': {}, 'realmRoles': [], 'clientRoles': {}, 'subGroups': [], 'access': {'view': True, 'manage': True, 'manageMembership': True}}

View File

@ -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

View File

@ -6,9 +6,45 @@ import os
import sys import sys
import traceback import traceback
import yaml
from cerberus import Validator, rules_set_registry, schema_registry
from admin import app 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: class loadConfig:
def __init__(self, app=None): def __init__(self, app=None):
try: try:
@ -34,9 +70,7 @@ class loadConfig:
app.config.setdefault( app.config.setdefault(
"VERIFY", True if os.environ["VERIFY"] == "true" else False "VERIFY", True if os.environ["VERIFY"] == "true" else False
) )
app.config.setdefault( app.config.setdefault("API_SECRET", os.environ.get("API_SECRET"))
"API_SECRET", os.environ.get("API_SECRET")
)
except Exception as e: except Exception as e:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise

View File

@ -0,0 +1,11 @@
name:
required: true
type: string
description:
required: false
type: string
default: "Api created"
parent:
required: false
type: string
default: ""

View File

@ -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

View File

@ -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

View File

@ -1,545 +1,298 @@
#!flask/bin/python #!flask/bin/python
# coding=utf-8 # coding=utf-8
import concurrent.futures
import json import json
import logging as log import logging as log
import os import os
import re import socket
import sys import sys
# import Queue
import threading
import time import time
import traceback import traceback
from uuid import uuid4
from flask import Response, jsonify, redirect, render_template, request, url_for from flask import request
from flask_login import current_user, login_required
from admin import app from admin import app
from ..lib.helpers import system_group from ..lib.api_exceptions import Error
from .decorators import is_admin from .decorators import has_token
threads = {"external": None}
# q = Queue.Queue()
from keycloak.exceptions import KeycloakGetError
from ..lib.dashboard import Dashboard
from ..lib.exceptions import UserExists, UserNotFound
dashboard = Dashboard()
@app.route("/sysadmin/api/resync") ## LISTS
@app.route("/api/resync") @app.route("/ddapi/users", methods=["GET"])
@login_required @has_token
def resync(): def ddapi_users():
return ( if request.method == "GET":
json.dumps(app.admin.resync_data()), sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k["username"])
200, users = []
{"Content-Type": "application/json"}, 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("/ddapi/users/filter", methods=["POST"])
@app.route("/api/users/<provider>", methods=["POST", "PUT", "GET", "DELETE"]) @has_token
@login_required def ddapi_users_search():
def users(provider=False):
if request.method == "DELETE":
if current_user.role != "admin":
return json.dumps({}), 301, {"Content-Type": "application/json"}
if provider == "keycloak":
return (
json.dumps(app.admin.delete_keycloak_users()),
200,
{"Content-Type": "application/json"},
)
if provider == "nextcloud":
return (
json.dumps(app.admin.delete_nextcloud_users()),
200,
{"Content-Type": "application/json"},
)
if provider == "moodle":
return (
json.dumps(app.admin.delete_moodle_users()),
200,
{"Content-Type": "application/json"},
)
if request.method == "POST": if request.method == "POST":
if current_user.role != "admin": data = request.get_json(force=True)
return json.dumps({}), 301, {"Content-Type": "application/json"} if not data.get("text"):
if provider == "moodle": raise Error("bad_request", "Incorrect data requested.")
return ( users = app.admin.get_mix_users()
json.dumps(app.admin.sync_to_moodle()), result = [user_parser(user) for user in filter_users(users, data["text"])]
200, sorted_result = sorted(result, key=lambda k: k["id"])
{"Content-Type": "application/json"}, return json.dumps(sorted_result), 200, {"Content-Type": "application/json"}
)
if provider == "nextcloud":
return (
json.dumps(app.admin.sync_to_nextcloud()),
200,
{"Content-Type": "application/json"},
)
if request.method == "PUT" and not provider:
if current_user.role != "admin":
return json.dumps({}), 301, {"Content-Type": "application/json"}
if "external" in threads.keys():
if threads["external"] is not None and threads["external"].is_alive():
return (
json.dumps(
{"msg": "Precondition failed: already working with users"}
),
412,
{"Content-Type": "application/json"},
)
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": @app.route("/ddapi/group/users", methods=["POST"])
for user in users: @has_token
user["keycloak_groups"] = [ def ddapi_group_users():
g for g in user["keycloak_groups"] if not system_group(g) 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"} elif data.get("path"):
@app.route("/api/users_bulk/<action>", methods=["PUT"])
@login_required
def users_bulk(action):
data = request.get_json(force=True)
if request.method == "PUT":
if action == "enable":
if "external" in threads.keys():
if threads["external"] is not None and threads["external"].is_alive():
return (
json.dumps(
{"msg": "Precondition failed: already operating users"}
),
412,
{"Content-Type": "application/json"},
)
else:
threads["external"] = None
try: try:
threads["external"] = threading.Thread( name = [
target=app.admin.enable_users, args=(data,) g["name"]
) for g in app.admin.get_mix_groups()
threads["external"].start() if g["path"] == data.get("path")
return json.dumps({}), 200, {"Content-Type": "application/json"} ][0]
group_users = [
user_parser(user)
for user in sorted_users
if name in user["keycloak_groups"]
]
except: except:
log.error(traceback.format_exc()) raise Error("not_found", "Group path not found in system")
return ( elif data.get("keycloak_id"):
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
try: try:
threads["external"] = threading.Thread( name = [
target=app.admin.disable_users, args=(data,) g["name"]
) for g in app.admin.get_mix_groups()
threads["external"].start() if g["id"] == data.get("keycloak_id")
return json.dumps({}), 200, {"Content-Type": "application/json"} ][0]
group_users = [
user_parser(user)
for user in sorted_users
if name in user["keycloak_groups"]
]
except: except:
log.error(traceback.format_exc()) raise Error("not_found", "Group keycloak_id not found in system")
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/<userid>", methods=["PUT"])
@login_required
def user_password(userid=False):
if request.method == "GET":
return (
json.dumps(app.admin.get_dice_pwd()),
200,
{"Content-Type": "application/json"},
)
if request.method == "PUT":
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/<userid>", 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/<group_id>", 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"])
else: else:
res = app.admin.delete_group_by_id(group_id) raise Error("bad_request", "Incorrect data requested.")
return json.dumps(res), 200, {"Content-Type": "application/json"} return json.dumps(group_users), 200, {"Content-Type": "application/json"}
@app.route("/api/groups") @app.route("/ddapi/roles", methods=["GET"])
@app.route("/api/groups/<provider>", methods=["POST", "PUT", "GET", "DELETE"]) @has_token
@login_required def ddapi_roles():
def groups(provider=False):
if request.method == "GET": if request.method == "GET":
sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: str(k["name"])) roles = []
if current_user.role != "admin": for role in sorted(app.admin.get_roles(), key=lambda k: k["name"]):
## internal groups should be avoided as are assigned with the role log.error(role)
sorted_groups = [sg for sg in sorted_groups if not system_group(sg["name"])] roles.append(
else: {
sorted_groups = [sg for sg in sorted_groups] "keycloak_id": role["id"],
return json.dumps(sorted_groups), 200, {"Content-Type": "application/json"} "id": role["name"],
if request.method == "DELETE": "name": role["name"],
if provider == "keycloak": "description": role.get("description", ""),
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!",
} }
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/<item>", 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/<item>", 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": "<b>Legal</b><br>This works! in lang: " + lang}),
200,
{"Content-Type": "application/json"},
) )
# if item == "privacy": return json.dumps(roles), 200, {"Content-Type": "application/json"}
# return json.dumps({ "html": "<b>Privacy policy</b><br>This works!"}), 200, {'Content-Type': 'application/json'}
@app.route("/ddapi/role/users", methods=["POST"])
@has_token
def ddapi_role_users():
if request.method == "POST": if request.method == "POST":
if item == "legal": data = request.get_json(force=True)
data = None 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: try:
data = request.json id = [
html = data["html"] r["id"]
lang = data["lang"] 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: except:
log.error(traceback.format_exc()) raise Error("not_found", "Role keycloak_id not found in system")
return json.dumps(data), 200, {"Content-Type": "application/json"} else:
# if item == "privacy": raise Error("bad_request", "Incorrect data requested.")
# data = None return json.dumps(role_users), 200, {"Content-Type": "application/json"}
# try:
# data = request.json
# html = data["html"] ## INDIVIDUAL ACTIONS
# lang = data["lang"] @app.route("/ddapi/user", methods=["POST"])
# except: @app.route("/ddapi/user/<user_ddid>", methods=["PUT", "GET", "DELETE"])
# log.error(traceback.format_exc()) @has_token
# return json.dumps(data), 200, {'Content-Type': 'application/json'} 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/<old_user_ddid>/<new_user_did>", 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/<id>", methods=["GET", "POST", "DELETE"])
# @app.route("/api/group/<group_id>", 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"]
]

View File

@ -3,6 +3,7 @@
import json import json
import logging as log import logging as log
import os import os
import socket
import sys import sys
import time import time
import traceback import traceback
@ -11,12 +12,11 @@ from flask import request
from admin import app 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"]) @app.route("/api/internal/users", methods=["GET"])
@is_internal_or_has_token @is_internal
def internal_users(): def internal_users():
log.error(socket.gethostbyname("isard-apps-wordpress")) log.error(socket.gethostbyname("isard-apps-wordpress"))
if request.method == "GET": if request.method == "GET":
@ -31,7 +31,7 @@ def internal_users():
@app.route("/api/internal/users/filter", methods=["POST"]) @app.route("/api/internal/users/filter", methods=["POST"])
@is_internal_or_has_token @is_internal
def internal_users_search(): def internal_users_search():
if request.method == "POST": if request.method == "POST":
data = request.get_json(force=True) data = request.get_json(force=True)
@ -42,7 +42,7 @@ def internal_users_search():
@app.route("/api/internal/groups", methods=["GET"]) @app.route("/api/internal/groups", methods=["GET"])
@is_internal_or_has_token @is_internal
def internal_groups(): def internal_groups():
if request.method == "GET": if request.method == "GET":
sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k["name"]) sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k["name"])
@ -61,7 +61,7 @@ def internal_groups():
@app.route("/api/internal/group/users", methods=["POST"]) @app.route("/api/internal/group/users", methods=["POST"])
@is_internal_or_has_token @is_internal
def internal_group_users(): def internal_group_users():
if request.method == "POST": if request.method == "POST":
data = request.get_json(force=True) data = request.get_json(force=True)
@ -80,7 +80,7 @@ def internal_group_users():
@app.route("/api/internal/roles", methods=["GET"]) @app.route("/api/internal/roles", methods=["GET"])
@is_internal_or_has_token @is_internal
def internal_roles(): def internal_roles():
if request.method == "GET": if request.method == "GET":
roles = [] roles = []
@ -98,7 +98,7 @@ def internal_roles():
@app.route("/api/internal/role/users", methods=["POST"]) @app.route("/api/internal/role/users", methods=["POST"])
@is_internal_or_has_token @is_internal
def internal_role_users(): def internal_role_users():
if request.method == "POST": if request.method == "POST":
data = request.get_json(force=True) data = request.get_json(force=True)

View File

@ -1,15 +1,17 @@
#!flask/bin/python #!flask/bin/python
# coding=utf-8 # coding=utf-8
import json
import logging as log
import os
import socket import socket
from functools import wraps 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 import redirect, request, url_for
from flask_login import current_user, logout_user from flask_login import current_user, logout_user
from jose import jwt
from ..auth.tokens import get_header_jwt_payload
def is_admin(fn): def is_admin(fn):
@ -34,22 +36,29 @@ def is_internal(fn):
## but we should check if it is internal net and not haproxy ## but we should check if it is internal net and not haproxy
if socket.gethostbyname("isard-apps-wordpress") == remote_addr: if socket.gethostbyname("isard-apps-wordpress") == remote_addr:
return fn(*args, **kwargs) return fn(*args, **kwargs)
logout_user() return (
return redirect(url_for("login")) json.dumps(
{
"error": "unauthorized",
"msg": "Unauthorized access",
}
),
401,
{"Content-Type": "application/json"},
)
return decorated_view return decorated_view
def has_token(fn): def has_token(fn):
@wraps(fn) @wraps(fn)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
payload = get_header_jwt_payload() payload = get_header_jwt_payload()
# if payload.get("role_id") != "admin":
# maintenance()
kwargs["payload"] = payload
return fn(*args, **kwargs) return fn(*args, **kwargs)
return decorated return decorated
def is_internal_or_has_token(fn): def is_internal_or_has_token(fn):
@wraps(fn) @wraps(fn)
def decorated_view(*args, **kwargs): def decorated_view(*args, **kwargs):
@ -58,32 +67,22 @@ def is_internal_or_has_token(fn):
if "X-Forwarded-For" in request.headers if "X-Forwarded-For" in request.headers
else request.remote_addr.split(",")[0] else request.remote_addr.split(",")[0]
) )
## Now only checks if it is wordpress container, payload = get_header_jwt_payload()
## 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: if socket.gethostbyname("isard-apps-wordpress") == remote_addr:
return fn(*args, **kwargs) return fn(*args, **kwargs)
else: payload = get_header_jwt_payload()
logout_user() return fn(*args, **kwargs)
return redirect(url_for("login"))
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 return decorated_view

View File

@ -41,9 +41,9 @@ if __name__ == "__main__":
app, app,
host="0.0.0.0", host="0.0.0.0",
port=9000, port=9000,
debug=False, debug=True,
# ssl_context="adhoc", )
# async_mode="threading", # ssl_context="adhoc",
) # , logger=logger, engineio_logger=engineio_logger) # async_mode="threading",
# ) # , logger=logger, engineio_logger=engineio_logger)
# , cors_allowed_origins="*" # , cors_allowed_origins="*"
# /usr/lib/python3.8/site-packages/certifi