Compare commits

...
This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.

11 Commits

Author SHA1 Message Date
Jens L fb0a88f2cf
providers/proxy: rework endpoints logic (#4993)
* providers/proxy: rework endpoints logic

again...this time with tests and better logic

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-18 18:55:30 +01:00
Jens L 4d8d405e70
blueprints: allow setting of token key in blueprint context (#4995)
closes #4717

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-18 18:55:25 +01:00
Jens L 1d5f399b61
web/admin: fix prompt field display (#4990)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-18 18:54:41 +01:00
Jens L bb575fcc10
web/elements: fix search select inconsistency (#4989)
* web/elements: fix search-select inconsistency

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* web/common: fix config having to be json converted everywhere

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* web/elements: refactor form without iron-form

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* web/admin: fix misc

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/package-lock.json
2023-03-18 18:54:33 +01:00
Jens L 13fd1afbb9
web/admin: fix inconsistent display of flows in selections (#4977)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-18 18:53:16 +01:00
Jens Langhammer f059b998cc
release: 2023.3.1 2023-03-16 18:09:53 +01:00
Jens L 3f48202dfe
web/flows: fix authenticator selector in dark mode (#4974)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-16 16:57:15 +01:00
Jens L 2a3ebb616b
providers/oauth2: fix response for response_type code and response_mode fragment (#4975) 2023-03-16 16:57:09 +01:00
Jens L ceab1f732d
providers/ldap: fix duplicate attributes (#4972)
closes #4971

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-16 12:14:57 +01:00
Jens Langhammer 01d2cce9ca
Merge branch 'main' into version-2023.3 2023-03-15 20:20:51 +01:00
Jens Langhammer 72f85defb8
release: 2023.3.0 2023-03-13 18:30:48 +01:00
51 changed files with 458 additions and 7863 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2023.3.0 current_version = 2023.3.1
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)

View File

@ -12,3 +12,6 @@ indent_size = 2
[*.{yaml,yml}] [*.{yaml,yml}]
indent_size = 2 indent_size = 2
[*.go]
indent_style = tab

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
from typing import Optional from typing import Optional
__version__ = "2023.3.0" __version__ = "2023.3.1"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -19,10 +19,8 @@ class Command(BaseCommand):
for blueprint_path in options.get("blueprints", []): for blueprint_path in options.get("blueprints", []):
content = BlueprintInstance(path=blueprint_path).retrieve() content = BlueprintInstance(path=blueprint_path).retrieve()
importer = Importer(content) importer = Importer(content)
valid, logs = importer.validate() valid, _ = importer.validate()
if not valid: if not valid:
for log in logs:
getattr(LOGGER, log.pop("log_level"))(**log)
self.stderr.write("blueprint invalid") self.stderr.write("blueprint invalid")
sys_exit(1) sys_exit(1)
importer.apply() importer.apply()

View File

@ -40,6 +40,10 @@ from authentik.lib.models import SerializerModel
from authentik.outposts.models import OutpostServiceConnection from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel from authentik.policies.models import Policy, PolicyBindingModel
# Context set when the serializer is created in a blueprint context
# Update website/developer-docs/blueprints/v1/models.md when used
SERIALIZER_CONTEXT_BLUEPRINT = "blueprint_entry"
def is_model_allowed(model: type[Model]) -> bool: def is_model_allowed(model: type[Model]) -> bool:
"""Check if model is allowed""" """Check if model is allowed"""
@ -158,7 +162,12 @@ class Importer:
raise EntryInvalidError(f"Model {model} not allowed") raise EntryInvalidError(f"Model {model} not allowed")
if issubclass(model, BaseMetaModel): if issubclass(model, BaseMetaModel):
serializer_class: type[Serializer] = model.serializer() serializer_class: type[Serializer] = model.serializer()
serializer = serializer_class(data=entry.get_attrs(self.__import)) serializer = serializer_class(
data=entry.get_attrs(self.__import),
context={
SERIALIZER_CONTEXT_BLUEPRINT: entry,
},
)
try: try:
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
except ValidationError as exc: except ValidationError as exc:
@ -217,7 +226,12 @@ class Importer:
always_merger.merge(full_data, updated_identifiers) always_merger.merge(full_data, updated_identifiers)
serializer_kwargs["data"] = full_data serializer_kwargs["data"] = full_data
serializer: Serializer = model().serializer(**serializer_kwargs) serializer: Serializer = model().serializer(
context={
SERIALIZER_CONTEXT_BLUEPRINT: entry,
},
**serializer_kwargs,
)
try: try:
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
except ValidationError as exc: except ValidationError as exc:

View File

@ -16,6 +16,7 @@ from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import OwnerSuperuserPermissions from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.blueprints.api import ManagedSerializer from authentik.blueprints.api import ManagedSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
@ -29,6 +30,11 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
user_obj = UserSerializer(required=False, source="user", read_only=True) user_obj = UserSerializer(required=False, source="user", read_only=True)
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["key"] = CharField()
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]: def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
"""Ensure only API or App password tokens are created.""" """Ensure only API or App password tokens are created."""
request: Request = self.context.get("request") request: Request = self.context.get("request")

View File

@ -355,6 +355,62 @@ class TestAuthorize(OAuthTestCase):
delta=5, delta=5,
) )
def test_full_fragment_code(self):
"""Test full authorization"""
flow = create_test_flow()
provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
client_secret=generate_key(),
authorization_flow=flow,
redirect_uris="http://localhost",
signing_key=self.keypair,
)
Application.objects.create(name="app", slug="app", provider=provider)
state = generate_id()
user = create_test_admin_user()
self.client.force_login(user)
with patch(
"authentik.providers.oauth2.id_token.get_login_event",
MagicMock(
return_value=Event(
action=EventAction.LOGIN,
context={PLAN_CONTEXT_METHOD: "password"},
created=now(),
)
),
):
# Step 1, initiate params and get redirect to flow
self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
"response_mode": "fragment",
"client_id": "test",
"state": state,
"scope": "openid",
"redirect_uri": "http://localhost",
"nonce": generate_id(),
},
)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
self.assertJSONEqual(
response.content.decode(),
{
"component": "xak-flow-redirect",
"type": ChallengeTypes.REDIRECT.value,
"to": (f"http://localhost#code={code.code}" f"&state={state}"),
},
)
self.assertAlmostEqual(
code.expires.timestamp() - now().timestamp(),
timedelta_from_string(provider.access_code_validity).total_seconds(),
delta=5,
)
def test_full_form_post_id_token(self): def test_full_form_post_id_token(self):
"""Test full authorization (form_post response)""" """Test full authorization (form_post response)"""
flow = create_test_flow() flow = create_test_flow()

View File

@ -514,7 +514,12 @@ class OAuthFulfillmentStage(StageView):
return urlunsplit(uri) return urlunsplit(uri)
if self.params.response_mode == ResponseMode.FRAGMENT: if self.params.response_mode == ResponseMode.FRAGMENT:
query_fragment = self.create_implicit_response(code) query_fragment = {}
if self.params.grant_type in [GrantTypes.AUTHORIZATION_CODE]:
query_fragment["code"] = code.code
query_fragment["state"] = [str(self.params.state) if self.params.state else ""]
else:
query_fragment = self.create_implicit_response(code)
uri = uri._replace( uri = uri._replace(
fragment=uri.fragment + urlencode(query_fragment, doseq=True), fragment=uri.fragment + urlencode(query_fragment, doseq=True),

View File

@ -32,7 +32,7 @@ services:
volumes: volumes:
- redis:/data - redis:/data
server: server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.3.0} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.3.1}
restart: unless-stopped restart: unless-stopped
command: server command: server
environment: environment:
@ -50,7 +50,7 @@ services:
- "${AUTHENTIK_PORT_HTTP:-9000}:9000" - "${AUTHENTIK_PORT_HTTP:-9000}:9000"
- "${AUTHENTIK_PORT_HTTPS:-9443}:9443" - "${AUTHENTIK_PORT_HTTPS:-9443}:9443"
worker: worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.3.0} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.3.1}
restart: unless-stopped restart: unless-stopped
command: worker command: worker
environment: environment:

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion()) return fmt.Sprintf("authentik@%s", FullVersion())
} }
const VERSION = "2023.3.0" const VERSION = "2023.3.1"

View File

@ -30,11 +30,15 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
// Only append attributes that don't already exist // Only append attributes that don't already exist
// TODO: Remove in 2023.3 // TODO: Remove in 2023.3
for _, rawAttr := range rawAttrs { for _, rawAttr := range rawAttrs {
exists := false
for _, attr := range attrs { for _, attr := range attrs {
if !strings.EqualFold(attr.Name, rawAttr.Name) { if strings.EqualFold(attr.Name, rawAttr.Name) {
attrs = append(attrs, rawAttr) exists = true
} }
} }
if !exists {
attrs = append(attrs, rawAttr)
}
} }
if u.IsActive == nil { if u.IsActive == nil {

View File

@ -36,11 +36,15 @@ func (lg *LDAPGroup) Entry() *ldap.Entry {
// Only append attributes that don't already exist // Only append attributes that don't already exist
// TODO: Remove in 2023.3 // TODO: Remove in 2023.3
for _, rawAttr := range rawAttrs { for _, rawAttr := range rawAttrs {
exists := false
for _, attr := range attrs { for _, attr := range attrs {
if !strings.EqualFold(attr.Name, rawAttr.Name) { if strings.EqualFold(attr.Name, rawAttr.Name) {
attrs = append(attrs, rawAttr) exists = true
} }
} }
if !exists {
attrs = append(attrs, rawAttr)
}
} }
objectClass := []string{constants.OCGroup, constants.OCGroupOfUniqueNames, constants.OCGroupOfNames, constants.OCAKGroup, constants.OCPosixGroup} objectClass := []string{constants.OCGroup, constants.OCGroupOfUniqueNames, constants.OCGroupOfNames, constants.OCAKGroup, constants.OCPosixGroup}

View File

@ -2,7 +2,6 @@ package application
import ( import (
"net/url" "net/url"
"strings"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"goauthentik.io/api/v3" "goauthentik.io/api/v3"
@ -31,40 +30,55 @@ func updateURL(rawUrl string, scheme string, host string) string {
func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bool) OIDCEndpoint { func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bool) OIDCEndpoint {
authUrl := p.OidcConfiguration.AuthorizationEndpoint authUrl := p.OidcConfiguration.AuthorizationEndpoint
endUrl := p.OidcConfiguration.EndSessionEndpoint endUrl := p.OidcConfiguration.EndSessionEndpoint
tokenUrl := p.OidcConfiguration.TokenEndpoint
jwksUrl := p.OidcConfiguration.JwksUri
issuer := p.OidcConfiguration.Issuer issuer := p.OidcConfiguration.Issuer
if config.Get().AuthentikHostBrowser != "" {
authUrl = strings.ReplaceAll(authUrl, authentikHost, config.Get().AuthentikHostBrowser)
endUrl = strings.ReplaceAll(endUrl, authentikHost, config.Get().AuthentikHostBrowser)
jwksUrl = strings.ReplaceAll(jwksUrl, authentikHost, config.Get().AuthentikHostBrowser)
issuer = strings.ReplaceAll(issuer, authentikHost, config.Get().AuthentikHostBrowser)
}
ep := OIDCEndpoint{ ep := OIDCEndpoint{
Endpoint: oauth2.Endpoint{ Endpoint: oauth2.Endpoint{
AuthURL: authUrl, AuthURL: authUrl,
TokenURL: tokenUrl, TokenURL: p.OidcConfiguration.TokenEndpoint,
AuthStyle: oauth2.AuthStyleInParams, AuthStyle: oauth2.AuthStyleInParams,
}, },
EndSessionEndpoint: endUrl, EndSessionEndpoint: endUrl,
JwksUri: jwksUrl, JwksUri: p.OidcConfiguration.JwksUri,
TokenIntrospection: p.OidcConfiguration.IntrospectionEndpoint, TokenIntrospection: p.OidcConfiguration.IntrospectionEndpoint,
Issuer: issuer, Issuer: issuer,
} }
if !embedded { // For the embedded outpost, we use the configure `authentik_host` for the browser URLs
// and localhost (which is what we've got from the API) for backchannel URLs
//
// For other outposts, when `AUTHENTIK_HOST_BROWSER` is set, we use that for the browser URLs
// and use what we got from the API for backchannel
hostBrowser := config.Get().AuthentikHostBrowser
if !embedded && hostBrowser == "" {
return ep return ep
} }
if authentikHost == "" { var newHost *url.URL
log.Warning("Outpost has localhost/blank API Connection but no authentik_host is configured.") if embedded {
return ep if authentikHost == "" {
log.Warning("Outpost has localhost/blank API Connection but no authentik_host is configured.")
return ep
}
aku, err := url.Parse(authentikHost)
if err != nil {
return ep
}
newHost = aku
} else if hostBrowser != "" {
aku, err := url.Parse(hostBrowser)
if err != nil {
return ep
}
newHost = aku
} }
aku, err := url.Parse(authentikHost) // Update all browser-accessed URLs to use the new host and scheme
if err != nil { ep.AuthURL = updateURL(authUrl, newHost.Scheme, newHost.Host)
return ep ep.EndSessionEndpoint = updateURL(endUrl, newHost.Scheme, newHost.Host)
// Update issuer to use the same host and scheme, which would normally break as we don't
// change the token URL here, but the token HTTP transport overwrites the Host header
//
// This is only used in embedded outposts as there we can guarantee that the request
// is routed correctly
if embedded {
ep.Issuer = updateURL(ep.Issuer, newHost.Scheme, newHost.Host)
} }
ep.AuthURL = updateURL(authUrl, aku.Scheme, aku.Host)
ep.EndSessionEndpoint = updateURL(endUrl, aku.Scheme, aku.Host)
ep.JwksUri = updateURL(jwksUrl, aku.Scheme, aku.Host)
ep.Issuer = updateURL(ep.Issuer, aku.Scheme, aku.Host)
return ep return ep
} }

View File

@ -0,0 +1,88 @@
package application
import (
"testing"
"github.com/stretchr/testify/assert"
"goauthentik.io/api/v3"
"goauthentik.io/internal/config"
)
func TestEndpointDefault(t *testing.T) {
pc := api.ProxyOutpostConfig{
OidcConfiguration: api.ProxyOutpostConfigOidcConfiguration{
AuthorizationEndpoint: "https://test.goauthentik.io/application/o/authorize/",
EndSessionEndpoint: "https://test.goauthentik.io/application/o/test-app/end-session/",
IntrospectionEndpoint: "https://test.goauthentik.io/application/o/introspect/",
Issuer: "https://test.goauthentik.io/application/o/test-app/",
JwksUri: "https://test.goauthentik.io/application/o/test-app/jwks/",
TokenEndpoint: "https://test.goauthentik.io/application/o/token/",
},
}
ep := GetOIDCEndpoint(pc, "https://authentik-host.test.goauthentik.io", false)
// Standard outpost, non embedded
// All URLs should use the host that they get from the config
assert.Equal(t, "https://test.goauthentik.io/application/o/authorize/", ep.AuthURL)
assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL)
assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/", ep.Issuer)
assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri)
assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint)
assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection)
}
func TestEndpointAuthentikHostBrowser(t *testing.T) {
c := config.Get()
c.AuthentikHostBrowser = "https://browser.test.goauthentik.io"
defer func() {
c.AuthentikHostBrowser = ""
}()
pc := api.ProxyOutpostConfig{
OidcConfiguration: api.ProxyOutpostConfigOidcConfiguration{
AuthorizationEndpoint: "https://test.goauthentik.io/application/o/authorize/",
EndSessionEndpoint: "https://test.goauthentik.io/application/o/test-app/end-session/",
IntrospectionEndpoint: "https://test.goauthentik.io/application/o/introspect/",
Issuer: "https://test.goauthentik.io/application/o/test-app/",
JwksUri: "https://test.goauthentik.io/application/o/test-app/jwks/",
TokenEndpoint: "https://test.goauthentik.io/application/o/token/",
UserinfoEndpoint: "https://test.goauthentik.io/application/o/userinfo/",
},
}
ep := GetOIDCEndpoint(pc, "https://authentik-host.test.goauthentik.io", false)
// Standard outpost, with AUTHENTIK_HOST_BROWSER set
// Only the authorize/end session URLs should be changed
assert.Equal(t, "https://browser.test.goauthentik.io/application/o/authorize/", ep.AuthURL)
assert.Equal(t, "https://browser.test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint)
assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL)
assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/", ep.Issuer)
assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri)
assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection)
}
func TestEndpointEmbedded(t *testing.T) {
pc := api.ProxyOutpostConfig{
OidcConfiguration: api.ProxyOutpostConfigOidcConfiguration{
AuthorizationEndpoint: "https://test.goauthentik.io/application/o/authorize/",
EndSessionEndpoint: "https://test.goauthentik.io/application/o/test-app/end-session/",
IntrospectionEndpoint: "https://test.goauthentik.io/application/o/introspect/",
Issuer: "https://test.goauthentik.io/application/o/test-app/",
JwksUri: "https://test.goauthentik.io/application/o/test-app/jwks/",
TokenEndpoint: "https://test.goauthentik.io/application/o/token/",
UserinfoEndpoint: "https://test.goauthentik.io/application/o/userinfo/",
},
}
ep := GetOIDCEndpoint(pc, "https://authentik-host.test.goauthentik.io", true)
// Embedded outpost
// Browser URLs should use the config of "authentik_host", everything else can use what's
// received from the API endpoint
// Token URL is an exception since it's sent via a special HTTP transport that overrides the
// HTTP Host header, to make sure it's the same value as the issuer
assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/authorize/", ep.AuthURL)
assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/test-app/", ep.Issuer)
assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL)
assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri)
assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint)
assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection)
}

View File

@ -105,7 +105,7 @@ filterwarnings = [
[tool.poetry] [tool.poetry]
name = "authentik" name = "authentik"
version = "2023.3.0" version = "2023.3.1"
description = "" description = ""
authors = ["authentik Team <hello@goauthentik.io>"] authors = ["authentik Team <hello@goauthentik.io>"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: authentik title: authentik
version: 2023.3.0 version: 2023.3.1
description: Making authentication simple. description: Making authentication simple.
contact: contact:
email: hello@goauthentik.io email: hello@goauthentik.io

7668
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -74,8 +74,6 @@
"@lingui/detect-locale": "^3.17.2", "@lingui/detect-locale": "^3.17.2",
"@lingui/macro": "^3.17.2", "@lingui/macro": "^3.17.2",
"@patternfly/patternfly": "^4.224.2", "@patternfly/patternfly": "^4.224.2",
"@polymer/iron-form": "^3.0.1",
"@polymer/paper-input": "^3.2.1",
"@rollup/plugin-babel": "^6.0.3", "@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-commonjs": "^24.0.1",
"@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-node-resolve": "^15.0.1",

View File

@ -61,6 +61,9 @@ export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> {
metrics.healthy += 1; metrics.healthy += 1;
} }
}); });
if (health.length < 1) {
metrics.unsynced += 1;
}
} catch { } catch {
metrics.unsynced += 1; metrics.unsynced += 1;
} }

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { KeyUnknown } from "@goauthentik/elements/forms/Form"; import { KeyUnknown } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
@ -60,7 +61,7 @@ export class TypeOAuthCodeApplicationWizardPage extends WizardFormPage {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -1,6 +1,10 @@
import { t } from "@lingui/macro"; import { t } from "@lingui/macro";
import { FlowDesignationEnum, LayoutEnum } from "@goauthentik/api"; import { Flow, FlowDesignationEnum, LayoutEnum } from "@goauthentik/api";
export function RenderFlowOption(flow: Flow): string {
return `${flow.slug} (${flow.name})`;
}
export function DesignationToLabel(designation: FlowDesignationEnum): string { export function DesignationToLabel(designation: FlowDesignationEnum): string {
switch (designation) { switch (designation) {

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG, tenant } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG, tenant } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
@ -9,9 +10,8 @@ import "@goauthentik/elements/forms/SearchSelect";
import { t } from "@lingui/macro"; import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { import {
CertificateKeyPair, CertificateKeyPair,
@ -19,6 +19,7 @@ import {
CoreGroupsListRequest, CoreGroupsListRequest,
CryptoApi, CryptoApi,
CryptoCertificatekeypairsListRequest, CryptoCertificatekeypairsListRequest,
CurrentTenant,
Flow, Flow,
FlowsApi, FlowsApi,
FlowsInstancesListDesignationEnum, FlowsInstancesListDesignationEnum,
@ -31,10 +32,14 @@ import {
@customElement("ak-provider-ldap-form") @customElement("ak-provider-ldap-form")
export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> { export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
loadInstance(pk: number): Promise<LDAPProvider> { @state()
return new ProvidersApi(DEFAULT_CONFIG).providersLdapRetrieve({ tenant?: CurrentTenant;
async loadInstance(pk: number): Promise<LDAPProvider> {
const provider = await new ProvidersApi(DEFAULT_CONFIG).providersLdapRetrieve({
id: pk, id: pk,
}); });
this.tenant = await tenant();
return provider;
} }
getSuccessMessage(): string { getSuccessMessage(): string {
@ -74,46 +79,36 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
?required=${true} ?required=${true}
name="authorizationFlow" name="authorizationFlow"
> >
${until( <ak-search-select
tenant().then((t) => { .fetchObjects=${async (query?: string): Promise<Flow[]> => {
return html` const args: FlowsInstancesListRequest = {
<ak-search-select ordering: "slug",
.fetchObjects=${async (query?: string): Promise<Flow[]> => { designation: FlowsInstancesListDesignationEnum.Authentication,
const args: FlowsInstancesListRequest = { };
ordering: "slug", if (query !== undefined) {
designation: args.search = query;
FlowsInstancesListDesignationEnum.Authentication, }
}; const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args);
if (query !== undefined) { return flows.results;
args.search = query; }}
} .renderElement=${(flow: Flow): string => {
const flows = await new FlowsApi( return RenderFlowOption(flow);
DEFAULT_CONFIG, }}
).flowsInstancesList(args); .renderDescription=${(flow: Flow): TemplateResult => {
return flows.results; return html`${flow.slug}`;
}} }}
.renderElement=${(flow: Flow): string => { .value=${(flow: Flow | undefined): string | undefined => {
return flow.name; return flow?.pk;
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .selected=${(flow: Flow): boolean => {
return html`${flow.slug}`; let selected = flow.pk === this.tenant?.flowAuthentication;
}} if (this.instance?.authorizationFlow === flow.pk) {
.value=${(flow: Flow | undefined): string | undefined => { selected = true;
return flow?.pk; }
}} return selected;
.selected=${(flow: Flow): boolean => { }}
let selected = flow.pk === t.flowAuthentication; >
if (this.instance?.authorizationFlow === flow.pk) { </ak-search-select>
selected = true;
}
return selected;
}}
>
</ak-search-select>
`;
}),
html`<option>${t`Loading...`}</option>`,
)}
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Flow used for users to authenticate. Currently only identification and password stages are supported.`} ${t`Flow used for users to authenticate. Currently only identification and password stages are supported.`}
</p> </p>

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first, randomString } from "@goauthentik/common/utils"; import { first, randomString } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
@ -96,7 +97,7 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
@ -318,7 +319,7 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
@ -88,7 +89,7 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { SentryIgnoredError } from "@goauthentik/common/errors"; import { SentryIgnoredError } from "@goauthentik/common/errors";
import { Form } from "@goauthentik/elements/forms/Form"; import { Form } from "@goauthentik/elements/forms/Form";
@ -59,7 +60,7 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"; import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
@ -431,7 +432,7 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;
@ -477,7 +478,7 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"; import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { PlexAPIClient, PlexResource, popupCenterScreen } from "@goauthentik/common/helpers/plex"; import { PlexAPIClient, PlexResource, popupCenterScreen } from "@goauthentik/common/helpers/plex";
@ -364,7 +365,7 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;
@ -410,7 +411,7 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"; import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
@ -496,7 +497,7 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;
@ -540,7 +541,7 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;
@ -586,7 +587,7 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
@ -146,7 +147,7 @@ export class AuthenticatorDuoStageForm extends ModelForm<AuthenticatorDuoStage,
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
@ -292,7 +293,7 @@ export class AuthenticatorSMSStageForm extends ModelForm<AuthenticatorSMSStage,
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
@ -93,7 +94,7 @@ export class AuthenticatorStaticStageForm extends ModelForm<AuthenticatorStaticS
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
@ -98,7 +99,7 @@ export class AuthenticatorTOTPStageForm extends ModelForm<AuthenticatorTOTPStage
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -162,7 +163,7 @@ export class AuthenticateWebAuthnStageForm extends ModelForm<AuthenticateWebAuth
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first, groupBy } from "@goauthentik/common/utils"; import { first, groupBy } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
@ -265,7 +266,7 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { dateTimeLocal, first } from "@goauthentik/common/utils"; import { dateTimeLocal, first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
@ -88,7 +89,7 @@ export class InvitationForm extends ModelForm<Invitation, string> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
@ -136,7 +137,7 @@ export class PasswordStageForm extends ModelForm<PasswordStage, string> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -77,7 +77,7 @@ export class PromptStageForm extends ModelForm<PromptStage, string> {
value=${ifDefined(prompt.pk)} value=${ifDefined(prompt.pk)}
?selected=${selected} ?selected=${selected}
> >
${t`${prompt.fieldKey} ("${prompt.label}", of type ${prompt.type})`} ${t`${prompt.name} ("${prompt.fieldKey}", of type ${prompt.type})`}
</option>`; </option>`;
}); });
}), }),

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
@ -165,7 +166,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;
@ -202,7 +203,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;
@ -237,7 +238,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;
@ -274,7 +275,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;
@ -312,7 +313,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;
@ -347,7 +348,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
return flows.results; return flows.results;
}} }}
.renderElement=${(flow: Flow): string => { .renderElement=${(flow: Flow): string => {
return flow.slug; return RenderFlowOption(flow);
}} }}
.renderDescription=${(flow: Flow): TemplateResult => { .renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`; return html`${flow.name}`;

View File

@ -7,19 +7,9 @@ import { EVENT_REFRESH, VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { activateLocale } from "@goauthentik/common/ui/locale"; import { activateLocale } from "@goauthentik/common/ui/locale";
import { import { Config, Configuration, CoreApi, CurrentTenant, RootApi } from "@goauthentik/api";
Config,
ConfigFromJSON,
Configuration,
CoreApi,
CurrentTenant,
CurrentTenantFromJSON,
RootApi,
} from "@goauthentik/api";
let globalConfigPromise: Promise<Config> | undefined = Promise.resolve( let globalConfigPromise: Promise<Config> | undefined = Promise.resolve(globalAK().config);
ConfigFromJSON(globalAK()?.config),
);
export function config(): Promise<Config> { export function config(): Promise<Config> {
if (!globalConfigPromise) { if (!globalConfigPromise) {
globalConfigPromise = new RootApi(DEFAULT_CONFIG).rootConfigRetrieve(); globalConfigPromise = new RootApi(DEFAULT_CONFIG).rootConfigRetrieve();
@ -52,9 +42,7 @@ export function tenantSetLocale(tenant: CurrentTenant) {
activateLocale(tenant.defaultLocale); activateLocale(tenant.defaultLocale);
} }
let globalTenantPromise: Promise<CurrentTenant> | undefined = Promise.resolve( let globalTenantPromise: Promise<CurrentTenant> | undefined = Promise.resolve(globalAK().tenant);
CurrentTenantFromJSON(globalAK()?.tenant),
);
export function tenant(): Promise<CurrentTenant> { export function tenant(): Promise<CurrentTenant> {
if (!globalTenantPromise) { if (!globalTenantPromise) {
globalTenantPromise = new CoreApi(DEFAULT_CONFIG) globalTenantPromise = new CoreApi(DEFAULT_CONFIG)
@ -82,7 +70,7 @@ export const DEFAULT_CONFIG = new Configuration({
middleware: [ middleware: [
new CSRFMiddleware(), new CSRFMiddleware(),
new EventMiddleware(), new EventMiddleware(),
new LoggingMiddleware(CurrentTenantFromJSON(globalAK()?.tenant)), new LoggingMiddleware(globalAK().tenant),
], ],
}); });

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger"; export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress"; export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current"; export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2023.3.0"; export const VERSION = "2023.3.1";
export const TITLE_DEFAULT = "authentik"; export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";"; export const ROUTE_SEPARATOR = ";";

View File

@ -1,6 +1,7 @@
import { Config, CurrentTenant } from "@goauthentik/api"; import { Config, ConfigFromJSON, CurrentTenant, CurrentTenantFromJSON } from "@goauthentik/api";
export interface GlobalAuthentik { export interface GlobalAuthentik {
_converted?: boolean;
locale?: string; locale?: string;
flow?: { flow?: {
layout: string; layout: string;
@ -13,11 +14,17 @@ export interface GlobalAuthentik {
} }
export interface AuthentikWindow { export interface AuthentikWindow {
authentik?: GlobalAuthentik; authentik: GlobalAuthentik;
} }
export function globalAK(): GlobalAuthentik | undefined { export function globalAK(): GlobalAuthentik {
return (window as unknown as AuthentikWindow).authentik; const ak = (window as unknown as AuthentikWindow).authentik;
if (ak && !ak._converted) {
ak._converted = true;
ak.tenant = CurrentTenantFromJSON(ak.tenant);
ak.config = ConfigFromJSON(ak.config);
}
return ak;
} }
export function docLink(path: string): string { export function docLink(path: string): string {

View File

@ -172,6 +172,6 @@ export class Interface extends AKElement {
async getTheme(): Promise<UiThemeEnum> { async getTheme(): Promise<UiThemeEnum> {
const config = await uiConfig(); const config = await uiConfig();
return config.theme.base; return config.theme?.base || UiThemeEnum.Automatic;
} }
} }

View File

@ -5,9 +5,6 @@ import { AKElement } from "@goauthentik/elements/Base";
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement"; import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect"; import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import "@polymer/iron-form/iron-form";
import { IronFormElement } from "@polymer/iron-form/iron-form";
import "@polymer/paper-input/paper-input";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
@ -110,63 +107,76 @@ export class Form<T> extends AKElement {
* Reset the inner iron-form * Reset the inner iron-form
*/ */
resetForm(): void { resetForm(): void {
const ironForm = this.shadowRoot?.querySelector("iron-form"); const form = this.shadowRoot?.querySelector<HTMLFormElement>("form");
ironForm?.reset(); form?.reset();
} }
getFormFiles(): { [key: string]: File } { getFormFiles(): { [key: string]: File } {
const ironForm = this.shadowRoot?.querySelector("iron-form");
const files: { [key: string]: File } = {}; const files: { [key: string]: File } = {};
if (!ironForm) { const elements =
return files; this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
} "ak-form-element-horizontal",
const elements = ironForm._getSubmittableElements(); ) || [];
for (let i = 0; i < elements.length; i++) { for (let i = 0; i < elements.length; i++) {
const element = elements[i] as HTMLInputElement; const element = elements[i];
if (element.tagName.toLowerCase() === "input" && element.type === "file") { element.requestUpdate();
if ((element.files || []).length < 1) { const inputElement = element.querySelector<HTMLInputElement>("[name]");
if (!inputElement) {
continue;
}
if (inputElement.tagName.toLowerCase() === "input" && inputElement.type === "file") {
if ((inputElement.files || []).length < 1) {
continue; continue;
} }
files[element.name] = (element.files || [])[0]; files[element.name] = (inputElement.files || [])[0];
} }
} }
return files; return files;
} }
serializeForm(): T | undefined { serializeForm(): T | undefined {
const form = this.shadowRoot?.querySelector<IronFormElement>("iron-form"); const elements =
if (!form) { this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
console.warn("authentik/forms: failed to find iron-form"); "ak-form-element-horizontal",
return; ) || [];
}
const elements: HTMLInputElement[] = form._getSubmittableElements();
const json: { [key: string]: unknown } = {}; const json: { [key: string]: unknown } = {};
elements.forEach((element) => { elements.forEach((element) => {
const values = form._serializeElementValues(element); element.requestUpdate();
if (element.hidden) { const inputElement = element.querySelector<HTMLInputElement>("[name]");
if (element.hidden || !inputElement) {
return; return;
} }
if (element.tagName.toLowerCase() === "select" && "multiple" in element.attributes) { if (
json[element.name] = values; inputElement.tagName.toLowerCase() === "select" &&
} else if (element.tagName.toLowerCase() === "input" && element.type === "date") { "multiple" in inputElement.attributes
json[element.name] = element.valueAsDate;
} else if (
element.tagName.toLowerCase() === "input" &&
element.type === "datetime-local"
) { ) {
json[element.name] = new Date(element.valueAsNumber); const selectElement = inputElement as unknown as HTMLSelectElement;
json[element.name] = Array.from(selectElement.selectedOptions).map((v) => v.value);
} else if ( } else if (
element.tagName.toLowerCase() === "input" && inputElement.tagName.toLowerCase() === "input" &&
"type" in element.dataset && inputElement.type === "date"
element.dataset["type"] === "datetime-local" ) {
json[element.name] = inputElement.valueAsDate;
} else if (
inputElement.tagName.toLowerCase() === "input" &&
inputElement.type === "datetime-local"
) {
json[element.name] = new Date(inputElement.valueAsNumber);
} else if (
inputElement.tagName.toLowerCase() === "input" &&
"type" in inputElement.dataset &&
inputElement.dataset["type"] === "datetime-local"
) { ) {
// Workaround for Firefox <93, since 92 and older don't support // Workaround for Firefox <93, since 92 and older don't support
// datetime-local fields // datetime-local fields
json[element.name] = new Date(element.value); json[element.name] = new Date(inputElement.value);
} else if (element.tagName.toLowerCase() === "input" && element.type === "checkbox") { } else if (
json[element.name] = element.checked; inputElement.tagName.toLowerCase() === "input" &&
} else if (element.tagName.toLowerCase() === "ak-search-select") { inputElement.type === "checkbox"
const select = element as unknown as SearchSelect<unknown>; ) {
json[element.name] = inputElement.checked;
} else if (inputElement.tagName.toLowerCase() === "ak-search-select") {
const select = inputElement as unknown as SearchSelect<unknown>;
let value: unknown; let value: unknown;
try { try {
value = select.toForm(); value = select.toForm();
@ -179,9 +189,7 @@ export class Form<T> extends AKElement {
} }
json[element.name] = value; json[element.name] = value;
} else { } else {
for (let v = 0; v < values.length; v++) { this.serializeFieldRecursive(inputElement, inputElement.value, json);
this.serializeFieldRecursive(element, values[v], json);
}
} }
}); });
return json as unknown as T; return json as unknown as T;
@ -213,11 +221,6 @@ export class Form<T> extends AKElement {
if (!data) { if (!data) {
return; return;
} }
const form = this.shadowRoot?.querySelector<IronFormElement>("iron-form");
if (!form) {
console.warn("authentik/forms: failed to find iron-form");
return;
}
return this.send(data) return this.send(data)
.then((r) => { .then((r) => {
showMessage({ showMessage({
@ -244,8 +247,12 @@ export class Form<T> extends AKElement {
throw errorMessage; throw errorMessage;
} }
// assign all input-related errors to their elements // assign all input-related errors to their elements
const elements: HorizontalFormElement[] = form._getSubmittableElements(); const elements =
this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
"ak-form-element-horizontal",
) || [];
elements.forEach((element) => { elements.forEach((element) => {
element.requestUpdate();
const elementName = element.name; const elementName = element.name;
if (!elementName) return; if (!elementName) return;
if (camelToSnake(elementName) in errorMessage) { if (camelToSnake(elementName) in errorMessage) {
@ -296,13 +303,7 @@ export class Form<T> extends AKElement {
} }
renderVisible(): TemplateResult { renderVisible(): TemplateResult {
return html`<iron-form return html` ${this.renderNonFieldErrors()} ${this.renderForm()}`;
@iron-form-presubmit=${(ev: Event) => {
this.submit(ev);
}}
>
${this.renderNonFieldErrors()} ${this.renderForm()}
</iron-form>`;
} }
render(): TemplateResult { render(): TemplateResult {

View File

@ -69,6 +69,10 @@ export class HorizontalFormElement extends AKElement {
@property() @property()
name = ""; name = "";
firstUpdated(): void {
this.updated();
}
updated(): void { updated(): void {
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => { this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => {
input.focus(); input.focus();
@ -89,7 +93,7 @@ export class HorizontalFormElement extends AKElement {
case "ak-chip-group": case "ak-chip-group":
case "ak-search-select": case "ak-search-select":
case "ak-radio": case "ak-radio":
(input as HTMLInputElement).name = this.name; input.setAttribute("name", this.name);
break; break;
default: default:
return; return;
@ -108,6 +112,7 @@ export class HorizontalFormElement extends AKElement {
} }
render(): TemplateResult { render(): TemplateResult {
this.updated();
return html`<div class="pf-c-form__group"> return html`<div class="pf-c-form__group">
<div class="pf-c-form__group-label"> <div class="pf-c-form__group-label">
<label class="pf-c-form__label"> <label class="pf-c-form__label">

View File

@ -70,6 +70,7 @@ export class SearchSelect<T> extends AKElement {
observer: IntersectionObserver; observer: IntersectionObserver;
dropdownUID: string; dropdownUID: string;
dropdownContainer: HTMLDivElement; dropdownContainer: HTMLDivElement;
isFetchingData = false;
constructor() { constructor() {
super(); super();
@ -103,13 +104,18 @@ export class SearchSelect<T> extends AKElement {
} }
updateData(): void { updateData(): void {
if (this.isFetchingData) {
return;
}
this.isFetchingData = true;
this.fetchObjects(this.query).then((objects) => { this.fetchObjects(this.query).then((objects) => {
this.objects = objects; objects.forEach((obj) => {
this.objects.forEach((obj) => {
if (this.selected && this.selected(obj, this.objects || [])) { if (this.selected && this.selected(obj, this.objects || [])) {
this.selectedObject = obj; this.selectedObject = obj;
} }
}); });
this.objects = objects;
this.isFetchingData = false;
}); });
} }
@ -200,9 +206,10 @@ export class SearchSelect<T> extends AKElement {
render( render(
html`<div html`<div
class="pf-c-dropdown pf-m-expanded" class="pf-c-dropdown pf-m-expanded"
?hidden=${!this.open}
style="position: fixed; inset: 0px auto auto 0px; z-index: 9999; transform: translate(${pos.x}px, ${pos.y + style="position: fixed; inset: 0px auto auto 0px; z-index: 9999; transform: translate(${pos.x}px, ${pos.y +
this.offsetHeight}px); width: ${pos.width}px;" this.offsetHeight}px); width: ${pos.width}px; ${this.open
? ""
: "visibility: hidden;"}"
> >
<ul <ul
class="pf-c-dropdown__menu pf-m-static" class="pf-c-dropdown__menu pf-m-static"
@ -249,6 +256,14 @@ export class SearchSelect<T> extends AKElement {
render(): TemplateResult { render(): TemplateResult {
this.renderMenu(); this.renderMenu();
let value = "";
if (!this.objects) {
value = t`Loading...`;
} else if (this.selectedObject) {
value = this.renderElement(this.selectedObject);
} else if (this.blankable) {
value = this.emptyOption;
}
return html`<div class="pf-c-select"> return html`<div class="pf-c-select">
<div class="pf-c-select__toggle pf-m-typeahead"> <div class="pf-c-select__toggle pf-m-typeahead">
<div class="pf-c-select__toggle-wrapper"> <div class="pf-c-select__toggle-wrapper">
@ -256,6 +271,7 @@ export class SearchSelect<T> extends AKElement {
class="pf-c-form-control pf-c-select__toggle-typeahead" class="pf-c-form-control pf-c-select__toggle-typeahead"
type="text" type="text"
placeholder=${this.placeholder} placeholder=${this.placeholder}
spellcheck="false"
@input=${(ev: InputEvent) => { @input=${(ev: InputEvent) => {
this.query = (ev.target as HTMLInputElement).value; this.query = (ev.target as HTMLInputElement).value;
this.updateData(); this.updateData();
@ -285,11 +301,7 @@ export class SearchSelect<T> extends AKElement {
this.open = false; this.open = false;
this.renderMenu(); this.renderMenu();
}} }}
.value=${this.selectedObject .value=${value}
? this.renderElement(this.selectedObject)
: this.blankable
? this.emptyOption
: ""}
/> />
</div> </div>
</div> </div>

View File

@ -68,6 +68,7 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
if (this.timer) { if (this.timer) {
console.debug("authentik/stages/password: cleared focus timer"); console.debug("authentik/stages/password: cleared focus timer");
window.clearInterval(this.timer); window.clearInterval(this.timer);
this.timer = undefined;
} }
} }

View File

@ -1,5 +1,3 @@
// @ts-ignore
window["polymerSkipLoadingFontRoboto"] = true;
import "construct-style-sheets-polyfill"; import "construct-style-sheets-polyfill";
import "@webcomponents/webcomponentsjs"; import "@webcomponents/webcomponentsjs";
import "lit/polyfill-support.js"; import "lit/polyfill-support.js";

View File

@ -18,7 +18,7 @@ When authenticating with a flow, you'll get an authenticated Session cookie, tha
### API Token ### API Token
Superusers can create tokens to authenticate as any user with a static key, which can optionally be expiring and auto-rotate. Users can create tokens to authenticate as any user with a static key, which can optionally be expiring and auto-rotate.
### JWT Token ### JWT Token

View File

@ -0,0 +1,27 @@
# Models
Some models behave differently and allow for access to different API fields when created via blueprint.
### `authentik_core.token`
:::info
Requires authentik 2023.4
:::
Via the standard API, a token's key cannot be changed, it can only be rotated. This is to ensure a high entropy in it's key, and to prevent insecure data from being used. However, when provisioning tokens via a blueprint, it may be required to set a token to an existing value.
With blueprints, the field `key` can be set, to set the token's key to any value.
For example:
```yaml
# [...]
- model: authentik_core.token
state: present
identifiers:
identifier: my-token
attrs:
key: this-should-be-a-long-value
user: !KeyOf my-user
intent: api
```

View File

@ -16,6 +16,7 @@ module.exports = {
"blueprints/v1/structure", "blueprints/v1/structure",
"blueprints/v1/tags", "blueprints/v1/tags",
"blueprints/v1/example", "blueprints/v1/example",
"blueprints/v1/models",
"blueprints/v1/meta", "blueprints/v1/meta",
], ],
}, },