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:
		@ -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=[
 | 
				
			||||||
 | 
				
			|||||||
@ -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"""
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										34
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								schema.yml
									
									
									
									
									
								
							@ -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
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user