Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Csp anayltics #260

Merged
merged 4 commits into from
Jun 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/open_inwoner/conf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@
"open_inwoner.openzaak",
"open_inwoner.questionnaire",
"open_inwoner.extended_sessions",
"open_inwoner.custom_csp",
]

MIDDLEWARE = [
Expand All @@ -174,6 +175,7 @@
"django.contrib.auth.middleware.AuthenticationMiddleware",
"csp.contrib.rate_limiting.RateLimitedCSPMiddleware",
"csp.middleware.CSPMiddleware",
"open_inwoner.custom_csp.middleware.UpdateCSPMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"axes.middleware.AxesMiddleware",
Expand Down
34 changes: 34 additions & 0 deletions src/open_inwoner/conf/fixtures/custom_csp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[
{
"model": "custom_csp.cspsetting",
"pk": 1,
"fields": {
"directive": "script-src",
"value": "https://www.googletagmanager.com"
}
},
{
"model": "custom_csp.cspsetting",
"pk": 2,
"fields": {
"directive": "script-src",
"value": "http://open-inwoner.matomo.cloud/"
}
},
{
"model": "custom_csp.cspsetting",
"pk": 3,
"fields": {
"directive": "connect-src",
"value": "http://open-inwoner.matomo.cloud/"
}
},
{
"model": "custom_csp.cspsetting",
"pk": 4,
"fields": {
"directive": "connect-src",
"value": "https://www.google-analytics.com"
}
}
]
Empty file.
22 changes: 22 additions & 0 deletions src/open_inwoner/custom_csp/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.contrib import admin

from .models import CSPSetting


@admin.register(CSPSetting)
class CSPSettingAdmin(admin.ModelAdmin):
fields = [
"directive",
"value",
]
list_display = [
"directive",
"value",
]
list_filter = [
"directive",
]
search_fields = [
"directive",
"value",
]
55 changes: 55 additions & 0 deletions src/open_inwoner/custom_csp/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from djchoices import ChoiceItem, DjangoChoices


class CSPDirective(DjangoChoices):
# via https://django-csp.readthedocs.io/en/latest/configuration.html
DEFAULT_SRC = ChoiceItem("default-src", label="default-src")
SCRIPT_SRC = ChoiceItem("script-src", label="script-src")
SCRIPT_SRC_ATTR = ChoiceItem("script-src-attr", label="script-src-attr")
SCRIPT_SRC_ELEM = ChoiceItem("script-src-elem", label="script-src-elem")
IMG_SRC = ChoiceItem("img-src", label="img-src")
OBJECT_SRC = ChoiceItem("object-src", label="object-src")
PREFETCH_SRC = ChoiceItem("prefetch-src", label="prefetch-src")
MEDIA_SRC = ChoiceItem("media-src", label="media-src")
FRAME_SRC = ChoiceItem("frame-src", label="frame-src")
FONT_SRC = ChoiceItem("font-src", label="font-src")
CONNECT_SRC = ChoiceItem("connect-src", label="connect-src")
STYLE_SRC = ChoiceItem("style-src", label="style-src")
STYLE_SRC_ATTR = ChoiceItem("style-src-attr", label="style-src-attr")
STYLE_SRC_ELEM = ChoiceItem("style-src-elem", label="style-src-elem")
BASE_URI = ChoiceItem(
"base-uri", label="base-uri"
) # Note: This doesn’t use default-src as a fall-back.
CHILD_SRC = ChoiceItem(
"child-src", label="child-src"
) # Note: Deprecated in CSP v3. Use frame-src and worker-src instead.
FRAME_ANCESTORS = ChoiceItem(
"frame-ancestors", label="frame-ancestors"
) # Note: This doesn’t use default-src as a fall-back.
NAVIGATE_TO = ChoiceItem(
"navigate-to", label="navigate-to"
) # Note: This doesn’t use default-src as a fall-back.
FORM_ACTION = ChoiceItem(
"form-action", label="form-action"
) # Note: This doesn’t use default-src as a fall-back.
SANDBOX = ChoiceItem(
"sandbox", label="sandbox"
) # Note: This doesn’t use default-src as a fall-back.
REPORT_URI = ChoiceItem(
"report-uri", label="report-uri"
) # Each URI can be a full or relative URI. None Note: This doesn’t use default-src as a fall-back.
REPORT_TO = ChoiceItem(
"report-to", label="report-to"
) # A string describing a reporting group. None Note: This doesn’t use default-src as a fall-back. See Section 1.2: https://w3c.github.io/reporting/#group
MANIFEST_SRC = ChoiceItem("manifest-src", label="manifest-src")
WORKER_SRC = ChoiceItem("worker-src", label="worker-src")
PLUGIN_TYPES = ChoiceItem(
"plugin-types", label="plugin-types"
) # Note: This doesn’t use default-src as a fall-back.
REQUIRE_SRI_FOR = ChoiceItem(
"require-sri-for", label="require-sri-for"
) # Valid values: script, style, or both. See: require-sri-for-known-tokens Note: This doesn’t use default-src as a fall-back.

# CSP_UPGRADE_INSECURE_REQUESTS # Include upgrade-insecure-requests directive. A boolean. False See: upgrade-insecure-requests
# CSP_BLOCK_ALL_MIXED_CONTENT # Include block-all-mixed-content directive. A boolean. False See: block-all-mixed-content
# CSP_INCLUDE_NONCE_IN # Include dynamically generated nonce in all listed directives, e.g. CSP_INCLUDE_NONCE_IN=['script-src'] will add 'nonce-<b64-value>' to the script-src directive.
67 changes: 67 additions & 0 deletions src/open_inwoner/custom_csp/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from typing import Dict, List

from .models import CSPSetting


class UpdateCSPMiddleware:
"""
adds database driven CSP directives by simulating the django-csp '@csp_update' decorator
https://django-csp.readthedocs.io/en/latest/decorators.html#csp-update
"""

def __init__(self, get_response):
self.get_response = get_response

def get_csp_update(self) -> Dict[str, List[str]]:
update = dict()

# known standard stuff from commonn settings
# _append_dict_list_values(
# update, GlobalConfiguration.get_solo().get_csp_updates()
# )
# dynamic admin driven
_append_dict_list_values(update, CSPSetting.objects.as_dict())

# TODO more contributions can be collected here
return update

def __call__(self, request):
response = self.get_response(request)

update = self.get_csp_update()
if not update:
return response

# we're basically copying/extending the @csp_update decorator
update = dict((k.lower().replace("_", "-"), v) for k, v in update.items())

# cooperate with possible data from actual decorator
have = getattr(response, "_csp_update", None)
if have:
_append_dict_list_values(have, update)
else:
response._csp_update = update

return response


def _merge_list_values(left, right) -> List[str]:
# combine strings or lists to a list with unique values
if isinstance(left, str):
left = [left]
if isinstance(right, str):
right = [right]
return list(set(*left, *right))


def _append_dict_list_values(target, source):
for k, v in source.items():
# normalise the directives
k = k.lower().replace("_", "-")
if k in target:
target[k] = _merge_list_values(target[k], v)
else:
if isinstance(v, str):
target[k] = [v]
else:
target[k] = list(set(v))
74 changes: 74 additions & 0 deletions src/open_inwoner/custom_csp/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Generated by Django 3.2.13 on 2022-06-08 14:34

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="CSPSetting",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"directive",
models.CharField(
choices=[
("default-src", "default-src"),
("script-src", "script-src"),
("script-src-attr", "script-src-attr"),
("script-src-elem", "script-src-elem"),
("img-src", "img-src"),
("object-src", "object-src"),
("prefetch-src", "prefetch-src"),
("media-src", "media-src"),
("frame-src", "frame-src"),
("font-src", "font-src"),
("connect-src", "connect-src"),
("style-src", "style-src"),
("style-src-attr", "style-src-attr"),
("style-src-elem", "style-src-elem"),
("base-uri", "base-uri"),
("child-src", "child-src"),
("frame-ancestors", "frame-ancestors"),
("navigate-to", "navigate-to"),
("form-action", "form-action"),
("sandbox", "sandbox"),
("report-uri", "report-uri"),
("report-to", "report-to"),
("manifest-src", "manifest-src"),
("worker-src", "worker-src"),
("plugin-types", "plugin-types"),
("require-sri-for", "require-sri-for"),
],
help_text="CSP header directive",
max_length=64,
verbose_name="directive",
),
),
(
"value",
models.CharField(
help_text="CSP header value",
max_length=128,
verbose_name="value",
),
),
],
options={
"ordering": ("directive", "value"),
},
),
]
Empty file.
36 changes: 36 additions & 0 deletions src/open_inwoner/custom_csp/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from collections import defaultdict

from django.db import models
from django.utils.translation import gettext_lazy as _

from .constants import CSPDirective


class CSPSettingQuerySet(models.QuerySet):
def as_dict(self):
ret = defaultdict(set)
for directive, value in self.values_list("directive", "value"):
ret[directive].add(value)
return {k: list(v) for k, v in ret.items()}


class CSPSetting(models.Model):
directive = models.CharField(
_("directive"),
max_length=64,
help_text=_("CSP header directive"),
choices=CSPDirective.choices,
)
value = models.CharField(
_("value"),
max_length=128,
help_text=_("CSP header value"),
)

objects = CSPSettingQuerySet.as_manager()

class Meta:
ordering = ("directive", "value")

def __str__(self):
return f"{self.directive} '{self.value}'"
2 changes: 1 addition & 1 deletion src/open_inwoner/js/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import './confirmation'
import './datepicker'
import './dropdown'
import './dropdown'
import './emoji-button'
// import './emoji-button'
import './header'
import './map'
import './message-file'
Expand Down