Compare commits
74 Commits
version/20
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
1883402b3d | |||
88a8b7d2fa | |||
987f03c4be | |||
1b3aacfa1d | |||
3a994ab2a4 | |||
d7713357f4 | |||
e7c03fdb14 | |||
6105956847 | |||
89028f175a | |||
f121098957 | |||
4ff32af343 | |||
972868c15c | |||
0bc57f571b | |||
9de5b6f93e | |||
cc744dc581 | |||
47006fc9d2 | |||
a03e48c5ce | |||
816b0c7d83 | |||
0edf4296c4 | |||
b8fdda50ec | |||
ab1840dd66 | |||
482491e93c | |||
2ca991ba3d | |||
b20c384f5a | |||
9ce8edbcd6 | |||
cb5b2148a3 | |||
d5702c6282 | |||
61a876b582 | |||
8c9748e4a0 | |||
6460245d5e | |||
b7979ad48e | |||
cbd95848e7 | |||
4704de937a | |||
394d8e99a4 | |||
a26f25ccd6 | |||
94257e0f50 | |||
b2a42a68a4 | |||
7895d59da3 | |||
b54c60d7af | |||
6bab3bf68e | |||
fdc09c658a | |||
a690a02f99 | |||
0e912fd647 | |||
27af330932 | |||
7187d28905 | |||
ca832b6090 | |||
53bd6bf06e | |||
813f271bdd | |||
63dc8fe7dc | |||
383f4e4dcf | |||
2896652fef | |||
cfe2648b62 | |||
8d49705c87 | |||
c99e6d8f2c | |||
0996bb500c | |||
3d4a45c93f | |||
0642af0b78 | |||
dce623dd7c | |||
646d174dd2 | |||
b8fdb82adc | |||
75d6cd1674 | |||
5c91658484 | |||
ebb44c992b | |||
233bb35ebe | |||
f60d0b9753 | |||
7e95c756b9 | |||
be26b92927 | |||
dd3ed1bfb9 | |||
6f56a61a64 | |||
2dee8034d3 | |||
d9d42020cc | |||
90298a2b6c | |||
7c17e7d52f | |||
fbb3ca98c1 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2022.5.1
|
||||
current_version = 2022.5.3
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||
|
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@ -10,6 +10,7 @@ exemptLabels:
|
||||
- enhancement
|
||||
- bug/confirmed
|
||||
- enhancement/confirmed
|
||||
- question
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
|
10
.github/workflows/release-publish.yml
vendored
10
.github/workflows/release-publish.yml
vendored
@ -30,9 +30,9 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik:2022.5.1,
|
||||
beryju/authentik:2022.5.3,
|
||||
beryju/authentik:latest,
|
||||
ghcr.io/goauthentik/server:2022.5.1,
|
||||
ghcr.io/goauthentik/server:2022.5.3,
|
||||
ghcr.io/goauthentik/server:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
@ -69,9 +69,9 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-${{ matrix.type }}:2022.5.1,
|
||||
beryju/authentik-${{ matrix.type }}:2022.5.3,
|
||||
beryju/authentik-${{ matrix.type }}:latest,
|
||||
ghcr.io/goauthentik/${{ matrix.type }}:2022.5.1,
|
||||
ghcr.io/goauthentik/${{ matrix.type }}:2022.5.3,
|
||||
ghcr.io/goauthentik/${{ matrix.type }}:latest
|
||||
file: ${{ matrix.type }}.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@ -152,7 +152,7 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
version: authentik@2022.5.1
|
||||
version: authentik@2022.5.3
|
||||
environment: beryjuorg-prod
|
||||
sourcemaps: './web/dist'
|
||||
url_prefix: '~/static/dist'
|
||||
|
@ -64,8 +64,8 @@ RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends build-essential pkg-config libxmlsec1-dev && \
|
||||
# Required for runtime
|
||||
apt-get install -y --no-install-recommends libxmlsec1-openssl libmaxminddb0 && \
|
||||
# Required for other things
|
||||
apt-get install -y --no-install-recommends runit && \
|
||||
# Required for bootstrap & healtcheck
|
||||
apt-get install -y --no-install-recommends curl runit && \
|
||||
pip install --no-cache-dir -r /requirements.txt && \
|
||||
apt-get remove --purge -y build-essential pkg-config libxmlsec1-dev && \
|
||||
apt-get autoremove --purge -y && \
|
||||
|
4
Makefile
4
Makefile
@ -65,7 +65,7 @@ gen-client-web:
|
||||
docker run \
|
||||
--rm -v ${PWD}:/local \
|
||||
--user ${UID}:${GID} \
|
||||
openapitools/openapi-generator-cli:v6.0.0-beta generate \
|
||||
openapitools/openapi-generator-cli:v6.0.0 generate \
|
||||
-i /local/schema.yml \
|
||||
-g typescript-fetch \
|
||||
-o /local/gen-ts-api \
|
||||
@ -83,7 +83,7 @@ gen-client-go:
|
||||
docker run \
|
||||
--rm -v ${PWD}:/local \
|
||||
--user ${UID}:${GID} \
|
||||
openapitools/openapi-generator-cli:v5.2.1 generate \
|
||||
openapitools/openapi-generator-cli:v6.0.0 generate \
|
||||
-i /local/schema.yml \
|
||||
-g go \
|
||||
-o /local/gen-go-api \
|
||||
|
@ -2,7 +2,7 @@
|
||||
from os import environ
|
||||
from typing import Optional
|
||||
|
||||
__version__ = "2022.5.1"
|
||||
__version__ = "2022.5.3"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -8,9 +8,6 @@ API Browser - {{ tenant.branding_title }}
|
||||
|
||||
{% block head %}
|
||||
<script type="module" src="{% static 'dist/rapidoc-min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<script>
|
||||
function getCookie(name) {
|
||||
let cookieValue = "";
|
||||
@ -34,16 +31,58 @@ window.addEventListener('DOMContentLoaded', (event) => {
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
img.logo {
|
||||
width: 100%;
|
||||
padding: 1rem 0.5rem 1.5rem 0.5rem;
|
||||
min-height: 48px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<rapi-doc
|
||||
spec-url="{{ path }}"
|
||||
heading-text="authentik"
|
||||
theme="dark"
|
||||
render-style="view"
|
||||
heading-text=""
|
||||
theme="light"
|
||||
render-style="read"
|
||||
default-schema-tab="schema"
|
||||
primary-color="#fd4b2d"
|
||||
nav-bg-color="#212427"
|
||||
bg-color="#000000"
|
||||
text-color="#000000"
|
||||
nav-text-color="#ffffff"
|
||||
nav-hover-bg-color="#3c3f42"
|
||||
nav-accent-color="#4f5255"
|
||||
nav-hover-text-color="#ffffff"
|
||||
use-path-in-nav-bar="true"
|
||||
nav-item-spacing="relaxed"
|
||||
allow-server-selection="false"
|
||||
show-header="false"
|
||||
allow-spec-url-load="false"
|
||||
allow-spec-file-load="false">
|
||||
<div slot="logo">
|
||||
<img src="{% static 'dist/assets/icons/icon.png' %}" style="width:50px; height:50px" />
|
||||
<div slot="nav-logo">
|
||||
<img class="logo" src="{% static 'dist/assets/icons/icon_left_brand.png' %}" />
|
||||
</div>
|
||||
</rapi-doc>
|
||||
<script>
|
||||
const rapidoc = document.querySelector("rapi-doc");
|
||||
const matcher = window.matchMedia("(prefers-color-scheme: light)");
|
||||
const changer = (ev) => {
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
let bg, text = "";
|
||||
if (matcher.matches) {
|
||||
bg = style.getPropertyValue('--pf-global--BackgroundColor--light-300');
|
||||
text = style.getPropertyValue('--pf-global--Color--300');
|
||||
} else {
|
||||
bg = style.getPropertyValue('--ak-dark-background');
|
||||
text = style.getPropertyValue('--ak-dark-foreground');
|
||||
}
|
||||
rapidoc.attributes.getNamedItem("bg-color").value = bg.trim();
|
||||
rapidoc.attributes.getNamedItem("text-color").value = text.trim();
|
||||
rapidoc.requestUpdate();
|
||||
};
|
||||
matcher.addEventListener("change", changer);
|
||||
window.addEventListener("load", changer);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
29
authentik/api/tests/test_viewsets.py
Normal file
29
authentik/api/tests/test_viewsets.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""authentik API Modelviewset tests"""
|
||||
from typing import Callable
|
||||
|
||||
from django.test import TestCase
|
||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from authentik.api.v3.urls import router
|
||||
|
||||
|
||||
class TestModelViewSets(TestCase):
|
||||
"""Test Viewset"""
|
||||
|
||||
|
||||
def viewset_tester_factory(test_viewset: type[ModelViewSet]) -> Callable:
|
||||
"""Test Viewset"""
|
||||
|
||||
def tester(self: TestModelViewSets):
|
||||
self.assertIsNotNone(getattr(test_viewset, "search_fields", None))
|
||||
filterset_class = getattr(test_viewset, "filterset_class", None)
|
||||
if not filterset_class:
|
||||
self.assertIsNotNone(getattr(test_viewset, "filterset_fields", None))
|
||||
|
||||
return tester
|
||||
|
||||
|
||||
for _, viewset, _ in router.registry:
|
||||
if not issubclass(viewset, (ModelViewSet, ReadOnlyModelViewSet)):
|
||||
continue
|
||||
setattr(TestModelViewSets, f"test_viewset_{viewset.__name__}", viewset_tester_factory(viewset))
|
@ -89,6 +89,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
"group",
|
||||
]
|
||||
lookup_field = "slug"
|
||||
filterset_fields = ["name", "slug"]
|
||||
ordering = ["name"]
|
||||
|
||||
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
|
||||
|
@ -66,6 +66,7 @@ class SourceViewSet(
|
||||
queryset = Source.objects.none()
|
||||
serializer_class = SourceSerializer
|
||||
lookup_field = "slug"
|
||||
search_fields = ["slug", "name"]
|
||||
|
||||
def get_queryset(self): # pragma: no cover
|
||||
return Source.objects.select_subclasses()
|
||||
|
@ -72,6 +72,7 @@ class UserSerializer(ModelSerializer):
|
||||
)
|
||||
groups_obj = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups")
|
||||
uid = CharField(read_only=True)
|
||||
username = CharField(max_length=150)
|
||||
|
||||
class Meta:
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
|
||||
def create_self_signed(apps, schema_editor):
|
||||
CertificateKeyPair = apps.get_model("authentik_crypto", "CertificateKeyPair")
|
||||
@ -9,7 +11,7 @@ def create_self_signed(apps, schema_editor):
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
|
||||
builder = CertificateBuilder()
|
||||
builder.build()
|
||||
builder.build(subject_alt_names=[f"{generate_id()}.self-signed.goauthentik.io"])
|
||||
CertificateKeyPair.objects.using(db_alias).create(
|
||||
name="authentik Self-signed Certificate",
|
||||
certificate_data=builder.certificate,
|
||||
|
@ -26,3 +26,4 @@ class NotificationWebhookMappingViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = NotificationWebhookMappingSerializer
|
||||
filterset_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
@ -32,3 +32,4 @@ class NotificationRuleViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = NotificationRuleSerializer
|
||||
filterset_fields = ["name", "severity", "group__name"]
|
||||
ordering = ["name"]
|
||||
search_fields = ["name", "group__name"]
|
||||
|
@ -68,6 +68,7 @@ class NotificationTransportViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = NotificationTransport.objects.all()
|
||||
serializer_class = NotificationTransportSerializer
|
||||
filterset_fields = ["name", "mode", "webhook_url", "send_once"]
|
||||
search_fields = ["name", "mode", "webhook_url"]
|
||||
ordering = ["name"]
|
||||
|
||||
@permission_required("authentik_events.change_notificationtransport")
|
||||
|
@ -26,9 +26,9 @@ IGNORED_MODELS = [
|
||||
StaticToken,
|
||||
]
|
||||
if settings.DEBUG:
|
||||
from silk.models import Request, Response
|
||||
from silk.models import Request, Response, SQLQuery
|
||||
|
||||
IGNORED_MODELS += [Request, Response]
|
||||
IGNORED_MODELS += [Request, Response, SQLQuery]
|
||||
IGNORED_MODELS = tuple(IGNORED_MODELS)
|
||||
|
||||
|
||||
|
@ -383,6 +383,7 @@ class Migration(migrations.Migration):
|
||||
models.ManyToManyField(
|
||||
help_text="Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.",
|
||||
to="authentik_events.NotificationTransport",
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -481,6 +481,7 @@ class NotificationRule(PolicyBindingModel):
|
||||
"selected, the notification will only be shown in the authentik UI."
|
||||
)
|
||||
),
|
||||
blank=True,
|
||||
)
|
||||
severity = models.TextField(
|
||||
choices=NotificationSeverity.choices,
|
||||
|
@ -35,3 +35,4 @@ class FlowStageBindingViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = FlowStageBinding.objects.all()
|
||||
serializer_class = FlowStageBindingSerializer
|
||||
filterset_fields = "__all__"
|
||||
search_fields = ["stage__name"]
|
||||
|
@ -9,6 +9,7 @@ from rest_framework.test import APITestCase
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.flows.challenge import ChallengeTypes
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, InvalidResponseAction
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.stages.dummy.models import DummyStage
|
||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||
|
||||
@ -24,8 +25,8 @@ class TestFlowInspector(APITestCase):
|
||||
def test(self):
|
||||
"""test inspector"""
|
||||
flow = Flow.objects.create(
|
||||
name="test-full",
|
||||
slug="test-full",
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
|
||||
|
@ -13,6 +13,26 @@ from authentik.policies.models import PolicyBinding
|
||||
from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
||||
from authentik.stages.user_login.models import UserLoginStage
|
||||
|
||||
STATIC_PROMPT_EXPORT = """{
|
||||
"version": 1,
|
||||
"entries": [
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "cb954fd4-65a5-4ad9-b1ee-180ee9559cf4"
|
||||
},
|
||||
"model": "authentik_stages_prompt.prompt",
|
||||
"attrs": {
|
||||
"field_key": "username",
|
||||
"label": "Username",
|
||||
"type": "username",
|
||||
"required": true,
|
||||
"placeholder": "Username",
|
||||
"order": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}"""
|
||||
|
||||
|
||||
class TestFlowTransfer(TransactionTestCase):
|
||||
"""Test flow transfer"""
|
||||
@ -58,6 +78,22 @@ class TestFlowTransfer(TransactionTestCase):
|
||||
|
||||
self.assertTrue(Flow.objects.filter(slug=flow_slug).exists())
|
||||
|
||||
def test_export_validate_import_re_import(self):
|
||||
"""Test export and import it twice"""
|
||||
count_initial = Prompt.objects.filter(field_key="username").count()
|
||||
|
||||
importer = FlowImporter(STATIC_PROMPT_EXPORT)
|
||||
self.assertTrue(importer.validate())
|
||||
self.assertTrue(importer.apply())
|
||||
|
||||
count_before = Prompt.objects.filter(field_key="username").count()
|
||||
self.assertEqual(count_initial + 1, count_before)
|
||||
|
||||
importer = FlowImporter(STATIC_PROMPT_EXPORT)
|
||||
self.assertTrue(importer.apply())
|
||||
|
||||
self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before)
|
||||
|
||||
def test_export_validate_import_policies(self):
|
||||
"""Test export and validate it"""
|
||||
flow_slug = generate_id()
|
||||
|
@ -115,6 +115,11 @@ class FlowImporter:
|
||||
serializer_kwargs["instance"] = model_instance
|
||||
else:
|
||||
self.logger.debug("initialise new instance", model=model, **updated_identifiers)
|
||||
model_instance = model()
|
||||
# pk needs to be set on the model instance otherwise a new one will be generated
|
||||
if "pk" in updated_identifiers:
|
||||
model_instance.pk = updated_identifiers["pk"]
|
||||
serializer_kwargs["instance"] = model_instance
|
||||
full_data = self.__update_pks_for_attrs(entry.attrs)
|
||||
full_data.update(updated_identifiers)
|
||||
serializer_kwargs["data"] = full_data
|
||||
@ -167,7 +172,7 @@ class FlowImporter:
|
||||
def validate(self) -> bool:
|
||||
"""Validate loaded flow export, ensure all models are allowed
|
||||
and serializers have no errors"""
|
||||
self.logger.debug("Starting flow import validaton")
|
||||
self.logger.debug("Starting flow import validation")
|
||||
if self.__import.version != 1:
|
||||
self.logger.warning("Invalid bundle version")
|
||||
return False
|
||||
|
@ -118,6 +118,7 @@ class DockerServiceConnectionViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = DockerServiceConnectionSerializer
|
||||
filterset_fields = ["name", "local", "url", "tls_verification", "tls_authentication"]
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
|
||||
class KubernetesServiceConnectionSerializer(ServiceConnectionSerializer):
|
||||
@ -152,3 +153,4 @@ class KubernetesServiceConnectionViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = KubernetesServiceConnectionSerializer
|
||||
filterset_fields = ["name", "local"]
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
@ -15,7 +15,7 @@ from yaml import safe_dump
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
|
||||
from authentik.outposts.docker_ssh import DockerInlineSSH
|
||||
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
|
||||
from authentik.outposts.docker_tls import DockerInlineTLS
|
||||
from authentik.outposts.managed import MANAGED_OUTPOST
|
||||
from authentik.outposts.models import (
|
||||
@ -35,6 +35,7 @@ class DockerClient(UpstreamDockerClient, BaseClient):
|
||||
def __init__(self, connection: DockerServiceConnection):
|
||||
self.tls = None
|
||||
self.ssh = None
|
||||
self.logger = get_logger()
|
||||
if connection.local:
|
||||
# Same result as DockerClient.from_env
|
||||
super().__init__(**kwargs_from_env())
|
||||
@ -42,8 +43,12 @@ class DockerClient(UpstreamDockerClient, BaseClient):
|
||||
parsed_url = urlparse(connection.url)
|
||||
tls_config = False
|
||||
if parsed_url.scheme == "ssh":
|
||||
self.ssh = DockerInlineSSH(parsed_url.hostname, connection.tls_authentication)
|
||||
self.ssh.write()
|
||||
try:
|
||||
self.ssh = DockerInlineSSH(parsed_url.hostname, connection.tls_authentication)
|
||||
self.ssh.write()
|
||||
except SSHManagedExternallyException as exc:
|
||||
# SSH config is managed externally
|
||||
self.logger.info(f"SSH Managed externally: {exc}")
|
||||
else:
|
||||
self.tls = DockerInlineTLS(
|
||||
verification_kp=connection.tls_verification,
|
||||
@ -57,7 +62,6 @@ class DockerClient(UpstreamDockerClient, BaseClient):
|
||||
)
|
||||
except SSHException as exc:
|
||||
raise ServiceConnectionInvalid from exc
|
||||
self.logger = get_logger()
|
||||
# Ensure the client actually works
|
||||
self.containers.list()
|
||||
|
||||
|
@ -16,6 +16,10 @@ def opener(path, flags):
|
||||
return os.open(path, flags, 0o700)
|
||||
|
||||
|
||||
class SSHManagedExternallyException(DockerException):
|
||||
"""Raised when the ssh config file is managed externally."""
|
||||
|
||||
|
||||
class DockerInlineSSH:
|
||||
"""Create paramiko ssh config from CertificateKeyPair"""
|
||||
|
||||
@ -29,9 +33,15 @@ class DockerInlineSSH:
|
||||
def __init__(self, host: str, keypair: CertificateKeyPair) -> None:
|
||||
self.host = host
|
||||
self.keypair = keypair
|
||||
self.config_path = Path("~/.ssh/config").expanduser()
|
||||
if self.config_path.exists() and HEADER not in self.config_path.read_text(encoding="utf-8"):
|
||||
# SSH Config file already exists and there's no header from us, meaning that it's
|
||||
# been externally mapped into the container for more complex configs
|
||||
raise SSHManagedExternallyException(
|
||||
"SSH Config exists and does not contain authentik header"
|
||||
)
|
||||
if not self.keypair:
|
||||
raise DockerException("keypair must be set for SSH connections")
|
||||
self.config_path = Path("~/.ssh/config").expanduser()
|
||||
self.header = f"{HEADER} - {self.host}\n"
|
||||
|
||||
def write_config(self, key_path: str) -> bool:
|
||||
|
@ -52,7 +52,7 @@ def m2m_changed_update(sender, instance: Model, action: str, **_):
|
||||
|
||||
@receiver(post_save)
|
||||
# pylint: disable=unused-argument
|
||||
def post_save_update(sender, instance: Model, **_):
|
||||
def post_save_update(sender, instance: Model, created: bool, **_):
|
||||
"""If an Outpost is saved, Ensure that token is created/updated
|
||||
|
||||
If an OutpostModel, or a model that is somehow connected to an OutpostModel is saved,
|
||||
@ -63,6 +63,9 @@ def post_save_update(sender, instance: Model, **_):
|
||||
return
|
||||
if not isinstance(instance, UPDATE_TRIGGERING_MODELS):
|
||||
return
|
||||
if isinstance(instance, Outpost) and created:
|
||||
LOGGER.info("New outpost saved, ensuring initial token and user are created")
|
||||
_ = instance.token
|
||||
outpost_post_save.delay(class_to_path(instance.__class__), instance.pk)
|
||||
|
||||
|
||||
|
@ -21,3 +21,4 @@ class DummyPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = DummyPolicySerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
@ -25,3 +25,4 @@ class EventMatcherPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = EventMatcherPolicySerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
@ -21,3 +21,4 @@ class PasswordExpiryPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = PasswordExpiryPolicySerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
@ -28,3 +28,4 @@ class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = ExpressionPolicySerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
@ -20,4 +20,5 @@ class HaveIBeenPwendPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = HaveIBeenPwendPolicy.objects.all()
|
||||
serializer_class = HaveIBeenPwendPolicySerializer
|
||||
filterset_fields = "__all__"
|
||||
search_fields = ["name", "password_field"]
|
||||
ordering = ["name"]
|
||||
|
@ -30,3 +30,4 @@ class PasswordPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = PasswordPolicySerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
@ -151,5 +151,5 @@ class PolicyProcess(PROCESS_CLASS):
|
||||
try:
|
||||
self.connection.send(self.profiling_wrapper())
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
LOGGER.warning(str(exc))
|
||||
LOGGER.warning("Policy failed to run", exc=exc)
|
||||
self.connection.send(PolicyResult(False, str(exc)))
|
||||
|
@ -26,6 +26,7 @@ class ReputationPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = ReputationPolicy.objects.all()
|
||||
serializer_class = ReputationPolicySerializer
|
||||
filterset_fields = "__all__"
|
||||
search_fields = ["name", "threshold"]
|
||||
ordering = ["name"]
|
||||
|
||||
|
||||
|
@ -47,6 +47,7 @@ class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
"uid_start_number": ["iexact"],
|
||||
"gid_start_number": ["iexact"],
|
||||
}
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
||||
|
||||
@ -81,3 +82,5 @@ class LDAPOutpostConfigViewSet(ReadOnlyModelViewSet):
|
||||
queryset = LDAPProvider.objects.filter(application__isnull=False)
|
||||
serializer_class = LDAPOutpostConfigSerializer
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
filterset_fields = ["name"]
|
||||
|
@ -71,6 +71,7 @@ class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
"property_mappings",
|
||||
"issuer_mode",
|
||||
]
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
||||
@extend_schema(
|
||||
|
@ -39,3 +39,4 @@ class ScopeMappingViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = ScopeMappingSerializer
|
||||
filterset_class = ScopeMappingFilter
|
||||
ordering = ["scope_name", "name"]
|
||||
search_fields = ["name", "scope_name"]
|
||||
|
@ -78,6 +78,28 @@ class TestAuthorize(OAuthTestCase):
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
|
||||
def test_invalid_redirect_uri_regex(self):
|
||||
"""test missing/invalid redirect URI"""
|
||||
OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="+",
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://localhost",
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
|
||||
def test_empty_redirect_uri(self):
|
||||
"""test empty redirect URI (configure in provider)"""
|
||||
OAuth2Provider.objects.create(
|
||||
|
@ -1,7 +1,8 @@
|
||||
"""authentik OAuth2 Authorization views"""
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from re import fullmatch
|
||||
from re import error as RegexError
|
||||
from re import escape, fullmatch
|
||||
from typing import Optional
|
||||
from urllib.parse import parse_qs, urlencode, urlparse, urlsplit, urlunsplit
|
||||
from uuid import uuid4
|
||||
@ -180,16 +181,26 @@ class OAuthAuthorizationParams:
|
||||
|
||||
if self.provider.redirect_uris == "":
|
||||
LOGGER.info("Setting redirect for blank redirect_uris", redirect=self.redirect_uri)
|
||||
self.provider.redirect_uris = self.redirect_uri
|
||||
self.provider.redirect_uris = escape(self.redirect_uri)
|
||||
self.provider.save()
|
||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
||||
|
||||
if not any(fullmatch(x, self.redirect_uri) for x in allowed_redirect_urls):
|
||||
LOGGER.warning(
|
||||
"Invalid redirect uri",
|
||||
redirect_uri=self.redirect_uri,
|
||||
excepted=allowed_redirect_urls,
|
||||
)
|
||||
if self.provider.redirect_uris == "*":
|
||||
LOGGER.info("Converting redirect_uris to regex", redirect=self.redirect_uri)
|
||||
self.provider.redirect_uris = ".*"
|
||||
self.provider.save()
|
||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
||||
|
||||
try:
|
||||
if not any(fullmatch(x, self.redirect_uri) for x in allowed_redirect_urls):
|
||||
LOGGER.warning(
|
||||
"Invalid redirect uri",
|
||||
redirect_uri=self.redirect_uri,
|
||||
excepted=allowed_redirect_urls,
|
||||
)
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||
except RegexError as exc:
|
||||
LOGGER.warning("Invalid regular expression configured", exc=exc)
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||
if self.request:
|
||||
raise AuthorizeError(
|
||||
|
@ -2,6 +2,7 @@
|
||||
from base64 import urlsafe_b64encode
|
||||
from dataclasses import InitVar, dataclass
|
||||
from hashlib import sha256
|
||||
from re import error as RegexError
|
||||
from re import fullmatch
|
||||
from typing import Any, Optional
|
||||
|
||||
@ -149,12 +150,16 @@ class TokenParams:
|
||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
||||
# At this point, no provider should have a blank redirect_uri, in case they do
|
||||
# this will check an empty array and raise an error
|
||||
if not any(fullmatch(x, self.redirect_uri) for x in allowed_redirect_urls):
|
||||
LOGGER.warning(
|
||||
"Invalid redirect uri",
|
||||
redirect_uri=self.redirect_uri,
|
||||
excepted=allowed_redirect_urls,
|
||||
)
|
||||
try:
|
||||
if not any(fullmatch(x, self.redirect_uri) for x in allowed_redirect_urls):
|
||||
LOGGER.warning(
|
||||
"Invalid redirect uri",
|
||||
redirect_uri=self.redirect_uri,
|
||||
excepted=allowed_redirect_urls,
|
||||
)
|
||||
raise TokenError("invalid_client")
|
||||
except RegexError as exc:
|
||||
LOGGER.warning("Invalid regular expression configured", exc=exc)
|
||||
raise TokenError("invalid_client")
|
||||
|
||||
try:
|
||||
@ -297,18 +302,7 @@ class TokenParams:
|
||||
raise TokenError("invalid_grant")
|
||||
|
||||
self.__check_policy_access(app, request, oauth_jwt=token)
|
||||
|
||||
self.user, _ = User.objects.update_or_create(
|
||||
username=f"{self.provider.name}-{token.get('sub')}",
|
||||
defaults={
|
||||
"attributes": {
|
||||
USER_ATTRIBUTE_GENERATED: True,
|
||||
USER_ATTRIBUTE_EXPIRES: token.get("exp"),
|
||||
},
|
||||
"last_login": now(),
|
||||
"name": f"Autogenerated user from application {app.name} (client credentials JWT)",
|
||||
},
|
||||
)
|
||||
self.__create_user_from_jwt(token, app)
|
||||
|
||||
Event.new(
|
||||
action=EventAction.LOGIN,
|
||||
@ -319,6 +313,23 @@ class TokenParams:
|
||||
PLAN_CONTEXT_APPLICATION=app,
|
||||
).from_http(request, user=self.user)
|
||||
|
||||
def __create_user_from_jwt(self, token: dict[str, Any], app: Application):
|
||||
"""Create user from JWT"""
|
||||
exp = token.get("exp")
|
||||
self.user, created = User.objects.update_or_create(
|
||||
username=f"{self.provider.name}-{token.get('sub')}",
|
||||
defaults={
|
||||
"attributes": {
|
||||
USER_ATTRIBUTE_GENERATED: True,
|
||||
},
|
||||
"last_login": now(),
|
||||
"name": f"Autogenerated user from application {app.name} (client credentials JWT)",
|
||||
},
|
||||
)
|
||||
if created and exp:
|
||||
self.user.attributes[USER_ATTRIBUTE_EXPIRES] = exp
|
||||
self.user.save()
|
||||
|
||||
|
||||
class TokenView(View):
|
||||
"""Generate tokens for clients"""
|
||||
|
@ -103,6 +103,7 @@ class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
"redirect_uris": ["iexact"],
|
||||
"cookie_domain": ["iexact"],
|
||||
}
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
||||
|
||||
@ -166,3 +167,5 @@ class ProxyOutpostConfigViewSet(ReadOnlyModelViewSet):
|
||||
queryset = ProxyProvider.objects.filter(application__isnull=False)
|
||||
serializer_class = ProxyOutpostConfigSerializer
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
filterset_fields = ["name"]
|
||||
|
@ -99,6 +99,7 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = SAMLProviderSerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
@extend_schema(
|
||||
responses={
|
||||
@ -216,4 +217,5 @@ class SAMLPropertyMappingViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = SAMLPropertyMapping.objects.all()
|
||||
serializer_class = SAMLPropertyMappingSerializer
|
||||
filterset_class = SAMLPropertyMappingFilter
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
@ -3,6 +3,7 @@ from base64 import b64decode
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from urllib.parse import quote_plus
|
||||
from xml.etree.ElementTree import ParseError # nosec
|
||||
|
||||
import xmlsec
|
||||
from defusedxml import ElementTree
|
||||
@ -175,7 +176,10 @@ class AuthNRequestParser:
|
||||
)
|
||||
except xmlsec.Error as exc:
|
||||
raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc
|
||||
return self._parse_xml(decoded_xml, relay_state)
|
||||
try:
|
||||
return self._parse_xml(decoded_xml, relay_state)
|
||||
except ParseError as exc:
|
||||
raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc
|
||||
|
||||
def idp_initiated(self) -> AuthNRequest:
|
||||
"""Create IdP Initiated AuthNRequest"""
|
||||
|
@ -91,6 +91,7 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
"property_mappings",
|
||||
"property_mappings_group",
|
||||
]
|
||||
search_fields = ["name", "slug"]
|
||||
ordering = ["name"]
|
||||
|
||||
@extend_schema(
|
||||
@ -142,4 +143,5 @@ class LDAPPropertyMappingViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = LDAPPropertyMapping.objects.all()
|
||||
serializer_class = LDAPPropertyMappingSerializer
|
||||
filterset_class = LDAPPropertyMappingFilter
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
@ -102,6 +102,7 @@ class OAuthSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
"consumer_key",
|
||||
"additional_scopes",
|
||||
]
|
||||
search_fields = ["name", "slug"]
|
||||
ordering = ["name"]
|
||||
|
||||
@extend_schema(
|
||||
|
@ -26,6 +26,7 @@ class UserOAuthSourceConnectionViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = UserOAuthSourceConnection.objects.all()
|
||||
serializer_class = UserOAuthSourceConnectionSerializer
|
||||
filterset_fields = ["source__slug"]
|
||||
search_fields = ["source__slug"]
|
||||
permission_classes = [OwnerSuperuserPermissions]
|
||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||
ordering = ["source__slug"]
|
||||
|
@ -60,6 +60,7 @@ class PlexSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
"client_id",
|
||||
"allow_friends",
|
||||
]
|
||||
search_fields = ["name", "slug"]
|
||||
ordering = ["name"]
|
||||
|
||||
@permission_required(None)
|
||||
|
@ -35,3 +35,4 @@ class PlexSourceConnectionViewSet(UsedByMixin, ModelViewSet):
|
||||
permission_classes = [OwnerSuperuserPermissions]
|
||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||
ordering = ["pk"]
|
||||
search_fields = ["source__slug"]
|
||||
|
@ -41,6 +41,7 @@ class SAMLSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = SAMLSourceSerializer
|
||||
lookup_field = "slug"
|
||||
filterset_fields = "__all__"
|
||||
search_fields = ["name", "slug"]
|
||||
ordering = ["name"]
|
||||
|
||||
@extend_schema(responses={200: SAMLMetadataSerializer(many=False)})
|
||||
|
@ -51,6 +51,7 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"client_id",
|
||||
"api_hostname",
|
||||
]
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
||||
@extend_schema(
|
||||
|
@ -36,6 +36,7 @@ class AuthenticatorSMSStageViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = AuthenticatorSMSStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
|
||||
class SMSDeviceSerializer(ModelSerializer):
|
||||
|
@ -29,6 +29,7 @@ class AuthenticatorStaticStageViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = AuthenticatorStaticStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
|
||||
class StaticDeviceTokenSerializer(ModelSerializer):
|
||||
|
@ -29,6 +29,7 @@ class AuthenticatorTOTPStageViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = AuthenticatorTOTPStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
|
||||
class TOTPDeviceSerializer(ModelSerializer):
|
||||
|
@ -41,3 +41,4 @@ class AuthenticatorValidateStageViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = AuthenticatorValidateStageSerializer
|
||||
filterset_fields = ["name", "not_configured_action", "configuration_stages"]
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
@ -168,11 +168,6 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
continue
|
||||
# check if device has been used within threshold and skip this stage if so
|
||||
if threshold.total_seconds() > 0:
|
||||
print("yeet")
|
||||
print(get_device_last_usage(device))
|
||||
print(_now - get_device_last_usage(device))
|
||||
print(threshold)
|
||||
print(_now - get_device_last_usage(device) <= threshold)
|
||||
if _now - get_device_last_usage(device) <= threshold:
|
||||
LOGGER.info("Device has been used within threshold", device=device)
|
||||
raise FlowSkipStageException()
|
||||
|
@ -33,6 +33,7 @@ class AuthenticateWebAuthnStageViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = AuthenticateWebAuthnStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
|
||||
class WebAuthnDeviceSerializer(ModelSerializer):
|
||||
|
@ -22,4 +22,5 @@ class CaptchaStageViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = CaptchaStage.objects.all()
|
||||
serializer_class = CaptchaStageSerializer
|
||||
filterset_fields = ["name", "public_key"]
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
@ -28,6 +28,7 @@ class ConsentStageViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = ConsentStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
|
||||
class UserConsentSerializer(StageSerializer):
|
||||
@ -60,6 +61,7 @@ class UserConsentViewSet(
|
||||
OrderingFilter,
|
||||
SearchFilter,
|
||||
]
|
||||
search_fields = ["user__username"]
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user if self.request else get_anonymous_user()
|
||||
|
@ -22,3 +22,4 @@ class DenyStageViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = DenyStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
@ -21,4 +21,5 @@ class DummyStageViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = DummyStage.objects.all()
|
||||
serializer_class = DummyStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
@ -68,6 +68,7 @@ class EmailStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"template",
|
||||
"activate_user_on_success",
|
||||
]
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
||||
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||
|
@ -40,4 +40,5 @@ class IdentificationStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"passwordless_flow",
|
||||
"show_source_labels",
|
||||
]
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
@ -41,6 +41,7 @@ class InvitationStageViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = InvitationStageSerializer
|
||||
filterset_class = InvitationStageFilter
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
|
||||
class InvitationSerializer(ModelSerializer):
|
||||
|
@ -29,4 +29,5 @@ class PasswordStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"configure_flow",
|
||||
"failed_attempts_before_cancel",
|
||||
]
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
@ -29,6 +29,7 @@ class PromptStageViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = PromptStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
|
||||
class PromptSerializer(ModelSerializer):
|
||||
@ -59,3 +60,4 @@ class PromptViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = Prompt.objects.all().prefetch_related("promptstage_set")
|
||||
serializer_class = PromptSerializer
|
||||
filterset_fields = ["field_key", "label", "type", "placeholder"]
|
||||
search_fields = ["field_key", "label", "type", "placeholder"]
|
||||
|
@ -22,3 +22,4 @@ class UserDeleteStageViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = UserDeleteStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
@ -23,4 +23,5 @@ class UserLoginStageViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = UserLoginStage.objects.all()
|
||||
serializer_class = UserLoginStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
@ -21,4 +21,5 @@ class UserLogoutStageViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = UserLogoutStage.objects.all()
|
||||
serializer_class = UserLogoutStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
@ -21,4 +21,5 @@ class UserWriteStageViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = UserWriteStage.objects.all()
|
||||
serializer_class = UserWriteStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
@ -20,7 +20,7 @@ from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
from authentik.stages.user_write.signals import user_write
|
||||
|
||||
LOGGER = get_logger()
|
||||
PLAN_CONTEXT_GROUPS = "group"
|
||||
PLAN_CONTEXT_GROUPS = "groups"
|
||||
|
||||
|
||||
class UserWriteStageView(StageView):
|
||||
|
@ -124,7 +124,7 @@ func attemptProxyStart(ws *web.WebServer, u *url.URL) {
|
||||
ws.ProxyServer = srv
|
||||
ac.Server = srv
|
||||
l.Debug("attempting to start outpost")
|
||||
err := ac.StartBackgorundTasks()
|
||||
err := ac.StartBackgroundTasks()
|
||||
if err != nil {
|
||||
l.WithError(err).Warning("outpost failed to start")
|
||||
attempt += 1
|
||||
|
@ -1,10 +1,16 @@
|
||||
---
|
||||
version: '3.2'
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:12-alpine
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready"]
|
||||
start_period: 20s
|
||||
interval: 30s
|
||||
retries: 5
|
||||
timeout: 5s
|
||||
volumes:
|
||||
- database:/var/lib/postgresql/data
|
||||
environment:
|
||||
@ -16,8 +22,14 @@ services:
|
||||
redis:
|
||||
image: redis:alpine
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
||||
start_period: 20s
|
||||
interval: 30s
|
||||
retries: 5
|
||||
timeout: 3s
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.5.1}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.5.3}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -38,7 +50,7 @@ services:
|
||||
- "0.0.0.0:${AUTHENTIK_PORT_HTTP:-9000}:9000"
|
||||
- "0.0.0.0:${AUTHENTIK_PORT_HTTPS:-9443}:9443"
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.5.1}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.5.3}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
6
go.mod
6
go.mod
@ -16,7 +16,7 @@ require (
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/imdario/mergo v0.3.12
|
||||
github.com/imdario/mergo v0.3.13
|
||||
github.com/jellydator/ttlcache/v3 v3.0.0
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||
github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba
|
||||
@ -26,7 +26,7 @@ require (
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/stretchr/testify v1.7.1
|
||||
goauthentik.io/api/v3 v3.2022041.10
|
||||
goauthentik.io/api/v3 v3.2022052.6
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b
|
||||
@ -73,5 +73,5 @@ require (
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0 // indirect
|
||||
)
|
||||
|
13
go.sum
13
go.sum
@ -221,8 +221,8 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jellydator/ttlcache/v3 v3.0.0 h1:zmFhqrB/4sKiEiJHhtseJsNRE32IMVmJSs4++4gaQO4=
|
||||
github.com/jellydator/ttlcache/v3 v3.0.0/go.mod h1:WwTaEmcXQ3MTjOm4bsZoDFiCu/hMvNWLO1w67RXz6h4=
|
||||
@ -358,8 +358,10 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
|
||||
goauthentik.io/api/v3 v3.2022041.10 h1:ClHdNzSW//oCv+H6Nr5qQQwKn+fe1sN37lwHEuLWqAA=
|
||||
goauthentik.io/api/v3 v3.2022041.10/go.mod h1:QM9J32HgYE4gL71lWAfAoXSPdSmLVLW08itfLI3Mo10=
|
||||
goauthentik.io/api/v3 v3.2022052.5 h1:kaW52rZZE+wUsp47Ab9OBaLCPNGbqQkCrQWkrbzy14Q=
|
||||
goauthentik.io/api/v3 v3.2022052.5/go.mod h1:QM9J32HgYE4gL71lWAfAoXSPdSmLVLW08itfLI3Mo10=
|
||||
goauthentik.io/api/v3 v3.2022052.6 h1:NF9WLbWWcqOViPhYbJoUUdILnXtOYJrjFmHHqL513wY=
|
||||
goauthentik.io/api/v3 v3.2022052.6/go.mod h1:QM9J32HgYE4gL71lWAfAoXSPdSmLVLW08itfLI3Mo10=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
@ -667,8 +669,9 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@ -25,4 +25,4 @@ func OutpostUserAgent() string {
|
||||
return fmt.Sprintf("goauthentik.io/outpost/%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2022.5.1"
|
||||
const VERSION = "2022.5.3"
|
||||
|
@ -27,7 +27,7 @@ const ConfigLogLevel = "log_level"
|
||||
type APIController struct {
|
||||
Client *api.APIClient
|
||||
Outpost api.Outpost
|
||||
GlobalConfig api.Config
|
||||
GlobalConfig *api.Config
|
||||
|
||||
Server Outpost
|
||||
|
||||
@ -113,7 +113,7 @@ func (a *APIController) Start() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = a.StartBackgorundTasks()
|
||||
err = a.StartBackgroundTasks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -165,7 +165,7 @@ func (a *APIController) OnRefresh() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *APIController) StartBackgorundTasks() error {
|
||||
func (a *APIController) StartBackgroundTasks() error {
|
||||
OutpostInfo.With(prometheus.Labels{
|
||||
"outpost_name": a.Outpost.Name,
|
||||
"outpost_type": a.Server.Type(),
|
||||
|
@ -13,7 +13,9 @@ import (
|
||||
"goauthentik.io/internal/constants"
|
||||
)
|
||||
|
||||
func doGlobalSetup(outpost api.Outpost, globalConfig api.Config) {
|
||||
var initialSetup = false
|
||||
|
||||
func doGlobalSetup(outpost api.Outpost, globalConfig *api.Config) {
|
||||
l := log.WithField("logger", "authentik.outpost")
|
||||
m := outpost.Managed.Get()
|
||||
level, ok := outpost.Config[ConfigLogLevel]
|
||||
@ -38,11 +40,12 @@ func doGlobalSetup(outpost api.Outpost, globalConfig api.Config) {
|
||||
} else {
|
||||
l.Debug("Managed outpost, not setting global log level")
|
||||
}
|
||||
l.WithField("hash", constants.BUILD("tagged")).WithField("version", constants.VERSION).Info("Starting authentik outpost")
|
||||
|
||||
if globalConfig.ErrorReporting.Enabled {
|
||||
dsn := "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8"
|
||||
l.WithField("env", globalConfig.ErrorReporting.Environment).Debug("Error reporting enabled")
|
||||
if !initialSetup {
|
||||
l.WithField("env", globalConfig.ErrorReporting.Environment).Debug("Error reporting enabled")
|
||||
}
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: dsn,
|
||||
Environment: globalConfig.ErrorReporting.Environment,
|
||||
@ -56,6 +59,11 @@ func doGlobalSetup(outpost api.Outpost, globalConfig api.Config) {
|
||||
l.WithField("env", globalConfig.ErrorReporting.Environment).WithError(err).Warning("Failed to initialise sentry")
|
||||
}
|
||||
}
|
||||
|
||||
if !initialSetup {
|
||||
l.WithField("hash", constants.BUILD("tagged")).WithField("version", constants.VERSION).Info("Starting authentik outpost")
|
||||
initialSetup = true
|
||||
}
|
||||
}
|
||||
|
||||
// GetTLSTransport Get a TLS transport instance, that skips verification if configured via environment variables.
|
||||
|
@ -50,7 +50,7 @@ func MockAK(outpost api.Outpost, globalConfig api.Config) *APIController {
|
||||
|
||||
ac := &APIController{
|
||||
Client: apiClient,
|
||||
GlobalConfig: globalConfig,
|
||||
GlobalConfig: &globalConfig,
|
||||
|
||||
token: token,
|
||||
logger: log,
|
||||
|
@ -48,8 +48,8 @@ func (sb *SessionBinder) Bind(username string, req *bind.Request) (ldap.LDAPResu
|
||||
result, err := sb.DirectBinder.Bind(username, req)
|
||||
// Only cache the result if there's been an error
|
||||
if err == nil {
|
||||
flags, ok := sb.si.GetFlags(req.BindDN)
|
||||
if !ok {
|
||||
flags := sb.si.GetFlags(req.BindDN)
|
||||
if flags == nil {
|
||||
sb.log.Error("user flags not set after bind")
|
||||
return result, err
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ type LDAPGroup struct {
|
||||
Member []string
|
||||
IsSuperuser bool
|
||||
IsVirtualGroup bool
|
||||
AKAttributes interface{}
|
||||
AKAttributes map[string]interface{}
|
||||
}
|
||||
|
||||
func (lg *LDAPGroup) Entry() *ldap.Entry {
|
||||
|
@ -38,7 +38,7 @@ type ProviderInstance struct {
|
||||
outpostPk int32
|
||||
searchAllowedGroups []*strfmt.UUID
|
||||
boundUsersMutex sync.RWMutex
|
||||
boundUsers map[string]flags.UserFlags
|
||||
boundUsers map[string]*flags.UserFlags
|
||||
|
||||
uidStartNumber int32
|
||||
gidStartNumber int32
|
||||
@ -68,16 +68,19 @@ func (pi *ProviderInstance) GetOutpostName() string {
|
||||
return pi.outpostName
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetFlags(dn string) (flags.UserFlags, bool) {
|
||||
func (pi *ProviderInstance) GetFlags(dn string) *flags.UserFlags {
|
||||
pi.boundUsersMutex.RLock()
|
||||
defer pi.boundUsersMutex.RUnlock()
|
||||
flags, ok := pi.boundUsers[dn]
|
||||
pi.boundUsersMutex.RUnlock()
|
||||
return flags, ok
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) SetFlags(dn string, flag flags.UserFlags) {
|
||||
pi.boundUsersMutex.Lock()
|
||||
pi.boundUsers[dn] = flag
|
||||
pi.boundUsers[dn] = &flag
|
||||
pi.boundUsersMutex.Unlock()
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@ func (ls *LDAPServer) Refresh() error {
|
||||
|
||||
// Get existing instance so we can transfer boundUsers
|
||||
existing := ls.getCurrentProvider(provider.Pk)
|
||||
users := make(map[string]flags.UserFlags)
|
||||
users := make(map[string]*flags.UserFlags)
|
||||
if existing != nil {
|
||||
existing.boundUsersMutex.RLock()
|
||||
users = existing.boundUsers
|
||||
|
@ -70,8 +70,8 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", req.BindDN, ds.si.GetBaseDN())
|
||||
}
|
||||
|
||||
flags, ok := ds.si.GetFlags(req.BindDN)
|
||||
if !ok {
|
||||
flags := ds.si.GetFlags(req.BindDN)
|
||||
if flags == nil {
|
||||
req.Log().Debug("User info not cached")
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ds.si.GetOutpostName(),
|
||||
@ -149,7 +149,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
return fmt.Errorf("failed to get userinfo")
|
||||
}
|
||||
|
||||
flags.UserInfo = &u
|
||||
flags.UserInfo = u
|
||||
}
|
||||
|
||||
u := make([]api.User, 1)
|
||||
|
@ -16,7 +16,7 @@ func (ms *MemorySearcher) FetchUsers() []api.User {
|
||||
return nil, err
|
||||
}
|
||||
ms.log.WithField("page", page).WithField("count", len(users.Results)).Debug("fetched users")
|
||||
return &users, nil
|
||||
return users, nil
|
||||
}
|
||||
page := 1
|
||||
users := make([]api.User, 0)
|
||||
@ -43,7 +43,7 @@ func (ms *MemorySearcher) FetchGroups() []api.Group {
|
||||
return nil, err
|
||||
}
|
||||
ms.log.WithField("page", page).WithField("count", len(groups.Results)).Debug("fetched groups")
|
||||
return &groups, nil
|
||||
return groups, nil
|
||||
}
|
||||
page := 1
|
||||
groups := make([]api.Group, 0)
|
||||
|
@ -73,8 +73,8 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", req.BindDN, ms.si.GetBaseDN())
|
||||
}
|
||||
|
||||
flags, ok := ms.si.GetFlags(req.BindDN)
|
||||
if !ok {
|
||||
flags := ms.si.GetFlags(req.BindDN)
|
||||
if flags == nil {
|
||||
req.Log().Debug("User info not cached")
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ms.si.GetOutpostName(),
|
||||
@ -141,7 +141,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
if flags.UserPk == u.Pk {
|
||||
//TODO: Is there a better way to clone this object?
|
||||
fg := api.NewGroup(g.Pk, g.NumPk, g.Name, g.Parent, g.ParentName, []int32{flags.UserPk}, []api.GroupMember{u})
|
||||
fg.SetAttributes(*g.Attributes)
|
||||
fg.SetAttributes(g.Attributes)
|
||||
fg.SetIsSuperuser(*g.IsSuperuser)
|
||||
groups = append(groups, group.FromAPIGroup(*fg, ms.si))
|
||||
break
|
||||
|
@ -31,7 +31,7 @@ type LDAPServerInstance interface {
|
||||
|
||||
UsersForGroup(api.Group) []string
|
||||
|
||||
GetFlags(dn string) (flags.UserFlags, bool)
|
||||
GetFlags(dn string) *flags.UserFlags
|
||||
SetFlags(dn string, flags flags.UserFlags)
|
||||
|
||||
GetBaseEntry() *ldap.Entry
|
||||
|
@ -36,13 +36,12 @@ func ldapResolveTypeSingle(in interface{}) *string {
|
||||
}
|
||||
}
|
||||
|
||||
func AKAttrsToLDAP(attrs interface{}) []*ldap.EntryAttribute {
|
||||
func AKAttrsToLDAP(attrs map[string]interface{}) []*ldap.EntryAttribute {
|
||||
attrList := []*ldap.EntryAttribute{}
|
||||
if attrs == nil {
|
||||
return attrList
|
||||
}
|
||||
a := attrs.(*map[string]interface{})
|
||||
for attrKey, attrValue := range *a {
|
||||
for attrKey, attrValue := range attrs {
|
||||
entry := &ldap.EntryAttribute{Name: attrKey}
|
||||
switch t := attrValue.(type) {
|
||||
case []string:
|
||||
|
@ -13,45 +13,45 @@ func Test_ldapResolveTypeSingle_nil(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAKAttrsToLDAP_String(t *testing.T) {
|
||||
var d *map[string]interface{}
|
||||
u := api.User{}
|
||||
|
||||
// normal string
|
||||
d = &map[string]interface{}{
|
||||
u.Attributes = map[string]interface{}{
|
||||
"foo": "bar",
|
||||
}
|
||||
assert.Equal(t, 1, len(AKAttrsToLDAP(d)))
|
||||
assert.Equal(t, "foo", AKAttrsToLDAP(d)[0].Name)
|
||||
assert.Equal(t, []string{"bar"}, AKAttrsToLDAP(d)[0].Values)
|
||||
assert.Equal(t, 1, len(AKAttrsToLDAP(u.Attributes)))
|
||||
assert.Equal(t, "foo", AKAttrsToLDAP(u.Attributes)[0].Name)
|
||||
assert.Equal(t, []string{"bar"}, AKAttrsToLDAP(u.Attributes)[0].Values)
|
||||
// pointer string
|
||||
d = &map[string]interface{}{
|
||||
u.Attributes = map[string]interface{}{
|
||||
"foo": api.PtrString("bar"),
|
||||
}
|
||||
assert.Equal(t, 1, len(AKAttrsToLDAP(d)))
|
||||
assert.Equal(t, "foo", AKAttrsToLDAP(d)[0].Name)
|
||||
assert.Equal(t, []string{"bar"}, AKAttrsToLDAP(d)[0].Values)
|
||||
assert.Equal(t, 1, len(AKAttrsToLDAP(u.Attributes)))
|
||||
assert.Equal(t, "foo", AKAttrsToLDAP(u.Attributes)[0].Name)
|
||||
assert.Equal(t, []string{"bar"}, AKAttrsToLDAP(u.Attributes)[0].Values)
|
||||
}
|
||||
|
||||
func TestAKAttrsToLDAP_String_List(t *testing.T) {
|
||||
var d *map[string]interface{}
|
||||
u := api.User{}
|
||||
// string list
|
||||
d = &map[string]interface{}{
|
||||
u.Attributes = map[string]interface{}{
|
||||
"foo": []string{"bar"},
|
||||
}
|
||||
assert.Equal(t, 1, len(AKAttrsToLDAP(d)))
|
||||
assert.Equal(t, "foo", AKAttrsToLDAP(d)[0].Name)
|
||||
assert.Equal(t, []string{"bar"}, AKAttrsToLDAP(d)[0].Values)
|
||||
assert.Equal(t, 1, len(AKAttrsToLDAP(u.Attributes)))
|
||||
assert.Equal(t, "foo", AKAttrsToLDAP(u.Attributes)[0].Name)
|
||||
assert.Equal(t, []string{"bar"}, AKAttrsToLDAP(u.Attributes)[0].Values)
|
||||
// pointer string list
|
||||
d = &map[string]interface{}{
|
||||
u.Attributes = map[string]interface{}{
|
||||
"foo": &[]string{"bar"},
|
||||
}
|
||||
assert.Equal(t, 1, len(AKAttrsToLDAP(d)))
|
||||
assert.Equal(t, "foo", AKAttrsToLDAP(d)[0].Name)
|
||||
assert.Equal(t, []string{"bar"}, AKAttrsToLDAP(d)[0].Values)
|
||||
assert.Equal(t, 1, len(AKAttrsToLDAP(u.Attributes)))
|
||||
assert.Equal(t, "foo", AKAttrsToLDAP(u.Attributes)[0].Name)
|
||||
assert.Equal(t, []string{"bar"}, AKAttrsToLDAP(u.Attributes)[0].Values)
|
||||
}
|
||||
|
||||
func TestAKAttrsToLDAP_Dict(t *testing.T) {
|
||||
// dict
|
||||
d := &map[string]interface{}{
|
||||
d := map[string]interface{}{
|
||||
"foo": map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
@ -64,7 +64,7 @@ func TestAKAttrsToLDAP_Dict(t *testing.T) {
|
||||
|
||||
func TestAKAttrsToLDAP_Mixed(t *testing.T) {
|
||||
// dict
|
||||
d := &map[string]interface{}{
|
||||
d := map[string]interface{}{
|
||||
"foo": []interface{}{
|
||||
"foo",
|
||||
6,
|
||||
|
@ -149,7 +149,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
|
||||
mux.HandleFunc("/outpost.goauthentik.io/sign_in", a.handleRedirect)
|
||||
mux.HandleFunc("/outpost.goauthentik.io/callback", a.handleCallback)
|
||||
mux.HandleFunc("/outpost.goauthentik.io/sign_out", a.handleSignOut)
|
||||
switch *p.Mode {
|
||||
switch *p.Mode.Get() {
|
||||
case api.PROXYMODE_PROXY:
|
||||
err = a.configureProxy()
|
||||
case api.PROXYMODE_FORWARD_SINGLE:
|
||||
@ -186,7 +186,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
|
||||
}
|
||||
|
||||
func (a *Application) Mode() api.ProxyMode {
|
||||
return *a.proxyConfig.Mode
|
||||
return *a.proxyConfig.Mode.Get()
|
||||
}
|
||||
|
||||
func (a *Application) ProxyConfig() api.ProxyOutpostConfig {
|
||||
|
@ -107,7 +107,7 @@ func (a *Application) ReportMisconfiguration(r *http.Request, msg string, fields
|
||||
Action: api.EVENTACTIONS_CONFIGURATION_ERROR,
|
||||
App: "authentik.providers.proxy", // must match python apps.py name
|
||||
ClientIp: *api.NewNullableString(api.PtrString(r.RemoteAddr)),
|
||||
Context: &fields,
|
||||
Context: fields,
|
||||
}
|
||||
_, _, err := a.ak.Client.EventsApi.EventsEventsCreate(context.Background()).EventRequest(req).Execute()
|
||||
if err != nil {
|
||||
|
@ -19,7 +19,7 @@ func urlMustParse(u string) *url.URL {
|
||||
|
||||
func TestIsAllowlisted_Proxy_Single(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_PROXY.Ptr()
|
||||
a.proxyConfig.Mode = *api.NewNullableProxyMode(api.PROXYMODE_PROXY.Ptr())
|
||||
|
||||
assert.Equal(t, false, a.IsAllowlisted(urlMustParse("")))
|
||||
a.UnauthenticatedRegex = []*regexp.Regexp{
|
||||
@ -30,7 +30,7 @@ func TestIsAllowlisted_Proxy_Single(t *testing.T) {
|
||||
|
||||
func TestIsAllowlisted_Proxy_Domain(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.Mode = *api.NewNullableProxyMode(api.PROXYMODE_FORWARD_DOMAIN.Ptr())
|
||||
|
||||
assert.Equal(t, false, a.IsAllowlisted(urlMustParse("")))
|
||||
a.UnauthenticatedRegex = []*regexp.Regexp{
|
||||
|
@ -56,9 +56,9 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque
|
||||
host := ""
|
||||
s, _ := a.sessions.Get(r, constants.SessionName)
|
||||
// Optional suffix, which is appended to the URL
|
||||
if *a.proxyConfig.Mode == api.PROXYMODE_FORWARD_SINGLE {
|
||||
if *a.proxyConfig.Mode.Get() == api.PROXYMODE_FORWARD_SINGLE {
|
||||
host = web.GetHost(r)
|
||||
} else if *a.proxyConfig.Mode == api.PROXYMODE_FORWARD_DOMAIN {
|
||||
} else if *a.proxyConfig.Mode.Get() == api.PROXYMODE_FORWARD_DOMAIN {
|
||||
eh, err := url.Parse(a.proxyConfig.ExternalHost)
|
||||
if err != nil {
|
||||
a.log.WithField("host", a.proxyConfig.ExternalHost).WithError(err).Warning("invalid external_host")
|
||||
|
@ -106,7 +106,7 @@ func TestForwardHandleNginx_Single_Claims(t *testing.T) {
|
||||
|
||||
func TestForwardHandleNginx_Domain_Blank(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.Mode = *api.NewNullableProxyMode(api.PROXYMODE_FORWARD_DOMAIN.Ptr())
|
||||
a.proxyConfig.CookieDomain = api.PtrString("foo")
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/nginx", nil)
|
||||
|
||||
@ -118,7 +118,7 @@ func TestForwardHandleNginx_Domain_Blank(t *testing.T) {
|
||||
|
||||
func TestForwardHandleNginx_Domain_Header(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.Mode = *api.NewNullableProxyMode(api.PROXYMODE_FORWARD_DOMAIN.Ptr())
|
||||
a.proxyConfig.CookieDomain = api.PtrString("foo")
|
||||
a.proxyConfig.ExternalHost = "http://auth.test.goauthentik.io"
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/nginx", nil)
|
||||
|
@ -100,7 +100,7 @@ func TestForwardHandleTraefik_Single_Claims(t *testing.T) {
|
||||
|
||||
func TestForwardHandleTraefik_Domain_Blank(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.Mode = *api.NewNullableProxyMode(api.PROXYMODE_FORWARD_DOMAIN.Ptr())
|
||||
a.proxyConfig.CookieDomain = api.PtrString("foo")
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/traefik", nil)
|
||||
|
||||
@ -112,7 +112,7 @@ func TestForwardHandleTraefik_Domain_Blank(t *testing.T) {
|
||||
|
||||
func TestForwardHandleTraefik_Domain_Header(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.Mode = *api.NewNullableProxyMode(api.PROXYMODE_FORWARD_DOMAIN.Ptr())
|
||||
a.proxyConfig.CookieDomain = api.PtrString("foo")
|
||||
a.proxyConfig.ExternalHost = "http://auth.test.goauthentik.io"
|
||||
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/traefik", nil)
|
||||
|
@ -34,7 +34,7 @@ func TestCheckRedirectParam(t *testing.T) {
|
||||
|
||||
func TestCheckRedirectParam_Domain(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.Mode = *api.NewNullableProxyMode(api.PROXYMODE_FORWARD_DOMAIN.Ptr())
|
||||
a.proxyConfig.CookieDomain = api.PtrString("t.goauthentik.io")
|
||||
req, _ := http.NewRequest("GET", "https://a.t.goauthentik.io/outpost.goauthentik.io/auth/start", nil)
|
||||
|
||||
|
@ -28,9 +28,8 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
|
||||
} else {
|
||||
rs.SetMaxAge(0)
|
||||
}
|
||||
rs.Options.Path = externalHost.Path
|
||||
rs.Options.Domain = *p.CookieDomain
|
||||
a.log.Debug("using redis session backend")
|
||||
a.log.Trace("using redis session backend")
|
||||
store = rs
|
||||
} else {
|
||||
dir := os.TempDir()
|
||||
@ -49,9 +48,8 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
|
||||
} else {
|
||||
cs.MaxAge(0)
|
||||
}
|
||||
cs.Options.Path = externalHost.Path
|
||||
cs.Options.Domain = *p.CookieDomain
|
||||
a.log.WithField("dir", dir).Debug("using filesystem session backend")
|
||||
a.log.WithField("dir", dir).Trace("using filesystem session backend")
|
||||
store = cs
|
||||
}
|
||||
return store
|
||||
|
@ -17,7 +17,7 @@ func newTestApplication() *Application {
|
||||
CookieSecret: api.PtrString(ak.TestSecret()),
|
||||
ExternalHost: "https://ext.t.goauthentik.io",
|
||||
CookieDomain: api.PtrString(""),
|
||||
Mode: api.PROXYMODE_FORWARD_SINGLE.Ptr(),
|
||||
Mode: *api.NewNullableProxyMode(api.PROXYMODE_FORWARD_SINGLE.Ptr()),
|
||||
SkipPathRegex: api.PtrString("/skip.*"),
|
||||
BasicAuthEnabled: api.PtrBool(true),
|
||||
BasicAuthUserAttribute: api.PtrString("username"),
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
|
||||
func TestRedirectToStart_Proxy(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_PROXY.Ptr()
|
||||
a.proxyConfig.Mode = *api.NewNullableProxyMode(api.PROXYMODE_PROXY.Ptr())
|
||||
a.proxyConfig.ExternalHost = "https://test.goauthentik.io"
|
||||
req, _ := http.NewRequest("GET", "/foo/bar/baz", nil)
|
||||
|
||||
@ -29,7 +29,7 @@ func TestRedirectToStart_Proxy(t *testing.T) {
|
||||
|
||||
func TestRedirectToStart_Forward(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_SINGLE.Ptr()
|
||||
a.proxyConfig.Mode = *api.NewNullableProxyMode(api.PROXYMODE_FORWARD_SINGLE.Ptr())
|
||||
a.proxyConfig.ExternalHost = "https://test.goauthentik.io"
|
||||
req, _ := http.NewRequest("GET", "/foo/bar/baz", nil)
|
||||
|
||||
@ -47,7 +47,7 @@ func TestRedirectToStart_Forward(t *testing.T) {
|
||||
func TestRedirectToStart_Forward_Domain_Invalid(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.CookieDomain = api.PtrString("foo")
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.Mode = *api.NewNullableProxyMode(api.PROXYMODE_FORWARD_DOMAIN.Ptr())
|
||||
a.proxyConfig.ExternalHost = "https://test.goauthentik.io"
|
||||
req, _ := http.NewRequest("GET", "/foo/bar/baz", nil)
|
||||
|
||||
@ -65,7 +65,7 @@ func TestRedirectToStart_Forward_Domain_Invalid(t *testing.T) {
|
||||
func TestRedirectToStart_Forward_Domain(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.CookieDomain = api.PtrString("goauthentik.io")
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.Mode = *api.NewNullableProxyMode(api.PROXYMODE_FORWARD_DOMAIN.Ptr())
|
||||
a.proxyConfig.ExternalHost = "https://test.goauthentik.io"
|
||||
req, _ := http.NewRequest("GET", "/foo/bar/baz", nil)
|
||||
|
||||
|
@ -50,7 +50,7 @@ func (ps *ProxyServer) lookupApp(r *http.Request) (*application.Application, str
|
||||
// Try to find application by directly looking up host first (proxy, forward_auth_single)
|
||||
a, ok := ps.apps[host]
|
||||
if ok {
|
||||
ps.log.WithField("host", host).WithField("app", a.ProxyConfig().Name).Debug("Found app based direct host match")
|
||||
ps.log.WithField("host", host).WithField("app", a.ProxyConfig().Name).Trace("Found app based direct host match")
|
||||
return a, host
|
||||
}
|
||||
// For forward_auth_domain, we don't have a direct app to domain relationship
|
||||
|
92
poetry.lock
generated
92
poetry.lock
generated
@ -477,14 +477,14 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "6.3.3"
|
||||
version = "6.4"
|
||||
description = "Code coverage measurement for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
tomli = {version = "*", optional = true, markers = "extra == \"toml\""}
|
||||
tomli = {version = "*", optional = true, markers = "python_version < \"3.11\" and extra == \"toml\""}
|
||||
|
||||
[package.extras]
|
||||
toml = ["tomli"]
|
||||
@ -927,7 +927,7 @@ python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "4.11.3"
|
||||
version = "4.11.4"
|
||||
description = "Read metadata from Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -2405,47 +2405,47 @@ constantly = [
|
||||
{file = "constantly-15.1.0.tar.gz", hash = "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35"},
|
||||
]
|
||||
coverage = [
|
||||
{file = "coverage-6.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df32ee0f4935a101e4b9a5f07b617d884a531ed5666671ff6ac66d2e8e8246d8"},
|
||||
{file = "coverage-6.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75b5dbffc334e0beb4f6c503fb95e6d422770fd2d1b40a64898ea26d6c02742d"},
|
||||
{file = "coverage-6.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:114944e6061b68a801c5da5427b9173a0dd9d32cd5fcc18a13de90352843737d"},
|
||||
{file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab88a01cd180b5640ccc9c47232e31924d5f9967ab7edd7e5c91c68eee47a69"},
|
||||
{file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad8f9068f5972a46d50fe5f32c09d6ee11da69c560fcb1b4c3baea246ca4109b"},
|
||||
{file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4cd696aa712e6cd16898d63cf66139dc70d998f8121ab558f0e1936396dbc579"},
|
||||
{file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c1a9942e282cc9d3ed522cd3e3cab081149b27ea3bda72d6f61f84eaf88c1a63"},
|
||||
{file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c06455121a089252b5943ea682187a4e0a5cf0a3fb980eb8e7ce394b144430a9"},
|
||||
{file = "coverage-6.3.3-cp310-cp310-win32.whl", hash = "sha256:cb5311d6ccbd22578c80028c5e292a7ab9adb91bd62c1982087fad75abe2e63d"},
|
||||
{file = "coverage-6.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:6d4a6f30f611e657495cc81a07ff7aa8cd949144e7667c5d3e680d73ba7a70e4"},
|
||||
{file = "coverage-6.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:79bf405432428e989cad7b8bc60581963238f7645ae8a404f5dce90236cc0293"},
|
||||
{file = "coverage-6.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:338c417613f15596af9eb7a39353b60abec9d8ce1080aedba5ecee6a5d85f8d3"},
|
||||
{file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db094a6a4ae6329ed322a8973f83630b12715654c197dd392410400a5bfa1a73"},
|
||||
{file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1414e8b124611bf4df8d77215bd32cba6e3425da8ce9c1f1046149615e3a9a31"},
|
||||
{file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:93b16b08f94c92cab88073ffd185070cdcb29f1b98df8b28e6649145b7f2c90d"},
|
||||
{file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fbc86ae8cc129c801e7baaafe3addf3c8d49c9c1597c44bdf2d78139707c3c62"},
|
||||
{file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b5ba058610e8289a07db2a57bce45a1793ec0d3d11db28c047aae2aa1a832572"},
|
||||
{file = "coverage-6.3.3-cp37-cp37m-win32.whl", hash = "sha256:8329635c0781927a2c6ae068461e19674c564e05b86736ab8eb29c420ee7dc20"},
|
||||
{file = "coverage-6.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:e5af1feee71099ae2e3b086ec04f57f9950e1be9ecf6c420696fea7977b84738"},
|
||||
{file = "coverage-6.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e814a4a5a1d95223b08cdb0f4f57029e8eab22ffdbae2f97107aeef28554517e"},
|
||||
{file = "coverage-6.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61f4fbf3633cb0713437291b8848634ea97f89c7e849c2be17a665611e433f53"},
|
||||
{file = "coverage-6.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3401b0d2ed9f726fadbfa35102e00d1b3547b73772a1de5508ef3bdbcb36afe7"},
|
||||
{file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8586b177b4407f988731eb7f41967415b2197f35e2a6ee1a9b9b561f6323c8e9"},
|
||||
{file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:892e7fe32191960da559a14536768a62e83e87bbb867e1b9c643e7e0fbce2579"},
|
||||
{file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:afb03f981fadb5aed1ac6e3dd34f0488e1a0875623d557b6fad09b97a942b38a"},
|
||||
{file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cbe91bc84be4e5ef0b1480d15c7b18e29c73bdfa33e07d3725da7d18e1b0aff2"},
|
||||
{file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:91502bf27cbd5c83c95cfea291ef387469f2387508645602e1ca0fd8a4ba7548"},
|
||||
{file = "coverage-6.3.3-cp38-cp38-win32.whl", hash = "sha256:c488db059848702aff30aa1d90ef87928d4e72e4f00717343800546fdbff0a94"},
|
||||
{file = "coverage-6.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6534fcdfb5c503affb6b1130db7b5bfc8a0f77fa34880146f7a5c117987d0"},
|
||||
{file = "coverage-6.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc692c9ee18f0dd3214843779ba6b275ee4bb9b9a5745ba64265bce911aefd1a"},
|
||||
{file = "coverage-6.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:462105283de203df8de58a68c1bb4ba2a8a164097c2379f664fa81d6baf94b81"},
|
||||
{file = "coverage-6.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc972d829ad5ef4d4c5fcabd2bbe2add84ce8236f64ba1c0c72185da3a273130"},
|
||||
{file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06f54765cdbce99901871d50fe9f41d58213f18e98b170a30ca34f47de7dd5e8"},
|
||||
{file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7835f76a081787f0ca62a53504361b3869840a1620049b56d803a8cb3a9eeea3"},
|
||||
{file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6f5fee77ec3384b934797f1873758f796dfb4f167e1296dc00f8b2e023ce6ee9"},
|
||||
{file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:baa8be8aba3dd1e976e68677be68a960a633a6d44c325757aefaa4d66175050f"},
|
||||
{file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d06380e777dd6b35ee936f333d55b53dc4a8271036ff884c909cf6e94be8b6c"},
|
||||
{file = "coverage-6.3.3-cp39-cp39-win32.whl", hash = "sha256:f8cabc5fd0091976ab7b020f5708335033e422de25e20ddf9416bdce2b7e07d8"},
|
||||
{file = "coverage-6.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c9441d57b0963cf8340268ad62fc83de61f1613034b79c2b1053046af0c5284"},
|
||||
{file = "coverage-6.3.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:d522f1dc49127eab0bfbba4e90fa068ecff0899bbf61bf4065c790ddd6c177fe"},
|
||||
{file = "coverage-6.3.3.tar.gz", hash = "sha256:2781c43bffbbec2b8867376d4d61916f5e9c4cc168232528562a61d1b4b01879"},
|
||||
{file = "coverage-6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50ed480b798febce113709846b11f5d5ed1e529c88d8ae92f707806c50297abf"},
|
||||
{file = "coverage-6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:26f8f92699756cb7af2b30720de0c5bb8d028e923a95b6d0c891088025a1ac8f"},
|
||||
{file = "coverage-6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60c2147921da7f4d2d04f570e1838db32b95c5509d248f3fe6417e91437eaf41"},
|
||||
{file = "coverage-6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:750e13834b597eeb8ae6e72aa58d1d831b96beec5ad1d04479ae3772373a8088"},
|
||||
{file = "coverage-6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af5b9ee0fc146e907aa0f5fb858c3b3da9199d78b7bb2c9973d95550bd40f701"},
|
||||
{file = "coverage-6.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a022394996419142b33a0cf7274cb444c01d2bb123727c4bb0b9acabcb515dea"},
|
||||
{file = "coverage-6.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5a78cf2c43b13aa6b56003707c5203f28585944c277c1f3f109c7b041b16bd39"},
|
||||
{file = "coverage-6.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9229d074e097f21dfe0643d9d0140ee7433814b3f0fc3706b4abffd1e3038632"},
|
||||
{file = "coverage-6.4-cp310-cp310-win32.whl", hash = "sha256:fb45fe08e1abc64eb836d187b20a59172053999823f7f6ef4f18a819c44ba16f"},
|
||||
{file = "coverage-6.4-cp310-cp310-win_amd64.whl", hash = "sha256:3cfd07c5889ddb96a401449109a8b97a165be9d67077df6802f59708bfb07720"},
|
||||
{file = "coverage-6.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:03014a74023abaf5a591eeeaf1ac66a73d54eba178ff4cb1fa0c0a44aae70383"},
|
||||
{file = "coverage-6.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c82f2cd69c71698152e943f4a5a6b83a3ab1db73b88f6e769fabc86074c3b08"},
|
||||
{file = "coverage-6.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b546cf2b1974ddc2cb222a109b37c6ed1778b9be7e6b0c0bc0cf0438d9e45a6"},
|
||||
{file = "coverage-6.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc173f1ce9ffb16b299f51c9ce53f66a62f4d975abe5640e976904066f3c835d"},
|
||||
{file = "coverage-6.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c53ad261dfc8695062fc8811ac7c162bd6096a05a19f26097f411bdf5747aee7"},
|
||||
{file = "coverage-6.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:eef5292b60b6de753d6e7f2d128d5841c7915fb1e3321c3a1fe6acfe76c38052"},
|
||||
{file = "coverage-6.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:543e172ce4c0de533fa892034cce260467b213c0ea8e39da2f65f9a477425211"},
|
||||
{file = "coverage-6.4-cp37-cp37m-win32.whl", hash = "sha256:00c8544510f3c98476bbd58201ac2b150ffbcce46a8c3e4fb89ebf01998f806a"},
|
||||
{file = "coverage-6.4-cp37-cp37m-win_amd64.whl", hash = "sha256:b84ab65444dcc68d761e95d4d70f3cfd347ceca5a029f2ffec37d4f124f61311"},
|
||||
{file = "coverage-6.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d548edacbf16a8276af13063a2b0669d58bbcfca7c55a255f84aac2870786a61"},
|
||||
{file = "coverage-6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:033ebec282793bd9eb988d0271c211e58442c31077976c19c442e24d827d356f"},
|
||||
{file = "coverage-6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742fb8b43835078dd7496c3c25a1ec8d15351df49fb0037bffb4754291ef30ce"},
|
||||
{file = "coverage-6.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d55fae115ef9f67934e9f1103c9ba826b4c690e4c5bcf94482b8b2398311bf9c"},
|
||||
{file = "coverage-6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd698341626f3c77784858427bad0cdd54a713115b423d22ac83a28303d1d95"},
|
||||
{file = "coverage-6.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:62d382f7d77eeeaff14b30516b17bcbe80f645f5cf02bb755baac376591c653c"},
|
||||
{file = "coverage-6.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:016d7f5cf1c8c84f533a3c1f8f36126fbe00b2ec0ccca47cc5731c3723d327c6"},
|
||||
{file = "coverage-6.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:69432946f154c6add0e9ede03cc43b96e2ef2733110a77444823c053b1ff5166"},
|
||||
{file = "coverage-6.4-cp38-cp38-win32.whl", hash = "sha256:83bd142cdec5e4a5c4ca1d4ff6fa807d28460f9db919f9f6a31babaaa8b88426"},
|
||||
{file = "coverage-6.4-cp38-cp38-win_amd64.whl", hash = "sha256:4002f9e8c1f286e986fe96ec58742b93484195defc01d5cc7809b8f7acb5ece3"},
|
||||
{file = "coverage-6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e4f52c272fdc82e7c65ff3f17a7179bc5f710ebc8ce8a5cadac81215e8326740"},
|
||||
{file = "coverage-6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b5578efe4038be02d76c344007b13119b2b20acd009a88dde8adec2de4f630b5"},
|
||||
{file = "coverage-6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8099ea680201c2221f8468c372198ceba9338a5fec0e940111962b03b3f716a"},
|
||||
{file = "coverage-6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a00441f5ea4504f5abbc047589d09e0dc33eb447dc45a1a527c8b74bfdd32c65"},
|
||||
{file = "coverage-6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e76bd16f0e31bc2b07e0fb1379551fcd40daf8cdf7e24f31a29e442878a827c"},
|
||||
{file = "coverage-6.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8d2e80dd3438e93b19e1223a9850fa65425e77f2607a364b6fd134fcd52dc9df"},
|
||||
{file = "coverage-6.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:341e9c2008c481c5c72d0e0dbf64980a4b2238631a7f9780b0fe2e95755fb018"},
|
||||
{file = "coverage-6.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:21e6686a95025927775ac501e74f5940cdf6fe052292f3a3f7349b0abae6d00f"},
|
||||
{file = "coverage-6.4-cp39-cp39-win32.whl", hash = "sha256:968ed5407f9460bd5a591cefd1388cc00a8f5099de9e76234655ae48cfdbe2c3"},
|
||||
{file = "coverage-6.4-cp39-cp39-win_amd64.whl", hash = "sha256:e35217031e4b534b09f9b9a5841b9344a30a6357627761d4218818b865d45055"},
|
||||
{file = "coverage-6.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:e637ae0b7b481905358624ef2e81d7fb0b1af55f5ff99f9ba05442a444b11e45"},
|
||||
{file = "coverage-6.4.tar.gz", hash = "sha256:727dafd7f67a6e1cad808dc884bd9c5a2f6ef1f8f6d2f22b37b96cb0080d4f49"},
|
||||
]
|
||||
cryptography = [
|
||||
{file = "cryptography-36.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6"},
|
||||
@ -2733,8 +2733,8 @@ idna = [
|
||||
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
||||
]
|
||||
importlib-metadata = [
|
||||
{file = "importlib_metadata-4.11.3-py3-none-any.whl", hash = "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6"},
|
||||
{file = "importlib_metadata-4.11.3.tar.gz", hash = "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539"},
|
||||
{file = "importlib_metadata-4.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"},
|
||||
{file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"},
|
||||
]
|
||||
incremental = [
|
||||
{file = "incremental-21.3.0-py2.py3-none-any.whl", hash = "sha256:92014aebc6a20b78a8084cdd5645eeaa7f74b8933f70fa3ada2cfbd1e3b54321"},
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user