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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
@@ -74,7 +104,7 @@
@@ -92,9 +122,9 @@
-->
- Keycloak
+ Select role
-
@@ -114,6 +144,7 @@
+
+
+
+
+ Quota *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- Groups
+ Select group(s)
+
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 @@
- |
Id |
Name |
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 @@
- |
- Avatar |
Enabled |
- Username |
+ Avatar |
+ Role |
Actions |
+ Username |
First |
Last |
Email |
Groups |
- Roles |
-
+ Quota |
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