Merge current master into extension gallery source permissions
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
ddb8ddd811
8 files changed
+210
-9
No files matched your search
@@ -7,6 +7,8 @@
|
||||
|
||||
- **Settings → Extensions gallery cards now link back to their source and show permissions in readable groups.** Gallery and Installed cards now surface a safe "Source" link from registry metadata (falling back to the extension registry path when needed), and the permissions disclosure renders WebUI API access, storage, DOM, sidecar/native-host, filesystem, and external-network declarations as plain rows instead of raw JSON. Thanks @franksong2702. (#5093, #5095)
|
||||
|
||||
- **Opt-in CSP `frame-src` allowlist so an extension can embed a self-hosted web app in an iframe.** A new `HERMES_WEBUI_CSP_FRAME_EXTRA` env var widens what the WebUI page may embed in an `<iframe>` (default is same-origin only), mirroring the existing `HERMES_WEBUI_CSP_CONNECT_EXTRA` knob — space-separated `http(s)` origins with optional wildcard subdomain and port, validated and ignored if malformed. This unblocks an "external app tab" extension (pin Grafana / Vaultwarden / your own dashboard as a tab). It only governs what *this* page can embed; `frame-ancestors` stays `'none'`, so it does not let anyone embed the WebUI itself. Default-off; with the var unset, behavior is unchanged. Documented in `docs/EXTENSIONS.md`.
|
||||
|
||||
- **`/moa` now works in the WebUI, routing the turn through your configured Mixture-of-Agents preset.** Typing `/moa <prompt>` runs that turn through the agent's MoA config (the same preset/usage you'd get from the CLI) instead of a plain single-model turn; `/moa` with no prompt shows the available usage. The MoA config is resolved **server-side** from your profile config — the browser only signals that an override is wanted, it can't supply the config — and it's applied per-turn (not persisted). It fails closed on gateway-backed sessions. Thanks @rodboev. (#5070, #5057)
|
||||
|
||||
- **Three new opt-in appearance skins: GitHub, Codex, and Terracotta.** All are CSS-only, namespaced under `[data-skin]`, and selectable from Settings → Appearance — they change nothing unless you pick them. GitHub uses a restrained graphite + Primer-blue palette; Codex a minimal editor look with a muted sage accent; Terracotta a warm clay accent on a soft neutral background. Thanks @gottipx (GitHub #4634, Codex #4636, Terracotta #4635 — renamed from the originally-proposed name to a descriptive material name).
|
||||
|
||||
+58
-5
@@ -60,6 +60,13 @@ _CSP_CONNECT_BASE = (
|
||||
_CSP_EXTRA_CONNECT_RE = _re.compile(
|
||||
r"^(?:https?|wss?)://(?:\*\.)?[A-Za-z0-9._~-]+(?::(?P<port>\d{1,5}|\*))?$"
|
||||
)
|
||||
# Validator for an opt-in frame-src allowlist entry (HERMES_WEBUI_CSP_FRAME_EXTRA).
|
||||
# Only http(s) origins (optional wildcard subdomain + optional port) are accepted —
|
||||
# the same shape as the connect-extra validator minus the ws/wss schemes, since an
|
||||
# iframe src is always http(s).
|
||||
_CSP_EXTRA_FRAME_RE = _re.compile(
|
||||
r"^https?://(?:\*\.)?[A-Za-z0-9._~-]+(?::(?P<port>\d{1,5}|\*))?$"
|
||||
)
|
||||
_CSP_HEADER_NAME = 'Content-Security-Policy'
|
||||
_CSP_SHARED_POLICY_TEMPLATE = (
|
||||
"default-src 'self' https://*.cloudflareaccess.com; "
|
||||
@@ -72,9 +79,16 @@ _CSP_SHARED_POLICY_TEMPLATE = (
|
||||
"font-src 'self' data: https://fonts.gstatic.com; "
|
||||
"media-src 'self' data: blob:; "
|
||||
"connect-src {connect_src}; "
|
||||
"frame-src {frame_src}; "
|
||||
"manifest-src 'self' https://*.cloudflareaccess.com; "
|
||||
"base-uri 'self'; form-action 'self'"
|
||||
)
|
||||
# Base frame-src: same-origin only by default (so the existing same-origin
|
||||
# dashboard/extension iframes keep working). An operator can widen it, opt-in,
|
||||
# via HERMES_WEBUI_CSP_FRAME_EXTRA — e.g. to embed a self-hosted dashboard in an
|
||||
# extension tab. This governs what THIS page may embed; it does NOT affect
|
||||
# frame-ancestors (who may embed the WebUI), which stays 'none'.
|
||||
_CSP_FRAME_BASE = "'self'"
|
||||
|
||||
|
||||
def _valid_csp_extra_connect_source(source: str) -> bool:
|
||||
@@ -101,21 +115,58 @@ def _csp_extra_connect_src() -> str:
|
||||
return " " + " ".join(sources)
|
||||
|
||||
|
||||
def _valid_csp_extra_frame_source(source: str) -> bool:
|
||||
match = _CSP_EXTRA_FRAME_RE.fullmatch(source)
|
||||
if not match:
|
||||
return False
|
||||
port = match.group("port")
|
||||
if not port or port == "*":
|
||||
return True
|
||||
try:
|
||||
return 1 <= int(port) <= 65535
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _csp_extra_frame_src() -> str:
|
||||
raw = os.getenv("HERMES_WEBUI_CSP_FRAME_EXTRA", "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
sources = raw.split()
|
||||
if not sources or any(not _valid_csp_extra_frame_source(src) for src in sources):
|
||||
logger.warning("Ignoring invalid HERMES_WEBUI_CSP_FRAME_EXTRA value")
|
||||
return ""
|
||||
return " " + " ".join(sources)
|
||||
|
||||
|
||||
def _csp_connect_src(extra_connect_src: str = "") -> str:
|
||||
return f"{_CSP_CONNECT_BASE} https://cdn.jsdelivr.net{extra_connect_src}"
|
||||
|
||||
|
||||
def _build_csp_enforced_policy(extra_connect_src: str | None = None) -> str:
|
||||
def _csp_frame_src(extra_frame_src: str = "") -> str:
|
||||
return f"{_CSP_FRAME_BASE}{extra_frame_src}"
|
||||
|
||||
|
||||
def _build_csp_enforced_policy(
|
||||
extra_connect_src: str | None = None,
|
||||
extra_frame_src: str | None = None,
|
||||
) -> str:
|
||||
if extra_connect_src is None:
|
||||
extra_connect_src = _csp_extra_connect_src()
|
||||
if extra_frame_src is None:
|
||||
extra_frame_src = _csp_extra_frame_src()
|
||||
return _CSP_SHARED_POLICY_TEMPLATE.format(
|
||||
connect_src=_csp_connect_src(extra_connect_src)
|
||||
connect_src=_csp_connect_src(extra_connect_src),
|
||||
frame_src=_csp_frame_src(extra_frame_src),
|
||||
)
|
||||
|
||||
|
||||
def _build_csp_report_only_policy(extra_connect_src: str | None = None) -> str:
|
||||
def _build_csp_report_only_policy(
|
||||
extra_connect_src: str | None = None,
|
||||
extra_frame_src: str | None = None,
|
||||
) -> str:
|
||||
return (
|
||||
_build_csp_enforced_policy(extra_connect_src)
|
||||
_build_csp_enforced_policy(extra_connect_src, extra_frame_src)
|
||||
+ "; report-uri /api/csp-report; report-to csp-endpoint"
|
||||
)
|
||||
|
||||
@@ -123,11 +174,13 @@ def _build_csp_report_only_policy(extra_connect_src: str | None = None) -> str:
|
||||
def _security_headers(handler):
|
||||
"""Add security headers to every response."""
|
||||
extra_connect_src = _csp_extra_connect_src()
|
||||
extra_frame_src = _csp_extra_frame_src()
|
||||
handler._csp_extra_connect_src = extra_connect_src
|
||||
handler._csp_extra_frame_src = extra_frame_src
|
||||
handler.send_header('X-Content-Type-Options', 'nosniff')
|
||||
handler.send_header('X-Frame-Options', 'DENY')
|
||||
handler.send_header('Referrer-Policy', 'same-origin')
|
||||
handler.send_header(_CSP_HEADER_NAME, _build_csp_enforced_policy(extra_connect_src))
|
||||
handler.send_header(_CSP_HEADER_NAME, _build_csp_enforced_policy(extra_connect_src, extra_frame_src))
|
||||
handler.send_header(
|
||||
'Permissions-Policy',
|
||||
'camera=(), microphone=(self), geolocation=(), clipboard-write=(self)'
|
||||
|
||||
@@ -251,6 +251,37 @@ paths are never returned by the status endpoint. If `health_path` is omitted,
|
||||
diagnostics use `/health`; if `health_path` is present but invalid, the sidecar is
|
||||
skipped rather than probed.
|
||||
|
||||
## Embedding an external web app in an iframe
|
||||
|
||||
By default the WebUI's Content-Security-Policy only allows it to embed
|
||||
**same-origin** content in an `<iframe>` (the `frame-src` directive falls back to
|
||||
`'self'`). An extension that wants to pin an external self-hosted web app — a
|
||||
Grafana board, Vaultwarden, a personal dashboard — as a tab therefore needs the
|
||||
operator to widen `frame-src`, opt-in, via an environment variable:
|
||||
|
||||
```bash
|
||||
# space-separated http(s) origins; optional *. subdomain wildcard and port.
|
||||
export HERMES_WEBUI_CSP_FRAME_EXTRA="https://grafana.example.com https://*.dash.example.com:8443"
|
||||
```
|
||||
|
||||
Rules and guarantees:
|
||||
|
||||
- Only `http(s)` origins are accepted (an iframe `src` is always http(s)).
|
||||
Entries may include a `*.` subdomain wildcard and a port or `*` port; a path,
|
||||
a `ws://`/`wss://` scheme, an invalid port, or any attempt to inject another
|
||||
directive is rejected and the whole value is ignored (with a logged warning).
|
||||
- This mirrors the existing `HERMES_WEBUI_CSP_CONNECT_EXTRA` knob (which widens
|
||||
`connect-src` for `fetch`/WebSocket); the two are independent.
|
||||
- It only governs what the WebUI page may **embed**. It does **not** touch
|
||||
`frame-ancestors`, which stays `'none'` — so widening `frame-src` never lets
|
||||
another site embed the WebUI itself.
|
||||
- Default-off: with the variable unset, the policy is unchanged (same-origin
|
||||
iframes only).
|
||||
|
||||
An "external app tab" extension should document the exact origin(s) it needs so
|
||||
the operator can set this knob deliberately, rather than assuming a wide-open
|
||||
policy.
|
||||
|
||||
## Static file serving
|
||||
|
||||
When `HERMES_WEBUI_EXTENSION_DIR` points at an existing directory, files under
|
||||
|
||||
@@ -292,12 +292,13 @@ class Handler(BaseHTTPRequestHandler):
|
||||
_CSP_REPORT_TO = '{"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"/api/csp-report"}]}'
|
||||
|
||||
@classmethod
|
||||
def csp_report_only_policy(cls, extra_connect_src=None) -> str:
|
||||
return _build_csp_report_only_policy(extra_connect_src)
|
||||
def csp_report_only_policy(cls, extra_connect_src=None, extra_frame_src=None) -> str:
|
||||
return _build_csp_report_only_policy(extra_connect_src, extra_frame_src)
|
||||
|
||||
def end_headers(self) -> None:
|
||||
extra_connect_src = getattr(self, "_csp_extra_connect_src", None)
|
||||
self.send_header("Content-Security-Policy-Report-Only", self.csp_report_only_policy(extra_connect_src))
|
||||
extra_frame_src = getattr(self, "_csp_extra_frame_src", None)
|
||||
self.send_header("Content-Security-Policy-Report-Only", self.csp_report_only_policy(extra_connect_src, extra_frame_src))
|
||||
self.send_header("Report-To", self._CSP_REPORT_TO)
|
||||
super().end_headers()
|
||||
|
||||
|
||||
@@ -681,6 +681,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'No extra permissions declared.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -2316,6 +2317,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Nessuna autorizzazione extra dichiarata.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -3942,6 +3944,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: '追加の権限は宣言されていません。',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -6276,6 +6279,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Дополнительные разрешения не указаны.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -7827,6 +7831,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'No se declararon permisos adicionales.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -9062,6 +9067,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Keine zusätzlichen Berechtigungen deklariert.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -10964,6 +10970,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: '未声明额外权限。',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -11821,6 +11828,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: '未宣告額外權限。',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -13343,6 +13351,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Nenhuma permissão extra declarada.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -14871,6 +14880,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: '추가 권한이 선언되지 않았습니다.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -16509,6 +16519,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Aucune permission supplémentaire déclarée.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -18103,6 +18114,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Ek izin bildirilmedi.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -19743,6 +19755,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Nie zadeklarowano dodatkowych uprawnień.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
@@ -21271,6 +21284,7 @@ const LOCALES = {
|
||||
ext_gallery_restart_required: 'Restart required',
|
||||
ext_gallery_permissions_show: 'Permissions',
|
||||
ext_gallery_permissions_hide: 'Hide permissions',
|
||||
ext_gallery_permissions_empty: 'Không khai báo quyền bổ sung nào.',
|
||||
ext_gallery_version: 'Version',
|
||||
ext_gallery_author: 'Author',
|
||||
ext_gallery_capabilities: 'Capabilities',
|
||||
|
||||
+1
-1
@@ -8616,7 +8616,7 @@ function _extensionPermissionSummary(perms){
|
||||
<span class="extension-gallery-permission-label">${esc(label)}</span>
|
||||
<span class="extension-gallery-permission-value">${esc(value)}</span>
|
||||
</div>`).join('')+'</div>'
|
||||
: '<div class="extension-gallery-permission-empty">No extra permissions declared.</div>';
|
||||
: `<div class="extension-gallery-permission-empty">${esc(t('ext_gallery_permissions_empty'))}</div>`;
|
||||
return `<details class="extension-gallery-perms">
|
||||
<summary>${esc(t('ext_gallery_permissions_show'))}</summary>
|
||||
${body}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Regression coverage for the configurable CSP frame-src allowlist knob.
|
||||
|
||||
`HERMES_WEBUI_CSP_FRAME_EXTRA` lets an operator widen what the WebUI page may
|
||||
embed in an <iframe> (e.g. a self-hosted dashboard pinned as an extension tab),
|
||||
opt-in and default-off. The base policy is same-origin only, and the knob never
|
||||
touches `frame-ancestors` (who may embed the WebUI), which stays 'none'.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def test_csp_frame_src_default_is_self_only(monkeypatch):
|
||||
from server import Handler
|
||||
|
||||
monkeypatch.delenv("HERMES_WEBUI_CSP_FRAME_EXTRA", raising=False)
|
||||
|
||||
policy = Handler.csp_report_only_policy()
|
||||
assert "frame-src 'self'; " in policy
|
||||
# frame-ancestors must remain locked down regardless.
|
||||
assert "frame-ancestors 'none'" in policy
|
||||
|
||||
|
||||
def test_csp_frame_src_includes_valid_extra_origins(monkeypatch):
|
||||
from server import Handler
|
||||
|
||||
monkeypatch.setenv(
|
||||
"HERMES_WEBUI_CSP_FRAME_EXTRA",
|
||||
"https://grafana.example.com https://*.dash.example.com:8443",
|
||||
)
|
||||
|
||||
policy = Handler.csp_report_only_policy()
|
||||
assert (
|
||||
"frame-src 'self' "
|
||||
"https://grafana.example.com https://*.dash.example.com:8443; "
|
||||
) in policy
|
||||
|
||||
|
||||
def test_csp_frame_src_extra_in_enforced_policy(monkeypatch):
|
||||
from api.helpers import _build_csp_enforced_policy
|
||||
|
||||
monkeypatch.setenv("HERMES_WEBUI_CSP_FRAME_EXTRA", "http://127.0.0.1:3000")
|
||||
enforced = _build_csp_enforced_policy()
|
||||
assert "frame-src 'self' http://127.0.0.1:3000;" in enforced
|
||||
|
||||
|
||||
def test_csp_frame_src_rejects_directive_injection(monkeypatch, caplog):
|
||||
from server import Handler
|
||||
|
||||
monkeypatch.setenv(
|
||||
"HERMES_WEBUI_CSP_FRAME_EXTRA",
|
||||
"https://ok.example.com; script-src *",
|
||||
)
|
||||
|
||||
policy = Handler.csp_report_only_policy()
|
||||
assert "https://ok.example.com" not in policy
|
||||
assert "script-src *" not in policy
|
||||
assert "frame-src 'self'; " in policy # falls back to safe default
|
||||
assert "Ignoring invalid HERMES_WEBUI_CSP_FRAME_EXTRA" in caplog.text
|
||||
|
||||
|
||||
def test_csp_frame_src_rejects_paths(monkeypatch):
|
||||
from server import Handler
|
||||
|
||||
monkeypatch.setenv("HERMES_WEBUI_CSP_FRAME_EXTRA", "https://app.example.com/embed")
|
||||
policy = Handler.csp_report_only_policy()
|
||||
assert "https://app.example.com/embed" not in policy
|
||||
|
||||
|
||||
def test_csp_frame_src_rejects_ws_scheme(monkeypatch):
|
||||
"""An iframe src is always http(s); ws/wss are not valid frame sources."""
|
||||
from server import Handler
|
||||
|
||||
monkeypatch.setenv("HERMES_WEBUI_CSP_FRAME_EXTRA", "wss://socket.example.com")
|
||||
policy = Handler.csp_report_only_policy()
|
||||
assert "wss://socket.example.com" not in policy
|
||||
assert "frame-src 'self'; " in policy
|
||||
|
||||
|
||||
def test_csp_frame_src_rejects_invalid_ports(monkeypatch):
|
||||
from server import Handler
|
||||
|
||||
monkeypatch.setenv("HERMES_WEBUI_CSP_FRAME_EXTRA", "https://app.example.com:99999")
|
||||
policy = Handler.csp_report_only_policy()
|
||||
assert "https://app.example.com:99999" not in policy
|
||||
|
||||
|
||||
def test_csp_frame_src_does_not_affect_connect_src(monkeypatch):
|
||||
"""The frame knob and the connect knob are independent."""
|
||||
from server import Handler
|
||||
|
||||
monkeypatch.setenv("HERMES_WEBUI_CSP_FRAME_EXTRA", "https://dash.example.com")
|
||||
monkeypatch.delenv("HERMES_WEBUI_CSP_CONNECT_EXTRA", raising=False)
|
||||
policy = Handler.csp_report_only_policy()
|
||||
# frame-extra present in frame-src ...
|
||||
assert "frame-src 'self' https://dash.example.com;" in policy
|
||||
# ... and NOT leaked into connect-src (which ends at cdn.jsdelivr.net).
|
||||
connect_seg = policy.split("connect-src", 1)[1].split(";", 1)[0]
|
||||
assert "dash.example.com" not in connect_seg
|
||||
@@ -259,6 +259,7 @@ def test_extensions_gallery_links_sources_and_humanizes_permissions():
|
||||
assert "extension-gallery-source-link" in helper_block
|
||||
assert "target=\"_blank\"" in helper_block
|
||||
assert "rel=\"noopener noreferrer\"" in helper_block
|
||||
assert "t('ext_gallery_permissions_empty')" in helper_block
|
||||
assert "webui_api" in helper_block
|
||||
assert "sidecar_commands" in helper_block
|
||||
assert "dom.mutates_core_views" in helper_block
|
||||
@@ -309,6 +310,7 @@ def test_extensions_i18n_keys_exist_for_all_locales():
|
||||
"settings_tab_extensions",
|
||||
"ext_gallery_next_step",
|
||||
"ext_gallery_after_install",
|
||||
"ext_gallery_permissions_empty",
|
||||
"ext_gallery_local_component_required",
|
||||
"ext_gallery_local_app_label",
|
||||
"ext_gallery_required_suffix",
|
||||
|
||||
Reference in new issue
Block a user