diff --git a/docs/env.md b/docs/env.md index 851e043b..ac6e18e2 100644 --- a/docs/env.md +++ b/docs/env.md @@ -6,104 +6,104 @@ Here we describe all environment variables that can be set for the docs applicat These are the environment variables you can set for the `impress-backend` container. -| Option | Description | default | -|------------------------------------------------- |----------------------------------------------------------------------------------------------------------------------------- |-------------------------------------------------------------------------| -| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated | -| AI_API_KEY | AI key to be used for AI Base url | | -| AI_BASE_URL | OpenAI compatible AI base url | | -| AI_FEATURE_ENABLED | Enable AI options | false | -| AI_MODEL | AI Model to use | | -| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true | -| API_USERS_LIST_LIMIT | Limit on API users | 5 | -| API_USERS_LIST_THROTTLE_RATE_BURST | Throttle rate for api on burst | 30/minute | -| API_USERS_LIST_THROTTLE_RATE_SUSTAINED | Throttle rate for api | 180/hour | -| AWS_S3_ACCESS_KEY_ID | Access id for s3 endpoint | | -| AWS_S3_ENDPOINT_URL | S3 endpoint | | -| AWS_S3_REGION_NAME | Region name for s3 endpoint | | -| AWS_S3_SECRET_ACCESS_KEY | Access key for s3 endpoint | | -| AWS_STORAGE_BUCKET_NAME | Bucket name for s3 endpoint | impress-media-storage | -| CACHES_DEFAULT_TIMEOUT | Cache default timeout | 30 | -| CACHES_KEY_PREFIX | The prefix used to every cache keys. | docs | -| COLLABORATION_API_URL | Collaboration api host | | -| COLLABORATION_SERVER_SECRET | Collaboration api secret | | -| COLLABORATION_WS_NOT_CONNECTED_READY_ONLY | Users not connected to the collaboration server cannot edit | false | -| COLLABORATION_WS_URL | Collaboration websocket url | | -| CONVERSION_API_CONTENT_FIELD | Conversion api content field | content | -| CONVERSION_API_ENDPOINT | Conversion API endpoint | convert-markdown | -| CONVERSION_API_SECURE | Require secure conversion api | false | -| CONVERSION_API_TIMEOUT | Conversion api timeout | 30 | -| CRISP_WEBSITE_ID | Crisp website id for support | | -| DB_ENGINE | Engine to use for database connections | django.db.backends.postgresql_psycopg2 | -| DB_HOST | Host of the database | localhost | -| DB_NAME | Name of the database | impress | -| DB_PASSWORD | Password to authenticate with | pass | -| DB_PORT | Port of the database | 5432 | -| DB_USER | User to authenticate with | dinum | -| DJANGO_ALLOWED_HOSTS | Allowed hosts | [] | -| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker transport options | {} | -| DJANGO_CELERY_BROKER_URL | Celery broker url | redis://redis:6379/0 | -| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false | -| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | List of origins allowed for CORS using regulair expressions | [] | -| DJANGO_CORS_ALLOWED_ORIGINS | List of origins allowed for CORS | [] | -| DJANGO_CSRF_TRUSTED_ORIGINS | CSRF trusted origins | [] | -| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend | -| DJANGO_EMAIL_BRAND_NAME | Brand name for email | | -| DJANGO_EMAIL_FROM | Email address used as sender | from@example.com | -| DJANGO_EMAIL_HOST | Hostname of email | | -| DJANGO_EMAIL_HOST_PASSWORD | Password to authenticate with on the email host | | -| DJANGO_EMAIL_HOST_USER | User to authenticate with on the email host | | -| DJANGO_EMAIL_LOGO_IMG | Logo for the email | | -| DJANGO_EMAIL_PORT | Port used to connect to email host | | -| DJANGO_EMAIL_USE_SSL | Use ssl for email host connection | false | -| DJANGO_EMAIL_USE_TLS | Use tls for email host connection | false | -| DJANGO_SECRET_KEY | Secret key | | -| DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] | -| DOCUMENT_ATTACHMENT_CHECK_UNSAFE_MIME_TYPES_ENABLED | Check mime type extension to determine if a file is unsafe or not. Can be disable if an antivirus is used in the MALWARE_DETECTION_BACKEND | True | -| DOCUMENT_IMAGE_MAX_SIZE | Maximum size of document in bytes | 10485760 | -| FRONTEND_CSS_URL | To add a external css file to the app | | -| FRONTEND_HOMEPAGE_FEATURE_ENABLED | Frontend feature flag to display the homepage | false | -| FRONTEND_THEME | Frontend theme to use | | -| LANGUAGE_CODE | Default language | en-us | -| LOGGING_LEVEL_LOGGERS_APP | Application logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO | -| LOGGING_LEVEL_LOGGERS_ROOT | Default logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO | -| LOGIN_REDIRECT_URL | Login redirect url | | -| LOGIN_REDIRECT_URL_FAILURE | Login redirect url on failure | | -| LOGOUT_REDIRECT_URL | Logout redirect url | | -| MALWARE_DETECTION_BACKEND | The malware detection backend use from the django-lasuite package | lasuite.malware_detection.backends.dummy.DummyBackend | -| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} | -| MEDIA_BASE_URL | | | -| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false | -| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} | -| OIDC_CREATE_USER | Create used on OIDC | false | -| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | true | -| OIDC_OP_AUTHORIZATION_ENDPOINT | Authorization endpoint for OIDC | | -| OIDC_OP_JWKS_ENDPOINT | JWKS endpoint for OIDC | | -| OIDC_OP_LOGOUT_ENDPOINT | Logout endpoint for OIDC | | -| OIDC_OP_TOKEN_ENDPOINT | Token endpoint for OIDC | | -| OIDC_OP_USER_ENDPOINT | User endpoint for OIDC | | -| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed hosts for OIDC redirect url | [] | -| OIDC_REDIRECT_REQUIRE_HTTPS | Require https for OIDC redirect url | false | -| OIDC_RP_CLIENT_ID | Client id used for OIDC | impress | -| OIDC_RP_CLIENT_SECRET | Client secret used for OIDC | | -| OIDC_RP_SCOPES | Scopes requested for OIDC | openid email | -| OIDC_RP_SIGN_ALGO | verification algorithm used OIDC tokens | RS256 | -| OIDC_STORE_ID_TOKEN | Store OIDC token | true | -| OIDC_USE_NONCE | Use nonce for OIDC | true | -| OIDC_USERINFO_FULLNAME_FIELDS | OIDC token claims to create full name | ["first_name", "last_name"] | -| OIDC_USERINFO_SHORTNAME_FIELD | OIDC token claims to create shortname | first_name | -| POSTHOG_KEY | Posthog key for analytics | | -| REDIS_URL | Cache url | redis://redis:6379/1 | -| SENTRY_DSN | Sentry host | | -| SESSION_COOKIE_AGE | duration of the cookie session | 60*60*12 | -| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | | false | -| STORAGES_STATICFILES_BACKEND | | whitenoise.storage.CompressedManifestStaticFilesStorage | -| THEME_CUSTOMIZATION_CACHE_TIMEOUT | Cache duration for the customization settings | 86400 | -| THEME_CUSTOMIZATION_FILE_PATH | Full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json | -| TRASHBIN_CUTOFF_DAYS | Trashbin cutoff | 30 | -| USER_OIDC_ESSENTIAL_CLAIMS | Essential claims in OIDC token | [] | -| Y_PROVIDER_API_BASE_URL | Y Provider url | | -| Y_PROVIDER_API_KEY | Y provider API key | | - +| Option | Description | default | +| ----------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- | +| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated | +| AI_API_KEY | AI key to be used for AI Base url | | +| AI_BASE_URL | OpenAI compatible AI base url | | +| AI_FEATURE_ENABLED | Enable AI options | false | +| AI_MODEL | AI Model to use | | +| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true | +| API_USERS_LIST_LIMIT | Limit on API users | 5 | +| API_USERS_LIST_THROTTLE_RATE_BURST | throttle rate for api on burst | 30/minute | +| API_USERS_LIST_THROTTLE_RATE_SUSTAINED | throttle rate for api | 180/hour | +| AWS_S3_ACCESS_KEY_ID | access id for s3 endpoint | | +| AWS_S3_ENDPOINT_URL | S3 endpoint | | +| AWS_S3_REGION_NAME | region name for s3 endpoint | | +| AWS_S3_SECRET_ACCESS_KEY | access key for s3 endpoint | | +| AWS_STORAGE_BUCKET_NAME | bucket name for s3 endpoint | impress-media-storage | +| CACHES_DEFAULT_TIMEOUT | cache default timeout | 30 | +| CACHES_KEY_PREFIX | The prefix used to every cache keys. | docs | +| COLLABORATION_API_URL | collaboration api host | | +| COLLABORATION_SERVER_SECRET | collaboration api secret | | +| COLLABORATION_WS_NOT_CONNECTED_READY_ONLY | Users not connected to the collaboration server cannot edit | false | +| COLLABORATION_WS_URL | collaboration websocket url | | +| CONVERSION_API_CONTENT_FIELD | Conversion api content field | content | +| CONVERSION_API_ENDPOINT | Conversion API endpoint | convert-markdown | +| CONVERSION_API_SECURE | Require secure conversion api | false | +| CONVERSION_API_TIMEOUT | Conversion api timeout | 30 | +| CONTENT_SECURITY_POLICY_DIRECTIVES | A dict of directives set in the Content-Security-Policy header | All directives are set to 'none' | +| CONTENT_SECURITY_POLICY_EXCLUDE_URL_PREFIXES | Url with this prefix will not have the header Content-Security-Policy included | | +| CRISP_WEBSITE_ID | crisp website id for support | | +| DB_ENGINE | engine to use for database connections | django.db.backends.postgresql_psycopg2 | +| DB_HOST | host of the database | localhost | +| DB_NAME | name of the database | impress | +| DB_PASSWORD | password to authenticate with | pass | +| DB_PORT | port of the database | 5432 | +| DB_USER | user to authenticate with | dinum | +| DJANGO_ALLOWED_HOSTS | allowed hosts | [] | +| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | celery broker transport options | {} | +| DJANGO_CELERY_BROKER_URL | celery broker url | redis://redis:6379/0 | +| DJANGO_CORS_ALLOW_ALL_ORIGINS | allow all CORS origins | false | +| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | list of origins allowed for CORS using regulair expressions | [] | +| DJANGO_CORS_ALLOWED_ORIGINS | list of origins allowed for CORS | [] | +| DJANGO_CSRF_TRUSTED_ORIGINS | CSRF trusted origins | [] | +| DJANGO_EMAIL_BACKEND | email backend library | django.core.mail.backends.smtp.EmailBackend | +| DJANGO_EMAIL_BRAND_NAME | brand name for email | | +| DJANGO_EMAIL_FROM | email address used as sender | from@example.com | +| DJANGO_EMAIL_HOST | host name of email | | +| DJANGO_EMAIL_HOST_PASSWORD | password to authenticate with on the email host | | +| DJANGO_EMAIL_HOST_USER | user to authenticate with on the email host | | +| DJANGO_EMAIL_LOGO_IMG | logo for the email | | +| DJANGO_EMAIL_PORT | port used to connect to email host | | +| DJANGO_EMAIL_USE_SSL | use sstl for email host connection | false | +| DJANGO_EMAIL_USE_TLS | use tls for email host connection | false | +| DJANGO_SECRET_KEY | secret key | | +| DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] | +| DOCUMENT_IMAGE_MAX_SIZE | maximum size of document in bytes | 10485760 | +| FRONTEND_CSS_URL | To add a external css file to the app | | +| FRONTEND_HOMEPAGE_FEATURE_ENABLED | frontend feature flag to display the homepage | false | +| FRONTEND_THEME | frontend theme to use | | +| LANGUAGE_CODE | default language | en-us | +| LOGGING_LEVEL_LOGGERS_APP | application logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO | +| LOGGING_LEVEL_LOGGERS_ROOT | default logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO | +| LOGIN_REDIRECT_URL | login redirect url | | +| LOGIN_REDIRECT_URL_FAILURE | login redirect url on failure | | +| LOGOUT_REDIRECT_URL | logout redirect url | | +| MALWARE_DETECTION_BACKEND | The malware detection backend use from the django-lasuite package | lasuite.malware_detection.backends.dummy.DummyBackend | +| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} | +| MEDIA_BASE_URL | | | +| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false | +| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} | +| OIDC_CREATE_USER | create used on OIDC | false | +| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | faillback to email for identification | true | +| OIDC_OP_AUTHORIZATION_ENDPOINT | Authorization endpoint for OIDC | | +| OIDC_OP_JWKS_ENDPOINT | JWKS endpoint for OIDC | | +| OIDC_OP_LOGOUT_ENDPOINT | Logout endpoint for OIDC | | +| OIDC_OP_TOKEN_ENDPOINT | Token endpoint for OIDC | | +| OIDC_OP_USER_ENDPOINT | User endpoint for OIDC | | +| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed hosts for OIDC redirect url | [] | +| OIDC_REDIRECT_REQUIRE_HTTPS | Require https for OIDC redirect url | false | +| OIDC_RP_CLIENT_ID | client id used for OIDC | impress | +| OIDC_RP_CLIENT_SECRET | client secret used for OIDC | | +| OIDC_RP_SCOPES | scopes requested for OIDC | openid email | +| OIDC_RP_SIGN_ALGO | verification algorithm used OIDC tokens | RS256 | +| OIDC_STORE_ID_TOKEN | Store OIDC token | true | +| OIDC_USE_NONCE | use nonce for OIDC | true | +| OIDC_USERINFO_FULLNAME_FIELDS | OIDC token claims to create full name | ["first_name", "last_name"] | +| OIDC_USERINFO_SHORTNAME_FIELD | OIDC token claims to create shortname | first_name | +| POSTHOG_KEY | posthog key for analytics | | +| REDIS_URL | cache url | redis://redis:6379/1 | +| SENTRY_DSN | sentry host | | +| SESSION_COOKIE_AGE | duration of the cookie session | 60*60*12 | +| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | | false | +| STORAGES_STATICFILES_BACKEND | | whitenoise.storage.CompressedManifestStaticFilesStorage | +| THEME_CUSTOMIZATION_CACHE_TIMEOUT | Cache duration for the customization settings | 86400 | +| THEME_CUSTOMIZATION_FILE_PATH | full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json | +| TRASHBIN_CUTOFF_DAYS | trashbin cutoff | 30 | +| USER_OIDC_ESSENTIAL_CLAIMS | essential claims in OIDC token | [] | +| Y_PROVIDER_API_BASE_URL | Y Provider url | | +| Y_PROVIDER_API_KEY | Y provider API key | | ## impress-frontend image diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 2250c91a..83904387 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -25,6 +25,8 @@ from django.utils.translation import gettext_lazy as _ import requests import rest_framework as drf from botocore.exceptions import ClientError +from csp.constants import NONE +from csp.decorators import csp_update from lasuite.malware_detection import malware_detection from rest_framework import filters, status, viewsets from rest_framework import response as drf_response @@ -1412,6 +1414,7 @@ class DocumentViewSet( name="", url_path="cors-proxy", ) + @csp_update({"img-src": [NONE, "data:"]}) def cors_proxy(self, request, *args, **kwargs): """ GET /api/v1.0/documents//cors-proxy @@ -1452,7 +1455,6 @@ class DocumentViewSet( content_type=content_type, headers={ "Content-Disposition": "attachment;", - "Content-Security-Policy": "default-src 'none'; img-src 'none' data:;", }, status=response.status_code, ) diff --git a/src/backend/core/tests/documents/test_api_documents_cors_proxy.py b/src/backend/core/tests/documents/test_api_documents_cors_proxy.py index 8f5d4219..5d8b02c2 100644 --- a/src/backend/core/tests/documents/test_api_documents_cors_proxy.py +++ b/src/backend/core/tests/documents/test_api_documents_cors_proxy.py @@ -23,10 +23,25 @@ def test_api_docs_cors_proxy_valid_url(): assert response.status_code == 200 assert response.headers["Content-Type"] == "image/png" assert response.headers["Content-Disposition"] == "attachment;" - assert ( - response.headers["Content-Security-Policy"] - == "default-src 'none'; img-src 'none' data:;" - ) + policy_list = sorted(response.headers["Content-Security-Policy"].split("; ")) + assert policy_list == [ + "base-uri 'none'", + "child-src 'none'", + "connect-src 'none'", + "default-src 'none'", + "font-src 'none'", + "form-action 'none'", + "frame-ancestors 'none'", + "frame-src 'none'", + "img-src 'none' data:", + "manifest-src 'none'", + "media-src 'none'", + "object-src 'none'", + "prefetch-src 'none'", + "script-src 'none'", + "style-src 'none'", + "worker-src 'none'", + ] assert response.streaming_content @@ -77,10 +92,25 @@ def test_api_docs_cors_proxy_authenticated_user_accessing_protected_doc(): assert response.status_code == 200 assert response.headers["Content-Type"] == "image/png" assert response.headers["Content-Disposition"] == "attachment;" - assert ( - response.headers["Content-Security-Policy"] - == "default-src 'none'; img-src 'none' data:;" - ) + policy_list = sorted(response.headers["Content-Security-Policy"].split("; ")) + assert policy_list == [ + "base-uri 'none'", + "child-src 'none'", + "connect-src 'none'", + "default-src 'none'", + "font-src 'none'", + "form-action 'none'", + "frame-ancestors 'none'", + "frame-src 'none'", + "img-src 'none' data:", + "manifest-src 'none'", + "media-src 'none'", + "object-src 'none'", + "prefetch-src 'none'", + "script-src 'none'", + "style-src 'none'", + "worker-src 'none'", + ] assert response.streaming_content diff --git a/src/backend/core/tests/test_api_config.py b/src/backend/core/tests/test_api_config.py index 2d74594c..cdc4a9cb 100644 --- a/src/backend/core/tests/test_api_config.py +++ b/src/backend/core/tests/test_api_config.py @@ -62,6 +62,25 @@ def test_api_config(is_authenticated): "AI_FEATURE_ENABLED": False, "theme_customization": {}, } + policy_list = sorted(response.headers["Content-Security-Policy"].split("; ")) + assert policy_list == [ + "base-uri 'none'", + "child-src 'none'", + "connect-src 'none'", + "default-src 'none'", + "font-src 'none'", + "form-action 'none'", + "frame-ancestors 'none'", + "frame-src 'none'", + "img-src 'none'", + "manifest-src 'none'", + "media-src 'none'", + "object-src 'none'", + "prefetch-src 'none'", + "script-src 'none'", + "style-src 'none'", + "worker-src 'none'", + ] @override_settings( diff --git a/src/backend/impress/settings.py b/src/backend/impress/settings.py index 2ce66a92..6e007fbc 100755 --- a/src/backend/impress/settings.py +++ b/src/backend/impress/settings.py @@ -18,9 +18,12 @@ from django.utils.translation import gettext_lazy as _ import sentry_sdk from configurations import Configuration, values +from csp.constants import NONE from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.logging import ignore_logger +# pylint: disable=too-many-lines + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DATA_DIR = os.getenv("DATA_DIR", os.path.join("/", "data")) @@ -289,6 +292,7 @@ class Base(Configuration): "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "dockerflow.django.middleware.DockerflowMiddleware", + "csp.middleware.CSPMiddleware", ] AUTHENTICATION_BACKENDS = [ @@ -322,6 +326,7 @@ class Base(Configuration): # OIDC third party "mozilla_django_oidc", "lasuite.malware_detection", + "csp", ] # Cache @@ -721,6 +726,38 @@ class Base(Configuration): environ_prefix=None, ) + # Content Security Policy + # See https://content-security-policy.com/ for more information. + CONTENT_SECURITY_POLICY = { + "EXCLUDE_URL_PREFIXES": values.ListValue( + [], + environ_name="CONTENT_SECURITY_POLICY_EXCLUDE_URL_PREFIXES", + environ_prefix=None, + ), + "DIRECTIVES": values.DictValue( + default={ + "default-src": [NONE], + "script-src": [NONE], + "style-src": [NONE], + "img-src": [NONE], + "connect-src": [NONE], + "font-src": [NONE], + "object-src": [NONE], + "media-src": [NONE], + "frame-src": [NONE], + "child-src": [NONE], + "form-action": [NONE], + "frame-ancestors": [NONE], + "base-uri": [NONE], + "worker-src": [NONE], + "manifest-src": [NONE], + "prefetch-src": [NONE], + }, + environ_name="CONTENT_SECURITY_POLICY_DIRECTIVES", + environ_prefix=None, + ), + } + # pylint: disable=invalid-name @property def ENVIRONMENT(self): diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index a5fd08c8..1aeb2e28 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "django-configurations==2.5.1", "django-cors-headers==4.7.0", "django-countries==7.6.1", + "django-csp==4.0", "django-filter==25.1", "django-lasuite[all]==0.0.9", "django-parler==2.3",