Commit 5491ff41 authored by BAIRE Anthony's avatar BAIRE Anthony

Manage ToS validation in the user_need_validation page

- remove the HasSignedTosMixin and to the verifications in
  AllgoAccessMixin instead (along with email verification)
- add Tos.get_latest() and User.has_agreed_tos
- ignore ToS agreement if the db has no ToS entries
parent 72f60afb
......@@ -16,8 +16,6 @@ from .models import (
Webapp,
WebappParameter,
WebappVersion,
Tos,
UserAgreement,
)
from .validators import docker_name_validator
......@@ -235,17 +233,3 @@ class WebappImportForm(forms.Form):
docker_name = forms.CharField(label="Short name", required=False,
validators=[docker_name_validator])
class TosForm(forms.ModelForm):
valid = forms.BooleanField(
required=True,
label=mark_safe('Check here to indicate that you have read and agree to the terms of the <a href="{% url \'main:tos_detail\' %}">A||Go policy</a>.'))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['tos'].initial = Tos.objects.order_by('-version').first()
class Meta:
model = UserAgreement
fields = ('tos',)
from django.conf import settings
from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse, resolve
from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect
from .models import Job, Tos, UserAgreement
from .models import Job
from .helpers import get_request_user
......@@ -19,7 +18,9 @@ from .helpers import get_request_user
class AllgoValidAccountMixin:
"""Common mixin for allgo accounts validation
An account is valid if at least one of its email address is verified.
An account is valid if:
- at least one of its email address is verified
- its owner agreed the latest ToS
In the case of the 'ProviderAccessMixin' the verified address must also
have its domain listed in ALLGO_ALLOWED_DEVELOPER_DOMAINS.
......@@ -50,6 +51,12 @@ class AllgoValidAccountMixin:
# user has no verified address
return redirect("main:user_need_validation")
# registered users must have agreed the latest ToS
if not user.has_agreed_tos:
return redirect("main:user_need_validation")
return super().dispatch(request, *args, **kwargs)
......@@ -98,22 +105,3 @@ class JobAuthMixin(AllgoValidAccountMixin, UserPassesTestMixin):
return JsonResponse({"error": "401 Unauthorized"}, status=401)
return super().handle_no_permission()
class HasSignedTosMixin(object):
"""Check if the user has signed the latest ToS"""
def dispatch(self, request, *args, **kwargs):
"""Check if the user has signed the latest ToS
Redirects to the ToS form to sign or let the user reach the page he
asked for
"""
redirect_to = request.path
url_name = resolve(redirect_to)
last_tos = Tos.objects.order_by('-version').first()
user_agreement = UserAgreement.objects.filter(user=request.user, tos=last_tos)
if user_agreement:
return super().dispatch(request, *args, **kwargs)
else:
return redirect(reverse('main:tos_validation', args=[url_name.url_name]))
......@@ -10,7 +10,6 @@ from django.core.validators import MinLengthValidator, MinValueValidator, \
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.shortcuts import redirect
from taggit.managers import TaggableManager
from allauth.account.models import EmailAddress
from allauth.socialaccount.models import SocialAccount
......@@ -627,6 +626,11 @@ class Tos(BaseModel):
def __str__(self):
return str(self.version)
@classmethod
def get_latest(cls):
"""Get the most up-to-date Terms of Service"""
return Tos.objects.order_by("-version").first()
class Meta:
db_table = "dj_tos"
......@@ -700,6 +704,15 @@ def is_provider(user: User):
# Add the `is_provider` method to the `User` model
auth.models.User.add_to_class('is_provider', is_provider)
@property
def has_agreed_tos(user: User):
"""Return true if the user has agreed the latest ToS (if present)"""
tos = Tos.get_latest()
return tos is None or UserAgreement.objects.filter(
user=user, tos=tos).exists()
auth.models.User.add_to_class('has_agreed_tos', has_agreed_tos)
# NOTE: because there is a circular dependency between models.py and
# helpers.py, we have to do this import after 'Job' and 'Webapp' are defined
from .helpers import is_allowed_ip_admin
......@@ -78,7 +78,6 @@ urlpatterns = [
# Terms of service urls
url(r'^tos$', views.TosDetail.as_view(), name='tos_detail'),
url(r'^tos_validation/(?P<url_name>[-\w]+)$', views.TosValidation.as_view(), name='tos_validation'),
# url(r'^runners/$', views.RunnerList.as_view(), name='runner_list'),
# url(r'^runners/_add$', views.RunnerCreate.as_view(), name='runner_create'),
......
......@@ -67,12 +67,11 @@ from .forms import (
WebappForm,
WebappSandboxForm,
WebappImportForm,
TosForm,
)
# Local imports
import config
from .helpers import get_base_url, get_ssh_data, upload_data, notify_controller, lookup_job_file, get_request_user, query_webapps_for_user
from .mixins import UserAccessMixin, ProviderAccessMixin, AllAccessMixin, JobAuthMixin, HasSignedTosMixin
from .mixins import UserAccessMixin, ProviderAccessMixin, AllAccessMixin, JobAuthMixin
from .models import (
AllgoUser,
DockerOs,
......@@ -207,7 +206,7 @@ class UserWebappList(AllAccessMixin, ListView):
return queryset
class WebappUpdate(UserAccessMixin, HasSignedTosMixin, SuccessMessageMixin, UpdateView):
class WebappUpdate(UserAccessMixin, SuccessMessageMixin, UpdateView):
"""Form to update the webapp data
Attributes:
......@@ -258,7 +257,7 @@ class WebappUpdate(UserAccessMixin, HasSignedTosMixin, SuccessMessageMixin, Upda
return super(WebappUpdate, self).form_invalid(form)
class WebappCreate(ProviderAccessMixin, HasSignedTosMixin, SuccessMessageMixin, CreateView):
class WebappCreate(ProviderAccessMixin, SuccessMessageMixin, CreateView):
"""Create a new webapp
Attributes:
......@@ -342,7 +341,7 @@ def get_rails_webapp_metadata(*, webapp_id=None, docker_name=None):
log.error("webapp import error: failed to get %s (%s)", url, e)
raise
class WebappImport(ProviderAccessMixin, HasSignedTosMixin, SuccessMessageMixin, FormView):
class WebappImport(ProviderAccessMixin, SuccessMessageMixin, FormView):
"""Import a new webapp
This only creates the Webapp entry (along with the tags and webapp
......@@ -453,7 +452,7 @@ class WebappImport(ProviderAccessMixin, HasSignedTosMixin, SuccessMessageMixin,
self.object = webapp
return super().form_valid(form)
class WebappVersionImport(UserAccessMixin, HasSignedTosMixin, DetailView):
class WebappVersionImport(UserAccessMixin, DetailView):
"""Import version
This view is enabled only for webapps created with imported=True
......@@ -548,7 +547,7 @@ class WebappVersionImport(UserAccessMixin, HasSignedTosMixin, DetailView):
return HttpResponseRedirect(request.path_info)
class WebappJson(UserAccessMixin, HasSignedTosMixin, DetailView):
class WebappJson(UserAccessMixin, DetailView):
"""json variant of the application details
(used by the /aio/apps/<DOCKER_NAME>/events endpoint)
......@@ -567,7 +566,7 @@ class WebappJson(UserAccessMixin, HasSignedTosMixin, DetailView):
"sandbox_state": webapp.get_sandbox_state_display(),
})
class WebappSandboxPanel(UserAccessMixin, HasSignedTosMixin, TemplateView):
class WebappSandboxPanel(UserAccessMixin, TemplateView):
"""Create a new sandbox for a given application
Attributes:
......@@ -785,7 +784,7 @@ class TagWebappList(AllAccessMixin, ListView):
# PROFILE
# -----------------------------------------------------------------------------
class UserUpdate(LoginRequiredMixin, HasSignedTosMixin, SuccessMessageMixin, UpdateView):
class UserUpdate(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
"""Update the user profile
Attributes:
......@@ -826,7 +825,7 @@ class UserUpdate(LoginRequiredMixin, HasSignedTosMixin, SuccessMessageMixin, Upd
return super(UserUpdate, self).get_context_data(**kwargs)
class UserToken(LoginRequiredMixin, HasSignedTosMixin, RedirectView):
class UserToken(LoginRequiredMixin, RedirectView):
"""Regenerate the user token"""
success_message = 'Token generated successfully.'
......@@ -844,7 +843,7 @@ class UserToken(LoginRequiredMixin, HasSignedTosMixin, RedirectView):
return reverse('main:user_detail')
class UserSSHAdd(LoginRequiredMixin, HasSignedTosMixin, SuccessMessageMixin, UpdateView):
class UserSSHAdd(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
"""Save a SSH key to the database.
Attributes:
......@@ -865,7 +864,7 @@ class UserSSHAdd(LoginRequiredMixin, HasSignedTosMixin, SuccessMessageMixin, Upd
return AllgoUser.objects.get(user_id=self.request.user.id)
class UserSSHDelete(LoginRequiredMixin, HasSignedTosMixin, RedirectView):
class UserSSHDelete(LoginRequiredMixin, RedirectView):
"""Delete the user SSH key"""
success_message = 'The SSH key has been successfully deleted.'
......@@ -883,7 +882,7 @@ class UserSSHDelete(LoginRequiredMixin, HasSignedTosMixin, RedirectView):
return reverse('main:user_detail')
class UserPasswordUpdate(LoginRequiredMixin, HasSignedTosMixin, SuccessMessageMixin, UpdateView):
class UserPasswordUpdate(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
"""Update the user's password.
We reuse the Django password form system in order to keep something robust
......@@ -935,8 +934,25 @@ class UserNeedValidation(LoginRequiredMixin, DetailView):
def get_object(self):
return None
def get_context_data(self, **kwargs):
# last ToS agreement by the user
agreement = UserAgreement.objects.filter(user=self.request.user
).order_by("-id").first()
# next ToS to be signed
next_tos = Tos.get_latest()
if agreement is not None and agreement.tos == next_tos:
# latest agreement already signed
next_tos = None
return super().get_context_data(
agreement=agreement, next_tos=next_tos, **kwargs)
def get(self, request):
if all(addr.verified for addr in request.user.email_addresses):
if all(addr.verified for addr in request.user.email_addresses
) and request.user.has_agreed_tos:
# redirect to the main page if validation is complete
messages.success(request, "Your account is fully validated")
return redirect("main:home")
......@@ -956,12 +972,24 @@ class UserNeedValidation(LoginRequiredMixin, DetailView):
messages.info(self.request,
"Confirmation e-mail sent to %s" % email_addr.email)
# store user agreement
if "yes_i_agree" in request.POST:
try:
tos = Tos.objects.get(id=int(request.POST["agree_tos_id"]))
except (KeyError, ValueError, ObjectDoesNotExist):
pass
else:
if UserAgreement.objects.get_or_create(
tos=tos, user=request.user)[1]:
messages.success(self.request,
"Terms of Service version %s agreed" % tos.version)
return redirect(request.path_info)
# JOBS
# -----------------------------------------------------------------------------
class JobList(UserAccessMixin, HasSignedTosMixin, ListView):
class JobList(UserAccessMixin, ListView):
"""Display the list of jobs for a given identified user
Attributes:
......@@ -1303,7 +1331,7 @@ class JobFileDownloadAll(JobAuthMixin, View):
# RUNNERS
# -----------------------------------------------------------------------------
class RunnerList(UserAccessMixin, HasSignedTosMixin, ListView):
class RunnerList(UserAccessMixin, ListView):
"""List all runners of a given user
Attributes:
......@@ -1341,7 +1369,7 @@ class RunnerList(UserAccessMixin, HasSignedTosMixin, ListView):
return super().get_context_data(**kwargs)
class RunnerCreate(ProviderAccessMixin, HasSignedTosMixin, SuccessMessageMixin, CreateView):
class RunnerCreate(ProviderAccessMixin, SuccessMessageMixin, CreateView):
"""Create a runner and save it into the database
Attributes:
......@@ -1388,7 +1416,7 @@ class RunnerCreate(ProviderAccessMixin, HasSignedTosMixin, SuccessMessageMixin,
return reverse_lazy('main:runner_update', args=(self.object.pk,))
class RunnerUpdate(UserAccessMixin, HasSignedTosMixin, SuccessMessageMixin, UpdateView):
class RunnerUpdate(UserAccessMixin, SuccessMessageMixin, UpdateView):
"""Update a runner and save it into the database
Attributes:
......@@ -1417,7 +1445,7 @@ class RunnerUpdate(UserAccessMixin, HasSignedTosMixin, SuccessMessageMixin, Upda
"allgo/runner", "-", self.object.token, get_base_url(self.request)]
return super().get_context_data(**kwargs)
class RunnerDelete(UserAccessMixin, HasSignedTosMixin, DeleteView):
class RunnerDelete(UserAccessMixin, DeleteView):
"""Delete a runner
Attributes:
......@@ -1463,37 +1491,6 @@ def auth(request):
return HttpResponse(status=403)
class TosValidation(SuccessMessageMixin, FormView):
model = UserAgreement
form_class = TosForm
success_message = 'You validated the Terms of Service.'
success_url = reverse_lazy('main:home')
template_name = 'tos_validation.html'
def form_valid(self, form):
""" Validate some fields before saving them
Ensure that the validation button has been checked (in case the javascript
system fails. If not an error message is displayed to the user and he
is redirected to this view.
"""
print(self.kwargs['url_name'])
obj = form.save(commit=False)
if not form.cleaned_data.get('valid'):
messages.add_message(self.request, messages.ERROR, 'You must accept the Terms of Service to use this service.')
log.warning("Someone tried to use the service without accepting the Terms of Service.")
return redirect('main:tos_validation', webapp.docker_name)
obj.user = self.request.user
obj.save()
url_name = 'main:' + self.kwargs['url_name']
return redirect(url_name)
def get_context_data(self, **kwargs):
""" Send to the template the lastest ToS data. """
kwargs['tos_data'] = Tos.objects.order_by('-version').first()
return super().get_context_data(**kwargs)
class TosDetail(DetailView):
template_name = 'tos_detail.html'
......
{% extends "base.html" %}
{% load converters htmlattrs %}
{% block title %}Validate the Terms of Service{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item active">Terms of Service</li>
{% endblock %}
{% block content %}
<div class="container">
<div class="allgo-page">
<form method="post">
{% csrf_token %}
<p>You can get the Terms of Service in <a href="{{ tos_data.url }}">PDF format</a>.</p>
<div class="form-group form-check">
{{ form.valid | add_class:"form-check-input"}}
{{ form.valid.label_tag }}
</div>
{{ form.tos.as_hidden }}
<input class="btn btn-primary" type="submit" value="Continue">
</form>
</div>
</div>
{% endblock %}
......@@ -16,7 +16,11 @@
</div>
<div>
<h5>Email address</h5>
<table class="table">
<p>Your e-mail address has to be verified. If it is still marked as
unverified, then you need to follow the link provided in the confirmation
e-mail that was sent during registration.</p>
<table class="table" style="width: 1px; white-space: nowrap;">
<tbody>
{% for addr in user.email_addresses %}
<tr>
......@@ -41,6 +45,50 @@
</tbody>
</table>
</div>
{% if agreement or next_tos %}
<div class="mt-3">
<h5>Terms of Service</h5>
<p>You have to agree to the latest version of the Terms of Service.</p>
<table class="table" style="width: 1px; white-space: nowrap;">
<tbody>
{% if agreement %}
<tr>
<td>
<a href="{{agreement.tos.url}}">Terms of Service version {{ agreement.tos.version }}</a>
{% if next_tos %}
<i>(previous)</i>
{% endif %}
</td>
<td class="text-success"><i>Agreed</i></td>
<td/>
</tr>
{% endif %}
{% if next_tos %}
<tr>
<td>
<a href="{{next_tos.url}}">Terms of Service version {{ next_tos.version }}</a>
<i>(current)</i>
<br>
</td>
<td class="text-danger"><i>Not agreed</i></td>
</tr>
<tr>
<td colspan="2">
<form class="form" method="post">
{% csrf_token %}
<input type="hidden" name="agree_tos_id" value="{{next_tos.id}}"/>
<input type="checkbox" name="yes_i_agree"/> Yes, I have read and I agree to the Terms of Service version {{next_tos.version}}
<input class="btn btn-primary ml-3" type="submit" value="Submit"/>
</form>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
{% endblock %}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment