stages/consent: start implementing user consent
This commit is contained in:
parent
37b2400cdb
commit
50612991fa
|
@ -11,7 +11,7 @@ class ConsentStageSerializer(ModelSerializer):
|
|||
class Meta:
|
||||
|
||||
model = ConsentStage
|
||||
fields = ["pk", "name"]
|
||||
fields = ["pk", "name", "mode", "consent_expire_in"]
|
||||
|
||||
|
||||
class ConsentStageViewSet(ModelViewSet):
|
||||
|
|
|
@ -14,7 +14,7 @@ class ConsentStageForm(forms.ModelForm):
|
|||
class Meta:
|
||||
|
||||
model = ConsentStage
|
||||
fields = ["name"]
|
||||
fields = ["name", "mode", "consent_expire_in"]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
# Generated by Django 3.0.8 on 2020-07-20 09:41
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import passbook.core.models
|
||||
import passbook.lib.utils.time
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_core", "0006_auto_20200709_1608"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("passbook_stages_consent", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="consentstage",
|
||||
name="consent_expire_in",
|
||||
field=models.TextField(
|
||||
default="weeks=4",
|
||||
help_text="Offset after which consent expires. (Format: hours=1;minutes=2;seconds=3).",
|
||||
validators=[passbook.lib.utils.time.timedelta_string_validator],
|
||||
verbose_name="Consent expires in",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="consentstage",
|
||||
name="mode",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("always_require", "Always Require"),
|
||||
("permanent", "Permanent"),
|
||||
("expiring", "Expiring"),
|
||||
],
|
||||
default="always_require",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserConsent",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"expires",
|
||||
models.DateTimeField(
|
||||
default=passbook.core.models.default_token_duration
|
||||
),
|
||||
),
|
||||
("expiring", models.BooleanField(default=True)),
|
||||
(
|
||||
"application",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="passbook_core.Application",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="pb_consent",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "User Consent",
|
||||
"verbose_name_plural": "User Consents",
|
||||
"unique_together": {("user", "application")},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,13 +1,39 @@
|
|||
"""passbook consent stage"""
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.core.models import Application, ExpiringModel, User
|
||||
from passbook.flows.models import Stage
|
||||
from passbook.lib.utils.time import timedelta_string_validator
|
||||
|
||||
|
||||
class ConsentMode(models.TextChoices):
|
||||
"""Modes a Consent Stage can operate in"""
|
||||
|
||||
ALWAYS_REQUIRE = "always_require"
|
||||
PERMANENT = "permanent"
|
||||
EXPIRING = "expiring"
|
||||
|
||||
|
||||
class ConsentStage(Stage):
|
||||
"""Prompt the user for confirmation."""
|
||||
|
||||
type = "passbook.stages.consent.stage.ConsentStage"
|
||||
mode = models.TextField(
|
||||
choices=ConsentMode.choices, default=ConsentMode.ALWAYS_REQUIRE
|
||||
)
|
||||
consent_expire_in = models.TextField(
|
||||
validators=[timedelta_string_validator],
|
||||
default="weeks=4",
|
||||
verbose_name="Consent expires in",
|
||||
help_text=_(
|
||||
(
|
||||
"Offset after which consent expires. "
|
||||
"(Format: hours=1;minutes=2;seconds=3)."
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
type = "passbook.stages.consent.stage.ConsentStageView"
|
||||
form = "passbook.stages.consent.forms.ConsentStageForm"
|
||||
|
||||
def __str__(self):
|
||||
|
@ -17,3 +43,20 @@ class ConsentStage(Stage):
|
|||
|
||||
verbose_name = _("Consent Stage")
|
||||
verbose_name_plural = _("Consent Stages")
|
||||
|
||||
|
||||
class UserConsent(ExpiringModel):
|
||||
"""Consent given by a user for an application"""
|
||||
|
||||
# TODO: Remove related_name when oidc provider is v2
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="pb_consent")
|
||||
application = models.ForeignKey(Application, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return f"User Consent {self.application} by {self.user}"
|
||||
|
||||
class Meta:
|
||||
|
||||
unique_together = (("user", "application"),)
|
||||
verbose_name = _("User Consent")
|
||||
verbose_name_plural = _("User Consents")
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
"""passbook consent stage"""
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.timezone import now
|
||||
from django.views.generic import FormView
|
||||
|
||||
from passbook.flows.planner import PLAN_CONTEXT_APPLICATION
|
||||
from passbook.flows.stage import StageView
|
||||
from passbook.lib.utils.time import timedelta_from_string
|
||||
from passbook.stages.consent.forms import ConsentForm
|
||||
from passbook.stages.consent.models import ConsentMode, ConsentStage, UserConsent
|
||||
|
||||
PLAN_CONTEXT_CONSENT_TEMPLATE = "consent_template"
|
||||
|
||||
|
||||
class ConsentStage(FormView, StageView):
|
||||
class ConsentStageView(FormView, StageView):
|
||||
"""Simple consent checker."""
|
||||
|
||||
form_class = ConsentForm
|
||||
|
@ -26,5 +31,40 @@ class ConsentStage(FormView, StageView):
|
|||
return [template_name]
|
||||
return super().get_template_names()
|
||||
|
||||
def form_valid(self, form):
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
current_stage: ConsentStage = self.executor.current_stage
|
||||
# For always require, we always show the form
|
||||
if current_stage.mode == ConsentMode.ALWAYS_REQUIRE:
|
||||
return super().get(request, *args, **kwargs)
|
||||
# at this point we need to check consent from database
|
||||
if PLAN_CONTEXT_APPLICATION not in self.executor.plan.context:
|
||||
# No application in this plan, hence we can't check DB and require user consent
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
|
||||
# TODO: Check for user in plan?
|
||||
if UserConsent.filter_not_expired(
|
||||
user=self.request.user, application=application
|
||||
).exists():
|
||||
return self.executor.stage_ok()
|
||||
|
||||
# No consent found, show form
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form: ConsentForm) -> HttpResponse:
|
||||
current_stage: ConsentStage = self.executor.current_stage
|
||||
if PLAN_CONTEXT_APPLICATION not in self.executor.plan.context:
|
||||
return self.executor.stage_ok()
|
||||
application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
|
||||
# Since we only get here when no consent exists, we can create it without update
|
||||
if current_stage.mode == ConsentMode.PERMANENT:
|
||||
UserConsent.objects.create(
|
||||
user=self.request.user, application=application, expiring=False
|
||||
)
|
||||
if current_stage.mode == ConsentMode.EXPIRING:
|
||||
UserConsent.objects.create(
|
||||
user=self.request.user,
|
||||
application=application,
|
||||
expires=now() + timedelta_from_string(current_stage.consent_expire_in),
|
||||
)
|
||||
return self.executor.stage_ok()
|
||||
|
|
12
swagger.yaml
12
swagger.yaml
|
@ -6609,6 +6609,18 @@ definitions:
|
|||
title: Name
|
||||
type: string
|
||||
minLength: 1
|
||||
mode:
|
||||
title: Mode
|
||||
type: string
|
||||
enum:
|
||||
- always_require
|
||||
- permanent
|
||||
- expiring
|
||||
consent_expire_in:
|
||||
title: Consent expires in
|
||||
description: 'Offset after which consent expires.(Format: hours=1;minutes=2;seconds=3).'
|
||||
type: string
|
||||
minLength: 1
|
||||
DummyStage:
|
||||
required:
|
||||
- name
|
||||
|
|
Reference in New Issue