Added a new personal event table to keep track of personal API key logins, and a management command to send out reports about activity to users with API keys. Added a weekly cronjob script to trigger weekly reports, and a monthly script for future use. Added a @require_api_key decorator to validate API keys for API key views and log in the API key owner. Modified the API key management urls to use create and disable rather than add and delete. Updated the API key list view. Added an API placeholder view function for ballot position setting, for test purposes. Added tests for the decorator and management command.

- Legacy-Id: 14426
This commit is contained in:
Henrik Levkowetz 2017-12-16 18:37:52 +00:00
parent 383b8b16b9
commit e7209c6e50
16 changed files with 425 additions and 81 deletions

15
bin/monthly Executable file
View file

@ -0,0 +1,15 @@
#!/bin/bash
# Weekly datatracker jobs.
#
# This script is expected to be triggered by cron from
# /etc/cron.d/datatracker
DTDIR=/a/www/ietf-datatracker/web
cd $DTDIR/
# Set up the virtual environment
source $DTDIR/env/bin/activate
logger -p user.info -t cron "Running $DTDIR/bin/monthly"

20
bin/weekly Executable file
View file

@ -0,0 +1,20 @@
#!/bin/bash
# Weekly datatracker jobs.
#
# This script is expected to be triggered by cron from
# /etc/cron.d/datatracker
DTDIR=/a/www/ietf-datatracker/web
cd $DTDIR/
# Set up the virtual environment
source $DTDIR/env/bin/activate
logger -p user.info -t cron "Running $DTDIR/bin/weekly"
# Send out weekly summaries of apikey usage
$DTDIR/ietf/manage.py send_apikey_usage_emails

View file

@ -3,6 +3,7 @@
from django.conf.urls import include
from ietf import api
from ietf.doc import views_ballot
from ietf.meeting import views as meeting_views
from ietf.submit import views as submit_views
from ietf.utils.urls import url
@ -15,6 +16,7 @@ urlpatterns = [
# Custom API endpoints
url(r'^notify/meeting/import_recordings/(?P<number>[a-z0-9-]+)/?$', meeting_views.api_import_recordings),
url(r'^submit/?$', submit_views.api_submit),
url(r'^iesg/position', views_ballot.api_set_position),
]
# Additional (standard) Tastypie endpoints
for n,a in api._api_list:

View file

@ -3,12 +3,14 @@
import datetime, json
from django.http import HttpResponseForbidden, HttpResponseRedirect, Http404
from django.shortcuts import render, get_object_or_404, redirect
from django.urls import reverse as urlreverse
from django.template.loader import render_to_string
from django import forms
from django.conf import settings
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, Http404
from django.shortcuts import render, get_object_or_404, redirect
from django.template.loader import render_to_string
from django.urls import reverse as urlreverse
from django.views.decorators.csrf import csrf_exempt
import debug # pyflakes:ignore
@ -23,12 +25,13 @@ from ietf.doc.mails import ( email_ballot_deferred, email_ballot_undeferred,
from ietf.doc.lastcall import request_last_call
from ietf.iesg.models import TelechatDate
from ietf.ietfauth.utils import has_role, role_required, is_authorized_in_doc_stream
from ietf.mailtrigger.utils import gather_address_lists
from ietf.mailtrigger.forms import CcSelectForm
from ietf.message.utils import infer_message
from ietf.name.models import BallotPositionName
from ietf.person.models import Person
from ietf.utils.mail import send_mail_text, send_mail_preformatted
from ietf.mailtrigger.utils import gather_address_lists
from ietf.mailtrigger.forms import CcSelectForm
from ietf.utils.decorators import require_user_api_key
BALLOT_CHOICES = (("yes", "Yes"),
("noobj", "No Objection"),
@ -233,6 +236,12 @@ def edit_position(request, name, ballot_id):
blocking_positions=json.dumps(blocking_positions),
))
@require_user_api_key
@role_required('Area Director', 'Secretariat')
@csrf_exempt
def api_set_position(request):
return HttpResponse("Done", status=200, content_type='text/plain')
@role_required('Area Director','Secretariat')
def send_ballot_comment(request, name, ballot_id):

View file

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Copyright The IETF Trust 2017, All Rights Reserved
from __future__ import print_function, unicode_literals
import datetime
from textwrap import dedent
from django.conf import settings
from django.core.management.base import BaseCommand
import debug # pyflakes:ignore
from ietf.person.models import PersonalApiKey, PersonApiKeyEvent
from ietf.utils.mail import send_mail
class Command(BaseCommand):
"""
Send out emails to all persons who have personal API keys about usage.
Usage is show over the given period, where the default period is 7 days.
"""
help = dedent(__doc__).strip()
def add_arguments(self, parser):
parser.add_argument('-d', '--days', dest='days', type=int, default=7,
help='The period over which to show usage.')
def handle(self, *filenames, **options):
"""
"""
self.verbosity = int(options.get('verbosity'))
days = options.get('days')
keys = PersonalApiKey.objects.filter(valid=True)
for key in keys:
earliest = datetime.datetime.now() - datetime.timedelta(days=days)
events = PersonApiKeyEvent.objects.filter(key=key, time__gt=earliest)
count = events.count()
events = events[:32]
if count:
key_name = key.hash()[:8]
subject = "API key usage for key '%s' for the last %s days" %(key_name, days)
to = key.person.email_address()
frm = settings.DEFAULT_FROM_EMAIL
send_mail(None, to, frm, subject, 'utils/apikey_usage_report.txt', {'person':key.person,
'days':days, 'key':key, 'key_name':key_name, 'count':count, 'events':events, } )

View file

@ -17,9 +17,10 @@ from ietf.utils.test_utils import TestCase, login_testing_unauthorized, uniconte
from ietf.utils.test_data import make_test_data, make_review_data
from ietf.utils.mail import outbox, empty_outbox
from ietf.group.models import Group, Role, RoleName
from ietf.group.factories import GroupFactory
from ietf.ietfauth.htpasswd import update_htpasswd_file
from ietf.mailinglists.models import Subscribed
from ietf.person.models import Person, Email
from ietf.person.models import Person, Email, PersonalApiKey, PERSON_API_KEY_ENDPOINTS
from ietf.person.factories import PersonFactory
from ietf.review.models import ReviewWish, UnavailablePeriod
from ietf.utils.decorators import skip_coverage
@ -497,7 +498,7 @@ class IetfAuthTests(TestCase):
self.assertEqual(prev, user)
self.assertTrue(user.check_password(u'password'))
def test_apikey(self):
def test_apikey_management(self):
person = PersonFactory()
url = urlreverse('ietf.ietfauth.views.apikey_index')
@ -511,33 +512,33 @@ class IetfAuthTests(TestCase):
self.assertContains(r, 'Get a new personal API key')
# Check the add key form content
url = urlreverse('ietf.ietfauth.views.apikey_add')
url = urlreverse('ietf.ietfauth.views.apikey_create')
r = self.client.get(url)
self.assertContains(r, 'Create a new personal API key')
self.assertContains(r, 'Endpoint')
# Add 2 keys
r = self.client.post(url, {'endpoint': '/api/submit'})
self.assertRedirects(r, urlreverse('ietf.ietfauth.views.apikey_index'))
r = self.client.post(url, {'endpoint': '/api/iesg/discuss'})
self.assertRedirects(r, urlreverse('ietf.ietfauth.views.apikey_index'))
for endpoint, display in PERSON_API_KEY_ENDPOINTS:
r = self.client.post(url, {'endpoint': endpoint})
self.assertRedirects(r, urlreverse('ietf.ietfauth.views.apikey_index'))
# Check api key list content
url = urlreverse('ietf.ietfauth.views.apikey_index')
r = self.client.get(url)
self.assertContains(r, '/api/submit')
self.assertContains(r, '/api/iesg/discuss')
for endpoint, display in PERSON_API_KEY_ENDPOINTS:
self.assertContains(r, endpoint)
q = PyQuery(r.content)
self.assertEqual(len(q('td code')), 2)
self.assertEqual(len(q('td code')), len(PERSON_API_KEY_ENDPOINTS)) # hash
self.assertEqual(len(q('td a:contains("Disable")')), len(PERSON_API_KEY_ENDPOINTS))
# Get one of the keys
key = person.apikeys.first()
# Check the delete key form content
url = urlreverse('ietf.ietfauth.views.apikey_del')
# Check the disable key form content
url = urlreverse('ietf.ietfauth.views.apikey_disable')
r = self.client.get(url)
self.assertContains(r, 'Delete a personal API key')
self.assertContains(r, 'Disable a personal API key')
self.assertContains(r, 'Key')
# Delete a key
@ -547,7 +548,98 @@ class IetfAuthTests(TestCase):
# Check the api key list content again
url = urlreverse('ietf.ietfauth.views.apikey_index')
r = self.client.get(url)
self.assertNotContains(r, key.endpoint)
q = PyQuery(r.content)
self.assertEqual(len(q('td code')), 1)
self.assertEqual(len(q('td code')), len(PERSON_API_KEY_ENDPOINTS)) # key hash
self.assertEqual(len(q('td a:contains("Disable")')), len(PERSON_API_KEY_ENDPOINTS)-1)
def test_apikey_usage(self):
BAD_KEY = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
person = PersonFactory()
area = GroupFactory(type_id='area')
area.role_set.create(name_id='ad', person=person, email=person.email())
url = urlreverse('ietf.ietfauth.views.apikey_create')
# Check that the url is protected, then log in
login_testing_unauthorized(self, person.user.username, url)
# Add keys
for endpoint, display in PERSON_API_KEY_ENDPOINTS:
r = self.client.post(url, {'endpoint': endpoint})
self.assertRedirects(r, urlreverse('ietf.ietfauth.views.apikey_index'))
for key in person.apikeys.all()[:3]:
url = key.endpoint
# successful access
r = self.client.post(url, {'apikey':key.hash(), 'dummy':'dummy',})
self.assertEqual(r.status_code, 200)
# bad method
r = self.client.put(url, {'apikey':key.hash()})
self.assertEqual(r.status_code, 405)
# missing apikey
r = self.client.post(url, {'dummy':'dummy',})
self.assertEqual(r.status_code, 400)
self.assertIn('Missing apikey parameter', unicontent(r))
# invalid apikey
r = self.client.post(url, {'apikey':BAD_KEY, 'dummy':'dummy',})
self.assertEqual(r.status_code, 400)
self.assertIn('Invalid apikey', unicontent(r))
# too long since regular login
person.user.last_login = datetime.datetime.now() - datetime.timedelta(days=settings.UTILS_APIKEY_GUI_LOGIN_LIMIT_DAYS+1)
person.user.save()
r = self.client.post(url, {'apikey':key.hash(), 'dummy':'dummy',})
self.assertEqual(r.status_code, 400)
self.assertIn('Too long since last regular login', unicontent(r))
person.user.last_login = datetime.datetime.now()
person.user.save()
# endpoint mismatch
key2 = PersonalApiKey.objects.create(person=person, endpoint='/')
r = self.client.post(url, {'apikey':key2.hash(), 'dummy':'dummy',})
self.assertEqual(r.status_code, 400)
self.assertIn('Apikey endpoint mismatch', unicontent(r))
key2.delete()
def test_send_apikey_report(self):
from ietf.ietfauth.management.commands.send_apikey_usage_emails import Command
from ietf.utils.mail import outbox, empty_outbox
person = PersonFactory()
url = urlreverse('ietf.ietfauth.views.apikey_create')
# Check that the url is protected, then log in
login_testing_unauthorized(self, person.user.username, url)
# Add keys
for endpoint, display in PERSON_API_KEY_ENDPOINTS:
r = self.client.post(url, {'endpoint': endpoint})
self.assertRedirects(r, urlreverse('ietf.ietfauth.views.apikey_index'))
# Use the endpoints (the form content will not be acceptable, but the
# apikey usage will be registered)
count = 2
# avoid usage across dates
if datetime.datetime.now().time() > datetime.time(hour=23, minute=59, second=58):
time.sleep(2)
for i in range(count):
for key in person.apikeys.all():
url = key.endpoint
self.client.post(url, {'apikey':key.hash(), 'dummy': 'dummy', })
date = str(datetime.date.today())
empty_outbox()
cmd = Command()
cmd.handle(verbosity=0, days=7)
self.assertEqual(len(outbox), len(PERSON_API_KEY_ENDPOINTS))
for mail in outbox:
body = mail.get_payload()
self.assertIn("API key usage", mail['subject'])
self.assertIn(" %s times" % count, body)
self.assertIn(date, body)

View file

@ -8,8 +8,8 @@ from ietf.utils.urls import url
urlpatterns = [
url(r'^$', views.index),
url(r'^apikey/?$', views.apikey_index),
url(r'^apikey/add/?$', views.apikey_add),
url(r'^apikey/del/?$', views.apikey_del),
url(r'^apikey/add/?$', views.apikey_create),
url(r'^apikey/del/?$', views.apikey_disable),
url(r'^confirmnewemail/(?P<auth>[^/]+)/$', views.confirm_new_email),
url(r'^create/$', views.create_account),
url(r'^create/confirm/(?P<auth>[^/]+)/$', views.confirm_account),

View file

@ -602,7 +602,7 @@ def apikey_index(request):
@login_required
@person_required
def apikey_add(request):
def apikey_create(request):
class ApiKeyForm(forms.ModelForm):
class Meta:
model = PersonalApiKey
@ -623,7 +623,7 @@ def apikey_add(request):
@login_required
@person_required
def apikey_del(request):
def apikey_disable(request):
person = request.user.person
choices = [ (k.hash(), str(k)) for k in person.apikeys.all() ]
#
@ -642,11 +642,12 @@ def apikey_del(request):
if form.is_valid():
hash = form.data['hash']
key = PersonalApiKey.validate_key(hash)
key.delete()
messages.success(request, "Deleted key %s" % hash)
key.valid = False
key.save()
messages.success(request, "Disabled key %s" % hash)
return redirect('ietf.ietfauth.views.apikey_index')
else:
messages.error(request, "Key validation failed; key not deleted")
messages.error(request, "Key validation failed; key not disabled")
else:
form = KeyDeleteForm(request.GET)
return render(request, 'form.html', {'form':form, 'title':"Delete a personal API key", 'description':'', 'button':'Delete key'})
return render(request, 'form.html', {'form':form, 'title':"Disable a personal API key", 'description':'', 'button':'Disable key'})

View file

@ -1,7 +1,7 @@
from django.contrib import admin
from ietf.person.models import Email, Alias, Person, PersonHistory, PersonalApiKey
from ietf.person.models import Email, Alias, Person, PersonHistory, PersonalApiKey, PersonEvent, PersonApiKeyEvent
from ietf.person.name import name_parts
class EmailAdmin(admin.ModelAdmin):
@ -49,3 +49,15 @@ class PersonalApiKeyAdmin(admin.ModelAdmin):
raw_id_fields = ['person', ]
search_fields = ['person__name', ]
admin.site.register(PersonalApiKey, PersonalApiKeyAdmin)
class PersonEventAdmin(admin.ModelAdmin):
list_display = ["id", "person", "time", "type", ]
search_fields = ["person__name", ]
raw_id_fields = ['person', ]
admin.site.register(PersonEvent, PersonEventAdmin)
class PersonApiKeyEventAdmin(admin.ModelAdmin):
list_display = ["id", "person", "time", "type", "key"]
search_fields = ["person__name", ]
raw_id_fields = ['person', ]
admin.site.register(PersonApiKeyEvent, PersonApiKeyEventAdmin)

View file

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.8 on 2017-12-15 08:19
from __future__ import unicode_literals
import datetime
from django.db import migrations, models
import django.db.models.deletion
import ietf.person.models
class Migration(migrations.Migration):
dependencies = [
('person', '0020_auto_20170701_0325'),
]
operations = [
migrations.CreateModel(
name='PersonalApiKey',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('endpoint', models.CharField(choices=[(b'/api/submit', b'/api/submit'), (b'/api/iesg/position', b'/api/iesg/position')], max_length=128)),
('created', models.DateTimeField(default=datetime.datetime.now)),
('valid', models.BooleanField(default=True)),
('salt', models.BinaryField(default=ietf.person.models.salt, max_length=12)),
('count', models.IntegerField(default=0)),
('latest', models.DateTimeField(blank=True, null=True)),
('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='apikeys', to='person.Person')),
],
),
migrations.CreateModel(
name='PersonEvent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.DateTimeField(default=datetime.datetime.now, help_text=b'When the event happened')),
('type', models.CharField(choices=[(b'apikey_login', b'API key login')], max_length=50)),
('desc', models.TextField()),
],
options={
'ordering': ['-time', '-id'],
},
),
migrations.CreateModel(
name='PersonApiKeyEvent',
fields=[
('personevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='person.PersonEvent')),
('key', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.PersonalApiKey')),
],
bases=('person.personevent',),
),
migrations.AddField(
model_name='personevent',
name='person',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Person'),
),
]

View file

@ -284,14 +284,14 @@ KEY_STRUCT = "i12s32s"
def salt():
return uuid.uuid4().bytes[:12]
API_KEY_ENDPOINTS = [
("/api/submit", "/api/submit"),
("/api/iesg/discuss", "/api/iesg/discuss"),
# Manual maintenance: List all endpoints that use @require_user_api_key here
PERSON_API_KEY_ENDPOINTS = [
("/api/iesg/position", "/api/iesg/position"),
]
class PersonalApiKey(models.Model):
person = models.ForeignKey(Person, related_name='apikeys')
endpoint = models.CharField(max_length=128, null=False, blank=False, choices=API_KEY_ENDPOINTS)
endpoint = models.CharField(max_length=128, null=False, blank=False, choices=PERSON_API_KEY_ENDPOINTS)
created = models.DateTimeField(default=datetime.datetime.now, null=False)
valid = models.BooleanField(default=True)
salt = models.BinaryField(default=salt, max_length=12, null=False, blank=False)
@ -303,7 +303,10 @@ class PersonalApiKey(models.Model):
import struct, hashlib, base64
key = base64.urlsafe_b64decode(six.binary_type(s))
id, salt, hash = struct.unpack(KEY_STRUCT, key)
k = cls.objects.get(id=id)
k = cls.objects.filter(id=id)
if not k.exists():
return None
k = k.first()
check = hashlib.sha256()
for v in (str(id), str(k.person.id), k.created.isoformat(), k.endpoint, str(k.valid), salt, settings.SECRET_KEY):
check.update(v)
@ -322,3 +325,23 @@ class PersonalApiKey(models.Model):
def __unicode__(self):
return "%s (%s): %s ..." % (self.endpoint, self.created.strftime("%Y-%m-%d %H:%M"), self.hash()[:16])
PERSON_EVENT_CHOICES = [
("apikey_login", "API key login"),
]
class PersonEvent(models.Model):
person = models.ForeignKey(Person)
time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened")
type = models.CharField(max_length=50, choices=PERSON_EVENT_CHOICES)
desc = models.TextField()
def __unicode__(self):
return u"%s %s at %s" % (self.person.plain_name(), self.get_type_display().lower(), self.time)
class Meta:
ordering = ['-time', '-id']
class PersonApiKeyEvent(PersonEvent):
key = models.ForeignKey(PersonalApiKey)

View file

@ -6,7 +6,7 @@ from tastypie.cache import SimpleCache
from ietf import api
from ietf.person.models import (Person, Email, Alias, PersonHistory, PersonalApiKey)
from ietf.person.models import (Person, Email, Alias, PersonHistory, PersonalApiKey, PersonEvent, PersonApiKeyEvent)
from ietf.utils.resources import UserResource
@ -102,3 +102,41 @@ class PersonalApiKeyResource(ModelResource):
"person": ALL_WITH_RELATIONS,
}
api.person.register(PersonalApiKeyResource())
class PersonEventResource(ModelResource):
person = ToOneField(PersonResource, 'person')
class Meta:
queryset = PersonEvent.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'personevent'
filtering = {
"id": ALL,
"time": ALL,
"type": ALL,
"desc": ALL,
"person": ALL_WITH_RELATIONS,
}
api.person.register(PersonEventResource())
class PersonApiKeyEventResource(ModelResource):
person = ToOneField(PersonResource, 'person')
personevent_ptr = ToOneField(PersonEventResource, 'personevent_ptr')
key = ToOneField(PersonalApiKeyResource, 'key')
class Meta:
queryset = PersonApiKeyEvent.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'personapikeyevent'
filtering = {
"id": ALL,
"time": ALL,
"type": ALL,
"desc": ALL,
"person": ALL_WITH_RELATIONS,
"personevent_ptr": ALL_WITH_RELATIONS,
"key": ALL_WITH_RELATIONS,
}
api.person.register(PersonApiKeyEventResource())

View file

@ -932,7 +932,7 @@ SILENCED_SYSTEM_CHECKS = [
STATS_NAMES_LIMIT = 25
UTILS_TEST_RANDOM_STATE_FILE = '.factoryboy_random_state'
UTILS_APIKEY_GUI_LOGIN_LIMIT_DAYS = 30
# Put the production SECRET_KEY in settings_local.py, and also any other
# sensitive or site-specific changes. DO NOT commit settings_local.py to svn.

View file

@ -20,16 +20,20 @@
<table class="table table-condensed">
{% for key in person.apikeys.all %}
{% if forloop.first %}
<tr ><th>Key</th><th>Created</th><th>Endpoint</th><th>Valid</th><th>&nbsp;</th></tr>
<tr ><th>Endpoint</th><th>Created</th><th>Latest use</th><th>Count</th><th>Valid</th></tr>
{% endif %}
<tr>
<td><code>{{ key.hash }}</code></td>
<td>{{ key.created }} </td>
<td>{{ key.endpoint }} </td>
<td>{{ key.created }} </td>
<td>{{ key.latest }} </td>
<td>{{ key.count }} </td>
<td>{{ key.valid }} </td>
<td>
</tr>
<tr>
<td style="border-top: 0" colspan="4"><code>{{ key.hash }}</code></td>
<td style="border-top: 0">
{% if key.valid %}
<a href="{%url 'ietf.ietfauth.views.apikey_del' %}?hash={{key.hash}}" class="btn btn-warning btn-xs del-apikey">Delete</a>
<a href="{%url 'ietf.ietfauth.views.apikey_disable' %}?hash={{key.hash}}" class="btn btn-warning btn-xs del-apikey">Disable</a>
{% endif %}
</td>
</tr>
@ -38,7 +42,7 @@
<tr><td></td></tr>
{% endfor %}
</table>
<a href="{% url 'ietf.ietfauth.views.apikey_add' %}" class="btn btn-default btn-sm add-apikey">Get a new personal API key</a>
<a href="{% url 'ietf.ietfauth.views.apikey_create' %}" class="btn btn-default btn-sm add-apikey">Get a new personal API key</a>
</div>
</div>

View file

@ -0,0 +1,14 @@
{% load ietf_filters %}{% filter wordwrap:78 %}
Dear {{ person.plain_name }},
This is a summary of API key usage during the last {{ days }} days for the key '{{ key_name }}',
created {{ key.created }} for endpoint {{ key.endpoint }}.
This API key was used {{ count }} times during this period. Showing {{ events|length }} access times:
{% for e in events %}
{{ e.time }}{% endfor %}
Best regards,
The IETF Secretariat
{% endfilter %}

View file

@ -1,10 +1,18 @@
# Copyright The IETF Trust 2016, All Rights Reserved
import datetime
from decorator import decorator
from django.conf import settings
from django.contrib.auth import login
from django.http import HttpResponse
from django.shortcuts import render
from test_runner import set_coverage_checking
import debug # pyflakes:ignore
from ietf.utils.test_runner import set_coverage_checking
from ietf.person.models import Person, PersonalApiKey, PersonApiKeyEvent
@decorator
def skip_coverage(f, *args, **kwargs):
@ -18,8 +26,6 @@ def skip_coverage(f, *args, **kwargs):
@decorator
def person_required(f, request, *args, **kwargs):
from ietf.person.models import Person
from django.shortcuts import render
if not request.user.is_authenticated:
raise ValueError("The @person_required decorator should be called after @login_required.")
try:
@ -27,43 +33,44 @@ def person_required(f, request, *args, **kwargs):
except Person.DoesNotExist:
return render(request, 'registration/missing_person.html')
return f(request, *args, **kwargs)
@decorator
def verify_user_api_key(f, request, *args, **kwargs):
from ietf.person.models import Person, PersonalApiKey
from django.shortcuts import render
if not request.user.is_authenticated:
raise ValueError("The @verify_user_api_key decorator should be called after @login_required.")
try:
person = request.user.person
except Person.DoesNotExist:
return render(request, 'registration/missing_person.html')
def require_user_api_key(f, request, *args, **kwargs):
def err(code, text):
return HttpResponse(text, status=code, content_type='text/plain')
# Check method and get hash
if request.method == 'POST':
hash = request.POST['apikey']
hash = request.POST.get('apikey')
elif request.method == 'GET':
hash = request.GET['apikey']
hash = request.GET.get('apikey')
else:
return render(request, 'base.html', {
'content': """
<h1>Missing API key</h1>
<p>
There is no apikey provided with this call.
Please create a valid Personal API key and use that with your request.
</p>
""",
})
return err(405, "Method not allowed")
if not hash:
return err(400, "Missing apikey parameter")
# Check hash
key = PersonalApiKey.validate_key(hash)
if key and key.person == person:
return f(request, key, *args, **kwargs)
else:
return render(request, 'base.html', {
'content': """
<h1>Bad API key</h1>
<p>
The API key provided with this cal is invalid.
Please create a valid Personal API key and use that with your request.
</p>
""",
})
if not key:
return err(400, "Invalid apikey")
# Check endpoint
urlpath = request.META.get('PATH_INFO')
if not (urlpath and urlpath == key.endpoint):
return err(400, "Apikey endpoint mismatch")
# Check time since regular login
person = key.person
last_login = person.user.last_login
time_limit = (datetime.datetime.now() - datetime.timedelta(days=settings.UTILS_APIKEY_GUI_LOGIN_LIMIT_DAYS))
if last_login < time_limit:
return err(400, "Too long since last regular login")
# Log in
login(request, person.user)
# restore the user.last_login field, so it reflects only gui logins
person.user.last_login = last_login
person.user.save()
# Update stats
key.count += 1
key.latest = datetime.datetime.now()
key.save()
PersonApiKeyEvent.objects.create(person=person, type='apikey_login', key=key, desc="Logged in with key ID %s, endpoint %s" % (key.id, key.endpoint))
# Execute decorated function
return f(request, *args, **kwargs)