Skip to content

Commit c58490b

Browse files
feat: django-rest-framework + Person/Email API (ietf-tools#8256)
* feat: django-rest-framework + Person/Email API (ietf-tools#8233) * chore: djangorestframework -> requirements.txt * chore: auth/perm/schema classes for drf * chore: settings for drf and friends * chore: comment that api/serializer.py is not DRF * feat: URL router for DRF * feat: simple api/v3/person/{id} endpoint * fix: actually working demo endpoint * chore: no auth for PersonViewSet * ci: params in ci-run-tests.yml * Revert "ci: params in ci-run-tests.yml" This reverts commit 03808dd. * feat: email addresses for person API * feat: email update api (WIP) * fix: working Email API endpoint * chore: annotate address format in api schema * chore: api adjustments * feat: expose SpectacularAPIView At least for now... * chore: better schema_path_prefix * feat: permissions for DRF API * refactor: use permissions classes * refactor: extract NewEmailForm validation for reuse * refactor: ietfauth.validators module * refactor: send new email conf req via helper * feat: API call to issue new address request * chore: move datatracker DRF api to /api/core/ * fix: unused import * fix: lint * test: drf URL names + API tests (ietf-tools#8248) * refactor: better drf URL naming * test: test person-detail view * test: permissions * test: add_email tests + stubs * test: test email update * test: test 404 vs 403 * fix: fix permissions * test: test email partial update * test: assert we have a nonexistent PK * chore: disable DRF api for now * chore: fix git inanity * fix: lint * test: disable tests of disabled code * test: more lint
1 parent c18900a commit c58490b

16 files changed

Lines changed: 650 additions & 53 deletions

ietf/api/apps.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@ def ready(self):
1212
interact with the database. See
1313
https://docs.djangoproject.com/en/4.2/ref/applications/#django.apps.AppConfig.ready
1414
"""
15+
# Populate our API list now that the app registry is set up
1516
populate_api_list()
17+
18+
# Import drf-spectacular extensions
19+
import ietf.api.schema # pyflakes: ignore

ietf/api/authentication.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright The IETF Trust 2024, All Rights Reserved
2+
#
3+
from rest_framework import authentication
4+
from django.contrib.auth.models import AnonymousUser
5+
6+
7+
class ApiKeyAuthentication(authentication.BaseAuthentication):
8+
"""API-Key header authentication"""
9+
10+
def authenticate(self, request):
11+
"""Extract the authentication token, if present
12+
13+
This does not validate the token, it just arranges for it to be available in request.auth.
14+
It's up to a Permissions class to validate it for the appropriate endpoint.
15+
"""
16+
token = request.META.get("HTTP_X_API_KEY", None)
17+
if token is None:
18+
return None
19+
return AnonymousUser(), token # available as request.user and request.auth

ietf/api/permissions.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright The IETF Trust 2024, All Rights Reserved
2+
#
3+
from rest_framework import permissions
4+
from ietf.api.ietf_utils import is_valid_token
5+
6+
7+
class HasApiKey(permissions.BasePermission):
8+
"""Permissions class that validates a token using is_valid_token
9+
10+
The view class must indicate the relevant endpoint by setting `api_key_endpoint`.
11+
Must be used with an Authentication class that puts a token in request.auth.
12+
"""
13+
def has_permission(self, request, view):
14+
endpoint = getattr(view, "api_key_endpoint", None)
15+
auth_token = getattr(request, "auth", None)
16+
if endpoint is not None and auth_token is not None:
17+
return is_valid_token(endpoint, auth_token)
18+
return False
19+
20+
21+
class IsOwnPerson(permissions.BasePermission):
22+
"""Permission to access own Person object"""
23+
def has_object_permission(self, request, view, obj):
24+
if not (request.user.is_authenticated and hasattr(request.user, "person")):
25+
return False
26+
return obj == request.user.person
27+
28+
29+
class BelongsToOwnPerson(permissions.BasePermission):
30+
"""Permission to access objects associated with own Person
31+
32+
Requires that the object have a "person" field that indicates ownership.
33+
"""
34+
def has_object_permission(self, request, view, obj):
35+
if not (request.user.is_authenticated and hasattr(request.user, "person")):
36+
return False
37+
return (
38+
hasattr(obj, "person") and obj.person == request.user.person
39+
)

ietf/api/routers.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright The IETF Trust 2024, All Rights Reserved
2+
"""Custom django-rest-framework routers"""
3+
from django.core.exceptions import ImproperlyConfigured
4+
from rest_framework import routers
5+
6+
class PrefixedSimpleRouter(routers.SimpleRouter):
7+
"""SimpleRouter that adds a dot-separated prefix to its basename"""
8+
def __init__(self, name_prefix="", *args, **kwargs):
9+
self.name_prefix = name_prefix
10+
if len(self.name_prefix) == 0 or self.name_prefix[-1] == ".":
11+
raise ImproperlyConfigured("Cannot use a name_prefix that is empty or ends with '.'")
12+
super().__init__(*args, **kwargs)
13+
14+
def get_default_basename(self, viewset):
15+
basename = super().get_default_basename(viewset)
16+
return f"{self.name_prefix}.{basename}"

ietf/api/schema.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright The IETF Trust 2024, All Rights Reserved
2+
#
3+
from drf_spectacular.extensions import OpenApiAuthenticationExtension
4+
5+
6+
class ApiKeyAuthenticationScheme(OpenApiAuthenticationExtension):
7+
"""Authentication scheme extension for the ApiKeyAuthentication
8+
9+
Used by drf-spectacular when rendering the OpenAPI schema
10+
"""
11+
target_class = "ietf.api.authentication.ApiKeyAuthentication"
12+
name = "apiKeyAuth"
13+
14+
def get_security_definition(self, auto_schema):
15+
return {
16+
"type": "apiKey",
17+
"description": "Shared secret in the X-Api-Key header",
18+
"name": "X-Api-Key",
19+
"in": "header",
20+
}

ietf/api/serializer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
# Copyright The IETF Trust 2018-2020, All Rights Reserved
1+
# Copyright The IETF Trust 2018-2024, All Rights Reserved
22
# -*- coding: utf-8 -*-
3+
"""Serialization utilities
34
5+
This is _not_ for django-rest-framework!
6+
"""
47

58
import hashlib
69
import json

0 commit comments

Comments
 (0)