Implement consent for third party orgs in commmunity auth
authorMagnus Hagander <magnus@hagander.net>
Wed, 30 May 2018 20:22:42 +0000 (16:22 -0400)
committerMagnus Hagander <magnus@hagander.net>
Wed, 30 May 2018 20:25:37 +0000 (16:25 -0400)
This adds a new model for CommunityAuthOrg representing the organisation
that runs the system that's being authenticated (e.g. PostgreSQL Europe
or PostgreSQL US). For this we just keep a name and a "is consent required" flag.

In the case where consent is required, we keep track on a per-user basis
of if they have given consent to sharing their data with this
organistion. If they haven't, we ask for it before completing the
redirect and actually sharing the data.

pgweb/account/admin.py
pgweb/account/forms.py
pgweb/account/migrations/0003_cauth_consent.py [new file with mode: 0644]
pgweb/account/models.py
pgweb/account/urls.py
pgweb/account/views.py
templates/account/communityauth_noinfo.html
templates/account/login.html

index 1652f36869a01a3cda61357d8596a438f8a0d05e..257b0a94aaa7429865a9f10d9fb12cb9b0983084 100644 (file)
@@ -6,7 +6,7 @@ from django import forms
 
 import base64
 
-from models import CommunityAuthSite
+from models import CommunityAuthSite, CommunityAuthOrg
 
 class CommunityAuthSiteAdminForm(forms.ModelForm):
        class Meta:
@@ -49,5 +49,6 @@ class PGUserAdmin(UserAdmin):
                return self.readonly_fields
 
 admin.site.register(CommunityAuthSite, CommunityAuthSiteAdmin)
+admin.site.register(CommunityAuthOrg)
 admin.site.unregister(User) # have to unregister default User Admin...
 admin.site.register(User, PGUserAdmin) # ...in order to add overrides
index 0e7363e580dd96c350ce1d40f005012743ba61ec..406e313c0b9085a0c710449dbbc6d1d7c15977fa 100644 (file)
@@ -38,6 +38,15 @@ class PgwebAuthenticationForm(AuthenticationForm):
                                return self.cleaned_data
                        raise e
 
+class CommunityAuthConsentForm(forms.Form):
+       consent = forms.BooleanField(help_text='Consent to sharing this data')
+       next = forms.CharField(widget=forms.widgets.HiddenInput())
+
+       def __init__(self, orgname, *args, **kwargs):
+               self.orgname = orgname
+               super(CommunityAuthConsentForm, self).__init__(*args, **kwargs)
+
+               self.fields['consent'].label = 'Consent to sharing data with {0}'.format(self.orgname)
 
 class SignupForm(forms.Form):
        username = forms.CharField(max_length=30)
diff --git a/pgweb/account/migrations/0003_cauth_consent.py b/pgweb/account/migrations/0003_cauth_consent.py
new file mode 100644 (file)
index 0000000..6998345
--- /dev/null
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.10 on 2018-05-29 17:20
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('account', '0002_lowercase_email'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='CommunityAuthConsent',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('consentgiven', models.DateTimeField()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='CommunityAuthOrg',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('orgname', models.CharField(help_text=b'Name of the organisation', max_length=100)),
+                ('require_consent', models.BooleanField(default=True)),
+            ],
+        ),
+        migrations.RunSQL("INSERT INTO account_communityauthorg (orgname, require_consent) VALUES ('PostgreSQL Global Development Group', false)", reverse_sql=migrations.RunSQL.noop),
+        migrations.AddField(
+            model_name='communityauthconsent',
+            name='org',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.CommunityAuthOrg'),
+        ),
+        migrations.AddField(
+            model_name='communityauthconsent',
+            name='user',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AddField(
+            model_name='communityauthsite',
+            name='org',
+            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='account.CommunityAuthOrg'),
+            preserve_default=False,
+        ),
+        migrations.AlterUniqueTogether(
+            name='communityauthconsent',
+            unique_together=set([('user', 'org')]),
+        ),
+    ]
index f4f3e9bf9e8e8e80b3872a659b7a335c8b6875db..269430eba891ff8115f6c7616b53324d59f0ea31 100644 (file)
@@ -1,6 +1,14 @@
 from django.db import models
 from django.contrib.auth.models import User
 
+class CommunityAuthOrg(models.Model):
+       orgname = models.CharField(max_length=100, null=False, blank=False,
+                                                          help_text="Name of the organisation")
+       require_consent = models.BooleanField(null=False, blank=False, default=True)
+
+       def __unicode__(self):
+               return self.orgname
+
 class CommunityAuthSite(models.Model):
        name = models.CharField(max_length=100, null=False, blank=False,
                                                        help_text="Note that the value in this field is shown on the login page, so make sure it's user-friendly!")
@@ -8,12 +16,21 @@ class CommunityAuthSite(models.Model):
        cryptkey = models.CharField(max_length=100, null=False, blank=False,
                                                                help_text="Use tools/communityauth/generate_cryptkey.py to create a key")
        comment = models.TextField(null=False, blank=True)
+       org = models.ForeignKey(CommunityAuthOrg, null=False, blank=False)
        cooloff_hours = models.IntegerField(null=False, blank=False, default=0,
                                                                                help_text="Number of hours a user must have existed in the systems before allowed to log in to this site")
 
        def __unicode__(self):
                return self.name
 
+class CommunityAuthConsent(models.Model):
+       user = models.ForeignKey(User, null=False, blank=False)
+       org = models.ForeignKey(CommunityAuthOrg, null=False, blank=False)
+       consentgiven = models.DateTimeField(null=False, blank=False)
+
+       class Meta:
+               unique_together = (('user', 'org'), )
+
 class EmailChangeToken(models.Model):
        user = models.OneToOneField(User, null=False, blank=False)
        email = models.EmailField(max_length=75, null=False, blank=False)
index 772f0e2feb92d2ae49dc879fd4ee4fc8e5f6f8e3..cee17968d444257fc647652a04b1627389222d31 100644 (file)
@@ -10,6 +10,7 @@ urlpatterns = [
        # Community authenticatoin
        url(r'^auth/(\d+)/$', pgweb.account.views.communityauth),
        url(r'^auth/(\d+)/logout/$', pgweb.account.views.communityauth_logout),
+       url(r'^auth/(\d+)/consent/$', pgweb.account.views.communityauth_consent),
        url(r'^auth/(\d+)/search/$', pgweb.account.views.communityauth_search),
        url(r'^auth/(\d+)/getkeys/(\d+/)?$', pgweb.account.views.communityauth_getkeys),
 
index db61c494223e4c520a5ce7139d13a6a08a9ae8c2..4e80e5aa69e4e380ff53cc2f7824dda9174d5b4f 100644 (file)
@@ -32,8 +32,9 @@ from pgweb.contributors.models import Contributor
 from pgweb.downloads.models import Product
 from pgweb.profserv.models import ProfessionalService
 
-from models import CommunityAuthSite, EmailChangeToken
+from models import CommunityAuthSite, CommunityAuthConsent, EmailChangeToken
 from forms import PgwebAuthenticationForm
+from forms import CommunityAuthConsentForm
 from forms import SignupForm, SignupOauthForm
 from forms import UserForm, UserProfileForm, ContributorForm
 from forms import ChangeEmailForm, PgwebPasswordResetForm
@@ -448,17 +449,18 @@ def communityauth(request, siteid):
        else:
                d = None
 
+       if d:
+               urldata = "?d=%s" % d
+       elif su:
+               urldata = "?su=%s" % su
+       else:
+               urldata = ""
+
        # Verify if the user is authenticated, and if he/she is not, generate
        # a login form that has information about which site is being logged
        # in to, and basic information about how the community login system
        # works.
        if not request.user.is_authenticated():
-               if d:
-                       urldata = "?d=%s" % d
-               elif su:
-                       urldata = "?su=%s" % su
-               else:
-                       urldata = ""
                if request.method == "POST" and 'next' in request.POST and 'this_is_the_login_form' in request.POST:
                        # This is a postback of the login form. So pick the next filed
                        # from that one, so we keep it across invalid password entries.
@@ -492,6 +494,11 @@ def communityauth(request, siteid):
                                'site': site,
                                })
 
+       if site.org.require_consent:
+               if not CommunityAuthConsent.objects.filter(org=site.org, user=request.user).exists():
+                       return HttpResponseRedirect('/account/auth/{0}/consent/?{1}'.format(siteid,
+                                                                                                                                                               urllib.urlencode({'next': '/account/auth/{0}/{1}'.format(siteid, urldata)})))
+
        info = {
                'u': request.user.username.encode('utf-8'),
                'f': request.user.first_name.encode('utf-8'),
@@ -531,6 +538,24 @@ def communityauth_logout(request, siteid):
        # Redirect user back to the specified suburl
        return HttpResponseRedirect("%s?s=logout" % site.redirecturl)
 
+def communityauth_consent(request, siteid):
+       org = get_object_or_404(CommunityAuthSite, id=siteid).org
+       if request.method == 'POST':
+               form = CommunityAuthConsentForm(org.orgname, data=request.POST)
+               if form.is_valid():
+                       CommunityAuthConsent(user=request.user, org=org, consentgiven=datetime.now()).save()
+                       return HttpResponseRedirect(form.cleaned_data['next'])
+       else:
+               form = CommunityAuthConsentForm(org.orgname, initial={'next': request.GET['next']})
+
+       return render_pgweb(request, 'account', 'base/form.html', {
+               'form': form,
+               'operation': 'Authentication',
+               'form_intro': 'The site you are about to log into is run by {0}. If you choose to proceed with this authentication, your name and email address will be shared with <em>{1}</em>.</p><p>Please confirm that you consent to this sharing.'.format(org.orgname, org.orgname),
+               'savebutton': 'Proceed with login',
+       })
+
+
 def _encrypt_site_response(site, s):
        # Encrypt it with the shared key (and IV!)
        r = Random.new()
index e6dd572ee21133f0e310dff3efc9adadf52af02f..4bea504a534d972c62eb4e31eeaf0d5b23036c04 100644 (file)
@@ -6,8 +6,7 @@ The website you are trying to log in to ({{sitename}}) is using the
 postgresql.org community login system. In this system you create a
 central account that is used to log into most postgresql.org services.
 Once you are logged into this account, you will automatically be
-logged in to the associated postgresql.org services. Note that this
-single sign on is only used for official postgresql.org websites.
+logged in to the associated postgresql.org services.
 </p>
 <p>
 Your account does not have all the fields filled out required to perform
index 653c965996d9c558a82ef9105975e74ffda07500..7402a966951456faf2dfbecd588b3bc2ac3dd072 100644 (file)
@@ -7,8 +7,7 @@ The website you are trying to log in to ({{sitename}}) is using the
 postgresql.org community login system. In this system you create a
 central account that is used to log into most postgresql.org services.
 Once you are logged into this account, you will automatically be
-logged in to the associated postgresql.org services. Note that this
-single sign on is only used for official postgresql.org websites.
+logged in to the associated postgresql.org services.
 {%else%}
 Please log in to your community account to reach this area.
 {%endif%}