diff --git a/authentik/stages/invitation/api.py b/authentik/stages/invitation/api.py index b1ce74cd5..a1b5d5836 100644 --- a/authentik/stages/invitation/api.py +++ b/authentik/stages/invitation/api.py @@ -39,6 +39,7 @@ class InvitationSerializer(ModelSerializer): "expires", "fixed_data", "created_by", + "single_use", ] depth = 2 diff --git a/authentik/stages/invitation/migrations/0004_invitation_single_use.py b/authentik/stages/invitation/migrations/0004_invitation_single_use.py new file mode 100644 index 000000000..f03d5e699 --- /dev/null +++ b/authentik/stages/invitation/migrations/0004_invitation_single_use.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2 on 2021-05-03 07:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_stages_invitation", "0003_auto_20201227_1210"), + ] + + operations = [ + migrations.AddField( + model_name="invitation", + name="single_use", + field=models.BooleanField( + default=False, + help_text="When enabled, the invitation will be deleted after usage.", + ), + ), + ] diff --git a/authentik/stages/invitation/models.py b/authentik/stages/invitation/models.py index 9e013b1f6..84c08d8ff 100644 --- a/authentik/stages/invitation/models.py +++ b/authentik/stages/invitation/models.py @@ -53,6 +53,11 @@ class Invitation(models.Model): invite_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) + single_use = models.BooleanField( + default=False, + help_text=_("When enabled, the invitation will be deleted after usage."), + ) + created_by = models.ForeignKey(User, on_delete=models.CASCADE) expires = models.DateTimeField(default=None, blank=True, null=True) fixed_data = models.JSONField( diff --git a/authentik/stages/invitation/stage.py b/authentik/stages/invitation/stage.py index 6ce02d2e3..5385411d4 100644 --- a/authentik/stages/invitation/stage.py +++ b/authentik/stages/invitation/stage.py @@ -1,4 +1,5 @@ """invitation stage logic""" +from copy import deepcopy from typing import Optional from django.http import HttpRequest, HttpResponse @@ -38,7 +39,9 @@ class InvitationStageView(StageView): return self.executor.stage_invalid() invite: Invitation = get_object_or_404(Invitation, pk=token) - self.executor.plan.context[PLAN_CONTEXT_PROMPT] = invite.fixed_data + self.executor.plan.context[PLAN_CONTEXT_PROMPT] = deepcopy(invite.fixed_data) self.executor.plan.context[INVITATION_IN_EFFECT] = True invitation_used.send(sender=self, request=request, invitation=invite) + if invite.single_use: + invite.delete() return self.executor.stage_ok() diff --git a/authentik/stages/invitation/tests.py b/authentik/stages/invitation/tests.py index b556dab81..44186979d 100644 --- a/authentik/stages/invitation/tests.py +++ b/authentik/stages/invitation/tests.py @@ -130,7 +130,9 @@ class TestUserLoginStage(TestCase): """Test with invitation, check data in session""" data = {"foo": "bar"} invite = Invitation.objects.create( - created_by=get_anonymous_user(), fixed_data=data + created_by=get_anonymous_user(), + fixed_data=data, + single_use=True ) plan = FlowPlan( @@ -156,6 +158,7 @@ class TestUserLoginStage(TestCase): force_str(response.content), {"to": reverse("authentik_core:root-redirect"), "type": "redirect"}, ) + self.assertFalse(Invitation.objects.filter(pk=invite.pk)) class TestInvitationsAPI(APITestCase): diff --git a/swagger.yaml b/swagger.yaml index b5ee65cab..c2509ca1d 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -18248,6 +18248,10 @@ definitions: x-nullable: true readOnly: true readOnly: true + single_use: + title: Single use + description: When enabled, the invitation will be deleted after usage. + type: boolean InvitationStage: required: - name diff --git a/web/src/locales/en.po b/web/src/locales/en.po index d099bda68..40906aa58 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -2841,6 +2841,10 @@ msgstr "Signing keypair" msgid "Single Prompts that can be used for Prompt Stages." msgstr "Single Prompts that can be used for Prompt Stages." +#: src/pages/stages/invitation/InvitationForm.ts:62 +msgid "Single use" +msgstr "Single use" + #: src/pages/providers/proxy/ProxyProviderForm.ts:173 msgid "Skip path regex" msgstr "Skip path regex" @@ -3877,6 +3881,10 @@ msgstr "When a valid username/email has been entered, and this option is enabled msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored." msgstr "When enabled, global Email connection settings will be used and connection settings below will be ignored." +#: src/pages/stages/invitation/InvitationForm.ts:66 +msgid "When enabled, the invitation will be deleted after usage." +msgstr "When enabled, the invitation will be deleted after usage." + #: src/pages/stages/identification/IdentificationStageForm.ts:94 msgid "When enabled, user fields are matched regardless of their casing." msgstr "When enabled, user fields are matched regardless of their casing." diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index a696cf5ff..857a494bc 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -2833,6 +2833,10 @@ msgstr "" msgid "Single Prompts that can be used for Prompt Stages." msgstr "" +#: src/pages/stages/invitation/InvitationForm.ts:62 +msgid "Single use" +msgstr "" + #: src/pages/providers/proxy/ProxyProviderForm.ts:173 msgid "Skip path regex" msgstr "" @@ -3865,6 +3869,10 @@ msgstr "" msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored." msgstr "" +#: src/pages/stages/invitation/InvitationForm.ts:66 +msgid "When enabled, the invitation will be deleted after usage." +msgstr "" + #: src/pages/stages/identification/IdentificationStageForm.ts:94 msgid "When enabled, user fields are matched regardless of their casing." msgstr "" diff --git a/web/src/pages/stages/invitation/InvitationForm.ts b/web/src/pages/stages/invitation/InvitationForm.ts index 7497706a6..4ccbca9bf 100644 --- a/web/src/pages/stages/invitation/InvitationForm.ts +++ b/web/src/pages/stages/invitation/InvitationForm.ts @@ -51,6 +51,17 @@ export class InvitationForm extends Form {

${t`Optional data which is loaded into the flow's 'prompt_data' context variable. YAML or JSON.`}

+ +
+ + +
+

+ ${t`When enabled, the invitation will be deleted after usage.`} +

+
`; }