Compare commits
	
		
			11 Commits
		
	
	
		
			version/20
			...
			version-20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fb0a88f2cf | |||
| 4d8d405e70 | |||
| 1d5f399b61 | |||
| bb575fcc10 | |||
| 13fd1afbb9 | |||
| f059b998cc | |||
| 3f48202dfe | |||
| 2a3ebb616b | |||
| ceab1f732d | |||
| 01d2cce9ca | |||
| 72f85defb8 | 
| @ -1,5 +1,5 @@ | ||||
| [bumpversion] | ||||
| current_version = 2023.3.0 | ||||
| current_version = 2023.3.1 | ||||
| tag = True | ||||
| commit = True | ||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) | ||||
|  | ||||
| @ -12,3 +12,6 @@ indent_size = 2 | ||||
|  | ||||
| [*.{yaml,yml}] | ||||
| indent_size = 2 | ||||
|  | ||||
| [*.go] | ||||
| indent_style = tab | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
| from os import environ | ||||
| from typing import Optional | ||||
|  | ||||
| __version__ = "2023.3.0" | ||||
| __version__ = "2023.3.1" | ||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -19,10 +19,8 @@ class Command(BaseCommand): | ||||
|         for blueprint_path in options.get("blueprints", []): | ||||
|             content = BlueprintInstance(path=blueprint_path).retrieve() | ||||
|             importer = Importer(content) | ||||
|             valid, logs = importer.validate() | ||||
|             valid, _ = importer.validate() | ||||
|             if not valid: | ||||
|                 for log in logs: | ||||
|                     getattr(LOGGER, log.pop("log_level"))(**log) | ||||
|                 self.stderr.write("blueprint invalid") | ||||
|                 sys_exit(1) | ||||
|             importer.apply() | ||||
|  | ||||
| @ -40,6 +40,10 @@ from authentik.lib.models import SerializerModel | ||||
| from authentik.outposts.models import OutpostServiceConnection | ||||
| from authentik.policies.models import Policy, PolicyBindingModel | ||||
|  | ||||
| # Context set when the serializer is created in a blueprint context | ||||
| # Update website/developer-docs/blueprints/v1/models.md when used | ||||
| SERIALIZER_CONTEXT_BLUEPRINT = "blueprint_entry" | ||||
|  | ||||
|  | ||||
| def is_model_allowed(model: type[Model]) -> bool: | ||||
|     """Check if model is allowed""" | ||||
| @ -158,7 +162,12 @@ class Importer: | ||||
|             raise EntryInvalidError(f"Model {model} not allowed") | ||||
|         if issubclass(model, BaseMetaModel): | ||||
|             serializer_class: type[Serializer] = model.serializer() | ||||
|             serializer = serializer_class(data=entry.get_attrs(self.__import)) | ||||
|             serializer = serializer_class( | ||||
|                 data=entry.get_attrs(self.__import), | ||||
|                 context={ | ||||
|                     SERIALIZER_CONTEXT_BLUEPRINT: entry, | ||||
|                 }, | ||||
|             ) | ||||
|             try: | ||||
|                 serializer.is_valid(raise_exception=True) | ||||
|             except ValidationError as exc: | ||||
| @ -217,7 +226,12 @@ class Importer: | ||||
|         always_merger.merge(full_data, updated_identifiers) | ||||
|         serializer_kwargs["data"] = full_data | ||||
|  | ||||
|         serializer: Serializer = model().serializer(**serializer_kwargs) | ||||
|         serializer: Serializer = model().serializer( | ||||
|             context={ | ||||
|                 SERIALIZER_CONTEXT_BLUEPRINT: entry, | ||||
|             }, | ||||
|             **serializer_kwargs, | ||||
|         ) | ||||
|         try: | ||||
|             serializer.is_valid(raise_exception=True) | ||||
|         except ValidationError as exc: | ||||
|  | ||||
| @ -16,6 +16,7 @@ from rest_framework.viewsets import ModelViewSet | ||||
| from authentik.api.authorization import OwnerSuperuserPermissions | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.blueprints.api import ManagedSerializer | ||||
| from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.users import UserSerializer | ||||
| from authentik.core.api.utils import PassiveSerializer | ||||
| @ -29,6 +30,11 @@ class TokenSerializer(ManagedSerializer, ModelSerializer): | ||||
|  | ||||
|     user_obj = UserSerializer(required=False, source="user", read_only=True) | ||||
|  | ||||
|     def __init__(self, *args, **kwargs) -> None: | ||||
|         super().__init__(*args, **kwargs) | ||||
|         if SERIALIZER_CONTEXT_BLUEPRINT in self.context: | ||||
|             self.fields["key"] = CharField() | ||||
|  | ||||
|     def validate(self, attrs: dict[Any, str]) -> dict[Any, str]: | ||||
|         """Ensure only API or App password tokens are created.""" | ||||
|         request: Request = self.context.get("request") | ||||
|  | ||||
| @ -355,6 +355,62 @@ class TestAuthorize(OAuthTestCase): | ||||
|                 delta=5, | ||||
|             ) | ||||
|  | ||||
|     def test_full_fragment_code(self): | ||||
|         """Test full authorization""" | ||||
|         flow = create_test_flow() | ||||
|         provider: OAuth2Provider = OAuth2Provider.objects.create( | ||||
|             name=generate_id(), | ||||
|             client_id="test", | ||||
|             client_secret=generate_key(), | ||||
|             authorization_flow=flow, | ||||
|             redirect_uris="http://localhost", | ||||
|             signing_key=self.keypair, | ||||
|         ) | ||||
|         Application.objects.create(name="app", slug="app", provider=provider) | ||||
|         state = generate_id() | ||||
|         user = create_test_admin_user() | ||||
|         self.client.force_login(user) | ||||
|         with patch( | ||||
|             "authentik.providers.oauth2.id_token.get_login_event", | ||||
|             MagicMock( | ||||
|                 return_value=Event( | ||||
|                     action=EventAction.LOGIN, | ||||
|                     context={PLAN_CONTEXT_METHOD: "password"}, | ||||
|                     created=now(), | ||||
|                 ) | ||||
|             ), | ||||
|         ): | ||||
|             # Step 1, initiate params and get redirect to flow | ||||
|             self.client.get( | ||||
|                 reverse("authentik_providers_oauth2:authorize"), | ||||
|                 data={ | ||||
|                     "response_type": "code", | ||||
|                     "response_mode": "fragment", | ||||
|                     "client_id": "test", | ||||
|                     "state": state, | ||||
|                     "scope": "openid", | ||||
|                     "redirect_uri": "http://localhost", | ||||
|                     "nonce": generate_id(), | ||||
|                 }, | ||||
|             ) | ||||
|             response = self.client.get( | ||||
|                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||
|             ) | ||||
|             code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first() | ||||
|             self.assertJSONEqual( | ||||
|                 response.content.decode(), | ||||
|                 { | ||||
|                     "component": "xak-flow-redirect", | ||||
|                     "type": ChallengeTypes.REDIRECT.value, | ||||
|                     "to": (f"http://localhost#code={code.code}" f"&state={state}"), | ||||
|                 }, | ||||
|             ) | ||||
|             self.assertAlmostEqual( | ||||
|                 code.expires.timestamp() - now().timestamp(), | ||||
|                 timedelta_from_string(provider.access_code_validity).total_seconds(), | ||||
|                 delta=5, | ||||
|             ) | ||||
|  | ||||
|     def test_full_form_post_id_token(self): | ||||
|         """Test full authorization (form_post response)""" | ||||
|         flow = create_test_flow() | ||||
|  | ||||
| @ -514,6 +514,11 @@ class OAuthFulfillmentStage(StageView): | ||||
|                 return urlunsplit(uri) | ||||
|  | ||||
|             if self.params.response_mode == ResponseMode.FRAGMENT: | ||||
|                 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( | ||||
|  | ||||
| @ -32,7 +32,7 @@ services: | ||||
|     volumes: | ||||
|       - redis:/data | ||||
|   server: | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.3.0} | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.3.1} | ||||
|     restart: unless-stopped | ||||
|     command: server | ||||
|     environment: | ||||
| @ -50,7 +50,7 @@ services: | ||||
|       - "${AUTHENTIK_PORT_HTTP:-9000}:9000" | ||||
|       - "${AUTHENTIK_PORT_HTTPS:-9443}:9443" | ||||
|   worker: | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.3.0} | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.3.1} | ||||
|     restart: unless-stopped | ||||
|     command: worker | ||||
|     environment: | ||||
|  | ||||
| @ -29,4 +29,4 @@ func UserAgent() string { | ||||
| 	return fmt.Sprintf("authentik@%s", FullVersion()) | ||||
| } | ||||
|  | ||||
| const VERSION = "2023.3.0" | ||||
| const VERSION = "2023.3.1" | ||||
|  | ||||
| @ -30,11 +30,15 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry { | ||||
| 	// Only append attributes that don't already exist | ||||
| 	// TODO: Remove in 2023.3 | ||||
| 	for _, rawAttr := range rawAttrs { | ||||
| 		exists := false | ||||
| 		for _, attr := range attrs { | ||||
| 			if !strings.EqualFold(attr.Name, rawAttr.Name) { | ||||
| 				attrs = append(attrs, rawAttr) | ||||
| 			if strings.EqualFold(attr.Name, rawAttr.Name) { | ||||
| 				exists = true | ||||
| 			} | ||||
| 		} | ||||
| 		if !exists { | ||||
| 			attrs = append(attrs, rawAttr) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if u.IsActive == nil { | ||||
|  | ||||
| @ -36,11 +36,15 @@ func (lg *LDAPGroup) Entry() *ldap.Entry { | ||||
| 	// Only append attributes that don't already exist | ||||
| 	// TODO: Remove in 2023.3 | ||||
| 	for _, rawAttr := range rawAttrs { | ||||
| 		exists := false | ||||
| 		for _, attr := range attrs { | ||||
| 			if !strings.EqualFold(attr.Name, rawAttr.Name) { | ||||
| 				attrs = append(attrs, rawAttr) | ||||
| 			if strings.EqualFold(attr.Name, rawAttr.Name) { | ||||
| 				exists = true | ||||
| 			} | ||||
| 		} | ||||
| 		if !exists { | ||||
| 			attrs = append(attrs, rawAttr) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	objectClass := []string{constants.OCGroup, constants.OCGroupOfUniqueNames, constants.OCGroupOfNames, constants.OCAKGroup, constants.OCPosixGroup} | ||||
|  | ||||
| @ -2,7 +2,6 @@ package application | ||||
|  | ||||
| import ( | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
|  | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| 	"goauthentik.io/api/v3" | ||||
| @ -31,29 +30,29 @@ func updateURL(rawUrl string, scheme string, host string) string { | ||||
| func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bool) OIDCEndpoint { | ||||
| 	authUrl := p.OidcConfiguration.AuthorizationEndpoint | ||||
| 	endUrl := p.OidcConfiguration.EndSessionEndpoint | ||||
| 	tokenUrl := p.OidcConfiguration.TokenEndpoint | ||||
| 	jwksUrl := p.OidcConfiguration.JwksUri | ||||
| 	issuer := p.OidcConfiguration.Issuer | ||||
| 	if config.Get().AuthentikHostBrowser != "" { | ||||
| 		authUrl = strings.ReplaceAll(authUrl, authentikHost, config.Get().AuthentikHostBrowser) | ||||
| 		endUrl = strings.ReplaceAll(endUrl, authentikHost, config.Get().AuthentikHostBrowser) | ||||
| 		jwksUrl = strings.ReplaceAll(jwksUrl, authentikHost, config.Get().AuthentikHostBrowser) | ||||
| 		issuer = strings.ReplaceAll(issuer, authentikHost, config.Get().AuthentikHostBrowser) | ||||
| 	} | ||||
| 	ep := OIDCEndpoint{ | ||||
| 		Endpoint: oauth2.Endpoint{ | ||||
| 			AuthURL:   authUrl, | ||||
| 			TokenURL:  tokenUrl, | ||||
| 			TokenURL:  p.OidcConfiguration.TokenEndpoint, | ||||
| 			AuthStyle: oauth2.AuthStyleInParams, | ||||
| 		}, | ||||
| 		EndSessionEndpoint: endUrl, | ||||
| 		JwksUri:            jwksUrl, | ||||
| 		JwksUri:            p.OidcConfiguration.JwksUri, | ||||
| 		TokenIntrospection: p.OidcConfiguration.IntrospectionEndpoint, | ||||
| 		Issuer:             issuer, | ||||
| 	} | ||||
| 	if !embedded { | ||||
| 	// For the embedded outpost, we use the configure `authentik_host` for the browser URLs | ||||
| 	// and localhost (which is what we've got from the API) for backchannel URLs | ||||
| 	// | ||||
| 	// For other outposts, when `AUTHENTIK_HOST_BROWSER` is set, we use that for the browser URLs | ||||
| 	// and use what we got from the API for backchannel | ||||
| 	hostBrowser := config.Get().AuthentikHostBrowser | ||||
| 	if !embedded && hostBrowser == "" { | ||||
| 		return ep | ||||
| 	} | ||||
| 	var newHost *url.URL | ||||
| 	if embedded { | ||||
| 		if authentikHost == "" { | ||||
| 			log.Warning("Outpost has localhost/blank API Connection but no authentik_host is configured.") | ||||
| 			return ep | ||||
| @ -62,9 +61,24 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bo | ||||
| 		if err != nil { | ||||
| 			return ep | ||||
| 		} | ||||
| 	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) | ||||
| 		newHost = aku | ||||
| 	} else if hostBrowser != "" { | ||||
| 		aku, err := url.Parse(hostBrowser) | ||||
| 		if err != nil { | ||||
| 			return ep | ||||
| 		} | ||||
| 		newHost = aku | ||||
| 	} | ||||
| 	// Update all browser-accessed URLs to use the new host and scheme | ||||
| 	ep.AuthURL = updateURL(authUrl, newHost.Scheme, newHost.Host) | ||||
| 	ep.EndSessionEndpoint = updateURL(endUrl, newHost.Scheme, newHost.Host) | ||||
| 	// Update issuer to use the same host and scheme, which would normally break as we don't | ||||
| 	// change the token URL here, but the token HTTP transport overwrites the Host header | ||||
| 	// | ||||
| 	// This is only used in embedded outposts as there we can guarantee that the request | ||||
| 	// is routed correctly | ||||
| 	if embedded { | ||||
| 		ep.Issuer = updateURL(ep.Issuer, newHost.Scheme, newHost.Host) | ||||
| 	} | ||||
| 	return ep | ||||
| } | ||||
|  | ||||
							
								
								
									
										88
									
								
								internal/outpost/proxyv2/application/endpoint_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								internal/outpost/proxyv2/application/endpoint_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,88 @@ | ||||
| package application | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"goauthentik.io/api/v3" | ||||
| 	"goauthentik.io/internal/config" | ||||
| ) | ||||
|  | ||||
| func TestEndpointDefault(t *testing.T) { | ||||
| 	pc := api.ProxyOutpostConfig{ | ||||
| 		OidcConfiguration: api.ProxyOutpostConfigOidcConfiguration{ | ||||
| 			AuthorizationEndpoint: "https://test.goauthentik.io/application/o/authorize/", | ||||
| 			EndSessionEndpoint:    "https://test.goauthentik.io/application/o/test-app/end-session/", | ||||
| 			IntrospectionEndpoint: "https://test.goauthentik.io/application/o/introspect/", | ||||
| 			Issuer:                "https://test.goauthentik.io/application/o/test-app/", | ||||
| 			JwksUri:               "https://test.goauthentik.io/application/o/test-app/jwks/", | ||||
| 			TokenEndpoint:         "https://test.goauthentik.io/application/o/token/", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	ep := GetOIDCEndpoint(pc, "https://authentik-host.test.goauthentik.io", false) | ||||
| 	// Standard outpost, non embedded | ||||
| 	// All URLs should use the host that they get from the config | ||||
| 	assert.Equal(t, "https://test.goauthentik.io/application/o/authorize/", ep.AuthURL) | ||||
| 	assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL) | ||||
| 	assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/", ep.Issuer) | ||||
| 	assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri) | ||||
| 	assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint) | ||||
| 	assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection) | ||||
| } | ||||
|  | ||||
| func TestEndpointAuthentikHostBrowser(t *testing.T) { | ||||
| 	c := config.Get() | ||||
| 	c.AuthentikHostBrowser = "https://browser.test.goauthentik.io" | ||||
| 	defer func() { | ||||
| 		c.AuthentikHostBrowser = "" | ||||
| 	}() | ||||
| 	pc := api.ProxyOutpostConfig{ | ||||
| 		OidcConfiguration: api.ProxyOutpostConfigOidcConfiguration{ | ||||
| 			AuthorizationEndpoint: "https://test.goauthentik.io/application/o/authorize/", | ||||
| 			EndSessionEndpoint:    "https://test.goauthentik.io/application/o/test-app/end-session/", | ||||
| 			IntrospectionEndpoint: "https://test.goauthentik.io/application/o/introspect/", | ||||
| 			Issuer:                "https://test.goauthentik.io/application/o/test-app/", | ||||
| 			JwksUri:               "https://test.goauthentik.io/application/o/test-app/jwks/", | ||||
| 			TokenEndpoint:         "https://test.goauthentik.io/application/o/token/", | ||||
| 			UserinfoEndpoint:      "https://test.goauthentik.io/application/o/userinfo/", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	ep := GetOIDCEndpoint(pc, "https://authentik-host.test.goauthentik.io", false) | ||||
| 	// Standard outpost, with AUTHENTIK_HOST_BROWSER set | ||||
| 	// Only the authorize/end session URLs should be changed | ||||
| 	assert.Equal(t, "https://browser.test.goauthentik.io/application/o/authorize/", ep.AuthURL) | ||||
| 	assert.Equal(t, "https://browser.test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint) | ||||
| 	assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL) | ||||
| 	assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/", ep.Issuer) | ||||
| 	assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri) | ||||
| 	assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection) | ||||
| } | ||||
|  | ||||
| func TestEndpointEmbedded(t *testing.T) { | ||||
| 	pc := api.ProxyOutpostConfig{ | ||||
| 		OidcConfiguration: api.ProxyOutpostConfigOidcConfiguration{ | ||||
| 			AuthorizationEndpoint: "https://test.goauthentik.io/application/o/authorize/", | ||||
| 			EndSessionEndpoint:    "https://test.goauthentik.io/application/o/test-app/end-session/", | ||||
| 			IntrospectionEndpoint: "https://test.goauthentik.io/application/o/introspect/", | ||||
| 			Issuer:                "https://test.goauthentik.io/application/o/test-app/", | ||||
| 			JwksUri:               "https://test.goauthentik.io/application/o/test-app/jwks/", | ||||
| 			TokenEndpoint:         "https://test.goauthentik.io/application/o/token/", | ||||
| 			UserinfoEndpoint:      "https://test.goauthentik.io/application/o/userinfo/", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	ep := GetOIDCEndpoint(pc, "https://authentik-host.test.goauthentik.io", true) | ||||
| 	// Embedded outpost | ||||
| 	// Browser URLs should use the config of "authentik_host", everything else can use what's | ||||
| 	// received from the API endpoint | ||||
| 	// Token URL is an exception since it's sent via a special HTTP transport that overrides the | ||||
| 	// HTTP Host header, to make sure it's the same value as the issuer | ||||
| 	assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/authorize/", ep.AuthURL) | ||||
| 	assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/test-app/", ep.Issuer) | ||||
| 	assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL) | ||||
| 	assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri) | ||||
| 	assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint) | ||||
| 	assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection) | ||||
| } | ||||
| @ -105,7 +105,7 @@ filterwarnings = [ | ||||
|  | ||||
| [tool.poetry] | ||||
| name = "authentik" | ||||
| version = "2023.3.0" | ||||
| version = "2023.3.1" | ||||
| description = "" | ||||
| authors = ["authentik Team <hello@goauthentik.io>"] | ||||
|  | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| openapi: 3.0.3 | ||||
| info: | ||||
|   title: authentik | ||||
|   version: 2023.3.0 | ||||
|   version: 2023.3.1 | ||||
|   description: Making authentication simple. | ||||
|   contact: | ||||
|     email: hello@goauthentik.io | ||||
|  | ||||
							
								
								
									
										7668
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7668
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -74,8 +74,6 @@ | ||||
|         "@lingui/detect-locale": "^3.17.2", | ||||
|         "@lingui/macro": "^3.17.2", | ||||
|         "@patternfly/patternfly": "^4.224.2", | ||||
|         "@polymer/iron-form": "^3.0.1", | ||||
|         "@polymer/paper-input": "^3.2.1", | ||||
|         "@rollup/plugin-babel": "^6.0.3", | ||||
|         "@rollup/plugin-commonjs": "^24.0.1", | ||||
|         "@rollup/plugin-node-resolve": "^15.0.1", | ||||
|  | ||||
| @ -61,6 +61,9 @@ export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> { | ||||
|                             metrics.healthy += 1; | ||||
|                         } | ||||
|                     }); | ||||
|                     if (health.length < 1) { | ||||
|                         metrics.unsynced += 1; | ||||
|                     } | ||||
|                 } catch { | ||||
|                     metrics.unsynced += 1; | ||||
|                 } | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { KeyUnknown } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| @ -60,7 +61,7 @@ export class TypeOAuthCodeApplicationWizardPage extends WizardFormPage { | ||||
|                         return flows.results; | ||||
|                     }} | ||||
|                     .renderElement=${(flow: Flow): string => { | ||||
|                         return flow.slug; | ||||
|                         return RenderFlowOption(flow); | ||||
|                     }} | ||||
|                     .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                         return html`${flow.name}`; | ||||
|  | ||||
| @ -1,6 +1,10 @@ | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { FlowDesignationEnum, LayoutEnum } from "@goauthentik/api"; | ||||
| import { Flow, FlowDesignationEnum, LayoutEnum } from "@goauthentik/api"; | ||||
|  | ||||
| export function RenderFlowOption(flow: Flow): string { | ||||
|     return `${flow.slug} (${flow.name})`; | ||||
| } | ||||
|  | ||||
| export function DesignationToLabel(designation: FlowDesignationEnum): string { | ||||
|     switch (designation) { | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG, tenant } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| @ -9,9 +10,8 @@ import "@goauthentik/elements/forms/SearchSelect"; | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { customElement, state } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { | ||||
|     CertificateKeyPair, | ||||
| @ -19,6 +19,7 @@ import { | ||||
|     CoreGroupsListRequest, | ||||
|     CryptoApi, | ||||
|     CryptoCertificatekeypairsListRequest, | ||||
|     CurrentTenant, | ||||
|     Flow, | ||||
|     FlowsApi, | ||||
|     FlowsInstancesListDesignationEnum, | ||||
| @ -31,10 +32,14 @@ import { | ||||
|  | ||||
| @customElement("ak-provider-ldap-form") | ||||
| export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> { | ||||
|     loadInstance(pk: number): Promise<LDAPProvider> { | ||||
|         return new ProvidersApi(DEFAULT_CONFIG).providersLdapRetrieve({ | ||||
|     @state() | ||||
|     tenant?: CurrentTenant; | ||||
|     async loadInstance(pk: number): Promise<LDAPProvider> { | ||||
|         const provider = await new ProvidersApi(DEFAULT_CONFIG).providersLdapRetrieve({ | ||||
|             id: pk, | ||||
|         }); | ||||
|         this.tenant = await tenant(); | ||||
|         return provider; | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
| @ -74,26 +79,20 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> { | ||||
|                 ?required=${true} | ||||
|                 name="authorizationFlow" | ||||
|             > | ||||
|                 ${until( | ||||
|                     tenant().then((t) => { | ||||
|                         return html` | ||||
|                 <ak-search-select | ||||
|                     .fetchObjects=${async (query?: string): Promise<Flow[]> => { | ||||
|                         const args: FlowsInstancesListRequest = { | ||||
|                             ordering: "slug", | ||||
|                                         designation: | ||||
|                                             FlowsInstancesListDesignationEnum.Authentication, | ||||
|                             designation: FlowsInstancesListDesignationEnum.Authentication, | ||||
|                         }; | ||||
|                         if (query !== undefined) { | ||||
|                             args.search = query; | ||||
|                         } | ||||
|                                     const flows = await new FlowsApi( | ||||
|                                         DEFAULT_CONFIG, | ||||
|                                     ).flowsInstancesList(args); | ||||
|                         const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args); | ||||
|                         return flows.results; | ||||
|                     }} | ||||
|                     .renderElement=${(flow: Flow): string => { | ||||
|                                     return flow.name; | ||||
|                         return RenderFlowOption(flow); | ||||
|                     }} | ||||
|                     .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                         return html`${flow.slug}`; | ||||
| @ -102,7 +101,7 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> { | ||||
|                         return flow?.pk; | ||||
|                     }} | ||||
|                     .selected=${(flow: Flow): boolean => { | ||||
|                                     let selected = flow.pk === t.flowAuthentication; | ||||
|                         let selected = flow.pk === this.tenant?.flowAuthentication; | ||||
|                         if (this.instance?.authorizationFlow === flow.pk) { | ||||
|                             selected = true; | ||||
|                         } | ||||
| @ -110,10 +109,6 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> { | ||||
|                     }} | ||||
|                 > | ||||
|                 </ak-search-select> | ||||
|                         `; | ||||
|                     }), | ||||
|                     html`<option>${t`Loading...`}</option>`, | ||||
|                 )} | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Flow used for users to authenticate. Currently only identification and password stages are supported.`} | ||||
|                 </p> | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first, randomString } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| @ -96,7 +97,7 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> { | ||||
|                         return flows.results; | ||||
|                     }} | ||||
|                     .renderElement=${(flow: Flow): string => { | ||||
|                         return flow.slug; | ||||
|                         return RenderFlowOption(flow); | ||||
|                     }} | ||||
|                     .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                         return html`${flow.name}`; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| @ -318,7 +319,7 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> { | ||||
|                         return flows.results; | ||||
|                     }} | ||||
|                     .renderElement=${(flow: Flow): string => { | ||||
|                         return flow.slug; | ||||
|                         return RenderFlowOption(flow); | ||||
|                     }} | ||||
|                     .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                         return html`${flow.name}`; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| @ -88,7 +89,7 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> { | ||||
|                         return flows.results; | ||||
|                     }} | ||||
|                     .renderElement=${(flow: Flow): string => { | ||||
|                         return flow.slug; | ||||
|                         return RenderFlowOption(flow); | ||||
|                     }} | ||||
|                     .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                         return html`${flow.name}`; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { SentryIgnoredError } from "@goauthentik/common/errors"; | ||||
| import { Form } from "@goauthentik/elements/forms/Form"; | ||||
| @ -59,7 +60,7 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> { | ||||
|                         return flows.results; | ||||
|                     }} | ||||
|                     .renderElement=${(flow: Flow): string => { | ||||
|                         return flow.slug; | ||||
|                         return RenderFlowOption(flow); | ||||
|                     }} | ||||
|                     .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                         return html`${flow.name}`; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"; | ||||
| import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| @ -431,7 +432,7 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> { | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
| @ -477,7 +478,7 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> { | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"; | ||||
| import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; | ||||
| import { PlexAPIClient, PlexResource, popupCenterScreen } from "@goauthentik/common/helpers/plex"; | ||||
| @ -364,7 +365,7 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> { | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
| @ -410,7 +411,7 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> { | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"; | ||||
| import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| @ -496,7 +497,7 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> { | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
| @ -540,7 +541,7 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> { | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
| @ -586,7 +587,7 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> { | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| @ -146,7 +147,7 @@ export class AuthenticatorDuoStageForm extends ModelForm<AuthenticatorDuoStage, | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| @ -292,7 +293,7 @@ export class AuthenticatorSMSStageForm extends ModelForm<AuthenticatorSMSStage, | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| @ -93,7 +94,7 @@ export class AuthenticatorStaticStageForm extends ModelForm<AuthenticatorStaticS | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| @ -98,7 +99,7 @@ export class AuthenticatorTOTPStageForm extends ModelForm<AuthenticatorTOTPStage | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
| @ -162,7 +163,7 @@ export class AuthenticateWebAuthnStageForm extends ModelForm<AuthenticateWebAuth | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first, groupBy } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| @ -265,7 +266,7 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { dateTimeLocal, first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| @ -88,7 +89,7 @@ export class InvitationForm extends ModelForm<Invitation, string> { | ||||
|                         return flows.results; | ||||
|                     }} | ||||
|                     .renderElement=${(flow: Flow): string => { | ||||
|                         return flow.slug; | ||||
|                         return RenderFlowOption(flow); | ||||
|                     }} | ||||
|                     .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                         return html`${flow.name}`; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| @ -136,7 +137,7 @@ export class PasswordStageForm extends ModelForm<PasswordStage, string> { | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
|  | ||||
| @ -77,7 +77,7 @@ export class PromptStageForm extends ModelForm<PromptStage, string> { | ||||
|                                                 value=${ifDefined(prompt.pk)} | ||||
|                                                 ?selected=${selected} | ||||
|                                             > | ||||
|                                                 ${t`${prompt.fieldKey} ("${prompt.label}", of type ${prompt.type})`} | ||||
|                                                 ${t`${prompt.name} ("${prompt.fieldKey}", of type ${prompt.type})`} | ||||
|                                             </option>`; | ||||
|                                         }); | ||||
|                                     }), | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| @ -165,7 +166,7 @@ export class TenantForm extends ModelForm<Tenant, string> { | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
| @ -202,7 +203,7 @@ export class TenantForm extends ModelForm<Tenant, string> { | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
| @ -237,7 +238,7 @@ export class TenantForm extends ModelForm<Tenant, string> { | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
| @ -274,7 +275,7 @@ export class TenantForm extends ModelForm<Tenant, string> { | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
| @ -312,7 +313,7 @@ export class TenantForm extends ModelForm<Tenant, string> { | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
| @ -347,7 +348,7 @@ export class TenantForm extends ModelForm<Tenant, string> { | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(flow: Flow): string => { | ||||
|                                 return flow.slug; | ||||
|                                 return RenderFlowOption(flow); | ||||
|                             }} | ||||
|                             .renderDescription=${(flow: Flow): TemplateResult => { | ||||
|                                 return html`${flow.name}`; | ||||
|  | ||||
| @ -7,19 +7,9 @@ import { EVENT_REFRESH, VERSION } from "@goauthentik/common/constants"; | ||||
| import { globalAK } from "@goauthentik/common/global"; | ||||
| import { activateLocale } from "@goauthentik/common/ui/locale"; | ||||
|  | ||||
| import { | ||||
|     Config, | ||||
|     ConfigFromJSON, | ||||
|     Configuration, | ||||
|     CoreApi, | ||||
|     CurrentTenant, | ||||
|     CurrentTenantFromJSON, | ||||
|     RootApi, | ||||
| } from "@goauthentik/api"; | ||||
| import { Config, Configuration, CoreApi, CurrentTenant, RootApi } from "@goauthentik/api"; | ||||
|  | ||||
| let globalConfigPromise: Promise<Config> | undefined = Promise.resolve( | ||||
|     ConfigFromJSON(globalAK()?.config), | ||||
| ); | ||||
| let globalConfigPromise: Promise<Config> | undefined = Promise.resolve(globalAK().config); | ||||
| export function config(): Promise<Config> { | ||||
|     if (!globalConfigPromise) { | ||||
|         globalConfigPromise = new RootApi(DEFAULT_CONFIG).rootConfigRetrieve(); | ||||
| @ -52,9 +42,7 @@ export function tenantSetLocale(tenant: CurrentTenant) { | ||||
|     activateLocale(tenant.defaultLocale); | ||||
| } | ||||
|  | ||||
| let globalTenantPromise: Promise<CurrentTenant> | undefined = Promise.resolve( | ||||
|     CurrentTenantFromJSON(globalAK()?.tenant), | ||||
| ); | ||||
| let globalTenantPromise: Promise<CurrentTenant> | undefined = Promise.resolve(globalAK().tenant); | ||||
| export function tenant(): Promise<CurrentTenant> { | ||||
|     if (!globalTenantPromise) { | ||||
|         globalTenantPromise = new CoreApi(DEFAULT_CONFIG) | ||||
| @ -82,7 +70,7 @@ export const DEFAULT_CONFIG = new Configuration({ | ||||
|     middleware: [ | ||||
|         new CSRFMiddleware(), | ||||
|         new EventMiddleware(), | ||||
|         new LoggingMiddleware(CurrentTenantFromJSON(globalAK()?.tenant)), | ||||
|         new LoggingMiddleware(globalAK().tenant), | ||||
|     ], | ||||
| }); | ||||
|  | ||||
|  | ||||
| @ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success"; | ||||
| export const ERROR_CLASS = "pf-m-danger"; | ||||
| export const PROGRESS_CLASS = "pf-m-in-progress"; | ||||
| export const CURRENT_CLASS = "pf-m-current"; | ||||
| export const VERSION = "2023.3.0"; | ||||
| export const VERSION = "2023.3.1"; | ||||
| export const TITLE_DEFAULT = "authentik"; | ||||
| export const ROUTE_SEPARATOR = ";"; | ||||
|  | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import { Config, CurrentTenant } from "@goauthentik/api"; | ||||
| import { Config, ConfigFromJSON, CurrentTenant, CurrentTenantFromJSON } from "@goauthentik/api"; | ||||
|  | ||||
| export interface GlobalAuthentik { | ||||
|     _converted?: boolean; | ||||
|     locale?: string; | ||||
|     flow?: { | ||||
|         layout: string; | ||||
| @ -13,11 +14,17 @@ export interface GlobalAuthentik { | ||||
| } | ||||
|  | ||||
| export interface AuthentikWindow { | ||||
|     authentik?: GlobalAuthentik; | ||||
|     authentik: GlobalAuthentik; | ||||
| } | ||||
|  | ||||
| export function globalAK(): GlobalAuthentik | undefined { | ||||
|     return (window as unknown as AuthentikWindow).authentik; | ||||
| export function globalAK(): GlobalAuthentik { | ||||
|     const ak = (window as unknown as AuthentikWindow).authentik; | ||||
|     if (ak && !ak._converted) { | ||||
|         ak._converted = true; | ||||
|         ak.tenant = CurrentTenantFromJSON(ak.tenant); | ||||
|         ak.config = ConfigFromJSON(ak.config); | ||||
|     } | ||||
|     return ak; | ||||
| } | ||||
|  | ||||
| export function docLink(path: string): string { | ||||
|  | ||||
| @ -172,6 +172,6 @@ export class Interface extends AKElement { | ||||
|  | ||||
|     async getTheme(): Promise<UiThemeEnum> { | ||||
|         const config = await uiConfig(); | ||||
|         return config.theme.base; | ||||
|         return config.theme?.base || UiThemeEnum.Automatic; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -5,9 +5,6 @@ import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect"; | ||||
| import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; | ||||
| import "@polymer/iron-form/iron-form"; | ||||
| import { IronFormElement } from "@polymer/iron-form/iron-form"; | ||||
| import "@polymer/paper-input/paper-input"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| @ -110,63 +107,76 @@ export class Form<T> extends AKElement { | ||||
|      * Reset the inner iron-form | ||||
|      */ | ||||
|     resetForm(): void { | ||||
|         const ironForm = this.shadowRoot?.querySelector("iron-form"); | ||||
|         ironForm?.reset(); | ||||
|         const form = this.shadowRoot?.querySelector<HTMLFormElement>("form"); | ||||
|         form?.reset(); | ||||
|     } | ||||
|  | ||||
|     getFormFiles(): { [key: string]: File } { | ||||
|         const ironForm = this.shadowRoot?.querySelector("iron-form"); | ||||
|         const files: { [key: string]: File } = {}; | ||||
|         if (!ironForm) { | ||||
|             return files; | ||||
|         } | ||||
|         const elements = ironForm._getSubmittableElements(); | ||||
|         const elements = | ||||
|             this.shadowRoot?.querySelectorAll<HorizontalFormElement>( | ||||
|                 "ak-form-element-horizontal", | ||||
|             ) || []; | ||||
|         for (let i = 0; i < elements.length; i++) { | ||||
|             const element = elements[i] as HTMLInputElement; | ||||
|             if (element.tagName.toLowerCase() === "input" && element.type === "file") { | ||||
|                 if ((element.files || []).length < 1) { | ||||
|             const element = elements[i]; | ||||
|             element.requestUpdate(); | ||||
|             const inputElement = element.querySelector<HTMLInputElement>("[name]"); | ||||
|             if (!inputElement) { | ||||
|                 continue; | ||||
|             } | ||||
|                 files[element.name] = (element.files || [])[0]; | ||||
|             if (inputElement.tagName.toLowerCase() === "input" && inputElement.type === "file") { | ||||
|                 if ((inputElement.files || []).length < 1) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 files[element.name] = (inputElement.files || [])[0]; | ||||
|             } | ||||
|         } | ||||
|         return files; | ||||
|     } | ||||
|  | ||||
|     serializeForm(): T | undefined { | ||||
|         const form = this.shadowRoot?.querySelector<IronFormElement>("iron-form"); | ||||
|         if (!form) { | ||||
|             console.warn("authentik/forms: failed to find iron-form"); | ||||
|             return; | ||||
|         } | ||||
|         const elements: HTMLInputElement[] = form._getSubmittableElements(); | ||||
|         const elements = | ||||
|             this.shadowRoot?.querySelectorAll<HorizontalFormElement>( | ||||
|                 "ak-form-element-horizontal", | ||||
|             ) || []; | ||||
|         const json: { [key: string]: unknown } = {}; | ||||
|         elements.forEach((element) => { | ||||
|             const values = form._serializeElementValues(element); | ||||
|             if (element.hidden) { | ||||
|             element.requestUpdate(); | ||||
|             const inputElement = element.querySelector<HTMLInputElement>("[name]"); | ||||
|             if (element.hidden || !inputElement) { | ||||
|                 return; | ||||
|             } | ||||
|             if (element.tagName.toLowerCase() === "select" && "multiple" in element.attributes) { | ||||
|                 json[element.name] = values; | ||||
|             } else if (element.tagName.toLowerCase() === "input" && element.type === "date") { | ||||
|                 json[element.name] = element.valueAsDate; | ||||
|             } else if ( | ||||
|                 element.tagName.toLowerCase() === "input" && | ||||
|                 element.type === "datetime-local" | ||||
|             if ( | ||||
|                 inputElement.tagName.toLowerCase() === "select" && | ||||
|                 "multiple" in inputElement.attributes | ||||
|             ) { | ||||
|                 json[element.name] = new Date(element.valueAsNumber); | ||||
|                 const selectElement = inputElement as unknown as HTMLSelectElement; | ||||
|                 json[element.name] = Array.from(selectElement.selectedOptions).map((v) => v.value); | ||||
|             } else if ( | ||||
|                 element.tagName.toLowerCase() === "input" && | ||||
|                 "type" in element.dataset && | ||||
|                 element.dataset["type"] === "datetime-local" | ||||
|                 inputElement.tagName.toLowerCase() === "input" && | ||||
|                 inputElement.type === "date" | ||||
|             ) { | ||||
|                 json[element.name] = inputElement.valueAsDate; | ||||
|             } else if ( | ||||
|                 inputElement.tagName.toLowerCase() === "input" && | ||||
|                 inputElement.type === "datetime-local" | ||||
|             ) { | ||||
|                 json[element.name] = new Date(inputElement.valueAsNumber); | ||||
|             } else if ( | ||||
|                 inputElement.tagName.toLowerCase() === "input" && | ||||
|                 "type" in inputElement.dataset && | ||||
|                 inputElement.dataset["type"] === "datetime-local" | ||||
|             ) { | ||||
|                 // Workaround for Firefox <93, since 92 and older don't support | ||||
|                 // datetime-local fields | ||||
|                 json[element.name] = new Date(element.value); | ||||
|             } else if (element.tagName.toLowerCase() === "input" && element.type === "checkbox") { | ||||
|                 json[element.name] = element.checked; | ||||
|             } else if (element.tagName.toLowerCase() === "ak-search-select") { | ||||
|                 const select = element as unknown as SearchSelect<unknown>; | ||||
|                 json[element.name] = new Date(inputElement.value); | ||||
|             } else if ( | ||||
|                 inputElement.tagName.toLowerCase() === "input" && | ||||
|                 inputElement.type === "checkbox" | ||||
|             ) { | ||||
|                 json[element.name] = inputElement.checked; | ||||
|             } else if (inputElement.tagName.toLowerCase() === "ak-search-select") { | ||||
|                 const select = inputElement as unknown as SearchSelect<unknown>; | ||||
|                 let value: unknown; | ||||
|                 try { | ||||
|                     value = select.toForm(); | ||||
| @ -179,9 +189,7 @@ export class Form<T> extends AKElement { | ||||
|                 } | ||||
|                 json[element.name] = value; | ||||
|             } else { | ||||
|                 for (let v = 0; v < values.length; v++) { | ||||
|                     this.serializeFieldRecursive(element, values[v], json); | ||||
|                 } | ||||
|                 this.serializeFieldRecursive(inputElement, inputElement.value, json); | ||||
|             } | ||||
|         }); | ||||
|         return json as unknown as T; | ||||
| @ -213,11 +221,6 @@ export class Form<T> extends AKElement { | ||||
|         if (!data) { | ||||
|             return; | ||||
|         } | ||||
|         const form = this.shadowRoot?.querySelector<IronFormElement>("iron-form"); | ||||
|         if (!form) { | ||||
|             console.warn("authentik/forms: failed to find iron-form"); | ||||
|             return; | ||||
|         } | ||||
|         return this.send(data) | ||||
|             .then((r) => { | ||||
|                 showMessage({ | ||||
| @ -244,8 +247,12 @@ export class Form<T> extends AKElement { | ||||
|                         throw errorMessage; | ||||
|                     } | ||||
|                     // assign all input-related errors to their elements | ||||
|                     const elements: HorizontalFormElement[] = form._getSubmittableElements(); | ||||
|                     const elements = | ||||
|                         this.shadowRoot?.querySelectorAll<HorizontalFormElement>( | ||||
|                             "ak-form-element-horizontal", | ||||
|                         ) || []; | ||||
|                     elements.forEach((element) => { | ||||
|                         element.requestUpdate(); | ||||
|                         const elementName = element.name; | ||||
|                         if (!elementName) return; | ||||
|                         if (camelToSnake(elementName) in errorMessage) { | ||||
| @ -296,13 +303,7 @@ export class Form<T> extends AKElement { | ||||
|     } | ||||
|  | ||||
|     renderVisible(): TemplateResult { | ||||
|         return html`<iron-form | ||||
|             @iron-form-presubmit=${(ev: Event) => { | ||||
|                 this.submit(ev); | ||||
|             }} | ||||
|         > | ||||
|             ${this.renderNonFieldErrors()} ${this.renderForm()} | ||||
|         </iron-form>`; | ||||
|         return html` ${this.renderNonFieldErrors()} ${this.renderForm()}`; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|  | ||||
| @ -69,6 +69,10 @@ export class HorizontalFormElement extends AKElement { | ||||
|     @property() | ||||
|     name = ""; | ||||
|  | ||||
|     firstUpdated(): void { | ||||
|         this.updated(); | ||||
|     } | ||||
|  | ||||
|     updated(): void { | ||||
|         this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => { | ||||
|             input.focus(); | ||||
| @ -89,7 +93,7 @@ export class HorizontalFormElement extends AKElement { | ||||
|                 case "ak-chip-group": | ||||
|                 case "ak-search-select": | ||||
|                 case "ak-radio": | ||||
|                     (input as HTMLInputElement).name = this.name; | ||||
|                     input.setAttribute("name", this.name); | ||||
|                     break; | ||||
|                 default: | ||||
|                     return; | ||||
| @ -108,6 +112,7 @@ export class HorizontalFormElement extends AKElement { | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         this.updated(); | ||||
|         return html`<div class="pf-c-form__group"> | ||||
|             <div class="pf-c-form__group-label"> | ||||
|                 <label class="pf-c-form__label"> | ||||
|  | ||||
| @ -70,6 +70,7 @@ export class SearchSelect<T> extends AKElement { | ||||
|     observer: IntersectionObserver; | ||||
|     dropdownUID: string; | ||||
|     dropdownContainer: HTMLDivElement; | ||||
|     isFetchingData = false; | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
| @ -103,13 +104,18 @@ export class SearchSelect<T> extends AKElement { | ||||
|     } | ||||
|  | ||||
|     updateData(): void { | ||||
|         if (this.isFetchingData) { | ||||
|             return; | ||||
|         } | ||||
|         this.isFetchingData = true; | ||||
|         this.fetchObjects(this.query).then((objects) => { | ||||
|             this.objects = objects; | ||||
|             this.objects.forEach((obj) => { | ||||
|             objects.forEach((obj) => { | ||||
|                 if (this.selected && this.selected(obj, this.objects || [])) { | ||||
|                     this.selectedObject = obj; | ||||
|                 } | ||||
|             }); | ||||
|             this.objects = objects; | ||||
|             this.isFetchingData = false; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| @ -200,9 +206,10 @@ export class SearchSelect<T> extends AKElement { | ||||
|         render( | ||||
|             html`<div | ||||
|                 class="pf-c-dropdown pf-m-expanded" | ||||
|                 ?hidden=${!this.open} | ||||
|                 style="position: fixed; inset: 0px auto auto 0px; z-index: 9999; transform: translate(${pos.x}px, ${pos.y + | ||||
|                 this.offsetHeight}px); width: ${pos.width}px;" | ||||
|                 this.offsetHeight}px); width: ${pos.width}px; ${this.open | ||||
|                     ? "" | ||||
|                     : "visibility: hidden;"}" | ||||
|             > | ||||
|                 <ul | ||||
|                     class="pf-c-dropdown__menu pf-m-static" | ||||
| @ -249,6 +256,14 @@ export class SearchSelect<T> extends AKElement { | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         this.renderMenu(); | ||||
|         let value = ""; | ||||
|         if (!this.objects) { | ||||
|             value = t`Loading...`; | ||||
|         } else if (this.selectedObject) { | ||||
|             value = this.renderElement(this.selectedObject); | ||||
|         } else if (this.blankable) { | ||||
|             value = this.emptyOption; | ||||
|         } | ||||
|         return html`<div class="pf-c-select"> | ||||
|             <div class="pf-c-select__toggle pf-m-typeahead"> | ||||
|                 <div class="pf-c-select__toggle-wrapper"> | ||||
| @ -256,6 +271,7 @@ export class SearchSelect<T> extends AKElement { | ||||
|                         class="pf-c-form-control pf-c-select__toggle-typeahead" | ||||
|                         type="text" | ||||
|                         placeholder=${this.placeholder} | ||||
|                         spellcheck="false" | ||||
|                         @input=${(ev: InputEvent) => { | ||||
|                             this.query = (ev.target as HTMLInputElement).value; | ||||
|                             this.updateData(); | ||||
| @ -285,11 +301,7 @@ export class SearchSelect<T> extends AKElement { | ||||
|                             this.open = false; | ||||
|                             this.renderMenu(); | ||||
|                         }} | ||||
|                         .value=${this.selectedObject | ||||
|                             ? this.renderElement(this.selectedObject) | ||||
|                             : this.blankable | ||||
|                             ? this.emptyOption | ||||
|                             : ""} | ||||
|                         .value=${value} | ||||
|                     /> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
| @ -68,6 +68,7 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng | ||||
|         if (this.timer) { | ||||
|             console.debug("authentik/stages/password: cleared focus timer"); | ||||
|             window.clearInterval(this.timer); | ||||
|             this.timer = undefined; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -1,5 +1,3 @@ | ||||
| // @ts-ignore | ||||
| window["polymerSkipLoadingFontRoboto"] = true; | ||||
| import "construct-style-sheets-polyfill"; | ||||
| import "@webcomponents/webcomponentsjs"; | ||||
| import "lit/polyfill-support.js"; | ||||
|  | ||||
| @ -18,7 +18,7 @@ When authenticating with a flow, you'll get an authenticated Session cookie, tha | ||||
|  | ||||
| ### API Token | ||||
|  | ||||
| Superusers can create tokens to authenticate as any user with a static key, which can optionally be expiring and auto-rotate. | ||||
| Users can create tokens to authenticate as any user with a static key, which can optionally be expiring and auto-rotate. | ||||
|  | ||||
| ### JWT Token | ||||
|  | ||||
|  | ||||
							
								
								
									
										27
									
								
								website/developer-docs/blueprints/v1/models.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								website/developer-docs/blueprints/v1/models.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| # Models | ||||
|  | ||||
| Some models behave differently and allow for access to different API fields when created via blueprint. | ||||
|  | ||||
| ### `authentik_core.token` | ||||
|  | ||||
| :::info | ||||
| Requires authentik 2023.4 | ||||
| ::: | ||||
|  | ||||
| Via the standard API, a token's key cannot be changed, it can only be rotated. This is to ensure a high entropy in it's key, and to prevent insecure data from being used. However, when provisioning tokens via a blueprint, it may be required to set a token to an existing value. | ||||
|  | ||||
| With blueprints, the field `key` can be set, to set the token's key to any value. | ||||
|  | ||||
| For example: | ||||
|  | ||||
| ```yaml | ||||
| # [...] | ||||
| - model: authentik_core.token | ||||
|   state: present | ||||
|   identifiers: | ||||
|       identifier: my-token | ||||
|   attrs: | ||||
|       key: this-should-be-a-long-value | ||||
|       user: !KeyOf my-user | ||||
|       intent: api | ||||
| ``` | ||||
| @ -16,6 +16,7 @@ module.exports = { | ||||
|                 "blueprints/v1/structure", | ||||
|                 "blueprints/v1/tags", | ||||
|                 "blueprints/v1/example", | ||||
|                 "blueprints/v1/models", | ||||
|                 "blueprints/v1/meta", | ||||
|             ], | ||||
|         }, | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	