diff options
author | Andy Doan <andy.doan@linaro.org> | 2014-09-18 18:13:32 -0500 |
---|---|---|
committer | Andy Doan <andy.doan@linaro.org> | 2014-09-18 18:13:32 -0500 |
commit | 62d2b26ef7addd11ef6c65a230b1bdc311578e93 (patch) | |
tree | 0f6ddb09bb045baa2e96451d102be22a9db20ed9 | |
parent | 845a67526a14fb5feb4278ae75280f0844bd8793 (diff) |
apiv2: add a "not-valid-til" restriction on a token
Change-Id: Ia5ec73640c2bc7e83c6d94b3eaff4cf664c463d6
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') |