Compare commits
10 Commits
version/20
...
version/0.
Author | SHA1 | Date | |
---|---|---|---|
7ac870bfe5 | |||
101b973cfe | |||
a1bd6bfe17 | |||
ebe0f84460 | |||
c8805cc082 | |||
db92178d0f | |||
ad029d3e0a | |||
b0bd68232d | |||
65355372ce | |||
53d9092022 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2021.1.1-rc1
|
current_version = 0.14.2-stable
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||||
@ -31,6 +31,6 @@ values =
|
|||||||
|
|
||||||
[bumpversion:file:authentik/__init__.py]
|
[bumpversion:file:authentik/__init__.py]
|
||||||
|
|
||||||
[bumpversion:file:outpost/pkg/version.go]
|
[bumpversion:file:proxy/pkg/version.go]
|
||||||
|
|
||||||
[bumpversion:file:web/src/constants.ts]
|
[bumpversion:file:web/src/constants.ts]
|
||||||
|
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -18,11 +18,11 @@ jobs:
|
|||||||
- name: Building Docker Image
|
- name: Building Docker Image
|
||||||
run: docker build
|
run: docker build
|
||||||
--no-cache
|
--no-cache
|
||||||
-t beryju/authentik:2021.1.1-rc1
|
-t beryju/authentik:0.14.2-stable
|
||||||
-t beryju/authentik:latest
|
-t beryju/authentik:latest
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/authentik:2021.1.1-rc1
|
run: docker push beryju/authentik:0.14.2-stable
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/authentik:latest
|
run: docker push beryju/authentik:latest
|
||||||
build-proxy:
|
build-proxy:
|
||||||
@ -48,11 +48,11 @@ jobs:
|
|||||||
cd proxy/
|
cd proxy/
|
||||||
docker build \
|
docker build \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
-t beryju/authentik-proxy:2021.1.1-rc1 \
|
-t beryju/authentik-proxy:0.14.2-stable \
|
||||||
-t beryju/authentik-proxy:latest \
|
-t beryju/authentik-proxy:latest \
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/authentik-proxy:2021.1.1-rc1
|
run: docker push beryju/authentik-proxy:0.14.2-stable
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/authentik-proxy:latest
|
run: docker push beryju/authentik-proxy:latest
|
||||||
build-static:
|
build-static:
|
||||||
@ -69,11 +69,11 @@ jobs:
|
|||||||
cd web/
|
cd web/
|
||||||
docker build \
|
docker build \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
-t beryju/authentik-static:2021.1.1-rc1 \
|
-t beryju/authentik-static:0.14.2-stable \
|
||||||
-t beryju/authentik-static:latest \
|
-t beryju/authentik-static:latest \
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/authentik-static:2021.1.1-rc1
|
run: docker push beryju/authentik-static:0.14.2-stable
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/authentik-static:latest
|
run: docker push beryju/authentik-static:latest
|
||||||
test-release:
|
test-release:
|
||||||
@ -107,5 +107,5 @@ jobs:
|
|||||||
SENTRY_PROJECT: authentik
|
SENTRY_PROJECT: authentik
|
||||||
SENTRY_URL: https://sentry.beryju.org
|
SENTRY_URL: https://sentry.beryju.org
|
||||||
with:
|
with:
|
||||||
tagName: 2021.1.1-rc1
|
tagName: 0.14.2-stable
|
||||||
environment: beryjuorg-prod
|
environment: beryjuorg-prod
|
||||||
|
69
Pipfile.lock
generated
69
Pipfile.lock
generated
@ -74,18 +74,18 @@
|
|||||||
},
|
},
|
||||||
"boto3": {
|
"boto3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b5052144034e490358c659d0e480c17a4e604fd3aee9a97ddfe6e361a245a4a5",
|
"sha256:197926eaf0065c2c503914a15edc75f4ac259c1e5ae6d17eabd1ba5d8ebd1554",
|
||||||
"sha256:efd6c96c98900e9fbf217f13cb58f59b793e51f69a1ce61817eefd31f17c6ef5"
|
"sha256:d6991e6fd7d0f63bf94282687700a91f5299b807e544cb3367e9b2faeeaf8c62"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.16.55"
|
"version": "==1.16.46"
|
||||||
},
|
},
|
||||||
"botocore": {
|
"botocore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:760d0c16c1474c2a46e3fa45e33ae7457b5cab7410737ab1692340ade764cc73",
|
"sha256:85ca6915ad5471e7f6cd1b00610b74601d2970cbf8e9b1bf255697154cf621a3",
|
||||||
"sha256:b34327d84b3bb5620fb54603677a9a973b167290c2c1e7ab69c4a46b201c6d46"
|
"sha256:f7d365c689070368a5a0857aa35a81d7c950556189f23065f42798f810a59cae"
|
||||||
],
|
],
|
||||||
"version": "==1.19.55"
|
"version": "==1.19.46"
|
||||||
},
|
},
|
||||||
"cachetools": {
|
"cachetools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -265,11 +265,11 @@
|
|||||||
},
|
},
|
||||||
"django": {
|
"django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2d78425ba74c7a1a74b196058b261b9733a8570782f4e2828974777ccca7edf7",
|
"sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2",
|
||||||
"sha256:efa2ab96b33b20c2182db93147a0c3cd7769d418926f9e9f140a60dca7c64ca9"
|
"sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.1.5"
|
"version": "==3.1.4"
|
||||||
},
|
},
|
||||||
"django-cors-middleware": {
|
"django-cors-middleware": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -351,8 +351,7 @@
|
|||||||
},
|
},
|
||||||
"djangorestframework": {
|
"djangorestframework": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7",
|
"sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7"
|
||||||
"sha256:0898182b4737a7b584a2c73735d89816343369f259fea932d90dc78e35d8ac33"
|
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.12.2"
|
"version": "==3.12.2"
|
||||||
@ -412,10 +411,10 @@
|
|||||||
},
|
},
|
||||||
"h11": {
|
"h11": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
|
"sha256:3c6c61d69c6f13d41f1b80ab0322f1872702a3ba26e12aa864c928f6a43fbaab",
|
||||||
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
|
"sha256:ab6c335e1b6ef34b205d5ca3e228c9299cc7218b049819ec84a388c2525e5d87"
|
||||||
],
|
],
|
||||||
"version": "==0.12.0"
|
"version": "==0.11.0"
|
||||||
},
|
},
|
||||||
"hiredis": {
|
"hiredis": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -487,10 +486,10 @@
|
|||||||
},
|
},
|
||||||
"hyperlink": {
|
"hyperlink": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b",
|
"sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af",
|
||||||
"sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"
|
"sha256:c528d405766f15a2c536230de7e160b65a08e20264d8891b3eb03307b0df3c63"
|
||||||
],
|
],
|
||||||
"version": "==21.0.0"
|
"version": "==20.0.1"
|
||||||
},
|
},
|
||||||
"idna": {
|
"idna": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -702,10 +701,10 @@
|
|||||||
},
|
},
|
||||||
"prompt-toolkit": {
|
"prompt-toolkit": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:ac329c69bd8564cb491940511957312c7b8959bb5b3cf3582b406068a51d5bb7",
|
"sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c",
|
||||||
"sha256:b8b3d0bde65da350290c46a8f54f336b3cbf5464a4ac11239668d986852e79d5"
|
"sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63"
|
||||||
],
|
],
|
||||||
"version": "==3.0.10"
|
"version": "==3.0.8"
|
||||||
},
|
},
|
||||||
"psycopg2-binary": {
|
"psycopg2-binary": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -956,11 +955,11 @@
|
|||||||
},
|
},
|
||||||
"rsa": {
|
"rsa": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:69805d6b69f56eb05b62daea3a7dbd7aa44324ad1306445e05da8060232d00f4",
|
"sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa",
|
||||||
"sha256:a8774e55b59fd9fc893b0d05e9bfc6f47081f46ff5b46f39ccf24631b7be356b"
|
"sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==4.7"
|
"version": "==4.6"
|
||||||
},
|
},
|
||||||
"ruamel.yaml": {
|
"ruamel.yaml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -971,10 +970,10 @@
|
|||||||
},
|
},
|
||||||
"s3transfer": {
|
"s3transfer": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1e28620e5b444652ed752cf87c7e0cb15b0e578972568c6609f0f18212f259ed",
|
"sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13",
|
||||||
"sha256:7fdddb4f22275cf1d32129e21f056337fd2a80b6ccef1664528145b72c49e6d2"
|
"sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db"
|
||||||
],
|
],
|
||||||
"version": "==0.3.4"
|
"version": "==0.3.3"
|
||||||
},
|
},
|
||||||
"sentry-sdk": {
|
"sentry-sdk": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1008,11 +1007,11 @@
|
|||||||
},
|
},
|
||||||
"structlog": {
|
"structlog": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:33dd6bd5f49355e52c1c61bb6a4f20d0b48ce0328cc4a45fe872d38b97a05ccd",
|
"sha256:7a48375db6274ed1d0ae6123c486472aa1d0890b08d314d2b016f3aa7f35990b",
|
||||||
"sha256:af79dfa547d104af8d60f86eac12fb54825f54a46bc998e4504ef66177103174"
|
"sha256:8a672be150547a93d90a7d74229a29e765be05bd156a35cdcc527ebf68e9af92"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==20.2.0"
|
"version": "==20.1.0"
|
||||||
},
|
},
|
||||||
"swagger-spec-validator": {
|
"swagger-spec-validator": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1374,11 +1373,11 @@
|
|||||||
},
|
},
|
||||||
"django": {
|
"django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2d78425ba74c7a1a74b196058b261b9733a8570782f4e2828974777ccca7edf7",
|
"sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2",
|
||||||
"sha256:efa2ab96b33b20c2182db93147a0c3cd7769d418926f9e9f140a60dca7c64ca9"
|
"sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.1.5"
|
"version": "==3.1.4"
|
||||||
},
|
},
|
||||||
"django-debug-toolbar": {
|
"django-debug-toolbar": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1418,10 +1417,10 @@
|
|||||||
},
|
},
|
||||||
"gitpython": {
|
"gitpython": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:42dbefd8d9e2576c496ed0059f3103dcef7125b9ce16f9d5f9c834aed44a1dac",
|
"sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b",
|
||||||
"sha256:867ec3dfb126aac0f8296b19fb63b8c4a399f32b4b6fafe84c4b10af5fa9f7b5"
|
"sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"
|
||||||
],
|
],
|
||||||
"version": "==3.1.12"
|
"version": "==3.1.11"
|
||||||
},
|
},
|
||||||
"iniconfig": {
|
"iniconfig": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
12
SECURITY.md
12
SECURITY.md
@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
| Version | Supported |
|
As authentik is currently in a pre-stable, only the latest "stable" version is supported. After authentik 1.0, this will change.
|
||||||
| ---------- | ------------------ |
|
|
||||||
| 0.13.x | :white_check_mark: |
|
| Version | Supported |
|
||||||
| 0.14.x | :white_check_mark: |
|
| -------- | ------------------ |
|
||||||
| 2021.1.x | :white_check_mark: |
|
| 0.12.x | :white_check_mark: |
|
||||||
|
| 0.13.x | :white_check_mark: |
|
||||||
|
| 0.14.x | :white_check_mark: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
"""authentik"""
|
"""authentik"""
|
||||||
__version__ = "2021.1.1-rc1"
|
__version__ = "0.14.2-stable"
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from packaging.version import parse
|
from packaging.version import parse
|
||||||
from requests import RequestException, get
|
from requests import RequestException, get
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik import __version__
|
from authentik import __version__
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
{% for task in object_list %}
|
{% for task in object_list %}
|
||||||
<tr role="row">
|
<tr role="row">
|
||||||
<th role="columnheader">
|
<th role="columnheader">
|
||||||
<span>{{ task.html_name|join:"_­" }}</span>
|
<pre>{{ task.task_name }}</pre>
|
||||||
</th>
|
</th>
|
||||||
<td role="cell">
|
<td role="cell">
|
||||||
<span>
|
<span>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from django import template
|
from django import template
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
@ -32,7 +32,7 @@ REQUEST_MOCK_VALID = Mock(
|
|||||||
return_value=MockResponse(
|
return_value=MockResponse(
|
||||||
200,
|
200,
|
||||||
"""{
|
"""{
|
||||||
"tag_name": "version/99999999.9999999"
|
"tag_name": "version/1.2.3"
|
||||||
}""",
|
}""",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -47,10 +47,10 @@ class TestAdminTasks(TestCase):
|
|||||||
def test_version_valid_response(self):
|
def test_version_valid_response(self):
|
||||||
"""Test Update checker with valid response"""
|
"""Test Update checker with valid response"""
|
||||||
update_latest_version.delay().get()
|
update_latest_version.delay().get()
|
||||||
self.assertEqual(cache.get(VERSION_CACHE_KEY), "99999999.9999999")
|
self.assertEqual(cache.get(VERSION_CACHE_KEY), "1.2.3")
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
Event.objects.filter(
|
Event.objects.filter(
|
||||||
action=EventAction.UPDATE_AVAILABLE, context__new_version="99999999.9999999"
|
action=EventAction.UPDATE_AVAILABLE, context__new_version="1.2.3"
|
||||||
).exists()
|
).exists()
|
||||||
)
|
)
|
||||||
# test that a consecutive check doesn't create a duplicate event
|
# test that a consecutive check doesn't create a duplicate event
|
||||||
@ -58,7 +58,7 @@ class TestAdminTasks(TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(
|
len(
|
||||||
Event.objects.filter(
|
Event.objects.filter(
|
||||||
action=EventAction.UPDATE_AVAILABLE, context__new_version="99999999.9999999"
|
action=EventAction.UPDATE_AVAILABLE, context__new_version="1.2.3"
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
1,
|
1,
|
||||||
|
@ -4,8 +4,6 @@ from django.urls import path
|
|||||||
from authentik.admin.views import (
|
from authentik.admin.views import (
|
||||||
applications,
|
applications,
|
||||||
certificate_key_pair,
|
certificate_key_pair,
|
||||||
events_notifications_rules,
|
|
||||||
events_notifications_transports,
|
|
||||||
flows,
|
flows,
|
||||||
groups,
|
groups,
|
||||||
outposts,
|
outposts,
|
||||||
@ -354,36 +352,4 @@ urlpatterns = [
|
|||||||
tasks.TaskListView.as_view(),
|
tasks.TaskListView.as_view(),
|
||||||
name="tasks",
|
name="tasks",
|
||||||
),
|
),
|
||||||
# Event Notification Transpots
|
|
||||||
path(
|
|
||||||
"events/transports/create/",
|
|
||||||
events_notifications_transports.NotificationTransportCreateView.as_view(),
|
|
||||||
name="notification-transport-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"events/transports/<uuid:pk>/update/",
|
|
||||||
events_notifications_transports.NotificationTransportUpdateView.as_view(),
|
|
||||||
name="notification-transport-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"events/transports/<uuid:pk>/delete/",
|
|
||||||
events_notifications_transports.NotificationTransportDeleteView.as_view(),
|
|
||||||
name="notification-transport-delete",
|
|
||||||
),
|
|
||||||
# Event Notification Rules
|
|
||||||
path(
|
|
||||||
"events/rules/create/",
|
|
||||||
events_notifications_rules.NotificationRuleCreateView.as_view(),
|
|
||||||
name="notification-rule-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"events/rules/<uuid:pk>/update/",
|
|
||||||
events_notifications_rules.NotificationRuleUpdateView.as_view(),
|
|
||||||
name="notification-rule-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"events/rules/<uuid:pk>/delete/",
|
|
||||||
events_notifications_rules.NotificationRuleDeleteView.as_view(),
|
|
||||||
name="notification-rule-delete",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
"""authentik NotificationRule administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView
|
|
||||||
from authentik.events.forms import NotificationRuleForm
|
|
||||||
from authentik.events.models import NotificationRule
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationRuleCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
BackSuccessUrlMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new NotificationRule"""
|
|
||||||
|
|
||||||
model = NotificationRule
|
|
||||||
form_class = NotificationRuleForm
|
|
||||||
permission_required = "authentik_events.add_NotificationRule"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully created Notification Rule")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationRuleUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
BackSuccessUrlMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update application"""
|
|
||||||
|
|
||||||
model = NotificationRule
|
|
||||||
form_class = NotificationRuleForm
|
|
||||||
permission_required = "authentik_events.change_NotificationRule"
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully updated Notification Rule")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationRuleDeleteView(
|
|
||||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
|
||||||
):
|
|
||||||
"""Delete application"""
|
|
||||||
|
|
||||||
model = NotificationRule
|
|
||||||
permission_required = "authentik_events.delete_NotificationRule"
|
|
||||||
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully deleted Notification Rule")
|
|
@ -1,64 +0,0 @@
|
|||||||
"""authentik NotificationTransport administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView
|
|
||||||
from authentik.events.forms import NotificationTransportForm
|
|
||||||
from authentik.events.models import NotificationTransport
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
BackSuccessUrlMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new NotificationTransport"""
|
|
||||||
|
|
||||||
model = NotificationTransport
|
|
||||||
form_class = NotificationTransportForm
|
|
||||||
permission_required = "authentik_events.add_notificationtransport"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully created Notification Transport")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
BackSuccessUrlMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update application"""
|
|
||||||
|
|
||||||
model = NotificationTransport
|
|
||||||
form_class = NotificationTransportForm
|
|
||||||
permission_required = "authentik_events.change_notificationtransport"
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully updated Notification Transport")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportDeleteView(
|
|
||||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
|
||||||
):
|
|
||||||
"""Delete application"""
|
|
||||||
|
|
||||||
model = NotificationTransport
|
|
||||||
permission_required = "authentik_events.delete_notificationtransport"
|
|
||||||
|
|
||||||
template_name = "generic/delete.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:shell")
|
|
||||||
success_message = _("Successfully deleted Notification Transport")
|
|
@ -5,11 +5,10 @@ from django.http.request import HttpRequest
|
|||||||
from django.http.response import HttpResponse
|
from django.http.response import HttpResponse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.admin.forms.overview import FlowCacheClearForm, PolicyCacheClearForm
|
from authentik.admin.forms.overview import FlowCacheClearForm, PolicyCacheClearForm
|
||||||
from authentik.admin.mixins import AdminRequiredMixin
|
from authentik.admin.mixins import AdminRequiredMixin
|
||||||
from authentik.core.api.applications import user_app_cache_key
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -27,9 +26,6 @@ class PolicyCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
|
|||||||
keys = cache.keys("policy_*")
|
keys = cache.keys("policy_*")
|
||||||
cache.delete_many(keys)
|
cache.delete_many(keys)
|
||||||
LOGGER.debug("Cleared Policy cache", keys=len(keys))
|
LOGGER.debug("Cleared Policy cache", keys=len(keys))
|
||||||
# Also delete user application cache
|
|
||||||
keys = user_app_cache_key("*")
|
|
||||||
cache.delete_many(keys)
|
|
||||||
return super().post(request, *args, **kwargs)
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ from typing import Any, Optional, Tuple, Union
|
|||||||
|
|
||||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.models import Token, TokenIntents, User
|
from authentik.core.models import Token, TokenIntents, User
|
||||||
|
|
||||||
|
@ -19,10 +19,7 @@ from authentik.core.api.sources import SourceViewSet
|
|||||||
from authentik.core.api.tokens import TokenViewSet
|
from authentik.core.api.tokens import TokenViewSet
|
||||||
from authentik.core.api.users import UserViewSet
|
from authentik.core.api.users import UserViewSet
|
||||||
from authentik.crypto.api import CertificateKeyPairViewSet
|
from authentik.crypto.api import CertificateKeyPairViewSet
|
||||||
from authentik.events.api.event import EventViewSet
|
from authentik.events.api import EventViewSet
|
||||||
from authentik.events.api.notification import NotificationViewSet
|
|
||||||
from authentik.events.api.notification_rule import NotificationRuleViewSet
|
|
||||||
from authentik.events.api.notification_transport import NotificationTransportViewSet
|
|
||||||
from authentik.flows.api import (
|
from authentik.flows.api import (
|
||||||
FlowCacheViewSet,
|
FlowCacheViewSet,
|
||||||
FlowStageBindingViewSet,
|
FlowStageBindingViewSet,
|
||||||
@ -40,7 +37,6 @@ from authentik.policies.api import (
|
|||||||
PolicyViewSet,
|
PolicyViewSet,
|
||||||
)
|
)
|
||||||
from authentik.policies.dummy.api import DummyPolicyViewSet
|
from authentik.policies.dummy.api import DummyPolicyViewSet
|
||||||
from authentik.policies.event_matcher.api import EventMatcherPolicyViewSet
|
|
||||||
from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet
|
from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet
|
||||||
from authentik.policies.expression.api import ExpressionPolicyViewSet
|
from authentik.policies.expression.api import ExpressionPolicyViewSet
|
||||||
from authentik.policies.group_membership.api import GroupMembershipPolicyViewSet
|
from authentik.policies.group_membership.api import GroupMembershipPolicyViewSet
|
||||||
@ -101,9 +97,6 @@ router.register("flows/bindings", FlowStageBindingViewSet)
|
|||||||
router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet)
|
router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet)
|
||||||
|
|
||||||
router.register("events/events", EventViewSet)
|
router.register("events/events", EventViewSet)
|
||||||
router.register("events/notifications", NotificationViewSet)
|
|
||||||
router.register("events/transports", NotificationTransportViewSet)
|
|
||||||
router.register("events/rules", NotificationRuleViewSet)
|
|
||||||
|
|
||||||
router.register("sources/all", SourceViewSet)
|
router.register("sources/all", SourceViewSet)
|
||||||
router.register("sources/ldap", LDAPSourceViewSet)
|
router.register("sources/ldap", LDAPSourceViewSet)
|
||||||
@ -114,7 +107,6 @@ router.register("policies/all", PolicyViewSet)
|
|||||||
router.register("policies/cached", PolicyCacheViewSet, basename="policies_cache")
|
router.register("policies/cached", PolicyCacheViewSet, basename="policies_cache")
|
||||||
router.register("policies/bindings", PolicyBindingViewSet)
|
router.register("policies/bindings", PolicyBindingViewSet)
|
||||||
router.register("policies/expression", ExpressionPolicyViewSet)
|
router.register("policies/expression", ExpressionPolicyViewSet)
|
||||||
router.register("policies/event_matcher", EventMatcherPolicyViewSet)
|
|
||||||
router.register("policies/group_membership", GroupMembershipPolicyViewSet)
|
router.register("policies/group_membership", GroupMembershipPolicyViewSet)
|
||||||
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
|
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
|
||||||
router.register("policies/password_expiry", PasswordExpiryPolicyViewSet)
|
router.register("policies/password_expiry", PasswordExpiryPolicyViewSet)
|
||||||
|
@ -4,7 +4,7 @@ from django.apps import AppConfig, apps
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin.sites import AlreadyRegistered
|
from django.contrib.admin.sites import AlreadyRegistered
|
||||||
from guardian.admin import GuardedModelAdmin
|
from guardian.admin import GuardedModelAdmin
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
"""Application API Views"""
|
"""Application API Views"""
|
||||||
from django.core.cache import cache
|
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.http.response import Http404
|
from django.http.response import Http404
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from guardian.shortcuts import get_objects_for_user
|
||||||
@ -19,11 +18,6 @@ from authentik.events.models import EventAction
|
|||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
|
|
||||||
|
|
||||||
def user_app_cache_key(user_pk: str) -> str:
|
|
||||||
"""Cache key where application list for user is saved"""
|
|
||||||
return f"user_app_cache_{user_pk}"
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationSerializer(ModelSerializer):
|
class ApplicationSerializer(ModelSerializer):
|
||||||
"""Application Serializer"""
|
"""Application Serializer"""
|
||||||
|
|
||||||
@ -78,15 +72,12 @@ class ApplicationViewSet(ModelViewSet):
|
|||||||
"""Custom list method that checks Policy based access instead of guardian"""
|
"""Custom list method that checks Policy based access instead of guardian"""
|
||||||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||||
self.paginate_queryset(queryset)
|
self.paginate_queryset(queryset)
|
||||||
allowed_applications = cache.get(user_app_cache_key(self.request.user.pk))
|
allowed_applications = []
|
||||||
if not allowed_applications:
|
for application in queryset:
|
||||||
allowed_applications = []
|
engine = PolicyEngine(application, self.request.user, self.request)
|
||||||
for application in queryset:
|
engine.build()
|
||||||
engine = PolicyEngine(application, self.request.user, self.request)
|
if engine.passing:
|
||||||
engine.build()
|
allowed_applications.append(application)
|
||||||
if engine.passing:
|
|
||||||
allowed_applications.append(application)
|
|
||||||
cache.set(user_app_cache_key(self.request.user.pk), allowed_applications)
|
|
||||||
serializer = self.get_serializer(allowed_applications, many=True)
|
serializer = self.get_serializer(allowed_applications, many=True)
|
||||||
return self.get_paginated_response(serializer.data)
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ class PropertyMappingSerializer(ModelSerializer):
|
|||||||
class PropertyMappingViewSet(ReadOnlyModelViewSet):
|
class PropertyMappingViewSet(ReadOnlyModelViewSet):
|
||||||
"""PropertyMapping Viewset"""
|
"""PropertyMapping Viewset"""
|
||||||
|
|
||||||
queryset = PropertyMapping.objects.none()
|
queryset = PropertyMapping.objects.all()
|
||||||
serializer_class = PropertyMappingSerializer
|
serializer_class = PropertyMappingSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -39,7 +39,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
class ProviderViewSet(ModelViewSet):
|
class ProviderViewSet(ModelViewSet):
|
||||||
"""Provider Viewset"""
|
"""Provider Viewset"""
|
||||||
|
|
||||||
queryset = Provider.objects.none()
|
queryset = Provider.objects.all()
|
||||||
serializer_class = ProviderSerializer
|
serializer_class = ProviderSerializer
|
||||||
filterset_fields = {
|
filterset_fields = {
|
||||||
"application": ["isnull"],
|
"application": ["isnull"],
|
||||||
|
@ -31,7 +31,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
class SourceViewSet(ReadOnlyModelViewSet):
|
class SourceViewSet(ReadOnlyModelViewSet):
|
||||||
"""Source Viewset"""
|
"""Source Viewset"""
|
||||||
|
|
||||||
queryset = Source.objects.none()
|
queryset = Source.objects.all()
|
||||||
serializer_class = SourceSerializer
|
serializer_class = SourceSerializer
|
||||||
lookup_field = "slug"
|
lookup_field = "slug"
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ class UserSerializer(ModelSerializer):
|
|||||||
class UserViewSet(ModelViewSet):
|
class UserViewSet(ModelViewSet):
|
||||||
"""User Viewset"""
|
"""User Viewset"""
|
||||||
|
|
||||||
queryset = User.objects.none()
|
queryset = User.objects.all()
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Channels base classes"""
|
"""Channels base classes"""
|
||||||
from channels.exceptions import DenyConnection
|
from channels.exceptions import DenyConnection
|
||||||
from channels.generic.websocket import JsonWebsocketConsumer
|
from channels.generic.websocket import JsonWebsocketConsumer
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.api.auth import token_from_header
|
from authentik.api.auth import token_from_header
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
@ -28,7 +28,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
|||||||
event = Event.new(
|
event = Event.new(
|
||||||
EventAction.PROPERTY_MAPPING_EXCEPTION,
|
EventAction.PROPERTY_MAPPING_EXCEPTION,
|
||||||
expression=expression_source,
|
expression=expression_source,
|
||||||
message=error_string,
|
error=error_string,
|
||||||
)
|
)
|
||||||
if "user" in self._context:
|
if "user" in self._context:
|
||||||
event.set_user(self._context["user"])
|
event.set_user(self._context["user"])
|
||||||
|
@ -15,7 +15,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from guardian.mixins import GuardianUserMixin
|
from guardian.mixins import GuardianUserMixin
|
||||||
from model_utils.managers import InheritanceManager
|
from model_utils.managers import InheritanceManager
|
||||||
from rest_framework.serializers import Serializer
|
from rest_framework.serializers import Serializer
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||||
from authentik.core.signals import password_changed
|
from authentik.core.signals import password_changed
|
||||||
|
@ -8,7 +8,7 @@ from dbbackup.db.exceptions import CommandConnectorError
|
|||||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||||
from django.core import management
|
from django.core import management
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.models import ExpiringModel
|
from authentik.core.models import ExpiringModel
|
||||||
from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
|
from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||||
|
@ -9,14 +9,14 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
<title>{% block title %}{% trans title|default:config.authentik.branding.title %}{% endblock %}</title>
|
<title>{% block title %}{% trans title|default:config.authentik.branding.title %}{% endblock %}</title>
|
||||||
<link rel="icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}">
|
<link rel="icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
|
||||||
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}">
|
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-addons.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-addons.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/fontawesome.min.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/fontawesome.min.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||||
<script src="{% url 'javascript-catalog' %}?v={{ ak_version }}"></script>
|
<script src="{% url 'javascript-catalog' %}"></script>
|
||||||
<script src="{% static 'dist/main.js' %}?v={{ ak_version }}" type="module"></script>
|
<script src="{% static 'dist/main.js' %}" type="module"></script>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load authentik_user_settings %}
|
{% load authentik_user_settings %}
|
||||||
{% load authentik_utils %}
|
|
||||||
|
|
||||||
<div class="pf-c-page">
|
<div class="pf-c-page">
|
||||||
<main role="main" class="pf-c-page__main" tabindex="-1">
|
<main role="main" class="pf-c-page__main" tabindex="-1">
|
||||||
@ -13,45 +12,47 @@
|
|||||||
<p>{% trans "Configure settings relevant to your user profile." %}</p>
|
<p>{% trans "Configure settings relevant to your user profile." %}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<ak-tabs>
|
<section class="pf-c-page__main-section">
|
||||||
<section slot="page-1" data-tab-title="{% trans 'User details' %}" class="pf-c-page__main-section pf-m-no-padding-mobile">
|
<div class="pf-u-display-flex pf-u-justify-content-center">
|
||||||
<div class="pf-u-display-flex pf-u-justify-content-center">
|
<div class="pf-u-w-75">
|
||||||
<div class="pf-u-w-75">
|
<ak-site-shell url="{% url 'authentik_core:user-details' %}">
|
||||||
<ak-site-shell url="{% url 'authentik_core:user-details' %}">
|
<div slot="body"></div>
|
||||||
<div slot="body"></div>
|
</ak-site-shell>
|
||||||
</ak-site-shell>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
<section slot="page-2" data-tab-title="{% trans 'Tokens' %}" class="pf-c-page__main-section pf-m-no-padding-mobile">
|
</section>
|
||||||
<ak-site-shell url="{% url 'authentik_core:user-tokens' %}">
|
<section class="pf-c-page__main-section">
|
||||||
<div slot="body"></div>
|
<div class="pf-u-display-flex pf-u-justify-content-center">
|
||||||
</ak-site-shell>
|
<div class="pf-u-w-75">
|
||||||
</section>
|
<ak-site-shell url="{% url 'authentik_core:user-tokens' %}">
|
||||||
{% user_stages as user_stages_loc %}
|
<div slot="body"></div>
|
||||||
{% for stage, stage_link in user_stages_loc.items %}
|
</ak-site-shell>
|
||||||
<section slot="page-{{ stage.pk }}" data-tab-title="{{ stage|verbose_name }}" class="pf-c-page__main-section pf-m-no-padding-mobile">
|
|
||||||
<div class="pf-u-display-flex pf-u-justify-content-center">
|
|
||||||
<div class="pf-u-w-75">
|
|
||||||
<ak-site-shell url="{{ stage_link }}">
|
|
||||||
<div slot="body"></div>
|
|
||||||
</ak-site-shell>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
{% endfor %}
|
</section>
|
||||||
{% user_sources as user_sources_loc %}
|
{% user_stages as user_stages_loc %}
|
||||||
{% for source, source_link in user_sources_loc.item %}
|
{% for stage in user_stages_loc %}
|
||||||
<section slot="page-{{ source.pk }}" data-tab-title="{{ source|verbose_name }}" class="pf-c-page__main-section pf-m-no-padding-mobile">
|
<section class="pf-c-page__main-section">
|
||||||
<div class="pf-u-display-flex pf-u-justify-content-center">
|
<div class="pf-u-display-flex pf-u-justify-content-center">
|
||||||
<div class="pf-u-w-75">
|
<div class="pf-u-w-75">
|
||||||
<ak-site-shell url="{{ source_link }}">
|
<ak-site-shell url="{{ stage }}">
|
||||||
<div slot="body"></div>
|
<div slot="body"></div>
|
||||||
</ak-site-shell>
|
</ak-site-shell>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
{% endfor %}
|
</section>
|
||||||
</ak-tabs>
|
{% endfor %}
|
||||||
|
{% user_sources as user_sources_loc %}
|
||||||
|
{% for source in user_sources_loc %}
|
||||||
|
<section class="pf-c-page__main-section">
|
||||||
|
<div class="pf-u-display-flex pf-u-justify-content-center">
|
||||||
|
<div class="pf-u-w-75">
|
||||||
|
<ak-site-shell url="{{ source }}">
|
||||||
|
<div slot="body"></div>
|
||||||
|
</ak-site-shell>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endfor %}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,26 +13,26 @@ register = template.Library()
|
|||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def user_stages(context: RequestContext) -> dict[Stage, str]:
|
def user_stages(context: RequestContext) -> list[str]:
|
||||||
"""Return list of all stages which apply to user"""
|
"""Return list of all stages which apply to user"""
|
||||||
_all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses()
|
_all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses()
|
||||||
matching_stages: dict[Stage, str] = {}
|
matching_stages: list[str] = []
|
||||||
for stage in _all_stages:
|
for stage in _all_stages:
|
||||||
user_settings = stage.ui_user_settings
|
user_settings = stage.ui_user_settings
|
||||||
if not user_settings:
|
if not user_settings:
|
||||||
continue
|
continue
|
||||||
matching_stages[stage] = user_settings
|
matching_stages.append(user_settings)
|
||||||
return matching_stages
|
return matching_stages
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def user_sources(context: RequestContext) -> dict[Source, str]:
|
def user_sources(context: RequestContext) -> list[str]:
|
||||||
"""Return a list of all sources which are enabled for the user"""
|
"""Return a list of all sources which are enabled for the user"""
|
||||||
user = context.get("request").user
|
user = context.get("request").user
|
||||||
_all_sources: Iterable[Source] = Source.objects.filter(
|
_all_sources: Iterable[Source] = Source.objects.filter(
|
||||||
enabled=True
|
enabled=True
|
||||||
).select_subclasses()
|
).select_subclasses()
|
||||||
matching_sources: dict[Source, str] = {}
|
matching_sources: list[str] = []
|
||||||
for source in _all_sources:
|
for source in _all_sources:
|
||||||
user_settings = source.ui_user_settings
|
user_settings = source.ui_user_settings
|
||||||
if not user_settings:
|
if not user_settings:
|
||||||
@ -40,5 +40,5 @@ def user_sources(context: RequestContext) -> dict[Source, str]:
|
|||||||
policy_engine = PolicyEngine(source, user, context.get("request"))
|
policy_engine = PolicyEngine(source, user, context.get("request"))
|
||||||
policy_engine.build()
|
policy_engine.build()
|
||||||
if policy_engine.passing:
|
if policy_engine.passing:
|
||||||
matching_sources[source] = user_settings
|
matching_sources.append(user_settings)
|
||||||
return matching_sources
|
return matching_sources
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.middleware import (
|
from authentik.core.middleware import (
|
||||||
SESSION_IMPERSONATE_ORIGINAL_USER,
|
SESSION_IMPERSONATE_ORIGINAL_USER,
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
"""Notification API Views"""
|
|
||||||
from rest_framework import mixins
|
|
||||||
from rest_framework.fields import ReadOnlyField
|
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
from rest_framework.viewsets import GenericViewSet
|
|
||||||
|
|
||||||
from authentik.events.api.event import EventSerializer
|
|
||||||
from authentik.events.models import Notification
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationSerializer(ModelSerializer):
|
|
||||||
"""Notification Serializer"""
|
|
||||||
|
|
||||||
body = ReadOnlyField()
|
|
||||||
severity = ReadOnlyField()
|
|
||||||
event = EventSerializer()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = Notification
|
|
||||||
fields = [
|
|
||||||
"pk",
|
|
||||||
"severity",
|
|
||||||
"body",
|
|
||||||
"created",
|
|
||||||
"event",
|
|
||||||
"seen",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationViewSet(
|
|
||||||
mixins.RetrieveModelMixin,
|
|
||||||
mixins.UpdateModelMixin,
|
|
||||||
mixins.DestroyModelMixin,
|
|
||||||
mixins.ListModelMixin,
|
|
||||||
GenericViewSet,
|
|
||||||
):
|
|
||||||
"""Notification Viewset"""
|
|
||||||
|
|
||||||
queryset = Notification.objects.all()
|
|
||||||
serializer_class = NotificationSerializer
|
|
||||||
filterset_fields = [
|
|
||||||
"severity",
|
|
||||||
"body",
|
|
||||||
"created",
|
|
||||||
"event",
|
|
||||||
"seen",
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
if not self.request:
|
|
||||||
return super().get_queryset()
|
|
||||||
return Notification.objects.filter(user=self.request.user)
|
|
@ -1,28 +0,0 @@
|
|||||||
"""NotificationRule API Views"""
|
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
|
||||||
|
|
||||||
from authentik.events.models import NotificationRule
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationRuleSerializer(ModelSerializer):
|
|
||||||
"""NotificationRule Serializer"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = NotificationRule
|
|
||||||
depth = 2
|
|
||||||
fields = [
|
|
||||||
"pk",
|
|
||||||
"name",
|
|
||||||
"transports",
|
|
||||||
"severity",
|
|
||||||
"group",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationRuleViewSet(ModelViewSet):
|
|
||||||
"""NotificationRule Viewset"""
|
|
||||||
|
|
||||||
queryset = NotificationRule.objects.all()
|
|
||||||
serializer_class = NotificationRuleSerializer
|
|
@ -1,66 +0,0 @@
|
|||||||
"""NotificationTransport API Views"""
|
|
||||||
from django.http.response import Http404
|
|
||||||
from guardian.shortcuts import get_objects_for_user
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.fields import SerializerMethodField
|
|
||||||
from rest_framework.request import Request
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
|
||||||
|
|
||||||
from authentik.events.models import (
|
|
||||||
Notification,
|
|
||||||
NotificationSeverity,
|
|
||||||
NotificationTransport,
|
|
||||||
NotificationTransportError,
|
|
||||||
TransportMode,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportSerializer(ModelSerializer):
|
|
||||||
"""NotificationTransport Serializer"""
|
|
||||||
|
|
||||||
mode_verbose = SerializerMethodField()
|
|
||||||
|
|
||||||
def get_mode_verbose(self, instance: NotificationTransport):
|
|
||||||
"""Return selected mode with a UI Label"""
|
|
||||||
return TransportMode(instance.mode).label
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = NotificationTransport
|
|
||||||
fields = [
|
|
||||||
"pk",
|
|
||||||
"name",
|
|
||||||
"mode",
|
|
||||||
"mode_verbose",
|
|
||||||
"webhook_url",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportViewSet(ModelViewSet):
|
|
||||||
"""NotificationTransport Viewset"""
|
|
||||||
|
|
||||||
queryset = NotificationTransport.objects.all()
|
|
||||||
serializer_class = NotificationTransportSerializer
|
|
||||||
|
|
||||||
@action(detail=True, methods=["post"])
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
def test(self, request: Request, pk=None) -> Response:
|
|
||||||
"""Send example notification using selected transport. Requires
|
|
||||||
Modify permissions."""
|
|
||||||
transports = get_objects_for_user(
|
|
||||||
request.user, "authentik_events.change_notificationtransport"
|
|
||||||
).filter(pk=pk)
|
|
||||||
if not transports.exists():
|
|
||||||
raise Http404
|
|
||||||
transport: NotificationTransport = transports.first()
|
|
||||||
notification = Notification(
|
|
||||||
severity=NotificationSeverity.NOTICE,
|
|
||||||
body=f"Test Notification from transport {transport.name}",
|
|
||||||
user=request.user,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
return Response(transport.send(notification))
|
|
||||||
except NotificationTransportError as exc:
|
|
||||||
return Response(str(exc.__cause__ or None), status=503)
|
|
@ -1,47 +0,0 @@
|
|||||||
"""authentik events NotificationTransport forms"""
|
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from authentik.events.models import NotificationRule, NotificationTransport
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportForm(forms.ModelForm):
|
|
||||||
"""NotificationTransport Form"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = NotificationTransport
|
|
||||||
fields = [
|
|
||||||
"name",
|
|
||||||
"mode",
|
|
||||||
"webhook_url",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput(),
|
|
||||||
"webhook_url": forms.TextInput(),
|
|
||||||
}
|
|
||||||
labels = {
|
|
||||||
"webhook_url": _("Webhook URL"),
|
|
||||||
}
|
|
||||||
help_texts = {
|
|
||||||
"webhook_url": _(
|
|
||||||
("Only required when the Generic or Slack Webhook is used.")
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationRuleForm(forms.ModelForm):
|
|
||||||
"""NotificationRule Form"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = NotificationRule
|
|
||||||
fields = [
|
|
||||||
"name",
|
|
||||||
"group",
|
|
||||||
"transports",
|
|
||||||
"severity",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput(),
|
|
||||||
}
|
|
@ -6,10 +6,9 @@ from django.contrib.auth.models import User
|
|||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import post_save, pre_delete
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from guardian.models import UserObjectPermission
|
|
||||||
|
|
||||||
from authentik.core.middleware import LOCAL
|
from authentik.core.middleware import LOCAL
|
||||||
from authentik.events.models import Event, EventAction, Notification
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.events.signals import EventNewThread
|
from authentik.events.signals import EventNewThread
|
||||||
from authentik.events.utils import model_to_dict
|
from authentik.events.utils import model_to_dict
|
||||||
|
|
||||||
@ -64,7 +63,7 @@ class AuditMiddleware:
|
|||||||
user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
|
user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
|
||||||
):
|
):
|
||||||
"""Signal handler for all object's post_save"""
|
"""Signal handler for all object's post_save"""
|
||||||
if isinstance(instance, (Event, Notification, UserObjectPermission)):
|
if isinstance(instance, Event):
|
||||||
return
|
return
|
||||||
|
|
||||||
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
||||||
@ -76,7 +75,7 @@ class AuditMiddleware:
|
|||||||
user: User, request: HttpRequest, sender, instance: Model, **_
|
user: User, request: HttpRequest, sender, instance: Model, **_
|
||||||
):
|
):
|
||||||
"""Signal handler for all object's pre_delete"""
|
"""Signal handler for all object's pre_delete"""
|
||||||
if isinstance(instance, (Event, Notification, UserObjectPermission)):
|
if isinstance(instance, Event):
|
||||||
return
|
return
|
||||||
|
|
||||||
EventNewThread(
|
EventNewThread(
|
||||||
|
@ -1,148 +0,0 @@
|
|||||||
# Generated by Django 3.1.4 on 2021-01-11 16:36
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
("authentik_policies", "0004_policy_execution_logging"),
|
|
||||||
("authentik_core", "0016_auto_20201202_2234"),
|
|
||||||
("authentik_events", "0009_auto_20201227_1210"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="NotificationTransport",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"uuid",
|
|
||||||
models.UUIDField(
|
|
||||||
default=uuid.uuid4,
|
|
||||||
editable=False,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("name", models.TextField(unique=True)),
|
|
||||||
(
|
|
||||||
"mode",
|
|
||||||
models.TextField(
|
|
||||||
choices=[
|
|
||||||
("webhook", "Generic Webhook"),
|
|
||||||
("webhook_slack", "Slack Webhook (Slack/Discord)"),
|
|
||||||
("email", "Email"),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("webhook_url", models.TextField(blank=True)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Notification Transport",
|
|
||||||
"verbose_name_plural": "Notification Transports",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="NotificationRule",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"policybindingmodel_ptr",
|
|
||||||
models.OneToOneField(
|
|
||||||
auto_created=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
parent_link=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
to="authentik_policies.policybindingmodel",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("name", models.TextField(unique=True)),
|
|
||||||
(
|
|
||||||
"severity",
|
|
||||||
models.TextField(
|
|
||||||
choices=[
|
|
||||||
("notice", "Notice"),
|
|
||||||
("warning", "Warning"),
|
|
||||||
("alert", "Alert"),
|
|
||||||
],
|
|
||||||
default="notice",
|
|
||||||
help_text="Controls which severity level the created notifications will have.",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"group",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
help_text="Define which group of users this notification should be sent and shown to. If left empty, Notification won't ben sent.",
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
to="authentik_core.group",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"transports",
|
|
||||||
models.ManyToManyField(
|
|
||||||
help_text="Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.",
|
|
||||||
to="authentik_events.NotificationTransport",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Notification Rule",
|
|
||||||
"verbose_name_plural": "Notification Rules",
|
|
||||||
},
|
|
||||||
bases=("authentik_policies.policybindingmodel",),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Notification",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"uuid",
|
|
||||||
models.UUIDField(
|
|
||||||
default=uuid.uuid4,
|
|
||||||
editable=False,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"severity",
|
|
||||||
models.TextField(
|
|
||||||
choices=[
|
|
||||||
("notice", "Notice"),
|
|
||||||
("warning", "Warning"),
|
|
||||||
("alert", "Alert"),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("body", models.TextField()),
|
|
||||||
("created", models.DateTimeField(auto_now_add=True)),
|
|
||||||
("seen", models.BooleanField(default=False)),
|
|
||||||
(
|
|
||||||
"event",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
to="authentik_events.event",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"user",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Notification",
|
|
||||||
"verbose_name_plural": "Notifications",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,165 +0,0 @@
|
|||||||
# Generated by Django 3.1.4 on 2021-01-10 18:57
|
|
||||||
|
|
||||||
from django.apps.registry import Apps
|
|
||||||
from django.db import migrations
|
|
||||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|
||||||
|
|
||||||
from authentik.events.models import EventAction, NotificationSeverity, TransportMode
|
|
||||||
|
|
||||||
|
|
||||||
def notify_configuration_error(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
Group = apps.get_model("authentik_core", "Group")
|
|
||||||
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
|
|
||||||
EventMatcherPolicy = apps.get_model(
|
|
||||||
"authentik_policies_event_matcher", "EventMatcherPolicy"
|
|
||||||
)
|
|
||||||
NotificationRule = apps.get_model("authentik_events", "NotificationRule")
|
|
||||||
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
|
|
||||||
|
|
||||||
admin_group = (
|
|
||||||
Group.objects.using(db_alias)
|
|
||||||
.filter(name="authentik Admins", is_superuser=True)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
|
|
||||||
name="default-match-configuration-error",
|
|
||||||
defaults={"action": EventAction.CONFIGURATION_ERROR},
|
|
||||||
)
|
|
||||||
trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
|
|
||||||
name="default-notify-configuration-error",
|
|
||||||
defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
|
|
||||||
)
|
|
||||||
trigger.transports.set(
|
|
||||||
NotificationTransport.objects.using(db_alias).filter(
|
|
||||||
name="default-email-transport"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
trigger.save()
|
|
||||||
PolicyBinding.objects.using(db_alias).update_or_create(
|
|
||||||
target=trigger,
|
|
||||||
policy=policy,
|
|
||||||
defaults={
|
|
||||||
"order": 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def notify_update(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
Group = apps.get_model("authentik_core", "Group")
|
|
||||||
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
|
|
||||||
EventMatcherPolicy = apps.get_model(
|
|
||||||
"authentik_policies_event_matcher", "EventMatcherPolicy"
|
|
||||||
)
|
|
||||||
NotificationRule = apps.get_model("authentik_events", "NotificationRule")
|
|
||||||
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
|
|
||||||
|
|
||||||
admin_group = (
|
|
||||||
Group.objects.using(db_alias)
|
|
||||||
.filter(name="authentik Admins", is_superuser=True)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
|
|
||||||
name="default-match-update",
|
|
||||||
defaults={"action": EventAction.UPDATE_AVAILABLE},
|
|
||||||
)
|
|
||||||
trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
|
|
||||||
name="default-notify-update",
|
|
||||||
defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
|
|
||||||
)
|
|
||||||
trigger.transports.set(
|
|
||||||
NotificationTransport.objects.using(db_alias).filter(
|
|
||||||
name="default-email-transport"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
trigger.save()
|
|
||||||
PolicyBinding.objects.using(db_alias).update_or_create(
|
|
||||||
target=trigger,
|
|
||||||
policy=policy,
|
|
||||||
defaults={
|
|
||||||
"order": 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def notify_exception(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
Group = apps.get_model("authentik_core", "Group")
|
|
||||||
PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
|
|
||||||
EventMatcherPolicy = apps.get_model(
|
|
||||||
"authentik_policies_event_matcher", "EventMatcherPolicy"
|
|
||||||
)
|
|
||||||
NotificationRule = apps.get_model("authentik_events", "NotificationRule")
|
|
||||||
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
|
|
||||||
|
|
||||||
admin_group = (
|
|
||||||
Group.objects.using(db_alias)
|
|
||||||
.filter(name="authentik Admins", is_superuser=True)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
policy_policy_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
|
|
||||||
name="default-match-policy-exception",
|
|
||||||
defaults={"action": EventAction.POLICY_EXCEPTION},
|
|
||||||
)
|
|
||||||
policy_pm_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
|
|
||||||
name="default-match-property-mapping-exception",
|
|
||||||
defaults={"action": EventAction.PROPERTY_MAPPING_EXCEPTION},
|
|
||||||
)
|
|
||||||
trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
|
|
||||||
name="default-notify-exception",
|
|
||||||
defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
|
|
||||||
)
|
|
||||||
trigger.transports.set(
|
|
||||||
NotificationTransport.objects.using(db_alias).filter(
|
|
||||||
name="default-email-transport"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
trigger.save()
|
|
||||||
PolicyBinding.objects.using(db_alias).update_or_create(
|
|
||||||
target=trigger,
|
|
||||||
policy=policy_policy_exc,
|
|
||||||
defaults={
|
|
||||||
"order": 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
PolicyBinding.objects.using(db_alias).update_or_create(
|
|
||||||
target=trigger,
|
|
||||||
policy=policy_pm_exc,
|
|
||||||
defaults={
|
|
||||||
"order": 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def transport_email_global(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
|
|
||||||
|
|
||||||
NotificationTransport.objects.using(db_alias).update_or_create(
|
|
||||||
name="default-email-transport",
|
|
||||||
defaults={"mode": TransportMode.EMAIL},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
(
|
|
||||||
"authentik_events",
|
|
||||||
"0010_notification_notificationtransport_notificationrule",
|
|
||||||
),
|
|
||||||
("authentik_core", "0016_auto_20201202_2234"),
|
|
||||||
("authentik_policies_event_matcher", "0003_auto_20210110_1907"),
|
|
||||||
("authentik_policies", "0004_policy_execution_logging"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(transport_email_global),
|
|
||||||
migrations.RunPython(notify_configuration_error),
|
|
||||||
migrations.RunPython(notify_update),
|
|
||||||
migrations.RunPython(notify_exception),
|
|
||||||
]
|
|
@ -1,6 +1,6 @@
|
|||||||
"""authentik events models"""
|
"""authentik events models"""
|
||||||
|
|
||||||
from inspect import getmodule, stack
|
from inspect import getmodule, stack
|
||||||
from smtplib import SMTPException
|
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
@ -9,29 +9,19 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from requests import RequestException, post
|
from structlog import get_logger
|
||||||
from structlog.stdlib import get_logger
|
|
||||||
|
|
||||||
from authentik import __version__
|
|
||||||
from authentik.core.middleware import (
|
from authentik.core.middleware import (
|
||||||
SESSION_IMPERSONATE_ORIGINAL_USER,
|
SESSION_IMPERSONATE_ORIGINAL_USER,
|
||||||
SESSION_IMPERSONATE_USER,
|
SESSION_IMPERSONATE_USER,
|
||||||
)
|
)
|
||||||
from authentik.core.models import Group, User
|
from authentik.core.models import User
|
||||||
from authentik.events.utils import cleanse_dict, get_user, sanitize_dict
|
from authentik.events.utils import cleanse_dict, get_user, sanitize_dict
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
from authentik.policies.models import PolicyBindingModel
|
|
||||||
from authentik.stages.email.tasks import send_mail
|
|
||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
|
||||||
|
|
||||||
LOGGER = get_logger("authentik.events")
|
LOGGER = get_logger("authentik.events")
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportError(SentryIgnoredException):
|
|
||||||
"""Error raised when a notification fails to be delivered"""
|
|
||||||
|
|
||||||
|
|
||||||
class EventAction(models.TextChoices):
|
class EventAction(models.TextChoices):
|
||||||
"""All possible actions to save into the events log"""
|
"""All possible actions to save into the events log"""
|
||||||
|
|
||||||
@ -114,12 +104,10 @@ class Event(models.Model):
|
|||||||
Events independently from requests.
|
Events independently from requests.
|
||||||
`user` arguments optionally overrides user from requests."""
|
`user` arguments optionally overrides user from requests."""
|
||||||
if hasattr(request, "user"):
|
if hasattr(request, "user"):
|
||||||
original_user = None
|
self.user = get_user(
|
||||||
if hasattr(request, "session"):
|
request.user,
|
||||||
original_user = request.session.get(
|
request.session.get(SESSION_IMPERSONATE_ORIGINAL_USER, None),
|
||||||
SESSION_IMPERSONATE_ORIGINAL_USER, None
|
)
|
||||||
)
|
|
||||||
self.user = get_user(request.user, original_user)
|
|
||||||
if user:
|
if user:
|
||||||
self.user = get_user(user)
|
self.user = get_user(user)
|
||||||
# Check if we're currently impersonating, and add that user
|
# Check if we're currently impersonating, and add that user
|
||||||
@ -139,7 +127,9 @@ class Event(models.Model):
|
|||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self._state.adding:
|
if not self._state.adding:
|
||||||
raise ValidationError("you may not edit an existing Event")
|
raise ValidationError(
|
||||||
|
"you may not edit an existing %s" % self._meta.model_name
|
||||||
|
)
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Created Event",
|
"Created Event",
|
||||||
action=self.action,
|
action=self.action,
|
||||||
@ -149,217 +139,7 @@ class Event(models.Model):
|
|||||||
)
|
)
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
|
||||||
def summary(self) -> str:
|
|
||||||
"""Return a summary of this event."""
|
|
||||||
if "message" in self.context:
|
|
||||||
return self.context["message"]
|
|
||||||
return f"{self.action}: {self.context}"
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"<Event action={self.action} user={self.user} context={self.context}>"
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _("Event")
|
verbose_name = _("Event")
|
||||||
verbose_name_plural = _("Events")
|
verbose_name_plural = _("Events")
|
||||||
|
|
||||||
|
|
||||||
class TransportMode(models.TextChoices):
|
|
||||||
"""Modes that a notification transport can send a notification"""
|
|
||||||
|
|
||||||
WEBHOOK = "webhook", _("Generic Webhook")
|
|
||||||
WEBHOOK_SLACK = "webhook_slack", _("Slack Webhook (Slack/Discord)")
|
|
||||||
EMAIL = "email", _("Email")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransport(models.Model):
|
|
||||||
"""Action which is executed when a Rule matches"""
|
|
||||||
|
|
||||||
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
|
||||||
|
|
||||||
name = models.TextField(unique=True)
|
|
||||||
mode = models.TextField(choices=TransportMode.choices)
|
|
||||||
|
|
||||||
webhook_url = models.TextField(blank=True)
|
|
||||||
|
|
||||||
def send(self, notification: "Notification") -> list[str]:
|
|
||||||
"""Send notification to user, called from async task"""
|
|
||||||
if self.mode == TransportMode.WEBHOOK:
|
|
||||||
return self.send_webhook(notification)
|
|
||||||
if self.mode == TransportMode.WEBHOOK_SLACK:
|
|
||||||
return self.send_webhook_slack(notification)
|
|
||||||
if self.mode == TransportMode.EMAIL:
|
|
||||||
return self.send_email(notification)
|
|
||||||
raise ValueError(f"Invalid mode {self.mode} set")
|
|
||||||
|
|
||||||
def send_webhook(self, notification: "Notification") -> list[str]:
|
|
||||||
"""Send notification to generic webhook"""
|
|
||||||
try:
|
|
||||||
response = post(
|
|
||||||
self.webhook_url,
|
|
||||||
json={
|
|
||||||
"body": notification.body,
|
|
||||||
"severity": notification.severity,
|
|
||||||
"user_email": notification.user.email,
|
|
||||||
"user_username": notification.user.username,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
except RequestException as exc:
|
|
||||||
raise NotificationTransportError(exc.response.text) from exc
|
|
||||||
return [
|
|
||||||
response.status_code,
|
|
||||||
response.text,
|
|
||||||
]
|
|
||||||
|
|
||||||
def send_webhook_slack(self, notification: "Notification") -> list[str]:
|
|
||||||
"""Send notification to slack or slack-compatible endpoints"""
|
|
||||||
fields = [
|
|
||||||
{
|
|
||||||
"title": _("Severity"),
|
|
||||||
"value": notification.severity,
|
|
||||||
"short": True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": _("Dispatched for user"),
|
|
||||||
"value": str(notification.user),
|
|
||||||
"short": True,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
if notification.event:
|
|
||||||
for key, value in notification.event.context.items():
|
|
||||||
if not isinstance(value, str):
|
|
||||||
continue
|
|
||||||
# https://birdie0.github.io/discord-webhooks-guide/other/field_limits.html
|
|
||||||
if len(fields) >= 25:
|
|
||||||
continue
|
|
||||||
fields.append({"title": key[:256], "value": value[:1024]})
|
|
||||||
body = {
|
|
||||||
"username": "authentik",
|
|
||||||
"icon_url": "https://goauthentik.io/img/icon.png",
|
|
||||||
"attachments": [
|
|
||||||
{
|
|
||||||
"author_name": "authentik",
|
|
||||||
"author_link": "https://goauthentik.io",
|
|
||||||
"author_icon": "https://goauthentik.io/img/icon.png",
|
|
||||||
"title": notification.body,
|
|
||||||
"color": "#fd4b2d",
|
|
||||||
"fields": fields,
|
|
||||||
"footer": f"authentik v{__version__}",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
if notification.event:
|
|
||||||
body["attachments"][0]["title"] = notification.event.action
|
|
||||||
body["attachments"][0]["text"] = notification.event.action
|
|
||||||
try:
|
|
||||||
response = post(self.webhook_url, json=body)
|
|
||||||
response.raise_for_status()
|
|
||||||
except RequestException as exc:
|
|
||||||
raise NotificationTransportError(exc.response.text) from exc
|
|
||||||
return [
|
|
||||||
response.status_code,
|
|
||||||
response.text,
|
|
||||||
]
|
|
||||||
|
|
||||||
def send_email(self, notification: "Notification") -> list[str]:
|
|
||||||
"""Send notification via global email configuration"""
|
|
||||||
body_trunc = (
|
|
||||||
(notification.body[:75] + "..")
|
|
||||||
if len(notification.body) > 75
|
|
||||||
else notification.body
|
|
||||||
)
|
|
||||||
mail = TemplateEmailMessage(
|
|
||||||
subject=f"authentik Notification: {body_trunc}",
|
|
||||||
template_name="email/setup.html",
|
|
||||||
to=[notification.user.email],
|
|
||||||
template_context={
|
|
||||||
"body": notification.body,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
# Email is sent directly here, as the call to send() should have been from a task.
|
|
||||||
try:
|
|
||||||
# pyright: reportGeneralTypeIssues=false
|
|
||||||
return send_mail(mail.__dict__) # pylint: disable=no-value-for-parameter
|
|
||||||
except (SMTPException, ConnectionError) as exc:
|
|
||||||
raise NotificationTransportError from exc
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"Notification Transport {self.name}"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
verbose_name = _("Notification Transport")
|
|
||||||
verbose_name_plural = _("Notification Transports")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationSeverity(models.TextChoices):
|
|
||||||
"""Severity images that a notification can have"""
|
|
||||||
|
|
||||||
NOTICE = "notice", _("Notice")
|
|
||||||
WARNING = "warning", _("Warning")
|
|
||||||
ALERT = "alert", _("Alert")
|
|
||||||
|
|
||||||
|
|
||||||
class Notification(models.Model):
|
|
||||||
"""Event Notification"""
|
|
||||||
|
|
||||||
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
|
||||||
severity = models.TextField(choices=NotificationSeverity.choices)
|
|
||||||
body = models.TextField()
|
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
|
||||||
event = models.ForeignKey(Event, on_delete=models.SET_NULL, null=True, blank=True)
|
|
||||||
seen = models.BooleanField(default=False)
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
body_trunc = (self.body[:75] + "..") if len(self.body) > 75 else self.body
|
|
||||||
return f"Notification for user {self.user}: {body_trunc}"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
verbose_name = _("Notification")
|
|
||||||
verbose_name_plural = _("Notifications")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationRule(PolicyBindingModel):
|
|
||||||
"""Decide when to create a Notification based on policies attached to this object."""
|
|
||||||
|
|
||||||
name = models.TextField(unique=True)
|
|
||||||
transports = models.ManyToManyField(
|
|
||||||
NotificationTransport,
|
|
||||||
help_text=_(
|
|
||||||
(
|
|
||||||
"Select which transports should be used to notify the user. If none are "
|
|
||||||
"selected, the notification will only be shown in the authentik UI."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
severity = models.TextField(
|
|
||||||
choices=NotificationSeverity.choices,
|
|
||||||
default=NotificationSeverity.NOTICE,
|
|
||||||
help_text=_(
|
|
||||||
"Controls which severity level the created notifications will have."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
group = models.ForeignKey(
|
|
||||||
Group,
|
|
||||||
help_text=_(
|
|
||||||
(
|
|
||||||
"Define which group of users this notification should be sent and shown to. "
|
|
||||||
"If left empty, Notification won't ben sent."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"Notification Rule {self.name}"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
verbose_name = _("Notification Rule")
|
|
||||||
verbose_name_plural = _("Notification Rules")
|
|
||||||
|
@ -7,16 +7,12 @@ from django.contrib.auth.signals import (
|
|||||||
user_logged_out,
|
user_logged_out,
|
||||||
user_login_failed,
|
user_login_failed,
|
||||||
)
|
)
|
||||||
from django.db.models.signals import post_save
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.core.signals import password_changed
|
from authentik.core.signals import password_changed
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.events.tasks import event_notification_handler
|
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
|
|
||||||
from authentik.flows.views import SESSION_KEY_PLAN
|
|
||||||
from authentik.stages.invitation.models import Invitation
|
from authentik.stages.invitation.models import Invitation
|
||||||
from authentik.stages.invitation.signals import invitation_used
|
from authentik.stages.invitation.signals import invitation_used
|
||||||
from authentik.stages.user_write.signals import user_write
|
from authentik.stages.user_write.signals import user_write
|
||||||
@ -48,11 +44,6 @@ class EventNewThread(Thread):
|
|||||||
def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
|
def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
|
||||||
"""Log successful login"""
|
"""Log successful login"""
|
||||||
thread = EventNewThread(EventAction.LOGIN, request)
|
thread = EventNewThread(EventAction.LOGIN, request)
|
||||||
if SESSION_KEY_PLAN in request.session:
|
|
||||||
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
|
|
||||||
if PLAN_CONTEXT_SOURCE in flow_plan.context:
|
|
||||||
# Login request came from an external source, save it in the context
|
|
||||||
thread.kwargs["using_source"] = flow_plan.context[PLAN_CONTEXT_SOURCE]
|
|
||||||
thread.user = user
|
thread.user = user
|
||||||
thread.run()
|
thread.run()
|
||||||
|
|
||||||
@ -104,10 +95,3 @@ def on_password_changed(sender, user: User, password: str, **_):
|
|||||||
"""Log password change"""
|
"""Log password change"""
|
||||||
thread = EventNewThread(EventAction.PASSWORD_SET, None, user=user)
|
thread = EventNewThread(EventAction.PASSWORD_SET, None, user=user)
|
||||||
thread.run()
|
thread.run()
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Event)
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def event_post_save_notification(sender, instance: Event, **_):
|
|
||||||
"""Start task to check if any policies trigger an notification on this event"""
|
|
||||||
event_notification_handler.delay(instance.event_uuid.hex)
|
|
||||||
|
@ -1,99 +0,0 @@
|
|||||||
"""Event notification tasks"""
|
|
||||||
from guardian.shortcuts import get_anonymous_user
|
|
||||||
from structlog import get_logger
|
|
||||||
|
|
||||||
from authentik.events.models import (
|
|
||||||
Event,
|
|
||||||
Notification,
|
|
||||||
NotificationRule,
|
|
||||||
NotificationTransport,
|
|
||||||
NotificationTransportError,
|
|
||||||
)
|
|
||||||
from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
|
|
||||||
from authentik.policies.engine import PolicyEngine, PolicyEngineMode
|
|
||||||
from authentik.policies.models import PolicyBinding
|
|
||||||
from authentik.root.celery import CELERY_APP
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task()
|
|
||||||
def event_notification_handler(event_uuid: str):
|
|
||||||
"""Start task for each trigger definition"""
|
|
||||||
for trigger in NotificationRule.objects.all():
|
|
||||||
event_trigger_handler.apply_async(
|
|
||||||
args=[event_uuid, trigger.name], queue="authentik_events"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task()
|
|
||||||
def event_trigger_handler(event_uuid: str, trigger_name: str):
|
|
||||||
"""Check if policies attached to NotificationRule match event"""
|
|
||||||
event: Event = Event.objects.get(event_uuid=event_uuid)
|
|
||||||
trigger: NotificationRule = NotificationRule.objects.get(name=trigger_name)
|
|
||||||
|
|
||||||
if "policy_uuid" in event.context:
|
|
||||||
policy_uuid = event.context["policy_uuid"]
|
|
||||||
if PolicyBinding.objects.filter(
|
|
||||||
target__in=NotificationRule.objects.all().values_list(
|
|
||||||
"pbm_uuid", flat=True
|
|
||||||
),
|
|
||||||
policy=policy_uuid,
|
|
||||||
).exists():
|
|
||||||
# If policy that caused this event to be created is attached
|
|
||||||
# to *any* NotificationRule, we return early.
|
|
||||||
# This is the most effective way to prevent infinite loops.
|
|
||||||
LOGGER.debug(
|
|
||||||
"e(trigger): attempting to prevent infinite loop", trigger=trigger
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not trigger.group:
|
|
||||||
LOGGER.debug("e(trigger): trigger has no group", trigger=trigger)
|
|
||||||
return
|
|
||||||
|
|
||||||
LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger)
|
|
||||||
policy_engine = PolicyEngine(trigger, get_anonymous_user())
|
|
||||||
policy_engine.mode = PolicyEngineMode.MODE_OR
|
|
||||||
policy_engine.empty_result = False
|
|
||||||
policy_engine.use_cache = False
|
|
||||||
policy_engine.request.context["event"] = event
|
|
||||||
policy_engine.build()
|
|
||||||
result = policy_engine.result
|
|
||||||
if not result.passing:
|
|
||||||
return
|
|
||||||
|
|
||||||
LOGGER.debug("e(trigger): event trigger matched", trigger=trigger)
|
|
||||||
# Create the notification objects
|
|
||||||
for user in trigger.group.users.all():
|
|
||||||
notification = Notification.objects.create(
|
|
||||||
severity=trigger.severity, body=event.summary, event=event, user=user
|
|
||||||
)
|
|
||||||
|
|
||||||
for transport in trigger.transports.all():
|
|
||||||
notification_transport.apply_async(
|
|
||||||
args=[notification.pk, transport.pk], queue="authentik_events"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task(
|
|
||||||
bind=True,
|
|
||||||
autoretry_for=(NotificationTransportError,),
|
|
||||||
retry_backoff=True,
|
|
||||||
base=MonitoredTask,
|
|
||||||
)
|
|
||||||
def notification_transport(
|
|
||||||
self: MonitoredTask, notification_pk: int, transport_pk: int
|
|
||||||
):
|
|
||||||
"""Send notification over specified transport"""
|
|
||||||
self.save_on_success = False
|
|
||||||
try:
|
|
||||||
notification: Notification = Notification.objects.get(pk=notification_pk)
|
|
||||||
transport: NotificationTransport = NotificationTransport.objects.get(
|
|
||||||
pk=transport_pk
|
|
||||||
)
|
|
||||||
transport.send(notification)
|
|
||||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
|
|
||||||
except NotificationTransportError as exc:
|
|
||||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
|
||||||
raise exc
|
|
@ -1,24 +0,0 @@
|
|||||||
"""Event API tests"""
|
|
||||||
|
|
||||||
from django.shortcuts import reverse
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from authentik.core.models import User
|
|
||||||
from authentik.events.models import Event, EventAction
|
|
||||||
|
|
||||||
|
|
||||||
class TestEventsAPI(APITestCase):
|
|
||||||
"""Test Event API"""
|
|
||||||
|
|
||||||
def test_top_n(self):
|
|
||||||
"""Test top_per_user"""
|
|
||||||
user = User.objects.get(username="akadmin")
|
|
||||||
self.client.force_login(user)
|
|
||||||
|
|
||||||
event = Event.new(EventAction.AUTHORIZE_APPLICATION)
|
|
||||||
event.save() # We save to ensure nothing is un-saveable
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("authentik_api:event-top-per-user"),
|
|
||||||
data={"filter_action": EventAction.AUTHORIZE_APPLICATION},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
@ -1,10 +1,9 @@
|
|||||||
"""event tests"""
|
"""events event tests"""
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
|
|
||||||
from authentik.core.models import Group
|
|
||||||
from authentik.events.models import Event
|
from authentik.events.models import Event
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
|
|
||||||
@ -14,24 +13,14 @@ class TestEvents(TestCase):
|
|||||||
|
|
||||||
def test_new_with_model(self):
|
def test_new_with_model(self):
|
||||||
"""Create a new Event passing a model as kwarg"""
|
"""Create a new Event passing a model as kwarg"""
|
||||||
test_model = Group.objects.create(name="test")
|
event = Event.new("unittest", test={"model": get_anonymous_user()})
|
||||||
event = Event.new("unittest", test={"model": test_model})
|
|
||||||
event.save() # We save to ensure nothing is un-saveable
|
event.save() # We save to ensure nothing is un-saveable
|
||||||
model_content_type = ContentType.objects.get_for_model(test_model)
|
model_content_type = ContentType.objects.get_for_model(get_anonymous_user())
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
event.context.get("test").get("model").get("app"),
|
event.context.get("test").get("model").get("app"),
|
||||||
model_content_type.app_label,
|
model_content_type.app_label,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_new_with_user(self):
|
|
||||||
"""Create a new Event passing a user as kwarg"""
|
|
||||||
event = Event.new("unittest", test={"model": get_anonymous_user()})
|
|
||||||
event.save() # We save to ensure nothing is un-saveable
|
|
||||||
self.assertEqual(
|
|
||||||
event.context.get("test").get("model").get("username"),
|
|
||||||
get_anonymous_user().username,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_new_with_uuid_model(self):
|
def test_new_with_uuid_model(self):
|
||||||
"""Create a new Event passing a model (with UUID PK) as kwarg"""
|
"""Create a new Event passing a model (with UUID PK) as kwarg"""
|
||||||
temp_model = DummyPolicy.objects.create(name="test", result=True)
|
temp_model = DummyPolicy.objects.create(name="test", result=True)
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
"""Event Middleware tests"""
|
|
||||||
|
|
||||||
from django.shortcuts import reverse
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from authentik.core.models import Application, User
|
|
||||||
from authentik.events.models import Event, EventAction
|
|
||||||
|
|
||||||
|
|
||||||
class TestEventsMiddleware(APITestCase):
|
|
||||||
"""Test Event Middleware"""
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
super().setUp()
|
|
||||||
self.user = User.objects.get(username="akadmin")
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
|
|
||||||
def test_create(self):
|
|
||||||
"""Test model creation event"""
|
|
||||||
self.client.post(
|
|
||||||
reverse("authentik_api:application-list"),
|
|
||||||
data={"name": "test-create", "slug": "test-create"},
|
|
||||||
)
|
|
||||||
self.assertTrue(Application.objects.filter(name="test-create").exists())
|
|
||||||
self.assertTrue(
|
|
||||||
Event.objects.filter(
|
|
||||||
action=EventAction.MODEL_CREATED,
|
|
||||||
context__model__model_name="application",
|
|
||||||
context__model__app="authentik_core",
|
|
||||||
context__model__name="test-create",
|
|
||||||
).exists()
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_delete(self):
|
|
||||||
"""Test model creation event"""
|
|
||||||
Application.objects.create(name="test-delete", slug="test-delete")
|
|
||||||
self.client.delete(
|
|
||||||
reverse("authentik_api:application-detail", kwargs={"slug": "test-delete"})
|
|
||||||
)
|
|
||||||
self.assertFalse(Application.objects.filter(name="test").exists())
|
|
||||||
self.assertTrue(
|
|
||||||
Event.objects.filter(
|
|
||||||
action=EventAction.MODEL_DELETED,
|
|
||||||
context__model__model_name="application",
|
|
||||||
context__model__app="authentik_core",
|
|
||||||
context__model__name="test-delete",
|
|
||||||
).exists()
|
|
||||||
)
|
|
@ -1,90 +0,0 @@
|
|||||||
"""Notification tests"""
|
|
||||||
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
from authentik.core.models import Group, User
|
|
||||||
from authentik.events.models import (
|
|
||||||
Event,
|
|
||||||
EventAction,
|
|
||||||
NotificationRule,
|
|
||||||
NotificationTransport,
|
|
||||||
)
|
|
||||||
from authentik.policies.event_matcher.models import EventMatcherPolicy
|
|
||||||
from authentik.policies.exceptions import PolicyException
|
|
||||||
from authentik.policies.models import PolicyBinding
|
|
||||||
|
|
||||||
|
|
||||||
class TestEventsNotifications(TestCase):
|
|
||||||
"""Test Event Notifications"""
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
self.group = Group.objects.create(name="test-group")
|
|
||||||
self.user = User.objects.create(name="test-user")
|
|
||||||
self.group.users.add(self.user)
|
|
||||||
self.group.save()
|
|
||||||
|
|
||||||
def test_trigger_empty(self):
|
|
||||||
"""Test trigger without any policies attached"""
|
|
||||||
transport = NotificationTransport.objects.create(name="transport")
|
|
||||||
trigger = NotificationRule.objects.create(name="trigger", group=self.group)
|
|
||||||
trigger.transports.add(transport)
|
|
||||||
trigger.save()
|
|
||||||
|
|
||||||
execute_mock = MagicMock()
|
|
||||||
with patch("authentik.events.models.NotificationTransport.send", execute_mock):
|
|
||||||
Event.new(EventAction.CUSTOM_PREFIX).save()
|
|
||||||
self.assertEqual(execute_mock.call_count, 0)
|
|
||||||
|
|
||||||
def test_trigger_single(self):
|
|
||||||
"""Test simple transport triggering"""
|
|
||||||
transport = NotificationTransport.objects.create(name="transport")
|
|
||||||
trigger = NotificationRule.objects.create(name="trigger", group=self.group)
|
|
||||||
trigger.transports.add(transport)
|
|
||||||
trigger.save()
|
|
||||||
matcher = EventMatcherPolicy.objects.create(
|
|
||||||
name="matcher", action=EventAction.CUSTOM_PREFIX
|
|
||||||
)
|
|
||||||
PolicyBinding.objects.create(target=trigger, policy=matcher, order=0)
|
|
||||||
|
|
||||||
execute_mock = MagicMock()
|
|
||||||
with patch("authentik.events.models.NotificationTransport.send", execute_mock):
|
|
||||||
Event.new(EventAction.CUSTOM_PREFIX).save()
|
|
||||||
self.assertEqual(execute_mock.call_count, 1)
|
|
||||||
|
|
||||||
def test_trigger_no_group(self):
|
|
||||||
"""Test trigger without group"""
|
|
||||||
trigger = NotificationRule.objects.create(name="trigger")
|
|
||||||
matcher = EventMatcherPolicy.objects.create(
|
|
||||||
name="matcher", action=EventAction.CUSTOM_PREFIX
|
|
||||||
)
|
|
||||||
PolicyBinding.objects.create(target=trigger, policy=matcher, order=0)
|
|
||||||
|
|
||||||
execute_mock = MagicMock()
|
|
||||||
with patch("authentik.events.models.NotificationTransport.send", execute_mock):
|
|
||||||
Event.new(EventAction.CUSTOM_PREFIX).save()
|
|
||||||
self.assertEqual(execute_mock.call_count, 0)
|
|
||||||
|
|
||||||
def test_policy_error_recursive(self):
|
|
||||||
"""Test Policy error which would cause recursion"""
|
|
||||||
transport = NotificationTransport.objects.create(name="transport")
|
|
||||||
NotificationRule.objects.filter(name__startswith="default").delete()
|
|
||||||
trigger = NotificationRule.objects.create(name="trigger", group=self.group)
|
|
||||||
trigger.transports.add(transport)
|
|
||||||
trigger.save()
|
|
||||||
matcher = EventMatcherPolicy.objects.create(
|
|
||||||
name="matcher", action=EventAction.CUSTOM_PREFIX
|
|
||||||
)
|
|
||||||
PolicyBinding.objects.create(target=trigger, policy=matcher, order=0)
|
|
||||||
|
|
||||||
execute_mock = MagicMock()
|
|
||||||
passes = MagicMock(side_effect=PolicyException)
|
|
||||||
with patch(
|
|
||||||
"authentik.policies.event_matcher.models.EventMatcherPolicy.passes", passes
|
|
||||||
):
|
|
||||||
with patch(
|
|
||||||
"authentik.events.models.NotificationTransport.send", execute_mock
|
|
||||||
):
|
|
||||||
Event.new(EventAction.CUSTOM_PREFIX).save()
|
|
||||||
self.assertEqual(passes.call_count, 0)
|
|
@ -5,10 +5,8 @@ from typing import Any, Dict, Optional
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.core.handlers.wsgi import WSGIRequest
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.base import Model
|
from django.db.models.base import Model
|
||||||
from django.http.request import HttpRequest
|
|
||||||
from django.views.debug import SafeExceptionReporterFilter
|
from django.views.debug import SafeExceptionReporterFilter
|
||||||
from guardian.utils import get_anonymous_user
|
from guardian.utils import get_anonymous_user
|
||||||
|
|
||||||
@ -85,14 +83,10 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
|||||||
value = asdict(value)
|
value = asdict(value)
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
final_dict[key] = sanitize_dict(value)
|
final_dict[key] = sanitize_dict(value)
|
||||||
elif isinstance(value, User):
|
|
||||||
final_dict[key] = sanitize_dict(get_user(value))
|
|
||||||
elif isinstance(value, models.Model):
|
elif isinstance(value, models.Model):
|
||||||
final_dict[key] = sanitize_dict(model_to_dict(value))
|
final_dict[key] = sanitize_dict(model_to_dict(value))
|
||||||
elif isinstance(value, UUID):
|
elif isinstance(value, UUID):
|
||||||
final_dict[key] = value.hex
|
final_dict[key] = value.hex
|
||||||
elif isinstance(value, (HttpRequest, WSGIRequest)):
|
|
||||||
continue
|
|
||||||
else:
|
else:
|
||||||
final_dict[key] = value
|
final_dict[key] = value
|
||||||
return final_dict
|
return final_dict
|
||||||
|
@ -7,7 +7,7 @@ from time import time
|
|||||||
from django import db
|
from django import db
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.test import RequestFactory
|
from django.test import RequestFactory
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik import __version__
|
from authentik import __version__
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
@ -3,7 +3,7 @@ from dataclasses import dataclass
|
|||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.flows.models import Stage
|
from authentik.flows.models import Stage
|
||||||
|
@ -8,7 +8,7 @@ from django.http import HttpRequest
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from model_utils.managers import InheritanceManager
|
from model_utils.managers import InheritanceManager
|
||||||
from rest_framework.serializers import BaseSerializer
|
from rest_framework.serializers import BaseSerializer
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.lib.models import InheritanceForeignKey, SerializerModel
|
from authentik.lib.models import InheritanceForeignKey, SerializerModel
|
||||||
from authentik.policies.models import PolicyBindingModel
|
from authentik.policies.models import PolicyBindingModel
|
||||||
|
@ -6,7 +6,7 @@ from django.core.cache import cache
|
|||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from sentry_sdk.hub import Hub
|
from sentry_sdk.hub import Hub
|
||||||
from sentry_sdk.tracing import Span
|
from sentry_sdk.tracing import Span
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.events.models import cleanse_dict
|
from authentik.events.models import cleanse_dict
|
||||||
@ -21,7 +21,6 @@ PLAN_CONTEXT_PENDING_USER = "pending_user"
|
|||||||
PLAN_CONTEXT_SSO = "is_sso"
|
PLAN_CONTEXT_SSO = "is_sso"
|
||||||
PLAN_CONTEXT_REDIRECT = "redirect"
|
PLAN_CONTEXT_REDIRECT = "redirect"
|
||||||
PLAN_CONTEXT_APPLICATION = "application"
|
PLAN_CONTEXT_APPLICATION = "application"
|
||||||
PLAN_CONTEXT_SOURCE = "source"
|
|
||||||
|
|
||||||
|
|
||||||
def cache_key(flow: Flow, user: Optional[User] = None) -> str:
|
def cache_key(flow: Flow, user: Optional[User] = None) -> str:
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from django.template.response import TemplateResponse
|
|||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||||
from django.views.generic import TemplateView, View
|
from django.views.generic import TemplateView, View
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.models import USER_ATTRIBUTE_DEBUG
|
from authentik.core.models import USER_ATTRIBUTE_DEBUG
|
||||||
from authentik.events.models import cleanse_dict
|
from authentik.events.models import cleanse_dict
|
||||||
|
@ -5,15 +5,13 @@ from contextlib import contextmanager
|
|||||||
from glob import glob
|
from glob import glob
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Any
|
from typing import Any, Dict
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from django.conf import ImproperlyConfigured
|
from django.conf import ImproperlyConfigured
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from authentik import __version__
|
|
||||||
|
|
||||||
SEARCH_PATHS = ["authentik/lib/default.yml", "/etc/authentik/config.yml", ""] + glob(
|
SEARCH_PATHS = ["authentik/lib/default.yml", "/etc/authentik/config.yml", ""] + glob(
|
||||||
"/etc/authentik/config.d/*.yml", recursive=True
|
"/etc/authentik/config.d/*.yml", recursive=True
|
||||||
)
|
)
|
||||||
@ -21,9 +19,10 @@ ENV_PREFIX = "AUTHENTIK"
|
|||||||
ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local")
|
ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local")
|
||||||
|
|
||||||
|
|
||||||
def context_processor(request: HttpRequest) -> dict[str, Any]:
|
def context_processor(request: HttpRequest) -> Dict[str, Any]:
|
||||||
"""Context Processor that injects config object into every template"""
|
"""Context Processor that injects config object into every template"""
|
||||||
return {"config": CONFIG.raw, "ak_version": __version__}
|
kwargs = {"config": CONFIG.raw}
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class ConfigLoader:
|
class ConfigLoader:
|
||||||
|
@ -21,17 +21,6 @@ error_reporting:
|
|||||||
environment: customer
|
environment: customer
|
||||||
send_pii: false
|
send_pii: false
|
||||||
|
|
||||||
# Global email settings
|
|
||||||
email:
|
|
||||||
host: localhost
|
|
||||||
port: 25
|
|
||||||
username: ""
|
|
||||||
password: ""
|
|
||||||
use_tls: false
|
|
||||||
use_ssl: false
|
|
||||||
timeout: 10
|
|
||||||
from: authentik@localhost
|
|
||||||
|
|
||||||
outposts:
|
outposts:
|
||||||
docker_image_base: "beryju/authentik" # this is prepended to -proxy:version
|
docker_image_base: "beryju/authentik" # this is prepended to -proxy:version
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from requests import Session
|
from requests import Session
|
||||||
from sentry_sdk.hub import Hub
|
from sentry_sdk.hub import Hub
|
||||||
from sentry_sdk.tracing import Span
|
from sentry_sdk.tracing import Span
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from ldap3.core.exceptions import LDAPException
|
|||||||
from redis.exceptions import ConnectionError as RedisConnectionError
|
from redis.exceptions import ConnectionError as RedisConnectionError
|
||||||
from redis.exceptions import RedisError, ResponseError
|
from redis.exceptions import RedisError, ResponseError
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
from websockets.exceptions import WebSocketException
|
from websockets.exceptions import WebSocketException
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
@ -52,11 +52,6 @@ class TaskInfo:
|
|||||||
|
|
||||||
task_description: Optional[str] = field(default=None)
|
task_description: Optional[str] = field(default=None)
|
||||||
|
|
||||||
@property
|
|
||||||
def html_name(self) -> list[str]:
|
|
||||||
"""Get task_name, but split on underscores, so we can join in the html template."""
|
|
||||||
return self.task_name.split("_")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def all() -> Dict[str, "TaskInfo"]:
|
def all() -> Dict[str, "TaskInfo"]:
|
||||||
"""Get all TaskInfo objects"""
|
"""Get all TaskInfo objects"""
|
||||||
|
@ -8,7 +8,7 @@ from django.http.request import HttpRequest
|
|||||||
from django.template import Context
|
from django.template import Context
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.utils.html import escape, mark_safe
|
from django.utils.html import escape, mark_safe
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
|
@ -5,7 +5,7 @@ from django.http import HttpResponse
|
|||||||
from django.shortcuts import redirect, reverse
|
from django.shortcuts import redirect, reverse
|
||||||
from django.urls import NoReverseMatch
|
from django.urls import NoReverseMatch
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ from django.db import ProgrammingError
|
|||||||
from docker.constants import DEFAULT_UNIX_SOCKET
|
from docker.constants import DEFAULT_UNIX_SOCKET
|
||||||
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
|
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
|
||||||
from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION
|
from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from channels.exceptions import DenyConnection
|
|||||||
from dacite import from_dict
|
from dacite import from_dict
|
||||||
from dacite.data import Data
|
from dacite.data import Data
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.channels import AuthJsonConsumer
|
from authentik.core.channels import AuthJsonConsumer
|
||||||
from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState
|
from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Base Controller"""
|
"""Base Controller"""
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
from structlog.testing import capture_logs
|
from structlog.testing import capture_logs
|
||||||
|
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
|
@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Generic, TypeVar
|
|||||||
|
|
||||||
from kubernetes.client import V1ObjectMeta
|
from kubernetes.client import V1ObjectMeta
|
||||||
from kubernetes.client.rest import ApiException
|
from kubernetes.client.rest import ApiException
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik import __version__
|
from authentik import __version__
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
@ -93,7 +93,7 @@ class KubernetesObjectReconciler(Generic[T]):
|
|||||||
def reconcile(self, current: T, reference: T):
|
def reconcile(self, current: T, reference: T):
|
||||||
"""Check what operations should be done, should be raised as
|
"""Check what operations should be done, should be raised as
|
||||||
ReconcileTrigger"""
|
ReconcileTrigger"""
|
||||||
if current.metadata.labels != reference.metadata.labels:
|
if current.metadata.annotations != reference.metadata.annotations:
|
||||||
raise NeedsUpdate()
|
raise NeedsUpdate()
|
||||||
|
|
||||||
def create(self, reference: T):
|
def create(self, reference: T):
|
||||||
|
@ -140,8 +140,5 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
|||||||
|
|
||||||
def update(self, current: V1Deployment, reference: V1Deployment):
|
def update(self, current: V1Deployment, reference: V1Deployment):
|
||||||
return self.api.patch_namespaced_deployment(
|
return self.api.patch_namespaced_deployment(
|
||||||
current.metadata.name,
|
current.metadata.name, self.namespace, reference
|
||||||
self.namespace,
|
|
||||||
reference,
|
|
||||||
field_manager=FIELD_MANAGER,
|
|
||||||
)
|
)
|
||||||
|
@ -67,8 +67,5 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
|
|||||||
|
|
||||||
def update(self, current: V1Secret, reference: V1Secret):
|
def update(self, current: V1Secret, reference: V1Secret):
|
||||||
return self.api.patch_namespaced_secret(
|
return self.api.patch_namespaced_secret(
|
||||||
current.metadata.name,
|
current.metadata.name, self.namespace, reference
|
||||||
self.namespace,
|
|
||||||
reference,
|
|
||||||
field_manager=FIELD_MANAGER,
|
|
||||||
)
|
)
|
||||||
|
@ -67,8 +67,5 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
|
|||||||
|
|
||||||
def update(self, current: V1Service, reference: V1Service):
|
def update(self, current: V1Service, reference: V1Service):
|
||||||
return self.api.patch_namespaced_service(
|
return self.api.patch_namespaced_service(
|
||||||
current.metadata.name,
|
current.metadata.name, self.namespace, reference
|
||||||
self.namespace,
|
|
||||||
reference,
|
|
||||||
field_manager=FIELD_MANAGER,
|
|
||||||
)
|
)
|
||||||
|
@ -24,7 +24,7 @@ from kubernetes.config.incluster_config import load_incluster_config
|
|||||||
from kubernetes.config.kube_config import load_kube_config_from_dict
|
from kubernetes.config.kube_config import load_kube_config_from_dict
|
||||||
from model_utils.managers import InheritanceManager
|
from model_utils.managers import InheritanceManager
|
||||||
from packaging.version import LegacyVersion, Version, parse
|
from packaging.version import LegacyVersion, Version, parse
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
from urllib3.exceptions import HTTPError
|
from urllib3.exceptions import HTTPError
|
||||||
|
|
||||||
from authentik import __version__
|
from authentik import __version__
|
||||||
|
@ -2,24 +2,17 @@
|
|||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import post_save, pre_delete
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.models import Provider
|
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
|
||||||
from authentik.lib.utils.reflection import class_to_path
|
from authentik.lib.utils.reflection import class_to_path
|
||||||
from authentik.outposts.models import Outpost, OutpostServiceConnection
|
from authentik.outposts.models import Outpost
|
||||||
from authentik.outposts.tasks import outpost_post_save, outpost_pre_delete
|
from authentik.outposts.tasks import outpost_post_save, outpost_pre_delete
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
UPDATE_TRIGGERING_MODELS = (
|
|
||||||
Outpost,
|
|
||||||
OutpostServiceConnection,
|
|
||||||
Provider,
|
|
||||||
CertificateKeyPair,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save)
|
@receiver(post_save)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
def post_save_update(sender, instance: Model, **_):
|
def post_save_update(sender, instance: Model, **_):
|
||||||
"""If an Outpost is saved, Ensure that token is created/updated
|
"""If an Outpost is saved, Ensure that token is created/updated
|
||||||
|
|
||||||
@ -29,8 +22,6 @@ def post_save_update(sender, instance: Model, **_):
|
|||||||
return
|
return
|
||||||
if instance.__module__ == "__fake__":
|
if instance.__module__ == "__fake__":
|
||||||
return
|
return
|
||||||
if sender not in UPDATE_TRIGGERING_MODELS:
|
|
||||||
return
|
|
||||||
outpost_post_save.delay(class_to_path(instance.__class__), instance.pk)
|
outpost_post_save.delay(class_to_path(instance.__class__), instance.pk)
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ from channels.layers import get_channel_layer
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models.base import Model
|
from django.db.models.base import Model
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
|
from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||||
from authentik.lib.utils.reflection import path_to_class
|
from authentik.lib.utils.reflection import path_to_class
|
||||||
@ -124,12 +124,14 @@ def outpost_post_save(model_class: str, model_pk: Any):
|
|||||||
_ = instance.token
|
_ = instance.token
|
||||||
LOGGER.debug("Trigger reconcile for outpost")
|
LOGGER.debug("Trigger reconcile for outpost")
|
||||||
outpost_controller.delay(instance.pk)
|
outpost_controller.delay(instance.pk)
|
||||||
|
return
|
||||||
|
|
||||||
if isinstance(instance, (OutpostModel, Outpost)):
|
if isinstance(instance, (OutpostModel, Outpost)):
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"triggering outpost update from outpostmodel/outpost", instance=instance
|
"triggering outpost update from outpostmodel/outpost", instance=instance
|
||||||
)
|
)
|
||||||
outpost_send_update(instance)
|
outpost_send_update(instance)
|
||||||
|
return
|
||||||
|
|
||||||
if isinstance(instance, OutpostServiceConnection):
|
if isinstance(instance, OutpostServiceConnection):
|
||||||
LOGGER.debug("triggering ServiceConnection state update", instance=instance)
|
LOGGER.debug("triggering ServiceConnection state update", instance=instance)
|
||||||
|
@ -7,7 +7,7 @@ from django.db import models
|
|||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.serializers import BaseSerializer
|
from rest_framework.serializers import BaseSerializer
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.policies.models import Policy
|
from authentik.policies.models import Policy
|
||||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
"""authentik policy engine"""
|
"""authentik policy engine"""
|
||||||
from enum import Enum
|
|
||||||
from multiprocessing import Pipe, set_start_method
|
from multiprocessing import Pipe, set_start_method
|
||||||
from multiprocessing.connection import Connection
|
from multiprocessing.connection import Connection
|
||||||
from typing import Iterator, List, Optional
|
from typing import Iterator, List, Optional
|
||||||
@ -8,7 +7,7 @@ from django.core.cache import cache
|
|||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from sentry_sdk.hub import Hub
|
from sentry_sdk.hub import Hub
|
||||||
from sentry_sdk.tracing import Span
|
from sentry_sdk.tracing import Span
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
|
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
|
||||||
@ -38,23 +37,12 @@ class PolicyProcessInfo:
|
|||||||
self.result = None
|
self.result = None
|
||||||
|
|
||||||
|
|
||||||
class PolicyEngineMode(Enum):
|
|
||||||
"""Decide how results of multiple policies should be combined."""
|
|
||||||
|
|
||||||
MODE_AND = "and"
|
|
||||||
MODE_OR = "or"
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyEngine:
|
class PolicyEngine:
|
||||||
"""Orchestrate policy checking, launch tasks and return result"""
|
"""Orchestrate policy checking, launch tasks and return result"""
|
||||||
|
|
||||||
use_cache: bool
|
use_cache: bool
|
||||||
request: PolicyRequest
|
request: PolicyRequest
|
||||||
|
|
||||||
mode: PolicyEngineMode
|
|
||||||
# Allow objects with no policies attached to pass
|
|
||||||
empty_result: bool
|
|
||||||
|
|
||||||
__pbm: PolicyBindingModel
|
__pbm: PolicyBindingModel
|
||||||
__cached_policies: List[PolicyResult]
|
__cached_policies: List[PolicyResult]
|
||||||
__processes: List[PolicyProcessInfo]
|
__processes: List[PolicyProcessInfo]
|
||||||
@ -64,10 +52,6 @@ class PolicyEngine:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None
|
self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None
|
||||||
):
|
):
|
||||||
self.mode = PolicyEngineMode.MODE_AND
|
|
||||||
# For backwards compatibility, set empty_result to true
|
|
||||||
# objects with no policies attached will pass.
|
|
||||||
self.empty_result = True
|
|
||||||
if not isinstance(pbm, PolicyBindingModel): # pragma: no cover
|
if not isinstance(pbm, PolicyBindingModel): # pragma: no cover
|
||||||
raise ValueError(f"{pbm} is not instance of PolicyBindingModel")
|
raise ValueError(f"{pbm} is not instance of PolicyBindingModel")
|
||||||
self.__pbm = pbm
|
self.__pbm = pbm
|
||||||
@ -82,10 +66,8 @@ class PolicyEngine:
|
|||||||
|
|
||||||
def _iter_bindings(self) -> Iterator[PolicyBinding]:
|
def _iter_bindings(self) -> Iterator[PolicyBinding]:
|
||||||
"""Make sure all Policies are their respective classes"""
|
"""Make sure all Policies are their respective classes"""
|
||||||
return (
|
return PolicyBinding.objects.filter(target=self.__pbm, enabled=True).order_by(
|
||||||
PolicyBinding.objects.filter(target=self.__pbm, enabled=True)
|
"order"
|
||||||
.order_by("order")
|
|
||||||
.iterator()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _check_policy_type(self, policy: Policy):
|
def _check_policy_type(self, policy: Policy):
|
||||||
@ -137,19 +119,24 @@ class PolicyEngine:
|
|||||||
x.result for x in self.__processes if x.result
|
x.result for x in self.__processes if x.result
|
||||||
]
|
]
|
||||||
all_results = list(process_results + self.__cached_policies)
|
all_results = list(process_results + self.__cached_policies)
|
||||||
|
final_result = PolicyResult(False)
|
||||||
|
final_result.messages = []
|
||||||
|
final_result.source_results = all_results
|
||||||
if len(all_results) < self.__expected_result_count: # pragma: no cover
|
if len(all_results) < self.__expected_result_count: # pragma: no cover
|
||||||
raise AssertionError("Got less results than polices")
|
raise AssertionError("Got less results than polices")
|
||||||
# No results, no policies attached -> passing
|
for result in all_results:
|
||||||
if len(all_results) == 0:
|
LOGGER.debug(
|
||||||
return PolicyResult(self.empty_result)
|
"P_ENG: result", passing=result.passing, messages=result.messages
|
||||||
passing = False
|
)
|
||||||
if self.mode == PolicyEngineMode.MODE_AND:
|
if result.messages:
|
||||||
passing = all([x.passing for x in all_results])
|
final_result.messages.extend(result.messages)
|
||||||
if self.mode == PolicyEngineMode.MODE_OR:
|
if not result.passing:
|
||||||
passing = any([x.passing for x in all_results])
|
final_result.messages = tuple(final_result.messages)
|
||||||
result = PolicyResult(passing)
|
final_result.passing = False
|
||||||
result.messages = tuple([y for x in all_results for y in x.messages])
|
return final_result
|
||||||
return result
|
final_result.messages = tuple(final_result.messages)
|
||||||
|
final_result.passing = True
|
||||||
|
return final_result
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def passing(self) -> bool:
|
def passing(self) -> bool:
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
"""Event Matcher Policy API"""
|
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
|
||||||
|
|
||||||
from authentik.policies.event_matcher.models import EventMatcherPolicy
|
|
||||||
from authentik.policies.forms import GENERAL_SERIALIZER_FIELDS
|
|
||||||
|
|
||||||
|
|
||||||
class EventMatcherPolicySerializer(ModelSerializer):
|
|
||||||
"""Event Matcher Policy Serializer"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = EventMatcherPolicy
|
|
||||||
fields = GENERAL_SERIALIZER_FIELDS + [
|
|
||||||
"action",
|
|
||||||
"client_ip",
|
|
||||||
"app",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class EventMatcherPolicyViewSet(ModelViewSet):
|
|
||||||
"""Event Matcher Policy Viewset"""
|
|
||||||
|
|
||||||
queryset = EventMatcherPolicy.objects.all()
|
|
||||||
serializer_class = EventMatcherPolicySerializer
|
|
@ -1,11 +0,0 @@
|
|||||||
"""authentik Event Matcher policy app config"""
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class AuthentikPoliciesEventMatcherConfig(AppConfig):
|
|
||||||
"""authentik Event Matcher policy app config"""
|
|
||||||
|
|
||||||
name = "authentik.policies.event_matcher"
|
|
||||||
label = "authentik_policies_event_matcher"
|
|
||||||
verbose_name = "authentik Policies.Event Matcher"
|
|
@ -1,25 +0,0 @@
|
|||||||
"""authentik Event Matcher Policy forms"""
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from authentik.policies.event_matcher.models import EventMatcherPolicy
|
|
||||||
from authentik.policies.forms import GENERAL_FIELDS
|
|
||||||
|
|
||||||
|
|
||||||
class EventMatcherPolicyForm(forms.ModelForm):
|
|
||||||
"""EventMatcherPolicy Form"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = EventMatcherPolicy
|
|
||||||
fields = GENERAL_FIELDS + [
|
|
||||||
"action",
|
|
||||||
"client_ip",
|
|
||||||
"app",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput(),
|
|
||||||
"client_ip": forms.TextInput(),
|
|
||||||
}
|
|
||||||
labels = {"client_ip": _("Client IP")}
|
|
@ -1,70 +0,0 @@
|
|||||||
# Generated by Django 3.1.4 on 2020-12-24 10:32
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_policies", "0004_policy_execution_logging"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="EventMatcherPolicy",
|
|
||||||
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",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"action",
|
|
||||||
models.TextField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("login", "Login"),
|
|
||||||
("login_failed", "Login Failed"),
|
|
||||||
("logout", "Logout"),
|
|
||||||
("user_write", "User Write"),
|
|
||||||
("suspicious_request", "Suspicious Request"),
|
|
||||||
("password_set", "Password Set"),
|
|
||||||
("token_view", "Token View"),
|
|
||||||
("invitation_created", "Invite Created"),
|
|
||||||
("invitation_used", "Invite Used"),
|
|
||||||
("authorize_application", "Authorize Application"),
|
|
||||||
("source_linked", "Source Linked"),
|
|
||||||
("impersonation_started", "Impersonation Started"),
|
|
||||||
("impersonation_ended", "Impersonation Ended"),
|
|
||||||
("policy_execution", "Policy Execution"),
|
|
||||||
("policy_exception", "Policy Exception"),
|
|
||||||
(
|
|
||||||
"property_mapping_exception",
|
|
||||||
"Property Mapping Exception",
|
|
||||||
),
|
|
||||||
("model_created", "Model Created"),
|
|
||||||
("model_updated", "Model Updated"),
|
|
||||||
("model_deleted", "Model Deleted"),
|
|
||||||
("update_available", "Update Available"),
|
|
||||||
("custom_", "Custom Prefix"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("client_ip", models.TextField(blank=True)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Group Membership Policy",
|
|
||||||
"verbose_name_plural": "Group Membership Policies",
|
|
||||||
},
|
|
||||||
bases=("authentik_policies.policy",),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,43 +0,0 @@
|
|||||||
# Generated by Django 3.1.4 on 2020-12-30 20:46
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_policies_event_matcher", "0001_initial"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="eventmatcherpolicy",
|
|
||||||
name="action",
|
|
||||||
field=models.TextField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("login", "Login"),
|
|
||||||
("login_failed", "Login Failed"),
|
|
||||||
("logout", "Logout"),
|
|
||||||
("user_write", "User Write"),
|
|
||||||
("suspicious_request", "Suspicious Request"),
|
|
||||||
("password_set", "Password Set"),
|
|
||||||
("token_view", "Token View"),
|
|
||||||
("invitation_used", "Invite Used"),
|
|
||||||
("authorize_application", "Authorize Application"),
|
|
||||||
("source_linked", "Source Linked"),
|
|
||||||
("impersonation_started", "Impersonation Started"),
|
|
||||||
("impersonation_ended", "Impersonation Ended"),
|
|
||||||
("policy_execution", "Policy Execution"),
|
|
||||||
("policy_exception", "Policy Exception"),
|
|
||||||
("property_mapping_exception", "Property Mapping Exception"),
|
|
||||||
("configuration_error", "Configuration Error"),
|
|
||||||
("model_created", "Model Created"),
|
|
||||||
("model_updated", "Model Updated"),
|
|
||||||
("model_deleted", "Model Deleted"),
|
|
||||||
("update_available", "Update Available"),
|
|
||||||
("custom_", "Custom Prefix"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,111 +0,0 @@
|
|||||||
# Generated by Django 3.1.4 on 2021-01-10 19:07
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_policies_event_matcher", "0002_auto_20201230_2046"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="eventmatcherpolicy",
|
|
||||||
name="app",
|
|
||||||
field=models.TextField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("authentik.admin", "authentik Admin"),
|
|
||||||
("authentik.api", "authentik API"),
|
|
||||||
("authentik.events", "authentik Events"),
|
|
||||||
("authentik.crypto", "authentik Crypto"),
|
|
||||||
("authentik.flows", "authentik Flows"),
|
|
||||||
("authentik.outposts", "authentik Outpost"),
|
|
||||||
("authentik.lib", "authentik lib"),
|
|
||||||
("authentik.policies", "authentik Policies"),
|
|
||||||
("authentik.policies.dummy", "authentik Policies.Dummy"),
|
|
||||||
(
|
|
||||||
"authentik.policies.event_matcher",
|
|
||||||
"authentik Policies.Event Matcher",
|
|
||||||
),
|
|
||||||
("authentik.policies.expiry", "authentik Policies.Expiry"),
|
|
||||||
("authentik.policies.expression", "authentik Policies.Expression"),
|
|
||||||
(
|
|
||||||
"authentik.policies.group_membership",
|
|
||||||
"authentik Policies.Group Membership",
|
|
||||||
),
|
|
||||||
("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"),
|
|
||||||
("authentik.policies.password", "authentik Policies.Password"),
|
|
||||||
("authentik.policies.reputation", "authentik Policies.Reputation"),
|
|
||||||
("authentik.providers.proxy", "authentik Providers.Proxy"),
|
|
||||||
("authentik.providers.oauth2", "authentik Providers.OAuth2"),
|
|
||||||
("authentik.providers.saml", "authentik Providers.SAML"),
|
|
||||||
("authentik.recovery", "authentik Recovery"),
|
|
||||||
("authentik.sources.ldap", "authentik Sources.LDAP"),
|
|
||||||
("authentik.sources.oauth", "authentik Sources.OAuth"),
|
|
||||||
("authentik.sources.saml", "authentik Sources.SAML"),
|
|
||||||
("authentik.stages.captcha", "authentik Stages.Captcha"),
|
|
||||||
("authentik.stages.consent", "authentik Stages.Consent"),
|
|
||||||
("authentik.stages.dummy", "authentik Stages.Dummy"),
|
|
||||||
("authentik.stages.email", "authentik Stages.Email"),
|
|
||||||
("authentik.stages.prompt", "authentik Stages.Prompt"),
|
|
||||||
(
|
|
||||||
"authentik.stages.identification",
|
|
||||||
"authentik Stages.Identification",
|
|
||||||
),
|
|
||||||
("authentik.stages.invitation", "authentik Stages.User Invitation"),
|
|
||||||
("authentik.stages.user_delete", "authentik Stages.User Delete"),
|
|
||||||
("authentik.stages.user_login", "authentik Stages.User Login"),
|
|
||||||
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
|
||||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
|
||||||
("authentik.stages.otp_static", "authentik OTP.Static"),
|
|
||||||
("authentik.stages.otp_time", "authentik OTP.Time"),
|
|
||||||
("authentik.stages.otp_validate", "authentik OTP.Validate"),
|
|
||||||
("authentik.stages.password", "authentik Stages.Password"),
|
|
||||||
("authentik.core", "authentik Core"),
|
|
||||||
],
|
|
||||||
default="",
|
|
||||||
help_text="Match events created by selected application. When left empty, all applications are matched.",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="eventmatcherpolicy",
|
|
||||||
name="action",
|
|
||||||
field=models.TextField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("login", "Login"),
|
|
||||||
("login_failed", "Login Failed"),
|
|
||||||
("logout", "Logout"),
|
|
||||||
("user_write", "User Write"),
|
|
||||||
("suspicious_request", "Suspicious Request"),
|
|
||||||
("password_set", "Password Set"),
|
|
||||||
("token_view", "Token View"),
|
|
||||||
("invitation_used", "Invite Used"),
|
|
||||||
("authorize_application", "Authorize Application"),
|
|
||||||
("source_linked", "Source Linked"),
|
|
||||||
("impersonation_started", "Impersonation Started"),
|
|
||||||
("impersonation_ended", "Impersonation Ended"),
|
|
||||||
("policy_execution", "Policy Execution"),
|
|
||||||
("policy_exception", "Policy Exception"),
|
|
||||||
("property_mapping_exception", "Property Mapping Exception"),
|
|
||||||
("configuration_error", "Configuration Error"),
|
|
||||||
("model_created", "Model Created"),
|
|
||||||
("model_updated", "Model Updated"),
|
|
||||||
("model_deleted", "Model Deleted"),
|
|
||||||
("update_available", "Update Available"),
|
|
||||||
("custom_", "Custom Prefix"),
|
|
||||||
],
|
|
||||||
help_text="Match created events with this action type. When left empty, all action types will be matched.",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="eventmatcherpolicy",
|
|
||||||
name="client_ip",
|
|
||||||
field=models.TextField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Matches Event's Client IP (strict matching, for network matching use an Expression Policy)",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,79 +0,0 @@
|
|||||||
# Generated by Django 3.1.4 on 2021-01-12 21:58
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_policies_event_matcher", "0003_auto_20210110_1907"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name="eventmatcherpolicy",
|
|
||||||
options={
|
|
||||||
"verbose_name": "Event Matcher Policy",
|
|
||||||
"verbose_name_plural": "Event Matcher Policies",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="eventmatcherpolicy",
|
|
||||||
name="app",
|
|
||||||
field=models.TextField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("authentik.admin", "authentik Admin"),
|
|
||||||
("authentik.api", "authentik API"),
|
|
||||||
("authentik.events", "authentik Events"),
|
|
||||||
("authentik.crypto", "authentik Crypto"),
|
|
||||||
("authentik.flows", "authentik Flows"),
|
|
||||||
("authentik.outposts", "authentik Outpost"),
|
|
||||||
("authentik.lib", "authentik lib"),
|
|
||||||
("authentik.policies", "authentik Policies"),
|
|
||||||
("authentik.policies.dummy", "authentik Policies.Dummy"),
|
|
||||||
(
|
|
||||||
"authentik.policies.event_matcher",
|
|
||||||
"authentik Policies.Event Matcher",
|
|
||||||
),
|
|
||||||
("authentik.policies.expiry", "authentik Policies.Expiry"),
|
|
||||||
("authentik.policies.expression", "authentik Policies.Expression"),
|
|
||||||
(
|
|
||||||
"authentik.policies.group_membership",
|
|
||||||
"authentik Policies.Group Membership",
|
|
||||||
),
|
|
||||||
("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"),
|
|
||||||
("authentik.policies.password", "authentik Policies.Password"),
|
|
||||||
("authentik.policies.reputation", "authentik Policies.Reputation"),
|
|
||||||
("authentik.providers.proxy", "authentik Providers.Proxy"),
|
|
||||||
("authentik.providers.oauth2", "authentik Providers.OAuth2"),
|
|
||||||
("authentik.providers.saml", "authentik Providers.SAML"),
|
|
||||||
("authentik.recovery", "authentik Recovery"),
|
|
||||||
("authentik.sources.ldap", "authentik Sources.LDAP"),
|
|
||||||
("authentik.sources.oauth", "authentik Sources.OAuth"),
|
|
||||||
("authentik.sources.saml", "authentik Sources.SAML"),
|
|
||||||
("authentik.stages.captcha", "authentik Stages.Captcha"),
|
|
||||||
("authentik.stages.consent", "authentik Stages.Consent"),
|
|
||||||
("authentik.stages.dummy", "authentik Stages.Dummy"),
|
|
||||||
("authentik.stages.email", "authentik Stages.Email"),
|
|
||||||
("authentik.stages.prompt", "authentik Stages.Prompt"),
|
|
||||||
(
|
|
||||||
"authentik.stages.identification",
|
|
||||||
"authentik Stages.Identification",
|
|
||||||
),
|
|
||||||
("authentik.stages.invitation", "authentik Stages.User Invitation"),
|
|
||||||
("authentik.stages.user_delete", "authentik Stages.User Delete"),
|
|
||||||
("authentik.stages.user_login", "authentik Stages.User Login"),
|
|
||||||
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
|
||||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
|
||||||
("authentik.stages.otp_static", "authentik Stages.OTP.Static"),
|
|
||||||
("authentik.stages.otp_time", "authentik Stages.OTP.Time"),
|
|
||||||
("authentik.stages.otp_validate", "authentik Stages.OTP.Validate"),
|
|
||||||
("authentik.stages.password", "authentik Stages.Password"),
|
|
||||||
("authentik.core", "authentik Core"),
|
|
||||||
],
|
|
||||||
default="",
|
|
||||||
help_text="Match events created by selected application. When left empty, all applications are matched.",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,88 +0,0 @@
|
|||||||
"""Event Matcher models"""
|
|
||||||
from typing import Type
|
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.db import models
|
|
||||||
from django.forms import ModelForm
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from rest_framework.serializers import BaseSerializer
|
|
||||||
|
|
||||||
from authentik.events.models import Event, EventAction
|
|
||||||
from authentik.policies.models import Policy
|
|
||||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
|
||||||
|
|
||||||
|
|
||||||
def app_choices() -> list[tuple[str, str]]:
|
|
||||||
"""Get a list of all installed applications that create events.
|
|
||||||
Returns a list of tuples containing (dotted.app.path, name)"""
|
|
||||||
choices = []
|
|
||||||
for app in apps.get_app_configs():
|
|
||||||
if app.label.startswith("authentik"):
|
|
||||||
choices.append((app.name, app.verbose_name))
|
|
||||||
return choices
|
|
||||||
|
|
||||||
|
|
||||||
class EventMatcherPolicy(Policy):
|
|
||||||
"""Passes when Event matches selected criteria."""
|
|
||||||
|
|
||||||
action = models.TextField(
|
|
||||||
choices=EventAction.choices,
|
|
||||||
blank=True,
|
|
||||||
help_text=_(
|
|
||||||
(
|
|
||||||
"Match created events with this action type. "
|
|
||||||
"When left empty, all action types will be matched."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
app = models.TextField(
|
|
||||||
choices=app_choices(),
|
|
||||||
blank=True,
|
|
||||||
default="",
|
|
||||||
help_text=_(
|
|
||||||
(
|
|
||||||
"Match events created by selected application. "
|
|
||||||
"When left empty, all applications are matched."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
client_ip = models.TextField(
|
|
||||||
blank=True,
|
|
||||||
help_text=_(
|
|
||||||
(
|
|
||||||
"Matches Event's Client IP (strict matching, "
|
|
||||||
"for network matching use an Expression Policy)"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def serializer(self) -> BaseSerializer:
|
|
||||||
from authentik.policies.event_matcher.api import (
|
|
||||||
EventMatcherPolicySerializer,
|
|
||||||
)
|
|
||||||
|
|
||||||
return EventMatcherPolicySerializer
|
|
||||||
|
|
||||||
@property
|
|
||||||
def form(self) -> Type[ModelForm]:
|
|
||||||
from authentik.policies.event_matcher.forms import EventMatcherPolicyForm
|
|
||||||
|
|
||||||
return EventMatcherPolicyForm
|
|
||||||
|
|
||||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
|
||||||
if "event" not in request.context:
|
|
||||||
return PolicyResult(False)
|
|
||||||
event: Event = request.context["event"]
|
|
||||||
if event.action == self.action:
|
|
||||||
return PolicyResult(True, "Action matched.")
|
|
||||||
if event.client_ip == self.client_ip:
|
|
||||||
return PolicyResult(True, "Client IP matched.")
|
|
||||||
if event.app == self.app:
|
|
||||||
return PolicyResult(True, "App matched.")
|
|
||||||
return PolicyResult(False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
verbose_name = _("Event Matcher Policy")
|
|
||||||
verbose_name_plural = _("Event Matcher Policies")
|
|
@ -1,68 +0,0 @@
|
|||||||
"""event_matcher tests"""
|
|
||||||
from django.test import TestCase
|
|
||||||
from guardian.shortcuts import get_anonymous_user
|
|
||||||
|
|
||||||
from authentik.events.models import Event, EventAction
|
|
||||||
from authentik.policies.event_matcher.models import EventMatcherPolicy
|
|
||||||
from authentik.policies.types import PolicyRequest
|
|
||||||
|
|
||||||
|
|
||||||
class TestEventMatcherPolicy(TestCase):
|
|
||||||
"""EventMatcherPolicy tests"""
|
|
||||||
|
|
||||||
def test_match_action(self):
|
|
||||||
"""Test match action"""
|
|
||||||
event = Event.new(EventAction.LOGIN)
|
|
||||||
request = PolicyRequest(get_anonymous_user())
|
|
||||||
request.context["event"] = event
|
|
||||||
policy: EventMatcherPolicy = EventMatcherPolicy.objects.create(
|
|
||||||
action=EventAction.LOGIN
|
|
||||||
)
|
|
||||||
response = policy.passes(request)
|
|
||||||
self.assertTrue(response.passing)
|
|
||||||
self.assertTupleEqual(response.messages, ("Action matched.",))
|
|
||||||
|
|
||||||
def test_match_client_ip(self):
|
|
||||||
"""Test match client_ip"""
|
|
||||||
event = Event.new(EventAction.LOGIN)
|
|
||||||
event.client_ip = "1.2.3.4"
|
|
||||||
request = PolicyRequest(get_anonymous_user())
|
|
||||||
request.context["event"] = event
|
|
||||||
policy: EventMatcherPolicy = EventMatcherPolicy.objects.create(
|
|
||||||
client_ip="1.2.3.4"
|
|
||||||
)
|
|
||||||
response = policy.passes(request)
|
|
||||||
self.assertTrue(response.passing)
|
|
||||||
self.assertTupleEqual(response.messages, ("Client IP matched.",))
|
|
||||||
|
|
||||||
def test_match_app(self):
|
|
||||||
"""Test match app"""
|
|
||||||
event = Event.new(EventAction.LOGIN)
|
|
||||||
event.app = "foo"
|
|
||||||
request = PolicyRequest(get_anonymous_user())
|
|
||||||
request.context["event"] = event
|
|
||||||
policy: EventMatcherPolicy = EventMatcherPolicy.objects.create(app="foo")
|
|
||||||
response = policy.passes(request)
|
|
||||||
self.assertTrue(response.passing)
|
|
||||||
self.assertTupleEqual(response.messages, ("App matched.",))
|
|
||||||
|
|
||||||
def test_drop(self):
|
|
||||||
"""Test drop event"""
|
|
||||||
event = Event.new(EventAction.LOGIN)
|
|
||||||
event.client_ip = "1.2.3.4"
|
|
||||||
request = PolicyRequest(get_anonymous_user())
|
|
||||||
request.context["event"] = event
|
|
||||||
policy: EventMatcherPolicy = EventMatcherPolicy.objects.create(
|
|
||||||
client_ip="1.2.3.5"
|
|
||||||
)
|
|
||||||
response = policy.passes(request)
|
|
||||||
self.assertFalse(response.passing)
|
|
||||||
|
|
||||||
def test_invalid(self):
|
|
||||||
"""Test passing event"""
|
|
||||||
request = PolicyRequest(get_anonymous_user())
|
|
||||||
policy: EventMatcherPolicy = EventMatcherPolicy.objects.create(
|
|
||||||
client_ip="1.2.3.4"
|
|
||||||
)
|
|
||||||
response = policy.passes(request)
|
|
||||||
self.assertFalse(response.passing)
|
|
@ -1,14 +1,6 @@
|
|||||||
"""policy exceptions"""
|
"""policy exceptions"""
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
|
|
||||||
|
|
||||||
class PolicyException(SentryIgnoredException):
|
class PolicyException(SentryIgnoredException):
|
||||||
"""Exception that should be raised during Policy Evaluation, and can be recovered from."""
|
"""Exception that should be raised during Policy Evaluation, and can be recovered from."""
|
||||||
|
|
||||||
src_exc: Optional[Exception] = None
|
|
||||||
|
|
||||||
def __init__(self, src_exc: Optional[Exception] = None) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.src_exc = src_exc
|
|
||||||
|
@ -7,7 +7,7 @@ from django.forms import ModelForm
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from rest_framework.serializers import BaseSerializer
|
from rest_framework.serializers import BaseSerializer
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.policies.models import Policy
|
from authentik.policies.models import Policy
|
||||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
"""authentik expression policy evaluator"""
|
"""authentik expression policy evaluator"""
|
||||||
from ipaddress import ip_address, ip_network
|
from ipaddress import ip_address, ip_network
|
||||||
|
from traceback import format_tb
|
||||||
from typing import TYPE_CHECKING, List, Optional
|
from typing import TYPE_CHECKING, List, Optional
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
|
from authentik.events.utils import model_to_dict, sanitize_dict
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_SSO
|
from authentik.flows.planner import PLAN_CONTEXT_SSO
|
||||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
from authentik.policies.exceptions import PolicyException
|
|
||||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -55,26 +57,32 @@ class PolicyEvaluator(BaseEvaluator):
|
|||||||
|
|
||||||
def handle_error(self, exc: Exception, expression_source: str):
|
def handle_error(self, exc: Exception, expression_source: str):
|
||||||
"""Exception Handler"""
|
"""Exception Handler"""
|
||||||
# So, this is a bit questionable. Essentially, we are edit the stacktrace
|
error_string = "\n".join(format_tb(exc.__traceback__) + [str(exc)])
|
||||||
# so the user only sees information relevant to them
|
event = Event.new(
|
||||||
# and none of our surrounding error handling
|
EventAction.POLICY_EXCEPTION,
|
||||||
exc.__traceback__ = exc.__traceback__.tb_next
|
expression=expression_source,
|
||||||
raise PolicyException(exc)
|
error=error_string,
|
||||||
|
request=self._context["request"],
|
||||||
|
)
|
||||||
|
if self.policy:
|
||||||
|
event.context["model"] = sanitize_dict(model_to_dict(self.policy))
|
||||||
|
if "http_request" in self._context:
|
||||||
|
event.from_http(self._context["http_request"])
|
||||||
|
else:
|
||||||
|
event.set_user(self._context["request"].user)
|
||||||
|
event.save()
|
||||||
|
|
||||||
def evaluate(self, expression_source: str) -> PolicyResult:
|
def evaluate(self, expression_source: str) -> PolicyResult:
|
||||||
"""Parse and evaluate expression. Policy is expected to return a truthy object.
|
"""Parse and evaluate expression. Policy is expected to return a truthy object.
|
||||||
Messages can be added using 'do ak_message()'."""
|
Messages can be added using 'do ak_message()'."""
|
||||||
try:
|
try:
|
||||||
result = super().evaluate(expression_source)
|
result = super().evaluate(expression_source)
|
||||||
except PolicyException as exc:
|
|
||||||
# PolicyExceptions should be propagated back to the process,
|
|
||||||
# which handles recording and returning a correct result
|
|
||||||
raise exc
|
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
LOGGER.warning("Expression error", exc=exc)
|
LOGGER.warning("Expression error", exc=exc)
|
||||||
return PolicyResult(False, str(exc))
|
return PolicyResult(False, str(exc))
|
||||||
else:
|
else:
|
||||||
policy_result = PolicyResult(False, *self._messages)
|
policy_result = PolicyResult(False)
|
||||||
|
policy_result.messages = tuple(self._messages)
|
||||||
if result is None:
|
if result is None:
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"Expression policy returned None",
|
"Expression policy returned None",
|
||||||
|
@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
|
|
||||||
from authentik.policies.exceptions import PolicyException
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.policies.expression.evaluator import PolicyEvaluator
|
from authentik.policies.expression.evaluator import PolicyEvaluator
|
||||||
from authentik.policies.expression.models import ExpressionPolicy
|
from authentik.policies.expression.models import ExpressionPolicy
|
||||||
from authentik.policies.types import PolicyRequest
|
from authentik.policies.types import PolicyRequest
|
||||||
@ -44,8 +44,30 @@ class TestEvaluator(TestCase):
|
|||||||
template = ";"
|
template = ";"
|
||||||
evaluator = PolicyEvaluator("test")
|
evaluator = PolicyEvaluator("test")
|
||||||
evaluator.set_policy_request(self.request)
|
evaluator.set_policy_request(self.request)
|
||||||
with self.assertRaises(PolicyException):
|
result = evaluator.evaluate(template)
|
||||||
evaluator.evaluate(template)
|
self.assertEqual(result.passing, False)
|
||||||
|
self.assertEqual(result.messages, ("invalid syntax (test, line 3)",))
|
||||||
|
self.assertTrue(
|
||||||
|
Event.objects.filter(
|
||||||
|
action=EventAction.POLICY_EXCEPTION,
|
||||||
|
context__expression=template,
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_undefined(self):
|
||||||
|
"""test undefined result"""
|
||||||
|
template = "{{ foo.bar }}"
|
||||||
|
evaluator = PolicyEvaluator("test")
|
||||||
|
evaluator.set_policy_request(self.request)
|
||||||
|
result = evaluator.evaluate(template)
|
||||||
|
self.assertEqual(result.passing, False)
|
||||||
|
self.assertEqual(result.messages, ("name 'foo' is not defined",))
|
||||||
|
self.assertTrue(
|
||||||
|
Event.objects.filter(
|
||||||
|
action=EventAction.POLICY_EXCEPTION,
|
||||||
|
context__expression=template,
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
def test_validate(self):
|
def test_validate(self):
|
||||||
"""test validate"""
|
"""test validate"""
|
||||||
|
@ -7,7 +7,7 @@ from django.forms import ModelForm
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from requests import get
|
from requests import get
|
||||||
from rest_framework.serializers import BaseSerializer
|
from rest_framework.serializers import BaseSerializer
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.policies.models import Policy, PolicyResult
|
from authentik.policies.models import Policy, PolicyResult
|
||||||
from authentik.policies.types import PolicyRequest
|
from authentik.policies.types import PolicyRequest
|
||||||
|
@ -64,10 +64,7 @@ class PolicyBinding(SerializerModel):
|
|||||||
return PolicyBindingSerializer
|
return PolicyBindingSerializer
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
try:
|
return f"Policy Binding {self.target} #{self.order} {self.policy}"
|
||||||
return f"Policy Binding {self.target} #{self.order} {self.policy}"
|
|
||||||
except PolicyBinding.target.RelatedObjectDoesNotExist: # pylint: disable=no-member
|
|
||||||
return f"Policy Binding - #{self.order} {self.policy}"
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ from django.db import models
|
|||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from rest_framework.serializers import BaseSerializer
|
from rest_framework.serializers import BaseSerializer
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.policies.models import Policy
|
from authentik.policies.models import Policy
|
||||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
"""authentik policy task"""
|
"""authentik policy task"""
|
||||||
from multiprocessing import Process
|
from multiprocessing import Process
|
||||||
from multiprocessing.connection import Connection
|
from multiprocessing.connection import Connection
|
||||||
from traceback import format_tb
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from sentry_sdk.hub import Hub
|
from sentry_sdk.hub import Hub
|
||||||
from sentry_sdk.tracing import Span
|
from sentry_sdk.tracing import Span
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.policies.exceptions import PolicyException
|
from authentik.policies.exceptions import PolicyException
|
||||||
@ -15,13 +14,12 @@ from authentik.policies.models import PolicyBinding
|
|||||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
TRACEBACK_HEADER = "Traceback (most recent call last):\n"
|
|
||||||
|
|
||||||
|
|
||||||
def cache_key(binding: PolicyBinding, request: PolicyRequest) -> str:
|
def cache_key(binding: PolicyBinding, request: PolicyRequest) -> str:
|
||||||
"""Generate Cache key for policy"""
|
"""Generate Cache key for policy"""
|
||||||
prefix = f"policy_{binding.policy_binding_uuid.hex}_{binding.policy.pk.hex}"
|
prefix = f"policy_{binding.policy_binding_uuid.hex}_{binding.policy.pk.hex}"
|
||||||
if request.http_request and hasattr(request.http_request, "session"):
|
if request.http_request:
|
||||||
prefix += f"_{request.http_request.session.session_key}"
|
prefix += f"_{request.http_request.session.session_key}"
|
||||||
if request.user:
|
if request.user:
|
||||||
prefix += f"#{request.user.pk}"
|
prefix += f"#{request.user.pk}"
|
||||||
@ -49,24 +47,6 @@ class PolicyProcess(Process):
|
|||||||
if connection:
|
if connection:
|
||||||
self.connection = connection
|
self.connection = connection
|
||||||
|
|
||||||
def create_event(self, action: str, message: str, **kwargs):
|
|
||||||
"""Create event with common values from `self.request` and `self.binding`."""
|
|
||||||
# Keep a reference to http_request even if its None, because cleanse_dict will remove it
|
|
||||||
http_request = self.request.http_request
|
|
||||||
event = Event.new(
|
|
||||||
action=action,
|
|
||||||
message=message,
|
|
||||||
policy_uuid=self.binding.policy.policy_uuid.hex,
|
|
||||||
binding=self.binding,
|
|
||||||
request=self.request,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
event.set_user(self.request.user)
|
|
||||||
if http_request:
|
|
||||||
event.from_http(http_request)
|
|
||||||
else:
|
|
||||||
event.save()
|
|
||||||
|
|
||||||
def execute(self) -> PolicyResult:
|
def execute(self) -> PolicyResult:
|
||||||
"""Run actual policy, returns result"""
|
"""Run actual policy, returns result"""
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
@ -78,23 +58,16 @@ class PolicyProcess(Process):
|
|||||||
try:
|
try:
|
||||||
policy_result = self.binding.policy.passes(self.request)
|
policy_result = self.binding.policy.passes(self.request)
|
||||||
if self.binding.policy.execution_logging:
|
if self.binding.policy.execution_logging:
|
||||||
self.create_event(
|
event = Event.new(
|
||||||
EventAction.POLICY_EXECUTION,
|
EventAction.POLICY_EXECUTION,
|
||||||
message="Policy Execution",
|
request=self.request,
|
||||||
result=policy_result,
|
result=policy_result,
|
||||||
)
|
)
|
||||||
|
event.set_user(self.request.user)
|
||||||
|
event.save()
|
||||||
except PolicyException as exc:
|
except PolicyException as exc:
|
||||||
# Either use passed original exception or whatever we have
|
LOGGER.debug("P_ENG(proc): error", exc=exc)
|
||||||
src_exc = exc.src_exc if exc.src_exc else exc
|
policy_result = PolicyResult(False, str(exc))
|
||||||
error_string = (
|
|
||||||
TRACEBACK_HEADER
|
|
||||||
+ "".join(format_tb(src_exc.__traceback__))
|
|
||||||
+ str(src_exc)
|
|
||||||
)
|
|
||||||
# Create policy exception event
|
|
||||||
self.create_event(EventAction.POLICY_EXCEPTION, message=error_string)
|
|
||||||
LOGGER.debug("P_ENG(proc): error", exc=src_exc)
|
|
||||||
policy_result = PolicyResult(False, str(src_exc))
|
|
||||||
policy_result.source_policy = self.binding.policy
|
policy_result.source_policy = self.binding.policy
|
||||||
# Invert result if policy.negate is set
|
# Invert result if policy.negate is set
|
||||||
if self.binding.negate:
|
if self.binding.negate:
|
||||||
@ -123,5 +96,5 @@ class PolicyProcess(Process):
|
|||||||
try:
|
try:
|
||||||
self.connection.send(self.execute())
|
self.connection.send(self.execute())
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
LOGGER.warning(str(exc))
|
LOGGER.warning(exc)
|
||||||
self.connection.send(PolicyResult(False, str(exc)))
|
self.connection.send(PolicyResult(False, str(exc)))
|
||||||
|
@ -3,7 +3,7 @@ from django.contrib.auth.signals import user_logged_in, user_login_failed
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
from authentik.policies.reputation.models import (
|
from authentik.policies.reputation.models import (
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Reputation tasks"""
|
"""Reputation tasks"""
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
|
from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.api.applications import user_app_cache_key
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -25,6 +23,3 @@ def invalidate_policy_cache(sender, instance, **_):
|
|||||||
total += len(keys)
|
total += len(keys)
|
||||||
cache.delete_many(keys)
|
cache.delete_many(keys)
|
||||||
LOGGER.debug("Invalidating policy cache", policy=instance, keys=total)
|
LOGGER.debug("Invalidating policy cache", policy=instance, keys=total)
|
||||||
# Also delete user application cache
|
|
||||||
keys = user_app_cache_key("*")
|
|
||||||
cache.delete_many(keys)
|
|
||||||
|
@ -4,7 +4,7 @@ from django.test import TestCase
|
|||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
from authentik.policies.engine import PolicyEngine, PolicyEngineMode
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.policies.expression.models import ExpressionPolicy
|
from authentik.policies.expression.models import ExpressionPolicy
|
||||||
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
|
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
|
||||||
from authentik.policies.tests.test_process import clear_policy_cache
|
from authentik.policies.tests.test_process import clear_policy_cache
|
||||||
@ -44,38 +44,15 @@ class TestPolicyEngine(TestCase):
|
|||||||
self.assertEqual(result.passing, True)
|
self.assertEqual(result.passing, True)
|
||||||
self.assertEqual(result.messages, ("dummy",))
|
self.assertEqual(result.messages, ("dummy",))
|
||||||
|
|
||||||
def test_engine_mode_and(self):
|
def test_engine(self):
|
||||||
"""Ensure all policies passes with AND mode (false and true -> false)"""
|
"""Ensure all policies passes (Mix of false and true -> false)"""
|
||||||
pbm = PolicyBindingModel.objects.create()
|
pbm = PolicyBindingModel.objects.create()
|
||||||
PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0)
|
PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0)
|
||||||
PolicyBinding.objects.create(target=pbm, policy=self.policy_true, order=1)
|
PolicyBinding.objects.create(target=pbm, policy=self.policy_true, order=1)
|
||||||
engine = PolicyEngine(pbm, self.user)
|
engine = PolicyEngine(pbm, self.user)
|
||||||
result = engine.build().result
|
result = engine.build().result
|
||||||
self.assertEqual(result.passing, False)
|
self.assertEqual(result.passing, False)
|
||||||
self.assertEqual(
|
self.assertEqual(result.messages, ("dummy",))
|
||||||
result.messages,
|
|
||||||
(
|
|
||||||
"dummy",
|
|
||||||
"dummy",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_engine_mode_or(self):
|
|
||||||
"""Ensure all policies passes with OR mode (false and true -> true)"""
|
|
||||||
pbm = PolicyBindingModel.objects.create()
|
|
||||||
PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0)
|
|
||||||
PolicyBinding.objects.create(target=pbm, policy=self.policy_true, order=1)
|
|
||||||
engine = PolicyEngine(pbm, self.user)
|
|
||||||
engine.mode = PolicyEngineMode.MODE_OR
|
|
||||||
result = engine.build().result
|
|
||||||
self.assertEqual(result.passing, True)
|
|
||||||
self.assertEqual(
|
|
||||||
result.messages,
|
|
||||||
(
|
|
||||||
"dummy",
|
|
||||||
"dummy",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_engine_negate(self):
|
def test_engine_negate(self):
|
||||||
"""Test negate flag"""
|
"""Test negate flag"""
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
"""policy process tests"""
|
"""policy process tests"""
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.test import RequestFactory, TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from authentik.core.models import Application, User
|
from authentik.core.models import User
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
from authentik.policies.expression.models import ExpressionPolicy
|
from authentik.policies.expression.models import ExpressionPolicy
|
||||||
@ -22,7 +22,6 @@ class TestPolicyProcess(TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
clear_policy_cache()
|
clear_policy_cache()
|
||||||
self.factory = RequestFactory()
|
|
||||||
self.user = User.objects.create_user(username="policyuser")
|
self.user = User.objects.create_user(username="policyuser")
|
||||||
|
|
||||||
def test_invalid(self):
|
def test_invalid(self):
|
||||||
@ -65,9 +64,7 @@ class TestPolicyProcess(TestCase):
|
|||||||
def test_exception(self):
|
def test_exception(self):
|
||||||
"""Test policy execution"""
|
"""Test policy execution"""
|
||||||
policy = Policy.objects.create()
|
policy = Policy.objects.create()
|
||||||
binding = PolicyBinding(
|
binding = PolicyBinding(policy=policy)
|
||||||
policy=policy, target=Application.objects.create(name="test")
|
|
||||||
)
|
|
||||||
|
|
||||||
request = PolicyRequest(self.user)
|
request = PolicyRequest(self.user)
|
||||||
response = PolicyProcess(binding, request, None).execute()
|
response = PolicyProcess(binding, request, None).execute()
|
||||||
@ -78,51 +75,31 @@ class TestPolicyProcess(TestCase):
|
|||||||
policy = DummyPolicy.objects.create(
|
policy = DummyPolicy.objects.create(
|
||||||
result=False, wait_min=0, wait_max=1, execution_logging=True
|
result=False, wait_min=0, wait_max=1, execution_logging=True
|
||||||
)
|
)
|
||||||
binding = PolicyBinding(
|
binding = PolicyBinding(policy=policy)
|
||||||
policy=policy, target=Application.objects.create(name="test")
|
|
||||||
)
|
|
||||||
|
|
||||||
http_request = self.factory.get("/")
|
|
||||||
http_request.user = self.user
|
|
||||||
|
|
||||||
request = PolicyRequest(self.user)
|
request = PolicyRequest(self.user)
|
||||||
request.http_request = http_request
|
|
||||||
response = PolicyProcess(binding, request, None).execute()
|
response = PolicyProcess(binding, request, None).execute()
|
||||||
self.assertEqual(response.passing, False)
|
self.assertEqual(response.passing, False)
|
||||||
self.assertEqual(response.messages, ("dummy",))
|
self.assertEqual(response.messages, ("dummy",))
|
||||||
|
|
||||||
events = Event.objects.filter(
|
events = Event.objects.filter(
|
||||||
action=EventAction.POLICY_EXECUTION,
|
action=EventAction.POLICY_EXECUTION,
|
||||||
context__policy_uuid=policy.policy_uuid.hex,
|
|
||||||
)
|
)
|
||||||
self.assertTrue(events.exists())
|
self.assertTrue(events.exists())
|
||||||
self.assertEqual(len(events), 1)
|
self.assertEqual(len(events), 1)
|
||||||
event = events.first()
|
event = events.first()
|
||||||
self.assertEqual(event.user["username"], self.user.username)
|
|
||||||
self.assertEqual(event.context["result"]["passing"], False)
|
self.assertEqual(event.context["result"]["passing"], False)
|
||||||
self.assertEqual(event.context["result"]["messages"], ["dummy"])
|
self.assertEqual(event.context["result"]["messages"], ["dummy"])
|
||||||
self.assertEqual(event.client_ip, "127.0.0.1")
|
|
||||||
|
|
||||||
def test_raises(self):
|
def test_raises(self):
|
||||||
"""Test policy that raises error"""
|
"""Test policy that raises error"""
|
||||||
policy_raises = ExpressionPolicy.objects.create(
|
policy_raises = ExpressionPolicy.objects.create(
|
||||||
name="raises", expression="{{ 0/0 }}"
|
name="raises", expression="{{ 0/0 }}"
|
||||||
)
|
)
|
||||||
binding = PolicyBinding(
|
binding = PolicyBinding(policy=policy_raises)
|
||||||
policy=policy_raises, target=Application.objects.create(name="test")
|
|
||||||
)
|
|
||||||
|
|
||||||
request = PolicyRequest(self.user)
|
request = PolicyRequest(self.user)
|
||||||
response = PolicyProcess(binding, request, None).execute()
|
response = PolicyProcess(binding, request, None).execute()
|
||||||
self.assertEqual(response.passing, False)
|
self.assertEqual(response.passing, False)
|
||||||
self.assertEqual(response.messages, ("division by zero",))
|
self.assertEqual(response.messages, ("division by zero",))
|
||||||
|
# self.assert
|
||||||
events = Event.objects.filter(
|
|
||||||
action=EventAction.POLICY_EXCEPTION,
|
|
||||||
context__policy_uuid=policy_raises.policy_uuid.hex,
|
|
||||||
)
|
|
||||||
self.assertTrue(events.exists())
|
|
||||||
self.assertEqual(len(events), 1)
|
|
||||||
event = events.first()
|
|
||||||
self.assertEqual(event.user["username"], self.user.username)
|
|
||||||
self.assertIn("division by zero", event.context["message"])
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Any, Optional
|
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
@ -19,7 +19,7 @@ class PolicyRequest:
|
|||||||
user: User
|
user: User
|
||||||
http_request: Optional[HttpRequest]
|
http_request: Optional[HttpRequest]
|
||||||
obj: Optional[Model]
|
obj: Optional[Model]
|
||||||
context: dict[str, Any]
|
context: Dict[str, str]
|
||||||
|
|
||||||
def __init__(self, user: User):
|
def __init__(self, user: User):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -37,10 +37,10 @@ class PolicyResult:
|
|||||||
"""Small data-class to hold policy results"""
|
"""Small data-class to hold policy results"""
|
||||||
|
|
||||||
passing: bool
|
passing: bool
|
||||||
messages: tuple[str, ...]
|
messages: Tuple[str, ...]
|
||||||
|
|
||||||
source_policy: Optional[Policy]
|
source_policy: Optional[Policy]
|
||||||
source_results: Optional[list["PolicyResult"]]
|
source_results: Optional[List["PolicyResult"]]
|
||||||
|
|
||||||
def __init__(self, passing: bool, *messages: str):
|
def __init__(self, passing: bool, *messages: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -7,7 +7,7 @@ from django.contrib.auth.views import redirect_to_login
|
|||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic.base import View
|
from django.views.generic.base import View
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.models import Application, Provider, User
|
from authentik.core.models import Application, Provider, User
|
||||||
from authentik.flows.views import SESSION_KEY_APPLICATION_PRE
|
from authentik.flows.views import SESSION_KEY_APPLICATION_PRE
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
"""OAuth errors"""
|
"""OAuth errors"""
|
||||||
from typing import Optional
|
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from authentik.events.models import Event, EventAction
|
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
from authentik.providers.oauth2.models import GrantTypes
|
from authentik.providers.oauth2.models import GrantTypes
|
||||||
|
|
||||||
@ -23,13 +21,6 @@ class OAuth2Error(SentryIgnoredException):
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return self.error
|
return self.error
|
||||||
|
|
||||||
def to_event(self, message: Optional[str] = None) -> Event:
|
|
||||||
"""Create configuration_error Event and save it."""
|
|
||||||
return Event.new(
|
|
||||||
EventAction.CONFIGURATION_ERROR,
|
|
||||||
message=message or self.description,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RedirectUriError(OAuth2Error):
|
class RedirectUriError(OAuth2Error):
|
||||||
"""The request fails due to a missing, invalid, or mismatching
|
"""The request fails due to a missing, invalid, or mismatching
|
||||||
@ -37,24 +28,10 @@ class RedirectUriError(OAuth2Error):
|
|||||||
|
|
||||||
error = "Redirect URI Error"
|
error = "Redirect URI Error"
|
||||||
description = (
|
description = (
|
||||||
"The request fails due to a missing, invalid, or mismatching "
|
"The request fails due to a missing, invalid, or mismatching"
|
||||||
"redirection URI (redirect_uri)."
|
" redirection URI (redirect_uri)."
|
||||||
)
|
)
|
||||||
|
|
||||||
provided_uri: str
|
|
||||||
allowed_uris: list[str]
|
|
||||||
|
|
||||||
def __init__(self, provided_uri: str, allowed_uris: list[str]) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.provided_uri = provided_uri
|
|
||||||
self.allowed_uris = allowed_uris
|
|
||||||
|
|
||||||
def to_event(self) -> Event:
|
|
||||||
return super().to_event(
|
|
||||||
f"Invalid redirect URI was used. Client used '{self.provided_uri}'. "
|
|
||||||
f"Allowed redirect URIs are {','.join(self.allowed_uris)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ClientIdError(OAuth2Error):
|
class ClientIdError(OAuth2Error):
|
||||||
"""The client identifier (client_id) is missing or invalid."""
|
"""The client identifier (client_id) is missing or invalid."""
|
||||||
@ -62,15 +39,6 @@ class ClientIdError(OAuth2Error):
|
|||||||
error = "Client ID Error"
|
error = "Client ID Error"
|
||||||
description = "The client identifier (client_id) is missing or invalid."
|
description = "The client identifier (client_id) is missing or invalid."
|
||||||
|
|
||||||
client_id: str
|
|
||||||
|
|
||||||
def __init__(self, client_id: str) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.client_id = client_id
|
|
||||||
|
|
||||||
def to_event(self) -> Event:
|
|
||||||
return super().to_event(f"Invalid client identifier: {self.client_id}.")
|
|
||||||
|
|
||||||
|
|
||||||
class UserAuthError(OAuth2Error):
|
class UserAuthError(OAuth2Error):
|
||||||
"""
|
"""
|
||||||
|
@ -6,7 +6,7 @@ from typing import List, Optional, Tuple
|
|||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||||
from django.utils.cache import patch_vary_headers
|
from django.utils.cache import patch_vary_headers
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.providers.oauth2.errors import BearerTokenError
|
from authentik.providers.oauth2.errors import BearerTokenError
|
||||||
from authentik.providers.oauth2.models import RefreshToken
|
from authentik.providers.oauth2.models import RefreshToken
|
||||||
|
@ -9,7 +9,7 @@ from django.http import HttpRequest, HttpResponse
|
|||||||
from django.http.response import Http404
|
from django.http.response import Http404
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
@ -145,7 +145,7 @@ class OAuthAuthorizationParams:
|
|||||||
)
|
)
|
||||||
except OAuth2Provider.DoesNotExist:
|
except OAuth2Provider.DoesNotExist:
|
||||||
LOGGER.warning("Invalid client identifier", client_id=self.client_id)
|
LOGGER.warning("Invalid client identifier", client_id=self.client_id)
|
||||||
raise ClientIdError(client_id=self.client_id)
|
raise ClientIdError()
|
||||||
self.check_redirect_uri()
|
self.check_redirect_uri()
|
||||||
self.check_scope()
|
self.check_scope()
|
||||||
self.check_nonce()
|
self.check_nonce()
|
||||||
@ -155,18 +155,24 @@ class OAuthAuthorizationParams:
|
|||||||
"""Redirect URI validation."""
|
"""Redirect URI validation."""
|
||||||
if not self.redirect_uri:
|
if not self.redirect_uri:
|
||||||
LOGGER.warning("Missing redirect uri.")
|
LOGGER.warning("Missing redirect uri.")
|
||||||
raise RedirectUriError("", self.provider.redirect_uris.split())
|
raise RedirectUriError()
|
||||||
if self.redirect_uri.lower() not in [
|
if self.redirect_uri.lower() not in [
|
||||||
x.lower() for x in self.provider.redirect_uris.split()
|
x.lower() for x in self.provider.redirect_uris.split()
|
||||||
]:
|
]:
|
||||||
|
Event.new(
|
||||||
|
EventAction.CONFIGURATION_ERROR,
|
||||||
|
provider=self.provider,
|
||||||
|
message="Invalid redirect URI was used.",
|
||||||
|
client_used=self.redirect_uri,
|
||||||
|
configured=self.provider.redirect_uris.split(),
|
||||||
|
).save()
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"Invalid redirect uri",
|
"Invalid redirect uri",
|
||||||
redirect_uri=self.redirect_uri,
|
redirect_uri=self.redirect_uri,
|
||||||
excepted=self.provider.redirect_uris.split(),
|
excepted=self.provider.redirect_uris.split(),
|
||||||
)
|
)
|
||||||
raise RedirectUriError(
|
raise RedirectUriError()
|
||||||
self.redirect_uri, self.provider.redirect_uris.split()
|
|
||||||
)
|
|
||||||
if self.request:
|
if self.request:
|
||||||
raise AuthorizeError(
|
raise AuthorizeError(
|
||||||
self.redirect_uri, "request_not_supported", self.grant_type, self.state
|
self.redirect_uri, "request_not_supported", self.grant_type, self.state
|
||||||
@ -256,12 +262,10 @@ class OAuthFulfillmentStage(StageView):
|
|||||||
).from_http(self.request)
|
).from_http(self.request)
|
||||||
return redirect(self.create_response_uri())
|
return redirect(self.create_response_uri())
|
||||||
except (ClientIdError, RedirectUriError) as error:
|
except (ClientIdError, RedirectUriError) as error:
|
||||||
error.to_event().from_http(request)
|
|
||||||
self.executor.stage_invalid()
|
self.executor.stage_invalid()
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
return bad_request_message(request, error.description, title=error.error)
|
return bad_request_message(request, error.description, title=error.error)
|
||||||
except AuthorizeError as error:
|
except AuthorizeError as error:
|
||||||
error.to_event().from_http(request)
|
|
||||||
self.executor.stage_invalid()
|
self.executor.stage_invalid()
|
||||||
return redirect(error.create_uri())
|
return redirect(error.create_uri())
|
||||||
|
|
||||||
@ -379,10 +383,8 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
|||||||
try:
|
try:
|
||||||
self.params = OAuthAuthorizationParams.from_request(self.request)
|
self.params = OAuthAuthorizationParams.from_request(self.request)
|
||||||
except AuthorizeError as error:
|
except AuthorizeError as error:
|
||||||
error.to_event().from_http(self.request)
|
|
||||||
raise RequestValidationError(redirect(error.create_uri()))
|
raise RequestValidationError(redirect(error.create_uri()))
|
||||||
except OAuth2Error as error:
|
except OAuth2Error as error:
|
||||||
error.to_event().from_http(self.request)
|
|
||||||
raise RequestValidationError(
|
raise RequestValidationError(
|
||||||
bad_request_message(self.request, error.description, title=error.error)
|
bad_request_message(self.request, error.description, title=error.error)
|
||||||
)
|
)
|
||||||
@ -396,7 +398,6 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
|||||||
self.params.grant_type,
|
self.params.grant_type,
|
||||||
self.params.state,
|
self.params.state,
|
||||||
)
|
)
|
||||||
error.to_event().from_http(self.request)
|
|
||||||
raise RequestValidationError(redirect(error.create_uri()))
|
raise RequestValidationError(redirect(error.create_uri()))
|
||||||
|
|
||||||
def resolve_provider_application(self):
|
def resolve_provider_application(self):
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user