mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-10-10 02:38:10 +02:00
fix(csp): resolve all CSP-related issues and extend webserver health checks
- Added _normalize_codes to support lists of valid HTTP status codes - Updated web_health_expectations to handle multiple codes, deduplication, and fallback logic - Extended unit tests with coverage for list/default combinations, invalid values, and alias behavior - Fixed Flowise CSP flags and whitelist entries - Adjusted Flowise, MinIO, and Pretix docker service resource limits - Updated docker-compose templates with explicit service_name - Corrected MinIO status_codes to 301 redirects ✅ All CSP errors fixed See details: https://chatgpt.com/share/68d557ad-fc10-800f-b68b-0411d20ea6eb
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
# roles/sys-ctl-hlth-webserver/filter_plugins/web_health_expectations.py
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
@@ -94,6 +93,26 @@ def _normalize_selection(group_names):
|
|||||||
raise ValueError("web_health_expectations: 'group_names' must be provided and non-empty")
|
raise ValueError("web_health_expectations: 'group_names' must be provided and non-empty")
|
||||||
return sel
|
return sel
|
||||||
|
|
||||||
|
def _normalize_codes(x):
|
||||||
|
"""
|
||||||
|
Accepts:
|
||||||
|
- single code (int or str)
|
||||||
|
- list/tuple/set of codes
|
||||||
|
Returns a de-duplicated list of valid ints (100..599) in original order.
|
||||||
|
"""
|
||||||
|
if x is None:
|
||||||
|
return []
|
||||||
|
if isinstance(x, (list, tuple, set)):
|
||||||
|
out = []
|
||||||
|
seen = set()
|
||||||
|
for v in x:
|
||||||
|
c = _valid_http_code(v)
|
||||||
|
if c is not None and c not in seen:
|
||||||
|
seen.add(c)
|
||||||
|
out.append(c)
|
||||||
|
return out
|
||||||
|
c = _valid_http_code(x)
|
||||||
|
return [c] if c is not None else []
|
||||||
|
|
||||||
def web_health_expectations(applications, www_enabled: bool = False, group_names=None, redirect_maps=None):
|
def web_health_expectations(applications, www_enabled: bool = False, group_names=None, redirect_maps=None):
|
||||||
"""Produce a **flat mapping**: domain -> [expected_status_codes].
|
"""Produce a **flat mapping**: domain -> [expected_status_codes].
|
||||||
@@ -138,17 +157,15 @@ def web_health_expectations(applications, www_enabled: bool = False, group_names
|
|||||||
sc_map = {}
|
sc_map = {}
|
||||||
if isinstance(sc_raw, Mapping):
|
if isinstance(sc_raw, Mapping):
|
||||||
for k, v in sc_raw.items():
|
for k, v in sc_raw.items():
|
||||||
code = _valid_http_code(v)
|
codes = _normalize_codes(v)
|
||||||
if code is not None:
|
if codes:
|
||||||
sc_map[str(k)] = code
|
sc_map[str(k)] = codes
|
||||||
|
|
||||||
if isinstance(canonical_raw, Mapping) and canonical_raw:
|
if isinstance(canonical_raw, Mapping) and canonical_raw:
|
||||||
for key, domains in canonical_raw.items():
|
for key, domains in canonical_raw.items():
|
||||||
domains_list = _to_list(domains, allow_mapping=False)
|
domains_list = _to_list(domains, allow_mapping=False)
|
||||||
code = _valid_http_code(sc_map.get(key))
|
codes = sc_map.get(key) or sc_map.get("default")
|
||||||
if code is None:
|
expected = list(codes) if codes else list(DEFAULT_OK)
|
||||||
code = _valid_http_code(sc_map.get("default"))
|
|
||||||
expected = [code] if code is not None else list(DEFAULT_OK)
|
|
||||||
for d in domains_list:
|
for d in domains_list:
|
||||||
if d:
|
if d:
|
||||||
expectations[d] = expected
|
expectations[d] = expected
|
||||||
@@ -156,8 +173,8 @@ def web_health_expectations(applications, www_enabled: bool = False, group_names
|
|||||||
for d in _to_list(canonical_raw, allow_mapping=True):
|
for d in _to_list(canonical_raw, allow_mapping=True):
|
||||||
if not d:
|
if not d:
|
||||||
continue
|
continue
|
||||||
code = _valid_http_code(sc_map.get("default"))
|
codes = sc_map.get("default")
|
||||||
expectations[d] = [code] if code is not None else list(DEFAULT_OK)
|
expectations[d] = list(codes) if codes else list(DEFAULT_OK)
|
||||||
|
|
||||||
for d in aliases:
|
for d in aliases:
|
||||||
if d:
|
if d:
|
||||||
|
@@ -12,38 +12,51 @@ server:
|
|||||||
- "flow.ai.{{ PRIMARY_DOMAIN }}"
|
- "flow.ai.{{ PRIMARY_DOMAIN }}"
|
||||||
aliases: []
|
aliases: []
|
||||||
csp:
|
csp:
|
||||||
flags: {}
|
flags:
|
||||||
#script-src-elem:
|
script-src-elem:
|
||||||
# unsafe-inline: true
|
unsafe-inline: true
|
||||||
#script-src:
|
|
||||||
# unsafe-inline: true
|
|
||||||
# unsafe-eval: true
|
|
||||||
#style-src:
|
|
||||||
# unsafe-inline: true
|
|
||||||
whitelist:
|
whitelist:
|
||||||
font-src:
|
font-src:
|
||||||
|
- https://fonts.gstatic.com
|
||||||
|
style-src-elem:
|
||||||
- https://fonts.googleapis.com
|
- https://fonts.googleapis.com
|
||||||
connect-src: []
|
script-src-elem:
|
||||||
|
- https://fonts.googleapis.com
|
||||||
|
- https://fonts.gstatic.com
|
||||||
|
- https://r.wdfl.co
|
||||||
|
connect-src: []
|
||||||
docker:
|
docker:
|
||||||
services:
|
services:
|
||||||
litellm:
|
litellm:
|
||||||
backup:
|
backup:
|
||||||
no_stop_required: true
|
no_stop_required: true
|
||||||
image: ghcr.io/berriai/litellm
|
image: ghcr.io/berriai/litellm
|
||||||
version: main-v1.77.3.dynamic_rates
|
version: main-v1.77.3.dynamic_rates
|
||||||
name: litellm
|
name: litellm
|
||||||
|
cpus: "1.0"
|
||||||
|
mem_reservation: "0.5g"
|
||||||
|
mem_limit: "1g"
|
||||||
|
pids_limit: 1024
|
||||||
qdrant:
|
qdrant:
|
||||||
backup:
|
backup:
|
||||||
no_stop_required: true
|
no_stop_required: true
|
||||||
image: qdrant/qdrant
|
image: qdrant/qdrant
|
||||||
version: latest
|
version: latest
|
||||||
name: qdrant
|
name: qdrant
|
||||||
|
cpus: "2.0"
|
||||||
|
mem_reservation: "2g"
|
||||||
|
mem_limit: "4g"
|
||||||
|
pids_limit: 2048
|
||||||
flowise:
|
flowise:
|
||||||
backup:
|
backup:
|
||||||
no_stop_required: true
|
no_stop_required: false # As long as SQLite is used
|
||||||
image: flowiseai/flowise
|
image: flowiseai/flowise
|
||||||
version: latest
|
version: latest
|
||||||
name: flowise
|
name: flowise
|
||||||
|
cpus: "1.0"
|
||||||
|
mem_reservation: "1g"
|
||||||
|
mem_limit: "2g"
|
||||||
|
pids_limit: 1024
|
||||||
redis:
|
redis:
|
||||||
enabled: false
|
enabled: false
|
||||||
database:
|
database:
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
{% include 'roles/docker-compose/templates/base.yml.j2' %}
|
{% include 'roles/docker-compose/templates/base.yml.j2' %}
|
||||||
litellm:
|
litellm:
|
||||||
|
{% set service_name = 'litellm' %}
|
||||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||||
image: {{ FLOWISE_LITELLM_IMAGE }}:{{ FLOWISE_LITELLM_VERSION }}
|
image: {{ FLOWISE_LITELLM_IMAGE }}:{{ FLOWISE_LITELLM_VERSION }}
|
||||||
container_name: {{ FLOWISE_LITELLM_CONTAINER }}
|
container_name: {{ FLOWISE_LITELLM_CONTAINER }}
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||||
|
|
||||||
qdrant:
|
qdrant:
|
||||||
|
{% set service_name = 'qdrant' %}
|
||||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||||
image: {{ FLOWISE_QDRANT_IMAGE }}:{{ FLOWISE_QDRANT_VERSION }}
|
image: {{ FLOWISE_QDRANT_IMAGE }}:{{ FLOWISE_QDRANT_VERSION }}
|
||||||
container_name: {{ FLOWISE_QDRANT_CONTAINER }}
|
container_name: {{ FLOWISE_QDRANT_CONTAINER }}
|
||||||
@@ -25,6 +27,7 @@
|
|||||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||||
|
|
||||||
flowise:
|
flowise:
|
||||||
|
{% set service_name = 'flowise' %}
|
||||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||||
image: {{ FLOWISE_IMAGE }}:{{ FLOWISE_VERSION }}
|
image: {{ FLOWISE_IMAGE }}:{{ FLOWISE_VERSION }}
|
||||||
container_name: {{ FLOWISE_CONTAINER }}
|
container_name: {{ FLOWISE_CONTAINER }}
|
||||||
|
@@ -10,7 +10,8 @@ features:
|
|||||||
ldap: false # OIDC is already activated so LDAP isn't necessary
|
ldap: false # OIDC is already activated so LDAP isn't necessary
|
||||||
server:
|
server:
|
||||||
status_codes:
|
status_codes:
|
||||||
api: 400
|
api: 301
|
||||||
|
console: 301
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
console: "console.s3.{{ PRIMARY_DOMAIN }}"
|
console: "console.s3.{{ PRIMARY_DOMAIN }}"
|
||||||
@@ -25,10 +26,14 @@ docker:
|
|||||||
services:
|
services:
|
||||||
minio:
|
minio:
|
||||||
backup:
|
backup:
|
||||||
no_stop_required: true
|
no_stop_required: false
|
||||||
image: quay.io/minio/minio
|
image: quay.io/minio/minio
|
||||||
version: latest
|
version: latest
|
||||||
name: minio
|
name: minio
|
||||||
|
cpus: "2.0"
|
||||||
|
mem_reservation: "2g"
|
||||||
|
mem_limit: "4g"
|
||||||
|
pids_limit: 2048
|
||||||
redis:
|
redis:
|
||||||
enabled: false
|
enabled: false
|
||||||
database:
|
database:
|
||||||
|
@@ -11,6 +11,10 @@ docker:
|
|||||||
name: pretix
|
name: pretix
|
||||||
backup:
|
backup:
|
||||||
no_stop_required: true
|
no_stop_required: true
|
||||||
|
cpus: "2.0"
|
||||||
|
mem_reservation: "1.5g"
|
||||||
|
mem_limit: "2g"
|
||||||
|
pids_limit: 1024
|
||||||
volumes:
|
volumes:
|
||||||
data: "pretix_data"
|
data: "pretix_data"
|
||||||
config: "pretix_config"
|
config: "pretix_config"
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
# tests/unit/roles/sys-ctl-hlth-webserver/filter_plugins/test_web_health_expectations.py
|
|
||||||
import os
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
import importlib.util
|
import importlib.util
|
||||||
@@ -272,7 +271,131 @@ class TestWebHealthExpectationsFilter(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertNotIn("ignored.example.org", out)
|
self.assertNotIn("ignored.example.org", out)
|
||||||
self.assertEqual(out["manual.example.org"], [301])
|
self.assertEqual(out["manual.example.org"], [301])
|
||||||
|
|
||||||
|
# --------- NEW: status_codes list support ---------
|
||||||
|
|
||||||
|
def test_flat_canonical_with_default_list(self):
|
||||||
|
apps = {"app-l1": {}}
|
||||||
|
self._configure_returns({
|
||||||
|
("app-l1", "server.domains.canonical"): ["l1.example.org"],
|
||||||
|
("app-l1", "server.domains.aliases"): [],
|
||||||
|
("app-l1", "server.status_codes"): {"default": [204, "302", 301]},
|
||||||
|
})
|
||||||
|
out = self.mod.web_health_expectations(apps, group_names=["app-l1"])
|
||||||
|
self.assertEqual(out["l1.example.org"], [204, 302, 301])
|
||||||
|
|
||||||
|
def test_keyed_canonical_with_list_and_default_list(self):
|
||||||
|
apps = {"app-l2": {}}
|
||||||
|
self._configure_returns({
|
||||||
|
("app-l2", "server.domains.canonical"): {
|
||||||
|
"api": ["api1.l2.example.org", "api2.l2.example.org"],
|
||||||
|
"web": "web.l2.example.org",
|
||||||
|
},
|
||||||
|
("app-l2", "server.domains.aliases"): [],
|
||||||
|
("app-l2", "server.status_codes"): {"api": [301, 403], "default": [200, 204]},
|
||||||
|
})
|
||||||
|
out = self.mod.web_health_expectations(apps, group_names=["app-l2"])
|
||||||
|
self.assertEqual(out["api1.l2.example.org"], [301, 403]) # per-key list wins
|
||||||
|
self.assertEqual(out["api2.l2.example.org"], [301, 403])
|
||||||
|
self.assertEqual(out["web.l2.example.org"], [200, 204]) # default list
|
||||||
|
|
||||||
|
def test_status_codes_strings_and_ints_and_out_of_range_ignored(self):
|
||||||
|
apps = {"app-l3": {}}
|
||||||
|
# 99 (<100) and 700 (>599) are ignored, "301" string is converted
|
||||||
|
self._configure_returns({
|
||||||
|
("app-l3", "server.domains.canonical"): ["l3.example.org"],
|
||||||
|
("app-l3", "server.domains.aliases"): [],
|
||||||
|
("app-l3", "server.status_codes"): {"default": ["301", 200, 99, 700]},
|
||||||
|
})
|
||||||
|
out = self.mod.web_health_expectations(apps, group_names=["app-l3"])
|
||||||
|
self.assertEqual(out["l3.example.org"], [301, 200])
|
||||||
|
|
||||||
|
def test_status_codes_deduplicate_preserve_order(self):
|
||||||
|
apps = {"app-l4": {}}
|
||||||
|
self._configure_returns({
|
||||||
|
("app-l4", "server.domains.canonical"): ["l4.example.org"],
|
||||||
|
("app-l4", "server.domains.aliases"): [],
|
||||||
|
("app-l4", "server.status_codes"): {"default": [301, 302, 301, 302, 200]},
|
||||||
|
})
|
||||||
|
out = self.mod.web_health_expectations(apps, group_names=["app-l4"])
|
||||||
|
self.assertEqual(out["l4.example.org"], [301, 302, 200]) # dedup but keep order
|
||||||
|
|
||||||
|
def test_key_specific_int_overrides_default_list(self):
|
||||||
|
apps = {"app-l5": {}}
|
||||||
|
self._configure_returns({
|
||||||
|
("app-l5", "server.domains.canonical"): {"console": ["c1.l5.example.org"]},
|
||||||
|
("app-l5", "server.domains.aliases"): [],
|
||||||
|
("app-l5", "server.status_codes"): {"console": 301, "default": [200, 204]},
|
||||||
|
})
|
||||||
|
out = self.mod.web_health_expectations(apps, group_names=["app-l5"])
|
||||||
|
self.assertEqual(out["c1.l5.example.org"], [301]) # per-key int beats default list
|
||||||
|
|
||||||
|
def test_key_specific_list_overrides_default_int(self):
|
||||||
|
apps = {"app-l6": {}}
|
||||||
|
self._configure_returns({
|
||||||
|
("app-l6", "server.domains.canonical"): {"api": "api.l6.example.org"},
|
||||||
|
("app-l6", "server.domains.aliases"): [],
|
||||||
|
("app-l6", "server.status_codes"): {"api": [301, 403], "default": 200},
|
||||||
|
})
|
||||||
|
out = self.mod.web_health_expectations(apps, group_names=["app-l6"])
|
||||||
|
self.assertEqual(out["api.l6.example.org"], [301, 403])
|
||||||
|
|
||||||
|
def test_invalid_default_list_falls_back_to_DEFAULT_OK(self):
|
||||||
|
apps = {"app-l7": {}}
|
||||||
|
# everything invalid → fall back to DEFAULT_OK
|
||||||
|
self._configure_returns({
|
||||||
|
("app-l7", "server.domains.canonical"): ["l7.example.org"],
|
||||||
|
("app-l7", "server.domains.aliases"): [],
|
||||||
|
("app-l7", "server.status_codes"): {"default": ["x", 42.42, {}, 700, 99]},
|
||||||
|
})
|
||||||
|
out = self.mod.web_health_expectations(apps, group_names=["app-l7"])
|
||||||
|
self.assertEqual(out["l7.example.org"], [200, 302, 301])
|
||||||
|
|
||||||
|
def test_key_with_invalid_list_uses_default_list(self):
|
||||||
|
apps = {"app-l8": {}}
|
||||||
|
self._configure_returns({
|
||||||
|
("app-l8", "server.domains.canonical"): {"web": "web.l8.example.org"},
|
||||||
|
("app-l8", "server.domains.aliases"): [],
|
||||||
|
("app-l8", "server.status_codes"): {"web": ["foo", None], "default": [204, 206]},
|
||||||
|
})
|
||||||
|
out = self.mod.web_health_expectations(apps, group_names=["app-l8"])
|
||||||
|
self.assertEqual(out["web.l8.example.org"], [204, 206])
|
||||||
|
|
||||||
|
def test_key_and_default_both_invalid_falls_back_to_DEFAULT_OK(self):
|
||||||
|
apps = {"app-l9": {}}
|
||||||
|
self._configure_returns({
|
||||||
|
("app-l9", "server.domains.canonical"): {"api": "api.l9.example.org"},
|
||||||
|
("app-l9", "server.domains.aliases"): [],
|
||||||
|
("app-l9", "server.status_codes"): {"api": ["bad"], "default": ["also", "bad"]},
|
||||||
|
})
|
||||||
|
out = self.mod.web_health_expectations(apps, group_names=["app-l9"])
|
||||||
|
self.assertEqual(out["api.l9.example.org"], [200, 302, 301])
|
||||||
|
|
||||||
|
def test_aliases_still_forced_to_301_even_with_default_list(self):
|
||||||
|
apps = {"app-l10": {}}
|
||||||
|
self._configure_returns({
|
||||||
|
("app-l10", "server.domains.canonical"): ["l10.example.org"],
|
||||||
|
("app-l10", "server.domains.aliases"): ["alias.l10.example.org"],
|
||||||
|
("app-l10", "server.status_codes"): {"default": [204, 206]},
|
||||||
|
})
|
||||||
|
out = self.mod.web_health_expectations(apps, group_names=["app-l10"])
|
||||||
|
self.assertEqual(out["l10.example.org"], [204, 206])
|
||||||
|
self.assertEqual(out["alias.l10.example.org"], [301])
|
||||||
|
|
||||||
|
def test_keyed_canonical_with_mixed_scalar_and_list_domains(self):
|
||||||
|
apps = {"app-l11": {}}
|
||||||
|
self._configure_returns({
|
||||||
|
("app-l11", "server.domains.canonical"): {
|
||||||
|
"api": "api.l11.example.org",
|
||||||
|
"view": ["v1.l11.example.org", "v2.l11.example.org"],
|
||||||
|
},
|
||||||
|
("app-l11", "server.domains.aliases"): [],
|
||||||
|
("app-l11", "server.status_codes"): {"view": [301, 307], "default": [200, 204]},
|
||||||
|
})
|
||||||
|
out = self.mod.web_health_expectations(apps, group_names=["app-l11"])
|
||||||
|
self.assertEqual(out["api.l11.example.org"], [200, 204]) # default
|
||||||
|
self.assertEqual(out["v1.l11.example.org"], [301, 307]) # per-key list
|
||||||
|
self.assertEqual(out["v2.l11.example.org"], [301, 307])
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
Reference in New Issue
Block a user