Skip to content

Commit

Permalink
Merge pull request #1288 from Amsterdam/feature/118181-entra
Browse files Browse the repository at this point in the history
First version entra
  • Loading branch information
NvdLaan authored Jan 29, 2025
2 parents 213a806 + 1d7098e commit 47165ec
Show file tree
Hide file tree
Showing 17 changed files with 145 additions and 71 deletions.
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

0 comments on commit 47165ec

Please sign in to comment.