mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-11-07 21:58:02 +00:00
Compare commits
16 Commits
468b6e734c
...
feature/dr
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e874408a7 | |||
| bebf76951c | |||
| aa1a901309 | |||
| d61c81634c | |||
| 265f815b48 | |||
| f8e5110730 | |||
| 37b213f96a | |||
| 5ef525eac9 | |||
| 295ae7e477 | |||
| c67ccc1df6 | |||
| cb483f60d1 | |||
| 2be73502ca | |||
| 57d5269b07 | |||
| 1eefdea050 | |||
| 561160504e | |||
| 9a4bf91276 |
@@ -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)
|
||||
|
||||
@@ -112,6 +112,10 @@ 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
|
||||
web-app-drupal:
|
||||
subnet: 192.168.104.80/28
|
||||
|
||||
# /24 Networks / 254 Usable Clients
|
||||
web-app-bigbluebutton:
|
||||
|
||||
@@ -80,6 +80,8 @@ ports:
|
||||
web-app-flowise: 8056
|
||||
web-app-minio_api: 8057
|
||||
web-app-minio_console: 8058
|
||||
web-app-mini-qr: 8059
|
||||
web-app-drupal: 8060
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }};
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -37,5 +37,5 @@ server:
|
||||
flags:
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
style-src:
|
||||
style-src-attr:
|
||||
unsafe-inline: true
|
||||
@@ -13,7 +13,7 @@ server:
|
||||
flags:
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
style-src:
|
||||
style-src-attr:
|
||||
unsafe-inline: true
|
||||
domains:
|
||||
canonical:
|
||||
|
||||
@@ -27,7 +27,7 @@ server:
|
||||
flags:
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
script-src:
|
||||
script-src-attr:
|
||||
unsafe-inline: true
|
||||
domains:
|
||||
canonical:
|
||||
|
||||
@@ -29,7 +29,7 @@ server:
|
||||
flags:
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
script-src:
|
||||
script-src-attr:
|
||||
unsafe-inline: true
|
||||
domains:
|
||||
canonical:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -10,7 +10,7 @@ features:
|
||||
server:
|
||||
csp:
|
||||
flags:
|
||||
style-src:
|
||||
style-src-attr:
|
||||
unsafe-inline: true
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
|
||||
29
roles/web-app-drupal/Administration.md
Normal file
29
roles/web-app-drupal/Administration.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Administration
|
||||
|
||||
## Shell access
|
||||
|
||||
```bash
|
||||
docker-compose exec -it application /bin/bash
|
||||
```
|
||||
|
||||
## Drush (inside the container)
|
||||
|
||||
```bash
|
||||
drush --version
|
||||
drush cr # Cache rebuild
|
||||
drush status # Site status
|
||||
drush cim -y # Config import (if using config sync)
|
||||
drush updb -y # Run DB updates
|
||||
```
|
||||
|
||||
## Database access (local DB service)
|
||||
|
||||
```bash
|
||||
docker-compose exec -it database /bin/mysql -u drupal -p
|
||||
```
|
||||
|
||||
## Test Email
|
||||
|
||||
```bash
|
||||
docker-compose exec -it application /bin/bash -lc 'echo "Test Email" | sendmail -v your-email@example.com'
|
||||
```
|
||||
32
roles/web-app-drupal/README.md
Normal file
32
roles/web-app-drupal/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Drupal
|
||||
|
||||
## Description
|
||||
|
||||
[Drupal](https://www.drupal.org/) is a powerful open-source CMS for building secure, extensible, and content-rich digital experiences.
|
||||
This role deploys a containerized **Drupal 10/11** instance optimized for production, including **msmtp** for outbound email, **Drush** for CLI administration, and **OpenID Connect (OIDC)** for SSO (e.g., Keycloak, Auth0, Azure AD).
|
||||
|
||||
## Overview
|
||||
|
||||
* **Flexible Content Model:** Entities, fields, and views for complex data needs.
|
||||
* **Security & Roles:** Fine-grained access control and active security team.
|
||||
* **Robust Ecosystem:** Thousands of modules and themes.
|
||||
* **CLI Automation:** Drush for installs, updates, and configuration import.
|
||||
* **OIDC SSO:** First-class login via external Identity Providers.
|
||||
|
||||
This automated Docker Compose deployment builds a custom Drupal image with Drush and msmtp, wires database credentials and config overrides via environment, and applies OIDC configuration via Ansible/Drush.
|
||||
|
||||
## OIDC
|
||||
|
||||
This role enables **OpenID Connect** via the `openid_connect` module and configures a **client entity** (e.g., `keycloak`) including endpoints and scopes. Global OIDC behavior (auto-create, link existing users, privacy) is set via `openid_connect.settings`.
|
||||
|
||||
## Further Resources
|
||||
|
||||
* [Drupal.org](https://www.drupal.org/)
|
||||
* [OpenID Connect module](https://www.drupal.org/project/openid_connect)
|
||||
|
||||
## Credits
|
||||
|
||||
Developed and maintained by **Kevin Veen-Birkenbach**
|
||||
Learn more at [veen.world](https://veen.world)
|
||||
Part of the [Infinito.Nexus Project](https://s.infinito.nexus/code)
|
||||
License: [Infinito.Nexus NonCommercial License](https://s.infinito.nexus/license)
|
||||
41
roles/web-app-drupal/config/main.yml
Normal file
41
roles/web-app-drupal/config/main.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
title: "Site"
|
||||
max_upload_size: "256M"
|
||||
features:
|
||||
matomo: true
|
||||
css: false
|
||||
desktop: true
|
||||
oidc: true
|
||||
central_database: true
|
||||
logout: true
|
||||
|
||||
server:
|
||||
csp:
|
||||
flags: {}
|
||||
whitelist: {}
|
||||
domains:
|
||||
canonical:
|
||||
- "drupal.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
enabled: true
|
||||
drupal:
|
||||
# Use a PHP 8.2+ base image to ensure compatibility with OIDC 2.x syntax
|
||||
version: "10-php8.2-apache"
|
||||
image: drupal
|
||||
name: drupal
|
||||
backup:
|
||||
no_stop_required: true
|
||||
volumes:
|
||||
data: drupal_data
|
||||
|
||||
rbac:
|
||||
roles:
|
||||
authenticated:
|
||||
description: "Logged-in user"
|
||||
content_editor:
|
||||
description: "Can create and edit content"
|
||||
site_admin:
|
||||
description: "Full site administration"
|
||||
23
roles/web-app-drupal/meta/main.yml
Normal file
23
roles/web-app-drupal/meta/main.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: >
|
||||
Drupal CMS in Docker with Drush, msmtp, and OpenID Connect (OIDC) SSO.
|
||||
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:
|
||||
- drupal
|
||||
- docker
|
||||
- cms
|
||||
- oidc
|
||||
- sso
|
||||
repository: "https://s.infinito.nexus/code"
|
||||
issue_tracker_url: "https://s.infinito.nexus/issues"
|
||||
documentation: "https://docs.infinito.nexus"
|
||||
logo:
|
||||
class: "fa-solid fa-droplet"
|
||||
run_after:
|
||||
- web-app-keycloak
|
||||
9
roles/web-app-drupal/schema/main.yml
Normal file
9
roles/web-app-drupal/schema/main.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
credentials:
|
||||
administrator_password:
|
||||
description: "Initial password for the Drupal admin account"
|
||||
algorithm: "sha256"
|
||||
validation: "^[a-f0-9]{64}$"
|
||||
hash_salt:
|
||||
description: "Drupal hash_salt value used for one-time logins, CSRF tokens, etc."
|
||||
algorithm: "sha256"
|
||||
validation: "^[a-f0-9]{64}$"
|
||||
11
roles/web-app-drupal/tasks/00_permissions.yml
Normal file
11
roles/web-app-drupal/tasks/00_permissions.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
- name: "Ensure sites/default/files exists and is writable"
|
||||
command: >
|
||||
docker exec -u root {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"set -e;
|
||||
d='{{ DRUPAL_DOCKER_HTML_PATH }}/sites/default';
|
||||
f=\"$d/files\";
|
||||
mkdir -p \"$f\";
|
||||
chown -R {{ DRUPAL_USER }}:{{ DRUPAL_USER }} \"$d\";
|
||||
find \"$d\" -type d -exec chmod 775 {} +;
|
||||
find \"$d\" -type f -exec chmod 664 {} +;"
|
||||
changed_when: true
|
||||
25
roles/web-app-drupal/tasks/01_settings_local_include.yml
Normal file
25
roles/web-app-drupal/tasks/01_settings_local_include.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
- name: "Ensure settings.php exists and includes settings.local.php"
|
||||
command: >
|
||||
docker exec -u root {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"set -e;
|
||||
f='{{ DRUPAL_DOCKER_CONF_PATH }}/settings.php';
|
||||
df='{{ DRUPAL_DOCKER_CONF_PATH }}/default.settings.php';
|
||||
if [ ! -f \"$f\" ] && [ -f \"$df\" ]; then
|
||||
cp \"$df\" \"$f\";
|
||||
chown www-data:www-data \"$f\";
|
||||
chmod 644 \"$f\";
|
||||
fi;
|
||||
php -r '
|
||||
$f=\"{{ DRUPAL_DOCKER_CONF_PATH }}/settings.php\";
|
||||
if (!file_exists($f)) { exit(0); }
|
||||
$c=file_get_contents($f);
|
||||
$inc=\"\\nif (file_exists(\\\"\$app_root/\$site_path/settings.local.php\\\")) { include \$app_root/\$site_path/settings.local.php; }\\n\";
|
||||
if (strpos($c, \"settings.local.php\") === false) {
|
||||
file_put_contents($f, $c.$inc);
|
||||
echo \"patched\";
|
||||
} else {
|
||||
echo \"exists\";
|
||||
}
|
||||
'"
|
||||
register: settings_local_include
|
||||
changed_when: "'patched' in settings_local_include.stdout"
|
||||
32
roles/web-app-drupal/tasks/02_install.yml
Normal file
32
roles/web-app-drupal/tasks/02_install.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
- name: "Wait for database readiness"
|
||||
command: >
|
||||
docker exec {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"php -r '
|
||||
$h=\"{{ database_host }}\"; $p={{ database_port }};
|
||||
$d=\"{{ database_name }}\"; $u=\"{{ database_username }}\"; $pw=\"{{ database_password }}\";
|
||||
$t=microtime(true)+60; $ok=false;
|
||||
while (microtime(true)<$t) {
|
||||
try { new PDO(\"mysql:host=$h;port=$p;dbname=$d;charset=utf8mb4\", $u, $pw,[PDO::ATTR_TIMEOUT=>2]); $ok=true; break; }
|
||||
catch (Exception $e) { usleep(300000); }
|
||||
}
|
||||
if (!$ok) { fwrite(STDERR, \"DB not ready\\n\"); exit(1); }'"
|
||||
register: db_wait
|
||||
retries: 1
|
||||
failed_when: db_wait.rc != 0
|
||||
|
||||
- name: "Run Drupal site:install via Drush"
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
command: >
|
||||
docker exec {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"/opt/drupal/vendor/bin/drush -r {{ DRUPAL_DOCKER_HTML_PATH }} site:install standard -y
|
||||
--db-url='mysql://{{ database_username }}:{{ database_password }}@{{ database_host }}:{{ database_port }}/{{ database_name }}'
|
||||
--site-name='{{ applications | get_app_conf(application_id, 'title', True) }}'
|
||||
--account-name='{{ applications | get_app_conf(application_id, 'users.administrator.username') }}'
|
||||
--account-mail='{{ applications | get_app_conf(application_id, 'users.administrator.email', True) }}'
|
||||
--account-pass='{{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }}'
|
||||
--uri='{{ DRUPAL_URL }}'"
|
||||
args:
|
||||
chdir: "{{ docker_compose.directories.instance }}"
|
||||
register: drupal_install
|
||||
changed_when: "'Installation complete' in drupal_install.stdout"
|
||||
failed_when: false
|
||||
12
roles/web-app-drupal/tasks/03_enable_modules.yml
Normal file
12
roles/web-app-drupal/tasks/03_enable_modules.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
- name: "Enable OpenID Connect core module"
|
||||
command: >
|
||||
docker exec {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"drush -r {{ DRUPAL_DOCKER_HTML_PATH }} en openid_connect -y"
|
||||
changed_when: true
|
||||
|
||||
- name: "Enable OpenID Connect Keycloak preset (submodule of openid_connect)"
|
||||
command: >
|
||||
docker exec {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"drush -r {{ DRUPAL_DOCKER_HTML_PATH }} en openid_connect_client_keycloak -y"
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
79
roles/web-app-drupal/tasks/04_configure_oidc.yml
Normal file
79
roles/web-app-drupal/tasks/04_configure_oidc.yml
Normal file
@@ -0,0 +1,79 @@
|
||||
- name: "Load OIDC vars"
|
||||
include_vars:
|
||||
file: "{{ role_path }}/vars/oidc.yml"
|
||||
name: oidc_vars
|
||||
|
||||
- name: "Apply openid_connect.settings (global)"
|
||||
loop: "{{ oidc_vars.oidc_settings | dict2items }}"
|
||||
loop_control:
|
||||
label: "{{ item.key }}"
|
||||
command: >
|
||||
docker exec {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"drush -r {{ DRUPAL_DOCKER_HTML_PATH }} cset -y
|
||||
openid_connect.settings {{ item.key }}
|
||||
{{ (item.value | to_json) if item.value is mapping or item.value is sequence else item.value }}"
|
||||
|
||||
- name: "Ensure/Update OIDC client entity (generic)"
|
||||
vars:
|
||||
client_id: "{{ oidc_vars.oidc_client.id }}"
|
||||
client_label: "{{ oidc_vars.oidc_client.label }}"
|
||||
plugin_id: "{{ oidc_vars.oidc_client.plugin }}"
|
||||
settings_b64: "{{ oidc_vars.oidc_client.settings | to_json | b64encode }}"
|
||||
command: >
|
||||
docker exec {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"drush -r {{ DRUPAL_DOCKER_HTML_PATH }} eval '
|
||||
$id=\"{{ client_id }}\";
|
||||
$label=\"{{ client_label }}\";
|
||||
$plugin=\"{{ plugin_id }}\";
|
||||
$settings=json_decode(base64_decode(\"{{ settings_b64 }}\"), TRUE);
|
||||
$storage=\\Drupal::entityTypeManager()->getStorage(\"openid_connect_client\");
|
||||
$e=$storage->load($id);
|
||||
if (!$e) {
|
||||
$e=$storage->create([
|
||||
\"id\"=> $id,
|
||||
\"label\"=> $label,
|
||||
\"status\"=> TRUE,
|
||||
\"plugin\"=> $plugin,
|
||||
\"settings\"=> $settings,
|
||||
]);
|
||||
$e->save();
|
||||
print \"created\";
|
||||
} else {
|
||||
$e->set(\"label\", $label);
|
||||
$e->set(\"plugin\", $plugin);
|
||||
$e->set(\"settings\", $settings);
|
||||
$e->set(\"status\", TRUE);
|
||||
$e->save();
|
||||
print \"updated\";
|
||||
}
|
||||
'"
|
||||
register: client_apply
|
||||
changed_when: "'created' in client_apply.stdout or 'updated' in client_apply.stdout"
|
||||
|
||||
- name: "Apply OIDC client settings"
|
||||
vars:
|
||||
client_id: "{{ oidc_vars.oidc_client.id }}"
|
||||
settings_map: "{{ oidc_vars.oidc_client.settings }}"
|
||||
kv: "{{ settings_map | dict2items }}"
|
||||
loop: "{{ kv }}"
|
||||
loop_control:
|
||||
label: "{{ item.key }}"
|
||||
command: >
|
||||
docker exec {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"drush -r {{ DRUPAL_DOCKER_HTML_PATH }} eval '
|
||||
$id=\"{{ client_id }}\";
|
||||
$key=\"{{ item.key }}\";
|
||||
$val=json_decode(base64_decode(\"{{ (item.value | to_json | b64encode) }}\"), true);
|
||||
$storage=\Drupal::entityTypeManager()->getStorage(\"openid_connect_client\");
|
||||
$c=$storage->load($id);
|
||||
$s=$c->get(\"settings\");
|
||||
$s[$key]=$val;
|
||||
$c->set(\"settings\", $s);
|
||||
$c->save();'"
|
||||
changed_when: true
|
||||
|
||||
- name: "Clear caches after OIDC config"
|
||||
command: >
|
||||
docker exec {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"drush -r {{ DRUPAL_DOCKER_HTML_PATH }} cr"
|
||||
changed_when: false
|
||||
19
roles/web-app-drupal/tasks/05_trusted_hosts.yml
Normal file
19
roles/web-app-drupal/tasks/05_trusted_hosts.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
- name: "Set trusted_host_patterns for canonical domains"
|
||||
vars:
|
||||
patterns: "{{ DRUPAL_DOMAINS
|
||||
| map('regex_replace','\\\\.','\\\\\\\\.')
|
||||
| map('regex_replace','^','^')
|
||||
| map('regex_replace','$','$')
|
||||
| list }}"
|
||||
php_array: "{{ patterns | to_json }}"
|
||||
command: >
|
||||
docker exec -u root {{ DRUPAL_CONTAINER }} bash -lc
|
||||
"php -r '
|
||||
$f="{{ DRUPAL_DOCKER_CONF_PATH }}/settings.local.php";
|
||||
$c=file_exists($f)?file_get_contents($f):"<?php\n";
|
||||
// Remove existing assignment of $settings[\"trusted_host_patterns\"] (if any)
|
||||
$c=preg_replace(\"/(\\\\$settings\\['trusted_host_patterns'\\]\\s*=).*?;/s\", \"\", $c);
|
||||
$c.="\n\$settings[\'trusted_host_patterns\'] = ".var_export(json_decode("{{ php_array|e }}", true), true).";\n";
|
||||
file_put_contents($f,$c);
|
||||
'"
|
||||
changed_when: true
|
||||
58
roles/web-app-drupal/tasks/main.yml
Normal file
58
roles/web-app-drupal/tasks/main.yml
Normal file
@@ -0,0 +1,58 @@
|
||||
- name: "Include role sys-stk-front-proxy for {{ application_id }}"
|
||||
include_role:
|
||||
name: sys-stk-front-proxy
|
||||
loop: "{{ DRUPAL_DOMAINS }}"
|
||||
loop_control:
|
||||
loop_var: domain
|
||||
vars:
|
||||
proxy_extra_configuration: "client_max_body_size {{ DRUPAL_MAX_UPLOAD_SIZE }};"
|
||||
http_port: "{{ ports.localhost.http[application_id] }}"
|
||||
|
||||
- name: "Load docker and DB for {{ application_id }}"
|
||||
include_role:
|
||||
name: sys-stk-back-stateful
|
||||
vars:
|
||||
docker_compose_flush_handlers: false
|
||||
|
||||
- name: "Transfer upload.ini to {{ DRUPAL_CONFIG_UPLOAD_ABS }}"
|
||||
template:
|
||||
src: upload.ini.j2
|
||||
dest: "{{ DRUPAL_CONFIG_UPLOAD_ABS }}"
|
||||
notify:
|
||||
- docker compose up
|
||||
- docker compose build
|
||||
|
||||
- name: "Transfer msmtprc to {{ DRUPAL_MSMTP_ABS }}"
|
||||
template:
|
||||
src: "{{ DRUPAL_MSMTP_SRC }}"
|
||||
dest: "{{ DRUPAL_MSMTP_ABS }}"
|
||||
notify: docker compose up
|
||||
|
||||
- name: "Transfer settings.local.php overrides"
|
||||
template:
|
||||
src: settings.local.php.j2
|
||||
dest: "{{ DRUPAL_SETTINGS_LOCAL_ABS }}"
|
||||
notify: docker compose up
|
||||
|
||||
- name: Flush handlers to make container ready
|
||||
meta: flush_handlers
|
||||
|
||||
- name: "Fix permissions for sites/default/files"
|
||||
include_tasks: 00_permissions.yml
|
||||
|
||||
- name: "Ensure settings.php includes settings.local.php"
|
||||
include_tasks: 01_settings_local_include.yml
|
||||
|
||||
- name: "Install Drupal (site:install)"
|
||||
include_tasks: 02_install.yml
|
||||
|
||||
- name: "Enable OIDC modules"
|
||||
include_tasks: 03_enable_modules.yml
|
||||
when: applications | get_app_conf(application_id, 'features.oidc')
|
||||
|
||||
- name: "Configure OIDC (global + client)"
|
||||
include_tasks: 04_configure_oidc.yml
|
||||
when: applications | get_app_conf(application_id, 'features.oidc')
|
||||
|
||||
- name: "Harden trusted host patterns"
|
||||
include_tasks: 05_trusted_hosts.yml
|
||||
83
roles/web-app-drupal/templates/Dockerfile.j2
Normal file
83
roles/web-app-drupal/templates/Dockerfile.j2
Normal file
@@ -0,0 +1,83 @@
|
||||
FROM {{ DRUPAL_IMAGE }}:{{ DRUPAL_VERSION }}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# System dependencies (mail support + MySQL client + basic tools)
|
||||
# -------------------------------------------------------------------
|
||||
RUN apt-get update && \
|
||||
apt-get install -y msmtp msmtp-mta git unzip zip less nano curl vim mariadb-client && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# PHP extensions required by Drupal/Drush bootstrap
|
||||
# -------------------------------------------------------------------
|
||||
RUN docker-php-ext-install -j"$(nproc)" pdo_mysql
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Install Composer
|
||||
# -------------------------------------------------------------------
|
||||
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
|
||||
&& php composer-setup.php --install-dir=/usr/local/bin --filename=composer \
|
||||
&& rm composer-setup.php
|
||||
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Build Drupal project with Drush + OpenID Connect
|
||||
# IMPORTANT:
|
||||
# - The Drupal base image uses /var/www/html as a symlink to {{ DRUPAL_DOCKER_HTML_PATH }}
|
||||
# - Therefore, the actual project root must be placed in /opt/drupal
|
||||
# -------------------------------------------------------------------
|
||||
RUN set -eux; \
|
||||
builddir="$(mktemp -d)"; \
|
||||
composer create-project --no-interaction --no-ansi --no-progress drupal/recommended-project:^10 "$builddir"; \
|
||||
composer --working-dir="$builddir" require -n drush/drush:^13 drupal/openid_connect:^2@beta; \
|
||||
rm -rf /opt/drupal/* /opt/drupal/.[!.]* /opt/drupal/..?* 2>/dev/null || true; \
|
||||
mkdir -p /opt/drupal; \
|
||||
cp -a "$builddir"/. /opt/drupal/; \
|
||||
rm -rf "$builddir"
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Make vendor binaries available in PATH
|
||||
# -------------------------------------------------------------------
|
||||
RUN ln -sf /opt/drupal/vendor/bin/drush /usr/local/bin/drush
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# PHP upload configuration
|
||||
# -------------------------------------------------------------------
|
||||
COPY {{ DRUPAL_CONFIG_UPLOAD_REL }} $PHP_INI_DIR/conf.d/
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Permissions and ownership fixes
|
||||
# -------------------------------------------------------------------
|
||||
RUN set -eux; \
|
||||
# Ensure all directories are traversable
|
||||
chmod 755 /var /var/www /opt /opt/drupal; \
|
||||
# Ensure correct ownership for Drupal files
|
||||
chown -R www-data:www-data /opt/drupal; \
|
||||
# Apply default permissions
|
||||
find /opt/drupal -type d -exec chmod 755 {} +; \
|
||||
find /opt/drupal -type f -exec chmod 644 {} +; \
|
||||
# Ensure vendor binaries are executable
|
||||
if [ -d /opt/drupal/vendor/bin ]; then chmod a+rx /opt/drupal/vendor/bin/*; fi; \
|
||||
if [ -f /opt/drupal/vendor/drush/drush/drush ]; then chmod a+rx /opt/drupal/vendor/drush/drush/drush; fi; \
|
||||
# Ensure the docroot ({{ DRUPAL_DOCKER_HTML_PATH }}) is accessible
|
||||
if [ -d {{ DRUPAL_DOCKER_HTML_PATH }} ]; then \
|
||||
chmod 755 {{ DRUPAL_DOCKER_HTML_PATH }}; \
|
||||
find {{ DRUPAL_DOCKER_HTML_PATH }} -type d -exec chmod 755 {} +; \
|
||||
fi; \
|
||||
# Ensure settings.local.php exists and is owned by www-data
|
||||
install -o www-data -g www-data -m 640 /dev/null {{ DRUPAL_DOCKER_HTML_PATH }}/sites/default/settings.local.php
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Runtime defaults
|
||||
# -------------------------------------------------------------------
|
||||
USER www-data
|
||||
WORKDIR /var/www/html # symlink pointing to {{ DRUPAL_DOCKER_HTML_PATH }}
|
||||
|
||||
# Ensure PATH for non-login shells includes /usr/local/bin
|
||||
ENV PATH="/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Build-time check (optional)
|
||||
# -------------------------------------------------------------------
|
||||
RUN /usr/local/bin/drush --version
|
||||
22
roles/web-app-drupal/templates/docker-compose.yml.j2
Normal file
22
roles/web-app-drupal/templates/docker-compose.yml.j2
Normal file
@@ -0,0 +1,22 @@
|
||||
{% include 'roles/docker-compose/templates/base.yml.j2' %}
|
||||
application:
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
image: {{ DRUPAL_CUSTOM_IMAGE }}
|
||||
container_name: {{ DRUPAL_CONTAINER }}
|
||||
{{ lookup('template', 'roles/docker-container/templates/build.yml.j2') | indent(4) }}
|
||||
ports:
|
||||
- "127.0.0.1:{{ ports.localhost.http[application_id] }}:80"
|
||||
volumes:
|
||||
- data:{{ DRUPAL_DOCKER_HTML_PATH }}/sites/default/files
|
||||
- {{ DRUPAL_MSMTP_ABS }}:/etc/msmtprc
|
||||
- {{ DRUPAL_SETTINGS_LOCAL_ABS }}:{{ DRUPAL_DOCKER_CONF_PATH }}/settings.local.php
|
||||
|
||||
{% include 'roles/docker-container/templates/healthcheck/msmtp_curl.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/depends_on/dmbs_excl.yml.j2' %}
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
||||
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
data:
|
||||
name: "{{ DRUPAL_VOLUME }}"
|
||||
7
roles/web-app-drupal/templates/env.j2
Normal file
7
roles/web-app-drupal/templates/env.j2
Normal file
@@ -0,0 +1,7 @@
|
||||
DRUPAL_DB_HOST= "{{ database_host }}:{{ database_port }}"
|
||||
DRUPAL_DB_USER= "{{ database_username }}"
|
||||
DRUPAL_DB_PASSWORD= "{{ database_password }}"
|
||||
DRUPAL_DB_NAME= "{{ database_name }}"
|
||||
|
||||
# Debug flags (optional)
|
||||
DRUPAL_DEBUG={{ MODE_DEBUG | lower }}
|
||||
49
roles/web-app-drupal/templates/settings.local.php.j2
Normal file
49
roles/web-app-drupal/templates/settings.local.php.j2
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Local settings overrides generated by Ansible.
|
||||
* - Reads DB + OIDC endpoints from environment variables.
|
||||
* - Sets $databases and selected $config overrides.
|
||||
*/
|
||||
|
||||
$env = getenv();
|
||||
|
||||
/** Database **/
|
||||
$host = getenv('DRUPAL_DB_HOST') ?: '{{ database_host }}:{{ database_port }}';
|
||||
$db = getenv('DRUPAL_DB_NAME') ?: '{{ database_name }}';
|
||||
$user = getenv('DRUPAL_DB_USER') ?: '{{ database_username }}';
|
||||
$pass = getenv('DRUPAL_DB_PASSWORD') ?: '{{ database_password }}';
|
||||
|
||||
$parts = explode(':', $host, 2);
|
||||
$hostname = $parts[0];
|
||||
$port = isset($parts[1]) ? (int)$parts[1] : 3306;
|
||||
|
||||
$databases['default']['default'] = [
|
||||
'database' => $db,
|
||||
'username' => $user,
|
||||
'password' => $pass,
|
||||
'prefix' => '',
|
||||
'host' => $hostname,
|
||||
'port' => $port,
|
||||
'namespace'=> 'Drupal\\Core\\Database\\Driver\\mysql',
|
||||
'driver' => 'mysql',
|
||||
];
|
||||
|
||||
/** OIDC endpoint hints (optional) — the real config is applied via Drush. */
|
||||
$config['openid_connect.settings']['automatic_account_creation'] = true;
|
||||
$config['openid_connect.settings']['always_save_userinfo'] = true;
|
||||
$config['openid_connect.settings']['link_existing_users'] = true;
|
||||
|
||||
/** Trusted host patterns can be extended by Ansible task 04_trusted_hosts.yml */
|
||||
|
||||
/** Enable local services YML if present */
|
||||
$settings['container_yamls'][] = $app_root . '/' . $site_path . '/services.local.yml';
|
||||
|
||||
// Reverse proxy optional über ENV setzen (z.B. "10.0.0.0/8, 172.16.0.0/12")
|
||||
$proxy = getenv('REVERSE_PROXY_ADDRESSES');
|
||||
if ($proxy) {
|
||||
$settings['reverse_proxy'] = TRUE;
|
||||
$settings['reverse_proxy_addresses'] = array_map('trim', explode(',', $proxy));
|
||||
}
|
||||
|
||||
/** Hash salt (from schema/credentials, hashed with SHA-256) */
|
||||
$settings['hash_salt'] = '{{ applications | get_app_conf(application_id, "credentials.hash_salt", True) }}';
|
||||
8
roles/web-app-drupal/templates/upload.ini.j2
Normal file
8
roles/web-app-drupal/templates/upload.ini.j2
Normal file
@@ -0,0 +1,8 @@
|
||||
file_uploads = On
|
||||
memory_limit = {{ DRUPAL_MAX_UPLOAD_SIZE }}
|
||||
upload_max_filesize = {{ DRUPAL_MAX_UPLOAD_SIZE }}
|
||||
post_max_size = {{ DRUPAL_MAX_UPLOAD_SIZE }}
|
||||
max_execution_time = 300
|
||||
|
||||
; Use msmtp as the Mail Transfer Agent
|
||||
sendmail_path = "/usr/bin/msmtp -t"
|
||||
4
roles/web-app-drupal/users/main.yml
Normal file
4
roles/web-app-drupal/users/main.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
users:
|
||||
administrator:
|
||||
username: "administrator"
|
||||
email: "administrator@{{ PRIMARY_DOMAIN }}"
|
||||
28
roles/web-app-drupal/vars/main.yml
Normal file
28
roles/web-app-drupal/vars/main.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
# General
|
||||
|
||||
application_id: "web-app-drupal"
|
||||
database_type: "mariadb"
|
||||
|
||||
# Drupal
|
||||
|
||||
DRUPAL_URL: "{{ domains | get_url(application_id, WEB_PROTOCOL) }}"
|
||||
DRUPAL_CUSTOM_IMAGE: "drupal_custom"
|
||||
DRUPAL_DOCKER_HTML_PATH: "/opt/drupal/web"
|
||||
DRUPAL_DOCKER_CONF_PATH: "{{ DRUPAL_DOCKER_HTML_PATH }}/sites/default"
|
||||
DRUPAL_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.drupal.version') }}"
|
||||
DRUPAL_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.drupal.image') }}"
|
||||
DRUPAL_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.drupal.name') }}"
|
||||
DRUPAL_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.data') }}"
|
||||
DRUPAL_DOMAINS: "{{ applications | get_app_conf(application_id, 'server.domains.canonical') }}"
|
||||
DRUPAL_USER: "www-data"
|
||||
|
||||
DRUPAL_CONFIG_UPLOAD_REL: "config/upload.ini"
|
||||
DRUPAL_CONFIG_UPLOAD_ABS: "{{ [docker_compose.directories.instance, DRUPAL_CONFIG_UPLOAD_REL] | path_join }}"
|
||||
|
||||
DRUPAL_SETTINGS_LOCAL_REL: "config/settings.local.php"
|
||||
DRUPAL_SETTINGS_LOCAL_ABS: "{{ [docker_compose.directories.instance, DRUPAL_SETTINGS_LOCAL_REL] | path_join }}"
|
||||
|
||||
DRUPAL_MSMTP_SRC: "{{ [ playbook_dir, 'roles/sys-svc-msmtp/templates/msmtprc.conf.j2' ] | path_join }}"
|
||||
DRUPAL_MSMTP_ABS: "{{ [ docker_compose.directories.config, 'msmtprc.conf'] | path_join }}"
|
||||
|
||||
DRUPAL_MAX_UPLOAD_SIZE: "{{ applications | get_app_conf(application_id, 'max_upload_size') }}"
|
||||
34
roles/web-app-drupal/vars/oidc.yml
Normal file
34
roles/web-app-drupal/vars/oidc.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
# OIDC configuration for Drupal's OpenID Connect module.
|
||||
|
||||
# Global settings for openid_connect.settings
|
||||
|
||||
oidc_settings:
|
||||
automatic_account_creation: true # Auto-create users on first login
|
||||
always_save_userinfo: true # Store latest userinfo on each login
|
||||
link_existing_users: true # Match existing users by email
|
||||
login_display: "button" # 'button' or 'form'
|
||||
enforced: false # If true, require login for the whole site
|
||||
|
||||
# OIDC client entity (e.g., 'keycloak')
|
||||
|
||||
oidc_client:
|
||||
id: "keycloak"
|
||||
label: "Keycloak"
|
||||
plugin: "generic" # use the built-in generic OIDC client plugin
|
||||
settings:
|
||||
client_id: "{{ OIDC.CLIENT.ID }}"
|
||||
client_secret: "{{ OIDC.CLIENT.SECRET }}"
|
||||
authorization_endpoint: "{{ OIDC.CLIENT.AUTHORIZE_URL }}"
|
||||
token_endpoint: "{{ OIDC.CLIENT.TOKEN_URL }}"
|
||||
userinfo_endpoint: "{{ OIDC.CLIENT.USER_INFO_URL }}"
|
||||
end_session_endpoint: "{{ OIDC.CLIENT.LOGOUT_URL }}"
|
||||
scopes:
|
||||
- "openid"
|
||||
- "email"
|
||||
- "profile"
|
||||
use_standard_claims: true
|
||||
# Optional claim mapping examples:
|
||||
# username_claim: "{{ OIDC.ATTRIBUTES.USERNAME }}"
|
||||
# email_claim: "{{ OIDC.ATTRIBUTES.EMAIL }}"
|
||||
# given_name_claim: "{{ OIDC.ATTRIBUTES.GIVEN_NAME }}"
|
||||
# family_name_claim: "{{ OIDC.ATTRIBUTES.FAMILY_NAME }}"
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -27,7 +27,7 @@ server:
|
||||
aliases: []
|
||||
csp:
|
||||
flags:
|
||||
style-src:
|
||||
style-src-attr:
|
||||
unsafe-inline: true
|
||||
whitelist:
|
||||
font-src:
|
||||
|
||||
@@ -24,7 +24,7 @@ server:
|
||||
flags:
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
style-src:
|
||||
style-src-attr:
|
||||
unsafe-inline: true
|
||||
whitelist:
|
||||
font-src:
|
||||
|
||||
@@ -27,3 +27,7 @@ server:
|
||||
domains:
|
||||
canonical:
|
||||
- lab.git.{{ PRIMARY_DOMAIN }}
|
||||
csp:
|
||||
flags:
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -14,7 +14,7 @@ server:
|
||||
aliases: []
|
||||
csp:
|
||||
flags:
|
||||
style-src:
|
||||
style-src-attr:
|
||||
unsafe-inline: true
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
# (Optional) specifically wait for the CLI installer script
|
||||
- name: "Check for CLI installer"
|
||||
command:
|
||||
argv: [ docker, exec, "{{ JOOMLA_CONTAINER }}", test, -f, /var/www/html/installation/joomla.php ]
|
||||
argv: [ docker, exec, "{{ JOOMLA_CONTAINER }}", test, -f, "{{ JOOMLA_INSTALLER_CLI_FILE }}" ]
|
||||
register: has_installer
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
@@ -32,7 +32,7 @@
|
||||
- exec
|
||||
- "{{ JOOMLA_CONTAINER }}"
|
||||
- php
|
||||
- /var/www/html/installation/joomla.php
|
||||
- "{{ JOOMLA_INSTALLER_CLI_FILE }}"
|
||||
- install
|
||||
- "--db-type={{ JOOMLA_DB_CONNECTOR }}"
|
||||
- "--db-host={{ database_host }}"
|
||||
|
||||
18
roles/web-app-joomla/tasks/05_reset_admin_password.yml
Normal file
18
roles/web-app-joomla/tasks/05_reset_admin_password.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
# Reset Joomla admin password via CLI (inside the container)
|
||||
- name: "Reset Joomla admin password (non-interactive CLI)"
|
||||
command:
|
||||
argv:
|
||||
- docker
|
||||
- exec
|
||||
- "{{ JOOMLA_CONTAINER }}"
|
||||
- php
|
||||
- "{{ JOOMLA_CLI_FILE }}"
|
||||
- user:reset-password
|
||||
- "--username"
|
||||
- "{{ JOOMLA_USER_NAME }}"
|
||||
- "--password"
|
||||
- "{{ JOOMLA_USER_PASSWORD }}"
|
||||
register: j_password_reset
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
changed_when: j_password_reset.rc == 0
|
||||
@@ -24,3 +24,7 @@
|
||||
- name: Include assert routines
|
||||
include_tasks: "04_assert.yml"
|
||||
when: MODE_ASSERT | bool
|
||||
|
||||
- name: Reset Admin Password
|
||||
include_tasks: 05_reset_admin_password.yml
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ JOOMLA_DOMAINS: "{{ applications | get_app_conf(application_id
|
||||
JOOMLA_SITE_NAME: "{{ SOFTWARE_NAME }} Joomla - CMS"
|
||||
JOOMLA_DB_CONNECTOR: "{{ 'pgsql' if database_type == 'postgres' else 'mysqli' }}"
|
||||
JOOMLA_CONFIG_FILE: "/var/www/html/configuration.php"
|
||||
JOOMLA_INSTALLER_CLI_FILE: "/var/www/html/installation/joomla.php"
|
||||
JOOMLA_CLI_FILE: "/var/www/html/cli/joomla.php"
|
||||
|
||||
# User
|
||||
JOOMLA_USER_NAME: "{{ users.administrator.username }}"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -13,6 +13,16 @@ server:
|
||||
aliases: []
|
||||
status_codes:
|
||||
default: 404
|
||||
csp:
|
||||
flags:
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
whitelist:
|
||||
script-src-elem:
|
||||
- "https://www.hcaptcha.com"
|
||||
- "https://js.hcaptcha.com"
|
||||
frame-src:
|
||||
- "https://newassets.hcaptcha.com/"
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -4,6 +4,11 @@ server:
|
||||
canonical:
|
||||
- "m.wiki.{{ PRIMARY_DOMAIN }}"
|
||||
aliases: []
|
||||
csp:
|
||||
flags:
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
26
roles/web-app-mini-qr/README.md
Normal file
26
roles/web-app-mini-qr/README.md
Normal 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`
|
||||
2
roles/web-app-mini-qr/TODO.md
Normal file
2
roles/web-app-mini-qr/TODO.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# To-dos
|
||||
- Remove clarity.ms
|
||||
38
roles/web-app-mini-qr/config/main.yml
Normal file
38
roles/web-app-mini-qr/config/main.yml
Normal 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: []
|
||||
27
roles/web-app-mini-qr/meta/main.yml
Normal file
27
roles/web-app-mini-qr/meta/main.yml
Normal 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: []
|
||||
7
roles/web-app-mini-qr/tasks/01_core.yml
Normal file
7
roles/web-app-mini-qr/tasks/01_core.yml
Normal 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
|
||||
4
roles/web-app-mini-qr/tasks/main.yml
Normal file
4
roles/web-app-mini-qr/tasks/main.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
- include_tasks: 01_core.yml
|
||||
when: run_once_web_app_mini_qr is not defined
|
||||
|
||||
12
roles/web-app-mini-qr/templates/docker-compose.yml.j2
Normal file
12
roles/web-app-mini-qr/templates/docker-compose.yml.j2
Normal 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' %}
|
||||
12
roles/web-app-mini-qr/vars/main.yml
Normal file
12
roles/web-app-mini-qr/vars/main.yml
Normal 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 }}"
|
||||
@@ -10,7 +10,7 @@ server:
|
||||
flags:
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
script-src:
|
||||
script-src-attr:
|
||||
unsafe-eval: true
|
||||
domains:
|
||||
canonical:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,13 +2,16 @@ 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:
|
||||
script-src-elem:
|
||||
- "https://www.hcaptcha.com"
|
||||
- "https://js.hcaptcha.com"
|
||||
font-src:
|
||||
- "data:"
|
||||
connect-src:
|
||||
@@ -19,6 +22,7 @@ server:
|
||||
frame-src:
|
||||
- "{{ WEBSOCKET_PROTOCOL }}://collabora.{{ PRIMARY_DOMAIN }}"
|
||||
- "{{ WEB_PROTOCOL }}://collabora.{{ PRIMARY_DOMAIN }}"
|
||||
- "https://newassets.hcaptcha.com/"
|
||||
worker-src:
|
||||
- "blob:"
|
||||
domains:
|
||||
@@ -82,7 +86,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"
|
||||
@@ -92,7 +96,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
command: "{{ NEXTCLOUD_DOCKER_EXEC_OCC }} maintenance:repair --include-expensive"
|
||||
register: occ_repair
|
||||
changed_when: "'No repairs needed' not in occ_repair.stdout"
|
||||
retries: 3
|
||||
delay: 10
|
||||
until: occ_repair.rc == 0
|
||||
|
||||
- name: Nextcloud | App update (retry once)
|
||||
command: "{{ NEXTCLOUD_DOCKER_EXEC_OCC }} app:update --all"
|
||||
|
||||
@@ -16,6 +16,13 @@
|
||||
- name: Flush all handlers immediately so that occ can be used
|
||||
meta: flush_handlers
|
||||
|
||||
- name: Wait until Redis is ready (PONG)
|
||||
command: "docker exec {{ NEXTCLOUD_REDIS_CONTAINER }} redis-cli ping"
|
||||
register: redis_ping
|
||||
retries: 60
|
||||
delay: 2
|
||||
until: (redis_ping.stdout | default('')) is search('PONG')
|
||||
|
||||
- name: Update\Upgrade Nextcloud
|
||||
include_tasks: 03_upgrade.yml
|
||||
when: MODE_UPDATE | bool
|
||||
|
||||
27
roles/web-app-nextcloud/templates/Dockerfiles/Whiteboard.j2
Normal file
27
roles/web-app-nextcloud/templates/Dockerfiles/Whiteboard.j2
Normal 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
|
||||
@@ -67,8 +67,12 @@
|
||||
{{ 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
|
||||
@@ -76,6 +80,7 @@
|
||||
|
||||
expose:
|
||||
- "{{ container_port }}"
|
||||
shm_size: 1g
|
||||
networks:
|
||||
default:
|
||||
ipv4_address: 192.168.102.71
|
||||
|
||||
@@ -65,6 +65,4 @@ CHROMIUM_FLAGS=--headless=new --no-sandbox --disable-gpu --disable-dev-shm-usage
|
||||
# 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
|
||||
# Deactivated @todo implement
|
||||
WHITEBOARD_ENABLE_RECORDING=false
|
||||
{% endif %}
|
||||
@@ -120,6 +120,7 @@ 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') }}"
|
||||
@@ -127,15 +128,20 @@ 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) }}"
|
||||
|
||||
## User Configuration
|
||||
NEXTCLOUD_DOCKER_USER_id: 82 # UID of the www-data user
|
||||
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_DOCKER_EXEC_OCC: "{{ NEXTCLOUD_DOCKER_EXEC }} {{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}" # Execute docker occ command
|
||||
|
||||
## Redis
|
||||
NEXTCLOUD_REDIS_CONTAINER: "{{ entity_name }}-redis"
|
||||
@@ -23,7 +23,7 @@ server:
|
||||
flags:
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
style-src:
|
||||
style-src-attr:
|
||||
unsafe-inline: true
|
||||
whitelist:
|
||||
font-src:
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -16,7 +16,7 @@ features:
|
||||
server:
|
||||
csp:
|
||||
flags:
|
||||
style-src:
|
||||
style-src-attr:
|
||||
unsafe-inline: true
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
|
||||
@@ -15,7 +15,7 @@ features:
|
||||
server:
|
||||
csp:
|
||||
flags:
|
||||
style-src:
|
||||
style-src-attr:
|
||||
unsafe-inline: true
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -32,7 +32,7 @@ server:
|
||||
worker-src:
|
||||
- "blob:"
|
||||
flags:
|
||||
script-src:
|
||||
script-src-attr:
|
||||
unsafe-eval: true
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
environment:
|
||||
JAVA_OPTS: >-
|
||||
{% if xwiki_oidc_enabled_switch| bool %}
|
||||
-Dxwiki.authentication.authservice=oidc
|
||||
-Dxwiki.authentication.authclass=org.xwiki.contrib.oidc.auth.OIDCAuthServiceImpl
|
||||
{% 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,6 +26,7 @@
|
||||
-Dxwiki.authentication.ldap.fields_mapping={{ XWIKI_LDAP_FIELDS_MAPPING }}
|
||||
-Dxwiki.authentication.ldap.update_user=1
|
||||
{% else %}
|
||||
-Dxwiki.authentication.authservice=standard
|
||||
-Dxwiki.authentication.authclass=com.xpn.xwiki.user.impl.xwiki.XWikiAuthServiceImpl
|
||||
{% endif %}
|
||||
volumes:
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -8,7 +8,7 @@ server:
|
||||
frame-ancestors:
|
||||
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
|
||||
flags:
|
||||
style-src:
|
||||
style-src-attr:
|
||||
unsafe-inline: true
|
||||
docker:
|
||||
services:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -11,7 +11,7 @@ server:
|
||||
aliases: []
|
||||
csp:
|
||||
flags:
|
||||
style-src:
|
||||
style-src-attr:
|
||||
unsafe-inline: true
|
||||
script-src-elem:
|
||||
unsafe-inline: true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user