Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add domain endpoints #17

Merged
merged 7 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
37 changes: 37 additions & 0 deletions httprequest_lego_provider/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.
"""Serializers."""

from rest_framework import serializers
yanksyoon marked this conversation as resolved.
Show resolved Hide resolved

from .models import Domain, DomainUserPermission


class DomainSerializer(serializers.ModelSerializer):
"""Serializer for the Domain objects."""

class Meta:
"""Serializer configuration.

Attributes:
model: the model to serialize.
fields: fields to serialize.
"""

model = Domain
fields = "__all__"


class DomainUserPermissionSerializer(serializers.ModelSerializer):
"""Serializer for the DomainUserPermission objects."""

class Meta:
"""Serializer configuration.

Attributes:
model: the model to serialize.
fields: fields to serialize.
"""

model = DomainUserPermission
fields = "__all__"
26 changes: 26 additions & 0 deletions httprequest_lego_provider/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,32 @@ def user_auth_token(username: str, user_password: str, user: User) -> str:
return base64.b64encode(bytes(f"{username}:{user_password}", "utf-8")).decode("utf-8")


@pytest.fixture(scope="module")
def admin_username() -> str:
"""Provide an admin username."""
return "test_admin_user"


@pytest.fixture(scope="module")
def admin_user_password() -> str:
"""Provide an admin user password."""
return secrets.token_hex()


@pytest.fixture(scope="function")
def admin_user(admin_username: str, admin_user_password: str) -> User:
"""Provide an admin user."""
return User.objects.create_user(admin_username, password=admin_user_password, is_staff=True)


@pytest.fixture(scope="function")
def admin_user_auth_token(admin_username: str, admin_user_password: str, admin_user: User) -> str:
"""Provide the auth_token for the admin user."""
return base64.b64encode(bytes(f"{admin_username}:{admin_user_password}", "utf-8")).decode(
"utf-8"
)


@pytest.fixture(scope="module")
def fqdn():
"""Provide a valid FQDN."""
Expand Down
144 changes: 138 additions & 6 deletions httprequest_lego_provider/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from unittest.mock import patch

import pytest
from django.contrib.auth.models import User
from django.test import Client

from httprequest_lego_provider.forms import FQDN_PREFIX
Expand Down Expand Up @@ -53,7 +54,7 @@ def test_post_present_when_auth_header_invalid(client: Client):
@pytest.mark.django_db
def test_post_present_when_logged_in_and_no_fqdn(client: Client, user_auth_token: str, fqdn: str):
"""
arrange: log in a user.
arrange: log in a non-admin user.
act: submit a POST request for the present URL.
assert: a 403 is returned.
"""
Expand All @@ -72,7 +73,7 @@ def test_post_present_when_logged_in_and_no_permission(
client: Client, user_auth_token: str, domain: Domain
):
"""
arrange: log in a user and insert a domain in the database.
arrange: log in a non-admin user and insert a domain in the database.
act: submit a POST request for the present URL.
assert: a 403 is returned.
"""
Expand Down Expand Up @@ -128,7 +129,7 @@ def test_post_present_when_logged_in_and_fqdn_invalid(client: Client, user_auth_
@pytest.mark.django_db
def test_get_present_when_logged_in(client: Client, user_auth_token: str):
"""
arrange: log in a user.
arrange: log in a non-admin user.
act: submit a GET request for the present URL.
assert: a 405 is returned.
"""
Expand All @@ -154,7 +155,7 @@ def test_post_cleanup_when_not_logged_in(client: Client):
@pytest.mark.django_db
def test_post_cleanup_when_logged_in_and_no_fqdn(client: Client, user_auth_token: str):
"""
arrange: log in a user.
arrange: log in a non-admin user.
act: submit a POST request for the cleanup URL.
assert: a 403 is returned.
"""
Expand All @@ -173,7 +174,7 @@ def test_post_cleanup_when_logged_in_and_no_permission(
client: Client, user_auth_token: str, domain: Domain
):
"""
arrange: log in a user.
arrange: log in a non-admin user.
act: submit a POST request for the cleanup URL.
assert: a 403 is returned.
"""
Expand Down Expand Up @@ -229,7 +230,7 @@ def test_post_cleanup_when_logged_in_and_fqdn_invalid(client: Client, user_auth_
@pytest.mark.django_db
def test_get_cleanup_when_logged_in(client: Client, user_auth_token: str):
"""
arrange: log in a user.
arrange: log in a non-admin user.
act: submit a GET request for the cleanup URL.
assert: a 405 is returned.
"""
Expand Down Expand Up @@ -264,3 +265,134 @@ def test_test_jwt_token_login(
)

assert response.status_code == 204


@pytest.mark.django_db
def test_post_domain_when_logged_in_as_non_admin_user(client: Client, user_auth_token: str):
"""
arrange: log in a non-admin user.
act: submit a POST request for the domain URL.
assert: a 403 is returned and the domain is not inserted in the database.
"""
response = client.post(
"/api/v1/domains/",
data={"fqdn": "example.com"},
headers={"AUTHORIZATION": f"Basic {user_auth_token}"},
)

with pytest.raises(Domain.DoesNotExist):
Domain.objects.get(fqdn="example.com")
assert response.status_code == 403


@pytest.mark.django_db
def test_post_domain_when_logged_in_as_admin_user(client: Client, admin_user_auth_token: str):
"""
arrange: log in an admin user.
arturo-seijas marked this conversation as resolved.
Show resolved Hide resolved
act: submit a POST request for the domain URL.
assert: a 201 is returned and the domain is inserted in the database.
"""
response = client.post(
"/api/v1/domains/",
data={"fqdn": "example.com"},
headers={"AUTHORIZATION": f"Basic {admin_user_auth_token}"},
)

assert Domain.objects.get(fqdn="example.com") is not None
assert response.status_code == 201


@pytest.mark.django_db
def test_post_domain_when_logged_in_as_admin_user_and_domain_invalid(
client: Client, admin_user_auth_token: str
):
"""
arrange: log in a admin user.
act: submit a POST request for the domain URL.
arturo-seijas marked this conversation as resolved.
Show resolved Hide resolved
assert: a 400 is returned.
"""
response = client.post(
"/api/v1/domains/",
data={"fqdn": "invalid-value"},
headers={"AUTHORIZATION": f"Basic {admin_user_auth_token}"},
)

with pytest.raises(Domain.DoesNotExist):
Domain.objects.get(fqdn="invalid-value")
assert response.status_code == 400


@pytest.mark.django_db
def test_post_domain_user_permission_when_logged_in_as_non_admin_user(
client: Client, user_auth_token: str, domain: Domain, user: User
):
"""
arrange: log in a non-admin user.
act: submit a POST request for the domain user permission URL.
assert: a 403 is returned and the domain is not inserted in the database.
"""
response = client.post(
"/api/v1/domain-user-permissions/",
data={"domain": domain.id, "user": user.id, "text": "whatever"},
headers={"AUTHORIZATION": f"Basic {user_auth_token}"},
)

assert not DomainUserPermission.objects.filter(user=user, domain=domain)
assert response.status_code == 403


@pytest.mark.django_db
def test_post_domain_user_permission_with_invalid_domain_when_logged_in_as_admin_user(
client: Client, admin_user_auth_token: str, user: User
):
"""
arrange: log in an admin user.
act: submit a POST request for the domain user permission URL for a non existing domain.
assert: a 400 is returned and the domain is not inserted in the database.
"""
response = client.post(
"/api/v1/domain-user-permissions/",
data={"domain": 1, "user": user.id, "text": "whatever"},
headers={"AUTHORIZATION": f"Basic {admin_user_auth_token}"},
)

assert not DomainUserPermission.objects.filter(user=user, domain=1)
assert response.status_code == 400


@pytest.mark.django_db
def test_post_domain_user_permission_with_invalid_user_when_logged_in_as_admin_user(
client: Client, admin_user_auth_token: str, domain: Domain
):
"""
arrange: log in an admin user.
act: submit a POST request for the domain user permission URL for a non existing user.
assert: a 400 is returned and the domain is not inserted in the database.
"""
response = client.post(
"/api/v1/domain-user-permissions/",
data={"domain": domain.id, "user": 99, "text": "whatever"},
headers={"AUTHORIZATION": f"Basic {admin_user_auth_token}"},
)

assert not DomainUserPermission.objects.filter(user=99, domain=domain)
assert response.status_code == 400


@pytest.mark.django_db
def test_post_domain_user_permission_when_logged_in_as_admin_user(
client: Client, admin_user_auth_token: str, user: User, domain: Domain
):
"""
arrange: log in an admin user.
act: submit a POST request for the domain user permission URL for a non existing domain.
arturo-seijas marked this conversation as resolved.
Show resolved Hide resolved
assert: a 201 is returned and the domain user permission is inserted in the database.
"""
response = client.post(
"/api/v1/domain-user-permissions/",
data={"domain": domain.id, "user": user.id, "text": "whatever"},
headers={"AUTHORIZATION": f"Basic {admin_user_auth_token}"},
)

assert DomainUserPermission.objects.filter(user=99, domain=domain) is not None
assert response.status_code == 201
6 changes: 6 additions & 0 deletions httprequest_lego_provider/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
"""Urls."""

from django.urls import include, path
from rest_framework.routers import DefaultRouter

from . import views

router = DefaultRouter()
router.register(r"domains", views.DomainViewSet)
router.register(r"domain-user-permissions", views.DomainUserPermissionViewSet)
arturo-seijas marked this conversation as resolved.
Show resolved Hide resolved

urlpatterns = [
path("api/v1/cleanup/", views.handle_cleanup, name="cleanup"),
path("api/v1/present/", views.handle_present, name="present"),
path("api/v1/accounts/", include("django.contrib.auth.urls")),
path("api/v1/", include(router.urls)),
]
33 changes: 33 additions & 0 deletions httprequest_lego_provider/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@
# See LICENSE file for licensing details.
"""Views."""

# pylint:disable=too-many-ancestors
arturo-seijas marked this conversation as resolved.
Show resolved Hide resolved

from typing import Optional

from django.core.exceptions import PermissionDenied
from django.http import HttpRequest, HttpResponse
from rest_framework import viewsets
from rest_framework.decorators import api_view
from rest_framework.permissions import IsAdminUser

from .dns import remove_dns_record, write_dns_record
from .forms import CleanupForm, PresentForm
from .models import Domain, DomainUserPermission
from .serializers import DomainSerializer, DomainUserPermissionSerializer


@api_view(["POST"])
Expand Down Expand Up @@ -66,3 +71,31 @@ def handle_cleanup(request: HttpRequest) -> Optional[HttpResponse]:
except Domain.DoesNotExist:
pass
raise PermissionDenied


class DomainViewSet(viewsets.ModelViewSet):
"""Views for the Domain.

Attributes:
queryset: query for the objects in the model.
serializer_class: class used for serialization.
permission_classes: list of classes to match permissions.
"""

queryset = Domain.objects.all()
serializer_class = DomainSerializer
permission_classes = [IsAdminUser]


class DomainUserPermissionViewSet(viewsets.ModelViewSet):
"""Views for the DomainUserPermission.

Attributes:
queryset: query for the objects in the model.
serializer_class: class used for serialization.
permission_classes: list of classes to match permissions.
"""

queryset = DomainUserPermission.objects.all()
serializer_class = DomainUserPermissionSerializer
permission_classes = [IsAdminUser]
Loading