From 8731d7d98ff37fe6b360a6c4f455fcadcaacbc41 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 22 Aug 2021 22:24:19 +0200 Subject: [PATCH] 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