stages/consent: start implementing user consent
This commit is contained in:
		@ -11,7 +11,7 @@ class ConsentStageSerializer(ModelSerializer):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        model = ConsentStage
 | 
					        model = ConsentStage
 | 
				
			||||||
        fields = ["pk", "name"]
 | 
					        fields = ["pk", "name", "mode", "consent_expire_in"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ConsentStageViewSet(ModelViewSet):
 | 
					class ConsentStageViewSet(ModelViewSet):
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,7 @@ class ConsentStageForm(forms.ModelForm):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        model = ConsentStage
 | 
					        model = ConsentStage
 | 
				
			||||||
        fields = ["name"]
 | 
					        fields = ["name", "mode", "consent_expire_in"]
 | 
				
			||||||
        widgets = {
 | 
					        widgets = {
 | 
				
			||||||
            "name": forms.TextInput(),
 | 
					            "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"""
 | 
					"""passbook consent stage"""
 | 
				
			||||||
 | 
					from django.db import models
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from passbook.core.models import Application, ExpiringModel, User
 | 
				
			||||||
from passbook.flows.models import Stage
 | 
					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):
 | 
					class ConsentStage(Stage):
 | 
				
			||||||
    """Prompt the user for confirmation."""
 | 
					    """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"
 | 
					    form = "passbook.stages.consent.forms.ConsentStageForm"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
@ -17,3 +43,20 @@ class ConsentStage(Stage):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        verbose_name = _("Consent Stage")
 | 
					        verbose_name = _("Consent Stage")
 | 
				
			||||||
        verbose_name_plural = _("Consent Stages")
 | 
					        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"""
 | 
					"""passbook consent stage"""
 | 
				
			||||||
from typing import Any, Dict, List
 | 
					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 django.views.generic import FormView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from passbook.flows.planner import PLAN_CONTEXT_APPLICATION
 | 
				
			||||||
from passbook.flows.stage import StageView
 | 
					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.forms import ConsentForm
 | 
				
			||||||
 | 
					from passbook.stages.consent.models import ConsentMode, ConsentStage, UserConsent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PLAN_CONTEXT_CONSENT_TEMPLATE = "consent_template"
 | 
					PLAN_CONTEXT_CONSENT_TEMPLATE = "consent_template"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ConsentStage(FormView, StageView):
 | 
					class ConsentStageView(FormView, StageView):
 | 
				
			||||||
    """Simple consent checker."""
 | 
					    """Simple consent checker."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    form_class = ConsentForm
 | 
					    form_class = ConsentForm
 | 
				
			||||||
@ -26,5 +31,40 @@ class ConsentStage(FormView, StageView):
 | 
				
			|||||||
            return [template_name]
 | 
					            return [template_name]
 | 
				
			||||||
        return super().get_template_names()
 | 
					        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()
 | 
					        return self.executor.stage_ok()
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								swagger.yaml
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								swagger.yaml
									
									
									
									
									
								
							@ -6609,6 +6609,18 @@ definitions:
 | 
				
			|||||||
        title: Name
 | 
					        title: Name
 | 
				
			||||||
        type: string
 | 
					        type: string
 | 
				
			||||||
        minLength: 1
 | 
					        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:
 | 
					  DummyStage:
 | 
				
			||||||
    required:
 | 
					    required:
 | 
				
			||||||
      - name
 | 
					      - name
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user