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

First version entra #1288

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
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 .env
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ HOST=http://172.17.0.1:8080
USE_DECOS_MOCK_DATA=False
SESSION_COOKIE_AGE=25200
AXES_ENABLED=False
BRP_CLIENT_ID = client_id
BRP_CLIENT_SECRET = client_secret
3 changes: 3 additions & 0 deletions .local.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Set these in your .local.env file
BRP_CLIENT_ID=
BRP_CLIENT_SECRET=
4 changes: 4 additions & 0 deletions app/apps/addresses/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ class ResidentsSerializer(serializers.Serializer):
_embedded = serializers.DictField()


class GetResidentsSerializer(serializers.Serializer):
obo_access_token = serializers.DictField()


class MeldingenSerializer(serializers.Serializer):
pageNumber = serializers.IntegerField()
pageSize = serializers.IntegerField()
Expand Down
25 changes: 11 additions & 14 deletions app/apps/addresses/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from apps.addresses.serializers import (
AddressSerializer,
DistrictSerializer,
GetResidentsSerializer,
HousingCorporationSerializer,
MeldingenSerializer,
RegistrationDetailsSerializer,
Expand Down Expand Up @@ -47,7 +48,7 @@ class AddressViewSet(
serializer_class = AddressSerializer
queryset = Address.objects.all()
lookup_field = "bag_id"
http_method_names = ["get", "patch"]
http_method_names = ["get", "patch", "post"]

def update(self, request, bag_id, *args, **kwargs):
address_instance = Address.objects.get(bag_id=bag_id)
Expand All @@ -62,11 +63,12 @@ def update(self, request, bag_id, *args, **kwargs):

@action(
detail=True,
methods=["get"],
methods=["post"],
serializer_class=ResidentsSerializer,
url_path="residents",
permission_classes=[permissions.CanAccessBRP],
)
@extend_schema(request={GetResidentsSerializer})
def residents_by_bag_id(self, request, bag_id):
# Get address
try:
Expand All @@ -86,18 +88,13 @@ def residents_by_bag_id(self, request, bag_id):

# nummeraanduiding_id should have been retrieved, so get BRP data
if address.nummeraanduiding_id:
try:
brp_data, status_code = get_brp_by_nummeraanduiding_id(
request, address.nummeraanduiding_id
)
serialized_residents = ResidentsSerializer(data=brp_data)
serialized_residents.is_valid(raise_exception=True)
return Response(serialized_residents.data, status=status_code)
except Exception:
return Response(
{"error": "BRP data could not be obtained"},
status=status.HTTP_403_FORBIDDEN,
)
obo_access_token = request.data.get("obo_access_token")
brp_data, status_code = get_brp_by_nummeraanduiding_id(
request, address.nummeraanduiding_id, obo_access_token
)
serialized_residents = ResidentsSerializer(data=brp_data)
serialized_residents.is_valid(raise_exception=True)
return Response(serialized_residents.data, status=status_code)

return Response(
{"error": "no nummeraanduiding_id found"}, status=status.HTTP_404_NOT_FOUND
Expand Down
3 changes: 1 addition & 2 deletions app/apps/cases/views/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
)
from apps.schedules.models import DaySegment, Priority, Schedule, WeekSegment
from apps.users.auth_apps import TopKeyAuth
from apps.users.permissions import CanAccessSensitiveCases
from apps.users.permissions import CanAccessSensitiveCases, IsInAuthorizedRealm
from apps.workflow.models import CaseUserTask, CaseWorkflow, WorkflowOption
from apps.workflow.serializers import (
CaseWorkflowSerializer,
Expand All @@ -56,7 +56,6 @@
from django_filters import rest_framework as filters
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from keycloak_oidc.drf.permissions import IsInAuthorizedRealm
from rest_framework import mixins, serializers, status, viewsets
from rest_framework.decorators import action, parser_classes
from rest_framework.pagination import LimitOffsetPagination
Expand Down
8 changes: 7 additions & 1 deletion app/apps/users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ class UserAdmin(UserAdmin):
"is_staff",
"is_superuser",
"groups",
"user_permissions",
)
},
),
Expand All @@ -47,3 +46,10 @@ class UserAdmin(UserAdmin):
list_display = ("id", "full_name", "email", "is_staff", "last_login", "date_joined")
search_fields = ("email",)
ordering = ("email",)
readonly_fields = (
"first_name",
"last_name",
"last_login",
"date_joined",
"username",
)
60 changes: 59 additions & 1 deletion app/apps/users/auth.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,71 @@
import logging
import time

from django.conf import settings
from django.core.exceptions import PermissionDenied
from drf_spectacular.contrib.rest_framework_simplejwt import SimpleJWTScheme
from keycloak_oidc.auth import OIDCAuthenticationBackend
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
from mozilla_django_oidc.contrib.drf import OIDCAuthentication
from rest_framework_simplejwt.authentication import JWTAuthentication

from .auth_dev import DevelopmentAuthenticationBackend


class OIDCAuthenticationBackend(OIDCAuthenticationBackend):
def save_user(self, user, claims):
user.first_name = claims.get("given_name", "")
user.last_name = claims.get("family_name", "")
user.save()
return user

def create_user(self, claims):
user = super(OIDCAuthenticationBackend, self).create_user(claims)
user = self.save_user(user, claims)
return user

def update_user(self, user, claims):
user = self.save_user(user, claims)
return user

def validate_issuer(self, payload):
issuer = self.get_settings("OIDC_OP_ISSUER")
if not issuer == payload["iss"]:
raise PermissionDenied(
'"iss": %r does not match configured value for OIDC_OP_ISSUER: %r'
% (payload["iss"], issuer)
)

def validate_audience(self, payload):
trusted_audiences = self.get_settings("OIDC_TRUSTED_AUDIENCES", [])
trusted_audiences = set(trusted_audiences)
audience = payload["aud"]
audience = set(audience)
distrusted_audiences = audience.difference(trusted_audiences)
if distrusted_audiences:
raise PermissionDenied(
'"aud" contains distrusted audiences: %r' % distrusted_audiences
)

def validate_expiry(self, payload):
expire_time = payload["exp"]
now = time.time()
if now > expire_time:
raise PermissionDenied(
"Access-token is expired %r > %r" % (now, expire_time)
)

def validate_access_token(self, payload):
self.validate_issuer(payload)
self.validate_audience(payload)
self.validate_expiry(payload)
return payload

def get_userinfo(self, access_token, id_token=None, payload=None):
userinfo = self.verify_token(access_token)
self.validate_access_token(userinfo)
return userinfo


LOGGER = logging.getLogger(__name__)

if settings.LOCAL_DEVELOPMENT_AUTHENTICATION:
Expand Down
11 changes: 0 additions & 11 deletions app/apps/users/auth_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -33,14 +32,4 @@ def authenticate(self, request):
user.first_name = DEFAULT_FIRST_NAME
user.last_name = DEFAULT_LAST_NAME
user.save()

realm_access_groups = settings.OIDC_AUTHORIZED_GROUPS
assert (
realm_access_groups
), "OIDC_AUTHORIZED_GROUPS access groups must be configured"

for realm_access_group in realm_access_groups:
group, _ = Group.objects.get_or_create(name=realm_access_group)
group.user_set.add(user)

return user
14 changes: 13 additions & 1 deletion app/apps/users/permissions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
from apps.cases.models import Case
from apps.users.auth_apps import TonKeyAuth, TopKeyAuth
from keycloak_oidc.drf.permissions import IsInAuthorizedRealm
from rest_framework.permissions import BasePermission, IsAuthenticated


class InAuthGroup(BasePermission):
def has_permission(self, request, view):
return bool(request.user and request.user.is_authenticated)


class IsInAuthorizedRealm(InAuthGroup):
"""
A permission to allow access if and only if a user is logged in,
and is a member of one of the OIDC_AUTHORIZED_GROUPS groups in Keycloak
"""


custom_permissions = [
# Permissions for cases/tasks
("create_case", "Create a new Case"),
Expand Down
2 changes: 1 addition & 1 deletion app/apps/users/tests/tests_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from django.core.exceptions import SuspiciousOperation
from django.test import TestCase
from keycloak_oidc.auth import OIDCAuthenticationBackend
from mozilla_django_oidc.contrib.drf import OIDCAuthenticationBackend

from app.utils.unittest_helpers import get_test_user

Expand Down
2 changes: 1 addition & 1 deletion app/apps/users/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import logging

from apps.users.permissions import IsInAuthorizedRealm
from django.contrib.auth.models import Permission
from django.http import HttpResponseBadRequest
from drf_spectacular.utils import extend_schema
from keycloak_oidc.drf.permissions import IsInAuthorizedRealm
from rest_framework import generics, serializers, status
from rest_framework.decorators import action
from rest_framework.response import Response
Expand Down
3 changes: 1 addition & 2 deletions app/apps/workflow/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from apps.main.pagination import EmptyPagination
from apps.summons.serializers import SummonTypeSerializer
from apps.users.auth_apps import TopKeyAuth
from apps.users.permissions import CanAccessSensitiveCases
from apps.users.permissions import CanAccessSensitiveCases, IsInAuthorizedRealm
from apps.workflow.serializers import (
CaseUserTaskSerializer,
CaseUserTaskTaskNameSerializer,
Expand All @@ -24,7 +24,6 @@
from django_filters import rest_framework as filters
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from keycloak_oidc.drf.permissions import IsInAuthorizedRealm
from rest_framework import mixins, serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework.pagination import LimitOffsetPagination
Expand Down
42 changes: 18 additions & 24 deletions app/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from celery.schedules import crontab
from dotenv import load_dotenv
from keycloak_oidc.default_settings import * # noqa
from opencensus.ext.azure.trace_exporter import AzureExporter

from .azure_settings import Azure
Expand All @@ -14,7 +13,6 @@

load_dotenv()

# config_integration.trace_integrations(["requests", "logging"])

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")
Expand Down Expand Up @@ -48,7 +46,6 @@
"django.contrib.postgres",
"corsheaders",
# Third party apps
"keycloak_oidc",
"rest_framework",
"rest_framework.authtoken",
"drf_spectacular",
Expand Down Expand Up @@ -162,9 +159,7 @@
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
),
"DEFAULT_PERMISSION_CLASSES": (
"keycloak_oidc.drf.permissions.IsInAuthorizedRealm",
),
"DEFAULT_PERMISSION_CLASSES": ("apps.users.permissions.IsInAuthorizedRealm",),
"DEFAULT_AUTHENTICATION_CLASSES": (
"apps.users.auth.AuthenticationClass",
"rest_framework.authentication.TokenAuthentication",
Expand Down Expand Up @@ -219,7 +214,7 @@
"level": LOGGING_LEVEL,
"propagate": True,
},
"mozilla_django_oidc": {"handlers": ["console"], "level": "INFO"},
"mozilla_django_oidc": {"handlers": ["console"], "level": LOGGING_LEVEL},
},
}

Expand Down Expand Up @@ -274,41 +269,38 @@ def filter_traces(envelope):
OIDC_AUTHORIZED_GROUPS
OIDC_OP_USER_ENDPOINT
"""
OIDC_RP_CLIENT_ID = os.environ.get("OIDC_RP_CLIENT_ID", None)
OIDC_RP_CLIENT_SECRET = os.environ.get("OIDC_RP_CLIENT_SECRET", None)
OIDC_USE_NONCE = False
OIDC_AUTHORIZED_GROUPS = (
"wonen_zaaksysteem",
"wonen_zaak",
"enable_persistent_token",
)
OIDC_AUTHENTICATION_CALLBACK_URL = "oidc-authenticate"

OIDC_RP_CLIENT_ID = os.environ.get(
"OIDC_RP_CLIENT_ID", "14c4257b-bcd1-4850-889e-7156c9efe2ec"
)
OIDC_OP_AUTHORIZATION_ENDPOINT = os.getenv(
"OIDC_OP_AUTHORIZATION_ENDPOINT",
"https://acc.iam.amsterdam.nl/auth/realms/datapunt-ad-acc/protocol/openid-connect/auth",
"https://login.microsoftonline.com/72fca1b1-2c2e-4376-a445-294d80196804/oauth2/v2.0/authorize",
)
OIDC_OP_TOKEN_ENDPOINT = os.getenv(
"OIDC_OP_TOKEN_ENDPOINT",
"https://acc.iam.amsterdam.nl/auth/realms/datapunt-ad-acc/protocol/openid-connect/token",
"https://login.microsoftonline.com/72fca1b1-2c2e-4376-a445-294d80196804/oauth2/v2.0/token",
)
OIDC_OP_USER_ENDPOINT = os.getenv(
"OIDC_OP_USER_ENDPOINT",
"https://acc.iam.amsterdam.nl/auth/realms/datapunt-ad-acc/protocol/openid-connect/userinfo",
"OIDC_OP_USER_ENDPOINT", "https://graph.microsoft.com/oidc/userinfo"
)
OIDC_OP_JWKS_ENDPOINT = os.getenv(
"OIDC_OP_JWKS_ENDPOINT",
"https://acc.iam.amsterdam.nl/auth/realms/datapunt-ad-acc/protocol/openid-connect/certs",
"https://login.microsoftonline.com/72fca1b1-2c2e-4376-a445-294d80196804/discovery/v2.0/keys",
)
OIDC_OP_LOGOUT_ENDPOINT = os.getenv(
"OIDC_OP_LOGOUT_ENDPOINT",
"https://acc.iam.amsterdam.nl/auth/realms/datapunt-ad-acc/protocol/openid-connect/logout",
OIDC_RP_SIGN_ALGO = "RS256"
OIDC_OP_ISSUER = os.getenv(
"OIDC_OP_ISSUER",
"https://sts.windows.net/72fca1b1-2c2e-4376-a445-294d80196804/",
)

OIDC_TRUSTED_AUDIENCES = f"api://{OIDC_RP_CLIENT_ID}"

LOCAL_DEVELOPMENT_AUTHENTICATION = (
os.getenv("LOCAL_DEVELOPMENT_AUTHENTICATION", False) == "True"
)

DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880
DATA_UPLOAD_MAX_NUMBER_FIELDS = 6000

Expand Down Expand Up @@ -362,11 +354,13 @@ def filter_traces(envelope):

BRP_API_URL = "/".join(
[
os.getenv("BRP_API_URL", "https://acc.bp.data.amsterdam.nl/brp"),
os.getenv("BRP_API_URL", "https://acc.bp.data.amsterdam.nl/entra/brp"),
"ingeschrevenpersonen",
]
)

BRP_CLIENT_ID = os.getenv("BRP_CLIENT_ID", "BRP_CLIENT_ID")
BRP_CLIENT_SECRET = os.getenv("BRP_CLIENT_SECRET", "BRP_CLIENT_SECRET")
# Secret keys which can be used to access certain parts of the API
SECRET_KEY_TOP_ZAKEN = os.getenv("SECRET_KEY_TOP_ZAKEN", None)
SECRET_KEY_TON_ZAKEN = os.getenv("SECRET_KEY_TON_ZAKEN", None)
Expand Down
1 change: 0 additions & 1 deletion app/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ click-didyoumean==0.3.1
click-plugins==1.1.1
click-repl==0.2.0
cryptography==43.0.3
datapunt-keycloak-oidc @ git+https://github.com/remyvdwereld/keycloak_oidc_top.git@main
debugpy==1.4.1
Django==4.2.16
django-axes==6.5.0
Expand Down
Loading
Loading