Added admin

root 2021-05-23 07:57:23 +02:00
parent 7a0176a6e3
commit 757beff4e2
70 changed files with 74255 additions and 11 deletions

3
.gitignore vendored
View File

@ -6,6 +6,9 @@ docker-compose.yml
**/custom.yaml
**/system.yaml
admin/src/node_modules
admin/src/admin/node_modules/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

37
admin/docker/Dockerfile Normal file
View File

@ -0,0 +1,37 @@
FROM alpine:3.12.0 as production
MAINTAINER isard <info@isardvdi.com>
RUN apk add python3 py3-pip py3-pyldap~=3.2.0
RUN pip3 install --upgrade pip
RUN apk add --no-cache --virtual .build_deps \
build-base \
python3-dev \
libffi-dev \
gcc python3-dev linux-headers musl-dev postgresql-dev
COPY admin/docker/requirements.pip3 /requirements.pip3
RUN pip3 install --no-cache-dir -r requirements.pip3
RUN apk del .build_deps
RUN apk add --no-cache curl py3-yaml yarn libpq
# 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
COPY admin/src /admin
RUN cd /admin/admin && yarn install
COPY admin/docker/docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
#EXPOSE 7039
WORKDIR /admin
CMD [ "python3", "start.py" ]

View File

@ -0,0 +1,5 @@
#!/bin/sh
ssh-keygen -A
cd /admin
python3 start.py &
/usr/sbin/sshd -D -e -f /etc/ssh/sshd_config

View File

@ -0,0 +1,17 @@
python-keycloak==0.24.0
bcrypt==3.1.7
cffi==1.14.0
click==7.1.2
Flask==1.1.2
Flask-Login==0.5.0
gevent==20.6.0
greenlet==0.4.16
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
pycparser==2.20
six==1.15.0
Werkzeug==1.0.1
zope.event==4.4
zope.interface==5.1.0
psycopg2==2.8.6

View File

@ -0,0 +1,81 @@
#!flask/bin/python
# coding=utf-8
import os
import logging as log
from flask import Flask, send_from_directory, render_template
app = Flask(__name__, static_url_path='')
app = Flask(__name__, template_folder='static/templates')
app.url_map.strict_slashes = False
'''
App secret key for encrypting cookies
You can generate one with:
import os
os.urandom(24)
And paste it here.
'''
app.secret_key = "Change this key!//\xf7\x83\xbe\x17\xfa\xa3zT\n\\]m\xa6\x8bF\xdd\r\xf7\x9e\x1d\x1f\x14'"
print('Starting isard-sso api...')
from admin.lib.load_config import loadConfig
try:
loadConfig(app)
except:
print('Could not get environment variables...')
from admin.lib.postup import Postup
Postup()
from admin.lib.admin import Admin
app.admin=Admin()
'''
Debug should be removed on production!
'''
if app.debug:
log.warning('Debug mode: {}'.format(app.debug))
else:
log.info('Debug mode: {}'.format(app.debug))
'''
Serve static files
'''
@app.route('/dd-admin/build/<path:path>')
def send_build(path):
return send_from_directory(os.path.join(app.root_path, 'node_modules/gentelella/build'), path)
@app.route('/dd-admin/vendors/<path:path>')
def send_vendors(path):
return send_from_directory(os.path.join(app.root_path, 'node_modules/gentelella/vendors'), path)
@app.route('/dd-admin/templates/<path:path>')
def send_templates(path):
return send_from_directory(os.path.join(app.root_path, 'templates'), path)
# @app.route('/templates/<path:path>')
# def send_templates(path):
# return send_from_directory(os.path.join(app.root_path, 'static/templates'), path)
@app.route('/dd-admin/static/<path:path>')
def send_static_js(path):
return send_from_directory(os.path.join(app.root_path, 'static'), path)
# @app.errorhandler(404)
# def not_found_error(error):
# return render_template('page_404.html'), 404
# @app.errorhandler(500)
# def internal_error(error):
# return render_template('page_500.html'), 500
'''
Import all views
'''
from .views import MenuViews

View File

@ -0,0 +1,159 @@
from admin import app
from .keycloak import Keycloak
from .moodle import Moodle
from .nextcloud import Nextcloud
from pprint import pprint
class Admin():
def __init__(self):
self.keycloak=Keycloak(verify=app.config['VERIFY'])
self.moodle=Moodle(verify=app.config['VERIFY'])
self.nextcloud=Nextcloud(verify=app.config['VERIFY'])
self.external={'users':[],
'groups':[],
'roles':[]}
#pprint(self.get_moodle_groups())
# 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
def get_mix_users(self):
kusers=self.get_keycloak_users()
musers=self.get_moodle_users()
nusers=self.get_nextcloud_users()
kusers_usernames=[u['username'] for u in kusers]
musers_usernames=[u['username'] for u in musers]
nusers_usernames=[u['username'] for u in nusers]
all_users_usernames=set(kusers_usernames+musers_usernames+nusers_usernames)
users=[]
for username in all_users_usernames:
theuser={}
keycloak_exists=[u for u in kusers if u['username'] == username]
if len(keycloak_exists):
theuser=keycloak_exists[0]
theuser['keycloak']=True
else:
theuser['id']=False
theuser['keycloak']=False
moodle_exists=[u for u in musers if u['username'] == username]
if len(moodle_exists):
theuser={**moodle_exists[0], **theuser}
theuser['moodle']=True
else:
theuser['moodle']=False
nextcloud_exists=[u for u in nusers if u['username'] == username]
if len(nextcloud_exists):
theuser={**nextcloud_exists[0], **theuser}
theuser['nextcloud']=True
else:
theuser['nextcloud']=False
users.append(theuser)
return users
def get_roles(self):
return self.keycloak.get_roles()
def get_keycloak_groups(self):
return self.keycloak.get_groups()
def get_moodle_groups(self):
return self.moodle.get_cohorts()
def get_nextcloud_groups(self):
return self.nextcloud.get_groups_list()
def get_groups(self):
kgroups=self.get_keycloak_groups()
mgroups=self.get_moodle_groups()
ngroups=self.get_nextcloud_groups()
kgroups=[] if kgroups is None else kgroups
mgroups=[] if mgroups is None else mgroups
ngroups=[] if ngroups is None else ngroups
kgroups_names=[g['name'] for g in kgroups]
mgroups_names=[g['name'] for g in mgroups]
ngroups_names=ngroups
all_groups_names=set(kgroups_names+mgroups_names+ngroups_names)
groups=[]
for name in all_groups_names:
thegroup={}
keycloak_exists=[g for g in kgroups if g['name'] == name]
if len(keycloak_exists):
thegroup=keycloak_exists[0]
thegroup['keycloak']=True
else:
thegroup['id']=False
thegroup['keycloak']=False
moodle_exists=[g for g in mgroups if g['name'] == name]
if len(moodle_exists):
thegroup['path']=''
thegroup={**moodle_exists[0], **thegroup}
thegroup['moodle']=True
else:
thegroup['moodle']=False
nextcloud_exists=[g for g in ngroups if g == name]
if len(nextcloud_exists):
nextcloud={"id":nextcloud_exists[0],
"name":nextcloud_exists[0],
"path":''}
thegroup={**nextcloud, **thegroup}
thegroup['nextcloud']=True
else:
thegroup['nextcloud']=False
groups.append(thegroup)
return groups
def get_external_users(self):
return self.external['users']
def get_external_groups(self):
return self.external['groups']
def get_external_roles(self):
return self.external['roles']

View File

@ -0,0 +1,230 @@
#!/usr/bin/env python
# coding=utf-8
import time ,os
from admin import app
from datetime import datetime, timedelta
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
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.keycloak_admin = KeycloakAdmin(server_url=url,
username=username,
password=password,
realm_name=realm,
verify=verify)
from pprint import pprint
######## Example create group and subgroup
# try:
# self.add_group('level1')
# except:
# self.delete_group(self.get_group('/level1')['id'])
# self.add_group('level1')
# self.add_group('level2',parent=self.get_group('/level1')['id'])
# pprint(self.get_groups())
######## Example roles
# try:
# self.add_role('superman')
# except:
# self.delete_role('superman')
# self.add_role('superman')
# pprint(self.get_roles())
## 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)
## GROUPS
def get_groups(self,with_subgroups=True):
groups = self.keycloak_admin.get_groups()
subgroups=[]
if with_subgroups:
for group in groups:
if len(group['subGroups']):
for sg in group['subGroups']:
subgroups.append(sg)
# import pprint
# return groups+subgroups
def get_group(self,path,recursive=True):
return self.keycloak_admin.get_group_by_path(path=path,search_in_subgroups=recursive)
def add_group(self,name,parent=None):
return self.keycloak_admin.create_group({"name":name}, parent=parent)
def delete_group(self,group_id):
return self.keycloak_admin.delete_group(group_id=group_id)
## ROLES
def get_roles(self):
return self.keycloak_admin.get_realm_roles()
def get_role(self,name):
return self.keycloak_admin.get_realm_role(name=name)
def add_role(self,name):
return self.keycloak_admin.create_realm_role({"name":name})
def delete_role(self,name):
return self.keycloak_admin.delete_realm_role(name)
## 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})
## SYSTEM
def get_server_info(self):
return self.keycloak_admin.get_server_info()
def get_server_clients(self):
return self.keycloak_admin.get_clients()
def get_server_rsa_key(self):
rsa_key = [k for k in self.keycloak_admin.get_keys()['keys'] if k['type']=='RSA'][0]
return {'name':rsa_key['kid'],'certificate':rsa_key['certificate']}
## CLIENTS
def add_moodle_client(self):
demo={
"id" : "a92d5417-92b6-4678-9cb9-51bc0edcee8c",
"clientId" : "https://moodle."+app.config['DOMAIN']+"/auth/saml2/sp/metadata.php",
"surrogateAuthRequired" : False,
"enabled" : True,
"alwaysDisplayInConsole" : False,
"clientAuthenticatorType" : "client-secret",
"redirectUris" : [ "https://moodle."+app.config['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+app.config['DOMAIN']+"" ],
"webOrigins" : [ "https://moodle."+app.config['DOMAIN']+"" ],
"notBefore" : 0,
"bearerOnly" : False,
"consentRequired" : False,
"standardFlowEnabled" : True,
"implicitFlowEnabled" : False,
"directAccessGrantsEnabled" : False,
"serviceAccountsEnabled" : False,
"publicClient" : False,
"frontchannelLogout" : True,
"protocol" : "saml",
"attributes" : {
"saml.force.post.binding" : True,
"saml.encrypt" : True,
"saml_assertion_consumer_url_post" : "https://moodle."+app.config['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+app.config['DOMAIN']+"",
"saml.server.signature" : True,
"saml.server.signature.keyinfo.ext" : False,
"saml.signing.certificate" : app.config['SP_CRT'],
"saml_single_logout_service_url_redirect" : "https://moodle."+app.config['DOMAIN']+"/auth/saml2/sp/saml2-logout.php/moodle."+app.config['DOMAIN']+"",
"saml.signature.algorithm" : "RSA_SHA256",
"saml_force_name_id_format" : False,
"saml.client.signature" : True,
"saml.encryption.certificate" : app.config['SP_PEM'],
"saml.authnstatement" : True,
"saml_name_id_format" : "username",
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#"
},
"authenticationFlowBindingOverrides" : { },
"fullScopeAllowed" : True,
"nodeReRegistrationTimeout" : -1,
"protocolMappers" : [ {
"id" : "9296daa3-4fc4-4b80-b007-5070f546ae13",
"name" : "X500 surname",
"protocol" : "saml",
"protocolMapper" : "saml-user-property-mapper",
"consentRequired" : False,
"config" : {
"attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"user.attribute" : "lastName",
"friendly.name" : "surname",
"attribute.name" : "urn:oid:2.5.4.4"
}
}, {
"id" : "ccecf6e4-d20a-4211-b67c-40200a6b2c5d",
"name" : "username",
"protocol" : "saml",
"protocolMapper" : "saml-user-property-mapper",
"consentRequired" : False,
"config" : {
"attribute.nameformat" : "Basic",
"user.attribute" : "username",
"friendly.name" : "username",
"attribute.name" : "username"
}
}, {
"id" : "53858403-eba2-4f6d-81d0-cced700b5719",
"name" : "X500 givenName",
"protocol" : "saml",
"protocolMapper" : "saml-user-property-mapper",
"consentRequired" : False,
"config" : {
"attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"user.attribute" : "firstName",
"friendly.name" : "givenName",
"attribute.name" : "urn:oid:2.5.4.42"
}
}, {
"id" : "20034db5-1d0e-4e66-b815-fb0440c6d1e2",
"name" : "X500 email",
"protocol" : "saml",
"protocolMapper" : "saml-user-property-mapper",
"consentRequired" : False,
"config" : {
"attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"user.attribute" : "email",
"friendly.name" : "email",
"attribute.name" : "urn:oid:1.2.840.113549.1.9.1"
}
} ],
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ],
"access" : {
"view" : True,
"configure" : True,
"manage" : True
}
}
return self.keycloak_admin.create_client(demo)

View File

@ -0,0 +1,20 @@
#!/usr/bin/env python
# coding=utf-8
from admin import app
import os, sys
import logging as log
import traceback
class loadConfig():
def __init__(self, app=None):
try:
app.config.setdefault('DOMAIN', os.environ['DOMAIN'])
app.config.setdefault('MOODLE_POSTGRES_USER', os.environ['MOODLE_POSTGRES_USER'])
app.config.setdefault('MOODLE_POSTGRES_PASSWORD', os.environ['MOODLE_POSTGRES_PASSWORD'])
app.config.setdefault('VERIFY', True if os.environ['VERIFY']=="true" else False)
except Exception as e:
log.error(traceback.format_exc())
raise

View File

@ -0,0 +1,83 @@
from requests import get, post
from admin import app
# 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=app.config["MOODLE_WS_TOKEN"],
url="https://moodle."+app.config["DOMAIN"],
endpoint="/webservice/rest/server.php",
verify=app.config["VERIFY"]):
self.key = key
self.url = url
self.endpoint = endpoint
self.verify=verify
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=self.verify)
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
def get_cohorts(self):
cohorts = self.call('core_cohort_get_cohorts')
return cohorts

View File

@ -0,0 +1,19 @@
#Add this app service
registres mdl_external_services
registres mdl_external_services_functions
registres mdl_external_services_users
# SAML
mdl_auth_saml2_idps
/opt/digitaldemocratic/data/moodle/saml2# ls
0f635d0e0f3874fff8b581c132e6c7a7.idp.xml moodle.santantoni.duckdns.org.crt moodle.santantoni.duckdns.org.pem
echo -n xml | md5sum
0f635d0e0f3874fff8b581c132e6c7a7
SELECT * FROM "mdl_config" WHERE ("name" LIKE '%saml%' OR "value" LIKE '%saml%') LIMIT 50 (0.001 s) Edita
Modify id name value
edita 3 auth email,saml2
privatekey_pass = mdl_config siteidentifier

View File

@ -0,0 +1,260 @@
#!/usr/bin/env python
# coding=utf-8
#from ..lib.log import *
from admin import app
import time,requests,json,pprint,os
import traceback
import logging as log
from .nextcloud_exc import *
class Nextcloud():
def __init__(self,
url="https://nextcloud."+app.config['DOMAIN'],
username=os.environ['NEXTCLOUD_ADMIN_USER'],
password=os.environ['NEXTCLOUD_ADMIN_PASSWORD'],
realm='master',
verify=True):
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']]
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

@ -0,0 +1,43 @@
#!/usr/bin/env python
# coding=utf-8
import time
from admin import app
from datetime import datetime, timedelta
import pprint
import logging
import traceback
import yaml, json
import psycopg2
class Postgres():
def __init__(self,host,database,user,password):
self.conn = psycopg2.connect(
host=host,
database=database,
user=user,
password=password)
self.cur = self.conn.cursor()
# def __del__(self):
# self.cur.close()
# self.conn.close()
def select(self,sql):
self.cur.execute(sql)
return self.cur.fetchall()
def update(self,sql):
self.cur.execute(sql)
self.conn.commit()
# return self.cur.fetchall()
# def update_moodle_saml_plugin(self):
# plugin[('idpmetadata', '<md:EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Name="urn:keycloak"><md:EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://sso.'+app.config['DOMAIN']+'/auth/realms/master"><md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor use="signing"><ds:KeyInfo><ds:KeyName>NrtA5ynG0htowP3SXw7dBJRIAMxn-1PwuuXwOwNhlRw</ds:KeyName><ds:X509Data><ds:X509Certificate>MIICmzCCAYMCBgF5jb0RCTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwNTIxMDcwMjI4WhcNMzEwNTIxMDcwNDA4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCI8xh/C0+frz3kgWiUbziTDls71R2YiXLSVE+bw7gbEgZUGCLhoEI679azMtIxmnzM/snIX+yTb12+XoYkgbiLTMPQfnH+Kiab6g3HL3KPfhqS+yWkFxOoCp6Ibmp7yPlVWuHH+MBfO8OBr/r8Ao7heFbuzjiLd1KG67rcoaxfDgMuBoEomg1bgEjFgHaQIrSC6OZzH0h987/arqufZXeXlfyiqScMPUi+u5IpDWSwz06UKP0k8mxzNSlpZ93CKOUSsV0SMLxqg7FQ3SGiOk577bGW9o9BDTkkmSo3Up6smc0LzwvvUwuNd0B1irGkWZFQN9OXJnJYf1InEebIMtmPAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADM34+qEGeBQ22luphVTuVJtGxcbxLx7DfsT0QfJD/OuxTTbNAa1VRyarb5juIAkqdj4y2quZna9ZXLecVo4RkwpzPoKoAkYA8b+kHnWqEwJi9iPrDvKb+GR0bBkLPN49YxIZ8IdKX/PRa3yuLHe+loiNsCaS/2ZK2KO46COsqU4QX1iVhF9kWphNLybjNAX45B6cJLsa1g0vXLdm3kv3SB4I2fErFVaOoDtFIjttoYlXdpUiThkPXBfr7N67P3dZHaS4tjJh+IZ8I6TINpcsH8dBkUhzYEIPHCePwSiC1w6WDBLNDuKt1mj1CZrLq+1x+Yhrs+QNRheEKGi89HZ8N0=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.santantoni.duckdns.org/auth/realms/master/protocol/saml/resolve" index="0"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.santantoni.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.santantoni.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.santantoni.duckdns.org/auth/realms/master/protocol/saml"/><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.santantoni.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.santantoni.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.santantoni.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.santantoni.duckdns.org/auth/realms/master/protocol/saml"/></md:IDPSSODescriptor></md:EntityDescriptor></md:EntitiesDescriptor>')]
# pg_update = """UPDATE mdl_config_plugins set title = %s where plugin = auth_saml2 and name ="""
# cursor.execute(pg_update, (title, bookid))
# connection.commit()
# count = cursor.rowcount
# print(count, "Successfully Updated!")

View File

@ -0,0 +1,86 @@
#!/usr/bin/env python
# coding=utf-8
import time, os
from admin import app
from datetime import datetime, timedelta
import pprint
import logging as log
import traceback
import yaml, json
import psycopg2
from .postgres import Postgres
# from .keycloak import Keycloak
# from .moodle import Moodle
import string, random
class Postup():
def __init__(self):
ready=False
while not ready:
try:
self.pg=Postgres('isard-apps-postgresql','moodle',app.config['MOODLE_POSTGRES_USER'],app.config['MOODLE_POSTGRES_PASSWORD'])
ready=True
except:
log.warning('Could not connect to moodle database. Retrying...')
time.sleep(2)
log.info('Connected to moodle database.')
ready=False
while not ready:
try:
with open(os.path.join(app.root_path, "../moodledata/saml2/moodle."+app.config['DOMAIN']+".crt"),"r") as crt:
app.config.setdefault('SP_CRT', crt.read())
ready=True
except IOError:
log.warning('Could not get moodle SAML2 crt certificate. Retrying...')
time.sleep(2)
except:
log.error(traceback.format_exc())
log.info('Got moodle srt certificate.')
ready=False
while not ready:
try:
with open(os.path.join(app.root_path, "../moodledata/saml2/moodle."+app.config['DOMAIN']+".pem"),"r") as pem:
app.config.setdefault('SP_PEM', pem.read())
ready=True
except IOError:
log.warning('Could not get moodle SAML2 pem certificate. Retrying...')
time.sleep(2)
log.info('Got moodle pem certificate.')
self.add_moodle_ws_token()
def add_moodle_ws_token(self):
try:
token=self.pg.select("""SELECT * FROM "mdl_external_tokens" WHERE "externalserviceid" = 3""")[0][1]
app.config.setdefault('MOODLE_WS_TOKEN',token)
return
except:
# log.error(traceback.format_exc())
None
try:
self.pg.update("""INSERT INTO "mdl_external_services" ("name", "enabled", "requiredcapability", "restrictedusers", "component", "timecreated", "timemodified", "shortname", "downloadfiles", "uploadfiles") VALUES
('dd admin', 1, '', 1, NULL, 1621719763, 1621719850, 'dd_admin', 0, 0);""")
self.pg.update("""INSERT INTO "mdl_external_services_functions" ("externalserviceid", "functionname") VALUES
(3, 'core_user_get_users'),
(3, 'core_cohort_get_cohorts');""")
self.pg.update("""INSERT INTO "mdl_external_services_users" ("externalserviceid", "userid", "iprestriction", "validuntil", "timecreated") VALUES
(3, 2, NULL, NULL, 1621719871);""")
b32=''.join(random.choices(string.ascii_uppercase + string.ascii_uppercase + string.ascii_lowercase, k = 32))
b64=''.join(random.choices(string.ascii_uppercase + string.ascii_uppercase + string.ascii_lowercase, k = 64))
self.pg.update("""INSERT INTO "mdl_external_tokens" ("token", "privatetoken", "tokentype", "userid", "externalserviceid", "sid", "contextid", "creatorid", "iprestriction", "validuntil", "timecreated", "lastaccess") VALUES
('%s', '%s', 0, 2, 3, NULL, 1, 2, NULL, 0, 1621831206, NULL);""" % (b32,b64))
app.config.setdefault('MOODLE_WS_TOKEN',b32)
except:
log.error(traceback.format_exc())
exit(1)
None

View File

@ -0,0 +1,6 @@
{
"dependencies": {
"font-linux": "^0.6.1",
"gentelella": "^1.4.0"
}
}

View File

@ -0,0 +1,135 @@
body {
color: #3b3e47;
background: #3b3e47
}
.dataTables_filter {float: left; position: absolute;}
.dataTables_filter input { max-width:90px;}
.roundbox{border-radius:4px;border:1px solid #AAAAAA;}
.blink {
animation: blink 2s steps(5, start) infinite;
-webkit-animation: blink 1s steps(5, start) infinite;
}
@keyframes blink {
to {
visibility: hidden;
}
}
@-webkit-keyframes blink {
to {
visibility: hidden;
}
}
.pnotify-center {
right: calc(50% - 150px) !important;
}
.ui-select-match-text{
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 40px;
}
.ui-select-toggle > .btn.btn-link {
margin-right: 10px;
top: 6px;
position: absolute;
right: 10px;
}
.fancytree-plain` span.fancytree-selected span.fancytree-title {
background-color: yellow;
color: black;
}
.fancytree-plain span.fancytree-active span.fancytree-title {
background-color: blue;
color: white;
}
.quota-form-input {
width: 100% !important;
overflow: hidden;
text-overflow: ellipsis;
}
/*
* Workaround to fix select2 placeholder cut off
* https://github.com/select2/select2/issues/291
* https://github.com/kartik-v/yii2-widgets/issues/324
*/
.select2-search, .select2-search__field {
width: 100% !important;
}
table.dataTable td.details-show > button > i:before,
table.dataTable td.details-control > button > i:before {
content: '\f067';
font-family: FontAwesome;
cursor: pointer;
color: white;
}
table.dataTable tr.shown td.details-show > button > i:before,
table.dataTable tr.shown td.details-control > button > i:before {
content: '\f068';
color: white;
}
.howto-desktops {
background-color:rgb(238, 238, 238);
cursor: pointer;
padding: 5px 17px;
}
.x_title h4, h3 {
margin: 5px 0 6px;
float: left;
display: block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.x_panel {
border: none
}
/* Sidebar */
.sidebar_logo {
width: 50px;
height: 50px;
}
.sidebar-footer {
background: #3b3e47;
}
#menu_toggle {
color: #3b3e47;
}
.logo_white {
filter: invert(1);
}
.left_col {
background: #3b3e47;
}
.nav.side-menu>li.active>a {
background: #3b3e47;
}
.nav_title {
background: #3b3e47;
margin-bottom: 15px;
}
.nav_menu {
background: white;
}

View File

@ -0,0 +1,354 @@
/*
* Copyright 2017 the Isard-vdi project authors:
* Josep Maria Viñolas Auquer
* Alberto Larraz Dalmases
* License: AGPLv3
*/
/**
* Resize function without multiple trigger
*
* Usage:
* $(window).smartresize(function(){
* // code here
* });
*/
(function($,sr){
// debouncing function from John Hann
// http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/
var debounce = function (func, threshold, execAsap) {
var timeout;
return function debounced () {
var obj = this, args = arguments;
function delayed () {
if (!execAsap)
func.apply(obj, args);
timeout = null;
}
if (timeout)
clearTimeout(timeout);
else if (execAsap)
func.apply(obj, args);
timeout = setTimeout(delayed, threshold || 100);
};
};
// smartresize
jQuery.fn[sr] = function(fn){ return fn ? this.bind('resize', debounce(fn)) : this.trigger(sr); };
})(jQuery,'smartresize');
/**
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
// Validator.js
// initialize the validator function
validator.message.date = 'not a real date';
// validate a field on "blur" event, a 'select' on 'change' event & a '.reuired' classed multifield on 'keyup':
$('form')
.on('blur', 'input[required], input.optional, select.required', validator.checkField)
.on('change', 'select.required', validator.checkField)
.on('keypress', 'input[required][pattern]', validator.keypress);
//~ .on('keypress', 'input[required][pattern]', function(){console.log('press')});
$('.multi.required').on('keyup blur', 'input', function() {
validator.checkField.apply($(this).siblings().last()[0]);
});
$('form').submit(function(e) {
e.preventDefault();
var submit = true;
// evaluate the form using generic validaing
if (!validator.checkAll($(this))) {
submit = false;
}
if (submit)
this.submit();
return false;
});
// /Validator.js
//PNotify
var stack_center = {"dir1": "down", "dir2": "right", "firstpos1": 25, "firstpos2": ($(window).width() / 2) - (Number(PNotify.prototype.options.width.replace(/\D/g, '')) / 2)};
$(window).resize(function(){
stack_center.firstpos2 = ($(window).width() / 2) - (Number(PNotify.prototype.options.width.replace(/\D/g, '')) / 2);
});
PNotify.prototype.options.styling = "bootstrap3";
// /PNotify
// Sidebar
var CURRENT_URL = window.location.href.split('#')[0].split('?')[0],
$BODY = $('body'),
$MENU_TOGGLE = $('#menu_toggle'),
$SIDEBAR_MENU = $('#sidebar-menu'),
$SIDEBAR_FOOTER = $('.sidebar-footer'),
$LEFT_COL = $('.left_col'),
$RIGHT_COL = $('.right_col'),
$NAV_MENU = $('.nav_menu'),
$FOOTER = $('footer');
function init_sidebar() {
// TODO: This is some kind of easy fix, maybe we can improve this
var setContentHeight = function () {
// reset height
$RIGHT_COL.css('min-height', $(window).height());
var bodyHeight = $BODY.outerHeight(),
footerHeight = $BODY.hasClass('footer_fixed') ? -10 : $FOOTER.height(),
leftColHeight = $LEFT_COL.eq(1).height() + $SIDEBAR_FOOTER.height(),
contentHeight = bodyHeight < leftColHeight ? leftColHeight : bodyHeight;
// normalize content
contentHeight -= $NAV_MENU.height() + footerHeight;
$RIGHT_COL.css('min-height', contentHeight);
};
$SIDEBAR_MENU.find('a').on('click', function(ev) {
var $li = $(this).parent();
if ($li.is('.active')) {
$li.removeClass('active active-sm');
$('ul:first', $li).slideUp(function() {
setContentHeight();
});
} else {
// prevent closing menu if we are on child menu
if (!$li.parent().is('.child_menu')) {
$SIDEBAR_MENU.find('li').removeClass('active active-sm');
$SIDEBAR_MENU.find('li ul').slideUp();
}else
{
if ( $BODY.is( ".nav-sm" ) )
{
$SIDEBAR_MENU.find( "li" ).removeClass( "active active-sm" );
$SIDEBAR_MENU.find( "li ul" ).slideUp();
}
}
$li.addClass('active');
$('ul:first', $li).slideDown(function() {
setContentHeight();
});
}
});
// toggle small or large menu
$MENU_TOGGLE.on('click', function() {
if ($BODY.hasClass('nav-md')) {
$SIDEBAR_MENU.find('li.active ul').hide();
$SIDEBAR_MENU.find('li.active').addClass('active-sm').removeClass('active');
} else {
$SIDEBAR_MENU.find('li.active-sm ul').show();
$SIDEBAR_MENU.find('li.active-sm').addClass('active').removeClass('active-sm');
}
$BODY.toggleClass('nav-md nav-sm');
setContentHeight();
});
// check active menu
$SIDEBAR_MENU.find('a[href="' + CURRENT_URL + '"]').parent('li').addClass('current-page');
$SIDEBAR_MENU.find('a').filter(function () {
return this.href == CURRENT_URL;
}).parent('li').addClass('current-page').parents('ul').slideDown(function() {
setContentHeight();
}).parent().addClass('active');
// recompute content when resizing
$(window).smartresize(function(){
setContentHeight();
});
setContentHeight();
// fixed sidebar
if ($.fn.mCustomScrollbar) {
$('.menu_fixed').mCustomScrollbar({
autoHideScrollbar: true,
theme: 'minimal',
mouseWheel:{ preventDefault: true }
});
}
};
// /Sidebar
$(document).ready(function() {
init_sidebar();
$('input').iCheck({
checkboxClass: 'icheckbox_flat-green',
radioClass: 'iradio_flat-green',
})
});
// Form serialization
(function($){
$.fn.serializeObject = function(){
var self = this,
json = {},
push_counters = {},
patterns = {
"validate": /^[a-z][a-z0-9_-]*(?:\[(?:\d*|[a-z0-9_-]+)\])*$/i,
"key": /[a-z0-9_-]+|(?=\[\])/gi,
"named": /^[a-z0-9_-]+$/i,
//~ "validate": /^[a-zA-Z][a-zA-Z0-9_]*(?:\[(?:\d*|[a-zA-Z0-9_]+)\])*$/,
//~ "key": /[a-zA-Z0-9_]+|(?=\[\])/g,
"push": /^$/,
"fixed": /^\d+$/,
//~ "named": /^[a-zA-Z0-9_]+$/
};
this.build = function(base, key, value){
base[key] = value;
return base;
};
this.push_counter = function(key){
if(push_counters[key] === undefined){
push_counters[key] = 0;
}
return push_counters[key]++;
};
$.each($(this).serializeArray(), function(){
// skip invalid keys
if(!patterns.validate.test(this.name)){
return;
}
var k,
keys = this.name.match(patterns.key),
merge = this.value,
reverse_key = this.name;
while((k = keys.pop()) !== undefined){
// adjust reverse_key
reverse_key = reverse_key.replace(new RegExp("\\[" + k + "\\]$"), '');
// push
if(k.match(patterns.push)){
merge = self.build([], self.push_counter(reverse_key), merge);
}
// fixed
else if(k.match(patterns.fixed)){
merge = self.build([], k, merge);
}
// named
else if(k.match(patterns.named)){
merge = self.build({}, k, merge);
}
}
json = $.extend(true, json, merge);
});
return json;
};
})(jQuery);
function dtUpdateInsertoLD(table, data, append){
//Quickly appends new data rows. Does not update rows
if(append == true){
table.rows.add(data);
//Locate and update rows by rowId or add if new
}else{
found=false;
table.rows().every( function ( rowIdx, tableLoop, rowLoop ) {
if(this.data().id==data.id){
table.row(rowIdx).data(data).invalidate();
found=true;
return false; //Break
}
});
if(!found){
table.row.add(data);
}
}
//Redraw table maintaining paging
table.draw(false);
}
function dtUpdateInsert(table, data, append){
//Quickly appends new data rows. Does not update rows
new_id=false
if(append == true){
table.rows.add(data);
new_id=true
//Locate and update rows by rowId or add if new
}else{
if(typeof(table.row('#'+data.id).id())=='undefined'){
// Does not exists yes
table.row.add(data);
new_id=true
}else{
// Exists, do update
table.row('#'+data.id).data(data).invalidate();
}
}
//Redraw table maintaining paging
table.draw(false);
return new_id
}
function dtUpdateOnly(table, data){
if(typeof(table.row('#'+data.id).id())=='undefined'){
// Does not exists yes
}else{
// Exists, do update
table.row('#'+data.id).data(data).invalidate();
}
//Redraw table maintaining paging
table.draw(false);
}
// Panel toolbox
$(document).ready(function() {
$('.collapse-link').on('click', function() {
var $BOX_PANEL = $(this).closest('.x_panel'),
$ICON = $(this).find('i'),
$BOX_CONTENT = $BOX_PANEL.find('.x_content');
// fix for some div with hardcoded fix class
if ($BOX_PANEL.attr('style')) {
$BOX_CONTENT.slideToggle(200, function(){
$BOX_PANEL.removeAttr('style');
});
} else {
$BOX_CONTENT.slideToggle(200);
$BOX_PANEL.css('height', 'auto');
}
$ICON.toggleClass('fa-chevron-up fa-chevron-down');
});
$('.close-link').click(function () {
var $BOX_PANEL = $(this).closest('.x_panel');
$BOX_PANEL.remove();
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,137 @@
$(document).on('shown.bs.modal', '#modalAddDesktop', function () {
modal_add_desktops.columns.adjust().draw();
});
$(document).ready(function() {
var path = "";
items = [];
document.getElementById('file-upload').addEventListener('change', readFile, false);
$('.btn-upload').on('click', function () {
$('#modalImport').modal({backdrop: 'static', keyboard: false}).modal('show');
$('#modalImportForm')[0].reset();
});
$("#modalImport #send").on('click', function(e){
var form = $('#modalImportForm');
//
form.parsley().validate();
if (form.parsley().isValid()){ // || 'unlimited' in formdata){
uploaded=JSON.parse(filecontents)
formdata = form.serializeObject()
console.log(formdata)
//socket.emit('bulkusers_add',{'data':data,'users':users})
//$('#modalImport #send').prop('disabled', true);
}
});
//DataTable Main renderer
var table = $('#users').DataTable({
"ajax": {
"url": "/dd-admin/external_users_list",
"dataSrc": ""
},
"language": {
"loadingRecords": '<i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i><span class="sr-only">Loading...</span>',
"emptyTable": "<h1>You don't have any user created yet.</h1><br><h2>Create one using the +Add new button on top right of this page.</h2>"
},
"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": "keycloak", "width": "10px" },
{ "data": "moodle", "width": "10px" },
{ "data": "nextcloud", "width": "10px" },
{ "data": "username", "width": "10px"},
{ "data": "first", "width": "10px"},
{ "data": "last", "width": "10px"},
{ "data": "email", "width": "10px"},
],
"order": [[4, 'asc']],
"columnDefs": [ {
"targets": 2,
"render": function ( data, type, full, meta ) {
if(full.keycloak){
return '<i class="fa fa-check" style="color:lightgreen"></i>'
}else{
return '<i class="fa fa-close" style="color:darkred"></i>'
};
}},
{
"targets": 3,
"render": function ( data, type, full, meta ) {
if(full.moodle){
return '<i class="fa fa-check" style="color:lightgreen"></i>'
}else{
return '<i class="fa fa-close" style="color:darkred"></i>'
};
}},
{
"targets": 4,
"render": function ( data, type, full, meta ) {
if(full.nextcloud){
return '<i class="fa fa-check" style="color:lightgreen"></i>'
}else{
return '<i class="fa fa-close" style="color:darkred"></i>'
};
}},
]
} );
});
function readFile (evt) {
path = "";
items = [];
var files = evt.target.files;
var file = files[0];
var reader = new FileReader();
reader.onload = function(event) {
filecontents=event.target.result;
$.each(JSON.parse(filecontents), walker);
console.log(path)
populate_path(items)
}
reader.readAsText(file, 'UTF-8')
}
function toObject(names, values) {
var result = {};
for (var i = 0; i < names.length; i++)
result[names[i]] = values[i];
return result;
}
function walker(key, value) {
var savepath = path;
path = path ? (path + "." + key) : key;
console.log("Visiting " + path);
items.push({path:path})
if (typeof value === "object") {
// Recurse into children
if(value.constructor === Array){
value=value[0]
}
if(typeof value == "object"){
$.each(value, walker);
}
}
path = savepath;
}
function populate_path(){
console.log(items)
$.each(items, function(key, value) {
$(".populate").append('<option value=' + value['path']+ '>' + value['path'] + '</option>');
// $("#users_group_dd").append('<option value=' + value['path'] + '>' + value['path'] + '</option>');
})
}

View File

@ -0,0 +1,74 @@
$(document).on('shown.bs.modal', '#modalAddDesktop', function () {
modal_add_desktops.columns.adjust().draw();
});
$(document).ready(function() {
$('.btn-new').on('click', function () {
$("#modalAdd")[0].reset();
$('#modalAddDesktop').modal({
backdrop: 'static',
keyboard: false
}).modal('show');
$('#modalAdd').parsley();
});
//DataTable Main renderer
var table = $('#groups').DataTable({
"ajax": {
"url": "/dd-admin/groups_list",
"dataSrc": ""
},
"language": {
"loadingRecords": '<i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i><span class="sr-only">Loading...</span>',
"emptyTable": "<h1>You don't have any group created yet.</h1><br><h2>Create one using the +Add new button on top right of this page.</h2>"
},
"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": "keycloak", "width": "10px" },
{ "data": "moodle", "width": "10px" },
{ "data": "nextcloud", "width": "10px" },
{ "data": "name", "width": "10px" },
{ "data": "path", "width": "10px" },
],
"order": [[3, 'asc']],
"columnDefs": [ {
"targets": 2,
"render": function ( data, type, full, meta ) {
if(full.keycloak){
return '<i class="fa fa-check" style="color:lightgreen"></i>'
}else{
return '<i class="fa fa-close" style="color:darkred"></i>'
};
}},
{
"targets": 3,
"render": function ( data, type, full, meta ) {
if(full.moodle){
return '<i class="fa fa-check" style="color:lightgreen"></i>'
}else{
return '<i class="fa fa-close" style="color:darkred"></i>'
};
}},
{
"targets": 4,
"render": function ( data, type, full, meta ) {
if(full.nextcloud){
return '<i class="fa fa-check" style="color:lightgreen"></i>'
}else{
return '<i class="fa fa-close" style="color:darkred"></i>'
};
}},
]
} );
})

View File

@ -0,0 +1,42 @@
$(document).on('shown.bs.modal', '#modalAddDesktop', function () {
modal_add_desktops.columns.adjust().draw();
});
$(document).ready(function() {
$('.btn-new').on('click', function () {
$("#modalAdd")[0].reset();
$('#modalAddDesktop').modal({
backdrop: 'static',
keyboard: false
}).modal('show');
$('#modalAdd').parsley();
});
//DataTable Main renderer
var table = $('#roles').DataTable({
"ajax": {
"url": "/dd-admin/roles_list",
"dataSrc": ""
},
"language": {
"loadingRecords": '<i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i><span class="sr-only">Loading...</span>',
"emptyTable": "<h1>You don't have any role created yet.</h1><br><h2>Create one using the +Add new button on top right of this page.</h2>"
},
"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']],
} );
});

View File

@ -0,0 +1,76 @@
$(document).on('shown.bs.modal', '#modalAddDesktop', function () {
modal_add_desktops.columns.adjust().draw();
});
$(document).ready(function() {
$('.btn-new').on('click', function () {
$("#modalAdd")[0].reset();
$('#modalAddDesktop').modal({
backdrop: 'static',
keyboard: false
}).modal('show');
$('#modalAdd').parsley();
});
//DataTable Main renderer
var table = $('#users').DataTable({
"ajax": {
"url": "/dd-admin/users_list",
"dataSrc": ""
},
"language": {
"loadingRecords": '<i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i><span class="sr-only">Loading...</span>',
"emptyTable": "<h1>You don't have any user created yet.</h1><br><h2>Create one using the +Add new button on top right of this page.</h2>"
},
"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": "keycloak", "width": "10px" },
{ "data": "moodle", "width": "10px" },
{ "data": "nextcloud", "width": "10px" },
{ "data": "username", "width": "10px"},
{ "data": "first", "width": "10px"},
{ "data": "last", "width": "10px"},
{ "data": "email", "width": "10px"},
],
"order": [[4, 'asc']],
"columnDefs": [ {
"targets": 2,
"render": function ( data, type, full, meta ) {
if(full.keycloak){
return '<i class="fa fa-check" style="color:lightgreen"></i>'
}else{
return '<i class="fa fa-close" style="color:darkred"></i>'
};
}},
{
"targets": 3,
"render": function ( data, type, full, meta ) {
if(full.moodle){
return '<i class="fa fa-check" style="color:lightgreen"></i>'
}else{
return '<i class="fa fa-close" style="color:darkred"></i>'
};
}},
{
"targets": 4,
"render": function ( data, type, full, meta ) {
if(full.nextcloud){
return '<i class="fa fa-check" style="color:lightgreen"></i>'
}else{
return '<i class="fa fa-close" style="color:darkred"></i>'
};
}},
]
} );
});

View File

@ -0,0 +1,146 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<!-- Meta, title, CSS, favicons, etc. -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="no-cache">
<meta http-equiv="Expires" content="-1">
<meta http-equiv="Cache-Control" content="no-cache">
<title>{{ title }} | Digital Democratic</title>
<link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" />
<!-- Fancytree -->
<link href="/dd-admin/static/vendor/fancytree/dist/skin-win8/ui.fancytree.css" rel="stylesheet">
<!-- Bootstrap -->
<link href="/dd-admin/vendors/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link href="/dd-admin/vendors/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<!-- Font Linux -->
<link href="/dd-admin/font-linux/font-linux.css" rel="stylesheet">
<!-- ion.rangeSlider -->
<link href="/dd-admin/vendors/ion.rangeSlider/css/ion.rangeSlider.css" rel="stylesheet">
<link href="/dd-admin/vendors/ion.rangeSlider/css/ion.rangeSlider.skinFlat.css" rel="stylesheet">
<!-- Datatables -->
<link href="/dd-admin/vendors/datatables.net-bs/css/dataTables.bootstrap.min.css" rel="stylesheet">
<!-- PNotify -->
<link href="/dd-admin/vendors/pnotify/dist/pnotify.css" media="all" rel="stylesheet" type="text/css" />
<link href="/dd-admin/vendors/pnotify/dist/pnotify.buttons.css" media="all" rel="stylesheet" type="text/css" />
<!-- iCheck -->
<link href="/dd-admin/vendors/iCheck/skins/flat/green.css" rel="stylesheet">
<link href="/dd-admin/vendors/select2/dist/css/select2.min.css" rel="stylesheet">
{% block css %}{% endblock %}
<!-- Custom Theme Style -->
<link href="/dd-admin/build/css/custom.css" rel="stylesheet">
<!-- Isard Style Sheet-->
<link href="/dd-admin/static/dd.css" rel="stylesheet">
</head>
<body class="nav-md">
<div class="container body">
<div class="main_container">
<div class="col-md-3 left_col">
<div class="left_col scroll-view">
{% include 'sidebar.html' %}
</div>
</div>
<!-- top navigation -->
{% include 'header.html' %}
<!-- /top navigation -->
<div class="right_col" role="main">
<!-- page content -->
{% block content %}
{% endblock %}
<!-- /page content -->
</div>
<!-- footer content -->
{% include 'footer.html' %}
<!-- /footer content -->
</div>
</div>
<div id="modal-lostconnection" class="modal fade" role="dialog" style="width:50%;margin-left:30%;margin-top:10%;z-index: 100000;">
<div class="modal-admin">
<div class="modal-content">
<div class="row text-center"><h2 style="margin-bottom:5px">Connection lost</h2></div>
<hr>
<div class="row">
<div class="col-md-1 col-sm-1 col-xs-12"></div>
<div class="col-md-10 col-sm-10 col-xs-12">
<div class="row text-center">Unable to contact server. There should be a problem with network or a heavy load.</div>
<br>
<div class="row text-center" style="margin-bottom:25px">
<i class="fa fa-circle-o-notch fa-spin fa-fw"></i> Trying to reconnect...
</div>
</div>
<div class="col-md-1 col-sm-1 col-xs-12">
</div>
</div>
</div>
</div>
</div>
<!-- jQuery -->
<script src="/dd-admin/vendors/jquery/dist/jquery.js"></script>
<!-- Bootstrap -->
<script src="/dd-admin/vendors/bootstrap/dist/js/bootstrap.min.js"></script>
<!-- NProgress -->
<script src="/dd-admin/vendors/nprogress/nprogress.js"></script>
<!-- Datatables -->
<script src="/dd-admin/vendors/datatables.net/js/jquery.dataTables.min.js"></script>
<script src="/dd-admin/vendors/datatables.net-bs/js/dataTables.bootstrap.min.js"></script>
<!-- Ion.RangeSlider -->
<script src="/dd-admin/vendors/ion.rangeSlider/js/ion.rangeSlider.min.js"></script>
<!-- PNotify -->
<script type="text/javascript" src="/dd-admin/vendors/pnotify/dist/pnotify.js"></script>
<script type="text/javascript" src="/dd-admin/vendors/pnotify/dist/pnotify.confirm.js"></script>
<script type="text/javascript" src="/dd-admin/vendors/pnotify/dist/pnotify.buttons.js"></script>
<!-- validator -->
<script src="/dd-admin/vendors/validator/validator.js"></script>
<!-- Parsley -->
<script src="/dd-admin/vendors/parsleyjs/dist/parsley.min.js"></script>
<!-- moment -->
<script src="/dd-admin/vendors/moment/min/moment.min.js"></script>
<!-- validator -->
<script src="/dd-admin/vendors/iCheck/icheck.min.js"></script>
<!-- bootstrap-progressbar -->
<script src="/dd-admin/vendors/bootstrap-progressbar/bootstrap-progressbar.min.js"></script>
<!-- ECharts -->
<script src="/dd-admin/vendors/echarts/dist/echarts.min.js"></script>
<!-- Select2 -->
<script src="/dd-admin/vendors/select2/dist/js/select2.full.min.js"></script>
<!-- SocketIO -->
<script src="/dd-admin/static/vendor/socket.io-2.3.1.slim.js"></script>
<!-- isard initializers -->
<script src="/dd-admin/static/dd.js"></script>
<!-- isard quota sse -->
<script src="/dd-admin/static/js/quota.js"></script>
<!-- Requirements for fancy tree -->
<script src="/dd-admin/static/vendor/fancytree/src/jquery-ui-dependencies/jquery-ui.min.js"></script>
<script src="/dd-admin/static/vendor/fancytree/dist/jquery.fancytree.min.js"></script>
<script src="/dd-admin/static/vendor/fancytree/src/jquery.fancytree.table.js"></script>
<!-- flashed messages with pnotify -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<script type="text/javascript">
new PNotify({
title: "{{ nav }}",
text: "{{ message }}",
hide: true,
delay: 2000,
//~ icon: 'fa fa-alert-sign',
opacity: 1,
type: "{{ category }}",
addclass: "pnotify-center"
});
</script>
{% endfor %}
{% endif %}
{% endwith %}
{% block pagescript %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,6 @@
<footer>
<div class="pull-right">
Digital Democratic - Administration | <a href="https://gitlab.com/digitaldemocratic/digitaldemocratic">gitlab</a>
</div>
<div class="clearfix"></div>
</footer>

View File

@ -0,0 +1,62 @@
<div class="top_nav" >
<!-- <div class="nav_menu">
<nav class="" role="navigation" >
<div class="nav toggle">
<a id="menu_toggle"><i class="fa fa-bars"></i></a>
</div>
<ul class="nav navbar-nav navbar-right">
<li class="">
<a href="javascript:" class="user-profile dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<img src="/dd-admin/static/img/user.png" alt="..." >
<span class=" fa fa-angle-down"></span>
</a>
<ul class="dropdown-menu dropdown-usermenu pull-right">
<li><a href="/dd-admin/profile"><i class="fa fa-gear pull-right"></i> Profile</a></li>
<li><a href="/dd-admin/logout"><i class="fa fa-sign-out pull-right"></i> Log Out</a></li>
</ul>
</li>
<li role="presentation" class="quota-play">
<a href="javascript:" class="dropdown-toggle info-number" data-toggle="dropdown" aria-expanded="false">
<i class="fa fa-play"></i>
<span class="badge"></span>
</a>
<ul id="menu1" class="dropdown-menu list-unstyled msg_list" role="menu">
<li>
<a>
<span class="image"><i class="fa fa-play"></i></span>
<span>
<span>Desktops running</span>
<span class="time"><span class="perc"></span>%</span>
</span>
<span class="message">
You have <span class="have"></span> running desktops of <span class="of"></span> in your quota.
</span>
</a>
</li>
</ul>
</li>
<li role="presentation" class="quota-desktops">
<a href="javascript:" class="dropdown-toggle info-number" data-toggle="dropdown" aria-expanded="false">
<i class="fa fa-desktop"></i>
<span class="badge"></span>
</a>
<ul id="menu1" class="dropdown-menu list-unstyled msg_list" role="menu">
<li>
<a>
<span class="image"><i class="fa fa-desktop"></i></span>
<span>
<span>Desktops</span>
<span class="time"><span class="perc"></span>%</span>
</span>
<span class="message">
You have <span class="have"></span> desktops of <span class="of"></span> in your quota.
</span>
</a>
</li>
</ul>
</li>
</ul>
</nav>
</div> -->
</div>

View File

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<!-- Meta, title, CSS, favicons, etc. -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login | Digital Democratic</title>
<!-- Bootstrap -->
<link href="/dd-admin/vendors/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link href="/dd-admin/vendors/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<!-- Animate.css -->
<link href="/dd-admin/vendors/animate.css/animate.min.css" rel="stylesheet">
<!-- PNotify -->
<link href="/dd-admin/vendors/pnotify/dist/pnotify.css" media="all" rel="stylesheet" type="text/css" />
<link href="/dd-admin/vendors/pnotify/dist/pnotify.buttons.css" media="all" rel="stylesheet" type="text/css" />
<!-- Custom Theme Style -->
<link href="/dd-admin/build/css/custom.min.css" rel="stylesheet">
</head>
<body class="login" style="background-color:rgb(245, 169, 174)">
<div>
<a class="hiddenanchor" id="signup"></a>
<a class="hiddenanchor" id="signin"></a>
<div class="login_wrapper">
<div class="animate form login_form">
<section class="login_content">
<form id="login-form" action="{{ url_for('login') }}" method="POST" novalidate>
<h1>Digital Democratic</h1>
<div>
<input type="text" name="user" class="form-control" placeholder="Username" required="" autofocus />
</div>
<div>
<input type="password" name="password" class="form-control" placeholder="Password" required="" />
</div>
<div>
<button type="submit" class="btn btn-default submit">Login</button>
</div>
<div class="clearfix"></div>
<div class="separator">
<div class="clearfix"></div>
<br />
<div>
<h1><i class="fa fa-user"></i> Digital Democratic</h1>
<p>©2020 All Rights Reserved. <a href="https://gitlab.com/digitaldemocratic/digitaldemocratic/LICENSE" target="_blank">AGPLv3</a></p>
</div>
</div>
</form>
</section>
</div>
</div>
</div>
</body>
<!-- jQuery -->
<script src="/dd-admin/vendors/jquery/dist/jquery.min.js"></script>
<!-- PNotify -->
<script type="text/javascript" src="/dd-admin/vendors/pnotify/dist/pnotify.js"></script>
<script type="text/javascript" src="/dd-admin/vendors/pnotify/dist/pnotify.confirm.js"></script>
<script type="text/javascript" src="/dd-admin/vendors/pnotify/dist/pnotify.buttons.js"></script>
<script>PNotify.prototype.options.styling = "bootstrap3";</script>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<script type="text/javascript">
new PNotify({
title: "{{ nav }}",
text: "{{ message }}",
hide: true,
//~ icon: 'fa fa-alert-sign',
opacity: 1,
type: "error",
addclass: "pnotify-center"
});
</script>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Isard restful ajax calls -->
<script src="/dd-admin/static/js/restful.js"></script>
</html>

View File

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<!-- Meta, title, CSS, favicons, etc. -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page not found! | Digital Democratic</title>
<!-- Bootstrap -->
<link href="../dd-admin/vendors/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link href="../dd-admin/vendors/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<!-- NProgress -->
<link href="../dd-admin/vendors/nprogress/nprogress.css" rel="stylesheet">
<!-- Custom Theme Style -->
<link href="../dd-admin/build/css/custom.min.css" rel="stylesheet">
</head>
<body class="nav-md">
<div class="container body">
<div class="main_container">
<!-- page content -->
<div class="col-md-12">
<div class="col-middle">
<div class="text-center text-center">
<h1 class="error-number">404</h1>
<h2>Sorry but we couldn't find this page</h2>
<p>This page you are looking for does not exist <a href="https://gitlab.com/digitaldemocratic/digitaldemocratic">Report this?</a>
<a href="/dd-admin/login">Go back to login page</a>
</p>
<!--
<div class="mid_center">
<h3>Search</h3>
<form>
<div class="col-xs-12 form-group pull-right top_search">
<div class="input-group">
<input type="text" class="form-control" placeholder="Search for...">
<span class="input-group-btn">
<button class="btn btn-default" type="button">Go!</button>
</span>
</div>
</div>
</form>
</div>
-->
</div>
</div>
</div>
<!-- /page content -->
</div>
</div>
<!-- jQuery -->
<script src="../dd-admin/vendors/jquery/dist/jquery.min.js"></script>
<!-- Bootstrap -->
<script src="../dd-admin/vendors/bootstrap/dist/js/bootstrap.min.js"></script>
<!-- FastClick -->
<script src="../dd-admin/vendors/fastclick/lib/fastclick.js"></script>
<!-- NProgress -->
<script src="../dd-admin/vendors/nprogress/nprogress.js"></script>
<!-- Custom Theme Scripts -->
<script src="../dd-admin/build/js/custom.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<!-- Meta, title, CSS, favicons, etc. -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page not allowed! | Digital Democratic</title>
<!-- Bootstrap -->
<link href="../dd-admin/vendors/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link href="../dd-admin/vendors/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<!-- NProgress -->
<link href="../dd-admin/vendors/nprogress/nprogress.css" rel="stylesheet">
<!-- Custom Theme Style -->
<link href="../dd-admin/build/css/custom.min.css" rel="stylesheet">
</head>
<body class="nav-md">
<div class="container body">
<div class="main_container">
<!-- page content -->
<div class="col-md-12">
<div class="col-middle">
<div class="text-center">
<h1 class="error-number">500</h1>
<h2>Internal Server Error</h2>
<p>We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing. <a href="https://gitlab.com/digitaldemocratic/digitaldemocratic">Report this?</a>
<a href="/dd-admin/login">Go back to login page</a>
</p>
<!--
<div class="mid_center">
<h3>Search</h3>
<form>
<div class="col-xs-12 form-group pull-right top_search">
<div class="input-group">
<input type="text" class="form-control" placeholder="Search for...">
<span class="input-group-btn">
<button class="btn btn-default" type="button">Go!</button>
</span>
</div>
</div>
</form>
</div>
-->
</div>
</div>
</div>
<!-- /page content -->
</div>
</div>
<!-- jQuery -->
<script src="../dd-admin/vendors/jquery/dist/jquery.min.js"></script>
<!-- Bootstrap -->
<script src="../dd-admin/vendors/bootstrap/dist/js/bootstrap.min.js"></script>
<!-- FastClick -->
<script src="../dd-admin/vendors/fastclick/lib/fastclick.js"></script>
<!-- NProgress -->
<script src="../dd-admin/vendors/nprogress/nprogress.js"></script>
<!-- Custom Theme Scripts -->
<script src="../dd-admin/build/js/custom.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,57 @@
<!-- extend base layout -->
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-md-12 col-sm-12 col-xs-12">
<div class="x_panel">
<div class="container for-about text-center" style="margin-top: 15px;">
<img src="/dd-admin/static/img/dd.svg" width="250px" height="250px">
<h1>Digital Democratic</h1>
<h1><small>Schools apps integrations</small></h1>
<div class="row" style="margin-top: 40px;">
<div class="col-lg-2 col-md-12 col-sm-12 col-xs-12"></div>
<div class="col-lg-2 col-md-6 col-sm-6 col-xs-12">
<a href="https://gitlab.com/digitaldemocratic/digitaldemocratic" target="_blank" style="color: deepskyblue">
<i class="fa fa-globe" style="font-size: 125px;" aria-hidden="true"></i>
<h1><small>Visit website</small></h1>
</a>
</div>
<div class="col-lg-2 col-md-6 col-sm-6 col-xs-12">
<a href="https://gitlab.com/digitaldemocratic/digitaldemocratic/-/issues" target="_blank" style="color: orange">
<i class="fa fa-gitlab fa-5x" style="font-size: 125px;" aria-hidden="true"></i>
<h1><small>Open an issue</small></h1>
</a>
</div>
<div class="col-lg-2 col-md-12 col-sm-12 col-xs-12"></div>
</div>
<div class="row" style="margin-top: 25px;">
<div class="col-md-4 col-sm-4 col-xs-4"></div>
<div class="col-md-2 col-sm-2 col-xs-12">
<i class="fa fa-envelope-o fa-5x" style="font-size: 125px;" aria-hidden="true"></i>
<h1>
<small>
Contact us at:
<br/>
info@digitaldemocratic.net
</small>
</h1>
</div>
<div class="col-md-2 col-sm-6 col-xs-12">
<p><img src="/dd-admin/static/img/agplv3-155x51.png" style="margin-top: 60px;"></p>
<h1 style="margin-top: 25px;"><small>License</small></h1>
</div>
<div class="col-md-4 col-sm-4 col-xs-4"></div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block pagescript %}
<script src="/dd-admin/static/js/restful.js"></script>
<script src="/dd-admin/static/js/quota_socket.js"></script>
{% endblock %}

View File

@ -0,0 +1,60 @@
<!-- extend base layout -->
{% extends "base.html" %}
{% block css %}
<!-- Ion.RangeSlider -->
<link href="/dd-admin/vendors/normalize-css/normalize.css" rel="stylesheet">
<!-- Switchery -->
<link href="/dd-admin/vendors/switchery/dist/switchery.min.css" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12 col-sm-12 col-xs-12">
<div class="x_panel">
<div class="x_title">
<h3><i class="fa fa-desktop"></i> External</h3>
<ul class="nav navbar-right panel_toolbox">
<li>
<a class="btn-upload"><span style="color: #5499c7; "><i class="fa fa-upload"></i> Upload</span></a>
<a class="btn-download"><span style="color: #5499c7; "><i class="fa fa-download"></i> Download</span></a>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="x_content">
<p class="text-muted font-13 m-b-30"></p>
<table id="users" class="table" width="100%">
<thead>
<tr>
<th></th>
<th>Id</th>
<th>Keycloak</th>
<th>Moodle</th>
<th>Nextcloud</th>
<th>Username</th>
<th>First</th>
<th>Last</th>
<th>email</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% include 'pages/modals/external_modals.html' %}
{% endblock %}
{% block pagescript %}
<!-- Ion.RangeSlider -->
<script src="/dd-admin/vendors/ion.rangeSlider/js/ion.rangeSlider.min.js"></script>
<!-- iCheck -->
<script src="/dd-admin/vendors/iCheck/icheck.min.js"></script>
<!-- Switchery -->
<script src="/dd-admin/vendors/switchery/dist/switchery.min.js"></script>
<!-- Desktops sse & modals -->
<script src="/dd-admin/static/js/external.js"></script>
{% endblock %}

View File

@ -0,0 +1,56 @@
<!-- extend base layout -->
{% extends "base.html" %}
{% block css %}
<!-- Ion.RangeSlider -->
<link href="/dd-admin/vendors/normalize-css/normalize.css" rel="stylesheet">
<!-- Switchery -->
<link href="/dd-admin/vendors/switchery/dist/switchery.min.css" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12 col-sm-12 col-xs-12">
<div class="x_panel">
<div class="x_title">
<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>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="x_content">
<p class="text-muted font-13 m-b-30"></p>
<table id="groups" class="table" width="100%">
<thead>
<tr>
<th></th>
<th>Id</th>
<th>Keycloak</th>
<th>Moodle</th>
<th>Nextcloud</th>
<th>Name</th>
<th>Path</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block pagescript %}
<!-- Ion.RangeSlider -->
<script src="/dd-admin/vendors/ion.rangeSlider/js/ion.rangeSlider.min.js"></script>
<!-- iCheck -->
<script src="/dd-admin/vendors/iCheck/icheck.min.js"></script>
<!-- Switchery -->
<script src="/dd-admin/vendors/switchery/dist/switchery.min.js"></script>
<!-- Desktops sse & modals -->
<script src="/dd-admin/static/js/groups.js"></script>
{% endblock %}

View File

@ -0,0 +1,116 @@
<div class="modal fade" id="modalImport" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
<h4 class="modal-title">
<i class="fa fa-plus fa-1x"> </i> <i class="fa fa-users"> </i> Import
</h4>
</div>
<!-- Modal Body -->
<div class="modal-body">
<form id="modalImportForm" class="form-horizontal form-label-left">
<div class="x_panel">
<div class="x_content">
<!--
<input id="id" hidden/>
-->
<div class="item form-group">
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="provider">Provider: <span class="required">*</span></label>
<div class="col-md-6 col-sm-6 col-xs-12">
<select id="provider" name="provider" class="form-control provider" data-quota="provider" required>
<option value="google">Google</option>
</select>
</div>
</div>
<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>
<div class="col-md-6 col-sm-6 col-xs-12">
<select id="format" name="format" class="form-control format" required>
<option value="json">JSON dump</option>
</select>
</div>
</div>
<div class="item form-group">
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="name">Import JSON <span class="required">*</span>
</label>
<div class="col-md-6 col-sm-6 col-xs-12">
<input type="file" id="file-upload" name="file-upload" enctype="multipart/form-data" />
<button id="btn-map" type="button" class="btn btn-success" data-dismiss="modal">Map fields</button>
</div>
</div>
</div>
<div class="x_panela" id="bulkusers-quota" style="padding: 5px;">
<p style="font-size: 18px;margin-bottom:0px;">Map keys</p>
<div class="item form-group">
<div class="item form-group">
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="users">Users dict: <span class="required">*</span></label>
<div class="col-md-6 col-sm-6 col-xs-12">
<select id="users" name="users" class="form-control users populate" required>
</select>
</div>
</div>
<div class="item form-group">
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="username">user name: <span class="required">*</span></label>
<div class="col-md-6 col-sm-6 col-xs-12">
<select id="username" name="username" class="form-control username populate" required>
</select>
</div>
</div>
<div class="item form-group">
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="firstname">first name: <span class="required">*</span></label>
<div class="col-md-6 col-sm-6 col-xs-12">
<select id="firstname" name="firstname" class="form-control firstname populate" required>
</select>
</div>
</div>
<div class="item form-group">
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="lastname">last name: <span class="required">*</span></label>
<div class="col-md-6 col-sm-6 col-xs-12">
<select id="lastname" name="lastname" class="form-control lastname populate" required>
</select>
</div>
</div>
<div class="item form-group">
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="usergroup">user group:<span class="required">*</span></label>
<div class="col-md-6 col-sm-6 col-xs-12">
<select id="usergroup" name="usergroup" class="form-control usergroup populate" required>
</select>
</div>
</div>
<div class="item form-group">
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="groups">Group dict:<span class="required">*</span></label>
<div class="col-md-6 col-sm-6 col-xs-12">
<select id="groups" name="groups" class="form-control groups populate" required>
</select>
</div>
</div>
</div>
</div>
</div>
<!-- Modal Footer -->
<div class="modal-footer">
<div class="form-group">
<div class="col-md-6 col-md-offset-3">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button id="send" type="button" class="btn btn-success">Process</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,52 @@
<!-- extend base layout -->
{% extends "base.html" %}
{% block css %}
<!-- Ion.RangeSlider -->
<link href="/dd-admin/vendors/normalize-css/normalize.css" rel="stylesheet">
<!-- Switchery -->
<link href="/dd-admin/vendors/switchery/dist/switchery.min.css" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12 col-sm-12 col-xs-12">
<div class="x_panel">
<div class="x_title">
<h3><i class="fa fa-user-secret"></i> Roles</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>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="x_content">
<p class="text-muted font-13 m-b-30"></p>
<table id="roles" class="table" width="100%">
<thead>
<tr>
<th></th>
<th>Id</th>
<th>Name</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block pagescript %}
<!-- Ion.RangeSlider -->
<script src="/dd-admin/vendors/ion.rangeSlider/js/ion.rangeSlider.min.js"></script>
<!-- iCheck -->
<script src="/dd-admin/vendors/iCheck/icheck.min.js"></script>
<!-- Switchery -->
<script src="/dd-admin/vendors/switchery/dist/switchery.min.js"></script>
<!-- Desktops sse & modals -->
<script src="/dd-admin/static/js/roles.js"></script>
{% endblock %}

View File

@ -0,0 +1,58 @@
<!-- extend base layout -->
{% extends "base.html" %}
{% block css %}
<!-- Ion.RangeSlider -->
<link href="/dd-admin/vendors/normalize-css/normalize.css" rel="stylesheet">
<!-- Switchery -->
<link href="/dd-admin/vendors/switchery/dist/switchery.min.css" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12 col-sm-12 col-xs-12">
<div class="x_panel">
<div class="x_title">
<h3><i class="fa fa-user"></i> Users</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>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="x_content">
<p class="text-muted font-13 m-b-30"></p>
<table id="users" class="table" width="100%">
<thead>
<tr>
<th></th>
<th>Id</th>
<th>Keycloak</th>
<th>Moodle</th>
<th>Nextcloud</th>
<th>Username</th>
<th>First</th>
<th>Last</th>
<th>email</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block pagescript %}
<!-- Ion.RangeSlider -->
<script src="/dd-admin/vendors/ion.rangeSlider/js/ion.rangeSlider.min.js"></script>
<!-- iCheck -->
<script src="/dd-admin/vendors/iCheck/icheck.min.js"></script>
<!-- Switchery -->
<script src="/dd-admin/vendors/switchery/dist/switchery.min.js"></script>
<!-- Desktops sse & modals -->
<script src="/dd-admin/static/js/users.js"></script>
{% endblock %}

View File

@ -0,0 +1,113 @@
<div style="display:none">
<div class="row template-detail-domain">
<div class="col-md-1 col-sm-1 col-xs-12">
<div class="row">
<div class="col-md-12 col-md-12 col-xs-12" id="actions-d.id" data-pk="d.id" data-name="d.name">
{% if(current_user.role!='user') %}
<div class="row">
<button class="btn btn-success btn-xs pull-right btn-jumperurl" type="button" data-placement="top" ><i class="fa fa-eye m-right-xs"></i>Viewer</button>
</div>
<div class="row">
<button class="btn btn-success btn-xs pull-right btn-template" type="button" data-placement="top" ><i class="fa fa-cube m-right-xs"></i>Template it</button>
</div>
<div class="row">
<button class="btn btn-success btn-xs pull-right btn-forcedhyp" type="button" data-placement="top" ><i class="fa fa-rocket m-right-xs"></i>Forced hyp</button>
</div>
<!-- Needed for admin -->
<div class="row">
<button class="btn btn-danger btn-xs pull-right btn-delete-template" type="button" data-placement="top" ><i class="fa fa-remove m-right-xs"></i>Delete</button>
</div>
{% endif %}
<div class="row">
<button class="btn btn-info btn-xs pull-right btn-edit" type="button" data-placement="top" ><i class="fa fa-pencil m-right-xs"></i>Edit</button>
</div>
<div class="row">
<button class="btn btn-danger btn-xs pull-right btn-delete" type="button" data-placement="top" ><i class="fa fa-remove m-right-xs"></i>Delete</button>
</div>
{% if(current_user.role=='admin') %}
<hr>
<div class="row">
<button class="btn btn-info btn-xs pull-right btn-xml" type="button" data-placement="top" ><i class="fa fa-file-code-o m-right-xs"></i>XML</button>
</div>
<div class="row">
<button class="btn btn-info btn-xs pull-right btn-events" type="button" data-placement="top" ><i class="fa fa-file-code-o m-right-xs"></i>Logs</button>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-md-11 col-sm-11 col-xs-12">
<div class="x_panel" style="background-color: #F7F7F7">
<div class="row">
<div class="col-md-12 col-md-12 col-xs-12">
<div class="x_panel">
<div class="x_content">
<h3>Status detailed info: <small id="status-detail-d.id"></small></h3>
</div>
</div>
</div>
</div>
<div class="row" >
<div class="col-md-4 col-md-4 col-xs-12">
<div id="hardware-d.id" class="x_content">
{% include '/snippets/domain_hardware.html' %}
</div>
</div>
<div class="col-md-8 col-md-8 col-xs-12">
{% if(current_user.role=='admin') %}
<div class="row">
<div class="col-md-12 col-md-12 col-xs-12">
<div class="x_content">
<div class="x_panel">
<div class="x_title">
<h3>Template tree<small></small></h3>
<div class="clearfix"></div>
</div>
<div class="x_content">
{% include '/snippets/template_tree.html' %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
<div class="row" >
<div class="col-md-12 col-md-12 col-xs-12">
<div id="hotplug-d.id" class="x_content">
{% include '/snippets/domain_hotplugged.html' %}
</div>
</div>
<!--
<div class="col-md-12 col-md-12 col-xs-12">
<div id="events-d.id" class="x_content">
include '/snippets/domain_genealogy.html'
</div>
</div>
-->
<!--
<div class="col-md-12 col-md-12 col-xs-12">
<div id="graphs-d.id" class="x_content">
include '/snippets/domain_graphs.html'
</div>
</div>
-->
</div>
</div>
</div>
<div class="row" >
<!--
<div class="col-md-12 col-md-12 col-xs-12">
<div id="derivates-d.id" class="x_content">
{% if(current_user.role=='admin') %}
include '/snippets/domain_derivates.html'
{% endif %}
</div>
</div>
-->
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,26 @@
<div class="navbar nav_title" style="border: 0;">
<a href="/dd-admin/about" class="site_title">
<img src="/dd-admin/static/img/dd.svg" class="sidebar_logo logo_white" alt="dd">
<span>Digital Democratic</span>
</a>
</div>
<div class="clearfix"></div>
<!-- sidebar menu -->
<div id="sidebar-menu" class="main_menu_side hidden-print main_menu">
<div class="menu_section">
<h3>Administration</h3>
<div class="clearfix"></div>
<ul class="nav side-menu">
<!-- <li><a href="/"><i class="fa fa-home"></i> Home</a></li> -->
<li><a href="/dd-admin/users"><i class="fa fa-user"></i> Users</a></li>
<li><a href="/dd-admin/groups"><i class="fa fa-users"></i> Groups</a></li>
<li><a href="/dd-admin/roles"><i class="fa fa-user-secret"></i> Roles</a></li>
<li><a href="/dd-admin/external"><i class="fa fa-external-link"></i> External</a></li>
</ul>
<ul class="nav side-menu">
<li><a href="/dd-admin/about"><i class="fa fa-question"></i> About</span></a>
</li>
</ul>
</div>
</div>

View File

@ -0,0 +1,53 @@
#!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
@app.route('/dd-admin/users')
# @login_required
def users():
return render_template('pages/users.html', title="Users", nav="Users")
@app.route('/dd-admin/users_list')
# @login_required
def users_list():
return json.dumps(app.admin.get_mix_users()), 200, {'Content-Type': 'application/json'}
@app.route('/dd-admin/roles')
# @login_required
def roles():
return render_template('pages/roles.html', title="Roles", nav="Roles")
@app.route('/dd-admin/roles_list')
# @login_required
def roles_list():
return json.dumps(app.admin.get_roles()), 200, {'Content-Type': 'application/json'}
@app.route('/dd-admin/groups')
# @login_required
def groups():
return render_template('pages/groups.html', title="Groups", nav="Groups")
@app.route('/dd-admin/groups_list')
# @login_required
def groups_list():
return json.dumps(app.admin.get_groups()), 200, {'Content-Type': 'application/json'}
@app.route('/dd-admin/external')
# @login_required
def external():
return render_template('pages/external.html', title="External", nav="External")
@app.route('/dd-admin/external_users_list')
# @login_required
def external_list():
return json.dumps(app.admin.get_external_users()), 200, {'Content-Type': 'application/json'}

View File

13
admin/src/admin/yarn.lock Normal file
View File

@ -0,0 +1,13 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
font-linux@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/font-linux/-/font-linux-0.6.1.tgz#d586f46336b7da06ea3b7f10f7aee2b6346eed4f"
integrity sha1-1Yb0Yza32gbqO38Q967itjRu7U8=
gentelella@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/gentelella/-/gentelella-1.4.0.tgz#b3d15fd9c40c6ea47dc7f36290c8f89aee95efc5"
integrity sha512-lp54+y6bwSLHF6KMstW2jD6oqV68vLMnFqMqATRp5a/8Tp52NYly7Q+5FZIMaNKATrb3EKx+BN/bKpaZ4BYLEw==

124
admin/src/moodle_saml.py Normal file
View File

@ -0,0 +1,124 @@
#!/usr/bin/env python
# coding=utf-8
import time, os
from datetime import datetime, timedelta
import pprint
import logging as log
import traceback
import yaml, json
import psycopg2
from admin.lib.postgres import Postgres
from admin.lib.keycloak import Keycloak
import string, random
app={}
app['config']={}
class MoodleSaml():
def __init__(self):
ready=False
while not ready:
try:
self.pg=Postgres('isard-apps-postgresql','moodle',app.config['MOODLE_POSTGRES_USER'],app.config['MOODLE_POSTGRES_PASSWORD'])
ready=True
except:
log.warning('Could not connect to moodle database. Retrying...')
time.sleep(2)
log.info('Connected to moodle database.')
ready=False
while not ready:
try:
privatekey_pass=self.get_privatekey_pass()
log.warning("The key: "+str(privatekey_pass))
if privatekey_pass.endswith(app.config['DOMAIN']):
app.config.setdefault('MOODLE_SAML_PRIVATEKEYPASS', privatekey_pass)
ready=True
except:
# print(traceback.format_exc())
log.warning('Could not get moodle site identifier. Retrying...')
time.sleep(2)
log.info('Got moodle site identifier.')
ready=False
while not ready:
try:
with open(os.path.join(app.root_path, "../moodledata/saml2/moodle."+app.config['DOMAIN']+".crt"),"r") as crt:
app.config.setdefault('SP_CRT', crt.read())
ready=True
except IOError:
log.warning('Could not get moodle SAML2 crt certificate. Retrying...')
time.sleep(2)
except:
log.error(traceback.format_exc())
log.info('Got moodle srt certificate.')
ready=False
while not ready:
try:
with open(os.path.join(app.root_path, "../moodledata/saml2/moodle."+app.config['DOMAIN']+".pem"),"r") as pem:
app.config.setdefault('SP_PEM', pem.read())
ready=True
except IOError:
log.warning('Could not get moodle SAML2 pem certificate. Retrying...')
time.sleep(2)
log.info('Got moodle pem certificate.')
## This seems related to the fact that the certificate generated the first time does'nt work.
## And when regenerating the certificate de privatekeypass seems not to be used and instead it
## will use always this code as filename: 0f635d0e0f3874fff8b581c132e6c7a7
## As this bug I'm not able to solve, the process is:
## 1.- Bring up moodle and regenerate certificates on saml2 plugin in plugins-authentication
## 2.- Execute this script
## 3.- Cleanup all caches in moodle (Development tab)
# with open(os.path.join(app.root_path, "../moodledata/saml2/"+app.config['MOODLE_SAML_PRIVATEKEYPASS'].replace("moodle."+app.config['DOMAIN'],'')+'.idp.xml'),"w") as xml:
# xml.write(self.parse_idp_metadata())
with open(os.path.join(app.root_path, "../moodledata/saml2/0f635d0e0f3874fff8b581c132e6c7a7.idp.xml"),"w") as xml:
xml.write(self.parse_idp_metadata())
log.info('Written SP file on moodledata.')
self.activate_saml_plugin()
self.set_moodle_saml_plugin()
self.set_keycloak_moodle_saml_plugin()
def activate_saml_plugin(self):
## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php
return self.pg.update("""UPDATE "mdl_config" SET value = 'email,saml2' WHERE "name" = 'auth'""")
def get_privatekey_pass(self):
return self.pg.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2]
def parse_idp_metadata(self):
keycloak=Keycloak()
rsa=keycloak.get_server_rsa_key()
keycloak=None
return '<md:EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Name="urn:keycloak"><md:EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://sso.'+app.config['DOMAIN']+'/auth/realms/master"><md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor use="signing"><ds:KeyInfo><ds:KeyName>'+rsa['name']+'</ds:KeyName><ds:X509Data><ds:X509Certificate>'+rsa['certificate']+'</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.'+app.config['DOMAIN']+'/auth/realms/master/protocol/saml/resolve" index="0"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.'+app.config['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.'+app.config['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.'+app.config['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.'+app.config['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.'+app.config['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.'+app.config['DOMAIN']+'/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.'+app.config['DOMAIN']+'/auth/realms/master/protocol/saml"/></md:IDPSSODescriptor></md:EntityDescriptor></md:EntitiesDescriptor>'
def set_keycloak_moodle_saml_plugin(self):
keycloak=Keycloak()
keycloak.add_moodle_client()
keycloak=None
def set_moodle_saml_plugin(self):
config={'idpmetadata': self.parse_idp_metadata(),
'certs_locked': '1',
'duallogin': '0',
'idpattr': 'username',
'autocreate': '1',
'saml_role_siteadmin_map': 'admin',
'saml_role_coursecreator_map': 'teacher',
'saml_role_manager_map': 'manager',
'field_map_email': 'email',
'field_map_firstname': 'givenName',
'field_map_lastname': 'surname'}
for name in config.keys():
self.pg.update("""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'auth_saml2' AND "name" = '%s'""" % (config[name],name))
self.pg.update("""INSERT INTO "mdl_auth_saml2_idps" ("metadataurl", "entityid", "activeidp", "defaultidp", "adminidp", "defaultname", "displayname", "logo", "alias", "whitelist") VALUES
('xml', 'https://sso.%s/auth/realms/master', 1, 0, 0, 'Login via SAML2', '', NULL, NULL, NULL);""" % (app.config['DOMAIN']))

10
admin/src/start.py Normal file
View File

@ -0,0 +1,10 @@
#!flask/bin/python
# coding=utf-8
from gevent import monkey
monkey.patch_all()
from admin import app
if __name__ == '__main__':
app.run(host='0.0.0.0', port=9000, debug=False) #, logger=logger, engineio_logger=engineio_logger)

View File

@ -0,0 +1,102 @@
import os,time,requests,json,getpass,pprint
import traceback
from keycloak_client_exc import *
class ApiClient():
def __init__(self,realm='master'):
##server=os.environ['KEYCLOAK_HOST']
server='isard-sso-keycloak'
self.base_url="http://"+server+":8080/auth/realms/"+realm
self.headers={"Content-Type": "application/x-www-form-urlencoded"}
self.payload={'username':'admin',
'password':'keycloakkeycloak',
'grant_type':'password',
'client_id':'admin-cli'}
self.token=self.get_token()
self.admin_url="http://"+server+":8080/auth/admin/realms/"+realm
# /admin/realms/${KEYCLOAK_REALM}/users/${$USER_ID}"
self.admin_headers={"Accept": "application/json",
"Authorization": "Bearer "+self.token}
def get_token(self):
path="/protocol/openid-connect/token"
resp = requests.post(self.base_url+path, data=self.payload, headers=self.headers)
if resp.status_code == 200: return json.loads(resp.text)['access_token']
print(" URL: "+self.base_url+path)
print("STATUS CODE: "+str(resp.status_code))
print(" RESPONSE: "+resp.text)
exit(1)
def get(self,path,status_code=200,data={},params={}):
resp = requests.get(self.admin_url+path, data=data, params=params, headers=self.admin_headers)
if resp.status_code == status_code: return json.loads(resp.text)
print(" URL: "+self.admin_url+path)
print("STATUS CODE: "+str(resp.status_code))
print(" RESPONSE: "+resp.text)
raise
def post(self,path,status_code=200,data={},params={},json={}):
resp = requests.post(self.admin_url+path, data=data, params=params, json=json, headers=self.admin_headers)
#if resp.status_code == status_code: return True
print(" URL: "+self.admin_url+path)
print("STATUS CODE: "+str(resp.status_code))
print(" RESPONSE: "+resp.text)
if resp.status_code == 409: raise keycloakUsernameEmailExists
raise keycloakError
class KeycloakClient():
def __init__(self,realm='master'):
## REFERENCE: https://www.keycloak.org/docs-api/13.0/rest-api/index.html
self.api=ApiClient()
def get_users(self,username=False,exact=True):
path='/users'
if not username: return self.api.get(path)
return self.api.get(path,params={"username":username,'exact':exact})
def add_user(self,username,first,last,email,password):
user={"firstName":first,
"lastName":last,
"email":last,
"enabled":"true",
"username":username,
"credentials":[{"type":"password",
"value":password,
"temporary":False}]}
try:
self.api.post('/users',status_code=201,json=user)
return True
except keycloakExists:
print('Username or email already exists')
except:
traceback.format_exc()
return False
def get_groups(self,name=False):
path='/groups'
if not name: return self.api.get(path)
return self.api.get(path,params={"name":name})
def add_group(self,name,subgroups=False):
group={"name":name}
try:
self.api.post('/groups',status_code=201,json=group)
return True
except keycloakExists:
print('Group name already exists')
except:
traceback.format_exc()
return False
kapi=KeycloakClient()
# print('GET USERS')
# pprint.pprint(kapi.get_users())
# print('GET ADMIN USER')
# pprint.pprint(kapi.get_users(username='admin'))
# print('ADD USER')
# print(kapi.add_user('pepito','Pepito','Grillo','info@info.com','añlsdkjf'))
# print('GET GROUPS')
# pprint.pprint(kapi.get_groups())
print('ADD GROUP')
pprint.pprint(kapi.add_group('pepito'))

View File

@ -0,0 +1,5 @@
class keycloakError(Exception):
pass
class keycloakExists(Exception):
pass

View File

@ -0,0 +1,34 @@
from keycloak import KeycloakOpenID
# Configure client
keycloak_openid = KeycloakOpenID(server_url="http://isard-sso-keycloak:8080/auth/",
client_id="admin-cli",
realm_name="master",
client_secret_key="secret")
# Get WellKnow
config_well_know = keycloak_openid.well_know()
# Get Token
token = keycloak_openid.token("admin", "keycloakkeycloak")
#token = keycloak_openid.token("user", "password", totp="012345")
print(token)
from keycloak import KeycloakAdmin
keycloak_admin = KeycloakAdmin(server_url="http://isard-sso-keycloak:8080/auth/",
username='admin',
password='keycloakkeycloak',
realm_name="master",
verify=True)
# Add user
new_user = keycloak_admin.create_user({"email": "example@example.com",
"username": "example@example.com",
"enabled": True,
"firstName": "Example",
"lastName": "Example"})
print(new_user)
user_id_keycloak = keycloak_admin.get_user_id("admin")
print(user_id_keycloak)

View File

@ -0,0 +1,28 @@
version: '3.7'
services:
dd-admin:
container_name: dd-admin
build:
context: ${BUILD_ROOT_PATH}
dockerfile: admin/docker/Dockerfile
target: production
args: ## DEVELOPMENT
SSH_ROOT_PWD: ${IPA_ADMIN_PWD}
SSH_PORT: 2022
networks:
- isard_net
ports:
- "2022:22"
- "9000:9000"
restart: unless-stopped
volumes:
- /etc/localtime:/etc/localtime:ro
- ${BUILD_ROOT_PATH}/admin/src:/admin # Revome in production
- ${BUILD_ROOT_PATH}/custom:/admin/custom #:ro in production
- ${DATA_FOLDER}/avatars:/admin/avatars:ro
- ${DATA_FOLDER}/moodle/saml2:/admin/moodledata/saml2:rw
env_file:
- .env
environment:
- VERIFY="false" # In development do not verify certificates
command: sleep infinity

View File

@ -19,7 +19,7 @@ services:
restart: unless-stopped
networks:
- isard_net
ports:
# ports:
# - published: 7039
# target: 7039
env_file:

View File

@ -0,0 +1,27 @@
version: '3.7'
services:
dd-backups:
container_name: dd-backups
image: prodrigestivill/postgres-backup-local
restart: always
volumes:
- /etc/localtime:/etc/localtime:ro
- ${BACKUP_FOLDER}/sso:/backups
# links:
# - ${KEYCLOAK_DB_ADDR}:${KEYCLOAK_DB_ADDR}
# depends_on:
# - ${KEYCLOAK_DB_ADDR}
environment:
- TZ="Europe/Madrid"
- POSTGRES_HOST=${KEYCLOAK_DB_ADDR}
- POSTGRES_DB=${KEYCLOAK_DB_DATABASE},moodle,nextcloud
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_EXTRA_OPTS=-Z9 --schema=public --blobs
- SCHEDULE=@every 0h30m00s
- BACKUP_KEEP_DAYS=7
- BACKUP_KEEP_WEEKS=4
- BACKUP_KEEP_MONTHS=6
- HEALTHCHECK_PORT=81
networks:
- isard_net

View File

@ -26,14 +26,9 @@ frontend website
mode http
bind :80
redirect scheme https if !{ ssl_fc }
# http-request set-header SSL_CLIENT_CERT %[ssl_c_der,base64]
http-request del-header ssl_client_cert unless { ssl_fc_has_crt }
http-request set-header ssl_client_cert -----BEGIN\ CERTIFICATE-----\ %[ssl_c_der,base64]\ -----END\ CERTIFICATE-----\ if { ssl_fc_has_crt }
http-request del-header ssl_client_cert unless { 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
#cookie JSESSIONID prefix nocache
#use_backend be_hydra if { path_beg /hydra }
#use_backend be_hydra if { path_beg /oauth2 }
acl is_nextcloud hdr_beg(host) nextcloud.
acl is_moodle hdr_beg(host) moodle.
@ -52,6 +47,7 @@ frontend website
use_backend be_oof if is_oof
use_backend be_wp if is_wp
use_backend be_etherpad if is_pad
use_backend be_admin if is_sso { path_beg /dd-admin }
use_backend be_sso if is_sso
use_backend be_ipa if is_ipa
use_backend be_api if is_api
@ -65,7 +61,6 @@ backend be_api
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
server api isard-sso-api:80 check port 80 inter 5s rise 2 fall 10 resolvers mydns init-addr none
#server api isard-sso-api:7039 check port 7039 inter 5s rise 2 fall 10 resolvers mydns init-addr none
backend be_ipa
mode http
@ -77,7 +72,7 @@ backend be_ipa
backend be_sso
mode http
option httpclose
option httpclose
#option http-server-close
option forwardfor
acl existing-x-forwarded-host req.hdr(X-Forwarded-Host) -m found
@ -86,6 +81,14 @@ backend be_sso
http-request add-header X-Forwarded-Proto https unless existing-x-forwarded-proto
server keycloak isard-sso-keycloak:8080 check port 8080 inter 5s rise 2 fall 10 resolvers mydns init-addr none
backend be_admin
mode http
acl existing-x-forwarded-host req.hdr(X-Forwarded-Host) -m found
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
server dd-admin dd-admin:9000 check port 9000 inter 5s rise 2 fall 10 resolvers mydns init-addr none
## APPS
backend be_moodle
mode http
@ -145,7 +148,7 @@ backend be_wp
stats uri /haproxy
stats realm Haproxy\ Statistics
stats refresh 5s
#stats auth staging:pep1n1ll0
#stats auth staging:mypassword
#acl authorized http_auth(AuthUsers)
#stats http-request auth unless authorized
timeout connect 5000ms

196
docs/SAML_README.md Normal file
View File

@ -0,0 +1,196 @@
# SAML Authentication
The authentication it is done with SAML plugins in the apps agains a central Keycloak identity provider (IdP) server. As the integration of client SAML with IdP is quite tedious this document describes how to integrate some of this apps into your keycloak server.
## Keycloak
The identity provider administration interface can be reached at https://sso.<yourdomain>. There you can add client apps, roles, groups, users, mappers, etc... Please read documentation at: https://www.keycloak.org/documentation
## Applications
In this document we will cover **Moodle**, **Nextcloud** and **Wordpress** SAML configuration plugins.
### Moodle
Install SAML plugin and follow this steps in **Moodle**:
1. Activate SAML2 plugin at the **Extensions** -> **Authentication** in Moodle. You should click on the eye. Then enter de *configuration* link.
2. Click on the **Regenerate SP certificate** button. Optionally set up your desired certificate data and accept. You will need to get back to SAML2 configuration plugin afterwards. The direct link page is: https://moodle.<yourdomain>/auth/saml2/regenerate.php
3. Click on the **Lock down** certificate button and accept. This will avoid SAML2 plugin to regenerate the certificate each time we restart Moodle (why has this annoying behaviour?)
4. Download **SAML2 Service Provider** xml and save it in a file (better right click and save to file). The direct link page is: https://moodle.<yourdomain>/auth/saml2/sp/metadata.php
Now go to your *keycloak* admin (https://sso.<yourdomain>.) and:
1. At **Clients** menú go to **create** new client and import the moodle **SAML2 Service Provider** xml and accept. The direct link is: https://sso.<yourdomain>/auth/admin/master/console/#/create/client/master
2. Now go to **Mappers** tab in this client and add this builtins:
1. *email*
2. *givenName*
3. *surname*
3. Now create **Custom Mapper** for username:
1. Name: `username`
Mapper Type: *User Property*
Property: `username`
Friendly Name: `username`
SAML Attribute Name: `username`
SAML Attribute NameFormat: *Basic*
4. Now *copy* the keycloak **SAML IdP xml data
5. Copy keycloak **SAML 2.0 Identity Provider Metadata** xml from **Realm Settings** and paste it into moodle SAML2 plugin **IdP metadata xml OR public xml URL** textbox. Note that you should copy it from a text editor, not the content from the browser view! The content should be one-liner!!! No break lines even it is an xml file.
Now go back to Moodle (if not already there as you just copied the IdP metadata into de SAML2 plugin) and set up this options in the SAML2 plugin:
- **Dual login**: No. But you should be aware that setting to *No* will automatically redirect from now on the logins to Keycloak SSO login page and you won't be able to access your moodle instance as admin if you don't use the alternate url **/login/index.php?saml=off**. This is what you want when you have checked that the SAML2 plugin is working as expected.
- **Mapping idP**: username
- **Auto create users**: Yes
- **Role Mapping**: Configure the same roles you created in keycloak for users. For example:
- Site administrators: admin
- Course creator: coursecreator
- Manager: manager
- **Data mapping**: With at least this fields moodle will skip user profile fill the first time he logs in as this three are the only required. You can set up more mappers in keycloak for this moodle SP and map it to the rest of the fields.
1. *email*
2. *givenName*
3. *surname*
If everything went ok you should now be redirected to Keycloak SSO login page and you can log in into your moodle with the users you already created in Keycloak and with the role assigned to them.
### NEXTCLOUD
TODO: Does not map email nor friendlyname (display name). Also not tested to add quota field in keycloak and map it to nextcloud.
(all credits to this hell set up goes to: https://janikvonrotz.ch/2020/04/21/configure-saml-authentication-for-nextcloud-with-keycloack/, RESPECT)
BEAWARE: of the good programmers, but very bad designers (I empathyze with them but...) The SAML plugin in nextcloud has some greyed out texts that are links! Damn, who did that! You don't realize that are links that open a lot of options that need to be filled in!
1. Copy the Keycloak realm RSA certificate from **Realm Settings** menu, in the **Keys** tab by clicking in the **Certificate** button of **RSA** (not the public key). It will show it in a modal form, just select it, copy and save it into file for later use.
2. Generate Nextcloud SP keys. Sorry, this step is needed. So you should generate your own ones. If you don't know how to install this just enter the nextcloud container (docker exec -ti isard-apps-nextcloud-app /bin/sh) and run the command there and copy the contents elsewhere with the *private.key* and *public.cert* names.
1. **openssl req -nodes -new -x509 -keyout private.key -out public.cert**
3. Install SAML plugin. Select **Integrated configuration** at first config page.
4. Configure at: https://nextcloud.<yourdomain>/settings/admin/saml or going to the **Settings** options in user menú.
1. **General**
1. Input box: **Attribute to map the UID to**: username
2. Input box: **Optional display name**: *anything you want as this won't be shown when we activate the direct redirect to keycloak SSO login.
2. **Service Provider Data**: WARNING: Copy them with BEGIN/END tags! WARNING nº2: You should click on the greyed out link, that doesn't seem a link, to the end of the line.
1. **x509**: public.key (generated before)
2. **Private key**: private.key (generated before)
3. **Identity Provider Data**
1. **Identifier of the IdP**: https://sso.<yourdomain>/auth/realms/master
2. **URL target of the IdP**: https://sso.<yourdomain>/auth/realms/master/protocol/saml
3. **URL Location of the IdP SLO request**: https://sso.<domain>/auth/realms/poc/protocol/saml
4. **Public X.509 certificate**: (The *RSA Certificate* from keycloak at the very first step number 1).
4. **Attribute mapping**
1. **email**: email
2. **user groups**: Role
5. **Security Settings** (check only this options)
1. **Signatures and encryption offered**
1. <samlp: AuthnRequest>
2. <samlp: logoutRequest>
3. <samlp: logoutResponse>
2. **Signatures and encryption required**
1. <samlp: Response>
2. <samlp: Assertion> [Metadata of the SP will offer this info]
6. Click and save the xml metadata from the bottom page button **Download XML metadata**.
If you reached this point you are almost done with Nextcloud SAML configuration if the *annoying* live update of this plugin page shows at the bottom the **Download XML metadata** with no errors. Now let's go back to **Keycloak admin console** and finish configuration.
1. At **Clients** menú go to **create** new client and import the nextcloud **SAML2 Service Provider** xml that you just downloaded and accept. The direct link is: https://sso.<yourdomain>/auth/admin/master/console/#/create/client/master.
1. My guru that I referenced at the beginning of this documentation says that you should set the **Client SAML Endpoint** to https://sso.<yourdomain>/auth/realms/master prior to accepting the uploaded xml data. I tested that this is not really needed.
2. Now go to **Mappers** tab in this client and create **Custom Mapper** fields: NOTE: ONLY USERNAME and ROLES WORKING. Nextcloud doesn't get email
1. Name: `username`
Mapper Type: *User Property*
Property: `username`
Friendly Name: `username`
SAML Attribute Name: `username`
SAML Attribute NameFormat: *Basic*
2. Add builtins:Name: `email`
Mapper Type: *User Property*
Property: `email`
Friendly Name: `email`
SAML Attribute Name: `email`
SAML Attribute NameFormat: *Basic*
3. Name: `roles`
Mapper Type: *Role List*
Role attribute name: `Roles`
Friendly Name: `Roles`
SAML Attribute NameFormat: *Basic*
Single Role Attribute: *On*
3. Then the main role seem to be from a single role attribute that should be set up in...
1. **Client scopes** menu
1. role_list
1. Mappers tab
1. role list
1. Single Role Attribute, that should be checked.
Now you should be able to test your Keycloak users/roles against nextcloud. If you need to access as admin into nextcloud again you should use this end url for your nextcloud domain: **/login?direct=1**
#### Debug SAML plugin
Trust me, this is important to be here as many settings/options/checkboxes can be missconfigured if you were not really awaken today.
Edit in the outside mount volume (/opt/digitaldemocratic/db/src/nextcloud/config/config.php) and restart nextcloud container (docker restart isard-apps-nextcloud-app):
```
<?php
$CONFIG = array (
'debug' => true,
...
```
### WORDPRESS
If you already set up Moodle and Nextcloud SAML plugins you are already the master of the universe and I will go faster at describing this one as you want to finish this.
NOTE: Client Id in Keycloak has to be exactly **php-saml**. It could be modified and set up at wordpress saml plugin (but better don't do experiments)
1. Install **OneLogin SAML plugin**
2. **STATUS**
1. Enable
3. **IDENTITY PROVIDER SETTINGS**
1. IdP ENTITY ID: Anything you want as won't be shown because we will redirect all logins to Keycloak SSO.
1. **SSO Service Url**: https://sso.digitaldemocratic.net/auth/realms/master/protocol/saml
2. **SLO Service Url**: https://sso.digitaldemocratic.net/auth/realms/master/protocol/saml
3. **X.509 Certificate**: Copy the Certificate (not the Public key) from the keycloak realm (https://sso.digitaldemocratic.net/auth/admin/master/console/#/realms/master/keys) without the begin/end lines in the cert.
4. **OPTIONS**
1. Create user if not exists
2. Update user data
3. Force SAML login (To access as admin look for the url at the end of this part)
4. Single Log Out
5. Match Wordpress account by: username ???
5. **ATTRIBUTE MAPPING**
1. Username: username
2. Email: email
4. First Name: givenName
5. Last Name: sn
6. Role: Role
6. **ROLE MAPPING**
1. Administrator: admins
2. Editor: managers
3. Author: coursecreators
...
4. Multiple role values...: true
7. **CUSTOMIZE ACTIONS AND LINKS**
1. Stay in WordPress after SLO
8. **ADVANCED SETTINGS**
1. Sign AuthnRequest
2. Sign LogoutRequest
3. Sign LogoutResponse
4. Service Provider X.509 Certificate & Service Provider Private Key: Generate both and paste it without the begin/end lines:
openssl req -nodes -new -x509 -keyout private.key -out public.cert
9. Download **Service Provider metadata** from top and add it to keycloak clients menu
10. Keycloak client mappers:
1. Name: `username`
Mapper Type: *User Property*
Property: `username`
Friendly Name: `username`
SAML Attribute Name: `username`
SAML Attribute NameFormat: *Basic*
2. Add builtins:Name: `email`
Mapper Type: *User Property*
Property: `email`
Friendly Name: `email`
SAML Attribute Name: `email`
SAML Attribute NameFormat: *Basic*
3. Name: `roles`
Mapper Type: *Role List*
Role attribute name: `Roles`
Friendly Name: `Roles`
SAML Attribute NameFormat: *Basic*
Single Role Attribute: *On*
To access as an admin again you should use the url: https://wp.<domain>/wp-login.php?normal

50
docs/develop.md Normal file
View File

@ -0,0 +1,50 @@
# SAML2 Plugin development environment (moodle)
NOTE: This could be completely outdated as the current version mounts moodle html source outside the container.
All this have to be done as the image doesn't let html external folder mounted as volume (image doesn't use root)
1. Start isard-apps-moodle docker with default config. Wait for moodle to be ready.
2. Enter docker and copy html to external folder:
1. docker exec -ti isard-apps-moodle /bin/sh
2. cd /var/www/html
3. mkdir /var/www/moodledata/html
4. cp -R . /var/www/moodledata/html
Now you open two terminals:
- docker exec -ti isard-apps-moodle /bin/sh
- docker logs isard-apps-moodle --follow
You can edit saml2 plugin from host (/opt/isard-office/moodle/data/html/auth/saml2) and copy it to the current html folder:
- /var/www/html/auth/saml2 $ cp -R /var/www/moodledata/html/auth/saml2/* .
When you finish developing get the new plugin code into a zip and in the correct src folder:
- cd ${DATA_FOLDER}/moodle/data/html/auth/ && zip -r <src git path>/isard-office/docker/moodle/plugins/auth_saml2.zip saml2
## SAML2 Plugin src
The modified source files are:
- auth.php (lines 570 to 595, sync_roles call added)
- locallib.php (function sync_roles)
Also the common plugin setup fields and lang strings:
- settings.php (lines 314 to 333)
- lang/en/auth_saml2.php (lines 24 to 29)
# Big Blue Button
TODO:
- Audio fails with docker in iptables=false and managed by firewalld in masquerade mode. This is due to coturn that doesn't like being behind nat.
- Firewalld + BBB: As BBB will 'take' the host interface we should:
- Remove /etc/docker/daemon.json the iptables: false
- firewall-cmd --zone=public --remove-interface=docker0 --permanent
- firewall-cmd --zone=docker --add-interface=docker0 --permanent
- Now the docker applies iptables as per container. Note that we don't have control over this from now on.
- Scalelite
- Script creation of base debian with virt-install and then replicate BBBs (partially done)

BIN
docs/img/classrooms.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

BIN
docs/img/cloud_storage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

3
docs/index.md Normal file
View File

@ -0,0 +1,3 @@
# Welcome
This site is built by [MkDocs+Gitlab](https://gitlab.com/pages/mkdocs). You can [browse its source code](https://gitlab.com/digitaldemocratic/digitaldemocratic).

View File

@ -0,0 +1,21 @@
pgbackups:
container_name: Backup
image: prodrigestivill/postgres-backup-local
restart: always
volumes:
- ./backup:/backups
links:
- db:db
depends_on:
- db
environment:
- POSTGRES_HOST=db
- POSTGRES_DB=${DB_NAME}
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_EXTRA_OPTS=-Z9 --schema=public --blobs
- SCHEDULE=@every 0h30m00s
- BACKUP_KEEP_DAYS=7
- BACKUP_KEEP_WEEKS=4
- BACKUP_KEEP_MONTHS=6
- HEALTHCHECK_PORT=81

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCtDWj9al/3Cd2N
UM1sP+8KXiSiPC+BIpEX5ep5G59qV1XRrjSykl8tJ0Na/57T18ZrViBS0RPiS83y
zKKpplcN0SDbNqz6yBDnt7CWH5m+XJGbwfWBDvfVX6wfe26ONekQpOnzsZxHHIiR
cVvE3qk4oVQuDZqaGZSykJoaCJUuou+k2gI+rF2lZl69sOkqCkKATc5D1r2IDlmz
Rj85k+QT/2r3iyQf2QaDnOGmiubEBaf3iwHaX0G9MQFb0YX1XHSrfXPkINa5Gf4I
W2DvV6BrT5Szbu732NQUBnak5Thsa/Ttg3gcJI176EV4Jb9z6AGl7NJd5iAQr/ZK
rrK10shBAgMBAAECggEAb0xeqA3QZqwbqBW96M89yGdAHG+lBeLbeolOwlF3uAcv
lMn77pWhTQMhmNcqqYjvfn1IELuTlEm4zV27iG0JNEO6ZALIQgqGhOFpW0Q7t2kF
5S1b3oNn9f2wUBcsxZ36pc/LAAbNQhch5pkHspiaMWfhIjVxp4aoUigaVIAMoo7s
wGVK/7N+aw6IlziVOmsexBSkf6LMvykjJCH0RfNuIHXhqdVbaz1jZkdbJF6frny9
n3c9gkFWc3+GnHTr4suehJnUOk7BWq3qBmwGexaIjSxNhucVlWNdiYBidTMq9vP6
jfu+ueoWyADu+W+0075PHY+co2sdqguiOixdc2SpIQKBgQDmi/bB8uPrdHBchyhq
ZhHsErUQ6CxtugVQxtl1bwc/UOeSof0nBRrmYadPWmU4zfTD8wqHgfLJhlSRstWD
wj4VP6rm8ipn1qwBG/vo/uTf/BojG4KctVDXLIRepaYozGGQSMxOetXWz6osI2Q/
j3ChMR+A1FK7m5iilfEzbeAY5QKBgQDAKHXCzGGbqHhvIzM1LB38hSpkV6xTaaog
tkQ9IOFuCkNOfZ8oNfvUMOhiHJGFaE6MdcfJntK4MzqvT5vi9YYTnzoTQTdo9cvq
zD5ZnQQOsy3AF8Fj+sjiW4/eaQNq9VsmZuWoSjhSfsP9jGXGeGxUcY3o0r07ifbO
u6LcY9ZILQKBgDWoO60WL8+8EO6oElL5IJC2JegicTy0f8o2DaSUS7aDyPHKu9Wa
DZGzBrKkUkyvOplkdn3lU7Ftjz89xQ3eZn6hi9AmapIyV2QGtFGdCX3L+fVT0MlS
Nddup/wzR4HVV5uyJcLaOey99lhBgHJ+mvMZMMDWKc86PoMQrMuQdgi1AoGAFmdh
O4IKy1Q8HnETMlrfcCayh5p1PBBwxnmZwSrJPcQyjr80xEJvBxFgtrev+8bqiZPd
5FMBLHrEl9YHTdHkfPsukTokVLd7u/duOZKF+5TGe8QJRzfhHgsg3gSOYnUS2Ipc
sl9c67ld7nzlDNvTfZDzw7Z2W6+9N+NGnL2DKU0CgYEAvrCbTagcCsbUj9YWhTLv
/p4J3/dZBNN5LAbHiSSfRiUK7BuiiNWJK7DR3jYIi4+y1k/xLCIqWV6fCPPXfq/T
nbkA1ttELLBF4FR0VrrTaN7U3uKqbuykeIAqT1ScqHfyNprUJeqH4aXtjzqvqnpT
fF3mBBj72rRoXUWNqFWf808=
-----END PRIVATE KEY-----

View File

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUec9JN0Cpq6ZiBtErme8aB/PQW3MwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTA0MDgwNTAyMTJaFw0yMTA1
MDgwNTAyMTJaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQCtDWj9al/3Cd2NUM1sP+8KXiSiPC+BIpEX5ep5G59q
V1XRrjSykl8tJ0Na/57T18ZrViBS0RPiS83yzKKpplcN0SDbNqz6yBDnt7CWH5m+
XJGbwfWBDvfVX6wfe26ONekQpOnzsZxHHIiRcVvE3qk4oVQuDZqaGZSykJoaCJUu
ou+k2gI+rF2lZl69sOkqCkKATc5D1r2IDlmzRj85k+QT/2r3iyQf2QaDnOGmiubE
Baf3iwHaX0G9MQFb0YX1XHSrfXPkINa5Gf4IW2DvV6BrT5Szbu732NQUBnak5Ths
a/Ttg3gcJI176EV4Jb9z6AGl7NJd5iAQr/ZKrrK10shBAgMBAAGjUzBRMB0GA1Ud
DgQWBBQ5e/VWrY796POIJ5VNO8USQbgzoDAfBgNVHSMEGDAWgBQ5e/VWrY796POI
J5VNO8USQbgzoDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAc
j4rzQfvjCDgB+CkmukFBYD2eOnNiaUWJABwU5s1cFWda9B2CoCAzHOHj2Sdi7Sso
5PBZ9LmrdNGzbJioUVUyEG72aRUxlxaJgAQKd7QN34Oic8Q+JlwKdP4Xm+mGk0T4
Q2esz56gbEsm9qIX7XHFbCt1gNVh+VjjB0ZRR1kPIhvdX2a/4X5lFVgr3dyYxz57
7ODc/gz6lTgnG71h9CEBuWA404BGZ1aGY1oj+FpZBYLoybqaAQrgtQUGM5KOTvGG
KmBraRPWyjVHGKrbWn9oUG8zBxKrz9Nzcu8lV9NDEB3xSqLo6qXvFrTdT58SPe6O
/VPef2l6eL0enjI62aqO
-----END CERTIFICATE-----

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
#docker exec -t isard-apps-postgresql pg_dumpall -c -U admin > dump_`date +%d-%m-%Y"_"%H_%M_%S`.sql
docker exec -t isard-apps-postgresql pg_dumpall -c -U admin | gzip > ./dump_$(date +"%Y-%m-%d_%H_%M_%S").gz

View File

@ -0,0 +1,3 @@
#docker exec -t isard-apps-postgresql pg_dumpall -c -U admin > dump_`date +%d-%m-%Y"_"%H_%M_%S`.sql
docker exec -t isard-apps-postgresql pg_dumpall -c -U admin | gzip > ./dump_$(date +"%Y-%m-%d_%H_%M_%S").gz
gunzip < $1 | docker exec -i isard-apps-postgresql psql -U admin -d $2

View File

@ -0,0 +1,10 @@
cp ../.env .
source .env
docker-compose stop isard-apps-nextcloud-app
docker rm isard-apps-nextcloud-app
rm -rf /opt/isard-office/nextcloud
echo "DROP DATABASE nextcloud;" | docker exec -i isard-apps-postgresql psql -U admin
docker-compose up -d isard-apps-nextcloud-app
docker-compose restart isard-apps-nextcloud-nginx
docker logs isard-apps-nextcloud-app --follow

View File

@ -0,0 +1,9 @@
cp ../.env .
source .env
docker-compose stop isard-apps-wordpress
docker rm isard-apps-wordpress
rm -rf /opt/isard-office/wordpress
echo "DROP DATABASE wordpress;" | docker exec -i isard-apps-postgresql psql -U admin
docker-compose up -d isard-apps-wordpress
docker-compose restart isard-apps-wordress-cli
docker logs isard-apps-wordpress --follow

View File

@ -0,0 +1 @@
gunzip < $1 | docker exec -i isard-apps-postgresql psql -U admin -d $2

View File

@ -0,0 +1,108 @@
{
"id" : "master",
"realm" : "master",
"displayName" : "Keycloak",
"displayNameHtml" : "<div class=\"kc-logo-text\"><span>Keycloak</span></div>",
"notBefore" : 0,
"revokeRefreshToken" : false,
"refreshTokenMaxReuse" : 0,
"accessTokenLifespan" : 60,
"accessTokenLifespanForImplicitFlow" : 900,
"ssoSessionIdleTimeout" : 1800,
"ssoSessionMaxLifespan" : 36000,
"ssoSessionIdleTimeoutRememberMe" : 0,
"ssoSessionMaxLifespanRememberMe" : 0,
"offlineSessionIdleTimeout" : 2592000,
"offlineSessionMaxLifespanEnabled" : false,
"offlineSessionMaxLifespan" : 5184000,
"clientSessionIdleTimeout" : 0,
"clientSessionMaxLifespan" : 0,
"clientOfflineSessionIdleTimeout" : 0,
"clientOfflineSessionMaxLifespan" : 0,
"accessCodeLifespan" : 60,
"accessCodeLifespanUserAction" : 300,
"accessCodeLifespanLogin" : 1800,
"actionTokenGeneratedByAdminLifespan" : 43200,
"actionTokenGeneratedByUserLifespan" : 300,
"enabled" : true,
"sslRequired" : "external",
"registrationAllowed" : false,
"registrationEmailAsUsername" : false,
"rememberMe" : false,
"verifyEmail" : false,
"loginWithEmailAllowed" : true,
"duplicateEmailsAllowed" : false,
"resetPasswordAllowed" : false,
"editUsernameAllowed" : false,
"bruteForceProtected" : false,
"permanentLockout" : false,
"maxFailureWaitSeconds" : 900,
"minimumQuickLoginWaitSeconds" : 60,
"waitIncrementSeconds" : 60,
"quickLoginCheckMilliSeconds" : 1000,
"maxDeltaTimeSeconds" : 43200,
"failureFactor" : 30,
"defaultRoles" : [ "offline_access", "uma_authorization" ],
"requiredCredentials" : [ "password" ],
"otpPolicyType" : "totp",
"otpPolicyAlgorithm" : "HmacSHA1",
"otpPolicyInitialCounter" : 0,
"otpPolicyDigits" : 6,
"otpPolicyLookAheadWindow" : 1,
"otpPolicyPeriod" : 30,
"otpSupportedApplications" : [ "FreeOTP", "Google Authenticator" ],
"webAuthnPolicyRpEntityName" : "keycloak",
"webAuthnPolicySignatureAlgorithms" : [ "ES256" ],
"webAuthnPolicyRpId" : "",
"webAuthnPolicyAttestationConveyancePreference" : "not specified",
"webAuthnPolicyAuthenticatorAttachment" : "not specified",
"webAuthnPolicyRequireResidentKey" : "not specified",
"webAuthnPolicyUserVerificationRequirement" : "not specified",
"webAuthnPolicyCreateTimeout" : 0,
"webAuthnPolicyAvoidSameAuthenticatorRegister" : false,
"webAuthnPolicyAcceptableAaguids" : [ ],
"webAuthnPolicyPasswordlessRpEntityName" : "keycloak",
"webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ],
"webAuthnPolicyPasswordlessRpId" : "",
"webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified",
"webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified",
"webAuthnPolicyPasswordlessRequireResidentKey" : "not specified",
"webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified",
"webAuthnPolicyPasswordlessCreateTimeout" : 0,
"webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false,
"webAuthnPolicyPasswordlessAcceptableAaguids" : [ ],
"browserSecurityHeaders" : {
"contentSecurityPolicyReportOnly" : "",
"xContentTypeOptions" : "nosniff",
"xRobotsTag" : "none",
"xFrameOptions" : "SAMEORIGIN",
"contentSecurityPolicy" : "frame-src 'self'; frame-ancestors *; object-src 'none';",
"xXSSProtection" : "1; mode=block",
"strictTransportSecurity" : "max-age=31536000; includeSubDomains"
},
"smtpServer" : { },
"loginTheme" : "liiibrelite",
"accountTheme" : "account-avatar",
"eventsEnabled" : false,
"eventsListeners" : [ "jboss-logging" ],
"enabledEventTypes" : [ ],
"adminEventsEnabled" : false,
"adminEventsDetailsEnabled" : false,
"identityProviders" : [ ],
"identityProviderMappers" : [ ],
"internationalizationEnabled" : false,
"supportedLocales" : [ "" ],
"browserFlow" : "browser",
"registrationFlow" : "registration",
"directGrantFlow" : "direct grant",
"resetCredentialsFlow" : "reset credentials",
"clientAuthenticationFlow" : "clients",
"dockerAuthenticationFlow" : "docker auth",
"attributes" : {
"clientOfflineSessionMaxLifespan" : "0",
"clientSessionIdleTimeout" : "0",
"clientSessionMaxLifespan" : "0",
"clientOfflineSessionIdleTimeout" : "0"
},
"userManagedAccessAllowed" : false
}

View File

@ -0,0 +1,95 @@
{
"id" : "a92d5417-92b6-4678-9cb9-51bc0edcee8c",
"clientId" : "https://moodle.[[DOMAIN]]/auth/saml2/sp/metadata.php",
"surrogateAuthRequired" : false,
"enabled" : true,
"alwaysDisplayInConsole" : false,
"clientAuthenticatorType" : "client-secret",
"redirectUris" : [ "https://moodle.[[DOMAIN]]/auth/saml2/sp/saml2-acs.php/moodle.[[DOMAIN]]" ],
"webOrigins" : [ "https://moodle.[[DOMAIN]]" ],
"notBefore" : 0,
"bearerOnly" : false,
"consentRequired" : false,
"standardFlowEnabled" : true,
"implicitFlowEnabled" : false,
"directAccessGrantsEnabled" : false,
"serviceAccountsEnabled" : false,
"publicClient" : false,
"frontchannelLogout" : true,
"protocol" : "saml",
"attributes" : {
"saml.force.post.binding" : "true",
"saml.encrypt" : "true",
"saml_assertion_consumer_url_post" : "https://moodle.[[DOMAIN]]/auth/saml2/sp/saml2-acs.php/moodle.[[DOMAIN]]",
"saml.server.signature" : "true",
"saml.server.signature.keyinfo.ext" : "false",
"saml.signing.certificate" : "[[SIGNING_CERTIFICATE]]",
"saml_single_logout_service_url_redirect" : "https://moodle.[[DOMAIN]]/auth/saml2/sp/saml2-logout.php/moodle.[[DOMAIN]]",
"saml.signature.algorithm" : "RSA_SHA256",
"saml_force_name_id_format" : "false",
"saml.client.signature" : "true",
"saml.encryption.certificate" : "[[ENCRYPTION_CERTIFICATE]]",
"saml.authnstatement" : "true",
"saml_name_id_format" : "username",
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#"
},
"authenticationFlowBindingOverrides" : { },
"fullScopeAllowed" : true,
"nodeReRegistrationTimeout" : -1,
"protocolMappers" : [ {
"id" : "9296daa3-4fc4-4b80-b007-5070f546ae13",
"name" : "X500 surname",
"protocol" : "saml",
"protocolMapper" : "saml-user-property-mapper",
"consentRequired" : false,
"config" : {
"attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"user.attribute" : "lastName",
"friendly.name" : "surname",
"attribute.name" : "urn:oid:2.5.4.4"
}
}, {
"id" : "ccecf6e4-d20a-4211-b67c-40200a6b2c5d",
"name" : "username",
"protocol" : "saml",
"protocolMapper" : "saml-user-property-mapper",
"consentRequired" : false,
"config" : {
"attribute.nameformat" : "Basic",
"user.attribute" : "username",
"friendly.name" : "username",
"attribute.name" : "username"
}
}, {
"id" : "53858403-eba2-4f6d-81d0-cced700b5719",
"name" : "X500 givenName",
"protocol" : "saml",
"protocolMapper" : "saml-user-property-mapper",
"consentRequired" : false,
"config" : {
"attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"user.attribute" : "firstName",
"friendly.name" : "givenName",
"attribute.name" : "urn:oid:2.5.4.42"
}
}, {
"id" : "20034db5-1d0e-4e66-b815-fb0440c6d1e2",
"name" : "X500 email",
"protocol" : "saml",
"protocolMapper" : "saml-user-property-mapper",
"consentRequired" : false,
"config" : {
"attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"user.attribute" : "email",
"friendly.name" : "email",
"attribute.name" : "urn:oid:1.2.840.113549.1.9.1"
}
} ],
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ],
"access" : {
"view" : true,
"configure" : true,
"manage" : true
}
}

View File

@ -0,0 +1,82 @@
, {
"id" : "bef873f0-2079-4876-8657-067de27d01b7",
"clientId" : "https://nextcloud.[[DOMAIN]]/apps/user_saml/saml/metadata",
"surrogateAuthRequired" : false,
"enabled" : true,
"alwaysDisplayInConsole" : false,
"clientAuthenticatorType" : "client-secret",
"redirectUris" : [ "https://nextcloud.[[DOMAIN]]/apps/user_saml/saml/acs" ],
"webOrigins" : [ "https://nextcloud.[[DOMAIN]]" ],
"notBefore" : 0,
"bearerOnly" : false,
"consentRequired" : false,
"standardFlowEnabled" : true,
"implicitFlowEnabled" : false,
"directAccessGrantsEnabled" : false,
"serviceAccountsEnabled" : false,
"publicClient" : false,
"frontchannelLogout" : true,
"protocol" : "saml",
"attributes" : {
"saml.assertion.signature" : "true",
"saml.force.post.binding" : "true",
"saml_assertion_consumer_url_post" : "https://nextcloud.[[DOMAIN]]/apps/user_saml/saml/acs",
"saml.server.signature" : "true",
"saml.server.signature.keyinfo.ext" : "false",
"saml.signing.certificate" : "[[SIGNING_CERTIFICATE]]",
"saml_single_logout_service_url_redirect" : "https://nextcloud.[[DOMAIN]]/apps/user_saml/saml/sls",
"saml.signature.algorithm" : "RSA_SHA256",
"saml_force_name_id_format" : "false",
"saml.client.signature" : "true",
"saml.authnstatement" : "true",
"saml_name_id_format" : "username",
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#"
},
"authenticationFlowBindingOverrides" : { },
"fullScopeAllowed" : true,
"nodeReRegistrationTimeout" : -1,
"protocolMappers" : [ {
"id" : "e8e4acff-da2b-46aa-8bdb-ba42171671d6",
"name" : "username",
"protocol" : "saml",
"protocolMapper" : "saml-user-attribute-mapper",
"consentRequired" : false,
"config" : {
"attribute.nameformat" : "Basic",
"user.attribute" : "username",
"friendly.name" : "username",
"attribute.name" : "username"
}
}, {
"id" : "28206b59-757b-4e3c-81cb-0b6053b1fd3d",
"name" : "email",
"protocol" : "saml",
"protocolMapper" : "saml-user-property-mapper",
"consentRequired" : false,
"config" : {
"attribute.nameformat" : "Basic",
"user.attribute" : "email",
"friendly.name" : "email",
"attribute.name" : "email"
}
}, {
"id" : "e51e04b9-f71a-42de-819e-dd9285246ada",
"name" : "Roles",
"protocol" : "saml",
"protocolMapper" : "saml-role-list-mapper",
"consentRequired" : false,
"config" : {
"single" : "true",
"attribute.nameformat" : "Basic",
"friendly.name" : "Roles",
"attribute.name" : "Roles"
}
} ],
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ],
"access" : {
"view" : true,
"configure" : true,
"manage" : true
}
}

View File

@ -0,0 +1,81 @@
{
"id" : "630601f8-25d1-4822-8741-c93affd2cd84",
"clientId" : "php-saml",
"surrogateAuthRequired" : false,
"enabled" : true,
"alwaysDisplayInConsole" : false,
"clientAuthenticatorType" : "client-secret",
"redirectUris" : [ "https://wp.[[DOMAIN]]/wp-login.php?saml_acs" ],
"webOrigins" : [ "https://wp.[[DOMAIN]]" ],
"notBefore" : 0,
"bearerOnly" : false,
"consentRequired" : false,
"standardFlowEnabled" : true,
"implicitFlowEnabled" : false,
"directAccessGrantsEnabled" : false,
"serviceAccountsEnabled" : false,
"publicClient" : false,
"frontchannelLogout" : true,
"protocol" : "saml",
"attributes" : {
"saml.force.post.binding" : "true",
"saml_assertion_consumer_url_post" : "https://wp.[[DOMAIN]]/wp-login.php?saml_acs",
"saml.server.signature" : "true",
"saml.server.signature.keyinfo.ext" : "false",
"saml.signing.certificate" : "[[SIGNING_CERTIFICATE]]",
"saml_single_logout_service_url_redirect" : "https://wp.[[DOMAIN]]/wp-login.php?saml_sls",
"saml.signature.algorithm" : "RSA_SHA256",
"saml_force_name_id_format" : "false",
"saml.client.signature" : "true",
"saml.authnstatement" : "true",
"saml_name_id_format" : "username",
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#"
},
"authenticationFlowBindingOverrides" : { },
"fullScopeAllowed" : true,
"nodeReRegistrationTimeout" : -1,
"protocolMappers" : [ {
"id" : "72c6175e-bd07-4c27-abd6-4e4ae38d834b",
"name" : "username",
"protocol" : "saml",
"protocolMapper" : "saml-user-attribute-mapper",
"consentRequired" : false,
"config" : {
"attribute.nameformat" : "Basic",
"user.attribute" : "username",
"friendly.name" : "username",
"attribute.name" : "username"
}
}, {
"id" : "abd6562f-4732-4da9-987f-b1a6ad6605fa",
"name" : "roles",
"protocol" : "saml",
"protocolMapper" : "saml-role-list-mapper",
"consentRequired" : false,
"config" : {
"single" : "true",
"attribute.nameformat" : "Basic",
"friendly.name" : "Roles",
"attribute.name" : "Role"
}
}, {
"id" : "50aafb71-d91c-4bc7-bb60-e1ae0222aab3",
"name" : "email",
"protocol" : "saml",
"protocolMapper" : "saml-user-property-mapper",
"consentRequired" : false,
"config" : {
"attribute.nameformat" : "Basic",
"user.attribute" : "email",
"friendly.name" : "email",
"attribute.name" : "email"
}
} ],
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ],
"access" : {
"view" : true,
"configure" : true,
"manage" : true
}
}

20
sysadm/01-sshd.conf Normal file
View File

@ -0,0 +1,20 @@
[DEFAULT]
# Ban IP/hosts for 24 hour ( 24h*3600s = 86400s):
bantime = 600
# An ip address/host is banned if it has generated "maxretry" during the last "findtime" seconds.
findtime = 60
maxretry = 3
# "ignoreip" can be a list of IP addresses, CIDR masks or DNS hosts. Fail2ban
# will not ban a host which matches an address in this list. Several addresses
# can be defined using space (and/or comma) separator. For example, add your
# static IP address that you always use for login such as 103.1.2.3
#ignoreip = 127.0.0.1/8 ::1 103.1.2.3
# Call iptables to ban IP address
#banaction = iptables-multiport
# Enable sshd protection
[sshd]
enabled = true

View File

@ -0,0 +1,18 @@
apt-get remove docker docker-engine docker.io containerd runc
apt-get install -y \
apt-transport-https \
ca-certificates \
curl \
gnupg-agent \
software-properties-common
curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -
add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/debian \
buster \
stable"
apt-get update -y
apt-get install -y docker-ce docker-ce-cli containerd.io
apt install python3-pip -y
pip3 install docker-compose

35
sysadm/debian_firewall.sh Executable file
View File

@ -0,0 +1,35 @@
apt install firewalld fail2ban -y
# Fixes bug in iptables 1.8
echo "deb http://deb.debian.org/debian buster-backports main" > /etc/apt/sources.list.d/buster-backports.list
apt update
apt install -y iptables -t buster-backports
#echo "Setting iptables to not use nf_tables"
update-alternatives --set iptables /usr/sbin/iptables-legacy
update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
#update-alternatives --set iptables /usr/sbin/iptables-legacy
echo "Setting docker to not open ports automatically..."
echo '{ "iptables": false }' > /etc/docker/daemon.json
cp 01* /etc/fail2ban/fail2ban.d/
echo "Setting firewalld to use iptables..."
sed -i 's/FirewallBackend=nftables/FirewallBackend=iptables/g' /etc/firewalld/firewalld.conf
rm -rf /etc/firewalld/zones/*
firewall-cmd --permanent --zone=public --change-interface=docker0
firewall-cmd --permanent --zone=public --add-masquerade
# This assumes a typical port 22 for ssh. If not just set it here with --add-port
firewall-cmd --permanent --zone=public --add-service=ssh
## OUTSIDE WORLD NEEDED PORTS FOR ISARDVDI WEB and VIEWERS
firewall-cmd --permanent --zone=public --add-port=443/tcp
firewall-cmd --permanent --zone=public --add-port=80/tcp
## LETS RESTART EVERYTHING.
systemctl restart firewalld
systemctl stop docker
systemctl start docker
systemctl restart fail2ban