Added admin
parent
7a0176a6e3
commit
757beff4e2
|
@ -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]
|
||||
|
|
|
@ -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" ]
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
ssh-keygen -A
|
||||
cd /admin
|
||||
python3 start.py &
|
||||
/usr/sbin/sshd -D -e -f /etc/ssh/sshd_config
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
@ -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']
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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!")
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"font-linux": "^0.6.1",
|
||||
"gentelella": "^1.4.0"
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 |
|
@ -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>');
|
||||
})
|
||||
}
|
|
@ -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>'
|
||||
};
|
||||
}},
|
||||
]
|
||||
} );
|
||||
})
|
|
@ -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']],
|
||||
} );
|
||||
});
|
|
@ -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>'
|
||||
};
|
||||
}},
|
||||
]
|
||||
} );
|
||||
});
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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">×</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>
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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'}
|
|
@ -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==
|
|
@ -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']))
|
|
@ -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)
|
|
@ -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'))
|
|
@ -0,0 +1,5 @@
|
|||
class keycloakError(Exception):
|
||||
pass
|
||||
|
||||
class keycloakExists(Exception):
|
||||
pass
|
|
@ -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)
|
|
@ -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
|
|
@ -19,7 +19,7 @@ services:
|
|||
restart: unless-stopped
|
||||
networks:
|
||||
- isard_net
|
||||
ports:
|
||||
# ports:
|
||||
# - published: 7039
|
||||
# target: 7039
|
||||
env_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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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)
|
Binary file not shown.
After Width: | Height: | Size: 294 KiB |
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
|
@ -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).
|
|
@ -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
|
|
@ -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-----
|
|
@ -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
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
gunzip < $1 | docker exec -i isard-apps-postgresql psql -U admin -d $2
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
Loading…
Reference in New Issue