Merge branch 'add-user-modal' into 'develop'

Fixed add/update/delete ops for users and groups

See merge request isard/isard-sso!45
Josep Maria Viñolas Auquer 2021-09-01 06:43:09 +00:00
commit da43fd10df
24 changed files with 1017 additions and 184 deletions

View File

@ -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 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 # SSH configuration
ARG SSH_ROOT_PWD # ARG SSH_ROOT_PWD
RUN apk add openssh # RUN apk add openssh
RUN echo "root:$SSH_ROOT_PWD" |chpasswd # RUN echo "root:$SSH_ROOT_PWD" |chpasswd
RUN sed -i \ # RUN sed -i \
-e 's|[#]*PermitRootLogin prohibit-password|PermitRootLogin yes|g' \ # -e 's|[#]*PermitRootLogin prohibit-password|PermitRootLogin yes|g' \
-e 's|[#]*PasswordAuthentication yes|PasswordAuthentication yes|g' \ # -e 's|[#]*PasswordAuthentication yes|PasswordAuthentication yes|g' \
-e 's|[#]*ChallengeResponseAuthentication yes|ChallengeResponseAuthentication yes|g' \ # -e 's|[#]*ChallengeResponseAuthentication yes|ChallengeResponseAuthentication yes|g' \
-e 's|[#]*UsePAM yes|UsePAM yes|g' \ # -e 's|[#]*UsePAM yes|UsePAM yes|g' \
-e 's|[#]#Port 22|Port 22|g' \ # -e 's|[#]#Port 22|Port 22|g' \
/etc/ssh/sshd_config # /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 COPY admin/src /admin
RUN cd /admin/admin && yarn install RUN cd /admin/admin && yarn install
COPY admin/docker/docker-entrypoint.sh / COPY admin/docker/run.sh /run.sh
ENTRYPOINT ["/docker-entrypoint.sh"]
#EXPOSE 7039 #EXPOSE 7039
WORKDIR /admin CMD [ "/run.sh" ]
CMD [ "python3", "start.py" ]

View File

@ -1,4 +1,4 @@
python-keycloak==0.24.0 #python-keycloak==0.24.0
bcrypt==3.1.7 bcrypt==3.1.7
cffi==1.14.0 cffi==1.14.0
click==7.1.2 click==7.1.2
@ -26,5 +26,5 @@ python-engineio==3.8.1
python-socketio==4.1.0 python-socketio==4.1.0
minio==7.0.3 minio==7.0.3
urllib3==1.26.6
flask-oidc==1.4.0 flask-oidc==1.4.0

View File

@ -1,10 +1,11 @@
#!/bin/sh #!/bin/sh
ssh-keygen -A # ssh-keygen -A
## Only in development ## Only in development
cd /admin/admin cd /admin/admin
yarn install yarn install
## End Only in development ## End Only in development
cd /admin cd /admin
export PYTHONWARNINGS="ignore:Unverified HTTPS request" export PYTHONWARNINGS="ignore:Unverified HTTPS request"
python3 start.py & python3 start.py
/usr/sbin/sshd -D -e -f /etc/ssh/sshd_config #&
# /usr/sbin/sshd -D -e -f /etc/ssh/sshd_config

View File

@ -18,8 +18,12 @@ options = diceware.handle_options(None)
options.wordlist = 'cat_ascii' options.wordlist = 'cat_ascii'
options.num = 3 options.num = 3
from .helpers import rand_password
from .exceptions import UserExists, UserNotFound
from .events import Events from .events import Events
import secrets
class Admin(): class Admin():
def __init__(self): def __init__(self):
ready=False ready=False
@ -70,7 +74,8 @@ class Admin():
try: try:
self.resync_data() self.resync_data()
ready=True ready=True
except: except Exception as e:
print(e)
log.error('Could not connect to moodle, waiting to be online...') log.error('Could not connect to moodle, waiting to be online...')
sleep(2) 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 '', "last": u['displayname'].split(' ')[1] if u['displayname'] is not None and len(u['displayname'].split(' '))>1 else '',
"email": u.get('email',''), "email": u.get('email',''),
"groups": u['groups'], "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')] for u in self.nextcloud.get_users_list() if u['username'] not in ['guest','ddadmin','admin'] and not u['username'].startswith('system')]
## TOO SLOW ## TOO SLOW
@ -332,12 +340,19 @@ class Admin():
theuser['nextcloud']=True theuser['nextcloud']=True
theuser['nextcloud_groups']=nextcloud_exists[0]['groups'] theuser['nextcloud_groups']=nextcloud_exists[0]['groups']
theuser['nextcloud_id']=nextcloud_exists[0]['id'] theuser['nextcloud_id']=nextcloud_exists[0]['id']
theuser['quota']=theuser['quota'] if theuser['quota'] != None and theuser['quota'] != 'none' else False
else: else:
theuser['nextcloud']=False theuser['nextcloud']=False
theuser['nextcloud_groups']=[] theuser['nextcloud_groups']=[]
theuser['quota']=False
theuser['quota_used_bytes']=False
del theuser['groups'] 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 return users
def get_roles(self): def get_roles(self):
@ -611,7 +626,7 @@ class Admin():
if u['first'] == '': u['first']=' ' if u['first'] == '': u['first']=' '
if u['last'] == '': u['last']=' ' if u['last'] == '': u['last']=' '
try: 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: except:
log.error(' -->> Error creating on moodle the user: '+u['username']) log.error(' -->> Error creating on moodle the user: '+u['username'])
# user_id=user['id'] # 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'])) log.warning(' NEXTCLOUD USERS: Creating nextcloud user: '+u['username']+' in groups '+str(u['keycloak_groups']))
try: try:
ev.increment({'name':u['username']}) 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: except ProviderItemExists:
log.warning('User '+u['username']+' already exists. Skipping...') log.warning('User '+u['username']+' already exists. Skipping...')
continue continue
@ -802,8 +817,130 @@ class Admin():
def user_update_password(self,userid,password,temporary): def user_update_password(self,userid,password,temporary):
return self.keycloak.update_user_pwd(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): 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') ev=Events('Deleting user','Deleting from moodle')
self.delete_moodle_user(userid) self.delete_moodle_user(userid)
ev.update_text('Deleting from nextcloud') ev.update_text('Deleting from nextcloud')
@ -816,4 +953,125 @@ class Admin():
return True return True
def get_user(self,userid): 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)

View File

@ -14,13 +14,15 @@ from flask_socketio import SocketIO, emit, join_room, leave_room, \
import base64 import base64
class Events(): 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.eid=str(base64.b64encode(os.urandom(32))[:8])
self.title=title self.title=title
self.text=text self.text=text
self.total=total self.total=total
self.table=table self.table=table
self.item=0 self.item=0
self.type=type
self.create() self.create()
def create(self): def create(self):
@ -28,7 +30,8 @@ class Events():
app.socketio.emit('notify-create', app.socketio.emit('notify-create',
json.dumps({'id':self.eid, json.dumps({'id':self.eid,
'title':self.title, 'title':self.title,
'text':self.text}), 'text':self.text,
'type':self.type}),
namespace='/sio', namespace='/sio',
room='admin') room='admin')
sleep(0.001) sleep(0.001)
@ -69,6 +72,7 @@ class Events():
'item':self.item, 'item':self.item,
'total':self.total, 'total':self.total,
'table':self.table, 'table':self.table,
'type':self.type,
'data':data}), 'data':data}),
namespace='/sio', namespace='/sio',
room='admin') room='admin')
@ -84,6 +88,7 @@ class Events():
'item':self.item, 'item':self.item,
'total':self.total, 'total':self.total,
'table':self.table, 'table':self.table,
'type':self.type,
'data':data}), 'data':data}),
namespace='/sio', namespace='/sio',
room='admin') room='admin')

View File

@ -0,0 +1,7 @@
#!/usr/bin/env python
# coding=utf-8
class UserExists(Exception):
pass
class UserNotFound(Exception):
pass

View File

@ -1,3 +1,5 @@
import random
import string
def filter_roles_list(role_list): def filter_roles_list(role_list):
client_roles=['admin','manager','teacher','student'] client_roles=['admin','manager','teacher','student']
@ -6,3 +8,10 @@ def filter_roles_list(role_list):
def filter_roles_listofdicts(role_listofdicts): def filter_roles_listofdicts(role_listofdicts):
client_roles=['admin','manager','teacher','student'] client_roles=['admin','manager','teacher','student']
return [r for r in role_listofdicts if r['name'] in client_roles] 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

View File

@ -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'])] # user['roles']= [r['name'] for r in self.keycloak_admin.get_realm_roles_of_user(user_id=user['id'])]
# return users # 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) # RETURNS string with keycloak user id (the main id in this app)
self.connect() self.connect()
username=username.lower() username=username.lower()
try: try:
uid=self.keycloak_admin.create_user({"email": email, uid=self.keycloak_admin.create_user({"email": email,
"username": username, "username": username,
"enabled": True, "enabled": enabled,
"firstName": first, "firstName": first,
"lastName": last, "lastName": last,
"credentials":[{"type":"password", "credentials":[{"type":"password",
@ -172,17 +172,30 @@ class KeycloakClient():
self.connect() self.connect()
return self.keycloak_admin.update_user( user_id, payload) 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() 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): # def add_user_role(self,user_id,role_id):
# self.connect() # self.connect()
# return self.keycloak_admin.assign_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test") # 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() self.connect()
pass print(roles)
return self.keycloak_admin.delete_realm_roles_of_user(user_id,roles)
def delete_user(self,userid): def delete_user(self,userid):
self.connect() self.connect()
@ -192,6 +205,10 @@ class KeycloakClient():
self.connect() self.connect()
return self.keycloak_admin.get_user_groups(user_id=userid) 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): def add_user_client_role(self,client_id,user_id,role_id,role_name):
self.connect() self.connect()
return self.keycloak_admin.assign_client_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test") 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): def add_group(self,name,parent=None,skip_exists=False):
self.connect() 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) return self.keycloak_admin.create_group({"name":name}, parent=parent)
def delete_group(self,group_id): def delete_group(self,group_id):
@ -313,7 +331,7 @@ class KeycloakClient():
def get_role(self,name): def get_role(self,name):
self.connect() 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=''): def add_role(self,name,description=''):
self.connect() self.connect()
@ -356,7 +374,8 @@ class KeycloakClient():
role=[r for r in self.keycloak_admin.get_realm_roles() if r['name']==role] role=[r for r in self.keycloak_admin.get_realm_roles() if r['name']==role]
except: except:
return False 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 ## CLIENTS
def delete_client(self,clientid): def delete_client(self,clientid):

View File

@ -3,8 +3,10 @@ from admin import app
import logging as log import logging as log
from pprint import pprint from pprint import pprint
import traceback
from .postgres import Postgres from .postgres import Postgres
from .exceptions import UserExists, UserNotFound
# Module variables to connect to moodle api # Module variables to connect to moodle api
@ -63,14 +65,33 @@ class Moodle():
response = post(self.url+self.endpoint, parameters, verify=self.verify) response = post(self.url+self.endpoint, parameters, verify=self.verify)
response = response.json() response = response.json()
if type(response) == dict and response.get('exception'): if type(response) == dict and response.get('exception'):
raise SystemError("Error calling Moodle API\n", response) raise SystemError(response)
return response return response
def create_user(self, email, username, password, first_name='-', last_name='-'): def create_user(self, email, username, password, first_name='-', last_name='-'):
if len(self.get_user_by('username',username)['users']):
raise UserExists
try:
data = [{'username': username, 'email':email, data = [{'username': username, 'email':email,
'password': password, 'firstname':first_name, 'lastname':last_name}] 'password': password, 'firstname':first_name, 'lastname':last_name}]
user = self.call('core_user_create_users', users=data) user = self.call('core_user_create_users', users=data)
return user 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): def delete_user(self, user_id):
user = self.call('core_user_delete_users', userids=[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): def get_user_by(self, key, value):
criteria = [{'key': key, 'value': value}] criteria = [{'key': key, 'value': value}]
try:
user = self.call('core_user_get_users', criteria=criteria) user = self.call('core_user_get_users', criteria=criteria)
except:
raise SystemError("Error calling Moodle API\n", traceback.format_exc())
return user return user
def get_users_with_groups_and_roles(self): def get_users_with_groups_and_roles(self):
@ -142,6 +166,10 @@ class Moodle():
user = self.call('core_cohort_add_cohort_members', members=members) user = self.call('core_cohort_add_cohort_members', members=members)
return user 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): def get_cohort_members(self, cohort_ids):
members = self.call('core_cohort_get_cohort_members', cohortids=cohort_ids) members = self.call('core_cohort_get_cohort_members', cohortids=cohort_ids)
#[0]['userids'] #[0]['userids']

View File

@ -4,6 +4,7 @@
#from ..lib.log import * #from ..lib.log import *
from admin import app from admin import app
import time,requests,json,pprint,os import time,requests,json,pprint,os
import urllib
import traceback import traceback
import logging as log import logging as log
from .nextcloud_exc import * 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) response = requests.request(method, url, data=data, auth=auth, verify=self.verify_cert, headers=headers)
if 'meta' in response.text: if 'meta' in response.text:
if '<statuscode>997</statuscode>' in response.text: raise ProviderUnauthorized if '<statuscode>997</statuscode>' in response.text: raise ProviderUnauthorized
# if '<statuscode>998</statuscode>' in response.text: raise ProviderInvalidQuery
return response.text return response.text
## At least the ProviderSslError is not being catched or not raised correctly ## 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] # 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] # list_dict_users = [dict(zip(fields, r)) for r in users_with_lists]
def get_users_list(self): 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 from oc_users as u
left join oc_group_user as gu on gu.uid = u.uid 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_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_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 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' 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) (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]
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] 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) # 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) # 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=''): 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} data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':groups,'email':email,'displayname':displayname}
# if not group: del data['groups[]'] # if not group: del data['groups[]']
@ -325,7 +403,8 @@ class Nextcloud():
# 103 - failed to add the group # 103 - failed to add the group
def delete_group(self,groupid): 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 = { headers = {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true', 'OCS-APIRequest': 'true',

View File

@ -5,6 +5,33 @@ $(document).on('shown.bs.modal', '#modalAddDesktop', function () {
$(document).ready(function() { $(document).ready(function() {
$.ajax({
type: "GET",
"url": "/api/groups",
success: function(data)
{
$(".groups-select").append(
'<option value="" default>None</option>'
)
data.forEach(element => {
var groupOrigins = [];
['keycloak'].forEach(o => {
if (element[o]) {
groupOrigins.push(o)
}
})
$(".groups-select").append(
'<option value="' + element.path + '">' + element.path + '</option>'
)
});
$('.groups-select').select2();
},
error: function(data)
{
alert('Something went wrong on our side...')
}
});
$('.btn-global-resync').on('click', function () { $('.btn-global-resync').on('click', function () {
$.ajax({ $.ajax({
type: "GET", type: "GET",
@ -37,19 +64,44 @@ $(document).ready(function() {
formdata = form.serializeObject() formdata = form.serializeObject()
console.log('NEW GROUP') console.log('NEW GROUP')
console.log(formdata) console.log(formdata)
// $.ajax({
// type: "POST", $.ajax({
// "url": "/groups_list", type: "POST",
// success: function(data) "url": "/api/group",
// { data: JSON.stringify(formdata),
// console.log('SUCCESS') complete: function(jqXHR, textStatus) {
// // $("#modalAddGroup").modal('hide'); switch (jqXHR.status) {
// }, case 200:
// error: function(data) $("#modalAddGroup").modal('hide');
// { break;
// alert('Something went wrong on our side...') 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 () { $('.btn-delete_keycloak').on('click', function () {
@ -84,11 +136,13 @@ $(document).ready(function() {
"deferRender": true, "deferRender": true,
"columns": [ "columns": [
{ {
"className": 'details-control', "className": 'actions-control',
"orderable": false, "orderable": false,
"data": null, "data": null,
"width": "10px", "width": "80px",
"defaultContent": '<button class="btn btn-xs btn-info" type="button" data-placement="top" ><i class="fa fa-plus"></i></button>' "defaultContent": '<button id="btn-delete" class="btn btn-xs" type="button" data-placement="top" ><i class="fa fa-times" style="color:darkred"></i></button>'
// \
// <button id="btn-edit" class="btn btn-xs" type="button" data-placement="top" ><i class="fa fa-pencil" style="color:darkblue"></i></button>'
}, },
{ "data": "name", "width": "10px" }, { "data": "name", "width": "10px" },
{ "data": "path", "width": "10px" }, { "data": "path", "width": "10px" },
@ -96,4 +150,73 @@ $(document).ready(function() {
"order": [[2, 'asc']], "order": [[2, 'asc']],
// "columnDefs": [ ] // "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;
}
});
}) })

View File

@ -45,16 +45,15 @@ $(document).ready(function() {
"rowId": "id", "rowId": "id",
"deferRender": true, "deferRender": true,
"columns": [ "columns": [
{
"className": 'details-control',
"orderable": false,
"data": null,
"width": "10px",
"defaultContent": '<button class="btn btn-xs btn-info" type="button" data-placement="top" ><i class="fa fa-plus"></i></button>'
},
{ "data": "id", "width": "10px" }, { "data": "id", "width": "10px" },
{ "data": "name", "width": "10px" }, { "data": "name", "width": "10px" },
], ],
"order": [[1, 'asc']], "order": [[1, 'asc']],
// "columnDefs": [ {
// "targets": 0,
// "render": function ( data, type, full, meta ) {
// // return '<object data="/static/img/missing.jpg" type="image/jpeg" width="25" height="25"><img src="/avatar/'+full.id+'" title="'+full.id+'" width="25" height="25"></object>'
// return '<img src="/avatar/'+full.name+'" title="'+full.name+'" width="25" height="25" onerror="if (this.src != \'/static/img/missing.jpg\') this.src = \'/static/img/missing.jpg\';">'
// }}]
} ); } );
}); });

View File

@ -21,7 +21,8 @@ socket.on('notify-create', function(data) {
notice[data.id] = new PNotify({ notice[data.id] = new PNotify({
title: data.title, title: data.title,
text: data.text, 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({ notice[data.id] = new PNotify({
title: data.title, title: data.title,
text: data.text, text: data.text,
hide: false hide: false,
type: data.type
}); });
} }
// console.log(data.text) // console.log(data.text)

View File

@ -87,33 +87,168 @@ $(document).ready(function() {
// Open new user modal // Open new user modal
$('.btn-new-user').on('click', function () { $('.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({ $('#modalAddUser').modal({
backdrop: 'static', backdrop: 'static',
keyboard: false keyboard: false
}).modal('show'); }).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 // Send new user form
$('#modalAddUser #send').on('click', function () { $('#modalAddUser #send').on('click', function () {
var form = $('#modalAddUserForm'); var form = $('#modalAddUserForm');
form.parsley().validate();
if (form.parsley().isValid()){
formdata = form.serializeObject() formdata = form.serializeObject()
console.log('NEW USER') // console.log('NEW USER')
console.log(formdata) // console.log(formdata)
// $.ajax({
// type: "POST", $.ajax({
// "url": "/groups_list", type: "POST",
// success: function(data) "url": "/api/user",
// { data: JSON.stringify(formdata),
// console.log('SUCCESS') complete: function(jqXHR, textStatus) {
// // $("#modalAddUser").modal('hide'); switch (jqXHR.status) {
// }, case 200:
// error: function(data) $("#modalAddUser").modal('hide');
// { break;
// alert('Something went wrong on our side...') 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 //DataTable Main renderer
var table = $('#users').DataTable({ var table = $('#users').DataTable({
"ajax": { "ajax": {
@ -127,7 +262,6 @@ $(document).ready(function() {
"rowId": "id", "rowId": "id",
"deferRender": true, "deferRender": true,
"columns": [ "columns": [
{"data": null, "defaultContent":'',"width": "1px"},
// { // {
// "className": 'details-control', // "className": 'details-control',
// "orderable": false, // "orderable": false,
@ -135,9 +269,9 @@ $(document).ready(function() {
// "width": "10px", // "width": "10px",
// "defaultContent": '<button class="btn btn-xs btn-info" type="button" data-placement="top" ><i class="fa fa-plus"></i></button>' // "defaultContent": '<button class="btn btn-xs btn-info" type="button" data-placement="top" ><i class="fa fa-plus"></i></button>'
// }, // },
{ "data": "enabled", "width": "1px" },
{ "data": "id", "width": "10px" }, { "data": "id", "width": "10px" },
{ "data": "enabled", "width": "10px" }, { "data": "roles", "width": "10px" },
{ "data": "username", "width": "10px"},
{ {
"className": 'actions-control', "className": 'actions-control',
"orderable": false, "orderable": false,
@ -147,11 +281,12 @@ $(document).ready(function() {
<button id="btn-password" class="btn btn-xs" type="button" data-placement="top" ><i class="fa fa-lock" style="color:orange"></i></button> \ <button id="btn-password" class="btn btn-xs" type="button" data-placement="top" ><i class="fa fa-lock" style="color:orange"></i></button> \
<button id="btn-edit" class="btn btn-xs" type="button" data-placement="top" ><i class="fa fa-pencil" style="color:darkblue"></i></button>' <button id="btn-edit" class="btn btn-xs" type="button" data-placement="top" ><i class="fa fa-pencil" style="color:darkblue"></i></button>'
}, },
{ "data": "username", "width": "10px"},
{ "data": "first", "width": "10px"}, { "data": "first", "width": "10px"},
{ "data": "last", "width": "150px"}, { "data": "last", "width": "150px"},
{ "data": "email", "width": "10px"}, { "data": "email", "width": "10px"},
{ "data": "keycloak_groups", "width": "50px" }, { "data": "keycloak_groups", "width": "50px" },
{ "data": "roles", "width": "10px" }, { "data": "quota", "width": "10px", "default": "-"},
], ],
"order": [[6, 'asc']], "order": [[6, 'asc']],
"columnDefs": [ { "columnDefs": [ {
@ -161,7 +296,7 @@ $(document).ready(function() {
return '<img src="/avatar/'+full.id+'" title="'+full.id+'" width="25" height="25" onerror="if (this.src != \'/static/img/missing.jpg\') this.src = \'/static/img/missing.jpg\';">' return '<img src="/avatar/'+full.id+'" title="'+full.id+'" width="25" height="25" onerror="if (this.src != \'/static/img/missing.jpg\') this.src = \'/static/img/missing.jpg\';">'
}}, }},
{ {
"targets": 2, "targets": 0,
"render": function ( data, type, full, meta ) { "render": function ( data, type, full, meta ) {
if(full.enabled){ if(full.enabled){
return '<i class="fa fa-check" style="color:lightgreen"></i>' return '<i class="fa fa-check" style="color:lightgreen"></i>'
@ -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 ) { "render": function ( data, type, full, meta ) {
return '<b>'+full.username+'</b>' return '<b>'+full.username+'</b>'
}}, }},
@ -182,32 +322,41 @@ $(document).ready(function() {
grups += '<span class="label label-primary" style="margin: 5px;">' + element + '</span>' grups += '<span class="label label-primary" style="margin: 5px;">' + element + '</span>'
}) })
return grups 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 () { // $('#users').find('tbody').on('click', 'td.details-control', function () {
var tr = $(this).closest('tr'); // var tr = $(this).closest('tr');
var row = table.row( tr ); // var row = table.row( tr );
if ( row.child.isShown() ) { // if ( row.child.isShown() ) {
// This row is already open - close it // // This row is already open - close it
row.child.hide(); // row.child.hide();
tr.removeClass('shown'); // tr.removeClass('shown');
} // }
else { // else {
// Close other rows // // Close other rows
if ( table.row( '.shown' ).length ) { // if ( table.row( '.shown' ).length ) {
$('.details-control', table.row( '.shown' ).node()).click(); // $('.details-control', table.row( '.shown' ).node()).click();
} // }
// Open this row // // Open this row
row.child( addUserDetailPannel(row.data()) ).show(); // row.child( addUserDetailPannel(row.data()) ).show();
tr.addClass('shown'); // tr.addClass('shown');
actionsUserDetail() // actionsUserDetail()
} // }
} ); // } );
$('#users').find(' tbody').on( 'click', 'button', function () { $('#users').find(' tbody').on( 'click', 'button', function () {
var data = table.row( $(this).parents('tr') ).data(); var data = table.row( $(this).parents('tr') ).data();
@ -291,7 +440,7 @@ $(document).ready(function() {
id=$('#modalPasswdUserForm #id').val(); id=$('#modalPasswdUserForm #id').val();
$.ajax({ $.ajax({
type: "PUT", type: "PUT",
url:"/api/user//+" + id, url:"/api/user_password/" + id,
data: JSON.stringify(formdata), data: JSON.stringify(formdata),
success: function(data) success: function(data)
{ {
@ -320,26 +469,15 @@ $(document).ready(function() {
} }
}); });
$("#modalEditUser #send").on('click', function(e){ // function addUserDetailPannel ( d ) {
var form = $('#modalEditUserForm'); // $newPanel = $template.clone();
form.parsley().validate(); // $newPanel.html(function(i, oldHtml){
if (form.parsley().isValid()){ // return oldHtml.replace(/d.id/g, d.id).replace(/d.username/g, d.username);
data=$('#modalEditUserForm').serializeObject(); // });
data['id']=$('#modalEditUserForm #id').val(); // return $newPanel
console.log('Editing user...') // }
console.log(data)
}
});
function addUserDetailPannel ( d ) { // function actionsUserDetail(){
$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(){
// $('.btn-passwd').on('click', function () { // $('.btn-passwd').on('click', function () {
// var closest=$(this).closest("div").parent(); // var closest=$(this).closest("div").parent();
@ -392,13 +530,15 @@ $(document).ready(function() {
// }).on('pnotify.cancel', function() { // }).on('pnotify.cancel', function() {
// }); // });
// }); // });
} // }
function setUserDefault(div_id, user_id) { function setUserDefault(div_id, user_id) {
$.ajax({ $.ajax({
type: "GET", type: "GET",
url:"/api/user/" + user_id, url:"/api/user/" + user_id,
success: function(data) success: function(data)
{ {
console.log(data)
if (data.enabled) { if (data.enabled) {
$(div_id + ' #enabled').iCheck('check') $(div_id + ' #enabled').iCheck('check')
} }
@ -407,11 +547,18 @@ $(document).ready(function() {
$(div_id + ' #email').val(data.email); $(div_id + ' #email').val(data.email);
$(div_id + ' #firstname').val(data.first); $(div_id + ' #firstname').val(data.first);
$(div_id + ' #lastname').val(data.last); $(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 + ' .groups-select').val(data.keycloak_groups);
// $(div_id + ' .role-moodle-select').val(data.keycloak_roles); // $(div_id + ' .role-moodle-select').val(data.keycloak_roles);
// $(div_id + ' .role-nextcloud-select').val(data.roles); // $(div_id + ' .role-nextcloud-select').val(data.roles);
$(div_id + ' .role-keycloak-select').val(data.roles[0]); $(div_id + ' .role-keycloak-select').val(data.roles[0]);
$('.groups-select').trigger('change'); $('.groups-select').trigger('change');
// $('.groups-select, .role-moodle-select, .role-nextcloud-select, .role-keycloak-select').trigger('change'); // $('.groups-select, .role-moodle-select, .role-nextcloud-select, .role-keycloak-select').trigger('change');
} }
}); });

View File

@ -15,7 +15,7 @@
<h3><i class="fa fa-users"></i> Groups</h3> <h3><i class="fa fa-users"></i> Groups</h3>
<ul class="nav navbar-right panel_toolbox"> <ul class="nav navbar-right panel_toolbox">
<li> <li>
<!-- <a class="btn-new"><span style="color: #5499c7; "><i class="fa fa-plus"></i> Add new</span></a> --> <a class="btn-new"><span style="color: #5499c7; "><i class="fa fa-plus"></i> Add new</span></a>
<!-- <a class="btn-delete_keycloak"><span style="color: #c75454; "><i class="fa fa-cross"></i> Delete all keycloak</span></a> --> <!-- <a class="btn-delete_keycloak"><span style="color: #c75454; "><i class="fa fa-cross"></i> Delete all keycloak</span></a> -->
</li> </li>
</ul> </ul>

View File

@ -36,6 +36,22 @@
</div> </div>
</div> </div>
</div> </div>
<div class="x_panel">
<div class="x_title">
<h4><i class="fa fa-users" aria-hidden="true"></i> Parent group</h4>
<div class="clearfix"></div>
</div>
<div class="x_content" style="padding: 0px;">
<div class="row">
<div class="col-md-12 col-xs-12">
<label class="control-label" for="id">Select parent group
</label>
<select class="groups-select roundbox" id="parent" name="parent" style="width:100%" required>
</select>
</div>
</div>
</div>
</div>
</form> </form>
<!-- Modal Footer --> <!-- Modal Footer -->
<div class="modal-footer"> <div class="modal-footer">

View File

@ -24,7 +24,7 @@
<div class="checkbox"> <div class="checkbox">
<label class=""> <label class="">
<div class="icheckbox_flat-green" style="position: relative;"> <div class="icheckbox_flat-green" style="position: relative;">
<input type="checkbox" id="jumperurl-check" name="jumperurl-check" class="flat" style="position: absolute; opacity: 0;"> <input type="checkbox" id="enabled" name="enabled" class="flat" style="position: absolute; opacity: 0;">
<ins class="iCheck-helper" style="position: absolute; top: 0%; left: 0%; display: block; width: 100%; height: 100%; margin: 0px; padding: 0px; background: rgb(255, 255, 255); border: 0px; opacity: 0;"> <ins class="iCheck-helper" style="position: absolute; top: 0%; left: 0%; display: block; width: 100%; height: 100%; margin: 0px; padding: 0px; background: rgb(255, 255, 255); border: 0px; opacity: 0;">
</ins> </ins>
</div> </div>
@ -42,31 +42,61 @@
<div class="col-md-6 col-xs-12"> <div class="col-md-6 col-xs-12">
<label class="control-label" for="name">Username <span class="required">*</span> <label class="control-label" for="name">Username <span class="required">*</span>
</label> </label>
<input id="username" class="roundbox" maxlength="40" pattern="^[-_àèìòùáéíóúñçÀÈÌÒÙÁÉÍÓÚÑÇ .a-zA-Z0-9]+$" data-parsley-length="[4, 40]" name="name" placeholder="Username" data-parsley-trigger="change" required type="text" style="width:100%"> <input id="username" class="roundbox" maxlength="40" pattern="^[-_àèìòùáéíóúñçÀÈÌÒÙÁÉÍÓÚÑÇ .a-zA-Z0-9]+$" data-parsley-length="[4, 40]" name="username" placeholder="Username" data-parsley-trigger="change" required type="text" style="width:100%">
</div> </div>
<div class="col-md-6 col-xs-12"> <div class="col-md-6 col-xs-12">
<label class="control-label" for="email">Email <span class="required">*</span> <label class="control-label" for="email">Email <span class="required">*</span>
</label> </label>
<input id="email" class="roundbox" data-validate-length-range="4,40" name="email" placeholder="Email" type="email" data-parsley-trigger="change" style="width:100%"> <input id="email" class="roundbox" data-validate-length-range="4,40" name="email" placeholder="Email" type="email" data-parsley-trigger="change" style="width:100%" required>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6 col-xs-12"> <div class="col-md-6 col-xs-12">
<label class="control-label" for="firstname">First name <span class="required">*</span> <label class="control-label" for="first">First name <span class="required">*</span>
</label> </label>
<input id="firstname" class="roundbox" name="firstname" placeholder="First name" type="text" style="width:100%"> <input id="first" class="roundbox" name="first" placeholder="First name" type="text" style="width:100%" required>
</div> </div>
<div class="col-md-6 col-xs-12"> <div class="col-md-6 col-xs-12">
<label class="control-label" for="lastname">Last name <span class="required">*</span> <label class="control-label" for="last">Last name <span class="required">*</span>
</label> </label>
<input id="lastname" class="roundbox" name="lastname" placeholder="First name" type="text" style="width:100%"> <input id="last" class="roundbox" name="last" placeholder="Last name" type="text" style="width:100%" required>
</div> </div>
</div> </div>
<div class="row">
<div class="col-md-6 col-xs-12">
<label class="control-label" for="quota">Quota <span class="required">*</span>
</label>
<select class="roundbox" name="quota" style="width:100%" required>
<option value="500 MB">500 MB</option>
<option value="1 GB">1 GB</option>
<option value="3 GB">3 GB</option>
<option value="5 GB">5 GB</option>
<option value=false>Unlimited</option>
</select>
</div>
<div class="col-md-6 col-xs-12">
<label class="control-label" for="password">Password <span class="required">*</span>
</label>
<input id="password" class="roundbox" name="password" placeholder="Password" type="text" style="width:100%"
data-parsley-minlength="10"
data-parsley-uppercase="2"
data-parsley-lowercase="2"
required>
</div>
</div>
</div>
</div>
<div class="x_panel">
<div class="x_title">
<h4><i class="fa fa-users" aria-hidden="true"></i> Groups</h4>
<div class="clearfix"></div>
</div>
<div class="x_content" style="padding: 0px;">
<div class="row"> <div class="row">
<div class="col-md-12 col-xs-12"> <div class="col-md-12 col-xs-12">
<label class="control-label" for="id">Group <label class="control-label" for="id">Select group(s)
</label> </label>
<select class="groups-select roundbox" name="groups[]" multiple="multiple" style="width:100%"> <select class="groups-select roundbox" name="groups[]" multiple="multiple" style="width:100%" required>
</select> </select>
</div> </div>
</div> </div>
@ -74,7 +104,7 @@
</div> </div>
<div class="x_panel"> <div class="x_panel">
<div class="x_title"> <div class="x_title">
<h4><i class="fa fa-user-secret" aria-hidden="true"></i> Roles</h4> <h4><i class="fa fa-user-secret" aria-hidden="true"></i> Role</h4>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<div class="x_content" style="padding: 0px;"> <div class="x_content" style="padding: 0px;">
@ -92,9 +122,9 @@
</select> </select>
</div> --> </div> -->
<div class="col-md-12 col-xs-12"> <div class="col-md-12 col-xs-12">
<label class="control-label" for="id">Keycloak <label class="control-label" for="id">Select role
</label> </label>
<select class="role-keycloak-select" name="keycloak" style="width:100%"> <select class="role-keycloak-select" name="role" style="width:100%" required>
</select> </select>
</div> </div>
</div> </div>
@ -114,6 +144,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" id="modalPasswdUser" tabindex="-1" role="dialog" aria-labelledby="modalPasswdUser" aria-hidden="true"> <div class="modal fade" id="modalPasswdUser" tabindex="-1" role="dialog" aria-labelledby="modalPasswdUser" aria-hidden="true">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
@ -165,6 +196,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" id="modalEditUser" tabindex="-1" role="dialog" aria-labelledby="modalEditUser" aria-hidden="true"> <div class="modal fade" id="modalEditUser" tabindex="-1" role="dialog" aria-labelledby="modalEditUser" aria-hidden="true">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
@ -234,12 +266,35 @@
<input id="lastname" class="roundbox" name="lastname" placeholder="First name" type="text" style="width:100%"> <input id="lastname" class="roundbox" name="lastname" placeholder="First name" type="text" style="width:100%">
</div> </div>
</div> </div>
<div class="row">
<div class="col-md-6 col-xs-12">
<label class="control-label" for="quota">Quota <span class="required">*</span>
</label>
<select class="roundbox" id="quota" name="quota" style="width:100%" required>
<option value="500 MB">500 MB</option>
<option value="1 GB">1 GB</option>
<option value="3 GB">3 GB</option>
<option value="5 GB">5 GB</option>
<option value=false>Unlimited</option>
</select>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
<div class="x_panel">
<div class="x_title">
<h4><i class="fa fa-users" aria-hidden="true"></i> Groups</h4>
<div class="clearfix"></div>
</div>
<div class="x_content" style="padding: 0px;">
<div class="row"> <div class="row">
<div class="col-md-12 col-xs-12"> <div class="col-md-12 col-xs-12">
<label class="control-label" for="id">Groups</label> <label class="control-label" for="id">Select group(s)
</label>
<select class="groups-select roundbox" name="groups[]" multiple="multiple" style="width:100%"> <select class="groups-select roundbox" name="groups[]" multiple="multiple" style="width:100%">
</select> </select>
</div> </div>

View File

@ -25,7 +25,6 @@
<table id="roles" class="table" width="100%"> <table id="roles" class="table" width="100%">
<thead> <thead>
<tr> <tr>
<th></th>
<th>Id</th> <th>Id</th>
<th>Name</th> <th>Name</th>
</tr> </tr>

View File

@ -24,17 +24,16 @@
<table id="users" class="table" width="100%"> <table id="users" class="table" width="100%">
<thead> <thead>
<tr> <tr>
<th></th>
<th>Avatar</th>
<th>Enabled</th> <th>Enabled</th>
<th>Username</th> <th>Avatar</th>
<th>Role</th>
<th>Actions</th> <th>Actions</th>
<th>Username</th>
<th>First</th> <th>First</th>
<th>Last</th> <th>Last</th>
<th>Email</th> <th>Email</th>
<th>Groups</th> <th>Groups</th>
<th>Roles</th> <th>Quota</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@ -9,7 +9,9 @@ import time,json
import sys,os import sys,os
from flask import render_template, Response, request, redirect, url_for, jsonify from flask import render_template, Response, request, redirect, url_for, jsonify
import concurrent.futures 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 Queue
import threading import threading
threads={} threads={}
@ -17,6 +19,8 @@ threads={}
from keycloak.exceptions import KeycloakGetError from keycloak.exceptions import KeycloakGetError
from ..lib.exceptions import UserExists, UserNotFound
@app.route('/api/resync') @app.route('/api/resync')
@login_required @login_required
def resync(): def resync():
@ -27,6 +31,7 @@ def resync():
@login_required @login_required
def users(provider=False): def users(provider=False):
if request.method == 'DELETE': if request.method == 'DELETE':
if current_user.role != 'admin': return json.dumps({}), 301, {'Content-Type': 'application/json'}
if provider == 'keycloak': if provider == 'keycloak':
return json.dumps(app.admin.delete_keycloak_users()), 200, {'Content-Type': 'application/json'} return json.dumps(app.admin.delete_keycloak_users()), 200, {'Content-Type': 'application/json'}
if provider == 'nextcloud': if provider == 'nextcloud':
@ -34,11 +39,18 @@ def users(provider=False):
if provider == 'moodle': if provider == 'moodle':
return json.dumps(app.admin.delete_moodle_users()), 200, {'Content-Type': 'application/json'} return json.dumps(app.admin.delete_moodle_users()), 200, {'Content-Type': 'application/json'}
if request.method == 'POST': if request.method == 'POST':
if current_user.role != 'admin': return json.dumps({}), 301, {'Content-Type': 'application/json'}
if provider == 'moodle': if provider == 'moodle':
return json.dumps(app.admin.sync_to_moodle()), 200, {'Content-Type': 'application/json'} return json.dumps(app.admin.sync_to_moodle()), 200, {'Content-Type': 'application/json'}
if provider == 'nextcloud': if provider == 'nextcloud':
return json.dumps(app.admin.sync_to_nextcloud()), 200, {'Content-Type': 'application/json'} 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 # Update pwd
@ -56,35 +68,84 @@ def user_password(userid=False):
res = app.admin.user_update_password(userid,password,temporary) res = app.admin.user_update_password(userid,password,temporary)
return json.dumps({}), 200, {'Content-Type': 'application/json'} return json.dumps({}), 200, {'Content-Type': 'application/json'}
except KeycloakGetError as e: except KeycloakGetError as e:
print(e.error_message.decode("utf-8")) log.error(e.error_message.decode("utf-8"))
return e.error_message, e.response_code, {'Content-Type': 'application/json'} 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 # User
@app.route('/api/user/<userid>', methods=['POST', 'PUT', 'GET', 'DELETE']) @app.route('/api/user', methods=['POST'])
@app.route('/api/user/<userid>', methods=['PUT', 'GET', 'DELETE'])
@login_required @login_required
def user(userid=None): def user(userid=None):
if request.method == 'DELETE': 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({}), 200, {'Content-Type': 'application/json'}
# return json.dumps(), 301, {'Content-Type': 'application/json'}
if request.method == 'POST': 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': 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': if request.method == 'DELETE':
pass pass
if request.method == 'GET': if request.method == 'GET':
res = app.admin.get_user(userid) user = app.admin.get_user(userid)
return json.dumps(res), 200, {'Content-Type': 'application/json'} 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') @app.route('/api/roles')
@login_required @login_required
def roles(): def roles():
sorted_roles = sorted(app.admin.get_roles(), key=lambda k: k['name']) 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'} return json.dumps(sorted_roles), 200, {'Content-Type': 'application/json'}
@app.route('/api/group', methods=['POST','DELETE'])
@app.route('/api/group/<group_id>', methods=['PUT', 'GET', 'DELETE'])
@login_required
def group(group_id=False):
if request.method == 'POST':
data=request.get_json(force=True)
data['parent']=data['parent'] if data['parent'] != '' else None
return json.dumps(app.admin.add_group(data)), 200, {'Content-Type': 'application/json'}
if request.method == 'DELETE':
try:
data=request.get_json(force=True)
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')
@app.route('/api/groups/<provider>', methods=['POST', 'PUT', 'GET', 'DELETE']) @app.route('/api/groups/<provider>', methods=['POST', 'PUT', 'GET', 'DELETE'])
@ -92,6 +153,11 @@ def roles():
def groups(provider=False): def groups(provider=False):
if request.method == 'GET': if request.method == 'GET':
sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k['name']) sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k['name'])
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'} return json.dumps(sorted_groups), 200, {'Content-Type': 'application/json'}
if request.method == 'DELETE': if request.method == 'DELETE':
if provider == 'keycloak': if provider == 'keycloak':

View File

@ -10,6 +10,7 @@ import sys,os
from flask import render_template, Response, request, redirect, url_for, jsonify, send_file from flask import render_template, Response, request, redirect, url_for, jsonify, send_file
import concurrent.futures import concurrent.futures
from flask_login import login_required from flask_login import login_required
from .decorators import is_admin
from pprint import pprint from pprint import pprint
from ..lib.avatars import Avatars from ..lib.avatars import Avatars
@ -70,17 +71,20 @@ def avatar(userid):
@app.route('/sysadmin/users') @app.route('/sysadmin/users')
@login_required @login_required
@is_admin
def web_sysadmin_users(): def web_sysadmin_users():
return render_template('pages/sysadmin/users.html', title="SysAdmin Users", nav="SysAdminUsers") return render_template('pages/sysadmin/users.html', title="SysAdmin Users", nav="SysAdminUsers")
@app.route('/sysadmin/groups') @app.route('/sysadmin/groups')
@login_required @login_required
@is_admin
def web_sysadmin_groups(): def web_sysadmin_groups():
return render_template('pages/sysadmin/groups.html', title="SysAdmin Groups", nav="SysAdminGroups") return render_template('pages/sysadmin/groups.html', title="SysAdmin Groups", nav="SysAdminGroups")
@app.route('/sysadmin/external') @app.route('/sysadmin/external')
@login_required @login_required
@is_admin
## SysAdmin role ## SysAdmin role
def web_sysadmin_external(): def web_sysadmin_external():
return render_template('pages/sysadmin/external.html', title="External", nav="External") return render_template('pages/sysadmin/external.html', title="External", nav="External")

View File

@ -3,9 +3,16 @@
from functools import wraps from functools import wraps
from flask import request, redirect, url_for from flask import request, redirect, url_for
from flask_login import logout_user from flask_login import current_user, logout_user
import socket 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): def is_internal(fn):
@wraps(fn) @wraps(fn)
def decorated_view(*args, **kwargs): def decorated_view(*args, **kwargs):

View File

@ -0,0 +1,7 @@
---
version: '3.7'
services:
isard-sso-admin:
volumes:
- ${BUILD_ROOT_PATH}/admin/src:/admin:rw
command: /bin/sleep infinity

View File

@ -6,14 +6,14 @@ services:
context: ${BUILD_ROOT_PATH} context: ${BUILD_ROOT_PATH}
dockerfile: admin/docker/Dockerfile dockerfile: admin/docker/Dockerfile
target: production target: production
args: ## DEVELOPMENT # args: ## DEVELOPMENT
SSH_ROOT_PWD: ${IPA_ADMIN_PWD} # SSH_ROOT_PWD: ${IPA_ADMIN_PWD}
SSH_PORT: 2022 # SSH_PORT: 2022
networks: networks:
- isard_net - isard_net
ports: # ports:
- "2022:22" # - "2022:22"
- "9000:9000" # - "9000:9000"
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
@ -26,4 +26,3 @@ services:
- .env - .env
environment: environment:
- VERIFY="false" # In development do not verify certificates - VERIFY="false" # In development do not verify certificates
command: sleep infinity