"""
app.extensions.talisman
=======================
"""
from __future__ import annotations
import json
import typing as t
from pathlib import Path
from flask import Flask
from flask_talisman import (
DEFAULT_CSP_POLICY,
DEFAULT_DOCUMENT_POLICY,
DEFAULT_FEATURE_POLICY,
DEFAULT_PERMISSIONS_POLICY,
SAMEORIGIN,
)
from flask_talisman import Talisman as _Talisman
from flask_talisman.talisman import (
DEFAULT_REFERRER_POLICY,
DEFAULT_SESSION_COOKIE_SAMESITE,
ONE_YEAR_IN_SECS,
)
[docs]
class ContentSecurityPolicy(t.Dict[str, t.Union[str, t.List[str]]]):
"""Object for setting default CSP and config override.
:param schemas: Path to schemas directory.
"""
def __init__(self, schemas: Path) -> None:
super().__init__(
json.loads((schemas / "csp.json").read_text(encoding="utf-8"))
)
def _format_value(
self, key: str, theirs: str | list[str]
) -> str | list[str]:
# start with a list, to combine "ours" and "theirs"
value = []
ours = self.get(key)
# loop through both to determine the types of each
for item in theirs, ours:
# if one of the items is a list concatenate it to the list
# we are constructing
if isinstance(item, list):
value += item
# if one of the items is a str append it to the list we are
# constructing
elif isinstance(item, str):
value.append(item)
# remove duplicates by converting the list to a set and then
# back to a list again
# for consistency and testing ensure the list is in
# alphabetical / numerical order
value = sorted(list(set(value)))
# if there is only one item left after removing duplicates then
# it can be the only item returned
if len(value) == 1:
return value[0]
return value
[docs]
def update_policy(self, update: ContentSecurityPolicy) -> None:
"""Combine a configured policy without overriding the existing.
If the result is a single item, add a ``str``.
If the result is a ``list``, remove duplicates and sort.
If either is a ``str`` and the other is a ``list``, add the
``str`` and the ``list`` contents to the new ``list``.
If both are ``str`` values add to the new ``list``.
:param update: A ``str`` or a ``list`` of ``str`` objects.
"""
for key, value in update.items():
self[key] = self._format_value(key, value)
[docs]
class Talisman(_Talisman): # pylint: disable=too-few-public-methods
"""Subclass ``flask_talisman.Talisman``.
With this the ``Talisman.init_app`` can be tweaked so that
everything remains encapsulated in the application factory, with no
need to add additional functions to the application..
"""
# pylint: disable=dangerous-default-value,too-many-arguments
# pylint: disable=too-many-locals,too-many-positional-arguments
[docs]
def init_app(
self,
app: Flask,
feature_policy: str | dict[str, str] = DEFAULT_FEATURE_POLICY,
permissions_policy: str | dict[str, str] = DEFAULT_PERMISSIONS_POLICY,
document_policy: str | dict[str, str] = DEFAULT_DOCUMENT_POLICY,
force_https: bool = True,
force_https_permanent: bool = False,
force_file_save: bool = False,
frame_options: str = SAMEORIGIN,
frame_options_allow_from: str | None = None,
strict_transport_security: bool = True,
strict_transport_security_preload: bool = False,
strict_transport_security_max_age: int = ONE_YEAR_IN_SECS,
strict_transport_security_include_subdomains: bool = True,
content_security_policy: str | dict[str, str] = DEFAULT_CSP_POLICY,
content_security_policy_report_uri: str | None = None,
content_security_policy_report_only: bool = False,
content_security_policy_nonce_in: list[str] | None = None,
referrer_policy: str = DEFAULT_REFERRER_POLICY,
session_cookie_secure: bool = True,
session_cookie_http_only: bool = True,
session_cookie_samesite: str = DEFAULT_SESSION_COOKIE_SAMESITE,
x_content_type_options: bool = True,
x_xss_protection: bool = True,
) -> None:
"""Set up this instance for use with app."""
csp = ContentSecurityPolicy(app.config["SCHEMAS"])
csp.update_policy(app.config["CSP"])
super().init_app(
app,
feature_policy=feature_policy,
permissions_policy=permissions_policy,
document_policy=document_policy,
force_https=force_https,
force_https_permanent=force_https_permanent,
force_file_save=force_file_save,
frame_options=frame_options,
frame_options_allow_from=frame_options_allow_from,
strict_transport_security=strict_transport_security,
strict_transport_security_preload=(
strict_transport_security_preload
),
strict_transport_security_max_age=(
strict_transport_security_max_age
),
strict_transport_security_include_subdomains=(
strict_transport_security_include_subdomains
),
content_security_policy=csp,
content_security_policy_report_uri="/report/csp_violations",
content_security_policy_report_only=app.config["CSP_REPORT_ONLY"],
content_security_policy_nonce_in=["style-src-attr"],
referrer_policy=referrer_policy,
session_cookie_secure=session_cookie_secure,
session_cookie_http_only=session_cookie_http_only,
session_cookie_samesite=session_cookie_samesite,
x_content_type_options=x_content_type_options,
x_xss_protection=x_xss_protection,
)