Skip to content

Commit 9a2adb5

Browse files
Merge pull request #9 from CivicDataLab/authorisation
Authorisation
2 parents 03a6124 + 413458b commit 9a2adb5

File tree

113 files changed

+10267
-286
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

113 files changed

+10267
-286
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,5 @@ resources/
153153

154154
#local env file
155155
.env
156-
api/migrations/*
156+
api/migrations/*
157+
authorization/migrations/*

.pre-commit-config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ repos:
5555
- psycopg2-binary
5656
- python-dotenv
5757
- uvicorn
58+
- djangorestframework-simplejwt
59+
- python-keycloak
60+
- django-activity-stream
5861

5962
args: [--config-file=mypy.ini]
6063
exclude: ^tests/

DataSpace/settings.py

Lines changed: 143 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
import structlog
1818
from decouple import config
1919

20+
# Import authorization settings
21+
from authorization.keycloak_settings import *
22+
2023
from .cache_settings import *
2124

2225
env = environ.Env(DEBUG=(bool, False))
2326
DEBUG = env.bool("DEBUG", default=True)
24-
25-
2627
# Build paths inside the project like this: BASE_DIR / 'subdir'.
2728
BASE_DIR = Path(__file__).resolve().parent.parent
2829
environ.Env.read_env(os.path.join(BASE_DIR, ".env"))
@@ -43,10 +44,10 @@
4344

4445
# Data indexing database
4546
DATA_DB_NAME = env("DATA_DB_NAME", default=str(BASE_DIR / "data.sqlite3"))
46-
DATA_DB_USER = env("DB_USER", default=DB_USER)
47-
DATA_DB_PASSWORD = env("DB_PASSWORD", default=DB_PASSWORD)
48-
DATA_DB_HOST = env("DB_HOST", default=DB_HOST)
49-
DATA_DB_PORT = env("DB_PORT", default=DB_PORT)
47+
DATA_DB_USER = env("DATA_DB_USER", default=DB_USER)
48+
DATA_DB_PASSWORD = env("DATA_DB_PASSWORD", default=DB_PASSWORD)
49+
DATA_DB_HOST = env("DATA_DB_HOST", default=DB_HOST)
50+
DATA_DB_PORT = env("DATA_DB_PORT", default=DB_PORT)
5051

5152
# SECURITY WARNING: don't run with debug turned on in production!
5253

@@ -64,42 +65,55 @@
6465

6566
CSRF_TRUSTED_ORIGINS = whitelisted_urls
6667

67-
# CORS settings
68-
if DEBUG:
69-
# In development, allow all origins
70-
CORS_ORIGIN_ALLOW_ALL = True
71-
else:
72-
# In production, only allow whitelisted origins
73-
CORS_ORIGIN_ALLOW_ALL = False
74-
CORS_ALLOWED_ORIGINS = whitelisted_urls
68+
# Explicitly disable automatic URL normalization to prevent redirects
69+
APPEND_SLASH = False
7570

76-
# Common CORS settings
71+
# Disable trailing slash redirects for GraphQL
72+
STRICT_URL_HANDLING = True
73+
74+
# Disable HTTPS redirects - this is critical
75+
SECURE_SSL_REDIRECT = False
76+
SECURITY_MIDDLEWARE_REDIRECT_HTTPS = False
77+
SECURE_PROXY_SSL_HEADER = None
78+
# Maximally permissive CORS settings to fix issues
79+
CORS_ORIGIN_ALLOW_ALL = True
7780
CORS_ALLOW_CREDENTIALS = True
78-
CORS_ALLOW_METHODS = [
79-
"DELETE",
80-
"GET",
81-
"OPTIONS",
82-
"PATCH",
83-
"POST",
84-
"PUT",
85-
]
81+
CORS_ALLOW_METHODS = ["*"]
82+
CORS_ALLOW_HEADERS = ["*"]
83+
CORS_EXPOSE_HEADERS = ["*"]
8684

87-
CORS_ALLOW_HEADERS = [
88-
"accept",
89-
"accept-encoding",
90-
"authorization",
91-
"content-type",
92-
"dnt",
93-
"origin",
94-
"user-agent",
95-
"x-csrftoken",
96-
"x-requested-with",
97-
"referer",
98-
"organization",
99-
"dataspace",
100-
"token",
101-
]
85+
# Apply CORS to all URLs including redirects
86+
CORS_URLS_REGEX = r".*"
10287

88+
# CORS preflight settings
89+
CORS_PREFLIGHT_MAX_AGE = 86400
90+
# Common CORS settings
91+
CORS_ALLOW_CREDENTIALS = True
92+
# CORS_ALLOW_METHODS = [
93+
# "DELETE",
94+
# "GET",
95+
# "OPTIONS",
96+
# "PATCH",
97+
# "POST",
98+
# "PUT",
99+
# ]
100+
101+
# CORS_ALLOW_HEADERS = [
102+
# "accept",
103+
# "accept-encoding",
104+
# "authorization",
105+
# "content-type",
106+
# "dnt",
107+
# "origin",
108+
# "user-agent",
109+
# "x-csrftoken",
110+
# "x-requested-with",
111+
# "referer",
112+
# "organization",
113+
# "dataspace",
114+
# "token",
115+
# "x-keycloak-token", # Add Keycloak token header
116+
# ]
103117
# Application definition
104118

105119
INSTALLED_APPS = [
@@ -109,35 +123,39 @@
109123
"django.contrib.sessions",
110124
"django.contrib.messages",
111125
"django.contrib.staticfiles",
112-
"corsheaders", # django-cors-headers package
126+
"corsheaders",
127+
"authorization.apps.AuthorizationConfig",
113128
"api.apps.ApiConfig",
114129
"strawberry_django",
115130
"rest_framework",
131+
"rest_framework_simplejwt",
116132
"django_elasticsearch_dsl",
117133
"django_elasticsearch_dsl_drf",
134+
"actstream",
118135
]
119136

120137
MIDDLEWARE = [
138+
"corsheaders.middleware.CorsMiddleware", # CORS middleware must be first
121139
"django.middleware.security.SecurityMiddleware",
122140
"django.contrib.sessions.middleware.SessionMiddleware",
123-
"corsheaders.middleware.CorsMiddleware",
124141
"django.middleware.common.CommonMiddleware",
125142
"django.middleware.csrf.CsrfViewMiddleware",
126143
"django.contrib.auth.middleware.AuthenticationMiddleware",
127144
"django.contrib.messages.middleware.MessageMiddleware",
128145
"django.middleware.clickjacking.XFrameOptionsMiddleware",
129146
"api.utils.middleware.ContextMiddleware",
147+
"api.middleware.request_validator.RequestValidationMiddleware",
148+
"api.middleware.logging.StructuredLoggingMiddleware",
130149
]
131150

132-
# Add debug toolbar middleware first if in debug mode
151+
# Add debug toolbar middleware if in debug mode
133152
if DEBUG:
134-
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
153+
MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware")
135154

136-
# Add our custom middleware
137155
MIDDLEWARE += [
138156
"api.middleware.rate_limit.rate_limit_middleware",
139-
"api.middleware.request_validator.RequestValidationMiddleware",
140-
"api.middleware.logging.StructuredLoggingMiddleware",
157+
"authorization.middleware.KeycloakAuthenticationMiddleware",
158+
"authorization.middleware.activity_consent.ActivityConsentMiddleware",
141159
]
142160

143161
ROOT_URLCONF = "DataSpace.urls"
@@ -164,6 +182,7 @@
164182
"FIELD_DESCRIPTION_FROM_HELP_TEXT": True,
165183
"TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING": True,
166184
"GENERATE_ENUMS_FROM_CHOICES": True,
185+
"DEFAULT_PERMISSION_CLASSES": ["authorization.graphql_permissions.AllowAny"],
167186
}
168187

169188
# Database
@@ -220,7 +239,7 @@
220239
# Static files (CSS, JavaScript, Images)
221240
# https://docs.djangoproject.com/en/4.0/howto/static-files/
222241

223-
STATIC_URL = "static/"
242+
# This STATIC_URL setting is overridden below
224243
MEDIA_URL = "public/"
225244
MEDIA_ROOT = os.path.join(BASE_DIR, "files", "public")
226245

@@ -229,6 +248,15 @@
229248

230249
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
231250
DJANGO_ALLOW_ASYNC_UNSAFE = True
251+
252+
# Custom User model
253+
AUTH_USER_MODEL = "authorization.User"
254+
255+
# Authentication backends
256+
AUTHENTICATION_BACKENDS = [
257+
"authorization.backends.KeycloakAuthenticationBackend",
258+
"django.contrib.auth.backends.ModelBackend",
259+
]
232260
ELASTICSEARCH_DSL = {
233261
"default": {
234262
"hosts": f"http://{os.getenv('ELASTICSEARCH_USERNAME', 'elastic')}:{os.getenv('ELASTICSEARCH_PASSWORD', 'changeme')}@elasticsearch:9200",
@@ -252,9 +280,11 @@
252280
# Django REST Framework settings
253281
REST_FRAMEWORK = {
254282
"DEFAULT_PERMISSION_CLASSES": [
255-
"rest_framework.permissions.AllowAny", # Allow unauthenticated access by default
283+
"rest_framework.permissions.AllowAny", # Allow public access by default
256284
],
257285
"DEFAULT_AUTHENTICATION_CLASSES": [
286+
"authorization.authentication.KeycloakAuthentication",
287+
"rest_framework_simplejwt.authentication.JWTAuthentication",
258288
"rest_framework.authentication.SessionAuthentication",
259289
"rest_framework.authentication.BasicAuthentication",
260290
],
@@ -381,13 +411,77 @@
381411

382412
# Security settings
383413
SECURE_BROWSER_XSS_FILTER = True
384-
SECURE_CONTENT_TYPE_NOSNIFF = True
414+
# Disable content type sniffing to fix MIME type issues
415+
SECURE_CONTENT_TYPE_NOSNIFF = False
385416
X_FRAME_OPTIONS = "DENY"
386417
SECURE_HSTS_SECONDS = 31536000
387418
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
388419
SECURE_HSTS_PRELOAD = True
389420

390421
if not DEBUG:
391-
SECURE_SSL_REDIRECT = True
422+
# SECURE_SSL_REDIRECT = True
392423
SESSION_COOKIE_SECURE = True
393424
CSRF_COOKIE_SECURE = True
425+
426+
427+
# Static files configuration
428+
# Make sure this is an absolute path
429+
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
430+
431+
# Make sure the URL starts and ends with a slash
432+
STATIC_URL = "/static/"
433+
434+
# Additional locations of static files - where Django will look for static files
435+
STATICFILES_DIRS = [
436+
os.path.join(BASE_DIR, "static"),
437+
]
438+
439+
# Make sure Django can find admin static files
440+
STATICFILES_FINDERS = [
441+
"django.contrib.staticfiles.finders.FileSystemFinder",
442+
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
443+
]
444+
445+
# Always use WhiteNoise for static files in both development and production
446+
# Insert WhiteNoise middleware after security middleware
447+
MIDDLEWARE.insert(
448+
MIDDLEWARE.index("django.middleware.security.SecurityMiddleware") + 1,
449+
"whitenoise.middleware.WhiteNoiseMiddleware",
450+
)
451+
452+
# Use the simplest WhiteNoise storage configuration to avoid MIME type issues
453+
STATICFILES_STORAGE = "whitenoise.storage.StaticFilesStorage"
454+
455+
# Disable compression to avoid MIME type issues
456+
WHITENOISE_ENABLE_COMPRESSION = False
457+
458+
# Don't add content-type headers (let the browser determine them)
459+
WHITENOISE_ADD_HEADERS = False
460+
461+
# Don't use the manifest feature which can cause issues with file references
462+
WHITENOISE_USE_FINDERS = True
463+
464+
# Don't use the root directory feature which can cause conflicts
465+
# WHITENOISE_ROOT = os.path.join(BASE_DIR, "static")
466+
467+
# Django Activity Stream settings
468+
ACTSTREAM_SETTINGS = {
469+
"MANAGER": "actstream.managers.ActionManager",
470+
"FETCH_RELATIONS": True,
471+
"USE_PREFETCH": True,
472+
"USE_JSONFIELD": True,
473+
"GFK_FETCH_DEPTH": 1,
474+
}
475+
476+
# Activity Stream Consent Settings
477+
ACTIVITY_CONSENT = {
478+
# If True, user consent is required for activity tracking
479+
# If False, consent is assumed and all activities are tracked
480+
"REQUIRE_CONSENT": env.bool("ACTIVITY_REQUIRE_CONSENT", default=True),
481+
# Default consent setting for new users
482+
"DEFAULT_CONSENT": env.bool("ACTIVITY_DEFAULT_CONSENT", default=False),
483+
# If True, anonymous activities are tracked (when consent is not required)
484+
"TRACK_ANONYMOUS": env.bool("ACTIVITY_TRACK_ANONYMOUS", default=False),
485+
# Maximum age of activities to keep (in days, 0 means keep forever)
486+
"MAX_AGE_DAYS": env.int("ACTIVITY_MAX_AGE_DAYS", default=0),
487+
}

DataSpace/urls.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444

4545
urlpatterns: URLPatternsList = [
4646
path("api/", include("api.urls")),
47+
path("auth/", include("authorization.urls")),
4748
path("admin/", admin.site.urls),
4849
# Health check endpoint
4950
path("health/", health.health_check, name="health_check"),
@@ -59,10 +60,20 @@
5960
path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"),
6061
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
6162

63+
# In debug mode, add static URL patterns and debug toolbar
6264
if settings.DEBUG:
65+
# Add static URL patterns for development
66+
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
67+
68+
# Add debug toolbar
6369
import debug_toolbar # type: ignore[import]
6470

6571
debug_patterns: URLPatternsList = [
6672
path("__debug__/", include(debug_toolbar.urls)),
6773
]
6874
urlpatterns = debug_patterns + cast(URLPatternsList, urlpatterns)
75+
76+
# In debug mode, explicitly serve admin static files
77+
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
78+
79+
urlpatterns += staticfiles_urlpatterns()

0 commit comments

Comments
 (0)