20 Commits

Author SHA1 Message Date
2f46b99e4e XWiki: add diagnostic and modern AuthService handling
- Added 05_set_authservice.yml to set XWikiPreferences.authenticationService
  to modern component hints (standard, oidc, ldap).
- Added _auth_diag.yml to introspect registered AuthService components and
  verify the active preference.
- Updated docker-compose.yml.j2 to use -Dxwiki.authentication.authservice
  instead of deprecated authclass syntax.
- Temporarily included AuthDiag task in 01_core.yml for runtime verification.

Context: https://chatgpt.com/share/69005d88-6bf8-800f-af41-73b0e5dc9c13
2025-10-28 07:07:42 +01:00
295ae7e477 Solved Mediawiki CPS bug whichg prevented OIDC Login 2025-10-27 20:33:07 +01:00
c67ccc1df6 Used path_join @ web-app-friendica 2025-10-26 15:48:28 +01:00
cb483f60d1 optimized for easier debugging 2025-10-25 12:52:17 +02:00
2be73502ca Solved tests 2025-10-25 11:46:36 +02:00
57d5269b07 CSP (Safari-safe): merge -elem/-attr into base; respect explicit disables; no mirror-back; header only for documents/workers
- Add CSP3 support for style/script: include -elem and -attr directives
- Base (style-src, script-src) now unions elem/attr (CSP2/Safari fallback)
- Respect explicit base disables (e.g. style-src.unsafe-inline: false)
- Hashes only when 'unsafe-inline' absent in the final base tokens
- Nginx: set CSP only for HTML/worker via header_filter_by_lua_block; drop for subresources
- Remove per-location header_filter; keep body_filter only
- Update app role flags to *-attr where appropriate; extend desktop CSS sources
- Add comprehensive unit tests for union/explicit-disable/no-mirror-back

Ref: https://chatgpt.com/share/68f87a0a-cebc-800f-bb3e-8c8ab4dee8ee
2025-10-22 13:53:06 +02:00
1eefdea050 Solved CSP errors for MiniQR 2025-10-22 12:49:22 +02:00
561160504e Add new web-app-mini-qr role
- Introduced new role 'web-app-mini-qr' to deploy the lightweight, self-hosted Mini-QR application.
- Added dedicated subnet and localhost port mapping (8059) in group_vars.
- Ensured proper dependency structure and run_once handling in MIG role.
- Included upstream reference and CSP whitelist for temporary clarity.ms removal tracking.
- Added README.md and meta information following the Infinito.Nexus web-app schema.

See: https://chatgpt.com/share/68f890ab-5960-800f-85f8-ba30bd4350fe
2025-10-22 10:07:35 +02:00
9a4bf91276 feat(nextcloud): enable custom Alpine-based Whiteboard image with Chromium & ffmpeg support
- Added role tasks to deploy templated Dockerfile for Whiteboard service
- Configured build context and custom image name (nextcloud_whiteboard_custom)
- Increased PID limits and shm_size for stable recording
- Adjusted user ID variable naming consistency
- Integrated path_join for service directory variables
- Fixed build permissions (install as root, revert to nobody)

Reference: ChatGPT conversation https://chatgpt.com/share/68f771c6-0e98-800f-99ca-9e367f4cd0c2
2025-10-21 13:44:11 +02:00
468b6e734c Deactivated whiteboar 2025-10-20 21:17:06 +02:00
83cb94b6ff Refactored Redis resource include macro and increased memory limits
- Replaced deprecated lookup(vars=...) in svc-db-redis with macro-based include (Ansible/Jinja safe)
- Redis now uses higher resource values (1 CPU, 1G reserved, 8G max, 512 pids)
- Enables stable Whiteboard operation with >3.5 GB Redis memory usage
- Related conversation: https://chatgpt.com/share/68f67a00-d598-800f-a6be-ee5987e66fba
2025-10-20 20:08:38 +02:00
6857295969 Fix variable definition test to recognize block-style Jinja 'set ... endset' statements
This update extends the regex to detect block-style variable definitions such as:
  {% set var %} ... {% endset %}
Previously, only inline 'set var =' syntax was recognized, causing false positives
like '_snippet' being flagged as undefined in Jinja templates.

Reference: https://chatgpt.com/share/68f6799a-eb80-800f-ab5c-7c196d4c4661
2025-10-20 20:04:40 +02:00
8ab398f679 nextcloud:whiteboard: wait for Redis before start (depends_on: service_healthy) to prevent early SocketClosedUnexpectedlyError
Context: added depends_on on redis for the Whiteboard service so websockets don’t crash when Redis isn’t ready yet. See discussion: https://chatgpt.com/share/68f65a3e-aa54-800f-a1a7-e6878775fd7e
2025-10-20 17:50:47 +02:00
31133ddd90 Enhancement: Fix for Nextcloud Whiteboard recording and collaboration server
- Added Chromium headless flags and writable font cache/tmp volumes
- Enabled WebSocket proxy forwarding for /whiteboard/
- Verified and adjusted CSP and frontend integration
- Added Whiteboard-related variables and volumes in main.yml

See ChatGPT conversation (20 Oct 2025):
https://chatgpt.com/share/68f655e1-fa3c-800f-b35f-4f875dfed4fd
2025-10-20 17:31:59 +02:00
783b1e152d Added numpy 2025-10-20 11:03:44 +02:00
eca567fefd Made gitea LDAP Source primary domain independent 2025-10-18 10:54:39 +02:00
905f461ee8 Add basic healthcheck to oauth2-proxy container template using binary version check for distroless compatibility
Reference: https://chatgpt.com/share/68f35550-4248-800f-9c6a-dbd49a48592e
2025-10-18 10:52:58 +02:00
9f0b259ba9 Merge branch 'master' of github.com:kevinveenbirkenbach/infinito-nexus 2025-10-18 09:41:18 +02:00
06e4323faa Added ansible environmnet 2025-10-17 23:07:43 +02:00
3d99226f37 Refactor BigBlueButton and backup task structure:
- Moved database seed variables from vars/main.yml to task-level include in BigBlueButton
- Simplified core include logic in sys-ctl-bkp-docker-2-loc
- Ensured clean conditional for BKP_DOCKER_2_LOC_DB_ENABLED
See: https://chatgpt.com/share/68f216f7-62d8-800f-94e3-c82e4418e51b (deutsch)
2025-10-17 12:14:39 +02:00
79 changed files with 810 additions and 180 deletions

View File

@@ -10,9 +10,23 @@ from module_utils.config_utils import get_app_conf
from module_utils.get_url import get_url
def _dedup_preserve(seq):
"""Return a list with stable order and unique items."""
seen = set()
out = []
for x in seq:
if x not in seen:
seen.add(x)
out.append(x)
return out
class FilterModule(object):
"""
Custom filters for Content Security Policy generation and CSP-related utilities.
Jinja filters for building a robust, CSP3-aware Content-Security-Policy header.
Safari/CSP2 compatibility is ensured by merging the -elem/-attr variants into the base
directives (style-src, script-src). We intentionally do NOT mirror back into -elem/-attr
to allow true CSP3 granularity on modern browsers.
"""
def filters(self):
@@ -61,11 +75,14 @@ class FilterModule(object):
"""
Returns CSP flag tokens (e.g., "'unsafe-eval'", "'unsafe-inline'") for a directive,
merging sane defaults with app config.
Default: 'unsafe-inline' is enabled for style-src and style-src-elem.
Defaults:
- For styles we enable 'unsafe-inline' by default (style-src, style-src-elem, style-src-attr),
because many apps rely on inline styles / style attributes.
- For scripts we do NOT enable 'unsafe-inline' by default.
"""
# Defaults that apply to all apps
default_flags = {}
if directive in ('style-src', 'style-src-elem'):
if directive in ('style-src', 'style-src-elem', 'style-src-attr'):
default_flags = {'unsafe-inline': True}
configured = get_app_conf(
@@ -76,7 +93,6 @@ class FilterModule(object):
{}
)
# Merge defaults with configured flags (configured overrides defaults)
merged = {**default_flags, **configured}
tokens = []
@@ -131,82 +147,148 @@ class FilterModule(object):
):
"""
Builds the Content-Security-Policy header value dynamically based on application settings.
- Flags (e.g., 'unsafe-eval', 'unsafe-inline') are read from server.csp.flags.<directive>,
with sane defaults applied in get_csp_flags (always 'unsafe-inline' for style-src and style-src-elem).
- Inline hashes are read from server.csp.hashes.<directive>.
- Whitelists are read from server.csp.whitelist.<directive>.
- Inline hashes are added only if the final tokens do NOT include 'unsafe-inline'.
Key points:
- CSP3-aware: supports base/elem/attr for styles and scripts.
- Safari/CSP2 fallback: base directives (style-src, script-src) always include
the union of their -elem/-attr variants.
- We do NOT mirror back into -elem/-attr; finer CSP3 rules remain effective
on modern browsers if you choose to use them.
- If the app explicitly disables a token on the *base* (e.g. style-src.unsafe-inline: false),
that token is removed from the merged base even if present in elem/attr.
- Inline hashes are added ONLY if that directive does NOT include 'unsafe-inline'.
- Whitelists/flags/hashes read from:
server.csp.whitelist.<directive>
server.csp.flags.<directive>
server.csp.hashes.<directive>
- “Smart defaults”:
* internal CDN for style/script elem and connect
* Matomo endpoints (if feature enabled) for script-elem/connect
* Simpleicons (if feature enabled) for connect
* reCAPTCHA (if feature enabled) for script-elem/frame-src
* frame-ancestors extended for desktop/logout/keycloak if enabled
"""
try:
directives = [
'default-src', # Fallback source list for content types not explicitly listed
'connect-src', # Allowed URLs for XHR, WebSockets, EventSource, fetch()
'frame-ancestors', # Who may embed this page
'frame-src', # Sources for nested browsing contexts (e.g., <iframe>)
'script-src', # Sources for script execution
'script-src-elem', # Sources for <script> elements
'style-src', # Sources for inline styles and <style>/<link> elements
'style-src-elem', # Sources for <style> and <link rel="stylesheet">
'font-src', # Sources for fonts
'worker-src', # Sources for workers
'manifest-src', # Sources for web app manifests
'media-src', # Sources for audio and video
'default-src',
'connect-src',
'frame-ancestors',
'frame-src',
'script-src',
'script-src-elem',
'script-src-attr',
'style-src',
'style-src-elem',
'style-src-attr',
'font-src',
'worker-src',
'manifest-src',
'media-src',
]
parts = []
tokens_by_dir = {}
explicit_flags_by_dir = {}
for directive in directives:
# Collect explicit flags (to later respect explicit "False" on base during merge)
explicit_flags = get_app_conf(
applications,
application_id,
'server.csp.flags.' + directive,
False,
{}
)
explicit_flags_by_dir[directive] = explicit_flags
tokens = ["'self'"]
# Load flags (includes defaults from get_csp_flags)
# 1) Flags (with sane defaults)
flags = self.get_csp_flags(applications, application_id, directive)
tokens += flags
# Allow fetching from internal CDN by default for selected directives
if directive in ['script-src-elem', 'connect-src', 'style-src-elem']:
# 2) Internal CDN defaults for selected directives
if directive in ('script-src-elem', 'connect-src', 'style-src-elem', 'style-src'):
tokens.append(get_url(domains, 'web-svc-cdn', web_protocol))
# Matomo integration if feature is enabled
if directive in ['script-src-elem', 'connect-src']:
# 3) Matomo (if enabled)
if directive in ('script-src-elem', 'connect-src'):
if self.is_feature_enabled(applications, matomo_feature_name, application_id):
tokens.append(get_url(domains, 'web-app-matomo', web_protocol))
# Simpleicons integration if feature is enabled
if directive in ['connect-src']:
# 4) Simpleicons (if enabled) typically used via connect-src (fetch)
if directive == 'connect-src':
if self.is_feature_enabled(applications, 'simpleicons', application_id):
tokens.append(get_url(domains, 'web-svc-simpleicons', web_protocol))
# ReCaptcha integration (scripts + frames) if feature is enabled
# 5) reCAPTCHA (if enabled) scripts + frames
if self.is_feature_enabled(applications, 'recaptcha', application_id):
if directive in ['script-src-elem', 'frame-src']:
if directive in ('script-src-elem', 'frame-src'):
tokens.append('https://www.gstatic.com')
tokens.append('https://www.google.com')
# Frame ancestors handling (desktop + logout support)
# 6) Frame ancestors (desktop + logout)
if directive == 'frame-ancestors':
if self.is_feature_enabled(applications, 'desktop', application_id):
# Allow being embedded by the desktop app domain (and potentially its parent)
# Allow being embedded by the desktop app domain's site
domain = domains.get('web-app-desktop')[0]
sld_tld = ".".join(domain.split(".")[-2:]) # e.g., example.com
tokens.append(f"{sld_tld}")
if self.is_feature_enabled(applications, 'logout', application_id):
# Allow embedding via logout proxy and Keycloak app
tokens.append(get_url(domains, 'web-svc-logout', web_protocol))
tokens.append(get_url(domains, 'web-app-keycloak', web_protocol))
# Custom whitelist entries
# 7) Custom whitelist
tokens += self.get_csp_whitelist(applications, application_id, directive)
# Add inline content hashes ONLY if final tokens do NOT include 'unsafe-inline'
# (Check tokens, not flags, to include defaults and later modifications.)
# 8) Inline hashes (only if this directive does NOT include 'unsafe-inline')
if "'unsafe-inline'" not in tokens:
for snippet in self.get_csp_inline_content(applications, application_id, directive):
tokens.append(self.get_csp_hash(snippet))
# Append directive
parts.append(f"{directive} {' '.join(tokens)};")
tokens_by_dir[directive] = _dedup_preserve(tokens)
# Static img-src directive (kept permissive for data/blob and any host)
# ----------------------------------------------------------
# CSP3 families → ensure CSP2 fallback (Safari-safe)
# Merge style/script families so base contains union of elem/attr.
# Respect explicit disables on the base (e.g. unsafe-inline=False).
# Do NOT mirror back into elem/attr (keep granularity).
# ----------------------------------------------------------
def _strip_if_disabled(unioned_tokens, explicit_flags, name):
"""
Remove a token (e.g. 'unsafe-inline') from the unioned token list
if it is explicitly disabled in the base directive flags.
"""
if isinstance(explicit_flags, dict) and explicit_flags.get(name) is False:
tok = f"'{name}'"
return [t for t in unioned_tokens if t != tok]
return unioned_tokens
def merge_family(base_key, elem_key, attr_key):
base = tokens_by_dir.get(base_key, [])
elem = tokens_by_dir.get(elem_key, [])
attr = tokens_by_dir.get(attr_key, [])
union = _dedup_preserve(base + elem + attr)
# Respect explicit disables on the base
explicit_base = explicit_flags_by_dir.get(base_key, {})
# The most relevant flags for script/style:
for flag_name in ('unsafe-inline', 'unsafe-eval'):
union = _strip_if_disabled(union, explicit_base, flag_name)
tokens_by_dir[base_key] = union # write back only to base
merge_family('style-src', 'style-src-elem', 'style-src-attr')
merge_family('script-src', 'script-src-elem', 'script-src-attr')
# ----------------------------------------------------------
# Assemble header
# ----------------------------------------------------------
parts = []
for directive in directives:
if directive in tokens_by_dir:
parts.append(f"{directive} {' '.join(tokens_by_dir[directive])};")
# Keep permissive img-src for data/blob + any host (as before)
parts.append("img-src * data: blob:;")
return ' '.join(parts)

View File

@@ -112,6 +112,8 @@ defaults_networks:
subnet: 192.168.104.32/28
web-svc-coturn:
subnet: 192.168.104.48/28
web-app-mini-qr:
subnet: 192.168.104.64/28
# /24 Networks / 254 Usable Clients
web-app-bigbluebutton:

View File

@@ -80,6 +80,7 @@ ports:
web-app-flowise: 8056
web-app-minio_api: 8057
web-app-minio_console: 8058
web-app-mini-qr: 8059
web-app-bigbluebutton: 48087 # This port is predefined by bbb. @todo Try to change this to a 8XXX port
public:
# The following ports should be changed to 22 on the subdomain via stream mapping

View File

@@ -3,4 +3,7 @@ collections:
- name: community.general
- name: hetzner.hcloud
yay:
- python-simpleaudio
- python-simpleaudio
- python-numpy
pacman:
- ansible

View File

@@ -16,5 +16,12 @@
retries: 30
networks:
- default
{{ lookup('template', 'roles/docker-container/templates/resource.yml.j2',vars={'service_name':'redis'}) | indent(4) }}
{% macro include_resource_for(svc, indent=4) -%}
{% set service_name = svc -%}
{%- set _snippet -%}
{% include 'roles/docker-container/templates/resource.yml.j2' %}
{%- endset -%}
{{ _snippet | indent(indent, true) }}
{%- endmacro %}
{{ include_resource_for('redis') }}
{{ "\n" }}

View File

@@ -1,9 +1,6 @@
- block:
- include_tasks: 01_core.yml
when:
- run_once_sys_ctl_bkp_docker_2_loc is not defined
- include_tasks: 01_core.yml
when: run_once_sys_ctl_bkp_docker_2_loc is not defined
- name: "include 04_seed-database-to-backup.yml"
include_tasks: 04_seed-database-to-backup.yml
when:
- BKP_DOCKER_2_LOC_DB_ENABLED | bool
when: BKP_DOCKER_2_LOC_DB_ENABLED | bool

View File

@@ -10,17 +10,6 @@
lua_need_request_body on;
header_filter_by_lua_block {
local ct = ngx.header.content_type or ""
if ct:lower():find("^text/html") then
ngx.ctx.is_html = true
-- IMPORTANT: body will be modified → drop Content-Length to avoid mismatches
ngx.header.content_length = nil
else
ngx.ctx.is_html = false
end
}
body_filter_by_lua_block {
-- Only process HTML responses
if not ngx.ctx.is_html then

View File

@@ -1,3 +1,3 @@
ssl_certificate {{ [ LETSENCRYPT_LIVE_PATH, ssl_cert_folder, 'fullchain.pem'] | path_join }};
ssl_certificate_key {{ [ LETSENCRYPT_LIVE_PATH, ssl_cert_folder, 'privkey.pem' ] | path_join }};
ssl_trusted_certificate {{ [ LETSENCRYPT_LIVE_PATH, ssl_cert_folder, 'chain.pem' ] | path_join }};
ssl_certificate {{ [ LETSENCRYPT_LIVE_PATH | mandatory, ssl_cert_folder | mandatory, 'fullchain.pem'] | path_join }};
ssl_certificate_key {{ [ LETSENCRYPT_LIVE_PATH | mandatory, ssl_cert_folder | mandatory, 'privkey.pem' ] | path_join }};
ssl_trusted_certificate {{ [ LETSENCRYPT_LIVE_PATH | mandatory, ssl_cert_folder | mandatory, 'chain.pem' ] | path_join }};

View File

@@ -1,2 +1,33 @@
add_header Content-Security-Policy "{{ applications | build_csp_header(application_id, domains) }}" always;
proxy_hide_header Content-Security-Policy; # Todo: Make this optional
# ===== Content Security Policy: only for documents and workers (no locations needed) =====
# 1) Define your CSP once (Jinja: escape double quotes to be safe)
set $csp "{{ applications | build_csp_header(application_id, domains) | replace('\"','\\\"') }}";
# 2) Send CSP ONLY for document responses; also for workers via Sec-Fetch-Dest
header_filter_by_lua_block {
local ct = ngx.header.content_type or ngx.header["Content-Type"] or ""
local dest = ngx.var.http_sec_fetch_dest or ""
local lct = ct:lower()
local is_html = lct:find("^text/html") or lct:find("^application/xhtml+xml")
local is_worker = (dest == "worker") or (dest == "serviceworker")
if is_html or is_worker then
ngx.header["Content-Security-Policy"] = ngx.var.csp
else
ngx.header["Content-Security-Policy"] = nil
ngx.header["Content-Security-Policy-Report-Only"] = nil
end
-- If you'll modify the body later, drop Content-Length on HTML
if is_html then
ngx.ctx.is_html = true
ngx.header.content_length = nil
else
ngx.ctx.is_html = false
end
}
# 3) Prevent upstream/app CSP (duplicates)
proxy_hide_header Content-Security-Policy;
proxy_hide_header Content-Security-Policy-Report-Only;

View File

@@ -18,10 +18,10 @@ server:
flags:
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
font-src:

View File

@@ -37,5 +37,5 @@ server:
flags:
script-src-elem:
unsafe-inline: true
style-src:
style-src-attr:
unsafe-inline: true

View File

@@ -13,7 +13,7 @@ server:
flags:
script-src-elem:
unsafe-inline: true
style-src:
style-src-attr:
unsafe-inline: true
domains:
canonical:

View File

@@ -21,6 +21,12 @@
- name: "Include Seed routines for '{{ application_id }}' database backup"
include_tasks: "{{ [ playbook_dir, 'roles/sys-ctl-bkp-docker-2-loc/tasks/04_seed-database-to-backup.yml' ] | path_join }}"
vars:
database_type: "postgres"
database_instance: "{{ entity_name }}"
database_password: "{{ applications | get_app_conf(application_id, 'credentials.postgresql_secret') }}"
database_username: "postgres"
database_name: "" # Multiple databases
- name: configure websocket_upgrade.conf
copy:

View File

@@ -2,13 +2,6 @@
application_id: "web-app-bigbluebutton"
entity_name: "{{ application_id | get_entity_name }}"
# Database configuration
database_type: "postgres"
database_instance: "{{ application_id | get_entity_name }}"
database_password: "{{ applications | get_app_conf(application_id, 'credentials.postgresql_secret') }}"
database_username: "postgres"
database_name: "" # Multiple databases
# Proxy
domain: "{{ domains | get_domain(application_id) }}"
http_port: "{{ ports.localhost.http[application_id] }}"

View File

@@ -27,7 +27,7 @@ server:
flags:
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
domains:
canonical:

View File

@@ -29,7 +29,7 @@ server:
flags:
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
domains:
canonical:

View File

@@ -15,6 +15,8 @@ server:
- https://code.jquery.com/
style-src-elem:
- https://cdn.jsdelivr.net
- https://kit.fontawesome.com
- https://code.jquery.com/
font-src:
- https://ka-f.fontawesome.com
- https://cdn.jsdelivr.net
@@ -25,7 +27,7 @@ server:
frame-src:
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
flags:
script-src:
script-src-attr:
unsafe-inline: true
domains:
canonical:

View File

@@ -10,7 +10,7 @@ features:
server:
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true

View File

@@ -12,9 +12,7 @@ server:
script-src-elem:
unsafe-inline: true
unsafe-eval: true
style-src:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-eval: true
whitelist:
connect-src:

View File

@@ -18,10 +18,10 @@ server:
flags:
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
oauth2_proxy:
application: "application"

View File

@@ -7,10 +7,10 @@ docker_compose_flush_handlers: false
# Friendica
friendica_container: "friendica"
friendica_no_validation: "{{ applications | get_app_conf(application_id, 'features.oidc', True) }}" # Email validation is not neccessary if OIDC is active
friendica_no_validation: "{{ applications | get_app_conf(application_id, 'features.oidc') }}" # Email validation is not neccessary if OIDC is active
friendica_application_base: "/var/www/html"
friendica_docker_ldap_config: "{{ friendica_application_base }}/config/ldapauth.config.php"
friendica_host_ldap_config: "{{ docker_compose.directories.volumes }}ldapauth.config.php"
friendica_config_dir: "{{ friendica_application_base }}/config"
friendica_config_file: "{{ friendica_config_dir }}/local.config.php"
friendica_docker_ldap_config: "{{ [ friendica_application_base, 'config/ldapauth.config.php' ] | path_join }}"
friendica_host_ldap_config: "{{ [ docker_compose.directories.volumes, 'ldapauth.config.php' ] | path_join }}"
friendica_config_dir: "{{ [ friendica_application_base, 'config' ] | path_join }}"
friendica_config_file: "{{ [ friendica_config_dir, 'local.config.php' ] | path_join }}"
friendica_user: "www-data"

View File

@@ -27,7 +27,7 @@ server:
aliases: []
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
font-src:

View File

@@ -24,7 +24,7 @@ server:
flags:
script-src-elem:
unsafe-inline: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
font-src:

View File

@@ -2,7 +2,7 @@
shell: |
docker exec -i --user {{ GITEA_USER }} {{ GITEA_CONTAINER }} \
gitea admin auth list \
| awk -v name="LDAP ({{ PRIMARY_DOMAIN }})" '$0 ~ name {print $1; exit}'
| awk -v name="LDAP ({{ SOFTWARE_NAME }})" '$0 ~ name {print $1; exit}'
args:
chdir: "{{ docker_compose.directories.instance }}"
register: ldap_source_id_raw

View File

@@ -29,7 +29,7 @@ server:
script-src-elem:
unsafe-inline: true
unsafe-eval: true
script-src:
script-src-attr:
unsafe-inline: true
unsafe-eval: true
domains:

View File

@@ -14,7 +14,7 @@ server:
aliases: []
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true

View File

@@ -19,9 +19,9 @@ server:
flags:
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
frame-src:

View File

@@ -18,12 +18,12 @@ features:
server:
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true
unsafe-eval: true
script-src:
script-src-attr:
unsafe-inline: true
domains:
aliases: []

View File

@@ -16,11 +16,11 @@ server:
aliases: []
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
unsafe-eval: true
rbac:

View File

@@ -17,12 +17,12 @@ server:
style-src-elem:
- https://fonts.googleapis.com
flags:
script-src:
script-src-attr:
unsafe-eval: true
script-src-elem:
unsafe-inline: true
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
unsafe-eval: true
domains:

View File

@@ -27,12 +27,12 @@ features:
server:
csp:
flags:
script-src:
script-src-attr:
unsafe-eval: true
script-src-elem:
unsafe-inline: true
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
connect-src:

View File

@@ -4,6 +4,11 @@ server:
canonical:
- "m.wiki.{{ PRIMARY_DOMAIN }}"
aliases: []
csp:
flags:
script-src-elem:
unsafe-inline: true
docker:
services:
database:

View File

@@ -11,7 +11,7 @@ MEDIAWIKI_URL: "{{ domains | get_url(application_id, WEB_PROT
MEDIAWIKI_HTML_DIR: "/var/www/html"
MEDIAWIKI_CONFIG_DIR: "{{ docker_compose.directories.config }}"
MEDIAWIKI_VOLUMES_DIR: "{{ docker_compose.directories.volumes }}"
MEDIAWIKI_LOCAL_MOUNT_DIR: "{{ MEDIAWIKI_VOLUMES_DIR }}/mw-local"
MEDIAWIKI_LOCAL_MOUNT_DIR: "{{ [ MEDIAWIKI_VOLUMES_DIR, 'mw-local' ] | path_join }}"
MEDIAWIKI_LOCAL_PATH: "/opt/mw-local"
## Docker

View File

@@ -29,7 +29,7 @@ server:
frame-ancestors:
- "*" # No damage if it's used somewhere on other websites, it anyhow looks like art
flags:
style-src:
style-src-attr:
unsafe-inline: true
domains:
canonical:

View File

@@ -23,3 +23,5 @@
- name: Build data (single async task)
include_tasks: 02_build_data.yml
when: MIG_BUILD_DATA | bool
- include_tasks: utils/run_once.yml

View File

@@ -1,7 +1,4 @@
---
- block:
- include_tasks: 01_core.yml
- include_tasks: utils/run_once.yml
name: "Setup Meta Infinite Graph"
- include_tasks: 01_core.yml
when: run_once_web_app_mig is not defined

View File

@@ -0,0 +1,26 @@
# Mini-QR
## Description
**Mini-QR** is a lightweight, self-hosted web application for generating QR codes instantly and privately.
It provides a minimal and elegant interface to convert any text, URL, or message into a QR code — directly in your browser, without external tracking or dependencies.
## Overview
Mini-QR is designed for simplicity, privacy, and speed.
It offers an ad-free interface that works entirely within your local environment, making it ideal for individuals, organizations, and educational institutions that value data sovereignty.
The app runs as a single Docker container and requires no database or backend setup, enabling secure and frictionless QR generation anywhere.
## Features
- **Instant QR code creation** — simply type or paste your content.
- **Privacy-friendly** — all generation happens client-side; no data leaves your server.
- **Open Source** — fully auditable and modifiable for custom integrations.
- **Responsive Design** — optimized for both desktop and mobile devices.
- **Docker-ready** — can be deployed in seconds using the official image.
## Further Resources
- 🧩 Upstream project: [lyqht/mini-qr](https://github.com/lyqht/mini-qr)
- 📦 Upstream Dockerfile: [View on GitHub](https://github.com/lyqht/mini-qr/blob/main/Dockerfile)
- 🌐 Docker Image: `ghcr.io/lyqht/mini-qr:latest`

View File

@@ -0,0 +1,2 @@
# To-dos
- Remove clarity.ms

View File

@@ -0,0 +1,38 @@
docker:
services:
redis:
enabled: false
database:
enabled: false
features:
matomo: true
css: true
desktop: true
logout: false
server:
csp:
whitelist:
script-src-elem:
# Propably some tracking code
# Anyhow implemented to pass CSP checks
# @todo Remove
- https://www.clarity.ms/
- https://scripts.clarity.ms/
connect-src:
- https://q.clarity.ms
- https://n.clarity.ms
- "data:"
style-src-elem: []
font-src: []
frame-ancestors: []
flags:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true
script-src-attr:
unsafe-eval: true
domains:
canonical:
- "qr.{{ PRIMARY_DOMAIN }}"
aliases: []

View File

@@ -0,0 +1,27 @@
galaxy_info:
author: "Kevin Veen-Birkenbach"
description: >
Mini-QR is a minimalist, self-hosted web application that allows users to
instantly generate QR codes in a privacy-friendly way.
license: "Infinito.Nexus NonCommercial License"
license_url: "https://s.infinito.nexus/license"
company: |
Kevin Veen-Birkenbach
Consulting & Coaching Solutions
https://www.veen.world
galaxy_tags:
- infinito
- qr
- webapp
- privacy
- utility
- education
- lightweight
repository: "https://github.com/lyqht/mini-qr"
issue_tracker_url: "https://github.com/lyqht/mini-qr/issues"
documentation: "https://github.com/lyqht/mini-qr"
logo:
class: "fa-solid fa-qrcode"
run_after: []
dependencies: []

View File

@@ -0,0 +1,7 @@
- name: "load docker, proxy for '{{ application_id }}'"
include_role:
name: sys-stk-full-stateless
vars:
docker_compose_flush_handlers: false
- include_tasks: utils/run_once.yml

View File

@@ -0,0 +1,4 @@
---
- include_tasks: 01_core.yml
when: run_once_web_app_mini_qr is not defined

View File

@@ -0,0 +1,12 @@
---
{% include 'roles/docker-compose/templates/base.yml.j2' %}
{% set container_port = 8080 %}
{{ application_id | get_entity_name }}:
{% include 'roles/docker-container/templates/base.yml.j2' %}
image: "{{ MINI_QR_IMAGE }}:{{ MINI_QR_VERSION }}"
container_name: "{{ MINI_QR_CONTAINER }}"
ports:
- 127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ container_port }}
{% include 'roles/docker-container/templates/networks.yml.j2' %}
{% include 'roles/docker-compose/templates/networks.yml.j2' %}

View File

@@ -0,0 +1,12 @@
# General
application_id: web-app-mini-qr
entity_name: "{{ application_id | get_entity_name }}"
# Docker
docker_compose_flush_handlers: false
docker_pull_git_repository: false
# Helper variables
MINI_QR_IMAGE: "ghcr.io/lyqht/mini-qr"
MINI_QR_VERSION: "latest"
MINI_QR_CONTAINER: "{{ entity_name }}"

View File

@@ -10,7 +10,7 @@ server:
flags:
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-eval: true
domains:
canonical:

View File

@@ -12,9 +12,9 @@ server:
script-src-elem:
unsafe-inline: true
unsafe-eval: true
script-src:
script-src-attr:
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
unsafe-eval: true
whitelist:

View File

@@ -19,9 +19,9 @@ server:
# Makes sense that all of the website content is available in the navigator
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-eval: true
script-src-elem:
unsafe-inline: true

View File

@@ -2,11 +2,11 @@ version: "production" # @see https://nextcloud.com/blog/nex
server:
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-eval: true
whitelist:
font-src:
@@ -28,13 +28,15 @@ server:
docker:
volumes:
data: nextcloud_data
whiteboard_tmp: nextcloud_whiteboard_tmp
whiteboard_fontcache: nextcloud_whiteboard_fontcache
services:
redis:
enabled: true
cpus: "0.25"
mem_reservation: "64m"
mem_limit: "256m"
pids_limit: 256
cpus: "1"
mem_reservation: "1g"
mem_limit: "8g"
pids_limit: 512
database:
enabled: true
cpus: "0.75"
@@ -80,7 +82,7 @@ docker:
cpus: "1.0"
mem_reservation: "256m"
mem_limit: "1g"
pids_limit: 512
pids_limit: 1024
whiteboard:
name: "nextcloud-whiteboard"
image: "ghcr.io/nextcloud-releases/whiteboard"
@@ -90,7 +92,7 @@ docker:
cpus: "0.25"
mem_reservation: "128m"
mem_limit: "512m"
pids_limit: 256
pids_limit: 1024
enabled: "{{ applications | get_app_conf('web-app-nextcloud', 'features.oidc', False, True, True) }}" # Activate OIDC for Nextcloud
# floavor decides which OICD plugin should be used.
# Available options: oidc_login, sociallogin

View File

@@ -14,6 +14,21 @@
vars:
docker_compose_flush_handlers: false
- block:
- name: "Create '{{ NEXTCLOUD_WHITEBOARD_SERVICE_DIRECTORY }}' Directory"
file:
path: "{{ NEXTCLOUD_WHITEBOARD_SERVICE_DIRECTORY }}"
state: directory
mode: "0755"
- name: "Deploy Whiteboard Dockerfile to '{{ NEXTCLOUD_WHITEBOARD_SERVICE_DOCKERFILE }}'"
template:
src: "Dockerfiles/Whiteboard.j2"
dest: "{{ NEXTCLOUD_WHITEBOARD_SERVICE_DOCKERFILE }}"
notify: docker compose build
when: NEXTCLOUD_WHITEBOARD_ENABLED | bool
- name: "create {{ NEXTCLOUD_HOST_CONF_ADD_PATH }}"
file:
path: "{{ NEXTCLOUD_HOST_CONF_ADD_PATH }}"
@@ -24,8 +39,8 @@
template:
src: "{{ item }}"
dest: "{{ NEXTCLOUD_HOST_CONF_ADD_PATH }}/{{ item | basename | regex_replace('\\.j2$', '') }}"
owner: "{{ NEXTCLOUD_DOCKER_USER_id }}"
group: "{{ NEXTCLOUD_DOCKER_USER_id }}"
owner: "{{ NEXTCLOUD_DOCKER_USER_ID }}"
group: "{{ NEXTCLOUD_DOCKER_USER_ID }}"
loop: "{{ lookup('fileglob', role_path ~ '/templates/config/*.j2', wantlist=True) }}"
notify: docker compose up

View File

@@ -0,0 +1,27 @@
FROM {{ NEXTCLOUD_WHITEBOARD_IMAGE }}:{{ NEXTCLOUD_WHITEBOARD_VERSION }}
# Temporarily switch to root so we can install packages
USER 0
# Install Chromium, ffmpeg, fonts, and runtime libraries for headless operation on Alpine
RUN apk add --no-cache \
chromium \
ffmpeg \
nss \
freetype \
harfbuzz \
ttf-dejavu \
ttf-liberation \
udev \
ca-certificates \
&& update-ca-certificates
# Ensure a consistent Chromium binary path
RUN if [ -x /usr/bin/chromium-browser ]; then ln -sf /usr/bin/chromium-browser /usr/bin/chromium; fi
# Environment variables used by Puppeteer
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \
PUPPETEER_SKIP_DOWNLOAD=true
# Switch back to the original non-root user (nobody)
USER 65534

View File

@@ -67,16 +67,29 @@
{{ service_name }}:
{% set container_port = NEXTCLOUD_WHITEBOARD_PORT_INTERNAL %}
{% include 'roles/docker-container/templates/base.yml.j2' %}
build:
context: .
dockerfile: {{ NEXTCLOUD_WHITEBOARD_SERVICE_DOCKERFILE }}
pull_policy: never
{% include 'roles/docker-container/templates/healthcheck/nc.yml.j2' %}
image: "{{ NEXTCLOUD_WHITEBOARD_IMAGE }}:{{ NEXTCLOUD_WHITEBOARD_VERSION }}"
image: "{{ NEXTCLOUD_WHITEBOARD_CUSTOM_IMAGE }}"
container_name: {{ NEXTCLOUD_WHITEBOARD_CONTAINER }}
volumes:
- whiteboard_tmp:/tmp
- whiteboard_fontcache:/var/cache/fontconfig
expose:
- "{{ container_port }}"
shm_size: 1g
networks:
default:
ipv4_address: 192.168.102.71
depends_on:
redis:
condition: service_healthy
{% endif %}
{% set service_name = NEXTCLOUD_CRON_SERVICE %}
{{ service_name }}:
container_name: "{{ NEXTCLOUD_CRON_CONTAINER }}"
@@ -99,5 +112,11 @@
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
data:
name: {{ NEXTCLOUD_VOLUME }}
{% if NEXTCLOUD_WHITEBOARD_ENABLED %}
whiteboard_tmp:
name: {{ NEXTCLOUD_WHITEBOARD_TMP_VOLUME }}
whiteboard_fontcache:
name: {{ NEXTCLOUD_WHITEBOARD_FRONTCACHE_VOLUME }}
{% endif %}
{% include 'roles/docker-compose/templates/networks.yml.j2' %}

View File

@@ -60,4 +60,9 @@ NEXTCLOUD_URL= "{{ NEXTCLOUD_URL }}"
JWT_SECRET_KEY= "{{ NEXTCLOUD_WHITEBOARD_JWT }}"
STORAGE_STRATEGY=redis
REDIS_URL=redis://redis:6379/0
# Chromium (headless) hardening for Whiteboard
CHROMIUM_FLAGS=--headless=new --no-sandbox --disable-gpu --disable-dev-shm-usage --use-gl=swiftshader --disable-software-rasterizer
# Falls das Image Chromium mitbringt Pfad meistens /usr/bin/chromium oder /usr/bin/chromium-browser:
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
PUPPETEER_SKIP_DOWNLOAD=true
{% endif %}

View File

@@ -23,6 +23,12 @@ server
{% include 'roles/sys-svc-proxy/templates/location/ws.conf.j2' %}
{% endif %}
{% if NEXTCLOUD_WHITEBOARD_ENABLED | bool %}
{% set location_ws = '^~ ' ~ NEXTCLOUD_WHITEBOARD_LOCATION %}
{% set ws_port = NEXTCLOUD_PORT %}
{% include 'roles/sys-svc-proxy/templates/location/ws.conf.j2' %}
{% endif %}
{% include 'roles/sys-svc-proxy/templates/location/html.conf.j2' %}
location ^~ /.well-known {

View File

@@ -116,24 +116,29 @@ NEXTCLOUD_HPB_TURN_STANDALONE_CONFIG: >-
}}
### Whiteboard
NEXTCLOUD_WHITEBOARD_SERVICE: "whiteboard"
NEXTCLOUD_WHITEBOARD_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.' ~ NEXTCLOUD_WHITEBOARD_SERVICE ~'.name') }}"
NEXTCLOUD_WHITEBOARD_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.' ~ NEXTCLOUD_WHITEBOARD_SERVICE ~'.image') }}"
NEXTCLOUD_WHITEBOARD_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.' ~ NEXTCLOUD_WHITEBOARD_SERVICE ~'.version') }}"
NEXTCLOUD_WHITEBOARD_ENABLED: "{{ applications | get_app_conf(application_id, 'plugins.' ~ NEXTCLOUD_WHITEBOARD_SERVICE ~'.enabled') }}"
NEXTCLOUD_WHITEBOARD_PORT_INTERNAL: "3002"
NEXTCLOUD_WHITEBOARD_JWT: "{{ applications | get_app_conf(application_id, 'credentials.' ~ NEXTCLOUD_WHITEBOARD_SERVICE ~'_jwt_secret') }}"
NEXTCLOUD_WHITEBOARD_LOCATION: "/whiteboard/"
NEXTCLOUD_WHITEBOARD_URL: "{{ [ NEXTCLOUD_URL, NEXTCLOUD_WHITEBOARD_LOCATION ] | url_join }}"
NEXTCLOUD_WHITEBOARD_SERVICE: "whiteboard"
NEXTCLOUD_WHITEBOARD_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.' ~ NEXTCLOUD_WHITEBOARD_SERVICE ~'.name') }}"
NEXTCLOUD_WHITEBOARD_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.' ~ NEXTCLOUD_WHITEBOARD_SERVICE ~'.image') }}"
NEXTCLOUD_WHITEBOARD_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.' ~ NEXTCLOUD_WHITEBOARD_SERVICE ~'.version') }}"
NEXTCLOUD_WHITEBOARD_CUSTOM_IMAGE: "nextcloud_whiteboard_custom"
NEXTCLOUD_WHITEBOARD_ENABLED: "{{ applications | get_app_conf(application_id, 'plugins.' ~ NEXTCLOUD_WHITEBOARD_SERVICE ~'.enabled') }}"
NEXTCLOUD_WHITEBOARD_PORT_INTERNAL: "3002"
NEXTCLOUD_WHITEBOARD_JWT: "{{ applications | get_app_conf(application_id, 'credentials.' ~ NEXTCLOUD_WHITEBOARD_SERVICE ~'_jwt_secret') }}"
NEXTCLOUD_WHITEBOARD_LOCATION: "/whiteboard/"
NEXTCLOUD_WHITEBOARD_URL: "{{ [ NEXTCLOUD_URL, NEXTCLOUD_WHITEBOARD_LOCATION ] | url_join }}"
NEXTCLOUD_WHITEBOARD_TMP_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.whiteboard_tmp') }}"
NEXTCLOUD_WHITEBOARD_FRONTCACHE_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.whiteboard_fontcache') }}"
NEXTCLOUD_WHITEBOARD_SERVICE_DIRECTORY: "{{ [ docker_compose.directories.services, 'whiteboard' ] | path_join }}"
NEXTCLOUD_WHITEBOARD_SERVICE_DOCKERFILE: "{{ [ NEXTCLOUD_WHITEBOARD_SERVICE_DIRECTORY, 'Dockerfile' ] | path_join }}"
### Collabora
NEXTCLOUD_COLLABORA_URL: "{{ domains | get_url('web-svc-collabora', WEB_PROTOCOL) }}"
NEXTCLOUD_COLLABORA_URL: "{{ domains | get_url('web-svc-collabora', WEB_PROTOCOL) }}"
## User Configuration
NEXTCLOUD_DOCKER_USER_id: 82 # UID of the www-data user
NEXTCLOUD_DOCKER_USER: "www-data" # Name of the www-data user (Set here to easy change it in the future)
NEXTCLOUD_DOCKER_USER_ID: 82 # UID of the www-data user
NEXTCLOUD_DOCKER_USER: "www-data" # Name of the www-data user (Set here to easy change it in the future)
## Execution
NEXTCLOUD_INTERNAL_OCC_COMMAND: "{{ [ NEXTCLOUD_DOCKER_WORK_DIRECTORY, 'occ'] | path_join }}"
NEXTCLOUD_DOCKER_EXEC: "docker exec -u {{ NEXTCLOUD_DOCKER_USER }} {{ NEXTCLOUD_CONTAINER }}" # General execute composition
NEXTCLOUD_DOCKER_EXEC_OCC: "{{ NEXTCLOUD_DOCKER_EXEC }} {{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}" # Execute docker occ command
NEXTCLOUD_INTERNAL_OCC_COMMAND: "{{ [ NEXTCLOUD_DOCKER_WORK_DIRECTORY, 'occ'] | path_join }}"
NEXTCLOUD_DOCKER_EXEC: "docker exec -u {{ NEXTCLOUD_DOCKER_USER }} {{ NEXTCLOUD_CONTAINER }}" # General execute composition
NEXTCLOUD_DOCKER_EXEC_OCC: "{{ NEXTCLOUD_DOCKER_EXEC }} {{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}" # Execute docker occ command

View File

@@ -1,11 +1,18 @@
{% if applications | get_app_conf(application_id, 'features.oauth2', False) %}
oauth2-proxy:
image: quay.io/oauth2-proxy/oauth2-proxy:{{ applications['web-app-oauth2-proxy'].version}}
image: quay.io/oauth2-proxy/oauth2-proxy:{{ applications['web-app-oauth2-proxy'].version }}
restart: {{ DOCKER_RESTART_POLICY }}
command: --config /oauth2-proxy.cfg
container_name: {{ application_id | get_entity_name }}-oauth2-proxy
hostname: oauth2-proxy
ports:
- 127.0.0.1:{{ ports.localhost.oauth2_proxy[application_id] }}:4180/tcp
volumes:
- "{{ docker_compose.directories.volumes }}{{ applications | get_app_conf('web-app-oauth2-proxy','configuration_file')}}:/oauth2-proxy.cfg"
healthcheck:
test: ["CMD", "/bin/oauth2-proxy", "--version"]
interval: 30s
timeout: 5s
retries: 1
start_period: 5s
{% endif %}

View File

@@ -23,7 +23,7 @@ server:
flags:
script-src-elem:
unsafe-inline: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
font-src:

View File

@@ -17,11 +17,6 @@ server:
flags:
script-src-elem:
unsafe-inline: true
#script-src:
# unsafe-inline: true
# unsafe-eval: true
#style-src:
# unsafe-inline: true
whitelist:
font-src: []
connect-src: []

View File

@@ -10,9 +10,9 @@ server:
flags:
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
frame-ancestors:

View File

@@ -16,7 +16,7 @@ features:
server:
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true

View File

@@ -15,7 +15,7 @@ features:
server:
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true

View File

@@ -9,13 +9,13 @@ features:
server:
csp:
flags:
script-src:
script-src-attr:
unsafe-eval: true
unsafe-inline: true
script-src-elem:
unsafe-inline: true
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
frame-ancestors:

View File

@@ -13,12 +13,12 @@ server:
aliases: []
csp:
flags:
script-src:
script-src-attr:
unsafe-inline: true
unsafe-eval: true
script-src-elem:
unsafe-inline: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
font-src:

View File

@@ -6,12 +6,12 @@ features:
server:
csp:
flags:
script-src:
script-src-attr:
unsafe-eval: true
script-src-elem:
unsafe-inline: true
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
domains:
canonical:

View File

@@ -69,9 +69,9 @@ server:
script-src-elem:
unsafe-inline: true
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-eval: true
domains:
canonical:

View File

@@ -17,11 +17,11 @@ features:
server:
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-eval: true
whitelist:
worker-src:

View File

@@ -32,7 +32,7 @@ server:
worker-src:
- "blob:"
flags:
script-src:
script-src-attr:
unsafe-eval: true
script-src-elem:
unsafe-inline: true

View File

@@ -36,6 +36,12 @@
- name: Load setup procedures for extensions
include_tasks: 04_extensions.yml
- name: "Set authentication service according to feature toggles"
include_tasks: 05_set_authservice.yml
- name: "Run AuthDiag (temporary)"
include_tasks: _auth_diag.yml
- block:
- name: "Create Final Docker Compose File"
template:

View File

@@ -0,0 +1,73 @@
---
# Sets XWikiPreferences.authenticationService to modern component hint (standard, oidc, ldap)
- name: "XWIKI | Compute target authservice hint"
set_fact:
_target_authservice: >-
{{
'oidc' if (XWIKI_OIDC_ENABLED | bool)
else ('ldap' if (XWIKI_LDAP_ENABLED | bool)
else 'standard')
}}
- name: "XWIKI | PUT Groovy page SetAuthService"
uri:
url: "{{ [XWIKI_REST_XWIKI_PAGES, 'SetAuthService'] | url_join }}"
method: PUT
user: "{{ XWIKI_SUPERADMIN_USERNAME }}"
password: "{{ XWIKI_SUPERADMIN_PASSWORD }}"
force_basic_auth: true
status_code: [200,201,202,204]
headers:
Content-Type: "application/xml"
Accept: "application/xml"
body: |
<page xmlns="http://www.xwiki.org">
<title>SetAuthService</title>
<content><![CDATA[
{% raw %}{{groovy}}{% endraw %}
try {
def doc = xwiki.getDocument('XWiki.XWikiPreferences')
def obj = doc.getObject('XWiki.XWikiPreferences', true)
obj.set('authenticationService', '{{ _target_authservice }}')
def engine = xcontext.context.getWiki()
engine.saveDocument(doc.getDocument(), "Set authentication service to {{ _target_authservice }}", true, xcontext.context)
print "OK::{{ _target_authservice }}"
} catch (Throwable t) {
print "ERROR::" + (t?.message ?: t?.toString())
}
{% raw %}{{/groovy}}{% endraw %}
]]></content>
<syntax>xwiki/2.1</syntax>
</page>
register: _put_auth_page
- name: "XWIKI | Execute SetAuthService"
uri:
url: "http://127.0.0.1:{{ XWIKI_HOST_PORT }}/bin/view/XWiki/SetAuthService?xpage=plain"
method: GET
user: "{{ XWIKI_SUPERADMIN_USERNAME }}"
password: "{{ XWIKI_SUPERADMIN_PASSWORD }}"
force_basic_auth: true
status_code: [200]
return_content: yes
register: _exec_auth_page
retries: 10
delay: 3
until: _exec_auth_page is succeeded
- name: "ASSERT | Auth service set"
assert:
that:
- _exec_auth_page.content is search("OK::")
fail_msg: "Failed to set XWikiPreferences.authenticationService: {{ _exec_auth_page.content | default('no content') }}"
- name: "XWIKI | Delete SetAuthService page"
uri:
url: "{{ [XWIKI_REST_XWIKI_PAGES, 'SetAuthService'] | url_join }}"
method: DELETE
user: "{{ XWIKI_SUPERADMIN_USERNAME }}"
password: "{{ XWIKI_SUPERADMIN_PASSWORD }}"
force_basic_auth: true
status_code: [204,200,202,404]
changed_when: false

View File

@@ -0,0 +1,68 @@
# roles/web-app-xwiki/tasks/_auth_diag.yml
- name: "XWIKI | PUT page XWiki.AuthDiag (Groovy)"
uri:
url: "{{ [XWIKI_REST_XWIKI_PAGES, 'AuthDiag'] | url_join }}"
method: PUT
user: "{{ XWIKI_SUPERADMIN_USERNAME }}"
password: "{{ XWIKI_SUPERADMIN_PASSWORD }}"
force_basic_auth: true
status_code: [200,201,202,204]
headers:
Content-Type: "application/xml"
Accept: "application/xml"
body: |
<page xmlns="http://www.xwiki.org">
<title>AuthDiag</title>
<content><![CDATA[
{% raw %}{{groovy}}{% endraw %}
import org.xwiki.security.authservice.AuthService
try {
def cm = services.component.componentManager
def hints = cm.getComponentDescriptorList(AuthService).collect{ it.roleHint }.sort()
def doc = xwiki.getDocument('XWiki.XWikiPreferences')
def obj = doc.getObject('XWiki.XWikiPreferences', true)
def pref = (obj.get('authenticationService') ?: 'unset')
println "HINTS::" + hints
println "PREF::" + pref
def chosenHint = (pref ?: 'standard')
def hasChosen = hints.contains(chosenHint)
println "HAS_CHOSEN::" + hasChosen + "::" + chosenHint
} catch (Throwable t) {
println "ERROR::" + (t?.message ?: t?.toString())
}
{% raw %}{{/groovy}}{% endraw %}
]]></content>
<syntax>xwiki/2.1</syntax>
</page>
register: _put_authdiag
changed_when: false
- name: "XWIKI | Run AuthDiag"
uri:
url: "http://127.0.0.1:{{ XWIKI_HOST_PORT }}/bin/view/XWiki/AuthDiag?xpage=plain"
method: GET
user: "{{ XWIKI_SUPERADMIN_USERNAME }}"
password: "{{ XWIKI_SUPERADMIN_PASSWORD }}"
force_basic_auth: true
status_code: [200]
return_content: yes
register: _authdiag_run
changed_when: false
- name: "DEBUG | AuthDiag output"
debug:
msg: "{{ _authdiag_run.content | regex_replace('<[^>]+>', '') | trim }}"
# Optional sauber machen:
- name: "XWIKI | DELETE AuthDiag page"
uri:
url: "{{ [XWIKI_REST_XWIKI_PAGES, 'AuthDiag'] | url_join }}"
method: DELETE
user: "{{ XWIKI_SUPERADMIN_USERNAME }}"
password: "{{ XWIKI_SUPERADMIN_PASSWORD }}"
force_basic_auth: true
status_code: [204,200,202,404]
changed_when: false

View File

@@ -8,10 +8,10 @@
- "127.0.0.1:{{ XWIKI_HOST_PORT }}:{{ container_port }}"
environment:
JAVA_OPTS: >-
{% if xwiki_oidc_enabled_switch| bool %}
-Dxwiki.authentication.authclass=org.xwiki.contrib.oidc.auth.OIDCAuthServiceImpl
{% if xwiki_oidc_enabled_switch | bool %}
-Dxwiki.authentication.authservice=oidc
{% elif xwiki_ldap_enabled_switch | bool %}
-Dxwiki.authentication.authclass=org.xwiki.contrib.ldap.XWikiLDAPAuthServiceImpl
-Dxwiki.authentication.authservice=ldap
-Dxwiki.authentication.ldap=1
-Dxwiki.authentication.ldap.trylocal={{ (XWIKI_LDAP_TRYLOCAL | bool) | ternary(1, 0) }}
-Dxwiki.authentication.ldap.group_mapping=XWiki.XWikiAdminGroup={{ XWIKI_LDAP_ADMIN_GROUP_DN }}
@@ -24,7 +24,7 @@
-Dxwiki.authentication.ldap.fields_mapping={{ XWIKI_LDAP_FIELDS_MAPPING }}
-Dxwiki.authentication.ldap.update_user=1
{% else %}
-Dxwiki.authentication.authclass=com.xpn.xwiki.user.impl.xwiki.XWikiAuthServiceImpl
-Dxwiki.authentication.authservice=standard
{% endif %}
volumes:
- "{{ XWIKI_HOST_PROPERTIES_PATH }}:/usr/local/tomcat/webapps/ROOT/WEB-INF/xwiki.properties"

View File

@@ -20,11 +20,11 @@ server:
aliases: []
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
locations:
admin: "/admin/"

View File

@@ -8,7 +8,7 @@ server:
frame-ancestors:
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
flags:
style-src:
style-src-attr:
unsafe-inline: true
docker:
services:

View File

@@ -32,4 +32,6 @@
and
('already present' not in (collabora_preview.stdout | default('')))
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
- include_tasks: utils/run_once.yml

View File

@@ -1,5 +1,3 @@
- block:
- name: "Load core functions for '{{ application_id }}'"
include_tasks: 01_core.yml
- include_tasks: utils/run_once.yml
- name: "Load core functions for '{{ application_id }}'"
include_tasks: 01_core.yml
when: run_once_web_svc_collabora is not defined

View File

@@ -11,7 +11,7 @@ server:
aliases: []
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true

View File

@@ -29,14 +29,14 @@ server:
csp:
whitelist: # URL's which should be whitelisted
script-src-elem: []
style-src: []
style-src-attr: []
font-src: []
connect-src: []
frame-src: []
flags: # Flags which should be set
style-src:
style-src-attr:
unsafe-inline: false
script-src:
script-src-attr:
unsafe-inline: false
script-src-elem:
unsafe-inline: false

View File

@@ -31,6 +31,8 @@ class TestCspConfigurationConsistency(unittest.TestCase):
"worker-src",
"manifest-src",
"media-src",
"style-src-attr",
"script-src-attr",
}
SUPPORTED_FLAGS = {"unsafe-eval", "unsafe-inline"}

View File

@@ -51,6 +51,9 @@ class TestVariableDefinitions(unittest.TestCase):
# {% set var = ... %} (allow trimmed variants)
self.jinja_set_def = re.compile(r'{%\s*-?\s*set\s+([a-zA-Z_]\w*)\s*=')
# {% set var %} ... {% endset %} (block-style set)
self.jinja_set_block_def = re.compile(r'{%\s*-?\s*set\s+([a-zA-Z_]\w*)\s*-?%}')
# {% for x in ... %} or {% for k, v in ... %} (allow trimmed variants)
self.jinja_for_def = re.compile(
@@ -159,6 +162,10 @@ class TestVariableDefinitions(unittest.TestCase):
for m in self.jinja_set_def.finditer(line):
self.defined.add(m.group(1))
# Count block-style set as a definition, too
for m in self.jinja_set_block_def.finditer(line):
self.defined.add(m.group(1))
for m in self.jinja_for_def.finditer(line):
self.defined.add(m.group(1))
if m.group(2):

View File

@@ -3,6 +3,7 @@ import hashlib
import base64
import sys
import os
import copy
sys.path.insert(
0,
@@ -322,6 +323,155 @@ class TestCspFilters(unittest.TestCase):
tokens = self._get_directive_tokens(header, 'style-src')
self.assertIn("'unsafe-inline'", tokens)
def test_style_family_union_flows_into_base_only_no_mirror_back(self):
"""
Sources allowed only in style-src-elem/attr must appear in style-src (CSP2/Safari fallback),
but we do NOT mirror back base→elem/attr.
"""
apps = copy.deepcopy(self.apps)
# Add distinct sources to elem and attr only
apps['app1']['server']['csp'].setdefault('whitelist', {})
apps['app1']['server']['csp']['whitelist']['style-src-elem'] = [
'https://elem-only.example.com'
]
apps['app1']['server']['csp']['whitelist']['style-src-attr'] = [
'https://attr-only.example.com'
]
header = self.filter.build_csp_header(apps, 'app1', self.domains, web_protocol='https')
base_tokens = self._get_directive_tokens(header, 'style-src')
elem_tokens = self._get_directive_tokens(header, 'style-src-elem')
attr_tokens = self._get_directive_tokens(header, 'style-src-attr')
# Base must include both elem/attr sources
self.assertIn('https://elem-only.example.com', base_tokens)
self.assertIn('https://attr-only.example.com', base_tokens)
# elem keeps its own sources; we did not force-copy base back into elem/attr
# (No strict negative assertion here; just verify elem retains its own source)
self.assertIn('https://elem-only.example.com', elem_tokens)
self.assertIn('https://attr-only.example.com', attr_tokens)
def test_style_explicit_disable_inline_on_base_survives_union(self):
"""
If style-src.unsafe-inline is explicitly set to False on the base,
it must be removed from the merged base even if elem/attr include it by default.
"""
apps = copy.deepcopy(self.apps)
# Explicitly disable unsafe-inline for the base
apps['app1'].setdefault('server', {}).setdefault('csp', {}).setdefault('flags', {}).setdefault('style-src', {})
apps['app1']['server']['csp']['flags']['style-src']['unsafe-inline'] = False
header = self.filter.build_csp_header(apps, 'app1', self.domains, web_protocol='https')
base_tokens = self._get_directive_tokens(header, 'style-src')
elem_tokens = self._get_directive_tokens(header, 'style-src-elem')
attr_tokens = self._get_directive_tokens(header, 'style-src-attr')
# Base must NOT have 'unsafe-inline'
self.assertNotIn("'unsafe-inline'", base_tokens)
# elem/attr may still have 'unsafe-inline' by default (granularity preserved)
self.assertIn("'unsafe-inline'", elem_tokens)
self.assertIn("'unsafe-inline'", attr_tokens)
def test_script_explicit_disable_inline_on_base_survives_union(self):
"""
If script-src.unsafe-inline is explicitly set to False (default anyway),
ensure the base remains without 'unsafe-inline' even if elem/attr enable it.
"""
apps = copy.deepcopy(self.apps)
# Force elem/attr to allow unsafe-inline explicitly
apps['app1'].setdefault('server', {}).setdefault('csp', {}).setdefault('flags', {})
apps['app1']['server']['csp']['flags']['script-src-elem'] = {'unsafe-inline': True}
apps['app1']['server']['csp']['flags']['script-src-attr'] = {'unsafe-inline': True}
# Explicitly disable on base (redundant but makes intent clear)
apps['app1']['server']['csp']['flags']['script-src'] = {
'unsafe-inline': False,
'unsafe-eval': True
}
header = self.filter.build_csp_header(apps, 'app1', self.domains, web_protocol='https')
base_tokens = self._get_directive_tokens(header, 'script-src')
elem_tokens = self._get_directive_tokens(header, 'script-src-elem')
attr_tokens = self._get_directive_tokens(header, 'script-src-attr')
# Base: no 'unsafe-inline'
self.assertNotIn("'unsafe-inline'", base_tokens)
# But elem/attr: yes
self.assertIn("'unsafe-inline'", elem_tokens)
self.assertIn("'unsafe-inline'", attr_tokens)
# Also ensure 'unsafe-eval' remains present on the base
self.assertIn("'unsafe-eval'", base_tokens)
def test_script_family_union_includes_elem_attr_hosts_in_base(self):
"""
Hosts present only under script-src-elem/attr must appear in script-src (base).
"""
apps = copy.deepcopy(self.apps)
apps['app1']['server']['csp'].setdefault('whitelist', {})
apps['app1']['server']['csp']['whitelist']['script-src-elem'] = [
'https://elem-scripts.example.com'
]
apps['app1']['server']['csp']['whitelist']['script-src-attr'] = [
'https://attr-scripts.example.com'
]
header = self.filter.build_csp_header(apps, 'app1', self.domains, web_protocol='https')
base_tokens = self._get_directive_tokens(header, 'script-src')
self.assertIn('https://elem-scripts.example.com', base_tokens)
self.assertIn('https://attr-scripts.example.com', base_tokens)
def test_hash_inclusion_uses_final_base_tokens_after_union(self):
"""
Ensure hash inclusion for style-src is evaluated after family union & explicit-disable logic.
If base ends up WITHOUT 'unsafe-inline' after union, hashes must be present.
"""
apps = copy.deepcopy(self.apps)
# Explicitly disable 'unsafe-inline' on base 'style-src' so hashes can be included
apps['app1'].setdefault('server', {}).setdefault('csp', {}).setdefault('flags', {}).setdefault('style-src', {})
apps['app1']['server']['csp']['flags']['style-src']['unsafe-inline'] = False
# Provide a style-src hash
content = "body { background: #abc; }"
apps['app1']['server']['csp'].setdefault('hashes', {})['style-src'] = content
expected_hash = self.filter.get_csp_hash(content)
header = self.filter.build_csp_header(apps, 'app1', self.domains, web_protocol='https')
base_tokens = self._get_directive_tokens(header, 'style-src')
self.assertNotIn("'unsafe-inline'", base_tokens) # confirm no unsafe-inline
self.assertIn(expected_hash, header) # hash must be present
def test_no_unintended_mirroring_back_to_elem_attr(self):
"""
Verify that we do not mirror base tokens back into elem/attr:
add a base-only host and ensure elem/attr don't automatically get it.
"""
apps = copy.deepcopy(self.apps)
apps['app1']['server']['csp'].setdefault('whitelist', {})
# Add a base-only host
apps['app1']['server']['csp']['whitelist']['style-src'] = ['https://base-only.example.com']
header = self.filter.build_csp_header(apps, 'app1', self.domains, web_protocol='https')
base_tokens = self._get_directive_tokens(header, 'style-src')
elem_tokens = self._get_directive_tokens(header, 'style-src-elem')
attr_tokens = self._get_directive_tokens(header, 'style-src-attr')
self.assertIn('https://base-only.example.com', base_tokens)
# Not strictly required to assert negatives, but this ensures "no mirror back":
self.assertNotIn('https://base-only.example.com', elem_tokens)
self.assertNotIn('https://base-only.example.com', attr_tokens)
if __name__ == '__main__':
unittest.main()