Skip to content

Commit

Permalink
Merge branch 'main' into web/bug/search-select-focus-issue
Browse files Browse the repository at this point in the history
* main: (37 commits)
  release: 2024.6.3
  website/docs: prepare 2024.6.3 release notes (#10775)
  website/scripts: updated readme, added docsmg.env file  (#10710)
  web: bump API Client version (#10777)
  root: fix opencontainers ref (#10776)
  core: applications api: properly select provider (#10373)
  root: remove warnings (#10774)
  enterprise/rac: fix error when listing connection tokens as non-superuser (#10771)
  core: bump golang.org/x/oauth2 from 0.21.0 to 0.22.0 (#10754)
  core: bump goauthentik.io/api/v3 from 3.2024062.1 to 3.2024062.2 (#10753)
  core: bump golang.org/x/sync from 0.7.0 to 0.8.0 (#10755)
  web: bump the rollup group across 1 directory with 3 updates (#10756)
  web: bump core-js from 3.37.1 to 3.38.0 in /web (#10757)
  web: bump @swc/core from 1.7.4 to 1.7.6 in /web/sfe (#10758)
  web: bump rollup from 4.19.2 to 4.20.0 in /web/sfe (#10759)
  core: bump black from 24.4.2 to 24.8.0 (#10760)
  core: bump msgraph-sdk from 1.5.3 to 1.5.4 (#10761)
  core: bump coverage from 7.6.0 to 7.6.1 (#10762)
  core: bump ruff from 0.5.5 to 0.5.6 (#10763)
  core: bump django-filter from 24.2 to 24.3 (#10764)
  ...
  • Loading branch information
kensternberg-authentik committed Aug 5, 2024
2 parents 99439dd + 280fbf4 commit c87da0f
Show file tree
Hide file tree
Showing 65 changed files with 4,856 additions and 666 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.6.2
current_version = 2024.6.3
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ jobs:
with:
image-name: ghcr.io/goauthentik/dev-server
- name: Comment on PR
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
uses: ./.github/actions/comment-pr-instructions
with:
tag: ${{ steps.ev.outputs.imageMainTag }}
15 changes: 8 additions & 7 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@
"todo-tree.tree.showCountsInTree": true,
"todo-tree.tree.showBadges": true,
"yaml.customTags": [
"!Find sequence",
"!KeyOf scalar",
"!Context scalar",
"!Context sequence",
"!Format sequence",
"!Condition sequence",
"!Env sequence",
"!Context scalar",
"!Enumerate sequence",
"!Env scalar",
"!If sequence"
"!Find sequence",
"!Format sequence",
"!If sequence",
"!Index scalar",
"!KeyOf scalar",
"!Value scalar"
],
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.importModuleSpecifierEnding": "index",
Expand Down
2 changes: 1 addition & 1 deletion authentik/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from os import environ

__version__ = "2024.6.2"
__version__ = "2024.6.3"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"


Expand Down
25 changes: 23 additions & 2 deletions authentik/blueprints/management/commands/make_blueprint_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,19 @@ def build(self):
)
model_path = f"{model._meta.app_label}.{model._meta.model_name}"
self.schema["properties"]["entries"]["items"]["oneOf"].append(
self.template_entry(model_path, serializer)
self.template_entry(model_path, model, serializer)
)

def template_entry(self, model_path: str, serializer: Serializer) -> dict:
def template_entry(self, model_path: str, model: type[Model], serializer: Serializer) -> dict:
"""Template entry for a single model"""
model_schema = self.to_jsonschema(serializer)
model_schema["required"] = []
def_name = f"model_{model_path}"
def_path = f"#/$defs/{def_name}"
self.schema["$defs"][def_name] = model_schema
def_name_perm = f"model_{model_path}_permissions"
def_path_perm = f"#/$defs/{def_name_perm}"
self.schema["$defs"][def_name_perm] = self.model_permissions(model)
return {
"type": "object",
"required": ["model", "identifiers"],
Expand All @@ -135,6 +138,7 @@ def template_entry(self, model_path: str, serializer: Serializer) -> dict:
"default": "present",
},
"conditions": {"type": "array", "items": {"type": "boolean"}},
"permissions": {"$ref": def_path_perm},
"attrs": {"$ref": def_path},
"identifiers": {"$ref": def_path},
},
Expand Down Expand Up @@ -185,3 +189,20 @@ def to_jsonschema(self, serializer: Serializer) -> dict:
if required:
result["required"] = required
return result

def model_permissions(self, model: type[Model]) -> dict:
perms = [x[0] for x in model._meta.permissions]
for action in model._meta.default_permissions:
perms.append(f"{action}_{model._meta.model_name}")
return {
"type": "array",
"items": {
"type": "object",
"required": ["permission"],
"properties": {
"permission": {"type": "string", "enum": perms},
"user": {"type": "integer"},
"role": {"type": "string"},
},
},
}
24 changes: 24 additions & 0 deletions authentik/blueprints/tests/fixtures/rbac_object.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
version: 1
entries:
- model: authentik_core.user
id: user
identifiers:
username: "%(id)s"
attrs:
name: "%(id)s"
- model: authentik_rbac.role
id: role
identifiers:
name: "%(id)s"
- model: authentik_flows.flow
identifiers:
slug: "%(id)s"
attrs:
designation: authentication
name: foo
title: foo
permissions:
- permission: view_flow
user: !KeyOf user
- permission: view_flow
role: !KeyOf role
8 changes: 8 additions & 0 deletions authentik/blueprints/tests/fixtures/rbac_role.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: 1
entries:
- model: authentik_rbac.role
identifiers:
name: "%(id)s"
attrs:
permissions:
- authentik_blueprints.view_blueprintinstance
9 changes: 9 additions & 0 deletions authentik/blueprints/tests/fixtures/rbac_user.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: 1
entries:
- model: authentik_core.user
identifiers:
username: "%(id)s"
attrs:
name: "%(id)s"
permissions:
- authentik_blueprints.view_blueprintinstance
57 changes: 57 additions & 0 deletions authentik/blueprints/tests/test_v1_rbac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Test blueprints v1"""

from django.test import TransactionTestCase
from guardian.shortcuts import get_perms

from authentik.blueprints.v1.importer import Importer
from authentik.core.models import User
from authentik.flows.models import Flow
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
from authentik.rbac.models import Role


class TestBlueprintsV1RBAC(TransactionTestCase):
"""Test Blueprints rbac attribute"""

def test_user_permission(self):
"""Test permissions"""
uid = generate_id()
import_yaml = load_fixture("fixtures/rbac_user.yaml", id=uid)

importer = Importer.from_string(import_yaml)
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
user = User.objects.filter(username=uid).first()
self.assertIsNotNone(user)
self.assertTrue(user.has_perms(["authentik_blueprints.view_blueprintinstance"]))

def test_role_permission(self):
"""Test permissions"""
uid = generate_id()
import_yaml = load_fixture("fixtures/rbac_role.yaml", id=uid)

importer = Importer.from_string(import_yaml)
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
role = Role.objects.filter(name=uid).first()
self.assertIsNotNone(role)
self.assertEqual(
list(role.group.permissions.all().values_list("codename", flat=True)),
["view_blueprintinstance"],
)

def test_object_permission(self):
"""Test permissions"""
uid = generate_id()
import_yaml = load_fixture("fixtures/rbac_object.yaml", id=uid)

importer = Importer.from_string(import_yaml)
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
flow = Flow.objects.filter(slug=uid).first()
user = User.objects.filter(username=uid).first()
role = Role.objects.filter(name=uid).first()
self.assertIsNotNone(flow)
self.assertEqual(get_perms(user, flow), ["view_flow"])
self.assertEqual(get_perms(role.group, flow), ["view_flow"])
23 changes: 22 additions & 1 deletion authentik/blueprints/v1/common.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""transfer common classes"""

from collections import OrderedDict
from collections.abc import Iterable, Mapping
from collections.abc import Generator, Iterable, Mapping
from copy import copy
from dataclasses import asdict, dataclass, field, is_dataclass
from enum import Enum
Expand Down Expand Up @@ -58,6 +58,15 @@ class BlueprintEntryDesiredState(Enum):
MUST_CREATED = "must_created"


@dataclass
class BlueprintEntryPermission:
"""Describe object-level permissions"""

permission: Union[str, "YAMLTag"]
user: Union[int, "YAMLTag", None] = field(default=None)
role: Union[str, "YAMLTag", None] = field(default=None)


@dataclass
class BlueprintEntry:
"""Single entry of a blueprint"""
Expand All @@ -69,6 +78,7 @@ class BlueprintEntry:
conditions: list[Any] = field(default_factory=list)
identifiers: dict[str, Any] = field(default_factory=dict)
attrs: dict[str, Any] | None = field(default_factory=dict)
permissions: list[BlueprintEntryPermission] = field(default_factory=list)

id: str | None = None

Expand Down Expand Up @@ -150,6 +160,17 @@ def get_model(self, blueprint: "Blueprint") -> str:
"""Get the blueprint model, with yaml tags resolved if present"""
return str(self.tag_resolver(self.model, blueprint))

def get_permissions(
self, blueprint: "Blueprint"
) -> Generator[BlueprintEntryPermission, None, None]:
"""Get permissions of this entry, with all yaml tags resolved"""
for perm in self.permissions:
yield BlueprintEntryPermission(
permission=self.tag_resolver(perm.permission, blueprint),
user=self.tag_resolver(perm.user, blueprint),
role=self.tag_resolver(perm.role, blueprint),
)

def check_all_conditions_match(self, blueprint: "Blueprint") -> bool:
"""Check all conditions of this entry match (evaluate to True)"""
return all(self.tag_resolver(self.conditions, blueprint))
Expand Down
29 changes: 28 additions & 1 deletion authentik/blueprints/v1/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from django.db.transaction import atomic
from django.db.utils import IntegrityError
from guardian.models import UserObjectPermission
from guardian.shortcuts import assign_perm
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import BaseSerializer, Serializer
from structlog.stdlib import BoundLogger, get_logger
Expand All @@ -35,6 +36,7 @@
PropertyMapping,
Provider,
Source,
User,
UserSourceConnection,
)
from authentik.enterprise.license import LicenseKey
Expand All @@ -54,11 +56,13 @@
from authentik.flows.models import FlowToken, Stage
from authentik.lib.models import SerializerModel
from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.reflection import get_apps
from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel
from authentik.policies.reputation.models import Reputation
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
from authentik.rbac.models import Role
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
from authentik.stages.authenticator_webauthn.models import WebAuthnDeviceType
from authentik.tenants.models import Tenant
Expand Down Expand Up @@ -136,6 +140,16 @@ def transaction_rollback():
pass


def rbac_models() -> dict:
models = {}
for app in get_apps():
for model in app.get_models():
if not is_model_allowed(model):
continue
models[model._meta.model_name] = app.label
return models


class Importer:
"""Import Blueprint from raw dict or YAML/JSON"""

Expand All @@ -154,7 +168,10 @@ def __init__(self, blueprint: Blueprint, context: dict | None = None):

def default_context(self):
"""Default context"""
return {"goauthentik.io/enterprise/licensed": LicenseKey.get_total().is_valid()}
return {
"goauthentik.io/enterprise/licensed": LicenseKey.get_total().is_valid(),
"goauthentik.io/rbac/models": rbac_models(),
}

@staticmethod
def from_string(yaml_input: str, context: dict | None = None) -> "Importer":
Expand Down Expand Up @@ -320,6 +337,15 @@ def _validate_single(self, entry: BlueprintEntry) -> BaseSerializer | None:
) from exc
return serializer

def _apply_permissions(self, instance: Model, entry: BlueprintEntry):
"""Apply object-level permissions for an entry"""
for perm in entry.get_permissions(self._import):
if perm.user is not None:
assign_perm(perm.permission, User.objects.get(pk=perm.user), instance)
if perm.role is not None:
role = Role.objects.get(pk=perm.role)
role.assign_permission(perm.permission, obj=instance)

def apply(self) -> bool:
"""Apply (create/update) models yaml, in database transaction"""
try:
Expand Down Expand Up @@ -384,6 +410,7 @@ def _apply_models(self, raise_errors=False) -> bool:
if "pk" in entry.identifiers:
self.__pk_map[entry.identifiers["pk"]] = instance.pk
entry._state = BlueprintEntryState(instance)
self._apply_permissions(instance, entry)
elif state == BlueprintEntryDesiredState.ABSENT:
instance: Model | None = serializer.instance
if instance.pk:
Expand Down
29 changes: 28 additions & 1 deletion authentik/core/api/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,12 @@ class Meta:
class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Application Viewset"""

queryset = Application.objects.all().prefetch_related("provider").prefetch_related("policies")
queryset = (
Application.objects.all()
.with_provider()
.prefetch_related("policies")
.prefetch_related("backchannel_providers")
)
serializer_class = ApplicationSerializer
search_fields = [
"name",
Expand Down Expand Up @@ -147,6 +152,15 @@ def _get_allowed_applications(
applications.append(application)
return applications

def _filter_applications_with_launch_url(
self, pagined_apps: Iterator[Application]
) -> list[Application]:
applications = []
for app in pagined_apps:
if app.get_launch_url():
applications.append(app)
return applications

@extend_schema(
parameters=[
OpenApiParameter(
Expand Down Expand Up @@ -204,6 +218,11 @@ def check_access(self, request: Request, slug: str) -> Response:
location=OpenApiParameter.QUERY,
type=OpenApiTypes.INT,
),
OpenApiParameter(
name="only_with_launch_url",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
),
]
)
def list(self, request: Request) -> Response:
Expand All @@ -216,6 +235,10 @@ def list(self, request: Request) -> Response:
if superuser_full_list and request.user.is_superuser:
return super().list(request)

only_with_launch_url = str(
request.query_params.get("only_with_launch_url", "false")
).lower()

queryset = self._filter_queryset_for_list(self.get_queryset())
paginator: Pagination = self.paginator
paginated_apps = paginator.paginate_queryset(queryset, request)
Expand Down Expand Up @@ -251,6 +274,10 @@ def list(self, request: Request) -> Response:
allowed_applications,
timeout=86400,
)

if only_with_launch_url == "true":
allowed_applications = self._filter_applications_with_launch_url(allowed_applications)

serializer = self.get_serializer(allowed_applications, many=True)
return self.get_paginated_response(serializer.data)

Expand Down
Loading

0 comments on commit c87da0f

Please sign in to comment.