Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d2496f4
enterprise/endpoints: use managed connector credentials
dominic-r Sep 14, 2026
b8736b7
api: retain legacy credential fields until frontend migration
dominic-r Sep 14, 2026
2e7338d
crypto: merge updated secrets dependencies into endpoint-connectors
dominic-r Sep 15, 2026
071dea9
Merge branch 'dominic/rotation-cloud-providers' into dominic/rotation…
dominic-r Sep 15, 2026
54bad9a
Merge branch 'dominic/rotation-cloud-providers' into dominic/rotation…
dominic-r Sep 16, 2026
712fc11
endpoints: preserve Google credential structure on replacement
dominic-r Sep 16, 2026
dfdcee1
Merge branch 'dominic/rotation-cloud-providers' into dominic/rotation…
dominic-r Sep 16, 2026
118d846
endpoints: use explicit credential references and validation signals
dominic-r Sep 20, 2026
a9a1ef1
sources/oauth: propagate the WeChat request fix
dominic-r Sep 20, 2026
0de226e
web/admin: keep credential forms aligned with this stack layer
dominic-r Sep 20, 2026
84e1f2c
secrets: merge updated rotation-cloud-providers
dominic-r Sep 23, 2026
1f1ef69
enterprise/endpoints: regenerate managed credential migrations
dominic-r Oct 5, 2026
d311d65
Merge branch 'dominic/rotation-cloud-providers' into dominic/rotation…
dominic-r Oct 5, 2026
07f4e77
Merge branch 'dominic/rotation-cloud-providers' into dominic/rotation…
dominic-r Oct 5, 2026
e665566
Merge branch 'dominic/rotation-cloud-providers' into dominic/rotation…
dominic-r Oct 5, 2026
1217590
Merge branch 'dominic/rotation-cloud-providers' into dominic/rotation…
dominic-r Oct 10, 2026
799401e
enterprise/endpoints: use JSON secrets for Google Chrome credentials
dominic-r Oct 10, 2026
6034f4b
Merge branch 'dominic/rotation-cloud-providers' into dominic/rotation…
dominic-r Oct 10, 2026
b7f8c9e
Merge branch 'dominic/rotation-cloud-providers' into dominic/rotation…
dominic-r Oct 10, 2026
22a893e
Merge branch 'dominic/rotation-cloud-providers' into dominic/rotation…
dominic-r Oct 10, 2026
36307b2
Merge branch 'dominic/rotation-cloud-providers' into dominic/rotation…
dominic-r Oct 10, 2026
f44f43b
Merge branch 'dominic/rotation-cloud-providers' into dominic/rotation…
dominic-r Oct 10, 2026
1e9ffd3
Merge branch 'dominic/rotation-cloud-providers' into dominic/rotation…
dominic-r Oct 10, 2026
e393729
enterprise/endpoints: require connector credentials on the model and …
dominic-r Oct 10, 2026
80001d1
Merge branch 'dominic/rotation-cloud-providers' into dominic/rotation…
dominic-r Oct 10, 2026
ab6af3a
enterprise/endpoints: regenerate the API schema for the credentials t…
dominic-r Oct 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions authentik/core/api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,6 @@ class ModelSerializer(BaseModelSerializer):
serializer_field_mapping = BaseModelSerializer.serializer_field_mapping.copy()
serializer_field_mapping[models.JSONField] = JSONDictField

def to_representation(self, instance: Model):
data = super().to_representation(instance)
# `secret_fields` are visible for users which are allowed to change the object
secret_fields = getattr(self.Meta, "secret_fields", [])
request = self.context.get("request")
if not secret_fields or not request:
return data
permission = f"{instance._meta.app_label}.change_{instance._meta.model_name}"
if request.user.has_perm(permission, instance):
return data
for field_name in secret_fields:
data.pop(field_name, None)
return data

def to_internal_value(self, data):
from authentik.crypto.secrets.api import SecretReferenceField

Expand Down
5 changes: 1 addition & 4 deletions authentik/enterprise/endpoints/connectors/fleet/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,11 @@ class Meta(ConnectorSerializer.Meta):
model = FleetConnector
fields = ConnectorSerializer.Meta.fields + [
"url",
"token",
"token_ref",
"headers_mapping",
"map_users",
"map_teams_access_group",
]
extra_kwargs = {
"token": {"write_only": True},
}


class FleetConnectorViewSet(UsedByMixin, ModelViewSet):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(self, *args, **kwargs):
base_url = base_url[:-1]
self._base_url = base_url
self._session = get_http_session()
self._session.headers["Authorization"] = f"Bearer {self.connector.token}"
self._session.headers["Authorization"] = f"Bearer {self.connector.token_ref.secret_value}"
if self.connector.headers_mapping:
self._session.headers.update(
sanitize_item(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 5.2.17 on 2026-10-10 19:49

import django.db.models.deletion
from django.db import migrations, models

from authentik.crypto.secrets.migrations._credential_values import move_credentials

FIELDS = [("token", "token_ref", "text", "Fleet API token")]


class Migration(migrations.Migration):

dependencies = [
("guardian", "0004_role_permissions"),
("authentik_rbac", "0011_initialpermissionspermission"),
("authentik_crypto_secrets", "0001_initial"),
("authentik_endpoints_connectors_fleet", "0001_initial"),
]

operations = [
migrations.AddField(
model_name="fleetconnector",
name="token_ref",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="fleet_connectors",
to="authentik_crypto_secrets.secret",
verbose_name="Fleet API Token",
),
),
move_credentials("authentik_endpoints_connectors_fleet", "FleetConnector", FIELDS),
]
10 changes: 10 additions & 0 deletions authentik/enterprise/endpoints/connectors/fleet/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@ class FleetConnector(Connector):
"""Ingest device data and policy compliance from a Fleet instance."""

url = models.URLField()
# Legacy column, kept for downgrades. Remove in 2027.2.
token = models.TextField()
token_ref = models.ForeignKey(
"authentik_crypto_secrets.Secret",
verbose_name=_("Fleet API Token"),
on_delete=models.PROTECT,
null=True,
default=None,
related_name="fleet_connectors",
)

headers_mapping = models.ForeignKey(
"authentik_events.NotificationWebhookMapping",
on_delete=models.SET_DEFAULT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from requests_mock import Mocker
from rest_framework.test import APITestCase

from authentik.crypto.secrets.tests.utils import create_test_secret
from authentik.endpoints.facts import OSFamily
from authentik.endpoints.models import Device
from authentik.enterprise.endpoints.connectors.fleet.models import FleetConnector
Expand All @@ -23,7 +24,7 @@ def setUp(self):
self.connector = FleetConnector.objects.create(
name=generate_id(),
url="http://localhost",
token=generate_id(),
token_ref=create_test_secret(generate_id()),
map_teams_access_group=True,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from authentik.core.tests.utils import (
create_test_flow,
)
from authentik.crypto.secrets.tests.utils import create_test_secret
from authentik.endpoints.models import Device, EndpointStage, StageMode
from authentik.enterprise.endpoints.connectors.fleet.models import FleetConnector
from authentik.enterprise.stages.mtls.stage import PLAN_CONTEXT_CERTIFICATE
Expand All @@ -21,7 +22,7 @@ class FleetConnectorStageTests(FlowTestCase):
def setUp(self):
super().setUp()
self.connector = FleetConnector.objects.create(
name=generate_id(), url="http://localhost", token=generate_id()
name=generate_id(), url="http://localhost", token_ref=create_test_secret(generate_id())
)

controller = self.connector.controller(self.connector)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from rest_framework.viewsets import ModelViewSet

from authentik.core.api.used_by import UsedByMixin
from authentik.crypto.secrets.models import SecretType
from authentik.endpoints.api.connectors import ConnectorSerializer
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.endpoints.connectors.google_chrome.models import GoogleChromeConnector
Expand All @@ -27,8 +28,8 @@ def get_chrome_url(self, _: GoogleChromeConnector) -> str | None:

class Meta:
model = GoogleChromeConnector
fields = ConnectorSerializer.Meta.fields + ["credentials", "chrome_url"]
secret_fields = ["credentials"]
fields = ConnectorSerializer.Meta.fields + ["credentials_ref", "chrome_url"]
extra_kwargs = {"credentials_ref": {"allowed_types": (SecretType.JSON,)}}


class GoogleChromeConnectorViewSet(UsedByMixin, ModelViewSet):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 5.2.17 on 2026-10-10 19:49

import django.db.models.deletion
from django.db import migrations, models

from authentik.crypto.secrets.migrations._credential_values import move_credentials

FIELDS = [("credentials", "credentials_ref", "json", "credentials")]


class Migration(migrations.Migration):

dependencies = [
("guardian", "0004_role_permissions"),
("authentik_rbac", "0011_initialpermissionspermission"),
("authentik_crypto_secrets", "0001_initial"),
("authentik_endpoints_connectors_google_chrome", "0001_initial"),
]

operations = [
migrations.AddField(
model_name="googlechromeconnector",
name="credentials_ref",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="google_chrome_connectors",
to="authentik_crypto_secrets.secret",
verbose_name="Google credentials",
),
),
migrations.AlterField(
model_name="googlechromeconnector",
name="credentials",
field=models.JSONField(default=dict),
),
move_credentials(
"authentik_endpoints_connectors_google_chrome",
"GoogleChromeConnector",
FIELDS,
value_permission="change",
),
]
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,23 @@
class GoogleChromeConnector(Connector):
"""Verify Google Chrome Device Trust connection for the user's browser."""

credentials = models.JSONField()
# Legacy column, kept for downgrades. Remove in 2027.2.
credentials = models.JSONField(default=dict)
credentials_ref = models.ForeignKey(
"authentik_crypto_secrets.Secret",
verbose_name=_("Google credentials"),
on_delete=models.PROTECT,
null=True,
default=None,
related_name="google_chrome_connectors",
)

def google_credentials(self):
try:
return {
"credentials": Credentials.from_service_account_info(
self.credentials, scopes=["https://www.googleapis.com/auth/verifiedaccess"]
self.credentials_ref.get_json(),
scopes=["https://www.googleapis.com/auth/verifiedaccess"],
),
}
except GoogleAuthError as exc:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from rest_framework.test import APITestCase

from authentik.core.tests.utils import RequestFactory
from authentik.crypto.secrets.models import SecretType
from authentik.crypto.secrets.tests.utils import create_test_secret
from authentik.endpoints.facts import OSFamily
from authentik.endpoints.models import Device
from authentik.enterprise.endpoints.connectors.google_chrome.controller import (
Expand All @@ -21,7 +23,7 @@ class TestGoogleChromeConnector(APITestCase):
def setUp(self):
self.connector = GoogleChromeConnector.objects.create(
name=generate_id(),
credentials={},
credentials_ref=create_test_secret("{}", SecretType.JSON),
)
self.factory = RequestFactory()
self.api_key = generate_id()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from django.urls import reverse

from authentik.core.tests.utils import create_test_flow
from authentik.crypto.secrets.models import SecretType
from authentik.crypto.secrets.tests.utils import create_test_secret
from authentik.endpoints.models import Device, EndpointStage, StageMode
from authentik.enterprise.endpoints.connectors.google_chrome.models import GoogleChromeConnector
from authentik.flows.models import FlowStageBinding
Expand All @@ -13,7 +15,7 @@ class TestGoogleChromeStage(FlowTestCase):
def setUp(self):
self.connector = GoogleChromeConnector.objects.create(
name=generate_id(),
credentials={},
credentials_ref=create_test_secret("{}", SecretType.JSON),
)

def _setup_flow(self, mode: StageMode) -> str:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from django.urls import reverse

from authentik.core.tests.utils import RequestFactory, create_test_flow
from authentik.crypto.secrets.models import SecretType
from authentik.crypto.secrets.tests.utils import create_test_secret
from authentik.endpoints.models import Device, EndpointStage
from authentik.enterprise.endpoints.connectors.google_chrome.models import GoogleChromeConnector
from authentik.enterprise.providers.google_workspace.clients.test_http import MockHTTP
Expand All @@ -19,7 +21,7 @@ def setUp(self):
self.flow = create_test_flow()
self.connector = GoogleChromeConnector.objects.create(
name=generate_id(),
credentials={},
credentials_ref=create_test_secret("{}", SecretType.JSON),
)
self.factory = RequestFactory()
self.api_key = generate_id()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.crypto.secrets.models import SecretType
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
AuthenticatorEndpointGDTCStage,
Expand All @@ -25,9 +26,9 @@ class Meta:
fields = StageSerializer.Meta.fields + [
"configure_flow",
"friendly_name",
"credentials",
"credentials_ref",
]
secret_fields = ["credentials"]
extra_kwargs = {"credentials_ref": {"allowed_types": (SecretType.JSON,)}}


class AuthenticatorEndpointGDTCStageViewSet(UsedByMixin, ModelViewSet):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 5.2.17 on 2026-10-10 19:49

import django.db.models.deletion
from django.db import migrations, models

from authentik.crypto.secrets.migrations._credential_values import move_credentials

FIELDS = [("credentials", "credentials_ref", "json", "credentials")]


class Migration(migrations.Migration):

dependencies = [
("guardian", "0004_role_permissions"),
("authentik_rbac", "0011_initialpermissionspermission"),
("authentik_crypto_secrets", "0001_initial"),
(
"authentik_stages_authenticator_endpoint_gdtc",
"0002_alter_authenticatorendpointgdtcstage_friendly_name",
),
]

operations = [
migrations.AddField(
model_name="authenticatorendpointgdtcstage",
name="credentials_ref",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="gdtc_stages",
to="authentik_crypto_secrets.secret",
verbose_name="Google credentials",
),
),
migrations.AlterField(
model_name="authenticatorendpointgdtcstage",
name="credentials",
field=models.JSONField(default=dict),
),
move_credentials(
"authentik_stages_authenticator_endpoint_gdtc",
"AuthenticatorEndpointGDTCStage",
FIELDS,
value_permission="change",
),
]
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,22 @@
class AuthenticatorEndpointGDTCStage(DeprecatedMixin, ConfigurableStage, FriendlyNamedStage, Stage):
"""Verify Google Chrome Device Trust connection for the user's browser."""

credentials = models.JSONField()
# Legacy column, kept for downgrades. Remove in 2027.2.
credentials = models.JSONField(default=dict)
credentials_ref = models.ForeignKey(
"authentik_crypto_secrets.Secret",
verbose_name=_("Google credentials"),
on_delete=models.PROTECT,
null=True,
default=None,
related_name="gdtc_stages",
)

def google_credentials(self):
return {
"credentials": Credentials.from_service_account_info(
self.credentials, scopes=["https://www.googleapis.com/auth/verifiedaccess"]
self.credentials_ref.get_json(),
scopes=["https://www.googleapis.com/auth/verifiedaccess"],
),
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from django.urls import reverse

from authentik.core.tests.utils import create_test_flow
from authentik.crypto.secrets.models import SecretType
from authentik.crypto.secrets.tests.utils import create_test_secret
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
AuthenticatorEndpointGDTCStage,
)
Expand All @@ -18,7 +20,7 @@ def setUp(self):
self.flow = create_test_flow()
self.stage = AuthenticatorEndpointGDTCStage.objects.create(
name=generate_id(),
credentials={},
credentials_ref=create_test_secret("{}", SecretType.JSON),
)
FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=0)
self.url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
Expand Down
Loading
Loading