Merge branch 'develop' into 'master'

Merge develop into master

See merge request isard/isard-sso!46
Josep Maria Viñolas Auquer 2021-09-01 06:45:14 +00:00
commit 4a2b1c3058
33 changed files with 1418 additions and 197 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
# 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" ]
CMD [ "/run.sh" ]

View File

@ -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

View File

@ -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
python3 start.py
#&
# /usr/sbin/sshd -D -e -f /etc/ssh/sshd_config

View File

@ -87,6 +87,7 @@ Import all views
from .views import LoginViews
from .views import WebViews
from .views import ApiViews
from .views import InternalViews

View File

@ -36,6 +36,11 @@ ram_users={
'id': os.environ["KEYCLOAK_USER"],
'password': os.environ["KEYCLOAK_PASSWORD"],
'role': 'admin',
},
os.environ["WORDPRESS_MARIADB_USER"]: {
'id': os.environ["WORDPRESS_MARIADB_USER"],
'password': os.environ["WORDPRESS_MARIADB_PASSWORD"],
'role': 'manager',
}
}

View File

@ -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)
@ -93,13 +98,17 @@ class Admin():
### User admin in group admin
try:
log.warning('KEYCLOAK: Adding group admin and user admin to this group')
self.keycloak.add_group('admin')
## Add default admin user to group admin (for nextcloud, just in case we go there)
admin_uid=self.keycloak_admin.get_user_id('admin')
self.keycloak_admin.group_user_add(admin_uid,gid)
admin_guid=self.keycloak.add_group('admin')
except:
pass
admin_guid=self.keycloak.get_group_by_path(path='/admin')['id']
try:
## Add default admin user to group admin
admin_uid=self.keycloak.get_user_id('admin')
self.keycloak.group_user_add(admin_uid,admin_guid)
log.warning('KEYCLOAK: OK')
except:
# print(traceback.format_exc())
print(traceback.format_exc())
log.warning('KEYCLOAK: Seems to be there already')
#### Add default groups
@ -266,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
@ -328,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):
@ -607,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']
@ -673,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
@ -798,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')
@ -812,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)

View File

@ -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')

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):
client_roles=['admin','manager','teacher','student']
@ -6,3 +8,10 @@ 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]
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'])]
# 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):

View File

@ -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']

View File

@ -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 *
@ -29,7 +30,11 @@ class Nextcloud():
def _request(self,method,url,data={},headers={'OCS-APIRequest':'true'},auth=False):
if auth == False: auth=self.auth
try:
return requests.request(method, url, data=data, auth=auth, verify=self.verify_cert, headers=headers).text
response = requests.request(method, url, data=data, auth=auth, verify=self.verify_cert, headers=headers)
if 'meta' in response.text:
if '<statuscode>997</statuscode>' in response.text: raise ProviderUnauthorized
# if '<statuscode>998</statuscode>' in response.text: raise ProviderInvalidQuery
return response.text
## At least the ProviderSslError is not being catched or not raised correctly
except requests.exceptions.HTTPError as errh:
@ -99,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
@ -107,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]
@ -160,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[]']
@ -322,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',

View File

@ -1,5 +1,8 @@
#!/usr/bin/env python
# coding=utf-8
class ProviderUnauthorized(Exception):
pass
class ProviderConnError(Exception):
pass

View File

@ -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(
'<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 () {
$.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": '<button class="btn btn-xs btn-info" type="button" data-placement="top" ><i class="fa fa-plus"></i></button>'
"className": 'actions-control',
"orderable": false,
"data": null,
"width": "80px",
"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": "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;
}
});
})

View File

@ -45,16 +45,15 @@ $(document).ready(function() {
"rowId": "id",
"deferRender": true,
"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": "name", "width": "10px" },
],
"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({
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)

View File

@ -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": '<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": "enabled", "width": "10px" },
{ "data": "username", "width": "10px"},
{ "data": "roles", "width": "10px" },
{
"className": 'actions-control',
"orderable": false,
@ -146,12 +280,13 @@ $(document).ready(function() {
"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-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>'
},
{ "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']],
"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\';">'
}},
{
"targets": 2,
"targets": 0,
"render": function ( data, type, full, meta ) {
if(full.enabled){
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 ) {
return '<b>'+full.username+'</b>'
}},
@ -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');
}
});

View File

@ -15,7 +15,7 @@
<h3><i class="fa fa-users"></i> Groups</h3>
<ul class="nav navbar-right panel_toolbox">
<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> -->
</li>
</ul>

View File

@ -36,6 +36,22 @@
</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>
<!-- Modal Footer -->
<div class="modal-footer">

View File

@ -24,7 +24,7 @@
<div class="checkbox">
<label class="">
<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>
</div>
@ -42,31 +42,61 @@
<div class="col-md-6 col-xs-12">
<label class="control-label" for="name">Username <span class="required">*</span>
</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 class="col-md-6 col-xs-12">
<label class="control-label" for="email">Email <span class="required">*</span>
</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 class="row">
<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>
<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 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>
<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 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="col-md-12 col-xs-12">
<label class="control-label" for="id">Group
<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%" required>
</select>
</div>
</div>
@ -74,7 +104,7 @@
</div>
<div class="x_panel">
<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>
<div class="x_content" style="padding: 0px;">
@ -92,9 +122,9 @@
</select>
</div> -->
<div class="col-md-12 col-xs-12">
<label class="control-label" for="id">Keycloak
<label class="control-label" for="id">Select role
</label>
<select class="role-keycloak-select" name="keycloak" style="width:100%">
<select class="role-keycloak-select" name="role" style="width:100%" required>
</select>
</div>
</div>
@ -114,6 +144,7 @@
</div>
</div>
</div>
<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-content">
@ -165,6 +196,7 @@
</div>
</div>
</div>
<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-content">
@ -234,12 +266,35 @@
<input id="lastname" class="roundbox" name="lastname" placeholder="First name" type="text" style="width:100%">
</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 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="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>
</div>

View File

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

View File

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

View File

@ -9,16 +9,18 @@ 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={}
# q = Queue.Queue()
from pprint import pprint
from keycloak.exceptions import KeycloakGetError
from ..lib.exceptions import UserExists, UserNotFound
@app.route('/api/resync')
@login_required
def resync():
@ -29,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':
@ -36,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
@ -58,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/<userid>', methods=['POST', 'PUT', 'GET', 'DELETE'])
@app.route('/api/user', methods=['POST'])
@app.route('/api/user/<userid>', methods=['PUT', 'GET', 'DELETE'])
@login_required
def user(userid=None):
if request.method == 'DELETE':
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/<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/<provider>', methods=['POST', 'PUT', 'GET', 'DELETE'])
@ -94,13 +153,16 @@ 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':
return json.dumps(app.admin.delete_keycloak_groups()), 200, {'Content-Type': 'application/json'}
### SYSADM USERS ONLY
@app.route('/api/external', methods=['POST', 'PUT', 'GET','DELETE'])

View File

@ -0,0 +1,107 @@
#!flask/bin/python
# coding=utf-8
from admin import app
import logging as log
import traceback
import time,json
import sys,os
from flask import request
from .decorators import is_internal
@app.route('/api/internal/users', methods=['GET'])
@is_internal
def internal_users():
if request.method == 'GET':
sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k['username'])
# group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']]
users=[]
for user in sorted_users:
if not user['enabled']: continue
users.append(user_parser(user))
return json.dumps(users), 200, {'Content-Type': 'application/json'}
@app.route('/api/internal/users/filter', methods=['POST'])
@is_internal
def internal_users_search():
if request.method == 'POST':
data=request.get_json(force=True)
users = app.admin.get_mix_users()
result = [user_parser(user) for user in filter_users(users, data['text'])]
sorted_result = sorted(result, key=lambda k: k['id'])
return json.dumps(sorted_result), 200, {'Content-Type': 'application/json'}
@app.route('/api/internal/groups', methods=['GET'])
@is_internal
def internal_groups():
if request.method == 'GET':
sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: k['name'])
groups=[]
for group in sorted_groups:
if not group['path'].startswith('/'): continue
groups.append({'id':group['path'],
'name':group['name'],
'description':group.get('description','')})
return json.dumps(groups), 200, {'Content-Type': 'application/json'}
@app.route('/api/internal/group/users', methods=['POST'])
@is_internal
def internal_group_users():
if request.method == 'POST':
data=request.get_json(force=True)
sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k['username'])
# group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']]
users=[]
for user in sorted_users:
if data['path'] not in user['keycloak_groups'] or not user['enabled']: continue
users.append(user)
if data.get('text',False) and data['text'] != '':
result = [user_parser(user) for user in filter_users(users, data['text'])]
else:
result = [user_parser(user) for user in users]
return json.dumps(result), 200, {'Content-Type': 'application/json'}
@app.route('/api/internal/roles', methods=['GET'])
@is_internal
def internal_roles():
if request.method == 'GET':
roles=[]
for role in sorted(app.admin.get_roles(), key=lambda k: k['name']):
if role['name'] == 'admin': continue
roles.append({'id':role['id'],
'name':role['name'],
'description':role.get('description','')})
return json.dumps(roles), 200, {'Content-Type': 'application/json'}
@app.route('/api/internal/role/users', methods=['POST'])
@is_internal
def internal_role_users():
if request.method == 'POST':
data=request.get_json(force=True)
sorted_users = sorted(app.admin.get_mix_users(), key=lambda k: k['username'])
# group_users = [user for user in sorted_users if data['path'] in user['keycloak_groups']]
users=[]
for user in sorted_users:
if data['role'] not in user['roles'] or not user['enabled']: continue
users.append(user)
if data.get('text',False) and data['text'] != '':
result = [user_parser(user) for user in filter_users(users, data['text'])]
else:
result = [user_parser(user) for user in users]
return json.dumps(result), 200, {'Content-Type': 'application/json'}
def user_parser(user):
return {'id':user['username'],
'first':user['first'],
'last':user['last'],
'role':user['roles'][0] if len(user['roles']) else None,
'email':user['email'],
'groups':user['keycloak_groups']}
def filter_users(users, text):
return [user for user in users
if text in user['username'] or
text in user['first'] or
text in user['last'] or
text in user['email']]

View File

@ -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")

View File

@ -0,0 +1,25 @@
#!flask/bin/python
# coding=utf-8
from functools import wraps
from flask import request, redirect, url_for
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):
remote_addr=request.headers['X-Forwarded-For'].split(',')[0] if 'X-Forwarded-For' in request.headers else request.remote_addr.split(',')[0]
## Now only checks if it is wordpress container,
## but we should check if it is internal net and not haproxy
if socket.gethostbyname('isard-apps-wordpress') == remote_addr: return fn(*args, **kwargs)
logout_user()
return redirect(url_for('login'))
return decorated_view

View File

@ -1,4 +1,4 @@
cd saml_certs
cd /admin/saml_certs
C=CA
L=Barcelona
O=localdomain
@ -6,5 +6,5 @@ CN_CA=$O
CN_HOST=*.$O
OU=$O
openssl req -nodes -new -x509 -keyout private.key -out public.cert -subj "/C=$C/L=$L/O=$O/CN=$CN_CA" -days 3650
cd ..
cd /admin
echo "Now run the python nextcloud and wordpress scripts"

View File

@ -0,0 +1,128 @@
#!/usr/bin/env python
import time ,os
from datetime import datetime, timedelta
import logging as log
import traceback
import yaml, json
from pprint import pprint
from jinja2 import Environment, FileSystemLoader
from keycloak import KeycloakAdmin
from postgres import Postgres
class KeycloakClient():
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
https://github.com/marcospereirampj/python-keycloak
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
"""
def __init__(self,
url="http://isard-sso-keycloak:8080/auth/",
username=os.environ['KEYCLOAK_USER'],
password=os.environ['KEYCLOAK_PASSWORD'],
realm='master',
verify=True):
self.url=url
self.username=username
self.password=password
self.realm=realm
self.verify=verify
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD'])
def connect(self):
self.keycloak_admin = KeycloakAdmin(server_url=self.url,
username=self.username,
password=self.password,
realm_name=self.realm,
verify=self.verify)
def update_pwds(self):
self.get_users()
def get_users(self):
self.connect()
users=self.get_users_with_groups_and_roles()
userupdate=[]
for u in users:
if u['username'] not in ['admin','ddadmin'] and not u['username'].startswith('system_'):
print('Generating password for user '+u['username'])
userupdate.append({'id':u['id'],
'username':u['username'],
'password': diceware.get_passphrase(options=options)})
with open("user_temp_passwd.csv","w") as csv:
for user in userupdate:
csv.write("%s,%s,%s\n"%(user['id'],user['username'],user['password']))
for u in userupdate:
print('Updating keycloak password for user '+u['username'])
self.update_user_pwd(u['id'],u['password'])
def update_user_pwd_temporary(self,temporary=False):
payload={"credentials":[{"temporary":temporary}]}
self.connect()
self.keycloak_admin.update_user( user_id, payload)
def get_users_with_groups_and_roles(self):
q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota
,json_agg(g."name") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2
,json_agg(r.name) as role
from user_entity as u
left join user_attribute as ua on ua.user_id=u.id and ua.name = 'quota'
left join user_group_membership as ugm on ugm.user_id = u.id
left join keycloak_group as g on g.id = ugm.group_id
left join keycloak_group as g_parent on g.parent_group = g_parent.id
left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id
left join user_role_mapping as rm on rm.user_id = u.id
left join keycloak_role as r on r.id = rm.role_id
group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value
order by u.username"""
# q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota, g.id, g.path, g.name,
# --,json_agg(g."name") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2
# --,json_agg(r.name) as role
# from user_entity as u
# left join user_attribute as ua on ua.user_id=u.id and ua.name = 'quota'
# left join user_group_membership as ugm on ugm.user_id = u.id
# left join keycloak_group as g on g.id = ugm.group_id
# --left join keycloak_group as g_parent on g.parent_group = g_parent.id
# --left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id
# left join user_role_mapping as rm on rm.user_id = u.id
# left join keycloak_role as r on r.id = rm.role_id
# --group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value
# order by u.username"""
# q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota
# ,json_agg(g."name") as group_name,json_agg(g."id") as group_id,json_agg(g."path") as group_path
# ,json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2
# ,json_agg(r.name) as role
# from user_entity as u
# left join user_attribute as ua on ua.user_id=u.id and ua.name = 'quota'
# left join user_group_membership as ugm on ugm.user_id = u.id
# left join keycloak_group as g on g.id = ugm.group_id
# left join keycloak_group as g_parent on g.parent_group = g_parent.id
# left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id
# left join user_role_mapping as rm on rm.user_id = u.id
# left join keycloak_role as r on r.id = rm.role_id
# group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value
# order by u.username"""
(headers,users)=self.keycloak_pg.select_with_headers(q)
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
return list_dict_users
k=KeycloakClient()
k.update_user_pwd_temporary()

View File

@ -0,0 +1,119 @@
#!/usr/bin/env python
import time ,os
from datetime import datetime, timedelta
import logging as log
import traceback
import yaml, json
from pprint import pprint
from jinja2 import Environment, FileSystemLoader
from keycloak import KeycloakAdmin
from postgres import Postgres
class KeycloakClient():
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
https://github.com/marcospereirampj/python-keycloak
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
"""
def __init__(self,
url="http://isard-sso-keycloak:8080/auth/",
username=os.environ['KEYCLOAK_USER'],
password=os.environ['KEYCLOAK_PASSWORD'],
realm='master',
verify=True):
self.url=url
self.username=username
self.password=password
self.realm=realm
self.verify=verify
self.keycloak_pg=Postgres('isard-apps-postgresql','keycloak',os.environ['KEYCLOAK_DB_USER'],os.environ['KEYCLOAK_DB_PASSWORD'])
def connect(self):
self.keycloak_admin = KeycloakAdmin(server_url=self.url,
username=self.username,
password=self.password,
realm_name=self.realm,
verify=self.verify)
def run(self):
self.get_users()
def get_users(self):
self.connect()
users=self.get_users_with_groups_and_roles()
for u in users:
if u['username']=='proves-meves': pprint(u)
print('Updating keycloak temporary for user '+u['username'])
self.update_user_pwd_temporary(u['id'])
def update_user_pwd_temporary(self,user_id,temporary=False):
payload={"credentials":[{"temporary":temporary}],
"requiredActions": []}
self.connect()
self.keycloak_admin.update_user( user_id, payload)
def get_users_with_groups_and_roles(self):
q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota
,json_agg(g."name") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2
,json_agg(r.name) as role
from user_entity as u
left join user_attribute as ua on ua.user_id=u.id and ua.name = 'quota'
left join user_group_membership as ugm on ugm.user_id = u.id
left join keycloak_group as g on g.id = ugm.group_id
left join keycloak_group as g_parent on g.parent_group = g_parent.id
left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id
left join user_role_mapping as rm on rm.user_id = u.id
left join keycloak_role as r on r.id = rm.role_id
group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value
order by u.username"""
# q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota, g.id, g.path, g.name,
# --,json_agg(g."name") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2
# --,json_agg(r.name) as role
# from user_entity as u
# left join user_attribute as ua on ua.user_id=u.id and ua.name = 'quota'
# left join user_group_membership as ugm on ugm.user_id = u.id
# left join keycloak_group as g on g.id = ugm.group_id
# --left join keycloak_group as g_parent on g.parent_group = g_parent.id
# --left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id
# left join user_role_mapping as rm on rm.user_id = u.id
# left join keycloak_role as r on r.id = rm.role_id
# --group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value
# order by u.username"""
# q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, ua.value as quota
# ,json_agg(g."name") as group_name,json_agg(g."id") as group_id,json_agg(g."path") as group_path
# ,json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2
# ,json_agg(r.name) as role
# from user_entity as u
# left join user_attribute as ua on ua.user_id=u.id and ua.name = 'quota'
# left join user_group_membership as ugm on ugm.user_id = u.id
# left join keycloak_group as g on g.id = ugm.group_id
# left join keycloak_group as g_parent on g.parent_group = g_parent.id
# left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id
# left join user_role_mapping as rm on rm.user_id = u.id
# left join keycloak_role as r on r.id = rm.role_id
# group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, ua.value
# order by u.username"""
(headers,users)=self.keycloak_pg.select_with_headers(q)
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
users_with_lists = [list(l[:-4])+([[]] if l[-4] == [None] else [list(set(l[-4]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-3]))]) +\
([[]] if l[-3] == [None] else [list(set(l[-2]))]) +\
([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
return list_dict_users
k=KeycloakClient()
k.run()

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}
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

View File

@ -166,6 +166,10 @@ backend be_wp
acl existing-x-forwarded-proto req.hdr(X-Forwarded-Proto) -m found
http-request add-header X-Forwarded-Host %[req.hdr(Host)] unless existing-x-forwarded-host
http-request add-header X-Forwarded-Proto https unless existing-x-forwarded-proto
http-request set-header X-SSL %[ssl_fc]
#reqadd X-Forwarded-Proto:\ https
http-request set-header X-Forwarded-Proto https
server wp isard-apps-wordpress:80 check port 80 inter 5s rise 2 fall 10 resolvers mydns init-addr none