Compare commits
34 Commits
version/20
...
version-20
Author | SHA1 | Date | |
---|---|---|---|
5a7508d2e0 | |||
9c31ea1aa6 | |||
18211a2033 | |||
b4cfc56e5e | |||
8e797fa76b | |||
1b91543add | |||
1cd59be8dc | |||
66bb68a747 | |||
aa4f7fb2b6 | |||
4f1c11c5ef | |||
add7a80fdc | |||
aac91c2e9d | |||
85e86351cd | |||
d767504474 | |||
f84cd6208c | |||
1ec540ea9a | |||
29fe731bbf | |||
26e66969c9 | |||
fe629f8b51 | |||
72b7642c5a | |||
a97f842112 | |||
16e6e4c3b7 | |||
dc0d715885 | |||
7ecd57ecff | |||
0cb4d64b57 | |||
a4fd58a0db | |||
fb6e8ca1eb | |||
4c41948e75 | |||
a5c8caf909 | |||
970655ab21 | |||
c8c7202c61 | |||
a3981dd3cd | |||
affafc31cf | |||
602aed674b |
@ -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>.*)
|
||||
|
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
@ -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
2
.gitignore
vendored
@ -200,4 +200,4 @@ media/
|
||||
*mmdb
|
||||
|
||||
.idea/
|
||||
api/
|
||||
/api/
|
||||
|
@ -1,3 +1,3 @@
|
||||
"""authentik"""
|
||||
__version__ = "2021.7.1"
|
||||
__version__ = "2021.7.3"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
@ -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",
|
||||
|
@ -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=[])
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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))
|
||||
|
54
authentik/providers/oauth2/tests/test_jwks.py
Normal file
54
authentik/providers/oauth2/tests/test_jwks.py
Normal 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), {})
|
@ -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"] = [
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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(
|
||||
|
@ -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"
|
||||
|
@ -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)"""
|
||||
|
@ -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(
|
||||
|
10
authentik/sources/plex/settings.py
Normal file
10
authentik/sources/plex/settings.py
Normal 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"},
|
||||
},
|
||||
}
|
43
authentik/sources/plex/tasks.py
Normal file
43
authentik/sources/plex/tasks.py
Normal 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()
|
@ -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()
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
|
@ -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()
|
||||
|
||||
|
@ -39,7 +39,8 @@ type ProviderInstance struct {
|
||||
}
|
||||
|
||||
type UserFlags struct {
|
||||
UserInfo api.User
|
||||
UserInfo *api.User
|
||||
UserPk int32
|
||||
CanSearch bool
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
114
schema.yml
114
schema.yml
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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 => {
|
||||
|
@ -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 = ";";
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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 => {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
67
web/src/pages/stages/invitation/InvitationListLink.ts
Normal file
67
web/src/pages/stages/invitation/InvitationListLink.ts
Normal 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>`;
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
|
@ -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>`;
|
||||
}
|
||||
|
||||
}
|
100
web/src/pages/user-settings/UserSelfForm.ts
Normal file
100
web/src/pages/user-settings/UserSelfForm.ts
Normal 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>`;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
Reference in New Issue
Block a user