Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens Langhammer
2026-06-11 17:05:22 +02:00
parent 8e5bf5a682
commit 31340afd15
8 changed files with 293 additions and 10 deletions
+6 -1
View File
@@ -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 = (
+51
View File
@@ -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)
View File
+74
View File
@@ -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)
+3 -2
View File
@@ -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
+112
View File
@@ -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<runtime.RequestOpts> {
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<runtime.ApiResponse<PaginatedApplicationList>> {
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<PaginatedApplicationList> {
const response = await this.coreApplicationsRequestableListRaw(
requestParameters,
initOverrides,
);
return await response.value();
}
/**
* Creates request options for coreApplicationsRetrieve without sending the request
*/
+45
View File
@@ -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
+2 -7
View File
@@ -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() {