diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index a3de32ab1..ab76bc284 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -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=[ diff --git a/authentik/core/tests/test_applications_api.py b/authentik/core/tests/test_applications_api.py index 5ffc3579d..43c501a91 100644 --- a/authentik/core/tests/test_applications_api.py +++ b/authentik/core/tests/test_applications_api.py @@ -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""" diff --git a/outpost/pkg/ldap/instance_bind.go b/outpost/pkg/ldap/instance_bind.go index aaa8909ef..7fb193ac5 100644 --- a/outpost/pkg/ldap/instance_bind.go +++ b/outpost/pkg/ldap/instance_bind.go @@ -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 } diff --git a/schema.yml b/schema.yml index 852cbb2f8..09c03aa20 100644 --- a/schema.yml +++ b/schema.yml @@ -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