Skip to content

Commit

Permalink
Login api
Browse files Browse the repository at this point in the history
This adds a json rest like api for login (post) logout (delete) and
who-am-i (get). Login is authenticated by whatever authentication
classes are active and a http cookie session is managed by the endpoint.

fixes #5932
  • Loading branch information
mdellweg committed Dec 18, 2024
1 parent 486851e commit ecf2e6c
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGES/5932.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a login api endpoint to result in an authorization cookie from any other sort of feasible authentication.
4 changes: 3 additions & 1 deletion pulpcore/app/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ def ready(self):

def _populate_access_policies(sender, apps, verbosity, **kwargs):
from pulpcore.app.util import get_view_urlpattern
from pulpcore.app.viewsets import LoginViewSet

try:
AccessPolicy = apps.get_model("core", "AccessPolicy")
Expand All @@ -273,7 +274,8 @@ def _populate_access_policies(sender, apps, verbosity, **kwargs):
print(_("AccessPolicy model does not exist. Skipping initialization."))
return

for viewset_batch in sender.named_viewsets.values():
extra_viewsets = [LoginViewSet]
for viewset_batch in list(sender.named_viewsets.values()) + [extra_viewsets]:
for viewset in viewset_batch:
access_policy = getattr(viewset, "DEFAULT_ACCESS_POLICY", None)
if access_policy is not None:
Expand Down
1 change: 1 addition & 0 deletions pulpcore/app/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
GroupRoleSerializer,
GroupSerializer,
GroupUserSerializer,
LoginSerializer,
NestedRoleSerializer,
RoleSerializer,
UserRoleSerializer,
Expand Down
12 changes: 12 additions & 0 deletions pulpcore/app/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from gettext import gettext as _

from django.contrib.auth import get_user_model
from django.contrib.auth import login as auth_login
from django.contrib.auth.models import Permission
from django.contrib.auth.hashers import make_password
from django.contrib.auth.password_validation import validate_password
Expand Down Expand Up @@ -490,3 +491,14 @@ def validate(self, data):
)
self.group_role_pks.append(qs.get().pk)
return data


class LoginSerializer(serializers.Serializer):
pulp_href = IdentityField(view_name="users-detail")
prn = PRNField()
username = serializers.CharField(read_only=True)

def create(self, validated_data):
user = self.context["request"].user
auth_login(self.context["request"], user)
return user
2 changes: 2 additions & 0 deletions pulpcore/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
)
from pulpcore.app.viewsets import (
ListRepositoryVersionViewSet,
LoginViewSet,
OrphansCleanupViewset,
ReclaimSpaceViewSet,
)
Expand Down Expand Up @@ -152,6 +153,7 @@ class PulpDefaultRouter(routers.DefaultRouter):
vs_tree.add_decendent(ViewSetNode(viewset))

special_views = [
path("login/", LoginViewSet.as_view()),
path("repair/", RepairView.as_view()),
path(
"orphans/cleanup/",
Expand Down
1 change: 1 addition & 0 deletions pulpcore/app/viewsets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
GroupViewSet,
GroupRoleViewSet,
GroupUserViewSet,
LoginViewSet,
RoleViewSet,
UserViewSet,
UserRoleViewSet,
Expand Down
36 changes: 35 additions & 1 deletion pulpcore/app/viewsets/user.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from gettext import gettext as _

from django.contrib.auth import get_user_model
from django.contrib.auth import logout as auth_logout
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldError
from django.shortcuts import get_object_or_404
from django_filters.rest_framework import filters
from django.db.models import Q, Count
from django.contrib.auth.models import Permission

from rest_framework import mixins, status
from rest_framework import generics, mixins, status
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import SAFE_METHODS
from rest_framework.response import Response
Expand All @@ -24,6 +25,7 @@
GroupSerializer,
GroupUserSerializer,
GroupRoleSerializer,
LoginSerializer,
RoleSerializer,
UserSerializer,
UserRoleSerializer,
Expand Down Expand Up @@ -422,3 +424,35 @@ class GroupRoleViewSet(
serializer_class = GroupRoleSerializer
queryset = GroupRole.objects.all()
ordering = ("-pulp_created",)


class LoginViewSet(generics.CreateAPIView):
serializer_class = LoginSerializer

DEFAULT_ACCESS_POLICY = {
"statements": [
{
"action": ["*"],
"principal": "authenticated",
"effect": "allow",
},
],
"creation_hooks": [],
}

@staticmethod
def urlpattern():
return "login"

@extend_schema(operation_id="login_read")
def get(self, request):
return Response(self.get_serializer(request.user).data)

@extend_schema(operation_id="logout")
def delete(self, request):
auth_logout(request)
return Response(status=204)


# Annotate without redefining the post method.
extend_schema(operation_id="login")(LoginViewSet.post)
94 changes: 94 additions & 0 deletions pulpcore/tests/functional/api/test_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import http
import pytest

pytestmark = [pytest.mark.parallel]


@pytest.fixture
def session_user(pulpcore_bindings, gen_user, anonymous_user):
old_cookie = pulpcore_bindings.client.cookie
user = gen_user()
with user:
response = pulpcore_bindings.LoginApi.login_with_http_info()
if isinstance(response, tuple):
# old bindings
_, _, headers = response
else:
# new bindings
headers = response.headers
cookie_jar = http.cookies.SimpleCookie(headers["set-cookie"])
# Use anonymous_user to remove the basic auth header from the api client.
with anonymous_user:
pulpcore_bindings.client.cookie = "; ".join(
(f"{k}={v.value}" for k, v in cookie_jar.items())
)
# Weird: You need to pass the CSRFToken as a header not a cookie...
pulpcore_bindings.client.set_default_header("X-CSRFToken", cookie_jar["csrftoken"].value)
yield user
pulpcore_bindings.client.cookie = old_cookie


def test_login_read_denies_anonymous(pulpcore_bindings, anonymous_user):
with anonymous_user:
with pytest.raises(pulpcore_bindings.module.ApiException) as exc:
pulpcore_bindings.LoginApi.login_read()
assert exc.value.status == 401


def test_login_read_returns_username(pulpcore_bindings, gen_user):
user = gen_user()
with user:
result = pulpcore_bindings.LoginApi.login_read()
assert result.username == user.username


def test_login_denies_anonymous(pulpcore_bindings, anonymous_user):
with anonymous_user:
with pytest.raises(pulpcore_bindings.module.ApiException) as exc:
pulpcore_bindings.LoginApi.login()
assert exc.value.status == 401


def test_login_sets_session_cookie(pulpcore_bindings, gen_user):
user = gen_user()
with user:
response = pulpcore_bindings.LoginApi.login_with_http_info()
if isinstance(response, tuple):
# old bindings
result, status, headers = response
else:
# new bindings
result = response.data
status = response.status
headers = response.headers
assert status == 201
assert result.username == user.username
cookie_jar = http.cookies.SimpleCookie(headers["set-cookie"])
assert cookie_jar["sessionid"].value != ""
assert cookie_jar["csrftoken"].value != ""


def test_session_cookie_is_authorization(pulpcore_bindings, anonymous_user, session_user):
result = pulpcore_bindings.LoginApi.login_read()
assert result.username == session_user.username


def test_logout_removes_sessionid(pulpcore_bindings, session_user):
response = pulpcore_bindings.LoginApi.logout_with_http_info()
if isinstance(response, tuple):
# old bindings
_, status, headers = response
else:
# new bindings
status = response.status
headers = response.headers
assert status == 204
cookie_jar = http.cookies.SimpleCookie(headers["set-cookie"])
assert cookie_jar["sessionid"].value == ""


def test_logout_denies_anonymous(pulpcore_bindings, anonymous_user):
with anonymous_user:
with pytest.raises(pulpcore_bindings.module.ApiException) as exc:
pulpcore_bindings.LoginApi.logout()
assert exc.value.status == 401

0 comments on commit ecf2e6c

Please sign in to comment.