enterprise/providers/ssf: fixes v2 (#13003)
* enterprise/providers/ssf: check providers's application's policies to determine if an ssf event should be sent Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add preview banner to ssf provider Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix and test Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.core.models import User
|
||||||
from authentik.enterprise.providers.ssf.models import (
|
from authentik.enterprise.providers.ssf.models import (
|
||||||
DeliveryMethods,
|
DeliveryMethods,
|
||||||
EventTypes,
|
EventTypes,
|
||||||
@ -17,6 +18,7 @@ from authentik.events.models import TaskStatus
|
|||||||
from authentik.events.system_tasks import SystemTask
|
from authentik.events.system_tasks import SystemTask
|
||||||
from authentik.lib.utils.http import get_http_session
|
from authentik.lib.utils.http import get_http_session
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
session = get_http_session()
|
session = get_http_session()
|
||||||
@ -43,10 +45,32 @@ def send_ssf_event(
|
|||||||
return _send_ssf_event.delay(payload)
|
return _send_ssf_event.delay(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_app_access(stream_uuid: str, event_data: dict) -> bool:
|
||||||
|
"""Check if event is related to user and if so, check
|
||||||
|
if the user has access to the application"""
|
||||||
|
stream = Stream.objects.filter(pk=stream_uuid).first()
|
||||||
|
if not stream:
|
||||||
|
return False
|
||||||
|
# `event_data` is a dict version of a StreamEvent
|
||||||
|
sub_id = event_data.get("payload", {}).get("sub_id", {})
|
||||||
|
email = sub_id.get("user", {}).get("email", None)
|
||||||
|
if not email:
|
||||||
|
return True
|
||||||
|
user = User.objects.filter(email=email).first()
|
||||||
|
if not user:
|
||||||
|
return True
|
||||||
|
engine = PolicyEngine(stream.provider.backchannel_application, user)
|
||||||
|
engine.use_cache = False
|
||||||
|
engine.build()
|
||||||
|
return engine.passing
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task()
|
@CELERY_APP.task()
|
||||||
def _send_ssf_event(event_data: list[tuple[str, dict]]):
|
def _send_ssf_event(event_data: list[tuple[str, dict]]):
|
||||||
tasks = []
|
tasks = []
|
||||||
for stream, data in event_data:
|
for stream, data in event_data:
|
||||||
|
if not _check_app_access(stream, data):
|
||||||
|
continue
|
||||||
event = StreamEvent.objects.create(**data)
|
event = StreamEvent.objects.create(**data)
|
||||||
tasks.extend(send_single_ssf_event(stream, str(event.uuid)))
|
tasks.extend(send_single_ssf_event(stream, str(event.uuid)))
|
||||||
main_task = group(*tasks)
|
main_task = group(*tasks)
|
||||||
|
@ -3,18 +3,20 @@ from uuid import uuid4
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application, Group
|
||||||
from authentik.core.tests.utils import (
|
from authentik.core.tests.utils import (
|
||||||
create_test_cert,
|
create_test_cert,
|
||||||
create_test_user,
|
create_test_user,
|
||||||
)
|
)
|
||||||
from authentik.enterprise.providers.ssf.models import (
|
from authentik.enterprise.providers.ssf.models import (
|
||||||
|
EventTypes,
|
||||||
SSFEventStatus,
|
SSFEventStatus,
|
||||||
SSFProvider,
|
SSFProvider,
|
||||||
Stream,
|
Stream,
|
||||||
StreamEvent,
|
StreamEvent,
|
||||||
)
|
)
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
|
from authentik.policies.models import PolicyBinding
|
||||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||||
|
|
||||||
|
|
||||||
@ -147,3 +149,20 @@ class TestSignals(APITestCase):
|
|||||||
self.assertEqual(event.payload["sub_id"]["format"], "complex")
|
self.assertEqual(event.payload["sub_id"]["format"], "complex")
|
||||||
self.assertEqual(event.payload["sub_id"]["user"]["format"], "email")
|
self.assertEqual(event.payload["sub_id"]["user"]["format"], "email")
|
||||||
self.assertEqual(event.payload["sub_id"]["user"]["email"], user.email)
|
self.assertEqual(event.payload["sub_id"]["user"]["email"], user.email)
|
||||||
|
|
||||||
|
def test_signal_policy_ignore(self):
|
||||||
|
"""Test event not being created for user that doesn't have access to the application"""
|
||||||
|
PolicyBinding.objects.create(
|
||||||
|
target=self.application, group=Group.objects.create(name=generate_id()), order=0
|
||||||
|
)
|
||||||
|
user = create_test_user()
|
||||||
|
self.client.force_login(user)
|
||||||
|
user.set_password(generate_id())
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
stream = Stream.objects.filter(provider=self.provider).first()
|
||||||
|
self.assertIsNotNone(stream)
|
||||||
|
event = StreamEvent.objects.filter(
|
||||||
|
stream=stream, type=EventTypes.CAEP_CREDENTIAL_CHANGE
|
||||||
|
).first()
|
||||||
|
self.assertIsNone(event)
|
||||||
|
@ -110,10 +110,14 @@ export class SSFProviderViewPage extends AKElement {
|
|||||||
if (!this.provider) {
|
if (!this.provider) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`<div
|
return html`<div slot="header" class="pf-c-banner pf-m-info">
|
||||||
class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"
|
${msg("SSF Provider is in preview.")}
|
||||||
|
<a href="mailto:hello+feature/ssf@goauthentik.io">${msg("Send us feedback!")}</a>
|
||||||
|
</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 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">
|
<div class="pf-c-card__body">
|
||||||
<dl class="pf-c-description-list">
|
<dl class="pf-c-description-list">
|
||||||
<div class="pf-c-description-list__group">
|
<div class="pf-c-description-list__group">
|
||||||
@ -121,7 +125,9 @@ export class SSFProviderViewPage extends AKElement {
|
|||||||
<span class="pf-c-description-list__text">${msg("Name")}</span>
|
<span class="pf-c-description-list__text">${msg("Name")}</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="pf-c-description-list__description">
|
<dd class="pf-c-description-list__description">
|
||||||
<div class="pf-c-description-list__text">${this.provider.name}</div>
|
<div class="pf-c-description-list__text">
|
||||||
|
${this.provider.name}
|
||||||
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-description-list__group">
|
<div class="pf-c-description-list__group">
|
||||||
|
Reference in New Issue
Block a user