Skip to content

Commit

Permalink
Updating stuff:
Browse files Browse the repository at this point in the history
- Moved test stuff to _/ rather than api/
- Moved some APISIX stuff to a middleware
- Reworked logging to universally be "log" and to use "debug" (maybe not done yet tho)
- Regenerated OpenAPI stuff
  • Loading branch information
jkachel committed Mar 5, 2024
1 parent 4ff5f2f commit ef3041c
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 95 deletions.
24 changes: 12 additions & 12 deletions authentication/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def keycloak_session_init(url, **kwargs):

def update_token(token):
log_str = f"Refreshing Keycloak token {token}"
log.warning(log_str)
log.debug(log_str)
KeycloakAdminToken.objects.all().delete()
KeycloakAdminToken.objects.create(
authorization_token=token.get("access_token"),
Expand All @@ -70,7 +70,7 @@ def check_for_token():
token = check_for_token()

log_str = f"Trying to start up a session with token {token.token_formatted}"
log.warning(log_str)
log.debug(log_str)

session = OAuth2Session(
client=client,
Expand All @@ -80,10 +80,10 @@ def check_for_token():
token_updater=update_token,
)

keycloak_info = session.get(url, **kwargs).json()
keycloak_response = session.get(url, **kwargs).json()
except (InvalidGrantError, TokenExpiredError) as ige:
log_str = f"Token error, trying to get a new token: {ige}"
log.warning(log_str)
log.debug(log_str)

session = OAuth2Session(client=client)
token = session.fetch_token(
Expand All @@ -95,12 +95,12 @@ def check_for_token():

update_token(token)
session = OAuth2Session(client=client, token=token)
keycloak_info = session.get(url, **kwargs).json()
keycloak_response = session.get(url, **kwargs).json()

log_str = f"Keycloak info returned: {keycloak_info}"
log.warning(log_str)
log_str = f"Keycloak response: {keycloak_response}"
log.debug(log_str)

return keycloak_info
return keycloak_response


def keycloak_get_user(user: User):
Expand All @@ -112,7 +112,7 @@ def keycloak_get_user(user: User):
)

log_str = f"Trying to get user info for {user.username}"
log.warning(log_str)
log.debug(log_str)

if user.keycloak_user_tokens.exists():
params = {"id": user.keycloak_user_tokens.first().keycloak_id}
Expand All @@ -122,17 +122,17 @@ def keycloak_get_user(user: User):
userinfo = keycloak_session_init(userinfo_url, verify=False, params=params)

if len(userinfo) == 0:
log.warning("Keycloak didn't return anything")
log.debug("Keycloak didn't return anything")
return None

return userinfo[0]


@app.task
def keycloak_update_user_account(user: int):
def keycloak_update_user_account(user_id: int):
"""Update the user account using info from Keycloak asynchronously."""

user = User.objects.get(id=user)
user = User.objects.get(id=user_id)

keycloak_user = keycloak_get_user(user)

Expand Down
8 changes: 6 additions & 2 deletions authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ class KeycloakAdminToken(TimestampedModel):

authorization_token = models.TextField()
refresh_token = models.TextField(blank=True)
authorization_token_expires_in = models.IntegerField()
refresh_token_expires_in = models.IntegerField(null=True)
authorization_token_expires_in = models.IntegerField(
help_text="Seconds until authentication token expires"
)
refresh_token_expires_in = models.IntegerField(
null=True, help_text="Seconds until refresh token expires"
)

def calculate_token_expiration(self) -> (int, int):
"""Calculate the expiration time of the tokens."""
Expand Down
31 changes: 0 additions & 31 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,6 @@ info:
title: ''
version: 0.0.0
paths:
/apisix_test_request/:
get:
operationId: apisix_test_request_retrieve
description: Test API request so we can see how the APISIX integration works.
tags:
- apisix_test_request
responses:
'200':
description: No response body
/authed_traefik_test_request/:
get:
operationId: authed_traefik_test_request_retrieve
description: Test API request so we can see how the Traefik integration works.
tags:
- authed_traefik_test_request
security:
- cookieAuth: []
responses:
'200':
description: No response body
/integrated_system/:
get:
operationId: integrated_system_list
Expand Down Expand Up @@ -339,17 +319,6 @@ paths:
responses:
'204':
description: No response body
/traefik_test_request/:
get:
operationId: traefik_test_request_retrieve
description: Test API request so we can see how the Traefik integration works.
tags:
- traefik_test_request
security:
- cookieAuth: []
responses:
'200':
description: No response body
components:
schemas:
IntegratedSystem:
Expand Down
27 changes: 27 additions & 0 deletions system_meta/private_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Private URLs for the system_meta app."""

from django.urls import re_path

from system_meta.views import (
apisix_test_request,
authed_traefik_test_request,
traefik_test_request,
)

urlpatterns = [
re_path(
r"^apisix_test_request/$",
apisix_test_request,
name="apisix_test_request",
),
re_path(
r"^traefik_test_request/$",
traefik_test_request,
name="traefik_test_request",
),
re_path(
r"^authed_traefik_test_request/$",
authed_traefik_test_request,
name="authed_traefik_test_request",
),
]
18 changes: 0 additions & 18 deletions system_meta/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
from system_meta.views import (
IntegratedSystemViewSet,
ProductViewSet,
apisix_test_request,
authed_traefik_test_request,
traefik_test_request,
)

router = routers.DefaultRouter()
Expand All @@ -18,19 +15,4 @@

urlpatterns = [
re_path("^", include(router.urls)),
re_path(
r"^apisix_test_request/$",
apisix_test_request,
name="apisix_test_request",
),
re_path(
r"^traefik_test_request/$",
traefik_test_request,
name="traefik_test_request",
),
re_path(
r"^authed_traefik_test_request/$",
authed_traefik_test_request,
name="authed_traefik_test_request",
),
]
58 changes: 27 additions & 31 deletions unified_ecommerce/authentication.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
"""Custom authentication for DRF"""

import base64
import json
import logging
import random
import string

import jwt
from django.contrib.auth import get_user_model
from django.contrib.auth.middleware import RemoteUserMiddleware
from rest_framework.authentication import BaseAuthentication
from rest_framework_jwt.authentication import JSONWebTokenAuthentication

Expand All @@ -17,7 +14,7 @@
HEADER_PREFIX = "Token "
HEADER_PREFIX_LENGTH = len(HEADER_PREFIX)

logger = logging.getLogger()
log = logging.getLogger()


class IgnoreExpiredJwtAuthentication(JSONWebTokenAuthentication):
Expand All @@ -36,7 +33,7 @@ def get_token_from_request(cls, request):
jwt_decode_handler(value)
except jwt.ExpiredSignatureError:
# if it is expired, treat it as if the user never passed a token
logger.debug("Ignoring expired JWT")
log.debug("Ignoring expired JWT")
return None
except: # pylint: disable=bare-except # noqa: E722, S110
# we're only interested in jwt.ExpiredSignature above
Expand Down Expand Up @@ -88,52 +85,51 @@ class ApiGatewayAuthentication(BaseAuthentication):
Handles authentication when behind an API gateway.
If the app is sitting in front of something like APISIX, the app will get
authentication information in the HTTP_X_USERINFO header. So, this decodes that,
and then attempts to authenticate the user or creates them if they don't exist.
authentication information through some sort of channel. A middleware can
take care of decoding that and placing the decoded data into the request,
and this backend will handle the authentication based on that data.
"""

def authenticate(self, request):
"""Authenticate the user based on HTTP_X_USERINFO."""
"""Authenticate the user based on request.api_gateway_userdata."""

user_info = request.META.get("HTTP_X_USERINFO", False)

if not user_info:
if (
not request
or not request.api_gateway_userdata
):
return None

decoded_user_info = json.loads(base64.b64decode(user_info))

logger.info(
"ApiGatewayAuthentication: Checking for existing user for %s",
decoded_user_info["preferred_username"],
)
(
email,
preferred_username,
given_name,
family_name,
) = request.api_gateway_userdata

try:
user = User.objects.filter(email=decoded_user_info["email"]).get()
user = User.objects.filter(email=email).get()

logger.info(
log.debug(
"ApiGatewayAuthentication: Found existing user for %s: %s",
decoded_user_info["preferred_username"],
preferred_username,
user,
)
except User.DoesNotExist:
logger.info("ApiGatewayAuthentication: User not found, creating")
log.debug(
"ApiGatewayAuthentication: User %s not found, creating",
preferred_username,
)
# Create a random password for the user, 32 characters long.
# We don't care about the password since APISIX (or whatever) has
# bounced the user to an authentication system elsewhere (like Keycloak).
user = User.objects.create_user(
decoded_user_info["preferred_username"],
decoded_user_info["email"],
preferred_username,
email,
"".join(random.choices(string.ascii_uppercase + string.digits, k=32)), # noqa: S311
)

user.first_name = decoded_user_info.get("given_name", None)
user.last_name = decoded_user_info.get("family_name", None)
user.first_name = given_name
user.last_name = family_name
user.save()

return (user, None)


class ForwardUserMiddleware(RemoteUserMiddleware):
"""RemoteUserMiddleware, but looks at X-Forwarded-User"""

header = "HTTP_X_FORWARDED_USER"
63 changes: 63 additions & 0 deletions unified_ecommerce/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Middleware for Unified Ecommerce."""

import base64
import json
import logging

from django.contrib.auth.middleware import RemoteUserMiddleware

log = logging.getLogger(__name__)


class ApisixUserMiddleware:
"""Checks for and processes APISIX-specific headers."""

def decode_apisix_headers(self, request):
"""Decode the APISIX-specific headers."""
user_info = request.META.get("HTTP_X_USERINFO", False)

if not user_info:
return None

try:
apisix_result = json.loads(base64.b64decode(user_info))
except json.JSONDecodeError:
log_message = (
"ApisixUserMiddleware: Result from HTTP_X_USERINFO "
f"is not valid JSON: {user_info}"
)
log.debug(log_message)

return None

log_message = f"ApisixUserMiddleware: Got {apisix_result}"
log.debug(log_message)

return {
"email": apisix_result["email"],
"preferred_username": apisix_result["preferred_username"],
"given_name": apisix_result["given_name"],
"family_name": apisix_result["family_name"],
}

def __init__(self, get_response):
"""Initialize the middleware."""
self.get_response = get_response

def __call__(self, request):
"""
Process any APISIX headers and put them in the request.
If valid data is found, then it will be put into the "api_gateway_userdata"
attribute of the request. Otherwise, it'll be set to None.
"""

request.api_gateway_userdata = self.decode_apisix_headers(request)

return self.get_response(request)


class ForwardUserMiddleware(RemoteUserMiddleware):
"""RemoteUserMiddleware, but looks at X-Forwarded-User"""

header = "HTTP_X_FORWARDED_USER"
3 changes: 2 additions & 1 deletion unified_ecommerce/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"unified_ecommerce.authentication.ForwardUserMiddleware",
"unified_ecommerce.middleware.ApisixUserMiddleware",
"unified_ecommerce.middleware.ForwardUserMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"corsheaders.middleware.CorsMiddleware",
Expand Down
2 changes: 2 additions & 0 deletions unified_ecommerce/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
# App Paths
re_path(r"^api/v0/meta/", include("system_meta.urls")),
# Private Paths
re_path(r"^_/v0/meta/", include("system_meta.private_urls")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

if settings.DEBUG:
Expand Down

0 comments on commit ef3041c

Please sign in to comment.