From 2c223b2b0229bf30ca035a7a455789e6cc1bf5c1 Mon Sep 17 00:00:00 2001 From: Antonio Manzano Date: Sun, 25 Jul 2021 18:54:58 +0200 Subject: [PATCH 1/6] Added temporary --- admin/src/scripts/temporary_no.py | 128 ++++++++++++++++++++++++++ admin/src/scripts/temporary_no.py.mod | 119 ++++++++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 admin/src/scripts/temporary_no.py create mode 100644 admin/src/scripts/temporary_no.py.mod diff --git a/admin/src/scripts/temporary_no.py b/admin/src/scripts/temporary_no.py new file mode 100644 index 0000000..8c38f8c --- /dev/null +++ b/admin/src/scripts/temporary_no.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +import time ,os +from datetime import datetime, timedelta + +import logging as log +import traceback +import yaml, json +from pprint import pprint + +from jinja2 import Environment, FileSystemLoader + +from keycloak import KeycloakAdmin +from postgres import Postgres + + +class KeycloakClient(): + """https://www.keycloak.org/docs-api/13.0/rest-api/index.html + https://github.com/marcospereirampj/python-keycloak + https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f + """ + def __init__(self, + url="http://isard-sso-keycloak:8080/auth/", + username=os.environ['KEYCLOAK_USER'], + password=os.environ['KEYCLOAK_PASSWORD'], + realm='master', + verify=True): + self.url=url + self.username=username + self.password=password + self.realm=realm + self.verify=verify + + self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD']) + + def connect(self): + self.keycloak_admin = KeycloakAdmin(server_url=self.url, + username=self.username, + password=self.password, + realm_name=self.realm, + verify=self.verify) + + + def update_pwds(self): + self.get_users() + + def get_users(self): + self.connect() + users=self.get_users_with_groups_and_roles() + userupdate=[] + for u in users: + if u['username'] not in ['admin','ddadmin'] and not u['username'].startswith('system_'): + print('Generating password for user '+u['username']) + userupdate.append({'id':u['id'], + 'username':u['username'], + 'password': diceware.get_passphrase(options=options)}) + with open("user_temp_passwd.csv","w") as csv: + for user in userupdate: + csv.write("%s,%s,%s\n"%(user['id'],user['username'],user['password'])) + + for u in userupdate: + print('Updating keycloak password for user '+u['username']) + self.update_user_pwd(u['id'],u['password']) + + def update_user_pwd_temporary(self,temporary=False): + payload={"credentials":[{"temporary":temporary}]} + self.connect() + self.keycloak_admin.update_user( user_id, payload) + + def get_users_with_groups_and_roles(self): + q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota + ,json_agg(g."name") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2 + ,json_agg(r.name) as role + from user_entity as u + left join user_attribute as ua on ua.user_id=u.id and ua.name = 'quota' + left join user_group_membership as ugm on ugm.user_id = u.id + left join keycloak_group as g on g.id = ugm.group_id + left join keycloak_group as g_parent on g.parent_group = g_parent.id + left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id + left join user_role_mapping as rm on rm.user_id = u.id + left join keycloak_role as r on r.id = rm.role_id + group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value + order by u.username""" + + # q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota, g.id, g.path, g.name, + # --,json_agg(g."name") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2 + # --,json_agg(r.name) as role + # from user_entity as u + # left join user_attribute as ua on ua.user_id=u.id and ua.name = 'quota' + # left join user_group_membership as ugm on ugm.user_id = u.id + # left join keycloak_group as g on g.id = ugm.group_id + # --left join keycloak_group as g_parent on g.parent_group = g_parent.id + # --left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id + # left join user_role_mapping as rm on rm.user_id = u.id + # left join keycloak_role as r on r.id = rm.role_id + # --group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value + # order by u.username""" + + # q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota + # ,json_agg(g."name") as group_name,json_agg(g."id") as group_id,json_agg(g."path") as group_path + # ,json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2 + # ,json_agg(r.name) as role + # from user_entity as u + # left join user_attribute as ua on ua.user_id=u.id and ua.name = 'quota' + # left join user_group_membership as ugm on ugm.user_id = u.id + # left join keycloak_group as g on g.id = ugm.group_id + # left join keycloak_group as g_parent on g.parent_group = g_parent.id + # left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id + # left join user_role_mapping as rm on rm.user_id = u.id + # left join keycloak_role as r on r.id = rm.role_id + # group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value + # order by u.username""" + (headers,users)=self.keycloak_pg.select_with_headers(q) + + users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\ + ([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\ + ([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\ + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users] + + users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\ + ([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\ + ([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\ + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists] + + list_dict_users = [dict(zip(headers, r)) for r in users_with_lists] + return list_dict_users + +k=KeycloakClient() +k.update_user_pwd_temporary() diff --git a/admin/src/scripts/temporary_no.py.mod b/admin/src/scripts/temporary_no.py.mod new file mode 100644 index 0000000..7aecb49 --- /dev/null +++ b/admin/src/scripts/temporary_no.py.mod @@ -0,0 +1,119 @@ +#!/usr/bin/env python +import time ,os +from datetime import datetime, timedelta + +import logging as log +import traceback +import yaml, json +from pprint import pprint + +from jinja2 import Environment, FileSystemLoader + +from keycloak import KeycloakAdmin +from postgres import Postgres + + +class KeycloakClient(): + """https://www.keycloak.org/docs-api/13.0/rest-api/index.html + https://github.com/marcospereirampj/python-keycloak + https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f + """ + def __init__(self, + url="http://isard-sso-keycloak:8080/auth/", + username=os.environ['KEYCLOAK_USER'], + password=os.environ['KEYCLOAK_PASSWORD'], + realm='master', + verify=True): + self.url=url + self.username=username + self.password=password + self.realm=realm + self.verify=verify + + self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD']) + + def connect(self): + self.keycloak_admin = KeycloakAdmin(server_url=self.url, + username=self.username, + password=self.password, + realm_name=self.realm, + verify=self.verify) + + + def run(self): + self.get_users() + + def get_users(self): + self.connect() + users=self.get_users_with_groups_and_roles() + for u in users: + if u['username']=='proves-meves': pprint(u) + print('Updating keycloak temporary for user '+u['username']) + self.update_user_pwd_temporary(u['id']) + + def update_user_pwd_temporary(self,user_id,temporary=False): + payload={"credentials":[{"temporary":temporary}], + "requiredActions": []} + self.connect() + self.keycloak_admin.update_user( user_id, payload) + + def get_users_with_groups_and_roles(self): + q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota + ,json_agg(g."name") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2 + ,json_agg(r.name) as role + from user_entity as u + left join user_attribute as ua on ua.user_id=u.id and ua.name = 'quota' + left join user_group_membership as ugm on ugm.user_id = u.id + left join keycloak_group as g on g.id = ugm.group_id + left join keycloak_group as g_parent on g.parent_group = g_parent.id + left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id + left join user_role_mapping as rm on rm.user_id = u.id + left join keycloak_role as r on r.id = rm.role_id + group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value + order by u.username""" + + # q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota, g.id, g.path, g.name, + # --,json_agg(g."name") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2 + # --,json_agg(r.name) as role + # from user_entity as u + # left join user_attribute as ua on ua.user_id=u.id and ua.name = 'quota' + # left join user_group_membership as ugm on ugm.user_id = u.id + # left join keycloak_group as g on g.id = ugm.group_id + # --left join keycloak_group as g_parent on g.parent_group = g_parent.id + # --left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id + # left join user_role_mapping as rm on rm.user_id = u.id + # left join keycloak_role as r on r.id = rm.role_id + # --group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value + # order by u.username""" + + # q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota + # ,json_agg(g."name") as group_name,json_agg(g."id") as group_id,json_agg(g."path") as group_path + # ,json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2 + # ,json_agg(r.name) as role + # from user_entity as u + # left join user_attribute as ua on ua.user_id=u.id and ua.name = 'quota' + # left join user_group_membership as ugm on ugm.user_id = u.id + # left join keycloak_group as g on g.id = ugm.group_id + # left join keycloak_group as g_parent on g.parent_group = g_parent.id + # left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id + # left join user_role_mapping as rm on rm.user_id = u.id + # left join keycloak_role as r on r.id = rm.role_id + # group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value + # order by u.username""" + (headers,users)=self.keycloak_pg.select_with_headers(q) + + users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\ + ([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\ + ([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\ + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users] + + users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\ + ([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\ + ([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\ + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists] + + list_dict_users = [dict(zip(headers, r)) for r in users_with_lists] + return list_dict_users + +k=KeycloakClient() +k.run() From 369efb980f04aaeb357eb177b245205f48f22ca3 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 23 Jul 2021 19:28:19 +0200 Subject: [PATCH 2/6] Internal api will open some ports internally to wordpress initially --- admin/src/admin/__init__.py | 1 + admin/src/admin/auth/authentication.py | 5 ++ admin/src/admin/views/ApiViews.py | 4 -- admin/src/admin/views/InternalViews.py | 70 ++++++++++++++++++++++++++ admin/src/admin/views/decorators.py | 18 +++++++ admin/src/wordpress_saml_onlycerts.py | 2 +- docker/haproxy/haproxy.conf | 4 ++ 7 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 admin/src/admin/views/InternalViews.py create mode 100644 admin/src/admin/views/decorators.py diff --git a/admin/src/admin/__init__.py b/admin/src/admin/__init__.py index f5ed74d..bf623a5 100644 --- a/admin/src/admin/__init__.py +++ b/admin/src/admin/__init__.py @@ -87,6 +87,7 @@ Import all views from .views import LoginViews from .views import WebViews from .views import ApiViews +from .views import InternalViews diff --git a/admin/src/admin/auth/authentication.py b/admin/src/admin/auth/authentication.py index 26b5a70..b89d42e 100644 --- a/admin/src/admin/auth/authentication.py +++ b/admin/src/admin/auth/authentication.py @@ -36,6 +36,11 @@ ram_users={ 'id': os.environ["KEYCLOAK_USER"], 'password': os.environ["KEYCLOAK_PASSWORD"], 'role': 'admin', + }, + os.environ["WORDPRESS_MARIADB_USER"]: { + 'id': os.environ["WORDPRESS_MARIADB_USER"], + 'password': os.environ["WORDPRESS_MARIADB_PASSWORD"], + 'role': 'manager', } } diff --git a/admin/src/admin/views/ApiViews.py b/admin/src/admin/views/ApiViews.py index e11a7ac..9c3287c 100644 --- a/admin/src/admin/views/ApiViews.py +++ b/admin/src/admin/views/ApiViews.py @@ -15,8 +15,6 @@ import threading threads={} # q = Queue.Queue() -from pprint import pprint - from keycloak.exceptions import KeycloakGetError @app.route('/api/resync') @@ -99,8 +97,6 @@ def groups(provider=False): if provider == 'keycloak': return json.dumps(app.admin.delete_keycloak_groups()), 200, {'Content-Type': 'application/json'} - - ### SYSADM USERS ONLY @app.route('/api/external', methods=['POST', 'PUT', 'GET','DELETE']) diff --git a/admin/src/admin/views/InternalViews.py b/admin/src/admin/views/InternalViews.py new file mode 100644 index 0000000..b0f9498 --- /dev/null +++ b/admin/src/admin/views/InternalViews.py @@ -0,0 +1,70 @@ +#!flask/bin/python +# coding=utf-8 +from admin import app +import logging as log +import traceback + +import time,json +import sys,os + +from flask import request +from .decorators import is_internal + +@app.route('/api/internal/users', methods=['GET']) +@is_internal +def internal_users(): + if request.method == 'GET': + sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k['username']) + # group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']] + users=[] + for user in sorted_users: + if not user['enabled']: continue + users.append(user_parser(user)) + return json.dumps(users), 200, {'Content-Type': 'application/json'} + +@app.route('/api/internal/users/filter', methods=['POST']) +@is_internal +def internal_users_search(): + if request.method == 'POST': + data=request.get_json(force=True) + users = app.admin.get_mix_users() + result = [user_parser(user) for user in users + if data['text'] in user['username'] or + data['text'] in user['first'] or + data['text'] in user['last'] or + data['text'] in user['email']] + sorted_result = sorted(result, key=lambda k: k['id']) + return json.dumps(sorted_result), 200, {'Content-Type': 'application/json'} + +@app.route('/api/internal/groups', methods=['GET']) +@is_internal +def internal_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({'id':group['path'], + 'name':group['name'], + 'description':group.get('description','')}) + return json.dumps(groups), 200, {'Content-Type': 'application/json'} + +@app.route('/api/internal/group/users', methods=['POST']) +@is_internal +def internal_group_users(): + if request.method == 'POST': + data=request.get_json(force=True) + sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k['username']) + # group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']] + users=[] + for user in sorted_users: + if data['path'] not in user['keycloak_groups'] or not user['enabled']: continue + users.append(user_parser(user)) + return json.dumps(users), 200, {'Content-Type': 'application/json'} + +def user_parser(user): + return {'id':user['username'], + 'first':user['first'], + 'last':user['last'], + 'role':user['roles'][0] if len(user['roles']) else None, + 'email':user['email'], + 'groups':user['keycloak_groups']} diff --git a/admin/src/admin/views/decorators.py b/admin/src/admin/views/decorators.py new file mode 100644 index 0000000..3b6c867 --- /dev/null +++ b/admin/src/admin/views/decorators.py @@ -0,0 +1,18 @@ +#!flask/bin/python +# coding=utf-8 + +from functools import wraps +from flask import request, redirect, url_for +from flask_login import logout_user +import socket + +def is_internal(fn): + @wraps(fn) + def decorated_view(*args, **kwargs): + remote_addr=request.headers['X-Forwarded-For'].split(',')[0] if 'X-Forwarded-For' in request.headers else request.remote_addr.split(',')[0] + ## Now only checks if it is wordpress container, + ## but we should check if it is internal net and not haproxy + if socket.gethostbyname('isard-apps-wordpress') == remote_addr: return fn(*args, **kwargs) + logout_user() + return redirect(url_for('login')) + return decorated_view \ No newline at end of file diff --git a/admin/src/wordpress_saml_onlycerts.py b/admin/src/wordpress_saml_onlycerts.py index b1e64cd..2cbc39c 100644 --- a/admin/src/wordpress_saml_onlycerts.py +++ b/admin/src/wordpress_saml_onlycerts.py @@ -126,4 +126,4 @@ class WordpressSaml(): self.db.update("""DELETE FROM wp_options WHERE option_name = 'onelogin_saml_advanced_settings_sp_x509cert'""") self.db.update("""DELETE FROM wp_options WHERE option_name = 'onelogin_saml_advanced_settings_sp_privatekey'""") -nw=WordpressSaml() \ No newline at end of file +nw=WordpressSaml() diff --git a/docker/haproxy/haproxy.conf b/docker/haproxy/haproxy.conf index 93e2a0a..d5fc9ff 100644 --- a/docker/haproxy/haproxy.conf +++ b/docker/haproxy/haproxy.conf @@ -166,6 +166,10 @@ backend be_wp acl existing-x-forwarded-proto req.hdr(X-Forwarded-Proto) -m found http-request add-header X-Forwarded-Host %[req.hdr(Host)] unless existing-x-forwarded-host http-request add-header X-Forwarded-Proto https unless existing-x-forwarded-proto + + http-request set-header X-SSL %[ssl_fc] + reqadd X-Forwarded-Proto:\ https + #http-request set-header X-Forwarded-Proto https server wp isard-apps-wordpress:80 check port 80 inter 5s rise 2 fall 10 resolvers mydns init-addr none From b8ade3bd108235829707f6795cdf7461b6e0e69b Mon Sep 17 00:00:00 2001 From: root Date: Fri, 23 Jul 2021 19:28:19 +0200 Subject: [PATCH 3/6] Wordpress api --- admin/src/admin/__init__.py | 1 + admin/src/admin/auth/authentication.py | 5 ++ admin/src/admin/views/ApiViews.py | 4 -- admin/src/admin/views/InternalViews.py | 83 ++++++++++++++++++++++++++ admin/src/admin/views/decorators.py | 18 ++++++ admin/src/wordpress_saml_onlycerts.py | 2 +- docker/haproxy/haproxy.conf | 4 ++ 7 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 admin/src/admin/views/InternalViews.py create mode 100644 admin/src/admin/views/decorators.py diff --git a/admin/src/admin/__init__.py b/admin/src/admin/__init__.py index f5ed74d..bf623a5 100644 --- a/admin/src/admin/__init__.py +++ b/admin/src/admin/__init__.py @@ -87,6 +87,7 @@ Import all views from .views import LoginViews from .views import WebViews from .views import ApiViews +from .views import InternalViews diff --git a/admin/src/admin/auth/authentication.py b/admin/src/admin/auth/authentication.py index 26b5a70..b89d42e 100644 --- a/admin/src/admin/auth/authentication.py +++ b/admin/src/admin/auth/authentication.py @@ -36,6 +36,11 @@ ram_users={ 'id': os.environ["KEYCLOAK_USER"], 'password': os.environ["KEYCLOAK_PASSWORD"], 'role': 'admin', + }, + os.environ["WORDPRESS_MARIADB_USER"]: { + 'id': os.environ["WORDPRESS_MARIADB_USER"], + 'password': os.environ["WORDPRESS_MARIADB_PASSWORD"], + 'role': 'manager', } } diff --git a/admin/src/admin/views/ApiViews.py b/admin/src/admin/views/ApiViews.py index e11a7ac..9c3287c 100644 --- a/admin/src/admin/views/ApiViews.py +++ b/admin/src/admin/views/ApiViews.py @@ -15,8 +15,6 @@ import threading threads={} # q = Queue.Queue() -from pprint import pprint - from keycloak.exceptions import KeycloakGetError @app.route('/api/resync') @@ -99,8 +97,6 @@ def groups(provider=False): if provider == 'keycloak': return json.dumps(app.admin.delete_keycloak_groups()), 200, {'Content-Type': 'application/json'} - - ### SYSADM USERS ONLY @app.route('/api/external', methods=['POST', 'PUT', 'GET','DELETE']) diff --git a/admin/src/admin/views/InternalViews.py b/admin/src/admin/views/InternalViews.py new file mode 100644 index 0000000..f33fd63 --- /dev/null +++ b/admin/src/admin/views/InternalViews.py @@ -0,0 +1,83 @@ +#!flask/bin/python +# coding=utf-8 +from admin import app +import logging as log +import traceback + +import time,json +import sys,os + +from flask import request +from .decorators import is_internal + +@app.route('/api/internal/users', methods=['GET']) +@is_internal +def internal_users(): + if request.method == 'GET': + sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k['username']) + # group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']] + users=[] + for user in sorted_users: + if not user['enabled']: continue + users.append(user_parser(user)) + return json.dumps(users), 200, {'Content-Type': 'application/json'} + +@app.route('/api/internal/users/filter', methods=['POST']) +@is_internal +def internal_users_search(): + if request.method == 'POST': + data=request.get_json(force=True) + users = app.admin.get_mix_users() + result = [user_parser(user) for user in users + if data['text'] in user['username'] or + data['text'] in user['first'] or + data['text'] in user['last'] or + data['text'] in user['email']] + sorted_result = sorted(result, key=lambda k: k['id']) + return json.dumps(sorted_result), 200, {'Content-Type': 'application/json'} + +@app.route('/api/internal/groups', methods=['GET']) +@is_internal +def internal_groups(): + if request.method == 'GET': + sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k['name']) + groups=[] + for group in sorted_groups: + if not group['path'].startswith('/'): continue + groups.append({'id':group['path'], + 'name':group['name'], + 'description':group.get('description','')}) + return json.dumps(groups), 200, {'Content-Type': 'application/json'} + +@app.route('/api/internal/group/users', methods=['POST']) +@is_internal +def internal_group_users(): + if request.method == 'POST': + data=request.get_json(force=True) + sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k['username']) + # group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']] + users=[] + for user in sorted_users: + if data['path'] not in user['keycloak_groups'] or not user['enabled']: continue + users.append(user_parser(user)) + return json.dumps(users), 200, {'Content-Type': 'application/json'} + +@app.route('/api/internal/roles', methods=['GET']) +@is_internal +def internal_roles(): + if request.method == 'GET': + roles=[] + for role in sorted(app.admin.get_roles(), key=lambda k: k['name']): + if role['name'] == 'admin': continue + roles.append({'id':role['id'], + 'name':role['name'], + 'description':role.get('description','')}) + return json.dumps(roles), 200, {'Content-Type': 'application/json'} + +def user_parser(user): + return {'id':user['username'], + 'first':user['first'], + 'last':user['last'], + 'role':user['roles'][0] if len(user['roles']) else None, + 'email':user['email'], + 'groups':user['keycloak_groups']} diff --git a/admin/src/admin/views/decorators.py b/admin/src/admin/views/decorators.py new file mode 100644 index 0000000..3b6c867 --- /dev/null +++ b/admin/src/admin/views/decorators.py @@ -0,0 +1,18 @@ +#!flask/bin/python +# coding=utf-8 + +from functools import wraps +from flask import request, redirect, url_for +from flask_login import logout_user +import socket + +def is_internal(fn): + @wraps(fn) + def decorated_view(*args, **kwargs): + remote_addr=request.headers['X-Forwarded-For'].split(',')[0] if 'X-Forwarded-For' in request.headers else request.remote_addr.split(',')[0] + ## Now only checks if it is wordpress container, + ## but we should check if it is internal net and not haproxy + if socket.gethostbyname('isard-apps-wordpress') == remote_addr: return fn(*args, **kwargs) + logout_user() + return redirect(url_for('login')) + return decorated_view \ No newline at end of file diff --git a/admin/src/wordpress_saml_onlycerts.py b/admin/src/wordpress_saml_onlycerts.py index b1e64cd..2cbc39c 100644 --- a/admin/src/wordpress_saml_onlycerts.py +++ b/admin/src/wordpress_saml_onlycerts.py @@ -126,4 +126,4 @@ class WordpressSaml(): self.db.update("""DELETE FROM wp_options WHERE option_name = 'onelogin_saml_advanced_settings_sp_x509cert'""") self.db.update("""DELETE FROM wp_options WHERE option_name = 'onelogin_saml_advanced_settings_sp_privatekey'""") -nw=WordpressSaml() \ No newline at end of file +nw=WordpressSaml() diff --git a/docker/haproxy/haproxy.conf b/docker/haproxy/haproxy.conf index 93e2a0a..ad5a37c 100644 --- a/docker/haproxy/haproxy.conf +++ b/docker/haproxy/haproxy.conf @@ -166,6 +166,10 @@ backend be_wp acl existing-x-forwarded-proto req.hdr(X-Forwarded-Proto) -m found http-request add-header X-Forwarded-Host %[req.hdr(Host)] unless existing-x-forwarded-host http-request add-header X-Forwarded-Proto https unless existing-x-forwarded-proto + + http-request set-header X-SSL %[ssl_fc] + #reqadd X-Forwarded-Proto:\ https + http-request set-header X-Forwarded-Proto https server wp isard-apps-wordpress:80 check port 80 inter 5s rise 2 fall 10 resolvers mydns init-addr none From 5048236e4ec6a7f45e55f5243534d7a491282511 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 24 Aug 2021 12:01:42 +0200 Subject: [PATCH 4/6] Added filter for roles and groups --- admin/src/admin/lib/admin.py | 16 +++++++---- admin/src/admin/lib/nextcloud.py | 5 +++- admin/src/admin/lib/nextcloud_exc.py | 3 ++ admin/src/admin/views/InternalViews.py | 38 +++++++++++++++++++++----- 4 files changed, 48 insertions(+), 14 deletions(-) diff --git a/admin/src/admin/lib/admin.py b/admin/src/admin/lib/admin.py index d98114e..41fa9c7 100644 --- a/admin/src/admin/lib/admin.py +++ b/admin/src/admin/lib/admin.py @@ -93,13 +93,17 @@ class Admin(): ### User admin in group admin try: log.warning('KEYCLOAK: Adding group admin and user admin to this group') - self.keycloak.add_group('admin') - ## Add default admin user to group admin (for nextcloud, just in case we go there) - admin_uid=self.keycloak_admin.get_user_id('admin') - self.keycloak_admin.group_user_add(admin_uid,gid) + admin_guid=self.keycloak.add_group('admin') + except: + pass + admin_guid=self.keycloak.get_group_by_path(path='/admin')['id'] + try: + ## Add default admin user to group admin + admin_uid=self.keycloak.get_user_id('admin') + self.keycloak.group_user_add(admin_uid,admin_guid) log.warning('KEYCLOAK: OK') except: - # print(traceback.format_exc()) + print(traceback.format_exc()) log.warning('KEYCLOAK: Seems to be there already') #### Add default groups @@ -812,4 +816,4 @@ class Admin(): return True def get_user(self,userid): - return [u for u in self.internal['users'] if u['id']==userid][0] \ No newline at end of file + return [u for u in self.internal['users'] if u['id']==userid][0] diff --git a/admin/src/admin/lib/nextcloud.py b/admin/src/admin/lib/nextcloud.py index c3bab4d..65677d7 100644 --- a/admin/src/admin/lib/nextcloud.py +++ b/admin/src/admin/lib/nextcloud.py @@ -29,7 +29,10 @@ class Nextcloud(): def _request(self,method,url,data={},headers={'OCS-APIRequest':'true'},auth=False): if auth == False: auth=self.auth try: - return requests.request(method, url, data=data, auth=auth, verify=self.verify_cert, headers=headers).text + response = requests.request(method, url, data=data, auth=auth, verify=self.verify_cert, headers=headers) + if 'meta' in response.text: + if '997' in response.text: raise ProviderUnauthorized + return response.text ## At least the ProviderSslError is not being catched or not raised correctly except requests.exceptions.HTTPError as errh: diff --git a/admin/src/admin/lib/nextcloud_exc.py b/admin/src/admin/lib/nextcloud_exc.py index 32baf33..827e93f 100644 --- a/admin/src/admin/lib/nextcloud_exc.py +++ b/admin/src/admin/lib/nextcloud_exc.py @@ -1,5 +1,8 @@ #!/usr/bin/env python # coding=utf-8 +class ProviderUnauthorized(Exception): + pass + class ProviderConnError(Exception): pass diff --git a/admin/src/admin/views/InternalViews.py b/admin/src/admin/views/InternalViews.py index f33fd63..c9916d9 100644 --- a/admin/src/admin/views/InternalViews.py +++ b/admin/src/admin/views/InternalViews.py @@ -28,11 +28,7 @@ def internal_users_search(): if request.method == 'POST': data=request.get_json(force=True) users = app.admin.get_mix_users() - result = [user_parser(user) for user in users - if data['text'] in user['username'] or - data['text'] in user['first'] or - data['text'] in user['last'] or - data['text'] in user['email']] + result = [user_parser(user) for user in filter_users(users, data['text'])] sorted_result = sorted(result, key=lambda k: k['id']) return json.dumps(sorted_result), 200, {'Content-Type': 'application/json'} @@ -59,8 +55,12 @@ def internal_group_users(): users=[] for user in sorted_users: if data['path'] not in user['keycloak_groups'] or not user['enabled']: continue - users.append(user_parser(user)) - return json.dumps(users), 200, {'Content-Type': 'application/json'} + users.append(user) + if data.get('text',False) and data['text'] != '': + result = [user_parser(user) for user in filter_users(users, data['text'])] + else: + result = [user_parser(user) for user in users] + return json.dumps(result), 200, {'Content-Type': 'application/json'} @app.route('/api/internal/roles', methods=['GET']) @is_internal @@ -74,6 +74,23 @@ def internal_roles(): 'description':role.get('description','')}) return json.dumps(roles), 200, {'Content-Type': 'application/json'} +@app.route('/api/internal/role/users', methods=['POST']) +@is_internal +def internal_role_users(): + if request.method == 'POST': + data=request.get_json(force=True) + sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k['username']) + # group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']] + users=[] + for user in sorted_users: + if data['role'] not in user['roles'] or not user['enabled']: continue + users.append(user) + if data.get('text',False) and data['text'] != '': + result = [user_parser(user) for user in filter_users(users, data['text'])] + else: + result = [user_parser(user) for user in users] + return json.dumps(result), 200, {'Content-Type': 'application/json'} + def user_parser(user): return {'id':user['username'], 'first':user['first'], @@ -81,3 +98,10 @@ def user_parser(user): 'role':user['roles'][0] if len(user['roles']) else None, 'email':user['email'], 'groups':user['keycloak_groups']} + +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']] \ No newline at end of file From ac1ca9227d7a31389352076dc7c2a489eb8c870a Mon Sep 17 00:00:00 2001 From: darta Date: Mon, 30 Aug 2021 13:23:34 +0200 Subject: [PATCH 5/6] fixed self signed generation certs --- admin/src/generate_certificates.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/admin/src/generate_certificates.sh b/admin/src/generate_certificates.sh index 3e7a233..e2beded 100755 --- a/admin/src/generate_certificates.sh +++ b/admin/src/generate_certificates.sh @@ -1,4 +1,4 @@ -cd saml_certs +cd /admin/saml_certs C=CA L=Barcelona O=localdomain @@ -6,5 +6,5 @@ CN_CA=$O CN_HOST=*.$O OU=$O openssl req -nodes -new -x509 -keyout private.key -out public.cert -subj "/C=$C/L=$L/O=$O/CN=$CN_CA" -days 3650 -cd .. -echo "Now run the python nextcloud and wordpress scripts" \ No newline at end of file +cd /admin +echo "Now run the python nextcloud and wordpress scripts" From 8731d7d98ff37fe6b360a6c4f455fcadcaacbc41 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 22 Aug 2021 22:24:19 +0200 Subject: [PATCH 6/6] User modal --- admin/docker/Dockerfile | 32 +- admin/docker/requirements.pip3 | 4 +- admin/docker/{docker-entrypoint.sh => run.sh} | 7 +- admin/src/admin/lib/admin.py | 272 ++++++++++++++++- admin/src/admin/lib/events.py | 9 +- admin/src/admin/lib/exceptions.py | 7 + admin/src/admin/lib/helpers.py | 11 +- admin/src/admin/lib/keycloak_client.py | 37 ++- admin/src/admin/lib/moodle.py | 40 ++- admin/src/admin/lib/nextcloud.py | 85 +++++- admin/src/admin/static/js/groups.js | 161 ++++++++-- admin/src/admin/static/js/roles.js | 15 +- admin/src/admin/static/js/status_socket.js | 6 +- admin/src/admin/static/js/users.js | 283 +++++++++++++----- .../admin/static/templates/pages/groups.html | 2 +- .../templates/pages/modals/groups_modals.html | 16 + .../templates/pages/modals/users_modals.html | 81 ++++- .../admin/static/templates/pages/roles.html | 1 - .../admin/static/templates/pages/users.html | 9 +- admin/src/admin/views/ApiViews.py | 90 +++++- admin/src/admin/views/WebViews.py | 4 + admin/src/admin/views/decorators.py | 9 +- docker-compose-parts/admin.devel.yml | 7 + docker-compose-parts/admin.yml | 13 +- 24 files changed, 1017 insertions(+), 184 deletions(-) rename admin/docker/{docker-entrypoint.sh => run.sh} (65%) create mode 100644 admin/src/admin/lib/exceptions.py create mode 100644 docker-compose-parts/admin.devel.yml diff --git a/admin/docker/Dockerfile b/admin/docker/Dockerfile index 8fd020b..d2b4f0b 100644 --- a/admin/docker/Dockerfile +++ b/admin/docker/Dockerfile @@ -17,23 +17,27 @@ RUN apk add --no-cache curl py3-yaml yarn libpq openssl RUN wget -O /usr/lib/python3.8/site-packages/diceware/wordlists/wordlist_cat_ascii.txt https://raw.githubusercontent.com/1ma/diceware-cat/master/cat-wordlist-ascii.txt # SSH configuration -ARG SSH_ROOT_PWD -RUN apk add openssh -RUN echo "root:$SSH_ROOT_PWD" |chpasswd -RUN sed -i \ - -e 's|[#]*PermitRootLogin prohibit-password|PermitRootLogin yes|g' \ - -e 's|[#]*PasswordAuthentication yes|PasswordAuthentication yes|g' \ - -e 's|[#]*ChallengeResponseAuthentication yes|ChallengeResponseAuthentication yes|g' \ - -e 's|[#]*UsePAM yes|UsePAM yes|g' \ - -e 's|[#]#Port 22|Port 22|g' \ - /etc/ssh/sshd_config +# ARG SSH_ROOT_PWD +# RUN apk add openssh +# RUN echo "root:$SSH_ROOT_PWD" |chpasswd +# RUN sed -i \ +# -e 's|[#]*PermitRootLogin prohibit-password|PermitRootLogin yes|g' \ +# -e 's|[#]*PasswordAuthentication yes|PasswordAuthentication yes|g' \ +# -e 's|[#]*ChallengeResponseAuthentication yes|ChallengeResponseAuthentication yes|g' \ +# -e 's|[#]*UsePAM yes|UsePAM yes|g' \ +# -e 's|[#]#Port 22|Port 22|g' \ +# /etc/ssh/sshd_config + +RUN apk add --no-cache git && \ + git clone -b delete_realm_roles https://github.com/isard-vdi/python-keycloak.git && \ + cd python-keycloak && \ + python3 setup.py install && \ + apk del git COPY admin/src /admin RUN cd /admin/admin && yarn install -COPY admin/docker/docker-entrypoint.sh / -ENTRYPOINT ["/docker-entrypoint.sh"] +COPY admin/docker/run.sh /run.sh #EXPOSE 7039 -WORKDIR /admin -CMD [ "python3", "start.py" ] \ No newline at end of file +CMD [ "/run.sh" ] \ No newline at end of file diff --git a/admin/docker/requirements.pip3 b/admin/docker/requirements.pip3 index 20b018a..b584583 100644 --- a/admin/docker/requirements.pip3 +++ b/admin/docker/requirements.pip3 @@ -1,4 +1,4 @@ -python-keycloak==0.24.0 +#python-keycloak==0.24.0 bcrypt==3.1.7 cffi==1.14.0 click==7.1.2 @@ -26,5 +26,5 @@ python-engineio==3.8.1 python-socketio==4.1.0 minio==7.0.3 - +urllib3==1.26.6 flask-oidc==1.4.0 \ No newline at end of file diff --git a/admin/docker/docker-entrypoint.sh b/admin/docker/run.sh similarity index 65% rename from admin/docker/docker-entrypoint.sh rename to admin/docker/run.sh index daedf29..b54e441 100755 --- a/admin/docker/docker-entrypoint.sh +++ b/admin/docker/run.sh @@ -1,10 +1,11 @@ #!/bin/sh -ssh-keygen -A +# ssh-keygen -A ## Only in development cd /admin/admin yarn install ## End Only in development cd /admin export PYTHONWARNINGS="ignore:Unverified HTTPS request" -python3 start.py & -/usr/sbin/sshd -D -e -f /etc/ssh/sshd_config \ No newline at end of file +python3 start.py +#& +# /usr/sbin/sshd -D -e -f /etc/ssh/sshd_config \ No newline at end of file diff --git a/admin/src/admin/lib/admin.py b/admin/src/admin/lib/admin.py index 41fa9c7..924a86a 100644 --- a/admin/src/admin/lib/admin.py +++ b/admin/src/admin/lib/admin.py @@ -18,8 +18,12 @@ options = diceware.handle_options(None) options.wordlist = 'cat_ascii' options.num = 3 +from .helpers import rand_password +from .exceptions import UserExists, UserNotFound from .events import Events +import secrets + class Admin(): def __init__(self): ready=False @@ -70,7 +74,8 @@ class Admin(): try: self.resync_data() ready=True - except: + except Exception as e: + print(e) log.error('Could not connect to moodle, waiting to be online...') sleep(2) @@ -270,7 +275,10 @@ class Admin(): "last": u['displayname'].split(' ')[1] if u['displayname'] is not None and len(u['displayname'].split(' '))>1 else '', "email": u.get('email',''), "groups": u['groups'], - "roles": False} + "roles": False, + "quota": u['quota'], + "quota_used_bytes": str(int(int(u['total_bytes'])/1024/1024/2))+' MB' if u['total_bytes'] != None else "0 MB"} + # The theoretical bytes returned do not map to any known unit. The division is just an approach while we investigate. for u in self.nextcloud.get_users_list() if u['username'] not in ['guest','ddadmin','admin'] and not u['username'].startswith('system')] ## TOO SLOW @@ -332,12 +340,19 @@ class Admin(): theuser['nextcloud']=True theuser['nextcloud_groups']=nextcloud_exists[0]['groups'] theuser['nextcloud_id']=nextcloud_exists[0]['id'] + theuser['quota']=theuser['quota'] if theuser['quota'] != None and theuser['quota'] != 'none' else False else: theuser['nextcloud']=False theuser['nextcloud_groups']=[] + theuser['quota']=False + theuser['quota_used_bytes']=False del theuser['groups'] - users.append(theuser) + if not len(theuser['roles']): + log.error(' SKIPPING USER WITHOUT ANY ROLE!!: '+theuser['username']+' . Should be fixed at keycloak level.') + continue + + users.append(theuser) return users def get_roles(self): @@ -611,7 +626,7 @@ class Admin(): if u['first'] == '': u['first']=' ' if u['last'] == '': u['last']=' ' try: - pprint(self.moodle.create_user(u['email'],u['username'],'1Random 1String',u['first'],u['last'])[0]) + pprint(self.moodle.create_user(u['email'],u['username'],secrets.token_urlsafe(16),u['first'],u['last'])[0]) except: log.error(' -->> Error creating on moodle the user: '+u['username']) # user_id=user['id'] @@ -677,7 +692,7 @@ class Admin(): log.warning(' NEXTCLOUD USERS: Creating nextcloud user: '+u['username']+' in groups '+str(u['keycloak_groups'])) try: ev.increment({'name':u['username']}) - self.nextcloud.add_user_with_groups(u['username'],'1Random 1String',500000000000,u['keycloak_groups'],u['email'],u['first']+' '+u['last']) + self.nextcloud.add_user_with_groups(u['username'],secrets.token_urlsafe(16),"500 GB",u['keycloak_groups'],u['email'],u['first']+' '+u['last']) except ProviderItemExists: log.warning('User '+u['username']+' already exists. Skipping...') continue @@ -802,8 +817,130 @@ class Admin(): def user_update_password(self,userid,password,temporary): return self.keycloak.update_user_pwd(userid,password,temporary) + def user_update(self,user): + log.warning('Updating user moodle, nextcloud keycloak') + ev=Events('Updating user','Updating user in keycloak') + + + ## Get actual user role + try: + internaluser = [u for u in self.internal['users'] if u['username'] == user['username']][0] + except: + raise UserNotFound + + if set(user['groups']) != set(internaluser['keycloak_groups']): + add_user_groups = list(set(user['groups']) - set(internaluser['keycloak_groups'])) + user_groups_with_system=user['groups']+['/admin','/manager','/teacher','/student'] + remove_user_groups = list(set(internaluser['keycloak_groups']) - set(user_groups_with_system)) + else: + add_user_groups=[] + remove_user_groups=[] + + print('PREVIOUS ADD USER GROUPS') + print(add_user_groups) + print('PREVIOUS REMOVE USER GROUPS') + print(remove_user_groups) + + if user['roles'][0] not in internaluser['roles']: + # Remove internaluser['roles'] + add_user_roles=user['roles'] + if '/'+user['roles'][0] not in add_user_groups: + add_user_groups.append('/'+user['roles'][0]) + remove_user_roles=internaluser['roles'] #Remove the previous role + if '/'+user['roles'][0] not in remove_user_groups: + remove_user_groups.append('/'+internaluser['roles'][0]) + else: + add_user_roles=[] + remove_user_roles=[] + # Add user['role'] + + print('ADD USER GROUPS') + print(add_user_groups) + print('REMOVE USER GROUPS') + print(remove_user_groups) + print('ADD USER ROLES') + print(add_user_roles) + print('REMOVE USER ROLES') + print(remove_user_roles) + + self.update_keycloak_user(internaluser['id'],user,remove_user_roles,remove_user_groups) + ev.update_text('Syncing data from applications...') + self.resync_data() + + ev.update_text('Updating user in moodle') + self.update_moodle_user(internaluser['id'],user,remove_user_groups) + + ev.update_text('Updating user in nextcloud') + self.update_nextcloud_user(internaluser['id'],user,remove_user_groups) + + ev.update_text('User updated') + return True + + def update_keycloak_user(self,user_id,user,remove_user_roles,remove_user_groups): + if len(remove_user_roles): + toremove=[] + # pprint(self.keycloak.get_user_realm_roles(user_id)) + for role in remove_user_roles: + toremove.append(self.keycloak.get_role(role)) + + # Also remove from group identical to role: + group_id = self.keycloak.get_group_by_path('/'+role)['id'] + self.keycloak.group_user_remove(user_id,group_id) + + self.keycloak.remove_user_roles(user_id,toremove) + + self.keycloak.assign_realm_roles(user_id,user['roles'][0]) + # Also add it to the group identical to role + group_id = self.keycloak.get_group_by_path('/'+user['roles'][0])['id'] + self.keycloak.group_user_add(user_id,group_id) + + if len(remove_user_groups): + for group in remove_user_groups: + group_id = self.keycloak.get_group_by_path(group)['id'] + self.keycloak.group_user_remove(user_id,group_id) + + for group in user['groups']: + group_id = self.keycloak.get_group_by_path(group)['id'] + self.keycloak.group_user_add(user_id,group_id) + + + + self.keycloak.user_update(user_id,user['enabled'],user['email'],user['firstname'],user['lastname']) + # So we should add it to the correct role + + return True + + def update_moodle_user(self,user_id,user,remove_user_groups): + internaluser=[u for u in self.internal['users'] if u['id']==user_id][0] + cohorts=self.moodle.get_cohorts() + if len(remove_user_groups): + for group in remove_user_groups: + cohort=[c for c in cohorts if c['name']==group][0] + try: + self.moodle.delete_user_in_cohort(user_id,cohort['id']) + except: + log.error('MOODLE: User '+user['username']+' it is not in cohort '+group) + + for group in user['groups']: + cohort=[c for c in cohorts if c['name']==group][0] + self.moodle.add_user_to_cohort(internaluser['moodle_id'],cohort['id']) + self.moodle.update_user(username=user['username'], email=user['email'], first_name=user['firstname'], last_name=user['lastname'], enabled=user['enabled']) + return True + + def update_nextcloud_user(self,user_id, user, remove_user_groups): + self.nextcloud.update_user(user['username'],{"quota":user['quota'],"email":user['email'],"displayname":user['firstname']+' '+user['lastname']}) + ## TODO: Disable de user? Is really needed? it is disabled in keycloak, so can't login again + ## ocs/v1.php/cloud/users/{userid}/disable + if len(remove_user_groups): + for group in remove_user_groups: + self.nextcloud.remove_user_from_group(user['username'],group) + + for group in user['groups']: + self.nextcloud.add_user_to_group(user['username'],group) + + def delete_user(self,userid): - log.warning('deleting user moodle, nextcloud keycloak') + log.warning('Deleting user moodle, nextcloud keycloak') ev=Events('Deleting user','Deleting from moodle') self.delete_moodle_user(userid) ev.update_text('Deleting from nextcloud') @@ -816,4 +953,125 @@ class Admin(): return True def get_user(self,userid): - return [u for u in self.internal['users'] if u['id']==userid][0] + user = [u for u in self.internal['users'] if u['id']==userid] + if not len(user): return False + return user[0] + + def get_user_username(self,username): + user = [u for u in self.internal['users'] if u['username']==username] + if not len(user): return False + return user[0] + + def add_user(self,u): + ### KEYCLOAK + ev=Events('Add user',u['username'],total=5) + log.warning(' KEYCLOAK USERS: Adding user: '+u['username']) + uid=self.keycloak.add_user(u['username'],u['first'],u['last'],u['email'],u['password'],enabled=u['enabled']) + self.av.add_user_default_avatar(uid,u['role']) + # Add user to role and group rolename + log.warning(' KEYCLOAK USERS: Assign user '+u['username']+' with initial pwd '+ u['password']+' to role '+u['role']) + self.keycloak.assign_realm_roles(uid,u['role']) + gid=self.keycloak.get_group_by_path(path='/'+u['role'])['id'] + self.keycloak.group_user_add(uid,gid) + ev.increment({'name':'Added to system','data':[]}) + # Add user to groups + for g in u['groups']: + parts=g.split('/') + sub='' + if len(parts)==0: + log.warning(' KEYCLOAK USERS: Skip assign user '+u['username']+' to any group as does not have one') + continue # NO GROUP + for i in range(1,len(parts)): + sub=sub+'/'+parts[i] + if sub=='/': continue # User with no path + log.warning(' KEYCLOAK USERS: Assign user '+u['username']+' to group '+ str(sub)) + gid=self.keycloak.get_group_by_path(path=sub)['id'] + self.keycloak.group_user_add(uid,gid) + ev.increment({'name':'Added to system groups','data':[]}) + ### MOODLE + # Add user + log.warning('Creating moodle user: '+u['username']) + try: + self.moodle.create_user(u['email'],u['username'],'*'+secrets.token_urlsafe(16),u['first'],u['last']) + ev.increment({'name':'Added to moodle','data':[]}) + except UserExists: + log.error(' -->> User already exists') + error=Events('User already exists.',str(se),type='error') + except SystemError as se: + log.error('Moodle create user error: '+str(se)) + error=Events('Moodle create user error',str(se),type='error') + except: + log.error(' -->> Error creating on moodle the user: '+u['username']) + print(traceback.format_exc()) + error=Events('Internal error','Check logs',type='error') + + # Add user to cohort + ## Get all existing moodle cohorts + cohorts=self.moodle.get_cohorts() + for g in u['groups']: + try: + cohort=[c for c in cohorts if c['name']==g][0] + except: + # print(traceback.format_exc()) + log.error(' MOODLE USER GROUPS: keycloak group '+g+' does not exist as moodle cohort. This should not happen. User '+u['username']+ ' not added.') + + try: + self.moodle.add_user_to_cohort(u['username'],cohort['id']) + except: + log.error(' MOODLE USER GROUPS: User '+u['username']+' already exists in cohort '+cohort['name']) + + ev.increment({'name':'Added to moodle cohorts','data':[]}) + ### NEXTCLOUD + log.warning(' NEXTCLOUD USERS: Creating nextcloud user: '+u['username']+' in groups '+str(u['groups'])) + try: + # Quota is in MB + self.nextcloud.add_user_with_groups(u['username'],secrets.token_urlsafe(16),u['quota'],u['groups'],u['email'],u['first']+' '+u['last']) + ev.increment({'name':'Added to nextcloud','data':[]}) + except ProviderItemExists: + log.warning('User '+u['username']+' already exists. Skipping...') + except: + log.error(traceback.format_exc()) + + self.resync_data() + + def add_group(self,g): + # TODO: Check if exists + + new_path=self.keycloak.add_group(g['name'],g['parent']) + if g['parent'] != None: + new_path=new_path['path'] + else: + new_path='/'+g['name'] + self.moodle.add_system_cohort(new_path,g['description']) + self.nextcloud.add_group(new_path) + + def delete_group_by_id(self,group_id): + # TODO: Check if exists + group = self.keycloak.get_group_by_id(group_id) + + try: + self.keycloak.delete_group(group['id']) + except: + log.error('KEYCLOAK: Could no delete group '+group['path']) + cohorts=self.moodle.get_cohorts() + + cohort = [c['id'] for c in cohorts if c['name'] == group['path']] + self.moodle.delete_cohorts(cohort) + + self.nextcloud.delete_group(group['path']) + # group = [g for g in self.internal['groups'] if g['id'] == group_id][0] + + def delete_group_by_path(self,path): + group =self.keycloak.get_group_by_path(path) + if group != None: + try: + self.keycloak.delete_group(group['id']) + except: + log.error('KEYCLOAK: Could no delete group '+group['path']) + cohorts=self.moodle.get_cohorts() + cohort = [c['id'] for c in cohorts if c['name'] == path] + if len(cohort): + self.moodle.delete_cohorts(cohort) + + self.nextcloud.delete_group(path) + diff --git a/admin/src/admin/lib/events.py b/admin/src/admin/lib/events.py index 63152b1..c854855 100644 --- a/admin/src/admin/lib/events.py +++ b/admin/src/admin/lib/events.py @@ -14,13 +14,15 @@ from flask_socketio import SocketIO, emit, join_room, leave_room, \ import base64 class Events(): - def __init__(self,title,text='',total=0,table=False): + def __init__(self,title,text='',total=0,table=False,type='info'): + # notice, info, success, and error self.eid=str(base64.b64encode(os.urandom(32))[:8]) self.title=title self.text=text self.total=total self.table=table self.item=0 + self.type=type self.create() def create(self): @@ -28,7 +30,8 @@ class Events(): app.socketio.emit('notify-create', json.dumps({'id':self.eid, 'title':self.title, - 'text':self.text}), + 'text':self.text, + 'type':self.type}), namespace='/sio', room='admin') sleep(0.001) @@ -69,6 +72,7 @@ class Events(): 'item':self.item, 'total':self.total, 'table':self.table, + 'type':self.type, 'data':data}), namespace='/sio', room='admin') @@ -84,6 +88,7 @@ class Events(): 'item':self.item, 'total':self.total, 'table':self.table, + 'type':self.type, 'data':data}), namespace='/sio', room='admin') diff --git a/admin/src/admin/lib/exceptions.py b/admin/src/admin/lib/exceptions.py new file mode 100644 index 0000000..2eec0fc --- /dev/null +++ b/admin/src/admin/lib/exceptions.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# coding=utf-8 +class UserExists(Exception): + pass + +class UserNotFound(Exception): + pass \ No newline at end of file diff --git a/admin/src/admin/lib/helpers.py b/admin/src/admin/lib/helpers.py index 24922e1..60de822 100644 --- a/admin/src/admin/lib/helpers.py +++ b/admin/src/admin/lib/helpers.py @@ -1,3 +1,5 @@ +import random +import string def filter_roles_list(role_list): client_roles=['admin','manager','teacher','student'] @@ -5,4 +7,11 @@ def filter_roles_list(role_list): def filter_roles_listofdicts(role_listofdicts): client_roles=['admin','manager','teacher','student'] - return [r for r in role_listofdicts if r['name'] in client_roles] \ No newline at end of file + return [r for r in role_listofdicts if r['name'] in client_roles] + +def rand_password(lenght): + characters = string.ascii_letters + string.digits + string.punctuation + passwd = ''.join(random.choice(characters) for i in range(lenght)) + while not any(ele.isupper() for ele in passwd): + passwd = ''.join(random.choice(characters) for i in range(lenght)) + return passwd \ No newline at end of file diff --git a/admin/src/admin/lib/keycloak_client.py b/admin/src/admin/lib/keycloak_client.py index 65bc058..d60f677 100644 --- a/admin/src/admin/lib/keycloak_client.py +++ b/admin/src/admin/lib/keycloak_client.py @@ -138,14 +138,14 @@ class KeycloakClient(): # user['roles']= [r['name'] for r in self.keycloak_admin.get_realm_roles_of_user(user_id=user['id'])] # return users - def add_user(self,username,first,last,email,password,group=False,temporary=True): + def add_user(self,username,first,last,email,password,group=False,temporary=True,enabled=True): # RETURNS string with keycloak user id (the main id in this app) self.connect() username=username.lower() try: uid=self.keycloak_admin.create_user({"email": email, "username": username, - "enabled": True, + "enabled": enabled, "firstName": first, "lastName": last, "credentials":[{"type":"password", @@ -172,17 +172,30 @@ class KeycloakClient(): self.connect() return self.keycloak_admin.update_user( user_id, payload) - def remove_user_group(self,user_id,group_id): + def user_update(self,user_id,enabled,email,first,last,groups=[],roles=[]): + ## NOTE: Roles didn't seem to be updated/added. Also not confident with groups + # Updates + payload={"enabled":enabled, + "email":email, + "firstName":first, + "lastName":last, + "groups":groups, + "realmRoles":roles} self.connect() - pass + return self.keycloak_admin.update_user( user_id, payload) + + def group_user_remove(self,user_id,group_id): + self.connect() + return self.keycloak_admin.group_user_remove(user_id,group_id) # def add_user_role(self,user_id,role_id): # self.connect() # return self.keycloak_admin.assign_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test") - def remove_user_role(self,user_id,role_id): + def remove_user_roles(self,user_id,roles): self.connect() - pass + print(roles) + return self.keycloak_admin.delete_realm_roles_of_user(user_id,roles) def delete_user(self,userid): self.connect() @@ -192,6 +205,10 @@ class KeycloakClient(): self.connect() return self.keycloak_admin.get_user_groups(user_id=userid) + def get_user_realm_roles(self,userid): + self.connect() + return self.keycloak_admin.get_realm_roles_of_user(user_id=userid) + def add_user_client_role(self,client_id,user_id,role_id,role_name): self.connect() return self.keycloak_admin.assign_client_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test") @@ -225,7 +242,8 @@ class KeycloakClient(): def add_group(self,name,parent=None,skip_exists=False): self.connect() - if parent is not None: parent=self.get_group_by_path(parent)['id'] + if parent != None: + parent=self.get_group_by_path(parent)['id'] return self.keycloak_admin.create_group({"name":name}, parent=parent) def delete_group(self,group_id): @@ -313,7 +331,7 @@ class KeycloakClient(): def get_role(self,name): self.connect() - return self.keycloak_admin.get_realm_role(name=name) + return self.keycloak_admin.get_realm_role(name) def add_role(self,name,description=''): self.connect() @@ -356,7 +374,8 @@ class KeycloakClient(): role=[r for r in self.keycloak_admin.get_realm_roles() if r['name']==role] except: return False - return self.keycloak_admin.assign_realm_roles(user_id=user_id, client_id=None, roles=role) + return self.keycloak_admin.assign_realm_roles(user_id=user_id, roles=role) + # return self.keycloak_admin.assign_realm_roles(user_id=user_id, client_id=None, roles=role) ## CLIENTS def delete_client(self,clientid): diff --git a/admin/src/admin/lib/moodle.py b/admin/src/admin/lib/moodle.py index 54da5eb..d2e7dcf 100644 --- a/admin/src/admin/lib/moodle.py +++ b/admin/src/admin/lib/moodle.py @@ -3,8 +3,10 @@ from admin import app import logging as log from pprint import pprint +import traceback from .postgres import Postgres +from .exceptions import UserExists, UserNotFound # Module variables to connect to moodle api @@ -63,14 +65,33 @@ class Moodle(): response = post(self.url+self.endpoint, parameters, verify=self.verify) response = response.json() if type(response) == dict and response.get('exception'): - raise SystemError("Error calling Moodle API\n", response) + raise SystemError(response) return response def create_user(self, email, username, password, first_name='-', last_name='-'): - data = [{'username': username, 'email':email, - 'password': password, 'firstname':first_name, 'lastname':last_name}] - user = self.call('core_user_create_users', users=data) - return user + if len(self.get_user_by('username',username)['users']): + raise UserExists + try: + data = [{'username': username, 'email':email, + 'password': password, 'firstname':first_name, 'lastname':last_name}] + user = self.call('core_user_create_users', users=data) + return user + except SystemError as se: + raise SystemError(se.args[0]['message']) + + def update_user(self, username, email, first_name, last_name, enabled=True): + user = self.get_user_by('username',username)['users'][0] + print(user) + if not len(user): + raise UserNotFound + try: + data = [{'id':user['id'],'username': username, 'email':email, + 'firstname':first_name, 'lastname':last_name, + 'suspended':0 if not enabled else 1}] + user = self.call('core_user_update_users', users=data) + return user + except SystemError as se: + raise SystemError(se.args[0]['message']) def delete_user(self, user_id): user = self.call('core_user_delete_users', userids=[user_id]) @@ -82,7 +103,10 @@ class Moodle(): def get_user_by(self, key, value): criteria = [{'key': key, 'value': value}] - user = self.call('core_user_get_users', criteria=criteria) + try: + user = self.call('core_user_get_users', criteria=criteria) + except: + raise SystemError("Error calling Moodle API\n", traceback.format_exc()) return user def get_users_with_groups_and_roles(self): @@ -142,6 +166,10 @@ class Moodle(): user = self.call('core_cohort_add_cohort_members', members=members) return user + def delete_user_in_cohort(self,userid,cohortid): + user = self.call('core_cohort_delete_cohort_members', cohortid=cohortid, userid=userid) + return user + def get_cohort_members(self, cohort_ids): members = self.call('core_cohort_get_cohort_members', cohortids=cohort_ids) #[0]['userids'] diff --git a/admin/src/admin/lib/nextcloud.py b/admin/src/admin/lib/nextcloud.py index 65677d7..05a785a 100644 --- a/admin/src/admin/lib/nextcloud.py +++ b/admin/src/admin/lib/nextcloud.py @@ -4,6 +4,7 @@ #from ..lib.log import * from admin import app import time,requests,json,pprint,os +import urllib import traceback import logging as log from .nextcloud_exc import * @@ -32,6 +33,7 @@ class Nextcloud(): response = requests.request(method, url, data=data, auth=auth, verify=self.verify_cert, headers=headers) if 'meta' in response.text: if '997' in response.text: raise ProviderUnauthorized + # if '998' in response.text: raise ProviderInvalidQuery return response.text ## At least the ProviderSslError is not being catched or not raised correctly @@ -102,7 +104,18 @@ class Nextcloud(): # users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists] # list_dict_users = [dict(zip(fields, r)) for r in users_with_lists] def get_users_list(self): - q = """select u.uid as username, adn.value as displayname, ade.value as email, json_agg(gg.displayname) as admin_groups,json_agg(g.displayname) as groups + # q = """select u.uid as username, adn.value as displayname, ade.value as email, json_agg(gg.displayname) as admin_groups,json_agg(g.displayname) as groups + # from oc_users as u + # left join oc_group_user as gu on gu.uid = u.uid + # left join oc_groups as g on gu.gid = g.gid + # left join oc_group_admin as ga on ga.uid = u.uid + # left join oc_groups as gg on gg.gid = ga.gid + # left join oc_accounts_data as adn on adn.uid = u.uid and adn.name = 'displayname' + # left join oc_accounts_data as ade on ade.uid = u.uid and ade.name = 'email' + # group by u.uid, adn.value, ade.value""" + + # With quotas + q = """select u.uid as username, configvalue as quota, sum(size) as total_bytes, adn.value as displayname, ade.value as email, json_agg(gg.displayname) as admin_groups,json_agg(g.displayname) as groups from oc_users as u left join oc_group_user as gu on gu.uid = u.uid left join oc_groups as g on gu.gid = g.gid @@ -110,7 +123,10 @@ class Nextcloud(): left join oc_groups as gg on gg.gid = ga.gid left join oc_accounts_data as adn on adn.uid = u.uid and adn.name = 'displayname' left join oc_accounts_data as ade on ade.uid = u.uid and ade.name = 'email' - group by u.uid, adn.value, ade.value""" + left join oc_preferences as pref on u.uid=pref.userid and appid='files' and configkey='quota' + left join oc_storages as s on s.id=CONCAT('home::',u.uid) + left join oc_filecache as fc on fc.storage = numeric_id + group by u.uid, adn.value, ade.value, pref.configvalue""" (headers,users)=self.nextcloud_pg.select_with_headers(q) users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users] users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists] @@ -163,6 +179,68 @@ class Nextcloud(): # 106 - no group specified (required for subadmins) # 107 - all errors that contain a hint - for example “Password is among the 1,000,000 most common ones. Please make it unique.” (this code was added in 12.0.6 & 13.0.1) + def update_user(self, userid, key_values): + # key_values={'quota':quota,'email':email,'displayname':displayname} + + url = self.apiurl + "users/" + userid + "?format=json" + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'OCS-APIRequest': 'true', + } + for k,v in key_values.items(): + data={"key":k,"value":v} + + try: + result = json.loads(self._request('PUT',url,data=data,headers=headers)) + if result['ocs']['meta']['statuscode'] == 100: return True + if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists + if result['ocs']['meta']['statuscode'] == 104: raise ProviderGroupNotExists + log.error('Get Nextcloud provider user add error: '+str(result)) + raise ProviderOpError + except: + log.error(traceback.format_exc()) + raise + + def add_user_to_group(self, userid, group_id): + data={'groupid':group_id} + + url = self.apiurl + "users/" + userid + "/groups?format=json" + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'OCS-APIRequest': 'true', + } + try: + result = json.loads(self._request('POST',url,data=data,headers=headers)) + if result['ocs']['meta']['statuscode'] == 100: return True + if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists + if result['ocs']['meta']['statuscode'] == 104: raise ProviderGroupNotExists + log.error('Get Nextcloud provider user add error: '+str(result)) + raise ProviderOpError + except: + log.error(traceback.format_exc()) + raise + + def remove_user_from_group(self, userid, group_id): + data={'groupid':group_id} + + url = self.apiurl + "users/" + userid + "/groups?format=json" + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'OCS-APIRequest': 'true', + } + try: + result = json.loads(self._request('DELETE',url,data=data,headers=headers)) + if result['ocs']['meta']['statuscode'] == 100: return True + if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists + if result['ocs']['meta']['statuscode'] == 104: + self.add_group(group) + # raise ProviderGroupNotExists + log.error('Get Nextcloud provider user add error: '+str(result)) + raise ProviderOpError + except: + log.error(traceback.format_exc()) + raise + def add_user_with_groups(self,userid,userpassword,quota=False,groups=[],email='',displayname=''): data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':groups,'email':email,'displayname':displayname} # if not group: del data['groups[]'] @@ -325,7 +403,8 @@ class Nextcloud(): # 103 - failed to add the group def delete_group(self,groupid): - url = self.apiurl + "groups/"+groupid+"?format=json" + group = urllib.parse.quote(groupid, safe='') + url = self.apiurl + "groups/"+group+"?format=json" headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'OCS-APIRequest': 'true', diff --git a/admin/src/admin/static/js/groups.js b/admin/src/admin/static/js/groups.js index 23e27af..f09a4a4 100644 --- a/admin/src/admin/static/js/groups.js +++ b/admin/src/admin/static/js/groups.js @@ -5,6 +5,33 @@ $(document).on('shown.bs.modal', '#modalAddDesktop', function () { $(document).ready(function() { + $.ajax({ + type: "GET", + "url": "/api/groups", + success: function(data) + { + $(".groups-select").append( + '' + ) + data.forEach(element => { + var groupOrigins = []; + ['keycloak'].forEach(o => { + if (element[o]) { + groupOrigins.push(o) + } + }) + $(".groups-select").append( + '' + ) + }); + $('.groups-select').select2(); + }, + error: function(data) + { + alert('Something went wrong on our side...') + } + }); + $('.btn-global-resync').on('click', function () { $.ajax({ type: "GET", @@ -37,19 +64,44 @@ $(document).ready(function() { formdata = form.serializeObject() console.log('NEW GROUP') console.log(formdata) - // $.ajax({ - // type: "POST", - // "url": "/groups_list", - // success: function(data) - // { - // console.log('SUCCESS') - // // $("#modalAddGroup").modal('hide'); - // }, - // error: function(data) - // { - // alert('Something went wrong on our side...') - // } - // }); + + $.ajax({ + type: "POST", + "url": "/api/group", + data: JSON.stringify(formdata), + complete: function(jqXHR, textStatus) { + switch (jqXHR.status) { + case 200: + $("#modalAddGroup").modal('hide'); + break; + case 409: + new PNotify({ + title: "Add group error", + text: $.parseJSON(jqXHR.responseText)['msg'], + hide: true, + delay: 3000, + icon: 'fa fa-alert-sign', + opacity: 1, + type: 'error' + }); + break; + case 412: + new PNotify({ + title: "Add group error", + text: $.parseJSON(jqXHR.responseText)['msg'], + hide: true, + delay: 3000, + icon: 'fa fa-alert-sign', + opacity: 1, + type: 'error' + }); + break; + default: + alert("Server error."); + } + } + }); + }); $('.btn-delete_keycloak').on('click', function () { @@ -84,16 +136,87 @@ $(document).ready(function() { "deferRender": true, "columns": [ { - "className": 'details-control', - "orderable": false, - "data": null, - "width": "10px", - "defaultContent": '' + "className": 'actions-control', + "orderable": false, + "data": null, + "width": "80px", + "defaultContent": '' + // \ + // ' }, { "data": "name", "width": "10px" }, { "data": "path", "width": "10px" }, ], "order": [[2, 'asc']], // "columnDefs": [ ] -} ); + } ); + + $('#groups').find(' tbody').on( 'click', 'button', function () { + var data = table.row( $(this).parents('tr') ).data(); + // var closest=$(this).closest("div").parent(); + // var pk=closest.attr("data-pk"); + // console.log(pk) + switch($(this).attr('id')){ + case 'btn-edit': + $("#modalEditGroupForm")[0].reset(); + $('#modalEditGroup').modal({ + backdrop: 'static', + keyboard: false + }).modal('show'); + // $('#modalEditGroup #user-avatar').attr("src","/avatar/"+data.id) + // setUserDefault('#modalEditGroup', data.id); + $('#modalEdit').parsley(); + break; + case 'btn-delete': + new PNotify({ + title: 'Confirmation Needed', + text: "Are you sure you want to delete group: "+data['name']+"?", + hide: false, + opacity: 0.9, + confirm: { + confirm: true + }, + buttons: { + closer: false, + sticker: false + }, + history: { + history: false + }, + addclass: 'pnotify-center' + }).get().on('pnotify.confirm', function() { + console.log(data) + if(data.id == false){ + $.ajax({ + type: "DELETE", + url:"/api/group", + data: JSON.stringify(data), + success: function(data) + { + table.ajax.reload(); + }, + error: function(data) + { + alert('Something went wrong on our side...') + } + }); + }else{ + $.ajax({ + type: "DELETE", + url:"/api/group/"+data.id, + success: function(data) + { + table.ajax.reload(); + }, + error: function(data) + { + alert('Something went wrong on our side...') + } + }); + } + }).on('pnotify.cancel', function() { + }); + break; + } + }); }) \ No newline at end of file diff --git a/admin/src/admin/static/js/roles.js b/admin/src/admin/static/js/roles.js index 0b18c67..ac135b2 100644 --- a/admin/src/admin/static/js/roles.js +++ b/admin/src/admin/static/js/roles.js @@ -45,16 +45,15 @@ $(document).ready(function() { "rowId": "id", "deferRender": true, "columns": [ - { - "className": 'details-control', - "orderable": false, - "data": null, - "width": "10px", - "defaultContent": '' - }, { "data": "id", "width": "10px" }, { "data": "name", "width": "10px" }, ], - "order": [[1, 'asc']], + "order": [[1, 'asc']], + // "columnDefs": [ { + // "targets": 0, + // "render": function ( data, type, full, meta ) { + // // return '' + // return '' + // }}] } ); }); \ No newline at end of file diff --git a/admin/src/admin/static/js/status_socket.js b/admin/src/admin/static/js/status_socket.js index 47c7519..5241a07 100644 --- a/admin/src/admin/static/js/status_socket.js +++ b/admin/src/admin/static/js/status_socket.js @@ -21,7 +21,8 @@ socket.on('notify-create', function(data) { notice[data.id] = new PNotify({ title: data.title, text: data.text, - hide: false + hide: false, + type: data.type }); }); @@ -36,7 +37,8 @@ socket.on('notify-increment', function(data) { notice[data.id] = new PNotify({ title: data.title, text: data.text, - hide: false + hide: false, + type: data.type }); } // console.log(data.text) diff --git a/admin/src/admin/static/js/users.js b/admin/src/admin/static/js/users.js index a4ee499..400fc5f 100644 --- a/admin/src/admin/static/js/users.js +++ b/admin/src/admin/static/js/users.js @@ -87,33 +87,168 @@ $(document).ready(function() { // Open new user modal $('.btn-new-user').on('click', function () { + $("#modalAddUserForm")[0].reset(); + $.ajax({ + type: "GET", + "url": "/api/user_password", + success: function(data) + { + $('#modalAddUser #password').val(data) + }, + error: function(data) + { + alert('Something went wrong on our side...') + } + }); + $('#modalAddUser #enabled').prop('checked',true).iCheck('update'); $('#modalAddUser').modal({ backdrop: 'static', keyboard: false }).modal('show'); }); + //has uppercase + window.Parsley.addValidator('uppercase', { + requirementType: 'number', + validateString: function(value, requirement) { + var uppercases = value.match(/[A-Z]/g) || []; + return uppercases.length >= requirement; + }, + messages: { + en: 'Your password must contain at least (%s) uppercase letter.' + } + }); + + //has lowercase + window.Parsley.addValidator('lowercase', { + requirementType: 'number', + validateString: function(value, requirement) { + var lowecases = value.match(/[a-z]/g) || []; + return lowecases.length >= requirement; + }, + messages: { + en: 'Your password must contain at least (%s) lowercase letter.' + } + }); // Send new user form $('#modalAddUser #send').on('click', function () { var form = $('#modalAddUserForm'); - formdata = form.serializeObject() - console.log('NEW USER') - console.log(formdata) - // $.ajax({ - // type: "POST", - // "url": "/groups_list", - // success: function(data) - // { - // console.log('SUCCESS') - // // $("#modalAddUser").modal('hide'); - // }, - // error: function(data) - // { - // alert('Something went wrong on our side...') - // } - // }); + form.parsley().validate(); + if (form.parsley().isValid()){ + formdata = form.serializeObject() + // console.log('NEW USER') + // console.log(formdata) + + $.ajax({ + type: "POST", + "url": "/api/user", + data: JSON.stringify(formdata), + complete: function(jqXHR, textStatus) { + switch (jqXHR.status) { + case 200: + $("#modalAddUser").modal('hide'); + break; + case 409: + new PNotify({ + title: "Add user error", + text: $.parseJSON(jqXHR.responseText)['msg'], + hide: true, + delay: 3000, + icon: 'fa fa-alert-sign', + opacity: 1, + type: 'error' + }); + break; + case 412: + new PNotify({ + title: "Add user error", + text: $.parseJSON(jqXHR.responseText)['msg'], + hide: true, + delay: 3000, + icon: 'fa fa-alert-sign', + opacity: 1, + type: 'error' + }); + break; + default: + alert("Server error."); + } + } + }); + } + table.ajax.reload(); }); + // $("#modalEditUser #send").on('click', function(e){ + // var form = $('#modalEditUserForm'); + // form.parsley().validate(); + // if (form.parsley().isValid()){ + // data=$('#modalEditUserForm').serializeObject(); + // data['id']=$('#modalEditUserForm #id').val(); + // console.log('Editing user...') + // console.log(data) + // } + // }); + + $('#modalEditUser #send').on('click', function () { + var form = $('#modalEditUserForm'); + form.parsley().validate(); + if (form.parsley().isValid()){ + formdata = form.serializeObject() + formdata['id']=$('#modalEditUserForm #id').val(); + formdata['username']=$('#modalEditUserForm #username').val(); + console.log('UPDATE USER') + console.log(formdata) + $.ajax({ + type: "PUT", + "url": "/api/user/"+formdata['id'], + data: JSON.stringify(formdata), + complete: function(jqXHR, textStatus) { + switch (jqXHR.status) { + case 200: + $("#modalEditUser").modal('hide'); + break; + case 404: + new PNotify({ + title: "Update user error", + text: $.parseJSON(jqXHR.responseText)['msg'], + hide: true, + delay: 3000, + icon: 'fa fa-alert-sign', + opacity: 1, + type: 'error' + }); + break; + case 409: + new PNotify({ + title: "Add user error", + text: $.parseJSON(jqXHR.responseText)['msg'], + hide: true, + delay: 3000, + icon: 'fa fa-alert-sign', + opacity: 1, + type: 'error' + }); + break; + case 412: + new PNotify({ + title: "Add user error", + text: $.parseJSON(jqXHR.responseText)['msg'], + hide: true, + delay: 3000, + icon: 'fa fa-alert-sign', + opacity: 1, + type: 'error' + }); + break; + default: + alert("Server error."); + } + } + }); + } + table.ajax.reload(); + }); //DataTable Main renderer var table = $('#users').DataTable({ "ajax": { @@ -127,17 +262,16 @@ $(document).ready(function() { "rowId": "id", "deferRender": true, "columns": [ - {"data": null, "defaultContent":'',"width": "1px"}, // { // "className": 'details-control', // "orderable": false, // "data": null, // "width": "10px", // "defaultContent": '' - // }, + // }, + { "data": "enabled", "width": "1px" }, { "data": "id", "width": "10px" }, - { "data": "enabled", "width": "10px" }, - { "data": "username", "width": "10px"}, + { "data": "roles", "width": "10px" }, { "className": 'actions-control', "orderable": false, @@ -146,14 +280,15 @@ $(document).ready(function() { "defaultContent": ' \ \ ' - }, - { "data": "first", "width": "10px"}, - { "data": "last", "width": "150px"}, + }, + { "data": "username", "width": "10px"}, + { "data": "first", "width": "10px"}, + { "data": "last", "width": "150px"}, { "data": "email", "width": "10px"}, { "data": "keycloak_groups", "width": "50px" }, - { "data": "roles", "width": "10px" }, + { "data": "quota", "width": "10px", "default": "-"}, ], - "order": [[6, 'asc']], + "order": [[6, 'asc']], "columnDefs": [ { "targets": 1, "render": function ( data, type, full, meta ) { @@ -161,7 +296,7 @@ $(document).ready(function() { return '' }}, { - "targets": 2, + "targets": 0, "render": function ( data, type, full, meta ) { if(full.enabled){ return '' @@ -170,7 +305,12 @@ $(document).ready(function() { }; }}, { - "targets": 3, + "targets": 2, + "render": function ( data, type, full, meta ) { + return full.roles[0][0].toUpperCase() + full.roles[0].slice(1); + }}, + { + "targets": 4, "render": function ( data, type, full, meta ) { return ''+full.username+'' }}, @@ -183,31 +323,40 @@ $(document).ready(function() { }) return grups }}, + { + "targets": 9, + "render": function ( data, type, full, meta ) { + if(full.quota == false){ + return 'Unlimited' + }else{ + return full.quota + } + }}, ] } ); - $template = $(".template-detail-users"); + // $template = $(".template-detail-users"); - $('#users').find('tbody').on('click', 'td.details-control', function () { - var tr = $(this).closest('tr'); - var row = table.row( tr ); + // $('#users').find('tbody').on('click', 'td.details-control', function () { + // var tr = $(this).closest('tr'); + // var row = table.row( tr ); - if ( row.child.isShown() ) { - // This row is already open - close it - row.child.hide(); - tr.removeClass('shown'); - } - else { - // Close other rows - if ( table.row( '.shown' ).length ) { - $('.details-control', table.row( '.shown' ).node()).click(); - } - // Open this row - row.child( addUserDetailPannel(row.data()) ).show(); - tr.addClass('shown'); - actionsUserDetail() - } - } ); + // if ( row.child.isShown() ) { + // // This row is already open - close it + // row.child.hide(); + // tr.removeClass('shown'); + // } + // else { + // // Close other rows + // if ( table.row( '.shown' ).length ) { + // $('.details-control', table.row( '.shown' ).node()).click(); + // } + // // Open this row + // row.child( addUserDetailPannel(row.data()) ).show(); + // tr.addClass('shown'); + // actionsUserDetail() + // } + // } ); $('#users').find(' tbody').on( 'click', 'button', function () { var data = table.row( $(this).parents('tr') ).data(); @@ -291,7 +440,7 @@ $(document).ready(function() { id=$('#modalPasswdUserForm #id').val(); $.ajax({ type: "PUT", - url:"/api/user//+" + id, + url:"/api/user_password/" + id, data: JSON.stringify(formdata), success: function(data) { @@ -320,26 +469,15 @@ $(document).ready(function() { } }); - $("#modalEditUser #send").on('click', function(e){ - var form = $('#modalEditUserForm'); - form.parsley().validate(); - if (form.parsley().isValid()){ - data=$('#modalEditUserForm').serializeObject(); - data['id']=$('#modalEditUserForm #id').val(); - console.log('Editing user...') - console.log(data) - } - }); + // function addUserDetailPannel ( d ) { + // $newPanel = $template.clone(); + // $newPanel.html(function(i, oldHtml){ + // return oldHtml.replace(/d.id/g, d.id).replace(/d.username/g, d.username); + // }); + // return $newPanel + // } - function addUserDetailPannel ( d ) { - $newPanel = $template.clone(); - $newPanel.html(function(i, oldHtml){ - return oldHtml.replace(/d.id/g, d.id).replace(/d.username/g, d.username); - }); - return $newPanel - } - - function actionsUserDetail(){ + // function actionsUserDetail(){ // $('.btn-passwd').on('click', function () { // var closest=$(this).closest("div").parent(); @@ -392,13 +530,15 @@ $(document).ready(function() { // }).on('pnotify.cancel', function() { // }); // }); - } + // } + function setUserDefault(div_id, user_id) { $.ajax({ type: "GET", url:"/api/user/" + user_id, success: function(data) { + console.log(data) if (data.enabled) { $(div_id + ' #enabled').iCheck('check') } @@ -407,11 +547,18 @@ $(document).ready(function() { $(div_id + ' #email').val(data.email); $(div_id + ' #firstname').val(data.first); $(div_id + ' #lastname').val(data.last); + if(data.quota == false){ + $(div_id + ' #quota').val('false') + }else{ + $(div_id + ' #quota').val(data.quota); + } $(div_id + ' .groups-select').val(data.keycloak_groups); + // $(div_id + ' .role-moodle-select').val(data.keycloak_roles); // $(div_id + ' .role-nextcloud-select').val(data.roles); $(div_id + ' .role-keycloak-select').val(data.roles[0]); $('.groups-select').trigger('change'); + // $('.groups-select, .role-moodle-select, .role-nextcloud-select, .role-keycloak-select').trigger('change'); } }); diff --git a/admin/src/admin/static/templates/pages/groups.html b/admin/src/admin/static/templates/pages/groups.html index 9f3a03f..73446e6 100644 --- a/admin/src/admin/static/templates/pages/groups.html +++ b/admin/src/admin/static/templates/pages/groups.html @@ -15,7 +15,7 @@

Groups

diff --git a/admin/src/admin/static/templates/pages/modals/groups_modals.html b/admin/src/admin/static/templates/pages/modals/groups_modals.html index f5a0c6d..694d233 100644 --- a/admin/src/admin/static/templates/pages/modals/groups_modals.html +++ b/admin/src/admin/static/templates/pages/modals/groups_modals.html @@ -36,6 +36,22 @@ +
+
+

Parent group

+
+
+
+
+
+ + +
+
+
+
+ +
+
+

Groups

+
+
+
-
@@ -74,7 +104,7 @@
-

Roles

+

Role

@@ -92,9 +122,9 @@
-->
-
@@ -114,6 +144,7 @@
+ + + + +
+
+

Groups

+
+
+
- +
diff --git a/admin/src/admin/static/templates/pages/roles.html b/admin/src/admin/static/templates/pages/roles.html index 877930d..6068fc5 100644 --- a/admin/src/admin/static/templates/pages/roles.html +++ b/admin/src/admin/static/templates/pages/roles.html @@ -25,7 +25,6 @@ - diff --git a/admin/src/admin/static/templates/pages/users.html b/admin/src/admin/static/templates/pages/users.html index 286d826..0278eab 100644 --- a/admin/src/admin/static/templates/pages/users.html +++ b/admin/src/admin/static/templates/pages/users.html @@ -24,17 +24,16 @@
Id Name
- - - + + + - - + diff --git a/admin/src/admin/views/ApiViews.py b/admin/src/admin/views/ApiViews.py index 9c3287c..c9869cc 100644 --- a/admin/src/admin/views/ApiViews.py +++ b/admin/src/admin/views/ApiViews.py @@ -9,7 +9,9 @@ import time,json import sys,os from flask import render_template, Response, request, redirect, url_for, jsonify import concurrent.futures -from flask_login import login_required +from flask_login import current_user, login_required +from .decorators import is_admin + # import Queue import threading threads={} @@ -17,6 +19,8 @@ threads={} from keycloak.exceptions import KeycloakGetError +from ..lib.exceptions import UserExists, UserNotFound + @app.route('/api/resync') @login_required def resync(): @@ -27,6 +31,7 @@ def resync(): @login_required def users(provider=False): if request.method == 'DELETE': + if current_user.role != 'admin': return json.dumps({}), 301, {'Content-Type': 'application/json'} if provider == 'keycloak': return json.dumps(app.admin.delete_keycloak_users()), 200, {'Content-Type': 'application/json'} if provider == 'nextcloud': @@ -34,11 +39,18 @@ def users(provider=False): if provider == 'moodle': return json.dumps(app.admin.delete_moodle_users()), 200, {'Content-Type': 'application/json'} if request.method == 'POST': + if current_user.role != 'admin': return json.dumps({}), 301, {'Content-Type': 'application/json'} if provider == 'moodle': return json.dumps(app.admin.sync_to_moodle()), 200, {'Content-Type': 'application/json'} if provider == 'nextcloud': return json.dumps(app.admin.sync_to_nextcloud()), 200, {'Content-Type': 'application/json'} - return json.dumps(app.admin.get_mix_users()), 200, {'Content-Type': 'application/json'} + + users = app.admin.get_mix_users() + + if current_user.role != 'admin': + for user in users: + user['keycloak_groups'] = [g for g in user['keycloak_groups'] if g not in ['/admin','/manager','/teacher','/student']] + return json.dumps(users), 200, {'Content-Type': 'application/json'} # Update pwd @@ -56,35 +68,84 @@ def user_password(userid=False): res = app.admin.user_update_password(userid,password,temporary) return json.dumps({}), 200, {'Content-Type': 'application/json'} except KeycloakGetError as e: - print(e.error_message.decode("utf-8")) - return e.error_message, e.response_code, {'Content-Type': 'application/json'} + log.error(e.error_message.decode("utf-8")) + return json.dumps({'msg':'Update password error.'}), 500, {'Content-Type': 'application/json'} - return json.dumps({}), 301, {'Content-Type': 'application/json'} + return json.dumps({}), 405, {'Content-Type': 'application/json'} # User -@app.route('/api/user/', methods=['POST', 'PUT', 'GET', 'DELETE']) +@app.route('/api/user', methods=['POST']) +@app.route('/api/user/', methods=['PUT', 'GET', 'DELETE']) @login_required def user(userid=None): if request.method == 'DELETE': - res = app.admin.delete_user(userid) + app.admin.delete_user(userid) return json.dumps({}), 200, {'Content-Type': 'application/json'} - # return json.dumps(), 301, {'Content-Type': 'application/json'} if request.method == 'POST': - pass + 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': - pass + 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': - res = app.admin.get_user(userid) - return json.dumps(res), 200, {'Content-Type': 'application/json'} + user = app.admin.get_user(userid) + if not user: return json.dumps({'msg':'User not found.'}), 404, {'Content-Type': 'application/json'} + return json.dumps(user), 200, {'Content-Type': 'application/json'} @app.route('/api/roles') @login_required def roles(): sorted_roles = sorted(app.admin.get_roles(), key=lambda k: k['name']) + if current_user.role != "admin": + sorted_roles = [sr for sr in sorted_roles if sr['name'] != 'admin'] return json.dumps(sorted_roles), 200, {'Content-Type': 'application/json'} +@app.route('/api/group', methods=['POST','DELETE']) +@app.route('/api/group/', methods=['PUT', 'GET', 'DELETE']) +@login_required +def group(group_id=False): + if request.method == 'POST': + data=request.get_json(force=True) + data['parent']=data['parent'] if data['parent'] != '' else None + return json.dumps(app.admin.add_group(data)), 200, {'Content-Type': 'application/json'} + if request.method == 'DELETE': + try: + data=request.get_json(force=True) + print(data) + except: + data=False + + if data: + res = app.admin.delete_group_by_path(data['path']) + else: + res = app.admin.delete_group_by_id(group_id) + return json.dumps(res), 200, {'Content-Type': 'application/json'} @app.route('/api/groups') @app.route('/api/groups/', methods=['POST', 'PUT', 'GET', 'DELETE']) @@ -92,6 +153,11 @@ def roles(): def groups(provider=False): if request.method == 'GET': sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k['name']) + if current_user.role != "admin": + ## internal groups should be avoided as are assigned with the role + sorted_groups = [sg for sg in sorted_groups if sg['path'] not in ['/admin','/manager','/teacher','/student'] and sg['path'].startswith('/')] + else: + sorted_groups = [sg for sg in sorted_groups if sg['path'].startswith('/')] return json.dumps(sorted_groups), 200, {'Content-Type': 'application/json'} if request.method == 'DELETE': if provider == 'keycloak': diff --git a/admin/src/admin/views/WebViews.py b/admin/src/admin/views/WebViews.py index 26565e7..fca1c94 100644 --- a/admin/src/admin/views/WebViews.py +++ b/admin/src/admin/views/WebViews.py @@ -10,6 +10,7 @@ import sys,os from flask import render_template, Response, request, redirect, url_for, jsonify, send_file import concurrent.futures from flask_login import login_required +from .decorators import is_admin from pprint import pprint from ..lib.avatars import Avatars @@ -70,17 +71,20 @@ def avatar(userid): @app.route('/sysadmin/users') @login_required +@is_admin def web_sysadmin_users(): return render_template('pages/sysadmin/users.html', title="SysAdmin Users", nav="SysAdminUsers") @app.route('/sysadmin/groups') @login_required +@is_admin def web_sysadmin_groups(): return render_template('pages/sysadmin/groups.html', title="SysAdmin Groups", nav="SysAdminGroups") @app.route('/sysadmin/external') @login_required +@is_admin ## SysAdmin role def web_sysadmin_external(): return render_template('pages/sysadmin/external.html', title="External", nav="External") diff --git a/admin/src/admin/views/decorators.py b/admin/src/admin/views/decorators.py index 3b6c867..6264a84 100644 --- a/admin/src/admin/views/decorators.py +++ b/admin/src/admin/views/decorators.py @@ -3,9 +3,16 @@ from functools import wraps from flask import request, redirect, url_for -from flask_login import logout_user +from flask_login import current_user, logout_user import socket +def is_admin(fn): + @wraps(fn) + def decorated_view(*args, **kwargs): + if current_user.role == 'admin': return fn(*args, **kwargs) + return redirect(url_for('login')) + return decorated_view + def is_internal(fn): @wraps(fn) def decorated_view(*args, **kwargs): diff --git a/docker-compose-parts/admin.devel.yml b/docker-compose-parts/admin.devel.yml new file mode 100644 index 0000000..2bd8527 --- /dev/null +++ b/docker-compose-parts/admin.devel.yml @@ -0,0 +1,7 @@ +--- +version: '3.7' +services: + isard-sso-admin: + volumes: + - ${BUILD_ROOT_PATH}/admin/src:/admin:rw + command: /bin/sleep infinity \ No newline at end of file diff --git a/docker-compose-parts/admin.yml b/docker-compose-parts/admin.yml index faa6038..1a2f412 100644 --- a/docker-compose-parts/admin.yml +++ b/docker-compose-parts/admin.yml @@ -6,14 +6,14 @@ services: context: ${BUILD_ROOT_PATH} dockerfile: admin/docker/Dockerfile target: production - args: ## DEVELOPMENT - SSH_ROOT_PWD: ${IPA_ADMIN_PWD} - SSH_PORT: 2022 + # args: ## DEVELOPMENT + # SSH_ROOT_PWD: ${IPA_ADMIN_PWD} + # SSH_PORT: 2022 networks: - isard_net - ports: - - "2022:22" - - "9000:9000" + # ports: + # - "2022:22" + # - "9000:9000" restart: unless-stopped volumes: - /etc/localtime:/etc/localtime:ro @@ -26,4 +26,3 @@ services: - .env environment: - VERIFY="false" # In development do not verify certificates - command: sleep infinity
Avatar EnabledUsernameAvatarRole ActionsUsername First Last Email GroupsRolesQuota