Compare commits
47 Commits
version/0.
...
version/0.
Author | SHA1 | Date | |
---|---|---|---|
c9f0d048a8 | |||
90a94b5e3e | |||
ae1a8842db | |||
a3b17d1ed4 | |||
41576e27be | |||
07082cb3aa | |||
426cb33fab | |||
9e4f840d2d | |||
e120d274e9 | |||
977d3f6ef9 | |||
ecdbc917a5 | |||
0083cd55df | |||
d380194e13 | |||
32f5d5ba72 | |||
e818416863 | |||
7eed70cfe9 | |||
ea6ca23f57 | |||
f056b026d6 | |||
1c0a6efeb1 | |||
17732eea08 | |||
aa5381fd59 | |||
ffee86fcf3 | |||
7ff7398aff | |||
67925a39f2 | |||
3b5e1c7b34 | |||
3e49acf7ae | |||
76764c4374 | |||
9f6f8e1b55 | |||
9590180c6c | |||
aef5c60a7b | |||
d4c9c667c9 | |||
96f0d582f0 | |||
7e8702a71e | |||
1524061480 | |||
434922f702 | |||
d2862ddc93 | |||
6e55431d4c | |||
01548c5e9c | |||
bf1dae2dbe | |||
59c93defcf | |||
a2a1a27502 | |||
e3227e7d54 | |||
1f4a8fffdb | |||
86b1183883 | |||
f781f4848c | |||
19824d693c | |||
0694b911a4 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.13.0-rc1
|
current_version = 0.13.0-rc4
|
||||||
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>.*)
|
||||||
@ -23,6 +23,8 @@ values =
|
|||||||
|
|
||||||
[bumpversion:file:helm/values.yaml]
|
[bumpversion:file:helm/values.yaml]
|
||||||
|
|
||||||
|
[bumpversion:file:helm/README.md]
|
||||||
|
|
||||||
[bumpversion:file:helm/Chart.yaml]
|
[bumpversion:file:helm/Chart.yaml]
|
||||||
|
|
||||||
[bumpversion:file:.github/workflows/release.yml]
|
[bumpversion:file:.github/workflows/release.yml]
|
||||||
|
15
.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:0.13.0-rc1
|
-t beryju/authentik:0.13.0-rc4
|
||||||
-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:0.13.0-rc1
|
run: docker push beryju/authentik:0.13.0-rc4
|
||||||
- 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:0.13.0-rc1 \
|
-t beryju/authentik-proxy:0.13.0-rc4 \
|
||||||
-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:0.13.0-rc1
|
run: docker push beryju/authentik-proxy:0.13.0-rc4
|
||||||
- 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,17 +69,18 @@ jobs:
|
|||||||
cd web/
|
cd web/
|
||||||
docker build \
|
docker build \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
-t beryju/authentik-static:0.13.0-rc1 \
|
-t beryju/authentik-static:0.13.0-rc4 \
|
||||||
-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:0.13.0-rc1
|
run: docker push beryju/authentik-static:0.13.0-rc4
|
||||||
- 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:
|
||||||
needs:
|
needs:
|
||||||
- build-server
|
- build-server
|
||||||
- build-static
|
- build-static
|
||||||
|
- build-proxy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
@ -106,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: 0.13.0-rc1
|
tagName: 0.13.0-rc4
|
||||||
environment: beryjuorg-prod
|
environment: beryjuorg-prod
|
||||||
|
@ -38,6 +38,7 @@ RUN apt-get update && \
|
|||||||
|
|
||||||
COPY ./authentik/ /authentik
|
COPY ./authentik/ /authentik
|
||||||
COPY ./pytest.ini /
|
COPY ./pytest.ini /
|
||||||
|
COPY ./xml /xml
|
||||||
COPY ./manage.py /
|
COPY ./manage.py /
|
||||||
COPY ./lifecycle/ /lifecycle
|
COPY ./lifecycle/ /lifecycle
|
||||||
|
|
||||||
|
5
Makefile
@ -1,5 +1,10 @@
|
|||||||
all: lint-fix lint coverage gen
|
all: lint-fix lint coverage gen
|
||||||
|
|
||||||
|
test-full:
|
||||||
|
coverage run manage.py test --failfast -v 3 .
|
||||||
|
coverage html
|
||||||
|
coverage report
|
||||||
|
|
||||||
test-integration:
|
test-integration:
|
||||||
k3d cluster create || exit 0
|
k3d cluster create || exit 0
|
||||||
k3d kubeconfig write -o ~/.kube/config --overwrite
|
k3d kubeconfig write -o ~/.kube/config --overwrite
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<img src="icons/icon_top_brand.svg" height="250" alt="authentik logo">
|
<img src="web/icons/icon_top_brand.svg" height="250" alt="authentik logo">
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -6,9 +6,9 @@ As authentik is currently in a pre-stable, only the latest "stable" version is s
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| -------- | ------------------ |
|
| -------- | ------------------ |
|
||||||
| 0.10.x | :white_check_mark: |
|
|
||||||
| 0.11.x | :white_check_mark: |
|
| 0.11.x | :white_check_mark: |
|
||||||
| 0.12.x | :white_check_mark: |
|
| 0.12.x | :white_check_mark: |
|
||||||
|
| 0.13.x | :white_check_mark: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
"""authentik"""
|
"""authentik"""
|
||||||
__version__ = "0.13.0-rc1"
|
__version__ = "0.13.0-rc4"
|
||||||
|
37
authentik/admin/tests/test_api.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
"""test admin api"""
|
||||||
|
from json import loads
|
||||||
|
|
||||||
|
from django.shortcuts import reverse
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from authentik import __version__
|
||||||
|
from authentik.core.models import Group, User
|
||||||
|
|
||||||
|
|
||||||
|
class TestAdminAPI(TestCase):
|
||||||
|
"""test admin api"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
|
self.user = User.objects.create(username="test-user")
|
||||||
|
self.group = Group.objects.create(name="superusers", is_superuser=True)
|
||||||
|
self.group.users.add(self.user)
|
||||||
|
self.group.save()
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def test_overview(self):
|
||||||
|
"""Test Overview API"""
|
||||||
|
response = self.client.get(reverse("authentik_api:admin_overview-list"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
body = loads(response.content)
|
||||||
|
self.assertEqual(body["version"], __version__)
|
||||||
|
|
||||||
|
def test_metrics(self):
|
||||||
|
"""Test metrics API"""
|
||||||
|
response = self.client.get(reverse("authentik_api:admin_metrics-list"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_tasks(self):
|
||||||
|
"""Test tasks metrics API"""
|
||||||
|
response = self.client.get(reverse("authentik_api:admin_system_tasks-list"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
@ -1,9 +1,13 @@
|
|||||||
"""admin tests"""
|
"""admin tests"""
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django import forms
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
from authentik.admin.views.policies_bindings import PolicyBindingCreateView
|
from authentik.admin.views.policies_bindings import PolicyBindingCreateView
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
|
from authentik.policies.forms import PolicyBindingForm
|
||||||
|
|
||||||
|
|
||||||
class TestPolicyBindingView(TestCase):
|
class TestPolicyBindingView(TestCase):
|
||||||
@ -18,9 +22,22 @@ class TestPolicyBindingView(TestCase):
|
|||||||
view = PolicyBindingCreateView(request=request)
|
view = PolicyBindingCreateView(request=request)
|
||||||
self.assertEqual(view.get_initial(), {})
|
self.assertEqual(view.get_initial(), {})
|
||||||
|
|
||||||
def test_with_param(self):
|
def test_with_params_invalid(self):
|
||||||
|
"""Test PolicyBindingCreateView with invalid get params"""
|
||||||
|
request = self.factory.get("/", {"target": uuid4()})
|
||||||
|
view = PolicyBindingCreateView(request=request)
|
||||||
|
self.assertEqual(view.get_initial(), {})
|
||||||
|
|
||||||
|
def test_with_params(self):
|
||||||
"""Test PolicyBindingCreateView with get params"""
|
"""Test PolicyBindingCreateView with get params"""
|
||||||
target = Application.objects.create(name="test")
|
target = Application.objects.create(name="test")
|
||||||
request = self.factory.get("/", {"target": target.pk.hex})
|
request = self.factory.get("/", {"target": target.pk.hex})
|
||||||
view = PolicyBindingCreateView(request=request)
|
view = PolicyBindingCreateView(request=request)
|
||||||
self.assertEqual(view.get_initial(), {"target": target, "order": 0})
|
self.assertEqual(view.get_initial(), {"target": target, "order": 0})
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
isinstance(
|
||||||
|
PolicyBindingForm(initial={"target": "foo"}).fields["target"].widget,
|
||||||
|
forms.HiddenInput,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
"""admin tests"""
|
"""admin tests"""
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django import forms
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
from authentik.admin.views.stages_bindings import StageBindingCreateView
|
from authentik.admin.views.stages_bindings import StageBindingCreateView
|
||||||
|
from authentik.flows.forms import FlowStageBindingForm
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
|
|
||||||
|
|
||||||
@ -18,9 +22,22 @@ class TestStageBindingView(TestCase):
|
|||||||
view = StageBindingCreateView(request=request)
|
view = StageBindingCreateView(request=request)
|
||||||
self.assertEqual(view.get_initial(), {})
|
self.assertEqual(view.get_initial(), {})
|
||||||
|
|
||||||
def test_with_param(self):
|
def test_with_params_invalid(self):
|
||||||
|
"""Test StageBindingCreateView with invalid get params"""
|
||||||
|
request = self.factory.get("/", {"target": uuid4()})
|
||||||
|
view = StageBindingCreateView(request=request)
|
||||||
|
self.assertEqual(view.get_initial(), {})
|
||||||
|
|
||||||
|
def test_with_params(self):
|
||||||
"""Test StageBindingCreateView with get params"""
|
"""Test StageBindingCreateView with get params"""
|
||||||
target = Flow.objects.create(name="test", slug="test")
|
target = Flow.objects.create(name="test", slug="test")
|
||||||
request = self.factory.get("/", {"target": target.pk.hex})
|
request = self.factory.get("/", {"target": target.pk.hex})
|
||||||
view = StageBindingCreateView(request=request)
|
view = StageBindingCreateView(request=request)
|
||||||
self.assertEqual(view.get_initial(), {"target": target, "order": 0})
|
self.assertEqual(view.get_initial(), {"target": target, "order": 0})
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
isinstance(
|
||||||
|
FlowStageBindingForm(initial={"target": "foo"}).fields["target"].widget,
|
||||||
|
forms.HiddenInput,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""API Authentication"""
|
"""API Authentication"""
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
|
from binascii import Error
|
||||||
from typing import Any, Optional, Tuple, Union
|
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
|
||||||
@ -24,7 +25,7 @@ def token_from_header(raw_header: bytes) -> Optional[Token]:
|
|||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
auth_credentials = b64decode(auth_credentials.encode()).decode()
|
auth_credentials = b64decode(auth_credentials.encode()).decode()
|
||||||
except UnicodeDecodeError:
|
except (UnicodeDecodeError, Error):
|
||||||
return None
|
return None
|
||||||
# Accept credentials with username and without
|
# Accept credentials with username and without
|
||||||
if ":" in auth_credentials:
|
if ":" in auth_credentials:
|
||||||
|
37
authentik/api/tests.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
"""Test API Authentication"""
|
||||||
|
from base64 import b64encode
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from guardian.shortcuts import get_anonymous_user
|
||||||
|
|
||||||
|
from authentik.api.auth import token_from_header
|
||||||
|
from authentik.core.models import Token, TokenIntents
|
||||||
|
|
||||||
|
|
||||||
|
class TestAPIAuth(TestCase):
|
||||||
|
"""Test API Authentication"""
|
||||||
|
|
||||||
|
def test_valid(self):
|
||||||
|
"""Test valid token"""
|
||||||
|
token = Token.objects.create(
|
||||||
|
intent=TokenIntents.INTENT_API, user=get_anonymous_user()
|
||||||
|
)
|
||||||
|
auth = b64encode(f":{token.key}".encode()).decode()
|
||||||
|
self.assertEqual(token_from_header(f"Basic {auth}".encode()), token)
|
||||||
|
|
||||||
|
def test_invalid_type(self):
|
||||||
|
"""Test invalid type"""
|
||||||
|
self.assertIsNone(token_from_header("foo bar".encode()))
|
||||||
|
|
||||||
|
def test_invalid_decode(self):
|
||||||
|
"""Test invalid bas64"""
|
||||||
|
self.assertIsNone(token_from_header("Basic bar".encode()))
|
||||||
|
|
||||||
|
def test_invalid_empty_password(self):
|
||||||
|
"""Test invalid with empty password"""
|
||||||
|
self.assertIsNone(token_from_header("Basic :".encode()))
|
||||||
|
|
||||||
|
def test_invalid_no_token(self):
|
||||||
|
"""Test invalid with no token"""
|
||||||
|
auth = b64encode(":abc".encode()).decode()
|
||||||
|
self.assertIsNone(token_from_header(f"Basic :{auth}".encode()))
|
@ -1,7 +1,10 @@
|
|||||||
"""Application API Views"""
|
"""Application API Views"""
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
|
from django.http.response import Http404
|
||||||
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
|
from rest_framework.generics import get_object_or_404
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
@ -71,8 +74,12 @@ class ApplicationViewSet(ModelViewSet):
|
|||||||
@action(detail=True)
|
@action(detail=True)
|
||||||
def metrics(self, request: Request, slug: str):
|
def metrics(self, request: Request, slug: str):
|
||||||
"""Metrics for application logins"""
|
"""Metrics for application logins"""
|
||||||
# TODO: Check app read and audit read perms
|
app = get_object_or_404(
|
||||||
app = Application.objects.get(slug=slug)
|
get_objects_for_user(request.user, "authentik_core.view_application"),
|
||||||
|
slug=slug,
|
||||||
|
)
|
||||||
|
if not request.user.has_perm("authentik_audit.view_event"):
|
||||||
|
raise Http404
|
||||||
return Response(
|
return Response(
|
||||||
get_events_per_1h(
|
get_events_per_1h(
|
||||||
action=EventAction.AUTHORIZE_APPLICATION,
|
action=EventAction.AUTHORIZE_APPLICATION,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""Channels base classes"""
|
"""Channels base classes"""
|
||||||
|
from channels.exceptions import DenyConnection
|
||||||
from channels.generic.websocket import JsonWebsocketConsumer
|
from channels.generic.websocket import JsonWebsocketConsumer
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
@ -17,16 +18,13 @@ class AuthJsonConsumer(JsonWebsocketConsumer):
|
|||||||
headers = dict(self.scope["headers"])
|
headers = dict(self.scope["headers"])
|
||||||
if b"authorization" not in headers:
|
if b"authorization" not in headers:
|
||||||
LOGGER.warning("WS Request without authorization header")
|
LOGGER.warning("WS Request without authorization header")
|
||||||
self.close()
|
raise DenyConnection()
|
||||||
return False
|
|
||||||
|
|
||||||
raw_header = headers[b"authorization"]
|
raw_header = headers[b"authorization"]
|
||||||
|
|
||||||
token = token_from_header(raw_header)
|
token = token_from_header(raw_header)
|
||||||
if not token:
|
if not token:
|
||||||
LOGGER.warning("Failed to authenticate")
|
LOGGER.warning("Failed to authenticate")
|
||||||
self.close()
|
raise DenyConnection()
|
||||||
return False
|
|
||||||
|
|
||||||
self.user = token.user
|
self.user = token.user
|
||||||
return True
|
|
||||||
|
@ -6,8 +6,6 @@
|
|||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<link rel="preload" href="{% static 'dist/assets/fonts/DINEngschriftStd.woff2' %}" as="font" type="font/woff2" crossorigin>
|
|
||||||
<link rel="preload" href="{% static 'dist/assets/fonts/DINEngschriftStd.woff' %}" as="font" type="font/woff" crossorigin>
|
|
||||||
<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>
|
||||||
|
26
authentik/core/templates/user/details.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="pf-c-card">
|
||||||
|
<div class="pf-c-card__header pf-c-title pf-m-md">
|
||||||
|
{% trans 'Update details' %}
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-card__body">
|
||||||
|
<form action="" method="post" class="pf-c-form pf-m-horizontal">
|
||||||
|
{% include 'partials/form_horizontal.html' with form=form %}
|
||||||
|
{% block beneath_form %}
|
||||||
|
{% endblock %}
|
||||||
|
<div class="pf-c-form__group pf-m-action">
|
||||||
|
<div class="pf-c-form__horizontal-group">
|
||||||
|
<div class="pf-c-form__actions">
|
||||||
|
<input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" />
|
||||||
|
{% if unenrollment_enabled %}
|
||||||
|
<a class="pf-c-button pf-m-danger"
|
||||||
|
href="{% url 'authentik_flows:default-unenrollment' %}?back={{ request.get_full_path }}">{%
|
||||||
|
trans "Delete account" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -15,29 +15,9 @@
|
|||||||
<section class="pf-c-page__main-section">
|
<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">
|
||||||
<div class="pf-c-card">
|
<ak-site-shell url="{% url 'authentik_core:user-details' %}">
|
||||||
<div class="pf-c-card__header pf-c-title pf-m-md">
|
<div slot="body"></div>
|
||||||
{% trans 'Update details' %}
|
</ak-site-shell>
|
||||||
</div>
|
|
||||||
<div class="pf-c-card__body">
|
|
||||||
<form action="" method="post" class="pf-c-form pf-m-horizontal">
|
|
||||||
{% include 'partials/form_horizontal.html' with form=form %}
|
|
||||||
{% block beneath_form %}
|
|
||||||
{% endblock %}
|
|
||||||
<div class="pf-c-form__group pf-m-action">
|
|
||||||
<div class="pf-c-form__horizontal-group">
|
|
||||||
<div class="pf-c-form__actions">
|
|
||||||
<input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" />
|
|
||||||
{% if unenrollment_enabled %}
|
|
||||||
<a class="pf-c-button pf-m-danger"
|
|
||||||
href="{% url 'authentik_flows:default-unenrollment' %}?back={{ request.get_full_path }}">{% trans "Delete account" %}</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -34,9 +34,3 @@ class TestOverviewViews(TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.client.get(reverse("authentik_core:overview")).status_code, 200
|
self.client.get(reverse("authentik_core:overview")).status_code, 200
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_user_settings(self):
|
|
||||||
"""Test user settings"""
|
|
||||||
self.assertEqual(
|
|
||||||
self.client.get(reverse("authentik_core:user-settings")).status_code, 200
|
|
||||||
)
|
|
||||||
|
@ -28,3 +28,9 @@ class TestUserViews(TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.client.get(reverse("authentik_core:user-settings")).status_code, 200
|
self.client.get(reverse("authentik_core:user-settings")).status_code, 200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_user_details(self):
|
||||||
|
"""Test UserDetailsView"""
|
||||||
|
self.assertEqual(
|
||||||
|
self.client.get(reverse("authentik_core:user-details")).status_code, 200
|
||||||
|
)
|
||||||
|
@ -7,6 +7,7 @@ urlpatterns = [
|
|||||||
path("", shell.ShellView.as_view(), name="shell"),
|
path("", shell.ShellView.as_view(), name="shell"),
|
||||||
# User views
|
# User views
|
||||||
path("-/user/", user.UserSettingsView.as_view(), name="user-settings"),
|
path("-/user/", user.UserSettingsView.as_view(), name="user-settings"),
|
||||||
|
path("-/user/details/", user.UserDetailsView.as_view(), name="user-details"),
|
||||||
path("-/user/tokens/", user.TokenListView.as_view(), name="user-tokens"),
|
path("-/user/tokens/", user.TokenListView.as_view(), name="user-tokens"),
|
||||||
path(
|
path(
|
||||||
"-/user/tokens/create/",
|
"-/user/tokens/create/",
|
||||||
|
@ -11,6 +11,7 @@ from django.http.response import HttpResponse
|
|||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
|
from django.views.generic.base import TemplateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from guardian.shortcuts import get_objects_for_user
|
||||||
|
|
||||||
@ -26,14 +27,20 @@ from authentik.flows.models import Flow, FlowDesignation
|
|||||||
from authentik.lib.views import CreateAssignPermView
|
from authentik.lib.views import CreateAssignPermView
|
||||||
|
|
||||||
|
|
||||||
class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
|
class UserSettingsView(TemplateView):
|
||||||
"""Update User settings"""
|
"""Multiple SiteShells for user details and all stages"""
|
||||||
|
|
||||||
template_name = "user/settings.html"
|
template_name = "user/settings.html"
|
||||||
|
|
||||||
|
|
||||||
|
class UserDetailsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
|
||||||
|
"""Update User details"""
|
||||||
|
|
||||||
|
template_name = "user/details.html"
|
||||||
form_class = UserDetailForm
|
form_class = UserDetailForm
|
||||||
|
|
||||||
success_message = _("Successfully updated user.")
|
success_message = _("Successfully updated user.")
|
||||||
success_url = reverse_lazy("authentik_core:user-settings")
|
success_url = reverse_lazy("authentik_core:user-details")
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
return self.request.user
|
return self.request.user
|
||||||
|
@ -22,16 +22,15 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
|||||||
def validate_key_data(self, value):
|
def validate_key_data(self, value):
|
||||||
"""Verify that input is a valid PEM RSA Key"""
|
"""Verify that input is a valid PEM RSA Key"""
|
||||||
# Since this field is optional, data can be empty.
|
# Since this field is optional, data can be empty.
|
||||||
if value == "":
|
if value != "":
|
||||||
return value
|
try:
|
||||||
try:
|
load_pem_private_key(
|
||||||
load_pem_private_key(
|
str.encode("\n".join([x.strip() for x in value.split("\n")])),
|
||||||
str.encode("\n".join([x.strip() for x in value.split("\n")])),
|
password=None,
|
||||||
password=None,
|
backend=default_backend(),
|
||||||
backend=default_backend(),
|
)
|
||||||
)
|
except ValueError:
|
||||||
except ValueError:
|
raise ValidationError("Unable to load private key.")
|
||||||
raise ValidationError("Unable to load private key.")
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -26,16 +26,15 @@ class CertificateKeyPairForm(forms.ModelForm):
|
|||||||
"""Verify that input is a valid PEM RSA Key"""
|
"""Verify that input is a valid PEM RSA Key"""
|
||||||
key_data = self.cleaned_data["key_data"]
|
key_data = self.cleaned_data["key_data"]
|
||||||
# Since this field is optional, data can be empty.
|
# Since this field is optional, data can be empty.
|
||||||
if key_data == "":
|
if key_data != "":
|
||||||
return key_data
|
try:
|
||||||
try:
|
load_pem_private_key(
|
||||||
load_pem_private_key(
|
str.encode("\n".join([x.strip() for x in key_data.split("\n")])),
|
||||||
str.encode("\n".join([x.strip() for x in key_data.split("\n")])),
|
password=None,
|
||||||
password=None,
|
backend=default_backend(),
|
||||||
backend=default_backend(),
|
)
|
||||||
)
|
except ValueError:
|
||||||
except ValueError:
|
raise forms.ValidationError("Unable to load private key.")
|
||||||
raise forms.ValidationError("Unable to load private key.")
|
|
||||||
return key_data
|
return key_data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -3,14 +3,17 @@ from unittest.mock import MagicMock, PropertyMock, patch
|
|||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from django.test import Client, TestCase
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
|
||||||
|
from authentik.core.models import User
|
||||||
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||||
from authentik.flows.markers import ReevaluateMarker, StageMarker
|
from authentik.flows.markers import ReevaluateMarker, StageMarker
|
||||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from authentik.flows.planner import FlowPlan
|
from authentik.flows.planner import FlowPlan, FlowPlanner
|
||||||
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN
|
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView
|
||||||
|
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
from authentik.policies.http import AccessDeniedResponse
|
from authentik.policies.http import AccessDeniedResponse
|
||||||
@ -35,7 +38,7 @@ class TestFlowExecutor(TestCase):
|
|||||||
"""Test views logic"""
|
"""Test views logic"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = Client()
|
self.request_factory = RequestFactory()
|
||||||
|
|
||||||
def test_existing_plan_diff_flow(self):
|
def test_existing_plan_diff_flow(self):
|
||||||
"""Check that a plan for a different flow cancels the current plan"""
|
"""Check that a plan for a different flow cancels the current plan"""
|
||||||
@ -276,6 +279,83 @@ class TestFlowExecutor(TestCase):
|
|||||||
{"type": "redirect", "to": reverse("authentik_core:shell")},
|
{"type": "redirect", "to": reverse("authentik_core:shell")},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_reevaluate_keep(self):
|
||||||
|
"""Test planner with re-evaluate (everything is kept)"""
|
||||||
|
flow = Flow.objects.create(
|
||||||
|
name="test-default-context",
|
||||||
|
slug="test-default-context",
|
||||||
|
designation=FlowDesignation.AUTHENTICATION,
|
||||||
|
)
|
||||||
|
true_policy = DummyPolicy.objects.create(result=True, wait_min=1, wait_max=2)
|
||||||
|
|
||||||
|
binding = FlowStageBinding.objects.create(
|
||||||
|
target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||||
|
)
|
||||||
|
binding2 = FlowStageBinding.objects.create(
|
||||||
|
target=flow,
|
||||||
|
stage=DummyStage.objects.create(name="dummy2"),
|
||||||
|
order=1,
|
||||||
|
re_evaluate_policies=True,
|
||||||
|
)
|
||||||
|
binding3 = FlowStageBinding.objects.create(
|
||||||
|
target=flow, stage=DummyStage.objects.create(name="dummy3"), order=2
|
||||||
|
)
|
||||||
|
|
||||||
|
PolicyBinding.objects.create(policy=true_policy, target=binding2, order=0)
|
||||||
|
|
||||||
|
# Here we patch the dummy policy to evaluate to true so the stage is included
|
||||||
|
with patch(
|
||||||
|
"authentik.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE
|
||||||
|
):
|
||||||
|
|
||||||
|
exec_url = reverse(
|
||||||
|
"authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}
|
||||||
|
)
|
||||||
|
# First request, run the planner
|
||||||
|
response = self.client.get(exec_url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||||
|
|
||||||
|
self.assertEqual(plan.stages[0], binding.stage)
|
||||||
|
self.assertEqual(plan.stages[1], binding2.stage)
|
||||||
|
self.assertEqual(plan.stages[2], binding3.stage)
|
||||||
|
|
||||||
|
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||||
|
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
|
||||||
|
self.assertIsInstance(plan.markers[2], StageMarker)
|
||||||
|
|
||||||
|
# Second request, this passes the first dummy stage
|
||||||
|
response = self.client.post(exec_url)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||||
|
|
||||||
|
self.assertEqual(plan.stages[0], binding2.stage)
|
||||||
|
self.assertEqual(plan.stages[1], binding3.stage)
|
||||||
|
|
||||||
|
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||||
|
self.assertIsInstance(plan.markers[1], StageMarker)
|
||||||
|
|
||||||
|
# Third request, this passes the first dummy stage
|
||||||
|
response = self.client.post(exec_url)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||||
|
|
||||||
|
self.assertEqual(plan.stages[0], binding3.stage)
|
||||||
|
|
||||||
|
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||||
|
|
||||||
|
# third request, this should trigger the re-evaluate
|
||||||
|
# We do this request without the patch, so the policy results in false
|
||||||
|
response = self.client.post(exec_url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
force_str(response.content),
|
||||||
|
{"type": "redirect", "to": reverse("authentik_core:shell")},
|
||||||
|
)
|
||||||
|
|
||||||
def test_reevaluate_remove_consecutive(self):
|
def test_reevaluate_remove_consecutive(self):
|
||||||
"""Test planner with re-evaluate (consecutive stages are removed)"""
|
"""Test planner with re-evaluate (consecutive stages are removed)"""
|
||||||
flow = Flow.objects.create(
|
flow = Flow.objects.create(
|
||||||
@ -351,3 +431,33 @@ class TestFlowExecutor(TestCase):
|
|||||||
force_str(response.content),
|
force_str(response.content),
|
||||||
{"type": "redirect", "to": reverse("authentik_core:shell")},
|
{"type": "redirect", "to": reverse("authentik_core:shell")},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_stageview_user_identifier(self):
|
||||||
|
"""Test PLAN_CONTEXT_PENDING_USER_IDENTIFIER"""
|
||||||
|
flow = Flow.objects.create(
|
||||||
|
name="test-default-context",
|
||||||
|
slug="test-default-context",
|
||||||
|
designation=FlowDesignation.AUTHENTICATION,
|
||||||
|
)
|
||||||
|
FlowStageBinding.objects.create(
|
||||||
|
target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
|
||||||
|
)
|
||||||
|
|
||||||
|
ident = "test-identifier"
|
||||||
|
|
||||||
|
user = User.objects.create(username="test-user")
|
||||||
|
request = self.request_factory.get(
|
||||||
|
reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||||
|
)
|
||||||
|
request.user = user
|
||||||
|
planner = FlowPlanner(flow)
|
||||||
|
plan = planner.plan(
|
||||||
|
request, default_context={PLAN_CONTEXT_PENDING_USER_IDENTIFIER: ident}
|
||||||
|
)
|
||||||
|
|
||||||
|
executor = FlowExecutorView()
|
||||||
|
executor.plan = plan
|
||||||
|
executor.flow = flow
|
||||||
|
|
||||||
|
stage_view = StageView(executor)
|
||||||
|
self.assertEqual(ident, stage_view.get_context_data()["user"].username)
|
||||||
|
@ -61,7 +61,7 @@ class DataclassEncoder(JSONEncoder):
|
|||||||
return asdict(o)
|
return asdict(o)
|
||||||
if isinstance(o, UUID):
|
if isinstance(o, UUID):
|
||||||
return str(o)
|
return str(o)
|
||||||
return super().default(o)
|
return super().default(o) # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
class EntryInvalidError(SentryIgnoredException):
|
class EntryInvalidError(SentryIgnoredException):
|
||||||
|
@ -11,7 +11,7 @@ from authentik.flows.transfer.common import (
|
|||||||
FlowBundle,
|
FlowBundle,
|
||||||
FlowBundleEntry,
|
FlowBundleEntry,
|
||||||
)
|
)
|
||||||
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
|
from authentik.policies.models import Policy, PolicyBinding
|
||||||
from authentik.stages.prompt.models import PromptStage
|
from authentik.stages.prompt.models import PromptStage
|
||||||
|
|
||||||
|
|
||||||
@ -31,11 +31,6 @@ class FlowExporter:
|
|||||||
|
|
||||||
def _prepare_pbm(self):
|
def _prepare_pbm(self):
|
||||||
self.pbm_uuids = [self.flow.pbm_uuid]
|
self.pbm_uuids = [self.flow.pbm_uuid]
|
||||||
for stage_subclass in Stage.__subclasses__():
|
|
||||||
if issubclass(stage_subclass, PolicyBindingModel):
|
|
||||||
self.pbm_uuids += stage_subclass.objects.filter(
|
|
||||||
flow=self.flow
|
|
||||||
).values_list("pbm_uuid", flat=True)
|
|
||||||
self.pbm_uuids += FlowStageBinding.objects.filter(target=self.flow).values_list(
|
self.pbm_uuids += FlowStageBinding.objects.filter(target=self.flow).values_list(
|
||||||
"pbm_uuid", flat=True
|
"pbm_uuid", flat=True
|
||||||
)
|
)
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
"""authentik lib navbar Templatetag"""
|
|
||||||
from django import template
|
|
||||||
from django.http import HttpRequest
|
|
||||||
from structlog import get_logger
|
|
||||||
|
|
||||||
register = template.Library()
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
ACTIVE_STRING = "pf-m-current"
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
|
||||||
def is_active(context, *args: str, **_) -> str:
|
|
||||||
"""Return whether a navbar link is active or not."""
|
|
||||||
request: HttpRequest = context.get("request")
|
|
||||||
if not request.resolver_match:
|
|
||||||
return ""
|
|
||||||
match = request.resolver_match
|
|
||||||
for url in args:
|
|
||||||
if ":" in url:
|
|
||||||
app_name, url = url.split(":")
|
|
||||||
if match.app_name == app_name and match.url_name == url:
|
|
||||||
return ACTIVE_STRING
|
|
||||||
else:
|
|
||||||
if match.url_name == url:
|
|
||||||
return ACTIVE_STRING
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
|
||||||
def is_active_url(context, view: str) -> str:
|
|
||||||
"""Return whether a navbar link is active or not."""
|
|
||||||
request: HttpRequest = context.get("request")
|
|
||||||
if not request.resolver_match:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
match = request.resolver_match
|
|
||||||
current_full_url = f"{match.app_name}:{match.url_name}"
|
|
||||||
|
|
||||||
if current_full_url == view:
|
|
||||||
return ACTIVE_STRING
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
|
||||||
def is_active_app(context, *args: str) -> str:
|
|
||||||
"""Return True if current link is from app"""
|
|
||||||
|
|
||||||
request: HttpRequest = context.get("request")
|
|
||||||
if not request.resolver_match:
|
|
||||||
return ""
|
|
||||||
for app_name in args:
|
|
||||||
if request.resolver_match.app_name == app_name:
|
|
||||||
return ACTIVE_STRING
|
|
||||||
return ""
|
|
0
authentik/lib/tests/__init__.py
Normal file
18
authentik/lib/tests/test_sentry.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
"""test sentry integration"""
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from authentik.lib.sentry import SentryIgnoredException, before_send
|
||||||
|
|
||||||
|
|
||||||
|
class TestSentry(TestCase):
|
||||||
|
"""test sentry integration"""
|
||||||
|
|
||||||
|
def test_error_not_sent(self):
|
||||||
|
"""Test SentryIgnoredError not sent"""
|
||||||
|
self.assertIsNone(
|
||||||
|
before_send(None, {"exc_info": (0, SentryIgnoredException(), 0)})
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_error_sent(self):
|
||||||
|
"""Test error sent"""
|
||||||
|
self.assertIsNone(before_send(None, {"exc_info": (0, ValueError(), 0)}))
|
@ -20,6 +20,8 @@ class TestTimeUtils(TestCase):
|
|||||||
"""Test invalid expression"""
|
"""Test invalid expression"""
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
timedelta_from_string("foo")
|
timedelta_from_string("foo")
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
timedelta_from_string("bar=baz")
|
||||||
|
|
||||||
def test_validation(self):
|
def test_validation(self):
|
||||||
"""Test Django model field validator"""
|
"""Test Django model field validator"""
|
@ -35,4 +35,6 @@ def timedelta_from_string(expr: str) -> datetime.timedelta:
|
|||||||
if key.lower() not in ALLOWED_KEYS:
|
if key.lower() not in ALLOWED_KEYS:
|
||||||
continue
|
continue
|
||||||
kwargs[key.lower()] = float(value)
|
kwargs[key.lower()] = float(value)
|
||||||
|
if len(kwargs) < 1:
|
||||||
|
raise ValueError("No valid keys to pass to timedelta")
|
||||||
return datetime.timedelta(**kwargs)
|
return datetime.timedelta(**kwargs)
|
||||||
|
@ -22,7 +22,6 @@ class AuthentikOutpostConfig(AppConfig):
|
|||||||
|
|
||||||
name = "authentik.outposts"
|
name = "authentik.outposts"
|
||||||
label = "authentik_outposts"
|
label = "authentik_outposts"
|
||||||
mountpoint = "outposts/"
|
|
||||||
verbose_name = "authentik Outpost"
|
verbose_name = "authentik Outpost"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
@ -2,8 +2,9 @@
|
|||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
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
|
||||||
@ -39,18 +40,16 @@ class WebsocketMessage:
|
|||||||
class OutpostConsumer(AuthJsonConsumer):
|
class OutpostConsumer(AuthJsonConsumer):
|
||||||
"""Handler for Outposts that connect over websockets for health checks and live updates"""
|
"""Handler for Outposts that connect over websockets for health checks and live updates"""
|
||||||
|
|
||||||
outpost: Outpost
|
outpost: Optional[Outpost] = None
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
if not super().connect():
|
super().connect()
|
||||||
return
|
|
||||||
uuid = self.scope["url_route"]["kwargs"]["pk"]
|
uuid = self.scope["url_route"]["kwargs"]["pk"]
|
||||||
outpost = get_objects_for_user(
|
outpost = get_objects_for_user(
|
||||||
self.user, "authentik_outposts.view_outpost"
|
self.user, "authentik_outposts.view_outpost"
|
||||||
).filter(pk=uuid)
|
).filter(pk=uuid)
|
||||||
if not outpost.exists():
|
if not outpost.exists():
|
||||||
self.close()
|
raise DenyConnection()
|
||||||
return
|
|
||||||
self.accept()
|
self.accept()
|
||||||
self.outpost = outpost.first()
|
self.outpost = outpost.first()
|
||||||
OutpostState(
|
OutpostState(
|
||||||
@ -60,7 +59,8 @@ class OutpostConsumer(AuthJsonConsumer):
|
|||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def disconnect(self, close_code):
|
def disconnect(self, close_code):
|
||||||
OutpostState.for_channel(self.outpost, self.channel_name).delete()
|
if self.outpost:
|
||||||
|
OutpostState.for_channel(self.outpost, self.channel_name).delete()
|
||||||
LOGGER.debug("removed channel from cache", channel_name=self.channel_name)
|
LOGGER.debug("removed channel from cache", channel_name=self.channel_name)
|
||||||
|
|
||||||
def receive_json(self, content: Data):
|
def receive_json(self, content: Data):
|
||||||
|
38
authentik/outposts/migrations/0014_auto_20201213_1407.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 3.1.4 on 2020-12-13 14:07
|
||||||
|
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
|
||||||
|
def update_config_prefix(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
alias = schema_editor.connection.alias
|
||||||
|
Outpost = apps.get_model("authentik_outposts", "Outpost")
|
||||||
|
|
||||||
|
for outpost in Outpost.objects.using(alias).all():
|
||||||
|
config = outpost._config
|
||||||
|
for key in list(config):
|
||||||
|
if "passbook" in key:
|
||||||
|
new_key = key.replace("passbook", "authentik")
|
||||||
|
config[new_key] = config[key]
|
||||||
|
del config[key]
|
||||||
|
outpost._config = config
|
||||||
|
outpost.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_outposts", "0013_auto_20201203_2009"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(update_config_prefix),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="dockerserviceconnection",
|
||||||
|
name="url",
|
||||||
|
field=models.TextField(
|
||||||
|
help_text="Can be in the format of 'unix://<path>' when connecting to a local docker daemon, or 'https://<hostname>:2376' when connecting to a remote system."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -140,7 +140,14 @@ class OutpostServiceConnection(models.Model):
|
|||||||
class DockerServiceConnection(OutpostServiceConnection):
|
class DockerServiceConnection(OutpostServiceConnection):
|
||||||
"""Service Connection to a Docker endpoint"""
|
"""Service Connection to a Docker endpoint"""
|
||||||
|
|
||||||
url = models.TextField()
|
url = models.TextField(
|
||||||
|
help_text=_(
|
||||||
|
(
|
||||||
|
"Can be in the format of 'unix://<path>' when connecting to a local docker daemon, "
|
||||||
|
"or 'https://<hostname>:2376' when connecting to a remote system."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
tls_verification = models.ForeignKey(
|
tls_verification = models.ForeignKey(
|
||||||
CertificateKeyPair,
|
CertificateKeyPair,
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -12,4 +12,9 @@ CELERY_BEAT_SCHEDULE = {
|
|||||||
"schedule": crontab(minute=0, hour="*"),
|
"schedule": crontab(minute=0, hour="*"),
|
||||||
"options": {"queue": "authentik_scheduled"},
|
"options": {"queue": "authentik_scheduled"},
|
||||||
},
|
},
|
||||||
|
"outpost_token_ensurer": {
|
||||||
|
"task": "authentik.outposts.tasks.outpost_token_ensurer",
|
||||||
|
"schedule": crontab(minute="*/5"),
|
||||||
|
"options": {"queue": "authentik_scheduled"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -90,6 +90,21 @@ def outpost_pre_delete(outpost_pk: str):
|
|||||||
ProxyKubernetesController(outpost, service_connection).down()
|
ProxyKubernetesController(outpost, service_connection).down()
|
||||||
|
|
||||||
|
|
||||||
|
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||||
|
def outpost_token_ensurer(self: MonitoredTask):
|
||||||
|
"""Periodically ensure that all Outposts have valid Service Accounts
|
||||||
|
and Tokens"""
|
||||||
|
all_outposts = Outpost.objects.all()
|
||||||
|
for outpost in all_outposts:
|
||||||
|
_ = outpost.token
|
||||||
|
self.set_status(
|
||||||
|
TaskResult(
|
||||||
|
TaskResultStatus.SUCCESSFUL,
|
||||||
|
[f"Successfully checked {len(all_outposts)} Outposts."],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task()
|
@CELERY_APP.task()
|
||||||
def outpost_post_save(model_class: str, model_pk: Any):
|
def outpost_post_save(model_class: str, model_pk: Any):
|
||||||
"""If an Outpost is saved, Ensure that token is created/updated
|
"""If an Outpost is saved, Ensure that token is created/updated
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
"""authentik outposts urls"""
|
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from authentik.outposts.views import KubernetesManifestView, SetupView
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path(
|
|
||||||
"<uuid:outpost_pk>/k8s/", KubernetesManifestView.as_view(), name="k8s-manifest"
|
|
||||||
),
|
|
||||||
path("<uuid:outpost_pk>/", SetupView.as_view(), name="setup"),
|
|
||||||
]
|
|
@ -1,89 +0,0 @@
|
|||||||
"""authentik outpost views"""
|
|
||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.db.models import Model
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.views import View
|
|
||||||
from django.views.generic import TemplateView
|
|
||||||
from guardian.shortcuts import get_objects_for_user
|
|
||||||
from structlog import get_logger
|
|
||||||
|
|
||||||
from authentik.core.models import User
|
|
||||||
from authentik.outposts.controllers.docker import DockerController
|
|
||||||
from authentik.outposts.models import (
|
|
||||||
DockerServiceConnection,
|
|
||||||
KubernetesServiceConnection,
|
|
||||||
Outpost,
|
|
||||||
OutpostType,
|
|
||||||
)
|
|
||||||
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
def get_object_for_user_or_404(user: User, perm: str, **filters) -> Model:
|
|
||||||
"""Wrapper that combines get_objects_for_user and get_object_or_404"""
|
|
||||||
return get_object_or_404(get_objects_for_user(user, perm), **filters)
|
|
||||||
|
|
||||||
|
|
||||||
class DockerComposeView(LoginRequiredMixin, View):
|
|
||||||
"""Generate docker-compose yaml"""
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, outpost_pk: str) -> HttpResponse:
|
|
||||||
"""Render docker-compose file"""
|
|
||||||
outpost: Outpost = get_object_for_user_or_404(
|
|
||||||
request.user,
|
|
||||||
"authentik_outposts.view_outpost",
|
|
||||||
pk=outpost_pk,
|
|
||||||
)
|
|
||||||
manifest = ""
|
|
||||||
if outpost.type == OutpostType.PROXY:
|
|
||||||
controller = DockerController(outpost, DockerServiceConnection())
|
|
||||||
manifest = controller.get_static_deployment()
|
|
||||||
|
|
||||||
return HttpResponse(manifest, content_type="text/vnd.yaml")
|
|
||||||
|
|
||||||
|
|
||||||
class KubernetesManifestView(LoginRequiredMixin, View):
|
|
||||||
"""Generate Kubernetes Deployment and SVC for proxy"""
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, outpost_pk: str) -> HttpResponse:
|
|
||||||
"""Render deployment template"""
|
|
||||||
outpost: Outpost = get_object_for_user_or_404(
|
|
||||||
request.user,
|
|
||||||
"authentik_outposts.view_outpost",
|
|
||||||
pk=outpost_pk,
|
|
||||||
)
|
|
||||||
manifest = ""
|
|
||||||
if outpost.type == OutpostType.PROXY:
|
|
||||||
controller = ProxyKubernetesController(
|
|
||||||
outpost, KubernetesServiceConnection()
|
|
||||||
)
|
|
||||||
manifest = controller.get_static_deployment()
|
|
||||||
|
|
||||||
return HttpResponse(manifest, content_type="text/vnd.yaml")
|
|
||||||
|
|
||||||
|
|
||||||
class SetupView(LoginRequiredMixin, TemplateView):
|
|
||||||
"""Setup view"""
|
|
||||||
|
|
||||||
def get_template_names(self) -> List[str]:
|
|
||||||
allowed = ["dc", "custom", "k8s_manual", "k8s_integration"]
|
|
||||||
setup_type = self.request.GET.get("type", "dc")
|
|
||||||
if setup_type not in allowed:
|
|
||||||
setup_type = allowed[0]
|
|
||||||
return [f"outposts/setup_{setup_type}.html"]
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
|
||||||
kwargs = super().get_context_data(**kwargs)
|
|
||||||
outpost: Outpost = get_object_for_user_or_404(
|
|
||||||
self.request.user,
|
|
||||||
"authentik_outposts.view_outpost",
|
|
||||||
pk=self.kwargs["outpost_pk"],
|
|
||||||
)
|
|
||||||
kwargs.update(
|
|
||||||
{"host": self.request.build_absolute_uri("/"), "outpost": outpost}
|
|
||||||
)
|
|
||||||
return kwargs
|
|
@ -50,6 +50,7 @@ class HaveIBeenPwendPolicy(Policy):
|
|||||||
field=self.password_field,
|
field=self.password_field,
|
||||||
fields=request.context.keys(),
|
fields=request.context.keys(),
|
||||||
)
|
)
|
||||||
|
return PolicyResult(False, _("Password not set in context"))
|
||||||
password = request.context[self.password_field]
|
password = request.context[self.password_field]
|
||||||
|
|
||||||
pw_hash = sha1(password.encode("utf-8")).hexdigest() # nosec
|
pw_hash = sha1(password.encode("utf-8")).hexdigest() # nosec
|
||||||
|
@ -10,6 +10,16 @@ from authentik.providers.oauth2.generators import generate_client_secret
|
|||||||
class TestHIBPPolicy(TestCase):
|
class TestHIBPPolicy(TestCase):
|
||||||
"""Test HIBP Policy"""
|
"""Test HIBP Policy"""
|
||||||
|
|
||||||
|
def test_invalid(self):
|
||||||
|
"""Test without password"""
|
||||||
|
policy = HaveIBeenPwendPolicy.objects.create(
|
||||||
|
name="test_invalid",
|
||||||
|
)
|
||||||
|
request = PolicyRequest(get_anonymous_user())
|
||||||
|
result: PolicyResult = policy.passes(request)
|
||||||
|
self.assertFalse(result.passing)
|
||||||
|
self.assertEqual(result.messages[0], "Password not set in context")
|
||||||
|
|
||||||
def test_false(self):
|
def test_false(self):
|
||||||
"""Failing password case"""
|
"""Failing password case"""
|
||||||
policy = HaveIBeenPwendPolicy.objects.create(
|
policy = HaveIBeenPwendPolicy.objects.create(
|
||||||
|
@ -50,6 +50,7 @@ class PasswordPolicy(Policy):
|
|||||||
field=self.password_field,
|
field=self.password_field,
|
||||||
fields=request.context.keys(),
|
fields=request.context.keys(),
|
||||||
)
|
)
|
||||||
|
return PolicyResult(False, _("Password not set in context"))
|
||||||
password = request.context[self.password_field]
|
password = request.context[self.password_field]
|
||||||
|
|
||||||
filter_regex = []
|
filter_regex = []
|
||||||
|
@ -9,6 +9,21 @@ from authentik.policies.types import PolicyRequest, PolicyResult
|
|||||||
class TestPasswordPolicy(TestCase):
|
class TestPasswordPolicy(TestCase):
|
||||||
"""Test Password Policy"""
|
"""Test Password Policy"""
|
||||||
|
|
||||||
|
def test_invalid(self):
|
||||||
|
"""Test without password"""
|
||||||
|
policy = PasswordPolicy.objects.create(
|
||||||
|
name="test_invalid",
|
||||||
|
amount_uppercase=1,
|
||||||
|
amount_lowercase=2,
|
||||||
|
amount_symbols=3,
|
||||||
|
length_min=24,
|
||||||
|
error_message="test message",
|
||||||
|
)
|
||||||
|
request = PolicyRequest(get_anonymous_user())
|
||||||
|
result: PolicyResult = policy.passes(request)
|
||||||
|
self.assertFalse(result.passing)
|
||||||
|
self.assertEqual(result.messages[0], "Password not set in context")
|
||||||
|
|
||||||
def test_false(self):
|
def test_false(self):
|
||||||
"""Failing password case"""
|
"""Failing password case"""
|
||||||
policy = PasswordPolicy.objects.create(
|
policy = PasswordPolicy.objects.create(
|
||||||
|
0
authentik/providers/oauth2/tests/__init__.py
Normal file
46
authentik/providers/oauth2/tests/test_views_authorize.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""Test authorize view"""
|
||||||
|
from django.test import RequestFactory, TestCase
|
||||||
|
|
||||||
|
from authentik.flows.models import Flow
|
||||||
|
from authentik.providers.oauth2.errors import (
|
||||||
|
AuthorizeError,
|
||||||
|
ClientIdError,
|
||||||
|
RedirectUriError,
|
||||||
|
)
|
||||||
|
from authentik.providers.oauth2.models import OAuth2Provider
|
||||||
|
from authentik.providers.oauth2.views.authorize import OAuthAuthorizationParams
|
||||||
|
|
||||||
|
|
||||||
|
class TestViewsAuthorize(TestCase):
|
||||||
|
"""Test authorize view"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_invalid_grant_type(self):
|
||||||
|
"""Test with invalid grant type"""
|
||||||
|
with self.assertRaises(AuthorizeError):
|
||||||
|
request = self.factory.get("/", data={"response_type": "invalid"})
|
||||||
|
OAuthAuthorizationParams.from_request(request)
|
||||||
|
|
||||||
|
def test_invalid_client_id(self):
|
||||||
|
"""Test invalid client ID"""
|
||||||
|
with self.assertRaises(ClientIdError):
|
||||||
|
request = self.factory.get(
|
||||||
|
"/", data={"response_type": "code", "client_id": "invalid"}
|
||||||
|
)
|
||||||
|
OAuthAuthorizationParams.from_request(request)
|
||||||
|
|
||||||
|
def test_missing_redirect_uri(self):
|
||||||
|
"""test missing redirect URI"""
|
||||||
|
OAuth2Provider.objects.create(
|
||||||
|
name="test",
|
||||||
|
client_id="test",
|
||||||
|
authorization_flow=Flow.objects.first(),
|
||||||
|
)
|
||||||
|
with self.assertRaises(RedirectUriError):
|
||||||
|
request = self.factory.get(
|
||||||
|
"/", data={"response_type": "code", "client_id": "test"}
|
||||||
|
)
|
||||||
|
OAuthAuthorizationParams.from_request(request)
|
@ -139,7 +139,7 @@ class OAuthAuthorizationParams:
|
|||||||
is_open_id = SCOPE_OPENID in self.scope
|
is_open_id = SCOPE_OPENID in self.scope
|
||||||
|
|
||||||
# Redirect URI validation.
|
# Redirect URI validation.
|
||||||
if is_open_id and not self.redirect_uri:
|
if not self.redirect_uri:
|
||||||
LOGGER.warning("Missing redirect uri.")
|
LOGGER.warning("Missing redirect uri.")
|
||||||
raise RedirectUriError()
|
raise RedirectUriError()
|
||||||
if self.redirect_uri.lower() not in [
|
if self.redirect_uri.lower() not in [
|
||||||
|
84
authentik/providers/saml/tests/test_schema.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
"""Test Requests and Responses against schema"""
|
||||||
|
from base64 import b64encode
|
||||||
|
|
||||||
|
from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
|
from django.test import RequestFactory, TestCase
|
||||||
|
from guardian.utils import get_anonymous_user
|
||||||
|
from lxml import etree # nosec
|
||||||
|
|
||||||
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
from authentik.flows.models import Flow
|
||||||
|
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||||
|
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
||||||
|
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
|
||||||
|
from authentik.providers.saml.tests.test_auth_n_request import dummy_get_response
|
||||||
|
from authentik.sources.saml.models import SAMLSource
|
||||||
|
from authentik.sources.saml.processors.request import RequestProcessor
|
||||||
|
|
||||||
|
|
||||||
|
class TestSchema(TestCase):
|
||||||
|
"""Test Requests and Responses against schema"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
cert = CertificateKeyPair.objects.first()
|
||||||
|
self.provider: SAMLProvider = SAMLProvider.objects.create(
|
||||||
|
authorization_flow=Flow.objects.get(
|
||||||
|
slug="default-provider-authorization-implicit-consent"
|
||||||
|
),
|
||||||
|
acs_url="http://testserver/source/saml/provider/acs/",
|
||||||
|
signing_kp=cert,
|
||||||
|
verification_kp=cert,
|
||||||
|
)
|
||||||
|
self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||||
|
self.provider.save()
|
||||||
|
self.source = SAMLSource.objects.create(
|
||||||
|
slug="provider",
|
||||||
|
issuer="authentik",
|
||||||
|
signing_kp=cert,
|
||||||
|
)
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_request_schema(self):
|
||||||
|
"""Test generated AuthNRequest against Schema"""
|
||||||
|
http_request = self.factory.get("/")
|
||||||
|
|
||||||
|
middleware = SessionMiddleware(dummy_get_response)
|
||||||
|
middleware.process_request(http_request)
|
||||||
|
http_request.session.save()
|
||||||
|
|
||||||
|
# First create an AuthNRequest
|
||||||
|
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||||
|
request = request_proc.build_auth_n()
|
||||||
|
|
||||||
|
metadata = etree.fromstring(request) # nosec
|
||||||
|
|
||||||
|
schema = etree.XMLSchema(
|
||||||
|
etree.parse("xml/saml-schema-protocol-2.0.xsd")
|
||||||
|
) # nosec
|
||||||
|
self.assertTrue(schema.validate(metadata))
|
||||||
|
|
||||||
|
def test_response_schema(self):
|
||||||
|
"""Test generated AuthNRequest against Schema"""
|
||||||
|
http_request = self.factory.get("/")
|
||||||
|
http_request.user = get_anonymous_user()
|
||||||
|
|
||||||
|
middleware = SessionMiddleware(dummy_get_response)
|
||||||
|
middleware.process_request(http_request)
|
||||||
|
http_request.session.save()
|
||||||
|
|
||||||
|
# First create an AuthNRequest
|
||||||
|
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||||
|
request = request_proc.build_auth_n()
|
||||||
|
|
||||||
|
# To get an assertion we need a parsed request (parsed by provider)
|
||||||
|
parsed_request = AuthNRequestParser(self.provider).parse(
|
||||||
|
b64encode(request.encode()).decode(), "test_state"
|
||||||
|
)
|
||||||
|
# Now create a response and convert it to string (provider)
|
||||||
|
response_proc = AssertionProcessor(self.provider, http_request, parsed_request)
|
||||||
|
response = response_proc.build_response()
|
||||||
|
|
||||||
|
metadata = etree.fromstring(response) # nosec
|
||||||
|
|
||||||
|
schema = etree.XMLSchema(etree.parse("xml/saml-schema-protocol-2.0.xsd"))
|
||||||
|
self.assertTrue(schema.validate(metadata))
|
@ -1,15 +1,6 @@
|
|||||||
"""Small helper functions"""
|
"""Small helper functions"""
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
|
||||||
from django.shortcuts import render
|
|
||||||
from django.template.context import Context
|
|
||||||
|
|
||||||
|
|
||||||
def render_xml(request: HttpRequest, template: str, ctx: Context) -> HttpResponse:
|
|
||||||
"""Render template with content_type application/xml"""
|
|
||||||
return render(request, template, context=ctx, content_type="application/xml")
|
|
||||||
|
|
||||||
|
|
||||||
def get_random_id() -> str:
|
def get_random_id() -> str:
|
||||||
"""Random hex id"""
|
"""Random hex id"""
|
||||||
|
@ -32,3 +32,10 @@ class TestRecovery(TestCase):
|
|||||||
reverse("authentik_recovery:use-token", kwargs={"key": token.key})
|
reverse("authentik_recovery:use-token", kwargs={"key": token.key})
|
||||||
)
|
)
|
||||||
self.assertEqual(int(self.client.session["_auth_user_id"]), token.user.pk)
|
self.assertEqual(int(self.client.session["_auth_user_id"]), token.user.pk)
|
||||||
|
|
||||||
|
def test_recovery_view_invalid(self):
|
||||||
|
"""Test recovery view with invalid token"""
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_recovery:use-token", kwargs={"key": "abc"})
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
@ -105,7 +105,16 @@ class ASGILogger:
|
|||||||
# https://code.djangoproject.com/ticket/31508
|
# https://code.djangoproject.com/ticket/31508
|
||||||
# https://github.com/encode/uvicorn/issues/266
|
# https://github.com/encode/uvicorn/issues/266
|
||||||
return
|
return
|
||||||
await self.app(scope, receive, send_hooked)
|
try:
|
||||||
|
await self.app(scope, receive, send_hooked)
|
||||||
|
except TypeError as exc:
|
||||||
|
# https://github.com/encode/uvicorn/issues/244
|
||||||
|
if exc.args == (
|
||||||
|
"An asyncio.Future, a coroutine or an awaitable is required",
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise exc
|
||||||
|
|
||||||
def _get_ip(self) -> str:
|
def _get_ip(self) -> str:
|
||||||
client_ip = None
|
client_ip = None
|
||||||
|
@ -4,7 +4,7 @@ from django.conf import settings
|
|||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
|
|
||||||
|
|
||||||
class PytestTestRunner:
|
class PytestTestRunner: # pragma: no cover
|
||||||
"""Runs pytest to discover and run tests."""
|
"""Runs pytest to discover and run tests."""
|
||||||
|
|
||||||
def __init__(self, verbosity=1, failfast=False, keepdb=False, **_):
|
def __init__(self, verbosity=1, failfast=False, keepdb=False, **_):
|
||||||
|
0
authentik/sources/oauth/tests/__init__.py
Normal file
41
authentik/sources/oauth/tests/test_type_discord.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"""Discord Type tests"""
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||||
|
from authentik.sources.oauth.types.discord import DiscordOAuth2Callback
|
||||||
|
|
||||||
|
# https://discord.com/developers/docs/resources/user#user-object
|
||||||
|
DISCORD_USER = {
|
||||||
|
"id": "80351110224678912",
|
||||||
|
"username": "Nelly",
|
||||||
|
"discriminator": "1337",
|
||||||
|
"avatar": "8342729096ea3675442027381ff50dfe",
|
||||||
|
"verified": True,
|
||||||
|
"email": "nelly@discord.com",
|
||||||
|
"flags": 64,
|
||||||
|
"premium_type": 1,
|
||||||
|
"public_flags": 64,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestTypeGitHub(TestCase):
|
||||||
|
"""OAuth Source tests"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.source = OAuthSource.objects.create(
|
||||||
|
name="test",
|
||||||
|
slug="test",
|
||||||
|
provider_type="openid-connect",
|
||||||
|
authorization_url="",
|
||||||
|
profile_url="",
|
||||||
|
consumer_key="",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_enroll_context(self):
|
||||||
|
"""Test GitHub Enrollment context"""
|
||||||
|
ak_context = DiscordOAuth2Callback().get_user_enroll_context(
|
||||||
|
self.source, UserOAuthSourceConnection(), DISCORD_USER
|
||||||
|
)
|
||||||
|
self.assertEqual(ak_context["username"], DISCORD_USER["username"])
|
||||||
|
self.assertEqual(ak_context["email"], DISCORD_USER["email"])
|
||||||
|
self.assertEqual(ak_context["name"], DISCORD_USER["username"])
|
71
authentik/sources/oauth/tests/test_type_github.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"""GitHub Type tests"""
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||||
|
from authentik.sources.oauth.types.github import GitHubOAuth2Callback
|
||||||
|
|
||||||
|
# https://developer.github.com/v3/users/#get-the-authenticated-user
|
||||||
|
GITHUB_USER = {
|
||||||
|
"login": "octocat",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/octocat",
|
||||||
|
"html_url": "https://github.com/octocat",
|
||||||
|
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||||
|
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||||
|
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": False,
|
||||||
|
"name": "monalisa octocat",
|
||||||
|
"company": "GitHub",
|
||||||
|
"blog": "https://github.com/blog",
|
||||||
|
"location": "San Francisco",
|
||||||
|
"email": "octocat@github.com",
|
||||||
|
"hireable": False,
|
||||||
|
"bio": "There once was...",
|
||||||
|
"twitter_username": "monatheoctocat",
|
||||||
|
"public_repos": 2,
|
||||||
|
"public_gists": 1,
|
||||||
|
"followers": 20,
|
||||||
|
"following": 0,
|
||||||
|
"created_at": "2008-01-14T04:33:35Z",
|
||||||
|
"updated_at": "2008-01-14T04:33:35Z",
|
||||||
|
"private_gists": 81,
|
||||||
|
"total_private_repos": 100,
|
||||||
|
"owned_private_repos": 100,
|
||||||
|
"disk_usage": 10000,
|
||||||
|
"collaborators": 8,
|
||||||
|
"two_factor_authentication": True,
|
||||||
|
"plan": {"name": "Medium", "space": 400, "private_repos": 20, "collaborators": 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestTypeGitHub(TestCase):
|
||||||
|
"""OAuth Source tests"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.source = OAuthSource.objects.create(
|
||||||
|
name="test",
|
||||||
|
slug="test",
|
||||||
|
provider_type="openid-connect",
|
||||||
|
authorization_url="",
|
||||||
|
profile_url="",
|
||||||
|
consumer_key="",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_enroll_context(self):
|
||||||
|
"""Test GitHub Enrollment context"""
|
||||||
|
ak_context = GitHubOAuth2Callback().get_user_enroll_context(
|
||||||
|
self.source, UserOAuthSourceConnection(), GITHUB_USER
|
||||||
|
)
|
||||||
|
self.assertEqual(ak_context["username"], GITHUB_USER["login"])
|
||||||
|
self.assertEqual(ak_context["email"], GITHUB_USER["email"])
|
||||||
|
self.assertEqual(ak_context["name"], GITHUB_USER["name"])
|
112
authentik/sources/oauth/tests/test_type_twitter.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
"""Twitter Type tests"""
|
||||||
|
from django.test import Client, TestCase
|
||||||
|
|
||||||
|
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||||
|
from authentik.sources.oauth.types.twitter import TwitterOAuthCallback
|
||||||
|
|
||||||
|
# https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/ \
|
||||||
|
# api-reference/get-account-verify_credentials
|
||||||
|
TWITTER_USER = {
|
||||||
|
"contributors_enabled": True,
|
||||||
|
"created_at": "Sat May 09 17:58:22 +0000 2009",
|
||||||
|
"default_profile": False,
|
||||||
|
"default_profile_image": False,
|
||||||
|
"description": "I taught your phone that thing you like.",
|
||||||
|
"favourites_count": 588,
|
||||||
|
"follow_request_sent": None,
|
||||||
|
"followers_count": 10625,
|
||||||
|
"following": None,
|
||||||
|
"friends_count": 1181,
|
||||||
|
"geo_enabled": True,
|
||||||
|
"id": 38895958,
|
||||||
|
"id_str": "38895958",
|
||||||
|
"is_translator": False,
|
||||||
|
"lang": "en",
|
||||||
|
"listed_count": 190,
|
||||||
|
"location": "San Francisco",
|
||||||
|
"name": "Sean Cook",
|
||||||
|
"notifications": None,
|
||||||
|
"profile_background_color": "1A1B1F",
|
||||||
|
"profile_background_image_url": "",
|
||||||
|
"profile_background_image_url_https": "",
|
||||||
|
"profile_background_tile": True,
|
||||||
|
"profile_image_url": "",
|
||||||
|
"profile_image_url_https": "",
|
||||||
|
"profile_link_color": "2FC2EF",
|
||||||
|
"profile_sidebar_border_color": "181A1E",
|
||||||
|
"profile_sidebar_fill_color": "252429",
|
||||||
|
"profile_text_color": "666666",
|
||||||
|
"profile_use_background_image": True,
|
||||||
|
"protected": False,
|
||||||
|
"screen_name": "theSeanCook",
|
||||||
|
"show_all_inline_media": True,
|
||||||
|
"status": {
|
||||||
|
"contributors": None,
|
||||||
|
"coordinates": {"coordinates": [-122.45037293, 37.76484123], "type": "Point"},
|
||||||
|
"created_at": "Tue Aug 28 05:44:24 +0000 2012",
|
||||||
|
"favorited": False,
|
||||||
|
"geo": {"coordinates": [37.76484123, -122.45037293], "type": "Point"},
|
||||||
|
"id": 240323931419062272,
|
||||||
|
"id_str": "240323931419062272",
|
||||||
|
"in_reply_to_screen_name": "messl",
|
||||||
|
"in_reply_to_status_id": 240316959173009410,
|
||||||
|
"in_reply_to_status_id_str": "240316959173009410",
|
||||||
|
"in_reply_to_user_id": 18707866,
|
||||||
|
"in_reply_to_user_id_str": "18707866",
|
||||||
|
"place": {
|
||||||
|
"attributes": {},
|
||||||
|
"bounding_box": {
|
||||||
|
"coordinates": [
|
||||||
|
[
|
||||||
|
[-122.45778216, 37.75932999],
|
||||||
|
[-122.44248216, 37.75932999],
|
||||||
|
[-122.44248216, 37.76752899],
|
||||||
|
[-122.45778216, 37.76752899],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"type": "Polygon",
|
||||||
|
},
|
||||||
|
"country": "United States",
|
||||||
|
"country_code": "US",
|
||||||
|
"full_name": "Ashbury Heights, San Francisco",
|
||||||
|
"id": "866269c983527d5a",
|
||||||
|
"name": "Ashbury Heights",
|
||||||
|
"place_type": "neighborhood",
|
||||||
|
"url": "http://api.twitter.com/1/geo/id/866269c983527d5a.json",
|
||||||
|
},
|
||||||
|
"retweet_count": 0,
|
||||||
|
"retweeted": False,
|
||||||
|
"source": "Twitter for iPhone",
|
||||||
|
"text": "@messl congrats! So happy for all 3 of you.",
|
||||||
|
"truncated": False,
|
||||||
|
},
|
||||||
|
"statuses_count": 2609,
|
||||||
|
"time_zone": "Pacific Time (US & Canada)",
|
||||||
|
"url": None,
|
||||||
|
"utc_offset": -28800,
|
||||||
|
"verified": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestTypeGitHub(TestCase):
|
||||||
|
"""OAuth Source tests"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
self.source = OAuthSource.objects.create(
|
||||||
|
name="test",
|
||||||
|
slug="test",
|
||||||
|
provider_type="openid-connect",
|
||||||
|
authorization_url="",
|
||||||
|
profile_url="",
|
||||||
|
consumer_key="",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_enroll_context(self):
|
||||||
|
"""Test Twitter Enrollment context"""
|
||||||
|
ak_context = TwitterOAuthCallback().get_user_enroll_context(
|
||||||
|
self.source, UserOAuthSourceConnection(), TWITTER_USER
|
||||||
|
)
|
||||||
|
self.assertEqual(ak_context["username"], TWITTER_USER["screen_name"])
|
||||||
|
self.assertEqual(ak_context["email"], TWITTER_USER.get("email", None))
|
||||||
|
self.assertEqual(ak_context["name"], TWITTER_USER["name"])
|
@ -1,15 +1,14 @@
|
|||||||
"""OAuth Source tests"""
|
"""OAuth Source tests"""
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from django.test import Client, TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from authentik.sources.oauth.models import OAuthSource
|
from authentik.sources.oauth.models import OAuthSource
|
||||||
|
|
||||||
|
|
||||||
class OAuthSourceTests(TestCase):
|
class TestOAuthSource(TestCase):
|
||||||
"""OAuth Source tests"""
|
"""OAuth Source tests"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = Client()
|
|
||||||
self.source = OAuthSource.objects.create(
|
self.source = OAuthSource.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
slug="test",
|
slug="test",
|
@ -11,7 +11,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
|
|||||||
class DiscordOAuthRedirect(OAuthRedirect):
|
class DiscordOAuthRedirect(OAuthRedirect):
|
||||||
"""Discord OAuth2 Redirect"""
|
"""Discord OAuth2 Redirect"""
|
||||||
|
|
||||||
def get_additional_parameters(self, source):
|
def get_additional_parameters(self, source): # pragma: no cover
|
||||||
return {
|
return {
|
||||||
"scope": "email identify",
|
"scope": "email identify",
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
|
|||||||
class FacebookOAuthRedirect(OAuthRedirect):
|
class FacebookOAuthRedirect(OAuthRedirect):
|
||||||
"""Facebook OAuth2 Redirect"""
|
"""Facebook OAuth2 Redirect"""
|
||||||
|
|
||||||
def get_additional_parameters(self, source):
|
def get_additional_parameters(self, source): # pragma: no cover
|
||||||
return {
|
return {
|
||||||
"scope": "email",
|
"scope": "email",
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
|
|||||||
class GoogleOAuthRedirect(OAuthRedirect):
|
class GoogleOAuthRedirect(OAuthRedirect):
|
||||||
"""Google OAuth2 Redirect"""
|
"""Google OAuth2 Redirect"""
|
||||||
|
|
||||||
def get_additional_parameters(self, source):
|
def get_additional_parameters(self, source): # pragma: no cover
|
||||||
return {
|
return {
|
||||||
"scope": "email profile",
|
"scope": "email profile",
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
|
|||||||
class OpenIDConnectOAuthRedirect(OAuthRedirect):
|
class OpenIDConnectOAuthRedirect(OAuthRedirect):
|
||||||
"""OpenIDConnect OAuth2 Redirect"""
|
"""OpenIDConnect OAuth2 Redirect"""
|
||||||
|
|
||||||
def get_additional_parameters(self, source: OAuthSource):
|
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
|
||||||
return {
|
return {
|
||||||
"scope": "openid email profile",
|
"scope": "openid email profile",
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
|
|||||||
class RedditOAuthRedirect(OAuthRedirect):
|
class RedditOAuthRedirect(OAuthRedirect):
|
||||||
"""Reddit OAuth2 Redirect"""
|
"""Reddit OAuth2 Redirect"""
|
||||||
|
|
||||||
def get_additional_parameters(self, source):
|
def get_additional_parameters(self, source): # pragma: no cover
|
||||||
return {
|
return {
|
||||||
"scope": "identity",
|
"scope": "identity",
|
||||||
"duration": "permanent",
|
"duration": "permanent",
|
||||||
|
@ -18,6 +18,6 @@ class TwitterOAuthCallback(OAuthCallback):
|
|||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"username": info.get("screen_name"),
|
"username": info.get("screen_name"),
|
||||||
"email": info.get("email"),
|
"email": info.get("email", None),
|
||||||
"name": info.get("name"),
|
"name": info.get("name"),
|
||||||
}
|
}
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
"""SAML Source tests"""
|
|
||||||
from defusedxml import ElementTree
|
|
||||||
from django.test import RequestFactory, TestCase
|
|
||||||
|
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
|
||||||
from authentik.sources.saml.models import SAMLSource
|
|
||||||
from authentik.sources.saml.processors.metadata import MetadataProcessor
|
|
||||||
|
|
||||||
|
|
||||||
class TestMetadataProcessor(TestCase):
|
|
||||||
"""Test MetadataProcessor"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.source = SAMLSource.objects.create(
|
|
||||||
slug="provider",
|
|
||||||
issuer="authentik",
|
|
||||||
signing_kp=CertificateKeyPair.objects.first(),
|
|
||||||
)
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
|
|
||||||
def test_metadata(self):
|
|
||||||
"""Test Metadata generation being valid"""
|
|
||||||
request = self.factory.get("/")
|
|
||||||
xml = MetadataProcessor(self.source, request).build_entity_descriptor()
|
|
||||||
metadata = ElementTree.fromstring(xml)
|
|
||||||
self.assertEqual(metadata.attrib["entityID"], "authentik")
|
|
0
authentik/sources/saml/tests/__init__.py
Normal file
55
authentik/sources/saml/tests/test_metadata.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
"""SAML Source tests"""
|
||||||
|
from defusedxml import ElementTree
|
||||||
|
from django.test import RequestFactory, TestCase
|
||||||
|
from lxml import etree # nosec
|
||||||
|
|
||||||
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
from authentik.sources.saml.models import SAMLSource
|
||||||
|
from authentik.sources.saml.processors.metadata import MetadataProcessor
|
||||||
|
|
||||||
|
|
||||||
|
class TestMetadataProcessor(TestCase):
|
||||||
|
"""Test MetadataProcessor"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_metadata_schema(self):
|
||||||
|
"""Test Metadata generation being valid"""
|
||||||
|
source = SAMLSource.objects.create(
|
||||||
|
slug="provider",
|
||||||
|
issuer="authentik",
|
||||||
|
signing_kp=CertificateKeyPair.objects.first(),
|
||||||
|
)
|
||||||
|
request = self.factory.get("/")
|
||||||
|
xml = MetadataProcessor(source, request).build_entity_descriptor()
|
||||||
|
metadata = etree.fromstring(xml) # nosec
|
||||||
|
|
||||||
|
schema = etree.XMLSchema(
|
||||||
|
etree.parse("xml/saml-schema-metadata-2.0.xsd")
|
||||||
|
) # nosec
|
||||||
|
self.assertTrue(schema.validate(metadata))
|
||||||
|
|
||||||
|
def test_metadata(self):
|
||||||
|
"""Test Metadata generation being valid"""
|
||||||
|
source = SAMLSource.objects.create(
|
||||||
|
slug="provider",
|
||||||
|
issuer="authentik",
|
||||||
|
signing_kp=CertificateKeyPair.objects.first(),
|
||||||
|
)
|
||||||
|
request = self.factory.get("/")
|
||||||
|
xml = MetadataProcessor(source, request).build_entity_descriptor()
|
||||||
|
metadata = ElementTree.fromstring(xml)
|
||||||
|
self.assertEqual(metadata.attrib["entityID"], "authentik")
|
||||||
|
|
||||||
|
def test_metadata_without_signautre(self):
|
||||||
|
"""Test Metadata generation being valid"""
|
||||||
|
source = SAMLSource.objects.create(
|
||||||
|
slug="provider",
|
||||||
|
issuer="authentik",
|
||||||
|
# signing_kp=CertificateKeyPair.objects.first(),
|
||||||
|
)
|
||||||
|
request = self.factory.get("/")
|
||||||
|
xml = MetadataProcessor(source, request).build_entity_descriptor()
|
||||||
|
metadata = ElementTree.fromstring(xml)
|
||||||
|
self.assertEqual(metadata.attrib["entityID"], "authentik")
|
@ -22,10 +22,10 @@
|
|||||||
</ul>
|
</ul>
|
||||||
{% if not state %}
|
{% if not state %}
|
||||||
{% if stage.configure_flow %}
|
{% if stage.configure_flow %}
|
||||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next={{ request.get_full_path }}" class="pf-c-button pf-m-primary">{% trans "Enable Static Tokens" %}</a>
|
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next={% url 'authentik_core:user-settings' %}" class="ak-root-link pf-c-button pf-m-primary">{% trans "Enable Static Tokens" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'authentik_stages_otp_static:disable' stage_uuid=stage.stage_uuid %}" class="pf-c-button pf-m-danger">{% trans "Disable Static Tokens" %}</a>
|
<a href="{% url 'authentik_stages_otp_static:disable' stage_uuid=stage.stage_uuid %}" class="ak-root-pf-c-button pf-m-danger">{% trans "Disable Static Tokens" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,4 +41,4 @@ class DisableView(LoginRequiredMixin, View):
|
|||||||
Event.new(
|
Event.new(
|
||||||
"static_otp_disable", message="User disabled Static OTP Tokens."
|
"static_otp_disable", message="User disabled Static OTP Tokens."
|
||||||
).from_http(request)
|
).from_http(request)
|
||||||
return redirect("authentik_stages_otp:otp-user-settings")
|
return redirect("authentik_core:user-settings")
|
||||||
|
@ -18,10 +18,10 @@
|
|||||||
<p>
|
<p>
|
||||||
{% if not state %}
|
{% if not state %}
|
||||||
{% if stage.configure_flow %}
|
{% if stage.configure_flow %}
|
||||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next={{ request.get_full_path }}" class="pf-c-button pf-m-primary">{% trans "Enable Time-based OTP" %}</a>
|
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next={% url 'authentik_core:user-settings' %}" class="ak-root-link pf-c-button pf-m-primary">{% trans "Enable Time-based OTP" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'authentik_stages_otp_time:disable' stage_uuid=stage.stage_uuid %}" class="pf-c-button pf-m-danger">{% trans "Disable Time-based OTP" %}</a>
|
<a href="{% url 'authentik_stages_otp_time:disable' stage_uuid=stage.stage_uuid %}" class="ak-root-pf-c-button pf-m-danger">{% trans "Disable Time-based OTP" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,4 +38,4 @@ class DisableView(LoginRequiredMixin, View):
|
|||||||
Event.new("totp_disable", message="User disabled Time-based OTP.").from_http(
|
Event.new("totp_disable", message="User disabled Time-based OTP.").from_http(
|
||||||
request
|
request
|
||||||
)
|
)
|
||||||
return redirect("authentik_stages_otp:otp-user-settings")
|
return redirect("authentik_core:user-settings")
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
{% trans 'Reset your password' %}
|
{% trans 'Reset your password' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-card__body">
|
<div class="pf-c-card__body">
|
||||||
<a class="pf-c-button pf-m-primary" href="{{ url }}">
|
<a class="pf-c-button pf-m-primary ak-root-link" href="{{ url }}">
|
||||||
{% trans 'Change password' %}
|
{% trans 'Change password' %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,6 +87,7 @@ class TestUserWriteStage(TestCase):
|
|||||||
"username": "test-user-new",
|
"username": "test-user-new",
|
||||||
"password": new_password,
|
"password": new_password,
|
||||||
"attribute_some-custom-attribute": "test",
|
"attribute_some-custom-attribute": "test",
|
||||||
|
"some_ignored_attribute": "bar",
|
||||||
}
|
}
|
||||||
session = self.client.session
|
session = self.client.session
|
||||||
session[SESSION_KEY_PLAN] = plan
|
session[SESSION_KEY_PLAN] = plan
|
||||||
@ -109,6 +110,7 @@ class TestUserWriteStage(TestCase):
|
|||||||
self.assertTrue(user_qs.exists())
|
self.assertTrue(user_qs.exists())
|
||||||
self.assertTrue(user_qs.first().check_password(new_password))
|
self.assertTrue(user_qs.first().check_password(new_password))
|
||||||
self.assertEqual(user_qs.first().attributes["some-custom-attribute"], "test")
|
self.assertEqual(user_qs.first().attributes["some-custom-attribute"], "test")
|
||||||
|
self.assertNotIn("some_ignored_attribute", user_qs.first().attributes)
|
||||||
|
|
||||||
@patch(
|
@patch(
|
||||||
"authentik.flows.views.to_stage_response",
|
"authentik.flows.views.to_stage_response",
|
||||||
|
@ -19,7 +19,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
server:
|
server:
|
||||||
image: beryju/authentik:${AUTHENTIK_TAG:-0.13.0-rc1}
|
image: beryju/authentik:${AUTHENTIK_TAG:-0.13.0-rc4}
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
AUTHENTIK_REDIS__HOST: redis
|
AUTHENTIK_REDIS__HOST: redis
|
||||||
@ -42,7 +42,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
worker:
|
worker:
|
||||||
image: beryju/authentik:${AUTHENTIK_TAG:-0.13.0-rc1}
|
image: beryju/authentik:${AUTHENTIK_TAG:-0.13.0-rc4}
|
||||||
command: worker
|
command: worker
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
@ -56,7 +56,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
static:
|
static:
|
||||||
image: beryju/authentik-static:${AUTHENTIK_TAG:-0.13.0-rc1}
|
image: beryju/authentik-static:${AUTHENTIK_TAG:-0.13.0-rc4}
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
labels:
|
labels:
|
||||||
|
@ -4,8 +4,8 @@ name: authentik
|
|||||||
home: https://goauthentik.io
|
home: https://goauthentik.io
|
||||||
sources:
|
sources:
|
||||||
- https://github.com/BeryJu/authentik
|
- https://github.com/BeryJu/authentik
|
||||||
version: "0.13.0-rc1"
|
version: "0.13.0-rc4"
|
||||||
icon: https://raw.githubusercontent.com/BeryJu/authentik/master/icons/icon.svg
|
icon: https://raw.githubusercontent.com/BeryJu/authentik/master/web/icons/icon.svg
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: postgresql
|
- name: postgresql
|
||||||
version: 9.4.1
|
version: 9.4.1
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
|-----------------------------------|-------------------------|-------------|
|
|-----------------------------------|-------------------------|-------------|
|
||||||
| image.name | beryju/authentik | Image used to run the authentik server and worker |
|
| image.name | beryju/authentik | Image used to run the authentik server and worker |
|
||||||
| image.name_static | beryju/authentik-static | Image used to run the authentik static server (CSS and JS Files) |
|
| image.name_static | beryju/authentik-static | Image used to run the authentik static server (CSS and JS Files) |
|
||||||
| image.tag | 0.12.5-stable | Image tag |
|
| image.tag | 0.13.0-rc4 | Image tag |
|
||||||
|
| image.pullPolicy | IfNotPresent | Image Pull Policy used for all deployments |
|
||||||
| serverReplicas | 1 | Replicas for the Server deployment |
|
| serverReplicas | 1 | Replicas for the Server deployment |
|
||||||
| workerReplicas | 1 | Replicas for the Worker deployment |
|
| workerReplicas | 1 | Replicas for the Worker deployment |
|
||||||
| kubernetesIntegration | true | Enable/disable the Kubernetes integration for authentik. This will create a service account for authentik to create and update outposts in authentik |
|
| kubernetesIntegration | true | Enable/disable the Kubernetes integration for authentik. This will create a service account for authentik to create and update outposts in authentik |
|
||||||
|
@ -24,7 +24,7 @@ spec:
|
|||||||
containers:
|
containers:
|
||||||
- name: {{ .Chart.Name }}-static
|
- name: {{ .Chart.Name }}-static
|
||||||
image: "{{ .Values.image.name_static }}:{{ .Values.image.tag }}"
|
image: "{{ .Values.image.name_static }}:{{ .Values.image.tag }}"
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: 80
|
containerPort: 80
|
||||||
|
@ -45,6 +45,7 @@ spec:
|
|||||||
initContainers:
|
initContainers:
|
||||||
- name: authentik-database-migrations
|
- name: authentik-database-migrations
|
||||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||||
|
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||||
args: [migrate]
|
args: [migrate]
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
@ -69,6 +70,7 @@ spec:
|
|||||||
containers:
|
containers:
|
||||||
- name: {{ .Chart.Name }}
|
- name: {{ .Chart.Name }}
|
||||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||||
|
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||||
args: [server]
|
args: [server]
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
|
@ -48,7 +48,7 @@ spec:
|
|||||||
containers:
|
containers:
|
||||||
- name: {{ .Chart.Name }}
|
- name: {{ .Chart.Name }}
|
||||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||||
args: [worker]
|
args: [worker]
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
image:
|
image:
|
||||||
tag: gh-master
|
tag: gh-master
|
||||||
|
pullPolicy: Always
|
||||||
|
|
||||||
serverReplicas: 1
|
serverReplicas: 1
|
||||||
workerReplicas: 1
|
workerReplicas: 1
|
||||||
|
@ -5,7 +5,8 @@ image:
|
|||||||
name: beryju/authentik
|
name: beryju/authentik
|
||||||
name_static: beryju/authentik-static
|
name_static: beryju/authentik-static
|
||||||
name_outposts: beryju/authentik # Prefix used for Outpost deployments, Outpost type and version is appended
|
name_outposts: beryju/authentik # Prefix used for Outpost deployments, Outpost type and version is appended
|
||||||
tag: 0.13.0-rc1
|
tag: 0.13.0-rc4
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
serverReplicas: 1
|
serverReplicas: 1
|
||||||
workerReplicas: 1
|
workerReplicas: 1
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
|
from redis import Redis
|
||||||
|
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
from lifecycle.migrate import BaseMigration
|
from lifecycle.migrate import BaseMigration
|
||||||
|
|
||||||
SQL_STATEMENT = """BEGIN TRANSACTION;
|
SQL_STATEMENT = """BEGIN TRANSACTION;
|
||||||
@ -103,3 +106,16 @@ class Migration(BaseMigration):
|
|||||||
def run(self):
|
def run(self):
|
||||||
self.cur.execute(SQL_STATEMENT)
|
self.cur.execute(SQL_STATEMENT)
|
||||||
self.con.commit()
|
self.con.commit()
|
||||||
|
# We also need to clean the cache to make sure no pickeled objects still exist
|
||||||
|
for db in [
|
||||||
|
CONFIG.y("redis.message_queue_db"),
|
||||||
|
CONFIG.y("redis.cache_db"),
|
||||||
|
CONFIG.y("redis.ws_db"),
|
||||||
|
]:
|
||||||
|
redis = Redis(
|
||||||
|
host=CONFIG.y("redis.host"),
|
||||||
|
port=6379,
|
||||||
|
db=db,
|
||||||
|
password=CONFIG.y("redis.password"),
|
||||||
|
)
|
||||||
|
redis.flushall()
|
@ -1,3 +1,3 @@
|
|||||||
package pkg
|
package pkg
|
||||||
|
|
||||||
const VERSION = "0.13.0-rc1"
|
const VERSION = "0.13.0-rc4"
|
||||||
|
@ -7087,6 +7087,9 @@ definitions:
|
|||||||
type: boolean
|
type: boolean
|
||||||
url:
|
url:
|
||||||
title: Url
|
title: Url
|
||||||
|
description: Can be in the format of 'unix://<path>' when connecting to a
|
||||||
|
local docker daemon, or 'https://<hostname>:2376' when connecting to a remote
|
||||||
|
system.
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
tls_verification:
|
tls_verification:
|
||||||
|
@ -142,7 +142,7 @@ class TestSourceOAuth2(SeleniumTestCase):
|
|||||||
|
|
||||||
# Wait until we've logged in
|
# Wait until we've logged in
|
||||||
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
||||||
self.driver.get(self.url("authentik_core:user-settings"))
|
self.driver.get(self.url("authentik_core:user-details"))
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||||
@ -224,7 +224,7 @@ class TestSourceOAuth2(SeleniumTestCase):
|
|||||||
|
|
||||||
# Wait until we've logged in
|
# Wait until we've logged in
|
||||||
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
||||||
self.driver.get(self.url("authentik_core:user-settings"))
|
self.driver.get(self.url("authentik_core:user-details"))
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||||
@ -317,7 +317,7 @@ class TestSourceOAuth1(SeleniumTestCase):
|
|||||||
sleep(2)
|
sleep(2)
|
||||||
# Wait until we've logged in
|
# Wait until we've logged in
|
||||||
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
||||||
self.driver.get(self.url("authentik_core:user-settings"))
|
self.driver.get(self.url("authentik_core:user-details"))
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"),
|
self.driver.find_element(By.ID, "id_username").get_attribute("value"),
|
||||||
|
@ -134,7 +134,7 @@ class TestSourceSAML(SeleniumTestCase):
|
|||||||
|
|
||||||
# Wait until we're logged in
|
# Wait until we're logged in
|
||||||
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
||||||
self.driver.get(self.url("authentik_core:user-settings"))
|
self.driver.get(self.url("authentik_core:user-details"))
|
||||||
|
|
||||||
# Wait until we've loaded the user info page
|
# Wait until we've loaded the user info page
|
||||||
self.assertNotEqual(
|
self.assertNotEqual(
|
||||||
@ -185,7 +185,7 @@ class TestSourceSAML(SeleniumTestCase):
|
|||||||
|
|
||||||
# Wait until we're logged in
|
# Wait until we're logged in
|
||||||
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
||||||
self.driver.get(self.url("authentik_core:user-settings"))
|
self.driver.get(self.url("authentik_core:user-details"))
|
||||||
|
|
||||||
# Wait until we've loaded the user info page
|
# Wait until we've loaded the user info page
|
||||||
self.assertNotEqual(
|
self.assertNotEqual(
|
||||||
@ -234,7 +234,7 @@ class TestSourceSAML(SeleniumTestCase):
|
|||||||
|
|
||||||
# Wait until we're logged in
|
# Wait until we're logged in
|
||||||
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
||||||
self.driver.get(self.url("authentik_core:user-settings"))
|
self.driver.get(self.url("authentik_core:user-details"))
|
||||||
|
|
||||||
# Wait until we've loaded the user info page
|
# Wait until we've loaded the user info page
|
||||||
self.assertNotEqual(
|
self.assertNotEqual(
|
||||||
|
95
tests/integration/test_outpost_docker.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
"""outpost tests"""
|
||||||
|
from shutil import rmtree
|
||||||
|
from tempfile import mkdtemp
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from docker import DockerClient, from_env
|
||||||
|
from docker.models.containers import Container
|
||||||
|
from docker.types.healthcheck import Healthcheck
|
||||||
|
|
||||||
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
from authentik.flows.models import Flow
|
||||||
|
from authentik.outposts.apps import AuthentikOutpostConfig
|
||||||
|
from authentik.outposts.controllers.docker import DockerController
|
||||||
|
from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostType
|
||||||
|
from authentik.providers.proxy.models import ProxyProvider
|
||||||
|
|
||||||
|
|
||||||
|
class OutpostDockerTests(TestCase):
|
||||||
|
"""Test Docker Controllers"""
|
||||||
|
|
||||||
|
def _start_container(self, ssl_folder: str) -> Container:
|
||||||
|
client: DockerClient = from_env()
|
||||||
|
container = client.containers.run(
|
||||||
|
image="docker.beryju.org/proxy/library/docker:dind",
|
||||||
|
detach=True,
|
||||||
|
network_mode="host",
|
||||||
|
remove=True,
|
||||||
|
privileged=True,
|
||||||
|
healthcheck=Healthcheck(
|
||||||
|
test=["CMD", "docker", "info"],
|
||||||
|
interval=5 * 100 * 1000000,
|
||||||
|
start_period=5 * 100 * 1000000,
|
||||||
|
),
|
||||||
|
environment={"DOCKER_TLS_CERTDIR": "/ssl"},
|
||||||
|
volumes={
|
||||||
|
f"{ssl_folder}/": {
|
||||||
|
"bind": "/ssl",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
while True:
|
||||||
|
container.reload()
|
||||||
|
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||||
|
if status == "healthy":
|
||||||
|
return container
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.ssl_folder = mkdtemp()
|
||||||
|
self.container = self._start_container(self.ssl_folder)
|
||||||
|
# Ensure that local connection have been created
|
||||||
|
AuthentikOutpostConfig.init_local_connection()
|
||||||
|
self.provider: ProxyProvider = ProxyProvider.objects.create(
|
||||||
|
name="test",
|
||||||
|
internal_host="http://localhost",
|
||||||
|
external_host="http://localhost",
|
||||||
|
authorization_flow=Flow.objects.first(),
|
||||||
|
)
|
||||||
|
authentication_kp = CertificateKeyPair.objects.create(
|
||||||
|
name="docker-authentication",
|
||||||
|
certificate_data=open(f"{self.ssl_folder}/client/cert.pem").read(),
|
||||||
|
key_data=open(f"{self.ssl_folder}/client/key.pem").read(),
|
||||||
|
)
|
||||||
|
verification_kp = CertificateKeyPair.objects.create(
|
||||||
|
name="docker-verification",
|
||||||
|
certificate_data=open(f"{self.ssl_folder}/client/ca.pem").read(),
|
||||||
|
)
|
||||||
|
self.service_connection = DockerServiceConnection.objects.create(
|
||||||
|
url="https://localhost:2376",
|
||||||
|
tls_verification=verification_kp,
|
||||||
|
tls_authentication=authentication_kp,
|
||||||
|
)
|
||||||
|
self.outpost: Outpost = Outpost.objects.create(
|
||||||
|
name="test",
|
||||||
|
type=OutpostType.PROXY,
|
||||||
|
service_connection=self.service_connection,
|
||||||
|
)
|
||||||
|
self.outpost.providers.add(self.provider)
|
||||||
|
self.outpost.save()
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
super().tearDown()
|
||||||
|
self.container.kill()
|
||||||
|
try:
|
||||||
|
rmtree(self.ssl_folder)
|
||||||
|
except PermissionError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_docker_controller(self):
|
||||||
|
"""test that deployment requires update"""
|
||||||
|
controller = DockerController(self.outpost, self.service_connection)
|
||||||
|
controller.up()
|
||||||
|
controller.down()
|
@ -8,3 +8,4 @@ FROM nginx
|
|||||||
|
|
||||||
COPY --from=npm-builder /static/robots.txt /usr/share/nginx/html/robots.txt
|
COPY --from=npm-builder /static/robots.txt /usr/share/nginx/html/robots.txt
|
||||||
COPY --from=npm-builder /static/dist/ /usr/share/nginx/html/static/dist/
|
COPY --from=npm-builder /static/dist/ /usr/share/nginx/html/static/dist/
|
||||||
|
COPY --from=npm-builder /static/authentik/ /usr/share/nginx/html/static/authentik/
|
||||||
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
@ -16,7 +16,7 @@ const resources = [
|
|||||||
{ src: "src/index.html", dest: "dist" },
|
{ src: "src/index.html", dest: "dist" },
|
||||||
{ src: "src/authentik.css", dest: "dist" },
|
{ src: "src/authentik.css", dest: "dist" },
|
||||||
{ src: "src/assets/*", dest: "dist/assets" },
|
{ src: "src/assets/*", dest: "dist/assets" },
|
||||||
{ src: "../icons/*", dest: "dist/assets/icons" },
|
{ src: "./icons/*", dest: "dist/assets/icons" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
|
@ -2,6 +2,7 @@ import { DefaultClient } from "./client";
|
|||||||
import * as Sentry from "@sentry/browser";
|
import * as Sentry from "@sentry/browser";
|
||||||
import { Integrations } from "@sentry/tracing";
|
import { Integrations } from "@sentry/tracing";
|
||||||
import { VERSION } from "../constants";
|
import { VERSION } from "../constants";
|
||||||
|
import { SentryIgnoredError } from "../common/errors";
|
||||||
|
|
||||||
export class Config {
|
export class Config {
|
||||||
branding_logo: string;
|
branding_logo: string;
|
||||||
@ -24,6 +25,12 @@ export class Config {
|
|||||||
integrations: [new Integrations.BrowserTracing()],
|
integrations: [new Integrations.BrowserTracing()],
|
||||||
tracesSampleRate: 1.0,
|
tracesSampleRate: 1.0,
|
||||||
environment: config.error_reporting_environment,
|
environment: config.error_reporting_environment,
|
||||||
|
beforeSend(event: Sentry.Event, hint: Sentry.EventHint) {
|
||||||
|
if (hint.originalException instanceof SentryIgnoredError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return event;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
console.debug("authentik/config: Sentry enabled.");
|
console.debug("authentik/config: Sentry enabled.");
|
||||||
}
|
}
|
||||||
|
@ -154,6 +154,9 @@ select[multiple] {
|
|||||||
background-color: var(--ak-dark-background-light);
|
background-color: var(--ak-dark-background-light);
|
||||||
color: var(--ak-dark-foreground);
|
color: var(--ak-dark-foreground);
|
||||||
}
|
}
|
||||||
|
.pf-c-form-control[readonly] {
|
||||||
|
background-color: var(--ak-dark-background-light);
|
||||||
|
}
|
||||||
.pf-c-button.pf-m-control {
|
.pf-c-button.pf-m-control {
|
||||||
--pf-c-button--after--BorderColor: var(--ak-dark-background-lighter) var(--ak-dark-background-lighter) var(--pf-c-button--m-control--after--BorderBottomColor) var(--ak-dark-background-lighter);
|
--pf-c-button--after--BorderColor: var(--ak-dark-background-lighter) var(--ak-dark-background-lighter) var(--pf-c-button--m-control--after--BorderBottomColor) var(--ak-dark-background-lighter);
|
||||||
background-color: var(--ak-dark-background-light);
|
background-color: var(--ak-dark-background-light);
|
||||||
|
1
web/src/common/errors.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export class SentryIgnoredError extends Error {}
|