Compare commits

..

5 Commits

Author SHA1 Message Date
f0256a0535 As alway, prettier has opinions 2024-07-16 14:04:23 -07:00
142a985914 Tightened the language. 2024-07-16 13:56:53 -07:00
a8531d498a Tests are updated and working. Had to revise the 'search-select' binding to work with the new search-select. 2024-07-16 13:49:17 -07:00
f8cb4e880b web: roll back update to sonar
Bloody dependabot.  We're not compatible with ESLint 9 yet, darnit, and yet
dependabot keeps pushing upgrades on us.
2024-07-16 09:23:28 -07:00
3ced637db3 web: grammar fix and lint update
1. Merged the two SonarJS lints into one
2. Fixed a grammatical error in RedirectStage
2024-07-16 09:19:57 -07:00
732 changed files with 12296 additions and 32577 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.6.3
current_version = 2024.6.1
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*))?

View File

@ -29,15 +29,9 @@ outputs:
imageTags:
description: "Docker image tags"
value: ${{ steps.ev.outputs.imageTags }}
imageNames:
description: "Docker image names"
value: ${{ steps.ev.outputs.imageNames }}
imageMainTag:
description: "Docker image main tag"
value: ${{ steps.ev.outputs.imageMainTag }}
imageMainName:
description: "Docker image main name"
value: ${{ steps.ev.outputs.imageMainName }}
runs:
using: "composite"

View File

@ -7,7 +7,7 @@ from time import time
parser = configparser.ConfigParser()
parser.read(".bumpversion.cfg")
should_build = str(len(os.environ.get("DOCKER_USERNAME", "")) > 0).lower()
should_build = str(os.environ.get("DOCKER_USERNAME", None) is not None).lower()
branch_name = os.environ["GITHUB_REF"]
if os.environ.get("GITHUB_HEAD_REF", "") != "":
@ -50,9 +50,8 @@ else:
f"{name}:gh-{safe_branch_name}-{int(time())}-{sha[:7]}{suffix}", # Use by FluxCD
]
image_main_tag = image_tags[0].split(":")[-1]
image_main_tag = image_tags[0]
image_tags_rendered = ",".join(image_tags)
image_names_rendered = ",".join(set(name.split(":")[0] for name in image_tags))
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
print(f"shouldBuild={should_build}", file=_output)
@ -60,6 +59,4 @@ with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
print(f"version={version}", file=_output)
print(f"prerelease={prerelease}", file=_output)
print(f"imageTags={image_tags_rendered}", file=_output)
print(f"imageNames={image_names_rendered}", file=_output)
print(f"imageMainTag={image_main_tag}", file=_output)
print(f"imageMainName={image_tags[0]}", file=_output)

View File

@ -35,8 +35,8 @@ jobs:
run: |
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
npm i @goauthentik/api@$VERSION
- name: Upgrade /web/packages/sfe
working-directory: web/packages/sfe
- name: Upgrade /web/sfe
working-directory: web/sfe
run: |
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
npm i @goauthentik/api@$VERSION

View File

@ -213,16 +213,13 @@ jobs:
permissions:
# Needed to upload contianer images to ghcr.io
packages: write
# Needed for attestation
id-token: write
attestations: write
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0
uses: docker/setup-qemu-action@v3.1.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
@ -244,7 +241,6 @@ jobs:
run: make gen-client-ts
- name: Build Docker Image
uses: docker/build-push-action@v6
id: push
with:
context: .
secrets: |
@ -255,15 +251,8 @@ jobs:
build-args: |
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache
cache-to: ${{ steps.ev.outputs.shouldBuild == 'true' && 'type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache,mode=max' || '' }}
cache-to: type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache,mode=max
platforms: linux/${{ matrix.arch }}
- uses: actions/attest-build-provenance@v1
id: attest
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
with:
subject-name: ${{ steps.ev.outputs.imageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
pr-comment:
needs:
- build
@ -285,7 +274,6 @@ 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 }}
tag: gh-${{ steps.ev.outputs.imageMainTag }}

View File

@ -71,15 +71,12 @@ jobs:
permissions:
# Needed to upload contianer images to ghcr.io
packages: write
# Needed for attestation
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0
uses: docker/setup-qemu-action@v3.1.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
@ -99,7 +96,6 @@ jobs:
- name: Generate API
run: make gen-client-go
- name: Build Docker Image
id: push
uses: docker/build-push-action@v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
@ -110,14 +106,7 @@ jobs:
platforms: linux/amd64,linux/arm64
context: .
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type }}:buildcache
cache-to: ${{ steps.ev.outputs.shouldBuild == 'true' && format('type=registry,ref=ghcr.io/goauthentik/dev-{0}:buildcache,mode=max', matrix.type) || '' }}
- uses: actions/attest-build-provenance@v1
id: attest
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
with:
subject-name: ${{ steps.ev.outputs.imageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
cache-to: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type }}:buildcache,mode=max
build-binary:
timeout-minutes: 120
needs:

View File

@ -28,8 +28,15 @@ jobs:
include:
- command: tsc
project: web
extra_setup: |
cd sfe/ && npm ci
- command: lit-analyse
project: web
extra_setup: |
# lit-analyse doesn't understand path rewrites, so make it
# belive it's an actual module
cd node_modules/@goauthentik
ln -s ../../src/ web
exclude:
- command: lint:lockfile
project: tests/wdio

View File

@ -11,13 +11,10 @@ jobs:
permissions:
# Needed to upload contianer images to ghcr.io
packages: write
# Needed for attestation
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0
uses: docker/setup-qemu-action@v3.1.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
@ -44,7 +41,6 @@ jobs:
mkdir -p ./gen-go-api
- name: Build Docker Image
uses: docker/build-push-action@v6
id: push
with:
context: .
push: true
@ -53,20 +49,11 @@ jobs:
GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }}
tags: ${{ steps.ev.outputs.imageTags }}
platforms: linux/amd64,linux/arm64
- uses: actions/attest-build-provenance@v1
id: attest
with:
subject-name: ${{ steps.ev.outputs.imageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
build-outpost:
runs-on: ubuntu-latest
permissions:
# Needed to upload contianer images to ghcr.io
packages: write
# Needed for attestation
id-token: write
attestations: write
strategy:
fail-fast: false
matrix:
@ -81,7 +68,7 @@ jobs:
with:
go-version-file: "go.mod"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0
uses: docker/setup-qemu-action@v3.1.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
@ -108,19 +95,12 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
uses: docker/build-push-action@v6
id: push
with:
push: true
tags: ${{ steps.ev.outputs.imageTags }}
file: ${{ matrix.type }}.Dockerfile
platforms: linux/amd64,linux/arm64
context: .
- uses: actions/attest-build-provenance@v1
id: attest
with:
subject-name: ${{ steps.ev.outputs.imageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
build-outpost-binary:
timeout-minutes: 120
runs-on: ubuntu-latest
@ -198,8 +178,8 @@ jobs:
image-name: ghcr.io/goauthentik/server
- name: Get static files from docker image
run: |
docker pull ${{ steps.ev.outputs.imageMainName }}
container=$(docker container create ${{ steps.ev.outputs.imageMainName }})
docker pull ${{ steps.ev.outputs.imageMainTag }}
container=$(docker container create ${{ steps.ev.outputs.imageMainTag }})
docker cp ${container}:web/ .
- name: Create a Sentry.io release
uses: getsentry/action-release@v1

View File

@ -16,6 +16,6 @@
"ms-python.black-formatter",
"redhat.vscode-yaml",
"Tobermory.es6-string-html",
"unifiedjs.vscode-mdx"
"unifiedjs.vscode-mdx",
]
}

2
.vscode/launch.json vendored
View File

@ -22,6 +22,6 @@
},
"justMyCode": true,
"django": true
}
},
]
}

21
.vscode/settings.json vendored
View File

@ -18,21 +18,20 @@
"sso",
"totp",
"traefik",
"webauthn"
"webauthn",
],
"todo-tree.tree.showCountsInTree": true,
"todo-tree.tree.showBadges": true,
"yaml.customTags": [
"!Condition sequence",
"!Context scalar",
"!Enumerate sequence",
"!Env scalar",
"!Find sequence",
"!Format sequence",
"!If sequence",
"!Index scalar",
"!KeyOf scalar",
"!Value scalar"
"!Context scalar",
"!Context sequence",
"!Format sequence",
"!Condition sequence",
"!Env sequence",
"!Env scalar",
"!If sequence"
],
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.importModuleSpecifierEnding": "index",
@ -49,7 +48,9 @@
"ignoreCase": false
}
],
"go.testFlags": ["-count=1"],
"go.testFlags": [
"-count=1"
],
"github-actions.workflows.pinned.workflows": [
".github/workflows/ci-main.yml"
]

62
.vscode/tasks.json vendored
View File

@ -2,67 +2,85 @@
"version": "2.0.0",
"tasks": [
{
"label": "authentik/core: make",
"label": "authentik[core]: format & test",
"command": "poetry",
"args": ["run", "make", "lint-fix", "lint"],
"presentation": {
"panel": "new"
},
"group": "test"
"args": [
"run",
"make"
],
"group": "build",
},
{
"label": "authentik/core: run",
"label": "authentik[core]: run",
"command": "poetry",
"args": ["run", "ak", "server"],
"args": [
"run",
"make",
"run",
],
"group": "build",
"presentation": {
"panel": "dedicated",
"group": "running"
}
},
},
{
"label": "authentik/web: make",
"label": "authentik[web]: format",
"command": "make",
"args": ["web"],
"group": "build"
"group": "build",
},
{
"label": "authentik/web: watch",
"label": "authentik[web]: watch",
"command": "make",
"args": ["web-watch"],
"group": "build",
"presentation": {
"panel": "dedicated",
"group": "running"
}
},
},
{
"label": "authentik: install",
"command": "make",
"args": ["install", "-j4"],
"group": "build"
"args": ["install"],
"group": "build",
},
{
"label": "authentik/website: make",
"label": "authentik: i18n-extract",
"command": "poetry",
"args": [
"run",
"make",
"i18n-extract"
],
"group": "build",
},
{
"label": "authentik[website]: format",
"command": "make",
"args": ["website"],
"group": "build"
"group": "build",
},
{
"label": "authentik/website: watch",
"label": "authentik[website]: watch",
"command": "make",
"args": ["website-watch"],
"group": "build",
"presentation": {
"panel": "dedicated",
"group": "running"
}
},
},
{
"label": "authentik/api: generate",
"label": "authentik[api]: generate",
"command": "poetry",
"args": ["run", "make", "gen"],
"args": [
"run",
"make",
"gen"
],
"group": "build"
}
},
]
}

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# Stage 1: Build website
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 as website-builder
FROM --platform=${BUILDPLATFORM} docker.io/node:22 as website-builder
ENV NODE_ENV=production
@ -20,7 +20,7 @@ COPY ./SECURITY.md /work/
RUN npm run build-bundled
# Stage 2: Build webui
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 as web-builder
FROM --platform=${BUILDPLATFORM} docker.io/node:22 as web-builder
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
@ -30,9 +30,12 @@ WORKDIR /work/web
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
--mount=type=bind,target=/work/web/packages/sfe/package.json,src=./web/packages/sfe/package.json \
--mount=type=bind,target=/work/web/sfe/package.json,src=./web/sfe/package.json \
--mount=type=bind,target=/work/web/sfe/package-lock.json,src=./web/sfe/package-lock.json \
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
npm ci --include=dev && \
cd sfe && \
npm ci --include=dev
COPY ./package.json /work
@ -40,7 +43,9 @@ COPY ./web /work/web/
COPY ./website /work/website/
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build
RUN npm run build && \
cd sfe && \
npm run build
# Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.22-fips-bookworm AS go-builder

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2024.6.3"
__version__ = "2024.6.1"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -23,11 +23,9 @@ class Command(BaseCommand):
for blueprint_path in options.get("blueprints", []):
content = BlueprintInstance(path=blueprint_path).retrieve()
importer = Importer.from_string(content)
valid, logs = importer.validate()
valid, _ = importer.validate()
if not valid:
self.stderr.write("Blueprint invalid")
for log in logs:
self.stderr.write(f"\t{log.logger}: {log.event}: {log.attributes}")
self.stderr.write("blueprint invalid")
sys_exit(1)
importer.apply()

View File

@ -113,19 +113,16 @@ class Command(BaseCommand):
)
model_path = f"{model._meta.app_label}.{model._meta.model_name}"
self.schema["properties"]["entries"]["items"]["oneOf"].append(
self.template_entry(model_path, model, serializer)
self.template_entry(model_path, serializer)
)
def template_entry(self, model_path: str, model: type[Model], serializer: Serializer) -> dict:
def template_entry(self, model_path: str, 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"],
@ -138,7 +135,6 @@ class Command(BaseCommand):
"default": "present",
},
"conditions": {"type": "array", "items": {"type": "boolean"}},
"permissions": {"$ref": def_path_perm},
"attrs": {"$ref": def_path},
"identifiers": {"$ref": def_path},
},
@ -189,20 +185,3 @@ class Command(BaseCommand):
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"},
},
},
}

View File

@ -1,24 +0,0 @@
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

View File

@ -1,8 +0,0 @@
version: 1
entries:
- model: authentik_rbac.role
identifiers:
name: "%(id)s"
attrs:
permissions:
- authentik_blueprints.view_blueprintinstance

View File

@ -1,9 +0,0 @@
version: 1
entries:
- model: authentik_core.user
identifiers:
username: "%(id)s"
attrs:
name: "%(id)s"
permissions:
- authentik_blueprints.view_blueprintinstance

View File

@ -1,57 +0,0 @@
"""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"])

View File

@ -1,7 +1,7 @@
"""transfer common classes"""
from collections import OrderedDict
from collections.abc import Generator, Iterable, Mapping
from collections.abc import Iterable, Mapping
from copy import copy
from dataclasses import asdict, dataclass, field, is_dataclass
from enum import Enum
@ -58,15 +58,6 @@ 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"""
@ -78,7 +69,6 @@ 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
@ -160,17 +150,6 @@ class BlueprintEntry:
"""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))
@ -328,10 +307,7 @@ class Find(YAMLTag):
else:
model_name = self.model_name
try:
model_class = apps.get_model(*model_name.split("."))
except LookupError as exc:
raise EntryInvalidError.from_entry(exc, entry) from exc
model_class = apps.get_model(*model_name.split("."))
query = Q()
for cond in self.conditions:

View File

@ -16,7 +16,6 @@ from django.db.models.query_utils import Q
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
@ -33,11 +32,9 @@ from authentik.blueprints.v1.common import (
from authentik.blueprints.v1.meta.registry import BaseMetaModel, registry
from authentik.core.models import (
AuthenticatedSession,
GroupSourceConnection,
PropertyMapping,
Provider,
Source,
User,
UserSourceConnection,
)
from authentik.enterprise.license import LicenseKey
@ -57,13 +54,11 @@ from authentik.events.utils import cleanse_dict
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
@ -92,7 +87,6 @@ def excluded_models() -> list[type[Model]]:
Source,
PropertyMapping,
UserSourceConnection,
GroupSourceConnection,
Stage,
OutpostServiceConnection,
Policy,
@ -142,16 +136,6 @@ 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"""
@ -170,10 +154,7 @@ class Importer:
def default_context(self):
"""Default context"""
return {
"goauthentik.io/enterprise/licensed": LicenseKey.get_total().is_valid(),
"goauthentik.io/rbac/models": rbac_models(),
}
return {"goauthentik.io/enterprise/licensed": LicenseKey.get_total().is_valid()}
@staticmethod
def from_string(yaml_input: str, context: dict | None = None) -> "Importer":
@ -233,17 +214,14 @@ class Importer:
return main_query | sub_query
def _validate_single(self, entry: BlueprintEntry) -> BaseSerializer | None: # noqa: PLR0915
def _validate_single(self, entry: BlueprintEntry) -> BaseSerializer | None:
"""Validate a single entry"""
if not entry.check_all_conditions_match(self._import):
self.logger.debug("One or more conditions of this entry are not fulfilled, skipping")
return None
model_app_label, model_name = entry.get_model(self._import).split(".")
try:
model: type[SerializerModel] = registry.get_model(model_app_label, model_name)
except LookupError as exc:
raise EntryInvalidError.from_entry(exc, entry) from exc
model: type[SerializerModel] = registry.get_model(model_app_label, model_name)
# Don't use isinstance since we don't want to check for inheritance
if not is_model_allowed(model):
raise EntryInvalidError.from_entry(f"Model {model} not allowed", entry)
@ -318,7 +296,10 @@ class Importer:
try:
full_data = self.__update_pks_for_attrs(entry.get_attrs(self._import))
except ValueError as exc:
raise EntryInvalidError.from_entry(exc, entry) from exc
raise EntryInvalidError.from_entry(
exc,
entry,
) from exc
always_merger.merge(full_data, updated_identifiers)
serializer_kwargs["data"] = full_data
@ -339,15 +320,6 @@ class Importer:
) 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:
@ -412,7 +384,6 @@ class Importer:
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:

View File

@ -55,7 +55,6 @@ class BrandSerializer(ModelSerializer):
"flow_unenrollment",
"flow_user_settings",
"flow_device_code",
"default_application",
"web_certificate",
"attributes",
]

View File

@ -9,6 +9,3 @@ class AuthentikBrandsConfig(AppConfig):
name = "authentik.brands"
label = "authentik_brands"
verbose_name = "authentik Brands"
mountpoints = {
"authentik.brands.urls_root": "",
}

View File

@ -1,26 +0,0 @@
# Generated by Django 5.0.6 on 2024-07-04 20:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_brands", "0006_brand_authentik_b_domain_b9b24a_idx_and_more"),
("authentik_core", "0035_alter_group_options_and_more"),
]
operations = [
migrations.AddField(
model_name="brand",
name="default_application",
field=models.ForeignKey(
default=None,
help_text="When set, external users will be redirected to this application after authenticating.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="authentik_core.application",
),
),
]

View File

@ -3,7 +3,6 @@
from uuid import uuid4
from django.db import models
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
@ -52,16 +51,6 @@ class Brand(SerializerModel):
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_device_code"
)
default_application = models.ForeignKey(
"authentik_core.Application",
null=True,
default=None,
on_delete=models.SET_DEFAULT,
help_text=_(
"When set, external users will be redirected to this application after authenticating."
),
)
web_certificate = models.ForeignKey(
CertificateKeyPair,
null=True,
@ -99,13 +88,3 @@ class Brand(SerializerModel):
models.Index(fields=["domain"]),
models.Index(fields=["default"]),
]
class WebfingerProvider(models.Model):
"""Provider which supports webfinger discovery"""
class Meta:
abstract = True
def webfinger(self, resource: str, request: HttpRequest) -> dict:
raise NotImplementedError()

View File

@ -5,11 +5,7 @@ from rest_framework.test import APITestCase
from authentik.brands.api import Themes
from authentik.brands.models import Brand
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_brand
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.providers.saml.models import SAMLProvider
class TestBrands(APITestCase):
@ -79,45 +75,3 @@ class TestBrands(APITestCase):
reverse("authentik_api:brand-list"), data={"domain": "bar", "default": True}
)
self.assertEqual(response.status_code, 400)
def test_webfinger_no_app(self):
"""Test Webfinger"""
create_test_brand()
self.assertJSONEqual(
self.client.get(reverse("authentik_brands:webfinger")).content.decode(), {}
)
def test_webfinger_not_supported(self):
"""Test Webfinger"""
brand = create_test_brand()
provider = SAMLProvider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
brand.default_application = app
brand.save()
self.assertJSONEqual(
self.client.get(reverse("authentik_brands:webfinger")).content.decode(), {}
)
def test_webfinger_oidc(self):
"""Test Webfinger"""
brand = create_test_brand()
provider = OAuth2Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
brand.default_application = app
brand.save()
self.assertJSONEqual(
self.client.get(reverse("authentik_brands:webfinger")).content.decode(),
{
"links": [
{
"href": f"http://testserver/application/o/{app.slug}/",
"rel": "http://openid.net/specs/connect/1.0/issuer",
}
],
"subject": None,
},
)

View File

@ -1,9 +0,0 @@
"""authentik brand root URLs"""
from django.urls import path
from authentik.brands.views.webfinger import WebFingerView
urlpatterns = [
path(".well-known/webfinger", WebFingerView.as_view(), name="webfinger"),
]

View File

@ -5,7 +5,7 @@ from typing import Any
from django.db.models import F, Q
from django.db.models import Value as V
from django.http.request import HttpRequest
from sentry_sdk import get_current_span
from sentry_sdk.hub import Hub
from authentik import get_full_version
from authentik.brands.models import Brand
@ -33,7 +33,7 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
brand = getattr(request, "brand", DEFAULT_BRAND)
tenant = getattr(request, "tenant", Tenant())
trace = ""
span = get_current_span()
span = Hub.current.scope.span
if span:
trace = span.to_traceparent()
return {

View File

@ -1,29 +0,0 @@
from typing import Any
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.views import View
from authentik.brands.models import Brand, WebfingerProvider
from authentik.core.models import Application
class WebFingerView(View):
"""Webfinger endpoint"""
def get(self, request: HttpRequest) -> HttpResponse:
brand: Brand = request.brand
if not brand.default_application:
return JsonResponse({})
application: Application = brand.default_application
provider = application.get_provider()
if not provider or not isinstance(provider, WebfingerProvider):
return JsonResponse({})
webfinger_data = provider.webfinger(request.GET.get("resource"), request)
return JsonResponse(webfinger_data)
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
response = super().dispatch(request, *args, **kwargs)
# RFC7033 spec
response["Access-Control-Allow-Origin"] = "*"
response["Content-Type"] = "application/jrd+json"
return response

View File

@ -103,12 +103,7 @@ class ApplicationSerializer(ModelSerializer):
class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Application Viewset"""
queryset = (
Application.objects.all()
.with_provider()
.prefetch_related("policies")
.prefetch_related("backchannel_providers")
)
queryset = Application.objects.all().prefetch_related("provider").prefetch_related("policies")
serializer_class = ApplicationSerializer
search_fields = [
"name",
@ -152,15 +147,6 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
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(
@ -218,11 +204,6 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
location=OpenApiParameter.QUERY,
type=OpenApiTypes.INT,
),
OpenApiParameter(
name="only_with_launch_url",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
),
]
)
def list(self, request: Request) -> Response:
@ -235,10 +216,6 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
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)
@ -274,10 +251,6 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
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)

View File

@ -2,13 +2,7 @@
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework.fields import (
BooleanField,
CharField,
DateTimeField,
IntegerField,
SerializerMethodField,
)
from rest_framework.fields import BooleanField, CharField, IntegerField, SerializerMethodField
from rest_framework.permissions import IsAdminUser, IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
@ -26,9 +20,6 @@ class DeviceSerializer(MetaNameSerializer):
name = CharField()
type = SerializerMethodField()
confirmed = BooleanField()
created = DateTimeField(read_only=True)
last_updated = DateTimeField(read_only=True)
last_used = DateTimeField(read_only=True, allow_null=True)
def get_type(self, instance: Device) -> str:
"""Get type of device"""

View File

@ -2,15 +2,8 @@
from json import dumps
from django_filters.filters import AllValuesMultipleFilter, BooleanFilter
from django_filters.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiParameter,
OpenApiResponse,
extend_schema,
extend_schema_field,
)
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework import mixins
from rest_framework.decorators import action
@ -74,18 +67,6 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri
]
class PropertyMappingFilterSet(FilterSet):
"""Filter for PropertyMapping"""
managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))
managed__isnull = BooleanFilter(field_name="managed", lookup_expr="isnull")
class Meta:
model = PropertyMapping
fields = ["name", "managed"]
class PropertyMappingViewSet(
TypesMixin,
mixins.RetrieveModelMixin,
@ -106,9 +87,11 @@ class PropertyMappingViewSet(
queryset = PropertyMapping.objects.select_subclasses()
serializer_class = PropertyMappingSerializer
filterset_class = PropertyMappingFilterSet
search_fields = [
"name",
]
filterset_fields = {"managed": ["isnull"]}
ordering = ["name"]
search_fields = ["name"]
@permission_required("authentik_core.view_propertymapping")
@extend_schema(

View File

@ -19,7 +19,7 @@ from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, ModelSerializer
from authentik.core.models import GroupSourceConnection, Source, UserSourceConnection
from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UserSettingSerializer
from authentik.lib.utils.file import (
FilePathSerializer,
@ -60,8 +60,6 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
"enabled",
"authentication_flow",
"enrollment_flow",
"user_property_mappings",
"group_property_mappings",
"component",
"verbose_name",
"verbose_name_plural",
@ -190,47 +188,6 @@ class UserSourceConnectionViewSet(
queryset = UserSourceConnection.objects.all()
serializer_class = UserSourceConnectionSerializer
permission_classes = [OwnerSuperuserPermissions]
filterset_fields = ["user", "source__slug"]
search_fields = ["source__slug"]
filterset_fields = ["user"]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
ordering = ["source__slug", "pk"]
class GroupSourceConnectionSerializer(SourceSerializer):
"""Group Source Connection Serializer"""
source = SourceSerializer(read_only=True)
class Meta:
model = GroupSourceConnection
fields = [
"pk",
"group",
"source",
"identifier",
"created",
]
extra_kwargs = {
"group": {"read_only": True},
"identifier": {"read_only": True},
"created": {"read_only": True},
}
class GroupSourceConnectionViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""Group-source connection Viewset"""
queryset = GroupSourceConnection.objects.all()
serializer_class = GroupSourceConnectionSerializer
permission_classes = [OwnerSuperuserPermissions]
filterset_fields = ["group", "source__slug"]
search_fields = ["source__slug"]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
ordering = ["source__slug", "pk"]
ordering = ["pk"]

View File

@ -5,7 +5,6 @@ from json import loads
from typing import Any
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.models import Permission
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.db.models.functions import ExtractHour
@ -34,21 +33,15 @@ from drf_spectacular.utils import (
)
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import (
BooleanField,
CharField,
ChoiceField,
DateTimeField,
IntegerField,
ListField,
SerializerMethodField,
)
from rest_framework.fields import CharField, IntegerField, ListField, SerializerMethodField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import (
BooleanField,
DateTimeField,
ListSerializer,
PrimaryKeyRelatedField,
ValidationError,
)
from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet
@ -85,7 +78,6 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.lib.avatars import get_avatar
from authentik.rbac.decorators import permission_required
from authentik.rbac.models import get_permission_choices
from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
@ -149,19 +141,12 @@ class UserSerializer(ModelSerializer):
super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["password"] = CharField(required=False, allow_null=True)
self.fields["permissions"] = ListField(
required=False, child=ChoiceField(choices=get_permission_choices())
)
def create(self, validated_data: dict) -> User:
"""If this serializer is used in the blueprint context, we allow for
directly setting a password. However should be done via the `set_password`
method instead of directly setting it like rest_framework."""
password = validated_data.pop("password", None)
permissions = Permission.objects.filter(
codename__in=[x.split(".")[1] for x in validated_data.pop("permissions", [])]
)
validated_data["user_permissions"] = permissions
instance: User = super().create(validated_data)
self._set_password(instance, password)
return instance
@ -170,10 +155,6 @@ class UserSerializer(ModelSerializer):
"""Same as `create` above, set the password directly if we're in a blueprint
context"""
password = validated_data.pop("password", None)
permissions = Permission.objects.filter(
codename__in=[x.split(".")[1] for x in validated_data.pop("permissions", [])]
)
validated_data["user_permissions"] = permissions
instance = super().update(instance, validated_data)
self._set_password(instance, password)
return instance

View File

@ -1,28 +0,0 @@
"""Change user type"""
from authentik.core.models import User, UserTypes
from authentik.tenants.management import TenantCommand
class Command(TenantCommand):
"""Change user type"""
def add_arguments(self, parser):
parser.add_argument("--type", type=str, required=True)
parser.add_argument("--all", action="store_true")
parser.add_argument("usernames", nargs="+", type=str)
def handle_per_tenant(self, **options):
new_type = UserTypes(options["type"])
qs = (
User.objects.exclude_anonymous()
.exclude(type=UserTypes.SERVICE_ACCOUNT)
.exclude(type=UserTypes.INTERNAL_SERVICE_ACCOUNT)
)
if options["usernames"] and options["all"]:
self.stderr.write("--all and usernames specified, only one can be specified")
return
if options["usernames"] and not options["all"]:
qs = qs.filter(username__in=options["usernames"])
updated = qs.update(type=new_type)
self.stdout.write(f"Updated {updated} users.")

View File

@ -1,43 +0,0 @@
# Generated by Django 5.0.2 on 2024-02-29 11:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0035_alter_group_options_and_more"),
]
operations = [
migrations.AddField(
model_name="source",
name="group_property_mappings",
field=models.ManyToManyField(
blank=True,
default=None,
related_name="source_grouppropertymappings_set",
to="authentik_core.propertymapping",
),
),
migrations.AddField(
model_name="source",
name="user_property_mappings",
field=models.ManyToManyField(
blank=True,
default=None,
related_name="source_userpropertymappings_set",
to="authentik_core.propertymapping",
),
),
migrations.AlterField(
model_name="source",
name="property_mappings",
field=models.ManyToManyField(
blank=True,
default=None,
related_name="source_set",
to="authentik_core.propertymapping",
),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.0.2 on 2024-02-29 11:21
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_ldap", "0005_remove_ldappropertymapping_object_field_and_more"),
("authentik_core", "0036_source_group_property_mappings_and_more"),
]
operations = [
migrations.RemoveField(
model_name="source",
name="property_mappings",
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.0.7 on 2024-07-22 13:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0037_remove_source_property_mappings"),
("authentik_flows", "0027_auto_20231028_1424"),
("authentik_policies", "0011_policybinding_failure_result_and_more"),
]
operations = [
migrations.AddIndex(
model_name="source",
index=models.Index(fields=["enabled"], name="authentik_c_enabled_d72365_idx"),
),
]

View File

@ -1,67 +0,0 @@
# Generated by Django 5.0.7 on 2024-08-01 18:52
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0038_source_authentik_c_enabled_d72365_idx"),
]
operations = [
migrations.AddField(
model_name="source",
name="group_matching_mode",
field=models.TextField(
choices=[
("identifier", "Use the source-specific identifier"),
(
"name_link",
"Link to a group with identical name. Can have security implications when a group name is used with another source.",
),
(
"name_deny",
"Use the group name, but deny enrollment when the name already exists.",
),
],
default="identifier",
help_text="How the source determines if an existing group should be used or a new group created.",
),
),
migrations.AlterField(
model_name="group",
name="name",
field=models.TextField(verbose_name="name"),
),
migrations.CreateModel(
name="GroupSourceConnection",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("created", models.DateTimeField(auto_now_add=True)),
("last_updated", models.DateTimeField(auto_now=True)),
("identifier", models.TextField()),
(
"group",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_core.group"
),
),
(
"source",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_core.source"
),
),
],
options={
"unique_together": {("group", "source")},
},
),
]

View File

@ -11,7 +11,6 @@ from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as DjangoUserManager
from django.db import models
from django.db.models import Q, QuerySet, options
from django.db.models.constants import LOOKUP_SEP
from django.http import HttpRequest
from django.utils.functional import SimpleLazyObject, cached_property
from django.utils.timezone import now
@ -29,7 +28,6 @@ from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.lib.avatars import get_avatar
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.generators import generate_id
from authentik.lib.merge import MERGE_LIST_UNIQUE
from authentik.lib.models import (
CreatedUpdatedModel,
DomainlessFormattedURLValidator,
@ -102,38 +100,6 @@ class UserTypes(models.TextChoices):
INTERNAL_SERVICE_ACCOUNT = "internal_service_account"
class AttributesMixin(models.Model):
"""Adds an attributes property to a model"""
attributes = models.JSONField(default=dict, blank=True)
class Meta:
abstract = True
def update_attributes(self, properties: dict[str, Any]):
"""Update fields and attributes, but correctly by merging dicts"""
for key, value in properties.items():
if key == "attributes":
continue
setattr(self, key, value)
final_attributes = {}
MERGE_LIST_UNIQUE.merge(final_attributes, self.attributes)
MERGE_LIST_UNIQUE.merge(final_attributes, properties.get("attributes", {}))
self.attributes = final_attributes
self.save()
@classmethod
def update_or_create_attributes(
cls, query: dict[str, Any], properties: dict[str, Any]
) -> tuple[models.Model, bool]:
"""Same as django's update_or_create but correctly updates attributes by merging dicts"""
instance = cls.objects.filter(**query).first()
if not instance:
return cls.objects.create(**properties), True
instance.update_attributes(properties)
return instance, False
class GroupQuerySet(CTEQuerySet):
def with_children_recursive(self):
"""Recursively get all groups that have the current queryset as parents
@ -168,12 +134,12 @@ class GroupQuerySet(CTEQuerySet):
return cte.join(Group, group_uuid=cte.col.group_uuid).with_cte(cte)
class Group(SerializerModel, AttributesMixin):
class Group(SerializerModel):
"""Group model which supports a basic hierarchy and has attributes"""
group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
name = models.TextField(_("name"))
name = models.CharField(_("name"), max_length=80)
is_superuser = models.BooleanField(
default=False, help_text=_("Users added to this group will be superusers.")
)
@ -188,27 +154,10 @@ class Group(SerializerModel, AttributesMixin):
on_delete=models.SET_NULL,
related_name="children",
)
attributes = models.JSONField(default=dict, blank=True)
objects = GroupQuerySet.as_manager()
class Meta:
unique_together = (
(
"name",
"parent",
),
)
indexes = [models.Index(fields=["name"])]
verbose_name = _("Group")
verbose_name_plural = _("Groups")
permissions = [
("add_user_to_group", _("Add user to group")),
("remove_user_from_group", _("Remove user from group")),
]
def __str__(self):
return f"Group {self.name}"
@property
def serializer(self) -> Serializer:
from authentik.core.api.groups import GroupSerializer
@ -233,6 +182,24 @@ class Group(SerializerModel, AttributesMixin):
qs = Group.objects.filter(group_uuid=self.group_uuid)
return qs.with_children_recursive()
def __str__(self):
return f"Group {self.name}"
class Meta:
unique_together = (
(
"name",
"parent",
),
)
indexes = [models.Index(fields=["name"])]
verbose_name = _("Group")
verbose_name_plural = _("Groups")
permissions = [
("add_user_to_group", _("Add user to group")),
("remove_user_from_group", _("Remove user from group")),
]
class UserQuerySet(models.QuerySet):
"""User queryset"""
@ -258,7 +225,7 @@ class UserManager(DjangoUserManager):
return self.get_queryset().exclude_anonymous()
class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
class User(SerializerModel, GuardianUserMixin, AbstractUser):
"""authentik User model, based on django's contrib auth user model."""
uuid = models.UUIDField(default=uuid4, editable=False, unique=True)
@ -270,30 +237,10 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
ak_groups = models.ManyToManyField("Group", related_name="users")
password_change_date = models.DateTimeField(auto_now_add=True)
attributes = models.JSONField(default=dict, blank=True)
objects = UserManager()
class Meta:
verbose_name = _("User")
verbose_name_plural = _("Users")
permissions = [
("reset_user_password", _("Reset Password")),
("impersonate", _("Can impersonate other users")),
("assign_user_permissions", _("Can assign permissions to users")),
("unassign_user_permissions", _("Can unassign permissions from users")),
("preview_user", _("Can preview user data sent to providers")),
("view_user_applications", _("View applications the user has access to")),
]
indexes = [
models.Index(fields=["last_login"]),
models.Index(fields=["password_change_date"]),
models.Index(fields=["uuid"]),
models.Index(fields=["path"]),
models.Index(fields=["type"]),
]
def __str__(self):
return self.username
@staticmethod
def default_path() -> str:
"""Get the default user path"""
@ -375,6 +322,25 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
"""Get avatar, depending on authentik.avatar setting"""
return get_avatar(self)
class Meta:
verbose_name = _("User")
verbose_name_plural = _("Users")
permissions = [
("reset_user_password", _("Reset Password")),
("impersonate", _("Can impersonate other users")),
("assign_user_permissions", _("Can assign permissions to users")),
("unassign_user_permissions", _("Can unassign permissions from users")),
("preview_user", _("Can preview user data sent to providers")),
("view_user_applications", _("View applications the user has access to")),
]
indexes = [
models.Index(fields=["last_login"]),
models.Index(fields=["password_change_date"]),
models.Index(fields=["uuid"]),
models.Index(fields=["path"]),
models.Index(fields=["type"]),
]
class Provider(SerializerModel):
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
@ -462,16 +428,6 @@ class BackchannelProvider(Provider):
abstract = True
class ApplicationQuerySet(QuerySet):
def with_provider(self) -> "QuerySet[Application]":
qs = self.select_related("provider")
for subclass in Provider.objects.get_queryset()._get_subclasses_recurse(Provider):
if LOOKUP_SEP in subclass:
continue
qs = qs.select_related(f"provider__{subclass}")
return qs
class Application(SerializerModel, PolicyBindingModel):
"""Every Application which uses authentik for authentication/identification/authorization
needs an Application record. Other authentication types can subclass this Model to
@ -503,8 +459,6 @@ class Application(SerializerModel, PolicyBindingModel):
meta_description = models.TextField(default="", blank=True)
meta_publisher = models.TextField(default="", blank=True)
objects = ApplicationQuerySet.as_manager()
@property
def serializer(self) -> Serializer:
from authentik.core.api.applications import ApplicationSerializer
@ -541,19 +495,16 @@ class Application(SerializerModel, PolicyBindingModel):
return url
def get_provider(self) -> Provider | None:
"""Get casted provider instance. Needs Application queryset with_provider"""
"""Get casted provider instance"""
if not self.provider:
return None
for subclass in Provider.objects.get_queryset()._get_subclasses_recurse(Provider):
# We don't care about recursion, skip nested models
if LOOKUP_SEP in subclass:
continue
try:
return getattr(self.provider, subclass)
except AttributeError:
pass
return None
# if the Application class has been cache, self.provider is set
# but doing a direct query lookup will fail.
# In that case, just return None
try:
return Provider.objects.get_subclass(pk=self.provider.pk)
except Provider.DoesNotExist:
return None
def __str__(self):
return str(self.name)
@ -583,19 +534,6 @@ class SourceUserMatchingModes(models.TextChoices):
)
class SourceGroupMatchingModes(models.TextChoices):
"""Different modes a source can handle new/returning groups"""
IDENTIFIER = "identifier", _("Use the source-specific identifier")
NAME_LINK = "name_link", _(
"Link to a group with identical name. Can have security implications "
"when a group name is used with another source."
)
NAME_DENY = "name_deny", _(
"Use the group name, but deny enrollment when the name already exists."
)
class Source(ManagedModel, SerializerModel, PolicyBindingModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
@ -605,12 +543,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
user_path_template = models.TextField(default="goauthentik.io/sources/%(slug)s")
enabled = models.BooleanField(default=True)
user_property_mappings = models.ManyToManyField(
"PropertyMapping", default=None, blank=True, related_name="source_userpropertymappings_set"
)
group_property_mappings = models.ManyToManyField(
"PropertyMapping", default=None, blank=True, related_name="source_grouppropertymappings_set"
)
property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=True)
icon = models.FileField(
upload_to="source-icons/",
default=None,
@ -645,14 +578,6 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
"a new user enrolled."
),
)
group_matching_mode = models.TextField(
choices=SourceGroupMatchingModes.choices,
default=SourceGroupMatchingModes.IDENTIFIER,
help_text=_(
"How the source determines if an existing group should be used or "
"a new group created."
),
)
objects = InheritanceManager()
@ -682,11 +607,6 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
"""Return component used to edit this object"""
raise NotImplementedError
@property
def property_mapping_type(self) -> "type[PropertyMapping]":
"""Return property mapping type used by this object"""
raise NotImplementedError
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
"""If source uses a http-based flow, return UI Information about the login
button. If source doesn't use http-based flow, return None."""
@ -697,14 +617,6 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
user settings are available, or UserSettingSerializer."""
return None
def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
"""Get base properties for a user to build final properties upon."""
raise NotImplementedError
def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
"""Get base properties for a group to build final properties upon."""
raise NotImplementedError
def __str__(self):
return str(self.name)
@ -720,11 +632,6 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
"name",
]
),
models.Index(
fields=[
"enabled",
]
),
]
@ -748,27 +655,6 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
unique_together = (("user", "source"),)
class GroupSourceConnection(SerializerModel, CreatedUpdatedModel):
"""Connection between Group and Source."""
group = models.ForeignKey(Group, on_delete=models.CASCADE)
source = models.ForeignKey(Source, on_delete=models.CASCADE)
identifier = models.TextField()
objects = InheritanceManager()
@property
def serializer(self) -> type[Serializer]:
"""Get serializer for this model"""
raise NotImplementedError
def __str__(self) -> str:
return f"Group-source connection (group={self.group_id}, source={self.source_id})"
class Meta:
unique_together = (("group", "source"),)
class ExpiringModel(models.Model):
"""Base Model which can expire, and is automatically cleaned up."""

View File

@ -52,8 +52,6 @@ def user_logged_in_session(sender, request: HttpRequest, user: User, **_):
@receiver(user_logged_out)
def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
"""Delete AuthenticatedSession if it exists"""
if not request.session or not request.session.session_key:
return
AuthenticatedSession.objects.filter(session_key=request.session.session_key).delete()

View File

@ -4,7 +4,7 @@ from enum import Enum
from typing import Any
from django.contrib import messages
from django.db import IntegrityError, transaction
from django.db import IntegrityError
from django.db.models.query_utils import Q
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
@ -12,20 +12,8 @@ from django.urls import reverse
from django.utils.translation import gettext as _
from structlog.stdlib import get_logger
from authentik.core.models import (
Group,
GroupSourceConnection,
Source,
SourceGroupMatchingModes,
SourceUserMatchingModes,
User,
UserSourceConnection,
)
from authentik.core.sources.mapper import SourceMapper
from authentik.core.sources.stage import (
PLAN_CONTEXT_SOURCES_CONNECTION,
PostSourceStage,
)
from authentik.core.models import Source, SourceUserMatchingModes, User, UserSourceConnection
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION, PostSourceStage
from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow, FlowToken, Stage, in_memory_stage
@ -48,10 +36,7 @@ from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
from authentik.stages.user_write.stage import PLAN_CONTEXT_USER_PATH
LOGGER = get_logger()
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
class Action(Enum):
@ -85,69 +70,48 @@ class SourceFlowManager:
or deny the request."""
source: Source
mapper: SourceMapper
request: HttpRequest
identifier: str
user_connection_type: type[UserSourceConnection] = UserSourceConnection
group_connection_type: type[GroupSourceConnection] = GroupSourceConnection
connection_type: type[UserSourceConnection] = UserSourceConnection
user_info: dict[str, Any]
enroll_info: dict[str, Any]
policy_context: dict[str, Any]
user_properties: dict[str, Any | dict[str, Any]]
groups_properties: dict[str, dict[str, Any | dict[str, Any]]]
def __init__(
self,
source: Source,
request: HttpRequest,
identifier: str,
user_info: dict[str, Any],
policy_context: dict[str, Any],
enroll_info: dict[str, Any],
) -> None:
self.source = source
self.mapper = SourceMapper(self.source)
self.request = request
self.identifier = identifier
self.user_info = user_info
self.enroll_info = enroll_info
self._logger = get_logger().bind(source=source, identifier=identifier)
self.policy_context = policy_context
self.user_properties = self.mapper.build_object_properties(
object_type=User, request=request, user=None, **self.user_info
)
self.groups_properties = {
group_id: self.mapper.build_object_properties(
object_type=Group,
request=request,
user=None,
group_id=group_id,
**self.user_info,
)
for group_id in self.user_properties.setdefault("groups", [])
}
del self.user_properties["groups"]
self.policy_context = {}
def get_action(self, **kwargs) -> tuple[Action, UserSourceConnection | None]: # noqa: PLR0911
"""decide which action should be taken"""
new_connection = self.user_connection_type(source=self.source, identifier=self.identifier)
new_connection = self.connection_type(source=self.source, identifier=self.identifier)
# When request is authenticated, always link
if self.request.user.is_authenticated:
new_connection.user = self.request.user
new_connection = self.update_user_connection(new_connection, **kwargs)
new_connection = self.update_connection(new_connection, **kwargs)
return Action.LINK, new_connection
existing_connections = self.user_connection_type.objects.filter(
existing_connections = self.connection_type.objects.filter(
source=self.source, identifier=self.identifier
)
if existing_connections.exists():
connection = existing_connections.first()
return Action.AUTH, self.update_user_connection(connection, **kwargs)
return Action.AUTH, self.update_connection(connection, **kwargs)
# No connection exists, but we match on identifier, so enroll
if self.source.user_matching_mode == SourceUserMatchingModes.IDENTIFIER:
# We don't save the connection here cause it doesn't have a user assigned yet
return Action.ENROLL, self.update_user_connection(new_connection, **kwargs)
return Action.ENROLL, self.update_connection(new_connection, **kwargs)
# Check for existing users with matching attributes
query = Q()
@ -156,24 +120,24 @@ class SourceFlowManager:
SourceUserMatchingModes.EMAIL_LINK,
SourceUserMatchingModes.EMAIL_DENY,
]:
if not self.user_properties.get("email", None):
self._logger.warning("Refusing to use none email")
if not self.enroll_info.get("email", None):
self._logger.warning("Refusing to use none email", source=self.source)
return Action.DENY, None
query = Q(email__exact=self.user_properties.get("email", None))
query = Q(email__exact=self.enroll_info.get("email", None))
if self.source.user_matching_mode in [
SourceUserMatchingModes.USERNAME_LINK,
SourceUserMatchingModes.USERNAME_DENY,
]:
if not self.user_properties.get("username", None):
self._logger.warning("Refusing to use none username")
if not self.enroll_info.get("username", None):
self._logger.warning("Refusing to use none username", source=self.source)
return Action.DENY, None
query = Q(username__exact=self.user_properties.get("username", None))
query = Q(username__exact=self.enroll_info.get("username", None))
self._logger.debug("trying to link with existing user", query=query)
matching_users = User.objects.filter(query)
# No matching users, always enroll
if not matching_users.exists():
self._logger.debug("no matching users found, enrolling")
return Action.ENROLL, self.update_user_connection(new_connection, **kwargs)
return Action.ENROLL, self.update_connection(new_connection, **kwargs)
user = matching_users.first()
if self.source.user_matching_mode in [
@ -181,7 +145,7 @@ class SourceFlowManager:
SourceUserMatchingModes.USERNAME_LINK,
]:
new_connection.user = user
new_connection = self.update_user_connection(new_connection, **kwargs)
new_connection = self.update_connection(new_connection, **kwargs)
return Action.LINK, new_connection
if self.source.user_matching_mode in [
SourceUserMatchingModes.EMAIL_DENY,
@ -192,10 +156,10 @@ class SourceFlowManager:
# Should never get here as default enroll case is returned above.
return Action.DENY, None # pragma: no cover
def update_user_connection(
def update_connection(
self, connection: UserSourceConnection, **kwargs
) -> UserSourceConnection: # pragma: no cover
"""Optionally make changes to the user connection after it is looked up/created."""
"""Optionally make changes to the connection after it is looked up/created."""
return connection
def get_flow(self, **kwargs) -> HttpResponse:
@ -251,31 +215,25 @@ class SourceFlowManager:
flow: Flow | None,
connection: UserSourceConnection,
stages: list[StageView] | None = None,
**flow_context,
**kwargs,
) -> HttpResponse:
"""Prepare Authentication Plan, redirect user FlowExecutor"""
# Ensure redirect is carried through when user was trying to
# authorize application
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user"
)
flow_context.update(
kwargs.update(
{
# Since we authenticate the user by their token, they have no backend set
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_SOURCE: self.source,
PLAN_CONTEXT_SOURCES_CONNECTION: connection,
PLAN_CONTEXT_SOURCE_GROUPS: self.groups_properties,
}
)
flow_context.update(self.policy_context)
kwargs.update(self.policy_context)
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
plan = token.plan
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
plan.context.update(flow_context)
plan.context.update(kwargs)
for stage in self.get_stages_to_append(flow):
plan.append_stage(stage)
if stages:
@ -294,8 +252,8 @@ class SourceFlowManager:
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user"
)
if PLAN_CONTEXT_REDIRECT not in flow_context:
flow_context[PLAN_CONTEXT_REDIRECT] = final_redirect
if PLAN_CONTEXT_REDIRECT not in kwargs:
kwargs[PLAN_CONTEXT_REDIRECT] = final_redirect
if not flow:
return bad_request_message(
@ -307,12 +265,9 @@ class SourceFlowManager:
# We append some stages so the initial flow we get might be empty
planner.allow_empty_flows = True
planner.use_cache = False
plan = planner.plan(self.request, flow_context)
plan = planner.plan(self.request, kwargs)
for stage in self.get_stages_to_append(flow):
plan.append_stage(stage)
plan.append_stage(
in_memory_stage(GroupUpdateStage, group_connection_type=self.group_connection_type)
)
if stages:
for stage in stages:
plan.append_stage(stage)
@ -399,123 +354,7 @@ class SourceFlowManager:
)
],
**{
PLAN_CONTEXT_PROMPT: delete_none_values(self.user_properties),
PLAN_CONTEXT_PROMPT: delete_none_values(self.enroll_info),
PLAN_CONTEXT_USER_PATH: self.source.get_user_path(),
},
)
class GroupUpdateStage(StageView):
"""Dynamically injected stage which updates the user after enrollment/authentication."""
def get_action(
self, group_id: str, group_properties: dict[str, Any | dict[str, Any]]
) -> tuple[Action, GroupSourceConnection | None]:
"""decide which action should be taken"""
new_connection = self.group_connection_type(source=self.source, identifier=group_id)
existing_connections = self.group_connection_type.objects.filter(
source=self.source, identifier=group_id
)
if existing_connections.exists():
return Action.LINK, existing_connections.first()
# No connection exists, but we match on identifier, so enroll
if self.source.group_matching_mode == SourceGroupMatchingModes.IDENTIFIER:
# We don't save the connection here cause it doesn't have a user assigned yet
return Action.ENROLL, new_connection
# Check for existing groups with matching attributes
query = Q()
if self.source.group_matching_mode in [
SourceGroupMatchingModes.NAME_LINK,
SourceGroupMatchingModes.NAME_DENY,
]:
if not group_properties.get("name", None):
LOGGER.warning(
"Refusing to use none group name", source=self.source, group_id=group_id
)
return Action.DENY, None
query = Q(name__exact=group_properties.get("name"))
LOGGER.debug(
"trying to link with existing group", source=self.source, query=query, group_id=group_id
)
matching_groups = Group.objects.filter(query)
# No matching groups, always enroll
if not matching_groups.exists():
LOGGER.debug(
"no matching groups found, enrolling", source=self.source, group_id=group_id
)
return Action.ENROLL, new_connection
group = matching_groups.first()
if self.source.group_matching_mode in [
SourceGroupMatchingModes.NAME_LINK,
]:
new_connection.group = group
return Action.LINK, new_connection
if self.source.group_matching_mode in [
SourceGroupMatchingModes.NAME_DENY,
]:
LOGGER.info(
"denying source because group exists",
source=self.source,
group=group,
group_id=group_id,
)
return Action.DENY, None
# Should never get here as default enroll case is returned above.
return Action.DENY, None # pragma: no cover
def handle_group(
self, group_id: str, group_properties: dict[str, Any | dict[str, Any]]
) -> Group | None:
action, connection = self.get_action(group_id, group_properties)
if action == Action.ENROLL:
group = Group.objects.create(**group_properties)
connection.group = group
connection.save()
return group
elif action == Action.LINK:
group = connection.group
group.update_attributes(group_properties)
connection.save()
return group
return None
def handle_groups(self) -> bool:
self.source: Source = self.executor.plan.context[PLAN_CONTEXT_SOURCE]
self.user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
self.group_connection_type: GroupSourceConnection = (
self.executor.current_stage.group_connection_type
)
raw_groups: dict[str, dict[str, Any | dict[str, Any]]] = self.executor.plan.context[
PLAN_CONTEXT_SOURCE_GROUPS
]
groups: list[Group] = []
for group_id, group_properties in raw_groups.items():
group = self.handle_group(group_id, group_properties)
if not group:
return False
groups.append(group)
with transaction.atomic():
self.user.ak_groups.remove(
*self.user.ak_groups.filter(groupsourceconnection__source=self.source)
)
self.user.ak_groups.add(*groups)
return True
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Stage used after the user has been enrolled to sync their groups from source data"""
if self.handle_groups():
return self.executor.stage_ok()
else:
return self.executor.stage_invalid("Failed to update groups. Please try again later.")
def post(self, request: HttpRequest) -> HttpResponse:
"""Wrapper for post requests"""
return self.get(request)

View File

@ -1,103 +0,0 @@
from typing import Any
from django.http import HttpRequest
from structlog.stdlib import get_logger
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.models import Group, PropertyMapping, Source, User
from authentik.events.models import Event, EventAction
from authentik.lib.merge import MERGE_LIST_UNIQUE
from authentik.lib.sync.mapper import PropertyMappingManager
from authentik.policies.utils import delete_none_values
LOGGER = get_logger()
class SourceMapper:
def __init__(self, source: Source):
self.source = source
def get_manager(
self, object_type: type[User | Group], context_keys: list[str]
) -> PropertyMappingManager:
"""Get property mapping manager for this source."""
qs = PropertyMapping.objects.none()
if object_type == User:
qs = self.source.user_property_mappings.all().select_subclasses()
elif object_type == Group:
qs = self.source.group_property_mappings.all().select_subclasses()
qs = qs.order_by("name")
return PropertyMappingManager(
qs,
self.source.property_mapping_type,
["source", "properties"] + context_keys,
)
def get_base_properties(
self, object_type: type[User | Group], **kwargs
) -> dict[str, Any | dict[str, Any]]:
"""Get base properties for a user or a group to build final properties upon."""
if object_type == User:
properties = self.source.get_base_user_properties(**kwargs)
properties.setdefault("path", self.source.get_user_path())
return properties
if object_type == Group:
return self.source.get_base_group_properties(**kwargs)
return {}
def build_object_properties(
self,
object_type: type[User | Group],
manager: "PropertyMappingManager | None" = None,
user: User | None = None,
request: HttpRequest | None = None,
**kwargs,
) -> dict[str, Any | dict[str, Any]]:
"""Build a user or group properties from the source configured property mappings."""
properties = self.get_base_properties(object_type, **kwargs)
if "attributes" not in properties:
properties["attributes"] = {}
if not manager:
manager = self.get_manager(object_type, list(kwargs.keys()))
evaluations = manager.iter_eval(
user=user,
request=request,
return_mapping=True,
source=self.source,
properties=properties,
**kwargs,
)
while True:
try:
value, mapping = next(evaluations)
except StopIteration:
break
except PropertyMappingExpressionException as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
message=f"Failed to evaluate property mapping: '{exc.mapping.name}'",
source=self,
mapping=exc.mapping,
).save()
LOGGER.warning(
"Mapping failed to evaluate",
exc=exc,
source=self,
mapping=exc.mapping,
)
raise exc
if not value or not isinstance(value, dict):
LOGGER.debug(
"Mapping evaluated to None or is not a dict. Skipping",
source=self,
mapping=mapping,
)
continue
MERGE_LIST_UNIQUE.merge(properties, value)
return delete_none_values(properties)

View File

@ -4,7 +4,7 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

View File

@ -38,9 +38,7 @@ class TestSourceFlowManager(TestCase):
def test_unauthenticated_enroll(self):
"""Test un-authenticated user enrolling"""
request = get_request("/", user=AnonymousUser())
flow_manager = OAuthSourceFlowManager(
self.source, request, self.identifier, {"info": {}}, {}
)
flow_manager = OAuthSourceFlowManager(self.source, request, self.identifier, {})
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.ENROLL)
response = flow_manager.get_flow()
@ -54,9 +52,7 @@ class TestSourceFlowManager(TestCase):
user=get_anonymous_user(), source=self.source, identifier=self.identifier
)
request = get_request("/", user=AnonymousUser())
flow_manager = OAuthSourceFlowManager(
self.source, request, self.identifier, {"info": {}}, {}
)
flow_manager = OAuthSourceFlowManager(self.source, request, self.identifier, {})
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.AUTH)
response = flow_manager.get_flow()
@ -68,9 +64,7 @@ class TestSourceFlowManager(TestCase):
"""Test authenticated user linking"""
user = User.objects.create(username="foo", email="foo@bar.baz")
request = get_request("/", user=user)
flow_manager = OAuthSourceFlowManager(
self.source, request, self.identifier, {"info": {}}, {}
)
flow_manager = OAuthSourceFlowManager(self.source, request, self.identifier, {})
action, connection = flow_manager.get_action()
self.assertEqual(action, Action.LINK)
self.assertIsNone(connection.pk)
@ -83,9 +77,7 @@ class TestSourceFlowManager(TestCase):
def test_unauthenticated_link(self):
"""Test un-authenticated user linking"""
flow_manager = OAuthSourceFlowManager(
self.source, get_request("/"), self.identifier, {"info": {}}, {}
)
flow_manager = OAuthSourceFlowManager(self.source, get_request("/"), self.identifier, {})
action, connection = flow_manager.get_action()
self.assertEqual(action, Action.LINK)
self.assertIsNone(connection.pk)
@ -98,7 +90,7 @@ class TestSourceFlowManager(TestCase):
# Without email, deny
flow_manager = OAuthSourceFlowManager(
self.source, get_request("/", user=AnonymousUser()), self.identifier, {"info": {}}, {}
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
)
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.DENY)
@ -108,12 +100,7 @@ class TestSourceFlowManager(TestCase):
self.source,
get_request("/", user=AnonymousUser()),
self.identifier,
{
"info": {
"email": "foo@bar.baz",
},
},
{},
{"email": "foo@bar.baz"},
)
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.LINK)
@ -126,7 +113,7 @@ class TestSourceFlowManager(TestCase):
# Without username, deny
flow_manager = OAuthSourceFlowManager(
self.source, get_request("/", user=AnonymousUser()), self.identifier, {"info": {}}, {}
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
)
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.DENY)
@ -136,10 +123,7 @@ class TestSourceFlowManager(TestCase):
self.source,
get_request("/", user=AnonymousUser()),
self.identifier,
{
"info": {"username": "foo"},
},
{},
{"username": "foo"},
)
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.LINK)
@ -156,11 +140,8 @@ class TestSourceFlowManager(TestCase):
get_request("/", user=AnonymousUser()),
self.identifier,
{
"info": {
"username": "bar",
},
"username": "bar",
},
{},
)
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.ENROLL)
@ -170,10 +151,7 @@ class TestSourceFlowManager(TestCase):
self.source,
get_request("/", user=AnonymousUser()),
self.identifier,
{
"info": {"username": "foo"},
},
{},
{"username": "foo"},
)
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.DENY)
@ -187,10 +165,7 @@ class TestSourceFlowManager(TestCase):
self.source,
get_request("/", user=AnonymousUser()),
self.identifier,
{
"info": {"username": "foo"},
},
{},
{"username": "foo"},
)
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.ENROLL)
@ -216,10 +191,7 @@ class TestSourceFlowManager(TestCase):
self.source,
get_request("/", user=AnonymousUser()),
self.identifier,
{
"info": {"username": "foo"},
},
{},
{"username": "foo"},
)
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.ENROLL)

View File

@ -1,237 +0,0 @@
"""Test Source flow_manager group update stage"""
from django.test import RequestFactory
from authentik.core.models import Group, SourceGroupMatchingModes
from authentik.core.sources.flow_manager import PLAN_CONTEXT_SOURCE_GROUPS, GroupUpdateStage
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, PLAN_CONTEXT_SOURCE, FlowPlan
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import FlowExecutorView
from authentik.lib.generators import generate_id
from authentik.sources.oauth.models import GroupOAuthSourceConnection, OAuthSource
class TestSourceFlowManager(FlowTestCase):
"""Test Source flow_manager group update stage"""
def setUp(self) -> None:
super().setUp()
self.factory = RequestFactory()
self.authentication_flow = create_test_flow()
self.enrollment_flow = create_test_flow()
self.source: OAuthSource = OAuthSource.objects.create(
name=generate_id(),
slug=generate_id(),
authentication_flow=self.authentication_flow,
enrollment_flow=self.enrollment_flow,
)
self.identifier = generate_id()
self.user = create_test_admin_user()
def test_nonexistant_group(self):
request = self.factory.get("/")
stage = GroupUpdateStage(
FlowExecutorView(
current_stage=in_memory_stage(
GroupUpdateStage, group_connection_type=GroupOAuthSourceConnection
),
plan=FlowPlan(
flow_pk=generate_id(),
context={
PLAN_CONTEXT_SOURCE: self.source,
PLAN_CONTEXT_PENDING_USER: self.user,
PLAN_CONTEXT_SOURCE_GROUPS: {
"group 1": {
"name": "group 1",
},
},
},
),
),
request=request,
)
self.assertTrue(stage.handle_groups())
self.assertTrue(Group.objects.filter(name="group 1").exists())
self.assertTrue(self.user.ak_groups.filter(name="group 1").exists())
self.assertTrue(
GroupOAuthSourceConnection.objects.filter(
group=Group.objects.get(name="group 1"), source=self.source
).exists()
)
def test_nonexistant_group_name_link(self):
self.source.group_matching_mode = SourceGroupMatchingModes.NAME_LINK
self.source.save()
request = self.factory.get("/")
stage = GroupUpdateStage(
FlowExecutorView(
current_stage=in_memory_stage(
GroupUpdateStage, group_connection_type=GroupOAuthSourceConnection
),
plan=FlowPlan(
flow_pk=generate_id(),
context={
PLAN_CONTEXT_SOURCE: self.source,
PLAN_CONTEXT_PENDING_USER: self.user,
PLAN_CONTEXT_SOURCE_GROUPS: {
"group 1": {
"name": "group 1",
},
},
},
),
),
request=request,
)
self.assertTrue(stage.handle_groups())
self.assertTrue(Group.objects.filter(name="group 1").exists())
self.assertTrue(self.user.ak_groups.filter(name="group 1").exists())
self.assertTrue(
GroupOAuthSourceConnection.objects.filter(
group=Group.objects.get(name="group 1"), source=self.source
).exists()
)
def test_existant_group_name_link(self):
self.source.group_matching_mode = SourceGroupMatchingModes.NAME_LINK
self.source.save()
group = Group.objects.create(name="group 1")
request = self.factory.get("/")
stage = GroupUpdateStage(
FlowExecutorView(
current_stage=in_memory_stage(
GroupUpdateStage, group_connection_type=GroupOAuthSourceConnection
),
plan=FlowPlan(
flow_pk=generate_id(),
context={
PLAN_CONTEXT_SOURCE: self.source,
PLAN_CONTEXT_PENDING_USER: self.user,
PLAN_CONTEXT_SOURCE_GROUPS: {
"group 1": {
"name": "group 1",
},
},
},
),
),
request=request,
)
self.assertTrue(stage.handle_groups())
self.assertTrue(Group.objects.filter(name="group 1").exists())
self.assertTrue(self.user.ak_groups.filter(name="group 1").exists())
self.assertTrue(
GroupOAuthSourceConnection.objects.filter(group=group, source=self.source).exists()
)
def test_nonexistant_group_name_deny(self):
self.source.group_matching_mode = SourceGroupMatchingModes.NAME_DENY
self.source.save()
request = self.factory.get("/")
stage = GroupUpdateStage(
FlowExecutorView(
current_stage=in_memory_stage(
GroupUpdateStage, group_connection_type=GroupOAuthSourceConnection
),
plan=FlowPlan(
flow_pk=generate_id(),
context={
PLAN_CONTEXT_SOURCE: self.source,
PLAN_CONTEXT_PENDING_USER: self.user,
PLAN_CONTEXT_SOURCE_GROUPS: {
"group 1": {
"name": "group 1",
},
},
},
),
),
request=request,
)
self.assertTrue(stage.handle_groups())
self.assertTrue(Group.objects.filter(name="group 1").exists())
self.assertTrue(self.user.ak_groups.filter(name="group 1").exists())
self.assertTrue(
GroupOAuthSourceConnection.objects.filter(
group=Group.objects.get(name="group 1"), source=self.source
).exists()
)
def test_existant_group_name_deny(self):
self.source.group_matching_mode = SourceGroupMatchingModes.NAME_DENY
self.source.save()
group = Group.objects.create(name="group 1")
request = self.factory.get("/")
stage = GroupUpdateStage(
FlowExecutorView(
current_stage=in_memory_stage(
GroupUpdateStage, group_connection_type=GroupOAuthSourceConnection
),
plan=FlowPlan(
flow_pk=generate_id(),
context={
PLAN_CONTEXT_SOURCE: self.source,
PLAN_CONTEXT_PENDING_USER: self.user,
PLAN_CONTEXT_SOURCE_GROUPS: {
"group 1": {
"name": "group 1",
},
},
},
),
),
request=request,
)
self.assertFalse(stage.handle_groups())
self.assertFalse(self.user.ak_groups.filter(name="group 1").exists())
self.assertFalse(
GroupOAuthSourceConnection.objects.filter(group=group, source=self.source).exists()
)
def test_group_updates(self):
self.source.group_matching_mode = SourceGroupMatchingModes.NAME_LINK
self.source.save()
other_group = Group.objects.create(name="other group")
old_group = Group.objects.create(name="old group")
new_group = Group.objects.create(name="new group")
self.user.ak_groups.set([other_group, old_group])
GroupOAuthSourceConnection.objects.create(
group=old_group, source=self.source, identifier=old_group.name
)
GroupOAuthSourceConnection.objects.create(
group=new_group, source=self.source, identifier=new_group.name
)
request = self.factory.get("/")
stage = GroupUpdateStage(
FlowExecutorView(
current_stage=in_memory_stage(
GroupUpdateStage, group_connection_type=GroupOAuthSourceConnection
),
plan=FlowPlan(
flow_pk=generate_id(),
context={
PLAN_CONTEXT_SOURCE: self.source,
PLAN_CONTEXT_PENDING_USER: self.user,
PLAN_CONTEXT_SOURCE_GROUPS: {
"new group": {
"name": "new group",
},
},
},
),
),
request=request,
)
self.assertTrue(stage.handle_groups())
self.assertFalse(self.user.ak_groups.filter(name="old group").exists())
self.assertTrue(self.user.ak_groups.filter(name="other group").exists())
self.assertTrue(self.user.ak_groups.filter(name="new group").exists())
self.assertEqual(self.user.ak_groups.count(), 2)

View File

@ -1,72 +0,0 @@
"""Test Source Property mappings"""
from django.test import TestCase
from authentik.core.models import Group, PropertyMapping, Source, User
from authentik.core.sources.mapper import SourceMapper
from authentik.lib.generators import generate_id
class ProxySource(Source):
@property
def property_mapping_type(self):
return PropertyMapping
def get_base_user_properties(self, **kwargs):
return {
"username": kwargs.get("username", None),
"email": kwargs.get("email", "default@authentik"),
}
def get_base_group_properties(self, **kwargs):
return {"name": kwargs.get("name", None)}
class Meta:
proxy = True
class TestSourcePropertyMappings(TestCase):
"""Test Source PropertyMappings"""
def test_base_properties(self):
source = ProxySource.objects.create(name=generate_id(), slug=generate_id(), enabled=True)
mapper = SourceMapper(source)
user_base_properties = mapper.get_base_properties(User, username="test1")
self.assertEqual(
user_base_properties,
{
"username": "test1",
"email": "default@authentik",
"path": f"goauthentik.io/sources/{source.slug}",
},
)
group_base_properties = mapper.get_base_properties(Group)
self.assertEqual(group_base_properties, {"name": None})
def test_build_properties(self):
source = ProxySource.objects.create(name=generate_id(), slug=generate_id(), enabled=True)
mapper = SourceMapper(source)
source.user_property_mappings.add(
PropertyMapping.objects.create(
name=generate_id(),
expression="""
return {"username": data.get("username", None), "email": None}
""",
)
)
properties = mapper.build_object_properties(
object_type=User, user=None, request=None, username="test1", data={"username": "test2"}
)
self.assertEqual(
properties,
{
"username": "test2",
"path": f"goauthentik.io/sources/{source.slug}",
"attributes": {},
},
)

View File

@ -6,6 +6,7 @@ from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.urls import path
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import RedirectView
from authentik.core.api.applications import ApplicationViewSet
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
@ -17,13 +18,9 @@ from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSe
from authentik.core.api.tokens import TokenViewSet
from authentik.core.api.transactional_applications import TransactionalApplicationView
from authentik.core.api.users import UserViewSet
from authentik.core.views.apps import RedirectToAppLaunch
from authentik.core.views import apps
from authentik.core.views.debug import AccessDeniedView
from authentik.core.views.interface import (
BrandDefaultRedirectView,
InterfaceView,
RootRedirectView,
)
from authentik.core.views.interface import InterfaceView
from authentik.core.views.session import EndSessionView
from authentik.flows.views.interface import FlowInterfaceView
from authentik.root.asgi_middleware import SessionMiddleware
@ -33,24 +30,26 @@ from authentik.root.middleware import ChannelsLoggingMiddleware
urlpatterns = [
path(
"",
login_required(RootRedirectView.as_view()),
login_required(
RedirectView.as_view(pattern_name="authentik_core:if-user", query_string=True)
),
name="root-redirect",
),
path(
# We have to use this format since everything else uses application/o or application/saml
# We have to use this format since everything else uses applications/o or applications/saml
"application/launch/<slug:application_slug>/",
RedirectToAppLaunch.as_view(),
apps.RedirectToAppLaunch.as_view(),
name="application-launch",
),
# Interfaces
path(
"if/admin/",
ensure_csrf_cookie(BrandDefaultRedirectView.as_view(template_name="if/admin.html")),
ensure_csrf_cookie(InterfaceView.as_view(template_name="if/admin.html")),
name="if-admin",
),
path(
"if/user/",
ensure_csrf_cookie(BrandDefaultRedirectView.as_view(template_name="if/user.html")),
ensure_csrf_cookie(InterfaceView.as_view(template_name="if/user.html")),
name="if-user",
),
path(

View File

@ -3,42 +3,13 @@
from json import dumps
from typing import Any
from django.http import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import redirect
from django.utils.translation import gettext as _
from django.views.generic.base import RedirectView, TemplateView
from django.views.generic.base import TemplateView
from rest_framework.request import Request
from authentik import get_build_hash
from authentik.admin.tasks import LOCAL_VERSION
from authentik.api.v3.config import ConfigView
from authentik.brands.api import CurrentBrandSerializer
from authentik.brands.models import Brand
from authentik.core.models import UserTypes
from authentik.policies.denied import AccessDeniedResponse
class RootRedirectView(RedirectView):
"""Root redirect view, redirect to brand's default application if set"""
pattern_name = "authentik_core:if-user"
query_string = True
def redirect_to_app(self, request: HttpRequest):
if request.user.is_authenticated and request.user.type == UserTypes.EXTERNAL:
brand: Brand = request.brand
if brand.default_application:
return redirect(
"authentik_core:application-launch",
application_slug=brand.default_application.slug,
)
return None
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
if redirect_response := RootRedirectView().redirect_to_app(request):
return redirect_response
return super().dispatch(request, *args, **kwargs)
class InterfaceView(TemplateView):
@ -52,20 +23,3 @@ class InterfaceView(TemplateView):
kwargs["build"] = get_build_hash()
kwargs["url_kwargs"] = self.kwargs
return super().get_context_data(**kwargs)
class BrandDefaultRedirectView(InterfaceView):
"""By default redirect to default app"""
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
if request.user.is_authenticated and request.user.type == UserTypes.EXTERNAL:
brand: Brand = request.brand
if brand.default_application:
return redirect(
"authentik_core:application-launch",
application_slug=brand.default_application.slug,
)
response = AccessDeniedResponse(self.request)
response.error_message = _("Interface can only be accessed by internal users.")
return response
return super().dispatch(request, *args, **kwargs)

View File

@ -76,7 +76,7 @@ class CertificateBuilder:
.subject_name(
x509.Name(
[
x509.NameAttribute(NameOID.COMMON_NAME, self.common_name[:64]),
x509.NameAttribute(NameOID.COMMON_NAME, self.common_name),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "authentik"),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "Self-signed"),
]

View File

@ -34,12 +34,6 @@ class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
]
class ConnectionTokenOwnerFilter(OwnerFilter):
"""Owner filter for connection tokens (checks session's user)"""
owner_key = "session__user"
class ConnectionTokenViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
@ -56,9 +50,4 @@ class ConnectionTokenViewSet(
search_fields = ["endpoint__name", "provider__name"]
ordering = ["endpoint__name", "provider__name"]
permission_classes = [OwnerSuperuserPermissions]
filter_backends = [
ConnectionTokenOwnerFilter,
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]

View File

@ -21,8 +21,6 @@ from authentik.enterprise.providers.rac.models import ConnectionToken, Endpoint
@receiver(user_logged_out)
def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
"""Disconnect any open RAC connections"""
if not request.session or not request.session.session_key:
return
layer = get_channel_layer()
async_to_sync(layer.group_send)(
RAC_CLIENT_GROUP_SESSION

View File

@ -5,6 +5,7 @@ from channels.sessions import CookieMiddleware
from django.urls import path
from django.views.decorators.csrf import ensure_csrf_cookie
from authentik.core.channels import TokenOutpostMiddleware
from authentik.enterprise.providers.rac.api.connection_tokens import ConnectionTokenViewSet
from authentik.enterprise.providers.rac.api.endpoints import EndpointViewSet
from authentik.enterprise.providers.rac.api.property_mappings import RACPropertyMappingViewSet
@ -12,7 +13,6 @@ from authentik.enterprise.providers.rac.api.providers import RACProviderViewSet
from authentik.enterprise.providers.rac.consumer_client import RACClientConsumer
from authentik.enterprise.providers.rac.consumer_outpost import RACOutpostConsumer
from authentik.enterprise.providers.rac.views import RACInterface, RACStartView
from authentik.outposts.channels import TokenOutpostMiddleware
from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.middleware import ChannelsLoggingMiddleware

View File

@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Optional, TypedDict
from django.http import HttpRequest
from geoip2.errors import GeoIP2Error
from geoip2.models import ASN
from sentry_sdk import start_span
from sentry_sdk import Hub
from authentik.events.context_processors.mmdb import MMDBContextProcessor
from authentik.lib.config import CONFIG
@ -48,7 +48,7 @@ class ASNContextProcessor(MMDBContextProcessor):
def asn(self, ip_address: str) -> ASN | None:
"""Wrapper for Reader.asn"""
with start_span(
with Hub.current.start_span(
op="authentik.events.asn.asn",
description=ip_address,
):

View File

@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Optional, TypedDict
from django.http import HttpRequest
from geoip2.errors import GeoIP2Error
from geoip2.models import City
from sentry_sdk import start_span
from sentry_sdk.hub import Hub
from authentik.events.context_processors.mmdb import MMDBContextProcessor
from authentik.lib.config import CONFIG
@ -49,7 +49,7 @@ class GeoIPContextProcessor(MMDBContextProcessor):
def city(self, ip_address: str) -> City | None:
"""Wrapper for Reader.city"""
with start_span(
with Hub.current.start_span(
op="authentik.events.geo.city",
description=ip_address,
):

View File

@ -35,7 +35,6 @@ IGNORED_MODELS = tuple(
_CTX_OVERWRITE_USER = ContextVar[User | None]("authentik_events_log_overwrite_user", default=None)
_CTX_IGNORE = ContextVar[bool]("authentik_events_log_ignore", default=False)
_CTX_REQUEST = ContextVar[HttpRequest | None]("authentik_events_log_request", default=None)
def should_log_model(model: Model) -> bool:
@ -150,13 +149,11 @@ class AuditMiddleware:
m2m_changed.disconnect(dispatch_uid=request.request_id)
def __call__(self, request: HttpRequest) -> HttpResponse:
_CTX_REQUEST.set(request)
self.connect(request)
response = self.get_response(request)
self.disconnect(request)
_CTX_REQUEST.set(None)
return response
def process_exception(self, request: HttpRequest, exception: Exception):
@ -170,7 +167,7 @@ class AuditMiddleware:
thread = EventNewThread(
EventAction.SUSPICIOUS_REQUEST,
request,
message=exception_to_string(exception),
message=str(exception),
)
thread.run()
elif before_send({}, {"exc_info": (None, exception, None)}) is not None:
@ -195,8 +192,6 @@ class AuditMiddleware:
return
if _CTX_IGNORE.get():
return
if request.request_id != _CTX_REQUEST.get().request_id:
return
user = self.get_user(request)
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
@ -210,8 +205,6 @@ class AuditMiddleware:
return
if _CTX_IGNORE.get():
return
if request.request_id != _CTX_REQUEST.get().request_id:
return
user = self.get_user(request)
EventNewThread(
@ -237,8 +230,6 @@ class AuditMiddleware:
return
if _CTX_IGNORE.get():
return
if request.request_id != _CTX_REQUEST.get().request_id:
return
user = self.get_user(request)
EventNewThread(

View File

@ -238,8 +238,6 @@ class Event(SerializerModel, ExpiringModel):
"args": cleanse_dict(QueryDict(request.META.get("QUERY_STRING", ""))),
"user_agent": request.META.get("HTTP_USER_AGENT", ""),
}
if hasattr(request, "request_id"):
self.context["http_request"]["request_id"] = request.request_id
# Special case for events created during flow execution
# since they keep the http query within a wrapped query
if QS_QUERY in self.context["http_request"]["args"]:

View File

@ -5,7 +5,7 @@ from typing import Any
from django.core.cache import cache
from django.http import HttpRequest
from sentry_sdk import start_span
from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span
from structlog.stdlib import BoundLogger, get_logger
@ -151,7 +151,9 @@ class FlowPlanner:
def plan(self, request: HttpRequest, default_context: dict[str, Any] | None = None) -> FlowPlan:
"""Check each of the flows' policies, check policies for each stage with PolicyBinding
and return ordered list"""
with start_span(op="authentik.flow.planner.plan", description=self.flow.slug) as span:
with Hub.current.start_span(
op="authentik.flow.planner.plan", description=self.flow.slug
) as span:
span: Span
span.set_data("flow", self.flow)
span.set_data("request", request)
@ -216,7 +218,7 @@ class FlowPlanner:
"""Build flow plan by checking each stage in their respective
order and checking the applied policies"""
with (
start_span(
Hub.current.start_span(
op="authentik.flow.planner.build_plan",
description=self.flow.slug,
) as span,

View File

@ -10,7 +10,7 @@ from django.urls import reverse
from django.views.generic.base import View
from prometheus_client import Histogram
from rest_framework.request import Request
from sentry_sdk import start_span
from sentry_sdk.hub import Hub
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.models import User
@ -123,7 +123,7 @@ class ChallengeStageView(StageView):
)
return self.executor.restart_flow(keep_context)
with (
start_span(
Hub.current.start_span(
op="authentik.flow.stage.challenge_invalid",
description=self.__class__.__name__,
),
@ -133,7 +133,7 @@ class ChallengeStageView(StageView):
):
return self.challenge_invalid(challenge)
with (
start_span(
Hub.current.start_span(
op="authentik.flow.stage.challenge_valid",
description=self.__class__.__name__,
),
@ -159,7 +159,7 @@ class ChallengeStageView(StageView):
def _get_challenge(self, *args, **kwargs) -> Challenge:
with (
start_span(
Hub.current.start_span(
op="authentik.flow.stage.get_challenge",
description=self.__class__.__name__,
),
@ -172,7 +172,7 @@ class ChallengeStageView(StageView):
except StageInvalidException as exc:
self.logger.debug("Got StageInvalidException", exc=exc)
return self.executor.stage_invalid()
with start_span(
with Hub.current.start_span(
op="authentik.flow.stage._get_challenge",
description=self.__class__.__name__,
):

View File

@ -18,8 +18,9 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, PolymorphicProxySerializer, extend_schema
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from sentry_sdk import capture_exception, start_span
from sentry_sdk import capture_exception
from sentry_sdk.api import set_tag
from sentry_sdk.hub import Hub
from structlog.stdlib import BoundLogger, get_logger
from authentik.brands.models import Brand
@ -153,7 +154,9 @@ class FlowExecutorView(APIView):
return plan
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
with start_span(op="authentik.flow.executor.dispatch", description=self.flow.slug) as span:
with Hub.current.start_span(
op="authentik.flow.executor.dispatch", description=self.flow.slug
) as span:
span.set_data("authentik Flow", self.flow.slug)
get_params = QueryDict(request.GET.get(QS_QUERY, ""))
if QS_KEY_TOKEN in get_params:
@ -271,7 +274,7 @@ class FlowExecutorView(APIView):
)
try:
with (
start_span(
Hub.current.start_span(
op="authentik.flow.executor.stage",
description=class_path,
) as span,
@ -322,7 +325,7 @@ class FlowExecutorView(APIView):
)
try:
with (
start_span(
Hub.current.start_span(
op="authentik.flow.executor.stage",
description=class_path,
) as span,

View File

@ -13,7 +13,7 @@ from lxml import etree # nosec
from lxml.etree import Element, SubElement # nosec
from requests.exceptions import ConnectionError, HTTPError, RequestException, Timeout
from authentik.lib.utils.dict import get_path_from_dict
from authentik.lib.config import get_path_from_dict
from authentik.lib.utils.http import get_http_session
from authentik.tenants.utils import get_current_tenant

View File

@ -19,8 +19,6 @@ from urllib.parse import quote_plus, urlparse
import yaml
from django.conf import ImproperlyConfigured
from authentik.lib.utils.dict import get_path_from_dict, set_path_in_dict
SEARCH_PATHS = ["authentik/lib/default.yml", "/etc/authentik/config.yml", ""] + glob(
"/etc/authentik/config.d/*.yml", recursive=True
)
@ -49,6 +47,29 @@ DEPRECATIONS = {
}
def get_path_from_dict(root: dict, path: str, sep=".", default=None) -> Any:
"""Recursively walk through `root`, checking each part of `path` separated by `sep`.
If at any point a dict does not exist, return default"""
for comp in path.split(sep):
if root and comp in root:
root = root.get(comp)
else:
return default
return root
def set_path_in_dict(root: dict, path: str, value: Any, sep="."):
"""Recursively walk through `root`, checking each part of `path` separated by `sep`
and setting the last value to `value`"""
# Walk each component of the path
path_parts = path.split(sep)
for comp in path_parts[:-1]:
if comp not in root:
root[comp] = {}
root = root.get(comp, {})
root[path_parts[-1]] = value
@dataclass(slots=True)
class Attr:
"""Single configuration attribute"""

View File

@ -13,7 +13,7 @@ from django.core.exceptions import FieldError
from django.utils.text import slugify
from guardian.shortcuts import get_anonymous_user
from rest_framework.serializers import ValidationError
from sentry_sdk import start_span
from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span
from structlog.stdlib import get_logger
@ -195,7 +195,7 @@ class BaseEvaluator:
"""Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
If any exception is raised during execution, it is raised.
The result is returned without any type-checking."""
with start_span(op="authentik.lib.evaluator.evaluate") as span:
with Hub.current.start_span(op="authentik.lib.evaluator.evaluate") as span:
span: Span
span.description = self._filename
span.set_data("expression", expression_source)

View File

@ -68,7 +68,7 @@ def sentry_init(**sentry_init_kwargs):
integrations=[
ArgvIntegration(),
StdlibIntegration(),
DjangoIntegration(transaction_style="function_name", cache_spans=True),
DjangoIntegration(transaction_style="function_name"),
CeleryIntegration(),
RedisIntegration(),
ThreadingIntegration(propagate_hub=True),

View File

@ -20,10 +20,6 @@ class PropertyMappingManager:
_evaluators: list[PropertyMappingEvaluator]
globals: dict
__has_compiled: bool
def __init__(
self,
qs: QuerySet[PropertyMapping],
@ -34,11 +30,10 @@ class PropertyMappingManager:
# we need a list of all parameter names that will be used during evaluation
context_keys: list[str],
) -> None:
self.query_set = qs.order_by("name")
self.query_set = qs
self.mapping_subclass = mapping_subclass
self.context_keys = context_keys
self.globals = {}
self.__has_compiled = False
self.compile()
def compile(self):
self._evaluators = []
@ -48,7 +43,6 @@ class PropertyMappingManager:
evaluator = PropertyMappingEvaluator(
mapping, **{key: None for key in self.context_keys}
)
evaluator._globals.update(self.globals)
# Compile and cache expression
evaluator.compile()
self._evaluators.append(evaluator)
@ -62,9 +56,6 @@ class PropertyMappingManager:
) -> Generator[tuple[dict, PropertyMapping], None]:
"""Iterate over all mappings that were pre-compiled and
execute all of them with the given context"""
if not self.__has_compiled:
self.compile()
self.__has_compiled = True
for mapping in self._evaluators:
mapping.set_context(user, request, **kwargs)
try:

View File

@ -229,8 +229,6 @@ class SyncTasks:
client.delete(instance)
except TransientSyncException as exc:
raise Retry() from exc
except SkipObjectException:
continue
except StopSync as exc:
self.logger.warning(exc, provider_pk=provider.pk)
@ -261,7 +259,5 @@ class SyncTasks:
client.update_group(group, operation, pk_set)
except TransientSyncException as exc:
raise Retry() from exc
except SkipObjectException:
continue
except StopSync as exc:
self.logger.warning(exc, provider_pk=provider.pk)

View File

@ -1,24 +0,0 @@
from typing import Any
def get_path_from_dict(root: dict, path: str, sep=".", default=None) -> Any:
"""Recursively walk through `root`, checking each part of `path` separated by `sep`.
If at any point a dict does not exist, return default"""
for comp in path.split(sep):
if root and comp in root:
root = root.get(comp)
else:
return default
return root
def set_path_in_dict(root: dict, path: str, value: Any, sep="."):
"""Recursively walk through `root`, checking each part of `path` separated by `sep`
and setting the last value to `value`"""
# Walk each component of the path
path_parts = path.split(sep)
for comp in path_parts[:-1]:
if comp not in root:
root[comp] = {}
root = root.get(comp, {})
root[path_parts[-1]] = value

View File

@ -2,6 +2,7 @@
from dataclasses import asdict
from channels.exceptions import DenyConnection
from channels.routing import URLRouter
from channels.testing import WebsocketCommunicator
from django.test import TransactionTestCase
@ -36,8 +37,9 @@ class TestOutpostWS(TransactionTestCase):
communicator = WebsocketCommunicator(
URLRouter(websocket.websocket_urlpatterns), f"/ws/outpost/{self.outpost.pk}/"
)
connected, _ = await communicator.connect()
self.assertFalse(connected)
with self.assertRaises(DenyConnection):
connected, _ = await communicator.connect()
self.assertFalse(connected)
async def test_auth_valid(self):
"""Test auth with token"""

View File

@ -2,13 +2,13 @@
from django.urls import path
from authentik.core.channels import TokenOutpostMiddleware
from authentik.outposts.api.outposts import OutpostViewSet
from authentik.outposts.api.service_connections import (
DockerServiceConnectionViewSet,
KubernetesServiceConnectionViewSet,
ServiceConnectionViewSet,
)
from authentik.outposts.channels import TokenOutpostMiddleware
from authentik.outposts.consumer import OutpostConsumer
from authentik.root.middleware import ChannelsLoggingMiddleware

View File

@ -7,7 +7,7 @@ from time import perf_counter
from django.core.cache import cache
from django.http import HttpRequest
from sentry_sdk import start_span
from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span
from structlog.stdlib import BoundLogger, get_logger
@ -111,7 +111,7 @@ class PolicyEngine:
def build(self) -> "PolicyEngine":
"""Build wrapper which monitors performance"""
with (
start_span(
Hub.current.start_span(
op="authentik.policy.engine.build",
description=self.__pbm,
) as span,

View File

@ -1,55 +0,0 @@
"""GeoIP Policy API Views"""
from django_countries import countries
from django_countries.serializer_fields import CountryField
from django_countries.serializers import CountryFieldMixin
from rest_framework import serializers
from rest_framework.generics import ListAPIView
from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer
from authentik.policies.geoip.models import GeoIPPolicy
from authentik.policies.geoip.serializer_fields import DetailedCountryField
class DetailedCountrySerializer(serializers.Serializer):
code = CountryField()
name = serializers.CharField()
class ISO3166View(ListAPIView):
"""Get all countries in ISO-3166-1"""
permission_classes = [AllowAny]
queryset = [{"code": code, "name": name} for (code, name) in countries]
serializer_class = DetailedCountrySerializer
filter_backends = []
pagination_class = None
class GeoIPPolicySerializer(CountryFieldMixin, PolicySerializer):
"""GeoIP Policy Serializer"""
countries_obj = serializers.ListField(
child=DetailedCountryField(), source="countries", read_only=True
)
class Meta:
model = GeoIPPolicy
fields = PolicySerializer.Meta.fields + [
"asns",
"countries",
"countries_obj",
]
class GeoIPPolicyViewSet(UsedByMixin, ModelViewSet):
"""GeoIP Viewset"""
queryset = GeoIPPolicy.objects.all()
serializer_class = GeoIPPolicySerializer
filterset_fields = ["name"]
ordering = ["name"]
search_fields = ["name"]

View File

@ -1,11 +0,0 @@
"""Authentik policy geoip app config"""
from django.apps import AppConfig
class AuthentikPolicyGeoIPConfig(AppConfig):
"""Authentik policy_geoip app config"""
name = "authentik.policies.geoip"
label = "authentik_policies_geoip"
verbose_name = "authentik Policies.GeoIP"

View File

@ -1,5 +0,0 @@
from authentik.lib.sentry import SentryIgnoredException
class GeoIPNotFoundException(SentryIgnoredException):
"""Exception raised when an IP is not found in a GeoIP database"""

View File

@ -1,52 +0,0 @@
# Generated by Django 5.0.7 on 2024-07-16 11:23
import django.contrib.postgres.fields
import django.db.models.deletion
import django_countries.fields
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_policies", "0011_policybinding_failure_result_and_more"),
]
operations = [
migrations.CreateModel(
name="GeoIPPolicy",
fields=[
(
"policy_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_policies.policy",
),
),
(
"asns",
django.contrib.postgres.fields.ArrayField(
base_field=models.IntegerField(), blank=True, default=list, size=None
),
),
(
"countries",
django_countries.fields.CountryField(blank=True, max_length=746, multiple=True),
),
],
options={
"verbose_name": "GeoIP Policy",
"verbose_name_plural": "GeoIP Policies",
"indexes": [
models.Index(fields=["policy_ptr_id"], name="authentik_p_policy__5cc4a9_idx")
],
},
bases=("authentik_policies.policy",),
),
]

View File

@ -1,92 +0,0 @@
"""GeoIP policy"""
from itertools import chain
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.translation import gettext as _
from django_countries.fields import CountryField
from rest_framework.serializers import BaseSerializer
from authentik.policies.exceptions import PolicyException
from authentik.policies.geoip.exceptions import GeoIPNotFoundException
from authentik.policies.models import Policy
from authentik.policies.types import PolicyRequest, PolicyResult
class GeoIPPolicy(Policy):
"""Ensure the user satisfies requirements of geography or network topology, based on IP
address."""
asns = ArrayField(models.IntegerField(), blank=True, default=list)
countries = CountryField(multiple=True, blank=True)
@property
def serializer(self) -> type[BaseSerializer]:
from authentik.policies.geoip.api import GeoIPPolicySerializer
return GeoIPPolicySerializer
@property
def component(self) -> str: # pragma: no cover
return "ak-policy-geoip-form"
def passes(self, request: PolicyRequest) -> PolicyResult:
"""
Passes if any of the following is true:
- the client IP is advertised by an autonomous system with ASN in the `asns`
- the client IP is geolocated in a country of `countries`
"""
results: list[PolicyResult] = []
if self.asns:
results.append(self.passes_asn(request))
if self.countries:
results.append(self.passes_country(request))
if not results:
return PolicyResult(True)
passing = any(r.passing for r in results)
messages = chain(*[r.messages for r in results])
result = PolicyResult(passing, *messages)
result.source_results = results
return result
def passes_asn(self, request: PolicyRequest) -> PolicyResult:
# This is not a single get chain because `request.context` can contain `{ "asn": None }`.
asn_data = request.context.get("asn")
asn = asn_data.get("asn") if asn_data else None
if not asn:
raise PolicyException(
GeoIPNotFoundException(_("GeoIP: client IP not found in ASN database."))
)
if asn not in self.asns:
message = _("Client IP is not part of an allowed autonomous system.")
return PolicyResult(False, message)
return PolicyResult(True)
def passes_country(self, request: PolicyRequest) -> PolicyResult:
# This is not a single get chain because `request.context` can contain `{ "geoip": None }`.
geoip_data = request.context.get("geoip")
country = geoip_data.get("country") if geoip_data else None
if not country:
raise PolicyException(
GeoIPNotFoundException(_("GeoIP: client IP address not found in City database."))
)
if country not in self.countries:
message = _("Client IP is not in an allowed country.")
return PolicyResult(False, message)
return PolicyResult(True)
class Meta(Policy.PolicyMeta):
verbose_name = _("GeoIP Policy")
verbose_name_plural = _("GeoIP Policies")

View File

@ -1,21 +0,0 @@
"""Workaround for https://github.com/SmileyChris/django-countries/issues/441"""
from django_countries.serializer_fields import CountryField
from drf_spectacular.utils import extend_schema_field, inline_serializer
from rest_framework import serializers
DETAILED_COUNTRY_SCHEMA = {
"code": CountryField(),
"name": serializers.CharField(),
}
@extend_schema_field(
inline_serializer(
"DetailedCountryField",
DETAILED_COUNTRY_SCHEMA,
)
)
class DetailedCountryField(CountryField):
def __init__(self):
super().__init__(country_dict=True)

View File

@ -1,128 +0,0 @@
"""geoip policy tests"""
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from authentik.policies.engine import PolicyRequest, PolicyResult
from authentik.policies.exceptions import PolicyException
from authentik.policies.geoip.exceptions import GeoIPNotFoundException
from authentik.policies.geoip.models import GeoIPPolicy
class TestGeoIPPolicy(TestCase):
"""Test GeoIP Policy"""
def setUp(self):
super().setUp()
self.request = PolicyRequest(get_anonymous_user())
self.context_disabled_geoip = {}
self.context_unknown_ip = {"asn": None, "geoip": None}
# 8.8.8.8
self.context = {
"asn": {"asn": 15169, "as_org": "GOOGLE", "network": "8.8.8.0/24"},
"geoip": {
"continent": "NA",
"country": "US",
"lat": 37.751,
"long": -97.822,
"city": "",
},
}
self.matching_asns = [13335, 15169]
self.matching_countries = ["US", "CA"]
self.mismatching_asns = [1, 2]
self.mismatching_countries = ["MX", "UA"]
def enrich_context_disabled_geoip(self):
pass
def enrich_context_unknown_ip(self):
self.request.context["asn"] = self.context_unknown_ip["asn"]
self.request.context["geoip"] = self.context_unknown_ip["geoip"]
def enrich_context(self):
self.request.context["asn"] = self.context["asn"]
self.request.context["geoip"] = self.context["geoip"]
def test_disabled_geoip(self):
"""Test that disabled GeoIP raises PolicyException with GeoIPNotFoundException"""
self.enrich_context_disabled_geoip()
policy = GeoIPPolicy.objects.create(
asns=self.matching_asns, countries=self.matching_countries
)
with self.assertRaises(PolicyException) as cm:
policy.passes(self.request)
self.assertIsInstance(cm.exception.src_exc, GeoIPNotFoundException)
def test_unknown_ip(self):
"""Test that unknown IP raises PolicyException with GeoIPNotFoundException"""
self.enrich_context_unknown_ip()
policy = GeoIPPolicy.objects.create(
asns=self.matching_asns, countries=self.matching_countries
)
with self.assertRaises(PolicyException) as cm:
policy.passes(self.request)
self.assertIsInstance(cm.exception.src_exc, GeoIPNotFoundException)
def test_empty_policy(self):
"""Test that empty policy passes"""
self.enrich_context()
policy = GeoIPPolicy.objects.create()
result: PolicyResult = policy.passes(self.request)
self.assertTrue(result.passing)
def test_policy_with_matching_asns(self):
"""Test that a policy with matching ASNs passes"""
self.enrich_context()
policy = GeoIPPolicy.objects.create(asns=self.matching_asns)
result: PolicyResult = policy.passes(self.request)
self.assertTrue(result.passing)
def test_policy_with_mismatching_asns(self):
"""Test that a policy with mismatching ASNs fails"""
self.enrich_context()
policy = GeoIPPolicy.objects.create(asns=self.mismatching_asns)
result: PolicyResult = policy.passes(self.request)
self.assertFalse(result.passing)
def test_policy_with_matching_countries(self):
"""Test that a policy with matching countries passes"""
self.enrich_context()
policy = GeoIPPolicy.objects.create(countries=self.matching_countries)
result: PolicyResult = policy.passes(self.request)
self.assertTrue(result.passing)
def test_policy_with_mismatching_countries(self):
"""Test that a policy with mismatching countries fails"""
self.enrich_context()
policy = GeoIPPolicy.objects.create(countries=self.mismatching_countries)
result: PolicyResult = policy.passes(self.request)
self.assertFalse(result.passing)
def test_policy_requires_only_one_match(self):
"""Test that a policy with one matching value passes"""
self.enrich_context()
policy = GeoIPPolicy.objects.create(
asns=self.mismatching_asns, countries=self.matching_countries
)
result: PolicyResult = policy.passes(self.request)
self.assertTrue(result.passing)

View File

@ -1,10 +0,0 @@
"""API URLs"""
from django.urls import path
from authentik.policies.geoip.api import GeoIPPolicyViewSet, ISO3166View
api_urlpatterns = [
("policies/geoip", GeoIPPolicyViewSet),
path("policies/geoip_iso3166/", ISO3166View.as_view(), name="iso-3166-view"),
]

View File

@ -4,7 +4,7 @@ from multiprocessing import get_context
from multiprocessing.connection import Connection
from django.core.cache import cache
from sentry_sdk import start_span
from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span
from structlog.stdlib import get_logger
@ -121,7 +121,7 @@ class PolicyProcess(PROCESS_CLASS):
def profiling_wrapper(self):
"""Run with profiling enabled"""
with (
start_span(
Hub.current.start_span(
op="authentik.policy.process.execute",
) as span,
HIST_POLICIES_EXECUTION_TIME.labels(

View File

@ -5,8 +5,7 @@ from django.db.models.query import Q
from django_filters.filters import BooleanFilter
from django_filters.filterset import FilterSet
from rest_framework.fields import CharField, ListField, SerializerMethodField
from rest_framework.mixins import ListModelMixin
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
@ -106,7 +105,7 @@ class LDAPOutpostConfigSerializer(ModelSerializer):
]
class LDAPOutpostConfigViewSet(ListModelMixin, GenericViewSet):
class LDAPOutpostConfigViewSet(ReadOnlyModelViewSet):
"""LDAPProvider Viewset"""
queryset = LDAPProvider.objects.filter(

View File

@ -1,10 +1,14 @@
"""OAuth2Provider API Views"""
from django_filters.filters import AllValuesMultipleFilter
from django_filters.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework.fields import CharField
from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.property_mappings import PropertyMappingFilterSet, PropertyMappingSerializer
from authentik.core.api.property_mappings import PropertyMappingSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.providers.oauth2.models import ScopeMapping
@ -29,12 +33,14 @@ class ScopeMappingSerializer(PropertyMappingSerializer):
]
class ScopeMappingFilter(PropertyMappingFilterSet):
class ScopeMappingFilter(FilterSet):
"""Filter for ScopeMapping"""
class Meta(PropertyMappingFilterSet.Meta):
managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))
class Meta:
model = ScopeMapping
fields = PropertyMappingFilterSet.Meta.fields + ["scope_name"]
fields = ["scope_name", "name", "managed"]
class ScopeMappingViewSet(UsedByMixin, ModelViewSet):

View File

@ -1,9 +1,9 @@
"""authentik oauth provider app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikProviderOAuth2Config(ManagedAppConfig):
class AuthentikProviderOAuth2Config(AppConfig):
"""authentik oauth provider app config"""
name = "authentik.providers.oauth2"
@ -13,4 +13,3 @@ class AuthentikProviderOAuth2Config(ManagedAppConfig):
"authentik.providers.oauth2.urls_root": "",
"authentik.providers.oauth2.urls": "application/o/",
}
default = True

View File

@ -22,7 +22,6 @@ from jwt import encode
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik.brands.models import WebfingerProvider
from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.generators import generate_code_fixed_length, generate_id, generate_key
@ -121,7 +120,7 @@ class ScopeMapping(PropertyMapping):
verbose_name_plural = _("Scope Mappings")
class OAuth2Provider(WebfingerProvider, Provider):
class OAuth2Provider(Provider):
"""OAuth2 Provider for generic OAuth and OpenID Connect Applications."""
client_type = models.CharField(
@ -289,24 +288,6 @@ class OAuth2Provider(WebfingerProvider, Provider):
key, alg = self.jwt_key
return encode(payload, key, algorithm=alg, headers=headers)
def webfinger(self, resource: str, request: HttpRequest):
return {
"subject": resource,
"links": [
{
"rel": "http://openid.net/specs/connect/1.0/issuer",
"href": request.build_absolute_uri(
reverse(
"authentik_providers_oauth2:provider-root",
kwargs={
"application_slug": self.application.slug,
},
)
),
},
],
}
class Meta:
verbose_name = _("OAuth2/OpenID Provider")
verbose_name_plural = _("OAuth2/OpenID Providers")

View File

@ -1,17 +0,0 @@
from hashlib import sha256
from django.contrib.auth.signals import user_logged_out
from django.dispatch import receiver
from django.http import HttpRequest
from authentik.core.models import User
from authentik.providers.oauth2.models import AccessToken
@receiver(user_logged_out)
def user_logged_out_oauth_access_token(sender, request: HttpRequest, user: User, **_):
"""Revoke access tokens upon user logout"""
if not request.session or not request.session.session_key:
return
hashed_session_key = sha256(request.session.session_key.encode("ascii")).hexdigest()
AccessToken.objects.filter(user=user, session_id=hashed_session_key).delete()

View File

@ -17,7 +17,7 @@ from django.views import View
from django.views.decorators.csrf import csrf_exempt
from guardian.shortcuts import get_anonymous_user
from jwt import PyJWK, PyJWT, PyJWTError, decode
from sentry_sdk import start_span
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik.core.middleware import CTX_AUTH_VIA
@ -118,7 +118,7 @@ class TokenParams:
)
def __check_policy_access(self, app: Application, request: HttpRequest, **kwargs):
with start_span(
with Hub.current.start_span(
op="authentik.providers.oauth2.token.policy",
):
user = self.user if self.user else get_anonymous_user()
@ -151,22 +151,22 @@ class TokenParams:
raise TokenError("invalid_client")
if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
with start_span(
with Hub.current.start_span(
op="authentik.providers.oauth2.post.parse.code",
):
self.__post_init_code(raw_code, request)
elif self.grant_type == GRANT_TYPE_REFRESH_TOKEN:
with start_span(
with Hub.current.start_span(
op="authentik.providers.oauth2.post.parse.refresh",
):
self.__post_init_refresh(raw_token, request)
elif self.grant_type in [GRANT_TYPE_CLIENT_CREDENTIALS, GRANT_TYPE_PASSWORD]:
with start_span(
with Hub.current.start_span(
op="authentik.providers.oauth2.post.parse.client_credentials",
):
self.__post_init_client_credentials(request)
elif self.grant_type == GRANT_TYPE_DEVICE_CODE:
with start_span(
with Hub.current.start_span(
op="authentik.providers.oauth2.post.parse.device_code",
):
self.__post_init_device_code(request)
@ -508,7 +508,7 @@ class TokenView(View):
def post(self, request: HttpRequest) -> HttpResponse:
"""Generate tokens for clients"""
try:
with start_span(
with Hub.current.start_span(
op="authentik.providers.oauth2.post.parse",
):
client_id, client_secret = extract_client_auth(request)
@ -519,7 +519,7 @@ class TokenView(View):
CTX_AUTH_VIA.set("oauth_client_secret")
self.params = TokenParams.parse(request, self.provider, client_id, client_secret)
with start_span(
with Hub.current.start_span(
op="authentik.providers.oauth2.post.response",
):
if self.params.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:

View File

@ -6,8 +6,7 @@ from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema_field
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, ListField, ReadOnlyField, SerializerMethodField
from rest_framework.mixins import ListModelMixin
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
@ -182,7 +181,7 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
]
class ProxyOutpostConfigViewSet(ListModelMixin, GenericViewSet):
class ProxyOutpostConfigViewSet(ReadOnlyModelViewSet):
"""ProxyProvider Viewset"""
queryset = ProxyProvider.objects.filter(application__isnull=False)

View File

@ -28,8 +28,7 @@ class ProxyDockerController(DockerController):
labels = super()._get_labels()
labels["traefik.enable"] = "true"
labels[f"traefik.http.routers.{traefik_name}-router.rule"] = (
f"({' || '.join([f'Host(`{host}`)' for host in hosts])})"
f" && PathPrefix(`/outpost.goauthentik.io`)"
f"Host({','.join(hosts)}) && PathPrefix(`/outpost.goauthentik.io`)"
)
labels[f"traefik.http.routers.{traefik_name}-router.tls"] = "true"
labels[f"traefik.http.routers.{traefik_name}-router.service"] = f"{traefik_name}-service"

View File

@ -12,8 +12,6 @@ from authentik.providers.proxy.tasks import proxy_on_logout
@receiver(user_logged_out)
def logout_proxy_revoke_direct(sender: type[User], request: HttpRequest, **_):
"""Catch logout by direct logout and forward to proxy providers"""
if not request.session or not request.session.session_key:
return
proxy_on_logout.delay(request.session.session_key)

View File

@ -0,0 +1,71 @@
"""RadiusProvider API Views"""
from rest_framework.fields import CharField, ListField
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.providers.radius.models import RadiusProvider
class RadiusProviderSerializer(ProviderSerializer):
"""RadiusProvider Serializer"""
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
class Meta:
model = RadiusProvider
fields = ProviderSerializer.Meta.fields + [
"client_networks",
# Shared secret is not a write-only field, as
# an admin might have to view it
"shared_secret",
"outpost_set",
"mfa_support",
]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
class RadiusProviderViewSet(UsedByMixin, ModelViewSet):
"""RadiusProvider Viewset"""
queryset = RadiusProvider.objects.all()
serializer_class = RadiusProviderSerializer
ordering = ["name"]
search_fields = ["name", "client_networks"]
filterset_fields = {
"application": ["isnull"],
"name": ["iexact"],
"authorization_flow__slug": ["iexact"],
"client_networks": ["iexact"],
}
class RadiusOutpostConfigSerializer(ModelSerializer):
"""RadiusProvider Serializer"""
application_slug = CharField(source="application.slug")
auth_flow_slug = CharField(source="authorization_flow.slug")
class Meta:
model = RadiusProvider
fields = [
"pk",
"name",
"application_slug",
"auth_flow_slug",
"client_networks",
"shared_secret",
"mfa_support",
]
class RadiusOutpostConfigViewSet(ReadOnlyModelViewSet):
"""RadiusProvider Viewset"""
queryset = RadiusProvider.objects.filter(application__isnull=False)
serializer_class = RadiusOutpostConfigSerializer
ordering = ["name"]
search_fields = ["name"]
filterset_fields = ["name"]

View File

@ -1,32 +0,0 @@
"""Radius Property mappings API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.property_mappings import PropertyMappingFilterSet, PropertyMappingSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.providers.radius.models import RadiusProviderPropertyMapping
class RadiusProviderPropertyMappingSerializer(PropertyMappingSerializer):
"""RadiusProviderPropertyMapping Serializer"""
class Meta:
model = RadiusProviderPropertyMapping
fields = PropertyMappingSerializer.Meta.fields
class RadiusProviderPropertyMappingFilter(PropertyMappingFilterSet):
"""Filter for RadiusProviderPropertyMapping"""
class Meta(PropertyMappingFilterSet.Meta):
model = RadiusProviderPropertyMapping
class RadiusProviderPropertyMappingViewSet(UsedByMixin, ModelViewSet):
"""RadiusProviderPropertyMapping Viewset"""
queryset = RadiusProviderPropertyMapping.objects.all()
serializer_class = RadiusProviderPropertyMappingSerializer
filterset_class = RadiusProviderPropertyMappingFilter
search_fields = ["name"]
ordering = ["name"]

View File

@ -1,177 +0,0 @@
"""RadiusProvider API Views"""
from base64 import b64encode
from django.conf import settings
from django.shortcuts import get_object_or_404
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from pyrad.dictionary import Attribute, Dictionary
from pyrad.packet import AuthPacket
from rest_framework.decorators import action
from rest_framework.fields import CharField, ListField
from rest_framework.mixins import ListModelMixin
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.models import Application
from authentik.events.models import Event, EventAction
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.sync.mapper import PropertyMappingManager
from authentik.lib.utils.errors import exception_to_string
from authentik.policies.api.exec import PolicyTestResultSerializer
from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult
from authentik.providers.radius.models import RadiusProvider, RadiusProviderPropertyMapping
class RadiusProviderSerializer(ProviderSerializer):
"""RadiusProvider Serializer"""
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
class Meta:
model = RadiusProvider
fields = ProviderSerializer.Meta.fields + [
"client_networks",
# Shared secret is not a write-only field, as
# an admin might have to view it
"shared_secret",
"outpost_set",
"mfa_support",
]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
class RadiusProviderViewSet(UsedByMixin, ModelViewSet):
"""RadiusProvider Viewset"""
queryset = RadiusProvider.objects.all()
serializer_class = RadiusProviderSerializer
ordering = ["name"]
search_fields = ["name", "client_networks"]
filterset_fields = {
"application": ["isnull"],
"name": ["iexact"],
"authorization_flow__slug": ["iexact"],
"client_networks": ["iexact"],
}
class RadiusOutpostConfigSerializer(ModelSerializer):
"""RadiusProvider Serializer"""
application_slug = CharField(source="application.slug")
auth_flow_slug = CharField(source="authorization_flow.slug")
class Meta:
model = RadiusProvider
fields = [
"pk",
"name",
"application_slug",
"auth_flow_slug",
"client_networks",
"shared_secret",
"mfa_support",
]
class RadiusOutpostConfigViewSet(ListModelMixin, GenericViewSet):
"""RadiusProvider Viewset"""
queryset = RadiusProvider.objects.filter(application__isnull=False)
serializer_class = RadiusOutpostConfigSerializer
ordering = ["name"]
search_fields = ["name"]
filterset_fields = ["name"]
class RadiusCheckAccessSerializer(PassiveSerializer):
attributes = CharField(required=False)
access = PolicyTestResultSerializer()
def get_attributes(self, provider: RadiusProvider):
mapper = PropertyMappingManager(
provider.property_mappings.all().order_by("name").select_subclasses(),
RadiusProviderPropertyMapping,
["packet"],
)
dict = Dictionary(
str(
settings.BASE_DIR
/ "authentik"
/ "providers"
/ "radius"
/ "dictionaries"
/ "dictionary"
)
)
packet = AuthPacket()
packet.secret = provider.shared_secret
packet.dict = dict
def define_attribute(
vendor_code: int,
vendor_name: str,
attribute_name: str,
attribute_code: int,
attribute_type: str,
):
"""Dynamically add attribute to Radius packet"""
# Ensure the vendor exists
if vendor_code not in dict.vendors.backward or vendor_name not in dict.vendors.forward:
dict.vendors.Add(vendor_name, vendor_code)
full_attribute_name = f"{vendor_name}-{attribute_name}"
if full_attribute_name not in dict.attributes:
dict.attributes[full_attribute_name] = Attribute(
attribute_name, attribute_code, attribute_type, vendor=vendor_name
)
mapper.globals["define_attribute"] = define_attribute
try:
for _ in mapper.iter_eval(self.request.user, self.request, packet=packet):
pass
except (PropertyMappingExpressionException, ControlFlowException) as exc:
# Value error can be raised when assigning invalid data to an attribute
Event.new(
EventAction.CONFIGURATION_ERROR,
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
mapping=exc.mapping,
).save()
return None
return b64encode(packet.RequestPacket()).decode()
@extend_schema(
request=None,
parameters=[OpenApiParameter("app_slug", OpenApiTypes.STR)],
responses={
200: RadiusCheckAccessSerializer(),
},
)
@action(detail=True)
def check_access(self, request: Request, pk) -> Response:
"""Check access to a single application by slug"""
provider = get_object_or_404(RadiusProvider, pk=pk)
application = get_object_or_404(Application, slug=request.query_params["app_slug"])
engine = PolicyEngine(application, request.user, request)
engine.use_cache = False
engine.build()
result = engine.result
access_response = PolicyResult(result.passing)
attributes = None
if result.passing:
attributes = self.get_attributes(provider)
response = self.RadiusCheckAccessSerializer(
instance={
"attributes": attributes,
"access": access_response,
}
)
return Response(response.data)

View File

@ -1,132 +0,0 @@
# -*- text -*-
# Copyright (C) 2023 The FreeRADIUS Server project and contributors
# This work is licensed under CC-BY version 4.0 https://creativecommons.org/licenses/by/4.0
# Version $Id$
#
# Version: $Id$
#
VENDOR Aruba 14823
BEGIN-VENDOR Aruba
ATTRIBUTE User-Role 1 string
ATTRIBUTE User-Vlan 2 integer
ATTRIBUTE Priv-Admin-User 3 integer
ATTRIBUTE Admin-Role 4 string
ATTRIBUTE Essid-Name 5 string
ATTRIBUTE Location-Id 6 string
ATTRIBUTE Port-Identifier 7 string
ATTRIBUTE MMS-User-Template 8 string
ATTRIBUTE Named-User-Vlan 9 string
ATTRIBUTE AP-Group 10 string
ATTRIBUTE Framed-IPv6-Address 11 string
ATTRIBUTE Device-Type 12 string
ATTRIBUTE No-DHCP-Fingerprint 14 integer
ATTRIBUTE Mdps-Device-Udid 15 string
ATTRIBUTE Mdps-Device-Imei 16 string
ATTRIBUTE Mdps-Device-Iccid 17 string
ATTRIBUTE Mdps-Max-Devices 18 integer
ATTRIBUTE Mdps-Device-Name 19 string
ATTRIBUTE Mdps-Device-Product 20 string
ATTRIBUTE Mdps-Device-Version 21 string
ATTRIBUTE Mdps-Device-Serial 22 string
ATTRIBUTE CPPM-Role 23 string
ATTRIBUTE AirGroup-User-Name 24 string
ATTRIBUTE AirGroup-Shared-User 25 string
ATTRIBUTE AirGroup-Shared-Role 26 string
ATTRIBUTE AirGroup-Device-Type 27 integer
ATTRIBUTE Auth-Survivability 28 string
ATTRIBUTE AS-User-Name 29 string
ATTRIBUTE AS-Credential-Hash 30 string
ATTRIBUTE WorkSpace-App-Name 31 string
ATTRIBUTE Mdps-Provisioning-Settings 32 string
ATTRIBUTE Mdps-Device-Profile 33 string
ATTRIBUTE AP-IP-Address 34 ipaddr
ATTRIBUTE AirGroup-Shared-Group 35 string
ATTRIBUTE User-Group 36 string
ATTRIBUTE Network-SSO-Token 37 string
ATTRIBUTE AirGroup-Version 38 integer
ATTRIBUTE Auth-SurvMethod 39 integer
ATTRIBUTE Port-Bounce-Host 40 integer
ATTRIBUTE Calea-Server-Ip 41 ipaddr
ATTRIBUTE Admin-Path 42 string
ATTRIBUTE Captive-Portal-URL 43 string
ATTRIBUTE MPSK-Passphrase 44 octets encrypt=2
ATTRIBUTE ACL-Server-Query-Info 45 string
ATTRIBUTE Command-String 46 string
ATTRIBUTE Network-Profile 47 string
ATTRIBUTE Admin-Device-Group 48 string
ATTRIBUTE PoE-Priority 49 integer
ATTRIBUTE Port-Auth-Mode 50 integer
ATTRIBUTE NAS-Filter-Rule 51 string
ATTRIBUTE QoS-Trust-Mode 52 integer
ATTRIBUTE UBT-Gateway-Role 53 string
ATTRIBUTE Gateway-Zone 54 string
ATTRIBUTE DPP-Bootstrapping-Key-SHA256 55 string
ATTRIBUTE DPP-Bootstrapping-Net-Access-Key-SHA256 56 string
ATTRIBUTE DPP-Bootstrapping-Key-B64 57 string
ATTRIBUTE STP-Admin-Edge-Port 58 integer
ATTRIBUTE UBT-Gateway-CPPM-Role 59 string
ATTRIBUTE AP-MAC-Address 60 string
ATTRIBUTE Device-MAC-Address 61 string
ATTRIBUTE MPSK-Key-Name 62 string
ATTRIBUTE Device-Traffic-Class 63 integer
ATTRIBUTE PVLAN-Port-Type 64 integer
ATTRIBUTE Network-Test 65 integer
ATTRIBUTE MPSK-Lookup-Info 66 string encrypt=1
ATTRIBUTE AVPair 67 string
ATTRIBUTE DPP-Service-Type 68 integer
ATTRIBUTE User-Mgmt-Interface 69 string
ATTRIBUTE Poe-Allocate-By-Method 70 integer
ATTRIBUTE DPP-AKMs 71 string
ATTRIBUTE DPP-Passphrase 72 string encrypt=2
VALUE AirGroup-Device-Type Personal-Device 1
VALUE AirGroup-Device-Type Shared-Device 2
VALUE AirGroup-Device-Type Deleted-Device 3
VALUE AirGroup-Version AirGroup-v1 1
VALUE AirGroup-Version AirGroup-v2 2
VALUE PoE-Priority Critical 0
VALUE PoE-Priority High 1
VALUE PoE-Priority Low 2
VALUE Port-Auth-Mode Infrastructure-Mode 1
VALUE Port-Auth-Mode Client-Mode 2
VALUE Port-Auth-Mode Multi-Domain-Mode 3
VALUE QoS-Trust-Mode DSCP 0
VALUE QoS-Trust-Mode QoS 1
VALUE QoS-Trust-Mode None 2
VALUE STP-Admin-Edge-Port Disable 0
VALUE STP-Admin-Edge-Port Enable 1
VALUE PVLAN-Port-Type None 0
VALUE PVLAN-Port-Type Promiscuous 1
VALUE PVLAN-Port-Type Secondary 2
VALUE DPP-Service-Type DDP-Boostrap-Authorization 1
VALUE DPP-Service-Type DDP-Identity-Update 2
VALUE DPP-Service-Type DDP-Net-Access 3
VALUE PoE-Allocate-By-Method Class 1
VALUE PoE-Allocate-By-Method Usage 2
END-VENDOR Aruba
ALIAS Aruba Vendor-Specific.Aruba

View File

@ -1,231 +0,0 @@
# -*- text -*-
# Copyright (C) 2023 The FreeRADIUS Server project and contributors
# This work is licensed under CC-BY version 4.0 https://creativecommons.org/licenses/by/4.0
# Version $Id$
#
# dictionary.cisco
#
# Accounting VSAs originally by
# "Marcelo M. Sosa Lugones" <marcelo@sosa.com.ar>
#
# Version: $Id$
#
# For documentation on Cisco RADIUS attributes, see:
#
# http://www.cisco.com/univercd/cc/td/doc/product/access/acs_serv/vapp_dev/vsaig3.htm
#
# For general documentation on Cisco RADIUS configuration, see:
#
# http://www.cisco.com/en/US/partner/tech/tk583/tk547/tsd_technology_support_sub-protocol_home.html
#
VENDOR Cisco 9
#
# Standard attribute
#
BEGIN-VENDOR Cisco
ATTRIBUTE AVPair 1 string
ATTRIBUTE NAS-Port 2 string
#
# T.37 Store-and-Forward attributes.
#
ATTRIBUTE Fax-Account-Id-Origin 3 string
ATTRIBUTE Fax-Msg-Id 4 string
ATTRIBUTE Fax-Pages 5 string
ATTRIBUTE Fax-Coverpage-Flag 6 string
ATTRIBUTE Fax-Modem-Time 7 string
ATTRIBUTE Fax-Connect-Speed 8 string
ATTRIBUTE Fax-Recipient-Count 9 string
ATTRIBUTE Fax-Process-Abort-Flag 10 string
ATTRIBUTE Fax-Dsn-Address 11 string
ATTRIBUTE Fax-Dsn-Flag 12 string
ATTRIBUTE Fax-Mdn-Address 13 string
ATTRIBUTE Fax-Mdn-Flag 14 string
ATTRIBUTE Fax-Auth-Status 15 string
ATTRIBUTE Email-Server-Address 16 string
ATTRIBUTE Email-Server-Ack-Flag 17 string
ATTRIBUTE Gateway-Id 18 string
ATTRIBUTE Call-Type 19 string
ATTRIBUTE Port-Used 20 string
ATTRIBUTE Abort-Cause 21 string
#
# Voice over IP attributes.
#
ATTRIBUTE h323-remote-address 23 string
ATTRIBUTE h323-conf-id 24 string
ATTRIBUTE h323-setup-time 25 string
ATTRIBUTE h323-call-origin 26 string
ATTRIBUTE h323-call-type 27 string
ATTRIBUTE h323-connect-time 28 string
ATTRIBUTE h323-disconnect-time 29 string
ATTRIBUTE h323-disconnect-cause 30 string
ATTRIBUTE h323-voice-quality 31 string
ATTRIBUTE h323-gw-id 33 string
ATTRIBUTE h323-incoming-conf-id 35 string
ATTRIBUTE Policy-Up 37 string
ATTRIBUTE Policy-Down 38 string
ATTRIBUTE Relay-Information-Option 46 string
ATTRIBUTE DHCP-User-Class 47 string
ATTRIBUTE DHCP-Vendor-Class 48 string
ATTRIBUTE DHCP-Relay-GiAddr 50 string
ATTRIBUTE Service-Name 51 string
ATTRIBUTE Parent-Session-Id 52 string
ATTRIBUTE Sub-QoS-Pol-In 55 string
ATTRIBUTE Sub-QoS-Pol-Out 56 string
ATTRIBUTE In-ACL 57 string
ATTRIBUTE Out-ACL 58 string
ATTRIBUTE Sub-PBR-Policy-In 59 string
ATTRIBUTE Sub-Activate-Service 60 string
ATTRIBUTE IPv6-In-ACL 61 string
ATTRIBUTE IPv6-Out-ACL 62 string
ATTRIBUTE Sub-Deactivate-Service 63 string
ATTRIBUTE DHCP-Subscriber-Id 65 string
ATTRIBUTE DHCPv6-Link-Address 66 string
ATTRIBUTE sip-conf-id 100 string
ATTRIBUTE h323-credit-amount 101 string
ATTRIBUTE h323-credit-time 102 string
ATTRIBUTE h323-return-code 103 string
ATTRIBUTE h323-prompt-id 104 string
ATTRIBUTE h323-time-and-day 105 string
ATTRIBUTE h323-redirect-number 106 string
ATTRIBUTE h323-preferred-lang 107 string
ATTRIBUTE h323-redirect-ip-address 108 string
ATTRIBUTE h323-billing-model 109 string
ATTRIBUTE h323-currency 110 string
ATTRIBUTE subscriber 111 string
ATTRIBUTE gw-rxd-cdn 112 string
ATTRIBUTE gw-final-xlated-cdn 113 string
ATTRIBUTE remote-media-address 114 string
ATTRIBUTE release-source 115 string
ATTRIBUTE gw-rxd-cgn 116 string
ATTRIBUTE gw-final-xlated-cgn 117 string
# SIP Attributes
ATTRIBUTE call-id 141 string
ATTRIBUTE session-protocol 142 string
ATTRIBUTE method 143 string
ATTRIBUTE prev-hop-via 144 string
ATTRIBUTE prev-hop-ip 145 string
ATTRIBUTE incoming-req-uri 146 string
ATTRIBUTE outgoing-req-uri 147 string
ATTRIBUTE next-hop-ip 148 string
ATTRIBUTE next-hop-dn 149 string
ATTRIBUTE sip-hdr 150 string
ATTRIBUTE dsp-id 151 string
#
# Extra attributes sent by the Cisco, if you configure
# "radius-server vsa accounting" (requires IOS11.2+).
#
ATTRIBUTE Multilink-ID 187 integer
ATTRIBUTE Num-In-Multilink 188 integer
ATTRIBUTE Pre-Input-Octets 190 integer
ATTRIBUTE Pre-Output-Octets 191 integer
ATTRIBUTE Pre-Input-Packets 192 integer
ATTRIBUTE Pre-Output-Packets 193 integer
ATTRIBUTE Maximum-Time 194 integer
ATTRIBUTE Disconnect-Cause 195 integer
ATTRIBUTE Data-Rate 197 integer
ATTRIBUTE PreSession-Time 198 integer
ATTRIBUTE PW-Lifetime 208 integer
ATTRIBUTE IP-Direct 209 integer
ATTRIBUTE PPP-VJ-Slot-Comp 210 integer
ATTRIBUTE PPP-Async-Map 212 integer
ATTRIBUTE IP-Pool-Definition 217 string
ATTRIBUTE Assign-IP-Pool 218 integer
ATTRIBUTE Route-IP 228 integer
ATTRIBUTE Link-Compression 233 integer
ATTRIBUTE Target-Util 234 integer
ATTRIBUTE Maximum-Channels 235 integer
ATTRIBUTE Data-Filter 242 integer
ATTRIBUTE Call-Filter 243 integer
ATTRIBUTE Idle-Limit 244 integer
ATTRIBUTE Subscriber-Password 249 string
ATTRIBUTE Account-Info 250 string
ATTRIBUTE Service-Info 251 string
ATTRIBUTE Command-Code 252 string
ATTRIBUTE Control-Info 253 string
ATTRIBUTE Xmit-Rate 255 integer
VALUE Disconnect-Cause No-Reason 0
VALUE Disconnect-Cause No-Disconnect 1
VALUE Disconnect-Cause Unknown 2
VALUE Disconnect-Cause Call-Disconnect 3
VALUE Disconnect-Cause CLID-Authentication-Failure 4
VALUE Disconnect-Cause No-Modem-Available 9
VALUE Disconnect-Cause No-Carrier 10
VALUE Disconnect-Cause Lost-Carrier 11
VALUE Disconnect-Cause No-Detected-Result-Codes 12
VALUE Disconnect-Cause User-Ends-Session 20
VALUE Disconnect-Cause Idle-Timeout 21
VALUE Disconnect-Cause Exit-Telnet-Session 22
VALUE Disconnect-Cause No-Remote-IP-Addr 23
VALUE Disconnect-Cause Exit-Raw-TCP 24
VALUE Disconnect-Cause Password-Fail 25
VALUE Disconnect-Cause Raw-TCP-Disabled 26
VALUE Disconnect-Cause Control-C-Detected 27
VALUE Disconnect-Cause EXEC-Program-Destroyed 28
VALUE Disconnect-Cause Close-Virtual-Connection 29
VALUE Disconnect-Cause End-Virtual-Connection 30
VALUE Disconnect-Cause Exit-Rlogin 31
VALUE Disconnect-Cause Invalid-Rlogin-Option 32
VALUE Disconnect-Cause Insufficient-Resources 33
VALUE Disconnect-Cause Timeout-PPP-LCP 40
VALUE Disconnect-Cause Failed-PPP-LCP-Negotiation 41
VALUE Disconnect-Cause Failed-PPP-PAP-Auth-Fail 42
VALUE Disconnect-Cause Failed-PPP-CHAP-Auth 43
VALUE Disconnect-Cause Failed-PPP-Remote-Auth 44
VALUE Disconnect-Cause PPP-Remote-Terminate 45
VALUE Disconnect-Cause PPP-Closed-Event 46
VALUE Disconnect-Cause NCP-Closed-PPP 47
VALUE Disconnect-Cause MP-Error-PPP 48
VALUE Disconnect-Cause PPP-Maximum-Channels 49
VALUE Disconnect-Cause Tables-Full 50
VALUE Disconnect-Cause Resources-Full 51
VALUE Disconnect-Cause Invalid-IP-Address 52
VALUE Disconnect-Cause Bad-Hostname 53
VALUE Disconnect-Cause Bad-Port 54
VALUE Disconnect-Cause Reset-TCP 60
VALUE Disconnect-Cause TCP-Connection-Refused 61
VALUE Disconnect-Cause Timeout-TCP 62
VALUE Disconnect-Cause Foreign-Host-Close-TCP 63
VALUE Disconnect-Cause TCP-Network-Unreachable 64
VALUE Disconnect-Cause TCP-Host-Unreachable 65
VALUE Disconnect-Cause TCP-Network-Admin-Unreachable 66
VALUE Disconnect-Cause TCP-Port-Unreachable 67
VALUE Disconnect-Cause Session-Timeout 100
VALUE Disconnect-Cause Session-Failed-Security 101
VALUE Disconnect-Cause Session-End-Callback 102
VALUE Disconnect-Cause Invalid-Protocol 120
VALUE Disconnect-Cause RADIUS-Disconnect 150
VALUE Disconnect-Cause Local-Admin-Disconnect 151
VALUE Disconnect-Cause SNMP-Disconnect 152
VALUE Disconnect-Cause V110-Retries 160
VALUE Disconnect-Cause PPP-Authentication-Timeout 170
VALUE Disconnect-Cause Local-Hangup 180
VALUE Disconnect-Cause Remote-Hangup 185
VALUE Disconnect-Cause T1-Quiesced 190
VALUE Disconnect-Cause Call-Duration 195
VALUE Disconnect-Cause VPN-User-Disconnect 600
VALUE Disconnect-Cause VPN-Carrier-Loss 601
VALUE Disconnect-Cause VPN-No-Resources 602
VALUE Disconnect-Cause VPN-Bad-Control-Packet 603
VALUE Disconnect-Cause VPN-Admin-Disconnect 604
VALUE Disconnect-Cause VPN-Tunnel-Shut 605
VALUE Disconnect-Cause VPN-Local-Disconnect 606
VALUE Disconnect-Cause VPN-Session-Limit 607
VALUE Disconnect-Cause VPN-Call-Redirect 608
END-VENDOR Cisco
ALIAS Cisco Vendor-Specific.Cisco

View File

@ -1,174 +0,0 @@
# -*- text -*-
# Copyright (C) 2023 The FreeRADIUS Server project and contributors
# This work is licensed under CC-BY version 4.0 https://creativecommons.org/licenses/by/4.0
# Version $Id$
#
# Microsoft's VSA's, from RFC 2548
#
# $Id$
#
VENDOR Microsoft 311
BEGIN-VENDOR Microsoft
ATTRIBUTE CHAP-Response 1 octets[50]
ATTRIBUTE CHAP-Error 2 string
ATTRIBUTE CHAP-CPW-1 3 octets[70]
ATTRIBUTE CHAP-CPW-2 4 octets[84]
ATTRIBUTE CHAP-LM-Enc-PW 5 octets
ATTRIBUTE CHAP-NT-Enc-PW 6 octets
ATTRIBUTE MPPE-Encryption-Policy 7 integer
VALUE MPPE-Encryption-Policy Encryption-Allowed 1
VALUE MPPE-Encryption-Policy Encryption-Required 2
# This is referred to as both singular and plural in the RFC.
# Plural seems to make more sense.
ATTRIBUTE MPPE-Encryption-Type 8 integer
ATTRIBUTE MPPE-Encryption-Types 8 integer
VALUE MPPE-Encryption-Types RC4-40bit-Allowed 1
VALUE MPPE-Encryption-Types RC4-128bit-Allowed 2
VALUE MPPE-Encryption-Types RC4-40or128-bit-Allowed 6
ATTRIBUTE RAS-Vendor 9 integer # content is Vendor-ID
ATTRIBUTE CHAP-Domain 10 string
ATTRIBUTE CHAP-Challenge 11 octets
ATTRIBUTE CHAP-MPPE-Keys 12 octets[24] encrypt=1
ATTRIBUTE BAP-Usage 13 integer
ATTRIBUTE Link-Utilization-Threshold 14 integer # values are 1-100
ATTRIBUTE Link-Drop-Time-Limit 15 integer
ATTRIBUTE MPPE-Send-Key 16 octets encrypt=2
ATTRIBUTE MPPE-Recv-Key 17 octets encrypt=2
ATTRIBUTE RAS-Version 18 string
ATTRIBUTE Old-ARAP-Password 19 octets
ATTRIBUTE New-ARAP-Password 20 octets
ATTRIBUTE ARAP-PW-Change-Reason 21 integer
ATTRIBUTE Filter 22 octets
ATTRIBUTE Acct-Auth-Type 23 integer
ATTRIBUTE Acct-EAP-Type 24 integer
ATTRIBUTE CHAP2-Response 25 octets[50]
ATTRIBUTE CHAP2-Success 26 octets
ATTRIBUTE CHAP2-CPW 27 octets[68]
ATTRIBUTE Primary-DNS-Server 28 ipaddr
ATTRIBUTE Secondary-DNS-Server 29 ipaddr
ATTRIBUTE Primary-NBNS-Server 30 ipaddr
ATTRIBUTE Secondary-NBNS-Server 31 ipaddr
#ATTRIBUTE ARAP-Challenge 33 octets[8]
## RNAP
#
# http://download.microsoft.com/download/9/5/E/95EF66AF-9026-4BB0-A41D-A4F81802D92C/%5BRNAP%5D.pdf
ATTRIBUTE RAS-Client-Name 34 string
ATTRIBUTE RAS-Client-Version 35 string
ATTRIBUTE Quarantine-IPFilter 36 octets
ATTRIBUTE Quarantine-Session-Timeout 37 integer
ATTRIBUTE User-Security-Identity 40 string
ATTRIBUTE Identity-Type 41 integer
ATTRIBUTE Service-Class 42 string
ATTRIBUTE Quarantine-User-Class 44 string
ATTRIBUTE Quarantine-State 45 integer
ATTRIBUTE Quarantine-Grace-Time 46 integer
ATTRIBUTE Network-Access-Server-Type 47 integer
ATTRIBUTE AFW-Zone 48 integer
VALUE AFW-Zone AFW-Zone-Boundary-Policy 1
VALUE AFW-Zone AFW-Zone-Unprotected-Policy 2
VALUE AFW-Zone AFW-Zone-Protected-Policy 3
ATTRIBUTE AFW-Protection-Level 49 integer
VALUE AFW-Protection-Level HECP-Response-Sign-Only 1
VALUE AFW-Protection-Level HECP-Response-Sign-And-Encrypt 2
ATTRIBUTE Machine-Name 50 string
ATTRIBUTE IPv6-Filter 51 octets
ATTRIBUTE IPv4-Remediation-Servers 52 octets
ATTRIBUTE IPv6-Remediation-Servers 53 octets
ATTRIBUTE RNAP-Not-Quarantine-Capable 54 integer
VALUE RNAP-Not-Quarantine-Capable SoH-Sent 0
VALUE RNAP-Not-Quarantine-Capable SoH-Not-Sent 1
ATTRIBUTE Quarantine-SOH 55 octets
ATTRIBUTE RAS-Correlation 56 octets
# Or this might be 56?
ATTRIBUTE Extended-Quarantine-State 57 integer
ATTRIBUTE HCAP-User-Groups 58 string
ATTRIBUTE HCAP-Location-Group-Name 59 string
ATTRIBUTE HCAP-User-Name 60 string
ATTRIBUTE User-IPv4-Address 61 ipaddr
ATTRIBUTE User-IPv6-Address 62 ipv6addr
ATTRIBUTE TSG-Device-Redirection 63 integer
#
# Integer Translations
#
# BAP-Usage Values
VALUE BAP-Usage Not-Allowed 0
VALUE BAP-Usage Allowed 1
VALUE BAP-Usage Required 2
# ARAP-Password-Change-Reason Values
VALUE ARAP-PW-Change-Reason Just-Change-Password 1
VALUE ARAP-PW-Change-Reason Expired-Password 2
VALUE ARAP-PW-Change-Reason Admin-Requires-Password-Change 3
VALUE ARAP-PW-Change-Reason Password-Too-Short 4
# Acct-Auth-Type Values
VALUE Acct-Auth-Type PAP 1
VALUE Acct-Auth-Type CHAP 2
VALUE Acct-Auth-Type CHAP-1 3
VALUE Acct-Auth-Type CHAP-2 4
VALUE Acct-Auth-Type EAP 5
# Acct-EAP-Type Values
VALUE Acct-EAP-Type MD5 4
VALUE Acct-EAP-Type OTP 5
VALUE Acct-EAP-Type Generic-Token-Card 6
VALUE Acct-EAP-Type TLS 13
# Identity-Type Values
VALUE Identity-Type Machine-Health-Check 1
VALUE Identity-Type Ignore-User-Lookup-Failure 2
# Quarantine-State Values
VALUE Quarantine-State Full-Access 0
VALUE Quarantine-State Quarantine 1
VALUE Quarantine-State Probation 2
# Network-Access-Server-Type Values
VALUE Network-Access-Server-Type Unspecified 0
VALUE Network-Access-Server-Type Terminal-Server-Gateway 1
VALUE Network-Access-Server-Type Remote-Access-Server 2
VALUE Network-Access-Server-Type DHCP-Server 3
VALUE Network-Access-Server-Type Wireless-Access-Point 4
VALUE Network-Access-Server-Type HRA 5
VALUE Network-Access-Server-Type HCAP-Server 6
# Extended-Quarantine-State Values
VALUE Extended-Quarantine-State Transition 1
VALUE Extended-Quarantine-State Infected 2
VALUE Extended-Quarantine-State Unknown 3
VALUE Extended-Quarantine-State No-Data 4
END-VENDOR Microsoft
ALIAS Microsoft Vendor-Specific.Microsoft

View File

@ -1,36 +0,0 @@
# Generated by Django 5.0.7 on 2024-07-17 10:01
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0035_alter_group_options_and_more"),
("authentik_providers_radius", "0002_radiusprovider_mfa_support"),
]
operations = [
migrations.CreateModel(
name="RadiusProviderPropertyMapping",
fields=[
(
"propertymapping_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.propertymapping",
),
),
],
options={
"verbose_name": "Radius Property Mapping",
"verbose_name_plural": "Radius Property Mappings",
},
bases=("authentik_core.propertymapping",),
),
]

Some files were not shown because too many files have changed in this diff Show More