added admin against SPs and avatars on api

root 2021-05-23 01:12:45 +02:00
parent 7edd696d9a
commit e482c77bff
41 changed files with 518 additions and 158 deletions

View File

@ -12,7 +12,7 @@ COPY admin/docker/requirements.pip3 /requirements.pip3
RUN pip3 install --no-cache-dir -r requirements.pip3 RUN pip3 install --no-cache-dir -r requirements.pip3
RUN apk del .build_deps RUN apk del .build_deps
RUN apk add curl RUN apk add curl py3-yaml
# SSH configuration # SSH configuration
ARG SSH_ROOT_PWD ARG SSH_ROOT_PWD

View File

@ -20,13 +20,14 @@ app.secret_key = "Change this key!//\xf7\x83\xbe\x17\xfa\xa3zT\n\\]m\xa6\x8bF\xd
print('Starting isard-sso api...') print('Starting isard-sso api...')
from api.lib.load_config import loadConfig from admin.lib.load_config import loadConfig
try: try:
loadConfig(app) loadConfig(app)
except: except:
print('Could not get environment variables...') print('Could not get environment variables...')
from admin.lib.admin import Admin
app.admin=Admin()
''' '''
Debug should be removed on production! Debug should be removed on production!

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,58 @@
from .keycloak import Keycloak
from .moodle import Moodle
from .nextcloud import Nextcloud
from pprint import pprint
class Admin():
def __init__(self):
URL="http://isard-sso-keycloak:8080/auth/"
USERNAME='admin'
PASSWORD='keycloakkeycloak'
REALM="master"
VERIFY_CERT=True
self.keycloak=Keycloak(URL,USERNAME,PASSWORD,REALM,VERIFY_CERT)
KEY = "a1d8b3248e1a3e3b839645503df0e3a5"
URL = "https://moodle.santantoni.duckdns.org"
ENDPOINT="/webservice/rest/server.php"
self.moodle=Moodle(KEY,URL,ENDPOINT)
URL="https://nextcloud.santantoni.duckdns.org"
USERNAME='admin'
PASSWORD='N3xtcl0ud'
VERIFY_CERT=False
self.nextcloud=Nextcloud(URL,USERNAME,PASSWORD,VERIFY_CERT)
pprint(self.get_moodle_users())
pprint(self.get_keycloak_users())
pprint(self.get_nextcloud_users())
def get_moodle_users(self):
users = self.moodle.get_user_by('email','%%')['users']
return [{"id":u['id'],
"username":u['username'],
"first": u['firstname'],
"last": u['lastname'],
"email": u['email']}
for u in users]
def get_keycloak_users(self):
users = self.keycloak.get_users()
return [{"id":u['id'],
"username":u['username'],
"first": u.get('firstName',None),
"last": u.get('lastName',None),
"email": u.get('email','')}
for u in users]
def get_nextcloud_users(self):
users = self.nextcloud.get_users_list()
users_list=[]
for user in users:
u=self.nextcloud.get_user(user)
users_list.append({"id":u['id'],
"username":u['id'],
"first": u['displayname'],
"last": None,
"email": u['email']})
return users_list

View File

@ -0,0 +1,65 @@
#!/usr/bin/env python
# coding=utf-8
import time
from admin import app as application
from datetime import datetime, timedelta
import pprint
import logging
import traceback
import yaml, json
from jinja2 import Environment, FileSystemLoader
from keycloak import KeycloakAdmin
class Keycloak():
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
https://github.com/marcospereirampj/python-keycloak
"""
def __init__(self,url,username,password,realm,verify=True):
self.keycloak_admin = KeycloakAdmin(server_url=url,
username=username,
password=password,
realm_name=realm,
verify=verify)
## USERS
def get_user_id(self,username):
return self.keycloak_admin.get_user_id(username)
def get_users(self):
return self.keycloak_admin.get_users({})
def add_user(self,username,first,last,email,password):
# Returns user id
return self.keycloak_admin.create_user({"email": email,
"username": username,
"enabled": True,
"firstName": first,
"lastName": last,
"credentials":[{"type":"password",
"value":password,
"temporary":False}]})
def add_user_role(self,client_id,user_id,role_id,role_name):
return self.keycloak_admin.assign_client_role(client_id="client_id", user_id="user_id", role_id="role_id", role_name="test")
def delete_user(self,userid):
return self.keycloak_admin.delete_user(user_id=userid)
## SYSTEM
def get_server_info(self):
return self.keycloak_admin.get_server_info()
def get_server_clients(self):
return self.keycloak_admin.get_clients()
## CLIENTS
def get_client_roles(self,client_id):
return self.keycloak_admin.get_client_roles(client_id=client_id)
# def add_client_role(self,client_id,roleName):
# return self.keycloak_admin.create_client_role(client_id=client_id, {'name': roleName, 'clientRole': True})

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf-8 # coding=utf-8
from api import app from admin import app
import os, sys import os, sys
import logging as log import logging as log

View File

@ -0,0 +1,73 @@
from requests import get, post
# Module variables to connect to moodle api
class Moodle():
"""https://github.com/mrcinv/moodle_api.py
https://docs.moodle.org/dev/Web_service_API_functions
https://docs.moodle.org/311/en/Using_web_services
"""
def __init__(self, key, url, endpoint):
self.key = key
self.url = url
self.endpoint = endpoint
def rest_api_parameters(self, in_args, prefix='', out_dict=None):
"""Transform dictionary/array structure to a flat dictionary, with key names
defining the structure.
Example usage:
>>> rest_api_parameters({'courses':[{'id':1,'name': 'course1'}]})
{'courses[0][id]':1,
'courses[0][name]':'course1'}
"""
if out_dict==None:
out_dict = {}
if not type(in_args) in (list,dict):
out_dict[prefix] = in_args
return out_dict
if prefix == '':
prefix = prefix + '{0}'
else:
prefix = prefix + '[{0}]'
if type(in_args)==list:
for idx, item in enumerate(in_args):
self.rest_api_parameters(item, prefix.format(idx), out_dict)
elif type(in_args)==dict:
for key, item in in_args.items():
self.rest_api_parameters(item, prefix.format(key), out_dict)
return out_dict
def call(self, fname, **kwargs):
"""Calls moodle API function with function name fname and keyword arguments.
Example:
>>> call_mdl_function('core_course_update_courses',
courses = [{'id': 1, 'fullname': 'My favorite course'}])
"""
parameters = self.rest_api_parameters(kwargs)
parameters.update({"wstoken": self.key, 'moodlewsrestformat': 'json', "wsfunction": fname})
response = post(self.url+self.endpoint, parameters, verify=False)
response = response.json()
if type(response) == dict and response.get('exception'):
raise SystemError("Error calling Moodle API\n", 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
def get_user_by(self, key, value):
criteria = [{'key': key, 'value': value}]
user = self.call('core_user_get_users', criteria=criteria)
return user
def enroll_user_to_course(self, user_id, course_id, role_id=5):
# 5 is student
data = [{'roleid': role_id, 'userid': user_id, 'courseid': course_id}]
enrolment = self.call('enrol_manual_enrol_users', enrolments=data)
return enrolment
def get_quiz_attempt(self, quiz_id, user_id):
attempts = self.call('mod_quiz_get_user_attempts', quizid=quiz_id, userid=user_id)
return attempts

View File

@ -0,0 +1,253 @@
#!/usr/bin/env python
# coding=utf-8
#from ..lib.log import *
import time,requests,json,pprint
import traceback
import logging as log
from .nextcloud_exc import *
class Nextcloud():
def __init__(self,url,username,password,verify):
self.verify_cert=verify
self.apiurl=url+'/ocs/v1.php/cloud/'
self.shareurl=url+'/ocs/v2.php/apps/files_sharing/api/v1/'
self.davurl=url+'/remote.php/dav/files/'
self.auth=(username,password)
self.user=username
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
## At least the ProviderSslError is not being catched or not raised correctly
except requests.exceptions.HTTPError as errh:
raise ProviderConnError
except requests.exceptions.Timeout as errt:
raise ProviderConnTimeout
except requests.exceptions.SSLError as err:
raise ProviderSslError
except requests.exceptions.ConnectionError as errc:
raise ProviderConnError
# except requests.exceptions.RequestException as err:
# raise ProviderError
except Exception as e:
if str(e) == 'an integer is required (got type bytes)':
raise ProviderConnError
raise ProviderError
def check_connection(self):
url = self.apiurl + "users/"+self.user+"?format=json"
try:
result = self._request('GET',url)
if json.loads(result)['ocs']['meta']['statuscode'] == 100: return True
raise ProviderError
except requests.exceptions.HTTPError as errh:
raise ProviderConnError
except requests.exceptions.ConnectionError as errc:
raise ProviderConnError
except requests.exceptions.Timeout as errt:
raise ProviderConnTimeout
except requests.exceptions.SSLError as err:
raise ProviderSslError
except requests.exceptions.RequestException as err:
raise ProviderError
except Exception as e:
if str(e) == 'an integer is required (got type bytes)':
raise ProviderConnError
raise ProviderError
def get_user(self,userid):
url = self.apiurl + "users/"+userid+"?format=json"
try:
result = json.loads(self._request('GET',url))
if result['ocs']['meta']['statuscode'] == 100: return result['ocs']['data']
raise ProviderItemNotExists
except:
log.error(traceback.format_exc())
raise
# 100 - successful
def get_users_list(self):
url = self.apiurl + "users?format=json"
try:
result = json.loads(self._request('GET',url))
if result['ocs']['meta']['statuscode'] == 100: return result['ocs']['data']['users']
log.error('Get Nextcloud provider users list error: '+str(result))
raise ProviderOpError
except:
log.error(traceback.format_exc())
raise
def add_user(self,userid,userpassword,quota,group='',email='',displayname=''):
data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname}
url = self.apiurl + "users?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
# 100 - successful
# 101 - invalid input data
# 102 - username already exists
# 103 - unknown error occurred whilst adding the user
# 104 - group does not exist
# 105 - insufficient privileges for group
# 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 delete_user(self,userid):
url = self.apiurl + "users/"+userid+"?format=json"
try:
result = json.loads(self._request('DELETE',url))
if result['ocs']['meta']['statuscode'] == 100: return True
if result['ocs']['meta']['statuscode'] == 101: raise ProviderUserNotExists
log.error(traceback.format_exc())
raise ProviderOpError
except:
log.error(traceback.format_exc())
raise
# 100 - successful
# 101 - failure
def enable_user(self,userid):
None
def disable_user(self,userid):
None
def exists_user_folder(self,userid,userpassword,folder='IsardVDI'):
auth=(userid,userpassword)
url = self.davurl + userid +"/" + folder+"?format=json"
headers = {
'Depth': '0',
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
}
try:
result = self._request('PROPFIND',url,auth=auth,headers=headers)
if '<d:status>HTTP/1.1 200 OK</d:status>' in result: return True
return False
except:
log.error(traceback.format_exc())
raise
def add_user_folder(self,userid,userpassword,folder='IsardVDI'):
auth=(userid,userpassword)
url = self.davurl + userid +"/" + folder+"?format=json"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
}
try:
result = self._request('MKCOL',url,auth=auth,headers=headers)
if result=='': return True
if '<s:message>The resource you tried to create already exists</s:message>' in result: raise ProviderItemExists
log.error(result.split('message>')[1].split('<')[0])
raise ProviderOpError
except:
log.error(traceback.format_exc())
raise
def exists_user_share_folder(self,userid,userpassword,folder='IsardVDI'):
auth=(userid,userpassword)
url = self.shareurl + "shares?format=json"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
}
try:
result = json.loads(self._request('GET', url, auth=auth, headers=headers))
if result['ocs']['meta']['statuscode']==200:
share=[s for s in result['ocs']['data'] if s['path'] == '/'+folder]
if len(share) >= 1:
# Should we delete all but the first (0) one?
return {'token': share[0]['token'],
'url': share[0]['url']}
raise ProviderItemNotExists
raise ProviderOpError
except:
log.error(traceback.format_exc())
raise
def add_user_share_folder(self,userid,userpassword,folder='IsardVDI'):
auth=(userid,userpassword)
data={'path':'/'+folder,'shareType':3}
url = self.shareurl + "shares?format=json"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
}
try:
result = json.loads(self._request('POST',url, data=data, auth=auth, headers=headers))
if result['ocs']['meta']['statuscode'] == 100 or result['ocs']['meta']['statuscode'] == 200:
return {'token': result['ocs']['data']['token'],
'url': result['ocs']['data']['url']}
log.error('Add user share folder error: '+result['ocs']['meta']['message'])
raise ProviderFolderNotExists
except:
log.error(traceback.format_exc())
raise
def get_group(self,userid):
None
def get_groups_list(self):
url = self.apiurl + "groups?format=json"
try:
result = json.loads(self._request('GET',url))
if result['ocs']['meta']['statuscode'] == 100: return [g for g in result['ocs']['data']['groups'] if '-' in g]
raise ProviderOpError
except:
log.error(traceback.format_exc())
raise
def add_group(self,groupid):
data={'groupid':groupid}
url = self.apiurl + "groups?format=json"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
}
try:
result = json.loads(self._request('POST',url, data=data, auth=self.auth, headers=headers))
if result['ocs']['meta']['statuscode'] == 100: return True
if result['ocs']['meta']['statuscode'] == 102: raise ProviderItemExists
raise ProviderOpError
except:
log.error(traceback.format_exc())
raise
# 100 - successful
# 101 - invalid input data
# 102 - group already exists
# 103 - failed to add the group
def delete_group(self,groupid):
url = self.apiurl + "groups/"+groupid+"?format=json"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-APIRequest': 'true',
}
try:
result = json.loads(self._request('DELETE',url, auth=self.auth, headers=headers))
if result['ocs']['meta']['statuscode'] == 100: return True
log.error(traceback.format_exc())
raise ProviderOpError
except:
log.error(traceback.format_exc())
raise
# 100 - successful
# 101 - invalid input data
# 102 - group already exists
# 103 - failed to add the group

View File

@ -0,0 +1,29 @@
#!/usr/bin/env python
# coding=utf-8
class ProviderConnError(Exception):
pass
class ProviderSslError(Exception):
pass
class ProviderConnTimeout(Exception):
pass
class ProviderError(Exception):
pass
class ProviderItemExists(Exception):
pass
class ProviderItemNotExists(Exception):
pass
class ProviderGroupNotExists(Exception):
pass
class ProviderFolderNotExists(Exception):
pass
class ProviderOpError(Exception):
pass

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,28 @@
#!flask/bin/python
# coding=utf-8
from admin import app
import logging as log
import traceback
from uuid import uuid4
import time,json
import sys,os
from flask import render_template, Response, request, redirect, url_for, jsonify
# from ..lib.admin import Admin
# menu = Menu()
# @app.route('/header/<format>', methods=['GET'])
# @app.route('/header/<format>/<application>', methods=['GET'])
# def api_v2_header(format,application=False):
# if application == False:
# if format == 'json':
# if application == False:
# return json.dumps(menu.get_header()), 200, {'Content-Type': 'application/json'}
# if format == 'html':
# if application == False:
# return render_template('header.html')
# if application == 'nextcloud':
# return render_template('header_nextcloud.html')
# if application == 'wordpress':
# return render_template('header_wordpress.html')

View File

@ -1,65 +0,0 @@
#!/usr/bin/env python
# coding=utf-8
# Copyright 2017 the Isard-vdi project authors:
# Josep Maria Viñolas Auquer
# Alberto Larraz Dalmases
# License: AGPLv3
import time
from api import app as application
from datetime import datetime, timedelta
import pprint
import logging
import traceback
import yaml, json
from jinja2 import Environment, FileSystemLoader
class Menu():
def __init__(self):
self.menudict=self.gen_header()
pprint.pprint(self.menudict)
self.write_headers()
None
def gen_header(self):
with open(r'system.yaml') as yml:
system=yaml.load(yml, Loader=yaml.FullLoader)
apps_internal = []
for app in system['apps_internal']:
app['href']='https://'+app['subdomain']+'.'+application.config['DOMAIN']+app['href']
del app['subdomain']
apps_internal.append(app)
with open(r'custom.yaml') as yml:
custom=yaml.load(yml, Loader=yaml.FullLoader)
custom['background_login']='https://api.'+application.config['DOMAIN']+custom['background_login']
custom['logo']='https://api.'+application.config['DOMAIN']+custom['logo']
menudict={**custom,**{'apps_internal':apps_internal}}
menudict['user']={}
menudict['user']['account']='https://sso.'+application.config['DOMAIN']+system['user']['account']
menudict['user']['avatar']='https://sso.'+application.config['DOMAIN']+system['user']['avatar']
menudict['user']['password']='https://sso.'+application.config['DOMAIN']+system['user']['password']
return menudict
def write_headers(self):
env = Environment(loader=FileSystemLoader('api/static/_templates'))
template = env.get_template('apps.html')
output_from_parsed_template = template.render(data=self.menudict)
print(output_from_parsed_template)
with open("api/static/templates/header.html", "w") as fh:
fh.write(output_from_parsed_template)
with open("api/static/templates/header_nextcloud.html", "w") as fh:
fh.write(output_from_parsed_template)
with open("api/static/templates/header_nextcloud.html", "a") as fh:
with open("api/static/_templates/nextcloud.html", "r") as nextcloud:
fh.write(nextcloud.read())
with open("api/static/templates/header.json", "w") as fh:
fh.write(json.dumps(self.menudict))
def get_header(self):
return self.menudict
# with open('menu.yaml', 'w') as yml:
# print(yaml.dump(header, yml, allow_unicode=True))

View File

@ -1,28 +0,0 @@
#!flask/bin/python
# coding=utf-8
from api import app
import logging as log
import traceback
from uuid import uuid4
import time,json
import sys,os
from flask import render_template, Response, request, redirect, url_for, jsonify
from ..lib.menu import Menu
menu = Menu()
@app.route('/header/<format>', methods=['GET'])
@app.route('/header/<format>/<application>', methods=['GET'])
def api_v2_header(format,application=False):
if application == False:
if format == 'json':
if application == False:
return json.dumps(menu.get_header()), 200, {'Content-Type': 'application/json'}
if format == 'html':
if application == False:
return render_template('header.html')
if application == 'nextcloud':
return render_template('header_nextcloud.html')
if application == 'wordpress':
return render_template('header_wordpress.html')

View File

@ -3,11 +3,8 @@
from gevent import monkey from gevent import monkey
monkey.patch_all() monkey.patch_all()
import yaml
from api import app from admin import app
# import pprint
# pprint.pprint(app.yaml)
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=9000, debug=False) #, logger=logger, engineio_logger=engineio_logger) app.run(host='0.0.0.0', port=9000, debug=False) #, logger=logger, engineio_logger=engineio_logger)

View File

@ -1,55 +0,0 @@
apps_internal:
- subdomain: nextcloud
href: /
icon: fa fa-cloud
name: Núvol + crear arxius
shortname: cloud
- subdomain: nextcloud
href: /apps/mail/setup
icon: fa fa-envelope-o
name: Correu
shortname: email
- subdomain: pad
href: /
icon: fa fa-file-text-o
name: Pads
shortname: pads
- subdomain: nextcloud
href: /apps/forms
icon: fa fa-check-square-o
name: Formularis
shortname: forms
- subdomain: nextcloud
href: /apps/polls
icon: fa fa-bar-chart
name: Enquestes
shortname: feedback
- subdomain: nextcloud
href: /apps/spreed
icon: fa fa-commenting-o
name: Xat
shortname: chat
- subdomain: nextcloud
href: /apps/calendar
icon: fa fa-calendar
name: Calendari
shortname: schedule
- subdomain: wp
href: /wp-login.php?saml_sso
icon: fa fa-rss
name: Webs
shortname: webs
- subdomain: nextcloud
href: /apps/bbb
icon: fa fa-video-camera
name: Reunions BBB
shortname: meets_bbb
- subdomain: nextcloud
href: /apps/photos
icon: fa fa-file-image-o
name: Fotos
shortname: photos
user:
account: /auth/realms/master/account
avatar: /auth/realms/master/avatar-provider
password: /auth/realms/master/password

View File

@ -28,4 +28,7 @@ new_user = keycloak_admin.create_user({"email": "example@example.com",
"enabled": True, "enabled": True,
"firstName": "Example", "firstName": "Example",
"lastName": "Example"}) "lastName": "Example"})
print(new_user) print(new_user)
user_id_keycloak = keycloak_admin.get_user_id("admin")
print(user_id_keycloak)

View File

@ -19,6 +19,7 @@ services:
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- ${BUILD_ROOT_PATH}/admin/src:/admin # Revome in production - ${BUILD_ROOT_PATH}/admin/src:/admin # Revome in production
- ${BUILD_ROOT_PATH}/custom:/admin/custom #:ro in production - ${BUILD_ROOT_PATH}/custom:/admin/custom #:ro in production
- ${DATA_FOLDER}/avatars:/admin/avatars:ro
env_file: env_file:
- .env - .env
command: sleep infinity command: sleep infinity

@ -1 +1 @@
Subproject commit 790afd2a9c70618e422b0e69ffa702f80a4ee1a6 Subproject commit 7a0176a6e31ced64dc32fdcc175a21601747c0f7