enterprise: initial enterprise (#5721)

* initial

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

* add user type

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

* add external users

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

* add ui, add more logic, add public JWT validation key

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

* revert to not use install_id as session jwt signing key

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

* fix more

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

* switch to PKI

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

* add more licensing stuff

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

* add install ID to form

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

* fix bugs

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

* start adding tests

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

* fixes

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

* use x5c correctly

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

* license checks

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

* use production CA

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

* more

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

* more UI stuff

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

* rename to summary

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

* update locale, improve ui

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

* add direct button

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

* update link

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

* format and such

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

* remove old attributes from ldap

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

* remove is_enterprise_licensed

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

* fix

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

* fix admin interface styling issue

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

* Update authentik/core/models.py

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Jens L. <jens@beryju.org>

* fix default case

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
This commit is contained in:
Jens L
2023-07-17 17:57:08 +02:00
committed by GitHub
parent cf799fca03
commit 41af486006
56 changed files with 2534 additions and 128 deletions

View File

@ -59,7 +59,6 @@ from authentik.core.middleware import (
SESSION_KEY_IMPERSONATE_USER,
)
from authentik.core.models import (
USER_ATTRIBUTE_SA,
USER_ATTRIBUTE_TOKEN_EXPIRING,
USER_PATH_SERVICE_ACCOUNT,
AuthenticatedSession,
@ -67,6 +66,7 @@ from authentik.core.models import (
Token,
TokenIntents,
User,
UserTypes,
)
from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import FlowNonApplicableException
@ -147,6 +147,18 @@ class UserSerializer(ModelSerializer):
raise ValidationError(_("No empty segments in user path allowed."))
return path
def validate_type(self, user_type: str) -> str:
"""Validate user type, internal_service_account is an internal value"""
if (
self.instance
and self.instance.type == UserTypes.INTERNAL_SERVICE_ACCOUNT
and user_type != UserTypes.INTERNAL_SERVICE_ACCOUNT.value
):
raise ValidationError("Can't change internal service account to other user type.")
if not self.instance and user_type == UserTypes.INTERNAL_SERVICE_ACCOUNT.value:
raise ValidationError("Setting a user to internal service account is not allowed.")
return user_type
class Meta:
model = User
fields = [
@ -163,6 +175,7 @@ class UserSerializer(ModelSerializer):
"attributes",
"uid",
"path",
"type",
]
extra_kwargs = {
"name": {"allow_blank": True},
@ -211,6 +224,7 @@ class UserSelfSerializer(ModelSerializer):
"avatar",
"uid",
"settings",
"type",
]
extra_kwargs = {
"is_active": {"read_only": True},
@ -329,6 +343,7 @@ class UsersFilter(FilterSet):
"attributes",
"groups_by_name",
"groups_by_pk",
"type",
]
@ -421,7 +436,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
user: User = User.objects.create(
username=username,
name=username,
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: expiring},
type=UserTypes.SERVICE_ACCOUNT,
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: expiring},
path=USER_PATH_SERVICE_ACCOUNT,
)
user.set_unusable_password()

View File

@ -0,0 +1,43 @@
# Generated by Django 4.1.7 on 2023-05-21 11:44
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_user_type(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
User = apps.get_model("authentik_core", "User")
from authentik.core.models import UserTypes
for user in User.objects.using(db_alias).all():
user.type = UserTypes.DEFAULT
if "goauthentik.io/user/service-account" in user.attributes:
user.type = UserTypes.SERVICE_ACCOUNT
if "goauthentik.io/user/override-ips" in user.attributes:
user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
user.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0029_provider_backchannel_applications_and_more"),
]
operations = [
migrations.AddField(
model_name="user",
name="type",
field=models.TextField(
choices=[
("default", "Default"),
("external", "External"),
("service_account", "Service Account"),
("internal_service_account", "Internal Service Account"),
],
default="default",
),
),
migrations.RunPython(migrate_user_type),
]

View File

@ -36,7 +36,6 @@ from authentik.root.install_id import get_install_id
LOGGER = get_logger()
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
USER_ATTRIBUTE_GENERATED = "goauthentik.io/user/generated"
USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires"
USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout"
@ -45,8 +44,6 @@ USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec
USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username"
USER_ATTRIBUTE_CHANGE_NAME = "goauthentik.io/user/can-change-name"
USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
USER_PATH_SYSTEM_PREFIX = "goauthentik.io"
USER_PATH_SERVICE_ACCOUNT = USER_PATH_SYSTEM_PREFIX + "/service-accounts"
@ -66,6 +63,21 @@ def default_token_key():
return generate_id(int(CONFIG.y("default_token_length")))
class UserTypes(models.TextChoices):
"""User types, both for grouping, licensing and permissions in the case
of the internal_service_account"""
DEFAULT = "default"
EXTERNAL = "external"
# User-created service accounts
SERVICE_ACCOUNT = "service_account"
# Special user type for internally managed and created service
# accounts, such as outpost users
INTERNAL_SERVICE_ACCOUNT = "internal_service_account"
class Group(SerializerModel):
"""Custom Group model which supports a basic hierarchy"""
@ -149,6 +161,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
uuid = models.UUIDField(default=uuid4, editable=False, unique=True)
name = models.TextField(help_text=_("User's display name."))
path = models.TextField(default="users")
type = models.TextField(choices=UserTypes.choices, default=UserTypes.DEFAULT)
sources = models.ManyToManyField("Source", through="UserSourceConnection")
ak_groups = models.ManyToManyField("Group", related_name="users")

View File

@ -1,6 +1,4 @@
"""authentik core signals"""
from typing import TYPE_CHECKING
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
@ -10,16 +8,13 @@ from django.db.models.signals import post_save, pre_delete, pre_save
from django.dispatch import receiver
from django.http.request import HttpRequest
from authentik.core.models import Application, AuthenticatedSession, BackchannelProvider
from authentik.core.models import Application, AuthenticatedSession, BackchannelProvider, User
# Arguments: user: User, password: str
password_changed = Signal()
# Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage
login_failed = Signal()
if TYPE_CHECKING:
from authentik.core.models import User
@receiver(post_save, sender=Application)
def post_save_application(sender: type[Model], instance, created: bool, **_):
@ -35,7 +30,7 @@ def post_save_application(sender: type[Model], instance, created: bool, **_):
@receiver(user_logged_in)
def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
def user_logged_in_session(sender, request: HttpRequest, user: User, **_):
"""Create an AuthenticatedSession from request"""
session = AuthenticatedSession.from_request(request, user)
@ -44,7 +39,7 @@ def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
@receiver(user_logged_out)
def user_logged_out_session(sender, request: HttpRequest, user: "User", **_):
def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
"""Delete AuthenticatedSession if it exists"""
AuthenticatedSession.objects.filter(session_key=request.session.session_key).delete()

View File

@ -8,11 +8,11 @@ from django.urls.base import reverse
from rest_framework.test import APITestCase
from authentik.core.models import (
USER_ATTRIBUTE_SA,
USER_ATTRIBUTE_TOKEN_EXPIRING,
AuthenticatedSession,
Token,
User,
UserTypes,
)
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
from authentik.flows.models import FlowDesignation
@ -141,7 +141,8 @@ class TestUsersAPI(APITestCase):
user_filter = User.objects.filter(
username="test-sa",
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
type=UserTypes.SERVICE_ACCOUNT,
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True},
)
self.assertTrue(user_filter.exists())
user: User = user_filter.first()
@ -166,7 +167,8 @@ class TestUsersAPI(APITestCase):
user_filter = User.objects.filter(
username="test-sa",
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: False, USER_ATTRIBUTE_SA: True},
type=UserTypes.SERVICE_ACCOUNT,
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: False},
)
self.assertTrue(user_filter.exists())
user: User = user_filter.first()
@ -192,7 +194,8 @@ class TestUsersAPI(APITestCase):
user_filter = User.objects.filter(
username="test-sa",
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
type=UserTypes.SERVICE_ACCOUNT,
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True},
)
self.assertTrue(user_filter.exists())
user: User = user_filter.first()
@ -218,7 +221,8 @@ class TestUsersAPI(APITestCase):
user_filter = User.objects.filter(
username="test-sa",
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
type=UserTypes.SERVICE_ACCOUNT,
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True},
)
self.assertTrue(user_filter.exists())
user: User = user_filter.first()