diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index 80151e2f6d..00a19a1c27 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -28,6 +28,7 @@ from authentik.core.api.utils import ModelSerializer, ThemedUrlsSerializer from authentik.core.apps import AppAccessWithoutBindings from authentik.core.models import Application, User from authentik.events.logs import LogEventSerializer, capture_logs +from authentik.lib.utils.reflection import ConditionalInheritance from authentik.policies.api.exec import PolicyTestResultSerializer from authentik.policies.engine import ListPolicyEngine, PolicyEngine from authentik.policies.types import CACHE_PREFIX, PolicyResult @@ -128,7 +129,11 @@ class ApplicationSerializer(ModelSerializer): } -class ApplicationViewSet(UsedByMixin, ModelViewSet): +class ApplicationViewSet( + ConditionalInheritance("authentik.pam.api.apps.ApplicationsRequestableMixin"), + UsedByMixin, + ModelViewSet, +): """Application Viewset""" queryset = ( diff --git a/authentik/pam/api/apps.py b/authentik/pam/api/apps.py new file mode 100644 index 0000000000..e1a8d63c79 --- /dev/null +++ b/authentik/pam/api/apps.py @@ -0,0 +1,51 @@ +from http import HTTPMethod + +from django.db.models import QuerySet +from drf_spectacular.utils import extend_schema +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.response import Response + +from authentik.api.pagination import Pagination +from authentik.core.api.applications import ApplicationSerializer +from authentik.core.apps import AppAccessWithoutBindings +from authentik.core.models import Application +from authentik.policies.engine import ListPolicyEngine +from authentik.policies.models import PolicyBinding + + +class ApplicationsRequestableMixin: + + queryset: QuerySet[Application] + + @extend_schema( + responses={ + 200: ApplicationSerializer(many=True), + }, + ) + @action(methods=[HTTPMethod.GET], detail=False) + def requestable(self, request: Request) -> Response: + """List applications which the current user can request access to""" + all_requestable_apps = self.queryset.filter(request_rules__isnull=False).prefetch_related( + "request_rules" + ) + + requestable_apps = [] + for app in all_requestable_apps: + print(app.request_rules.all()) + engine = ListPolicyEngine(app.request_rules.all()) + engine.empty_result = AppAccessWithoutBindings.get() + print(request.user) + print(PolicyBinding.objects.filter(user=request.user)) + print(PolicyBinding.objects.filter(target=app.request_rules.first())) + applicable_rules = list(engine.evaluate_for(request.user, request)) + print(applicable_rules) + if len(applicable_rules) > 0: + requestable_apps.append(app) + print(requestable_apps) + + paginator: Pagination = self.paginator + paginated_apps = paginator.paginate_queryset(requestable_apps, request) + + serializer = self.get_serializer(paginated_apps, many=True) + return self.get_paginated_response(serializer.data) diff --git a/authentik/pam/tests/__init__.py b/authentik/pam/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/authentik/pam/tests/test_apps.py b/authentik/pam/tests/test_apps.py new file mode 100644 index 0000000000..e798cab508 --- /dev/null +++ b/authentik/pam/tests/test_apps.py @@ -0,0 +1,74 @@ +from json import loads + +from django.urls import reverse +from rest_framework.test import APITestCase + +from authentik.core.models import Application +from authentik.core.tests.utils import create_test_user +from authentik.lib.generators import generate_id +from authentik.pam.models import PolicyBindingModelRequestRule +from authentik.policies.models import PolicyBinding + + +class AppRequestTests(APITestCase): + def setUp(self): + Application.objects.all().delete() + + def test_requestable_none(self): + user = create_test_user() + self.client.force_login(user) + res = self.client.get(reverse("authentik_api:application-requestable")) + content = loads(res.content.decode()) + self.assertEqual(content["pagination"]["count"], 0) + self.assertEqual(len(content["results"]), 0) + + def test_requestable_no_policy(self): + user = create_test_user() + self.client.force_login(user) + + app = Application.objects.create( + name=generate_id(), + slug=generate_id(), + ) + PolicyBindingModelRequestRule.objects.create(pbm=app) + + res = self.client.get(reverse("authentik_api:application-requestable")) + content = loads(res.content.decode()) + self.assertEqual(content["pagination"]["count"], 1) + self.assertEqual(len(content["results"]), 1) + self.assertEqual(content["results"][0]["slug"], app.slug) + + def test_requestable_no_access(self): + other_user = create_test_user() + + user = create_test_user() + self.client.force_login(user) + + app = Application.objects.create( + name=generate_id(), + slug=generate_id(), + ) + rule = PolicyBindingModelRequestRule.objects.create(pbm=app, name=generate_id()) + PolicyBinding.objects.create(target=rule, user=other_user, order=0) + + res = self.client.get(reverse("authentik_api:application-requestable")) + content = loads(res.content.decode()) + self.assertEqual(content["pagination"]["count"], 0) + self.assertEqual(len(content["results"]), 0) + + def test_requestable_access(self): + user = create_test_user() + self.client.force_login(user) + + app = Application.objects.create( + name=generate_id(), + slug=generate_id(), + ) + rule = PolicyBindingModelRequestRule.objects.create(pbm=app, name=generate_id()) + PolicyBinding.objects.create(target=rule, user=user, order=0) + + res = self.client.get(reverse("authentik_api:application-requestable")) + content = loads(res.content.decode()) + self.assertEqual(content["pagination"]["count"], 1) + self.assertEqual(len(content["results"]), 1) + self.assertEqual(content["results"][0]["slug"], app.slug) diff --git a/authentik/policies/engine.py b/authentik/policies/engine.py index 25e01d3197..90e817e636 100644 --- a/authentik/policies/engine.py +++ b/authentik/policies/engine.py @@ -233,9 +233,9 @@ class PolicyEngine: return self.result.passing -class ListPolicyEngine: +class ListPolicyEngine[T: PolicyBindingModel]: - def __init__(self, objs: QuerySet[PolicyBindingModel]): + def __init__(self, objs: QuerySet[T]): self.qs = objs self.empty_result = True @@ -244,5 +244,6 @@ class ListPolicyEngine: engine = PolicyEngine(obj, request.user, request) engine.empty_result = self.empty_result engine.build() + print(engine.result) if engine.passing: yield obj diff --git a/packages/client-ts/src/apis/CoreApi.ts b/packages/client-ts/src/apis/CoreApi.ts index 2740ca957e..27318bda38 100644 --- a/packages/client-ts/src/apis/CoreApi.ts +++ b/packages/client-ts/src/apis/CoreApi.ts @@ -195,6 +195,19 @@ export interface CoreApplicationsPartialUpdateRequest { patchedApplicationRequest?: PatchedApplicationRequest; } +export interface CoreApplicationsRequestableListRequest { + group?: string; + metaDescription?: string; + metaLaunchUrl?: string; + metaPublisher?: string; + name?: string; + ordering?: string; + page?: number; + pageSize?: number; + search?: string; + slug?: string; +} + export interface CoreApplicationsRetrieveRequest { slug: string; } @@ -1428,6 +1441,105 @@ export class CoreApi extends runtime.BaseAPI { return await response.value(); } + /** + * Creates request options for coreApplicationsRequestableList without sending the request + */ + async coreApplicationsRequestableListRequestOpts( + requestParameters: CoreApplicationsRequestableListRequest, + ): Promise { + const queryParameters: any = {}; + + if (requestParameters["group"] != null) { + queryParameters["group"] = requestParameters["group"]; + } + + if (requestParameters["metaDescription"] != null) { + queryParameters["meta_description"] = requestParameters["metaDescription"]; + } + + if (requestParameters["metaLaunchUrl"] != null) { + queryParameters["meta_launch_url"] = requestParameters["metaLaunchUrl"]; + } + + if (requestParameters["metaPublisher"] != null) { + queryParameters["meta_publisher"] = requestParameters["metaPublisher"]; + } + + if (requestParameters["name"] != null) { + queryParameters["name"] = requestParameters["name"]; + } + + if (requestParameters["ordering"] != null) { + queryParameters["ordering"] = requestParameters["ordering"]; + } + + if (requestParameters["page"] != null) { + queryParameters["page"] = requestParameters["page"]; + } + + if (requestParameters["pageSize"] != null) { + queryParameters["page_size"] = requestParameters["pageSize"]; + } + + if (requestParameters["search"] != null) { + queryParameters["search"] = requestParameters["search"]; + } + + if (requestParameters["slug"] != null) { + queryParameters["slug"] = requestParameters["slug"]; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.accessToken) { + const token = this.configuration.accessToken; + const tokenString = await token("authentik", []); + + if (tokenString) { + headerParameters["Authorization"] = `Bearer ${tokenString}`; + } + } + + let urlPath = `/core/applications/requestable/`; + + return { + path: urlPath, + method: "GET", + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * List applications which the current user can request access to + */ + async coreApplicationsRequestableListRaw( + requestParameters: CoreApplicationsRequestableListRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const requestOptions = + await this.coreApplicationsRequestableListRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => + PaginatedApplicationListFromJSON(jsonValue), + ); + } + + /** + * List applications which the current user can request access to + */ + async coreApplicationsRequestableList( + requestParameters: CoreApplicationsRequestableListRequest = {}, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.coreApplicationsRequestableListRaw( + requestParameters, + initOverrides, + ); + return await response.value(); + } + /** * Creates request options for coreApplicationsRetrieve without sending the request */ diff --git a/schema.yml b/schema.yml index dab478f726..0d920bfad8 100644 --- a/schema.yml +++ b/schema.yml @@ -2975,6 +2975,51 @@ paths: $ref: '#/components/responses/ValidationErrorResponse' '403': $ref: '#/components/responses/GenericErrorResponse' + /core/applications/requestable/: + get: + operationId: core_applications_requestable_list + description: List applications which the current user can request access to + parameters: + - in: query + name: group + schema: + type: string + - in: query + name: meta_description + schema: + type: string + - in: query + name: meta_launch_url + schema: + type: string + - in: query + name: meta_publisher + schema: + type: string + - $ref: '#/components/parameters/QueryName' + - $ref: '#/components/parameters/QueryPaginationOrdering' + - $ref: '#/components/parameters/QueryPaginationPage' + - $ref: '#/components/parameters/QueryPaginationPageSize' + - $ref: '#/components/parameters/QuerySearch' + - in: query + name: slug + schema: + type: string + tags: + - core + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedApplicationList' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' /core/authenticated_sessions/: get: operationId: core_authenticated_sessions_list diff --git a/web/src/user/discover/DiscoverPage.ts b/web/src/user/discover/DiscoverPage.ts index c6ffbfbc8d..49431fb398 100644 --- a/web/src/user/discover/DiscoverPage.ts +++ b/web/src/user/discover/DiscoverPage.ts @@ -15,6 +15,7 @@ import PFContent from "@patternfly/patternfly/components/Content/content.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css"; import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; import PFPage from "@patternfly/patternfly/components/Page/page.css"; +import { aki } from "#common/api/client"; @customElement("ak-discovery") export class DiscoverPage extends AKElement { @@ -26,13 +27,7 @@ export class DiscoverPage extends AKElement { public override connectedCallback(): void { super.connectedCallback(); - new CoreApi(DEFAULT_CONFIG) - .coreApplicationsList({ - superuserFullList: true, - }) - .then((apps) => { - this.apps = apps; - }); + aki(CoreApi).coreApplicationsRequestableList({}).then(apps => this.apps = apps); } render() {