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:
@ -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()
|
||||
|
||||
43
authentik/core/migrations/0030_user_type.py
Normal file
43
authentik/core/migrations/0030_user_type.py
Normal 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),
|
||||
]
|
||||
@ -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")
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
Reference in New Issue
Block a user