feat(api): users and groups actions
parent
c2fc854f16
commit
e9a6c7108d
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: "Api created"
|
||||||
|
parent:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ""
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"]
|
||||||
|
]
|
||||||
|
|
|
@ -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)
|
|
@ -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
|
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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
Loading…
Reference in New Issue