Skip to content

Commit ecf2e6c

Browse files
committed
Login api
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
1 parent 486851e commit ecf2e6c

File tree

8 files changed

+149
-2
lines changed

8 files changed

+149
-2
lines changed

CHANGES/5932.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a login api endpoint to result in an authorization cookie from any other sort of feasible authentication.

pulpcore/app/apps.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ def ready(self):
265265

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

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

276-
for viewset_batch in sender.named_viewsets.values():
277+
extra_viewsets = [LoginViewSet]
278+
for viewset_batch in list(sender.named_viewsets.values()) + [extra_viewsets]:
277279
for viewset in viewset_batch:
278280
access_policy = getattr(viewset, "DEFAULT_ACCESS_POLICY", None)
279281
if access_policy is not None:

pulpcore/app/serializers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
GroupRoleSerializer,
114114
GroupSerializer,
115115
GroupUserSerializer,
116+
LoginSerializer,
116117
NestedRoleSerializer,
117118
RoleSerializer,
118119
UserRoleSerializer,

pulpcore/app/serializers/user.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from gettext import gettext as _
44

55
from django.contrib.auth import get_user_model
6+
from django.contrib.auth import login as auth_login
67
from django.contrib.auth.models import Permission
78
from django.contrib.auth.hashers import make_password
89
from django.contrib.auth.password_validation import validate_password
@@ -490,3 +491,14 @@ def validate(self, data):
490491
)
491492
self.group_role_pks.append(qs.get().pk)
492493
return data
494+
495+
496+
class LoginSerializer(serializers.Serializer):
497+
pulp_href = IdentityField(view_name="users-detail")
498+
prn = PRNField()
499+
username = serializers.CharField(read_only=True)
500+
501+
def create(self, validated_data):
502+
user = self.context["request"].user
503+
auth_login(self.context["request"], user)
504+
return user

pulpcore/app/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
)
2323
from pulpcore.app.viewsets import (
2424
ListRepositoryVersionViewSet,
25+
LoginViewSet,
2526
OrphansCleanupViewset,
2627
ReclaimSpaceViewSet,
2728
)
@@ -152,6 +153,7 @@ class PulpDefaultRouter(routers.DefaultRouter):
152153
vs_tree.add_decendent(ViewSetNode(viewset))
153154

154155
special_views = [
156+
path("login/", LoginViewSet.as_view()),
155157
path("repair/", RepairView.as_view()),
156158
path(
157159
"orphans/cleanup/",

pulpcore/app/viewsets/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
GroupViewSet,
7777
GroupRoleViewSet,
7878
GroupUserViewSet,
79+
LoginViewSet,
7980
RoleViewSet,
8081
UserViewSet,
8182
UserRoleViewSet,

pulpcore/app/viewsets/user.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
from gettext import gettext as _
22

33
from django.contrib.auth import get_user_model
4+
from django.contrib.auth import logout as auth_logout
45
from django.contrib.contenttypes.models import ContentType
56
from django.core.exceptions import FieldError
67
from django.shortcuts import get_object_or_404
78
from django_filters.rest_framework import filters
89
from django.db.models import Q, Count
910
from django.contrib.auth.models import Permission
1011

11-
from rest_framework import mixins, status
12+
from rest_framework import generics, mixins, status
1213
from rest_framework.exceptions import PermissionDenied
1314
from rest_framework.permissions import SAFE_METHODS
1415
from rest_framework.response import Response
@@ -24,6 +25,7 @@
2425
GroupSerializer,
2526
GroupUserSerializer,
2627
GroupRoleSerializer,
28+
LoginSerializer,
2729
RoleSerializer,
2830
UserSerializer,
2931
UserRoleSerializer,
@@ -422,3 +424,35 @@ class GroupRoleViewSet(
422424
serializer_class = GroupRoleSerializer
423425
queryset = GroupRole.objects.all()
424426
ordering = ("-pulp_created",)
427+
428+
429+
class LoginViewSet(generics.CreateAPIView):
430+
serializer_class = LoginSerializer
431+
432+
DEFAULT_ACCESS_POLICY = {
433+
"statements": [
434+
{
435+
"action": ["*"],
436+
"principal": "authenticated",
437+
"effect": "allow",
438+
},
439+
],
440+
"creation_hooks": [],
441+
}
442+
443+
@staticmethod
444+
def urlpattern():
445+
return "login"
446+
447+
@extend_schema(operation_id="login_read")
448+
def get(self, request):
449+
return Response(self.get_serializer(request.user).data)
450+
451+
@extend_schema(operation_id="logout")
452+
def delete(self, request):
453+
auth_logout(request)
454+
return Response(status=204)
455+
456+
457+
# Annotate without redefining the post method.
458+
extend_schema(operation_id="login")(LoginViewSet.post)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import http
2+
import pytest
3+
4+
pytestmark = [pytest.mark.parallel]
5+
6+
7+
@pytest.fixture
8+
def session_user(pulpcore_bindings, gen_user, anonymous_user):
9+
old_cookie = pulpcore_bindings.client.cookie
10+
user = gen_user()
11+
with user:
12+
response = pulpcore_bindings.LoginApi.login_with_http_info()
13+
if isinstance(response, tuple):
14+
# old bindings
15+
_, _, headers = response
16+
else:
17+
# new bindings
18+
headers = response.headers
19+
cookie_jar = http.cookies.SimpleCookie(headers["set-cookie"])
20+
# Use anonymous_user to remove the basic auth header from the api client.
21+
with anonymous_user:
22+
pulpcore_bindings.client.cookie = "; ".join(
23+
(f"{k}={v.value}" for k, v in cookie_jar.items())
24+
)
25+
# Weird: You need to pass the CSRFToken as a header not a cookie...
26+
pulpcore_bindings.client.set_default_header("X-CSRFToken", cookie_jar["csrftoken"].value)
27+
yield user
28+
pulpcore_bindings.client.cookie = old_cookie
29+
30+
31+
def test_login_read_denies_anonymous(pulpcore_bindings, anonymous_user):
32+
with anonymous_user:
33+
with pytest.raises(pulpcore_bindings.module.ApiException) as exc:
34+
pulpcore_bindings.LoginApi.login_read()
35+
assert exc.value.status == 401
36+
37+
38+
def test_login_read_returns_username(pulpcore_bindings, gen_user):
39+
user = gen_user()
40+
with user:
41+
result = pulpcore_bindings.LoginApi.login_read()
42+
assert result.username == user.username
43+
44+
45+
def test_login_denies_anonymous(pulpcore_bindings, anonymous_user):
46+
with anonymous_user:
47+
with pytest.raises(pulpcore_bindings.module.ApiException) as exc:
48+
pulpcore_bindings.LoginApi.login()
49+
assert exc.value.status == 401
50+
51+
52+
def test_login_sets_session_cookie(pulpcore_bindings, gen_user):
53+
user = gen_user()
54+
with user:
55+
response = pulpcore_bindings.LoginApi.login_with_http_info()
56+
if isinstance(response, tuple):
57+
# old bindings
58+
result, status, headers = response
59+
else:
60+
# new bindings
61+
result = response.data
62+
status = response.status
63+
headers = response.headers
64+
assert status == 201
65+
assert result.username == user.username
66+
cookie_jar = http.cookies.SimpleCookie(headers["set-cookie"])
67+
assert cookie_jar["sessionid"].value != ""
68+
assert cookie_jar["csrftoken"].value != ""
69+
70+
71+
def test_session_cookie_is_authorization(pulpcore_bindings, anonymous_user, session_user):
72+
result = pulpcore_bindings.LoginApi.login_read()
73+
assert result.username == session_user.username
74+
75+
76+
def test_logout_removes_sessionid(pulpcore_bindings, session_user):
77+
response = pulpcore_bindings.LoginApi.logout_with_http_info()
78+
if isinstance(response, tuple):
79+
# old bindings
80+
_, status, headers = response
81+
else:
82+
# new bindings
83+
status = response.status
84+
headers = response.headers
85+
assert status == 204
86+
cookie_jar = http.cookies.SimpleCookie(headers["set-cookie"])
87+
assert cookie_jar["sessionid"].value == ""
88+
89+
90+
def test_logout_denies_anonymous(pulpcore_bindings, anonymous_user):
91+
with anonymous_user:
92+
with pytest.raises(pulpcore_bindings.module.ApiException) as exc:
93+
pulpcore_bindings.LoginApi.logout()
94+
assert exc.value.status == 401

0 commit comments

Comments
 (0)