Compare commits
11 Commits
flows/buff
...
providers/
| Author | SHA1 | Date | |
|---|---|---|---|
| 7549a6b83d | |||
| bb45b714e2 | |||
| 20e07486ee | |||
| 0cb7cf2c96 | |||
| 07736a90b2 | |||
| ec28a86259 | |||
| 260800c60b | |||
| ee4780394d | |||
| 23b746941f | |||
| 3c2ce40afd | |||
| 2aceed285e |
2
Makefile
2
Makefile
@ -94,7 +94,7 @@ gen-build: ## Extract the schema from the database
|
||||
AUTHENTIK_DEBUG=true \
|
||||
AUTHENTIK_TENANTS__ENABLED=true \
|
||||
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
||||
uv run ak make_blueprint_schema > blueprints/schema.json
|
||||
uv run ak make_blueprint_schema --file blueprints/schema.json
|
||||
AUTHENTIK_DEBUG=true \
|
||||
AUTHENTIK_TENANTS__ENABLED=true \
|
||||
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
||||
|
||||
@ -72,20 +72,33 @@ class Command(BaseCommand):
|
||||
"additionalProperties": True,
|
||||
},
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [],
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/$defs/blueprint_entry"},
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/$defs/blueprint_entry"},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"$defs": {},
|
||||
"$defs": {"blueprint_entry": {"oneOf": []}},
|
||||
}
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--file", type=str)
|
||||
|
||||
@no_translations
|
||||
def handle(self, *args, **options):
|
||||
def handle(self, *args, file: str, **options):
|
||||
"""Generate JSON Schema for blueprints"""
|
||||
self.build()
|
||||
self.stdout.write(dumps(self.schema, indent=4, default=Command.json_default))
|
||||
with open(file, "w") as _schema:
|
||||
_schema.write(dumps(self.schema, indent=4, default=Command.json_default))
|
||||
|
||||
@staticmethod
|
||||
def json_default(value: Any) -> Any:
|
||||
@ -112,7 +125,7 @@ class Command(BaseCommand):
|
||||
}
|
||||
)
|
||||
model_path = f"{model._meta.app_label}.{model._meta.model_name}"
|
||||
self.schema["properties"]["entries"]["items"]["oneOf"].append(
|
||||
self.schema["$defs"]["blueprint_entry"]["oneOf"].append(
|
||||
self.template_entry(model_path, model, serializer)
|
||||
)
|
||||
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
version: 1
|
||||
entries:
|
||||
- identifiers:
|
||||
name: "%(id)s"
|
||||
slug: "%(id)s"
|
||||
model: authentik_flows.flow
|
||||
state: present
|
||||
attrs:
|
||||
designation: stage_configuration
|
||||
title: foo
|
||||
foo:
|
||||
- identifiers:
|
||||
name: "%(id)s"
|
||||
slug: "%(id)s"
|
||||
model: authentik_flows.flow
|
||||
state: present
|
||||
attrs:
|
||||
designation: stage_configuration
|
||||
title: foo
|
||||
|
||||
@ -191,11 +191,18 @@ class Blueprint:
|
||||
"""Dataclass used for a full export"""
|
||||
|
||||
version: int = field(default=1)
|
||||
entries: list[BlueprintEntry] = field(default_factory=list)
|
||||
entries: list[BlueprintEntry] | dict[str, list[BlueprintEntry]] = field(default_factory=list)
|
||||
context: dict = field(default_factory=dict)
|
||||
|
||||
metadata: BlueprintMetadata | None = field(default=None)
|
||||
|
||||
def iter_entries(self) -> Iterable[BlueprintEntry]:
|
||||
if isinstance(self.entries, dict):
|
||||
for _section, entries in self.entries.items():
|
||||
yield from entries
|
||||
else:
|
||||
yield from self.entries
|
||||
|
||||
|
||||
class YAMLTag:
|
||||
"""Base class for all YAML Tags"""
|
||||
@ -226,7 +233,7 @@ class KeyOf(YAMLTag):
|
||||
self.id_from = node.value
|
||||
|
||||
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
|
||||
for _entry in blueprint.entries:
|
||||
for _entry in blueprint.iter_entries():
|
||||
if _entry.id == self.id_from and _entry._state.instance:
|
||||
# Special handling for PolicyBindingModels, as they'll have a different PK
|
||||
# which is used when creating policy bindings
|
||||
|
||||
@ -384,7 +384,7 @@ class Importer:
|
||||
def _apply_models(self, raise_errors=False) -> bool:
|
||||
"""Apply (create/update) models yaml"""
|
||||
self.__pk_map = {}
|
||||
for entry in self._import.entries:
|
||||
for entry in self._import.iter_entries():
|
||||
model_app_label, model_name = entry.get_model(self._import).split(".")
|
||||
try:
|
||||
model: type[SerializerModel] = registry.get_model(model_app_label, model_name)
|
||||
|
||||
@ -69,7 +69,6 @@ SESSION_KEY_APPLICATION_PRE = "authentik/flows/application_pre"
|
||||
SESSION_KEY_GET = "authentik/flows/get"
|
||||
SESSION_KEY_POST = "authentik/flows/post"
|
||||
SESSION_KEY_HISTORY = "authentik/flows/history"
|
||||
SESSION_KEY_AUTH_STARTED = "authentik/flows/auth_started"
|
||||
QS_KEY_TOKEN = "flow_token" # nosec
|
||||
QS_QUERY = "query"
|
||||
|
||||
@ -455,7 +454,6 @@ class FlowExecutorView(APIView):
|
||||
SESSION_KEY_APPLICATION_PRE,
|
||||
SESSION_KEY_PLAN,
|
||||
SESSION_KEY_GET,
|
||||
SESSION_KEY_AUTH_STARTED,
|
||||
# We might need the initial POST payloads for later requests
|
||||
# SESSION_KEY_POST,
|
||||
# We don't delete the history on purpose, as a user might
|
||||
|
||||
@ -6,8 +6,7 @@ from django.shortcuts import get_object_or_404
|
||||
from ua_parser.user_agent_parser import Parse
|
||||
|
||||
from authentik.core.views.interface import InterfaceView
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.flows.views.executor import SESSION_KEY_AUTH_STARTED
|
||||
from authentik.flows.models import Flow
|
||||
|
||||
|
||||
class FlowInterfaceView(InterfaceView):
|
||||
@ -15,12 +14,6 @@ class FlowInterfaceView(InterfaceView):
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
||||
if (
|
||||
not self.request.user.is_authenticated
|
||||
and flow.designation == FlowDesignation.AUTHENTICATION
|
||||
):
|
||||
self.request.session[SESSION_KEY_AUTH_STARTED] = True
|
||||
self.request.session.save()
|
||||
kwargs["flow"] = flow
|
||||
kwargs["flow_background_url"] = flow.background_url(self.request)
|
||||
kwargs["inspector"] = "inspector" in self.request.GET
|
||||
|
||||
@ -39,4 +39,3 @@ class AuthentikPoliciesConfig(ManagedAppConfig):
|
||||
label = "authentik_policies"
|
||||
verbose_name = "authentik Policies"
|
||||
default = True
|
||||
mountpoint = "policy/"
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
{% extends 'login/base_full.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<script>
|
||||
let redirecting = false;
|
||||
const checkAuth = async () => {
|
||||
if (redirecting) return true;
|
||||
const url = "{{ check_auth_url }}";
|
||||
console.debug("authentik/policies/buffer: Checking authentication...");
|
||||
try {
|
||||
const result = await fetch(url, {
|
||||
method: "HEAD",
|
||||
});
|
||||
if (result.status >= 400) {
|
||||
return false
|
||||
}
|
||||
console.debug("authentik/policies/buffer: Continuing");
|
||||
redirecting = true;
|
||||
if ("{{ auth_req_method }}" === "post") {
|
||||
document.querySelector("form").submit();
|
||||
} else {
|
||||
window.location.assign("{{ continue_url|escapejs }}");
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let timeout = 100;
|
||||
let offset = 20;
|
||||
let attempt = 0;
|
||||
const main = async () => {
|
||||
attempt += 1;
|
||||
await checkAuth();
|
||||
console.debug(`authentik/policies/buffer: Waiting ${timeout}ms...`);
|
||||
setTimeout(main, timeout);
|
||||
timeout += (offset * attempt);
|
||||
if (timeout >= 2000) {
|
||||
timeout = 2000;
|
||||
}
|
||||
}
|
||||
document.addEventListener("visibilitychange", async () => {
|
||||
if (document.hidden) return;
|
||||
console.debug("authentik/policies/buffer: Checking authentication on tab activate...");
|
||||
await checkAuth();
|
||||
});
|
||||
main();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% trans 'Waiting for authentication...' %} - {{ brand.branding_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
{% trans 'Waiting for authentication...' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form class="pf-c-form" method="{{ auth_req_method }}" action="{{ continue_url }}">
|
||||
{% if auth_req_method == "post" %}
|
||||
{% for key, value in auth_req_body.items %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<div class="pf-c-empty-state__icon">
|
||||
<span class="pf-c-spinner pf-m-xl" role="progressbar">
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans "You're already authenticating in another tab. This page will refresh once authentication is completed." %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<a href="{{ auth_req_url }}" class="pf-c-button pf-m-primary pf-m-block">
|
||||
{% trans "Authenticate in this tab" %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@ -1,121 +0,0 @@
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http import HttpResponse
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.models import Application, Provider
|
||||
from authentik.core.tests.utils import create_test_flow, create_test_user
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import dummy_get_response
|
||||
from authentik.policies.views import (
|
||||
QS_BUFFER_ID,
|
||||
SESSION_KEY_BUFFER,
|
||||
BufferedPolicyAccessView,
|
||||
BufferView,
|
||||
PolicyAccessView,
|
||||
)
|
||||
|
||||
|
||||
class TestPolicyViews(TestCase):
|
||||
"""Test PolicyAccessView"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.factory = RequestFactory()
|
||||
self.user = create_test_user()
|
||||
|
||||
def test_pav(self):
|
||||
"""Test simple policy access view"""
|
||||
provider = Provider.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||
|
||||
class TestView(PolicyAccessView):
|
||||
def resolve_provider_application(self):
|
||||
self.provider = provider
|
||||
self.application = app
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return HttpResponse("foo")
|
||||
|
||||
req = self.factory.get("/")
|
||||
req.user = self.user
|
||||
res = TestView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.content, b"foo")
|
||||
|
||||
def test_pav_buffer(self):
|
||||
"""Test simple policy access view"""
|
||||
provider = Provider.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
||||
|
||||
class TestView(BufferedPolicyAccessView):
|
||||
def resolve_provider_application(self):
|
||||
self.provider = provider
|
||||
self.application = app
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return HttpResponse("foo")
|
||||
|
||||
req = self.factory.get("/")
|
||||
req.user = AnonymousUser()
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(req)
|
||||
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
|
||||
req.session.save()
|
||||
res = TestView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
self.assertTrue(res.url.startswith(reverse("authentik_policies:buffer")))
|
||||
|
||||
def test_pav_buffer_skip(self):
|
||||
"""Test simple policy access view (skip buffer)"""
|
||||
provider = Provider.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
||||
|
||||
class TestView(BufferedPolicyAccessView):
|
||||
def resolve_provider_application(self):
|
||||
self.provider = provider
|
||||
self.application = app
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return HttpResponse("foo")
|
||||
|
||||
req = self.factory.get("/?skip_buffer=true")
|
||||
req.user = AnonymousUser()
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(req)
|
||||
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
|
||||
req.session.save()
|
||||
res = TestView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
self.assertTrue(res.url.startswith(reverse("authentik_flows:default-authentication")))
|
||||
|
||||
def test_buffer(self):
|
||||
"""Test buffer view"""
|
||||
uid = generate_id()
|
||||
req = self.factory.get(f"/?{QS_BUFFER_ID}={uid}")
|
||||
req.user = AnonymousUser()
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(req)
|
||||
ts = generate_id()
|
||||
req.session[SESSION_KEY_BUFFER % uid] = {
|
||||
"method": "get",
|
||||
"body": {},
|
||||
"url": f"/{ts}",
|
||||
}
|
||||
req.session.save()
|
||||
|
||||
res = BufferView.as_view()(req)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertIn(ts, res.render().content.decode())
|
||||
@ -1,14 +1,7 @@
|
||||
"""API URLs"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from authentik.policies.api.bindings import PolicyBindingViewSet
|
||||
from authentik.policies.api.policies import PolicyViewSet
|
||||
from authentik.policies.views import BufferView
|
||||
|
||||
urlpatterns = [
|
||||
path("buffer", BufferView.as_view(), name="buffer"),
|
||||
]
|
||||
|
||||
api_urlpatterns = [
|
||||
("policies/all", PolicyViewSet),
|
||||
|
||||
@ -1,37 +1,23 @@
|
||||
"""authentik access helper classes"""
|
||||
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import AccessMixin
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.http import HttpRequest, HttpResponse, QueryDict
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic.base import TemplateView, View
|
||||
from django.views.generic.base import View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import Application, Provider, User
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views.executor import (
|
||||
SESSION_KEY_APPLICATION_PRE,
|
||||
SESSION_KEY_AUTH_STARTED,
|
||||
SESSION_KEY_PLAN,
|
||||
SESSION_KEY_POST,
|
||||
)
|
||||
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_POST
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.policies.denied import AccessDeniedResponse
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
QS_BUFFER_ID = "af_bf_id"
|
||||
QS_SKIP_BUFFER = "skip_buffer"
|
||||
SESSION_KEY_BUFFER = "authentik/policies/pav_buffer/%s"
|
||||
|
||||
|
||||
class RequestValidationError(SentryIgnoredException):
|
||||
@ -139,65 +125,3 @@ class PolicyAccessView(AccessMixin, View):
|
||||
for message in result.messages:
|
||||
messages.error(self.request, _(message))
|
||||
return result
|
||||
|
||||
|
||||
def url_with_qs(url: str, **kwargs):
|
||||
"""Update/set querystring of `url` with the parameters in `kwargs`. Original query string
|
||||
parameters are retained"""
|
||||
if "?" not in url:
|
||||
return url + f"?{urlencode(kwargs)}"
|
||||
url, _, qs = url.partition("?")
|
||||
qs = QueryDict(qs, mutable=True)
|
||||
qs.update(kwargs)
|
||||
return url + f"?{urlencode(qs.items())}"
|
||||
|
||||
|
||||
class BufferView(TemplateView):
|
||||
"""Buffer view"""
|
||||
|
||||
template_name = "policies/buffer.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
buf_id = self.request.GET.get(QS_BUFFER_ID)
|
||||
buffer: dict = self.request.session.get(SESSION_KEY_BUFFER % buf_id)
|
||||
kwargs["auth_req_method"] = buffer["method"]
|
||||
kwargs["auth_req_body"] = buffer["body"]
|
||||
kwargs["auth_req_url"] = url_with_qs(buffer["url"], **{QS_SKIP_BUFFER: True})
|
||||
kwargs["check_auth_url"] = reverse("authentik_api:user-me")
|
||||
kwargs["continue_url"] = url_with_qs(buffer["url"], **{QS_BUFFER_ID: buf_id})
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class BufferedPolicyAccessView(PolicyAccessView):
|
||||
"""PolicyAccessView which buffers access requests in case the user is not logged in"""
|
||||
|
||||
def handle_no_permission(self):
|
||||
plan: FlowPlan | None = self.request.session.get(SESSION_KEY_PLAN)
|
||||
authenticating = self.request.session.get(SESSION_KEY_AUTH_STARTED)
|
||||
if plan:
|
||||
flow = Flow.objects.filter(pk=plan.flow_pk).first()
|
||||
if not flow or flow.designation != FlowDesignation.AUTHENTICATION:
|
||||
LOGGER.debug("Not buffering request, no flow or flow not for authentication")
|
||||
return super().handle_no_permission()
|
||||
if not plan and authenticating is None:
|
||||
LOGGER.debug("Not buffering request, no flow plan active")
|
||||
return super().handle_no_permission()
|
||||
if self.request.GET.get(QS_SKIP_BUFFER):
|
||||
LOGGER.debug("Not buffering request, explicit skip")
|
||||
return super().handle_no_permission()
|
||||
buffer_id = str(uuid4())
|
||||
LOGGER.debug("Buffering access request", bf_id=buffer_id)
|
||||
self.request.session[SESSION_KEY_BUFFER % buffer_id] = {
|
||||
"body": self.request.POST,
|
||||
"url": self.request.build_absolute_uri(self.request.get_full_path()),
|
||||
"method": self.request.method.lower(),
|
||||
}
|
||||
return redirect(
|
||||
url_with_qs(reverse("authentik_policies:buffer"), **{QS_BUFFER_ID: buffer_id})
|
||||
)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
if QS_BUFFER_ID in self.request.GET:
|
||||
self.request.session.pop(SESSION_KEY_BUFFER % self.request.GET[QS_BUFFER_ID], None)
|
||||
return response
|
||||
|
||||
@ -15,6 +15,7 @@ class OAuth2Error(SentryIgnoredException):
|
||||
|
||||
error: str
|
||||
description: str
|
||||
cause: str | None = None
|
||||
|
||||
def create_dict(self):
|
||||
"""Return error as dict for JSON Rendering"""
|
||||
@ -34,6 +35,10 @@ class OAuth2Error(SentryIgnoredException):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def with_cause(self, cause: str):
|
||||
self.cause = cause
|
||||
return self
|
||||
|
||||
|
||||
class RedirectUriError(OAuth2Error):
|
||||
"""The request fails due to a missing, invalid, or mismatching
|
||||
|
||||
@ -12,7 +12,7 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.providers.oauth2.constants import TOKEN_TYPE
|
||||
from authentik.providers.oauth2.constants import SCOPE_OFFLINE_ACCESS, SCOPE_OPENID, TOKEN_TYPE
|
||||
from authentik.providers.oauth2.errors import AuthorizeError, ClientIdError, RedirectUriError
|
||||
from authentik.providers.oauth2.models import (
|
||||
AccessToken,
|
||||
@ -43,7 +43,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
|
||||
)
|
||||
with self.assertRaises(AuthorizeError):
|
||||
with self.assertRaises(AuthorizeError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@ -53,6 +53,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.error, "unsupported_response_type")
|
||||
|
||||
def test_invalid_client_id(self):
|
||||
"""Test invalid client ID"""
|
||||
@ -68,7 +69,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
|
||||
)
|
||||
with self.assertRaises(AuthorizeError):
|
||||
with self.assertRaises(AuthorizeError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@ -79,19 +80,30 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.error, "request_not_supported")
|
||||
|
||||
def test_invalid_redirect_uri(self):
|
||||
"""test missing/invalid redirect URI"""
|
||||
def test_invalid_redirect_uri_missing(self):
|
||||
"""test missing redirect URI"""
|
||||
OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
with self.assertRaises(RedirectUriError) as cm:
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
self.assertEqual(cm.exception.cause, "redirect_uri_missing")
|
||||
|
||||
def test_invalid_redirect_uri(self):
|
||||
"""test invalid redirect URI"""
|
||||
OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@ -101,6 +113,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.cause, "redirect_uri_no_match")
|
||||
|
||||
def test_blocked_redirect_uri(self):
|
||||
"""test missing/invalid redirect URI"""
|
||||
@ -108,9 +121,9 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "data:local.invalid")],
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "data:localhost")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
with self.assertRaises(RedirectUriError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@ -120,6 +133,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.cause, "redirect_uri_forbidden_scheme")
|
||||
|
||||
def test_invalid_redirect_uri_empty(self):
|
||||
"""test missing/invalid redirect URI"""
|
||||
@ -129,9 +143,6 @@ class TestAuthorize(OAuthTestCase):
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@ -150,12 +161,9 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid?")],
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.REGEX, "http://local.invalid?")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
with self.assertRaises(RedirectUriError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@ -165,6 +173,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.cause, "redirect_uri_no_match")
|
||||
|
||||
def test_redirect_uri_invalid_regex(self):
|
||||
"""test missing/invalid redirect URI (invalid regex)"""
|
||||
@ -172,12 +181,9 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "+")],
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.REGEX, "+")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
with self.assertRaises(RedirectUriError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@ -187,23 +193,22 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.cause, "redirect_uri_no_match")
|
||||
|
||||
def test_empty_redirect_uri(self):
|
||||
"""test empty redirect URI (configure in provider)"""
|
||||
def test_redirect_uri_regex(self):
|
||||
"""test valid redirect URI (regex)"""
|
||||
OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.REGEX, ".+")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://localhost",
|
||||
"redirect_uri": "http://foo.bar.baz",
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
@ -258,7 +263,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
GrantTypes.IMPLICIT,
|
||||
)
|
||||
# Implicit without openid scope
|
||||
with self.assertRaises(AuthorizeError):
|
||||
with self.assertRaises(AuthorizeError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@ -285,7 +290,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
self.assertEqual(
|
||||
OAuthAuthorizationParams.from_request(request).grant_type, GrantTypes.HYBRID
|
||||
)
|
||||
with self.assertRaises(AuthorizeError):
|
||||
with self.assertRaises(AuthorizeError) as cm:
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
@ -295,6 +300,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.error, "unsupported_response_type")
|
||||
|
||||
def test_full_code(self):
|
||||
"""Test full authorization"""
|
||||
@ -615,3 +621,54 @@ class TestAuthorize(OAuthTestCase):
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def test_openid_missing_invalid(self):
|
||||
"""test request requiring an OpenID scope to be set"""
|
||||
OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||
)
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "id_token",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://localhost",
|
||||
"scope": "",
|
||||
},
|
||||
)
|
||||
with self.assertRaises(AuthorizeError) as cm:
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
self.assertEqual(cm.exception.cause, "scope_openid_missing")
|
||||
|
||||
@apply_blueprint("system/providers-oauth2.yaml")
|
||||
def test_offline_access_invalid(self):
|
||||
"""test request for offline_access with invalid response type"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
managed__in=[
|
||||
"goauthentik.io/providers/oauth2/scope-openid",
|
||||
"goauthentik.io/providers/oauth2/scope-offline_access",
|
||||
]
|
||||
)
|
||||
)
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "id_token",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://localhost",
|
||||
"scope": f"{SCOPE_OPENID} {SCOPE_OFFLINE_ACCESS}",
|
||||
"nonce": generate_id(),
|
||||
},
|
||||
)
|
||||
parsed = OAuthAuthorizationParams.from_request(request)
|
||||
self.assertNotIn(SCOPE_OFFLINE_ACCESS, parsed.scope)
|
||||
|
||||
@ -30,7 +30,7 @@ from authentik.flows.stage import StageView
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.lib.views import bad_request_message
|
||||
from authentik.policies.types import PolicyRequest
|
||||
from authentik.policies.views import BufferedPolicyAccessView, RequestValidationError
|
||||
from authentik.policies.views import PolicyAccessView, RequestValidationError
|
||||
from authentik.providers.oauth2.constants import (
|
||||
PKCE_METHOD_PLAIN,
|
||||
PKCE_METHOD_S256,
|
||||
@ -190,7 +190,7 @@ class OAuthAuthorizationParams:
|
||||
allowed_redirect_urls = self.provider.redirect_uris
|
||||
if not self.redirect_uri:
|
||||
LOGGER.warning("Missing redirect uri.")
|
||||
raise RedirectUriError("", allowed_redirect_urls)
|
||||
raise RedirectUriError("", allowed_redirect_urls).with_cause("redirect_uri_missing")
|
||||
|
||||
if len(allowed_redirect_urls) < 1:
|
||||
LOGGER.info("Setting redirect for blank redirect_uris", redirect=self.redirect_uri)
|
||||
@ -219,10 +219,14 @@ class OAuthAuthorizationParams:
|
||||
provider=self.provider,
|
||||
)
|
||||
if not match_found:
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls).with_cause(
|
||||
"redirect_uri_no_match"
|
||||
)
|
||||
# Check against forbidden schemes
|
||||
if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls).with_cause(
|
||||
"redirect_uri_forbidden_scheme"
|
||||
)
|
||||
|
||||
def check_scope(self, github_compat=False):
|
||||
"""Ensure openid scope is set in Hybrid flows, or when requesting an id_token"""
|
||||
@ -251,7 +255,9 @@ class OAuthAuthorizationParams:
|
||||
or self.response_type in [ResponseTypes.ID_TOKEN, ResponseTypes.ID_TOKEN_TOKEN]
|
||||
):
|
||||
LOGGER.warning("Missing 'openid' scope.")
|
||||
raise AuthorizeError(self.redirect_uri, "invalid_scope", self.grant_type, self.state)
|
||||
raise AuthorizeError(
|
||||
self.redirect_uri, "invalid_scope", self.grant_type, self.state
|
||||
).with_cause("scope_openid_missing")
|
||||
if SCOPE_OFFLINE_ACCESS in self.scope:
|
||||
# https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
||||
# Don't explicitly request consent with offline_access, as the spec allows for
|
||||
@ -286,7 +292,9 @@ class OAuthAuthorizationParams:
|
||||
return
|
||||
if not self.nonce:
|
||||
LOGGER.warning("Missing nonce for OpenID Request")
|
||||
raise AuthorizeError(self.redirect_uri, "invalid_request", self.grant_type, self.state)
|
||||
raise AuthorizeError(
|
||||
self.redirect_uri, "invalid_request", self.grant_type, self.state
|
||||
).with_cause("none_missing")
|
||||
|
||||
def check_code_challenge(self):
|
||||
"""PKCE validation of the transformation method."""
|
||||
@ -326,7 +334,7 @@ class OAuthAuthorizationParams:
|
||||
return code
|
||||
|
||||
|
||||
class AuthorizationFlowInitView(BufferedPolicyAccessView):
|
||||
class AuthorizationFlowInitView(PolicyAccessView):
|
||||
"""OAuth2 Flow initializer, checks access to application and starts flow"""
|
||||
|
||||
params: OAuthAuthorizationParams
|
||||
@ -345,10 +353,10 @@ class AuthorizationFlowInitView(BufferedPolicyAccessView):
|
||||
self.request, github_compat=self.github_compat
|
||||
)
|
||||
except AuthorizeError as error:
|
||||
LOGGER.warning(error.description, redirect_uri=error.redirect_uri)
|
||||
LOGGER.warning(error.description, redirect_uri=error.redirect_uri, cause=error.cause)
|
||||
raise RequestValidationError(error.get_response(self.request)) from None
|
||||
except OAuth2Error as error:
|
||||
LOGGER.warning(error.description)
|
||||
LOGGER.warning(error.description, cause=error.cause)
|
||||
raise RequestValidationError(
|
||||
bad_request_message(self.request, error.description, title=error.error)
|
||||
) from None
|
||||
|
||||
@ -18,11 +18,14 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
||||
from authentik.flows.stage import RedirectStage
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.policies.views import BufferedPolicyAccessView
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
PLAN_CONNECTION_SETTINGS = "connection_settings"
|
||||
|
||||
|
||||
class RACStartView(BufferedPolicyAccessView):
|
||||
class RACStartView(PolicyAccessView):
|
||||
"""Start a RAC connection by checking access and creating a connection token"""
|
||||
|
||||
endpoint: Endpoint
|
||||
@ -109,10 +112,15 @@ class RACFinalStage(RedirectStage):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_challenge(self, *args, **kwargs) -> RedirectChallenge:
|
||||
settings = self.executor.plan.context.get(PLAN_CONNECTION_SETTINGS)
|
||||
if not settings:
|
||||
settings = self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).get(
|
||||
PLAN_CONNECTION_SETTINGS
|
||||
)
|
||||
token = ConnectionToken.objects.create(
|
||||
provider=self.provider,
|
||||
endpoint=self.endpoint,
|
||||
settings=self.executor.plan.context.get("connection_settings", {}),
|
||||
settings=settings or {},
|
||||
session=self.request.session["authenticatedsession"],
|
||||
expires=now() + timedelta_from_string(self.provider.connection_expiry),
|
||||
expiring=True,
|
||||
|
||||
@ -35,8 +35,8 @@ REQUEST_KEY_SAML_SIG_ALG = "SigAlg"
|
||||
REQUEST_KEY_SAML_RESPONSE = "SAMLResponse"
|
||||
REQUEST_KEY_RELAY_STATE = "RelayState"
|
||||
|
||||
PLAN_CONTEXT_SAML_AUTH_N_REQUEST = "authentik/providers/saml/authn_request"
|
||||
PLAN_CONTEXT_SAML_LOGOUT_REQUEST = "authentik/providers/saml/logout_request"
|
||||
SESSION_KEY_AUTH_N_REQUEST = "authentik/providers/saml/authn_request"
|
||||
SESSION_KEY_LOGOUT_REQUEST = "authentik/providers/saml/logout_request"
|
||||
|
||||
|
||||
# This View doesn't have a URL on purpose, as its called by the FlowExecutor
|
||||
@ -50,11 +50,10 @@ class SAMLFlowFinalView(ChallengeStageView):
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
application: Application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
|
||||
provider: SAMLProvider = get_object_or_404(SAMLProvider, pk=application.provider_id)
|
||||
if PLAN_CONTEXT_SAML_AUTH_N_REQUEST not in self.executor.plan.context:
|
||||
self.logger.warning("No AuthNRequest in context")
|
||||
if SESSION_KEY_AUTH_N_REQUEST not in self.request.session:
|
||||
return self.executor.stage_invalid()
|
||||
|
||||
auth_n_request: AuthNRequest = self.executor.plan.context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST]
|
||||
auth_n_request: AuthNRequest = self.request.session.pop(SESSION_KEY_AUTH_N_REQUEST)
|
||||
try:
|
||||
response = AssertionProcessor(provider, request, auth_n_request).build_response()
|
||||
except SAMLException as exc:
|
||||
@ -107,3 +106,6 @@ class SAMLFlowFinalView(ChallengeStageView):
|
||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||
# We'll never get here since the challenge redirects to the SP
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
def cleanup(self):
|
||||
self.request.session.pop(SESSION_KEY_AUTH_N_REQUEST, None)
|
||||
|
||||
@ -19,9 +19,9 @@ from authentik.providers.saml.exceptions import CannotHandleAssertion
|
||||
from authentik.providers.saml.models import SAMLProvider
|
||||
from authentik.providers.saml.processors.logout_request_parser import LogoutRequestParser
|
||||
from authentik.providers.saml.views.flows import (
|
||||
PLAN_CONTEXT_SAML_LOGOUT_REQUEST,
|
||||
REQUEST_KEY_RELAY_STATE,
|
||||
REQUEST_KEY_SAML_REQUEST,
|
||||
SESSION_KEY_LOGOUT_REQUEST,
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
@ -33,10 +33,6 @@ class SAMLSLOView(PolicyAccessView):
|
||||
|
||||
flow: Flow
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.plan_context = {}
|
||||
|
||||
def resolve_provider_application(self):
|
||||
self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"])
|
||||
self.provider: SAMLProvider = get_object_or_404(
|
||||
@ -63,7 +59,6 @@ class SAMLSLOView(PolicyAccessView):
|
||||
request,
|
||||
{
|
||||
PLAN_CONTEXT_APPLICATION: self.application,
|
||||
**self.plan_context,
|
||||
},
|
||||
)
|
||||
plan.append_stage(in_memory_stage(SessionEndStage))
|
||||
@ -88,7 +83,7 @@ class SAMLSLOBindingRedirectView(SAMLSLOView):
|
||||
self.request.GET[REQUEST_KEY_SAML_REQUEST],
|
||||
relay_state=self.request.GET.get(REQUEST_KEY_RELAY_STATE, None),
|
||||
)
|
||||
self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request
|
||||
self.request.session[SESSION_KEY_LOGOUT_REQUEST] = logout_request
|
||||
except CannotHandleAssertion as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
@ -116,7 +111,7 @@ class SAMLSLOBindingPOSTView(SAMLSLOView):
|
||||
payload[REQUEST_KEY_SAML_REQUEST],
|
||||
relay_state=payload.get(REQUEST_KEY_RELAY_STATE, None),
|
||||
)
|
||||
self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request
|
||||
self.request.session[SESSION_KEY_LOGOUT_REQUEST] = logout_request
|
||||
except CannotHandleAssertion as exc:
|
||||
LOGGER.info(str(exc))
|
||||
return bad_request_message(self.request, str(exc))
|
||||
|
||||
@ -15,16 +15,16 @@ from authentik.flows.models import in_memory_stage
|
||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
||||
from authentik.flows.views.executor import SESSION_KEY_POST
|
||||
from authentik.lib.views import bad_request_message
|
||||
from authentik.policies.views import BufferedPolicyAccessView
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
||||
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
|
||||
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
|
||||
from authentik.providers.saml.views.flows import (
|
||||
PLAN_CONTEXT_SAML_AUTH_N_REQUEST,
|
||||
REQUEST_KEY_RELAY_STATE,
|
||||
REQUEST_KEY_SAML_REQUEST,
|
||||
REQUEST_KEY_SAML_SIG_ALG,
|
||||
REQUEST_KEY_SAML_SIGNATURE,
|
||||
SESSION_KEY_AUTH_N_REQUEST,
|
||||
SAMLFlowFinalView,
|
||||
)
|
||||
from authentik.stages.consent.stage import (
|
||||
@ -35,14 +35,10 @@ from authentik.stages.consent.stage import (
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class SAMLSSOView(BufferedPolicyAccessView):
|
||||
class SAMLSSOView(PolicyAccessView):
|
||||
"""SAML SSO Base View, which plans a flow and injects our final stage.
|
||||
Calls get/post handler."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.plan_context = {}
|
||||
|
||||
def resolve_provider_application(self):
|
||||
self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"])
|
||||
self.provider: SAMLProvider = get_object_or_404(
|
||||
@ -72,7 +68,6 @@ class SAMLSSOView(BufferedPolicyAccessView):
|
||||
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
||||
% {"application": self.application.name},
|
||||
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
|
||||
**self.plan_context,
|
||||
},
|
||||
)
|
||||
except FlowNonApplicableException:
|
||||
@ -88,7 +83,7 @@ class SAMLSSOView(BufferedPolicyAccessView):
|
||||
|
||||
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||
"""GET and POST use the same handler, but we can't
|
||||
override .dispatch easily because BufferedPolicyAccessView's dispatch"""
|
||||
override .dispatch easily because PolicyAccessView's dispatch"""
|
||||
return self.get(request, application_slug)
|
||||
|
||||
|
||||
@ -108,7 +103,7 @@ class SAMLSSOBindingRedirectView(SAMLSSOView):
|
||||
self.request.GET.get(REQUEST_KEY_SAML_SIGNATURE),
|
||||
self.request.GET.get(REQUEST_KEY_SAML_SIG_ALG),
|
||||
)
|
||||
self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request
|
||||
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
|
||||
except CannotHandleAssertion as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
@ -142,7 +137,7 @@ class SAMLSSOBindingPOSTView(SAMLSSOView):
|
||||
payload[REQUEST_KEY_SAML_REQUEST],
|
||||
payload.get(REQUEST_KEY_RELAY_STATE),
|
||||
)
|
||||
self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request
|
||||
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
|
||||
except CannotHandleAssertion as exc:
|
||||
LOGGER.info(str(exc))
|
||||
return bad_request_message(self.request, str(exc))
|
||||
@ -156,4 +151,4 @@ class SAMLSSOBindingInitView(SAMLSSOView):
|
||||
"""Create SAML Response from scratch"""
|
||||
LOGGER.debug("No SAML Request, using IdP-initiated flow.")
|
||||
auth_n_request = AuthNRequestParser(self.provider).idp_initiated()
|
||||
self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request
|
||||
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -6,18 +6,18 @@
|
||||
# Translators:
|
||||
# jcamat, 2022
|
||||
# Angel, 2024
|
||||
# Iamanaws, 2024
|
||||
# Marcelo Elizeche Landó, 2025
|
||||
# Jens L. <jens@goauthentik.io>, 2025
|
||||
# Iamanaws, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
|
||||
"POT-Creation-Date: 2025-06-04 00:12+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n"
|
||||
"Last-Translator: Iamanaws, 2025\n"
|
||||
"Language-Team: Spanish (https://app.transifex.com/authentik/teams/119923/es/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@ -111,7 +111,7 @@ msgstr "Certificado Web usado por el servidor web Core de authentik"
|
||||
|
||||
#: authentik/brands/models.py
|
||||
msgid "Certificates used for client authentication."
|
||||
msgstr ""
|
||||
msgstr "Certificados utilizados para la autenticación del cliente."
|
||||
|
||||
#: authentik/brands/models.py
|
||||
msgid "Brand"
|
||||
@ -131,7 +131,7 @@ msgstr "Descripción adicional no disponible."
|
||||
|
||||
#: authentik/core/api/groups.py
|
||||
msgid "Cannot set group as parent of itself."
|
||||
msgstr "No se puede establecer el grupo como padre de sí mismo."
|
||||
msgstr "No se puede establecer un grupo como su propio padre."
|
||||
|
||||
#: authentik/core/api/providers.py
|
||||
msgid ""
|
||||
@ -183,11 +183,11 @@ msgstr "Remueve usuario del grupo"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Enable superuser status"
|
||||
msgstr "Habiliar estado de \"superusuario\""
|
||||
msgstr "Habilitar el estado de superusuario"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Disable superuser status"
|
||||
msgstr "Deshabiliar estado de \"superusuario\""
|
||||
msgstr "Deshabilitar el estado de superusuario"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "User's display name."
|
||||
@ -241,7 +241,7 @@ msgstr "Flujo utilizado al autorizar a este proveedor."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Flow used ending the session from a provider."
|
||||
msgstr "Flujo usado para terminar la sesión de un proveedor."
|
||||
msgstr "Flujo utilizado para finalizar la sesión desde un proveedor."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
@ -273,11 +273,11 @@ msgstr "Aplicaciones"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Application Entitlement"
|
||||
msgstr ""
|
||||
msgstr "Derecho de Aplicación"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Application Entitlements"
|
||||
msgstr ""
|
||||
msgstr "Derechos de Aplicación"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Use the source-specific identifier"
|
||||
@ -288,9 +288,9 @@ msgid ""
|
||||
"Link to a user with identical email address. Can have security implications "
|
||||
"when a source doesn't validate email addresses."
|
||||
msgstr ""
|
||||
"Apunta a un usuario con una dirección de correo electrónico idéntica. Puede "
|
||||
"tener implicaciones de seguridad cuando una fuente no valida la dirección de"
|
||||
" correo electrónico."
|
||||
"Enlace a un usuario con la misma dirección de correo electrónico. Puede "
|
||||
"tener implicaciones de seguridad cuando una fuente no valida las direcciones"
|
||||
" de correo electrónico."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
@ -305,8 +305,8 @@ msgid ""
|
||||
"Link to a user with identical username. Can have security implications when "
|
||||
"a username is used with another source."
|
||||
msgstr ""
|
||||
"Enlace a un usuario con un nombre de usuario idéntico. Puede tener "
|
||||
"implicaciones de seguridad cuando se usa un nombre de usuario con otra "
|
||||
"Enlace a un usuario con el mismo nombre de usuario. Puede tener "
|
||||
"implicaciones de seguridad cuando un nombre de usuario se utiliza con otra "
|
||||
"fuente."
|
||||
|
||||
#: authentik/core/models.py
|
||||
@ -322,8 +322,8 @@ msgid ""
|
||||
"Link to a group with identical name. Can have security implications when a "
|
||||
"group name is used with another source."
|
||||
msgstr ""
|
||||
"Enlace a un grupo con un nombre idéntico. Puede tener implicaciones de "
|
||||
"seguridad cuando se utiliza un nombre de grupo con otra fuente."
|
||||
"Enlace a un grupo con el mismo nombre. Puede tener implicaciones de "
|
||||
"seguridad cuando un nombre de grupo se utiliza con otra fuente."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Use the group name, but deny enrollment when the name already exists."
|
||||
@ -385,7 +385,7 @@ msgstr "Asignaciones de Propiedades"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "session data"
|
||||
msgstr ""
|
||||
msgstr "datos de sesión"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Session"
|
||||
@ -424,7 +424,7 @@ msgstr "¡Autenticado exitosamente con {source}!"
|
||||
#: authentik/core/sources/flow_manager.py
|
||||
#, python-brace-format
|
||||
msgid "Successfully linked {source}!"
|
||||
msgstr "¡{source} vinculado exitosamente!"
|
||||
msgstr "¡{source} enlazado correctamente!"
|
||||
|
||||
#: authentik/core/sources/flow_manager.py
|
||||
msgid "Source is not configured for enrollment."
|
||||
@ -476,11 +476,11 @@ msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Certificate-Key Pair"
|
||||
msgstr "Par de claves de certificado"
|
||||
msgstr "Par Certificado-Clave"
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Certificate-Key Pairs"
|
||||
msgstr "Pares de claves de certificado"
|
||||
msgstr "Pares Certificado-Clave"
|
||||
|
||||
#: authentik/enterprise/api.py
|
||||
msgid "Enterprise is required to create/update this object."
|
||||
@ -511,7 +511,7 @@ msgstr ""
|
||||
|
||||
#: authentik/enterprise/policies/unique_password/models.py
|
||||
msgid "Number of passwords to check against."
|
||||
msgstr ""
|
||||
msgstr "Número de contraseñas contra las que verificar."
|
||||
|
||||
#: authentik/enterprise/policies/unique_password/models.py
|
||||
#: authentik/policies/password/models.py
|
||||
@ -521,18 +521,20 @@ msgstr "La contraseña no se ha establecido en contexto"
|
||||
#: authentik/enterprise/policies/unique_password/models.py
|
||||
msgid "This password has been used previously. Please choose a different one."
|
||||
msgstr ""
|
||||
"Esta contraseña se ha utilizado anteriormente. Por favor, elija una "
|
||||
"diferente."
|
||||
|
||||
#: authentik/enterprise/policies/unique_password/models.py
|
||||
msgid "Password Uniqueness Policy"
|
||||
msgstr ""
|
||||
msgstr "Política de Unicidad de Contraseñas"
|
||||
|
||||
#: authentik/enterprise/policies/unique_password/models.py
|
||||
msgid "Password Uniqueness Policies"
|
||||
msgstr ""
|
||||
msgstr "Políticas de Unicidad de Contraseñas"
|
||||
|
||||
#: authentik/enterprise/policies/unique_password/models.py
|
||||
msgid "User Password History"
|
||||
msgstr ""
|
||||
msgstr "Historial de Contraseñas del Usuario"
|
||||
|
||||
#: authentik/enterprise/policy.py
|
||||
msgid "Enterprise required to access this feature."
|
||||
@ -617,39 +619,39 @@ msgstr "Clave de firma"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Key used to sign the SSF Events."
|
||||
msgstr ""
|
||||
msgstr "Clave utilizada para firmar los eventos SSF."
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Shared Signals Framework Provider"
|
||||
msgstr ""
|
||||
msgstr "Proveedor del Marco de Señales Compartidas"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Shared Signals Framework Providers"
|
||||
msgstr ""
|
||||
msgstr "Proveedores del Marco de Señales Compartidas"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "Add stream to SSF provider"
|
||||
msgstr ""
|
||||
msgstr "Agregar flujo de datos al proveedor SSF"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Stream"
|
||||
msgstr ""
|
||||
msgstr "Flujo de Datos SSF"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Streams"
|
||||
msgstr ""
|
||||
msgstr "Flujos de Datos SSF"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Stream Event"
|
||||
msgstr ""
|
||||
msgstr "Evento de Flujo de Datos SSF"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
msgid "SSF Stream Events"
|
||||
msgstr ""
|
||||
msgstr "Eventos de Flujos de Datos SSF"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/tasks.py
|
||||
msgid "Failed to send request"
|
||||
msgstr "Falló envio de petición"
|
||||
msgstr "Error al enviar la solicitud"
|
||||
|
||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
||||
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
|
||||
@ -681,26 +683,29 @@ msgid ""
|
||||
"option has a higher priority than the `client_certificate` option on "
|
||||
"`Brand`."
|
||||
msgstr ""
|
||||
"Configura las autoridades certificadoras para validar el certificado. Esta "
|
||||
"opción tiene una prioridad mayor que la opción `client_certificate` en "
|
||||
"`Brand`."
|
||||
|
||||
#: authentik/enterprise/stages/mtls/models.py
|
||||
msgid "Mutual TLS Stage"
|
||||
msgstr ""
|
||||
msgstr "Etapa de TLS mutuo"
|
||||
|
||||
#: authentik/enterprise/stages/mtls/models.py
|
||||
msgid "Mutual TLS Stages"
|
||||
msgstr ""
|
||||
msgstr "Etapas de TLS mutuo"
|
||||
|
||||
#: authentik/enterprise/stages/mtls/models.py
|
||||
msgid "Permissions to pass Certificates for outposts."
|
||||
msgstr ""
|
||||
msgstr "Permisos para pasar Certificados a los puestos avanzados."
|
||||
|
||||
#: authentik/enterprise/stages/mtls/stage.py
|
||||
msgid "Certificate required but no certificate was given."
|
||||
msgstr ""
|
||||
msgstr "Se requiere certificado, pero no se proporcionó ninguno."
|
||||
|
||||
#: authentik/enterprise/stages/mtls/stage.py
|
||||
msgid "No user found for certificate."
|
||||
msgstr ""
|
||||
msgstr "No se encontró usuario para el certificado."
|
||||
|
||||
#: authentik/enterprise/stages/source/models.py
|
||||
msgid ""
|
||||
@ -753,12 +758,16 @@ msgid ""
|
||||
"Customize the body of the request. Mapping should return data that is JSON-"
|
||||
"serializable."
|
||||
msgstr ""
|
||||
"Personaliza el cuerpo de la solicitud. El mapeo debe devolver datos que sean"
|
||||
" serializables en JSON."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Configure additional headers to be sent. Mapping should return a dictionary "
|
||||
"of key-value pairs"
|
||||
msgstr ""
|
||||
"Configura encabezados adicionales para enviar. El mapeo debe devolver un "
|
||||
"diccionario de pares clave-valor"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
@ -786,7 +795,7 @@ msgstr "Transporte de notificaciones"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Notification Transports"
|
||||
msgstr "Transportes de notificación"
|
||||
msgstr "Medios de Notificación"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Notice"
|
||||
@ -813,9 +822,9 @@ msgid ""
|
||||
"Select which transports should be used to notify the user. If none are "
|
||||
"selected, the notification will only be shown in the authentik UI."
|
||||
msgstr ""
|
||||
"Seleccione qué transportes se deben usar para notificar al usuario. Si no se"
|
||||
" selecciona ninguno, la notificación solo se mostrará en la interfaz de "
|
||||
"usuario de authentik."
|
||||
"Selecciona qué medios se deben usar para notificar al usuario. Si no se "
|
||||
"selecciona ninguno, la notificación solo se mostrará en la interfaz de "
|
||||
"authentik."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Controls which severity level the created notifications will have."
|
||||
@ -987,7 +996,7 @@ msgstr "Evalúa políticas durante el proceso de planeación del Flujo."
|
||||
|
||||
#: authentik/flows/models.py
|
||||
msgid "Evaluate policies when the Stage is presented to the user."
|
||||
msgstr ""
|
||||
msgstr "Evaluar las políticas cuando la Etapa se presenta al usuario."
|
||||
|
||||
#: authentik/flows/models.py
|
||||
msgid ""
|
||||
@ -1034,6 +1043,8 @@ msgid ""
|
||||
"When enabled, provider will not modify or create objects in the remote "
|
||||
"system."
|
||||
msgstr ""
|
||||
"Cuando está habilitado, el proveedor no modificará ni creará objetos en el "
|
||||
"sistema remoto."
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Starting full provider sync"
|
||||
@ -1041,20 +1052,21 @@ msgstr "Iniciando sincronización completa de proveedor"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Syncing users"
|
||||
msgstr ""
|
||||
msgstr "Sincronizando usuarios"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Syncing groups"
|
||||
msgstr ""
|
||||
msgstr "Sincronizando grupos"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "Syncing page {page} of groups"
|
||||
msgstr "Sincronizando página {page} de grupos"
|
||||
msgid "Syncing page {page} of {object_type}"
|
||||
msgstr "Sincronizando página {page} de {object_type}"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Dropping mutating request due to dry run"
|
||||
msgstr ""
|
||||
"Descartando solicitud de mutación debido a ejecución en modo de simulación"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
#, python-brace-format
|
||||
@ -1233,7 +1245,7 @@ msgstr ""
|
||||
|
||||
#: authentik/policies/expiry/models.py
|
||||
msgid "Password has expired."
|
||||
msgstr "La contraseña ha caducado."
|
||||
msgstr "La contraseña ha expirado."
|
||||
|
||||
#: authentik/policies/expiry/models.py
|
||||
msgid "Password Expiry Policy"
|
||||
@ -1271,7 +1283,7 @@ msgstr "La IP del cliente no está en un país permitido."
|
||||
|
||||
#: authentik/policies/geoip/models.py
|
||||
msgid "Distance from previous authentication is larger than threshold."
|
||||
msgstr "La distancia desde la autenticación previa es mayor que el límite."
|
||||
msgstr "La distancia desde la autenticación anterior es mayor que el umbral."
|
||||
|
||||
#: authentik/policies/geoip/models.py
|
||||
msgid "Distance is further than possible."
|
||||
@ -1320,7 +1332,7 @@ msgstr "Vinculación de Políticas"
|
||||
|
||||
#: authentik/policies/models.py
|
||||
msgid "Policy Bindings"
|
||||
msgstr "Vinculaciones de políticas"
|
||||
msgstr "Vinculaciones de Políticas"
|
||||
|
||||
#: authentik/policies/models.py
|
||||
msgid ""
|
||||
@ -1594,11 +1606,11 @@ msgstr "ES256 (Encriptación Asimétrica)"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "ES384 (Asymmetric Encryption)"
|
||||
msgstr ""
|
||||
msgstr "ES384 (Encriptación Asimétrica)"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "ES512 (Asymmetric Encryption)"
|
||||
msgstr ""
|
||||
msgstr "ES512 (Encriptación Asimétrica)"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Scope used by the client"
|
||||
@ -1813,7 +1825,7 @@ msgstr "Valida Certificados SSL de servidores de origen"
|
||||
|
||||
#: authentik/providers/proxy/models.py
|
||||
msgid "Internal host SSL Validation"
|
||||
msgstr "Validación SSL de host interno"
|
||||
msgstr "Validación SSL del host interno"
|
||||
|
||||
#: authentik/providers/proxy/models.py
|
||||
msgid ""
|
||||
@ -2027,7 +2039,7 @@ msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "AuthnContextClassRef Property Mapping"
|
||||
msgstr ""
|
||||
msgstr "Asignación de Propiedades de AuthnContextClassRef"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
@ -2035,6 +2047,9 @@ msgid ""
|
||||
"empty, the AuthnContextClassRef will be set based on which authentication "
|
||||
"methods the user used to authenticate."
|
||||
msgstr ""
|
||||
"Configura cómo se creará el valor de AuthnContextClassRef. Si se deja vacío,"
|
||||
" el AuthnContextClassRef se establecerá según los métodos de autenticación "
|
||||
"que el usuario haya utilizado para autenticarse."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
@ -2184,11 +2199,11 @@ msgstr "Predeterminado"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "AWS"
|
||||
msgstr ""
|
||||
msgstr "AWS"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Slack"
|
||||
msgstr ""
|
||||
msgstr "Slack"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Base URL to SCIM requests, usually ends in /v2"
|
||||
@ -2200,11 +2215,13 @@ msgstr "Token de Autenticación"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Compatibility Mode"
|
||||
msgstr ""
|
||||
msgstr "Modo de Compatibilidad SCIM"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
|
||||
msgstr ""
|
||||
"Modificar el comportamiento de authentik para implementaciones SCIM "
|
||||
"específicas de proveedores."
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Provider"
|
||||
@ -2232,7 +2249,7 @@ msgstr "Roles"
|
||||
|
||||
#: authentik/rbac/models.py
|
||||
msgid "Initial Permissions"
|
||||
msgstr ""
|
||||
msgstr "Permisos Iniciales"
|
||||
|
||||
#: authentik/rbac/models.py
|
||||
msgid "System permission"
|
||||
@ -2270,7 +2287,7 @@ msgstr ""
|
||||
|
||||
#: authentik/recovery/views.py
|
||||
msgid "Used recovery-link to authenticate."
|
||||
msgstr "Se usó el enlace de recuperación para autenticarse."
|
||||
msgstr "Se utilizó un enlace de recuperación para autenticarse."
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "Kerberos realm"
|
||||
@ -2282,7 +2299,7 @@ msgstr "krb5.conf personalizado a usar. Usa el del sistema por defecto."
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "KAdmin server type"
|
||||
msgstr ""
|
||||
msgstr "Tipo de servidor KAdmin"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "Sync users from Kerberos into authentik"
|
||||
@ -2290,23 +2307,24 @@ msgstr "Sincronizar usuarios desde Kerberos hacia Authentik"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "When a user changes their password, sync it back to Kerberos"
|
||||
msgstr "Cuando un usuario cambia su contraseña, sincronizarlo hacia Kerberos"
|
||||
msgstr ""
|
||||
"Cuando un usuario cambie su contraseña, sincronizarla de vuelta a Kerberos."
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "Principal to authenticate to kadmin for sync."
|
||||
msgstr "Principal para autenticarse como kadmin para la sincronización."
|
||||
msgstr "Principal para autenticarse en kadmin para la sincronización."
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "Password to authenticate to kadmin for sync"
|
||||
msgstr "Contraseña para autenticarse como kadmin para la sincronización"
|
||||
msgstr "Contraseña para autenticarse en kadmin para la sincronización"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid ""
|
||||
"Keytab to authenticate to kadmin for sync. Must be base64-encoded or in the "
|
||||
"form TYPE:residual"
|
||||
msgstr ""
|
||||
"Keytab para autenticarse como kadmin para la sincronización. Debe estar "
|
||||
"codificado en base64 o en el formato TIPO:residual"
|
||||
"Keytab para autenticarse en kadmin para la sincronización. Debe estar "
|
||||
"codificado en base64 o en el formato TIPO:residuo"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid ""
|
||||
@ -2322,7 +2340,7 @@ msgid ""
|
||||
"HTTP@hostname"
|
||||
msgstr ""
|
||||
"Forzar el uso de un nombre de servidor específico para SPNEGO. Debe estar en"
|
||||
" el formato HTTP@nombredelservidor"
|
||||
" el formato HTTP@nombre_de_host"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "SPNEGO keytab base64-encoded or path to keytab in the form FILE:path"
|
||||
@ -2339,8 +2357,8 @@ msgid ""
|
||||
"If enabled, the authentik-stored password will be updated upon login with "
|
||||
"the Kerberos password backend"
|
||||
msgstr ""
|
||||
"Si está habilitado, la contraseña almacenada por authentik será actualizada "
|
||||
"al iniciar sesión con el backend de contraseñas Kerberos"
|
||||
"Si está habilitado, la contraseña almacenada en authentik se actualizará al "
|
||||
"iniciar sesión con el backend de contraseñas de Kerberos."
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "Kerberos Source"
|
||||
@ -2388,7 +2406,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Asegúrate de que tienes entradas válidas\n"
|
||||
" (se obtienen a través de kinit) \n"
|
||||
" (obtenibles mediante kinit) \n"
|
||||
" y de haber configurado correctamente el navegador.\n"
|
||||
" Por favor, contacta a tu administrador.\n"
|
||||
" "
|
||||
@ -2453,6 +2471,10 @@ msgstr "DN de grupo de adición"
|
||||
msgid "Consider Objects matching this filter to be Users."
|
||||
msgstr "Considere que los objetos que coinciden con este filtro son usuarios."
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "Attribute which matches the value of `group_membership_field`."
|
||||
msgstr "Atributo que coincide con el valor de `group_membership_field`."
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "Field which contains members of a group."
|
||||
msgstr "Campo que contiene los miembros de un grupo."
|
||||
@ -2485,12 +2507,17 @@ msgid ""
|
||||
"attribute. This allows nested group resolution on systems like FreeIPA and "
|
||||
"Active Directory"
|
||||
msgstr ""
|
||||
"Buscar la pertenencia a grupos basándose en un atributo del usuario en lugar"
|
||||
" de un atributo del grupo. Esto permite la resolución de grupos anidados en "
|
||||
"sistemas como FreeIPA y Active Directory"
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid ""
|
||||
"Delete authentik users and groups which were previously supplied by this "
|
||||
"source, but are now missing from it."
|
||||
msgstr ""
|
||||
"Eliminar usuarios y grupos de authentik que fueron proporcionados "
|
||||
"previamente por esta fuente, pero que ahora están ausentes."
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "LDAP Source"
|
||||
@ -2512,22 +2539,24 @@ msgstr "Asignaciones de Propiedades de Fuente de LDAP"
|
||||
msgid ""
|
||||
"Unique ID used while checking if this object still exists in the directory."
|
||||
msgstr ""
|
||||
"ID único utilizado para verificar si este objeto aún existe en el "
|
||||
"directorio."
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "User LDAP Source Connection"
|
||||
msgstr ""
|
||||
msgstr "Conexión de Fuente LDAP de Usuario"
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "User LDAP Source Connections"
|
||||
msgstr ""
|
||||
msgstr "Conexiones de Fuente LDAP de Usuario"
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "Group LDAP Source Connection"
|
||||
msgstr ""
|
||||
msgstr "Conexión de Fuente LDAP de Grupo"
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "Group LDAP Source Connections"
|
||||
msgstr ""
|
||||
msgstr "Conexiones de Fuente LDAP de Grupo"
|
||||
|
||||
#: authentik/sources/ldap/signals.py
|
||||
msgid "Password does not match Active Directory Complexity."
|
||||
@ -2539,11 +2568,11 @@ msgstr "No se recibió ningún token."
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "HTTP Basic Authentication"
|
||||
msgstr ""
|
||||
msgstr "Autenticación Básica HTTP"
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "Include the client ID and secret as request parameters"
|
||||
msgstr ""
|
||||
msgstr "Incluir el ID de cliente y el secreto como parámetros de la solicitud"
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "Request Token URL"
|
||||
@ -2590,6 +2619,8 @@ msgid ""
|
||||
"How to perform authentication during an authorization_code token request "
|
||||
"flow"
|
||||
msgstr ""
|
||||
"Cómo realizar la autenticación durante un flujo de solicitud de token con "
|
||||
"authorization_code"
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "OAuth Source"
|
||||
@ -2907,7 +2938,7 @@ msgstr "Conexiones de Fuente de SAML de Grupo"
|
||||
#: authentik/sources/saml/views.py
|
||||
#, python-brace-format
|
||||
msgid "Continue to {source_name}"
|
||||
msgstr ""
|
||||
msgstr "Continuar a {source_name}"
|
||||
|
||||
#: authentik/sources/scim/models.py
|
||||
msgid "SCIM Source"
|
||||
@ -2943,7 +2974,7 @@ msgstr "Dispositivos Duo"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
msgid "Email OTP"
|
||||
msgstr ""
|
||||
msgstr "OTP por Correo Electrónico"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
#: authentik/stages/email/models.py
|
||||
@ -2964,11 +2995,11 @@ msgstr ""
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
msgid "Email Authenticator Setup Stage"
|
||||
msgstr ""
|
||||
msgstr "Etapa de Configuración del Autenticador de Correo Electrónico"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
msgid "Email Authenticator Setup Stages"
|
||||
msgstr ""
|
||||
msgstr "Etapas de Configuración del Autenticador de Correo Electrónico"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
#: authentik/stages/authenticator_email/stage.py
|
||||
@ -2979,11 +3010,11 @@ msgstr ""
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
msgid "Email Device"
|
||||
msgstr "Dispositivo de Email"
|
||||
msgstr "Dispositivo de correo electrónico"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
msgid "Email Devices"
|
||||
msgstr "Dispositivos de Email"
|
||||
msgstr "Dispositivos de correo electrónico"
|
||||
|
||||
#: authentik/stages/authenticator_email/stage.py
|
||||
#: authentik/stages/authenticator_sms/stage.py
|
||||
@ -2993,7 +3024,7 @@ msgstr "El código no coincide"
|
||||
|
||||
#: authentik/stages/authenticator_email/stage.py
|
||||
msgid "Invalid email"
|
||||
msgstr "Email Inválido"
|
||||
msgstr "Correo electrónico inválido"
|
||||
|
||||
#: authentik/stages/authenticator_email/templates/email/email_otp.html
|
||||
#: authentik/stages/email/templates/email/password_reset.html
|
||||
@ -3013,6 +3044,9 @@ msgid ""
|
||||
" Email MFA code.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Código MFA por correo electrónico.\n"
|
||||
" "
|
||||
|
||||
#: authentik/stages/authenticator_email/templates/email/email_otp.html
|
||||
#, python-format
|
||||
@ -3022,7 +3056,8 @@ msgid ""
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Si no solicitaste este código, por favor ignora este correo. El código anterior es válido por %(expires)s."
|
||||
" Si no solicitaste este código, por favor ignora este correo. El código anterior es válido por %(expires)s.\n"
|
||||
" "
|
||||
|
||||
#: authentik/stages/authenticator_email/templates/email/email_otp.txt
|
||||
#: authentik/stages/email/templates/email/password_reset.txt
|
||||
@ -3035,6 +3070,8 @@ msgid ""
|
||||
"\n"
|
||||
"Email MFA code\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Código MFA por correo electrónico\n"
|
||||
|
||||
#: authentik/stages/authenticator_email/templates/email/email_otp.txt
|
||||
#, python-format
|
||||
@ -3276,8 +3313,8 @@ msgstr "No se pudo validar el token"
|
||||
msgid ""
|
||||
"Offset after which consent expires. (Format: hours=1;minutes=2;seconds=3)."
|
||||
msgstr ""
|
||||
"Compensación después de la cual caduca el consentimiento. (Formato: horas = "
|
||||
"1; minutos = 2; segundos = 3)."
|
||||
"Desfase después del cual expira el consentimiento. (Formato: "
|
||||
"hours=1;minutes=2;seconds=3)."
|
||||
|
||||
#: authentik/stages/consent/models.py
|
||||
msgid "Consent Stage"
|
||||
@ -3297,7 +3334,7 @@ msgstr "Consentimientos del usuario"
|
||||
|
||||
#: authentik/stages/consent/stage.py
|
||||
msgid "Invalid consent token, re-showing prompt"
|
||||
msgstr ""
|
||||
msgstr "Token de consentimiento inválido, mostrando el aviso nuevamente"
|
||||
|
||||
#: authentik/stages/deny/models.py
|
||||
msgid "Deny Stage"
|
||||
@ -3317,11 +3354,11 @@ msgstr "Etapas ficticias"
|
||||
|
||||
#: authentik/stages/email/flow.py
|
||||
msgid "Continue to confirm this email address."
|
||||
msgstr ""
|
||||
msgstr "Continúa para confirmar esta dirección de correo electrónico."
|
||||
|
||||
#: authentik/stages/email/flow.py
|
||||
msgid "Link was already used, please request a new link."
|
||||
msgstr ""
|
||||
msgstr "El enlace ya fue utilizado, por favor, solícita uno nuevo."
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Password Reset"
|
||||
@ -3445,7 +3482,8 @@ msgid ""
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Si no solicitaste un cambio de contraseña, por favor ignora este correo. El enlace anterior es válido por %(expires)s."
|
||||
" Si no solicitaste un cambio de contraseña, por favor ignora este correo. El enlace anterior es válido por %(expires)s.\n"
|
||||
" "
|
||||
|
||||
#: authentik/stages/email/templates/email/password_reset.txt
|
||||
msgid ""
|
||||
@ -3529,24 +3567,26 @@ msgid ""
|
||||
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
|
||||
" to skip straight to entering their password."
|
||||
msgstr ""
|
||||
"Mostrar al usuario la opción \"Recordarme en este dispositivo\", permitiendo"
|
||||
" que los usuarios recurrentes pasen directamente a ingresar su contraseña."
|
||||
|
||||
#: authentik/stages/identification/models.py
|
||||
msgid "Optional enrollment flow, which is linked at the bottom of the page."
|
||||
msgstr ""
|
||||
"Flujo de inscripción opcional, que está vinculado en la parte inferior de la"
|
||||
" página."
|
||||
"Flujo de inscripción opcional, que se enlaza en la parte inferior de la "
|
||||
"página."
|
||||
|
||||
#: authentik/stages/identification/models.py
|
||||
msgid "Optional recovery flow, which is linked at the bottom of the page."
|
||||
msgstr ""
|
||||
"Flujo de recuperación opcional, que está vinculado en la parte inferior de "
|
||||
"la página."
|
||||
"Flujo de recuperación opcional, que se enlaza en la parte inferior de la "
|
||||
"página."
|
||||
|
||||
#: authentik/stages/identification/models.py
|
||||
msgid "Optional passwordless flow, which is linked at the bottom of the page."
|
||||
msgstr ""
|
||||
"Flujo sin contraseña opcional, el cual está vinculado en la parte inferior "
|
||||
"de la página."
|
||||
"Flujo opcional sin contraseña, que se enlaza en la parte inferior de la "
|
||||
"página."
|
||||
|
||||
#: authentik/stages/identification/models.py
|
||||
msgid "Specify which sources should be shown."
|
||||
@ -3780,11 +3820,11 @@ msgstr "Las contraseñas no coinciden."
|
||||
|
||||
#: authentik/stages/redirect/api.py
|
||||
msgid "Target URL should be present when mode is Static."
|
||||
msgstr ""
|
||||
msgstr "La URL de destino debe estar presente cuando el modo es Estático."
|
||||
|
||||
#: authentik/stages/redirect/api.py
|
||||
msgid "Target Flow should be present when mode is Flow."
|
||||
msgstr ""
|
||||
msgstr "El Flujo de Destino debe estar presente cuando el modo es Flujo."
|
||||
|
||||
#: authentik/stages/redirect/models.py
|
||||
msgid "Redirect Stage"
|
||||
@ -3841,10 +3881,6 @@ msgstr "Etapas de inicio de"
|
||||
msgid "No Pending user to login."
|
||||
msgstr "Ningún usuario pendiente para iniciar sesión."
|
||||
|
||||
#: authentik/stages/user_login/stage.py
|
||||
msgid "Successfully logged in!"
|
||||
msgstr "¡Se ha iniciado sesión correctamente!"
|
||||
|
||||
#: authentik/stages/user_logout/models.py
|
||||
msgid "User Logout Stage"
|
||||
msgstr "Etapa de cierre de sesión del usuario"
|
||||
@ -3920,10 +3956,12 @@ msgstr ""
|
||||
#: authentik/tenants/models.py
|
||||
msgid "Reputation cannot decrease lower than this value. Zero or negative."
|
||||
msgstr ""
|
||||
"La reputación no puede disminuir por debajo de este valor. Cero o negativo."
|
||||
|
||||
#: authentik/tenants/models.py
|
||||
msgid "Reputation cannot increase higher than this value. Zero or positive."
|
||||
msgstr ""
|
||||
"La reputación no puede aumentar por encima de este valor. Cero o positivo."
|
||||
|
||||
#: authentik/tenants/models.py
|
||||
msgid "The option configures the footer links on the flow executor pages."
|
||||
@ -3946,8 +3984,8 @@ msgstr "Personificación habilitada/deshabilitada globalmente."
|
||||
#: authentik/tenants/models.py
|
||||
msgid "Require administrators to provide a reason for impersonating a user."
|
||||
msgstr ""
|
||||
"Requerir a los administradores proporcionar una razón para suplantar un "
|
||||
"usuario."
|
||||
"Requerir que los administradores proporcionen una razón para personificar a "
|
||||
"un usuario."
|
||||
|
||||
#: authentik/tenants/models.py
|
||||
msgid "Default token duration"
|
||||
@ -3959,7 +3997,7 @@ msgstr "Longitud predeterminada del token"
|
||||
|
||||
#: authentik/tenants/models.py
|
||||
msgid "Tenant"
|
||||
msgstr "inquilino"
|
||||
msgstr "Inquilino"
|
||||
|
||||
#: authentik/tenants/models.py
|
||||
msgid "Tenants"
|
||||
|
||||
@ -410,77 +410,3 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
"Permission denied",
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
"default/flow-default-invalidation-flow.yaml",
|
||||
)
|
||||
@apply_blueprint("default/flow-default-provider-authorization-implicit-consent.yaml")
|
||||
@apply_blueprint("system/providers-oauth2.yaml")
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_authorization_consent_implied_parallel(self):
|
||||
"""test OpenID Provider flow (default authorization flow with implied consent)"""
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
signing_key=create_test_cert(),
|
||||
redirect_uris=[
|
||||
RedirectURI(
|
||||
RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth"
|
||||
)
|
||||
],
|
||||
authorization_flow=authorization_flow,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
scope_name__in=[
|
||||
SCOPE_OPENID,
|
||||
SCOPE_OPENID_EMAIL,
|
||||
SCOPE_OPENID_PROFILE,
|
||||
SCOPE_OFFLINE_ACCESS,
|
||||
]
|
||||
)
|
||||
)
|
||||
Application.objects.create(
|
||||
name=generate_id(),
|
||||
slug=self.app_slug,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
self.driver.get(self.live_server_url)
|
||||
login_window = self.driver.current_window_handle
|
||||
|
||||
self.driver.switch_to.new_window("tab")
|
||||
grafana_window = self.driver.current_window_handle
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
|
||||
self.driver.switch_to.window(login_window)
|
||||
self.login()
|
||||
|
||||
self.driver.switch_to.window(grafana_window)
|
||||
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||
self.driver.get("http://localhost:3000/profile")
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||
self.user.name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute("value"),
|
||||
self.user.name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=email]").get_attribute("value"),
|
||||
self.user.email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=login]").get_attribute("value"),
|
||||
self.user.email,
|
||||
)
|
||||
|
||||
@ -20,7 +20,7 @@ from tests.e2e.utils import SeleniumTestCase, retry
|
||||
class TestProviderSAML(SeleniumTestCase):
|
||||
"""test SAML Provider flow"""
|
||||
|
||||
def setup_client(self, provider: SAMLProvider, force_post: bool = False, **kwargs):
|
||||
def setup_client(self, provider: SAMLProvider, force_post: bool = False):
|
||||
"""Setup client saml-sp container which we test SAML against"""
|
||||
metadata_url = (
|
||||
self.url(
|
||||
@ -40,7 +40,6 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
"SP_ENTITY_ID": provider.issuer,
|
||||
"SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
||||
"SP_METADATA_URL": metadata_url,
|
||||
**kwargs,
|
||||
},
|
||||
)
|
||||
|
||||
@ -112,74 +111,6 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
[self.user.email],
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
"default/flow-default-invalidation-flow.yaml",
|
||||
)
|
||||
@apply_blueprint(
|
||||
"default/flow-default-provider-authorization-implicit-consent.yaml",
|
||||
)
|
||||
@apply_blueprint(
|
||||
"system/providers-saml.yaml",
|
||||
)
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_sp_initiated_implicit_post(self):
|
||||
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
provider: SAMLProvider = SAMLProvider.objects.create(
|
||||
name="saml-test",
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="authentik-e2e",
|
||||
issuer="authentik-e2e",
|
||||
sp_binding=SAMLBindings.POST,
|
||||
authorization_flow=authorization_flow,
|
||||
signing_kp=create_test_cert(),
|
||||
)
|
||||
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="SAML",
|
||||
slug="authentik-saml",
|
||||
provider=provider,
|
||||
)
|
||||
self.setup_client(provider, True)
|
||||
self.driver.get("http://localhost:9009")
|
||||
self.login()
|
||||
self.wait_for_url("http://localhost:9009/")
|
||||
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
|
||||
[self.user.name],
|
||||
)
|
||||
self.assertEqual(
|
||||
body["attr"][
|
||||
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
||||
],
|
||||
[self.user.username],
|
||||
)
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
|
||||
[self.user.username],
|
||||
)
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
|
||||
[str(self.user.pk)],
|
||||
)
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
|
||||
[self.user.email],
|
||||
)
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
|
||||
[self.user.email],
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
@ -519,81 +450,3 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
lambda driver: driver.current_url.startswith(should_url),
|
||||
f"URL {self.driver.current_url} doesn't match expected URL {should_url}",
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
"default/flow-default-invalidation-flow.yaml",
|
||||
)
|
||||
@apply_blueprint(
|
||||
"default/flow-default-provider-authorization-implicit-consent.yaml",
|
||||
)
|
||||
@apply_blueprint(
|
||||
"system/providers-saml.yaml",
|
||||
)
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_sp_initiated_implicit_post_buffer(self):
|
||||
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
provider: SAMLProvider = SAMLProvider.objects.create(
|
||||
name="saml-test",
|
||||
acs_url=f"http://{self.host}:9009/saml/acs",
|
||||
audience="authentik-e2e",
|
||||
issuer="authentik-e2e",
|
||||
sp_binding=SAMLBindings.POST,
|
||||
authorization_flow=authorization_flow,
|
||||
signing_kp=create_test_cert(),
|
||||
)
|
||||
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="SAML",
|
||||
slug="authentik-saml",
|
||||
provider=provider,
|
||||
)
|
||||
self.setup_client(provider, True, SP_ROOT_URL=f"http://{self.host}:9009")
|
||||
|
||||
self.driver.get(self.live_server_url)
|
||||
login_window = self.driver.current_window_handle
|
||||
self.driver.switch_to.new_window("tab")
|
||||
client_window = self.driver.current_window_handle
|
||||
# We need to access the SP on the same host as the IdP for SameSite cookies
|
||||
self.driver.get(f"http://{self.host}:9009")
|
||||
|
||||
self.driver.switch_to.window(login_window)
|
||||
self.login()
|
||||
self.driver.switch_to.window(client_window)
|
||||
|
||||
self.wait_for_url(f"http://{self.host}:9009/")
|
||||
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
|
||||
[self.user.name],
|
||||
)
|
||||
self.assertEqual(
|
||||
body["attr"][
|
||||
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
||||
],
|
||||
[self.user.username],
|
||||
)
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
|
||||
[self.user.username],
|
||||
)
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
|
||||
[str(self.user.pk)],
|
||||
)
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
|
||||
[self.user.email],
|
||||
)
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
|
||||
[self.user.email],
|
||||
)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-private-textarea-input.js";
|
||||
import "@goauthentik/components/ak-secret-textarea-input.js";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
@ -46,7 +46,7 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-private-textarea-input
|
||||
<ak-secret-textarea-input
|
||||
label=${msg("Certificate")}
|
||||
name="certificateData"
|
||||
input-hint="code"
|
||||
@ -54,8 +54,8 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
|
||||
required
|
||||
?revealed=${this.instance === undefined}
|
||||
help=${msg("PEM-encoded Certificate data.")}
|
||||
></ak-private-textarea-input>
|
||||
<ak-private-textarea-input
|
||||
></ak-secret-textarea-input>
|
||||
<ak-secret-textarea-input
|
||||
label=${msg("Private Key")}
|
||||
name="keyData"
|
||||
input-hint="code"
|
||||
@ -63,7 +63,7 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
|
||||
help=${msg(
|
||||
"Optional Private Key. If this is set, you can use this keypair for encryption.",
|
||||
)}
|
||||
></ak-private-textarea-input>`;
|
||||
></ak-secret-textarea-input>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/common/constants";
|
||||
import "@goauthentik/components/ak-private-textarea-input.js";
|
||||
import "@goauthentik/components/ak-secret-textarea-input.js";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
@ -62,13 +62,13 @@ export class EnterpriseLicenseForm extends ModelForm<License, string> {
|
||||
value="${ifDefined(this.installID)}"
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-private-textarea-input
|
||||
<ak-secret-textarea-input
|
||||
name="key"
|
||||
?revealed=${this.instance === undefined}
|
||||
label=${msg("License key")}
|
||||
input-hint="code"
|
||||
>
|
||||
</ak-private-textarea-input>`;
|
||||
</ak-secret-textarea-input>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
propertyMappingsSelector,
|
||||
} from "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderFormHelpers.js";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-hidden-text-input";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
@ -68,21 +69,15 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
|
||||
${msg("Client ID for the app registration.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Client Secret")}
|
||||
required
|
||||
<ak-hidden-text-input
|
||||
name="clientSecret"
|
||||
label=${msg("Client Secret")}
|
||||
value="${this.instance?.clientSecret ?? ""}"
|
||||
input-hint="code"
|
||||
required
|
||||
.help=${msg("Client secret for the app registration.")}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${this.instance?.clientSecret ?? ""}"
|
||||
class="pf-c-form-control pf-m-monospace"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Client secret for the app registration.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</ak-hidden-text-input>
|
||||
<ak-form-element-horizontal label=${msg("Tenant ID")} required name="tenantId">
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
akOAuthRedirectURIInput,
|
||||
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
|
||||
import { ascii_letters, digits, randomString } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-hidden-text-input";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/components/ak-textarea-input";
|
||||
@ -166,17 +167,16 @@ export function renderForm(
|
||||
input-hint="code"
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-text-input
|
||||
<ak-hidden-text-input
|
||||
name="clientSecret"
|
||||
label=${msg("Client Secret")}
|
||||
value="${provider?.clientSecret ?? randomString(128, ascii_letters + digits)}"
|
||||
input-hint="code"
|
||||
?hidden=${!showClientSecret}
|
||||
>
|
||||
</ak-text-input>
|
||||
</ak-hidden-text-input>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Redirect URIs/Origins (RegEx)")}
|
||||
required
|
||||
name="redirectUris"
|
||||
>
|
||||
<ak-array-input
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { ascii_letters, digits, randomString } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-hidden-text-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
@ -74,14 +76,14 @@ export function renderForm(
|
||||
<ak-form-group expanded>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
<ak-hidden-text-input
|
||||
name="sharedSecret"
|
||||
label=${msg("Shared secret")}
|
||||
.errorMessages=${errors?.sharedSecret ?? []}
|
||||
value=${provider?.sharedSecret ?? randomString(128, ascii_letters + digits)}
|
||||
required
|
||||
input-hint="code"
|
||||
></ak-text-input>
|
||||
></ak-hidden-text-input>
|
||||
<ak-text-input
|
||||
name="clientNetworks"
|
||||
label=${msg("Client Networks")}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-hidden-text-input";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
@ -50,7 +51,7 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
|
||||
>
|
||||
</ak-switch-input>
|
||||
|
||||
<ak-text-input
|
||||
<ak-hidden-text-input
|
||||
name="token"
|
||||
label=${msg("Token")}
|
||||
value="${provider?.token ?? ""}"
|
||||
@ -60,7 +61,7 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
|
||||
"Token to authenticate with. Currently only bearer authentication is supported.",
|
||||
)}
|
||||
input-hint="code"
|
||||
></ak-text-input>
|
||||
></ak-hidden-text-input>
|
||||
<ak-radio-input
|
||||
name="compatibilityMode"
|
||||
label=${msg("Compatibility Mode")}
|
||||
|
||||
@ -7,8 +7,8 @@ import {
|
||||
UserMatchingModeToLabel,
|
||||
} from "@goauthentik/admin/sources/oauth/utils";
|
||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-private-text-input.js";
|
||||
import "@goauthentik/components/ak-private-textarea-input.js";
|
||||
import "@goauthentik/components/ak-secret-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-textarea-input.js";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/components/ak-textarea-input";
|
||||
@ -248,22 +248,22 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
|
||||
value=${ifDefined(this.instance?.syncPrincipal)}
|
||||
help=${msg("Principal used to authenticate to the KDC for syncing.")}
|
||||
></ak-text-input>
|
||||
<ak-private-text-input
|
||||
<ak-secret-text-input
|
||||
name="syncPassword"
|
||||
label=${msg("Sync password")}
|
||||
?revealed=${this.instance === undefined}
|
||||
help=${msg(
|
||||
"Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided.",
|
||||
)}
|
||||
></ak-private-text-input>
|
||||
<ak-private-textarea-input
|
||||
></ak-secret-text-input>
|
||||
<ak-secret-textarea-input
|
||||
name="syncKeytab"
|
||||
label=${msg("Sync keytab")}
|
||||
?revealed=${this.instance === undefined}
|
||||
help=${msg(
|
||||
"Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
|
||||
)}
|
||||
></ak-private-textarea-input>
|
||||
></ak-secret-textarea-input>
|
||||
<ak-text-input
|
||||
name="syncCcache"
|
||||
label=${msg("Sync credentials cache")}
|
||||
@ -285,14 +285,14 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
|
||||
"Force the use of a specific server name for SPNEGO. Must be in the form HTTP@domain",
|
||||
)}
|
||||
></ak-text-input>
|
||||
<ak-private-textarea-input
|
||||
<ak-secret-textarea-input
|
||||
name="spnegoKeytab"
|
||||
label=${msg("SPNEGO keytab")}
|
||||
?revealed=${this.instance === undefined}
|
||||
help=${msg(
|
||||
"Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
|
||||
)}
|
||||
></ak-private-textarea-input>
|
||||
></ak-secret-textarea-input>
|
||||
<ak-text-input
|
||||
name="spnegoCcache"
|
||||
label=${msg("SPNEGO credentials cache")}
|
||||
|
||||
@ -2,7 +2,7 @@ import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import { placeholderHelperText } from "@goauthentik/admin/helperText";
|
||||
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-private-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-text-input.js";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
@ -260,11 +260,11 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-private-text-input
|
||||
<ak-secret-text-input
|
||||
label=${msg("Bind Password")}
|
||||
name="bindPassword"
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-text-input>
|
||||
<ak-form-element-horizontal label=${msg("Base DN")} required name="baseDn">
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@ -8,8 +8,8 @@ import {
|
||||
UserMatchingModeToLabel,
|
||||
} from "@goauthentik/admin/sources/oauth/utils";
|
||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-private-textarea-input.js";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-secret-textarea-input.js";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
@ -441,14 +441,14 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">${msg("Also known as Client ID.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-private-textarea-input
|
||||
<ak-secret-textarea-input
|
||||
label=${msg("Consumer secret")}
|
||||
name="consumerSecret"
|
||||
input-hint="code"
|
||||
help=${msg("Also known as Client Secret.")}
|
||||
required
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-textarea-input>
|
||||
></ak-secret-textarea-input>
|
||||
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@ -128,7 +128,7 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
|
||||
this.doAuth();
|
||||
}}
|
||||
>
|
||||
${msg("Re-authenticate with plex")}
|
||||
${msg("Re-authenticate with Plex")}
|
||||
</button>
|
||||
<ak-form-element-horizontal name="allowFriends">
|
||||
<label class="pf-c-switch">
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-private-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-text-input.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
@ -95,13 +95,13 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-private-text-input
|
||||
<ak-secret-text-input
|
||||
name="clientSecret"
|
||||
label=${msg("Secret key")}
|
||||
input-hint="code"
|
||||
required
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-text-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
@ -125,12 +125,12 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
|
||||
spellcheck="false"
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-private-text-input
|
||||
<ak-secret-text-input
|
||||
name="adminSecretKey"
|
||||
label=${msg("Secret key")}
|
||||
input-hint="code"
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-text-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group expanded>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-private-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-text-input.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
@ -77,11 +77,11 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-private-text-input
|
||||
<ak-secret-text-input
|
||||
name="password"
|
||||
label=${msg("SMTP Password")}
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-text-input>
|
||||
|
||||
<ak-form-element-horizontal name="useTls">
|
||||
<label class="pf-c-switch">
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-number-input";
|
||||
import "@goauthentik/components/ak-private-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-text-input.js";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
@ -70,7 +70,7 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-private-text-input
|
||||
<ak-secret-text-input
|
||||
name="privateKey"
|
||||
label=${msg("Private Key")}
|
||||
input-hint="code"
|
||||
@ -79,7 +79,7 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
|
||||
help=${msg(
|
||||
"Private key, acquired from https://www.google.com/recaptcha/intro/v3.html.",
|
||||
)}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-text-input>
|
||||
|
||||
<ak-switch-input
|
||||
name="interactive"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-private-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-text-input.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||
@ -73,11 +73,11 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-private-text-input
|
||||
<ak-secret-text-input
|
||||
label=${msg("SMTP Password")}
|
||||
name="password"
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-text-input>
|
||||
<ak-form-element-horizontal name="useTls">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { dateTimeLocal } from "@goauthentik/common/temporal";
|
||||
import "@goauthentik/components/ak-hidden-text-input";
|
||||
import { Form } from "@goauthentik/elements/forms/Form";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModalForm } from "@goauthentik/elements/forms/ModalForm";
|
||||
@ -124,19 +125,14 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> {
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Password")}>
|
||||
<input
|
||||
type="text"
|
||||
readonly
|
||||
value=${ifDefined(this.result?.token)}
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Valid for 360 days, after which the password will automatically rotate. You can copy the password from the Token List.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-hidden-text-input
|
||||
label=${msg("Password")}
|
||||
value="${this.result?.token ?? ""}"
|
||||
.help=${msg(
|
||||
"Valid for 360 days, after which the password will automatically rotate. You can copy the password from the Token List.",
|
||||
)}
|
||||
>
|
||||
</ak-hidden-text-input>
|
||||
</form>`;
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { AKElement, type AKElementProps } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement.js";
|
||||
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
@ -6,6 +6,19 @@ import { property } from "lit/decorators.js";
|
||||
|
||||
type HelpType = TemplateResult | typeof nothing;
|
||||
|
||||
export interface HorizontalLightComponentProps<T> extends AKElementProps {
|
||||
name: string;
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
help?: string;
|
||||
bighelp?: TemplateResult | TemplateResult[];
|
||||
hidden?: boolean;
|
||||
invalid?: boolean;
|
||||
errorMessages?: string[];
|
||||
value?: T;
|
||||
inputHint?: string;
|
||||
}
|
||||
|
||||
export class HorizontalLightComponent<T> extends AKElement {
|
||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||
// we're not actually using that and, for the meantime, we need the form handlers to be able to
|
||||
@ -18,37 +31,81 @@ export class HorizontalLightComponent<T> extends AKElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name attribute for the form element
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, reflect: true })
|
||||
name!: string;
|
||||
|
||||
/**
|
||||
* The label for the input control
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, reflect: true })
|
||||
label = "";
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
required = false;
|
||||
|
||||
/**
|
||||
* Help text to display below the form element. Optional
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, reflect: true })
|
||||
help = "";
|
||||
|
||||
/**
|
||||
* Extended help content. Optional. Expects to be a TemplateResult
|
||||
* @property
|
||||
*/
|
||||
@property({ type: Object })
|
||||
bighelp?: TemplateResult | TemplateResult[];
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
hidden = false;
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
invalid = false;
|
||||
|
||||
/**
|
||||
* @property
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
errorMessages: string[] = [];
|
||||
|
||||
/**
|
||||
* @attribute
|
||||
* @property
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
value?: T;
|
||||
|
||||
/**
|
||||
* Input hint.
|
||||
* - `code`: uses a monospace font and disables spellcheck & autocomplete
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, attribute: "input-hint" })
|
||||
inputHint = "";
|
||||
|
||||
renderControl() {
|
||||
protected renderControl() {
|
||||
throw new Error("Must be implemented in a subclass");
|
||||
}
|
||||
|
||||
|
||||
159
web/src/components/ak-hidden-text-input.ts
Normal file
159
web/src/components/ak-hidden-text-input.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import { bound } from "#elements/decorators/bound";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
HorizontalLightComponent,
|
||||
HorizontalLightComponentProps,
|
||||
} from "./HorizontalLightComponent";
|
||||
import "./ak-visibility-toggle.js";
|
||||
import type { VisibilityToggleProps } from "./ak-visibility-toggle.js";
|
||||
|
||||
type BaseProps = HorizontalLightComponentProps<string> &
|
||||
Pick<VisibilityToggleProps, "showMessage" | "hideMessage">;
|
||||
|
||||
export interface AkHiddenTextInputProps extends BaseProps {
|
||||
revealed: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export type InputLike = HTMLTextAreaElement | HTMLInputElement;
|
||||
|
||||
/**
|
||||
* @element ak-hidden-text-input
|
||||
* @class AkHiddenTextInput
|
||||
*
|
||||
* A text-input field with a visibility control, so you can show/hide sensitive fields.
|
||||
*
|
||||
* ## CSS Parts
|
||||
* @csspart container - The main container div
|
||||
* @csspart input - The input element
|
||||
* @csspart toggle - The visibility toggle button
|
||||
*
|
||||
*/
|
||||
@customElement("ak-hidden-text-input")
|
||||
export class AkHiddenTextInput<T extends InputLike = HTMLInputElement>
|
||||
extends HorizontalLightComponent<string>
|
||||
implements AkHiddenTextInputProps
|
||||
{
|
||||
public static get styles() {
|
||||
return [
|
||||
css`
|
||||
main {
|
||||
display: flex;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, reflect: true })
|
||||
public value = "";
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public revealed = false;
|
||||
|
||||
/**
|
||||
* Text for when the input has no set value
|
||||
*
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String })
|
||||
public placeholder?: string;
|
||||
|
||||
/**
|
||||
* Specify kind of help the browser should try to provide
|
||||
*
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String })
|
||||
public autocomplete?: "none" | AutoFill;
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, attribute: "show-message" })
|
||||
public showMessage = msg("Show field content");
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, attribute: "hide-message" })
|
||||
public hideMessage = msg("Hide field content");
|
||||
|
||||
@query("#main > input")
|
||||
protected inputField!: T;
|
||||
|
||||
@bound
|
||||
private handleToggleVisibility() {
|
||||
this.revealed = !this.revealed;
|
||||
|
||||
// Maintain focus on input after toggle
|
||||
this.updateComplete.then(() => {
|
||||
if (this.inputField && document.activeElement === this) {
|
||||
this.inputField.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Because of the peculiarities of how HorizontalLightComponent works, keeping its content
|
||||
// in the LightDom so the inner components actually inherit styling, the normal `css` options
|
||||
// aren't available. Embedding styles is bad styling, and we'll fix it in the next style
|
||||
// refresh.
|
||||
protected renderInputField(setValue: (ev: InputEvent) => void, code: boolean) {
|
||||
return html` <input
|
||||
style="flex: 1 1 auto; min-width: 0;"
|
||||
part="input"
|
||||
type=${this.revealed ? "text" : "password"}
|
||||
@input=${setValue}
|
||||
value=${ifDefined(this.value)}
|
||||
placeholder=${ifDefined(this.placeholder)}
|
||||
class="${classMap({
|
||||
"pf-c-form-control": true,
|
||||
"pf-m-monospace": code,
|
||||
})}"
|
||||
spellcheck=${code ? "false" : "true"}
|
||||
?required=${this.required}
|
||||
/>`;
|
||||
}
|
||||
|
||||
protected override renderControl() {
|
||||
const code = this.inputHint === "code";
|
||||
const setValue = (ev: InputEvent) => {
|
||||
this.value = (ev.target as T).value;
|
||||
};
|
||||
return html` <div style="display: flex; gap: 0.25rem">
|
||||
${this.renderInputField(setValue, code)}
|
||||
<!-- -->
|
||||
<ak-visibility-toggle
|
||||
part="toggle"
|
||||
style="flex: 0 0 auto; align-self: flex-start"
|
||||
?open=${this.revealed}
|
||||
show-message=${this.showMessage}
|
||||
hide-message=${this.hideMessage}
|
||||
@click=${() => (this.revealed = !this.revealed)}
|
||||
></ak-visibility-toggle>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-hidden-text-input": AkHiddenTextInput;
|
||||
}
|
||||
}
|
||||
128
web/src/components/ak-hidden-textarea-input.ts
Normal file
128
web/src/components/ak-hidden-textarea-input.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { AkHiddenTextInput, type AkHiddenTextInputProps } from "./ak-hidden-text-input.js";
|
||||
|
||||
export interface AkHiddenTextAreaInputProps extends AkHiddenTextInputProps {
|
||||
/**
|
||||
* Number of visible text lines (rows)
|
||||
*/
|
||||
rows?: number;
|
||||
|
||||
/**
|
||||
* Number of visible character width (cols)
|
||||
*/
|
||||
cols?: number;
|
||||
|
||||
/**
|
||||
* How the textarea can be resized
|
||||
*/
|
||||
resize?: "none" | "both" | "horizontal" | "vertical";
|
||||
|
||||
/**
|
||||
* Whether text should wrap
|
||||
*/
|
||||
wrap?: "soft" | "hard" | "off";
|
||||
}
|
||||
|
||||
/**
|
||||
* @element ak-hidden-text-input
|
||||
* @class AkHiddenTextInput
|
||||
*
|
||||
* A text-input field with a visibility control, so you can show/hide sensitive fields.
|
||||
*
|
||||
* ## CSS Parts
|
||||
* @csspart container - The main container div
|
||||
* @csspart input - The input element
|
||||
* @csspart toggle - The visibility toggle button
|
||||
*
|
||||
*/
|
||||
@customElement("ak-hidden-textarea-input")
|
||||
export class AkHiddenTextAreaInput
|
||||
extends AkHiddenTextInput<HTMLTextAreaElement>
|
||||
implements AkHiddenTextAreaInputProps
|
||||
{
|
||||
/* These are mostly just forwarded to the textarea component. */
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Number })
|
||||
rows?: number = 4;
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Number })
|
||||
cols?: number;
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*
|
||||
* You want `resize=true` so that the resize value is visible in the component tag, activating
|
||||
* the CSS associated with these values.
|
||||
*/
|
||||
@property({ type: String, reflect: true })
|
||||
resize?: "none" | "both" | "horizontal" | "vertical" = "vertical";
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String })
|
||||
wrap?: "soft" | "hard" | "off" = "soft";
|
||||
|
||||
@query("#main > textarea")
|
||||
protected inputField!: HTMLTextAreaElement;
|
||||
|
||||
get displayValue() {
|
||||
const value = this.value ?? "";
|
||||
if (this.revealed) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value
|
||||
.split("\n")
|
||||
.reduce((acc: string[], line: string) => [...acc, "*".repeat(line.length)], [])
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
// TODO: Because of the peculiarities of how HorizontalLightComponent works, keeping its content
|
||||
// in the LightDom so the inner components actually inherit styling, the normal `css` options
|
||||
// aren't available. Embedding styles is bad styling, and we'll fix it in the next style
|
||||
// refresh.
|
||||
protected override renderInputField(setValue: (ev: InputEvent) => void, code: boolean) {
|
||||
const wrap = this.revealed ? this.wrap : "soft";
|
||||
|
||||
return html`
|
||||
<textarea
|
||||
style="flex: 1 1 auto; min-width: 0;"
|
||||
part="textarea"
|
||||
@input=${setValue}
|
||||
placeholder=${ifDefined(this.placeholder)}
|
||||
rows=${ifDefined(this.rows)}
|
||||
cols=${ifDefined(this.cols)}
|
||||
wrap=${ifDefined(wrap)}
|
||||
class=${classMap({
|
||||
"pf-c-form-control": true,
|
||||
"pf-m-monospace": code,
|
||||
})}
|
||||
spellcheck=${code ? "false" : "true"}
|
||||
?required=${this.required}
|
||||
>
|
||||
${this.displayValue}</textarea
|
||||
>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-hidden-textarea-input": AkHiddenTextAreaInput;
|
||||
}
|
||||
}
|
||||
@ -8,8 +8,8 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { HorizontalLightComponent } from "./HorizontalLightComponent";
|
||||
|
||||
@customElement("ak-private-text-input")
|
||||
export class AkPrivateTextInput extends HorizontalLightComponent<string> {
|
||||
@customElement("ak-secret-text-input")
|
||||
export class AkSecretTextInput extends HorizontalLightComponent<string> {
|
||||
@property({ type: String, reflect: true })
|
||||
public value = "";
|
||||
|
||||
@ -23,7 +23,7 @@ export class AkPrivateTextInput extends HorizontalLightComponent<string> {
|
||||
this.revealed = true;
|
||||
}
|
||||
|
||||
#renderPrivateInput() {
|
||||
#renderSecretInput() {
|
||||
return html`<div class="pf-c-form__horizontal-group" @click=${() => this.#onReveal()}>
|
||||
<input
|
||||
class="pf-c-form-control"
|
||||
@ -60,14 +60,14 @@ export class AkPrivateTextInput extends HorizontalLightComponent<string> {
|
||||
}
|
||||
|
||||
public override renderControl() {
|
||||
return this.revealed ? this.renderVisibleInput() : this.#renderPrivateInput();
|
||||
return this.revealed ? this.renderVisibleInput() : this.#renderSecretInput();
|
||||
}
|
||||
}
|
||||
|
||||
export default AkPrivateTextInput;
|
||||
export default AkSecretTextInput;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-private-text-input": AkPrivateTextInput;
|
||||
"ak-secret-text-input": AkSecretTextInput;
|
||||
}
|
||||
}
|
||||
@ -5,10 +5,10 @@ import { customElement, property } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { AkPrivateTextInput } from "./ak-private-text-input.js";
|
||||
import { AkSecretTextInput } from "./ak-secret-text-input.js";
|
||||
|
||||
@customElement("ak-private-textarea-input")
|
||||
export class AkPrivateTextAreaInput extends AkPrivateTextInput {
|
||||
@customElement("ak-secret-textarea-input")
|
||||
export class AkSecretTextAreaInput extends AkSecretTextInput {
|
||||
protected override renderVisibleInput() {
|
||||
const code = this.inputHint === "code";
|
||||
const setValue = (ev: InputEvent) => {
|
||||
@ -34,10 +34,10 @@ export class AkPrivateTextAreaInput extends AkPrivateTextInput {
|
||||
}
|
||||
}
|
||||
|
||||
export default AkPrivateTextAreaInput;
|
||||
export default AkSecretTextAreaInput;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-private-textarea-input": AkPrivateTextAreaInput;
|
||||
"ak-secret-textarea-input": AkSecretTextAreaInput;
|
||||
}
|
||||
}
|
||||
89
web/src/components/ak-visibility-toggle.ts
Normal file
89
web/src/components/ak-visibility-toggle.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
export interface VisibilityToggleProps {
|
||||
open: boolean;
|
||||
disabled: boolean;
|
||||
showMessage: string;
|
||||
hideMessage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @component ak-visibility-toggle
|
||||
* @class VisibilityToggle
|
||||
*
|
||||
* A straightforward two-state iconic button we use in a few places as way of telling users to hide
|
||||
* or show something secret, such as a password or private key. Expects the client to manage its
|
||||
* state.
|
||||
*
|
||||
* @events
|
||||
* - click: when the toggle is clicked.
|
||||
*/
|
||||
@customElement("ak-visibility-toggle")
|
||||
export class VisibilityToggle extends AKElement implements VisibilityToggleProps {
|
||||
static get styles() {
|
||||
return [PFBase, PFButton];
|
||||
}
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
open = false;
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
disabled = false;
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, attribute: "show-message" })
|
||||
showMessage = msg("Show field content");
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, attribute: "hide-message" })
|
||||
hideMessage = msg("Hide field content");
|
||||
|
||||
render() {
|
||||
const [label, icon] = this.open
|
||||
? [this.hideMessage, "fa-eye"]
|
||||
: [this.showMessage, "fa-eye-slash"];
|
||||
|
||||
const onClick = (ev: PointerEvent) => {
|
||||
ev.stopPropagation();
|
||||
this.dispatchEvent(new PointerEvent(ev.type, ev));
|
||||
};
|
||||
|
||||
return html`<button
|
||||
aria-label=${label}
|
||||
title=${label}
|
||||
@click=${onClick}
|
||||
?disabled=${this.disabled}
|
||||
class="pf-c-button pf-m-control"
|
||||
type="button"
|
||||
>
|
||||
<i class="fas ${icon}" aria-hidden="true"></i>
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-visibility-toggle": VisibilityToggle;
|
||||
}
|
||||
}
|
||||
93
web/src/components/stories/ak-hidden-text-input.stories.ts
Normal file
93
web/src/components/stories/ak-hidden-text-input.stories.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import type { Meta, StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import "../ak-hidden-text-input";
|
||||
import { type AkHiddenTextInput, type AkHiddenTextInputProps } from "../ak-hidden-text-input.js";
|
||||
|
||||
const metadata: Meta<AkHiddenTextInputProps> = {
|
||||
title: "Components / <ak-hidden-text-input>",
|
||||
component: "ak-hidden-text-input",
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: `
|
||||
# Hidden Text Input Component
|
||||
|
||||
A text-input field with a visibility control, so you can show/hide sensitive fields.
|
||||
`,
|
||||
},
|
||||
},
|
||||
layout: "padded",
|
||||
},
|
||||
argTypes: {
|
||||
label: {
|
||||
control: "text",
|
||||
description: "Label text for the input field",
|
||||
},
|
||||
value: {
|
||||
control: "text",
|
||||
description: "Current value of the input",
|
||||
},
|
||||
revealed: {
|
||||
control: "boolean",
|
||||
description: "Whether the text is currently visible",
|
||||
},
|
||||
placeholder: {
|
||||
control: "text",
|
||||
description: "Placeholder text for the input",
|
||||
},
|
||||
required: {
|
||||
control: "boolean",
|
||||
description: "Whether the input is required",
|
||||
},
|
||||
inputHint: {
|
||||
control: "select",
|
||||
options: ["text", "code"],
|
||||
description: "Input type hint for styling and behavior",
|
||||
},
|
||||
showMessage: {
|
||||
control: "text",
|
||||
description: "Custom message for show action",
|
||||
},
|
||||
hideMessage: {
|
||||
control: "text",
|
||||
description: "Custom message for hide action",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
type Story = StoryObj<AkHiddenTextInput>;
|
||||
|
||||
const Template: Story = {
|
||||
args: {
|
||||
label: "Hidden Text Input",
|
||||
value: "",
|
||||
revealed: false,
|
||||
},
|
||||
render: (args) => html`
|
||||
<ak-hidden-text-input
|
||||
label=${ifDefined(args.label)}
|
||||
value=${ifDefined(args.value)}
|
||||
?revealed=${args.revealed}
|
||||
placeholder=${ifDefined(args.placeholder)}
|
||||
?required=${args.required}
|
||||
input-hint=${ifDefined(args.inputHint)}
|
||||
show-message=${ifDefined(args.showMessage)}
|
||||
hide-message=${ifDefined(args.hideMessage)}
|
||||
></ak-hidden-text-input>
|
||||
`,
|
||||
};
|
||||
|
||||
export const Password: Story = {
|
||||
...Template,
|
||||
args: {
|
||||
label: "Password",
|
||||
placeholder: "Enter your password",
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
140
web/src/components/stories/ak-hidden-textarea-input.stories.ts
Normal file
140
web/src/components/stories/ak-hidden-textarea-input.stories.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import type { Meta, StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import "../ak-hidden-textarea-input";
|
||||
import {
|
||||
type AkHiddenTextAreaInput,
|
||||
type AkHiddenTextAreaInputProps,
|
||||
} from "../ak-hidden-textarea-input.js";
|
||||
|
||||
const metadata: Meta<AkHiddenTextAreaInputProps> = {
|
||||
title: "Components / <ak-hidden-textarea-input>",
|
||||
component: "ak-hidden-textarea-input",
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: `
|
||||
# Hidden Textarea Input Component
|
||||
|
||||
A textarea input field with a visibility control, so you can show/hide sensitive fields.
|
||||
`,
|
||||
},
|
||||
},
|
||||
layout: "padded",
|
||||
},
|
||||
argTypes: {
|
||||
label: {
|
||||
control: "text",
|
||||
description: "Label text for the input field",
|
||||
},
|
||||
value: {
|
||||
control: "text",
|
||||
description: "Current value of the input",
|
||||
},
|
||||
revealed: {
|
||||
control: "boolean",
|
||||
description: "Whether the text is currently visible",
|
||||
},
|
||||
placeholder: {
|
||||
control: "text",
|
||||
description: "Placeholder text for the input",
|
||||
},
|
||||
required: {
|
||||
control: "boolean",
|
||||
description: "Whether the input is required",
|
||||
},
|
||||
inputHint: {
|
||||
control: "select",
|
||||
options: ["text", "code"],
|
||||
description: "Input type hint for styling and behavior",
|
||||
},
|
||||
showMessage: {
|
||||
control: "text",
|
||||
description: "Custom message for show action",
|
||||
},
|
||||
hideMessage: {
|
||||
control: "text",
|
||||
description: "Custom message for hide action",
|
||||
},
|
||||
rows: {
|
||||
control: { type: "number", min: 1, max: 50 },
|
||||
description: "Number of visible text lines",
|
||||
},
|
||||
cols: {
|
||||
control: { type: "number", min: 10, max: 200 },
|
||||
description: "Number of visible character width",
|
||||
},
|
||||
resize: {
|
||||
control: "select",
|
||||
options: ["none", "both", "horizontal", "vertical"],
|
||||
description: "How the textarea can be resized",
|
||||
},
|
||||
wrap: {
|
||||
control: "select",
|
||||
options: ["soft", "hard", "off"],
|
||||
description: "Text wrapping behavior",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
type Story = StoryObj<AkHiddenTextAreaInput>;
|
||||
|
||||
const Template: Story = {
|
||||
args: {
|
||||
label: "Hidden Textarea Input",
|
||||
value: "",
|
||||
revealed: false,
|
||||
rows: 4,
|
||||
},
|
||||
render: (args) => html`
|
||||
<ak-hidden-textarea-input
|
||||
label=${ifDefined(args.label)}
|
||||
value=${ifDefined(args.value)}
|
||||
?revealed=${args.revealed}
|
||||
placeholder=${ifDefined(args.placeholder)}
|
||||
rows=${ifDefined(args.rows)}
|
||||
cols=${ifDefined(args.cols)}
|
||||
resize=${ifDefined(args.resize)}
|
||||
wrap=${ifDefined(args.wrap)}
|
||||
?required=${args.required}
|
||||
input-hint=${ifDefined(args.inputHint)}
|
||||
show-message=${ifDefined(args.showMessage)}
|
||||
hide-message=${ifDefined(args.hideMessage)}
|
||||
></ak-hidden-textarea-input>
|
||||
`,
|
||||
};
|
||||
|
||||
export const SslCertificate: Story = {
|
||||
...Template,
|
||||
args: {
|
||||
label: "SSL Certificate",
|
||||
value: `-----BEGIN CERTIFICATE-----
|
||||
MIIDXTCCAkWgAwIBAgIJAKoK/heBjcOuMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQwHhcNMTcwNTEwMTk0MDA2WhcNMTgwNTEwMTk0MDA2WjBF
|
||||
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE3MDUxMDE5NDAwNloXDTE4MDUxMDE5
|
||||
NDAwNlowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNV
|
||||
BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||
ggEPADCCAQoCggEBALdUlNS31SzxwoFShahGfjHj6GgpcVbzL1Siq0Pqnf82T6M2
|
||||
EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggE
|
||||
BAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqn
|
||||
f82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgM
|
||||
BAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeM
|
||||
Hyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDu
|
||||
neMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJ
|
||||
kPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAE=
|
||||
-----END CERTIFICATE-----`,
|
||||
inputHint: "code",
|
||||
rows: 15,
|
||||
resize: "vertical",
|
||||
showMessage: "Show certificate content",
|
||||
hideMessage: "Hide certificate content",
|
||||
autocomplete: "off",
|
||||
},
|
||||
};
|
||||
121
web/src/components/stories/ak-visibility-toggle.stories.ts
Normal file
121
web/src/components/stories/ak-visibility-toggle.stories.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import type { Meta, StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import "../ak-visibility-toggle";
|
||||
import { type VisibilityToggle, type VisibilityToggleProps } from "../ak-visibility-toggle.js";
|
||||
|
||||
const metadata: Meta<VisibilityToggleProps> = {
|
||||
title: "Elements/<ak-visibility-toggle>",
|
||||
component: "ak-visibility-toggle",
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: `
|
||||
# Visibility Toggle Component
|
||||
|
||||
A straightforward two-state iconic button for toggling the visibility of sensitive content such as passwords, private keys, or other secret information.
|
||||
|
||||
- Use for sensitive content that users might want to temporarily reveal
|
||||
- There are default hide/show messages for screen readers, but they can be overridden
|
||||
- Clients always handle the state
|
||||
- The \`open\` state is false by default; we assume you want sensitive content hidden at start
|
||||
`,
|
||||
},
|
||||
},
|
||||
layout: "padded",
|
||||
},
|
||||
argTypes: {
|
||||
open: {
|
||||
control: "boolean",
|
||||
description: "Whether the toggle is in the 'show' state (true) or 'hide' state (false)",
|
||||
},
|
||||
showMessage: {
|
||||
control: "text",
|
||||
description:
|
||||
'Message for screen readers when in hide state (default: "Show field content")',
|
||||
},
|
||||
hideMessage: {
|
||||
control: "text",
|
||||
description:
|
||||
'Message for screen readers when in show state (default: "Hide field content")',
|
||||
},
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
description: "Whether the button should be disabled (for demo purposes)",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
type Story = StoryObj<VisibilityToggle>;
|
||||
|
||||
const Template: Story = {
|
||||
args: {
|
||||
open: false,
|
||||
showMessage: "Show field content",
|
||||
hideMessage: "Hide field content",
|
||||
},
|
||||
render: (args) => html`
|
||||
<ak-visibility-toggle
|
||||
?open=${args.open}
|
||||
show-message=${ifDefined(args.showMessage)}
|
||||
hide-message=${ifDefined(args.hideMessage)}
|
||||
@click=${(e: Event) => {
|
||||
const target = e.target as VisibilityToggle;
|
||||
target.open = !target.open;
|
||||
// In a real application, you would also toggle the visibility
|
||||
// of the associated content here
|
||||
}}
|
||||
></ak-visibility-toggle>
|
||||
`,
|
||||
};
|
||||
|
||||
// Password field integration example
|
||||
export const PasswordFieldExample: Story = {
|
||||
args: {
|
||||
showMessage: "Reveal password",
|
||||
hideMessage: "Conceal password",
|
||||
},
|
||||
render: () => {
|
||||
let isVisible = false;
|
||||
|
||||
const toggleVisibility = (e: Event) => {
|
||||
isVisible = !isVisible;
|
||||
const toggle = e.target as VisibilityToggle;
|
||||
const passwordField = document.querySelector("#demo-password") as HTMLInputElement;
|
||||
|
||||
toggle.open = isVisible;
|
||||
if (passwordField) {
|
||||
passwordField.type = isVisible ? "text" : "password";
|
||||
}
|
||||
};
|
||||
|
||||
return html`
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; max-width: 300px;">
|
||||
<label for="demo-password" style="font-weight: bold;">Password:</label>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input
|
||||
id="demo-password"
|
||||
type="password"
|
||||
value="supersecretpassword123"
|
||||
style="flex: 1; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px;"
|
||||
readonly
|
||||
/>
|
||||
<ak-visibility-toggle
|
||||
?open=${isVisible}
|
||||
show-message="Show password"
|
||||
hide-message="Hide password"
|
||||
@click=${toggleVisibility}
|
||||
></ak-visibility-toggle>
|
||||
</div>
|
||||
<p style="font-size: 0.875rem; color: #666;">
|
||||
Click the eye icon to toggle password visibility
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
};
|
||||
@ -16,8 +16,12 @@ import { property } from "lit/decorators.js";
|
||||
|
||||
import { UiThemeEnum } from "@goauthentik/api";
|
||||
|
||||
export interface AKElementProps {
|
||||
activeTheme: ResolvedUITheme;
|
||||
}
|
||||
|
||||
@localized()
|
||||
export class AKElement extends LitElement {
|
||||
export class AKElement extends LitElement implements AKElementProps {
|
||||
//#region Static Properties
|
||||
|
||||
public static styles?: Array<CSSResult | CSSModule>;
|
||||
|
||||
@ -33,7 +33,7 @@ import {
|
||||
|
||||
function localeComparator(a: DualSelectPair, b: DualSelectPair) {
|
||||
const aSortBy = a[2] || a[0];
|
||||
const bSortBy = b[2] || a[0];
|
||||
const bSortBy = b[2] || b[0];
|
||||
|
||||
return aSortBy.localeCompare(bSortBy);
|
||||
}
|
||||
|
||||
@ -3001,11 +3001,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Load servers</source>
|
||||
<target>Server laden</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s24f405197ede5ebb">
|
||||
<source>Re-authenticate with plex</source>
|
||||
<target>Mit Plex erneut authentifizieren</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc297b2e13c28ecf9">
|
||||
<source>Allow friends to authenticate via Plex, even if you don't share any servers</source>
|
||||
@ -9241,6 +9236,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -2414,10 +2414,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Load servers</source>
|
||||
<target>Load servers</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s24f405197ede5ebb">
|
||||
<source>Re-authenticate with plex</source>
|
||||
<target>Re-authenticate with plex</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc297b2e13c28ecf9">
|
||||
<source>Allow friends to authenticate via Plex, even if you don't share any servers</source>
|
||||
<target>Allow friends to authenticate via Plex, even if you don't share any servers</target>
|
||||
@ -7748,6 +7744,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -2982,11 +2982,6 @@ no se aprueba cuando una o ambas de las opciones seleccionadas son iguales o sup
|
||||
<source>Load servers</source>
|
||||
<target>Servidores de carga</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s24f405197ede5ebb">
|
||||
<source>Re-authenticate with plex</source>
|
||||
<target>Vuelva a autenticarse con plex</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc297b2e13c28ecf9">
|
||||
<source>Allow friends to authenticate via Plex, even if you don't share any servers</source>
|
||||
@ -9301,6 +9296,15 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -3010,11 +3010,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Load servers</source>
|
||||
<target>Charger les serveurs</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s24f405197ede5ebb">
|
||||
<source>Re-authenticate with plex</source>
|
||||
<target>Se ré-authentifier avec Plex</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc297b2e13c28ecf9">
|
||||
<source>Allow friends to authenticate via Plex, even if you don't share any servers</source>
|
||||
@ -9870,6 +9865,15 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -3011,11 +3011,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Load servers</source>
|
||||
<target>Carico server</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s24f405197ede5ebb">
|
||||
<source>Re-authenticate with plex</source>
|
||||
<target>Riautenticarsi con plex</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc297b2e13c28ecf9">
|
||||
<source>Allow friends to authenticate via Plex, even if you don't share any servers</source>
|
||||
@ -9853,6 +9848,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -2973,11 +2973,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Load servers</source>
|
||||
<target>서버 로드</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s24f405197ede5ebb">
|
||||
<source>Re-authenticate with plex</source>
|
||||
<target>Plex로 다시 인증하기</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc297b2e13c28ecf9">
|
||||
<source>Allow friends to authenticate via Plex, even if you don't share any servers</source>
|
||||
@ -9209,6 +9204,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -2987,11 +2987,6 @@ slaagt niet wanneer een of beide geselecteerde opties gelijk zijn aan of boven d
|
||||
<source>Load servers</source>
|
||||
<target>Servers laden</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s24f405197ede5ebb">
|
||||
<source>Re-authenticate with plex</source>
|
||||
<target>Opnieuw authenticeren met Plex</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc297b2e13c28ecf9">
|
||||
<source>Allow friends to authenticate via Plex, even if you don't share any servers</source>
|
||||
@ -9113,6 +9108,15 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -3012,11 +3012,6 @@ nie przechodzi, gdy jedna lub obie wybrane opcje są równe lub wyższe od progu
|
||||
<source>Load servers</source>
|
||||
<target>Załaduj serwery</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s24f405197ede5ebb">
|
||||
<source>Re-authenticate with plex</source>
|
||||
<target>Ponowne uwierzytelnienie za pomocą plex</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc297b2e13c28ecf9">
|
||||
<source>Allow friends to authenticate via Plex, even if you don't share any servers</source>
|
||||
@ -9536,6 +9531,15 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -2990,11 +2990,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Load servers</source>
|
||||
<target>Ĺōàď śēŕvēŕś</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s24f405197ede5ebb">
|
||||
<source>Re-authenticate with plex</source>
|
||||
<target>Ŕē-àũţĥēńţĩćàţē ŵĩţĥ ƥĺēx</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc297b2e13c28ecf9">
|
||||
<source>Allow friends to authenticate via Plex, even if you don't share any servers</source>
|
||||
@ -9545,4 +9540,13 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
</trans-unit>
|
||||
</body></file></xliff>
|
||||
|
||||
@ -3011,11 +3011,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Load servers</source>
|
||||
<target>Загрузить серверы</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s24f405197ede5ebb">
|
||||
<source>Re-authenticate with plex</source>
|
||||
<target>Повторная аутентификация с помощью plex</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc297b2e13c28ecf9">
|
||||
<source>Allow friends to authenticate via Plex, even if you don't share any servers</source>
|
||||
@ -9628,6 +9623,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -2990,11 +2990,6 @@ Belirlenen seçeneklerden biri veya her ikisi de eşiğe eşit veya eşiğin üz
|
||||
<source>Load servers</source>
|
||||
<target>Sunucuları yükle</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s24f405197ede5ebb">
|
||||
<source>Re-authenticate with plex</source>
|
||||
<target>Plex ile yeniden kimlik doğrulama</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc297b2e13c28ecf9">
|
||||
<source>Allow friends to authenticate via Plex, even if you don't share any servers</source>
|
||||
@ -9600,6 +9595,15 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -2128,9 +2128,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="s91f389c796720a81">
|
||||
<source>Load servers</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s24f405197ede5ebb">
|
||||
<source>Re-authenticate with plex</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc297b2e13c28ecf9">
|
||||
<source>Allow friends to authenticate via Plex, even if you don't share any servers</source>
|
||||
</trans-unit>
|
||||
@ -6362,6 +6359,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@ -3011,11 +3011,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Load servers</source>
|
||||
<target>加载服务器</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s24f405197ede5ebb">
|
||||
<source>Re-authenticate with plex</source>
|
||||
<target>使用 Plex 重新验证身份</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc297b2e13c28ecf9">
|
||||
<source>Allow friends to authenticate via Plex, even if you don't share any servers</source>
|
||||
@ -9880,6 +9875,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
<target>生成新的证书密钥对</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -2290,10 +2290,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Load servers</source>
|
||||
<target>加载服务器</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s24f405197ede5ebb">
|
||||
<source>Re-authenticate with plex</source>
|
||||
<target>使用 plex 重新进行身份验证</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc297b2e13c28ecf9">
|
||||
<source>Allow friends to authenticate via Plex, even if you don't share any servers</source>
|
||||
<target>允许好友通过Plex进行身份验证,即使您不共享任何服务器</target>
|
||||
@ -7448,6 +7444,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -2972,11 +2972,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Load servers</source>
|
||||
<target>載入伺服器</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s24f405197ede5ebb">
|
||||
<source>Re-authenticate with plex</source>
|
||||
<target>使用 plex 重新身分認證</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc297b2e13c28ecf9">
|
||||
<source>Allow friends to authenticate via Plex, even if you don't share any servers</source>
|
||||
@ -9188,6 +9183,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
Reference in New Issue
Block a user