enterprise/providers/ssf: test conformance (#21383)

* bump conformance server

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

* add support for rfc push

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

* make format and aud optional

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

* fix some endpoints

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

* force 401

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

* implement get and patch for streams

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

* cleanup

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

* enable async stream deletion

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

* allow configuring remote certificate validation

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

* add verification endpoint

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

* add support for authorization_header

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

* set default aud cause spec cant agree with itself

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

* bump timeout

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

* fix header `typ`

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

* enabled -> status

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

* re-migrate

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

* gen

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

* more tests

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

* more tests and a fix

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

* make streams deletable

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

* and more logs and fix a silly bug

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

* add stream status endpoint

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

* move ssf out of preview

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

* unrelated typing fix

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

* format

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

* sigh

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

* more tests

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2026-04-05 15:35:39 +01:00
committed by GitHub
parent f38584b343
commit ea2bdde5a3
47 changed files with 1503 additions and 170 deletions
+3 -3
View File
@@ -796,11 +796,11 @@ class Application(SerializerModel, PolicyBindingModel):
def backchannel_provider_for[T: Provider](self, provider_type: type[T], **kwargs) -> T | None:
"""Get Backchannel provider for a specific type"""
providers = self.backchannel_providers.filter(
provider: BackchannelProvider | None = self.backchannel_providers.filter(
**{f"{provider_type._meta.model_name}__isnull": False},
**kwargs,
)
return getattr(providers.first(), provider_type._meta.model_name)
).first()
return getattr(provider, provider_type._meta.model_name) if provider else None
def __str__(self):
return str(self.name)
@@ -52,6 +52,7 @@ class SSFProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
"oidc_auth_providers_obj",
"ssf_url",
"event_retention",
"push_verify_certificates",
]
extra_kwargs = {}
@@ -1,6 +1,7 @@
"""SSF Stream API Views"""
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.ssf.api.providers import SSFProviderSerializer
@@ -16,6 +17,7 @@ class SSFStreamSerializer(ModelSerializer):
model = Stream
fields = [
"pk",
"status",
"provider",
"provider_obj",
"delivery_method",
@@ -27,7 +29,12 @@ class SSFStreamSerializer(ModelSerializer):
]
class SSFStreamViewSet(ReadOnlyModelViewSet):
class SSFStreamViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""SSFStream Viewset"""
queryset = Stream.objects.all()
@@ -0,0 +1,43 @@
# Generated by Django 5.2.12 on 2026-04-04 16:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_ssf", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="ssfprovider",
name="push_verify_certificates",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="stream",
name="authorization_header",
field=models.TextField(default=None, null=True),
),
migrations.AddField(
model_name="stream",
name="status",
field=models.TextField(
choices=[("enabled", "Enabled"), ("paused", "Paused"), ("disabled", "Disabled")],
default="enabled",
),
),
migrations.AlterField(
model_name="stream",
name="delivery_method",
field=models.TextField(
choices=[
("https://schemas.openid.net/secevent/risc/delivery-method/push", "Risc Push"),
("https://schemas.openid.net/secevent/risc/delivery-method/poll", "Risc Poll"),
("urn:ietf:rfc:8935", "SSF RFC Push"),
("urn:ietf:rfc:8936", "SSF RFC Pull"),
]
),
),
]
+16 -1
View File
@@ -33,6 +33,8 @@ class DeliveryMethods(models.TextChoices):
RISC_PUSH = "https://schemas.openid.net/secevent/risc/delivery-method/push"
RISC_POLL = "https://schemas.openid.net/secevent/risc/delivery-method/poll"
RFC_PUSH = "urn:ietf:rfc:8935", _("SSF RFC Push")
RFC_PULL = "urn:ietf:rfc:8936", _("SSF RFC Pull")
class SSFEventStatus(models.TextChoices):
@@ -43,6 +45,13 @@ class SSFEventStatus(models.TextChoices):
SENT = "sent"
class StreamStatus(models.TextChoices):
ENABLED = "enabled"
PAUSED = "paused"
DISABLED = "disabled"
class SSFProvider(TasksModel, BackchannelProvider):
"""Shared Signals Framework provider to allow applications to
receive user events from authentik."""
@@ -54,6 +63,8 @@ class SSFProvider(TasksModel, BackchannelProvider):
help_text=_("Key used to sign the SSF Events."),
)
push_verify_certificates = models.BooleanField(default=True)
oidc_auth_providers = models.ManyToManyField(OAuth2Provider, blank=True, default=None)
token = models.ForeignKey(Token, on_delete=models.CASCADE, null=True, default=None)
@@ -106,10 +117,14 @@ class Stream(models.Model):
"""SSF Stream"""
uuid = models.UUIDField(default=uuid4, primary_key=True, editable=False)
status = models.TextField(choices=StreamStatus.choices, default=StreamStatus.ENABLED)
provider = models.ForeignKey(SSFProvider, on_delete=models.CASCADE)
delivery_method = models.TextField(choices=DeliveryMethods.choices)
endpoint_url = models.TextField(null=True)
authorization_header = models.TextField(null=True, default=None)
events_requested = ArrayField(models.TextField(choices=EventTypes.choices), default=list)
format = models.TextField()
@@ -146,7 +161,7 @@ class Stream(models.Model):
}
def encode(self, data: dict) -> str:
headers = {}
headers = {"typ": "secevent+jwt"}
if self.provider.signing_key:
headers["kid"] = self.provider.signing_key.kid
key, alg = self.provider.jwt_key
+26 -5
View File
@@ -16,6 +16,7 @@ from authentik.enterprise.providers.ssf.models import (
SSFEventStatus,
Stream,
StreamEvent,
StreamStatus,
)
from authentik.lib.utils.http import get_http_session
from authentik.lib.utils.time import timedelta_from_string
@@ -88,23 +89,42 @@ def send_ssf_event(stream_uuid: UUID, event_data: dict[str, Any]):
self.set_uid(event.pk)
if event.status == SSFEventStatus.SENT:
return
if stream.delivery_method != DeliveryMethods.RISC_PUSH:
if stream.delivery_method not in [DeliveryMethods.RISC_PUSH, DeliveryMethods.RFC_PUSH]:
return
headers = {"Content-Type": "application/secevent+jwt", "Accept": "application/json"}
if stream.authorization_header:
headers["Authorization"] = stream.authorization_header
try:
response = session.post(
event.stream.endpoint_url,
data=event.stream.encode(event.payload),
headers={"Content-Type": "application/secevent+jwt", "Accept": "application/json"},
headers=headers,
verify=stream.provider.push_verify_certificates,
timeout=180,
)
response.raise_for_status()
event.status = SSFEventStatus.SENT
event.save()
return
self.info("Event successfully sent", status=response.status_code)
# Cleanup, if we were the last pending message for this stream and it has been deleted
# (status=StreamStatus.DISABLED), then we can delete the stream
if (
not StreamEvent.objects.filter(
stream=stream,
status__in=[SSFEventStatus.PENDING_FAILED, SSFEventStatus.PENDING_NEW],
).exists()
and stream.status == StreamStatus.DISABLED
):
LOGGER.info(
"Deleting inactive stream as all pending messages were sent.", stream=stream
)
self.info("Deleting inactive stream as all pending messages were sent.")
stream.delete()
except RequestException as exc:
LOGGER.warning("Failed to send SSF event", exc=exc)
LOGGER.warning("Failed to send SSF event", exc=exc, stream=stream)
attrs = {}
if exc.response:
if exc.response is not None:
attrs["response"] = {
"content": exc.response.text,
"status": exc.response.status_code,
@@ -113,5 +133,6 @@ def send_ssf_event(stream_uuid: UUID, event_data: dict[str, Any]):
self.warning("Failed to send request", **attrs)
# Re-up the expiry of the stream event
event.expires = now() + timedelta_from_string(event.stream.provider.event_retention)
self.info(f"Event will be re-sent at {event.expires}")
event.status = SSFEventStatus.PENDING_FAILED
event.save()
@@ -0,0 +1,170 @@
import json
from dataclasses import asdict
from django.urls import reverse
from django.utils import timezone
from rest_framework.test import APITestCase
from authentik.core.models import Application, Token, TokenIntents
from authentik.core.tests.utils import (
create_test_admin_user,
create_test_cert,
create_test_flow,
create_test_user,
)
from authentik.enterprise.providers.ssf.models import (
SSFEventStatus,
SSFProvider,
Stream,
StreamEvent,
)
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider
class TestSSFAuth(APITestCase):
def setUp(self):
self.application = Application.objects.create(name=generate_id(), slug=generate_id())
self.provider = SSFProvider.objects.create(
name=generate_id(),
signing_key=create_test_cert(),
backchannel_application=self.application,
)
def test_stream_add_token(self):
"""test stream add (token auth)"""
res = self.client.post(
reverse(
"authentik_providers_ssf:stream",
kwargs={"application_slug": self.application.slug},
),
data={
"iss": "https://authentik.company/.well-known/ssf-configuration/foo/5",
"aud": ["https://app.authentik.company"],
"delivery": {
"method": "https://schemas.openid.net/secevent/risc/delivery-method/push",
"endpoint_url": "https://app.authentik.company",
},
"events_requested": [
"https://schemas.openid.net/secevent/caep/event-type/credential-change",
"https://schemas.openid.net/secevent/caep/event-type/session-revoked",
],
"format": "iss_sub",
},
HTTP_AUTHORIZATION=f"Bearer {self.provider.token.key}",
)
self.assertEqual(res.status_code, 201)
stream = Stream.objects.filter(provider=self.provider).first()
self.assertIsNotNone(stream)
event = StreamEvent.objects.filter(stream=stream).first()
self.assertIsNotNone(event)
self.assertEqual(event.status, SSFEventStatus.PENDING_FAILED)
self.assertEqual(
event.payload["events"],
{"https://schemas.openid.net/secevent/ssf/event-type/verification": {"state": None}},
)
def test_stream_add_oidc(self):
"""test stream add (oidc auth)"""
provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
)
self.application.provider = provider
self.application.save()
user = create_test_admin_user()
token = AccessToken.objects.create(
provider=provider,
user=user,
token=generate_id(),
auth_time=timezone.now(),
_scope="openid user profile",
_id_token=json.dumps(
asdict(
IDToken("foo", "bar"),
)
),
)
res = self.client.post(
reverse(
"authentik_providers_ssf:stream",
kwargs={"application_slug": self.application.slug},
),
data={
"iss": "https://authentik.company/.well-known/ssf-configuration/foo/5",
"aud": ["https://app.authentik.company"],
"delivery": {
"method": "https://schemas.openid.net/secevent/risc/delivery-method/push",
"endpoint_url": "https://app.authentik.company",
},
"events_requested": [
"https://schemas.openid.net/secevent/caep/event-type/credential-change",
"https://schemas.openid.net/secevent/caep/event-type/session-revoked",
],
"format": "iss_sub",
},
HTTP_AUTHORIZATION=f"Bearer {token.token}",
)
self.assertEqual(res.status_code, 201)
stream = Stream.objects.filter(provider=self.provider).first()
self.assertIsNotNone(stream)
event = StreamEvent.objects.filter(stream=stream).first()
self.assertIsNotNone(event)
self.assertEqual(event.status, SSFEventStatus.PENDING_FAILED)
self.assertEqual(
event.payload["events"],
{"https://schemas.openid.net/secevent/ssf/event-type/verification": {"state": None}},
)
def test_token_invalid(self):
res = self.client.post(
reverse(
"authentik_providers_ssf:stream",
kwargs={"application_slug": self.application.slug},
),
data={
"iss": "https://authentik.company/.well-known/ssf-configuration/foo/5",
"aud": ["https://app.authentik.company"],
"delivery": {
"method": "https://schemas.openid.net/secevent/risc/delivery-method/push",
"endpoint_url": "https://app.authentik.company",
},
"events_requested": [
"https://schemas.openid.net/secevent/caep/event-type/credential-change",
"https://schemas.openid.net/secevent/caep/event-type/session-revoked",
],
"format": "iss_sub",
},
HTTP_AUTHORIZATION=f"Bearer {self.provider.token.key}a",
)
# Response code needs to be 401 according to spec
self.assertEqual(res.status_code, 401)
def test_token_unrelated(self):
token = Token.objects.create(
identifier=generate_id(), user=create_test_user(), intent=TokenIntents.INTENT_API
)
res = self.client.post(
reverse(
"authentik_providers_ssf:stream",
kwargs={"application_slug": self.application.slug},
),
data={
"iss": "https://authentik.company/.well-known/ssf-configuration/foo/5",
"aud": ["https://app.authentik.company"],
"delivery": {
"method": "https://schemas.openid.net/secevent/risc/delivery-method/push",
"endpoint_url": "https://app.authentik.company",
},
"events_requested": [
"https://schemas.openid.net/secevent/caep/event-type/credential-change",
"https://schemas.openid.net/secevent/caep/event-type/session-revoked",
],
"format": "iss_sub",
},
HTTP_AUTHORIZATION=f"Bearer {token.key}",
)
# Response code needs to be 401 according to spec
self.assertEqual(res.status_code, 401)
@@ -44,3 +44,15 @@ class TestConfiguration(APITestCase):
self.assertEqual(res.status_code, 200)
content = json.loads(res.content)
self.assertEqual(content["spec_version"], "1_0-ID2")
def test_config_not_found(self):
"""test SSF configuration (authenticated)"""
self.provider.delete()
res = self.client.get(
reverse(
"authentik_providers_ssf:configuration",
kwargs={"application_slug": self.application.slug},
),
HTTP_AUTHORIZATION=f"Bearer {self.provider.token.key}",
)
self.assertEqual(res.status_code, 404)
@@ -1,21 +1,18 @@
import json
from dataclasses import asdict
from uuid import uuid4
from django.urls import reverse
from django.utils import timezone
from rest_framework.test import APITestCase
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.core.tests.utils import create_test_cert
from authentik.enterprise.providers.ssf.models import (
SSFEventStatus,
SSFProvider,
Stream,
StreamEvent,
StreamStatus,
)
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider
class TestStream(APITestCase):
@@ -87,29 +84,71 @@ class TestStream(APITestCase):
{"delivery": {"method": ["Polling for SSF events is not currently supported."]}},
)
def test_stream_add_oidc(self):
"""test stream add (oidc auth)"""
provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
)
self.application.provider = provider
self.application.save()
user = create_test_admin_user()
token = AccessToken.objects.create(
provider=provider,
user=user,
token=generate_id(),
auth_time=timezone.now(),
_scope="openid user profile",
_id_token=json.dumps(
asdict(
IDToken("foo", "bar"),
)
def test_stream_delete(self):
"""delete stream"""
stream = Stream.objects.create(provider=self.provider)
res = self.client.delete(
reverse(
"authentik_providers_ssf:stream",
kwargs={"application_slug": self.application.slug},
),
HTTP_AUTHORIZATION=f"Bearer {self.provider.token.key}",
)
self.assertEqual(res.status_code, 204)
stream.refresh_from_db()
self.assertEqual(stream.status, StreamStatus.DISABLED)
res = self.client.post(
def test_stream_get(self):
"""get stream"""
Stream.objects.create(provider=self.provider)
res = self.client.get(
reverse(
"authentik_providers_ssf:stream",
kwargs={"application_slug": self.application.slug},
),
HTTP_AUTHORIZATION=f"Bearer {self.provider.token.key}",
)
self.assertEqual(res.status_code, 200)
def test_stream_get_filter_query(self):
"""get stream"""
other_stream = Stream.objects.create(provider=self.provider)
stream = Stream.objects.create(provider=self.provider)
res = self.client.get(
reverse(
"authentik_providers_ssf:stream",
kwargs={"application_slug": self.application.slug},
)
+ f"?stream_id={stream.pk}",
HTTP_AUTHORIZATION=f"Bearer {self.provider.token.key}",
)
self.assertEqual(res.status_code, 200)
self.assertIn(str(stream.pk), res.content.decode())
self.assertNotIn(str(other_stream.pk), res.content.decode())
def test_stream_patch(self):
"""patch stream"""
other_stream = Stream.objects.create(provider=self.provider)
stream = Stream.objects.create(provider=self.provider)
res = self.client.patch(
reverse(
"authentik_providers_ssf:stream",
kwargs={"application_slug": self.application.slug},
),
data={
"delivery": {"endpoint_url": "https://localhost"},
"stream_id": str(stream.pk),
},
HTTP_AUTHORIZATION=f"Bearer {self.provider.token.key}",
)
self.assertEqual(res.status_code, 200)
self.assertIn(str(stream.pk), res.content.decode())
self.assertNotIn(str(other_stream.pk), res.content.decode())
def test_stream_put(self):
"""put stream"""
stream = Stream.objects.create(provider=self.provider)
res = self.client.put(
reverse(
"authentik_providers_ssf:stream",
kwargs={"application_slug": self.application.slug},
@@ -126,29 +165,63 @@ class TestStream(APITestCase):
"https://schemas.openid.net/secevent/caep/event-type/session-revoked",
],
"format": "iss_sub",
"stream_id": str(stream.pk),
},
HTTP_AUTHORIZATION=f"Bearer {token.token}",
)
self.assertEqual(res.status_code, 201)
stream = Stream.objects.filter(provider=self.provider).first()
self.assertIsNotNone(stream)
event = StreamEvent.objects.filter(stream=stream).first()
self.assertIsNotNone(event)
self.assertEqual(event.status, SSFEventStatus.PENDING_FAILED)
self.assertEqual(
event.payload["events"],
{"https://schemas.openid.net/secevent/ssf/event-type/verification": {"state": None}},
HTTP_AUTHORIZATION=f"Bearer {self.provider.token.key}",
)
self.assertEqual(res.status_code, 200)
self.assertIn(str(stream.pk), res.content.decode())
stream.refresh_from_db()
self.assertEqual(stream.aud, ["https://app.authentik.company"])
def test_stream_delete(self):
"""delete stream"""
def test_stream_verify(self):
"""Test stream verify"""
stream = Stream.objects.create(provider=self.provider)
res = self.client.delete(
res = self.client.post(
reverse(
"authentik_providers_ssf:stream",
"authentik_providers_ssf:stream-verify",
kwargs={"application_slug": self.application.slug},
),
data={
"stream_id": str(stream.pk),
},
HTTP_AUTHORIZATION=f"Bearer {self.provider.token.key}",
)
self.assertEqual(res.status_code, 204)
self.assertFalse(Stream.objects.filter(pk=stream.pk).exists())
def test_stream_status(self):
"""Test stream status"""
stream = Stream.objects.create(provider=self.provider)
res = self.client.get(
reverse(
"authentik_providers_ssf:stream-status",
kwargs={"application_slug": self.application.slug},
),
data={
"stream_id": str(stream.pk),
},
HTTP_AUTHORIZATION=f"Bearer {self.provider.token.key}",
)
self.assertEqual(res.status_code, 200)
self.assertJSONEqual(
res.content,
{
"stream_id": str(stream.pk),
"status": str(stream.status),
},
)
def test_stream_status_not_found(self):
"""Test stream status"""
Stream.objects.create(provider=self.provider)
res = self.client.get(
reverse(
"authentik_providers_ssf:stream-status",
kwargs={"application_slug": self.application.slug},
),
data={
"stream_id": str(uuid4()),
},
HTTP_AUTHORIZATION=f"Bearer {self.provider.token.key}",
)
self.assertEqual(res.status_code, 404)
@@ -0,0 +1,123 @@
from jwt import decode_complete
from requests_mock import Mocker
from rest_framework.test import APITestCase
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_cert
from authentik.enterprise.providers.ssf.models import (
DeliveryMethods,
EventTypes,
SSFProvider,
Stream,
StreamStatus,
)
from authentik.enterprise.providers.ssf.tasks import send_ssf_event
from authentik.lib.generators import generate_id
from authentik.tasks.models import TaskLog
class TestTasks(APITestCase):
def setUp(self):
self.application = Application.objects.create(name=generate_id(), slug=generate_id())
self.provider = SSFProvider.objects.create(
name=generate_id(),
signing_key=create_test_cert(),
backchannel_application=self.application,
)
def test_push_simple(self):
stream = Stream.objects.create(
provider=self.provider,
delivery_method=DeliveryMethods.RFC_PUSH,
endpoint_url="http://localhost/ssf-push",
)
event_data = stream.prepare_event_payload(
EventTypes.SET_VERIFICATION,
{"state": None},
sub_id={"format": "opaque", "id": str(stream.uuid)},
)
with Mocker() as mocker:
mocker.post("http://localhost/ssf-push", status_code=202)
send_ssf_event.send_with_options(
args=(stream.pk, event_data), rel_obj=stream.provider
).get_result(block=True, timeout=1)
self.assertEqual(
mocker.request_history[0].headers["Content-Type"], "application/secevent+jwt"
)
jwt = decode_complete(mocker.request_history[0].body, options={"verify_signature": False})
self.assertEqual(jwt["header"]["typ"], "secevent+jwt")
self.assertIsNone(jwt["payload"]["events"][EventTypes.SET_VERIFICATION]["state"])
def test_push_auth(self):
auth = generate_id()
stream = Stream.objects.create(
provider=self.provider,
delivery_method=DeliveryMethods.RFC_PUSH,
endpoint_url="http://localhost/ssf-push",
authorization_header=auth,
)
event_data = stream.prepare_event_payload(
EventTypes.SET_VERIFICATION,
{"state": None},
sub_id={"format": "opaque", "id": str(stream.uuid)},
)
with Mocker() as mocker:
mocker.post("http://localhost/ssf-push", status_code=202)
send_ssf_event.send_with_options(
args=(stream.pk, event_data), rel_obj=stream.provider
).get_result(block=True, timeout=1)
self.assertEqual(mocker.request_history[0].headers["Authorization"], auth)
self.assertEqual(
mocker.request_history[0].headers["Content-Type"], "application/secevent+jwt"
)
jwt = decode_complete(mocker.request_history[0].body, options={"verify_signature": False})
self.assertEqual(jwt["header"]["typ"], "secevent+jwt")
self.assertIsNone(jwt["payload"]["events"][EventTypes.SET_VERIFICATION]["state"])
def test_push_stream_disable(self):
auth = generate_id()
stream = Stream.objects.create(
provider=self.provider,
delivery_method=DeliveryMethods.RFC_PUSH,
endpoint_url="http://localhost/ssf-push",
authorization_header=auth,
status=StreamStatus.DISABLED,
)
event_data = stream.prepare_event_payload(
EventTypes.SET_VERIFICATION,
{"state": None},
sub_id={"format": "opaque", "id": str(stream.uuid)},
)
with Mocker() as mocker:
mocker.post("http://localhost/ssf-push", status_code=202)
send_ssf_event.send_with_options(
args=(stream.pk, event_data), rel_obj=stream.provider
).get_result(block=True, timeout=1)
jwt = decode_complete(mocker.request_history[0].body, options={"verify_signature": False})
self.assertEqual(jwt["header"]["typ"], "secevent+jwt")
self.assertIsNone(jwt["payload"]["events"][EventTypes.SET_VERIFICATION]["state"])
self.assertFalse(Stream.objects.filter(pk=stream.pk).exists())
def test_push_error(self):
stream = Stream.objects.create(
provider=self.provider,
delivery_method=DeliveryMethods.RFC_PUSH,
endpoint_url="http://localhost/ssf-push",
)
event_data = stream.prepare_event_payload(
EventTypes.SET_VERIFICATION,
{"state": None},
sub_id={"format": "opaque", "id": str(stream.uuid)},
)
with Mocker() as mocker:
mocker.post("http://localhost/ssf-push", text="error", status_code=400)
send_ssf_event.send_with_options(
args=(stream.pk, event_data), rel_obj=stream.provider
).get_result(block=True, timeout=1)
logs = (
TaskLog.objects.filter(task__actor_name=send_ssf_event.actor_name)
.order_by("timestamp")
.filter(event="Failed to send request")
.first()
)
self.assertEqual(logs.attributes, {"response": {"status": 400, "content": "error"}})
+15 -1
View File
@@ -6,7 +6,11 @@ from authentik.enterprise.providers.ssf.api.providers import SSFProviderViewSet
from authentik.enterprise.providers.ssf.api.streams import SSFStreamViewSet
from authentik.enterprise.providers.ssf.views.configuration import ConfigurationView
from authentik.enterprise.providers.ssf.views.jwks import JWKSview
from authentik.enterprise.providers.ssf.views.stream import StreamView
from authentik.enterprise.providers.ssf.views.stream import (
StreamStatusView,
StreamVerifyView,
StreamView,
)
urlpatterns = [
path(
@@ -24,6 +28,16 @@ urlpatterns = [
StreamView.as_view(),
name="stream",
),
path(
"application/ssf/<slug:application_slug>/stream/verify/",
StreamVerifyView.as_view(),
name="stream-verify",
),
path(
"application/ssf/<slug:application_slug>/stream/status/",
StreamStatusView.as_view(),
name="stream-status",
),
]
api_urlpatterns = [
@@ -64,3 +64,7 @@ class SSFTokenAuth(BaseAuthentication):
if jwt_token:
return (jwt_token.user, token)
return None
# Required to correctly propagate a 401 header which the SSF spec requires
def authenticate_header(self, request):
return "SSF"
@@ -1,10 +1,10 @@
from django.http import HttpRequest
from django.http import Http404, HttpRequest
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.models import Application
from authentik.enterprise.providers.ssf.models import SSFProvider
from authentik.enterprise.providers.ssf.models import SSFProvider, Stream, StreamStatus
from authentik.enterprise.providers.ssf.views.auth import SSFTokenAuth
@@ -21,3 +21,18 @@ class SSFView(APIView):
def get_authenticators(self):
return [SSFTokenAuth(self)]
class SSFStreamView(SSFView):
def get_object(self, any_status=False) -> Stream:
streams = Stream.objects.filter(provider=self.provider)
if not any_status:
streams = streams.filter(status__in=[StreamStatus.ENABLED, StreamStatus.PAUSED])
if "stream_id" in self.request.query_params:
streams = streams.filter(pk=self.request.query_params["stream_id"])
if "stream_id" in self.request.data:
streams = streams.filter(pk=self.request.data["stream_id"])
stream = streams.first()
if not stream:
raise Http404()
return stream
@@ -47,9 +47,23 @@ class ConfigurationView(SSFView):
},
)
),
"delivery_methods_supported": [
DeliveryMethods.RISC_PUSH,
],
"verification_endpoint": self.request.build_absolute_uri(
reverse(
"authentik_providers_ssf:stream-verify",
kwargs={
"application_slug": application.slug,
},
)
),
"status_endpoint": self.request.build_absolute_uri(
reverse(
"authentik_providers_ssf:stream-status",
kwargs={
"application_slug": application.slug,
},
)
),
"delivery_methods_supported": [DeliveryMethods.RISC_PUSH, DeliveryMethods.RFC_PUSH],
"authorization_schemes": [{"spec_urn": "urn:ietf:rfc:6749"}],
}
return JsonResponse(data)
@@ -1,3 +1,5 @@
from uuid import uuid4
from django.http import HttpRequest
from django.urls import reverse
from rest_framework.exceptions import PermissionDenied, ValidationError
@@ -13,9 +15,10 @@ from authentik.enterprise.providers.ssf.models import (
EventTypes,
SSFProvider,
Stream,
StreamStatus,
)
from authentik.enterprise.providers.ssf.tasks import send_ssf_events
from authentik.enterprise.providers.ssf.views.base import SSFView
from authentik.enterprise.providers.ssf.views.base import SSFStreamView
LOGGER = get_logger()
@@ -23,6 +26,7 @@ LOGGER = get_logger()
class StreamDeliverySerializer(PassiveSerializer):
method = ChoiceField(choices=[(x.value, x.value) for x in DeliveryMethods])
endpoint_url = CharField(required=False)
authorization_header = CharField(required=False)
def validate_method(self, method: DeliveryMethods):
"""Currently only push is supported"""
@@ -31,7 +35,7 @@ class StreamDeliverySerializer(PassiveSerializer):
return method
def validate(self, attrs: dict) -> dict:
if attrs["method"] == DeliveryMethods.RISC_PUSH:
if attrs.get("method") in [DeliveryMethods.RISC_PUSH, DeliveryMethods.RFC_PUSH]:
if not attrs.get("endpoint_url"):
raise ValidationError("Endpoint URL is required when using push.")
return attrs
@@ -42,8 +46,8 @@ class StreamSerializer(ModelSerializer):
events_requested = ListField(
child=ChoiceField(choices=[(x.value, x.value) for x in EventTypes])
)
format = CharField()
aud = ListField(child=CharField())
format = CharField(default="iss_sub")
aud = ListField(child=CharField(), allow_empty=True, default=list)
def create(self, validated_data):
provider: SSFProvider = validated_data["provider"]
@@ -58,15 +62,19 @@ class StreamSerializer(ModelSerializer):
)
# Ensure that streams always get SET verification events sent to them
validated_data["events_requested"].append(EventTypes.SET_VERIFICATION)
stream_id = uuid4()
default_aud = f"goauthentik.io/providers/ssf/{str(stream_id)}"
return super().create(
{
"delivery_method": validated_data["delivery"]["method"],
"endpoint_url": validated_data["delivery"].get("endpoint_url"),
"authorization_header": validated_data["delivery"].get("authorization_header"),
"format": validated_data["format"],
"provider": validated_data["provider"],
"events_requested": validated_data["events_requested"],
"aud": validated_data["aud"],
"aud": validated_data["aud"] or [default_aud],
"iss": iss,
"pk": stream_id,
}
)
@@ -101,7 +109,14 @@ class StreamResponseSerializer(PassiveSerializer):
return [x.value for x in EventTypes]
class StreamView(SSFView):
class StreamView(SSFStreamView):
def get(self, request: Request, *args, **kwargs):
stream = self.get_object()
return Response(
StreamResponseSerializer(instance=stream, context={"request": request}).data
)
@validate(StreamSerializer)
def post(self, request: Request, *args, body: StreamSerializer, **kwargs) -> Response:
if not request.user.has_perm("authentik_providers_ssf.add_stream", self.provider):
@@ -109,6 +124,8 @@ class StreamView(SSFView):
"User does not have permission to create stream for this provider."
)
instance: Stream = body.save(provider=self.provider)
LOGGER.info("Sending verification event", stream=instance)
send_ssf_events(
EventTypes.SET_VERIFICATION,
{
@@ -120,10 +137,56 @@ class StreamView(SSFView):
response = StreamResponseSerializer(instance=instance, context={"request": request}).data
return Response(response, status=201)
def patch(self, request: Request, *args, **kwargs) -> Response:
stream = self.get_object()
serializer = StreamSerializer(stream, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
response = StreamResponseSerializer(
instance=serializer.instance, context={"request": request}
).data
return Response(response, status=200)
def put(self, request: Request, *args, **kwargs) -> Response:
stream = self.get_object()
serializer = StreamSerializer(stream, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
response = StreamResponseSerializer(
instance=serializer.instance, context={"request": request}
).data
return Response(response, status=200)
def delete(self, request: Request, *args, **kwargs) -> Response:
streams = Stream.objects.filter(provider=self.provider)
# Technically this parameter is required by the spec...
if "stream_id" in request.query_params:
streams = streams.filter(stream_id=request.query_params["stream_id"])
streams.delete()
stream = self.get_object()
stream.status = StreamStatus.DISABLED
stream.save()
return Response(status=204)
class StreamVerifyView(SSFStreamView):
def post(self, request: Request, *args, **kwargs):
stream = self.get_object()
state = request.data.get("state", None)
send_ssf_events(
EventTypes.SET_VERIFICATION,
{
"state": state,
},
stream_filter={"pk": stream.uuid},
sub_id={"format": "opaque", "id": str(stream.uuid)},
)
return Response(status=204)
class StreamStatusView(SSFStreamView):
def get(self, request: Request, *args, **kwargs):
stream = self.get_object(any_status=True)
return Response(
{
"stream_id": str(stream.pk),
"status": str(stream.status),
}
)
+4
View File
@@ -7507,6 +7507,10 @@
"type": "string",
"minLength": 1,
"title": "Event retention"
},
"push_verify_certificates": {
"type": "boolean",
"title": "Push verify certificates"
}
},
"required": []
+113
View File
@@ -23,6 +23,119 @@ import (
// SsfAPIService SsfAPI service
type SsfAPIService service
type ApiSsfStreamsDestroyRequest struct {
ctx context.Context
ApiService *SsfAPIService
uuid string
}
func (r ApiSsfStreamsDestroyRequest) Execute() (*http.Response, error) {
return r.ApiService.SsfStreamsDestroyExecute(r)
}
/*
SsfStreamsDestroy Method for SsfStreamsDestroy
SSFStream Viewset
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param uuid A UUID string identifying this SSF Stream.
@return ApiSsfStreamsDestroyRequest
*/
func (a *SsfAPIService) SsfStreamsDestroy(ctx context.Context, uuid string) ApiSsfStreamsDestroyRequest {
return ApiSsfStreamsDestroyRequest{
ApiService: a,
ctx: ctx,
uuid: uuid,
}
}
// Execute executes the request
func (a *SsfAPIService) SsfStreamsDestroyExecute(r ApiSsfStreamsDestroyRequest) (*http.Response, error) {
var (
localVarHTTPMethod = http.MethodDelete
localVarPostBody interface{}
formFiles []formFile
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "SsfAPIService.SsfStreamsDestroy")
if err != nil {
return nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/ssf/streams/{uuid}/"
localVarPath = strings.Replace(localVarPath, "{"+"uuid"+"}", url.PathEscape(parameterValueToString(r.uuid, "uuid")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 400 {
var v ValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
return localVarHTTPResponse, newErr
}
if localVarHTTPResponse.StatusCode == 403 {
var v GenericError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarHTTPResponse, newErr
}
return localVarHTTPResponse, nil
}
type ApiSsfStreamsListRequest struct {
ctx context.Context
ApiService *SsfAPIService
+4
View File
@@ -23,12 +23,16 @@ type DeliveryMethodEnum string
const (
DELIVERYMETHODENUM_HTTPS___SCHEMAS_OPENID_NET_SECEVENT_RISC_DELIVERY_METHOD_PUSH DeliveryMethodEnum = "https://schemas.openid.net/secevent/risc/delivery-method/push"
DELIVERYMETHODENUM_HTTPS___SCHEMAS_OPENID_NET_SECEVENT_RISC_DELIVERY_METHOD_POLL DeliveryMethodEnum = "https://schemas.openid.net/secevent/risc/delivery-method/poll"
DELIVERYMETHODENUM_URN_IETF_RFC_8935 DeliveryMethodEnum = "urn:ietf:rfc:8935"
DELIVERYMETHODENUM_URN_IETF_RFC_8936 DeliveryMethodEnum = "urn:ietf:rfc:8936"
)
// All allowed values of DeliveryMethodEnum enum
var AllowedDeliveryMethodEnumEnumValues = []DeliveryMethodEnum{
"https://schemas.openid.net/secevent/risc/delivery-method/push",
"https://schemas.openid.net/secevent/risc/delivery-method/poll",
"urn:ietf:rfc:8935",
"urn:ietf:rfc:8936",
}
func (v *DeliveryMethodEnum) UnmarshalJSON(src []byte) error {
+41 -4
View File
@@ -22,10 +22,11 @@ var _ MappedNullable = &PatchedSSFProviderRequest{}
type PatchedSSFProviderRequest struct {
Name *string `json:"name,omitempty"`
// Key used to sign the SSF Events.
SigningKey *string `json:"signing_key,omitempty"`
OidcAuthProviders []int32 `json:"oidc_auth_providers,omitempty"`
EventRetention *string `json:"event_retention,omitempty"`
AdditionalProperties map[string]interface{}
SigningKey *string `json:"signing_key,omitempty"`
OidcAuthProviders []int32 `json:"oidc_auth_providers,omitempty"`
EventRetention *string `json:"event_retention,omitempty"`
PushVerifyCertificates *bool `json:"push_verify_certificates,omitempty"`
AdditionalProperties map[string]interface{}
}
type _PatchedSSFProviderRequest PatchedSSFProviderRequest
@@ -175,6 +176,38 @@ func (o *PatchedSSFProviderRequest) SetEventRetention(v string) {
o.EventRetention = &v
}
// GetPushVerifyCertificates returns the PushVerifyCertificates field value if set, zero value otherwise.
func (o *PatchedSSFProviderRequest) GetPushVerifyCertificates() bool {
if o == nil || IsNil(o.PushVerifyCertificates) {
var ret bool
return ret
}
return *o.PushVerifyCertificates
}
// GetPushVerifyCertificatesOk returns a tuple with the PushVerifyCertificates field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *PatchedSSFProviderRequest) GetPushVerifyCertificatesOk() (*bool, bool) {
if o == nil || IsNil(o.PushVerifyCertificates) {
return nil, false
}
return o.PushVerifyCertificates, true
}
// HasPushVerifyCertificates returns a boolean if a field has been set.
func (o *PatchedSSFProviderRequest) HasPushVerifyCertificates() bool {
if o != nil && !IsNil(o.PushVerifyCertificates) {
return true
}
return false
}
// SetPushVerifyCertificates gets a reference to the given bool and assigns it to the PushVerifyCertificates field.
func (o *PatchedSSFProviderRequest) SetPushVerifyCertificates(v bool) {
o.PushVerifyCertificates = &v
}
func (o PatchedSSFProviderRequest) MarshalJSON() ([]byte, error) {
toSerialize, err := o.ToMap()
if err != nil {
@@ -197,6 +230,9 @@ func (o PatchedSSFProviderRequest) ToMap() (map[string]interface{}, error) {
if !IsNil(o.EventRetention) {
toSerialize["event_retention"] = o.EventRetention
}
if !IsNil(o.PushVerifyCertificates) {
toSerialize["push_verify_certificates"] = o.PushVerifyCertificates
}
for key, value := range o.AdditionalProperties {
toSerialize[key] = value
@@ -223,6 +259,7 @@ func (o *PatchedSSFProviderRequest) UnmarshalJSON(data []byte) (err error) {
delete(additionalProperties, "signing_key")
delete(additionalProperties, "oidc_auth_providers")
delete(additionalProperties, "event_retention")
delete(additionalProperties, "push_verify_certificates")
o.AdditionalProperties = additionalProperties
}
+44 -7
View File
@@ -32,13 +32,14 @@ type SSFProvider struct {
// Return internal model name
MetaModelName string `json:"meta_model_name"`
// Key used to sign the SSF Events.
SigningKey string `json:"signing_key"`
TokenObj Token `json:"token_obj"`
OidcAuthProviders []int32 `json:"oidc_auth_providers,omitempty"`
OidcAuthProvidersObj []Provider `json:"oidc_auth_providers_obj"`
SsfUrl NullableString `json:"ssf_url"`
EventRetention *string `json:"event_retention,omitempty"`
AdditionalProperties map[string]interface{}
SigningKey string `json:"signing_key"`
TokenObj Token `json:"token_obj"`
OidcAuthProviders []int32 `json:"oidc_auth_providers,omitempty"`
OidcAuthProvidersObj []Provider `json:"oidc_auth_providers_obj"`
SsfUrl NullableString `json:"ssf_url"`
EventRetention *string `json:"event_retention,omitempty"`
PushVerifyCertificates *bool `json:"push_verify_certificates,omitempty"`
AdditionalProperties map[string]interface{}
}
type _SSFProvider SSFProvider
@@ -376,6 +377,38 @@ func (o *SSFProvider) SetEventRetention(v string) {
o.EventRetention = &v
}
// GetPushVerifyCertificates returns the PushVerifyCertificates field value if set, zero value otherwise.
func (o *SSFProvider) GetPushVerifyCertificates() bool {
if o == nil || IsNil(o.PushVerifyCertificates) {
var ret bool
return ret
}
return *o.PushVerifyCertificates
}
// GetPushVerifyCertificatesOk returns a tuple with the PushVerifyCertificates field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *SSFProvider) GetPushVerifyCertificatesOk() (*bool, bool) {
if o == nil || IsNil(o.PushVerifyCertificates) {
return nil, false
}
return o.PushVerifyCertificates, true
}
// HasPushVerifyCertificates returns a boolean if a field has been set.
func (o *SSFProvider) HasPushVerifyCertificates() bool {
if o != nil && !IsNil(o.PushVerifyCertificates) {
return true
}
return false
}
// SetPushVerifyCertificates gets a reference to the given bool and assigns it to the PushVerifyCertificates field.
func (o *SSFProvider) SetPushVerifyCertificates(v bool) {
o.PushVerifyCertificates = &v
}
func (o SSFProvider) MarshalJSON() ([]byte, error) {
toSerialize, err := o.ToMap()
if err != nil {
@@ -402,6 +435,9 @@ func (o SSFProvider) ToMap() (map[string]interface{}, error) {
if !IsNil(o.EventRetention) {
toSerialize["event_retention"] = o.EventRetention
}
if !IsNil(o.PushVerifyCertificates) {
toSerialize["push_verify_certificates"] = o.PushVerifyCertificates
}
for key, value := range o.AdditionalProperties {
toSerialize[key] = value
@@ -466,6 +502,7 @@ func (o *SSFProvider) UnmarshalJSON(data []byte) (err error) {
delete(additionalProperties, "oidc_auth_providers_obj")
delete(additionalProperties, "ssf_url")
delete(additionalProperties, "event_retention")
delete(additionalProperties, "push_verify_certificates")
o.AdditionalProperties = additionalProperties
}
+41 -4
View File
@@ -23,10 +23,11 @@ var _ MappedNullable = &SSFProviderRequest{}
type SSFProviderRequest struct {
Name string `json:"name"`
// Key used to sign the SSF Events.
SigningKey string `json:"signing_key"`
OidcAuthProviders []int32 `json:"oidc_auth_providers,omitempty"`
EventRetention *string `json:"event_retention,omitempty"`
AdditionalProperties map[string]interface{}
SigningKey string `json:"signing_key"`
OidcAuthProviders []int32 `json:"oidc_auth_providers,omitempty"`
EventRetention *string `json:"event_retention,omitempty"`
PushVerifyCertificates *bool `json:"push_verify_certificates,omitempty"`
AdditionalProperties map[string]interface{}
}
type _SSFProviderRequest SSFProviderRequest
@@ -162,6 +163,38 @@ func (o *SSFProviderRequest) SetEventRetention(v string) {
o.EventRetention = &v
}
// GetPushVerifyCertificates returns the PushVerifyCertificates field value if set, zero value otherwise.
func (o *SSFProviderRequest) GetPushVerifyCertificates() bool {
if o == nil || IsNil(o.PushVerifyCertificates) {
var ret bool
return ret
}
return *o.PushVerifyCertificates
}
// GetPushVerifyCertificatesOk returns a tuple with the PushVerifyCertificates field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *SSFProviderRequest) GetPushVerifyCertificatesOk() (*bool, bool) {
if o == nil || IsNil(o.PushVerifyCertificates) {
return nil, false
}
return o.PushVerifyCertificates, true
}
// HasPushVerifyCertificates returns a boolean if a field has been set.
func (o *SSFProviderRequest) HasPushVerifyCertificates() bool {
if o != nil && !IsNil(o.PushVerifyCertificates) {
return true
}
return false
}
// SetPushVerifyCertificates gets a reference to the given bool and assigns it to the PushVerifyCertificates field.
func (o *SSFProviderRequest) SetPushVerifyCertificates(v bool) {
o.PushVerifyCertificates = &v
}
func (o SSFProviderRequest) MarshalJSON() ([]byte, error) {
toSerialize, err := o.ToMap()
if err != nil {
@@ -180,6 +213,9 @@ func (o SSFProviderRequest) ToMap() (map[string]interface{}, error) {
if !IsNil(o.EventRetention) {
toSerialize["event_retention"] = o.EventRetention
}
if !IsNil(o.PushVerifyCertificates) {
toSerialize["push_verify_certificates"] = o.PushVerifyCertificates
}
for key, value := range o.AdditionalProperties {
toSerialize[key] = value
@@ -228,6 +264,7 @@ func (o *SSFProviderRequest) UnmarshalJSON(data []byte) (err error) {
delete(additionalProperties, "signing_key")
delete(additionalProperties, "oidc_auth_providers")
delete(additionalProperties, "event_retention")
delete(additionalProperties, "push_verify_certificates")
o.AdditionalProperties = additionalProperties
}
+37
View File
@@ -22,6 +22,7 @@ var _ MappedNullable = &SSFStream{}
// SSFStream SSFStream Serializer
type SSFStream struct {
Pk string `json:"pk"`
Status *SSFStreamStatusEnum `json:"status,omitempty"`
Provider int32 `json:"provider"`
ProviderObj SSFProvider `json:"provider_obj"`
DeliveryMethod DeliveryMethodEnum `json:"delivery_method"`
@@ -82,6 +83,38 @@ func (o *SSFStream) SetPk(v string) {
o.Pk = v
}
// GetStatus returns the Status field value if set, zero value otherwise.
func (o *SSFStream) GetStatus() SSFStreamStatusEnum {
if o == nil || IsNil(o.Status) {
var ret SSFStreamStatusEnum
return ret
}
return *o.Status
}
// GetStatusOk returns a tuple with the Status field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *SSFStream) GetStatusOk() (*SSFStreamStatusEnum, bool) {
if o == nil || IsNil(o.Status) {
return nil, false
}
return o.Status, true
}
// HasStatus returns a boolean if a field has been set.
func (o *SSFStream) HasStatus() bool {
if o != nil && !IsNil(o.Status) {
return true
}
return false
}
// SetStatus gets a reference to the given SSFStreamStatusEnum and assigns it to the Status field.
func (o *SSFStream) SetStatus(v SSFStreamStatusEnum) {
o.Status = &v
}
// GetProvider returns the Provider field value
func (o *SSFStream) GetProvider() int32 {
if o == nil {
@@ -320,6 +353,9 @@ func (o SSFStream) MarshalJSON() ([]byte, error) {
func (o SSFStream) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["pk"] = o.Pk
if !IsNil(o.Status) {
toSerialize["status"] = o.Status
}
toSerialize["provider"] = o.Provider
toSerialize["provider_obj"] = o.ProviderObj
toSerialize["delivery_method"] = o.DeliveryMethod
@@ -383,6 +419,7 @@ func (o *SSFStream) UnmarshalJSON(data []byte) (err error) {
if err = json.Unmarshal(data, &additionalProperties); err == nil {
delete(additionalProperties, "pk")
delete(additionalProperties, "status")
delete(additionalProperties, "provider")
delete(additionalProperties, "provider_obj")
delete(additionalProperties, "delivery_method")
+113
View File
@@ -0,0 +1,113 @@
/*
authentik
Making authentication simple.
API version: 2026.5.0-rc1
Contact: hello@goauthentik.io
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package api
import (
"encoding/json"
"fmt"
)
// SSFStreamStatusEnum the model 'SSFStreamStatusEnum'
type SSFStreamStatusEnum string
// List of SSFStreamStatusEnum
const (
SSFSTREAMSTATUSENUM_ENABLED SSFStreamStatusEnum = "enabled"
SSFSTREAMSTATUSENUM_PAUSED SSFStreamStatusEnum = "paused"
SSFSTREAMSTATUSENUM_DISABLED SSFStreamStatusEnum = "disabled"
)
// All allowed values of SSFStreamStatusEnum enum
var AllowedSSFStreamStatusEnumEnumValues = []SSFStreamStatusEnum{
"enabled",
"paused",
"disabled",
}
func (v *SSFStreamStatusEnum) UnmarshalJSON(src []byte) error {
var value string
err := json.Unmarshal(src, &value)
if err != nil {
return err
}
enumTypeValue := SSFStreamStatusEnum(value)
for _, existing := range AllowedSSFStreamStatusEnumEnumValues {
if existing == enumTypeValue {
*v = enumTypeValue
return nil
}
}
return fmt.Errorf("%+v is not a valid SSFStreamStatusEnum", value)
}
// NewSSFStreamStatusEnumFromValue returns a pointer to a valid SSFStreamStatusEnum
// for the value passed as argument, or an error if the value passed is not allowed by the enum
func NewSSFStreamStatusEnumFromValue(v string) (*SSFStreamStatusEnum, error) {
ev := SSFStreamStatusEnum(v)
if ev.IsValid() {
return &ev, nil
} else {
return nil, fmt.Errorf("invalid value '%v' for SSFStreamStatusEnum: valid values are %v", v, AllowedSSFStreamStatusEnumEnumValues)
}
}
// IsValid return true if the value is valid for the enum, false otherwise
func (v SSFStreamStatusEnum) IsValid() bool {
for _, existing := range AllowedSSFStreamStatusEnumEnumValues {
if existing == v {
return true
}
}
return false
}
// Ptr returns reference to SSFStreamStatusEnum value
func (v SSFStreamStatusEnum) Ptr() *SSFStreamStatusEnum {
return &v
}
type NullableSSFStreamStatusEnum struct {
value *SSFStreamStatusEnum
isSet bool
}
func (v NullableSSFStreamStatusEnum) Get() *SSFStreamStatusEnum {
return v.value
}
func (v *NullableSSFStreamStatusEnum) Set(val *SSFStreamStatusEnum) {
v.value = val
v.isSet = true
}
func (v NullableSSFStreamStatusEnum) IsSet() bool {
return v.isSet
}
func (v *NullableSSFStreamStatusEnum) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableSSFStreamStatusEnum(val *SSFStreamStatusEnum) *NullableSSFStreamStatusEnum {
return &NullableSSFStreamStatusEnum{value: val, isSet: true}
}
func (v NullableSSFStreamStatusEnum) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableSSFStreamStatusEnum) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}
+51
View File
@@ -12,6 +12,15 @@ use serde::{Deserialize, Serialize, de::Error as _};
use super::{ContentType, Error, configuration};
use crate::{apis::ResponseContent, models};
/// struct for typed errors of method [`ssf_streams_destroy`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SsfStreamsDestroyError {
Status400(models::ValidationError),
Status403(models::GenericError),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`ssf_streams_list`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
@@ -30,6 +39,48 @@ pub enum SsfStreamsRetrieveError {
UnknownValue(serde_json::Value),
}
/// SSFStream Viewset
pub async fn ssf_streams_destroy(
configuration: &configuration::Configuration,
uuid: &str,
) -> Result<(), Error<SsfStreamsDestroyError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_path_uuid = uuid;
let uri_str = format!(
"{}/ssf/streams/{uuid}/",
configuration.base_path,
uuid = crate::apis::urlencode(p_path_uuid)
);
let mut req_builder = configuration
.client
.request(reqwest::Method::DELETE, &uri_str);
if let Some(ref user_agent) = configuration.user_agent {
req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}
if let Some(ref token) = configuration.bearer_access_token {
req_builder = req_builder.bearer_auth(token.to_owned());
};
let req = req_builder.build()?;
let resp = configuration.client.execute(req).await?;
let status = resp.status();
if !status.is_client_error() && !status.is_server_error() {
Ok(())
} else {
let content = resp.text().await?;
let entity: Option<SsfStreamsDestroyError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// SSFStream Viewset
pub async fn ssf_streams_list(
configuration: &configuration::Configuration,
+6
View File
@@ -17,6 +17,10 @@ pub enum DeliveryMethodEnum {
HttpsColonSlashSlashSchemasOpenidNetSlashSeceventSlashRiscSlashDeliveryMethodSlashPush,
#[serde(rename = "https://schemas.openid.net/secevent/risc/delivery-method/poll")]
HttpsColonSlashSlashSchemasOpenidNetSlashSeceventSlashRiscSlashDeliveryMethodSlashPoll,
#[serde(rename = "urn:ietf:rfc:8935")]
UrnColonIetfColonRfcColon8935,
#[serde(rename = "urn:ietf:rfc:8936")]
UrnColonIetfColonRfcColon8936,
}
impl std::fmt::Display for DeliveryMethodEnum {
@@ -24,6 +28,8 @@ impl std::fmt::Display for DeliveryMethodEnum {
match self {
Self::HttpsColonSlashSlashSchemasOpenidNetSlashSeceventSlashRiscSlashDeliveryMethodSlashPush => write!(f, "https://schemas.openid.net/secevent/risc/delivery-method/push"),
Self::HttpsColonSlashSlashSchemasOpenidNetSlashSeceventSlashRiscSlashDeliveryMethodSlashPoll => write!(f, "https://schemas.openid.net/secevent/risc/delivery-method/poll"),
Self::UrnColonIetfColonRfcColon8935 => write!(f, "urn:ietf:rfc:8935"),
Self::UrnColonIetfColonRfcColon8936 => write!(f, "urn:ietf:rfc:8936"),
}
}
}
+2
View File
@@ -1528,6 +1528,8 @@ pub mod ssf_provider_request;
pub use self::ssf_provider_request::SsfProviderRequest;
pub mod ssf_stream;
pub use self::ssf_stream::SsfStream;
pub mod ssf_stream_status_enum;
pub use self::ssf_stream_status_enum::SsfStreamStatusEnum;
pub mod stage;
pub use self::stage::Stage;
pub mod stage_mode_enum;
@@ -25,6 +25,11 @@ pub struct PatchedSsfProviderRequest {
pub oidc_auth_providers: Option<Vec<i32>>,
#[serde(rename = "event_retention", skip_serializing_if = "Option::is_none")]
pub event_retention: Option<String>,
#[serde(
rename = "push_verify_certificates",
skip_serializing_if = "Option::is_none"
)]
pub push_verify_certificates: Option<bool>,
}
impl PatchedSsfProviderRequest {
@@ -35,6 +40,7 @@ impl PatchedSsfProviderRequest {
signing_key: None,
oidc_auth_providers: None,
event_retention: None,
push_verify_certificates: None,
}
}
}
+6
View File
@@ -45,6 +45,11 @@ pub struct SsfProvider {
pub ssf_url: Option<String>,
#[serde(rename = "event_retention", skip_serializing_if = "Option::is_none")]
pub event_retention: Option<String>,
#[serde(
rename = "push_verify_certificates",
skip_serializing_if = "Option::is_none"
)]
pub push_verify_certificates: Option<bool>,
}
impl SsfProvider {
@@ -74,6 +79,7 @@ impl SsfProvider {
oidc_auth_providers_obj,
ssf_url,
event_retention: None,
push_verify_certificates: None,
}
}
}
+6
View File
@@ -25,6 +25,11 @@ pub struct SsfProviderRequest {
pub oidc_auth_providers: Option<Vec<i32>>,
#[serde(rename = "event_retention", skip_serializing_if = "Option::is_none")]
pub event_retention: Option<String>,
#[serde(
rename = "push_verify_certificates",
skip_serializing_if = "Option::is_none"
)]
pub push_verify_certificates: Option<bool>,
}
impl SsfProviderRequest {
@@ -35,6 +40,7 @@ impl SsfProviderRequest {
signing_key,
oidc_auth_providers: None,
event_retention: None,
push_verify_certificates: None,
}
}
}
+3
View File
@@ -15,6 +15,8 @@ use crate::models;
pub struct SsfStream {
#[serde(rename = "pk")]
pub pk: uuid::Uuid,
#[serde(rename = "status", skip_serializing_if = "Option::is_none")]
pub status: Option<models::SsfStreamStatusEnum>,
#[serde(rename = "provider")]
pub provider: i32,
#[serde(rename = "provider_obj")]
@@ -50,6 +52,7 @@ impl SsfStream {
) -> SsfStream {
SsfStream {
pk,
status: None,
provider,
provider_obj,
delivery_method,
@@ -0,0 +1,38 @@
// authentik
//
// Making authentication simple.
//
// The version of the OpenAPI document: 2026.5.0-rc1
// Contact: hello@goauthentik.io
// Generated by: https://openapi-generator.tech
use serde::{Deserialize, Serialize};
use crate::models;
///
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum SsfStreamStatusEnum {
#[serde(rename = "enabled")]
Enabled,
#[serde(rename = "paused")]
Paused,
#[serde(rename = "disabled")]
Disabled,
}
impl std::fmt::Display for SsfStreamStatusEnum {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Enabled => write!(f, "enabled"),
Self::Paused => write!(f, "paused"),
Self::Disabled => write!(f, "disabled"),
}
}
}
impl Default for SsfStreamStatusEnum {
fn default() -> SsfStreamStatusEnum {
Self::Enabled
}
}
+1 -1
View File
@@ -15,4 +15,4 @@ build:
--git-repo-id authentik \
--git-user-id goauthentik
rm -rf "${PWD}/.openapi-generator"
npx prettier --cache --write -u "${PWD}"
npx prettier --write -u "${PWD}"
+67
View File
@@ -16,6 +16,10 @@ import type { DeliveryMethodEnum, PaginatedSSFStreamList, SSFStream } from "../m
import { PaginatedSSFStreamListFromJSON, SSFStreamFromJSON } from "../models/index";
import * as runtime from "../runtime";
export interface SsfStreamsDestroyRequest {
uuid: string;
}
export interface SsfStreamsListRequest {
deliveryMethod?: DeliveryMethodEnum;
endpointUrl?: string;
@@ -34,6 +38,69 @@ export interface SsfStreamsRetrieveRequest {
*
*/
export class SsfApi extends runtime.BaseAPI {
/**
* Creates request options for ssfStreamsDestroy without sending the request
*/
async ssfStreamsDestroyRequestOpts(
requestParameters: SsfStreamsDestroyRequest,
): Promise<runtime.RequestOpts> {
if (requestParameters["uuid"] == null) {
throw new runtime.RequiredError(
"uuid",
'Required parameter "uuid" was null or undefined when calling ssfStreamsDestroy().',
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.accessToken) {
const token = this.configuration.accessToken;
const tokenString = await token("authentik", []);
if (tokenString) {
headerParameters["Authorization"] = `Bearer ${tokenString}`;
}
}
let urlPath = `/ssf/streams/{uuid}/`;
urlPath = urlPath.replace(
`{${"uuid"}}`,
encodeURIComponent(String(requestParameters["uuid"])),
);
return {
path: urlPath,
method: "DELETE",
headers: headerParameters,
query: queryParameters,
};
}
/**
* SSFStream Viewset
*/
async ssfStreamsDestroyRaw(
requestParameters: SsfStreamsDestroyRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<void>> {
const requestOptions = await this.ssfStreamsDestroyRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
return new runtime.VoidApiResponse(response);
}
/**
* SSFStream Viewset
*/
async ssfStreamsDestroy(
requestParameters: SsfStreamsDestroyRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<void> {
await this.ssfStreamsDestroyRaw(requestParameters, initOverrides);
}
/**
* Creates request options for ssfStreamsList without sending the request
*/
+2
View File
@@ -21,6 +21,8 @@ export const DeliveryMethodEnum = {
"https://schemas.openid.net/secevent/risc/delivery-method/push",
HttpsSchemasOpenidNetSeceventRiscDeliveryMethodPoll:
"https://schemas.openid.net/secevent/risc/delivery-method/poll",
UrnIetfRfc8935: "urn:ietf:rfc:8935",
UrnIetfRfc8936: "urn:ietf:rfc:8936",
UnknownDefaultOpenApi: "11184809",
} as const;
export type DeliveryMethodEnum = (typeof DeliveryMethodEnum)[keyof typeof DeliveryMethodEnum];
@@ -42,6 +42,12 @@ export interface PatchedSSFProviderRequest {
* @memberof PatchedSSFProviderRequest
*/
eventRetention?: string;
/**
*
* @type {boolean}
* @memberof PatchedSSFProviderRequest
*/
pushVerifyCertificates?: boolean;
}
/**
@@ -70,6 +76,8 @@ export function PatchedSSFProviderRequestFromJSONTyped(
oidcAuthProviders:
json["oidc_auth_providers"] == null ? undefined : json["oidc_auth_providers"],
eventRetention: json["event_retention"] == null ? undefined : json["event_retention"],
pushVerifyCertificates:
json["push_verify_certificates"] == null ? undefined : json["push_verify_certificates"],
};
}
@@ -90,5 +98,6 @@ export function PatchedSSFProviderRequestToJSONTyped(
signing_key: value["signingKey"],
oidc_auth_providers: value["oidcAuthProviders"],
event_retention: value["eventRetention"],
push_verify_certificates: value["pushVerifyCertificates"],
};
}
+9
View File
@@ -95,6 +95,12 @@ export interface SSFProvider {
* @memberof SSFProvider
*/
eventRetention?: string;
/**
*
* @type {boolean}
* @memberof SSFProvider
*/
pushVerifyCertificates?: boolean;
}
/**
@@ -137,6 +143,8 @@ export function SSFProviderFromJSONTyped(json: any, ignoreDiscriminator: boolean
oidcAuthProvidersObj: (json["oidc_auth_providers_obj"] as Array<any>).map(ProviderFromJSON),
ssfUrl: json["ssf_url"],
eventRetention: json["event_retention"] == null ? undefined : json["event_retention"],
pushVerifyCertificates:
json["push_verify_certificates"] == null ? undefined : json["push_verify_certificates"],
};
}
@@ -167,5 +175,6 @@ export function SSFProviderToJSONTyped(
signing_key: value["signingKey"],
oidc_auth_providers: value["oidcAuthProviders"],
event_retention: value["eventRetention"],
push_verify_certificates: value["pushVerifyCertificates"],
};
}
+9
View File
@@ -42,6 +42,12 @@ export interface SSFProviderRequest {
* @memberof SSFProviderRequest
*/
eventRetention?: string;
/**
*
* @type {boolean}
* @memberof SSFProviderRequest
*/
pushVerifyCertificates?: boolean;
}
/**
@@ -70,6 +76,8 @@ export function SSFProviderRequestFromJSONTyped(
oidcAuthProviders:
json["oidc_auth_providers"] == null ? undefined : json["oidc_auth_providers"],
eventRetention: json["event_retention"] == null ? undefined : json["event_retention"],
pushVerifyCertificates:
json["push_verify_certificates"] == null ? undefined : json["push_verify_certificates"],
};
}
@@ -90,5 +98,6 @@ export function SSFProviderRequestToJSONTyped(
signing_key: value["signingKey"],
oidc_auth_providers: value["oidcAuthProviders"],
event_retention: value["eventRetention"],
push_verify_certificates: value["pushVerifyCertificates"],
};
}
+10
View File
@@ -18,6 +18,8 @@ import type { EventsRequestedEnum } from "./EventsRequestedEnum";
import { EventsRequestedEnumFromJSON, EventsRequestedEnumToJSON } from "./EventsRequestedEnum";
import type { SSFProvider } from "./SSFProvider";
import { SSFProviderFromJSON } from "./SSFProvider";
import type { SSFStreamStatusEnum } from "./SSFStreamStatusEnum";
import { SSFStreamStatusEnumFromJSON, SSFStreamStatusEnumToJSON } from "./SSFStreamStatusEnum";
/**
* SSFStream Serializer
@@ -31,6 +33,12 @@ export interface SSFStream {
* @memberof SSFStream
*/
readonly pk: string;
/**
*
* @type {SSFStreamStatusEnum}
* @memberof SSFStream
*/
status?: SSFStreamStatusEnum;
/**
*
* @type {number}
@@ -104,6 +112,7 @@ export function SSFStreamFromJSONTyped(json: any, ignoreDiscriminator: boolean):
}
return {
pk: json["pk"],
status: json["status"] == null ? undefined : SSFStreamStatusEnumFromJSON(json["status"]),
provider: json["provider"],
providerObj: SSFProviderFromJSON(json["provider_obj"]),
deliveryMethod: DeliveryMethodEnumFromJSON(json["delivery_method"]),
@@ -131,6 +140,7 @@ export function SSFStreamToJSONTyped(
}
return {
status: SSFStreamStatusEnumToJSON(value["status"]),
provider: value["provider"],
delivery_method: DeliveryMethodEnumToJSON(value["deliveryMethod"]),
endpoint_url: value["endpointUrl"],
+58
View File
@@ -0,0 +1,58 @@
/* tslint:disable */
/* eslint-disable */
/**
* authentik
* Making authentication simple.
*
* The version of the OpenAPI document: 2026.5.0-rc1
* Contact: hello@goauthentik.io
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
*
* @export
*/
export const SSFStreamStatusEnum = {
Enabled: "enabled",
Paused: "paused",
Disabled: "disabled",
UnknownDefaultOpenApi: "11184809",
} as const;
export type SSFStreamStatusEnum = (typeof SSFStreamStatusEnum)[keyof typeof SSFStreamStatusEnum];
export function instanceOfSSFStreamStatusEnum(value: any): boolean {
for (const key in SSFStreamStatusEnum) {
if (Object.prototype.hasOwnProperty.call(SSFStreamStatusEnum, key)) {
if (SSFStreamStatusEnum[key as keyof typeof SSFStreamStatusEnum] === value) {
return true;
}
}
}
return false;
}
export function SSFStreamStatusEnumFromJSON(json: any): SSFStreamStatusEnum {
return SSFStreamStatusEnumFromJSONTyped(json, false);
}
export function SSFStreamStatusEnumFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): SSFStreamStatusEnum {
return json as SSFStreamStatusEnum;
}
export function SSFStreamStatusEnumToJSON(value?: SSFStreamStatusEnum | null): any {
return value as any;
}
export function SSFStreamStatusEnumToJSONTyped(
value: any,
ignoreDiscriminator: boolean,
): SSFStreamStatusEnum {
return value as SSFStreamStatusEnum;
}
+1
View File
@@ -745,6 +745,7 @@ export * from "./SMSDeviceRequest";
export * from "./SSFProvider";
export * from "./SSFProviderRequest";
export * from "./SSFStream";
export * from "./SSFStreamStatusEnum";
export * from "./Schedule";
export * from "./ScheduleRequest";
export * from "./ScopeMapping";
+38
View File
@@ -26629,6 +26629,28 @@ paths:
$ref: '#/components/responses/ValidationErrorResponse'
'403':
$ref: '#/components/responses/GenericErrorResponse'
delete:
operationId: ssf_streams_destroy
description: SSFStream Viewset
parameters:
- in: path
name: uuid
schema:
type: string
format: uuid
description: A UUID string identifying this SSF Stream.
required: true
tags:
- ssf
security:
- authentik: []
responses:
'204':
description: No response body
'400':
$ref: '#/components/responses/ValidationErrorResponse'
'403':
$ref: '#/components/responses/GenericErrorResponse'
/stages/all/:
get:
operationId: stages_all_list
@@ -36574,6 +36596,8 @@ components:
enum:
- https://schemas.openid.net/secevent/risc/delivery-method/push
- https://schemas.openid.net/secevent/risc/delivery-method/poll
- urn:ietf:rfc:8935
- urn:ietf:rfc:8936
type: string
DeniedActionEnum:
enum:
@@ -50276,6 +50300,8 @@ components:
event_retention:
type: string
minLength: 1
push_verify_certificates:
type: boolean
PatchedScheduleRequest:
type: object
properties:
@@ -54696,6 +54722,8 @@ components:
readOnly: true
event_retention:
type: string
push_verify_certificates:
type: boolean
required:
- component
- meta_model_name
@@ -54725,6 +54753,8 @@ components:
event_retention:
type: string
minLength: 1
push_verify_certificates:
type: boolean
required:
- name
- signing_key
@@ -54737,6 +54767,8 @@ components:
format: uuid
readOnly: true
title: Uuid
status:
$ref: '#/components/schemas/SSFStreamStatusEnum'
provider:
type: integer
provider_obj:
@@ -54767,6 +54799,12 @@ components:
- pk
- provider
- provider_obj
SSFStreamStatusEnum:
enum:
- enabled
- paused
- disabled
type: string
Schedule:
type: object
properties:
+3 -3
View File
@@ -1,15 +1,15 @@
services:
mongodb:
image: mongo:6.0.13
httpd:
image: ghcr.io/beryju/oidc-conformance-suite-httpd:v5.1.32
nginx:
image: ghcr.io/beryju/oidc-conformance-suite-nginx:v5.1.41
ports:
- "8443:8443"
- "8444:8444"
depends_on:
- server
server:
image: ghcr.io/beryju/oidc-conformance-suite-server:v5.1.32
image: ghcr.io/beryju/oidc-conformance-suite-server:v5.1.41
ports:
- "9999:9999"
extra_hosts:
@@ -6,6 +6,7 @@ import "#elements/forms/FormGroup";
import "#elements/forms/HorizontalFormElement";
import "#elements/forms/SearchSelect/index";
import "#elements/utils/TimeDeltaHelp";
import "#components/ak-switch-input";
import { DEFAULT_CONFIG } from "#common/api/config";
@@ -76,6 +77,12 @@ export class SSFProviderFormPage extends BaseProviderForm<SSFProvider> {
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">${msg("Key used to sign the events.")}</p>
</ak-form-element-horizontal>
<ak-switch-input
name="pushVerifyCertificates"
label=${msg("Verify Push stream endpoints' certificate")}
?checked=${this.instance?.pushVerifyCertificates ?? true}
>
</ak-switch-input>
<ak-form-element-horizontal
label=${msg("Event Retention")}
required
@@ -24,7 +24,6 @@ import { msg } from "@lit/localize";
import { CSSResult, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
@@ -58,7 +57,6 @@ export class SSFProviderViewPage extends AKElement {
PFDescriptionList,
PFForm,
PFFormControl,
PFBanner,
PFList,
PFList,
];
@@ -120,80 +118,70 @@ export class SSFProviderViewPage extends AKElement {
return nothing;
}
const [appLabel, modelName] = ModelEnum.AuthentikProvidersSsfSsfprovider.split(".");
return html`<div slot="header" class="pf-c-banner pf-m-info">
${msg("SSF Provider is in preview.")}
<a href="mailto:hello+feature/ssf@goauthentik.io">${msg("Send us feedback!")}</a>
return html`<div
class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"
>
<div class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-4-col-on-xl pf-m-4-col-on-2xl">
<div class="pf-c-card__body">
${renderDescriptionList([
[msg("Name"), html`${this.provider.name}`],
[
msg("URL"),
html`<input
class="pf-c-form-control pf-m-monospace"
readonly
type="text"
value=${this.provider.ssfUrl || ""}
placeholder=${this.provider.ssfUrl
? msg("SSF URL")
: msg("No assigned application")}
/>`,
],
[
msg("Federated OAuth2/OpenID Providers"),
(this.provider.oidcAuthProvidersObj || []).length > 0
? html`<ul class="pf-c-list">
${this.provider.oidcAuthProvidersObj.map((provider) => {
return html`
<li>
<a href="#/core/providers/${provider.pk}">
${provider.name}
</a>
</li>
`;
})}
</ul>`
: html`-`,
],
[
msg("Related actions"),
html`<ak-forms-modal>
<span slot="submit">${msg("Save Changes")}</span>
<span slot="header">${msg("Update SSF Provider")}</span>
<ak-provider-ssf-form slot="form" .instancePk=${this.provider.pk}>
</ak-provider-ssf-form>
<button slot="trigger" class="pf-c-button pf-m-primary pf-m-block">
${msg("Edit")}
</button>
</ak-forms-modal>`,
],
])}
</div>
</div>
<div class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter">
<div
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-4-col-on-xl pf-m-4-col-on-2xl"
>
<div class="pf-c-card__body">
${renderDescriptionList([
[msg("Name"), html`${this.provider.name}`],
[
msg("URL"),
html`<input
class="pf-c-form-control pf-m-monospace"
readonly
type="text"
value=${this.provider.ssfUrl || ""}
placeholder=${this.provider.ssfUrl
? msg("SSF URL")
: msg("No assigned application")}
/>`,
],
[
msg("Federated OAuth2/OpenID Providers"),
(this.provider.oidcAuthProvidersObj || []).length > 0
? html`<ul class="pf-c-list">
${this.provider.oidcAuthProvidersObj.map((provider) => {
return html`
<li>
<a href="#/core/providers/${provider.pk}">
${provider.name}
</a>
</li>
`;
})}
</ul>`
: html`-`,
],
[
msg("Related actions"),
html`<ak-forms-modal>
<span slot="submit">${msg("Save Changes")}</span>
<span slot="header">${msg("Update SSF Provider")}</span>
<ak-provider-ssf-form
slot="form"
.instancePk=${this.provider.pk}
>
</ak-provider-ssf-form>
<button
slot="trigger"
class="pf-c-button pf-m-primary pf-m-block"
>
${msg("Edit")}
</button>
</ak-forms-modal>`,
],
])}
</div>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-8-col-on-2xl">
<div class="pf-c-card__title">${msg("Streams")}</div>
<ak-provider-ssf-stream-list .providerId=${this.providerID}>
</ak-provider-ssf-stream-list>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col-on-2xl">
<div class="pf-c-card__title">${msg("Tasks")}</div>
<ak-task-list
.relObjAppLabel=${appLabel}
.relObjModel=${modelName}
.relObjId="${this.provider.pk}"
></ak-task-list>
</div>
</div>`;
<div class="pf-c-card pf-l-grid__item pf-m-8-col-on-2xl">
<div class="pf-c-card__title">${msg("Streams")}</div>
<ak-provider-ssf-stream-list .providerId=${this.providerID}>
</ak-provider-ssf-stream-list>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col-on-2xl">
<div class="pf-c-card__title">${msg("Tasks")}</div>
<ak-task-list
.relObjAppLabel=${appLabel}
.relObjModel=${modelName}
.relObjId="${this.provider.pk}"
></ak-task-list>
</div>
</div>`;
}
}
+49 -4
View File
@@ -8,17 +8,24 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";
import renderDescriptionList from "#components/DescriptionList";
import { SSFDeliveryMethodToLabel } from "#admin/providers/ssf/utils";
import { SsfApi, SSFStream } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html } from "lit";
import { CSSResult, html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
@customElement("ak-provider-ssf-stream-list")
export class SSFProviderStreamList extends Table<SSFStream> {
protected override searchEnabled = true;
checkbox = true;
clearOnRefresh = true;
public checkbox: boolean = true;
public clearOnRefresh: boolean = true;
public expandable: boolean = true;
@property({ type: Number })
providerId?: number;
@@ -26,13 +33,43 @@ export class SSFProviderStreamList extends Table<SSFStream> {
@property()
order = "name";
static styles: CSSResult[] = [...super.styles, PFDescriptionList];
async apiEndpoint(): Promise<PaginatedResponse<SSFStream>> {
return new SsfApi(DEFAULT_CONFIG).ssfStreamsList({
provider: this.providerId,
...(await this.defaultEndpointConfig()),
pageSize: 10,
});
}
protected override renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
object-label=${msg("Stream(s)")}
.objects=${this.selectedElements}
.delete=${(item: SSFStream) => {
return new SsfApi(DEFAULT_CONFIG).ssfStreamsDestroy({
uuid: item.pk,
});
}}
.metadata=${(item: SSFStream) => {
return [
{ key: msg("Audience"), value: item.aud },
{
key: msg("Delivery method"),
value: SSFDeliveryMethodToLabel(item.deliveryMethod),
},
{ key: msg("Endpoint"), value: item.endpointUrl ?? "-" },
];
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
protected override rowLabel(item: SSFStream): string | null {
return item.aud?.join(", ") ?? null;
}
@@ -40,10 +77,18 @@ export class SSFProviderStreamList extends Table<SSFStream> {
protected columns: TableColumn[] = [
// ---
[msg("Audience"), "aud"],
[msg("Delivery Method"), "delivery_method"],
];
protected renderExpanded(item: SSFStream): SlottedTemplateResult {
return html`${renderDescriptionList([
[msg("Delivery method"), html`${SSFDeliveryMethodToLabel(item.deliveryMethod)}`],
[msg("Endpoint"), html`${item.endpointUrl ?? "-"}`],
])}`;
}
row(item: SSFStream): SlottedTemplateResult[] {
return [html`${item.aud}`];
return [html`${item.aud}`, html`${SSFDeliveryMethodToLabel(item.deliveryMethod)}`];
}
}
+16
View File
@@ -0,0 +1,16 @@
import { DeliveryMethodEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
export function SSFDeliveryMethodToLabel(method?: DeliveryMethodEnum): string {
if (!method) return "";
switch (method) {
case DeliveryMethodEnum.HttpsSchemasOpenidNetSeceventRiscDeliveryMethodPoll:
case DeliveryMethodEnum.UrnIetfRfc8936:
return msg("Pull");
case DeliveryMethodEnum.HttpsSchemasOpenidNetSeceventRiscDeliveryMethodPush:
case DeliveryMethodEnum.UrnIetfRfc8935:
return msg("Push");
}
return "";
}
@@ -4,8 +4,7 @@ sidebar_label: SSF Provider
description: "Overview of SSF and the authentik SSF provider"
authentik_version: "2025.2.0"
authentik_enterprise: true
authentik_preview: true
tags: [Shared Signals Framework, SSF, Apple Business Manager]
tags: [Shared Signals Framework, SSF, Apple Business Manager, Apple School Manager]
---
The Shared Signals Framework (SSF) provider allows you to integrate applications with the Shared Signals Framework protocol.