Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
# Docker Compose Deployments for development use
The docker-compose deployments in this directory are for developement purposes only. They are
not actively maintained and not to be used in production.
@@ -0,0 +1,161 @@
## Basic Settings ##
# Define the docker compose log driver used.
# Defaults to local
LOG_DRIVER=
# If you're on an internet facing server, comment out following line.
# It skips certificate validation for various parts of КуСфера and is
# needed when self signed certificates are used.
INSECURE=true
## Features ##
COMPOSE_FILE=docker-compose.yml:traefik.yml:keycloak.yml:ldap-server.yml
## Traefik Settings ##
# Note: Traefik is always enabled and can't be disabled.
# Serve Traefik dashboard.
# Defaults to "false".
TRAEFIK_DASHBOARD=
# Domain of Traefik, where you can find the dashboard.
# Defaults to "traefik.qsfera.test"
TRAEFIK_DOMAIN=
# Basic authentication for the traefik dashboard.
# Defaults to user "admin" and password "admin" (written as: "admin:$2y$05$KDHu3xq92SPaO3G8Ybkc7edd51pPLJcG1nWk3lmlrIdANQ/B6r5pq").
# To create user:password pair, it's possible to use this command:
# echo $(htpasswd -nB user) | sed -e s/\\$/\\$\\$/g
TRAEFIK_BASIC_AUTH_USERS=
# Email address for obtaining LetsEncrypt certificates.
# Needs only be changed if this is a public facing server.
TRAEFIK_ACME_MAIL=
# Set to the following for testing to check the certificate process:
# "https://acme-staging-v02.api.letsencrypt.org/directory"
# With staging configured, there will be an SSL error in the browser.
# When certificates are displayed and are emitted by # "Fake LE Intermediate X1",
# the process went well and the envvar can be reset to empty to get valid certificates.
TRAEFIK_ACME_CASERVER=
# Enable the Traefik ACME (Automatic Certificate Management Environment) for automatic SSL certificate management.
TRAEFIK_SERVICES_TLS_CONFIG="tls.certresolver=letsencrypt"
# Enable Traefik to use local certificates.
#TRAEFIK_SERVICES_TLS_CONFIG="tls=true"
# You also need to provide a config file in ./config/traefik/dynamic/certs.yml
# Example:
# cat ./config/traefik/dynamic/certs.yml
# tls:
# certificates:
# - certFile: /certs/qsfera.test.crt
# keyFile: /certs/qsfera.test.key
# stores:
# - default
#
# The certificates need to copied into ./certs/, the absolute path inside the container is /certs/.
# You can also use TRAEFIK_CERTS_DIR=/path/on/host to set the path to the certificates directory.
# Enable the access log for Traefik by setting the following variable to true.
TRAEFIK_ACCESS_LOG=
# Configure the log level for Traefik.
# Possible values are "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL" and "PANIC". Default is "ERROR".
TRAEFIK_LOG_LEVEL=
## КуСфера Settings ##
# The qsfera container image.
# For production releases: "qsfera/qsfera"
# For rolling releases: "qsfera/qsfera-rolling"
# Defaults to production if not set otherwise
OC_DOCKER_IMAGE=qsfera/qsfera-rolling
# The qsfera container version.
# Defaults to "latest" and points to the latest stable tag.
OC_DOCKER_TAG=
# Domain of qsfera, where you can find the frontend.
# Defaults to "cloud.qsfera.test"
OC_DOMAIN=
# Demo users should not be created on a production instance,
# because their passwords are public. Defaults to "false".
# If demo users is set to "true", the following user accounts are created automatically:
# alan, mary, margaret, dennis and lynn - the password is 'demo' for all.
DEMO_USERS=
# Admin Password for the КуСфера admin user.
# NOTE: This is only needed when using the built-in LDAP server (idm).
# If you are using an external LDAP server, the admin password is managed by the LDAP server.
# NOTE: This variable needs to be set before the first start of КуСфера. Changes to this variable after the first start will be IGNORED.
# If not set, qsfera will not work properly. The container will be restarting.
# After the first initialization, the admin password can only be changed via the КуСфера User Settings UI or by using the КуСфера CLI.
# Documentation: https://docs.qsfera.eu/docs/admin/resources/common-issues#-change-admin-password-set-in-env
INITIAL_ADMIN_PASSWORD=
# Define the qsfera loglevel used.
#
LOG_LEVEL=
# Define the kind of logging.
# The default log can be read by machines.
# Set this to true to make the log human readable.
# LOG_PRETTY=true
#
# Define the qsfera storage location. Set the paths for config and data to a local path.
# Ensure that the configuration and data directories are owned by the user and group with ID 1000:1000.
# This matches the default user inside the container and avoids permission issues when accessing files.
# Note that especially the data directory can grow big.
# Leaving it default stores data in docker internal volumes.
# OC_CONFIG_DIR=/your/local/qsfera/config
# OC_DATA_DIR=/your/local/qsfera/data
### Compose Configuration ###
# Path separator for supplemental compose files specified in COMPOSE_FILE.
COMPOSE_PATH_SEPARATOR=:
### Ldap Settings ###
# LDAP is always needed for КуСфера to store user data as there is no relational database.
# The built-in LDAP server should used for testing purposes or small installations only.
# For production installations, it is recommended to use an external LDAP server.
# We are using OpenLDAP as the default LDAP server because it is proven to be stable and reliable.
# This LDAP configuration is known to work with КуСфера and provides a blueprint for
# configuring an external LDAP server based on other products like Microsoft Active Directory or other LDAP servers.
#
# Password of LDAP bind user "cn=admin,dc=qsfera,dc=eu". Defaults to "admin"
LDAP_BIND_PASSWORD=
# The LDAP server also creates an qsfera admin user dn: uid=admin,ou=users,dc=qsfera,dc=eu
# The initial password for this user is "admin"
# NOTE: This password can only be set once, if you want to change it later, you have to use the КуСфера User Settings UI.
# If you changed the password and lost it, you need to execute the following LDAP query to reset it:
# enter the ldap-server container with `docker compose exec ldap-server sh`
# and run the following command to change the password:
# ldappasswd -H ldap://127.0.0.1:1389 -D "cn=admin,dc=qsfera,dc=eu" -W "uid=admin,ou=users,dc=qsfera,dc=eu"
# You will be prompted for the LDAP bind password.
# The output should provide you a new password for the admin user.
### Keycloak Settings ###
# Keycloak is an open-source identity and access management solution.
# We are using Keycloak as the default identity provider on production installations.
# It can be used to federate authentication with other identity providers like
# Microsoft Entra ID, ADFS or other SAML/OIDC providers.
# The use of Keycloak as bridge between КуСфера and other identity providers creates more control over the
# authentication process, the allowed clients and the session management.
# Keycloak also manages the Role Based Access Control (RBAC) for КуСфера.
# Keycloak can be used in two different modes:
# 1. Autoprovisioning: New users are automatically created in qsfera when they log in for the first time.
# 2. Shared User Directory: Users are created in Keycloak and can be used in КуСфера immediately
# because the LDAP server is connected to both Keycloak and КуСфера.
# Only use one of the two modes at a time.
## Autoprovisioning Mode ##
# Use together with idm/external-idp.yml
# If you want to use a keycloak for local testing, you can use testing/external-keycloak.yml and testing/ldap-manager.yml
# Domain of your Identity Provider.
IDP_DOMAIN=
# IdP Issuer URL, which is used to identify the Identity Provider.
# We need the complete URL, including the protocol (http or https) and the realm.
# Example: "https://keycloak.qsfera.test/realms/qsfera"
IDP_ISSUER_URL=
# Url of the account edit page from your Identity Provider.
IDP_ACCOUNT_URL=
## Shared User Directory Mode ##
# Use together with idm/ldap-keycloak.yml and traefik/ldap-keycloak.yml
# Domain for Keycloak. Defaults to "keycloak.qsfera.test".
KEYCLOAK_DOMAIN=
# Admin user login name. Defaults to "kcadmin".
KEYCLOAK_ADMIN=
# Admin user login password. Defaults to "admin".
KEYCLOAK_ADMIN_PASSWORD=
# Keycloak Database username. Defaults to "keycloak".
KC_DB_USERNAME=
# Keycloak Database password. Defaults to "keycloak".
KC_DB_PASSWORD=
@@ -0,0 +1,49 @@
# Development/Test Deployment for a multi-tenacy setup
The docker compose files in this directory are derived from the
qsfera-compose project and can be used to deploy a Development or Testing
environment for a multi-tenancy setup of КуСфера. It consists of the
following services:
* `provisioning`: The КуСфера graph service deployed in a standalone mode. It
is configured to provide the libregraph education API for managing tenants
and users. The `ldap-server`service (see below) is used to store the tenants
and users.
* `ldap-server`: An OpenLDAP server that is used by the provisioning service to
store tenants and users. Used by the КуСфера services as the user directory
(for looking up users and searching for sharees).
* `keycloak`: The OpenID Connect Provider used for authenticating users. The
pre-loaded realm is configured to add `tenantid` claim into the identity and
access tokens. It's also currently consuming user from the `ldap-server`
(this federation will likely go away in the future and is optional for future
configurations).
* `qsfera`: The КуСфера configured so that is hides users from different
tenants from each other.
To deploy the setup, run:
```bash
docker compose -f docker-compose.yml -f keycloak.yml -f ldap-server.yml -f traefik.yml up
```
Once deployed you can use the `initialize_users.go` to create a couple of example
tenants and some users in each tenant:
* Tenant `Famous Coders` with users `dennis` and `grace`
* Tenant `Scientists` with users `einstein` and `marie`
The passwords for the users is set to `demo` in keycloak
```
> go run initialize_users.go
Created tenant: Famous Coders with id fc58e19a-3a2a-4afc-90ec-8f94986db340
Created user: Dennis Ritchie with id ee1e14e7-b00b-4eec-8b03-a6bf0e29c77c
Created user: Grace Hopper with id a29f3afd-e4a3-4552-91e8-cc99e26bffce
Created tenant: Scientists with id 18406c53-e2d6-4e83-98b6-a55880eef195
Created user: Albert Einstein with id 12023d37-d6ce-4f19-a318-b70866f265ba
Created user: Marie Curie with id 30c3c825-c37d-4e85-8195-0142e4884872
Setting password for user: grace
Setting password for user: marie
Setting password for user: dennis
Setting password for user: einstein
```
@@ -0,0 +1,63 @@
{
"clientId": "КуСфераAndroid",
"name": "КуСфера Android App",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"oc://android.qsfera.eu"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "oc://android.qsfera.eu",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}
@@ -0,0 +1,64 @@
{
"clientId": "КуСфераDesktop",
"name": "КуСфера Desktop Client",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"http://127.0.0.1",
"http://localhost"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "+",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}
@@ -0,0 +1,63 @@
{
"clientId": "КуСфераIOS",
"name": "КуСфера iOS App",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"oc://ios.qsfera.eu"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "oc://ios.qsfera.eu",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}
@@ -0,0 +1,66 @@
{
"clientId": "Cyberduck",
"name": "Cyberduck",
"description": "File transfer utility client",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"x-cyberduck-action:oauth",
"x-mountainduck-action:oauth"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}
@@ -0,0 +1,74 @@
{
"clientId": "web",
"name": "КуСфера Web App",
"description": "",
"rootUrl": "{{OC_URL}}",
"adminUrl": "{{OC_URL}}",
"baseUrl": "",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"{{OC_URL}}/",
"{{OC_URL}}/oidc-callback.html",
"{{OC_URL}}/oidc-silent-redirect.html"
],
"webOrigins": [
"{{OC_URL}}"
],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.url": "{{OC_URL}}/backchannel_logout",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}
@@ -0,0 +1,8 @@
#!/bin/bash
printenv
# replace qsfera domain and LDAP password in keycloak realm import
mkdir /opt/keycloak/data/import
sed -e "s/cloud.qsfera.test/${OC_DOMAIN}/g" -e "s/ldap-admin-password/${LDAP_ADMIN_PASSWORD:-admin}/g" /opt/keycloak/data/import-dist/qsfera-realm.json > /opt/keycloak/data/import/qsfera-realm.json
# run original docker-entrypoint
/opt/keycloak/bin/kc.sh "$@"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,38 @@
:root {
--pf-global--primary-color--100: #e2baff;
--pf-global--primary-color--200: #e2baff;
--pf-global--primary-color--dark-100: #e2baff;
--pf-global--Color--light-100: #20434f;
}
@font-face {
font-family: QSfera;
src: url('../fonts/QSfera500-Regular.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: QSfera;
src: url('../fonts/QSfera750-Bold.woff2') format('woff2');
font-weight: bold;
font-style: normal;
}
body {
font-family: "QSfera", "Open Sans", Helvetica, Arial, sans-serif;
background: url(../img/background.png) no-repeat center !important;
background-size: cover !important;
}
.kc-logo-text {
background-image: url(../img/logo.svg) !important;
background-size: contain;
width: 400px;
margin: 0 !important;
}
#kc-header-wrapper{
display: flex;
justify-content: center;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@@ -0,0 +1,15 @@
<svg width="170" height="35" viewBox="0 0 170 35" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="КуСфера">
<g transform="scale(0.1944444444)">
<path fill="#061D2B" d="M34,12h112C158,12 168,22 168,34v112C168,158 158,168 146,168H34C22,168 12,158 12,146V34C12,22 22,12 34,12z"/>
<path fill="#123E55" fill-opacity="0.74" d="M12,45C48,19 87,20 121,48C145,68 158,68 168,60V34C168,22 158,12 146,12H34C22,12 12,22 12,34z"/>
<path fill="#19B8C9" fill-opacity="0.18" d="M12,126C46,105 82,104 114,124C139,139 156,139 168,128V146C168,158 158,168 146,168H34C22,168 12,158 12,146z"/>
<path fill="none" stroke="#20D6E6" stroke-linecap="round" stroke-width="10" d="M75,47C100,29 138,39 153,70C167,100 151,135 121,147"/>
<path fill="none" stroke="#9AF8FF" stroke-linecap="round" stroke-width="4" d="M81,45C101,35 130,39 146,62"/>
<path fill="#21D1D6" d="M48,52h25v50h-25z"/>
<path fill="#F8FCFF" d="M48,102h25v42h-25z"/>
<path fill="#18BFD0" d="M77,94L111,55H143L99,103z"/>
<path fill="#F8FCFF" d="M77,104L100,82L148,144H116z"/>
<path fill="#9DF8FF" fill-opacity="0.45" d="M48,52h25v8h-25z"/>
</g>
<text x="47" y="24.5" fill="#20D6E6" font-family="Inter, Segoe UI, Arial, sans-serif" font-size="22" font-weight="700">КуСфера</text>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,19 @@
document.addEventListener("DOMContentLoaded", function () {
const setLogoUrl = (url) => {
const logoTextSelector = document.querySelector(".kc-logo-text");
if (!logoTextSelector) {
return
}
const link = document.createElement("a");
link.href = url;
link.target = "_blank";
const parent = logoTextSelector.parentNode;
parent.insertBefore(link, logoTextSelector);
link.appendChild(logoTextSelector);
}
setLogoUrl('')
});
@@ -0,0 +1,5 @@
parent=keycloak
import=common/keycloak
styles=css/login.css css/theme.css
scripts=js/script.js
@@ -0,0 +1,9 @@
#!/bin/bash
echo "Running custom LDAP entrypoint script..."
if [ ! -f /opt/bitnami/openldap/share/openldap.key ]
then
openssl req -x509 -newkey rsa:4096 -keyout /opt/bitnami/openldap/share/openldap.key -out /opt/bitnami/openldap/share/openldap.crt -sha256 -days 365 -batch -nodes
fi
# run original docker-entrypoint
/opt/bitnami/scripts/openldap/entrypoint.sh "$@"
@@ -0,0 +1,20 @@
dn: dc=qsfera,dc=eu
objectClass: organization
objectClass: dcObject
dc: qsfera
o: qsfera
dn: ou=users,dc=qsfera,dc=eu
objectClass: organizationalUnit
ou: users
dn: cn=admin,dc=qsfera,dc=eu
objectClass: inetOrgPerson
objectClass: person
cn: admin
sn: admin
uid: ldapadmin
dn: ou=tenants,dc=qsfera,dc=eu
objectClass: organizationalUnit
ou: tenants
@@ -0,0 +1,20 @@
dn: uid=admin,ou=users,dc=qsfera,dc=eu
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: admin
givenName: Admin
sn: Admin
cn: admin
displayName: Admin
description: An admin for this КуСфера instance.
mail: admin@example.org
userPassword:: e1NTSEF9UWhmaFB3dERydTUydURoWFFObDRMbzVIckI3TkI5Nmo==
dn: cn=administrators,ou=groups,dc=qsfera,dc=eu
objectClass: groupOfNames
objectClass: top
cn: administrators
description: КуСфера Administrators
member: uid=admin,ou=users,dc=qsfera,dc=eu
@@ -0,0 +1,47 @@
directives:
child-src:
- '''self'''
connect-src:
- '''self'''
- 'blob:'
- 'https://${COMPANION_DOMAIN|companion.qsfera.test}${TRAEFIK_PORT_HTTPS}/'
- 'wss://${COMPANION_DOMAIN|companion.qsfera.test}${TRAEFIK_PORT_HTTPS}/'
- 'https://raw.githubusercontent.com/qsfera-eu/awesome-apps/'
- 'https://${IDP_DOMAIN|keycloak.qsfera.test}${TRAEFIK_PORT_HTTPS}/'
- 'https://update.qsfera.eu/'
default-src:
- '''none'''
font-src:
- '''self'''
frame-ancestors:
- '''self'''
frame-src:
- '''self'''
- 'blob:'
- 'https://embed.diagrams.net/'
# In contrary to bash and docker the default is given after the | character
- 'https://${COLLABORA_DOMAIN|collabora.qsfera.test}${TRAEFIK_PORT_HTTPS}/'
# This is needed for the external-sites web extension when embedding sites
- 'https://docs.qsfera.eu'
img-src:
- '''self'''
- 'data:'
- 'blob:'
- 'https://raw.githubusercontent.com/qsfera-eu/awesome-apps/'
- 'https://tile.openstreetmap.org/'
# In contrary to bash and docker the default is given after the | character
- 'https://${COLLABORA_DOMAIN|collabora.qsfera.test}${TRAEFIK_PORT_HTTPS}/'
manifest-src:
- '''self'''
media-src:
- '''self'''
object-src:
- '''self'''
- 'blob:'
script-src:
- '''self'''
- '''unsafe-inline'''
- 'https://${IDP_DOMAIN|keycloak.qsfera.test}${TRAEFIK_PORT_HTTPS}/'
style-src:
- '''self'''
- '''unsafe-inline'''
@@ -0,0 +1,40 @@
# This adds four additional routes to the proxy. Forwarding
# request on '/carddav/', '/caldav/' and the respective '/.well-knwown'
# endpoints to the radicale container and setting the required headers.
additional_policies:
- name: default
routes:
- endpoint: /caldav/
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /caldav
- endpoint: /.well-known/caldav
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /caldav
- endpoint: /carddav/
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /carddav
- endpoint: /.well-known/carddav
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /carddav
# To enable the radicale web UI add this rule.
# "unprotected" is True because the Web UI itself ask for
# the password.
# Also set "type" to "internal" in the config/radicale/config
# - endpoint: /caldav/.web/
# backend: http://radicale:5232/
# unprotected: true
# skip_x_access_token: true
# additional_headers:
# - X-Script-Name: /caldav
@@ -0,0 +1,72 @@
set -e
printenv
# Function to add arguments to the command
add_arg() {
TRAEFIK_CMD="$TRAEFIK_CMD $1"
}
# Initialize the base command
TRAEFIK_CMD="traefik"
# Base Traefik arguments (from your existing configuration)
add_arg "--log.level=${TRAEFIK_LOG_LEVEL:-ERROR}"
# enable dashboard
add_arg "--api.dashboard=true"
# define entrypoints
add_arg "--entryPoints.http.address=:80"
add_arg "--entryPoints.http.http.redirections.entryPoint.to=https"
add_arg "--entryPoints.http.http.redirections.entryPoint.scheme=https"
add_arg "--entryPoints.https.address=:443"
# change default timeouts for long-running requests
# this is needed for webdav clients that do not support the TUS protocol
add_arg "--entryPoints.https.transport.respondingTimeouts.readTimeout=12h"
add_arg "--entryPoints.https.transport.respondingTimeouts.writeTimeout=12h"
add_arg "--entryPoints.https.transport.respondingTimeouts.idleTimeout=3m"
# docker provider (get configuration from container labels)
add_arg "--providers.docker.endpoint=unix:///var/run/docker.sock"
add_arg "--providers.docker.exposedByDefault=false"
# access log
add_arg "--accessLog=${TRAEFIK_ACCESS_LOG:-false}"
add_arg "--accessLog.format=json"
add_arg "--accessLog.fields.headers.names.X-Request-Id=keep"
# Add Let's Encrypt configuration if enabled
if [ "${TRAEFIK_SERVICES_TLS_CONFIG}" = "tls.certresolver=letsencrypt" ]; then
echo "Configuring Traefik with Let's Encrypt..."
add_arg "--certificatesResolvers.letsencrypt.acme.email=${TRAEFIK_ACME_MAIL:-example@example.org}"
add_arg "--certificatesResolvers.letsencrypt.acme.storage=/certs/acme.json"
add_arg "--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=http"
add_arg "--certificatesResolvers.letsencrypt.acme.caserver=${TRAEFIK_ACME_CASERVER:-https://acme-v02.api.letsencrypt.org/directory}"
fi
# Add local certificate configuration if enabled
if [ "${TRAEFIK_SERVICES_TLS_CONFIG}" = "tls=true" ]; then
echo "Configuring Traefik with local certificates..."
add_arg "--providers.file.directory=/etc/traefik/dynamic"
add_arg "--providers.file.watch=true"
fi
# Warning if neither certificate method is enabled
if [ "${TRAEFIK_SERVICES_TLS_CONFIG}" != "tls=true" ] && [ "${TRAEFIK_SERVICES_TLS_CONFIG}" != "tls.certresolver=letsencrypt" ]; then
echo "WARNING: Neither Let's Encrypt nor local certificates are enabled."
echo "HTTPS will not work properly without certificate configuration."
fi
# Add any custom arguments from environment variable
if [ -n "${TRAEFIK_CUSTOM_ARGS}" ]; then
echo "Adding custom Traefik arguments: ${TRAEFIK_CUSTOM_ARGS}"
TRAEFIK_CMD="$TRAEFIK_CMD $TRAEFIK_CUSTOM_ARGS"
fi
# Add any additional arguments passed to the script
for arg in "$@"; do
add_arg "$arg"
done
# Print the final command for debugging
echo "Starting Traefik with command:"
echo "$TRAEFIK_CMD"
# Execute Traefik
exec $TRAEFIK_CMD
@@ -0,0 +1,119 @@
---
services:
# КуСфера instance configured for multi-tenancy using keycloak as identity provider
# The graph service is setup to consume users via the CS3 API.
qsfera:
image: ${OC_DOCKER_IMAGE:-qsfera/qsfera-rolling}:${OC_DOCKER_TAG:-latest}
# changelog: https://github.com/qsfera/server/tree/main/changelog
# release notes: https://docs.qsfera.eu/qsfera_release_notes.html
networks:
qsfera-net:
entrypoint:
- /bin/sh
# run qsfera init to initialize a configuration file with random secrets
# it will fail on subsequent runs, because the config file already exists
# therefore we ignore the error and then start the qsfera server
command: ["-c", "qsfera init || true; qsfera server"]
environment:
OC_MULTI_TENANT_ENABLED: "true"
# enable services that are not started automatically
OC_URL: https://${OC_DOMAIN:-cloud.qsfera.test}
OC_LOG_LEVEL: ${LOG_LEVEL:-info}
OC_LOG_COLOR: "${LOG_PRETTY:-false}"
OC_LOG_PRETTY: "${LOG_PRETTY:-false}"
OC_EXCLUDE_RUN_SERVICES: idm,idp
PROXY_ROLE_ASSIGNMENT_DRIVER: "oidc"
OC_OIDC_ISSUER: https://${KEYCLOAK_DOMAIN:-keycloak.qsfera.test}/realms/qsfera
PROXY_OIDC_REWRITE_WELLKNOWN: "true"
WEB_OIDC_CLIENT_ID: ${OC_OIDC_CLIENT_ID:-web}
PROXY_USER_OIDC_CLAIM: "uuid"
PROXY_USER_CS3_CLAIM: "userid"
WEB_OPTION_ACCOUNT_EDIT_LINK_HREF: "https://${KEYCLOAK_DOMAIN:-keycloak.qsfera.test}/realms/qsfera/account"
# admin and demo accounts must be created in Keycloak
OC_ADMIN_USER_ID: ""
SETTINGS_SETUP_DEFAULT_ASSIGNMENTS: "false"
GRAPH_ASSIGN_DEFAULT_USER_ROLE: "false"
GRAPH_USERNAME_MATCH: "none"
GROUPS_DRIVER: "null"
# This is needed to set the correct CSP rules for КуСфера
IDP_DOMAIN: ${KEYCLOAK_DOMAIN:-keycloak.qsfera.test}
# do not use SSL between the reverse proxy and КуСфера
PROXY_TLS: "false"
# INSECURE: needed if КуСфера / reverse proxy is using self generated certificates
OC_INSECURE: "${INSECURE:-false}"
# basic auth (not recommended, but needed for eg. WebDav clients that do not support OpenID Connect)
PROXY_ENABLE_BASIC_AUTH: "false"
GRAPH_IDENTITY_BACKEND: "cs3"
PROXY_CSP_CONFIG_FILE_LOCATION: /etc/qsfera/csp.yaml
OC_LDAP_URI: ldaps://ldap-server:1636
OC_LDAP_INSECURE: "true"
OC_LDAP_BIND_DN: "cn=admin,dc=qsfera,dc=eu"
OC_LDAP_BIND_PASSWORD: ${LDAP_BIND_PASSWORD:-admin}
OC_LDAP_USER_BASE_DN: "ou=users,dc=qsfera,dc=eu"
OC_LDAP_USER_SCHEMA_TENANT_ID: "qsferaMemberOfSchool"
PROXY_LOG_LEVEL: "debug"
volumes:
- ./config/qsfera/csp.yaml:/etc/qsfera/csp.yaml
# configure the .env file to use own paths instead of docker internal volumes
- ${OC_CONFIG_DIR:-qsfera-config}:/etc/qsfera
- ${OC_DATA_DIR:-qsfera-data}:/var/lib/qsfera
logging:
driver: ${LOG_DRIVER:-local}
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.qsfera.entrypoints=https"
- "traefik.http.routers.qsfera.rule=Host(`${OC_DOMAIN:-cloud.qsfera.test}`)"
- "traefik.http.routers.qsfera.service=qsfera"
- "traefik.http.services.qsfera.loadbalancer.server.port=9200"
- "traefik.http.routers.qsfera.${TRAEFIK_SERVICES_TLS_CONFIG}"
# Stand-alone instance of the 'graph' service to serve the provisioning API
provsioning:
image: ${OC_DOCKER_IMAGE:-qsfera/qsfera-rolling}:${OC_DOCKER_TAG:-latest}
networks:
qsfera-net:
entrypoint:
- /bin/sh
# run qsfera init to initialize a configuration file with random secrets
# it will fail on subsequent runs, because the config file already exists
# therefore we ignore the error and then start the qsfera server
command: ["-c", "qsfera init || true; qsfera graph server"]
environment:
OC_LOG_LEVEL: "debug"
OC_LOG_COLOR: "${LOG_PRETTY:-false}"
OC_LOG_PRETTY: "${LOG_PRETTY:-false}"
# This just runs the standalone graph service we don't need access to the registry
MICRO_REGISTRY: "memory"
# INSECURE: needed if КуСфера / reverse proxy is using self generated certificates
OC_INSECURE: "${INSECURE:-false}"
GRAPH_HTTP_ADDR: "0.0.0.0:9120"
GRAPH_HTTP_API_TOKEN: "${PROVISIONING_API_TOKEN:-changeme}"
# disable listening for events
GRAPH_EVENTS_ENDPOINT: ""
GRAPH_STORE_NODES: ""
GRAPH_ASSIGN_DEFAULT_USER_ROLE: "false"
GRAPH_USERNAME_MATCH: "none"
GRAPH_LDAP_EDUCATION_RESOURCES_ENABLED: "true"
GRAPH_LDAP_SCHOOL_BASE_DN: "ou=tenants,dc=qsfera,dc=eu"
OC_LDAP_URI: ldaps://ldap-server:1636
OC_LDAP_INSECURE: "true"
OC_LDAP_BIND_DN: "cn=admin,dc=qsfera,dc=eu"
OC_LDAP_BIND_PASSWORD: ${LDAP_BIND_PASSWORD:-admin}
OC_LDAP_USER_BASE_DN: "ou=users,dc=qsfera,dc=eu"
OC_LDAP_USER_FILTER: "(objectclass=inetOrgPerson)"
volumes:
# configure the .env file to use own paths instead of docker internal volumes
- ${PROVISIONING_CONFIG_DIR:-provisioning-config}:/etc/qsfera
logging:
driver: ${LOG_DRIVER:-local}
restart: always
ports:
- "9120:9120"
volumes:
qsfera-config:
qsfera-data:
provisioning-config:
networks:
qsfera-net:
@@ -0,0 +1,175 @@
package main
import (
"context"
"crypto/tls"
"fmt"
"github.com/Nerzal/gocloak/v13"
"github.com/go-resty/resty/v2"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
const (
provisioningAPIURL = "http://localhost:9120/graph"
provisioningAuthToken = "changeme"
)
type tenantWithUsers struct {
tenant libregraph.EducationSchool
users []libregraph.EducationUser
}
var demoTenants = []tenantWithUsers{
{
tenant: libregraph.EducationSchool{
DisplayName: libregraph.PtrString("Famous Coders"),
ExternalId: libregraph.PtrString("famouscodersExternalID"),
},
users: []libregraph.EducationUser{
{
DisplayName: libregraph.PtrString("Dennis Ritchie"),
OnPremisesSamAccountName: libregraph.PtrString("dennis"),
Mail: libregraph.PtrString("dennis@example.org"),
ExternalId: libregraph.PtrString("ExternalID1"),
},
{
DisplayName: libregraph.PtrString("Grace Hopper"),
OnPremisesSamAccountName: libregraph.PtrString("grace"),
Mail: libregraph.PtrString("grace@example.org"),
ExternalId: libregraph.PtrString("ExternalID2"),
},
},
},
{
tenant: libregraph.EducationSchool{
DisplayName: libregraph.PtrString("Scientists"),
ExternalId: libregraph.PtrString("scientistsExternalID"),
},
users: []libregraph.EducationUser{
{
DisplayName: libregraph.PtrString("Albert Einstein"),
OnPremisesSamAccountName: libregraph.PtrString("einstein"),
Mail: libregraph.PtrString("einstein@example.org"),
ExternalId: libregraph.PtrString("ExternalID3"),
},
{
DisplayName: libregraph.PtrString("Marie Curie"),
OnPremisesSamAccountName: libregraph.PtrString("marie"),
Mail: libregraph.PtrString("marie@example.org"),
ExternalId: libregraph.PtrString("ExternalID4"),
},
},
},
}
func main() {
lgconf := libregraph.NewConfiguration()
lgconf.Servers = libregraph.ServerConfigurations{
{
URL: provisioningAPIURL,
},
}
lgconf.DefaultHeader = map[string]string{"Authorization": "Bearer " + provisioningAuthToken}
lgclient := libregraph.NewAPIClient(lgconf)
for _, tenant := range demoTenants {
tenantid, err := createTenant(lgclient, tenant.tenant)
if err != nil {
fmt.Printf("Failed to create tenant: %s\n", err)
continue
}
for _, user := range tenant.users {
userid1, err := createUser(lgclient, user)
if err != nil {
fmt.Printf("Failed to create user: %s\n", err)
continue
}
_, err = lgclient.EducationSchoolApi.AddUserToSchool(context.TODO(), tenantid).EducationUserReference(libregraph.EducationUserReference{
OdataId: libregraph.PtrString(fmt.Sprintf("%s/education/users/%s", provisioningAPIURL, userid1)),
}).Execute()
if err != nil {
fmt.Printf("Failed to add user to tenant: %s\n", err)
continue
}
}
}
resetAllUserPasswords()
setUserRoles()
}
func createUser(client *libregraph.APIClient, user libregraph.EducationUser) (string, error) {
newUser, _, err := client.EducationUserApi.CreateEducationUser(context.TODO()).EducationUser(user).Execute()
if err != nil {
fmt.Printf("Failed to create user: %s\n", err)
return "", err
}
fmt.Printf("Created user: %s with id %s\n", newUser.GetDisplayName(), newUser.GetId())
return newUser.GetId(), nil
}
func createTenant(client *libregraph.APIClient, tenant libregraph.EducationSchool) (string, error) {
newTenant, _, err := client.EducationSchoolApi.CreateSchool(context.TODO()).EducationSchool(tenant).Execute()
if err != nil {
fmt.Printf("Failed to create tenant: %s\n", err)
return "", err
}
fmt.Printf("Created tenant: %s with id %s\n", newTenant.GetDisplayName(), newTenant.GetId())
return newTenant.GetId(), nil
}
func setUserRoles() {
tls := tls.Config{InsecureSkipVerify: true}
restyClient := resty.New().SetTLSClientConfig(&tls)
client := gocloak.NewClient("https://keycloak.qsfera.test")
client.SetRestyClient(restyClient)
ctx := context.Background()
token, err := client.LoginAdmin(ctx, "kcadmin", "admin", "master")
if err != nil {
fmt.Printf("Failed to login: %s\n", err)
panic("Something wrong with the credentials or url")
}
role, _ := client.GetRealmRole(ctx, token.AccessToken, "qsfera", "qsferaUser")
users, err := client.GetUsers(ctx, token.AccessToken, "qsfera", gocloak.GetUsersParams{})
if err != nil {
fmt.Printf("%s\n", err)
panic("Oh no!, failed to list users :(")
}
for _, user := range users {
err := client.AddRealmRoleToUser(ctx, token.AccessToken, "qsfera", *user.ID, []gocloak.Role{
*role,
})
if err != nil {
fmt.Printf("Failed to assign role to user %s: %s\n", *user.Username, err)
}
}
}
func resetAllUserPasswords() {
tls := tls.Config{InsecureSkipVerify: true}
restyClient := resty.New().SetTLSClientConfig(&tls)
client := gocloak.NewClient("https://keycloak.qsfera.test")
client.SetRestyClient(restyClient)
ctx := context.Background()
token, err := client.LoginAdmin(ctx, "kcadmin", "admin", "master")
if err != nil {
fmt.Printf("Failed to login: %s\n", err)
panic("Something wrong with the credentials or url")
}
users, err := client.GetUsers(ctx, token.AccessToken, "qsfera", gocloak.GetUsersParams{})
if err != nil {
fmt.Printf("%s\n", err)
panic("Oh no!, failed to list users :(")
}
for _, user := range users {
fmt.Printf("Setting password for user: %s\n", *user.Username)
err = client.SetPassword(ctx, token.AccessToken, *user.ID, "qsfera", "demo", false)
if err != nil {
fmt.Printf("Failed to set password for user %s: %s\n", *user.Username, err)
}
}
}
@@ -0,0 +1,55 @@
---
services:
qsfera:
environment:
postgres:
image: postgres:alpine
networks:
qsfera-net:
volumes:
- keycloak_postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: ${KC_DB_USERNAME:-keycloak}
POSTGRES_PASSWORD: ${KC_DB_PASSWORD:-keycloak}
logging:
driver: ${LOG_DRIVER:-local}
restart: always
keycloak:
labels:
- "traefik.enable=true"
- "traefik.http.routers.keycloak.entrypoints=https"
- "traefik.http.routers.keycloak.rule=Host(`${KEYCLOAK_DOMAIN:-keycloak.qsfera.test}`)"
- "traefik.http.routers.keycloak.${TRAEFIK_SERVICES_TLS_CONFIG}"
- "traefik.http.routers.keycloak.service=keycloak"
- "traefik.http.services.keycloak.loadbalancer.server.port=8080"
image: quay.io/keycloak/keycloak:26.4
networks:
qsfera-net:
command: [ "start", "--spi-connections-http-client-default-disable-trust-manager=${INSECURE:-false}", "--import-realm" ]
entrypoint: [ "/bin/sh", "/opt/keycloak/bin/docker-entrypoint-override.sh" ]
volumes:
- "./config/keycloak/docker-entrypoint-override.sh:/opt/keycloak/bin/docker-entrypoint-override.sh"
- "./config/keycloak/qsfera-realm.dist.json:/opt/keycloak/data/import-dist/qsfera-realm.json"
- "./config/keycloak/themes/qsfera:/opt/keycloak/themes/qsfera"
environment:
OC_DOMAIN: ${OC_DOMAIN:-cloud.qsfera.test}
KC_HOSTNAME: ${KEYCLOAK_DOMAIN:-keycloak.qsfera.test}
KC_DB: postgres
KC_DB_URL: "jdbc:postgresql://postgres:5432/keycloak"
KC_DB_USERNAME: ${KC_DB_USERNAME:-keycloak}
KC_DB_PASSWORD: ${KC_DB_PASSWORD:-keycloak}
KC_FEATURES: impersonation
KC_PROXY_HEADERS: xforwarded
KC_HTTP_ENABLED: true
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-kcadmin}
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin}
depends_on:
- postgres
logging:
driver: ${LOG_DRIVER:-local}
restart: always
volumes:
keycloak_postgres_data:
@@ -0,0 +1,32 @@
---
services:
ldap-server:
image: bitnamilegacy/openldap:2.6
networks:
qsfera-net:
entrypoint: [ "/bin/sh", "/opt/bitnami/scripts/openldap/docker-entrypoint-override.sh", "/opt/bitnami/scripts/openldap/run.sh" ]
environment:
BITNAMI_DEBUG: true
LDAP_TLS_VERIFY_CLIENT: never
LDAP_ENABLE_TLS: "yes"
LDAP_TLS_CA_FILE: /opt/bitnami/openldap/share/openldap.crt
LDAP_TLS_CERT_FILE: /opt/bitnami/openldap/share/openldap.crt
LDAP_TLS_KEY_FILE: /opt/bitnami/openldap/share/openldap.key
LDAP_ROOT: "dc=qsfera,dc=eu"
LDAP_ADMIN_PASSWORD: ${LDAP_BIND_PASSWORD:-admin}
ports:
- "127.0.0.1:389:1389"
- "127.0.0.1:636:1636"
volumes:
# Only use the base ldif file to create the base structure
- ./config/ldap/ldif/10_base.ldif:/ldifs/10_base.ldif
# Use the custom schema from qsfera because we are in full control of the ldap server
- ../shared/config/ldap/schemas/10_qsfera_schema.ldif:/schemas/10_qsfera_schema.ldif
- ../shared/config/ldap/schemas/20_qsfera_education_schema.ldif:/schemas/20_qsfera_education_schema.ldif
- ./config/ldap/docker-entrypoint-override.sh:/opt/bitnami/scripts/openldap/docker-entrypoint-override.sh
- ${LDAP_CERTS_DIR:-ldap-certs}:/opt/bitnami/openldap/share
- ${LDAP_DATA_DIR:-ldap-data}:/bitnami/openldap
volumes:
ldap-certs:
ldap-data:
@@ -0,0 +1,24 @@
---
# This file can be used to be added to the qsfera_full example
# to browse the LDAP server with a web interface.
# This is not a production ready setup.
services:
ldap-manager:
image: phpldapadmin/phpldapadmin:latest
networks:
qsfera-net:
environment:
LDAP_HOST: ldap-server
LDAP_PORT: 1389
LDAP_LOGIN_OBJECTCLASS: "inetOrgPerson"
APP_URL: "https://${LDAP_MANAGER_DOMAIN:-ldap.qsfera.test}"
labels:
- "traefik.enable=true"
- "traefik.http.routers.ldap-manager.entrypoints=https"
- "traefik.http.routers.ldap-manager.rule=Host(`${LDAP_MANAGER_DOMAIN:-ldap.qsfera.test}`)"
- "traefik.http.routers.ldap-manager.${TRAEFIK_SERVICES_TLS_CONFIG}"
- "traefik.http.routers.ldap-manager.service=ldap-manager"
- "traefik.http.services.ldap-manager.loadbalancer.server.port=8080"
logging:
driver: ${LOG_DRIVER:-local}
restart: always
@@ -0,0 +1,45 @@
---
services:
qsfera:
labels:
- "traefik.enable=true"
- "traefik.http.routers.qsfera.entrypoints=https"
- "traefik.http.routers.qsfera.rule=Host(`${OC_DOMAIN:-cloud.qsfera.test}`)"
- "traefik.http.routers.qsfera.service=qsfera"
- "traefik.http.services.qsfera.loadbalancer.server.port=9200"
- "traefik.http.routers.qsfera.${TRAEFIK_SERVICES_TLS_CONFIG}"
traefik:
image: traefik:v3.6.7
# release notes: https://github.com/traefik/traefik/releases
networks:
qsfera-net:
aliases:
- ${OC_DOMAIN:-cloud.qsfera.test}
- ${KEYCLOAK_DOMAIN:-keycloak.qsfera.test}
entrypoint: [ "/bin/sh", "/opt/traefik/bin/docker-entrypoint-override.sh"]
environment:
- "TRAEFIK_SERVICES_TLS_CONFIG=${TRAEFIK_SERVICES_TLS_CONFIG:-tls.certresolver=letsencrypt}"
- "TRAEFIK_ACME_MAIL=${TRAEFIK_ACME_MAIL:-example@example.org}"
- "TRAEFIK_ACME_CASERVER=${TRAEFIK_ACME_CASERVER:-https://acme-v02.api.letsencrypt.org/directory}"
- "TRAEFIK_LOG_LEVEL=${TRAEFIK_LOG_LEVEL:-ERROR}"
- "TRAEFIK_ACCESS_LOG=${TRAEFIK_ACCESS_LOG:-false}"
ports:
- "80:80"
- "443:443"
volumes:
- "${DOCKER_SOCKET_PATH:-/var/run/docker.sock}:/var/run/docker.sock:ro"
- "./config/traefik/docker-entrypoint-override.sh:/opt/traefik/bin/docker-entrypoint-override.sh"
- "${TRAEFIK_CERTS_DIR:-./certs}:/certs"
- "./config/traefik/dynamic:/etc/traefik/dynamic"
labels:
- "traefik.enable=${TRAEFIK_DASHBOARD:-false}"
# defaults to admin:admin
- "traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_BASIC_AUTH_USERS:-admin:$$apr1$$4vqie50r$$YQAmQdtmz5n9rEALhxJ4l.}"
- "traefik.http.routers.traefik.entrypoints=https"
- "traefik.http.routers.traefik.rule=Host(`${TRAEFIK_DOMAIN:-traefik.qsfera.test}`)"
- "traefik.http.routers.traefik.middlewares=traefik-auth"
- "traefik.http.routers.traefik.${TRAEFIK_SERVICES_TLS_CONFIG}"
- "traefik.http.routers.traefik.service=api@internal"
logging:
driver: ${LOG_DRIVER:-local}
restart: always
@@ -0,0 +1,18 @@
---
document this deployment example in: docs/qsfera/deployment/qsfera_full.md
---
# КуСфера WOPI Deployment Example
This deployment example is documented in two locations for different audiences:
* In the [Admin Documentation](https://docs.qsfera.eu/docs/admin/intro)\
Providing two variants using detailed configuration step by step guides:\
[Docker Compose Setup](https://docs.qsfera.eu/docs/admin/getting-started/container/docker-compose) and [Docker Compose Local](https://docs.qsfera.eu/docs/admin/getting-started/container/docker-compose-local).\
Note that these examples use LetsEncrypt certificates and are intended for production use.
* In the [Developer Documentation](https://docs.qsfera.eu/docs/dev/intro)\
Providing details which are more developer focused. This description can also be used when deviating from the default.\
Note that this examples uses self signed certificates and is intended for testing purposes.
@@ -0,0 +1,31 @@
---
services:
qsfera:
environment:
ANTIVIRUS_SCANNER_TYPE: "clamav"
ANTIVIRUS_CLAMAV_SOCKET: "/var/run/clamav/clamd.sock"
ANTIVIRUS_MAX_SCAN_SIZE_MODE: ${ANTIVIRUS_MAX_SCAN_SIZE_MODE:-partial}
ANTIVIRUS_MAX_SCAN_SIZE: ${ANTIVIRUS_MAX_SCAN_SIZE:-100MB}
# the antivirus service needs manual startup, see .env and qsfera.yaml for START_ADDITIONAL_SERVICES
# configure the antivirus service
POSTPROCESSING_STEPS: "virusscan"
# PROXY_TLS is set to "false", the download url has no https
STORAGE_USERS_DATA_GATEWAY_URL: http://qsfera:9200/data
volumes:
- "clamav-socket:/var/run/clamav"
clamav:
image: clamav/clamav:${CLAMAV_DOCKER_TAG:-latest}
# release notes: https://blog.clamav.net
networks:
qsfera-net:
volumes:
- "clamav-socket:/tmp"
- "clamav-db:/var/lib/clamav"
logging:
driver: ${LOG_DRIVER:-local}
restart: always
volumes:
clamav-socket:
clamav-db:
@@ -0,0 +1,87 @@
---
services:
traefik:
networks:
qsfera-net:
aliases:
- ${COLLABORA_DOMAIN:-collabora.qsfera.test}
- ${WOPISERVER_DOMAIN:-wopiserver.qsfera.test}
qsfera:
environment:
# make collabora the secure view app
FRONTEND_APP_HANDLER_SECURE_VIEW_APP_ADDR: qsfera.api.collaboration.CollaboraOnline
GRAPH_AVAILABLE_ROLES: "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5,a8d5fe5e-96e3-418d-825b-534dbdf22b99,fb6c3e19-e378-47e5-b277-9732f9de6e21,58c63c02-1d89-4572-916a-870abc5a1b7d,2d00ce52-1fc2-4dbc-8b95-a73b73395f5a,1c996275-f1c9-4e71-abdf-a42f6495e960,312c0871-5ef7-4b3a-85b6-0e4074c64049,aa97fe03-7980-45ac-9e50-b325749fd7e6"
collaboration:
image: ${OC_DOCKER_IMAGE:-qsfera/qsfera-rolling}:${OC_DOCKER_TAG:-latest}
networks:
qsfera-net:
depends_on:
qsfera:
condition: service_started
collabora:
condition: service_healthy
entrypoint:
- /bin/sh
command: [ "-c", "qsfera collaboration server" ]
environment:
COLLABORATION_GRPC_ADDR: 0.0.0.0:9301
COLLABORATION_HTTP_ADDR: 0.0.0.0:9300
MICRO_REGISTRY: "nats-js-kv"
MICRO_REGISTRY_ADDRESS: "qsfera:9233"
COLLABORATION_WOPI_SRC: https://${WOPISERVER_DOMAIN:-wopiserver.qsfera.test}
COLLABORATION_APP_NAME: "CollaboraOnline"
COLLABORATION_APP_PRODUCT: "Collabora"
COLLABORATION_APP_ADDR: https://${COLLABORA_DOMAIN:-collabora.qsfera.test}
COLLABORATION_APP_ICON: https://${COLLABORA_DOMAIN:-collabora.qsfera.test}/favicon.ico
COLLABORATION_APP_INSECURE: "${INSECURE:-true}"
COLLABORATION_CS3API_DATAGATEWAY_INSECURE: "${INSECURE:-true}"
COLLABORATION_LOG_LEVEL: ${LOG_LEVEL:-info}
OC_URL: https://${OC_DOMAIN:-cloud.qsfera.test}
volumes:
# configure the .env file to use own paths instead of docker internal volumes
- ${OC_CONFIG_DIR:-qsfera-config}:/etc/qsfera
labels:
- "traefik.enable=true"
- "traefik.http.routers.collaboration.entrypoints=https"
- "traefik.http.routers.collaboration.rule=Host(`${WOPISERVER_DOMAIN:-wopiserver.qsfera.test}`)"
- "traefik.http.routers.collaboration.tls.certresolver=http"
- "traefik.http.routers.collaboration.service=collaboration"
- "traefik.http.services.collaboration.loadbalancer.server.port=9300"
logging:
driver: ${LOG_DRIVER:-local}
restart: always
collabora:
image: collabora/code:25.04.2.1.1
# release notes: https://www.collaboraonline.com/release-notes/
networks:
qsfera-net:
environment:
aliasgroup1: https://${WOPISERVER_DOMAIN:-wopiserver.qsfera.test}:443
DONT_GEN_SSL_CERT: "YES"
extra_params: |
--o:ssl.enable=${COLLABORA_SSL_ENABLE:-true} \
--o:ssl.ssl_verification=${COLLABORA_SSL_VERIFICATION:-true} \
--o:ssl.termination=true \
--o:welcome.enable=false \
--o:net.frame_ancestors=${OC_DOMAIN:-cloud.qsfera.test}
username: ${COLLABORA_ADMIN_USER:-admin}
password: ${COLLABORA_ADMIN_PASSWORD:-admin}
cap_add:
- MKNOD
labels:
- "traefik.enable=true"
- "traefik.http.routers.collabora.entrypoints=https"
- "traefik.http.routers.collabora.rule=Host(`${COLLABORA_DOMAIN:-collabora.qsfera.test}`)"
- "traefik.http.routers.collabora.tls.certresolver=http"
- "traefik.http.routers.collabora.service=collabora"
- "traefik.http.services.collabora.loadbalancer.server.port=9980"
logging:
driver: ${LOG_DRIVER:-local}
restart: always
entrypoint: ['/bin/bash', '-c']
command: ['coolconfig generate-proof-key && /start-collabora-online.sh']
healthcheck:
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/9980 && echo -e 'GET /hosting/discovery HTTP/1.1\r\nHost: localhost:9980\r\n\r\n' >&3 && head -n 1 <&3 | grep '200 OK'"]
@@ -0,0 +1,63 @@
{
"clientId": "КуСфераAndroid",
"name": "КуСфера Android App",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"oc://android.qsfera.eu"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "oc://android.qsfera.eu",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}
@@ -0,0 +1,64 @@
{
"clientId": "КуСфераDesktop",
"name": "КуСфера Desktop Client",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"http://127.0.0.1",
"http://localhost"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "+",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}
@@ -0,0 +1,63 @@
{
"clientId": "КуСфераIOS",
"name": "КуСфера iOS App",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"oc://ios.qsfera.eu"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "oc://ios.qsfera.eu",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}
@@ -0,0 +1,66 @@
{
"clientId": "Cyberduck",
"name": "Cyberduck",
"description": "File transfer utility client",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"x-cyberduck-action:oauth",
"x-mountainduck-action:oauth"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}
@@ -0,0 +1,74 @@
{
"clientId": "web",
"name": "КуСфера Web App",
"description": "",
"rootUrl": "{{OC_URL}}",
"adminUrl": "{{OC_URL}}",
"baseUrl": "",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"{{OC_URL}}/",
"{{OC_URL}}/oidc-callback.html",
"{{OC_URL}}/oidc-silent-redirect.html"
],
"webOrigins": [
"{{OC_URL}}"
],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.url": "{{OC_URL}}/backchannel_logout",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"groups",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}
@@ -0,0 +1,8 @@
#!/bin/bash
printenv
# replace qsfera domain and LDAP password in keycloak realm import
mkdir /opt/keycloak/data/import
sed -e "s/cloud.qsfera.test/${OC_DOMAIN}/g" -e "s/ldap-admin-password/${LDAP_ADMIN_PASSWORD:-admin}/g" /opt/keycloak/data/import-dist/qsfera-realm.json > /opt/keycloak/data/import/qsfera-realm.json
# run original docker-entrypoint
/opt/keycloak/bin/kc.sh "$@"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,9 @@
#!/bin/bash
printenv
if [ ! -f /opt/bitnami/openldap/share/openldap.key ]
then
openssl req -x509 -newkey rsa:4096 -keyout /opt/bitnami/openldap/share/openldap.key -out /opt/bitnami/openldap/share/openldap.crt -sha256 -days 365 -batch -nodes
fi
# run original docker-entrypoint
/opt/bitnami/scripts/openldap/entrypoint.sh "$@"
@@ -0,0 +1,24 @@
dn: dc=qsfera,dc=eu
objectClass: organization
objectClass: dcObject
dc: qsfera
o: qsfera
dn: ou=users,dc=qsfera,dc=eu
objectClass: organizationalUnit
ou: users
dn: cn=admin,dc=qsfera,dc=eu
objectClass: inetOrgPerson
objectClass: person
cn: admin
sn: admin
uid: ldapadmin
dn: ou=groups,dc=qsfera,dc=eu
objectClass: organizationalUnit
ou: groups
dn: ou=custom,ou=groups,dc=qsfera,dc=eu
objectClass: organizationalUnit
ou: custom
@@ -0,0 +1,84 @@
# Start dn with uid (user identifier / login), not cn (Firstname + Surname)
dn: uid=alan,ou=users,dc=qsfera,dc=eu
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: alan
givenName: Alan
sn: Turing
cn: alan
displayName: Alan Turing
description: An English mathematician, computer scientist, logician, cryptanalyst, philosopher and theoretical biologist. He was highly influential in the development of theoretical computer science, providing a formalisation of the concepts of algorithm and computation with the Turing machine.
mail: alan@example.org
userPassword:: e1NTSEF9Y2ZMdVlqMTBDUFpLWE44VC9mQ0FzYnFHQmtyZExJeGg=
dn: uid=lynn,ou=users,dc=qsfera,dc=eu
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: lynn
givenName: Lynn
sn: Conway
cn: lynn
displayName: Lynn Conway
description: An American computer scientist, electrical engineer, and transgender activist.
mail: lynn@example.org
userPassword:: e1NTSEF9Y2ZMdVlqMTBDUFpLWE44VC9mQ0FzYnFHQmtyZExJeGg=
dn: uid=mary,ou=users,dc=qsfera,dc=eu
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: mary
givenName: Mary
sn: Kenneth Keller
cn: mary
displayName: Mary Kenneth Keller
description: Mary Kenneth Keller of the Sisters of Charity of the Blessed Virgin Mary was a pioneer in computer science.
mail: mary@example.org
userPassword:: e1NTSEF9Y2ZMdVlqMTBDUFpLWE44VC9mQ0FzYnFHQmtyZExJeGg=
dn: uid=margaret,ou=users,dc=qsfera,dc=eu
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: margaret
givenName: Margaret
sn: Hamilton
cn: margaret
displayName: Margaret Hamilton
description: A director of the Software Engineering Division of the MIT Instrumentation Laboratory, which developed on-board flight software for NASA's Apollo program.
mail: margaret@example.org
userPassword:: e1NTSEF9Y2ZMdVlqMTBDUFpLWE44VC9mQ0FzYnFHQmtyZExJeGg=
dn: uid=dennis,ou=users,dc=qsfera,dc=eu
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: dennis
givenName: Dennis
sn: Ritchie
cn: dennis
displayName: Dennis Ritchie
description: American computer scientist. He created the C programming language and the Unix operating system and B language with long-time colleague Ken Thompson.
mail: dennis@example.org
userPassword:: e1NTSEF9Y2ZMdVlqMTBDUFpLWE44VC9mQ0FzYnFHQmtyZExJeGg=
dn: uid=admin,ou=users,dc=qsfera,dc=eu
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: admin
givenName: Admin
sn: Admin
cn: admin
displayName: Admin
description: An admin for this КуСфера instance.
mail: admin@example.org
userPassword:: e1NTSEF9UWhmaFB3dERydTUydURoWFFObDRMbzVIckI3TkI5Nmo==
@@ -0,0 +1,70 @@
dn: cn=users,ou=groups,dc=qsfera,dc=eu
objectClass: groupOfNames
objectClass: top
cn: users
description: Users
member: uid=alan,ou=users,dc=qsfera,dc=eu
member: uid=mary,ou=users,dc=qsfera,dc=eu
member: uid=margaret,ou=users,dc=qsfera,dc=eu
member: uid=dennis,ou=users,dc=qsfera,dc=eu
member: uid=lynn,ou=users,dc=qsfera,dc=eu
member: uid=admin,ou=users,dc=qsfera,dc=eu
dn: cn=chess-lovers,ou=groups,dc=qsfera,dc=eu
objectClass: groupOfNames
objectClass: top
cn: chess-lovers
description: Chess lovers
member: uid=alan,ou=users,dc=qsfera,dc=eu
dn: cn=machine-lovers,ou=groups,dc=qsfera,dc=eu
objectClass: groupOfNames
objectClass: top
cn: machine-lovers
description: Machine Lovers
member: uid=alan,ou=users,dc=qsfera,dc=eu
dn: cn=bible-readers,ou=groups,dc=qsfera,dc=eu
objectClass: groupOfNames
objectClass: top
cn: bible-readers
description: Bible readers
member: uid=mary,ou=users,dc=qsfera,dc=eu
dn: cn=apollos,ou=groups,dc=qsfera,dc=eu
objectClass: groupOfNames
objectClass: top
cn: apollos
description: Contributors to the Appollo mission
member: uid=margaret,ou=users,dc=qsfera,dc=eu
dn: cn=unix-lovers,ou=groups,dc=qsfera,dc=eu
objectClass: groupOfNames
objectClass: top
cn: unix-lovers
description: Unix lovers
member: uid=dennis,ou=users,dc=qsfera,dc=eu
dn: cn=basic-haters,ou=groups,dc=qsfera,dc=eu
objectClass: groupOfNames
objectClass: top
cn: basic-haters
description: Haters of the Basic programming language
member: uid=dennis,ou=users,dc=qsfera,dc=eu
dn: cn=vlsi-lovers,ou=groups,dc=qsfera,dc=eu
objectClass: groupOfNames
objectClass: top
cn: vlsi-lovers
description: Lovers of VLSI microchip design
member: uid=lynn,ou=users,dc=qsfera,dc=eu
dn: cn=programmers,ou=groups,dc=qsfera,dc=eu
objectClass: groupOfNames
objectClass: top
cn: programmers
description: Computer Programmers
member: uid=alan,ou=users,dc=qsfera,dc=eu
member: uid=margaret,ou=users,dc=qsfera,dc=eu
member: uid=dennis,ou=users,dc=qsfera,dc=eu
member: uid=lynn,ou=users,dc=qsfera,dc=eu
@@ -0,0 +1,65 @@
app_registry:
mimetypes:
- mime_type: application/pdf
extension: pdf
name: PDF
description: PDF document
icon: ''
default_app: ''
allow_creation: false
- mime_type: application/vnd.oasis.opendocument.text
extension: odt
name: OpenDocument
description: OpenDocument text document
icon: ''
default_app: CollaboraOnline
allow_creation: true
- mime_type: application/vnd.oasis.opendocument.spreadsheet
extension: ods
name: OpenSpreadsheet
description: OpenDocument spreadsheet document
icon: ''
default_app: CollaboraOnline
allow_creation: true
- mime_type: application/vnd.oasis.opendocument.presentation
extension: odp
name: OpenPresentation
description: OpenDocument presentation document
icon: ''
default_app: CollaboraOnline
allow_creation: true
- mime_type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
extension: docx
name: Microsoft Word
description: Microsoft Word document
icon: ''
default_app: CollaboraOnline
allow_creation: true
- mime_type: application/vnd.openxmlformats-officedocument.wordprocessingml.form
extension: docxf
name: Form Document
description: Form Document
icon: ''
default_app: CollaboraOnline
allow_creation: true
- mime_type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
extension: xlsx
name: Microsoft Excel
description: Microsoft Excel document
icon: ''
default_app: CollaboraOnline
allow_creation: true
- mime_type: application/vnd.openxmlformats-officedocument.presentationml.presentation
extension: pptx
name: Microsoft PowerPoint
description: Microsoft PowerPoint document
icon: ''
default_app: CollaboraOnline
allow_creation: true
- mime_type: application/vnd.jupyter
extension: ipynb
name: Jupyter Notebook
description: Jupyter Notebook
icon: ''
default_app: ''
allow_creation: true
@@ -0,0 +1,17 @@
importer:
config:
companionUrl: https://${COMPANION_DOMAIN|companion.qsfera.test}
supportedClouds:
- WebdavPublicLink
#- OneDrive # needs a client id and secret
#- GoogleDrive # needs a client id and secret and an addition to the DNS zone
external-sites:
config:
sites:
# For settings see: https://github.com/opencloud-eu/web-extensions/tree/main/packages/web-app-external-sites
- name: Documentation
url: "https://docs.qsfera.eu"
target: embedded
color: "#E2BAFF"
icon: cloud
priority: 50
@@ -0,0 +1,5 @@
password
12345678
123
КуСфера
КуСфера-1
@@ -0,0 +1,44 @@
directives:
child-src:
- '''self'''
connect-src:
- '''self'''
- 'blob:'
- 'https://${COMPANION_DOMAIN|companion.qsfera.test}/'
- 'wss://${COMPANION_DOMAIN|companion.qsfera.test}/'
- 'https://raw.githubusercontent.com/qsfera-eu/awesome-apps/'
- 'https://${KEYCLOAK_DOMAIN|keycloak.qsfera.test}/'
default-src:
- '''none'''
font-src:
- '''self'''
frame-ancestors:
- '''self'''
frame-src:
- '''self'''
- 'blob:'
- 'https://embed.diagrams.net/'
# In contrary to bash and docker the default is given after the | character
- 'https://${COLLABORA_DOMAIN|collabora.qsfera.test}/'
# This is needed for the external-sites web extension when embedding sites
- 'https://docs.qsfera.eu'
img-src:
- '''self'''
- 'data:'
- 'blob:'
- 'https://raw.githubusercontent.com/qsfera-eu/awesome-apps/'
# In contrary to bash and docker the default is given after the | character
- 'https://${COLLABORA_DOMAIN|collabora.qsfera.test}/'
manifest-src:
- '''self'''
media-src:
- '''self'''
object-src:
- '''self'''
- 'blob:'
script-src:
- '''self'''
- '''unsafe-inline'''
style-src:
- '''self'''
- '''unsafe-inline'''
@@ -0,0 +1,40 @@
# This adds four additional routes to the proxy. Forwarding
# request on '/carddav/', '/caldav/' and the respective '/.well-knwown'
# endpoints to the radicale container and setting the required headers.
additional_policies:
- name: default
routes:
- endpoint: /caldav/
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /caldav
- endpoint: /.well-known/caldav
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /caldav
- endpoint: /carddav/
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /carddav
- endpoint: /.well-known/carddav
backend: http://radicale:5232
remote_user_header: X-Remote-User
skip_x_access_token: true
additional_headers:
- X-Script-Name: /carddav
# To enable the radicale web UI add this rule.
# "unprotected" is True because the Web UI itself ask for
# the password.
# Also set "type" to "internal" in the config/radicale/config
# - endpoint: /caldav/.web/
# backend: http://radicale:5232/
# unprotected: true
# skip_x_access_token: true
# additional_headers:
# - X-Script-Name: /caldav
@@ -0,0 +1,325 @@
# -*- mode: conf -*-
# vim:ft=cfg
# Config file for Radicale - A simple calendar server
#
# Place it into /etc/radicale/config (global)
# or ~/.config/radicale/config (user)
#
# The current values are the default ones
[server]
# CalDAV server hostnames separated by a comma
# IPv4 syntax: address:port
# IPv6 syntax: [address]:port
# Hostname syntax (using "getaddrinfo" to resolve to IPv4/IPv6 adress(es)): hostname:port
# For example: 0.0.0.0:9999, [::]:9999, localhost:9999
hosts = 0.0.0.0:5232
# Max parallel connections
#max_connections = 8
# Max size of request body (bytes)
#max_content_length = 100000000
# Socket timeout (seconds)
#timeout = 30
# SSL flag, enable HTTPS protocol
#ssl = False
# SSL certificate path
#certificate = /etc/ssl/radicale.cert.pem
# SSL private key
#key = /etc/ssl/radicale.key.pem
# CA certificate for validating clients. This can be used to secure
# TCP traffic between Radicale and a reverse proxy
#certificate_authority =
# SSL protocol, secure configuration: ALL -SSLv3 -TLSv1 -TLSv1.1
#protocol = (default)
# SSL ciphersuite, secure configuration: DHE:ECDHE:-NULL:-SHA (see also "man openssl-ciphers")
#ciphersuite = (default)
# script name to strip from URI if called by reverse proxy
#script_name = (default taken from HTTP_X_SCRIPT_NAME or SCRIPT_NAME)
[encoding]
# Encoding for responding requests
#request = utf-8
# Encoding for storing local collections
#stock = utf-8
[auth]
# Authentication method
# Value: none | htpasswd | remote_user | http_x_remote_user | dovecot | ldap | oauth2 | pam | denyall
type = http_x_remote_user
# Cache logins for until expiration time
#cache_logins = false
# Expiration time for caching successful logins in seconds
#cache_successful_logins_expiry = 15
## Expiration time of caching failed logins in seconds
#cache_failed_logins_expiry = 90
# Ignore modifyTimestamp and createTimestamp attributes. Required e.g. for Authentik LDAP server
#ldap_ignore_attribute_create_modify_timestamp = false
# URI to the LDAP server
#ldap_uri = ldap://localhost
# The base DN where the user accounts have to be searched
#ldap_base = ##BASE_DN##
# The reader DN of the LDAP server
#ldap_reader_dn = CN=ldapreader,CN=Users,##BASE_DN##
# Password of the reader DN
#ldap_secret = ldapreader-secret
# Path of the file containing password of the reader DN
#ldap_secret_file = /run/secrets/ldap_password
# the attribute to read the group memberships from in the user's LDAP entry (default: not set)
#ldap_groups_attribute = memberOf
# The filter to find the DN of the user. This filter must contain a python-style placeholder for the login
#ldap_filter = (&(objectClass=person)(uid={0}))
# the attribute holding the value to be used as username after authentication
#ldap_user_attribute = cn
# Use ssl on the ldap connection
# Soon to be deprecated, use ldap_security instead
#ldap_use_ssl = False
# the encryption mode to be used: tls, starttls, default is none
#ldap_security = none
# The certificate verification mode. Works for ssl and starttls. NONE, OPTIONAL, default is REQUIRED
#ldap_ssl_verify_mode = REQUIRED
# The path to the CA file in pem format which is used to certificate the server certificate
#ldap_ssl_ca_file =
# Connection type for dovecot authentication (AF_UNIX|AF_INET|AF_INET6)
# Note: credentials are transmitted in cleartext
#dovecot_connection_type = AF_UNIX
# The path to the Dovecot client authentication socket (eg. /run/dovecot/auth-client on Fedora). Radicale must have read / write access to the socket.
#dovecot_socket = /var/run/dovecot/auth-client
# Host of via network exposed dovecot socket
#dovecot_host = localhost
# Port of via network exposed dovecot socket
#dovecot_port = 12345
# IMAP server hostname
# Syntax: address | address:port | [address]:port | imap.server.tld
#imap_host = localhost
# Secure the IMAP connection
# Value: tls | starttls | none
#imap_security = tls
# OAuth2 token endpoint URL
#oauth2_token_endpoint = <URL>
# PAM service
#pam_serivce = radicale
# PAM group user should be member of
#pam_group_membership =
# Htpasswd filename
#htpasswd_filename = /etc/radicale/users
# Htpasswd encryption method
# Value: plain | bcrypt | md5 | sha256 | sha512 | autodetect
# bcrypt requires the installation of 'bcrypt' module.
#htpasswd_encryption = autodetect
# Enable caching of htpasswd file based on size and mtime_ns
#htpasswd_cache = False
# Incorrect authentication delay (seconds)
#delay = 1
# Message displayed in the client when a password is needed
#realm = Radicale - Password Required
# Convert username to lowercase, must be true for case-insensitive auth providers
#lc_username = False
# Strip domain name from username
#strip_domain = False
[rights]
# Rights backend
# Value: authenticated | owner_only | owner_write | from_file
#type = owner_only
# File for rights management from_file
#file = /etc/radicale/rights
# Permit delete of a collection (global)
#permit_delete_collection = True
# Permit overwrite of a collection (global)
#permit_overwrite_collection = True
[storage]
# Storage backend
# Value: multifilesystem | multifilesystem_nolock
#type = multifilesystem
# Folder for storing local collections, created if not present
#filesystem_folder = /var/lib/radicale/collections
# Folder for storing cache of local collections, created if not present
# Note: only used in case of use_cache_subfolder_* options are active
# Note: can be used on multi-instance setup to cache files on local node (see below)
#filesystem_cache_folder = (filesystem_folder)
# Use subfolder 'collection-cache' for 'item' cache file structure instead of inside collection folder
# Note: can be used on multi-instance setup to cache 'item' on local node
#use_cache_subfolder_for_item = False
# Use subfolder 'collection-cache' for 'history' cache file structure instead of inside collection folder
# Note: use only on single-instance setup, will break consistency with client in multi-instance setup
#use_cache_subfolder_for_history = False
# Use subfolder 'collection-cache' for 'sync-token' cache file structure instead of inside collection folder
# Note: use only on single-instance setup, will break consistency with client in multi-instance setup
#use_cache_subfolder_for_synctoken = False
# Use last modifiction time (nanoseconds) and size (bytes) for 'item' cache instead of SHA256 (improves speed)
# Note: check used filesystem mtime precision before enabling
# Note: conversion is done on access, bulk conversion can be done offline using storage verification option: radicale --verify-storage
#use_mtime_and_size_for_item_cache = False
# Use configured umask for folder creation (not applicable for OS Windows)
# Useful value: 0077 | 0027 | 0007 | 0022
#folder_umask = (system default, usual 0022)
# Delete sync token that are older (seconds)
#max_sync_token_age = 2592000
# Skip broken item instead of triggering an exception
#skip_broken_item = True
# Command that is run after changes to storage, default is emtpy
# Supported placeholders:
# %(user)s: logged-in user
# %(cwd)s : current working directory
# %(path)s: full path of item
# Command will be executed with base directory defined in filesystem_folder
# For "git" check DOCUMENTATION.md for bootstrap instructions
# Example(test): echo \"user=%(user)s path=%(path)s cwd=%(cwd)s\"
# Example(git): git add -A && (git diff --cached --quiet || git commit -m "Changes by \"%(user)s\"")
#hook =
# Create predefined user collections
#
# json format:
#
# {
# "def-addressbook": {
# "D:displayname": "Personal Address Book",
# "tag": "VADDRESSBOOK"
# },
# "def-calendar": {
# "C:supported-calendar-component-set": "VEVENT,VJOURNAL,VTODO",
# "D:displayname": "Personal Calendar",
# "tag": "VCALENDAR"
# }
# }
#
predefined_collections = {
"def-addressbook": {
"D:displayname": "Personal Address Book",
"tag": "VADDRESSBOOK"
},
"def-calendar": {
"C:supported-calendar-component-set": "VEVENT,VJOURNAL,VTODO",
"D:displayname": "Personal Calendar",
"tag": "VCALENDAR"
}
}
[web]
# Web interface backend
# Value: none | internal
type = none
[logging]
# Threshold for the logger
# Value: debug | info | warning | error | critical
#level = info
# Don't include passwords in logs
#mask_passwords = True
# Log bad PUT request content
#bad_put_request_content = False
# Log backtrace on level=debug
#backtrace_on_debug = False
# Log request header on level=debug
#request_header_on_debug = False
# Log request content on level=debug
#request_content_on_debug = False
# Log response content on level=debug
#response_content_on_debug = False
# Log rights rule which doesn't match on level=debug
#rights_rule_doesnt_match_on_debug = False
# Log storage cache actions on level=debug
#storage_cache_actions_on_debug = False
[headers]
# Additional HTTP headers
#Access-Control-Allow-Origin = *
[hook]
# Hook types
# Value: none | rabbitmq
#type = none
#rabbitmq_endpoint =
#rabbitmq_topic =
#rabbitmq_queue_type = classic
[reporting]
# When returning a free-busy report, limit the number of returned
# occurences per event to prevent DOS attacks.
#max_freebusy_occurrence = 10000
@@ -0,0 +1,9 @@
---
services:
collaboration:
command: [ "-c", "dlv --listen=:40000 --headless=true --continue --check-go-version=false --api-version=2 --accept-multiclient exec /usr/bin/qsfera collaboration server" ]
environment:
COLLABORATION_LOG_LEVEL: debug
ports:
- 40001:40000
@@ -0,0 +1,9 @@
---
services:
collaboration-oo:
command: [ "-c", "dlv --listen=:40002 --headless=true --continue --check-go-version=false --api-version=2 --accept-multiclient exec /usr/bin/qsfera collaboration server" ]
environment:
COLLABORATION_LOG_LEVEL: debug
ports:
- 40002:40002
@@ -0,0 +1,7 @@
---
services:
qsfera:
command: [ "-c", "qsfera init || true; dlv --listen=:40000 --headless=true --continue --check-go-version=false --api-version=2 --accept-multiclient exec /usr/bin/qsfera server" ]
ports:
- 40000:40000
@@ -0,0 +1,6 @@
---
services:
qsfera:
environment:
STORAGE_USERS_DRIVER: decomposed
@@ -0,0 +1,14 @@
---
services:
qsfera:
environment:
# activate decomposeds3 storage driver
STORAGE_USERS_DRIVER: decomposeds3
# keep system data on qsfera storage since this are only small files atm
STORAGE_SYSTEM_DRIVER: decomposed
# decomposeds3 specific settings
STORAGE_USERS_DECOMPOSEDS3_ENDPOINT: ${DECOMPOSEDS3_ENDPOINT:-http://minio:9000}
STORAGE_USERS_DECOMPOSEDS3_REGION: ${DECOMPOSEDS3_REGION:-default}
STORAGE_USERS_DECOMPOSEDS3_ACCESS_KEY: ${DECOMPOSEDS3_ACCESS_KEY:-qsfera}
STORAGE_USERS_DECOMPOSEDS3_SECRET_KEY: ${DECOMPOSEDS3_SECRET_KEY:-qsfera-secret-key}
STORAGE_USERS_DECOMPOSEDS3_BUCKET: ${DECOMPOSEDS3_BUCKET:-qsfera-bucket}
@@ -0,0 +1,62 @@
---
services:
traefik:
image: traefik:v3.3.1
# release notes: https://github.com/traefik/traefik/releases
networks:
qsfera-net:
command:
- "--log.level=${TRAEFIK_LOG_LEVEL:-ERROR}"
# letsencrypt configuration
- "--certificatesResolvers.http.acme.email=${TRAEFIK_ACME_MAIL:-example@example.org}"
- "--certificatesResolvers.http.acme.storage=/certs/acme.json"
- "--certificatesResolvers.http.acme.httpChallenge.entryPoint=http"
- "--certificatesResolvers.http.acme.caserver=${TRAEFIK_ACME_CASERVER:-https://acme-v02.api.letsencrypt.org/directory}"
# enable dashboard
- "--api.dashboard=true"
# define entrypoints
- "--entryPoints.http.address=:80"
- "--entryPoints.http.http.redirections.entryPoint.to=https"
- "--entryPoints.http.http.redirections.entryPoint.scheme=https"
- "--entryPoints.https.address=:443"
# http2 optimizations
- "--entryPoints.https.http2.maxConcurrentStreams=512"
- "--serversTransport.maxIdleConnsPerHost=100"
# allow self signed certificate from КуСфера
- "--serversTransport.insecureSkipVerify=true"
# change default timeouts for long-running requests
# this is needed for webdav clients that do not support the TUS protocol
- "--entryPoints.https.transport.respondingTimeouts.readTimeout=12h"
- "--entryPoints.https.transport.respondingTimeouts.writeTimeout=12h"
- "--entryPoints.https.transport.respondingTimeouts.idleTimeout=3m"
# docker provider (get configuration from container labels)
- "--providers.docker.endpoint=unix:///var/run/docker.sock"
- "--providers.docker.exposedByDefault=false"
# access log
- "--accessLog=true"
- "--accessLog.format=json"
- "--accessLog.fields.headers.names.X-Request-Id=keep"
ports:
- "80:80"
- "443:443"
volumes:
- "${DOCKER_SOCKET_PATH:-/var/run/docker.sock}:/var/run/docker.sock:ro"
- "certs:/certs"
labels:
- "traefik.enable=${TRAEFIK_DASHBOARD:-false}"
# defaults to admin:admin
- "traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_BASIC_AUTH_USERS:-admin:$$apr1$$4vqie50r$$YQAmQdtmz5n9rEALhxJ4l.}"
- "traefik.http.routers.traefik.entrypoints=https"
- "traefik.http.routers.traefik.rule=Host(`${TRAEFIK_DOMAIN:-traefik.qsfera.test}`)"
- "traefik.http.routers.traefik.middlewares=traefik-auth"
- "traefik.http.routers.traefik.tls.certresolver=http"
- "traefik.http.routers.traefik.service=api@internal"
logging:
driver: ${LOG_DRIVER:-local}
restart: always
volumes:
certs:
networks:
qsfera-net:
@@ -0,0 +1,34 @@
---
services:
qsfera:
environment:
NOTIFICATIONS_SMTP_HOST: inbucket
NOTIFICATIONS_SMTP_PORT: 2500
NOTIFICATIONS_SMTP_SENDER: КуСфера notifications <notifications@${OC_DOMAIN:-cloud.qsfera.test}>
NOTIFICATIONS_SMTP_USERNAME: notifications@${OC_DOMAIN:-cloud.qsfera.test}
# the mail catcher uses self signed certificates
NOTIFICATIONS_SMTP_INSECURE: "true"
inbucket:
image: inbucket/inbucket
# changelog: https://github.com/inbucket/inbucket/blob/main/CHANGELOG.md
networks:
- qsfera-net
entrypoint:
- /bin/sh
command: [ "-c", "apk add openssl; openssl req -subj '/CN=inbucket.test' -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout /tmp/server.key -out /tmp/server.crt; /start-inbucket.sh" ]
environment:
INBUCKET_SMTP_TLSENABLED: "true"
INBUCKET_SMTP_TLSPRIVKEY: /tmp/server.key
INBUCKET_SMTP_TLSCERT: /tmp/server.crt
INBUCKET_STORAGE_MAILBOXMSGCAP: 1000
labels:
- "traefik.enable=true"
- "traefik.http.routers.inbucket.entrypoints=https"
- "traefik.http.routers.inbucket.rule=Host(`${INBUCKET_DOMAIN:-mail.qsfera.test}`)"
- "traefik.http.routers.inbucket.tls.certresolver=http"
- "traefik.http.routers.inbucket.service=inbucket"
- "traefik.http.services.inbucket.loadbalancer.server.port=9000"
logging:
driver: ${LOG_DRIVER:-local}
restart: always
@@ -0,0 +1,41 @@
---
services:
qsfera:
environment:
# Keycloak IDP specific configuration for auto-provisioning
OC_LDAP_SERVER_WRITE_ENABLED: "true"
PROXY_AUTOPROVISION_ACCOUNTS: "true"
# Use the `sub` claim from keycloak for the user ID
# Keycloak uses the keycloak user ID as the `sub` claim
PROXY_USER_OIDC_CLAIM: "sub"
# Use the `sub` claim as identifier during autoprovisioning
# That mitigates problems when a user is renamed in keycloak
PROXY_AUTOPROVISION_CLAIM_USERNAME: "sub"
PROXY_USER_CS3_CLAIM: "username"
# This triggers the creation of the qsferaUUID during the provisioning of users and groups
GRAPH_LDAP_SERVER_UUID: "false"
# This is the default value, we need to set it here because we overwrite the values
OC_LDAP_USER_SCHEMA_ID: "qsferaUUID"
# This is the default value, we need to set it here because we overwrite the values
OC_LDAP_GROUP_SCHEMA_ID: "qsferaUUID"
# This is the default value, we need to set it here because we overwrite the values
OC_LDAP_DISABLE_USER_MECHANISM: "attribute"
# These values should only be set in keycloak, because qsfera updates them from the claims
FRONTEND_READONLY_USER_ATTRIBUTES: "user.onPremisesSamAccountName,user.displayName,user.mail,user.passwordProfile,user.memberOf"
ldap-server:
volumes:
# Use an empty named volume to overwrite the inherited values
- empty-dir:/ldifs
# Only use the base ldif file to create the base structure
- ./config/ldap/ldif/10_base.ldif:/ldifs/10_base.ldif
# Use the custom schema from qsfera because we are in full control of the ldap server
- ../shared/config/ldap/schemas/10_qsfera_schema.ldif:/schemas/10_qsfera_schema.ldif
- ./config/ldap/docker-entrypoint-override.sh:/opt/bitnami/scripts/openldap/docker-entrypoint-override.sh
- ldap-certs:/opt/bitnami/openldap/share
- ldap-data:/bitnami/openldap
keycloak:
volumes:
- "./config/keycloak/docker-entrypoint-override.sh:/opt/keycloak/bin/docker-entrypoint-override.sh"
- "./config/keycloak/qsfera-realm-autoprovisioning.dist.json:/opt/keycloak/data/import-dist/qsfera-realm.json"
volumes:
empty-dir:
@@ -0,0 +1,74 @@
---
services:
traefik:
networks:
qsfera-net:
aliases:
- ${KEYCLOAK_DOMAIN:-keycloak.qsfera.test}
qsfera:
environment:
# Keycloak IDP specific configuration
PROXY_AUTOPROVISION_ACCOUNTS: "false"
PROXY_ROLE_ASSIGNMENT_DRIVER: "oidc"
OC_OIDC_ISSUER: https://${KEYCLOAK_DOMAIN:-keycloak.qsfera.test}/realms/${KEYCLOAK_REALM:-qsfera}
PROXY_OIDC_REWRITE_WELLKNOWN: "true"
WEB_OIDC_CLIENT_ID: ${OC_OIDC_CLIENT_ID:-web}
PROXY_USER_OIDC_CLAIM: "uuid"
PROXY_USER_CS3_CLAIM: "userid"
WEB_OPTION_ACCOUNT_EDIT_LINK_HREF: "https://${KEYCLOAK_DOMAIN:-keycloak.qsfera.test}/realms/${KEYCLOAK_REALM:-qsfera}/account"
# admin and demo accounts must be created in Keycloak
OC_ADMIN_USER_ID: ""
SETTINGS_SETUP_DEFAULT_ASSIGNMENTS: "false"
GRAPH_ASSIGN_DEFAULT_USER_ROLE: "false"
GRAPH_USERNAME_MATCH: "none"
KEYCLOAK_DOMAIN: ${KEYCLOAK_DOMAIN:-keycloak.qsfera.test}
postgres:
image: postgres:alpine
networks:
qsfera-net:
volumes:
- keycloak_postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: keycloak
logging:
driver: ${LOG_DRIVER:-local}
restart: always
keycloak:
image: quay.io/keycloak/keycloak:25.0.0
networks:
qsfera-net:
command: ["start", "--proxy=edge", "--spi-connections-http-client-default-disable-trust-manager=${INSECURE:-false}", "--import-realm"]
entrypoint: ["/bin/sh", "/opt/keycloak/bin/docker-entrypoint-override.sh"]
volumes:
- "./config/keycloak/docker-entrypoint-override.sh:/opt/keycloak/bin/docker-entrypoint-override.sh"
- "./config/keycloak/qsfera-realm.dist.json:/opt/keycloak/data/import-dist/qsfera-realm.json"
environment:
OC_DOMAIN: ${OC_DOMAIN:-cloud.qsfera.test}
KC_HOSTNAME: ${KEYCLOAK_DOMAIN:-keycloak.qsfera.test}
KC_DB: postgres
KC_DB_URL: "jdbc:postgresql://postgres:5432/keycloak"
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: keycloak
KC_FEATURES: impersonation
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN_USER:-admin}
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin}
labels:
- "traefik.enable=true"
- "traefik.http.routers.keycloak.entrypoints=https"
- "traefik.http.routers.keycloak.rule=Host(`${KEYCLOAK_DOMAIN:-keycloak.qsfera.test}`)"
- "traefik.http.routers.keycloak.tls.certresolver=http"
- "traefik.http.routers.keycloak.service=keycloak"
- "traefik.http.services.keycloak.loadbalancer.server.port=8080"
depends_on:
- postgres
logging:
driver: ${LOG_DRIVER:-local}
restart: always
volumes:
keycloak_postgres_data:
@@ -0,0 +1,59 @@
---
services:
traefik:
networks:
qsfera-net:
qsfera:
environment:
# Ldap IDP specific configuration
OC_LDAP_URI: ldaps://ldap-server:1636
OC_LDAP_INSECURE: "true"
OC_LDAP_BIND_DN: "cn=admin,dc=qsfera,dc=eu"
OC_LDAP_BIND_PASSWORD: ${LDAP_ADMIN_PASSWORD:-admin}
OC_LDAP_GROUP_BASE_DN: "ou=groups,dc=qsfera,dc=eu"
OC_LDAP_GROUP_SCHEMA_ID: "entryUUID"
OC_LDAP_USER_BASE_DN: "ou=users,dc=qsfera,dc=eu"
OC_LDAP_USER_FILTER: "(objectclass=inetOrgPerson)"
OC_LDAP_USER_SCHEMA_ID: "entryUUID"
OC_LDAP_DISABLE_USER_MECHANISM: "none"
GRAPH_LDAP_SERVER_UUID: "true"
GRAPH_LDAP_GROUP_CREATE_BASE_DN: "ou=custom,ou=groups,dc=qsfera,dc=eu"
GRAPH_LDAP_REFINT_ENABLED: "true" # osixia has refint enabled.
FRONTEND_READONLY_USER_ATTRIBUTES: "user.onPremisesSamAccountName,user.displayName,user.mail,user.passwordProfile,user.accountEnabled,user.appRoleAssignments"
OC_LDAP_SERVER_WRITE_ENABLED: "false" # assuming the external ldap is not writable
# OC_RUN_SERVICES specifies to start all services except glauth, idm and accounts. These are replaced by external services
OC_EXCLUDE_RUN_SERVICES: idm
ldap-server:
image: bitnamilegacy/openldap:2.6
networks:
qsfera-net:
entrypoint: ["/bin/sh", "/opt/bitnami/scripts/openldap/docker-entrypoint-override.sh", "/opt/bitnami/scripts/openldap/run.sh" ]
environment:
BITNAMI_DEBUG: true
LDAP_TLS_VERIFY_CLIENT: never
LDAP_ENABLE_TLS: "yes"
LDAP_TLS_CA_FILE: /opt/bitnami/openldap/share/openldap.crt
LDAP_TLS_CERT_FILE: /opt/bitnami/openldap/share/openldap.crt
LDAP_TLS_KEY_FILE: /opt/bitnami/openldap/share/openldap.key
LDAP_ROOT: "dc=qsfera,dc=eu"
LDAP_ADMIN_PASSWORD: ${LDAP_ADMIN_PASSWORD:-admin}
ports:
- "127.0.0.1:389:1389"
- "127.0.0.1:636:1636"
volumes:
- ./config/ldap/ldif:/ldifs
- ./config/ldap/docker-entrypoint-override.sh:/opt/bitnami/scripts/openldap/docker-entrypoint-override.sh
- ldap-certs:/opt/bitnami/openldap/share
- ldap-data:/bitnami/openldap
logging:
driver: ${LOG_DRIVER:-local}
restart: always
volumes:
ldap-certs:
ldap-data:
networks:
qsfera-net:
@@ -0,0 +1,32 @@
---
services:
minio:
image: minio/minio:latest
# release notes: https://github.com/minio/minio/releases
networks:
qsfera-net:
entrypoint:
- /bin/sh
command:
[
"-c",
"mkdir -p /data/${DECOMPOSEDS3_BUCKET:-qsfera-bucket} && minio server --console-address ':9001' /data",
]
volumes:
- minio-data:/data
environment:
MINIO_ROOT_USER: ${DECOMPOSEDS3_ACCESS_KEY:-qsfera}
MINIO_ROOT_PASSWORD: ${DECOMPOSEDS3_SECRET_KEY:-qsfera-secret-key}
labels:
- "traefik.enable=true"
- "traefik.http.routers.minio.entrypoints=https"
- "traefik.http.routers.minio.rule=Host(`${MINIO_DOMAIN:-minio.qsfera.test}`)"
- "traefik.http.routers.minio.tls.certresolver=http"
- "traefik.http.routers.minio.service=minio"
- "traefik.http.services.minio.loadbalancer.server.port=9001"
logging:
driver: ${LOG_DRIVER:-local}
restart: always
volumes:
minio-data:
@@ -0,0 +1,26 @@
---
services:
qsfera:
environment:
# tracing
OC_TRACING_ENABLED: "true"
OC_TRACING_TYPE: "jaeger"
OC_TRACING_ENDPOINT: jaeger-agent:6831
# metrics
# if КуСфера runs as a single process, all <debug>/metrics endpoints
# will expose the same metrics, so it's sufficient to query one endpoint
PROXY_DEBUG_ADDR: 0.0.0.0:9205
collaboration-oo:
environment:
# tracing
OC_TRACING_ENABLED: "true"
OC_TRACING_TYPE: "jaeger"
OC_TRACING_ENDPOINT: jaeger-agent:6831
# metrics
COLLABORATION_DEBUG_ADDR: 0.0.0.0:9304
networks:
qsfera-net:
external: true
@@ -0,0 +1,26 @@
---
services:
qsfera:
environment:
# tracing
OC_TRACING_ENABLED: "true"
OC_TRACING_TYPE: "jaeger"
OC_TRACING_ENDPOINT: jaeger-agent:6831
# metrics
# if КуСфера runs as a single process, all <debug>/metrics endpoints
# will expose the same metrics, so it's sufficient to query one endpoint
PROXY_DEBUG_ADDR: 0.0.0.0:9205
collaboration:
environment:
# tracing
OC_TRACING_ENABLED: "true"
OC_TRACING_TYPE: "jaeger"
OC_TRACING_ENDPOINT: jaeger-agent:6831
# metrics
COLLABORATION_DEBUG_ADDR: 0.0.0.0:9304
networks:
qsfera-net:
external: true
@@ -0,0 +1,82 @@
---
services:
traefik:
networks:
qsfera-net:
aliases:
- ${OC_DOMAIN:-cloud.qsfera.test}
qsfera:
image: ${OC_DOCKER_IMAGE:-qsfera/qsfera-rolling}:${OC_DOCKER_TAG:-latest}
# changelog: https://github.com/qsfera/server/tree/main/changelog
# release notes: https://docs.qsfera.eu/qsfera_release_notes.html
networks:
qsfera-net:
entrypoint:
- /bin/sh
# run qsfera init to initialize a configuration file with random secrets
# it will fail on subsequent runs, because the config file already exists
# therefore we ignore the error and then start the qsfera server
command: ["-c", "qsfera init || true; qsfera server"]
environment:
# enable services that are not started automatically
OC_ADD_RUN_SERVICES: ${START_ADDITIONAL_SERVICES}
OC_URL: https://${OC_DOMAIN:-cloud.qsfera.test}
OC_LOG_LEVEL: ${LOG_LEVEL:-info}
OC_LOG_COLOR: "${LOG_PRETTY:-false}"
OC_LOG_PRETTY: "${LOG_PRETTY:-false}"
# do not use SSL between Traefik and КуСфера
PROXY_TLS: "true"
# make the REVA gateway accessible to the app drivers
GATEWAY_GRPC_ADDR: 0.0.0.0:9142
# INSECURE: needed if КуСфера / Traefik is using self generated certificates
OC_INSECURE: "${INSECURE:-false}"
# basic auth (not recommended, but needed for eg. WebDav clients that do not support OpenID Connect)
PROXY_ENABLE_BASIC_AUTH: "${PROXY_ENABLE_BASIC_AUTH:-false}"
# admin user password
IDM_ADMIN_PASSWORD: "${ADMIN_PASSWORD:-admin}" # this overrides the admin password from the configuration file
# demo users
IDM_CREATE_DEMO_USERS: "${DEMO_USERS:-false}"
# idp login form settings
IDP_DEFAULT_SIGNIN_PAGE_TEXT: "${IDP_DEFAULT_SIGNIN_PAGE_TEXT}"
# email server (if configured)
NOTIFICATIONS_SMTP_HOST: "${SMTP_HOST}"
NOTIFICATIONS_SMTP_PORT: "${SMTP_PORT}"
NOTIFICATIONS_SMTP_SENDER: "${SMTP_SENDER:-КуСфера notifications <notifications@${OC_DOMAIN:-cloud.qsfera.test}>}"
NOTIFICATIONS_SMTP_USERNAME: "${SMTP_USERNAME}"
NOTIFICATIONS_SMTP_PASSWORD: "${SMTP_PASSWORD}"
NOTIFICATIONS_SMTP_INSECURE: "${SMTP_INSECURE}"
NOTIFICATIONS_SMTP_AUTHENTICATION: "${SMTP_AUTHENTICATION}"
NOTIFICATIONS_SMTP_ENCRYPTION: "${SMTP_TRANSPORT_ENCRYPTION:-none}"
FRONTEND_ARCHIVER_MAX_SIZE: "10000000000"
# make the registry available to the app provider containers
MICRO_REGISTRY_ADDRESS: 127.0.0.1:9233
NATS_NATS_HOST: 0.0.0.0
NATS_NATS_PORT: 9233
PROXY_CSP_CONFIG_FILE_LOCATION: /etc/qsfera/csp.yaml
# these three vars are needed to the csp config file to include the web office apps and the importer
COLLABORA_DOMAIN: ${COLLABORA_DOMAIN:-collabora.qsfera.test}
COMPANION_DOMAIN: ${COMPANION_DOMAIN:-companion.qsfera.test}
# enable to allow using the banned passwords list
OC_PASSWORD_POLICY_BANNED_PASSWORDS_LIST: banned-password-list.txt
volumes:
- ./config/qsfera/app-registry.yaml:/etc/qsfera/app-registry.yaml
- ./config/qsfera/csp.yaml:/etc/qsfera/csp.yaml
- ./config/qsfera/banned-password-list.txt:/etc/qsfera/banned-password-list.txt
# configure the .env file to use own paths instead of docker internal volumes
- ${OC_CONFIG_DIR:-qsfera-config}:/etc/qsfera
- ${OC_DATA_DIR:-qsfera-data}:/var/lib/qsfera
labels:
- "traefik.enable=true"
- "traefik.http.routers.qsfera.entrypoints=https"
- "traefik.http.routers.qsfera.rule=Host(`${OC_DOMAIN:-cloud.qsfera.test}`)"
- "traefik.http.routers.qsfera.tls.certresolver=http"
- "traefik.http.routers.qsfera.service=qsfera"
- "traefik.http.services.qsfera.loadbalancer.server.port=9200"
- "traefik.http.services.qsfera.loadbalancer.server.scheme=https"
logging:
driver: ${LOG_DRIVER:-local}
restart: always
volumes:
qsfera-config:
qsfera-data:
@@ -0,0 +1,18 @@
---
services:
qsfera:
volumes:
# external sites needs to have additional routes configured in the proxy
- ./config/qsfera/proxy.yaml:/etc/qsfera/proxy.yaml
radicale:
image: ${RADICALE_DOCKER_IMAGE:-opencloudeu/radicale}:${RADICALE_DOCKER_TAG:-latest}
networks:
qsfera-net:
logging:
driver: ${LOG_DRIVER:-local}
restart: always
volumes:
- ./config/radicale/config:/etc/radicale/config
- ${RADICALE_DATA_DIR:-radicale-data}:/var/lib/radicale
volumes:
radicale-data:
@@ -0,0 +1,17 @@
---
services:
tika:
image: ${TIKA_IMAGE:-apache/tika:latest-full}
# release notes: https://tika.apache.org
networks:
qsfera-net:
restart: always
logging:
driver: ${LOG_DRIVER:-local}
qsfera:
environment:
# fulltext search
SEARCH_EXTRACTOR_TYPE: tika
SEARCH_EXTRACTOR_TIKA_TIKA_URL: http://tika:9998
FRONTEND_FULL_TEXT_SEARCH_ENABLED: "true"
@@ -0,0 +1,15 @@
---
services:
qsfera:
depends_on:
drawio-init:
condition: service_completed_successfully
drawio-init:
image: opencloudeu/web-extensions:draw-io-1.0.0
user: root
volumes:
- qsfera-apps:/apps
entrypoint:
- /bin/sh
command: ["-c", "cp -R /usr/share/nginx/html/draw-io/ /apps"]
@@ -0,0 +1,7 @@
services:
qsfera:
volumes:
- qsfera-apps:/var/lib/qsfera/web/assets/apps
volumes:
qsfera-apps:
@@ -0,0 +1,18 @@
---
services:
qsfera:
volumes:
# external sites needs to have sites configured in the web.yaml
- ./config/qsfera/apps.yaml:/etc/qsfera/apps.yaml
depends_on:
externalsites-init:
condition: service_completed_successfully
externalsites-init:
image: opencloudeu/web-extensions:external-sites-1.0.0
user: root
volumes:
- qsfera-apps:/apps
entrypoint:
- /bin/sh
command: ["-c", "cp -R /usr/share/nginx/html/external-sites/ /apps"]
@@ -0,0 +1,52 @@
---
services:
traefik:
networks:
qsfera-net:
aliases:
- ${COMPANION_DOMAIN:-companion.qsfera.test}
qsfera:
volumes:
# the cloud importer needs to be enabled in the web.yaml
- ./config/qsfera/apps.yaml:/etc/qsfera/apps.yaml
depends_on:
importer-init:
condition: service_completed_successfully
importer-init:
image: opencloudeu/web-extensions:importer-1.0.0
user: root
volumes:
- qsfera-apps:/apps
entrypoint:
- /bin/sh
command: [ "-c", "cp -R /usr/share/nginx/html/importer/ /apps" ]
companion:
image: ${COMPANION_IMAGE:-transloadit/companion:5.5.0}
networks:
- qsfera-net
environment:
NODE_ENV: production
NODE_TLS_REJECT_UNAUTHORIZED: 0
COMPANION_DATADIR: /tmp/companion/
COMPANION_DOMAIN: ${COMPANION_DOMAIN:-companion.qsfera.test}
COMPANION_PROTOCOL: https
COMPANION_UPLOAD_URLS: "^https://${OC_DOMAIN:-cloud.qsfera.test}/"
COMPANION_ONEDRIVE_KEY: "${COMPANION_ONEDRIVE_KEY}"
COMPANION_ONEDRIVE_SECRET: "${COMPANION_ONEDRIVE_SECRET}"
volumes:
- companion-data:/tmp/companion/
labels:
- "traefik.enable=true"
- "traefik.http.routers.companion.entrypoints=https"
- "traefik.http.routers.companion.rule=Host(`${COMPANION_DOMAIN:-companion.qsfera.test}`)"
- "traefik.http.routers.companion.tls.certresolver=http"
- "traefik.http.routers.companion.service=companion"
- "traefik.http.services.companion.loadbalancer.server.port=3020"
logging:
driver: ${LOG_DRIVER:-local}
restart: always
volumes:
companion-data:
@@ -0,0 +1,15 @@
---
services:
qsfera:
depends_on:
jsonviewer-init:
condition: service_completed_successfully
jsonviewer-init:
image: opencloudeu/web-extensions:json-viewer-1.0.0
user: root
volumes:
- qsfera-apps:/apps
entrypoint:
- /bin/sh
command: ["-c", "cp -R /usr/share/nginx/html/json-viewer/ /apps"]
@@ -0,0 +1,15 @@
---
services:
qsfera:
depends_on:
progressbars-init:
condition: service_completed_successfully
progressbars-init:
image: opencloudeu/web-extensions:progress-bars-1.0.0
user: root
volumes:
- qsfera-apps:/apps
entrypoint:
- /bin/sh
command: ["-c", "cp -R /usr/share/nginx/html/progress-bars/ /apps"]
@@ -0,0 +1,15 @@
---
services:
qsfera:
depends_on:
unzip-init:
condition: service_completed_successfully
unzip-init:
image: opencloudeu/web-extensions:unzip-1.0.2
user: root
volumes:
- qsfera-apps:/apps
entrypoint:
- /bin/sh
command: ["-c", "cp -R /usr/share/nginx/html/unzip/ /apps"]
@@ -0,0 +1,17 @@
package postprocessing
import future.keywords.if
import data.utils
default granted := true
granted = false if {
not utils.is_extension_allowed(input.resource.name)
}
granted = false if {
bytes := qsfera.resource.download(input.resource.url)
mimetype := qsfera.mimetype.detect(bytes)
not utils.is_mimetype_allowed(mimetype)
}
@@ -0,0 +1,33 @@
package proxy
import future.keywords.if
import data.utils
default granted := true
granted = false if {
input.request.method == "PUT"
pathPrefixes := [
"/dav",
"/remote.php/webdav",
"/remote.php/dav",
"/webdav",
]
restricted := pathPrefixes[_]
startswith(input.request.path, restricted)
not utils.is_extension_allowed(input.resource.name)
}
granted = false if {
input.request.method == "POST"
pathPrefixes := [
"/data",
"/dav",
"/remote.php/webdav",
"/remote.php/dav",
"/webdav",
]
restricted := pathPrefixes[_]
startswith(input.request.path, restricted)
not utils.is_extension_allowed(input.resource.name)
}
@@ -0,0 +1,22 @@
package utils
ALLOWED_RESOURCE_EXTENSIONS := [
".apk", ".avi", ".bat", ".bmp", ".css", ".csv", ".doc", ".docm", ".docx",
".docxf", ".dotx", ".eml", ".epub", ".htm", ".html", ".ipa", ".jar", ".java",
".jpg", ".js", ".json", ".mp3", ".mp4", ".msg", ".odp", ".ods", ".odt", ".oform",
".ots", ".ott", ".pdf", ".php", ".png", ".potm", ".potx", ".ppsm", ".ppsx", ".ppt",
".pptm", ".pptx", ".py", ".rtf", ".sb3", ".sprite3", ".sql", ".svg", ".tif", ".tiff",
".txt", ".xls", ".xlsm", ".xlsx", ".xltm", ".xltx", ".xml", ".zip", ".md"
]
is_extension_allowed(identifier) {
extension := ALLOWED_RESOURCE_EXTENSIONS[_]
endswith(identifier, extension)
}
is_mimetype_allowed(mimetype) {
extensions := qsfera.mimetype.extensions(mimetype)
extension := extensions[_]
is_extension_allowed(extension)
}
@@ -0,0 +1,24 @@
---
# This file can be used to be added to the qsfera_full example
# to browse the LDAP server with a web interface.
# This is not a production ready setup.
services:
ldap-manager:
image: phpldapadmin/phpldapadmin:latest
networks:
qsfera-net:
environment:
LDAP_HOST: ldap-server
LDAP_PORT: 1389
LDAP_LOGIN_OBJECTCLASS: "inetOrgPerson"
APP_URL: "https://${LDAP_MANAGER_DOMAIN:-ldap.qsfera.test}"
labels:
- "traefik.enable=true"
- "traefik.http.routers.ldap-manager.entrypoints=https"
- "traefik.http.routers.ldap-manager.rule=Host(`${LDAP_MANAGER_DOMAIN:-ldap.qsfera.test}`)"
- "traefik.http.routers.ldap-manager.tls.certresolver=http"
- "traefik.http.routers.ldap-manager.service=ldap-manager"
- "traefik.http.services.ldap-manager.loadbalancer.server.port=8080"
logging:
driver: ${LOG_DRIVER:-local}
restart: always
@@ -0,0 +1,39 @@
# This LDIF files describes the КуСфера schema
dn: cn=qsfera,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: qsfera
olcObjectIdentifier: qsferaOid 1.3.6.1.4.1.63016
# We'll use qsferaOid:1 subarc for LDAP related stuff
# qsferaOid:1.1 for AttributeTypes and qsferaOid:1.2 for ObjectClasses
olcAttributeTypes: ( qsferaOid:1.1.1 NAME 'qsferaUUID'
DESC 'A non-reassignable and persistent account ID)'
EQUALITY uuidMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.1.16.1 SINGLE-VALUE )
olcAttributeTypes: ( qsferaOid:1.1.2 NAME 'qsferaExternalIdentity'
DESC 'A triple separated by "$" representing the objectIdentity resource type of the Graph API ( signInType $ issuer $ issuerAssignedId )'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: ( qsferaOid:1.1.3 NAME 'qsferaUserEnabled'
DESC 'A boolean value indicating if the user is enabled'
EQUALITY booleanMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE)
olcAttributeTypes: ( qsferaOid:1.1.4 NAME 'qsferaUserType'
DESC 'User type (e.g. Member or Guest)'
EQUALITY caseIgnoreMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )
olcAttributeTypes: ( qsferaOid:1.1.5 NAME 'qsferaLastSignInTimestamp'
DESC 'The timestamp of the last sign-in'
EQUALITY generalizedTimeMatch
ORDERING generalizedTimeOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE )
olcObjectClasses: ( qsferaOid:1.2.1 NAME 'qsferaObject'
DESC 'КуСфера base objectclass'
AUXILIARY
MAY ( qsferaUUID ) )
olcObjectClasses: ( qsferaOid:1.2.2 NAME 'qsferaUser'
DESC 'КуСфера User objectclass'
SUP qsferaObject
AUXILIARY
MAY ( qsferaExternalIdentity $ qsferaUserEnabled $ qsferaUserType $ qsferaLastSignInTimestamp) )
@@ -0,0 +1,45 @@
# This LDIF files describes the LDAP schema related to the Education
# endpoints of the libregraph API
dn: cn=qsferaEdu,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: qsferaEdu
olcAttributeTypes: ( qsferaOid:1.1.6 NAME 'qsferaMemberOfSchool'
DESC 'Used as a backreference to the school(s) to which a user or class is assigned'
EQUALITY uuidMatch
SYNTAX 1.3.6.1.1.16.1 )
olcAttributeTypes: ( qsferaOid:1.1.7 NAME 'qsferaEducationExternalId'
DESC 'An externally assigned string id for an education related object'
EQUALITY caseIgnoreMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: ( qsferaOid:1.1.8 NAME 'qsferaEducationClassType'
DESC 'category of a class e.g. "course" or "class"'
EQUALITY caseIgnoreMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: ( qsferaOid:1.1.9 NAME 'qsferaEducationTeacherMember'
DESC 'references a user who is assigned as a teacher to a class'
EQUALITY distinguishedNameMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )
olcAttributeTypes: ( qsferaOid:1.1.10 NAME 'qsferaEducationSchoolNumber'
DESC 'An externally assigned identifier for a school'
EQUALITY caseIgnoreMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: ( qsferaOid:1.1.11 NAME 'qsferaEducationSchoolTerminationTimestamp'
DESC 'A Timestamp at which a school is considered to be disabled'
EQUALITY generalizedTimeMatch
ORDERING generalizedTimeOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )
olcObjectClasses: ( qsferaOid:1.2.3 NAME 'qsferaEducationUser'
DESC 'КуСфера education user objectclass'
SUP qsferaUser
AUXILIARY
MAY ( qsferaEducationExternalId $ userClass $ qsferaMemberOfSchool ) )
olcObjectClasses: ( qsferaOid:1.2.4 NAME 'qsferaEducationClass'
DESC 'КуСфера education class objectclass'
SUP qsferaObject
AUXILIARY
MAY ( qsferaEducationExternalId $ qsferaEducationClassType $ qsferaEducationTeacherMember $ qsferaMemberOfSchool ) )
olcObjectClasses: ( qsferaOid:1.2.5 NAME 'qsferaEducationSchool'
DESC 'КуСфера education school objectclass'
SUP qsferaObject
AUXILIARY
MAY ( qsferaEducationSchoolNumber $ qsferaEducationSchoolTerminationTimestamp $ qsferaEducationExternalId) )