core: make application's check_access API return a PolicyResult and accept for_user as superuser

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-05-26 11:47:23 +02:00
parent 309d80a921
commit 523621daa2
4 changed files with 68 additions and 22 deletions

View file

@ -13,7 +13,12 @@ from drf_spectacular.utils import (
inline_serializer,
)
from rest_framework.decorators import action
from rest_framework.fields import CharField, FileField, SerializerMethodField
from rest_framework.fields import (
CharField,
FileField,
IntegerField,
SerializerMethodField,
)
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.response import Response
@ -25,9 +30,11 @@ from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.api.decorators import permission_required
from authentik.core.api.providers import ProviderSerializer
from authentik.core.models import Application
from authentik.core.models import Application, User
from authentik.events.models import EventAction
from authentik.policies.api.exec import PolicyTestResultSerializer
from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult
from authentik.stages.user_login.stage import USER_LOGIN_AUTHENTICATED
LOGGER = get_logger()
@ -112,23 +119,34 @@ class ApplicationViewSet(ModelViewSet):
return applications
@extend_schema(
request=inline_serializer(
"CheckAccessRequest", fields={"for_user": IntegerField(required=False)}
),
responses={
204: OpenApiResponse(description="Access granted"),
403: OpenApiResponse(description="Access denied"),
}
200: PolicyTestResultSerializer(),
404: OpenApiResponse(description="for_user user not found"),
},
)
@action(detail=True, methods=["GET"])
@action(detail=True, methods=["POST"])
# pylint: disable=unused-argument
def check_access(self, request: Request, slug: str) -> Response:
"""Check access to a single application by slug"""
# Don't use self.get_object as that checks for view_application permission
# which the user might not have, even if they have access
application = get_object_or_404(Application, slug=slug)
engine = PolicyEngine(application, self.request.user, self.request)
# If the current user is superuser, they can set `for_user`
for_user = self.request.user
if self.request.user.is_superuser and "for_user" in request.data:
for_user = get_object_or_404(User, pk=request.data.get("for_user"))
engine = PolicyEngine(application, for_user, self.request)
engine.build()
if engine.passing:
return Response(status=204)
return Response(status=403)
result = engine.result
response = PolicyTestResultSerializer(PolicyResult(False))
if result.passing:
response = PolicyTestResultSerializer(PolicyResult(True))
if self.request.user.is_superuser:
response = PolicyTestResultSerializer(result)
return Response(response.data)
@extend_schema(
parameters=[

View file

@ -26,20 +26,26 @@ class TestApplicationsAPI(APITestCase):
def test_check_access(self):
"""Test check_access operation"""
self.client.force_login(self.user)
response = self.client.get(
response = self.client.post(
reverse(
"authentik_api:application-check-access",
kwargs={"slug": self.allowed.slug},
)
)
self.assertEqual(response.status_code, 204)
response = self.client.get(
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content), {"messages": [], "passing": True}
)
response = self.client.post(
reverse(
"authentik_api:application-check-access",
kwargs={"slug": self.denied.slug},
)
)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content), {"messages": ["dummy"], "passing": False}
)
def test_list(self):
"""Test list operation without superuser_full_list"""

View file

@ -74,8 +74,8 @@ func (pi *ProviderInstance) Bind(username string, bindDN, bindPW string, conn ne
if !passed {
return ldap.LDAPResultInvalidCredentials, nil
}
r, err := apiClient.CoreApi.CoreApplicationsCheckAccessRetrieve(context.Background(), pi.appSlug).Execute()
if r.StatusCode == 403 {
p, _, err := apiClient.CoreApi.CoreApplicationsCheckAccessCreate(context.Background(), pi.appSlug).Execute()
if !p.Passing {
pi.log.WithField("bindDN", bindDN).Info("Access denied for user")
return ldap.LDAPResultInsufficientAccessRights, nil
}

View file

@ -1368,8 +1368,8 @@ paths:
'403':
$ref: '#/components/schemas/GenericError'
/api/v2beta/core/applications/{slug}/check_access/:
get:
operationId: core_applications_check_access_retrieve
post:
operationId: core_applications_check_access_create
description: Check access to a single application by slug
parameters:
- in: path
@ -1380,16 +1380,33 @@ paths:
required: true
tags:
- core
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CheckAccessRequestRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/CheckAccessRequestRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/CheckAccessRequestRequest'
security:
- authentik: []
- cookieAuth: []
responses:
'204':
description: Access granted
'403':
description: Access denied
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PolicyTestResult'
description: ''
'404':
description: for_user user not found
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/api/v2beta/core/applications/{slug}/metrics/:
get:
operationId: core_applications_metrics_list
@ -16120,6 +16137,11 @@ components:
- shell
- redirect
type: string
CheckAccessRequestRequest:
type: object
properties:
for_user:
type: integer
ClientTypeEnum:
enum:
- confidential