Compare commits

..

34 Commits

Author SHA1 Message Date
5a7508d2e0 core: fix token expiration not being updated upon key rotation
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-12 17:24:19 +02:00
9c31ea1aa6 core: fix expired tokens not being returned by API
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-12 17:24:19 +02:00
18211a2033 release: 2021.7.3 2021-08-05 19:23:03 +02:00
b4cfc56e5e web/admin: fix source form's userMatchingMode being swapped
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	web/src/pages/sources/oauth/OAuthSourceForm.ts
#	web/src/pages/sources/plex/PlexSourceForm.ts
2021-08-05 18:48:02 +02:00
8e797fa76b outpost/ldap: fix errors with new UserSelf serializer
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-05 18:16:06 +02:00
1b91543add core: add UserSelfSerializer and separate method for users to update themselves with limited fields
rework user settings page to better use form
closes #1227

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	authentik/core/api/users.py
#	web/src/elements/forms/ModelForm.ts
#	web/src/pages/user-settings/UserDetailsPage.ts
#	web/src/pages/user-settings/UserSettingsPage.ts
2021-08-05 17:47:45 +02:00
1cd59be8dc web/admin: fix email being required
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	web/src/pages/user-settings/UserDetailsPage.ts
#	web/src/pages/users/UserForm.ts
2021-08-05 17:46:28 +02:00
66bb68a747 lifecycle: decrease default worker count on compose
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-05 09:44:58 +02:00
aa4f7fb2b6 providers/saml: fix error when PropertyMapping return value isn't string
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-04 00:22:07 +02:00
4f1c11c5ef providers/saml: add WantAssertionsSigned
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	authentik/providers/saml/processors/metadata_parser.py
2021-08-04 00:21:54 +02:00
add7a80fdc release: 2021.7.2 2021-08-01 19:11:50 +02:00
aac91c2e9d stages/email: handle OSError
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-01 18:25:53 +02:00
85e86351cd flows: fix flows not redirecting correctly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-01 18:25:53 +02:00
d767504474 flows: don't check redirect URL when set from flow plan (set from authentik or policy)
closes #1203

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-01 15:23:46 +02:00
f84cd6208c flows: fix unhandled error in stage execution not being logged as SYSTEM_EXCEPTION event
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-01 15:23:46 +02:00
1ec540ea9a providers/saml: fix metadata being inaccessible without authentication
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-01 15:23:46 +02:00
29fe731bbf providers/saml: fix Error when getting metadata for invalid ID
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-01 14:09:22 +02:00
26e66969c9 stages/invitation: delete invite only after full enrollment flow is completed
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-01 13:22:02 +02:00
fe629f8b51 web/admin: fix empty column when no invitation expiry was set
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-01 13:22:02 +02:00
72b7642c5a outposts: catch invalid ServiceConnection error in outpost controller
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-01 12:33:21 +02:00
a97f842112 sources/plex: add background task to monitor validity of plex token
closes #1205

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-01 12:33:21 +02:00
16e6e4c3b7 web/admin: add re-authenticate button for plex
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#1205
2021-08-01 12:33:21 +02:00
dc0d715885 web/admin: add UI to copy invitation link
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-01 12:33:20 +02:00
7ecd57ecff outpost: bump timer for periodic config reloads
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-08-01 12:33:20 +02:00
0cb4d64b57 stages/email: fix error when re-requesting email after token has expired
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-30 09:39:42 +02:00
a4fd58a0db events: ensure fallback result is set for on_failure
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-30 09:39:42 +02:00
fb6e8ca1eb events: remove default result for MonitoredTasks, only save when result was set
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-29 22:43:29 +02:00
4c41948e75 e2e: fix broken selenium by locking images
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-29 21:53:09 +02:00
a5c8caf909 providers/oauth2: fix error when requesting jwks keys with no rs256 aet
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-29 21:22:59 +02:00
970655ab21 ci: fix sentry sourcemap path
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-29 21:22:52 +02:00
c8c7202c61 web/admin: fix LDAP Provider bind flow list being empty
closes #1192

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-29 11:35:54 +02:00
a3981dd3cd providers/proxy: fix hosts for ingress not being compared correctly
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-29 11:35:50 +02:00
affafc31cf sources/ldap: improve ms-ad password complexity checking
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-29 11:35:47 +02:00
602aed674b web/admin: fully remove response cloning due to errors
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-07-29 11:35:44 +02:00
54 changed files with 792 additions and 276 deletions

View File

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

View File

@ -33,14 +33,14 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik:2021.7.1,
beryju/authentik:2021.7.3,
beryju/authentik:latest,
ghcr.io/goauthentik/server:2021.7.1,
ghcr.io/goauthentik/server:2021.7.3,
ghcr.io/goauthentik/server:latest
platforms: linux/amd64,linux/arm64
context: .
- name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.7.1', 'rc') }}
if: ${{ github.event_name == 'release' && !contains('2021.7.3', 'rc') }}
run: |
docker pull beryju/authentik:latest
docker tag beryju/authentik:latest beryju/authentik:stable
@ -75,14 +75,14 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik-proxy:2021.7.1,
beryju/authentik-proxy:2021.7.3,
beryju/authentik-proxy:latest,
ghcr.io/goauthentik/proxy:2021.7.1,
ghcr.io/goauthentik/proxy:2021.7.3,
ghcr.io/goauthentik/proxy:latest
file: proxy.Dockerfile
platforms: linux/amd64,linux/arm64
- name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.7.1', 'rc') }}
if: ${{ github.event_name == 'release' && !contains('2021.7.3', 'rc') }}
run: |
docker pull beryju/authentik-proxy:latest
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
@ -117,14 +117,14 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik-ldap:2021.7.1,
beryju/authentik-ldap:2021.7.3,
beryju/authentik-ldap:latest,
ghcr.io/goauthentik/ldap:2021.7.1,
ghcr.io/goauthentik/ldap:2021.7.3,
ghcr.io/goauthentik/ldap:latest
file: ldap.Dockerfile
platforms: linux/amd64,linux/arm64
- name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.7.1', 'rc') }}
if: ${{ github.event_name == 'release' && !contains('2021.7.3', 'rc') }}
run: |
docker pull beryju/authentik-ldap:latest
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
@ -176,7 +176,7 @@ jobs:
SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org
with:
version: authentik@2021.7.1
version: authentik@2021.7.3
environment: beryjuorg-prod
sourcemaps: './web/dist'
url_prefix: /static/dist
url_prefix: '~/static/dist'

2
.gitignore vendored
View File

@ -200,4 +200,4 @@ media/
*mmdb
.idea/
api/
/api/

View File

@ -1,3 +1,3 @@
"""authentik"""
__version__ = "2021.7.1"
__version__ = "2021.7.3"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -48,7 +48,7 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
"""Token Viewset"""
lookup_field = "identifier"
queryset = Token.filter_not_expired()
queryset = Token.objects.all()
serializer_class = TokenSerializer
search_fields = [
"identifier",

View File

@ -10,6 +10,7 @@ from drf_spectacular.utils import extend_schema, extend_schema_field
from guardian.utils import get_anonymous_user
from rest_framework.decorators import action
from rest_framework.fields import CharField, JSONField, SerializerMethodField
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import (
@ -62,12 +63,40 @@ class UserSerializer(ModelSerializer):
]
class UserSelfSerializer(ModelSerializer):
"""User Serializer for information a user can retrieve about themselves and
update about themselves"""
is_superuser = BooleanField(read_only=True)
avatar = CharField(read_only=True)
groups = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups")
uid = CharField(read_only=True)
class Meta:
model = User
fields = [
"pk",
"username",
"name",
"is_active",
"is_superuser",
"groups",
"email",
"avatar",
"uid",
]
extra_kwargs = {
"is_active": {"read_only": True},
}
class SessionUserSerializer(PassiveSerializer):
"""Response for the /user/me endpoint, returns the currently active user (as `user` property)
and, if this user is being impersonated, the original user in the `original` property."""
user = UserSerializer()
original = UserSerializer(required=False)
user = UserSelfSerializer()
original = UserSelfSerializer(required=False)
class UserMetricsSerializer(PassiveSerializer):
@ -158,12 +187,36 @@ class UserViewSet(UsedByMixin, ModelViewSet):
data={"user": UserSerializer(request.user).data}
)
if SESSION_IMPERSONATE_USER in request._request.session:
serializer.initial_data["original"] = UserSerializer(
serializer.initial_data["original"] = UserSelfSerializer(
request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
).data
serializer.is_valid()
return Response(serializer.data)
@extend_schema(
request=UserSelfSerializer, responses={200: SessionUserSerializer(many=False)}
)
@action(
methods=["PUT"],
detail=False,
pagination_class=None,
filter_backends=[],
permission_classes=[IsAuthenticated],
)
def update_self(self, request: Request) -> Response:
"""Allow users to change information on their own profile"""
data = UserSelfSerializer(
instance=User.objects.get(pk=request.user.pk), data=request.data
)
if not data.is_valid():
return Response(data.errors)
new_user = data.save()
# If we're impersonating, we need to update that user object
# since it caches the full object
if SESSION_IMPERSONATE_USER in request.session:
request.session[SESSION_IMPERSONATE_USER] = new_user
return self.me(request)
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
@extend_schema(responses={200: UserMetricsSerializer(many=False)})
@action(detail=True, pagination_class=None, filter_backends=[])

View File

@ -438,6 +438,7 @@ class Token(ManagedModel, ExpiringModel):
from authentik.events.models import Event, EventAction
self.key = default_token_key()
self.expires = default_token_duration()
self.save(*args, **kwargs)
Event.new(
action=EventAction.SECRET_ROTATE,

View File

@ -114,7 +114,7 @@ class MonitoredTask(Task):
# For tasks that should only be listed if they failed, set this to False
save_on_success: bool
_result: TaskResult
_result: Optional[TaskResult]
_uid: Optional[str]
@ -122,7 +122,7 @@ class MonitoredTask(Task):
super().__init__(*args, **kwargs)
self.save_on_success = True
self._uid = None
self._result = TaskResult(status=TaskResultStatus.ERROR, messages=[])
self._result = None
self.result_timeout_hours = 6
self.start = default_timer()
@ -138,25 +138,30 @@ class MonitoredTask(Task):
def after_return(
self, status, retval, task_id, args: list[Any], kwargs: dict[str, Any], einfo
):
if not self._result.uid:
self._result.uid = self._uid
if self.save_on_success:
TaskInfo(
task_name=self.__name__,
task_description=self.__doc__,
start_timestamp=self.start,
finish_timestamp=default_timer(),
finish_time=datetime.now(),
result=self._result,
task_call_module=self.__module__,
task_call_func=self.__name__,
task_call_args=args,
task_call_kwargs=kwargs,
).save(self.result_timeout_hours)
if self._result:
if not self._result.uid:
self._result.uid = self._uid
if self.save_on_success:
TaskInfo(
task_name=self.__name__,
task_description=self.__doc__,
start_timestamp=self.start,
finish_timestamp=default_timer(),
finish_time=datetime.now(),
result=self._result,
task_call_module=self.__module__,
task_call_func=self.__name__,
task_call_args=args,
task_call_kwargs=kwargs,
).save(self.result_timeout_hours)
return super().after_return(status, retval, task_id, args, kwargs, einfo=einfo)
# pylint: disable=too-many-arguments
def on_failure(self, exc, task_id, args, kwargs, einfo):
if not self._result:
self._result = TaskResult(
status=TaskResultStatus.ERROR, messages=[str(exc)]
)
if not self._result.uid:
self._result.uid = self._uid
TaskInfo(

View File

@ -26,7 +26,7 @@ from sentry_sdk import capture_exception
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.models import USER_ATTRIBUTE_DEBUG
from authentik.events.models import cleanse_dict
from authentik.events.models import Event, EventAction, cleanse_dict
from authentik.flows.challenge import (
AccessDeniedChallenge,
Challenge,
@ -52,6 +52,7 @@ from authentik.flows.planner import (
FlowPlanner,
)
from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.errors import exception_to_string
from authentik.lib.utils.reflection import all_subclasses, class_to_path
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
from authentik.tenants.models import Tenant
@ -203,6 +204,18 @@ class FlowExecutorView(APIView):
except InvalidStageError as exc:
return self.stage_invalid(str(exc))
def handle_exception(self, exc: Exception) -> HttpResponse:
"""Handle exception in stage execution"""
if settings.DEBUG or settings.TEST:
raise exc
capture_exception(exc)
self._logger.warning(exc)
Event.new(
action=EventAction.SYSTEM_EXCEPTION,
message=exception_to_string(exc),
).from_http(self.request)
return to_stage_response(self.request, FlowErrorResponse(self.request, exc))
@extend_schema(
responses={
200: PolymorphicProxySerializer(
@ -237,11 +250,7 @@ class FlowExecutorView(APIView):
stage_response = self.current_stage_view.get(request, *args, **kwargs)
return to_stage_response(request, stage_response)
except Exception as exc: # pylint: disable=broad-except
if settings.DEBUG or settings.TEST:
raise exc
capture_exception(exc)
self._logger.warning(exc)
return to_stage_response(request, FlowErrorResponse(request, exc))
return self.handle_exception(exc)
@extend_schema(
responses={
@ -278,11 +287,7 @@ class FlowExecutorView(APIView):
stage_response = self.current_stage_view.post(request, *args, **kwargs)
return to_stage_response(request, stage_response)
except Exception as exc: # pylint: disable=broad-except
if settings.DEBUG or settings.TEST:
raise exc
capture_exception(exc)
self._logger.warning(exc)
return to_stage_response(request, FlowErrorResponse(request, exc))
return self.handle_exception(exc)
def _initiate_plan(self) -> FlowPlan:
planner = FlowPlanner(self.flow)
@ -317,13 +322,15 @@ class FlowExecutorView(APIView):
"""User Successfully passed all stages"""
# Since this is wrapped by the ExecutorShell, the next argument is saved in the session
# extract the next param before cancel as that cleans it
next_param = None
if self.plan:
next_param = self.plan.context.get(PLAN_CONTEXT_REDIRECT)
if not next_param:
next_param = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:root-redirect"
)
if self.plan and PLAN_CONTEXT_REDIRECT in self.plan.context:
# The context `redirect` variable can only be set by
# an expression policy or authentik itself, so we don't
# check if its an absolute URL or a relative one
self.cancel()
return redirect(self.plan.context.get(PLAN_CONTEXT_REDIRECT))
next_param = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:root-redirect"
)
self.cancel()
return to_stage_response(self.request, redirect_with_qs(next_param))

View File

@ -22,7 +22,7 @@ def redirect_with_qs(view: str, get_query_set=None, **kwargs) -> HttpResponse:
except NoReverseMatch:
if not is_url_absolute(view):
return redirect(view)
LOGGER.debug("redirect target is not a valid view", view=view)
LOGGER.warning("redirect target is not a valid view", view=view)
raise
else:
if get_query_set:

View File

@ -28,6 +28,7 @@ from authentik.outposts.models import (
OutpostServiceConnection,
OutpostState,
OutpostType,
ServiceConnectionInvalid,
)
from authentik.providers.ldap.controllers.docker import LDAPDockerController
from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesController
@ -114,7 +115,7 @@ def outpost_controller(
for log in logs:
LOGGER.debug(log)
LOGGER.debug("-----------------Outpost Controller logs end-------------------")
except ControllerException as exc:
except (ControllerException, ServiceConnectionInvalid) as exc:
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
else:
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, logs))

View File

@ -0,0 +1,54 @@
"""JWKS tests"""
import json
from django.test import RequestFactory
from django.urls.base import reverse
from django.utils.encoding import force_str
from authentik.core.models import Application
from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.providers.oauth2.tests.utils import OAuthTestCase
class TestJWKS(OAuthTestCase):
"""Test JWKS view"""
def setUp(self) -> None:
super().setUp()
self.factory = RequestFactory()
def test_rs256(self):
"""Test JWKS request with RS256"""
provider = OAuth2Provider.objects.create(
name="test",
client_id="test",
authorization_flow=Flow.objects.first(),
redirect_uris="http://local.invalid",
rsa_key=CertificateKeyPair.objects.first(),
)
app = Application.objects.create(name="test", slug="test", provider=provider)
response = self.client.get(
reverse(
"authentik_providers_oauth2:jwks", kwargs={"application_slug": app.slug}
)
)
body = json.loads(force_str(response.content))
self.assertEqual(len(body["keys"]), 1)
def test_hs256(self):
"""Test JWKS request with HS256"""
provider = OAuth2Provider.objects.create(
name="test",
client_id="test",
authorization_flow=Flow.objects.first(),
redirect_uris="http://local.invalid",
)
app = Application.objects.create(name="test", slug="test", provider=provider)
response = self.client.get(
reverse(
"authentik_providers_oauth2:jwks", kwargs={"application_slug": app.slug}
)
)
self.assertJSONEqual(force_str(response.content), {})

View File

@ -30,7 +30,7 @@ class JWKSView(View):
response_data = {}
if provider.jwt_alg == JWTAlgorithms.RS256:
if provider.jwt_alg == JWTAlgorithms.RS256 and provider.rsa_key:
public_key: RSAPublicKey = provider.rsa_key.private_key.public_key()
public_numbers = public_key.public_numbers()
response_data["keys"] = [

View File

@ -60,12 +60,12 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
expected_hosts.sort()
expected_hosts_tls.sort()
have_hosts = [rule.host for rule in reference.spec.rules]
have_hosts = [rule.host for rule in current.spec.rules]
have_hosts.sort()
have_hosts_tls = []
for tls_config in reference.spec.tls:
if tls_config:
for tls_config in current.spec.tls:
if tls_config and tls_config.hosts:
have_hosts_tls += tls_config.hosts
have_hosts_tls.sort()

View File

@ -2,7 +2,7 @@
from xml.etree.ElementTree import ParseError # nosec
from defusedxml.ElementTree import fromstring
from django.http.response import HttpResponse
from django.http.response import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@ -116,7 +116,10 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
def metadata(self, request: Request, pk: int) -> Response:
"""Return metadata as XML string"""
# We don't use self.get_object() on purpose as this view is un-authenticated
provider = get_object_or_404(SAMLProvider, pk=pk)
try:
provider = get_object_or_404(SAMLProvider, pk=pk)
except ValueError:
raise Http404
try:
metadata = MetadataProcessor(provider, request).build_entity_descriptor()
if "download" in request._request.GET:

View File

@ -163,7 +163,7 @@ class AssertionProcessor:
provider=self.provider,
)
if value is not None:
name_id.text = value
name_id.text = str(value)
return name_id
except PropertyMappingExpressionException as exc:
Event.new(

View File

@ -134,10 +134,18 @@ class ServiceProviderMetadataParser:
# For now we'll only look at the first descriptor.
# Even if multiple descriptors exist, we can only configure one
descriptor = sp_sso_descriptors[0]
auth_n_request_signed = (
descriptor.attrib["AuthnRequestsSigned"].lower() == "true"
)
assertion_signed = descriptor.attrib["WantAssertionsSigned"].lower() == "true"
auth_n_request_signed = False
if "AuthnRequestsSigned" in descriptor.attrib:
auth_n_request_signed = (
descriptor.attrib["AuthnRequestsSigned"].lower() == "true"
)
assertion_signed = False
if "WantAssertionsSigned" in descriptor.attrib:
assertion_signed = (
descriptor.attrib["WantAssertionsSigned"].lower() == "true"
)
acs_services = descriptor.findall(
f"{{{NS_SAML_METADATA}}}AssertionConsumerService"

View File

@ -20,6 +20,7 @@ class TestSAMLProviderAPI(APITestCase):
def test_metadata(self):
"""Test metadata export (normal)"""
self.client.logout()
provider = SAMLProvider.objects.create(
name="test",
authorization_flow=Flow.objects.get(
@ -34,6 +35,7 @@ class TestSAMLProviderAPI(APITestCase):
def test_metadata_download(self):
"""Test metadata export (download)"""
self.client.logout()
provider = SAMLProvider.objects.create(
name="test",
authorization_flow=Flow.objects.get(
@ -50,6 +52,7 @@ class TestSAMLProviderAPI(APITestCase):
def test_metadata_invalid(self):
"""Test metadata export (invalid)"""
self.client.logout()
# Provider without application
provider = SAMLProvider.objects.create(
name="test",
@ -61,6 +64,10 @@ class TestSAMLProviderAPI(APITestCase):
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk}),
)
self.assertEqual(200, response.status_code)
response = self.client.get(
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": "abc"}),
)
self.assertEqual(404, response.status_code)
def test_import_success(self):
"""Test metadata import (success case)"""

View File

@ -105,15 +105,17 @@ class LDAPPasswordChanger:
if len(user_attributes["sAMAccountName"]) >= 3:
if password.lower() in user_attributes["sAMAccountName"].lower():
return False
display_name_tokens = split(
RE_DISPLAYNAME_SEPARATORS, user_attributes["displayName"]
)
for token in display_name_tokens:
# Ignore tokens under 3 chars
if len(token) < 3:
continue
if token.lower() in password.lower():
return False
# No display name set, can't check any further
if len(user_attributes["displayName"]) < 1:
return True
for display_name in user_attributes["displayName"]:
display_name_tokens = split(RE_DISPLAYNAME_SEPARATORS, display_name)
for token in display_name_tokens:
# Ignore tokens under 3 chars
if len(token) < 3:
continue
if token.lower() in password.lower():
return False
return True
def ad_password_complexity(

View File

@ -0,0 +1,10 @@
"""Plex source settings"""
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
"check_plex_token": {
"task": "authentik.sources.plex.tasks.check_plex_token_all",
"schedule": crontab(minute="31", hour="*/3"),
"options": {"queue": "authentik_scheduled"},
},
}

View File

@ -0,0 +1,43 @@
"""Plex tasks"""
from requests import RequestException
from authentik.events.models import Event, EventAction
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.root.celery import CELERY_APP
from authentik.sources.plex.models import PlexSource
from authentik.sources.plex.plex import PlexAuth
@CELERY_APP.task()
def check_plex_token_all():
"""Check plex token for all plex sources"""
for source in PlexSource.objects.all():
check_plex_token.delay(source.slug)
@CELERY_APP.task(bind=True, base=MonitoredTask)
def check_plex_token(self: MonitoredTask, source_slug: int):
"""Check the validity of a Plex source."""
sources = PlexSource.objects.filter(slug=source_slug)
if not sources.exists():
return
source: PlexSource = sources.first()
self.set_uid(source.slug)
auth = PlexAuth(source, source.plex_token)
try:
auth.get_user_info()
self.set_status(
TaskResult(TaskResultStatus.SUCCESSFUL, ["Plex token is valid."])
)
except RequestException as exc:
self.set_status(
TaskResult(
TaskResultStatus.ERROR,
["Plex token is invalid/an error occurred:", str(exc)],
)
)
Event.new(
EventAction.CONFIGURATION_ERROR,
message=f"Plex token invalid, please re-authenticate source.\n{str(exc)}",
source=source,
).save()

View File

@ -1,10 +1,13 @@
"""plex Source tests"""
from django.test import TestCase
from requests.exceptions import RequestException
from requests_mock import Mocker
from authentik.events.models import Event, EventAction
from authentik.providers.oauth2.generators import generate_client_secret
from authentik.sources.plex.models import PlexSource
from authentik.sources.plex.plex import PlexAuth
from authentik.sources.plex.tasks import check_plex_token_all
USER_INFO_RESPONSE = {
"id": 1234123419,
@ -62,3 +65,18 @@ class TestPlexSource(TestCase):
with Mocker() as mocker:
mocker.get("https://plex.tv/api/v2/resources", json=RESOURCES_RESPONSE)
self.assertTrue(api.check_server_overlap())
def test_check_task(self):
"""Test token check task"""
with Mocker() as mocker:
mocker.get("https://plex.tv/api/v2/user", json=USER_INFO_RESPONSE)
check_plex_token_all()
self.assertFalse(
Event.objects.filter(action=EventAction.CONFIGURATION_ERROR).exists()
)
with Mocker() as mocker:
mocker.get("https://plex.tv/api/v2/user", exc=RequestException())
check_plex_token_all()
self.assertTrue(
Event.objects.filter(action=EventAction.CONFIGURATION_ERROR).exists()
)

View File

@ -67,10 +67,15 @@ class EmailStageView(ChallengeStageView):
"user": pending_user,
"identifier": f"ak-email-stage-{current_stage.name}-{pending_user}",
}
tokens = Token.filter_not_expired(**token_filters)
# Don't check for validity here, we only care if the token exists
tokens = Token.objects.filter(**token_filters)
if not tokens.exists():
return Token.objects.create(expires=now() + valid_delta, **token_filters)
return tokens.first()
token = tokens.first()
# Check if token is expired and rotate key if so
if token.is_expired:
token.expire_action()
return token
def send_email(self):
"""Helper function that sends the actual email. Implies that you've

View File

@ -91,7 +91,7 @@ def send_mail(
messages=["Successfully sent Mail."],
)
)
except (SMTPException, ConnectionError) as exc:
except (SMTPException, ConnectionError, OSError) as exc:
LOGGER.debug("Error sending email, retrying...", exc=exc)
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
raise exc

View File

@ -1,18 +1,23 @@
"""invitation stage logic"""
from copy import deepcopy
from typing import Optional
from deepmerge import always_merger
from django.http import HttpRequest, HttpResponse
from django.http.response import HttpResponseBadRequest
from django.shortcuts import get_object_or_404
from structlog.stdlib import get_logger
from authentik.flows.models import in_memory_stage
from authentik.flows.stage import StageView
from authentik.flows.views import SESSION_KEY_GET
from authentik.stages.invitation.models import Invitation, InvitationStage
from authentik.stages.invitation.signals import invitation_used
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
LOGGER = get_logger()
INVITATION_TOKEN_KEY = "token" # nosec
INVITATION_IN_EFFECT = "invitation_in_effect"
INVITATION = "invitation"
class InvitationStageView(StageView):
@ -39,9 +44,37 @@ class InvitationStageView(StageView):
return self.executor.stage_invalid()
invite: Invitation = get_object_or_404(Invitation, pk=token)
self.executor.plan.context[PLAN_CONTEXT_PROMPT] = deepcopy(invite.fixed_data)
self.executor.plan.context[INVITATION_IN_EFFECT] = True
self.executor.plan.context[INVITATION] = invite
context = {}
always_merger.merge(
context, self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {})
)
always_merger.merge(context, invite.fixed_data)
self.executor.plan.context[PLAN_CONTEXT_PROMPT] = context
invitation_used.send(sender=self, request=request, invitation=invite)
if invite.single_use:
invite.delete()
self.executor.plan.append_stage(in_memory_stage(InvitationFinalStageView))
return self.executor.stage_ok()
class InvitationFinalStageView(StageView):
"""Final stage which is injected by invitation stage. Deletes
the used invitation."""
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Call get as this request may be called with post"""
return self.get(request, *args, **kwargs)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Delete invitation if single_use is active"""
invitation: Invitation = self.executor.plan.context.get(INVITATION, None)
if not invitation:
LOGGER.warning("InvitationFinalStageView stage called without invitation")
return HttpResponseBadRequest
if not invitation.single_use:
return self.executor.stage_ok()
invitation.delete()
return self.executor.stage_ok()

View File

@ -156,11 +156,13 @@ class TestUserLoginStage(TestCase):
base_url = reverse(
"authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}
)
response = self.client.get(base_url)
response = self.client.get(base_url, follow=True)
session = self.client.session
plan: FlowPlan = session[SESSION_KEY_PLAN]
self.assertEqual(plan.context[PLAN_CONTEXT_PROMPT], data)
self.assertEqual(
plan.context[PLAN_CONTEXT_PROMPT], data | plan.context[PLAN_CONTEXT_PROMPT]
)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(

View File

@ -21,7 +21,7 @@ services:
networks:
- internal
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.7.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.7.3}
restart: unless-stopped
command: server
environment:
@ -44,7 +44,7 @@ services:
- "0.0.0.0:9000:9000"
- "0.0.0.0:9443:9443"
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.7.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.7.3}
restart: unless-stopped
command: worker
networks:

View File

@ -17,4 +17,4 @@ func OutpostUserAgent() string {
return fmt.Sprintf("authentik-outpost@%s (%s)", VERSION, BUILD())
}
const VERSION = "2021.7.1"
const VERSION = "2021.7.3"

View File

@ -116,7 +116,7 @@ func (ac *APIController) startWSHealth() {
func (ac *APIController) startIntervalUpdater() {
logger := ac.logger.WithField("loop", "interval-updater")
ticker := time.NewTicker(time.Second * 150)
ticker := time.NewTicker(5 * time.Minute)
for ; true; <-ticker.C {
err := ac.Server.Refresh()
if err != nil {

View File

@ -75,7 +75,7 @@ func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPRes
pi.boundUsersMutex.Lock()
cs := pi.SearchAccessCheck(userInfo.User)
pi.boundUsers[req.BindDN] = UserFlags{
UserInfo: userInfo.User,
UserPk: userInfo.User.Pk,
CanSearch: cs != nil,
}
if pi.boundUsers[req.BindDN].CanSearch {
@ -88,7 +88,7 @@ func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPRes
}
// SearchAccessCheck Check if the current user is allowed to search
func (pi *ProviderInstance) SearchAccessCheck(user api.User) *string {
func (pi *ProviderInstance) SearchAccessCheck(user api.UserSelf) *string {
for _, group := range user.Groups {
for _, allowedGroup := range pi.searchAllowedGroups {
pi.log.WithField("userGroup", group.Pk).WithField("allowedGroup", allowedGroup).Trace("Checking search access")

View File

@ -11,9 +11,17 @@ import (
"goauthentik.io/api"
)
func (pi *ProviderInstance) SearchMe(user api.User) (ldap.ServerSearchResult, error) {
func (pi *ProviderInstance) SearchMe(req SearchRequest, f UserFlags) (ldap.ServerSearchResult, error) {
if f.UserInfo == nil {
u, _, err := pi.s.ac.Client.CoreApi.CoreUsersRetrieve(req.ctx, f.UserInfo.Pk).Execute()
if err != nil {
req.log.WithError(err).Warning("Failed to get user info")
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Failed to get userinfo")
}
f.UserInfo = &u
}
entries := make([]*ldap.Entry, 1)
entries[0] = pi.UserEntry(user)
entries[0] = pi.UserEntry(*f.UserInfo)
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
}
@ -42,7 +50,7 @@ func (pi *ProviderInstance) Search(req SearchRequest) (ldap.ServerSearchResult,
}
if !flags.CanSearch {
pi.log.Debug("User can't search, showing info about user")
return pi.SearchMe(flags.UserInfo)
return pi.SearchMe(req, flags)
}
accsp.Finish()

View File

@ -39,7 +39,8 @@ type ProviderInstance struct {
}
type UserFlags struct {
UserInfo api.User
UserInfo *api.User
UserPk int32
CanSearch bool
}

View File

@ -51,7 +51,8 @@ logconfig_dict = {
if SERVICE_HOST_ENV_NAME in os.environ:
workers = 2
else:
workers = int(os.environ.get("WORKERS", cpu_count() * 2 + 1))
default_workers = max(cpu_count() * 0.25, 1) + 1 # Minimum of 2 workers
workers = int(os.environ.get("WORKERS", default_workers))
threads = 4
warnings.simplefilter("once")

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2021.7.1
version: 2021.7.3
description: Making authentication simple.
contact:
email: hello@beryju.org
@ -3185,6 +3185,38 @@ paths:
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/api/v2beta/core/users/update_self/:
put:
operationId: core_users_update_self_update
description: Allow users to change information on their own profile
tags:
- core
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UserSelfRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/UserSelfRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/UserSelfRequest'
required: true
security:
- authentik: []
- cookieAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/SessionUser'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/api/v2beta/crypto/certificatekeypairs/:
get:
operationId: crypto_certificatekeypairs_list
@ -27577,9 +27609,9 @@ components:
and, if this user is being impersonated, the original user in the `original` property.
properties:
user:
$ref: '#/components/schemas/User'
$ref: '#/components/schemas/UserSelf'
original:
$ref: '#/components/schemas/User'
$ref: '#/components/schemas/UserSelf'
required:
- user
SetIconRequest:
@ -28478,6 +28510,82 @@ components:
required:
- name
- username
UserSelf:
type: object
description: |-
User Serializer for information a user can retrieve about themselves and
update about themselves
properties:
pk:
type: integer
readOnly: true
title: ID
username:
type: string
description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_
only.
pattern: ^[\w.@+-]+$
maxLength: 150
name:
type: string
description: User's display name.
is_active:
type: boolean
readOnly: true
title: Active
description: Designates whether this user should be treated as active. Unselect
this instead of deleting accounts.
is_superuser:
type: boolean
readOnly: true
groups:
type: array
items:
$ref: '#/components/schemas/Group'
readOnly: true
email:
type: string
format: email
title: Email address
maxLength: 254
avatar:
type: string
readOnly: true
uid:
type: string
readOnly: true
required:
- avatar
- groups
- is_active
- is_superuser
- name
- pk
- uid
- username
UserSelfRequest:
type: object
description: |-
User Serializer for information a user can retrieve about themselves and
update about themselves
properties:
username:
type: string
description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_
only.
pattern: ^[\w.@+-]+$
maxLength: 150
name:
type: string
description: User's display name.
email:
type: string
format: email
title: Email address
maxLength: 254
required:
- name
- username
UserSetting:
type: object
description: Serializer for User settings for stages and sources

View File

@ -2,7 +2,7 @@ version: '3.7'
services:
chrome:
image: selenium/standalone-chrome:3.141
image: selenium/standalone-chrome:3.141.59-20210713
volumes:
- /dev/shm:/dev/shm
network_mode: host

View File

@ -2,7 +2,7 @@ version: '3.7'
services:
chrome:
image: selenium/standalone-chrome-debug:3.141
image: selenium/standalone-chrome-debug:3.141.59-20210713
volumes:
- /dev/shm:/dev/shm
network_mode: host

View File

@ -1,20 +1,36 @@
"""Test Controllers"""
from typing import Optional
import yaml
from django.test import TestCase
from structlog.stdlib import get_logger
from authentik.flows.models import Flow
from authentik.outposts.controllers.kubernetes import KubernetesController
from authentik.outposts.models import KubernetesServiceConnection, Outpost, OutpostType
from authentik.outposts.tasks import outpost_local_connection
from authentik.providers.proxy.controllers.k8s.ingress import IngressReconciler
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
from authentik.providers.proxy.models import ProxyProvider
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
LOGGER = get_logger()
class TestProxyKubernetes(TestCase):
"""Test Controllers"""
controller: Optional[KubernetesController]
def setUp(self):
# Ensure that local connection have been created
outpost_local_connection()
self.controller = None
def tearDown(self) -> None:
if self.controller:
for log in self.controller.down_with_logs():
LOGGER.info(log)
return super().tearDown()
def test_kubernetes_controller_static(self):
"""Test Kubernetes Controller"""
@ -33,18 +49,26 @@ class TestProxyKubernetes(TestCase):
outpost.providers.add(provider)
outpost.save()
controller = ProxyKubernetesController(outpost, service_connection)
manifest = controller.get_static_deployment()
self.controller = ProxyKubernetesController(outpost, service_connection)
manifest = self.controller.get_static_deployment()
self.assertEqual(len(list(yaml.load_all(manifest, Loader=yaml.SafeLoader))), 4)
def test_kubernetes_controller_deploy(self):
"""Test Kubernetes Controller"""
def test_kubernetes_controller_ingress(self):
"""Test Kubernetes Controller's Ingress"""
provider: ProxyProvider = ProxyProvider.objects.create(
name="test",
internal_host="http://localhost",
external_host="http://localhost",
external_host="https://localhost",
authorization_flow=Flow.objects.first(),
)
provider2: ProxyProvider = ProxyProvider.objects.create(
name="test2",
internal_host="http://otherhost",
external_host="https://otherhost",
mode=ProxyMode.FORWARD_SINGLE,
authorization_flow=Flow.objects.first(),
)
service_connection = KubernetesServiceConnection.objects.first()
outpost: Outpost = Outpost.objects.create(
name="test",
@ -52,8 +76,19 @@ class TestProxyKubernetes(TestCase):
service_connection=service_connection,
)
outpost.providers.add(provider)
outpost.save()
controller = ProxyKubernetesController(outpost, service_connection)
controller.up()
controller.down()
self.controller = ProxyKubernetesController(outpost, service_connection)
ingress_rec = IngressReconciler(self.controller)
ingress = ingress_rec.retrieve()
self.assertEqual(len(ingress.spec.rules), 1)
self.assertEqual(ingress.spec.rules[0].host, "localhost")
# add provider, check again
outpost.providers.add(provider2)
ingress = ingress_rec.retrieve()
self.assertEqual(len(ingress.spec.rules), 2)
self.assertEqual(ingress.spec.rules[0].host, "localhost")
self.assertEqual(ingress.spec.rules[1].host, "otherhost")

View File

@ -33,15 +33,7 @@ export function configureSentry(canDoPpi: boolean = false): Promise<Config> {
}
}
if (hint.originalException instanceof Response) {
const response = hint.originalException as Response;
// We only care about server errors
if (response.status < 500) {
return null;
}
// Need to clone the response, otherwise the .text() and .json() can't be re-used
const resCopy = response.clone();
const body = await resCopy.json();
event.message = `${response.status} ${response.url}: ${JSON.stringify(body)}`
return null;
}
if (event.exception) {
me().then(user => {

View File

@ -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 = "2021.7.1";
export const VERSION = "2021.7.3";
export const PAGE_SIZE = 20;
export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";";

View File

@ -3,18 +3,20 @@ import { EVENT_REFRESH } from "../../constants";
import { Form } from "./Form";
export abstract class ModelForm<T, PKT extends string | number> extends Form<T> {
viewportCheck = true;
abstract loadInstance(pk: PKT): Promise<T>;
@property({attribute: false})
set instancePk(value: PKT) {
this._instancePk = value;
if (this.isInViewport) {
this.loadInstance(value).then(instance => {
this.instance = instance;
this.requestUpdate();
});
if (this.viewportCheck && !this.isInViewport) {
return;
}
this.loadInstance(value).then((instance) => {
this.instance = instance;
this.requestUpdate();
});
}
private _instancePk?: PKT;

View File

@ -1077,7 +1077,7 @@ msgstr "Delete Refresh Code"
msgid "Delete Session"
msgstr "Delete Session"
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
msgid "Delete account"
msgstr "Delete account"
@ -1297,7 +1297,7 @@ msgstr "Either no applications are defined, or you don't have access to any."
#: src/flows/stages/identification/IdentificationStage.ts
#: src/pages/events/TransportForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
#: src/pages/users/UserForm.ts
#: src/pages/users/UserViewPage.ts
msgid "Email"
@ -1434,7 +1434,6 @@ msgstr "Everything is ok."
msgid "Exception"
msgstr "Exception"
#: src/pages/flows/FlowListPage.ts
#: src/pages/flows/FlowViewPage.ts
msgid "Execute"
msgstr "Execute"
@ -1487,7 +1486,6 @@ msgstr "Expiry date"
msgid "Explicit Consent"
msgstr "Explicit Consent"
#: src/pages/flows/FlowListPage.ts
#: src/pages/flows/FlowViewPage.ts
msgid "Export"
msgstr "Export"
@ -2080,6 +2078,10 @@ msgstr "Link to a user with identical email address. Can have security implicati
msgid "Link to a user with identical username address. Can have security implications when a username is used with another source."
msgstr "Link to a user with identical username address. Can have security implications when a username is used with another source."
#: src/pages/stages/invitation/InvitationListLink.ts
msgid "Link to use the invitation."
msgstr "Link to use the invitation."
#: src/pages/sources/oauth/OAuthSourceForm.ts
#: src/pages/sources/plex/PlexSourceForm.ts
msgid "Link users on unique identifier"
@ -2109,7 +2111,7 @@ msgstr "Load servers"
#: src/flows/stages/prompt/PromptStage.ts
#: src/pages/applications/ApplicationViewPage.ts
#: src/pages/applications/ApplicationViewPage.ts
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
#: src/utils.ts
msgid "Loading"
msgstr "Loading"
@ -2168,6 +2170,7 @@ msgstr "Loading"
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/invitation/InvitationListLink.ts
#: src/pages/stages/password/PasswordStageForm.ts
#: src/pages/stages/prompt/PromptStageForm.ts
#: src/pages/stages/prompt/PromptStageForm.ts
@ -2397,7 +2400,7 @@ msgstr "My Applications"
#: src/pages/stages/user_login/UserLoginStageForm.ts
#: src/pages/stages/user_logout/UserLogoutStageForm.ts
#: src/pages/stages/user_write/UserWriteStageForm.ts
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
#: src/pages/users/UserForm.ts
#: src/pages/users/UserListPage.ts
#: src/pages/users/UserViewPage.ts
@ -3010,6 +3013,10 @@ msgstr "RSA-SHA384"
msgid "RSA-SHA512"
msgstr "RSA-SHA512"
#: src/pages/sources/plex/PlexSourceForm.ts
msgid "Re-authenticate with plex"
msgstr "Re-authenticate with plex"
#: src/pages/flows/StageBindingForm.ts
msgid "Re-evaluate policies"
msgstr "Re-evaluate policies"
@ -3109,7 +3116,7 @@ msgstr "Request token URL"
msgid "Required"
msgstr "Required"
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
#: src/pages/users/UserForm.ts
msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
msgstr "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
@ -3263,6 +3270,10 @@ msgstr "Select a provider that this application should use. Alternatively, creat
msgid "Select all rows"
msgstr "Select all rows"
#: src/pages/stages/invitation/InvitationListLink.ts
msgid "Select an enrollment flow"
msgstr "Select an enrollment flow"
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts
msgid "Select an identification method."
msgstr "Select an identification method."
@ -3752,7 +3763,7 @@ msgstr "Successfully updated binding."
msgid "Successfully updated certificate-key pair."
msgstr "Successfully updated certificate-key pair."
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
msgid "Successfully updated details."
msgstr "Successfully updated details."
@ -4284,7 +4295,7 @@ msgstr "Up-to-date!"
#: src/pages/stages/StageListPage.ts
#: src/pages/stages/prompt/PromptListPage.ts
#: src/pages/tenants/TenantListPage.ts
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts
@ -4387,7 +4398,7 @@ msgstr "Update User"
msgid "Update available"
msgstr "Update available"
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSettingsPage.ts
msgid "Update details"
msgstr "Update details"
@ -4520,7 +4531,7 @@ msgstr "User {0}"
msgid "User's avatar"
msgstr "User's avatar"
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
#: src/pages/users/UserForm.ts
msgid "User's display name."
msgstr "User's display name."
@ -4540,7 +4551,7 @@ msgstr "Userinfo URL"
#: src/flows/stages/identification/IdentificationStage.ts
#: src/pages/policies/reputation/UserReputationListPage.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
#: src/pages/users/UserForm.ts
#: src/pages/users/UserViewPage.ts
msgid "Username"

View File

@ -1071,7 +1071,7 @@ msgstr ""
msgid "Delete Session"
msgstr ""
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
msgid "Delete account"
msgstr ""
@ -1289,7 +1289,7 @@ msgstr ""
#: src/flows/stages/identification/IdentificationStage.ts
#: src/pages/events/TransportForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
#: src/pages/users/UserForm.ts
#: src/pages/users/UserViewPage.ts
msgid "Email"
@ -1426,7 +1426,6 @@ msgstr ""
msgid "Exception"
msgstr ""
#: src/pages/flows/FlowListPage.ts
#: src/pages/flows/FlowViewPage.ts
msgid "Execute"
msgstr ""
@ -1479,7 +1478,6 @@ msgstr ""
msgid "Explicit Consent"
msgstr ""
#: src/pages/flows/FlowListPage.ts
#: src/pages/flows/FlowViewPage.ts
msgid "Export"
msgstr ""
@ -2072,6 +2070,10 @@ msgstr ""
msgid "Link to a user with identical username address. Can have security implications when a username is used with another source."
msgstr ""
#: src/pages/stages/invitation/InvitationListLink.ts
msgid "Link to use the invitation."
msgstr ""
#: src/pages/sources/oauth/OAuthSourceForm.ts
#: src/pages/sources/plex/PlexSourceForm.ts
msgid "Link users on unique identifier"
@ -2101,7 +2103,7 @@ msgstr ""
#: src/flows/stages/prompt/PromptStage.ts
#: src/pages/applications/ApplicationViewPage.ts
#: src/pages/applications/ApplicationViewPage.ts
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
#: src/utils.ts
msgid "Loading"
msgstr ""
@ -2160,6 +2162,7 @@ msgstr ""
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/invitation/InvitationListLink.ts
#: src/pages/stages/password/PasswordStageForm.ts
#: src/pages/stages/prompt/PromptStageForm.ts
#: src/pages/stages/prompt/PromptStageForm.ts
@ -2389,7 +2392,7 @@ msgstr ""
#: src/pages/stages/user_login/UserLoginStageForm.ts
#: src/pages/stages/user_logout/UserLogoutStageForm.ts
#: src/pages/stages/user_write/UserWriteStageForm.ts
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
#: src/pages/users/UserForm.ts
#: src/pages/users/UserListPage.ts
#: src/pages/users/UserViewPage.ts
@ -3002,6 +3005,10 @@ msgstr ""
msgid "RSA-SHA512"
msgstr ""
#: src/pages/sources/plex/PlexSourceForm.ts
msgid "Re-authenticate with plex"
msgstr ""
#: src/pages/flows/StageBindingForm.ts
msgid "Re-evaluate policies"
msgstr ""
@ -3101,7 +3108,7 @@ msgstr ""
msgid "Required"
msgstr ""
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
#: src/pages/users/UserForm.ts
msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
msgstr ""
@ -3255,6 +3262,10 @@ msgstr ""
msgid "Select all rows"
msgstr ""
#: src/pages/stages/invitation/InvitationListLink.ts
msgid "Select an enrollment flow"
msgstr ""
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts
msgid "Select an identification method."
msgstr ""
@ -3744,7 +3755,7 @@ msgstr ""
msgid "Successfully updated certificate-key pair."
msgstr ""
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
msgid "Successfully updated details."
msgstr ""
@ -4269,7 +4280,7 @@ msgstr ""
#: src/pages/stages/StageListPage.ts
#: src/pages/stages/prompt/PromptListPage.ts
#: src/pages/tenants/TenantListPage.ts
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts
@ -4372,7 +4383,7 @@ msgstr ""
msgid "Update available"
msgstr ""
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSettingsPage.ts
msgid "Update details"
msgstr ""
@ -4505,7 +4516,7 @@ msgstr ""
msgid "User's avatar"
msgstr ""
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
#: src/pages/users/UserForm.ts
msgid "User's display name."
msgstr ""
@ -4525,7 +4536,7 @@ msgstr ""
#: src/flows/stages/identification/IdentificationStage.ts
#: src/pages/policies/reputation/UserReputationListPage.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/UserSelfForm.ts
#: src/pages/users/UserForm.ts
#: src/pages/users/UserViewPage.ts
msgid "Username"

View File

@ -54,7 +54,7 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
name="authorizationFlow">
<select class="pf-c-form-control">
${until(tenant().then(t => {
new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
ordering: "pk",
designation: FlowsInstancesListDesignationEnum.Authentication,
}).then(flows => {

View File

@ -124,16 +124,16 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
<option value=${UserMatchingModeEnum.Identifier} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.Identifier}>
${t`Link users on unique identifier`}
</option>
<option value=${UserMatchingModeEnum.UsernameLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameLink}>
<option value=${UserMatchingModeEnum.EmailLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailLink}>
${t`Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses`}
</option>
<option value=${UserMatchingModeEnum.UsernameDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameDeny}>
<option value=${UserMatchingModeEnum.EmailDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailDeny}>
${t`Use the user's email address, but deny enrollment when the email address already exists.`}
</option>
<option value=${UserMatchingModeEnum.EmailLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailLink}>
<option value=${UserMatchingModeEnum.UsernameLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameLink}>
${t`Link to a user with identical username address. Can have security implications when a username is used with another source.`}
</option>
<option value=${UserMatchingModeEnum.EmailDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailDeny}>
<option value=${UserMatchingModeEnum.UsernameDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameDeny}>
${t`Use the user's username, but deny enrollment when the username already exists.`}
</option>
</select>

View File

@ -85,7 +85,13 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
${t`Load servers`}
</button>`;
}
return html`<ak-form-element-horizontal name="allowFriends">
return html`
<button class="pf-c-button pf-m-secondary" type="button" @click=${() => {
this.doAuth();
}}>
${t`Re-authenticate with plex`}
</button>
<ak-form-element-horizontal name="allowFriends">
<div class="pf-c-check">
<input type="checkbox" class="pf-c-check__input" ?checked=${first(this.instance?.allowFriends, true)}>
<label class="pf-c-check__label">
@ -140,16 +146,16 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
<option value=${UserMatchingModeEnum.Identifier} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.Identifier}>
${t`Link users on unique identifier`}
</option>
<option value=${UserMatchingModeEnum.UsernameLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameLink}>
<option value=${UserMatchingModeEnum.EmailLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailLink}>
${t`Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses`}
</option>
<option value=${UserMatchingModeEnum.UsernameDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameDeny}>
<option value=${UserMatchingModeEnum.EmailDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailDeny}>
${t`Use the user's email address, but deny enrollment when the email address already exists.`}
</option>
<option value=${UserMatchingModeEnum.EmailLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailLink}>
<option value=${UserMatchingModeEnum.UsernameLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameLink}>
${t`Link to a user with identical username address. Can have security implications when a username is used with another source.`}
</option>
<option value=${UserMatchingModeEnum.EmailDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailDeny}>
<option value=${UserMatchingModeEnum.UsernameDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameDeny}>
${t`Use the user's username, but deny enrollment when the username already exists.`}
</option>
</select>

View File

@ -0,0 +1,67 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { until } from "lit-html/directives/until";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import AKGlobal from "../../../authentik.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
import { FlowsApi, FlowsInstancesListDesignationEnum } from "authentik-api";
import { DEFAULT_CONFIG } from "../../../api/Config";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
@customElement("ak-stage-invitation-list-link")
export class InvitationListLink extends LitElement {
@property()
invitation?: string
@property()
selectedFlow?: string;
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFFormControl, PFFlex, PFDescriptionList, AKGlobal];
}
renderLink(): string {
return `${window.location.protocol}//${window.location.host}/if/flow/${this.selectedFlow}/?token=${this.invitation}`;
}
render(): TemplateResult {
return html`<dl class="pf-c-description-list pf-m-horizontal">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${t`Select an enrollment flow`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<select class="pf-c-form-control" @change=${(ev: Event) => {
const current = (ev.target as HTMLInputElement).value;
this.selectedFlow = current;
}}>
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
ordering: "pk",
designation: FlowsInstancesListDesignationEnum.Enrollment,
}).then(flows => {
return flows.results.map(flow => {
return html`<option value=${flow.slug} ?selected=${flow.slug === this.selectedFlow}>${flow.slug}</option>`;
});
}), html`<option>${t`Loading...`}</option>`)}
</select>
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${t`Link to use the invitation.`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<input class="pf-c-form-control" readonly type="text" value=${this.renderLink()} />
</div>
</dd>
</div>
</dl>`;
}
}

View File

@ -8,6 +8,7 @@ import "../../../elements/buttons/SpinnerButton";
import "../../../elements/forms/DeleteForm";
import "../../../elements/forms/ModalForm";
import "./InvitationForm";
import "./InvitationListLink";
import { TableColumn } from "../../../elements/table/Table";
import { PAGE_SIZE } from "../../../constants";
import { Invitation, StagesApi } from "authentik-api";
@ -15,6 +16,8 @@ import { DEFAULT_CONFIG } from "../../../api/Config";
@customElement("ak-stage-invitation-list")
export class InvitationListPage extends TablePage<Invitation> {
expandable = true;
searchEnabled(): boolean {
return true;
}
@ -53,7 +56,7 @@ export class InvitationListPage extends TablePage<Invitation> {
return [
html`${item.pk}`,
html`${item.createdBy?.username}`,
html`${item.expires?.toLocaleString()}`,
html`${item.expires?.toLocaleString() || "-"}`,
html`
<ak-forms-delete
.obj=${item}
@ -75,6 +78,18 @@ export class InvitationListPage extends TablePage<Invitation> {
];
}
renderExpanded(item: Invitation): TemplateResult {
return html`
<td role="cell" colspan="3">
<div class="pf-c-table__expandable-row-content">
<ak-stage-invitation-list-link invitation=${item.pk}></ak-stage-invitation-list-link>
</div>
</td>
<td></td>
<td></td>
<td></td>`;
}
renderToolbar(): TemplateResult {
return html`
<ak-forms-modal>

View File

@ -1,101 +0,0 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import AKGlobal from "../../authentik.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import { CoreApi, User } from "authentik-api";
import { me } from "../../api/Users";
import { ifDefined } from "lit-html/directives/if-defined";
import { DEFAULT_CONFIG, tenant } from "../../api/Config";
import "../../elements/forms/FormElement";
import "../../elements/EmptyState";
import "../../elements/forms/Form";
import "../../elements/forms/HorizontalFormElement";
import { until } from "lit-html/directives/until";
@customElement("ak-user-details")
export class UserDetailsPage extends LitElement {
static get styles(): CSSResult[] {
return [PFBase, PFCard, PFForm, PFFormControl, PFButton, AKGlobal];
}
@property({attribute: false})
user?: User;
firstUpdated(): void {
me().then((user) => {
this.user = user.user;
});
}
render(): TemplateResult {
if (!this.user) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
}
return html`<div class="pf-c-card">
<div class="pf-c-card__title">
${t`Update details`}
</div>
<div class="pf-c-card__body">
<ak-form
successMessage=${t`Successfully updated details.`}
.send=${(data: unknown) => {
return new CoreApi(DEFAULT_CONFIG).coreUsersUpdate({
id: this.user?.pk || 0,
userRequest: data as User
});
}}>
<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal
label=${t`Username`}
?required=${true}
name="username">
<input type="text" value="${ifDefined(this.user?.username)}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${t`Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Name`}
?required=${true}
name="name">
<input type="text" value="${ifDefined(this.user?.name)}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${t`User's display name.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Email`}
?required=${true}
name="email">
<input type="email" value="${ifDefined(this.user?.email)}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<div class="pf-c-form__group pf-m-action">
<div class="pf-c-form__horizontal-group">
<div class="pf-c-form__actions">
<button class="pf-c-button pf-m-primary">
${t`Update`}
</button>
${until(tenant().then(tenant => {
if (tenant.flowUnenrollment) {
return html`<a class="pf-c-button pf-m-danger"
href="/if/flow/${tenant.flowUnenrollment}">
${t`Delete account`}
</a>`;
}
return html``;
}))}
</div>
</div>
</div>
</form>
</ak-form>
</div>
</div>`;
}
}

View File

@ -0,0 +1,100 @@
import { t } from "@lingui/macro";
import { customElement, html, TemplateResult } from "lit-element";
import { CoreApi, UserSelf } from "authentik-api";
import { ifDefined } from "lit-html/directives/if-defined";
import { DEFAULT_CONFIG, tenant } from "../../api/Config";
import "../../elements/forms/FormElement";
import "../../elements/EmptyState";
import "../../elements/forms/Form";
import "../../elements/forms/HorizontalFormElement";
import { until } from "lit-html/directives/until";
import { ModelForm } from "../../elements/forms/ModelForm";
@customElement("ak-user-self-form")
export class UserSelfForm extends ModelForm<UserSelf, number> {
viewportCheck = false;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadInstance(pk: number): Promise<UserSelf> {
return new CoreApi(DEFAULT_CONFIG).coreUsersMeRetrieve().then((su) => {
return su.user;
});
}
getSuccessMessage(): string {
return t`Successfully updated details.`;
}
send = (data: UserSelf): Promise<UserSelf> => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersUpdateSelfUpdate({
userSelfRequest: data,
})
.then((su) => {
return su.user;
});
};
renderForm(): TemplateResult {
if (!this.instance) {
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Username`} ?required=${true} name="username">
<input
type="text"
value="${ifDefined(this.instance?.username)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">${t`User's display name.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Email`} name="email">
<input
type="email"
value="${ifDefined(this.instance?.email)}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
<div class="pf-c-form__group pf-m-action">
<div class="pf-c-form__horizontal-group">
<div class="pf-c-form__actions">
<button
@click=${(ev: Event) => {
return this.submit(ev);
}}
class="pf-c-button pf-m-primary"
>
${t`Update`}
</button>
${until(
tenant().then((tenant) => {
if (tenant.flowUnenrollment) {
return html`<a
class="pf-c-button pf-m-danger"
href="/if/flow/${tenant.flowUnenrollment}"
>
${t`Delete account`}
</a>`;
}
return html``;
}),
)}
</div>
</div>
</div>
</form>`;
}
}

View File

@ -20,7 +20,7 @@ import { ifDefined } from "lit-html/directives/if-defined";
import "../../elements/Tabs";
import "../../elements/PageHeader";
import "./tokens/UserTokenList";
import "./UserDetailsPage";
import "./UserSelfForm";
import "./settings/UserSettingsAuthenticatorDuo";
import "./settings/UserSettingsAuthenticatorStatic";
import "./settings/UserSettingsAuthenticatorTOTP";
@ -95,8 +95,17 @@ export class UserSettingsPage extends LitElement {
description=${t`Configure settings relevant to your user profile.`}>
</ak-page-header>
<ak-tabs ?vertical="${true}" style="height: 100%;">
<section slot="page-details" data-tab-title="${t`User details`}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<ak-user-details></ak-user-details>
<section
slot="page-details"
data-tab-title="${t`User details`}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__title">${t`Update details`}</div>
<div class="pf-c-card__body">
<ak-user-self-form .instancePk=${1}></ak-user-self-form>
</div>
</div>
</section>
<section slot="page-tokens" data-tab-title="${t`Tokens`}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<ak-user-token-list></ak-user-token-list>

View File

@ -58,9 +58,8 @@ export class UserForm extends ModelForm<User, number> {
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Email`}
?required=${true}
name="email">
<input type="email" autocomplete="off" value="${ifDefined(this.instance?.email)}" class="pf-c-form-control" required>
<input type="email" autocomplete="off" value="${ifDefined(this.instance?.email)}" class="pf-c-form-control">
</ak-form-element-horizontal>
<ak-form-element-horizontal
name="isActive">

View File

@ -12,11 +12,11 @@ This installation method is for test-setups and small-scale productive setups.
## Preparation
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/goauthentik/authentik/version/2021.7.1/docker-compose.yml). Place it in a directory of your choice.
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/goauthentik/authentik/version/2021.7.3/docker-compose.yml). Place it in a directory of your choice.
To optionally enable error-reporting, run `echo AUTHENTIK_ERROR_REPORTING__ENABLED=true >> .env`
To optionally deploy a different version run `echo AUTHENTIK_TAG=2021.7.1 >> .env`
To optionally deploy a different version run `echo AUTHENTIK_TAG=2021.7.3 >> .env`
If this is a fresh authentik install run the following commands to generate a password:

View File

@ -11,7 +11,7 @@ version: "3.5"
services:
authentik_proxy:
image: ghcr.io/goauthentik/proxy:2021.7.1
image: ghcr.io/goauthentik/proxy:2021.7.3
ports:
- 4180:4180
- 4443:4443
@ -21,7 +21,7 @@ services:
AUTHENTIK_TOKEN: token-generated-by-authentik
# Or, for the LDAP Outpost
authentik_proxy:
image: ghcr.io/goauthentik/ldap:2021.7.1
image: ghcr.io/goauthentik/ldap:2021.7.3
ports:
- 389:3389
environment:

View File

@ -14,7 +14,7 @@ metadata:
app.kubernetes.io/instance: __OUTPOST_NAME__
app.kubernetes.io/managed-by: goauthentik.io
app.kubernetes.io/name: authentik-proxy
app.kubernetes.io/version: 2021.7.1
app.kubernetes.io/version: 2021.7.3
name: authentik-outpost-api
stringData:
authentik_host: "__AUTHENTIK_URL__"
@ -29,7 +29,7 @@ metadata:
app.kubernetes.io/instance: __OUTPOST_NAME__
app.kubernetes.io/managed-by: goauthentik.io
app.kubernetes.io/name: authentik-proxy
app.kubernetes.io/version: 2021.7.1
app.kubernetes.io/version: 2021.7.3
name: authentik-outpost
spec:
ports:
@ -54,7 +54,7 @@ metadata:
app.kubernetes.io/instance: __OUTPOST_NAME__
app.kubernetes.io/managed-by: goauthentik.io
app.kubernetes.io/name: authentik-proxy
app.kubernetes.io/version: 2021.7.1
app.kubernetes.io/version: 2021.7.3
name: authentik-outpost
spec:
selector:
@ -62,14 +62,14 @@ spec:
app.kubernetes.io/instance: __OUTPOST_NAME__
app.kubernetes.io/managed-by: goauthentik.io
app.kubernetes.io/name: authentik-proxy
app.kubernetes.io/version: 2021.7.1
app.kubernetes.io/version: 2021.7.3
template:
metadata:
labels:
app.kubernetes.io/instance: __OUTPOST_NAME__
app.kubernetes.io/managed-by: goauthentik.io
app.kubernetes.io/name: authentik-proxy
app.kubernetes.io/version: 2021.7.1
app.kubernetes.io/version: 2021.7.3
spec:
containers:
- env:
@ -88,7 +88,7 @@ spec:
secretKeyRef:
key: authentik_host_insecure
name: authentik-outpost-api
image: ghcr.io/goauthentik/proxy:2021.7.1
image: ghcr.io/goauthentik/proxy:2021.7.3
name: proxy
ports:
- containerPort: 4180
@ -110,7 +110,7 @@ metadata:
app.kubernetes.io/instance: __OUTPOST_NAME__
app.kubernetes.io/managed-by: goauthentik.io
app.kubernetes.io/name: authentik-proxy
app.kubernetes.io/version: 2021.7.1
app.kubernetes.io/version: 2021.7.3
name: authentik-outpost
spec:
rules: