├── .gitignore ├── LICENSE ├── README ├── key ├── __init__.py ├── admin │ ├── __init__.py │ ├── admin.py │ └── forms.py ├── auth.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto__add_field_apikeyprofile_last_access.py │ ├── 0003_auto__chg_field_apikey_logged_ip.py │ ├── 0004_auto__chg_field_apikey_key.py │ ├── 0005_auto__del_apikey__del_apikeyprofile.py │ ├── 0006_auto__add_apikey__add_apikeyprofile.py │ └── __init__.py ├── models │ ├── __init__.py │ ├── actions.py │ └── models.py ├── settings.py ├── signals.py ├── templates │ ├── admin │ │ └── key │ │ │ └── apikeyprofile │ │ │ └── change_form.html │ ├── base.html │ └── key │ │ ├── apikey_confirm_delete.html │ │ ├── apikey_form.html │ │ ├── apikey_list.html │ │ ├── base.html │ │ └── key.html ├── tests │ ├── __init__.py │ ├── test_key.py │ └── test_urls.py ├── urls.py └── views.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *~ 4 | .installed.cfg 5 | bin 6 | develop-eggs 7 | dist 8 | downloads 9 | eggs 10 | parts 11 | src/*.egg-info 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Redistribution and use in source and binary forms, with or without 2 | modification, are permitted provided that the following conditions 3 | are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright 6 | notice, this list of conditions and the following disclaimer. 7 | 2. Redistributions in binary form must reproduce the above copyright 8 | notice, this list of conditions and the following disclaimer in the 9 | documentation and/or other materials provided with the distribution. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 12 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 13 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 14 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 15 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 16 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 17 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 18 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 19 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 20 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | django-apikey 2 | ============= 3 | 4 | Simple, straight forward API key authentication for django-piston. 5 | 6 | Requirements (can be install with pip install -r requirements.txt): 7 | django-piston 8 | Using django.contrib.auth 9 | 10 | You can install into a virtualenv and run the tests: 11 | ./manage.py test key --settings="key.settings" 12 | 13 | There is a simple template file in templates/keys.html. This file is used 14 | by all the project's views. 15 | 16 | 17 | To use django-apikey with django-piston: 18 | 19 | from key.auth import ApiKeyAuthentication 20 | auth = ApiKeyAuthentication( ) 21 | bucket_handler = Resource( handler=MyHandler, authentication=auth ) 22 | 23 | 24 | By default, clients use the X-Api-Authorization header to pass the API key: 25 | 26 | headers['X-Api-Authorization'] = 'your.api.key.here' 27 | 28 | This can be modified in the settings.py of your Django project by using the APIKEY_AUTHORIZATION_HEADER setting, e.g.: 29 | 30 | APIKEY_AUTHORIZATION_HEADER = 'X-MyCoolApp-Authorization' 31 | 32 | This setting defaults to the X-Api-Authorization header. 33 | 34 | The numer of keys that a User can be defined in your project's settings.py. \ 35 | Setting APIKEY_MAX_KEYS to a positive integer will create a limit on the number of keys an individual User can create. Setting it to -1 allows Users to create an 36 | unlimited number of keys. 37 | 38 | APIKEY_MAX_KEYS = -1 39 | 40 | For example, this will allow a User to make an unlimited number of keys. 41 | 42 | You can control the length of the key by setting APIKEY_KEY_SIZE. Currently, there is a maximum 43 | key size of 32 characters. 44 | 45 | The following signals are generated / used by this package: 46 | 47 | api_user_created : generated when a ApiKeyProfile is attached to a User 48 | api_key_created : generated when a ApiKey is created 49 | api_user_logged_in : generated when a user connects to REST service with key authentication 50 | api_user_logged_out : generated when a user explicitly logs out of the REST service 51 | 52 | 53 | You must create ApiKeyProfiles right from your code (as you may want restrict some users from having access to your API), to do that, you can just add the following code into some of your models.py files: 54 | 55 | from django.contrib.auth.models import User 56 | from django.db.models.signals import post_save 57 | from key.models.actions import create_key_profile_signal 58 | 59 | post_save.connect(create_key_profile_signal, sender=User) 60 | 61 | Whenever you create a new user, it will create a new ApiKeyProfile. -------------------------------------------------------------------------------- /key/__init__.py: -------------------------------------------------------------------------------- 1 | import models 2 | import forms 3 | import views 4 | import signals 5 | -------------------------------------------------------------------------------- /key/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from key.admin.admin import * 2 | from key.admin.forms import * 3 | 4 | __all__ = ['ApiKeyProfileAdmin', 5 | 'ApiKeyAdmin', 6 | ] 7 | -------------------------------------------------------------------------------- /key/admin/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from key.models import * 3 | from key.admin.forms import * 4 | 5 | class ApiKeyInline(admin.TabularInline): 6 | model = ApiKey 7 | extra = 0 8 | max_num = 0 9 | fields = ('key', 'logged_ip', 'last_used', 'created') 10 | readonly_fields = ('key', 'logged_ip') 11 | 12 | class ApiKeyAdmin(admin.ModelAdmin): 13 | form = ApiKeyAdminForm 14 | 15 | def has_add_permission(self, *args, **kwargs): 16 | return False 17 | 18 | def queryset(self, request): 19 | if request.user.is_superuser: 20 | return ApiKey.objects.all() 21 | else: 22 | p = ApiKeyProfile.objects.get(user=request.user) 23 | return p.api_keys.all() 24 | 25 | class ApiKeyProfileAdmin(admin.ModelAdmin): 26 | form = ApiKeyProfileAdminForm 27 | inlines = (ApiKeyInline,) 28 | 29 | def has_add_permission(self, *args, **kwargs): 30 | return False 31 | 32 | 33 | admin.site.register(ApiKey, ApiKeyAdmin) 34 | admin.site.register(ApiKeyProfile, ApiKeyProfileAdmin) 35 | 36 | 37 | -------------------------------------------------------------------------------- /key/admin/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import * 2 | import logging 3 | from key.models import * 4 | 5 | class ApiKeyProfileAdminForm(ModelForm): 6 | class Meta: 7 | model = ApiKeyProfile 8 | readonly_fields = ('last_access',) 9 | 10 | def clean(self, *args, **kwargs): 11 | cleaned_data = super(ApiKeyProfileAdminForm, self).clean(*args, **kwargs) 12 | if self.data.has_key('generate'): 13 | generate_unique_api_key(self.instance.user) 14 | return cleaned_data 15 | 16 | class ApiKeyAdminForm(ModelForm): 17 | class Meta: 18 | model = ApiKey 19 | readonly_fields = ('key', 'logged_ip') 20 | 21 | def save(self, commit=True, *args, **kwargs): 22 | model = super(ApiKeyAdminForm, self).save(commit=False, *args, **kwargs) 23 | if not model.key: 24 | model = generate_unique_key_code(model.profile.user, model) 25 | if commit: 26 | model.save() 27 | return model 28 | -------------------------------------------------------------------------------- /key/auth.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.core import cache 3 | from django.contrib.auth.models import * 4 | from key.models import ApiKey 5 | from key.signals import api_user_logged_in 6 | from key import settings 7 | import logging 8 | import hashlib 9 | 10 | def _key_cache_key(auth_string): 11 | kstr = 'key.%s' % auth_string 12 | return hashlib.md5(kstr).hexdigest() 13 | 14 | class ApiKeyAuthentication(object): 15 | def is_authenticated(self, request): 16 | auth_header = getattr(settings, 'AUTH_HEADER') 17 | auth_header = 'HTTP_%s' % (auth_header.upper().replace('-', '_')) 18 | auth_string = request.META.get(auth_header) 19 | if not auth_string: 20 | return False 21 | try: 22 | key = ApiKey.objects.get(key=auth_string) 23 | except ApiKey.DoesNotExist, e: 24 | return False 25 | request.user = key.profile.user 26 | if not key.profile.user.has_perm('key.can_use_api'): 27 | return False 28 | key.login(request.META.get('REMOTE_ADDR')) 29 | return True 30 | 31 | def challenge(self): 32 | auth_header = getattr(settings, 'AUTH_HEADER') 33 | resp = HttpResponse('Authorization Required') 34 | resp['WWW-Authenticate'] = 'KeyBasedAuthentication realm="API"' 35 | resp[auth_header] = 'Key Needed' 36 | resp.status_code = 401 37 | return resp 38 | 39 | 40 | class CachingAuthentication(ApiKeyAuthentication): 41 | def is_authenticated(self, request): 42 | auth_header = getattr(settings, 'AUTH_HEADER') 43 | auth_header = 'HTTP_%s' % (auth_header.upper().replace('-', '_')) 44 | auth_string = request.META.get(auth_header) 45 | if not auth_string: 46 | return False 47 | try: 48 | user = cache.get_cache('default').get(_key_cache_key(auth_string)) 49 | if user: 50 | request.user = user 51 | else: 52 | rv = super(CachingAuthentication, self).is_authenticated(request) 53 | if rv: 54 | key = ApiKey.objects.get(key=auth_string) 55 | cache.get_cache('default').set(_key_cache_key(key.key), 56 | request.user) 57 | except ApiKey.DoesNotExist: 58 | return False 59 | request.key = auth_string 60 | return True 61 | 62 | def challenge(self): 63 | auth_header = getattr(settings, 'AUTH_HEADER') 64 | resp = HttpResponse('Authorization Required') 65 | resp['WWW-Authenticate'] = 'KeyBasedAuthentication realm="API"' 66 | resp[auth_header] = 'Key Needed' 67 | resp.status_code = 401 68 | return resp 69 | -------------------------------------------------------------------------------- /key/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import * 2 | from key.models import * 3 | 4 | -------------------------------------------------------------------------------- /key/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'ApiKeyProfile' 12 | db.create_table('key_apikeyprofile', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='key_profile', unique=True, to=orm['auth.User'])), 15 | ('max_keys', self.gf('django.db.models.fields.IntegerField')(default=-1)), 16 | )) 17 | db.send_create_signal('key', ['ApiKeyProfile']) 18 | 19 | # Adding model 'ApiKey' 20 | db.create_table('key_apikey', ( 21 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 22 | ('profile', self.gf('django.db.models.fields.related.ForeignKey')(related_name='api_keys', to=orm['key.ApiKeyProfile'])), 23 | ('key', self.gf('django.db.models.fields.CharField')(unique=True, max_length=32)), 24 | ('logged_ip', self.gf('django.db.models.fields.IPAddressField')(max_length=15, null=True)), 25 | ('last_used', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.utcnow)), 26 | ('created', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.utcnow)), 27 | )) 28 | db.send_create_signal('key', ['ApiKey']) 29 | 30 | 31 | def backwards(self, orm): 32 | 33 | # Deleting model 'ApiKeyProfile' 34 | db.delete_table('key_apikeyprofile') 35 | 36 | # Deleting model 'ApiKey' 37 | db.delete_table('key_apikey') 38 | 39 | 40 | models = { 41 | 'auth.group': { 42 | 'Meta': {'object_name': 'Group'}, 43 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 44 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 45 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 46 | }, 47 | 'auth.permission': { 48 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 49 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 50 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 51 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 52 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 53 | }, 54 | 'auth.user': { 55 | 'Meta': {'object_name': 'User'}, 56 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 57 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 58 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 59 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 60 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 61 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 62 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 63 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 64 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 65 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 66 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 67 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 68 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 69 | }, 70 | 'contenttypes.contenttype': { 71 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 72 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 73 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 75 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 76 | }, 77 | 'key.apikey': { 78 | 'Meta': {'ordering': "['-created']", 'object_name': 'ApiKey'}, 79 | 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), 80 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 81 | 'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}), 82 | 'last_used': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), 83 | 'logged_ip': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True'}), 84 | 'profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'api_keys'", 'to': "orm['key.ApiKeyProfile']"}) 85 | }, 86 | 'key.apikeyprofile': { 87 | 'Meta': {'object_name': 'ApiKeyProfile'}, 88 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 89 | 'max_keys': ('django.db.models.fields.IntegerField', [], {'default': '-1'}), 90 | 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'key_profile'", 'unique': 'True', 'to': "orm['auth.User']"}) 91 | } 92 | } 93 | 94 | complete_apps = ['key'] 95 | -------------------------------------------------------------------------------- /key/migrations/0002_auto__add_field_apikeyprofile_last_access.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'ApiKeyProfile.last_access' 12 | db.add_column('key_apikeyprofile', 'last_access', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.utcnow), keep_default=False) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Deleting field 'ApiKeyProfile.last_access' 18 | db.delete_column('key_apikeyprofile', 'last_access') 19 | 20 | 21 | models = { 22 | 'auth.group': { 23 | 'Meta': {'object_name': 'Group'}, 24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 26 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 27 | }, 28 | 'auth.permission': { 29 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 30 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 31 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 34 | }, 35 | 'auth.user': { 36 | 'Meta': {'object_name': 'User'}, 37 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 38 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 39 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 40 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 43 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 44 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 47 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 48 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 49 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 50 | }, 51 | 'contenttypes.contenttype': { 52 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 53 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 57 | }, 58 | 'key.apikey': { 59 | 'Meta': {'ordering': "['-created']", 'object_name': 'ApiKey'}, 60 | 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), 61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}), 63 | 'last_used': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), 64 | 'logged_ip': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True'}), 65 | 'profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'api_keys'", 'to': "orm['key.ApiKeyProfile']"}) 66 | }, 67 | 'key.apikeyprofile': { 68 | 'Meta': {'object_name': 'ApiKeyProfile'}, 69 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 70 | 'last_access': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), 71 | 'max_keys': ('django.db.models.fields.IntegerField', [], {'default': '-1'}), 72 | 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'key_profile'", 'unique': 'True', 'to': "orm['auth.User']"}) 73 | } 74 | } 75 | 76 | complete_apps = ['key'] 77 | -------------------------------------------------------------------------------- /key/migrations/0003_auto__chg_field_apikey_logged_ip.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Changing field 'ApiKey.logged_ip' 12 | db.alter_column('key_apikey', 'logged_ip', self.gf('django.db.models.fields.CharField')(max_length=32, null=True)) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Changing field 'ApiKey.logged_ip' 18 | db.alter_column('key_apikey', 'logged_ip', self.gf('django.db.models.fields.IPAddressField')(max_length=15, null=True)) 19 | 20 | 21 | models = { 22 | 'auth.group': { 23 | 'Meta': {'object_name': 'Group'}, 24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 26 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 27 | }, 28 | 'auth.permission': { 29 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 30 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 31 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 34 | }, 35 | 'auth.user': { 36 | 'Meta': {'object_name': 'User'}, 37 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 38 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 39 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 40 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 43 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 44 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 47 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 48 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 49 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 50 | }, 51 | 'contenttypes.contenttype': { 52 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 53 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 57 | }, 58 | 'key.apikey': { 59 | 'Meta': {'ordering': "['-created']", 'object_name': 'ApiKey'}, 60 | 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), 61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'key': ('django.db.models.fields.CharField', [], {'default': 'None', 'unique': 'True', 'max_length': '32', 'blank': 'True'}), 63 | 'last_used': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), 64 | 'logged_ip': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True', 'blank': 'True'}), 65 | 'profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'api_keys'", 'to': "orm['key.ApiKeyProfile']"}) 66 | }, 67 | 'key.apikeyprofile': { 68 | 'Meta': {'object_name': 'ApiKeyProfile'}, 69 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 70 | 'last_access': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), 71 | 'max_keys': ('django.db.models.fields.IntegerField', [], {'default': '-1'}), 72 | 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'key_profile'", 'unique': 'True', 'to': "orm['auth.User']"}) 73 | } 74 | } 75 | 76 | complete_apps = ['key'] 77 | -------------------------------------------------------------------------------- /key/migrations/0004_auto__chg_field_apikey_key.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Changing field 'ApiKey.key' 12 | db.alter_column('key_apikey', 'key', self.gf('django.db.models.fields.CharField')(unique=True, max_length=16)) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Changing field 'ApiKey.key' 18 | db.alter_column('key_apikey', 'key', self.gf('django.db.models.fields.CharField')(max_length=32, unique=True)) 19 | 20 | 21 | models = { 22 | 'auth.group': { 23 | 'Meta': {'object_name': 'Group'}, 24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 26 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 27 | }, 28 | 'auth.permission': { 29 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 30 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 31 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 34 | }, 35 | 'auth.user': { 36 | 'Meta': {'object_name': 'User'}, 37 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 38 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 39 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 40 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 43 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 44 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 47 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 48 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 49 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 50 | }, 51 | 'contenttypes.contenttype': { 52 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 53 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 57 | }, 58 | 'key.apikey': { 59 | 'Meta': {'ordering': "['-created']", 'object_name': 'ApiKey'}, 60 | 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), 61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'key': ('django.db.models.fields.CharField', [], {'default': 'None', 'unique': 'True', 'max_length': '16', 'blank': 'True'}), 63 | 'last_used': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), 64 | 'logged_ip': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True', 'blank': 'True'}), 65 | 'profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'api_keys'", 'to': "orm['key.ApiKeyProfile']"}) 66 | }, 67 | 'key.apikeyprofile': { 68 | 'Meta': {'object_name': 'ApiKeyProfile'}, 69 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 70 | 'last_access': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), 71 | 'max_keys': ('django.db.models.fields.IntegerField', [], {'default': '-1'}), 72 | 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'key_profile'", 'unique': 'True', 'to': "orm['auth.User']"}) 73 | } 74 | } 75 | 76 | complete_apps = ['key'] 77 | -------------------------------------------------------------------------------- /key/migrations/0005_auto__del_apikey__del_apikeyprofile.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Deleting model 'ApiKey' 12 | db.delete_table('key_apikey') 13 | 14 | # Deleting model 'ApiKeyProfile' 15 | db.delete_table('key_apikeyprofile') 16 | 17 | 18 | def backwards(self, orm): 19 | 20 | # Adding model 'ApiKey' 21 | db.create_table('key_apikey', ( 22 | ('profile', self.gf('django.db.models.fields.related.ForeignKey')(related_name='api_keys', to=orm['key.ApiKeyProfile'])), 23 | ('last_used', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.utcnow)), 24 | ('logged_ip', self.gf('django.db.models.fields.CharField')(default=None, max_length=32, null=True, blank=True)), 25 | ('created', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.utcnow)), 26 | ('key', self.gf('django.db.models.fields.CharField')(default=None, max_length=16, unique=True, blank=True)), 27 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 28 | )) 29 | db.send_create_signal('key', ['ApiKey']) 30 | 31 | # Adding model 'ApiKeyProfile' 32 | db.create_table('key_apikeyprofile', ( 33 | ('max_keys', self.gf('django.db.models.fields.IntegerField')(default=-1)), 34 | ('last_access', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.utcnow)), 35 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 36 | ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='key_profile', unique=True, to=orm['auth.User'])), 37 | )) 38 | db.send_create_signal('key', ['ApiKeyProfile']) 39 | 40 | 41 | models = { 42 | 43 | } 44 | 45 | complete_apps = ['key'] 46 | -------------------------------------------------------------------------------- /key/migrations/0006_auto__add_apikey__add_apikeyprofile.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'ApiKey' 12 | db.create_table('key_apikey', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('profile', self.gf('django.db.models.fields.related.ForeignKey')(related_name='api_keys', to=orm['key.ApiKeyProfile'])), 15 | ('key', self.gf('django.db.models.fields.CharField')(default=None, unique=True, max_length=16, blank=True)), 16 | ('logged_ip', self.gf('django.db.models.fields.CharField')(default=None, max_length=32, null=True, blank=True)), 17 | ('last_used', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.utcnow)), 18 | ('created', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.utcnow)), 19 | )) 20 | db.send_create_signal('key', ['ApiKey']) 21 | 22 | # Adding model 'ApiKeyProfile' 23 | db.create_table('key_apikeyprofile', ( 24 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 25 | ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='key_profile', unique=True, to=orm['auth.User'])), 26 | ('max_keys', self.gf('django.db.models.fields.IntegerField')(default=-1)), 27 | ('last_access', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.utcnow)), 28 | )) 29 | db.send_create_signal('key', ['ApiKeyProfile']) 30 | 31 | 32 | def backwards(self, orm): 33 | 34 | # Deleting model 'ApiKey' 35 | db.delete_table('key_apikey') 36 | 37 | # Deleting model 'ApiKeyProfile' 38 | db.delete_table('key_apikeyprofile') 39 | 40 | 41 | models = { 42 | 'auth.group': { 43 | 'Meta': {'object_name': 'Group'}, 44 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 45 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 46 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 47 | }, 48 | 'auth.permission': { 49 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 50 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 51 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 52 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 53 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 54 | }, 55 | 'auth.user': { 56 | 'Meta': {'object_name': 'User'}, 57 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 58 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 59 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 60 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 63 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 64 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 65 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 66 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 67 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 68 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 69 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 70 | }, 71 | 'contenttypes.contenttype': { 72 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 73 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 74 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 75 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 76 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 77 | }, 78 | 'key.apikey': { 79 | 'Meta': {'ordering': "['-created']", 'object_name': 'ApiKey'}, 80 | 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), 81 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 82 | 'key': ('django.db.models.fields.CharField', [], {'default': 'None', 'unique': 'True', 'max_length': '16', 'blank': 'True'}), 83 | 'last_used': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), 84 | 'logged_ip': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True', 'blank': 'True'}), 85 | 'profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'api_keys'", 'to': "orm['key.ApiKeyProfile']"}) 86 | }, 87 | 'key.apikeyprofile': { 88 | 'Meta': {'object_name': 'ApiKeyProfile'}, 89 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 90 | 'last_access': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), 91 | 'max_keys': ('django.db.models.fields.IntegerField', [], {'default': '-1'}), 92 | 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'key_profile'", 'unique': 'True', 'to': "orm['auth.User']"}) 93 | } 94 | } 95 | 96 | complete_apps = ['key'] 97 | -------------------------------------------------------------------------------- /key/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scoursen/django-apikey/06e20836d54b66f51ff46144698dcffcd07db5e5/key/migrations/__init__.py -------------------------------------------------------------------------------- /key/models/__init__.py: -------------------------------------------------------------------------------- 1 | from key.models.models import * 2 | from key.models.actions import * 3 | 4 | __all__ = ['ApiKeyProfile', 5 | 'ApiKey', 6 | 'generate_unique_key_code', 7 | 'generate_unique_api_key', 8 | 'create_group', 9 | 'assign_api_key_permissions', 10 | 'MAX_KEYS', 11 | 'USE_API_GROUP', 12 | 'KEY_SIZE', 13 | ] 14 | 15 | -------------------------------------------------------------------------------- /key/models/actions.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User, Permission, user_logged_in 2 | from django.core.exceptions import PermissionDenied 3 | from datetime import datetime 4 | from django.db.models.signals import post_save 5 | from django.db.models.signals import post_delete 6 | from key.models import * 7 | from key.signals import * 8 | import logging 9 | import time 10 | import hashlib 11 | 12 | def perm_check(user): 13 | try: 14 | if user.key_profile: 15 | return 16 | except: 17 | if user.has_perm("key.has_api_key_profile"): 18 | profile, created = ApiKeyProfile.objects.get_or_create(user=user) 19 | if created: 20 | api_user_created.send(sender=user.__class__, 21 | instance=user) 22 | else: 23 | raise PermissionDenied 24 | else: 25 | raise PermissionDenied 26 | 27 | def generate_unique_key_code(user): 28 | perm_check(user) 29 | while True: 30 | now = datetime.utcnow() 31 | kstr = hashlib.md5('%s-%s' % (user.email, now)).hexdigest()[:KEY_SIZE] 32 | key, created = ApiKey.objects.get_or_create(profile=user.key_profile, 33 | key=kstr, 34 | created=now) 35 | if created: 36 | try: 37 | key.save() 38 | return key 39 | except IntegriyError: 40 | pass 41 | time.sleep(0.01) 42 | 43 | def generate_unique_api_key(user): 44 | key = generate_unique_key_code(user) 45 | api_key_created.send(sender=key.__class__, instance=key) 46 | return key 47 | 48 | def update_profile_timestamps(sender, instance, created, *args, **kwargs): 49 | instance.profile.last_access = datetime.utcnow() 50 | instance.profile.save() 51 | post_save.connect(update_profile_timestamps, sender=ApiKey, dispatch_uid='update_profile_timstamps') 52 | 53 | def send_login_logout_signals(sender, instance, created, *args, **kwargs): 54 | if instance.logged_ip: 55 | api_user_logged_in.send(sender=instance.profile.user.__class__, 56 | user=instance.profile.user) 57 | else: 58 | api_user_logged_out.send(sender=instance.profile.user.__class__, 59 | user=instance.profile.user) 60 | post_save.connect(send_login_logout_signals, sender=ApiKey, dispatch_uid='send_loging_logout_signals') 61 | 62 | def create_key_profile(user): 63 | profile, c = ApiKeyProfile.objects.get_or_create(user=user) 64 | if c: 65 | api_user_created.send(sender=user.__class__, 66 | instance=user) 67 | return profile 68 | 69 | def create_key_profile_signal(sender, instance, created, *args, **kwargs): 70 | if created: 71 | create_key_profile(instance) 72 | -------------------------------------------------------------------------------- /key/models/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User, Group, Permission 3 | from django.contrib.contenttypes.models import ContentType 4 | from django.conf import settings 5 | from datetime import datetime 6 | from django.db.utils import IntegrityError 7 | import logging 8 | import time 9 | import hashlib 10 | from key.settings import MAX_KEYS, KEY_SIZE, USE_API_GROUP 11 | from key.signals import api_user_logged_in 12 | 13 | class ApiKeyProfile(models.Model): 14 | user = models.OneToOneField(User, related_name='key_profile') 15 | max_keys = models.IntegerField(default=MAX_KEYS) 16 | last_access = models.DateTimeField(default=datetime.utcnow) 17 | 18 | class Meta: 19 | app_label = 'key' 20 | permissions = ( 21 | ('has_api_key_profile', 'Has an API key profile'), 22 | ('can_use_api', 'Can use the API'), 23 | ) 24 | 25 | def available_keys(self): 26 | if self.max_keys == -1: 27 | return 'Unlimited' 28 | return self.max_keys - self.api_keys.count() 29 | 30 | def can_make_api_key(self): 31 | if self.available_keys() > 0: 32 | return True 33 | 34 | def __unicode__(self): 35 | return "ApiKeyProfile: %s, %s" % (self.api_keys.count(), 36 | self.max_keys) 37 | 38 | class ApiKey(models.Model): 39 | profile = models.ForeignKey(ApiKeyProfile, related_name='api_keys') 40 | key = models.CharField(max_length=KEY_SIZE, unique=True, blank=True, default='') 41 | logged_ip = models.CharField(max_length=32, blank=True, null=True, default=None) 42 | last_used = models.DateTimeField(default=datetime.utcnow) 43 | created = models.DateTimeField(default=datetime.utcnow) 44 | 45 | class Meta: 46 | app_label = 'key' 47 | ordering = ['-created'] 48 | permissions = ( 49 | ('can_make_api_key', 'Can generate an API key'), 50 | ) 51 | 52 | 53 | def login(self, ip_address): 54 | self.logged_ip = ip_address 55 | self.save() 56 | 57 | def logout(self): 58 | self.logged_ip = None 59 | self.save() 60 | 61 | def __unicode__(self): 62 | return 'ApiKey: %s' % (self.key) 63 | 64 | def assign_api_key_permissions(user_or_group): 65 | perm_list = getattr(user_or_group, 'permissions', 66 | getattr(user_or_group, 'user_permissions')) 67 | ct = ContentType.objects.get(app_label='key', model='apikeyprofile') 68 | hakp, _ = Permission.objects.get_or_create(codename='has_api_key_profile', content_type=ct) 69 | cua, _ = Permission.objects.get_or_create(codename='can_use_api', content_type=ct) 70 | ct = ContentType.objects.get(app_label='key', model='apikey') 71 | cmak, _ = Permission.objects.get_or_create(codename='can_make_api_key', content_type=ct) 72 | p = Permission.objects.get(codename='change_apikeyprofile') 73 | perm_list.add(hakp, cua, cmak, p) 74 | return user_or_group 75 | 76 | def create_group(): 77 | if USE_API_GROUP: 78 | gr, cr = Group.objects.get_or_create(name='API User') 79 | if cr: 80 | assign_api_key_permissions(gr) 81 | return gr 82 | -------------------------------------------------------------------------------- /key/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | DATABASES = { 4 | 'default': { 5 | 'ENGINE': 'django.db.backends.sqlite3', 6 | 'NAME': 'my_db', 7 | } 8 | } 9 | INSTALLED_APPS = ['key', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.admin'] 10 | 11 | 12 | ROOT_URLCONF = 'key.urls' 13 | 14 | MAX_KEYS = getattr(settings, 'APIKEY_MAX_KEYS', -1) 15 | KEY_SIZE = getattr(settings, 'APIKEY_KEY_SIZE', 32) 16 | USE_API_GROUP = getattr(settings, 'APIKEY_USE_API_GROUP', False) 17 | AUTH_HEADER = getattr(settings, 'APIKEY_AUTHORIZATION_HEADER', 'X-Api-Authorization') 18 | 19 | 20 | def reload(): 21 | global MAX_KEYS, KEY_SIZE, USE_API_GROUP, AUTH_HEADER 22 | MAX_KEYS = getattr(settings, 'APIKEY_MAX_KEYS', -1) 23 | KEY_SIZE = getattr(settings, 'APIKEY_KEY_SIZE', 32) 24 | USE_API_GROUP = getattr(settings, 'APIKEY_USE_API_GROUP', False) 25 | AUTH_HEADER = getattr(settings, 'APIKEY_AUTHORIZATION_HEADER', 'X-Api-Authorization') 26 | 27 | -------------------------------------------------------------------------------- /key/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | api_user_created = Signal(providing_args=['instance']) 4 | api_key_created = Signal(providing_args=['instance']) 5 | api_user_logged_in = Signal(providing_args=['instance']) 6 | api_user_logged_out = Signal(providing_args=['instance']) 7 | -------------------------------------------------------------------------------- /key/templates/admin/key/apikeyprofile/change_form.html: -------------------------------------------------------------------------------- 1 | {%extends "admin/change_form.html" %} 2 | 3 | {%block after_related_objects%} 4 | {%if perms.can_make_api_key%} 5 | {%if original.pk%} 6 | 7 | {%endif%} 8 | {%endif%} 9 | {%endblock%} 10 | -------------------------------------------------------------------------------- /key/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | You should never see this 4 | 5 | 6 |

If you are seeing this, django-apikey's base.html template is being 7 | found by the template renderer before your site's base.html. You should 8 | place the app that has the desired base.html earlier in the INSTALLED_APPS 9 | into settings.py. 10 |

You could also replace base.html in the <path-to-django-apikey>/key/templates/base.html with other content. 11 | {%block content%} 12 | {%endblock%} 13 | 14 | 15 | -------------------------------------------------------------------------------- /key/templates/key/apikey_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {%extends "key/base.html"%} 2 | 3 | {%block content%} 4 |

Are you sure you want to delete this API key? 5 |

Be sure to remove any references to this code -- {{object.key}} -- in any of your system configurations or code. 6 |

7 | {%csrf_token%} 8 | 9 | 10 |
11 | 12 | {%endblock%} 13 | -------------------------------------------------------------------------------- /key/templates/key/apikey_form.html: -------------------------------------------------------------------------------- 1 | {%extends "key/base.html" %} 2 | 3 | {%block content%} 4 |
5 | {% csrf_token %} 6 | {{form.as_p}} 7 | 8 |
9 | 10 | {%endblock%} 11 | -------------------------------------------------------------------------------- /key/templates/key/apikey_list.html: -------------------------------------------------------------------------------- 1 | {%extends "key/base.html" %} 2 | 3 | {%block content%} 4 | {%for key in object_list%} 5 | {%if forloop.first%} 6 |

Active Keys for {{user.username}}

7 | 12 | {%endif%} 13 | {%empty%} 14 |

No active keys for {{user.username}}

15 | {%endfor%} 16 | {%if can_make_api_key %} 17 | {%if available_keys == 'Unlimited' %} 18 |

You can create an unlmited number of keys. 19 | {%else%} 20 |

You can create {{available_keys}} more keys. 21 | {%endif%} 22 |

23 | {% csrf_token %} 24 | 25 |
26 | 27 | {%endif%} 28 | 29 | {%endblock%} 30 | -------------------------------------------------------------------------------- /key/templates/key/base.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | -------------------------------------------------------------------------------- /key/templates/key/key.html: -------------------------------------------------------------------------------- 1 | {%extends "key/base.html" %} 2 | 3 | {%block content%} 4 | {%if keys%} 5 |

Active Keys for {{user.username}}

6 | 11 | {%else%} 12 |

No active keys for {{user.username}}

13 | {%endif%} 14 | {%if can_make_api_key %} 15 | {%if available_keys == 'Unlimited' %} 16 |

You can create an unlmited number of keys. 17 | {%else%} 18 |

You can create {{available_keys}} more keys. 19 | {%endif%} 20 |

21 | {% csrf_token %} 22 | 23 |
24 | 25 | {%endif%} 26 | 27 | {%endblock%} 28 | -------------------------------------------------------------------------------- /key/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from test_key import * 2 | -------------------------------------------------------------------------------- /key/tests/test_key.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, Client 2 | from django.contrib.auth.models import * 3 | from django.core.cache import cache 4 | from django.core.urlresolvers import reverse 5 | from django.core.exceptions import PermissionDenied 6 | from django.conf import settings 7 | from django.db.models.signals import post_save 8 | from key.models import * 9 | from key.signals import * 10 | import logging 11 | import hashlib 12 | import test_urls 13 | 14 | class ApiKeyTest(TestCase): 15 | def __init__(self, *args, **kwargs): 16 | settings.USE_API_GROUP = kwargs.get('USE_API_GROUP', False) 17 | if kwargs.has_key('USE_API_GROUP'): 18 | del kwargs['USE_API_GROUP'] 19 | super(ApiKeyTest, self).__init__(*args, **kwargs) 20 | 21 | def setUp(self): 22 | cache.clear() 23 | create_group() 24 | self.user = User.objects.create_user(username='ApiKeyTest', 25 | email='ApiKeyTest@example.com', 26 | password='ApiKeyTestPassword') 27 | if USE_API_GROUP: 28 | gr = Group.objects.get(name="API User") 29 | self.user.groups.add(gr) 30 | self.user.save() 31 | gr.save() 32 | else: 33 | assign_api_key_permissions(self.user) 34 | k = generate_unique_api_key(self.user) 35 | self.assertTrue(k is not None) 36 | self.assertNotEquals(k.key, None) 37 | kstr = k.key 38 | self.unauthorized_user = User.objects.create_user(username="NonAuthorized", 39 | email="NonAuthorized@example.com", 40 | password="NonAuthorizedPassword") 41 | self.unauthorized_user.save() 42 | 43 | def test_key_profile(self): 44 | profile = self.user.key_profile 45 | self.assertNotEquals(profile, None) 46 | self.assertEquals(profile.user, self.user) 47 | self.assertEquals(profile.max_keys, MAX_KEYS) 48 | self.profile_signaled = False 49 | def check_signal(sender, instance, created, *args, **kwargs): 50 | self.profile_signaled = True 51 | post_save.connect(check_signal, sender=ApiKeyProfile, 52 | dispatch_uid='test_check_signal') 53 | k = profile.api_keys.all()[0] 54 | k.save() 55 | self.assertTrue(self.profile_signaled) 56 | 57 | def test_key_generation(self): 58 | k = generate_unique_api_key(self.user) 59 | ksize = KEY_SIZE 60 | my_kstr = hashlib.md5('%s-%s' % (self.user.email, 61 | k.created)).hexdigest()[:ksize] 62 | self.assertTrue(self.user.key_profile.can_make_api_key()) 63 | self.assertEquals(self.user.key_profile.available_keys(), 'Unlimited') 64 | self.assertEquals(len(my_kstr), len(k.key)) 65 | self.assertEquals(my_kstr, k.key) 66 | self.assertEquals(self.user.key_profile.api_keys.count(), 2) 67 | k.delete() 68 | self.assertEquals(self.user.key_profile.api_keys.count(), 1) 69 | current_max = self.user.key_profile.max_keys 70 | self.user.key_profile.max_keys = 2 71 | self.user.key_profile.save() 72 | self.assertEquals(self.user.key_profile.max_keys, 2) 73 | self.assertEquals(self.user.key_profile.available_keys(), 1) 74 | self.assertTrue(self.user.key_profile.can_make_api_key()) 75 | self.user.key_profile.max_keys = 1 76 | self.user.key_profile.save() 77 | self.assertEquals(self.user.key_profile.max_keys, 1) 78 | self.assertEquals(self.user.key_profile.available_keys(), 0) 79 | self.assertFalse(self.user.key_profile.can_make_api_key()) 80 | self.user.key_profile.max_keys = current_max 81 | self.user.key_profile.save() 82 | k = self.user.key_profile.api_keys.all()[0] 83 | self.assertEquals(self.user.key_profile.__unicode__(), 'ApiKeyProfile: 1, %s' % current_max) 84 | self.assertEquals(k.__unicode__(), 'ApiKey: %s' % k.key) 85 | 86 | def test_authentication(self): 87 | def do_test_authentication(auth_header): 88 | client = Client() 89 | def t_user_logged_in(*args, **kwargs): 90 | self.user_login_signalled = True 91 | def t_user_logged_out(*args, **kwargs): 92 | self.user_logout_signalled = True 93 | auth_header = 'HTTP_%s' % (auth_header.upper().replace('-', '_')) 94 | key = self.user.key_profile.api_keys.all()[0] 95 | extra = {auth_header: key.key} 96 | self.user_logout_signalled, self.user_login_signalled = False, False 97 | api_user_logged_in.connect(t_user_logged_in, dispatch_uid='t_user_logged_in') 98 | api_user_logged_out.connect(t_user_logged_out, dispatch_uid='t_user_logged_out') 99 | rv = client.get(reverse('test_key_list'), **extra) 100 | self.assertEquals(rv.status_code, 200) 101 | key.login('127.0.0.1') 102 | self.assertEquals(key.logged_ip, '127.0.0.1') 103 | self.assertTrue(self.user_login_signalled) 104 | 105 | rv = client.get(reverse('test_key_view', args=(key.pk,))) 106 | self.assertEquals(rv.status_code,401) 107 | rv = client.get(reverse('test_key_view', args=(key.pk,)), **extra) 108 | self.assertEquals(rv.status_code,200) 109 | rv = client.get(reverse('test_key_view', args=(key.pk+1,)), **extra) 110 | self.assertEquals(rv.status_code, 404) 111 | 112 | rv = client.get(reverse('test_key_list')) 113 | self.assertEquals(rv.status_code, 401) 114 | self.user.key_profile.api_keys.all()[0].logout() 115 | key.logout() 116 | self.assertTrue(self.user_logout_signalled) 117 | self.assertEquals(key.logged_ip, None) 118 | import key.settings 119 | original = getattr(settings, 'APIKEY_AUTHORIZATION_HEADER', 'X-Api-Authorization') 120 | setattr(settings, 'APIKEY_AUTHORIZATION_HEADER', 'X-Api-Authorization') 121 | key.settings.reload() 122 | self.assertEquals(key.settings.AUTH_HEADER, 'X-Api-Authorization') 123 | do_test_authentication('X-Api-Authorization') 124 | setattr(settings, 'APIKEY_AUTHORIZATION_HEADER', 'X-MyCoolApp-Key') 125 | key.settings.reload() 126 | self.assertEquals(key.settings.AUTH_HEADER, 'X-MyCoolApp-Key') 127 | do_test_authentication('X-MyCoolApp-Key') 128 | setattr(settings, 'APIKEY_AUTHORIZATION_HEADER', original) 129 | key.settings.reload() 130 | self.assertEquals(key.settings.AUTH_HEADER, original) 131 | 132 | def test_perm_check(self): 133 | client = Client() 134 | client.login(username="NonAuthorized", password="NonAuthorizedPassword") 135 | rv = client.get(reverse('key.create')) 136 | self.assertEquals(rv.status_code, 302) 137 | self.assertRaises(PermissionDenied, 138 | generate_unique_api_key, 139 | self.unauthorized_user) 140 | 141 | def test_views(self): 142 | client = Client() 143 | rv = client.get(reverse('key.list')) 144 | self.assertEquals(rv.status_code, 302) 145 | rv = client.get(reverse('key.create')) 146 | self.assertEquals(rv.status_code, 302) 147 | rv = client.get(reverse('key.delete',args=('ValueDoesntMatter',))) 148 | self.assertEquals(rv.status_code, 302) 149 | client = Client() 150 | self.assertTrue(client.login(username='ApiKeyTest', 151 | password='ApiKeyTestPassword')) 152 | u = User.objects.get(username='ApiKeyTest') 153 | self.assertTrue(u.check_password('ApiKeyTestPassword')) 154 | self.assertTrue(u.has_perm('key.can_make_api_key')) 155 | self.assertTrue(u.has_perm('key.has_api_key_profile')) 156 | rv = client.get(reverse('key.list')) 157 | self.assertEquals(rv.status_code, 200) 158 | original_list = list(self.user.key_profile.api_keys.all()) 159 | self.assertEquals(self.user.key_profile.api_keys.count(), 1) 160 | rv = client.post(reverse('key.create')) 161 | self.assertEquals(rv.status_code, 302) 162 | self.assertEquals(rv['Location'], 163 | 'http://testserver' + reverse('key.list')) 164 | rv = client.get(rv['Location']) 165 | self.assertEquals(rv.status_code, 200) 166 | self.assertEquals(self.user.key_profile.api_keys.count(), 2) 167 | k = self.user.key_profile.api_keys.latest('created') 168 | self.assertEquals(k, k) 169 | [self.assertNotEquals(k, x) for x in original_list] 170 | found = False 171 | self.assertTrue(k in self.user.key_profile.api_keys.all()) 172 | rv = client.post(reverse('key.delete',args=(k.key,)), {'action': 'Cancel'}) 173 | self.assertEquals(rv.status_code, 302) 174 | rv = client.post(reverse('key.delete',args=(k.key,))) 175 | self.assertTrue(k not in self.user.key_profile.api_keys.all()) 176 | self.assertEquals(rv.status_code, 302) 177 | self.assertEquals(rv['Location'], 178 | 'http://testserver' + reverse('key.list')) 179 | self.assertEquals(self.user.key_profile.api_keys.count(), 1) 180 | rv = client.get('/admin/key/') 181 | self.assertEquals(rv.status_code, 200) 182 | rv = client.get('/admin/key/apikey/1/') 183 | self.assertEquals(rv.status_code, 200) 184 | rv = client.get('/admin/key/apikeyprofile/1/') 185 | self.assertEquals(rv.status_code, 200) 186 | self.user.key_profile.api_keys.all().delete() 187 | 188 | class ApiKeyGroupTest(ApiKeyTest): 189 | def __init__(self, *args, **kwargs): 190 | super(ApiKeyGroupTest, self).__init__(USE_API_GROUP=True, *args, **kwargs) 191 | -------------------------------------------------------------------------------- /key/tests/test_urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from django.views.decorators.cache import cache_page 3 | from piston.resource import Resource 4 | from piston.handler import BaseHandler, AnonymousBaseHandler 5 | from piston.utils import * 6 | from key.auth import * 7 | import key.urls 8 | 9 | class KeyHandler(BaseHandler): 10 | allowed_methods = ('GET',) 11 | model = ApiKey 12 | 13 | def read(self, request): 14 | return ApiKey.objects.filter(profile=request.user.key_profile) 15 | 16 | 17 | class KeyViewHandler(BaseHandler): 18 | allowed_methods = ('GET',) 19 | model = ApiKey 20 | 21 | def read(self, request, key_pk): 22 | try: 23 | return ApiKey.objects.get(profile=request.user.key_profile, 24 | pk=key_pk) 25 | except self.model.DoesNotExist: 26 | return rc.NOT_FOUND 27 | 28 | auth = ApiKeyAuthentication() 29 | key_handler = Resource(handler=KeyHandler, 30 | authentication=auth) 31 | key_view_handler = Resource(handler=KeyViewHandler, 32 | authentication=auth) 33 | 34 | test_patterns = patterns('', 35 | url(r'^k/$', key_handler, name="test_key_list" ), 36 | url(r'^k/(?P\d+?)$', key_view_handler, name="test_key_view"), 37 | ) 38 | 39 | key.urls.urlpatterns = key.urls.urlpatterns + test_patterns 40 | -------------------------------------------------------------------------------- /key/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from key.views import * 3 | 4 | urlpatterns = patterns('key.views', 5 | url(r'^create_key/$', KeyCreateView.as_view(), 6 | name='key.create' ), 7 | url(r'^keys/$', KeyListView.as_view(), 8 | name='key.list' ), 9 | url(r'^delete_key/(?P.*)/$', 10 | KeyDeleteView.as_view(), 11 | name='key.delete' ), 12 | ) 13 | 14 | import sys 15 | if 'test' in sys.argv: 16 | from django.contrib import admin 17 | admin.autodiscover() 18 | urlpatterns += patterns('', url(r'^admin/', include(admin.site.urls))) 19 | -------------------------------------------------------------------------------- /key/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import HttpResponse, Http404, HttpResponseRedirect 3 | from django.template import RequestContext 4 | from django.shortcuts import render_to_response, get_object_or_404, redirect 5 | from django.contrib.auth.decorators import login_required, permission_required 6 | from django.views.generic import * 7 | from django.views.generic.base import TemplateResponseMixin, View 8 | from django.utils.decorators import method_decorator 9 | from django.views.decorators.http import condition, last_modified 10 | from django.views.decorators.cache import cache_control, cache_page 11 | from django.core.urlresolvers import reverse 12 | from django.core.cache import cache 13 | from key.models import ApiKey, generate_unique_api_key 14 | from key.forms import * 15 | from django.contrib.auth.models import User 16 | import hashlib 17 | import datetime 18 | import logging 19 | 20 | class ProtectedView(object): 21 | @method_decorator(permission_required('key.has_api_key_profile')) 22 | def dispatch(self, *args, **kwargs): 23 | return super(ProtectedView, self).dispatch(*args, **kwargs) 24 | 25 | def get_context_data(self, **kwargs): 26 | context = super(ProtectedView, self).get_context_data(**kwargs) 27 | context['request'] = self.request 28 | return context 29 | 30 | class KeyCreateView(ProtectedView, CreateView): 31 | def get_queryset(self): 32 | profile = self.request.user.key_profile 33 | return ApiKey.objects.filter(profile=profile) 34 | 35 | def get_success_url(self): 36 | return reverse('key.list') 37 | 38 | def post(self, request, *args, **kwargs): 39 | self.object = generate_unique_api_key(self.request.user) 40 | self.object.save() 41 | return HttpResponseRedirect(self.get_success_url()) 42 | 43 | 44 | class KeyListView(ProtectedView, ListView): 45 | def get_queryset(self): 46 | profile = self.request.user.key_profile 47 | return ApiKey.objects.filter(profile=profile) 48 | 49 | def get_context_data(self, **kwargs): 50 | context = super(KeyListView, self).get_context_data(**kwargs) 51 | user = self.request.user 52 | context['user'] = user 53 | context['can_make_api_key'] = user.key_profile.can_make_api_key() 54 | context['available_keys'] = user.key_profile.available_keys() 55 | return context 56 | 57 | class KeyDeleteView(ProtectedView, DeleteView): 58 | def get_success_url(self): 59 | return reverse('key.list') 60 | 61 | def get_object(self): 62 | return self.get_queryset().get(key=self.kwargs['key_code']) 63 | 64 | def get_queryset(self): 65 | profile = self.request.user.key_profile 66 | return profile.api_keys.all() 67 | 68 | def post(self, request, *args, **kwargs): 69 | if self.request.POST.get('action') == 'Cancel': 70 | return HttpResponseRedirect(self.get_success_url()) 71 | return super(KeyDeleteView, self).post(request, *args, **kwargs) 72 | 73 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.11.29 2 | django-piston 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup(name='django-key', 4 | version='0.5.2', 5 | description='Simple, straight forward API key for django-piston.', 6 | author='Steve Coursen', 7 | author_email='smcoursen@gmail.com', 8 | packages=find_packages(), 9 | license="BSD", 10 | url="https://github.com/scoursen/django-apikey", 11 | install_requires=['setuptools',], 12 | include_package_data=True, 13 | setup_requires=['setuptools_git',], 14 | ) 15 | --------------------------------------------------------------------------------