diff --git a/authentik/endpoints/__init__.py b/authentik/endpoints/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/authentik/endpoints/apps.py b/authentik/endpoints/apps.py new file mode 100644 index 0000000000..dbb5d42e18 --- /dev/null +++ b/authentik/endpoints/apps.py @@ -0,0 +1,12 @@ +"""authentik endpoints app config""" + +from authentik.blueprints.apps import ManagedAppConfig + + +class AuthentikEndpointsConfig(ManagedAppConfig): + """authentik endpoints app config""" + + name = "authentik.endpoints" + label = "authentik_endpoints" + verbose_name = "authentik Endpoints" + default = True diff --git a/authentik/endpoints/common_data.py b/authentik/endpoints/common_data.py new file mode 100644 index 0000000000..33c2ccd359 --- /dev/null +++ b/authentik/endpoints/common_data.py @@ -0,0 +1,47 @@ +from enum import Enum + +from pydantic import BaseModel + + +class UNSUPPORTED(BaseModel): + pass + + +class OSFamily(Enum): + + linux = "linux" + unix = "unix" + bsd = "bsd" + windows = "windows" + macOS = "mac_os" + android = "android" + iOS = "i_os" + other = "other" + +class CommonDeviceData(BaseModel): + class Disk(BaseModel): + encryption: bool + + class OS(BaseModel): + firewall_enabled: bool + family: OSFamily + name: str + version: str + + class Network(BaseModel): + hostname: str + dns_servers: list[str] + + class Hardware(BaseModel): + model: str + manufacturer: str + + class Software(BaseModel): + name: str + version: str + + os: OS | UNSUPPORTED + disks: list[Disk] | UNSUPPORTED + network: Network | UNSUPPORTED + hardware: Hardware | UNSUPPORTED + software: list[Software] | UNSUPPORTED diff --git a/authentik/endpoints/connector.py b/authentik/endpoints/connector.py new file mode 100644 index 0000000000..247469fc49 --- /dev/null +++ b/authentik/endpoints/connector.py @@ -0,0 +1,16 @@ +from authentik.blueprints import models + + +class EnrollmentMethods(models.TextChoices): + AUTOMATIC_USER = "automatic_user" # Automatically enrolled through user action + AUTOMATIC_API = "automatic_api" # Automatically enrolled through connector integration + MANUAL_USER = "manual_user" # Manually enrolled + + +class BaseConnector: + + def __init__(self) -> None: + pass + + def supported_enrollment_methods(self) -> list[EnrollmentMethods]: + return [] diff --git a/authentik/endpoints/connectors/__init__.py b/authentik/endpoints/connectors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/authentik/endpoints/connectors/google_chrome/__init__.py b/authentik/endpoints/connectors/google_chrome/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/authentik/endpoints/connectors/google_chrome/apps.py b/authentik/endpoints/connectors/google_chrome/apps.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/authentik/endpoints/connectors/google_chrome/connector.py b/authentik/endpoints/connectors/google_chrome/connector.py new file mode 100644 index 0000000000..cdc9b08db9 --- /dev/null +++ b/authentik/endpoints/connectors/google_chrome/connector.py @@ -0,0 +1,7 @@ +from authentik.endpoints.connector import BaseConnector, EnrollmentMethods + + +class GoogleChromeConnector(BaseConnector): + + def supported_enrollment_methods(self) -> list[EnrollmentMethods]: + return [EnrollmentMethods.AUTOMATIC_USER] diff --git a/authentik/endpoints/connectors/google_chrome/models.py b/authentik/endpoints/connectors/google_chrome/models.py new file mode 100644 index 0000000000..3edefd481a --- /dev/null +++ b/authentik/endpoints/connectors/google_chrome/models.py @@ -0,0 +1,7 @@ +from django.db import models + +from authentik.endpoints.models import Connector + + +class GoogleChromeConnector(Connector): + credentials = models.JSONField() diff --git a/authentik/endpoints/migrations/0001_initial.py b/authentik/endpoints/migrations/0001_initial.py new file mode 100644 index 0000000000..284d0321af --- /dev/null +++ b/authentik/endpoints/migrations/0001_initial.py @@ -0,0 +1,125 @@ +# Generated by Django 5.0.9 on 2024-09-24 19:16 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Connector", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("connector_uuid", models.UUIDField(default=uuid.uuid4)), + ("name", models.TextField()), + ( + "enrollment_method", + models.TextField( + choices=[ + ("automatic_user", "Automatic User"), + ("automatic_api", "Automatic Api"), + ("manual_user", "Manual User"), + ] + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Device", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("device_uuid", models.UUIDField(default=uuid.uuid4)), + ("identifier", models.TextField(unique=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="DeviceConnection", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("device_connection_uuid", models.UUIDField(default=uuid.uuid4)), + ("data", models.JSONField(default=dict)), + ( + "connection", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="authentik_endpoints.connector", + ), + ), + ( + "device", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="authentik_endpoints.device" + ), + ), + ], + ), + migrations.AddField( + model_name="device", + name="connections", + field=models.ManyToManyField( + through="authentik_endpoints.DeviceConnection", to="authentik_endpoints.connector" + ), + ), + migrations.CreateModel( + name="DeviceUser", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("device_user_uuid", models.UUIDField(default=uuid.uuid4)), + ("is_primary", models.BooleanField()), + ( + "device", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="authentik_endpoints.device" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + ], + ), + migrations.AddField( + model_name="device", + name="users", + field=models.ManyToManyField( + through="authentik_endpoints.DeviceUser", to=settings.AUTH_USER_MODEL + ), + ), + ] diff --git a/authentik/endpoints/migrations/__init__.py b/authentik/endpoints/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/authentik/endpoints/models.py b/authentik/endpoints/models.py new file mode 100644 index 0000000000..2a72d71c40 --- /dev/null +++ b/authentik/endpoints/models.py @@ -0,0 +1,40 @@ +from uuid import uuid4 + +from django.db import models +from django.utils.functional import cached_property + +from authentik.core.models import User +from authentik.endpoints.common_data import CommonDeviceData +from authentik.lib.models import SerializerModel + + +class Device(SerializerModel): + device_uuid = models.UUIDField(default=uuid4) + + identifier = models.TextField(unique=True) + users = models.ManyToManyField(User, through="DeviceUser") + connections = models.ManyToManyField("Connector", through="DeviceConnection") + + @cached_property + def data(self) -> CommonDeviceData: + pass + + +class DeviceUser(models.Model): + device_user_uuid = models.UUIDField(default=uuid4) + device = models.ForeignKey("Device", on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE) + is_primary = models.BooleanField() + + +class DeviceConnection(models.Model): + device_connection_uuid = models.UUIDField(default=uuid4) + device = models.ForeignKey("Device", on_delete=models.CASCADE) + connection = models.ForeignKey("Connector", on_delete=models.CASCADE) + data = models.JSONField(default=dict) + + +class Connector(SerializerModel): + connector_uuid = models.UUIDField(default=uuid4) + + name = models.TextField() diff --git a/authentik/root/settings.py b/authentik/root/settings.py index be55981a17..f3f86429c5 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -73,6 +73,7 @@ TENANT_APPS = [ "authentik.admin", "authentik.api", "authentik.crypto", + "authentik.endpoints", "authentik.flows", "authentik.outposts", "authentik.policies.dummy",