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

View File

@ -26,20 +26,26 @@ class TestApplicationsAPI(APITestCase):
def test_check_access(self): def test_check_access(self):
"""Test check_access operation""" """Test check_access operation"""
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get( response = self.client.post(
reverse( reverse(
"authentik_api:application-check-access", "authentik_api:application-check-access",
kwargs={"slug": self.allowed.slug}, kwargs={"slug": self.allowed.slug},
) )
) )
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, 200)
response = self.client.get( self.assertJSONEqual(
force_str(response.content), {"messages": [], "passing": True}
)
response = self.client.post(
reverse( reverse(
"authentik_api:application-check-access", "authentik_api:application-check-access",
kwargs={"slug": self.denied.slug}, 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): def test_list(self):
"""Test list operation without superuser_full_list""" """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 { if !passed {
return ldap.LDAPResultInvalidCredentials, nil return ldap.LDAPResultInvalidCredentials, nil
} }
r, err := apiClient.CoreApi.CoreApplicationsCheckAccessRetrieve(context.Background(), pi.appSlug).Execute() p, _, err := apiClient.CoreApi.CoreApplicationsCheckAccessCreate(context.Background(), pi.appSlug).Execute()
if r.StatusCode == 403 { if !p.Passing {
pi.log.WithField("bindDN", bindDN).Info("Access denied for user") pi.log.WithField("bindDN", bindDN).Info("Access denied for user")
return ldap.LDAPResultInsufficientAccessRights, nil return ldap.LDAPResultInsufficientAccessRights, nil
} }

View File

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