Compare commits
38 Commits
version/20
...
version-20
Author | SHA1 | Date | |
---|---|---|---|
fb0a88f2cf | |||
4d8d405e70 | |||
1d5f399b61 | |||
bb575fcc10 | |||
13fd1afbb9 | |||
f059b998cc | |||
3f48202dfe | |||
2a3ebb616b | |||
ceab1f732d | |||
01d2cce9ca | |||
fd9293e3e8 | |||
520de8d5b0 | |||
bbdb0df42e | |||
9310d4cdc0 | |||
86f9056d3f | |||
5375637eda | |||
109f06c3ae | |||
a3744da3a5 | |||
ff1feb653b | |||
4a11d89a08 | |||
73d7b5f110 | |||
8b7a92068b | |||
ff1532da13 | |||
6eafa2346d | |||
681644b854 | |||
de4d388e0a | |||
cbe2cb51e7 | |||
9176c71075 | |||
1c05e4ca09 | |||
2d55d3c743 | |||
0a9482b28a | |||
4b1440944e | |||
75794defc6 | |||
59a92dbacd | |||
b81ddf2b80 | |||
9ccd1ce08b | |||
6f6d22da13 | |||
095850f038 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2023.3.0
|
||||
current_version = 2023.3.1
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
||||
|
@ -7,8 +7,11 @@ charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[html]
|
||||
[*.html]
|
||||
indent_size = 2
|
||||
|
||||
[yaml]
|
||||
[*.{yaml,yml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
@ -6,9 +6,8 @@ Authentik takes security very seriously. We follow the rules of [responsible dis
|
||||
|
||||
| Version | Supported |
|
||||
| --------- | ------------------ |
|
||||
| 2022.12.x | :white_check_mark: |
|
||||
| 2023.1.x | :white_check_mark: |
|
||||
| 2023.2.x | :white_check_mark: |
|
||||
| 2023.3.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
from os import environ
|
||||
from typing import Optional
|
||||
|
||||
__version__ = "2023.3.0"
|
||||
__version__ = "2023.3.1"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -55,11 +55,11 @@ class AuthentikBlueprintsConfig(ManagedAppConfig):
|
||||
"""Load v1 tasks"""
|
||||
self.import_module("authentik.blueprints.v1.tasks")
|
||||
|
||||
def reconcile_blueprints_discover(self):
|
||||
def reconcile_blueprints_discovery(self):
|
||||
"""Run blueprint discovery"""
|
||||
from authentik.blueprints.v1.tasks import blueprints_discover, clear_failed_blueprints
|
||||
from authentik.blueprints.v1.tasks import blueprints_discovery, clear_failed_blueprints
|
||||
|
||||
blueprints_discover.delay()
|
||||
blueprints_discovery.delay()
|
||||
clear_failed_blueprints.delay()
|
||||
|
||||
def import_models(self):
|
||||
|
@ -19,10 +19,8 @@ class Command(BaseCommand):
|
||||
for blueprint_path in options.get("blueprints", []):
|
||||
content = BlueprintInstance(path=blueprint_path).retrieve()
|
||||
importer = Importer(content)
|
||||
valid, logs = importer.validate()
|
||||
valid, _ = importer.validate()
|
||||
if not valid:
|
||||
for log in logs:
|
||||
getattr(LOGGER, log.pop("log_level"))(**log)
|
||||
self.stderr.write("blueprint invalid")
|
||||
sys_exit(1)
|
||||
importer.apply()
|
||||
|
@ -5,7 +5,7 @@ from authentik.lib.utils.time import fqdn_rand
|
||||
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
"blueprints_v1_discover": {
|
||||
"task": "authentik.blueprints.v1.tasks.blueprints_discover",
|
||||
"task": "authentik.blueprints.v1.tasks.blueprints_discovery",
|
||||
"schedule": crontab(minute=fqdn_rand("blueprints_v1_discover"), hour="*"),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
},
|
||||
|
@ -1,6 +1,5 @@
|
||||
"""Blueprint helpers"""
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
from django.apps import apps
|
||||
@ -45,13 +44,3 @@ def reconcile_app(app_name: str):
|
||||
return wrapper
|
||||
|
||||
return wrapper_outer
|
||||
|
||||
|
||||
def load_yaml_fixture(path: str, **kwargs) -> str:
|
||||
"""Load yaml fixture, optionally formatting it with kwargs"""
|
||||
with open(Path(__file__).resolve().parent / Path(path), "r", encoding="utf-8") as _fixture:
|
||||
fixture = _fixture.read()
|
||||
try:
|
||||
return fixture % kwargs
|
||||
except TypeError:
|
||||
return fixture
|
||||
|
@ -3,12 +3,12 @@ from os import environ
|
||||
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from authentik.blueprints.tests import load_yaml_fixture
|
||||
from authentik.blueprints.v1.exporter import FlowExporter
|
||||
from authentik.blueprints.v1.importer import Importer, transaction_rollback
|
||||
from authentik.core.models import Group
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import load_fixture
|
||||
from authentik.policies.expression.models import ExpressionPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
@ -113,14 +113,14 @@ class TestBlueprintsV1(TransactionTestCase):
|
||||
"""Test export and import it twice"""
|
||||
count_initial = Prompt.objects.filter(field_key="username").count()
|
||||
|
||||
importer = Importer(load_yaml_fixture("fixtures/static_prompt_export.yaml"))
|
||||
importer = Importer(load_fixture("fixtures/static_prompt_export.yaml"))
|
||||
self.assertTrue(importer.validate()[0])
|
||||
self.assertTrue(importer.apply())
|
||||
|
||||
count_before = Prompt.objects.filter(field_key="username").count()
|
||||
self.assertEqual(count_initial + 1, count_before)
|
||||
|
||||
importer = Importer(load_yaml_fixture("fixtures/static_prompt_export.yaml"))
|
||||
importer = Importer(load_fixture("fixtures/static_prompt_export.yaml"))
|
||||
self.assertTrue(importer.apply())
|
||||
|
||||
self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before)
|
||||
@ -130,7 +130,7 @@ class TestBlueprintsV1(TransactionTestCase):
|
||||
ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").delete()
|
||||
Group.objects.filter(name="test").delete()
|
||||
environ["foo"] = generate_id()
|
||||
importer = Importer(load_yaml_fixture("fixtures/tags.yaml"), {"bar": "baz"})
|
||||
importer = Importer(load_fixture("fixtures/tags.yaml"), {"bar": "baz"})
|
||||
self.assertTrue(importer.validate()[0])
|
||||
self.assertTrue(importer.apply())
|
||||
policy = ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").first()
|
||||
|
@ -1,10 +1,10 @@
|
||||
"""Test blueprints v1"""
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from authentik.blueprints.tests import load_yaml_fixture
|
||||
from authentik.blueprints.v1.importer import Importer
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import load_fixture
|
||||
|
||||
|
||||
class TestBlueprintsV1Conditions(TransactionTestCase):
|
||||
@ -14,7 +14,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
|
||||
"""Test conditions fulfilled"""
|
||||
flow_slug1 = generate_id()
|
||||
flow_slug2 = generate_id()
|
||||
import_yaml = load_yaml_fixture(
|
||||
import_yaml = load_fixture(
|
||||
"fixtures/conditions_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
|
||||
)
|
||||
|
||||
@ -31,7 +31,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
|
||||
"""Test conditions not fulfilled"""
|
||||
flow_slug1 = generate_id()
|
||||
flow_slug2 = generate_id()
|
||||
import_yaml = load_yaml_fixture(
|
||||
import_yaml = load_fixture(
|
||||
"fixtures/conditions_not_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
|
||||
)
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
"""Test blueprints v1"""
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from authentik.blueprints.tests import load_yaml_fixture
|
||||
from authentik.blueprints.v1.importer import Importer
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import load_fixture
|
||||
|
||||
|
||||
class TestBlueprintsV1State(TransactionTestCase):
|
||||
@ -13,7 +13,7 @@ class TestBlueprintsV1State(TransactionTestCase):
|
||||
def test_state_present(self):
|
||||
"""Test state present"""
|
||||
flow_slug = generate_id()
|
||||
import_yaml = load_yaml_fixture("fixtures/state_present.yaml", id=flow_slug)
|
||||
import_yaml = load_fixture("fixtures/state_present.yaml", id=flow_slug)
|
||||
|
||||
importer = Importer(import_yaml)
|
||||
self.assertTrue(importer.validate()[0])
|
||||
@ -39,7 +39,7 @@ class TestBlueprintsV1State(TransactionTestCase):
|
||||
def test_state_created(self):
|
||||
"""Test state created"""
|
||||
flow_slug = generate_id()
|
||||
import_yaml = load_yaml_fixture("fixtures/state_created.yaml", id=flow_slug)
|
||||
import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug)
|
||||
|
||||
importer = Importer(import_yaml)
|
||||
self.assertTrue(importer.validate()[0])
|
||||
@ -65,7 +65,7 @@ class TestBlueprintsV1State(TransactionTestCase):
|
||||
def test_state_absent(self):
|
||||
"""Test state absent"""
|
||||
flow_slug = generate_id()
|
||||
import_yaml = load_yaml_fixture("fixtures/state_created.yaml", id=flow_slug)
|
||||
import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug)
|
||||
|
||||
importer = Importer(import_yaml)
|
||||
self.assertTrue(importer.validate()[0])
|
||||
@ -74,7 +74,7 @@ class TestBlueprintsV1State(TransactionTestCase):
|
||||
flow: Flow = Flow.objects.filter(slug=flow_slug).first()
|
||||
self.assertEqual(flow.slug, flow_slug)
|
||||
|
||||
import_yaml = load_yaml_fixture("fixtures/state_absent.yaml", id=flow_slug)
|
||||
import_yaml = load_fixture("fixtures/state_absent.yaml", id=flow_slug)
|
||||
importer = Importer(import_yaml)
|
||||
self.assertTrue(importer.validate()[0])
|
||||
self.assertTrue(importer.apply())
|
||||
|
@ -6,7 +6,7 @@ from django.test import TransactionTestCase
|
||||
from yaml import dump
|
||||
|
||||
from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus
|
||||
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_discover, blueprints_find
|
||||
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_discovery, blueprints_find
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
@ -53,7 +53,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
|
||||
file.seek(0)
|
||||
file_hash = sha512(file.read().encode()).hexdigest()
|
||||
file.flush()
|
||||
blueprints_discover() # pylint: disable=no-value-for-parameter
|
||||
blueprints_discovery() # pylint: disable=no-value-for-parameter
|
||||
instance = BlueprintInstance.objects.filter(name=blueprint_id).first()
|
||||
self.assertEqual(instance.last_applied_hash, file_hash)
|
||||
self.assertEqual(
|
||||
@ -81,7 +81,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
|
||||
)
|
||||
)
|
||||
file.flush()
|
||||
blueprints_discover() # pylint: disable=no-value-for-parameter
|
||||
blueprints_discovery() # pylint: disable=no-value-for-parameter
|
||||
blueprint = BlueprintInstance.objects.filter(name="foo").first()
|
||||
self.assertEqual(
|
||||
blueprint.last_applied_hash,
|
||||
@ -106,7 +106,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
|
||||
)
|
||||
)
|
||||
file.flush()
|
||||
blueprints_discover() # pylint: disable=no-value-for-parameter
|
||||
blueprints_discovery() # pylint: disable=no-value-for-parameter
|
||||
blueprint.refresh_from_db()
|
||||
self.assertEqual(
|
||||
blueprint.last_applied_hash,
|
||||
|
@ -40,6 +40,10 @@ from authentik.lib.models import SerializerModel
|
||||
from authentik.outposts.models import OutpostServiceConnection
|
||||
from authentik.policies.models import Policy, PolicyBindingModel
|
||||
|
||||
# Context set when the serializer is created in a blueprint context
|
||||
# Update website/developer-docs/blueprints/v1/models.md when used
|
||||
SERIALIZER_CONTEXT_BLUEPRINT = "blueprint_entry"
|
||||
|
||||
|
||||
def is_model_allowed(model: type[Model]) -> bool:
|
||||
"""Check if model is allowed"""
|
||||
@ -158,7 +162,12 @@ class Importer:
|
||||
raise EntryInvalidError(f"Model {model} not allowed")
|
||||
if issubclass(model, BaseMetaModel):
|
||||
serializer_class: type[Serializer] = model.serializer()
|
||||
serializer = serializer_class(data=entry.get_attrs(self.__import))
|
||||
serializer = serializer_class(
|
||||
data=entry.get_attrs(self.__import),
|
||||
context={
|
||||
SERIALIZER_CONTEXT_BLUEPRINT: entry,
|
||||
},
|
||||
)
|
||||
try:
|
||||
serializer.is_valid(raise_exception=True)
|
||||
except ValidationError as exc:
|
||||
@ -217,7 +226,12 @@ class Importer:
|
||||
always_merger.merge(full_data, updated_identifiers)
|
||||
serializer_kwargs["data"] = full_data
|
||||
|
||||
serializer: Serializer = model().serializer(**serializer_kwargs)
|
||||
serializer: Serializer = model().serializer(
|
||||
context={
|
||||
SERIALIZER_CONTEXT_BLUEPRINT: entry,
|
||||
},
|
||||
**serializer_kwargs,
|
||||
)
|
||||
try:
|
||||
serializer.is_valid(raise_exception=True)
|
||||
except ValidationError as exc:
|
||||
|
@ -76,7 +76,7 @@ class BlueprintEventHandler(FileSystemEventHandler):
|
||||
return
|
||||
if isinstance(event, FileCreatedEvent):
|
||||
LOGGER.debug("new blueprint file created, starting discovery")
|
||||
blueprints_discover.delay()
|
||||
blueprints_discovery.delay()
|
||||
if isinstance(event, FileModifiedEvent):
|
||||
path = Path(event.src_path)
|
||||
root = Path(CONFIG.y("blueprints_dir")).absolute()
|
||||
@ -134,7 +134,7 @@ def blueprints_find():
|
||||
throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True
|
||||
)
|
||||
@prefill_task
|
||||
def blueprints_discover(self: MonitoredTask):
|
||||
def blueprints_discovery(self: MonitoredTask):
|
||||
"""Find blueprints and check if they need to be created in the database"""
|
||||
count = 0
|
||||
for blueprint in blueprints_find():
|
||||
|
@ -16,6 +16,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||
from authentik.api.authorization import OwnerSuperuserPermissions
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.blueprints.api import ManagedSerializer
|
||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.users import UserSerializer
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
@ -29,6 +30,11 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
|
||||
|
||||
user_obj = UserSerializer(required=False, source="user", read_only=True)
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
|
||||
self.fields["key"] = CharField()
|
||||
|
||||
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
||||
"""Ensure only API or App password tokens are created."""
|
||||
request: Request = self.context.get("request")
|
||||
|
@ -43,14 +43,14 @@ class TestApplicationsAPI(APITestCase):
|
||||
self.assertEqual(
|
||||
self.client.patch(
|
||||
reverse("authentik_api:application-detail", kwargs={"slug": self.allowed.slug}),
|
||||
{"meta_launch_url": "https://%(username)s.test.goauthentik.io/%(username)s"},
|
||||
{"meta_launch_url": "https://%(username)s-test.test.goauthentik.io/%(username)s"},
|
||||
).status_code,
|
||||
200,
|
||||
)
|
||||
self.allowed.refresh_from_db()
|
||||
self.assertEqual(
|
||||
self.allowed.get_launch_url(self.user),
|
||||
f"https://{self.user.username}.test.goauthentik.io/{self.user.username}",
|
||||
f"https://{self.user.username}-test.test.goauthentik.io/{self.user.username}",
|
||||
)
|
||||
|
||||
def test_set_icon(self):
|
||||
|
@ -41,7 +41,7 @@ class TaskResult:
|
||||
|
||||
def with_error(self, exc: Exception) -> "TaskResult":
|
||||
"""Since errors might not always be pickle-able, set the traceback"""
|
||||
self.messages.append(str(exc))
|
||||
self.messages.append(exception_to_string(exc))
|
||||
return self
|
||||
|
||||
|
||||
|
@ -81,7 +81,8 @@ class DomainlessFormattedURLValidator(DomainlessURLValidator):
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.host_re = r"([%\(\)a-zA-Z])+" + self.domain_re + self.domain_re
|
||||
self.formatter_re = r"([%\(\)a-zA-Z])*"
|
||||
self.host_re = "(" + self.formatter_re + self.hostname_re + self.domain_re + "|localhost)"
|
||||
self.regex = _lazy_re_compile(
|
||||
r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
|
||||
r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
|
||||
|
@ -1,4 +1,7 @@
|
||||
"""Test utils"""
|
||||
from inspect import currentframe
|
||||
from pathlib import Path
|
||||
|
||||
from django.contrib.messages.middleware import MessageMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http import HttpRequest
|
||||
@ -11,6 +14,21 @@ def dummy_get_response(request: HttpRequest): # pragma: no cover
|
||||
return None
|
||||
|
||||
|
||||
def load_fixture(path: str, **kwargs) -> str:
|
||||
"""Load fixture, optionally formatting it with kwargs"""
|
||||
current = currentframe()
|
||||
parent = current.f_back
|
||||
calling_file_path = parent.f_globals["__file__"]
|
||||
with open(
|
||||
Path(calling_file_path).resolve().parent / Path(path), "r", encoding="utf-8"
|
||||
) as _fixture:
|
||||
fixture = _fixture.read()
|
||||
try:
|
||||
return fixture % kwargs
|
||||
except TypeError:
|
||||
return fixture
|
||||
|
||||
|
||||
def get_request(*args, user=None, **kwargs):
|
||||
"""Get a request with usable session"""
|
||||
request = RequestFactory().get(*args, **kwargs)
|
||||
|
@ -38,13 +38,17 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
|
||||
if OUTPOST_REMOTE_IP_HEADER not in request.META or OUTPOST_TOKEN_HEADER not in request.META:
|
||||
return None
|
||||
fake_ip = request.META[OUTPOST_REMOTE_IP_HEADER]
|
||||
tokens = Token.filter_not_expired(
|
||||
key=request.META.get(OUTPOST_TOKEN_HEADER), intent=TokenIntents.INTENT_API
|
||||
token = (
|
||||
Token.filter_not_expired(
|
||||
key=request.META.get(OUTPOST_TOKEN_HEADER), intent=TokenIntents.INTENT_API
|
||||
)
|
||||
.select_related("user")
|
||||
.first()
|
||||
)
|
||||
if not tokens.exists():
|
||||
if not token:
|
||||
LOGGER.warning("Attempted remote-ip override without token", fake_ip=fake_ip)
|
||||
return None
|
||||
user = tokens.first().user
|
||||
user = token.user
|
||||
if not user.group_attributes(request).get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False):
|
||||
LOGGER.warning(
|
||||
"Remote-IP override: user doesn't have permission",
|
||||
|
@ -19,9 +19,9 @@ CELERY_BEAT_SCHEDULE = {
|
||||
"schedule": crontab(minute=fqdn_rand("outpost_token_ensurer"), hour="*/8"),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
},
|
||||
"outpost_local_connection": {
|
||||
"task": "authentik.outposts.tasks.outpost_local_connection",
|
||||
"schedule": crontab(minute=fqdn_rand("outpost_local_connection"), hour="*/8"),
|
||||
"outpost_connection_discovery": {
|
||||
"task": "authentik.outposts.tasks.outpost_connection_discovery",
|
||||
"schedule": crontab(minute=fqdn_rand("outpost_connection_discovery"), hour="*/8"),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
},
|
||||
}
|
||||
|
@ -236,28 +236,33 @@ def _outpost_single_update(outpost: Outpost):
|
||||
async_to_sync(closing_send)(channel, {"type": "event.update"})
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
def outpost_local_connection():
|
||||
@CELERY_APP.task(
|
||||
base=MonitoredTask,
|
||||
bind=True,
|
||||
)
|
||||
def outpost_connection_discovery(self: MonitoredTask):
|
||||
"""Checks the local environment and create Service connections."""
|
||||
status = TaskResult(TaskResultStatus.SUCCESSFUL)
|
||||
if not CONFIG.y_bool("outposts.discover"):
|
||||
LOGGER.info("Outpost integration discovery is disabled")
|
||||
status.messages.append("Outpost integration discovery is disabled")
|
||||
self.set_status(status)
|
||||
return
|
||||
# Explicitly check against token filename, as that's
|
||||
# only present when the integration is enabled
|
||||
if Path(SERVICE_TOKEN_FILENAME).exists():
|
||||
LOGGER.info("Detected in-cluster Kubernetes Config")
|
||||
status.messages.append("Detected in-cluster Kubernetes Config")
|
||||
if not KubernetesServiceConnection.objects.filter(local=True).exists():
|
||||
LOGGER.debug("Created Service Connection for in-cluster")
|
||||
status.messages.append("Created Service Connection for in-cluster")
|
||||
KubernetesServiceConnection.objects.create(
|
||||
name="Local Kubernetes Cluster", local=True, kubeconfig={}
|
||||
)
|
||||
# For development, check for the existence of a kubeconfig file
|
||||
kubeconfig_path = Path(KUBE_CONFIG_DEFAULT_LOCATION).expanduser()
|
||||
if kubeconfig_path.exists():
|
||||
LOGGER.info("Detected kubeconfig")
|
||||
status.messages.append("Detected kubeconfig")
|
||||
kubeconfig_local_name = f"k8s-{gethostname()}"
|
||||
if not KubernetesServiceConnection.objects.filter(name=kubeconfig_local_name).exists():
|
||||
LOGGER.debug("Creating kubeconfig Service Connection")
|
||||
status.messages.append("Creating kubeconfig Service Connection")
|
||||
with kubeconfig_path.open("r", encoding="utf8") as _kubeconfig:
|
||||
KubernetesServiceConnection.objects.create(
|
||||
name=kubeconfig_local_name,
|
||||
@ -266,11 +271,12 @@ def outpost_local_connection():
|
||||
unix_socket_path = urlparse(DEFAULT_UNIX_SOCKET).path
|
||||
socket = Path(unix_socket_path)
|
||||
if socket.exists() and access(socket, R_OK):
|
||||
LOGGER.info("Detected local docker socket")
|
||||
status.messages.append("Detected local docker socket")
|
||||
if len(DockerServiceConnection.objects.filter(local=True)) == 0:
|
||||
LOGGER.debug("Created Service Connection for docker")
|
||||
status.messages.append("Created Service Connection for docker")
|
||||
DockerServiceConnection.objects.create(
|
||||
name="Local Docker connection",
|
||||
local=True,
|
||||
url=unix_socket_path,
|
||||
)
|
||||
self.set_status(status)
|
||||
|
@ -26,6 +26,7 @@ class LDAPProviderSerializer(ProviderSerializer):
|
||||
"search_mode",
|
||||
"bind_mode",
|
||||
]
|
||||
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
||||
|
||||
|
||||
class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
|
@ -39,6 +39,7 @@ class OAuth2ProviderSerializer(ProviderSerializer):
|
||||
"issuer_mode",
|
||||
"jwks_sources",
|
||||
]
|
||||
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
||||
|
||||
|
||||
class OAuth2ProviderSetupURLs(PassiveSerializer):
|
||||
|
@ -355,6 +355,62 @@ class TestAuthorize(OAuthTestCase):
|
||||
delta=5,
|
||||
)
|
||||
|
||||
def test_full_fragment_code(self):
|
||||
"""Test full authorization"""
|
||||
flow = create_test_flow()
|
||||
provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=flow,
|
||||
redirect_uris="http://localhost",
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
state = generate_id()
|
||||
user = create_test_admin_user()
|
||||
self.client.force_login(user)
|
||||
with patch(
|
||||
"authentik.providers.oauth2.id_token.get_login_event",
|
||||
MagicMock(
|
||||
return_value=Event(
|
||||
action=EventAction.LOGIN,
|
||||
context={PLAN_CONTEXT_METHOD: "password"},
|
||||
created=now(),
|
||||
)
|
||||
),
|
||||
):
|
||||
# Step 1, initiate params and get redirect to flow
|
||||
self.client.get(
|
||||
reverse("authentik_providers_oauth2:authorize"),
|
||||
data={
|
||||
"response_type": "code",
|
||||
"response_mode": "fragment",
|
||||
"client_id": "test",
|
||||
"state": state,
|
||||
"scope": "openid",
|
||||
"redirect_uri": "http://localhost",
|
||||
"nonce": generate_id(),
|
||||
},
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{
|
||||
"component": "xak-flow-redirect",
|
||||
"type": ChallengeTypes.REDIRECT.value,
|
||||
"to": (f"http://localhost#code={code.code}" f"&state={state}"),
|
||||
},
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
code.expires.timestamp() - now().timestamp(),
|
||||
timedelta_from_string(provider.access_code_validity).total_seconds(),
|
||||
delta=5,
|
||||
)
|
||||
|
||||
def test_full_form_post_id_token(self):
|
||||
"""Test full authorization (form_post response)"""
|
||||
flow = create_test_flow()
|
||||
|
@ -514,7 +514,12 @@ class OAuthFulfillmentStage(StageView):
|
||||
return urlunsplit(uri)
|
||||
|
||||
if self.params.response_mode == ResponseMode.FRAGMENT:
|
||||
query_fragment = self.create_implicit_response(code)
|
||||
query_fragment = {}
|
||||
if self.params.grant_type in [GrantTypes.AUTHORIZATION_CODE]:
|
||||
query_fragment["code"] = code.code
|
||||
query_fragment["state"] = [str(self.params.state) if self.params.state else ""]
|
||||
else:
|
||||
query_fragment = self.create_implicit_response(code)
|
||||
|
||||
uri = uri._replace(
|
||||
fragment=uri.fragment + urlencode(query_fragment, doseq=True),
|
||||
|
@ -95,6 +95,7 @@ class ProxyProviderSerializer(ProviderSerializer):
|
||||
"refresh_token_validity",
|
||||
"outpost_set",
|
||||
]
|
||||
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
||||
|
||||
|
||||
class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
|
@ -154,6 +154,7 @@ class SAMLProviderSerializer(ProviderSerializer):
|
||||
"url_slo_post",
|
||||
"url_slo_redirect",
|
||||
]
|
||||
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
||||
|
||||
|
||||
class SAMLMetadataSerializer(PassiveSerializer):
|
||||
|
@ -10,8 +10,8 @@ from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import load_fixture
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.tests.test_metadata import load_fixture
|
||||
|
||||
|
||||
class TestSAMLProviderAPI(APITestCase):
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""Test Service-Provider Metadata Parser"""
|
||||
from pathlib import Path
|
||||
|
||||
import xmlsec
|
||||
from defusedxml.lxml import fromstring
|
||||
from django.test import RequestFactory, TestCase
|
||||
@ -9,6 +7,7 @@ from lxml import etree # nosec
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import load_fixture
|
||||
from authentik.lib.xml import lxml_from_string
|
||||
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.metadata import MetadataProcessor
|
||||
@ -16,12 +15,6 @@ from authentik.providers.saml.processors.metadata_parser import ServiceProviderM
|
||||
from authentik.sources.saml.processors.constants import NS_MAP
|
||||
|
||||
|
||||
def load_fixture(path: str, **kwargs) -> str:
|
||||
"""Load fixture"""
|
||||
with open(Path(__file__).resolve().parent / Path(path), "r", encoding="utf-8") as _fixture:
|
||||
return _fixture.read()
|
||||
|
||||
|
||||
class TestServiceProviderMetadataParser(TestCase):
|
||||
"""Test ServiceProviderMetadataParser parsing and creation of SAML Provider"""
|
||||
|
||||
|
@ -73,12 +73,12 @@ def task_error_hook(task_id, exception: Exception, traceback, *args, **kwargs):
|
||||
def _get_startup_tasks() -> list[Callable]:
|
||||
"""Get all tasks to be run on startup"""
|
||||
from authentik.admin.tasks import clear_update_notifications
|
||||
from authentik.outposts.tasks import outpost_controller_all, outpost_local_connection
|
||||
from authentik.outposts.tasks import outpost_connection_discovery, outpost_controller_all
|
||||
from authentik.providers.proxy.tasks import proxy_set_defaults
|
||||
|
||||
return [
|
||||
clear_update_notifications,
|
||||
outpost_local_connection,
|
||||
outpost_connection_discovery,
|
||||
outpost_controller_all,
|
||||
proxy_set_defaults,
|
||||
]
|
||||
|
10
authentik/sources/saml/tests/fixtures/response_error.xml
vendored
Normal file
10
authentik/sources/saml/tests/fixtures/response_error.xml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://127.0.0.1:9443/source/saml/google/acs/" ID="_ee7a8865ac457e7b22cb4f16b39ceca9" IssueInstant="2022-10-14T13:52:04.479Z" Version="2.0">
|
||||
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://accounts.google.com/o/saml2?idpid=</saml2:Issuer>
|
||||
<saml2p:Status>
|
||||
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Requester">
|
||||
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:RequestDenied"></saml2p:StatusCode>
|
||||
</saml2p:StatusCode>
|
||||
<saml2p:StatusMessage>Invalid request, ACS Url in request http://localhost:9000/source/saml/google/acs/ doesn't match configured ACS Url https://127.0.0.1:9443/source/saml/google/acs/.</saml2p:StatusMessage>
|
||||
</saml2p:Status>
|
||||
</saml2p:Response>
|
40
authentik/sources/saml/tests/fixtures/response_success.xml
vendored
Normal file
40
authentik/sources/saml/tests/fixtures/response_success.xml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://127.0.0.1:9443/source/saml/google/acs/" ID="_1e17063957f10819a5a8e147971fec22" InResponseTo="_157fb504b59f4ae3919f74896a6b8565" IssueInstant="2022-10-14T14:11:49.590Z" Version="2.0">
|
||||
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://accounts.google.com/o/saml2?idpid=</saml2:Issuer>
|
||||
<saml2p:Status>
|
||||
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"></saml2p:StatusCode>
|
||||
</saml2p:Status>
|
||||
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_346001c5708ffd118c40edbc0c72fc60" IssueInstant="2022-10-14T14:11:49.590Z" Version="2.0">
|
||||
<saml2:Issuer>https://accounts.google.com/o/saml2?idpid=</saml2:Issuer>
|
||||
<saml2:Subject>
|
||||
<saml2:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">jens@goauthentik.io</saml2:NameID>
|
||||
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
|
||||
<saml2:SubjectConfirmationData InResponseTo="_157fb504b59f4ae3919f74896a6b8565" NotOnOrAfter="2022-10-14T14:16:49.590Z" Recipient="https://127.0.0.1:9443/source/saml/google/acs/"></saml2:SubjectConfirmationData>
|
||||
</saml2:SubjectConfirmation>
|
||||
</saml2:Subject>
|
||||
<saml2:Conditions NotBefore="2022-10-14T14:06:49.590Z" NotOnOrAfter="2022-10-14T14:16:49.590Z">
|
||||
<saml2:AudienceRestriction>
|
||||
<saml2:Audience>https://accounts.google.com/o/saml2?idpid=</saml2:Audience>
|
||||
</saml2:AudienceRestriction>
|
||||
</saml2:Conditions>
|
||||
<saml2:AttributeStatement>
|
||||
<saml2:Attribute Name="name">
|
||||
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">foo</saml2:AttributeValue>
|
||||
</saml2:Attribute>
|
||||
<saml2:Attribute Name="sn">
|
||||
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">bar</saml2:AttributeValue>
|
||||
</saml2:Attribute>
|
||||
<saml2:Attribute Name="email">
|
||||
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">foo@bar.baz</saml2:AttributeValue>
|
||||
</saml2:Attribute>
|
||||
</saml2:AttributeStatement>
|
||||
<saml2:AuthnStatement AuthnInstant="2022-10-14T12:16:21.000Z" SessionIndex="_346001c5708ffd118c40edbc0c72fc60">
|
||||
<saml2:AuthnContext>
|
||||
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml2:AuthnContextClassRef>
|
||||
</saml2:AuthnContext>
|
||||
</saml2:AuthnStatement>
|
||||
</saml2:Assertion>
|
||||
</saml2p:Response>
|
@ -6,64 +6,10 @@ from django.test import RequestFactory, TestCase
|
||||
|
||||
from authentik.core.tests.utils import create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import dummy_get_response
|
||||
from authentik.lib.tests.utils import dummy_get_response, load_fixture
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.sources.saml.processors.response import ResponseProcessor
|
||||
|
||||
RESPONSE_ERROR = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://127.0.0.1:9443/source/saml/google/acs/" ID="_ee7a8865ac457e7b22cb4f16b39ceca9" IssueInstant="2022-10-14T13:52:04.479Z" Version="2.0">
|
||||
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://accounts.google.com/o/saml2?idpid=</saml2:Issuer>
|
||||
<saml2p:Status>
|
||||
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Requester">
|
||||
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:RequestDenied"></saml2p:StatusCode>
|
||||
</saml2p:StatusCode>
|
||||
<saml2p:StatusMessage>Invalid request, ACS Url in request http://localhost:9000/source/saml/google/acs/ doesn't match configured ACS Url https://127.0.0.1:9443/source/saml/google/acs/.</saml2p:StatusMessage>
|
||||
</saml2p:Status>
|
||||
</saml2p:Response>
|
||||
"""
|
||||
|
||||
RESPONSE_SUCCESS = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://127.0.0.1:9443/source/saml/google/acs/" ID="_1e17063957f10819a5a8e147971fec22" InResponseTo="_157fb504b59f4ae3919f74896a6b8565" IssueInstant="2022-10-14T14:11:49.590Z" Version="2.0">
|
||||
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://accounts.google.com/o/saml2?idpid=</saml2:Issuer>
|
||||
<saml2p:Status>
|
||||
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"></saml2p:StatusCode>
|
||||
</saml2p:Status>
|
||||
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_346001c5708ffd118c40edbc0c72fc60" IssueInstant="2022-10-14T14:11:49.590Z" Version="2.0">
|
||||
<saml2:Issuer>https://accounts.google.com/o/saml2?idpid=</saml2:Issuer>
|
||||
<saml2:Subject>
|
||||
<saml2:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">jens@goauthentik.io</saml2:NameID>
|
||||
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
|
||||
<saml2:SubjectConfirmationData InResponseTo="_157fb504b59f4ae3919f74896a6b8565" NotOnOrAfter="2022-10-14T14:16:49.590Z" Recipient="https://127.0.0.1:9443/source/saml/google/acs/"></saml2:SubjectConfirmationData>
|
||||
</saml2:SubjectConfirmation>
|
||||
</saml2:Subject>
|
||||
<saml2:Conditions NotBefore="2022-10-14T14:06:49.590Z" NotOnOrAfter="2022-10-14T14:16:49.590Z">
|
||||
<saml2:AudienceRestriction>
|
||||
<saml2:Audience>https://accounts.google.com/o/saml2?idpid=</saml2:Audience>
|
||||
</saml2:AudienceRestriction>
|
||||
</saml2:Conditions>
|
||||
<saml2:AttributeStatement>
|
||||
<saml2:Attribute Name="name">
|
||||
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">foo</saml2:AttributeValue>
|
||||
</saml2:Attribute>
|
||||
<saml2:Attribute Name="sn">
|
||||
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">bar</saml2:AttributeValue>
|
||||
</saml2:Attribute>
|
||||
<saml2:Attribute Name="email">
|
||||
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">foo@bar.baz</saml2:AttributeValue>
|
||||
</saml2:Attribute>
|
||||
</saml2:AttributeStatement>
|
||||
<saml2:AuthnStatement AuthnInstant="2022-10-14T12:16:21.000Z" SessionIndex="_346001c5708ffd118c40edbc0c72fc60">
|
||||
<saml2:AuthnContext>
|
||||
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml2:AuthnContextClassRef>
|
||||
</saml2:AuthnContext>
|
||||
</saml2:AuthnStatement>
|
||||
</saml2:Assertion>
|
||||
</saml2p:Response>
|
||||
"""
|
||||
|
||||
|
||||
class TestResponseProcessor(TestCase):
|
||||
"""Test ResponseProcessor"""
|
||||
@ -80,7 +26,12 @@ class TestResponseProcessor(TestCase):
|
||||
def test_status_error(self):
|
||||
"""Test error status"""
|
||||
request = self.factory.post(
|
||||
"/", data={"SAMLResponse": b64encode(RESPONSE_ERROR.encode()).decode()}
|
||||
"/",
|
||||
data={
|
||||
"SAMLResponse": b64encode(
|
||||
load_fixture("fixtures/response_error.xml").encode()
|
||||
).decode()
|
||||
},
|
||||
)
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
@ -99,7 +50,12 @@ class TestResponseProcessor(TestCase):
|
||||
def test_success(self):
|
||||
"""Test success"""
|
||||
request = self.factory.post(
|
||||
"/", data={"SAMLResponse": b64encode(RESPONSE_SUCCESS.encode()).decode()}
|
||||
"/",
|
||||
data={
|
||||
"SAMLResponse": b64encode(
|
||||
load_fixture("fixtures/response_success.xml").encode()
|
||||
).decode()
|
||||
},
|
||||
)
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
|
@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.1.7 on 2023-03-13 19:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_stages_authenticator_webauthn",
|
||||
"0007_rename_last_used_on_webauthndevice_last_t",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="webauthndevice",
|
||||
name="credential_id",
|
||||
field=models.TextField(unique=True),
|
||||
),
|
||||
]
|
@ -119,7 +119,7 @@ class WebAuthnDevice(SerializerModel, Device):
|
||||
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
||||
|
||||
name = models.TextField(max_length=200)
|
||||
credential_id = models.CharField(max_length=300, unique=True)
|
||||
credential_id = models.TextField(unique=True)
|
||||
public_key = models.TextField()
|
||||
sign_count = models.IntegerField(default=0)
|
||||
rp_id = models.CharField(max_length=253)
|
||||
|
@ -32,7 +32,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.3.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.3.1}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -50,7 +50,7 @@ services:
|
||||
- "${AUTHENTIK_PORT_HTTP:-9000}:9000"
|
||||
- "${AUTHENTIK_PORT_HTTPS:-9443}:9443"
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.3.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.3.1}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
4
go.mod
4
go.mod
@ -25,7 +25,7 @@ require (
|
||||
github.com/prometheus/client_golang v1.14.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
goauthentik.io/api/v3 v3.2023022.15
|
||||
goauthentik.io/api/v3 v3.2023030.3
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.6.0
|
||||
golang.org/x/sync v0.1.0
|
||||
@ -75,7 +75,7 @@ require (
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.29.0 // indirect
|
||||
google.golang.org/protobuf v1.29.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
8
go.sum
8
go.sum
@ -384,8 +384,8 @@ go.opentelemetry.io/otel/sdk v1.11.1 h1:F7KmQgoHljhUuJyA+9BiU+EkJfyX5nVVF4wyzWZp
|
||||
go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
|
||||
go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk=
|
||||
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
|
||||
goauthentik.io/api/v3 v3.2023022.15 h1:wIz6nxi06l1zhsSCPaMuoXiOoGvcaBCPGmkNOdq+CNc=
|
||||
goauthentik.io/api/v3 v3.2023022.15/go.mod h1:XPPKEa2Snpu7Gd8hJPPB5o0n9FtOV4y0ISZMm09wQ4c=
|
||||
goauthentik.io/api/v3 v3.2023030.3 h1:ZctGEzkmv1kgeJkK57m3KFBazkbpWzKG7SiUVfAsJjY=
|
||||
goauthentik.io/api/v3 v3.2023030.3/go.mod h1:3uF9ZMVzMVljmsL3cnjaNGQ/lJM8FtcIWOySuK9CCYU=
|
||||
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=
|
||||
@ -689,8 +689,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0=
|
||||
google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
|
||||
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b h1:U/Uqd1232+wrnHOvWNaxrNqn/kFnr4yu4blgPtQt0N8=
|
||||
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b/go.mod h1:fgfIZMlsafAHpspcks2Bul+MWUNw/2dyQmjC2faKjtg=
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2023.3.0"
|
||||
const VERSION = "2023.3.1"
|
||||
|
@ -30,11 +30,15 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
|
||||
// Only append attributes that don't already exist
|
||||
// TODO: Remove in 2023.3
|
||||
for _, rawAttr := range rawAttrs {
|
||||
exists := false
|
||||
for _, attr := range attrs {
|
||||
if !strings.EqualFold(attr.Name, rawAttr.Name) {
|
||||
attrs = append(attrs, rawAttr)
|
||||
if strings.EqualFold(attr.Name, rawAttr.Name) {
|
||||
exists = true
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
attrs = append(attrs, rawAttr)
|
||||
}
|
||||
}
|
||||
|
||||
if u.IsActive == nil {
|
||||
|
@ -36,11 +36,15 @@ func (lg *LDAPGroup) Entry() *ldap.Entry {
|
||||
// Only append attributes that don't already exist
|
||||
// TODO: Remove in 2023.3
|
||||
for _, rawAttr := range rawAttrs {
|
||||
exists := false
|
||||
for _, attr := range attrs {
|
||||
if !strings.EqualFold(attr.Name, rawAttr.Name) {
|
||||
attrs = append(attrs, rawAttr)
|
||||
if strings.EqualFold(attr.Name, rawAttr.Name) {
|
||||
exists = true
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
attrs = append(attrs, rawAttr)
|
||||
}
|
||||
}
|
||||
|
||||
objectClass := []string{constants.OCGroup, constants.OCGroupOfUniqueNames, constants.OCGroupOfNames, constants.OCAKGroup, constants.OCPosixGroup}
|
||||
|
@ -2,7 +2,6 @@ package application
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api/v3"
|
||||
@ -31,40 +30,55 @@ func updateURL(rawUrl string, scheme string, host string) string {
|
||||
func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bool) OIDCEndpoint {
|
||||
authUrl := p.OidcConfiguration.AuthorizationEndpoint
|
||||
endUrl := p.OidcConfiguration.EndSessionEndpoint
|
||||
tokenUrl := p.OidcConfiguration.TokenEndpoint
|
||||
jwksUrl := p.OidcConfiguration.JwksUri
|
||||
issuer := p.OidcConfiguration.Issuer
|
||||
if config.Get().AuthentikHostBrowser != "" {
|
||||
authUrl = strings.ReplaceAll(authUrl, authentikHost, config.Get().AuthentikHostBrowser)
|
||||
endUrl = strings.ReplaceAll(endUrl, authentikHost, config.Get().AuthentikHostBrowser)
|
||||
jwksUrl = strings.ReplaceAll(jwksUrl, authentikHost, config.Get().AuthentikHostBrowser)
|
||||
issuer = strings.ReplaceAll(issuer, authentikHost, config.Get().AuthentikHostBrowser)
|
||||
}
|
||||
ep := OIDCEndpoint{
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: authUrl,
|
||||
TokenURL: tokenUrl,
|
||||
TokenURL: p.OidcConfiguration.TokenEndpoint,
|
||||
AuthStyle: oauth2.AuthStyleInParams,
|
||||
},
|
||||
EndSessionEndpoint: endUrl,
|
||||
JwksUri: jwksUrl,
|
||||
JwksUri: p.OidcConfiguration.JwksUri,
|
||||
TokenIntrospection: p.OidcConfiguration.IntrospectionEndpoint,
|
||||
Issuer: issuer,
|
||||
}
|
||||
if !embedded {
|
||||
// For the embedded outpost, we use the configure `authentik_host` for the browser URLs
|
||||
// and localhost (which is what we've got from the API) for backchannel URLs
|
||||
//
|
||||
// For other outposts, when `AUTHENTIK_HOST_BROWSER` is set, we use that for the browser URLs
|
||||
// and use what we got from the API for backchannel
|
||||
hostBrowser := config.Get().AuthentikHostBrowser
|
||||
if !embedded && hostBrowser == "" {
|
||||
return ep
|
||||
}
|
||||
if authentikHost == "" {
|
||||
log.Warning("Outpost has localhost/blank API Connection but no authentik_host is configured.")
|
||||
return ep
|
||||
var newHost *url.URL
|
||||
if embedded {
|
||||
if authentikHost == "" {
|
||||
log.Warning("Outpost has localhost/blank API Connection but no authentik_host is configured.")
|
||||
return ep
|
||||
}
|
||||
aku, err := url.Parse(authentikHost)
|
||||
if err != nil {
|
||||
return ep
|
||||
}
|
||||
newHost = aku
|
||||
} else if hostBrowser != "" {
|
||||
aku, err := url.Parse(hostBrowser)
|
||||
if err != nil {
|
||||
return ep
|
||||
}
|
||||
newHost = aku
|
||||
}
|
||||
aku, err := url.Parse(authentikHost)
|
||||
if err != nil {
|
||||
return ep
|
||||
// Update all browser-accessed URLs to use the new host and scheme
|
||||
ep.AuthURL = updateURL(authUrl, newHost.Scheme, newHost.Host)
|
||||
ep.EndSessionEndpoint = updateURL(endUrl, newHost.Scheme, newHost.Host)
|
||||
// Update issuer to use the same host and scheme, which would normally break as we don't
|
||||
// change the token URL here, but the token HTTP transport overwrites the Host header
|
||||
//
|
||||
// This is only used in embedded outposts as there we can guarantee that the request
|
||||
// is routed correctly
|
||||
if embedded {
|
||||
ep.Issuer = updateURL(ep.Issuer, newHost.Scheme, newHost.Host)
|
||||
}
|
||||
ep.AuthURL = updateURL(authUrl, aku.Scheme, aku.Host)
|
||||
ep.EndSessionEndpoint = updateURL(endUrl, aku.Scheme, aku.Host)
|
||||
ep.JwksUri = updateURL(jwksUrl, aku.Scheme, aku.Host)
|
||||
ep.Issuer = updateURL(ep.Issuer, aku.Scheme, aku.Host)
|
||||
return ep
|
||||
}
|
||||
|
88
internal/outpost/proxyv2/application/endpoint_test.go
Normal file
88
internal/outpost/proxyv2/application/endpoint_test.go
Normal file
@ -0,0 +1,88 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"goauthentik.io/api/v3"
|
||||
"goauthentik.io/internal/config"
|
||||
)
|
||||
|
||||
func TestEndpointDefault(t *testing.T) {
|
||||
pc := api.ProxyOutpostConfig{
|
||||
OidcConfiguration: api.ProxyOutpostConfigOidcConfiguration{
|
||||
AuthorizationEndpoint: "https://test.goauthentik.io/application/o/authorize/",
|
||||
EndSessionEndpoint: "https://test.goauthentik.io/application/o/test-app/end-session/",
|
||||
IntrospectionEndpoint: "https://test.goauthentik.io/application/o/introspect/",
|
||||
Issuer: "https://test.goauthentik.io/application/o/test-app/",
|
||||
JwksUri: "https://test.goauthentik.io/application/o/test-app/jwks/",
|
||||
TokenEndpoint: "https://test.goauthentik.io/application/o/token/",
|
||||
},
|
||||
}
|
||||
|
||||
ep := GetOIDCEndpoint(pc, "https://authentik-host.test.goauthentik.io", false)
|
||||
// Standard outpost, non embedded
|
||||
// All URLs should use the host that they get from the config
|
||||
assert.Equal(t, "https://test.goauthentik.io/application/o/authorize/", ep.AuthURL)
|
||||
assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL)
|
||||
assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/", ep.Issuer)
|
||||
assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri)
|
||||
assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint)
|
||||
assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection)
|
||||
}
|
||||
|
||||
func TestEndpointAuthentikHostBrowser(t *testing.T) {
|
||||
c := config.Get()
|
||||
c.AuthentikHostBrowser = "https://browser.test.goauthentik.io"
|
||||
defer func() {
|
||||
c.AuthentikHostBrowser = ""
|
||||
}()
|
||||
pc := api.ProxyOutpostConfig{
|
||||
OidcConfiguration: api.ProxyOutpostConfigOidcConfiguration{
|
||||
AuthorizationEndpoint: "https://test.goauthentik.io/application/o/authorize/",
|
||||
EndSessionEndpoint: "https://test.goauthentik.io/application/o/test-app/end-session/",
|
||||
IntrospectionEndpoint: "https://test.goauthentik.io/application/o/introspect/",
|
||||
Issuer: "https://test.goauthentik.io/application/o/test-app/",
|
||||
JwksUri: "https://test.goauthentik.io/application/o/test-app/jwks/",
|
||||
TokenEndpoint: "https://test.goauthentik.io/application/o/token/",
|
||||
UserinfoEndpoint: "https://test.goauthentik.io/application/o/userinfo/",
|
||||
},
|
||||
}
|
||||
|
||||
ep := GetOIDCEndpoint(pc, "https://authentik-host.test.goauthentik.io", false)
|
||||
// Standard outpost, with AUTHENTIK_HOST_BROWSER set
|
||||
// Only the authorize/end session URLs should be changed
|
||||
assert.Equal(t, "https://browser.test.goauthentik.io/application/o/authorize/", ep.AuthURL)
|
||||
assert.Equal(t, "https://browser.test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint)
|
||||
assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL)
|
||||
assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/", ep.Issuer)
|
||||
assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri)
|
||||
assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection)
|
||||
}
|
||||
|
||||
func TestEndpointEmbedded(t *testing.T) {
|
||||
pc := api.ProxyOutpostConfig{
|
||||
OidcConfiguration: api.ProxyOutpostConfigOidcConfiguration{
|
||||
AuthorizationEndpoint: "https://test.goauthentik.io/application/o/authorize/",
|
||||
EndSessionEndpoint: "https://test.goauthentik.io/application/o/test-app/end-session/",
|
||||
IntrospectionEndpoint: "https://test.goauthentik.io/application/o/introspect/",
|
||||
Issuer: "https://test.goauthentik.io/application/o/test-app/",
|
||||
JwksUri: "https://test.goauthentik.io/application/o/test-app/jwks/",
|
||||
TokenEndpoint: "https://test.goauthentik.io/application/o/token/",
|
||||
UserinfoEndpoint: "https://test.goauthentik.io/application/o/userinfo/",
|
||||
},
|
||||
}
|
||||
|
||||
ep := GetOIDCEndpoint(pc, "https://authentik-host.test.goauthentik.io", true)
|
||||
// Embedded outpost
|
||||
// Browser URLs should use the config of "authentik_host", everything else can use what's
|
||||
// received from the API endpoint
|
||||
// Token URL is an exception since it's sent via a special HTTP transport that overrides the
|
||||
// HTTP Host header, to make sure it's the same value as the issuer
|
||||
assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/authorize/", ep.AuthURL)
|
||||
assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/test-app/", ep.Issuer)
|
||||
assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL)
|
||||
assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri)
|
||||
assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint)
|
||||
assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection)
|
||||
}
|
@ -105,7 +105,7 @@ filterwarnings = [
|
||||
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2023.3.0"
|
||||
version = "2023.3.1"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
|
22
schema.yml
22
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2023.3.0
|
||||
version: 2023.3.1
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
@ -30195,7 +30195,6 @@ components:
|
||||
authorization_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used when authorizing this provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
@ -30268,6 +30267,7 @@ components:
|
||||
required:
|
||||
- assigned_application_name
|
||||
- assigned_application_slug
|
||||
- authorization_flow
|
||||
- component
|
||||
- meta_model_name
|
||||
- name
|
||||
@ -30285,7 +30285,6 @@ components:
|
||||
authorization_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used when authorizing this provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
@ -30328,6 +30327,7 @@ components:
|
||||
bind_mode:
|
||||
$ref: '#/components/schemas/LDAPAPIAccessMode'
|
||||
required:
|
||||
- authorization_flow
|
||||
- name
|
||||
LDAPSource:
|
||||
type: object
|
||||
@ -30927,7 +30927,6 @@ components:
|
||||
authorization_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used when authorizing this provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
@ -31027,6 +31026,7 @@ components:
|
||||
required:
|
||||
- assigned_application_name
|
||||
- assigned_application_slug
|
||||
- authorization_flow
|
||||
- component
|
||||
- meta_model_name
|
||||
- name
|
||||
@ -31043,7 +31043,6 @@ components:
|
||||
authorization_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used when authorizing this provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
@ -31121,6 +31120,7 @@ components:
|
||||
authenticate.
|
||||
title: Any JWT signed by the JWK of the selected source can be used to authenticate.
|
||||
required:
|
||||
- authorization_flow
|
||||
- name
|
||||
OAuth2ProviderSetupURLs:
|
||||
type: object
|
||||
@ -35608,7 +35608,6 @@ components:
|
||||
authorization_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used when authorizing this provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
@ -35838,7 +35837,6 @@ components:
|
||||
authorization_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used when authorizing this provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
@ -36299,7 +36297,6 @@ components:
|
||||
authorization_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used when authorizing this provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
@ -36426,7 +36423,6 @@ components:
|
||||
authorization_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used when authorizing this provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
@ -37873,7 +37869,6 @@ components:
|
||||
authorization_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used when authorizing this provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
@ -37981,6 +37976,7 @@ components:
|
||||
required:
|
||||
- assigned_application_name
|
||||
- assigned_application_slug
|
||||
- authorization_flow
|
||||
- client_id
|
||||
- component
|
||||
- external_host
|
||||
@ -38001,7 +37997,6 @@ components:
|
||||
authorization_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used when authorizing this provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
@ -38075,6 +38070,7 @@ components:
|
||||
description: 'Tokens not valid on or after current time + this value (Format:
|
||||
hours=1;minutes=2;seconds=3).'
|
||||
required:
|
||||
- authorization_flow
|
||||
- external_host
|
||||
- name
|
||||
RedirectChallenge:
|
||||
@ -38315,7 +38311,6 @@ components:
|
||||
authorization_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used when authorizing this provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
@ -38431,6 +38426,7 @@ components:
|
||||
- acs_url
|
||||
- assigned_application_name
|
||||
- assigned_application_slug
|
||||
- authorization_flow
|
||||
- component
|
||||
- meta_model_name
|
||||
- name
|
||||
@ -38471,7 +38467,6 @@ components:
|
||||
authorization_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used when authorizing this provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
@ -38542,6 +38537,7 @@ components:
|
||||
* `post` - Post
|
||||
required:
|
||||
- acs_url
|
||||
- authorization_flow
|
||||
- name
|
||||
SAMLSource:
|
||||
type: object
|
||||
|
@ -17,7 +17,7 @@ from authentik.core.models import Application
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostConfig, OutpostType
|
||||
from authentik.outposts.tasks import outpost_local_connection
|
||||
from authentik.outposts.tasks import outpost_connection_discovery
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
|
||||
@ -210,7 +210,7 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_proxy_connectivity(self):
|
||||
"""Test proxy connectivity over websocket"""
|
||||
outpost_local_connection()
|
||||
outpost_connection_discovery() # pylint: disable=no-value-for-parameter
|
||||
proxy: ProxyProvider = ProxyProvider.objects.create(
|
||||
name="proxy_provider",
|
||||
authorization_flow=Flow.objects.get(
|
||||
|
@ -19,7 +19,7 @@ from authentik.outposts.models import (
|
||||
OutpostType,
|
||||
default_outpost_config,
|
||||
)
|
||||
from authentik.outposts.tasks import outpost_local_connection
|
||||
from authentik.outposts.tasks import outpost_connection_discovery
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
from tests.e2e.utils import get_docker_tag
|
||||
|
||||
@ -58,7 +58,7 @@ class OutpostDockerTests(ChannelsLiveServerTestCase):
|
||||
self.ssl_folder = mkdtemp()
|
||||
self.container = self._start_container(self.ssl_folder)
|
||||
# Ensure that local connection have been created
|
||||
outpost_local_connection()
|
||||
outpost_connection_discovery() # pylint: disable=no-value-for-parameter
|
||||
self.provider: ProxyProvider = ProxyProvider.objects.create(
|
||||
name="test",
|
||||
internal_host="http://localhost",
|
||||
|
@ -10,7 +10,7 @@ from authentik.lib.config import CONFIG
|
||||
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
|
||||
from authentik.outposts.models import KubernetesServiceConnection, Outpost, OutpostType
|
||||
from authentik.outposts.tasks import outpost_local_connection
|
||||
from authentik.outposts.tasks import outpost_connection_discovery
|
||||
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
|
||||
@ -21,7 +21,7 @@ class OutpostKubernetesTests(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# Ensure that local connection have been created
|
||||
outpost_local_connection()
|
||||
outpost_connection_discovery() # pylint: disable=no-value-for-parameter
|
||||
self.provider: ProxyProvider = ProxyProvider.objects.create(
|
||||
name="test",
|
||||
internal_host="http://localhost",
|
||||
|
@ -18,7 +18,7 @@ from authentik.outposts.models import (
|
||||
OutpostType,
|
||||
default_outpost_config,
|
||||
)
|
||||
from authentik.outposts.tasks import outpost_local_connection
|
||||
from authentik.outposts.tasks import outpost_connection_discovery
|
||||
from authentik.providers.proxy.controllers.docker import DockerController
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
from tests.e2e.utils import get_docker_tag
|
||||
@ -58,7 +58,7 @@ class TestProxyDocker(ChannelsLiveServerTestCase):
|
||||
self.ssl_folder = mkdtemp()
|
||||
self.container = self._start_container(self.ssl_folder)
|
||||
# Ensure that local connection have been created
|
||||
outpost_local_connection()
|
||||
outpost_connection_discovery() # pylint: disable=no-value-for-parameter
|
||||
self.provider: ProxyProvider = ProxyProvider.objects.create(
|
||||
name="test",
|
||||
internal_host="http://localhost",
|
||||
|
@ -8,7 +8,7 @@ from structlog.stdlib import get_logger
|
||||
from authentik.core.tests.utils import create_test_flow
|
||||
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||
from authentik.outposts.models import KubernetesServiceConnection, Outpost, OutpostType
|
||||
from authentik.outposts.tasks import outpost_local_connection
|
||||
from authentik.outposts.tasks import outpost_connection_discovery
|
||||
from authentik.providers.proxy.controllers.k8s.ingress import IngressReconciler
|
||||
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
||||
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
|
||||
@ -23,7 +23,7 @@ class TestProxyKubernetes(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
# Ensure that local connection have been created
|
||||
outpost_local_connection()
|
||||
outpost_connection_discovery() # pylint: disable=no-value-for-parameter
|
||||
self.controller = None
|
||||
|
||||
def tearDown(self) -> None:
|
||||
|
7999
web/package-lock.json
generated
7999
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -53,7 +53,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.0",
|
||||
"@babel/core": "^7.21.3",
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@babel/plugin-transform-runtime": "^7.21.0",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
@ -66,7 +66,7 @@
|
||||
"@codemirror/theme-one-dark": "^6.1.1",
|
||||
"@formatjs/intl-listformat": "^7.1.9",
|
||||
"@fortawesome/fontawesome-free": "^6.3.0",
|
||||
"@goauthentik/api": "^2023.2.2-1678400303",
|
||||
"@goauthentik/api": "^2023.3.0-1678747008",
|
||||
"@hcaptcha/types": "^1.0.3",
|
||||
"@jackfranklin/rollup-plugin-markdown": "^0.4.0",
|
||||
"@lingui/cli": "^3.17.2",
|
||||
@ -74,22 +74,20 @@
|
||||
"@lingui/detect-locale": "^3.17.2",
|
||||
"@lingui/macro": "^3.17.2",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@polymer/iron-form": "^3.0.1",
|
||||
"@polymer/paper-input": "^3.2.1",
|
||||
"@rollup/plugin-babel": "^6.0.3",
|
||||
"@rollup/plugin-commonjs": "^24.0.1",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-replace": "^5.0.2",
|
||||
"@rollup/plugin-typescript": "^11.0.0",
|
||||
"@sentry/browser": "^7.42.0",
|
||||
"@sentry/tracing": "^7.42.0",
|
||||
"@sentry/browser": "^7.43.0",
|
||||
"@sentry/tracing": "^7.43.0",
|
||||
"@squoosh/cli": "^0.7.3",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
||||
"@types/chart.js": "^2.9.37",
|
||||
"@types/codemirror": "5.60.7",
|
||||
"@types/grecaptcha": "^3.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.1",
|
||||
"@typescript-eslint/parser": "^5.54.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||
"@typescript-eslint/parser": "^5.55.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.7.0",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-tsconfig-paths": "^1.0.3",
|
||||
@ -98,7 +96,7 @@
|
||||
"chartjs-adapter-moment": "^1.0.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"core-js": "^3.29.0",
|
||||
"core-js": "^3.29.1",
|
||||
"country-flag-icons": "^1.5.5",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
|
@ -61,6 +61,9 @@ export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> {
|
||||
metrics.healthy += 1;
|
||||
}
|
||||
});
|
||||
if (health.length < 1) {
|
||||
metrics.unsynced += 1;
|
||||
}
|
||||
} catch {
|
||||
metrics.unsynced += 1;
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
@ -60,7 +61,7 @@ export class TypeOAuthCodeApplicationWizardPage extends WizardFormPage {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { FlowDesignationEnum, LayoutEnum } from "@goauthentik/api";
|
||||
import { Flow, FlowDesignationEnum, LayoutEnum } from "@goauthentik/api";
|
||||
|
||||
export function RenderFlowOption(flow: Flow): string {
|
||||
return `${flow.slug} (${flow.name})`;
|
||||
}
|
||||
|
||||
export function DesignationToLabel(designation: FlowDesignationEnum): string {
|
||||
switch (designation) {
|
||||
|
@ -36,7 +36,7 @@ export class InitialServiceConnectionWizardPage extends WizardPage {
|
||||
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
|
||||
.forEach((radio) => {
|
||||
if (radio.checked) {
|
||||
this.host.isValid = true;
|
||||
radio.dispatchEvent(new CustomEvent("change"));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -42,7 +42,7 @@ export class InitialPolicyWizardPage extends WizardPage {
|
||||
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
|
||||
.forEach((radio) => {
|
||||
if (radio.checked) {
|
||||
this.host.isValid = true;
|
||||
radio.dispatchEvent(new CustomEvent("change"));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -39,7 +39,7 @@ export class InitialPropertyMappingWizardPage extends WizardPage {
|
||||
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
|
||||
.forEach((radio) => {
|
||||
if (radio.checked) {
|
||||
this.host.isValid = true;
|
||||
radio.dispatchEvent(new CustomEvent("change"));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -41,7 +41,7 @@ export class InitialProviderWizardPage extends WizardPage {
|
||||
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
|
||||
.forEach((radio) => {
|
||||
if (radio.checked) {
|
||||
this.host.isValid = true;
|
||||
radio.dispatchEvent(new CustomEvent("change"));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG, tenant } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
@ -9,9 +10,8 @@ import "@goauthentik/elements/forms/SearchSelect";
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { until } from "lit/directives/until.js";
|
||||
|
||||
import {
|
||||
CertificateKeyPair,
|
||||
@ -19,6 +19,7 @@ import {
|
||||
CoreGroupsListRequest,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
CurrentTenant,
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
@ -31,10 +32,14 @@ import {
|
||||
|
||||
@customElement("ak-provider-ldap-form")
|
||||
export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
||||
loadInstance(pk: number): Promise<LDAPProvider> {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersLdapRetrieve({
|
||||
@state()
|
||||
tenant?: CurrentTenant;
|
||||
async loadInstance(pk: number): Promise<LDAPProvider> {
|
||||
const provider = await new ProvidersApi(DEFAULT_CONFIG).providersLdapRetrieve({
|
||||
id: pk,
|
||||
});
|
||||
this.tenant = await tenant();
|
||||
return provider;
|
||||
}
|
||||
|
||||
getSuccessMessage(): string {
|
||||
@ -74,46 +79,36 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
${until(
|
||||
tenant().then((t) => {
|
||||
return html`
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation:
|
||||
FlowsInstancesListDesignationEnum.Authentication,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const flows = await new FlowsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).flowsInstancesList(args);
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.name;
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.slug}`;
|
||||
}}
|
||||
.value=${(flow: Flow | undefined): string | undefined => {
|
||||
return flow?.pk;
|
||||
}}
|
||||
.selected=${(flow: Flow): boolean => {
|
||||
let selected = flow.pk === t.flowAuthentication;
|
||||
if (this.instance?.authorizationFlow === flow.pk) {
|
||||
selected = true;
|
||||
}
|
||||
return selected;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
`;
|
||||
}),
|
||||
html`<option>${t`Loading...`}</option>`,
|
||||
)}
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Authentication,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args);
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.slug}`;
|
||||
}}
|
||||
.value=${(flow: Flow | undefined): string | undefined => {
|
||||
return flow?.pk;
|
||||
}}
|
||||
.selected=${(flow: Flow): boolean => {
|
||||
let selected = flow.pk === this.tenant?.flowAuthentication;
|
||||
if (this.instance?.authorizationFlow === flow.pk) {
|
||||
selected = true;
|
||||
}
|
||||
return selected;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Flow used for users to authenticate. Currently only identification and password stages are supported.`}
|
||||
</p>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first, randomString } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
@ -96,7 +97,7 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
@ -318,7 +319,7 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
@ -88,7 +89,7 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||
import { Form } from "@goauthentik/elements/forms/Form";
|
||||
@ -59,7 +60,7 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -38,7 +38,7 @@ export class InitialSourceWizardPage extends WizardPage {
|
||||
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
|
||||
.forEach((radio) => {
|
||||
if (radio.checked) {
|
||||
this.host.isValid = true;
|
||||
radio.dispatchEvent(new CustomEvent("change"));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
|
||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
@ -431,7 +432,7 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
@ -477,7 +478,7 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
|
||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||
import { PlexAPIClient, PlexResource, popupCenterScreen } from "@goauthentik/common/helpers/plex";
|
||||
@ -364,7 +365,7 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
@ -410,7 +411,7 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
|
||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
@ -496,7 +497,7 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
@ -540,7 +541,7 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
@ -586,7 +587,7 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -55,7 +55,7 @@ export class InitialStageWizardPage extends WizardPage {
|
||||
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
|
||||
.forEach((radio) => {
|
||||
if (radio.checked) {
|
||||
this.host.isValid = true;
|
||||
radio.dispatchEvent(new CustomEvent("change"));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
@ -146,7 +147,7 @@ export class AuthenticatorDuoStageForm extends ModelForm<AuthenticatorDuoStage,
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
@ -292,7 +293,7 @@ export class AuthenticatorSMSStageForm extends ModelForm<AuthenticatorSMSStage,
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
@ -93,7 +94,7 @@ export class AuthenticatorStaticStageForm extends ModelForm<AuthenticatorStaticS
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
@ -98,7 +99,7 @@ export class AuthenticatorTOTPStageForm extends ModelForm<AuthenticatorTOTPStage
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
@ -162,7 +163,7 @@ export class AuthenticateWebAuthnStageForm extends ModelForm<AuthenticateWebAuth
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first, groupBy } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
@ -265,7 +266,7 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { dateTimeLocal, first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
@ -88,7 +89,7 @@ export class InvitationForm extends ModelForm<Invitation, string> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
@ -136,7 +137,7 @@ export class PasswordStageForm extends ModelForm<PasswordStage, string> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -77,7 +77,7 @@ export class PromptStageForm extends ModelForm<PromptStage, string> {
|
||||
value=${ifDefined(prompt.pk)}
|
||||
?selected=${selected}
|
||||
>
|
||||
${t`${prompt.fieldKey} ("${prompt.label}", of type ${prompt.type})`}
|
||||
${t`${prompt.name} ("${prompt.fieldKey}", of type ${prompt.type})`}
|
||||
</option>`;
|
||||
});
|
||||
}),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
@ -165,7 +166,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
@ -202,7 +203,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
@ -237,7 +238,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
@ -274,7 +275,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
@ -312,7 +313,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
@ -347,7 +348,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
return RenderFlowOption(flow);
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
|
@ -68,14 +68,6 @@ export class UserForm extends ModelForm<User, number> {
|
||||
${t`User's primary identifier. 150 characters or fewer.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Path`} ?required=${true} name="path">
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.path, "users")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Name`} name="name">
|
||||
<input
|
||||
type="text"
|
||||
@ -110,6 +102,14 @@ export class UserForm extends ModelForm<User, number> {
|
||||
${t`Designates whether this user should be treated as active. Unselect this instead of deleting accounts.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Path`} ?required=${true} name="path">
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.path, "users")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Attributes`} ?required=${true} name="attributes">
|
||||
<ak-codemirror
|
||||
mode="yaml"
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 987 KiB |
@ -7,19 +7,9 @@ import { EVENT_REFRESH, VERSION } from "@goauthentik/common/constants";
|
||||
import { globalAK } from "@goauthentik/common/global";
|
||||
import { activateLocale } from "@goauthentik/common/ui/locale";
|
||||
|
||||
import {
|
||||
Config,
|
||||
ConfigFromJSON,
|
||||
Configuration,
|
||||
CoreApi,
|
||||
CurrentTenant,
|
||||
CurrentTenantFromJSON,
|
||||
RootApi,
|
||||
} from "@goauthentik/api";
|
||||
import { Config, Configuration, CoreApi, CurrentTenant, RootApi } from "@goauthentik/api";
|
||||
|
||||
let globalConfigPromise: Promise<Config> | undefined = Promise.resolve(
|
||||
ConfigFromJSON(globalAK()?.config),
|
||||
);
|
||||
let globalConfigPromise: Promise<Config> | undefined = Promise.resolve(globalAK().config);
|
||||
export function config(): Promise<Config> {
|
||||
if (!globalConfigPromise) {
|
||||
globalConfigPromise = new RootApi(DEFAULT_CONFIG).rootConfigRetrieve();
|
||||
@ -52,9 +42,7 @@ export function tenantSetLocale(tenant: CurrentTenant) {
|
||||
activateLocale(tenant.defaultLocale);
|
||||
}
|
||||
|
||||
let globalTenantPromise: Promise<CurrentTenant> | undefined = Promise.resolve(
|
||||
CurrentTenantFromJSON(globalAK()?.tenant),
|
||||
);
|
||||
let globalTenantPromise: Promise<CurrentTenant> | undefined = Promise.resolve(globalAK().tenant);
|
||||
export function tenant(): Promise<CurrentTenant> {
|
||||
if (!globalTenantPromise) {
|
||||
globalTenantPromise = new CoreApi(DEFAULT_CONFIG)
|
||||
@ -82,7 +70,7 @@ export const DEFAULT_CONFIG = new Configuration({
|
||||
middleware: [
|
||||
new CSRFMiddleware(),
|
||||
new EventMiddleware(),
|
||||
new LoggingMiddleware(CurrentTenantFromJSON(globalAK()?.tenant)),
|
||||
new LoggingMiddleware(globalAK().tenant),
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
||||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const VERSION = "2023.3.0";
|
||||
export const VERSION = "2023.3.1";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Config, CurrentTenant } from "@goauthentik/api";
|
||||
import { Config, ConfigFromJSON, CurrentTenant, CurrentTenantFromJSON } from "@goauthentik/api";
|
||||
|
||||
export interface GlobalAuthentik {
|
||||
_converted?: boolean;
|
||||
locale?: string;
|
||||
flow?: {
|
||||
layout: string;
|
||||
@ -13,11 +14,17 @@ export interface GlobalAuthentik {
|
||||
}
|
||||
|
||||
export interface AuthentikWindow {
|
||||
authentik?: GlobalAuthentik;
|
||||
authentik: GlobalAuthentik;
|
||||
}
|
||||
|
||||
export function globalAK(): GlobalAuthentik | undefined {
|
||||
return (window as unknown as AuthentikWindow).authentik;
|
||||
export function globalAK(): GlobalAuthentik {
|
||||
const ak = (window as unknown as AuthentikWindow).authentik;
|
||||
if (ak && !ak._converted) {
|
||||
ak._converted = true;
|
||||
ak.tenant = CurrentTenantFromJSON(ak.tenant);
|
||||
ak.config = ConfigFromJSON(ak.config);
|
||||
}
|
||||
return ak;
|
||||
}
|
||||
|
||||
export function docLink(path: string): string {
|
||||
|
@ -88,7 +88,10 @@ body {
|
||||
border-color: transparent;
|
||||
}
|
||||
.pf-c-tabs__item.pf-m-current {
|
||||
--pf-c-tabs__link--after--BorderColor: #fd4b2d;
|
||||
--pf-c-tabs__link--after--BorderColor: var(--ak-accent);
|
||||
}
|
||||
.pf-c-tabs__link {
|
||||
--pf-c-tabs__link--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
.pf-c-tabs.pf-m-vertical .pf-c-tabs__link {
|
||||
background-color: transparent;
|
||||
|
@ -172,6 +172,6 @@ export class Interface extends AKElement {
|
||||
|
||||
async getTheme(): Promise<UiThemeEnum> {
|
||||
const config = await uiConfig();
|
||||
return config.theme.base;
|
||||
return config.theme?.base || UiThemeEnum.Automatic;
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,6 @@ import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
|
||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||
import "@polymer/iron-form/iron-form";
|
||||
import { IronFormElement } from "@polymer/iron-form/iron-form";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
@ -110,63 +107,76 @@ export class Form<T> extends AKElement {
|
||||
* Reset the inner iron-form
|
||||
*/
|
||||
resetForm(): void {
|
||||
const ironForm = this.shadowRoot?.querySelector("iron-form");
|
||||
ironForm?.reset();
|
||||
const form = this.shadowRoot?.querySelector<HTMLFormElement>("form");
|
||||
form?.reset();
|
||||
}
|
||||
|
||||
getFormFiles(): { [key: string]: File } {
|
||||
const ironForm = this.shadowRoot?.querySelector("iron-form");
|
||||
const files: { [key: string]: File } = {};
|
||||
if (!ironForm) {
|
||||
return files;
|
||||
}
|
||||
const elements = ironForm._getSubmittableElements();
|
||||
const elements =
|
||||
this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
|
||||
"ak-form-element-horizontal",
|
||||
) || [];
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i] as HTMLInputElement;
|
||||
if (element.tagName.toLowerCase() === "input" && element.type === "file") {
|
||||
if ((element.files || []).length < 1) {
|
||||
const element = elements[i];
|
||||
element.requestUpdate();
|
||||
const inputElement = element.querySelector<HTMLInputElement>("[name]");
|
||||
if (!inputElement) {
|
||||
continue;
|
||||
}
|
||||
if (inputElement.tagName.toLowerCase() === "input" && inputElement.type === "file") {
|
||||
if ((inputElement.files || []).length < 1) {
|
||||
continue;
|
||||
}
|
||||
files[element.name] = (element.files || [])[0];
|
||||
files[element.name] = (inputElement.files || [])[0];
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
serializeForm(): T | undefined {
|
||||
const form = this.shadowRoot?.querySelector<IronFormElement>("iron-form");
|
||||
if (!form) {
|
||||
console.warn("authentik/forms: failed to find iron-form");
|
||||
return;
|
||||
}
|
||||
const elements: HTMLInputElement[] = form._getSubmittableElements();
|
||||
const elements =
|
||||
this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
|
||||
"ak-form-element-horizontal",
|
||||
) || [];
|
||||
const json: { [key: string]: unknown } = {};
|
||||
elements.forEach((element) => {
|
||||
const values = form._serializeElementValues(element);
|
||||
if (element.hidden) {
|
||||
element.requestUpdate();
|
||||
const inputElement = element.querySelector<HTMLInputElement>("[name]");
|
||||
if (element.hidden || !inputElement) {
|
||||
return;
|
||||
}
|
||||
if (element.tagName.toLowerCase() === "select" && "multiple" in element.attributes) {
|
||||
json[element.name] = values;
|
||||
} else if (element.tagName.toLowerCase() === "input" && element.type === "date") {
|
||||
json[element.name] = element.valueAsDate;
|
||||
} else if (
|
||||
element.tagName.toLowerCase() === "input" &&
|
||||
element.type === "datetime-local"
|
||||
if (
|
||||
inputElement.tagName.toLowerCase() === "select" &&
|
||||
"multiple" in inputElement.attributes
|
||||
) {
|
||||
json[element.name] = new Date(element.valueAsNumber);
|
||||
const selectElement = inputElement as unknown as HTMLSelectElement;
|
||||
json[element.name] = Array.from(selectElement.selectedOptions).map((v) => v.value);
|
||||
} else if (
|
||||
element.tagName.toLowerCase() === "input" &&
|
||||
"type" in element.dataset &&
|
||||
element.dataset["type"] === "datetime-local"
|
||||
inputElement.tagName.toLowerCase() === "input" &&
|
||||
inputElement.type === "date"
|
||||
) {
|
||||
json[element.name] = inputElement.valueAsDate;
|
||||
} else if (
|
||||
inputElement.tagName.toLowerCase() === "input" &&
|
||||
inputElement.type === "datetime-local"
|
||||
) {
|
||||
json[element.name] = new Date(inputElement.valueAsNumber);
|
||||
} else if (
|
||||
inputElement.tagName.toLowerCase() === "input" &&
|
||||
"type" in inputElement.dataset &&
|
||||
inputElement.dataset["type"] === "datetime-local"
|
||||
) {
|
||||
// Workaround for Firefox <93, since 92 and older don't support
|
||||
// datetime-local fields
|
||||
json[element.name] = new Date(element.value);
|
||||
} else if (element.tagName.toLowerCase() === "input" && element.type === "checkbox") {
|
||||
json[element.name] = element.checked;
|
||||
} else if (element.tagName.toLowerCase() === "ak-search-select") {
|
||||
const select = element as unknown as SearchSelect<unknown>;
|
||||
json[element.name] = new Date(inputElement.value);
|
||||
} else if (
|
||||
inputElement.tagName.toLowerCase() === "input" &&
|
||||
inputElement.type === "checkbox"
|
||||
) {
|
||||
json[element.name] = inputElement.checked;
|
||||
} else if (inputElement.tagName.toLowerCase() === "ak-search-select") {
|
||||
const select = inputElement as unknown as SearchSelect<unknown>;
|
||||
let value: unknown;
|
||||
try {
|
||||
value = select.toForm();
|
||||
@ -179,9 +189,7 @@ export class Form<T> extends AKElement {
|
||||
}
|
||||
json[element.name] = value;
|
||||
} else {
|
||||
for (let v = 0; v < values.length; v++) {
|
||||
this.serializeFieldRecursive(element, values[v], json);
|
||||
}
|
||||
this.serializeFieldRecursive(inputElement, inputElement.value, json);
|
||||
}
|
||||
});
|
||||
return json as unknown as T;
|
||||
@ -213,11 +221,6 @@ export class Form<T> extends AKElement {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const form = this.shadowRoot?.querySelector<IronFormElement>("iron-form");
|
||||
if (!form) {
|
||||
console.warn("authentik/forms: failed to find iron-form");
|
||||
return;
|
||||
}
|
||||
return this.send(data)
|
||||
.then((r) => {
|
||||
showMessage({
|
||||
@ -244,8 +247,12 @@ export class Form<T> extends AKElement {
|
||||
throw errorMessage;
|
||||
}
|
||||
// assign all input-related errors to their elements
|
||||
const elements: HorizontalFormElement[] = form._getSubmittableElements();
|
||||
const elements =
|
||||
this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
|
||||
"ak-form-element-horizontal",
|
||||
) || [];
|
||||
elements.forEach((element) => {
|
||||
element.requestUpdate();
|
||||
const elementName = element.name;
|
||||
if (!elementName) return;
|
||||
if (camelToSnake(elementName) in errorMessage) {
|
||||
@ -296,13 +303,7 @@ export class Form<T> extends AKElement {
|
||||
}
|
||||
|
||||
renderVisible(): TemplateResult {
|
||||
return html`<iron-form
|
||||
@iron-form-presubmit=${(ev: Event) => {
|
||||
this.submit(ev);
|
||||
}}
|
||||
>
|
||||
${this.renderNonFieldErrors()} ${this.renderForm()}
|
||||
</iron-form>`;
|
||||
return html` ${this.renderNonFieldErrors()} ${this.renderForm()}`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
|
@ -69,6 +69,10 @@ export class HorizontalFormElement extends AKElement {
|
||||
@property()
|
||||
name = "";
|
||||
|
||||
firstUpdated(): void {
|
||||
this.updated();
|
||||
}
|
||||
|
||||
updated(): void {
|
||||
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => {
|
||||
input.focus();
|
||||
@ -89,7 +93,7 @@ export class HorizontalFormElement extends AKElement {
|
||||
case "ak-chip-group":
|
||||
case "ak-search-select":
|
||||
case "ak-radio":
|
||||
(input as HTMLInputElement).name = this.name;
|
||||
input.setAttribute("name", this.name);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
@ -108,6 +112,7 @@ export class HorizontalFormElement extends AKElement {
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
this.updated();
|
||||
return html`<div class="pf-c-form__group">
|
||||
<div class="pf-c-form__group-label">
|
||||
<label class="pf-c-form__label">
|
||||
|
@ -70,6 +70,7 @@ export class SearchSelect<T> extends AKElement {
|
||||
observer: IntersectionObserver;
|
||||
dropdownUID: string;
|
||||
dropdownContainer: HTMLDivElement;
|
||||
isFetchingData = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@ -103,13 +104,18 @@ export class SearchSelect<T> extends AKElement {
|
||||
}
|
||||
|
||||
updateData(): void {
|
||||
if (this.isFetchingData) {
|
||||
return;
|
||||
}
|
||||
this.isFetchingData = true;
|
||||
this.fetchObjects(this.query).then((objects) => {
|
||||
this.objects = objects;
|
||||
this.objects.forEach((obj) => {
|
||||
objects.forEach((obj) => {
|
||||
if (this.selected && this.selected(obj, this.objects || [])) {
|
||||
this.selectedObject = obj;
|
||||
}
|
||||
});
|
||||
this.objects = objects;
|
||||
this.isFetchingData = false;
|
||||
});
|
||||
}
|
||||
|
||||
@ -200,9 +206,10 @@ export class SearchSelect<T> extends AKElement {
|
||||
render(
|
||||
html`<div
|
||||
class="pf-c-dropdown pf-m-expanded"
|
||||
?hidden=${!this.open}
|
||||
style="position: fixed; inset: 0px auto auto 0px; z-index: 9999; transform: translate(${pos.x}px, ${pos.y +
|
||||
this.offsetHeight}px); width: ${pos.width}px;"
|
||||
this.offsetHeight}px); width: ${pos.width}px; ${this.open
|
||||
? ""
|
||||
: "visibility: hidden;"}"
|
||||
>
|
||||
<ul
|
||||
class="pf-c-dropdown__menu pf-m-static"
|
||||
@ -249,6 +256,14 @@ export class SearchSelect<T> extends AKElement {
|
||||
|
||||
render(): TemplateResult {
|
||||
this.renderMenu();
|
||||
let value = "";
|
||||
if (!this.objects) {
|
||||
value = t`Loading...`;
|
||||
} else if (this.selectedObject) {
|
||||
value = this.renderElement(this.selectedObject);
|
||||
} else if (this.blankable) {
|
||||
value = this.emptyOption;
|
||||
}
|
||||
return html`<div class="pf-c-select">
|
||||
<div class="pf-c-select__toggle pf-m-typeahead">
|
||||
<div class="pf-c-select__toggle-wrapper">
|
||||
@ -256,6 +271,7 @@ export class SearchSelect<T> extends AKElement {
|
||||
class="pf-c-form-control pf-c-select__toggle-typeahead"
|
||||
type="text"
|
||||
placeholder=${this.placeholder}
|
||||
spellcheck="false"
|
||||
@input=${(ev: InputEvent) => {
|
||||
this.query = (ev.target as HTMLInputElement).value;
|
||||
this.updateData();
|
||||
@ -285,11 +301,7 @@ export class SearchSelect<T> extends AKElement {
|
||||
this.open = false;
|
||||
this.renderMenu();
|
||||
}}
|
||||
.value=${this.selectedObject
|
||||
? this.renderElement(this.selectedObject)
|
||||
: this.blankable
|
||||
? this.emptyOption
|
||||
: ""}
|
||||
.value=${value}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -553,7 +553,7 @@ export class FlowExecutor extends Interface implements StageHost {
|
||||
? html`
|
||||
<li>
|
||||
<a
|
||||
href="https://unsplash.com/@saishmenon"
|
||||
href="https://unsplash.com/@aaronburden"
|
||||
>${t`Background image`}</a
|
||||
>
|
||||
</li>
|
||||
|
@ -68,6 +68,7 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
|
||||
if (this.timer) {
|
||||
console.debug("authentik/stages/password: cleared focus timer");
|
||||
window.clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
// @ts-ignore
|
||||
window["polymerSkipLoadingFontRoboto"] = true;
|
||||
import "construct-style-sheets-polyfill";
|
||||
import "@webcomponents/webcomponentsjs";
|
||||
import "lit/polyfill-support.js";
|
||||
|
@ -83,9 +83,6 @@ export class UserInterface extends Interface {
|
||||
.pf-c-page {
|
||||
background-color: transparent;
|
||||
}
|
||||
:host([theme="dark"]) .pf-c-page {
|
||||
background-color: var(--ak-dark-background);
|
||||
}
|
||||
.background-wrapper {
|
||||
background-color: var(--pf-c-page--BackgroundColor) !important;
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ When authenticating with a flow, you'll get an authenticated Session cookie, tha
|
||||
|
||||
### API Token
|
||||
|
||||
Superusers can create tokens to authenticate as any user with a static key, which can optionally be expiring and auto-rotate.
|
||||
Users can create tokens to authenticate as any user with a static key, which can optionally be expiring and auto-rotate.
|
||||
|
||||
### JWT Token
|
||||
|
||||
|
27
website/developer-docs/blueprints/v1/models.md
Normal file
27
website/developer-docs/blueprints/v1/models.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Models
|
||||
|
||||
Some models behave differently and allow for access to different API fields when created via blueprint.
|
||||
|
||||
### `authentik_core.token`
|
||||
|
||||
:::info
|
||||
Requires authentik 2023.4
|
||||
:::
|
||||
|
||||
Via the standard API, a token's key cannot be changed, it can only be rotated. This is to ensure a high entropy in it's key, and to prevent insecure data from being used. However, when provisioning tokens via a blueprint, it may be required to set a token to an existing value.
|
||||
|
||||
With blueprints, the field `key` can be set, to set the token's key to any value.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
# [...]
|
||||
- model: authentik_core.token
|
||||
state: present
|
||||
identifiers:
|
||||
identifier: my-token
|
||||
attrs:
|
||||
key: this-should-be-a-long-value
|
||||
user: !KeyOf my-user
|
||||
intent: api
|
||||
```
|
@ -27,6 +27,8 @@
|
||||
|
||||
- Update `website/sidebars.js` to include the new release notes, and move the oldest release into the `Previous versions` category.
|
||||
|
||||
If the release notes are created in advance without a fixed date for the release, only add them to the sidebar once the release is published.
|
||||
|
||||
- Run `make website`
|
||||
|
||||
#### For subsequent releases:
|
||||
|
@ -1,14 +1,14 @@
|
||||
---
|
||||
title: docker-compose installation
|
||||
title: Docker Compose installation
|
||||
---
|
||||
|
||||
This installation method is for test-setups and small-scale productive setups.
|
||||
|
||||
## Requirements
|
||||
|
||||
- A Linux host with at least 2 CPU cores and 2 GB of RAM.
|
||||
- docker
|
||||
- docker-compose
|
||||
- A host with at least 2 CPU cores and 2 GB of RAM
|
||||
- Docker
|
||||
- Docker Compose
|
||||
|
||||
## Preparation
|
||||
|
||||
@ -31,7 +31,7 @@ echo "AUTHENTIK_ERROR_REPORTING__ENABLED=true" >> .env
|
||||
|
||||
It is also recommended to configure global email credentials. These are used by authentik to notify you about alerts and configuration issues. They can also be used by [Email stages](../flow/stages/email/) to send verification/recovery emails.
|
||||
|
||||
Append this block to your `.env` file
|
||||
To configure email credentials, append this block to your `.env` file
|
||||
|
||||
```shell
|
||||
# SMTP Host Emails are sent to
|
||||
@ -49,55 +49,55 @@ AUTHENTIK_EMAIL__TIMEOUT=10
|
||||
AUTHENTIK_EMAIL__FROM=authentik@localhost
|
||||
```
|
||||
|
||||
## Running on Port 80/443
|
||||
## Configure for port 80/443
|
||||
|
||||
By default, authentik listens on port 9000 for HTTP and 9443 for HTTPS. To change this, you can set the following variables in `.env`:
|
||||
By default, authentik listens on port 9000 for HTTP and 9443 for HTTPS. To change the default and instead use ports 80 and 443, you can set the following variables in `.env`:
|
||||
|
||||
```shell
|
||||
AUTHENTIK_PORT_HTTP=80
|
||||
AUTHENTIK_PORT_HTTPS=443
|
||||
```
|
||||
|
||||
Afterwards, make sure to run `docker-compose up -d`.
|
||||
Be sure to run `docker-compose up -d` to rebuild with the new port numbers.
|
||||
|
||||
## Startup
|
||||
|
||||
Afterwards, run these commands to finish
|
||||
Afterwards, run these commands to finish:
|
||||
|
||||
```shell
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
The compose file statically references the latest version available at the time of downloading the compose file, which can be overridden with the `AUTHENTIK_TAG` environment variable.
|
||||
The `docker-compose.yml` file statically references the latest version available at the time of downloading the compose file, which can be overridden with the `AUTHENTIK_TAG` environment variable.
|
||||
|
||||
authentik will then be reachable on port 9000 (HTTP) and port 9443 (HTTPS).
|
||||
authentik is then reachable (by default) on port 9000 (HTTP) and port 9443 (HTTPS).
|
||||
|
||||
To start the initial setup, navigate to `https://<your server>/if/flow/initial-setup/`. There you will be prompted to set a password for the akadmin user.
|
||||
To start the initial setup, navigate to `https://<your server's IP or hostname>:9000/if/flow/initial-setup/`.
|
||||
|
||||
There you will be prompted to set a password for the akadmin user (the default user).
|
||||
|
||||
## Explanation
|
||||
|
||||
:::warning
|
||||
The server assumes to have local timezone as UTC.
|
||||
All internals are handled in UTC, whenever a time is displayed to the user in UI it gets localized.
|
||||
All internals are handled in UTC; whenever a time is displayed to the user in UI it gets localized.
|
||||
Do not update or mount `/etc/timezone` or `/etc/localtime` in the authentik containers.
|
||||
This will not give any advantages.
|
||||
On the contrary, it will cause problems with OAuth and SAML authentication,
|
||||
e.g. [see this GitHub issue](https://github.com/goauthentik/authentik/issues/3005).
|
||||
:::
|
||||
|
||||
The docker-compose project contains the following containers:
|
||||
The Docker-Compose project contains the following containers:
|
||||
|
||||
- server
|
||||
|
||||
This is the backend service, which does all the logic, runs the API and the actual SSO part. It also runs the frontend, hosts the JS/CSS files, and also serves the files you've uploaded for icons/etc.
|
||||
This is the backend service, which does all the logic, plus runs the API and the SSO functionality. It also runs the frontend, hosts the JS/CSS files, and serves the files you've uploaded for icons/etc.
|
||||
|
||||
- worker
|
||||
|
||||
This container executes background tasks, everything you can see on the _System Tasks_ page in the frontend.
|
||||
|
||||
- redis & postgresql
|
||||
- redis (for cache)
|
||||
|
||||
Cache and database respectively.
|
||||
|
||||
Additionally, if you've enabled GeoIP, there is a container running that regularly updates the GeoIP database.
|
||||
- postgresql (default database)
|
||||
|
@ -2,7 +2,7 @@
|
||||
title: Docker
|
||||
---
|
||||
|
||||
The docker integration will automatically deploy and manage outpost containers using the Docker HTTP API.
|
||||
The Docker integration automatically deploys and manages outpost containers using the Docker HTTP API.
|
||||
|
||||
This integration has the advantage over manual deployments of automatic updates (whenever authentik is updated, it updates the outposts), and authentik can (in a future version) automatically rotate the token that the outpost uses to communicate with the core authentik server.
|
||||
|
||||
@ -10,8 +10,8 @@ The following outpost settings are used:
|
||||
|
||||
- `object_naming_template`: Configures how the container is called
|
||||
- `container_image`: Optionally overwrites the standard container image (see [Configuration](../../installation/configuration.md) to configure the global default)
|
||||
- `docker_network`: The docker network the container should be added to. This needs to be modified if you plan to connect to authentik using the internal hostname.
|
||||
- `docker_map_ports`: Enable/disable the mapping of ports. When using a proxy outpost with traefik for example, you might not want to bind ports as they are routed through traefik.
|
||||
- `docker_network`: The Docker network the container should be added to. This needs to be modified if you plan to connect to authentik using the internal hostname.
|
||||
- `docker_map_ports`: Enable/disable the mapping of ports. When using a proxy outpost with Traefik for example, you might not want to bind ports as they are routed through Traefik.
|
||||
- `docker_labels`: Optional additional labels that can be applied to the container.
|
||||
|
||||
The container is created with the following hardcoded properties:
|
||||
@ -20,7 +20,7 @@ The container is created with the following hardcoded properties:
|
||||
|
||||
- `io.goauthentik.outpost-uuid`: Used by authentik to identify the container, and to allow for name changes.
|
||||
|
||||
Additionally, the proxy outposts have the following extra labels to add themselves into traefik automatically.
|
||||
Additionally, the proxy outposts have the following extra labels to add themselves into Traefik automatically.
|
||||
|
||||
- `traefik.enable`: "true"
|
||||
- `traefik.http.routers.ak-outpost-<outpost-name>-router.rule`: `Host(...)`
|
||||
@ -32,7 +32,7 @@ The container is created with the following hardcoded properties:
|
||||
|
||||
## Permissions
|
||||
|
||||
To minimise the potential risks of mapping the docker socket into a container/giving an application access to the docker API, many people use Projects like [docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy). authentik requires these permissions from the docker API:
|
||||
To minimise the potential risks of mapping the Docker socket into a container/giving an application access to the Docker API, many people use Projects like [docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy). authentik requires these permissions from the Docker API:
|
||||
|
||||
- Images/Pull: authentik tries to pre-pull the custom image if one is configured, otherwise falling back to the default image.
|
||||
- Containers/Read: Gather infos about currently running container
|
||||
@ -42,18 +42,18 @@ To minimise the potential risks of mapping the docker socket into a container/gi
|
||||
|
||||
## Remote hosts (TLS)
|
||||
|
||||
To connect remote hosts, you can follow this Guide from Docker [Use TLS (HTTPS) to protect the Docker daemon socket](https://docs.docker.com/engine/security/protect-access/#use-tls-https-to-protect-the-docker-daemon-socket) to configure Docker.
|
||||
To connect remote hosts, follow this guide from Docker [Use TLS (HTTPS) to protect the Docker daemon socket](https://docs.docker.com/engine/security/protect-access/#use-tls-https-to-protect-the-docker-daemon-socket) to configure Docker.
|
||||
|
||||
Afterwards, create two Certificate-keypairs in authentik:
|
||||
Afterwards, create two certificate-keypairs in authentik:
|
||||
|
||||
- `Docker CA`, with the contents of `~/.docker/ca.pem` as Certificate
|
||||
- `Docker Cert`, with the contents of `~/.docker/cert.pem` as Certificate and `~/.docker/key.pem` as Private key.
|
||||
- `Docker Cert`, with the contents of `~/.docker/cert.pem` as the certificate and `~/.docker/key.pem` as the private key.
|
||||
|
||||
Create an integration with `Docker CA` as _TLS Verification Certificate_ and `Docker Cert` as _TLS Authentication Certificate_.
|
||||
|
||||
## Remote hosts (SSH)
|
||||
|
||||
Starting with authentik 2021.12.5, you can connect to remote docker hosts using SSH. To configure this, create a new SSH keypair using these commands:
|
||||
Starting with authentik 2021.12.5, you can connect to remote Docker hosts using SSH. To configure this, create a new SSH keypair using these commands:
|
||||
|
||||
```
|
||||
# Generate the keypair itself, using RSA keys in the PEM format
|
||||
|
@ -7,11 +7,19 @@ Using forward auth uses your existing reverse proxy to do the proxying, and only
|
||||
To use forward auth instead of proxying, you have to change a couple of settings.
|
||||
In the Proxy Provider, make sure to use one of the Forward auth modes.
|
||||
|
||||
## Single application
|
||||
## Forward auth modes
|
||||
|
||||
The only configuration difference between single application mode and domain level mode is the host that you specify.
|
||||
|
||||
For single application, you'd use the domain that the application is running on, and only `/outpost.goauthentik.io` is redirected to the outpost.
|
||||
|
||||
For domain level, you'd use the same domain as authentik.
|
||||
|
||||
### Single application
|
||||
|
||||
Single application mode works for a single application hosted on its dedicated subdomain. This has the advantage that you can still do per-application access policies in authentik.
|
||||
|
||||
## Domain level
|
||||
### Domain level
|
||||
|
||||
To use forward auth instead of proxying, you have to change a couple of settings.
|
||||
In the Proxy Provider, make sure to use the _Forward auth (domain level)_ mode.
|
||||
@ -21,10 +29,13 @@ This mode differs from the _Forward auth (single application)_ mode in the follo
|
||||
- You don't have to configure an application in authentik for each domain
|
||||
- Users don't have to authorize multiple times
|
||||
|
||||
There are however also some downsides, mainly the fact that you **can't** restrict individual applications to different users.
|
||||
There are, however, also some downsides, mainly the fact that you **can't** restrict individual applications to different users.
|
||||
|
||||
The only configuration difference between single application and domain level is the host you specify.
|
||||
## Configuration templates
|
||||
|
||||
For single application, you'd use the domain which the application is running on, and only `/outpost.goauthentik.io` is redirected to the outpost.
|
||||
For configuration templates for each web server, refer to the following:
|
||||
|
||||
For domain level, you'd use the same domain as authentik.
|
||||
import DocCardList from "@theme/DocCardList";
|
||||
import { useCurrentSidebarCategory } from "@docusaurus/theme-common";
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items} />
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user