Skip to content

Commit

Permalink
Enable authentication (#9)
Browse files Browse the repository at this point in the history
* tokenauthentication

* unit tests use auth

* update functional test with token, https

* run with gunicorn

* let ingress controller enforce https

* switch to bearer token
  • Loading branch information
scrungus authored Jul 29, 2024
1 parent a6e09a7 commit 1a44e8f
Show file tree
Hide file tree
Showing 16 changed files with 136 additions and 33 deletions.
5 changes: 2 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,11 @@ ENV PYTHONUNBUFFERED 1
# Install application configuration using flexi-settings
ENV DJANGO_SETTINGS_MODULE flexi_settings.settings
ENV DJANGO_FLEXI_SETTINGS_ROOT /etc/coral-credits/settings.py
COPY ./etc/coral-credits /etc/coral-credits
COPY ./etc/ /etc/
RUN mkdir -p /etc/coral-credits/settings.d

# By default, serve the app on port 8080 using the app user
EXPOSE 8080
USER $APP_UID
ENTRYPOINT ["tini", "-g", "--"]
#TODO(tylerchristie): use gunicorn + wsgi like azimuth
CMD ["python", "/coral-credits/manage.py", "runserver", "0.0.0.0:8080"]
CMD ["/venv/bin/gunicorn", "--config", "/etc/gunicorn/conf.py", "coral_credits.wsgi:application"]
9 changes: 8 additions & 1 deletion charts/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,11 @@ app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
{{ include "coral-credits.selectorLabels" . }}
{{- end }}
{{- end }}

{{/*
Secrets
*/}}
{{- define "coral-credits.djangoSecretName" -}}
{{- default (printf "%s-django-env" (include "coral-credits.fullname" .)) -}}
{{- end -}}
33 changes: 29 additions & 4 deletions charts/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,31 @@ spec:
initContainers:
- name: migrate-db
image: {{ printf "%s:%s" .Values.image.repository (default .Chart.AppVersion .Values.image.tag) }}
command:
- python
- /coral-credits/manage.py
- migrate
env:
{{ with (include "coral-credits.djangoSecretName" . ) }}
- name: DJANGO_SUPERUSER_USERNAME
valueFrom:
secretKeyRef:
name: {{ quote . }}
key: username
- name: DJANGO_SUPERUSER_EMAIL
valueFrom:
secretKeyRef:
name: {{ quote . }}
key: email
- name: DJANGO_SUPERUSER_PASSWORD
valueFrom:
secretKeyRef:
name: {{ quote . }}
key: password
{{ end }}
command: ["/bin/sh"]
# Capture exit code from createsuperuser as it is not idempotent.
args:
- -c
- >-
python /coral-credits/manage.py migrate &&
python /coral-credits/manage.py createsuperuser --no-input || echo $?
volumeMounts:
- name: data
mountPath: /data
Expand Down Expand Up @@ -53,6 +74,8 @@ spec:
- name: runtime-settings
mountPath: /etc/coral-credits/settings.d
readOnly: true
- name: tmp
mountPath: /tmp
{{- with .Values.nodeSelector }}
nodeSelector: {{ toYaml . | nindent 8 }}
{{- end }}
Expand All @@ -69,3 +92,5 @@ spec:
- name: runtime-settings
secret:
secretName: {{ include "coral-credits.fullname" . }}
- name: tmp
emptyDir: {}
12 changes: 12 additions & 0 deletions charts/templates/django_superuser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "coral-credits.djangoSecretName" . }}
labels: {{ include "coral-credits.labels" . | nindent 4 }}
type: Opaque
# Use data because of https://github.com/helm/helm/issues/10010
# Not doing so means that AWX-related keys are not removed on transition to the CRD
stringData:
password: {{ .Values.settings.superuserPassword | default (randAlphaNum 64) }}
username: {{ .Values.settings.superuserUsername | default "admin" }}
email: {{ .Values.settings.superuserEmail | default "[email protected]" }}
6 changes: 6 additions & 0 deletions charts/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ settings:
# If not given, a randomly generated key will be used
# However this will be different on each deployment which may cause sessions to be terminated
secretKey:
# Same for the django superuser password
superuserPassword:
# Superuser username
superuserUsername:
# Superuser email
superuserEmail:
# Use debug mode (recommended false in production)
debug: false
# Database settings
Expand Down
2 changes: 1 addition & 1 deletion coral_credits/api/tests/allocation_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def test_credit_allocation_resource_create_success(
)

# Make the API call
response = api_client.post(url, request_data, format="json")
response = api_client.post(url, request_data, format="json", secure=True)

# Check that the request was successful
assert response.status_code == status.HTTP_200_OK, (
Expand Down
15 changes: 13 additions & 2 deletions coral_credits/api/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from datetime import datetime, timedelta

from django.contrib.auth.models import User
from django.utils.timezone import make_aware
import pytest
from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient

import coral_credits.api.models as models
Expand All @@ -14,6 +16,13 @@ def pytest_configure(config):
config.END_DATE = config.START_DATE + timedelta(days=1)


# Get auth token
@pytest.fixture
def token():
user = User.objects.create_user(username="testuser", password="12345")
return Token.objects.create(user=user)


# Fixtures defining all the necessary database entries for testing
@pytest.fixture
def resource_classes():
Expand Down Expand Up @@ -55,8 +64,10 @@ def credit_allocation(account, request):


@pytest.fixture
def api_client():
return APIClient()
def api_client(token):
client = APIClient()
client.credentials(HTTP_AUTHORIZATION="Bearer " + token.key)
return client


@pytest.fixture
Expand Down
2 changes: 2 additions & 0 deletions coral_credits/api/tests/consumer_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def test_valid_create_request(
url,
data=json.dumps(request_data),
content_type="application/json",
secure=True,
)

assert response.status_code == status.HTTP_204_NO_CONTENT, (
Expand Down Expand Up @@ -139,6 +140,7 @@ def test_create_request_insufficient_credits(
url,
data=json.dumps(request_data),
content_type="application/json",
secure=True,
)

assert response.status_code == status.HTTP_403_FORBIDDEN, (
Expand Down
15 changes: 7 additions & 8 deletions coral_credits/api/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.db import transaction
from django.db.utils import IntegrityError
from django.shortcuts import get_object_or_404
from rest_framework import status, viewsets
from rest_framework import permissions, status, viewsets
from rest_framework.response import Response

from coral_credits.api import db_exceptions, db_utils, models, serializers
Expand All @@ -16,8 +16,7 @@ class CreditAllocationViewSet(viewsets.ModelViewSet):
class CreditAllocationResourceViewSet(viewsets.ModelViewSet):
queryset = models.CreditAllocationResource.objects.all()
serializer_class = serializers.CreditAllocationResourceSerializer
# TODO(tylerchristie): enable authentication
# permission_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticated]

def create(self, request, allocation_pk=None):
"""Allocate credits to a dictionary of resource classes.
Expand Down Expand Up @@ -66,25 +65,25 @@ def _validate_request(self, request):
class ResourceClassViewSet(viewsets.ModelViewSet):
queryset = models.ResourceClass.objects.all()
serializer_class = serializers.ResourceClassSerializer
# permission_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticated]


class ResourceProviderViewSet(viewsets.ModelViewSet):
queryset = models.ResourceProvider.objects.all()
serializer_class = serializers.ResourceProviderSerializer
# permission_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticated]


class ResourceProviderAccountViewSet(viewsets.ModelViewSet):
queryset = models.ResourceProviderAccount.objects.all()
serializer_class = serializers.ResourceProviderAccountSerializer
# permission_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticated]


class AccountViewSet(viewsets.ModelViewSet):
queryset = models.CreditAccount.objects.all()
serializer_class = serializers.CreditAccountSerializer
# permission_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticated]

def retrieve(self, request, pk=None):
"""Retreives a Credit Account Summary"""
Expand Down Expand Up @@ -134,7 +133,7 @@ def retrieve(self, request, pk=None):


class ConsumerViewSet(viewsets.ModelViewSet):
# permission_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticated]

def create(self, request):
return self._create_or_update(request)
Expand Down
5 changes: 5 additions & 0 deletions coral_credits/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from rest_framework.authentication import TokenAuthentication


class BearerTokenAuthentication(TokenAuthentication):
keyword = "Bearer"
4 changes: 4 additions & 0 deletions coral_credits/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"rest_framework.authtoken",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
Expand All @@ -57,6 +58,9 @@

REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_AUTHENTICATION_CLASSES": [
"coral_credits.auth.BearerTokenAuthentication",
],
}

ROOT_URLCONF = "coral_credits.urls"
Expand Down
3 changes: 3 additions & 0 deletions coral_credits/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from django.contrib import admin
from django.http import HttpResponse
from django.urls import include, path
from rest_framework.authtoken import views as drfviews
from rest_framework_nested import routers

from coral_credits.api import views
Expand Down Expand Up @@ -51,4 +52,6 @@ def status(request):
path("", include(allocation_router.urls)),
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
path("admin/", admin.site.urls),
# TODO(tylerchristie): probably need some permissions/scoping
path("api-token-auth/", drfviews.obtain_auth_token),
]
4 changes: 4 additions & 0 deletions etc/coral-credits/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"rest_framework.authtoken",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
Expand All @@ -47,6 +48,9 @@

REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_AUTHENTICATION_CLASSES": [
"coral_credits.auth.BearerTokenAuthentication",
],
}

ROOT_URLCONF = "coral_credits.urls"
Expand Down
12 changes: 12 additions & 0 deletions etc/gunicorn/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Default settings for gunicorn
# Also allows for overriding with environment variables
import os

# Configure the bind address
_host = os.environ.get("GUNICORN_HOST", "0.0.0.0")
_port = os.environ.get("GUNICORN_PORT", "8080")
bind = os.environ.get("GUNICORN_BIND", "{}:{}".format(_host, _port))

# TODO(tylerchristie): configure workers and threads?

# TODO(tylerchristie): configure logging
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ djangorestframework==3.15.2
drf-nested-routers==0.94.1
django-extensions==3.2.3
drf-spectacular==0.27.2
gunicorn==22.0.0
tzdata==2024.1
Loading

0 comments on commit 1a44e8f

Please sign in to comment.