From b285814e24686d6512ec9dc99a7c46346c476358 Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Sun, 3 Oct 2021 00:30:35 +0200
Subject: [PATCH 01/18] sources/ldap: fix logic error in Active Directory
account disabled status
Signed-off-by: Jens Langhammer
---
CONTRIBUTING.md | 2 +-
Makefile | 6 +-----
authentik/sources/ldap/sync/users.py | 2 +-
pyproject.toml | 2 +-
web/package-lock.json | 4 ++--
5 files changed, 6 insertions(+), 10 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8b7c42585..e9f4d9ea4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -117,7 +117,7 @@ This section guides you through submitting a bug report for authentik. Following
Whenever authentik encounters an error, it will be logged as an Event with the type `system_exception`. This event type has a button to directly open a pre-filled GitHub issue form.
-This form will have the full stack trace of the error that ocurred and shouldn't contain any sensitive data.
+This form will have the full stack trace of the error that occurred and shouldn't contain any sensitive data.
### Suggesting Enhancements
diff --git a/Makefile b/Makefile
index 59b6159eb..373d2bfba 100644
--- a/Makefile
+++ b/Makefile
@@ -20,11 +20,7 @@ test:
lint-fix:
isort authentik tests lifecycle
black authentik tests lifecycle
- codespell -I .github/codespell-words.txt -w authentik
- codespell -I .github/codespell-words.txt -w internal
- codespell -I .github/codespell-words.txt -w cmd
- codespell -I .github/codespell-words.txt -w web/src
- codespell -I .github/codespell-words.txt -w website/src
+ codespell -I .github/codespell-words.txt -S 'web/src/locales/**' -w authentik internal cmd web/src website/src
lint:
pyright authentik tests lifecycle
diff --git a/authentik/sources/ldap/sync/users.py b/authentik/sources/ldap/sync/users.py
index 42b6a6f60..6a8aefb27 100644
--- a/authentik/sources/ldap/sync/users.py
+++ b/authentik/sources/ldap/sync/users.py
@@ -78,6 +78,6 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
ak_user.save()
if "userAccountControl" in attributes:
uac = UserAccountControl(attributes.get("userAccountControl"))
- ak_user.is_active = not uac.ACCOUNTDISABLE
+ ak_user.is_active = UserAccountControl.ACCOUNTDISABLE in uac
ak_user.save()
return user_count
diff --git a/pyproject.toml b/pyproject.toml
index 95e1750e1..f4709f32a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -53,7 +53,7 @@ disable = [
"cyclic-import",
"protected-access",
"raise-missing-from",
- # To preverse django's translation function we need to use %-formatting
+ # To preserve django's translation function we need to use %-formatting
"consider-using-f-string",
]
diff --git a/web/package-lock.json b/web/package-lock.json
index b4d3661a2..008b49638 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -6992,7 +6992,7 @@
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/view==",
"engines": {
"node": ">=0.10.0"
}
@@ -13757,7 +13757,7 @@
"require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/view=="
},
"require-main-filename": {
"version": "2.0.0",
From fcd879034c4dcba356cc70f30cf64e6f1f0bd51f Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Sat, 2 Oct 2021 21:17:15 +0200
Subject: [PATCH 02/18] outpost/proxy: fix missing negation for internal host
ssl verification
Signed-off-by: Jens Langhammer
---
internal/outpost/proxyv2/application/mode_proxy.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/internal/outpost/proxyv2/application/mode_proxy.go b/internal/outpost/proxyv2/application/mode_proxy.go
index 14c0e1e63..6630d1687 100644
--- a/internal/outpost/proxyv2/application/mode_proxy.go
+++ b/internal/outpost/proxyv2/application/mode_proxy.go
@@ -17,7 +17,7 @@ import (
func (a *Application) getUpstreamTransport() http.RoundTripper {
return &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: *a.proxyConfig.InternalHostSslValidation},
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: !*a.proxyConfig.InternalHostSslValidation},
}
}
From 39d87841d0fbca04624ae3229747e109a830988e Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Sat, 2 Oct 2021 22:00:23 +0200
Subject: [PATCH 03/18] outposts/proxy: add new headers with unified naming
Signed-off-by: Jens Langhammer
---
authentik/providers/proxy/controllers/k8s/traefik.py | 7 +++++++
internal/outpost/proxyv2/application/mode_common.go | 9 +++++++++
2 files changed, 16 insertions(+)
diff --git a/authentik/providers/proxy/controllers/k8s/traefik.py b/authentik/providers/proxy/controllers/k8s/traefik.py
index f453a2e5b..8f3cb816a 100644
--- a/authentik/providers/proxy/controllers/k8s/traefik.py
+++ b/authentik/providers/proxy/controllers/k8s/traefik.py
@@ -109,11 +109,18 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
address=f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik",
authResponseHeaders=[
"Set-Cookie",
+ # Legacy headers, remove after 2022.1
"X-Auth-Username",
"X-Auth-Groups",
"X-Forwarded-Email",
"X-Forwarded-Preferred-Username",
"X-Forwarded-User",
+ # New headers, unique prefix
+ "X-authentik-username",
+ "X-authentik-groups",
+ "X-authentik-email",
+ "X-authentik-name",
+ "X-authentik-uid",
],
trustForwardHeader=True,
)
diff --git a/internal/outpost/proxyv2/application/mode_common.go b/internal/outpost/proxyv2/application/mode_common.go
index 64d244201..b49438e20 100644
--- a/internal/outpost/proxyv2/application/mode_common.go
+++ b/internal/outpost/proxyv2/application/mode_common.go
@@ -9,12 +9,21 @@ import (
func (a *Application) addHeaders(r *http.Request, c *Claims) {
// https://goauthentik.io/docs/providers/proxy/proxy
+
+ // Legacy headers, remove after 2022.1
r.Header.Set("X-Auth-Username", c.PreferredUsername)
r.Header.Set("X-Auth-Groups", strings.Join(c.Groups, "|"))
r.Header.Set("X-Forwarded-Email", c.Email)
r.Header.Set("X-Forwarded-Preferred-Username", c.PreferredUsername)
r.Header.Set("X-Forwarded-User", c.Sub)
+ // New headers, unique prefix
+ r.Header.Set("X-authentik-username", c.PreferredUsername)
+ r.Header.Set("X-authentik-groups", strings.Join(c.Groups, "|"))
+ r.Header.Set("X-authentik-email", c.Email)
+ r.Header.Set("X-authentik-name", c.Name)
+ r.Header.Set("X-authentik-uid", c.Sub)
+
userAttributes := c.Proxy.UserAttributes
// Attempt to set basic auth based on user's attributes
if *a.proxyConfig.BasicAuthEnabled {
From d676cf6e3fbc83aa47ed73ffb3ce25bcc749e597 Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Sat, 2 Oct 2021 22:00:37 +0200
Subject: [PATCH 04/18] outposts/proxy: show full error message when user is
authenticated
Signed-off-by: Jens Langhammer
---
internal/outpost/proxyv2/application/error.go | 7 ++++++-
internal/outpost/proxyv2/application/mode_proxy.go | 2 +-
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/internal/outpost/proxyv2/application/error.go b/internal/outpost/proxyv2/application/error.go
index 1c4f75cdf..d2f50b8e4 100644
--- a/internal/outpost/proxyv2/application/error.go
+++ b/internal/outpost/proxyv2/application/error.go
@@ -1,6 +1,7 @@
package application
import (
+ "fmt"
"html/template"
"net/http"
@@ -8,8 +9,9 @@ import (
)
// NewProxyErrorHandler creates a ProxyErrorHandler using the template given.
-func NewProxyErrorHandler(errorTemplate *template.Template) func(http.ResponseWriter, *http.Request, error) {
+func (a *Application) newProxyErrorHandler(errorTemplate *template.Template) func(http.ResponseWriter, *http.Request, error) {
return func(rw http.ResponseWriter, req *http.Request, proxyErr error) {
+ claims, _ := a.getClaims(req)
log.WithError(proxyErr).Warning("Error proxying to upstream server")
rw.WriteHeader(http.StatusBadGateway)
data := struct {
@@ -21,6 +23,9 @@ func NewProxyErrorHandler(errorTemplate *template.Template) func(http.ResponseWr
Message: "Error proxying to upstream server",
ProxyPrefix: "/akprox",
}
+ if claims != nil {
+ data.Message = fmt.Sprintf("Error proxying to upstream server: %s", proxyErr.Error())
+ }
err := errorTemplate.Execute(rw, data)
if err != nil {
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
diff --git a/internal/outpost/proxyv2/application/mode_proxy.go b/internal/outpost/proxyv2/application/mode_proxy.go
index 6630d1687..7c25f0932 100644
--- a/internal/outpost/proxyv2/application/mode_proxy.go
+++ b/internal/outpost/proxyv2/application/mode_proxy.go
@@ -29,7 +29,7 @@ func (a *Application) configureProxy() error {
}
rp := &httputil.ReverseProxy{Director: a.proxyModifyRequest(u)}
rp.Transport = ak.NewTracingTransport(context.TODO(), a.getUpstreamTransport())
- rp.ErrorHandler = NewProxyErrorHandler(templates.GetTemplates())
+ rp.ErrorHandler = a.newProxyErrorHandler(templates.GetTemplates())
rp.ModifyResponse = a.proxyModifyResponse
a.mux.PathPrefix("/").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
claims, err := a.getClaims(r)
From c296e1214c8071296e8a936db19b8598fc65af90 Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Sun, 3 Oct 2021 00:51:52 +0200
Subject: [PATCH 05/18] web: fix package lock
Signed-off-by: Jens Langhammer
---
authentik/sources/ldap/sync/users.py | 2 +-
authentik/sources/ldap/tests/mock_ad.py | 5 ++++-
web/package-lock.json | 4 ++--
3 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/authentik/sources/ldap/sync/users.py b/authentik/sources/ldap/sync/users.py
index 6a8aefb27..e48485af8 100644
--- a/authentik/sources/ldap/sync/users.py
+++ b/authentik/sources/ldap/sync/users.py
@@ -78,6 +78,6 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
ak_user.save()
if "userAccountControl" in attributes:
uac = UserAccountControl(attributes.get("userAccountControl"))
- ak_user.is_active = UserAccountControl.ACCOUNTDISABLE in uac
+ ak_user.is_active = UserAccountControl.ACCOUNTDISABLE not in uac
ak_user.save()
return user_count
diff --git a/authentik/sources/ldap/tests/mock_ad.py b/authentik/sources/ldap/tests/mock_ad.py
index 3418e52af..0858fe86a 100644
--- a/authentik/sources/ldap/tests/mock_ad.py
+++ b/authentik/sources/ldap/tests/mock_ad.py
@@ -2,6 +2,8 @@
from ldap3 import MOCK_SYNC, OFFLINE_AD_2012_R2, Connection, Server
+from authentik.sources.ldap.sync.vendor.ad import UserAccountControl
+
def mock_ad_connection(password: str) -> Connection:
"""Create mock AD connection"""
@@ -54,7 +56,8 @@ def mock_ad_connection(password: str) -> Connection:
"objectSid": "user0",
"objectClass": "person",
"distinguishedName": "cn=user0,ou=users,dc=goauthentik,dc=io",
- "userAccountControl": 66050,
+ "userAccountControl": UserAccountControl.ACCOUNTDISABLE
+ + UserAccountControl.NORMAL_ACCOUNT,
},
)
# User without SID
diff --git a/web/package-lock.json b/web/package-lock.json
index 008b49638..b4d3661a2 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -6992,7 +6992,7 @@
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/view==",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"engines": {
"node": ">=0.10.0"
}
@@ -13757,7 +13757,7 @@
"require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/view=="
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="
},
"require-main-filename": {
"version": "2.0.0",
From d28fcca34471c38dc1743af65cbf61b85c6edd11 Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Sun, 3 Oct 2021 19:09:52 +0200
Subject: [PATCH 06/18] outposts: check ports of deployment in kubernetes
outpost controller
Signed-off-by: Jens Langhammer
---
authentik/outposts/controllers/k8s/deployment.py | 7 ++++++-
authentik/outposts/controllers/k8s/service.py | 9 +++------
authentik/outposts/controllers/k8s/utils.py | 12 ++++++++++++
3 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/authentik/outposts/controllers/k8s/deployment.py b/authentik/outposts/controllers/k8s/deployment.py
index 9c9aefc70..47353f94c 100644
--- a/authentik/outposts/controllers/k8s/deployment.py
+++ b/authentik/outposts/controllers/k8s/deployment.py
@@ -18,6 +18,7 @@ from kubernetes.client import (
from authentik.outposts.controllers.base import FIELD_MANAGER
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate
+from authentik.outposts.controllers.k8s.utils import compare_ports
from authentik.outposts.models import Outpost
if TYPE_CHECKING:
@@ -35,7 +36,10 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
self.outpost = self.controller.outpost
def reconcile(self, current: V1Deployment, reference: V1Deployment):
- super().reconcile(current, reference)
+ compare_ports(
+ current.spec.template.spec.containers[0].ports,
+ reference.spec.template.spec.containers[0].ports,
+ )
if current.spec.replicas != reference.spec.replicas:
raise NeedsUpdate()
if (
@@ -43,6 +47,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
!= reference.spec.template.spec.containers[0].image
):
raise NeedsUpdate()
+ super().reconcile(current, reference)
def get_pod_meta(self) -> dict[str, str]:
"""Get common object metadata"""
diff --git a/authentik/outposts/controllers/k8s/service.py b/authentik/outposts/controllers/k8s/service.py
index 12b50c222..d84d64ea6 100644
--- a/authentik/outposts/controllers/k8s/service.py
+++ b/authentik/outposts/controllers/k8s/service.py
@@ -4,8 +4,9 @@ from typing import TYPE_CHECKING
from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
from authentik.outposts.controllers.base import FIELD_MANAGER
-from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsRecreate
+from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
+from authentik.outposts.controllers.k8s.utils import compare_ports
if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController
@@ -19,11 +20,7 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
self.api = CoreV1Api(controller.client)
def reconcile(self, current: V1Service, reference: V1Service):
- if len(current.spec.ports) != len(reference.spec.ports):
- raise NeedsRecreate()
- for port in reference.spec.ports:
- if port not in current.spec.ports:
- raise NeedsRecreate()
+ compare_ports(current.spec, reference.spec)
# run the base reconcile last, as that will probably raise NeedsUpdate
# after an authentik update. However the ports might have also changed during
# the update, so this causes the service to be re-created with higher
diff --git a/authentik/outposts/controllers/k8s/utils.py b/authentik/outposts/controllers/k8s/utils.py
index ed9663064..ad158e7e8 100644
--- a/authentik/outposts/controllers/k8s/utils.py
+++ b/authentik/outposts/controllers/k8s/utils.py
@@ -1,8 +1,11 @@
"""k8s utils"""
from pathlib import Path
+from kubernetes.client.models.v1_container_port import V1ContainerPort
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
+from authentik.outposts.controllers.k8s.base import NeedsRecreate
+
def get_namespace() -> str:
"""Get the namespace if we're running in a pod, otherwise default to default"""
@@ -11,3 +14,12 @@ def get_namespace() -> str:
with open(path, "r", encoding="utf8") as _namespace_file:
return _namespace_file.read()
return "default"
+
+
+def compare_ports(current: list[V1ContainerPort], reference: list[V1ContainerPort]):
+ """Compare ports of a list"""
+ if len(current) != len(reference):
+ raise NeedsRecreate()
+ for port in reference:
+ if port not in current:
+ raise NeedsRecreate()
From e31a3307b5be6eb0f14f7698edfadcf0905ea52b Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Sun, 3 Oct 2021 19:14:27 +0200
Subject: [PATCH 07/18] providers/proxy: always check ingress secret in
kubernetes controller
Signed-off-by: Jens Langhammer
---
authentik/providers/proxy/controllers/k8s/ingress.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/authentik/providers/proxy/controllers/k8s/ingress.py b/authentik/providers/proxy/controllers/k8s/ingress.py
index 07d3d8c08..601cf9c1b 100644
--- a/authentik/providers/proxy/controllers/k8s/ingress.py
+++ b/authentik/providers/proxy/controllers/k8s/ingress.py
@@ -63,8 +63,15 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
have_hosts_tls = []
if current.spec.tls:
for tls_config in current.spec.tls:
- if tls_config and tls_config.hosts:
+ if not tls_config:
+ continue
+ if tls_config.hosts:
have_hosts_tls += tls_config.hosts
+ if (
+ tls_config.secret_name
+ != self.controller.outpost.config.kubernetes_ingress_secret_name
+ ):
+ raise NeedsUpdate()
have_hosts_tls.sort()
if have_hosts != expected_hosts:
From 45f99fbaf0b11e32a1d1dd995816c4480d54a907 Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Sun, 3 Oct 2021 19:25:18 +0200
Subject: [PATCH 08/18] outposts: fix circular import in kubernetes controller
Signed-off-by: Jens Langhammer
---
authentik/outposts/controllers/k8s/base.py | 14 +-------------
authentik/outposts/controllers/k8s/deployment.py | 3 ++-
authentik/outposts/controllers/k8s/secret.py | 3 ++-
authentik/outposts/controllers/k8s/triggers.py | 14 ++++++++++++++
authentik/outposts/controllers/k8s/utils.py | 2 +-
.../providers/proxy/controllers/k8s/ingress.py | 7 ++-----
.../providers/proxy/controllers/k8s/traefik.py | 3 ++-
tests/integration/test_outpost_kubernetes.py | 2 +-
8 files changed, 25 insertions(+), 23 deletions(-)
create mode 100644 authentik/outposts/controllers/k8s/triggers.py
diff --git a/authentik/outposts/controllers/k8s/base.py b/authentik/outposts/controllers/k8s/base.py
index d06a72b53..de7c244a2 100644
--- a/authentik/outposts/controllers/k8s/base.py
+++ b/authentik/outposts/controllers/k8s/base.py
@@ -10,7 +10,7 @@ from structlog.stdlib import get_logger
from urllib3.exceptions import HTTPError
from authentik import __version__
-from authentik.lib.sentry import SentryIgnoredException
+from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate
from authentik.outposts.managed import MANAGED_OUTPOST
if TYPE_CHECKING:
@@ -20,18 +20,6 @@ if TYPE_CHECKING:
T = TypeVar("T", V1Pod, V1Deployment)
-class ReconcileTrigger(SentryIgnoredException):
- """Base trigger raised by child classes to notify us"""
-
-
-class NeedsRecreate(ReconcileTrigger):
- """Exception to trigger a complete recreate of the Kubernetes Object"""
-
-
-class NeedsUpdate(ReconcileTrigger):
- """Exception to trigger an update to the Kubernetes Object"""
-
-
class KubernetesObjectReconciler(Generic[T]):
"""Base Kubernetes Reconciler, handles the basic logic."""
diff --git a/authentik/outposts/controllers/k8s/deployment.py b/authentik/outposts/controllers/k8s/deployment.py
index 47353f94c..5d3d1a74c 100644
--- a/authentik/outposts/controllers/k8s/deployment.py
+++ b/authentik/outposts/controllers/k8s/deployment.py
@@ -17,7 +17,8 @@ from kubernetes.client import (
)
from authentik.outposts.controllers.base import FIELD_MANAGER
-from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate
+from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
+from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
from authentik.outposts.controllers.k8s.utils import compare_ports
from authentik.outposts.models import Outpost
diff --git a/authentik/outposts/controllers/k8s/secret.py b/authentik/outposts/controllers/k8s/secret.py
index d7cb8c03c..fc8dc8296 100644
--- a/authentik/outposts/controllers/k8s/secret.py
+++ b/authentik/outposts/controllers/k8s/secret.py
@@ -5,7 +5,8 @@ from typing import TYPE_CHECKING
from kubernetes.client import CoreV1Api, V1Secret
from authentik.outposts.controllers.base import FIELD_MANAGER
-from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate
+from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
+from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController
diff --git a/authentik/outposts/controllers/k8s/triggers.py b/authentik/outposts/controllers/k8s/triggers.py
new file mode 100644
index 000000000..284acd3bc
--- /dev/null
+++ b/authentik/outposts/controllers/k8s/triggers.py
@@ -0,0 +1,14 @@
+"""exceptions used by the kubernetes reconciler to trigger updates"""
+from authentik.lib.sentry import SentryIgnoredException
+
+
+class ReconcileTrigger(SentryIgnoredException):
+ """Base trigger raised by child classes to notify us"""
+
+
+class NeedsRecreate(ReconcileTrigger):
+ """Exception to trigger a complete recreate of the Kubernetes Object"""
+
+
+class NeedsUpdate(ReconcileTrigger):
+ """Exception to trigger an update to the Kubernetes Object"""
diff --git a/authentik/outposts/controllers/k8s/utils.py b/authentik/outposts/controllers/k8s/utils.py
index ad158e7e8..c44200555 100644
--- a/authentik/outposts/controllers/k8s/utils.py
+++ b/authentik/outposts/controllers/k8s/utils.py
@@ -4,7 +4,7 @@ from pathlib import Path
from kubernetes.client.models.v1_container_port import V1ContainerPort
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
-from authentik.outposts.controllers.k8s.base import NeedsRecreate
+from authentik.outposts.controllers.k8s.triggers import NeedsRecreate
def get_namespace() -> str:
diff --git a/authentik/providers/proxy/controllers/k8s/ingress.py b/authentik/providers/proxy/controllers/k8s/ingress.py
index 601cf9c1b..14eaa0d6e 100644
--- a/authentik/providers/proxy/controllers/k8s/ingress.py
+++ b/authentik/providers/proxy/controllers/k8s/ingress.py
@@ -14,11 +14,8 @@ from kubernetes.client import (
from kubernetes.client.models.networking_v1beta1_ingress_rule import NetworkingV1beta1IngressRule
from authentik.outposts.controllers.base import FIELD_MANAGER
-from authentik.outposts.controllers.k8s.base import (
- KubernetesObjectReconciler,
- NeedsRecreate,
- NeedsUpdate,
-)
+from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
+from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
if TYPE_CHECKING:
diff --git a/authentik/providers/proxy/controllers/k8s/traefik.py b/authentik/providers/proxy/controllers/k8s/traefik.py
index 8f3cb816a..623c343a8 100644
--- a/authentik/providers/proxy/controllers/k8s/traefik.py
+++ b/authentik/providers/proxy/controllers/k8s/traefik.py
@@ -6,7 +6,8 @@ from dacite import from_dict
from kubernetes.client import ApiextensionsV1Api, CustomObjectsApi
from authentik.outposts.controllers.base import FIELD_MANAGER
-from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate
+from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
+from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
if TYPE_CHECKING:
diff --git a/tests/integration/test_outpost_kubernetes.py b/tests/integration/test_outpost_kubernetes.py
index a82261854..40d96e833 100644
--- a/tests/integration/test_outpost_kubernetes.py
+++ b/tests/integration/test_outpost_kubernetes.py
@@ -3,8 +3,8 @@ from django.test import TestCase
from authentik.flows.models import Flow
from authentik.lib.config import CONFIG
-from authentik.outposts.controllers.k8s.base import NeedsUpdate
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
+from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
from authentik.outposts.controllers.kubernetes import KubernetesController
from authentik.outposts.models import KubernetesServiceConnection, Outpost, OutpostType
from authentik.outposts.tasks import outpost_local_connection
From 3634bf4629147c0ac6e062e33eefab6646a42837 Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Sun, 3 Oct 2021 22:54:07 +0200
Subject: [PATCH 09/18] tests/integration: fix tests failing due to incorrect
comparison
Signed-off-by: Jens Langhammer
---
authentik/outposts/controllers/k8s/service.py | 2 +-
tests/integration/test_outpost_kubernetes.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/authentik/outposts/controllers/k8s/service.py b/authentik/outposts/controllers/k8s/service.py
index d84d64ea6..0d04ffb89 100644
--- a/authentik/outposts/controllers/k8s/service.py
+++ b/authentik/outposts/controllers/k8s/service.py
@@ -20,7 +20,7 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
self.api = CoreV1Api(controller.client)
def reconcile(self, current: V1Service, reference: V1Service):
- compare_ports(current.spec, reference.spec)
+ compare_ports(current.spec.ports, reference.spec.ports)
# run the base reconcile last, as that will probably raise NeedsUpdate
# after an authentik update. However the ports might have also changed during
# the update, so this causes the service to be re-created with higher
diff --git a/tests/integration/test_outpost_kubernetes.py b/tests/integration/test_outpost_kubernetes.py
index 40d96e833..c9000ede8 100644
--- a/tests/integration/test_outpost_kubernetes.py
+++ b/tests/integration/test_outpost_kubernetes.py
@@ -5,9 +5,9 @@ from authentik.flows.models import Flow
from authentik.lib.config import CONFIG
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
-from authentik.outposts.controllers.kubernetes import KubernetesController
from authentik.outposts.models import KubernetesServiceConnection, Outpost, OutpostType
from authentik.outposts.tasks import outpost_local_connection
+from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
from authentik.providers.proxy.models import ProxyProvider
@@ -35,7 +35,7 @@ class OutpostKubernetesTests(TestCase):
def test_deployment_reconciler(self):
"""test that deployment requires update"""
- controller = KubernetesController(self.outpost, self.service_connection)
+ controller = ProxyKubernetesController(self.outpost, self.service_connection)
deployment_reconciler = DeploymentReconciler(controller)
self.assertIsNotNone(deployment_reconciler.retrieve())
From c0329140920d121ddaeca1c23db56ee657dfab90 Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Mon, 4 Oct 2021 09:41:16 +0200
Subject: [PATCH 10/18] web/admin: fix search group label
Signed-off-by: Jens Langhammer
---
web/src/pages/providers/ldap/LDAPProviderForm.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/src/pages/providers/ldap/LDAPProviderForm.ts b/web/src/pages/providers/ldap/LDAPProviderForm.ts
index dc3f859d8..ec6aec83e 100644
--- a/web/src/pages/providers/ldap/LDAPProviderForm.ts
+++ b/web/src/pages/providers/ldap/LDAPProviderForm.ts
@@ -95,7 +95,7 @@ export class LDAPProviderFormPage extends ModelForm {
${t`Flow used for users to authenticate. Currently only identification and password stages are supported.`}
-
+
Date: Mon, 4 Oct 2021 17:51:27 +0200
Subject: [PATCH 12/18] events: add missing migration
Signed-off-by: Jens Langhammer
---
...alter_notificationtransport_webhook_url.py | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
create mode 100644 authentik/events/migrations/0019_alter_notificationtransport_webhook_url.py
diff --git a/authentik/events/migrations/0019_alter_notificationtransport_webhook_url.py b/authentik/events/migrations/0019_alter_notificationtransport_webhook_url.py
new file mode 100644
index 000000000..4738d3e0a
--- /dev/null
+++ b/authentik/events/migrations/0019_alter_notificationtransport_webhook_url.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.2.7 on 2021-10-04 15:31
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_events", "0018_auto_20210911_2217"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="notificationtransport",
+ name="webhook_url",
+ field=models.TextField(blank=True, validators=[django.core.validators.URLValidator()]),
+ ),
+ ]
From 83991c743ec701463cc05ca5372ad878ab6e466e Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Mon, 4 Oct 2021 18:03:08 +0200
Subject: [PATCH 13/18] lifecycle: switch to h11 uvicorn worker for now
Signed-off-by: Jens Langhammer
---
lifecycle/gunicorn.conf.py | 28 +---------------------------
1 file changed, 1 insertion(+), 27 deletions(-)
diff --git a/lifecycle/gunicorn.conf.py b/lifecycle/gunicorn.conf.py
index 37f9ea9d8..76391a553 100644
--- a/lifecycle/gunicorn.conf.py
+++ b/lifecycle/gunicorn.conf.py
@@ -1,7 +1,6 @@
"""Gunicorn config"""
import os
import pwd
-import warnings
from multiprocessing import cpu_count
import structlog
@@ -17,7 +16,7 @@ try:
except KeyError:
pass
-worker_class = "uvicorn.workers.UvicornWorker"
+worker_class = "uvicorn.workers.UvicornH11Worker"
# Docker containers don't have /tmp as tmpfs
if os.path.exists("/dev/shm"): # nosec
worker_tmp_dir = "/dev/shm" # nosec
@@ -61,28 +60,3 @@ else:
default_workers = max(cpu_count() * 0.25, 1) + 1 # Minimum of 2 workers
workers = int(os.environ.get("WORKERS", default_workers))
threads = int(os.environ.get("THREADS", 4))
-
-warnings.filterwarnings(
- "ignore",
- message="defusedxml.lxml is no longer supported and will be removed in a future release.",
-)
-warnings.filterwarnings(
- "ignore",
- message="defusedxml.cElementTree is deprecated, import from defusedxml.ElementTree instead.",
-)
-warnings.filterwarnings(
- "ignore",
- message=(
- "'django_prometheus' defines default_app_config = 'django_prometheus.apps.DjangoPromethe"
- "usConfig'. Django now detects this configuration automatically. You can remove d"
- "efault_app_config."
- ),
-)
-warnings.filterwarnings(
- "ignore",
- message=(
- "'dbbackup' defines default_app_config = 'dbbackup.apps.DbbackupConfig'. Django now det"
- "ects this configuration automatically. You can remove default_app_config."
- ),
-)
-warnings.simplefilter("once")
From b612a82e1613a739d4bbde0fa4f380753767fc72 Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Mon, 4 Oct 2021 18:04:19 +0200
Subject: [PATCH 14/18] outposts: don't always build permissions on
outpost.user access, only in signals and tasks
Signed-off-by: Jens Langhammer
---
authentik/outposts/models.py | 32 +++++++++++++++++++-------------
authentik/outposts/tasks.py | 3 ++-
tests/e2e/test_provider_proxy.py | 4 ++--
3 files changed, 23 insertions(+), 16 deletions(-)
diff --git a/authentik/outposts/models.py b/authentik/outposts/models.py
index a4a822964..48fbb325e 100644
--- a/authentik/outposts/models.py
+++ b/authentik/outposts/models.py
@@ -341,19 +341,8 @@ class Outpost(ManagedModel):
"""Username for service user"""
return f"ak-outpost-{self.uuid.hex}"
- @property
- def user(self) -> User:
- """Get/create user with access to all required objects"""
- users = User.objects.filter(username=self.user_identifier)
- if not users.exists():
- user: User = User.objects.create(username=self.user_identifier)
- user.set_unusable_password()
- user.save()
- else:
- user = users.first()
- user.attributes[USER_ATTRIBUTE_SA] = True
- user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
- user.save()
+ def build_user_permissions(self, user: User):
+ """Create per-object and global permissions for outpost service-account"""
# To ensure the user only has the correct permissions, we delete all of them and re-add
# the ones the user needs
with transaction.atomic():
@@ -397,6 +386,23 @@ class Outpost(ManagedModel):
"Updated service account's permissions",
perms=UserObjectPermission.objects.filter(user=user),
)
+
+ @property
+ def user(self) -> User:
+ """Get/create user with access to all required objects"""
+ users = User.objects.filter(username=self.user_identifier)
+ should_create_user = not users.exists()
+ if should_create_user:
+ user: User = User.objects.create(username=self.user_identifier)
+ user.set_unusable_password()
+ user.save()
+ else:
+ user = users.first()
+ user.attributes[USER_ATTRIBUTE_SA] = True
+ user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
+ user.save()
+ if should_create_user:
+ self.build_user_permissions(user)
return user
@property
diff --git a/authentik/outposts/tasks.py b/authentik/outposts/tasks.py
index 549f2e32b..fb2db9c77 100644
--- a/authentik/outposts/tasks.py
+++ b/authentik/outposts/tasks.py
@@ -126,6 +126,7 @@ def outpost_token_ensurer(self: MonitoredTask):
all_outposts = Outpost.objects.all()
for outpost in all_outposts:
_ = outpost.token
+ outpost.build_user_permissions(outpost.user)
self.set_status(
TaskResult(
TaskResultStatus.SUCCESSFUL,
@@ -196,7 +197,7 @@ def _outpost_single_update(outpost: Outpost, layer=None):
# Ensure token again, because this function is called when anything related to an
# OutpostModel is saved, so we can be sure permissions are right
_ = outpost.token
- _ = outpost.user
+ outpost.build_user_permissions(outpost.user)
if not layer: # pragma: no cover
layer = get_channel_layer()
for state in OutpostState.for_outpost(outpost):
diff --git a/tests/e2e/test_provider_proxy.py b/tests/e2e/test_provider_proxy.py
index 5a9b25fde..f5fa80ec7 100644
--- a/tests/e2e/test_provider_proxy.py
+++ b/tests/e2e/test_provider_proxy.py
@@ -93,7 +93,7 @@ class TestProviderProxy(SeleniumTestCase):
)
outpost.providers.add(proxy)
outpost.save()
- _ = outpost.user
+ outpost.build_user_permissions(outpost.user)
self.proxy_container = self.start_proxy(outpost)
@@ -157,7 +157,7 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
)
outpost.providers.add(proxy)
outpost.save()
- _ = outpost.user
+ outpost.build_user_permissions(outpost.user)
# Wait until outpost healthcheck succeeds
healthcheck_retries = 0
From 73bb778d6288b06d176fda73592f0456ace7fe80 Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Mon, 4 Oct 2021 18:37:05 +0200
Subject: [PATCH 15/18] stages/user_login: add check for user.is_active and
tests
Signed-off-by: Jens Langhammer
---
authentik/stages/user_login/stage.py | 8 ++++++--
authentik/stages/user_login/tests.py | 26 ++++++++++++++++++++++++++
2 files changed, 32 insertions(+), 2 deletions(-)
diff --git a/authentik/stages/user_login/stage.py b/authentik/stages/user_login/stage.py
index d545d8559..4d4e184fd 100644
--- a/authentik/stages/user_login/stage.py
+++ b/authentik/stages/user_login/stage.py
@@ -5,6 +5,7 @@ from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from structlog.stdlib import get_logger
+from authentik.core.models import User
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import StageView
from authentik.lib.utils.time import timedelta_from_string
@@ -32,9 +33,12 @@ class UserLoginStageView(StageView):
backend = self.executor.plan.context.get(
PLAN_CONTEXT_AUTHENTICATION_BACKEND, BACKEND_INBUILT
)
+ user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
+ if not user.is_active:
+ LOGGER.warning("User is not active, login will not work.")
login(
self.request,
- self.executor.plan.context[PLAN_CONTEXT_PENDING_USER],
+ user,
backend=backend,
)
delta = timedelta_from_string(self.executor.current_stage.session_duration)
@@ -45,7 +49,7 @@ class UserLoginStageView(StageView):
LOGGER.debug(
"Logged in",
backend=backend,
- user=self.executor.plan.context[PLAN_CONTEXT_PENDING_USER],
+ user=user,
flow_slug=self.executor.flow.slug,
session_duration=self.executor.current_stage.session_duration,
)
diff --git a/authentik/stages/user_login/tests.py b/authentik/stages/user_login/tests.py
index 0940324a6..9a43daa3b 100644
--- a/authentik/stages/user_login/tests.py
+++ b/authentik/stages/user_login/tests.py
@@ -109,3 +109,29 @@ class TestUserLoginStage(APITestCase):
},
},
)
+
+ def test_inactive_account(self):
+ """Test with a valid pending user and backend"""
+ self.user.is_active = False
+ self.user.save()
+ plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
+ plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
+ session = self.client.session
+ session[SESSION_KEY_PLAN] = plan
+ session.save()
+
+ response = self.client.get(
+ reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertJSONEqual(
+ force_str(response.content),
+ {
+ "component": "xak-flow-redirect",
+ "to": reverse("authentik_core:root-redirect"),
+ "type": ChallengeTypes.REDIRECT.value,
+ },
+ )
+ response = self.client.get(reverse("authentik_api:application-list"))
+ self.assertEqual(response.status_code, 403)
From cb37e5c10e08a9e64ace17f7864357e1ca2ccef8 Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Mon, 4 Oct 2021 18:47:51 +0200
Subject: [PATCH 16/18] stages/email: add activate_user_on_success flag, add
for all example flows
Signed-off-by: Jens Langhammer
# Conflicts:
# web/src/locales/fr_FR.po
---
authentik/stages/email/api.py | 2 +
...004_emailstage_activate_user_on_success.py | 20 +++++++++
authentik/stages/email/models.py | 4 ++
authentik/stages/email/stage.py | 3 ++
authentik/stages/email/tests/test_stage.py | 4 ++
schema.yml | 13 ++++++
web/src/locales/en.po | 44 ++++++++++++++++---
web/src/locales/pseudo-LOCALE.po | 42 ++++++++++++++++--
web/src/pages/stages/email/EmailStageForm.ts | 15 +++++++
.../enrollment-email-verification.akflow | 3 +-
.../flows/recovery-email-verification.akflow | 3 +-
11 files changed, 142 insertions(+), 11 deletions(-)
create mode 100644 authentik/stages/email/migrations/0004_emailstage_activate_user_on_success.py
diff --git a/authentik/stages/email/api.py b/authentik/stages/email/api.py
index 636d0bafb..ec9d461a4 100644
--- a/authentik/stages/email/api.py
+++ b/authentik/stages/email/api.py
@@ -43,6 +43,7 @@ class EmailStageSerializer(StageSerializer):
"token_expiry",
"subject",
"template",
+ "activate_user_on_success",
]
extra_kwargs = {"password": {"write_only": True}}
@@ -65,6 +66,7 @@ class EmailStageViewSet(UsedByMixin, ModelViewSet):
"token_expiry",
"subject",
"template",
+ "activate_user_on_success",
]
ordering = ["name"]
diff --git a/authentik/stages/email/migrations/0004_emailstage_activate_user_on_success.py b/authentik/stages/email/migrations/0004_emailstage_activate_user_on_success.py
new file mode 100644
index 000000000..77c8be05f
--- /dev/null
+++ b/authentik/stages/email/migrations/0004_emailstage_activate_user_on_success.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2.7 on 2021-10-04 16:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_stages_email", "0003_auto_20210404_1054"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="emailstage",
+ name="activate_user_on_success",
+ field=models.BooleanField(
+ default=False, help_text="Activate users upon completion of stage."
+ ),
+ ),
+ ]
diff --git a/authentik/stages/email/models.py b/authentik/stages/email/models.py
index b9b7fe12d..7584d7190 100644
--- a/authentik/stages/email/models.py
+++ b/authentik/stages/email/models.py
@@ -71,6 +71,10 @@ class EmailStage(Stage):
timeout = models.IntegerField(default=10)
from_address = models.EmailField(default="system@authentik.local")
+ activate_user_on_success = models.BooleanField(
+ default=False, help_text=_("Activate users upon completion of stage.")
+ )
+
token_expiry = models.IntegerField(
default=30, help_text=_("Time in minutes the token sent is valid.")
)
diff --git a/authentik/stages/email/stage.py b/authentik/stages/email/stage.py
index 77f29e0a0..8dee06f64 100644
--- a/authentik/stages/email/stage.py
+++ b/authentik/stages/email/stage.py
@@ -106,6 +106,9 @@ class EmailStageView(ChallengeStageView):
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = token.user
token.delete()
messages.success(request, _("Successfully verified Email."))
+ if self.executor.current_stage.activate_user_on_success:
+ self.executor.plan.context[PLAN_CONTEXT_PENDING_USER].is_active = True
+ self.executor.plan.context[PLAN_CONTEXT_PENDING_USER].save()
return self.executor.stage_ok()
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
LOGGER.debug("No pending user")
diff --git a/authentik/stages/email/tests/test_stage.py b/authentik/stages/email/tests/test_stage.py
index c67ce8881..f0a1e6c9a 100644
--- a/authentik/stages/email/tests/test_stage.py
+++ b/authentik/stages/email/tests/test_stage.py
@@ -31,6 +31,7 @@ class TestEmailStage(APITestCase):
)
self.stage = EmailStage.objects.create(
name="email",
+ activate_user_on_success=True,
)
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
@@ -84,6 +85,8 @@ class TestEmailStage(APITestCase):
"""Test with token"""
# Make sure token exists
self.test_pending_user()
+ self.user.is_active = False
+ self.user.save()
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
session = self.client.session
session[SESSION_KEY_PLAN] = plan
@@ -125,3 +128,4 @@ class TestEmailStage(APITestCase):
session = self.client.session
plan: FlowPlan = session[SESSION_KEY_PLAN]
self.assertEqual(plan.context[PLAN_CONTEXT_PENDING_USER], self.user)
+ self.assertTrue(plan.context[PLAN_CONTEXT_PENDING_USER].is_active)
diff --git a/schema.yml b/schema.yml
index 27e7084a2..d094ce173 100644
--- a/schema.yml
+++ b/schema.yml
@@ -15638,6 +15638,10 @@ paths:
operationId: stages_email_list
description: EmailStage Viewset
parameters:
+ - in: query
+ name: activate_user_on_success
+ schema:
+ type: boolean
- in: query
name: from_address
schema:
@@ -19811,6 +19815,9 @@ components:
type: string
template:
type: string
+ activate_user_on_success:
+ type: boolean
+ description: Activate users upon completion of stage.
required:
- component
- name
@@ -19863,6 +19870,9 @@ components:
type: string
template:
type: string
+ activate_user_on_success:
+ type: boolean
+ description: Activate users upon completion of stage.
required:
- name
ErrorDetail:
@@ -25370,6 +25380,9 @@ components:
type: string
template:
type: string
+ activate_user_on_success:
+ type: boolean
+ description: Activate users upon completion of stage.
PatchedEventMatcherPolicyRequest:
type: object
description: Event Matcher Policy Serializer
diff --git a/web/src/locales/en.po b/web/src/locales/en.po
index e2e0a518f..483387081 100644
--- a/web/src/locales/en.po
+++ b/web/src/locales/en.po
@@ -159,6 +159,10 @@ msgstr "Actions over the last 24 hours"
msgid "Activate"
msgstr "Activate"
+#: src/pages/stages/email/EmailStageForm.ts
+msgid "Activate pending user on success"
+msgstr "Activate pending user on success"
+
#: src/pages/groups/MemberSelectModal.ts
#: src/pages/users/UserListPage.ts
#: src/pages/users/UserListPage.ts
@@ -294,6 +298,10 @@ msgstr "Application"
msgid "Application Icon"
msgstr "Application Icon"
+#: src/elements/charts/UserChart.ts
+msgid "Application authorizations"
+msgstr "Application authorizations"
+
#: src/pages/events/utils.ts
msgid "Application authorized"
msgstr "Application authorized"
@@ -439,6 +447,10 @@ msgstr "Authorization URL"
msgid "Authorization flow"
msgstr "Authorization flow"
+#: src/elements/charts/ApplicationAuthorizeChart.ts
+msgid "Authorizations"
+msgstr "Authorizations"
+
#: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts
msgid "Authorize URL"
msgstr "Authorize URL"
@@ -1724,6 +1736,11 @@ msgstr "External Host"
msgid "External host"
msgstr "External host"
+#: src/elements/charts/AdminLoginsChart.ts
+#: src/elements/charts/UserChart.ts
+msgid "Failed Logins"
+msgstr "Failed Logins"
+
#: src/pages/stages/password/PasswordStageForm.ts
msgid "Failed attempts before cancel"
msgstr "Failed attempts before cancel"
@@ -1778,9 +1795,13 @@ msgstr "Field of the user object this value is written to."
msgid "Field which contains a unique Identifier."
msgstr "Field which contains a unique Identifier."
+#:
+#~ msgid "Field which contains members of a group."
+#~ msgstr "Field which contains members of a group."
+
#: src/pages/sources/ldap/LDAPSourceForm.ts
-msgid "Field which contains members of a group."
-msgstr "Field which contains members of a group."
+msgid "Field which contains members of a group. Note that if using the \"memberUid\" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'"
+msgstr "Field which contains members of a group. Note that if using the \"memberUid\" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'"
#: src/pages/stages/prompt/PromptStageForm.ts
msgid "Fields"
@@ -3607,6 +3628,10 @@ msgstr "Scopes"
msgid "Score"
msgstr "Score"
+#: src/pages/providers/ldap/LDAPProviderForm.ts
+msgid "Search group"
+msgstr "Search group"
+
#: src/elements/table/TableSearch.ts
#: src/user/LibraryPage.ts
msgid "Search..."
@@ -4027,6 +4052,11 @@ msgstr "Subject-alt name"
msgid "Successful"
msgstr "Successful"
+#: src/elements/charts/AdminLoginsChart.ts
+#: src/elements/charts/UserChart.ts
+msgid "Successful Logins"
+msgstr "Successful Logins"
+
#: src/pages/flows/FlowListPage.ts
msgid "Successfully cleared flow cache"
msgstr "Successfully cleared flow cache"
@@ -4354,9 +4384,9 @@ msgstr "Sync"
msgid "Sync groups"
msgstr "Sync groups"
-#: src/pages/providers/ldap/LDAPProviderForm.ts
-msgid "Sync parent group"
-msgstr "Sync parent group"
+#:
+#~ msgid "Sync parent group"
+#~ msgstr "Sync parent group"
#: src/pages/sources/ldap/LDAPSourceViewPage.ts
msgid "Sync status"
@@ -5212,6 +5242,10 @@ msgstr "Webhook Mapping"
msgid "Webhook URL"
msgstr "Webhook URL"
+#: src/pages/stages/email/EmailStageForm.ts
+msgid "When a user returns from the email successfully, their account will be activated."
+msgstr "When a user returns from the email successfully, their account will be activated."
+
#: src/pages/stages/identification/IdentificationStageForm.ts
msgid "When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown."
msgstr "When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown."
diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po
index 0a3ad1a61..de14a6654 100644
--- a/web/src/locales/pseudo-LOCALE.po
+++ b/web/src/locales/pseudo-LOCALE.po
@@ -159,6 +159,10 @@ msgstr ""
msgid "Activate"
msgstr ""
+#: src/pages/stages/email/EmailStageForm.ts
+msgid "Activate pending user on success"
+msgstr ""
+
#: src/pages/groups/MemberSelectModal.ts
#: src/pages/users/UserListPage.ts
#: src/pages/users/UserListPage.ts
@@ -294,6 +298,10 @@ msgstr ""
msgid "Application Icon"
msgstr ""
+#: src/elements/charts/UserChart.ts
+msgid "Application authorizations"
+msgstr ""
+
#: src/pages/events/utils.ts
msgid "Application authorized"
msgstr ""
@@ -435,6 +443,10 @@ msgstr ""
msgid "Authorization flow"
msgstr ""
+#: src/elements/charts/ApplicationAuthorizeChart.ts
+msgid "Authorizations"
+msgstr ""
+
#: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts
msgid "Authorize URL"
msgstr ""
@@ -1716,6 +1728,11 @@ msgstr ""
msgid "External host"
msgstr ""
+#: src/elements/charts/AdminLoginsChart.ts
+#: src/elements/charts/UserChart.ts
+msgid "Failed Logins"
+msgstr ""
+
#: src/pages/stages/password/PasswordStageForm.ts
msgid "Failed attempts before cancel"
msgstr ""
@@ -1770,8 +1787,12 @@ msgstr ""
msgid "Field which contains a unique Identifier."
msgstr ""
+#:
+#~ msgid "Field which contains members of a group."
+#~ msgstr ""
+
#: src/pages/sources/ldap/LDAPSourceForm.ts
-msgid "Field which contains members of a group."
+msgid "Field which contains members of a group. Note that if using the \"memberUid\" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'"
msgstr ""
#: src/pages/stages/prompt/PromptStageForm.ts
@@ -3599,6 +3620,10 @@ msgstr ""
msgid "Score"
msgstr ""
+#: src/pages/providers/ldap/LDAPProviderForm.ts
+msgid "Search group"
+msgstr ""
+
#: src/elements/table/TableSearch.ts
#: src/user/LibraryPage.ts
msgid "Search..."
@@ -4019,6 +4044,11 @@ msgstr ""
msgid "Successful"
msgstr ""
+#: src/elements/charts/AdminLoginsChart.ts
+#: src/elements/charts/UserChart.ts
+msgid "Successful Logins"
+msgstr ""
+
#: src/pages/flows/FlowListPage.ts
msgid "Successfully cleared flow cache"
msgstr ""
@@ -4346,9 +4376,9 @@ msgstr ""
msgid "Sync groups"
msgstr ""
-#: src/pages/providers/ldap/LDAPProviderForm.ts
-msgid "Sync parent group"
-msgstr ""
+#:
+#~ msgid "Sync parent group"
+#~ msgstr ""
#: src/pages/sources/ldap/LDAPSourceViewPage.ts
msgid "Sync status"
@@ -5197,6 +5227,10 @@ msgstr ""
msgid "Webhook URL"
msgstr ""
+#: src/pages/stages/email/EmailStageForm.ts
+msgid "When a user returns from the email successfully, their account will be activated."
+msgstr ""
+
#: src/pages/stages/identification/IdentificationStageForm.ts
msgid "When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown."
msgstr ""
diff --git a/web/src/pages/stages/email/EmailStageForm.ts b/web/src/pages/stages/email/EmailStageForm.ts
index 740d03937..1159b5552 100644
--- a/web/src/pages/stages/email/EmailStageForm.ts
+++ b/web/src/pages/stages/email/EmailStageForm.ts
@@ -147,6 +147,21 @@ export class EmailStageForm extends ModelForm {
${t`Stage-specific settings`}