aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndy Doan <andy.doan@linaro.org>2014-09-18 18:13:32 -0500
committerAndy Doan <andy.doan@linaro.org>2014-09-18 18:13:32 -0500
commit62d2b26ef7addd11ef6c65a230b1bdc311578e93 (patch)
tree0f6ddb09bb045baa2e96451d102be22a9db20ed9
parent845a67526a14fb5feb4278ae75280f0844bd8793 (diff)
apiv2: add a "not-valid-til" restriction on a token
Change-Id: Ia5ec73640c2bc7e83c6d94b3eaff4cf664c463d6
-rw-r--r--license_protected_downloads/api/v2.py33
-rw-r--r--license_protected_downloads/migrations/0006_auto__add_field_apitoken_not_valid_til.py56
-rw-r--r--license_protected_downloads/models.py5
-rw-r--r--license_protected_downloads/tests/test_api_v2.py21
4 files changed, 103 insertions, 12 deletions
diff --git a/license_protected_downloads/api/v2.py b/license_protected_downloads/api/v2.py
index 58251e9..ab53afb 100644
--- a/license_protected_downloads/api/v2.py
+++ b/license_protected_downloads/api/v2.py
@@ -17,7 +17,15 @@ def token_as_dict(token):
expires = token.expires
if expires:
expires = expires.isoformat()
- return {'id': token.token, 'expires': expires, 'ip': token.ip}
+ not_valid_til = token.not_valid_til
+ if not_valid_til:
+ not_valid_til = not_valid_til.isoformat()
+ return {
+ 'id': token.token,
+ 'not_valid_til': not_valid_til,
+ 'expires': expires,
+ 'ip': token.ip
+ }
class RestException(Exception):
@@ -73,24 +81,25 @@ class TokenResource(RestResource):
data.append(token_as_dict(token))
return HttpResponse(json.dumps(data), content_type='application/json')
- def _parse_expires(self):
- expires = None
- if 'expires' in self.request.POST:
- expires = self.request.POST['expires']
- if expires.isdigit():
+ def _parse_time(self, field):
+ ts = None
+ if field in self.request.POST:
+ ts = self.request.POST[field]
+ if ts.isdigit():
# accept a time in seconds for expiration
- expires = datetime.datetime.now() + datetime.timedelta(
- seconds=int(expires))
+ ts = datetime.datetime.now() + datetime.timedelta(
+ seconds=int(ts))
else:
# accept ISO8601 formatted datetime
- expires = datetime.datetime.strptime(
- expires, '%Y-%m-%dT%H:%M:%S.%f')
- return expires
+ ts = datetime.datetime.strptime(ts, '%Y-%m-%dT%H:%M:%S.%f')
+ return ts
def POST(self):
ip = self.request.POST.get('ip', None)
token = APIToken.objects.create(
- key=self.api_key, expires=self._parse_expires(), ip=ip)
+ key=self.api_key, ip=ip,
+ expires=self._parse_time('expires'),
+ not_valid_til=self._parse_time('not_valid_til'))
response = HttpResponse(status=201)
response['Location'] = self.request.path + token.token
return response
diff --git a/license_protected_downloads/migrations/0006_auto__add_field_apitoken_not_valid_til.py b/license_protected_downloads/migrations/0006_auto__add_field_apitoken_not_valid_til.py
new file mode 100644
index 0000000..9963254
--- /dev/null
+++ b/license_protected_downloads/migrations/0006_auto__add_field_apitoken_not_valid_til.py
@@ -0,0 +1,56 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'APIToken.not_valid_til'
+ db.add_column('license_protected_downloads_apitoken', 'not_valid_til', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'APIToken.not_valid_til'
+ db.delete_column('license_protected_downloads_apitoken', 'not_valid_til')
+
+
+ models = {
+ 'license_protected_downloads.apikeystore': {
+ 'Meta': {'object_name': 'APIKeyStore'},
+ 'description': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '40'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
+ 'last_used': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'null': 'True', 'blank': 'True'}),
+ 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'license_protected_downloads.apilog': {
+ 'Meta': {'object_name': 'APILog'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'key': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['license_protected_downloads.APIKeyStore']", 'null': 'True', 'blank': 'True'}),
+ 'label': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'path': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'license_protected_downloads.apitoken': {
+ 'Meta': {'object_name': 'APIToken'},
+ 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'ip': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+ 'key': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['license_protected_downloads.APIKeyStore']"}),
+ 'not_valid_til': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'token': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'})
+ },
+ 'license_protected_downloads.license': {
+ 'Meta': {'object_name': 'License'},
+ 'digest': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'theme': ('django.db.models.fields.CharField', [], {'max_length': '60'})
+ }
+ }
+
+ complete_apps = ['license_protected_downloads']
diff --git a/license_protected_downloads/models.py b/license_protected_downloads/models.py
index 701069d..64d58e8 100644
--- a/license_protected_downloads/models.py
+++ b/license_protected_downloads/models.py
@@ -61,6 +61,9 @@ class APIToken(models.Model):
key = models.ForeignKey(APIKeyStore)
expires = models.DateTimeField(
blank=True, null=True, help_text='Limit the duration of this token')
+ not_valid_til = models.DateTimeField(
+ blank=True, null=True,
+ help_text='Prevent this token from being immediately available')
ip = ip_field(required=False)
def save(self, *args, **kwargs):
@@ -71,6 +74,8 @@ class APIToken(models.Model):
def valid_request(self, request):
if self.expires and self.expires < datetime.datetime.now():
return False
+ if self.not_valid_til and self.not_valid_til > datetime.datetime.now():
+ return False
if self.ip and get_ip(request) != self.ip:
return False
diff --git a/license_protected_downloads/tests/test_api_v2.py b/license_protected_downloads/tests/test_api_v2.py
index 0d68020..6cdb4f8 100644
--- a/license_protected_downloads/tests/test_api_v2.py
+++ b/license_protected_downloads/tests/test_api_v2.py
@@ -66,6 +66,20 @@ class APIv2Tests(TestCase):
self.assertEqual(
expires.isoformat(), json.loads(resp.content)['expires'])
+ def test_token_create_not_valid_til(self):
+ ts = datetime.datetime.now() + datetime.timedelta(minutes=1)
+ data = {'not_valid_til': ts.isoformat()}
+ resp = self.client.post(
+ '/api/v2/token/', data=data, HTTP_AUTHTOKEN=self.api_key.key)
+ self.assertEqual(201, resp.status_code)
+
+ # get it to verify
+ resp = self.client.get(
+ resp['Location'], HTTP_AUTHTOKEN=self.api_key.key)
+ self.assertEqual(200, resp.status_code)
+ self.assertEqual(
+ ts.isoformat(), json.loads(resp.content)['not_valid_til'])
+
def test_token_create_ip(self):
data = {'ip': 'foo'}
resp = self.client.post(
@@ -104,6 +118,13 @@ class APIv2Tests(TestCase):
'/api/v2/publish/a', HTTP_AUTHTOKEN=token.token)
self.assertEqual(401, resp.status_code)
+ def test_publish_not_valid_til_token(self):
+ ts = datetime.datetime.now() + datetime.timedelta(minutes=1)
+ token = APIToken.objects.create(key=self.api_key, not_valid_til=ts)
+ resp = self.client.post(
+ '/api/v2/publish/a', HTTP_AUTHTOKEN=token.token)
+ self.assertEqual(401, resp.status_code)
+
def test_publish_ip_token(self):
token = APIToken.objects.create(key=self.api_key, ip='127.0.0.1').token
self._send_file('/api/v2/publish/foo', token, 'content')