parent child groups

root 2021-05-31 15:28:06 +02:00
parent defd0b51fe
commit c02ef731af
17 changed files with 277 additions and 29 deletions

View File

@ -16,4 +16,11 @@ zope.event==4.4
zope.interface==5.1.0 zope.interface==5.1.0
psycopg2==2.8.6 psycopg2==2.8.6
Flask-SocketIO==2.8.6 Flask-SocketIO==2.8.6
mysql-connector-python==8.0.25 mysql-connector-python==8.0.25
#Flask-SocketIO==2.8.6
#gevent==1.4.0
#greenlet==0.4.15
python-engineio==3.8.1
python-socketio==4.1.0

View File

@ -32,6 +32,8 @@ Postup()
from admin.lib.admin import Admin from admin.lib.admin import Admin
app.admin=Admin() app.admin=Admin()
app.ready=False
''' '''
Debug should be removed on production! Debug should be removed on production!
''' '''
@ -84,3 +86,4 @@ from .views import AvatarViews

View File

@ -30,6 +30,19 @@ class Admin():
sleep(2) sleep(2)
log.warning('Keycloak connected.') log.warning('Keycloak connected.')
# try:
# self.keycloak.add_group('parent1')
# except:
# pass
# try:
# self.keycloak.add_group('child1','/parent1')
# except:
# pass
# try:
# self.keycloak.add_group('child2','/parent1')
# except:
# pass
ready=False ready=False
while not ready: while not ready:
try: try:
@ -306,6 +319,7 @@ class Admin():
theuser={**moodle_exists[0], **theuser} theuser={**moodle_exists[0], **theuser}
theuser['moodle']=True theuser['moodle']=True
theuser['moodle_groups']=moodle_exists[0]['groups'] theuser['moodle_groups']=moodle_exists[0]['groups']
theuser['moodle_id']=moodle_exists[0]['id']
else: else:
theuser['moodle']=False theuser['moodle']=False
theuser['moodle_groups']=[] theuser['moodle_groups']=[]
@ -315,6 +329,7 @@ class Admin():
theuser={**nextcloud_exists[0], **theuser} theuser={**nextcloud_exists[0], **theuser}
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']
else: else:
theuser['nextcloud']=False theuser['nextcloud']=False
theuser['nextcloud_groups']=[] theuser['nextcloud_groups']=[]
@ -406,15 +421,18 @@ class Admin():
for g in data['data']['groups']: for g in data['data']['groups']:
# for m in data['data']['d_members']: # for m in data['data']['d_members']:
groups.append({'provider':data['provider'], # data['provider']
groups.append({'provider':'external',
"id": g['id'], "id": g['id'],
"mailid": g['email'].split('@')[0],
"name": g['name'], "name": g['name'],
"description": g['description']}) "description": g['description']})
self.external['groups']=groups self.external['groups']=groups
users=[] users=[]
for u in data['data']['users']: for u in data['data']['users']:
users.append({'provider':data['provider'], # data['provider']
users.append({'provider':'external',
'id':u['id'], 'id':u['id'],
'email': u['primaryEmail'], 'email': u['primaryEmail'],
'first': u['name']['givenName'], 'first': u['name']['givenName'],
@ -424,6 +442,12 @@ class Admin():
'roles':[]}) 'roles':[]})
self.external['users']=users self.external['users']=users
## Add groups to users (now they only have their orgUnitPath)
for g in self.external['groups']:
for useringroup in data['data']['d_members'][g['mailid']]:
for u in self.external['users']:
if u['id'] == useringroup['id']:
u['groups']=u['groups']+[g['name']]
return True return True
def sync_external(self): def sync_external(self):
@ -468,7 +492,41 @@ class Admin():
log.error(traceback.format_exc()) log.error(traceback.format_exc())
log.warning('Could not remove user: '+u['username']) log.warning('Could not remove user: '+u['username'])
def delete_keycloak_groups(self): def delete_nextcloud_users(self):
for u in self.internal['users']:
if u['nextcloud'] and not u['keycloak']:
if u['roles'] and 'admin' in u['roles']: continue
log.info('Removing nextcloud user: '+u['username'])
try:
self.nextcloud.delete_user(u['nextcloud_id'])
socketio.emit('update',
json.dumps({'status':True,'item':'user','action':'delete','itemdata':u}),
namespace='/isard-sso-admin/sio',
room='admin')
except:
log.error(traceback.format_exc())
log.warning('Could not remove user: '+u['username'])
def delete_moodle_users(self):
userids=[]
usernames=[]
for u in self.internal['users']:
if u['moodle'] and not u['keycloak']:
userids.append(u['moodle_id'])
usernames.append(u['username'])
log.warning('Removing moodle users: '+','.join(usernames))
try:
self.moodle.delete_users(userids)
socketio.emit('update',
json.dumps({'status':True,'item':'user','action':'delete','itemdata':u}),
namespace='/isard-sso-admin/sio',
room='admin')
except:
log.error(traceback.format_exc())
log.warning('Could not remove users: '+','.join(usernames))
def delete_moodle_groups(self):
for g in self.internal['groups']: for g in self.internal['groups']:
if not g['keycloak']: continue if not g['keycloak']: continue
# Do not remove admin group. It should not exist in keycloak, only in nextcloud # Do not remove admin group. It should not exist in keycloak, only in nextcloud
@ -478,4 +536,11 @@ class Admin():
self.keycloak.delete_group(g['id']) self.keycloak.delete_group(g['id'])
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
log.warning('Could not remove group: '+g['name']) log.warning('Could not remove group: '+g['name'])
def external_roleassign(self,data):
for newuserid in data['ids']:
for externaluser in self.external['users']:
if externaluser['id'] == newuserid:
externaluser['roles']=[data['action']]
return True

View File

@ -164,6 +164,10 @@ class KeycloakClient():
def add_group(self,name,parent=None): def add_group(self,name,parent=None):
self.connect() self.connect()
if parent is not None: parent=self.get_group(parent)['id']
print(name)
print(parent)
print('DONE')
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):

View File

@ -2,7 +2,7 @@ from requests import get, post
from admin import app from admin import app
import logging as log import logging as log
from pprint import pprint
from .postgres import Postgres from .postgres import Postgres
@ -72,6 +72,14 @@ class Moodle():
user = self.call('core_user_create_users', users=data) user = self.call('core_user_create_users', users=data)
return user return user
def delete_user(self, user_id):
user = self.call('core_user_delete_users', userids=[user_id])
return user
def delete_users(self, userids):
user = self.call('core_user_delete_users', userids=userids)
return user
def get_user_by(self, key, value): def get_user_by(self, key, value):
criteria = [{'key': key, 'value': value}] criteria = [{'key': key, 'value': value}]
user = self.call('core_user_get_users', criteria=criteria) user = self.call('core_user_get_users', criteria=criteria)

View File

@ -128,7 +128,7 @@ class Nextcloud():
def add_user(self,userid,userpassword,quota=False,group=False,email='',displayname=''): def add_user(self,userid,userpassword,quota=False,group=False,email='',displayname=''):
data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname} data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname}
if not group: del data['group'] if not group: del data['groups[]']
if not quota: del data['quota'] if not quota: del data['quota']
# if group: # if group:
# data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname} # data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname}

View File

@ -54,6 +54,60 @@ $(document).ready(function() {
} }
}); });
$('#action_role').on('change', function () {
action=$(this).val();
names=''
ids=[]
if(users_table.rows('.active').data().length){
$.each(users_table.rows('.active').data(),function(key, value){
names+=value['name']+'\n';
ids.push(value['id']);
});
var text = "You are about to assign role "+action+" these users:\n\n "+names
}else{
$.each(users_table.rows({filter: 'applied'}).data(),function(key, value){
ids.push(value['id']);
});
var text = "You are about to assign role "+action+" "+users_table.rows({filter: 'applied'}).data().length+" users!\n All the users in list!"
}
new PNotify({
title: 'Role assignment!',
text: 'You will asign the role '+action,
hide: false,
opacity: 0.9,
confirm: {
confirm: true
},
buttons: {
closer: false,
sticker: false
},
history: {
history: false
},
addclass: 'pnotify-center'
}).get().on('pnotify.confirm', function() {
$.ajax({
type: "PUT",
url:"/isard-sso-admin/external/roles",
data: JSON.stringify({'ids':ids,'action':action}),
success: function(data)
{
console.log('SUCCESS')
users_table.ajax.reload();
groups_table.ajax.reload();
},
error: function(data)
{
alert('Something went wrong on our side...')
}
});
}).on('pnotify.cancel', function() {
$('#action_role option[value="none"]').prop("selected",true);
});
} );
//DataTable Main renderer //DataTable Main renderer
var users_table = $('#users').DataTable({ var users_table = $('#users').DataTable({
"ajax": { "ajax": {
@ -74,8 +128,8 @@ $(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": "provider", "width": "10px" }, // { "data": "provider", "width": "10px" },
{ "data": "id", "width": "10px" }, // { "data": "id", "width": "10px" },
{ "data": "username", "width": "10px"}, { "data": "username", "width": "10px"},
{ "data": "first", "width": "10px"}, { "data": "first", "width": "10px"},
{ "data": "last", "width": "10px"}, { "data": "last", "width": "10px"},
@ -84,9 +138,15 @@ $(document).ready(function() {
{ "data": "roles", "width": "10px"}, { "data": "roles", "width": "10px"},
], ],
"order": [[3, 'asc']], "order": [[3, 'asc']],
"columnDefs": [ ] "columnDefs": [ {
"targets": 5,
"render": function ( data, type, full, meta ) {
return "<li>" + full.groups.join("</li><li>") + "</li>"
}}
]
} ); } );
var groups_table = $('#groups').DataTable({ var groups_table = $('#groups').DataTable({
"ajax": { "ajax": {
"url": "/isard-sso-admin/external_groups_list", "url": "/isard-sso-admin/external_groups_list",
@ -106,8 +166,8 @@ $(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": "id", "width": "10px" }, // { "data": "id", "width": "10px" },
{ "data": "provider", "width": "10px" }, // { "data": "provider", "width": "10px" },
{ "data": "name", "width": "10px" }, { "data": "name", "width": "10px" },
{ "data": "description", "width": "10px"}, { "data": "description", "width": "10px"},
], ],

View File

@ -1,13 +1,13 @@
// SocketIO // SocketIO
socket = io.connect(location.protocol+'//' + document.domain + ':' + location.port+'/isard-sso-admin/sio'); socket = io.connect(location.protocol+'//' + document.domain +'/isard-sso-admin/sio');
console.log(location.protocol+'//' + document.domain +'/isard-sso-admin/sio')
socket.on('connect', function() { socket.on('connect', function() {
connection_done(); // connection_done();
console.log('Listening status socket'); console.log('Listening status socket');
}); });
socket.on('connect_error', function(data) { socket.on('connect_error', function(data) {
connection_lost(); // connection_lost();
}); });
socket.on('update', function(data) { socket.on('update', function(data) {

View File

@ -112,6 +112,41 @@ $(document).ready(function() {
}); });
}); });
$('.btn-delete_nextcloud').on('click', function () {
$.ajax({
type: "DELETE",
url:"/isard-sso-admin/users/nextcloud",
success: function(data)
{
console.log('SUCCESS')
// $("#modalImport").modal('hide');
// users_table.ajax.reload();
// groups_table.ajax.reload();
},
error: function(data)
{
alert('Something went wrong on our side...')
}
});
});
$('.btn-delete_moodle').on('click', function () {
$.ajax({
type: "DELETE",
url:"/isard-sso-admin/users/moodle",
success: function(data)
{
console.log('SUCCESS')
// $("#modalImport").modal('hide');
// users_table.ajax.reload();
// groups_table.ajax.reload();
},
error: function(data)
{
alert('Something went wrong on our side...')
}
});
});
$('.btn-sync_to_moodle').on('click', function () { $('.btn-sync_to_moodle').on('click', function () {
$.ajax({ $.ajax({
type: "POST", type: "POST",

View File

@ -14,6 +14,19 @@
<div class="x_title"> <div class="x_title">
<h3><i class="fa fa-desktop"></i> External</h3> <h3><i class="fa fa-desktop"></i> External</h3>
<ul class="nav navbar-right panel_toolbox"> <ul class="nav navbar-right panel_toolbox">
<li>
<div class="item form-group">
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="action_role">Assign role: <span class="required">*</span></label>
<div class="col-md-6 col-sm-6 col-xs-12">
<select id="action_role" name="action_role" class="form-control action" required>
<option value=''>Select role</option>
<option value='manager'>Manager</option>
<option value='teacher'>Teacher</option>
<option value='student'>Student</option>
</select>
</div>
</div>
</li>
<li> <li>
<a class="btn-sync"><span style="color: #5499c7; "><i class="fa fa-rocket"></i> Sync to system</span></a> <a class="btn-sync"><span style="color: #5499c7; "><i class="fa fa-rocket"></i> Sync to system</span></a>
<a class="btn-upload"><span style="color: #5499c7; "><i class="fa fa-upload"></i> Upload</span></a> <a class="btn-upload"><span style="color: #5499c7; "><i class="fa fa-upload"></i> Upload</span></a>
@ -28,8 +41,6 @@
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
<th>Provider</th>
<th>Id</th>
<th>Username</th> <th>Username</th>
<th>First</th> <th>First</th>
<th>Last</th> <th>Last</th>
@ -48,8 +59,6 @@
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
<th>Id</th>
<th>Provider</th>
<th>Name</th> <th>Name</th>
<th>Description</th> <th>Description</th>
</tr> </tr>

View File

@ -22,12 +22,12 @@
<input id="id" hidden/> <input id="id" hidden/>
--> -->
<div class="item form-group"> <!-- <div class="item form-group">
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="provider">Provider name: <span class="required">*</span></label> <label class="control-label col-md-3 col-sm-3 col-xs-12" for="provider">Provider name: <span class="required">*</span></label>
<div class="col-md-6 col-sm-6 col-xs-12"> <div class="col-md-6 col-sm-6 col-xs-12">
<input id="provider" name="provider" placeholder="" type="text" style="width:100%"> <input id="provider" name="provider" placeholder="" type="text" style="width:100%">
</div> </div>
</div> </div> -->
<div class="item form-group"> <div class="item form-group">
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="format">Format: <span class="required">*</span></label> <label class="control-label col-md-3 col-sm-3 col-xs-12" for="format">Format: <span class="required">*</span></label>
<div class="col-md-6 col-sm-6 col-xs-12"> <div class="col-md-6 col-sm-6 col-xs-12">
@ -46,7 +46,7 @@
</div> </div>
<div class="x_panela" id="bulkusers-quota" style="padding: 5px;"> <!-- <div class="x_panela" id="bulkusers-quota" style="padding: 5px;">
<p style="font-size: 18px;margin-bottom:0px;">Map User keys</p> <p style="font-size: 18px;margin-bottom:0px;">Map User keys</p>
<div class="item form-group"> <div class="item form-group">
@ -114,7 +114,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div> -->
</div> </div>
<!-- Modal Footer --> <!-- Modal Footer -->

View File

@ -30,6 +30,12 @@
<button class="btn btn-danger btn-xs btn-delete_keycloak"> <button class="btn btn-danger btn-xs btn-delete_keycloak">
<i class="fa fa-trash"></i> Delete all keycloak <i class="fa fa-trash"></i> Delete all keycloak
</button> </button>
<button class="btn btn-danger btn-xs btn-delete_nextcloud">
<i class="fa fa-trash"></i> Delete missing keycloak in nextcloud
</button>
<button class="btn btn-danger btn-xs btn-delete_moodle">
<i class="fa fa-trash"></i> Delete missing keycloak in moodle
</button>
<table id="users" class="table" width="100%"> <table id="users" class="table" width="100%">
<thead> <thead>
<tr> <tr>
@ -68,5 +74,6 @@
<!-- Switchery --> <!-- Switchery -->
<script src="/isard-sso-admin/vendors/switchery/dist/switchery.min.js"></script> <script src="/isard-sso-admin/vendors/switchery/dist/switchery.min.js"></script>
<!-- Desktops sse & modals --> <!-- Desktops sse & modals -->
<script src="/isard-sso-admin/static/js/users.js"></script> <script src="/isard-sso-admin/static/js/users.js"></script>
<script src="/isard-sso-admin/static/js/status_socket.js"></script>
{% endblock %} {% endblock %}

View File

@ -23,6 +23,10 @@ def users(provider=False):
if request.method == 'DELETE': if request.method == 'DELETE':
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':
return json.dumps(app.admin.delete_nextcloud_users()), 200, {'Content-Type': 'application/json'}
if provider == 'moodle':
return json.dumps(app.admin.delete_moodle_users()), 200, {'Content-Type': 'application/json'}
if request.method == 'POST': if request.method == 'POST':
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'}
@ -79,4 +83,10 @@ def external_users_list():
@app.route('/isard-sso-admin/external_groups_list') @app.route('/isard-sso-admin/external_groups_list')
# @login_required # @login_required
def external_groups_list(): def external_groups_list():
return json.dumps(app.admin.get_external_groups()), 200, {'Content-Type': 'application/json'} return json.dumps(app.admin.get_external_groups()), 200, {'Content-Type': 'application/json'}
@app.route('/isard-sso-admin/external/roles', methods=['PUT'])
# @login_required
def external_roles():
if request.method == 'PUT':
return json.dumps(app.admin.external_roleassign(request.get_json(force=True))), 200, {'Content-Type': 'application/json'}

View File

@ -0,0 +1,16 @@
from flask_socketio import SocketIO, emit, join_room, leave_room, \
close_room, rooms, disconnect, send
from admin import app
socketio = SocketIO(app)
@socketio.on('connect', namespace='/isard-sso-admin/sio')
def socketio_connect():
join_room('admin')
socketio.emit('update',
json.dumps('Joined'),
namespace='/isard-sso-admin/sio',
room='admin')
@socketio.on('disconnect', namespace='/isard-sso-admin/sio')
def socketio_domains_disconnect():
None

View File

@ -134,6 +134,7 @@ class MoodleSaml():
'duallogin': '1', 'duallogin': '1',
'idpattr': 'username', 'idpattr': 'username',
'autocreate': '1', 'autocreate': '1',
'anyauth': '1',
'saml_role_siteadmin_map': 'admin', 'saml_role_siteadmin_map': 'admin',
'saml_role_coursecreator_map': 'teacher', 'saml_role_coursecreator_map': 'teacher',
'saml_role_manager_map': 'manager', 'saml_role_manager_map': 'manager',

View File

@ -3,8 +3,26 @@
from gevent import monkey from gevent import monkey
monkey.patch_all() monkey.patch_all()
from flask_socketio import SocketIO, emit, join_room, leave_room, \
close_room, rooms, disconnect, send
import json
from admin import app from admin import app
socketio = SocketIO(app)
socketio.init_app(app, cors_allowed_origins="*")
# from .admin.views.Socketio import *
@socketio.on('connect', namespace='/isard-sso-admin/sio')
def socketio_connect():
join_room('admin')
socketio.emit('update',
json.dumps('Joined'),
namespace='/isard-sso-admin/sio',
room='admin')
@socketio.on('disconnect', namespace='/isard-sso-admin/sio')
def socketio_domains_disconnect():
None
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=9000, debug=False) #, logger=logger, engineio_logger=engineio_logger) socketio.run(app,host='0.0.0.0', port=9000, debug=False, cors_allowed_origins="*") #, logger=logger, engineio_logger=engineio_logger)

View File

@ -34,6 +34,9 @@ frontend website
http-request set-header ssl_client_cert -----BEGIN\ CERTIFICATE-----\ %[ssl_c_der,base64]\ -----END\ CERTIFICATE-----\ if { ssl_fc_has_crt } http-request set-header ssl_client_cert -----BEGIN\ CERTIFICATE-----\ %[ssl_c_der,base64]\ -----END\ CERTIFICATE-----\ if { ssl_fc_has_crt }
bind :443 ssl crt /certs/chain.pem bind :443 ssl crt /certs/chain.pem
acl is_upgrade hdr(Connection) -i upgrade
acl is_websocket hdr(Upgrade) -i websocket
acl is_nextcloud hdr_beg(host) nextcloud. acl is_nextcloud hdr_beg(host) nextcloud.
acl is_moodle hdr_beg(host) moodle. acl is_moodle hdr_beg(host) moodle.
acl is_jitsi hdr_beg(host) jitsi. acl is_jitsi hdr_beg(host) jitsi.
@ -51,11 +54,13 @@ frontend website
use_backend be_oof if is_oof use_backend be_oof if is_oof
use_backend be_wp if is_wp use_backend be_wp if is_wp
use_backend be_etherpad if is_pad use_backend be_etherpad if is_pad
use_backend be_admin if is_sso { path_beg /socket.io }
use_backend be_adminer if is_sso { path_beg /isard-sso-adminer } use_backend be_adminer if is_sso { path_beg /isard-sso-adminer }
use_backend be_admin if is_sso { path_beg /isard-sso-admin } use_backend be_admin if is_sso { path_beg /isard-sso-admin }
use_backend be_sso if is_sso use_backend be_sso if is_sso
use_backend be_ipa if is_ipa use_backend be_ipa if is_ipa
use_backend be_api if is_api use_backend be_api if is_api
# default_backend be_sso # default_backend be_sso