Skip to content
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
20 changes: 20 additions & 0 deletions docs/release-notes/artifacts/pr0412.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version_schema: 2

changes:
- title: Added authentication to haproxy-route-policy REST API
author: tphan025
type: minor
description: >
Configured Django REST Framework with JWT and session-based authentication
as default authentication classes, requiring all API endpoints to be accessed
by authenticated users. Added test_settings_authenticated.py for auth-enabled
tests, a dedicated unit-auth tox environment, and integration tests verifying
that unauthenticated requests are rejected and authenticated requests succeed.
Added djangorestframework-simplejwt as a dependency.
urls:
pr:
- https://github.com/canonical/haproxy-operator/pull/412
related_doc:
related_issue:
visibility: public
highlight: false
12 changes: 12 additions & 0 deletions haproxy-route-policy/haproxy_route_policy/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,18 @@

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

# django rest framework options
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.BasicAuthentication",
"rest_framework.authentication.SessionAuthentication",
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}

env_log_level = os.getenv("DJANGO_LOG_LEVEL", "INFO").upper()
if env_log_level not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
env_log_level = "INFO"
Expand Down
7 changes: 7 additions & 0 deletions haproxy-route-policy/haproxy_route_policy/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,10 @@
"NAME": ":memory:",
}
}

REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.AllowAny",
],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2026 Canonical Ltd.
# See LICENSE file for licensing details.

"""Django settings for running tests with SQLite in an authenticated setup."""

from haproxy_route_policy.settings import * # noqa: F401, F403

# Mock secret key for testing.
SECRET_KEY = "test-secret-key"

DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
}
}
8 changes: 8 additions & 0 deletions haproxy-route-policy/haproxy_route_policy/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,18 @@

from django.contrib import admin
from django.urls import include, path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenVerifyView,
)

from policy import urls as policy_urls

urlpatterns = [
path("admin/", admin.site.urls),
path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("api/token/verify/", TokenVerifyView.as_view(), name="token_verify"),
path("", include(policy_urls)),
]
85 changes: 85 additions & 0 deletions haproxy-route-policy/policy/tests/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copyright 2026 Canonical Ltd.
# See LICENSE file for licensing details.

"""Authentication tests."""

from django.test import TestCase, tag
from rest_framework.test import APIClient
from django.contrib.auth.models import User


@tag("auth")
class TestAuthenticationRequired(TestCase):
"""Tests that endpoints require authentication."""

def setUp(self):
self.client = APIClient()

def test_list_requests_unauthenticated(self):
"""GET /api/v1/requests returns 401/403 without auth."""
response = self.client.get("/api/v1/requests")
self.assertIn(response.status_code, [401, 403])

def test_create_requests_unauthenticated(self):
"""POST /api/v1/requests returns 401/403 without auth."""
response = self.client.post("/api/v1/requests", [], format="json")
self.assertIn(response.status_code, [401, 403])

def test_list_rules_unauthenticated(self):
"""GET /api/v1/rules returns 401/403 without auth."""
response = self.client.get("/api/v1/rules")
self.assertIn(response.status_code, [401, 403])


@tag("auth")
class TestAuthenticated(TestCase):
"""Tests endpoints as an authenticated user."""

def setUp(self):
self.user = User.objects.create_user("admin", "admin@example.com", "admin")
self.client = APIClient()
# Add nosec to ignore bandit warning as this is for testing.
self.client.login(username="admin", password="admin") # nosec

def test_create_requests_authenticated(self):
"""POST /api/v1/requests returns 201 with auth."""
payload = [
{
"relation_id": 1,
"hostname_acls": ["example.com"],
"backend_name": "backend-1",
"paths": ["/api"],
"port": 443,
},
{
"relation_id": 2,
"backend_name": "backend-2",
"port": 443,
},
]
response = self.client.post("/api/v1/requests", data=payload, format="json")
self.assertEqual(response.status_code, 201)

def test_create_rules_authenticated(self):
"""POST /api/v1/rules returns 201 with auth."""
payload = {
"name": "Test Rule",
"action": "allow",
"kind": "hostname_and_path_match",
"parameters": {
"hostnames": ["example.com"],
"paths": ["/api"],
},
}
response = self.client.post("/api/v1/rules", data=payload, format="json")
self.assertEqual(response.status_code, 201)

def test_list_requests_authenticated(self):
"""GET /api/v1/requests returns 200 with auth."""
response = self.client.get("/api/v1/requests")
self.assertEqual(response.status_code, 200)

def test_list_rules_authenticated(self):
"""GET /api/v1/rules returns 200 with auth."""
response = self.client.get("/api/v1/rules")
self.assertEqual(response.status_code, 200)
4 changes: 4 additions & 0 deletions haproxy-route-policy/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ requires-python = ">=3.12"
dependencies = [
"django>=6.0.3",
"djangorestframework>=3.16.1",
"djangorestframework-simplejwt>=5.5.1",
"validators>=0.35.0",
"whitenoise>=6.12.0",
]

[dependency-groups]
auth = [
"djangorestframework-simplejwt>=5.5.1",
]
coverage-report = [
"coverage[toml]>=7.13.5",
]
Expand Down
20 changes: 19 additions & 1 deletion haproxy-route-policy/tox.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ skipsdist = true
skip_missing_interpreters = true
requires = ["tox>=4.21"]
no_package = true
envlist = ["lint", "unit", "static", "coverage-report"]
envlist = ["lint", "unit", "unit-auth", "static", "coverage-report"]

[env_run_base]
passenv = ["PYTHONPATH"]
Expand All @@ -26,11 +26,29 @@ commands = [
"test",
"policy",
"--settings=haproxy_route_policy.test_settings",
"--exclude-tag=auth",
"-v2",
],
]
dependency_groups = ["unit"]


[env.unit_auth]
description = "Run auth unit tests"
commands = [
[
"coverage",
"run",
"manage.py",
"test",
"policy",
"--settings=haproxy_route_policy.test_settings_authenticated",
"--tag=auth",
"-v2",
],
]
dependency_groups = ["unit", "auth"]

[env.lint]
description = "Check code against coding style standards"
commands = [
Expand Down
29 changes: 29 additions & 0 deletions haproxy-route-policy/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading