├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── codecov.yml ├── requirements ├── requirements-base.txt └── requirements-testing.txt ├── rest_framework_api_key ├── __init__.py ├── admin.py ├── helpers.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py └── permissions.py ├── runtests.py ├── setup.py ├── tests ├── __init__.py ├── settings.py ├── test_admin.py ├── test_models.py ├── test_views.py ├── urls.py └── views.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: false 4 | 5 | python: 6 | - "3.5" 7 | 8 | env: 9 | - TOX_ENV=py27-django18 10 | - TOX_ENV=py34-django18 11 | - TOX_ENV=py35-django18 12 | 13 | - TOX_ENV=py27-django19 14 | - TOX_ENV=py34-django19 15 | - TOX_ENV=py35-django19 16 | 17 | - TOX_ENV=py27-django110 18 | - TOX_ENV=py34-django110 19 | - TOX_ENV=py35-django110 20 | 21 | - TOX_ENV=py27-djangomaster 22 | - TOX_ENV=py34-djangomaster 23 | - TOX_ENV=py35-djangomaster 24 | 25 | cache: 26 | - pip 27 | 28 | install: 29 | - pip install tox 30 | - pip install codecov 31 | 32 | script: 33 | - tox -e $TOX_ENV 34 | 35 | after_success: 36 | - codecov 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Emmanouil Konstantinidis 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include MANIFEST.in 4 | 5 | global-exclude __pycache__ 6 | global-exclude *.py[co] 7 | 8 | prune docs 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-rest-framework-api-key [![travis][travis-image]][travis-url] [![codecov][codecov-image]][codecov-url] [![pypi][pypi-image]][pypi-url] 2 | Authenticate Web APIs made with Django REST Framework 3 | 4 | 5 | ### Supports 6 | 7 | - Python (2.7, 3.3, 3.4, 3.5) 8 | - Django (1.8, 1.9, 1.10) 9 | - Django Rest Framework (3+) 10 | 11 | 12 | ### Installation 13 | 14 | Install using pip: 15 | 16 | pip install drfapikey 17 | 18 | Add 'rest_framework_api_key' to your `INSTALLED_APPS` setting: 19 | 20 | INSTALLED_APPS = ( 21 | ... 22 | 'rest_framework_api_key', 23 | ) 24 | 25 | Finally set the django-rest-framework permissions under your django settings: 26 | 27 | REST_FRAMEWORK = { 28 | 'DEFAULT_PERMISSION_CLASSES': ( 29 | 'rest_framework_api_key.permissions.HasAPIAccess', 30 | ) 31 | } 32 | 33 | 34 | ### Example Request 35 | 36 | ```python 37 | response = requests.get( 38 | url="http://0.0.0.0:8000/api/login", 39 | headers={ 40 | "Api-Key": "fd8b4a98c8f53035aeab410258430e2d86079c93", 41 | }, 42 | ) 43 | ``` 44 | 45 | 46 | ### Tests 47 | 48 | pyvenv env 49 | source env/bin/activate 50 | pip install -r requirements/requirements-testing.txt 51 | python runtests.py 52 | 53 | 54 | ### Contributing 55 | 56 | 1. Fork it! 57 | 2. Create your feature branch: `git checkout -b my-new-feature` 58 | 3. Commit your changes: `git commit -am 'Add some feature'` 59 | 4. Push to the branch: `git push origin my-new-feature` 60 | 5. Submit a pull request 61 | 6. Make sure tests are passing 62 | 63 | 64 | [travis-image]: https://travis-ci.org/manosim/django-rest-framework-api-key.svg?branch=master 65 | [travis-url]: https://travis-ci.org/manosim/django-rest-framework-api-key 66 | 67 | [codecov-image]: https://codecov.io/github/manosim/django-rest-framework-api-key/coverage.svg?branch=master 68 | [codecov-url]:https://codecov.io/github/manosim/django-rest-framework-api-key?branch=master 69 | 70 | [pypi-image]: https://badge.fury.io/py/drfapikey.svg 71 | [pypi-url]: https://pypi.python.org/pypi/drfapikey/ 72 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: "70...100" 5 | 6 | status: 7 | project: false 8 | patch: false 9 | changes: false 10 | 11 | comment: off 12 | -------------------------------------------------------------------------------- /requirements/requirements-base.txt: -------------------------------------------------------------------------------- 1 | djangorestframework>=3.3 2 | -------------------------------------------------------------------------------- /requirements/requirements-testing.txt: -------------------------------------------------------------------------------- 1 | -r requirements-base.txt 2 | 3 | # PyTest for running the tests. 4 | pytest==3.0.0 5 | pytest-cov==2.3.1 6 | pytest-django==3.0.0 7 | 8 | flake8==3.0.4 9 | -------------------------------------------------------------------------------- /rest_framework_api_key/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.3" 2 | -------------------------------------------------------------------------------- /rest_framework_api_key/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib import messages 3 | from rest_framework_api_key.models import APIKey 4 | from rest_framework_api_key.helpers import generate_key 5 | 6 | 7 | class ApiKeyAdmin(admin.ModelAdmin): 8 | list_display = ('id', 'name', 'created', 'modified') 9 | 10 | fieldsets = ( 11 | ('Required Information', {'fields': ('name',)}), 12 | ('Additional Information', {'fields': ('key_message',)}), 13 | ) 14 | readonly_fields = ('key_message',) 15 | 16 | search_fields = ('id', 'name',) 17 | 18 | def has_delete_permission(self, request, obj=None): 19 | return False 20 | 21 | def key_message(self, obj): 22 | if obj.key: 23 | return "Hidden" 24 | return "The API Key will be generated once you click save." 25 | 26 | def save_model(self, request, obj, form, change): 27 | if not obj.key: 28 | obj.key = generate_key() 29 | messages.add_message(request, messages.WARNING, ('The API Key for %s is %s. Please note it since you will not be able to see it again.' % (obj.name, obj.key))) 30 | obj.save() 31 | 32 | admin.site.register(APIKey, ApiKeyAdmin) 33 | -------------------------------------------------------------------------------- /rest_framework_api_key/helpers.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import os 3 | 4 | 5 | def generate_key(): 6 | return binascii.hexlify(os.urandom(20)).decode() 7 | -------------------------------------------------------------------------------- /rest_framework_api_key/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | import uuid 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='APIKey', 16 | fields=[ 17 | ('id', models.UUIDField(editable=False, serialize=False, default=uuid.uuid4, primary_key=True)), 18 | ('created', models.DateTimeField(auto_now_add=True)), 19 | ('modified', models.DateTimeField(auto_now=True)), 20 | ('name', models.CharField(unique=True, max_length=50)), 21 | ('key', models.CharField(unique=True, max_length=40)), 22 | ], 23 | options={ 24 | 'ordering': ['-created'], 25 | 'verbose_name_plural': 'API Keys', 26 | }, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /rest_framework_api_key/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manosim/django-rest-framework-api-key/347ed51f2788fab2bd31699f54e05b1a6c1580ab/rest_framework_api_key/migrations/__init__.py -------------------------------------------------------------------------------- /rest_framework_api_key/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from django.db import models 3 | 4 | 5 | class APIKey(models.Model): 6 | 7 | class Meta: 8 | verbose_name_plural = "API Keys" 9 | ordering = ['-created'] 10 | 11 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 12 | created = models.DateTimeField(auto_now_add=True) 13 | modified = models.DateTimeField(auto_now=True) 14 | 15 | name = models.CharField(max_length=50, unique=True) 16 | key = models.CharField(max_length=40, unique=True) 17 | 18 | def __str__(self): 19 | return self.name 20 | -------------------------------------------------------------------------------- /rest_framework_api_key/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | from rest_framework_api_key.models import APIKey 3 | 4 | 5 | class HasAPIAccess(permissions.BasePermission): 6 | message = 'Invalid or missing API Key.' 7 | 8 | def has_permission(self, request, view): 9 | api_key = request.META.get('HTTP_API_KEY', '') 10 | return APIKey.objects.filter(key=api_key).exists() 11 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import subprocess 4 | import pytest 5 | 6 | 7 | FLAKE8_ARGS = ['rest_framework_api_key', 'tests/', '--ignore=E501'] 8 | PYTEST_ARGS = ['tests', '--cov=rest_framework_api_key', '--tb=short', '-rw'] 9 | 10 | 11 | def exit_on_failure(command, message=None): 12 | if command: 13 | sys.exit(command) 14 | 15 | 16 | def flake8_main(args): 17 | print('Running: flake8', FLAKE8_ARGS) 18 | command = subprocess.call(['flake8'] + args) 19 | print("" if command else "Success. flake8 passed.") 20 | return command 21 | 22 | 23 | def run_tests_coverage(): 24 | if __name__ == "__main__": 25 | pytest.main(PYTEST_ARGS) 26 | 27 | exit_on_failure(flake8_main(FLAKE8_ARGS)) 28 | exit_on_failure(run_tests_coverage()) 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name='drfapikey', 5 | version=__import__('rest_framework_api_key').__version__, 6 | author="Emmanouil Konstantinidis", 7 | author_email="hello@manos.im", 8 | packages=find_packages(), 9 | include_package_data=True, 10 | url="https://github.com/manosim/django-rest-framework-api-key", 11 | license='BSD', 12 | description="Authenticate Web APIs made with Django REST Framework", 13 | long_description=open("README.md").read(), 14 | install_requires=[], 15 | classifiers=[ 16 | "Development Status :: 4 - Beta", 17 | "Environment :: Web Environment", 18 | "Framework :: Django", 19 | "Framework :: Django :: 1.7", 20 | "Framework :: Django :: 1.8", 21 | "Framework :: Django :: 1.9", 22 | "License :: OSI Approved :: BSD License", 23 | "Operating System :: OS Independent", 24 | "Programming Language :: Python :: 2.7", 25 | "Programming Language :: Python :: 3.2", 26 | "Programming Language :: Python :: 3.3", 27 | "Programming Language :: Python :: 3.4", 28 | "Programming Language :: Python :: 3.5", 29 | "Topic :: Software Development :: Libraries :: Python Modules", 30 | "Framework :: Django", 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manosim/django-rest-framework-api-key/347ed51f2788fab2bd31699f54e05b1a6c1580ab/tests/__init__.py -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | 5 | SECRET_KEY = 'django-rest-framework-api-key' 6 | 7 | DATABASES = { 8 | 'default': { 9 | 'ENGINE': 'django.db.backends.sqlite3', 10 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 11 | } 12 | } 13 | 14 | INSTALLED_APPS = [ 15 | # Django Apps 16 | 'django.contrib.admin', 17 | 'django.contrib.auth', 18 | 'django.contrib.contenttypes', 19 | 'django.contrib.sessions', 20 | 'django.contrib.staticfiles', 21 | 22 | # External Packages 23 | "rest_framework", 24 | "rest_framework_api_key", 25 | 26 | # Test apps 27 | "tests" 28 | ] 29 | 30 | REST_FRAMEWORK = { 31 | 'DEFAULT_PERMISSION_CLASSES': ( 32 | 'rest_framework_api_key.permissions.HasAPIAccess', 33 | ) 34 | } 35 | 36 | MIDDLEWARE_CLASSES = ( 37 | 'django.contrib.sessions.middleware.SessionMiddleware', 38 | 'django.middleware.common.CommonMiddleware', 39 | 'django.middleware.csrf.CsrfViewMiddleware', 40 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 41 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 42 | 'django.contrib.messages.middleware.MessageMiddleware', 43 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 44 | 'django.middleware.security.SecurityMiddleware', 45 | ) 46 | 47 | ROOT_URLCONF = 'tests.urls' 48 | 49 | STATIC_URL = '/static/' 50 | 51 | TEMPLATES = [ 52 | { 53 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 54 | 'DIRS': [], 55 | 'APP_DIRS': True, 56 | 'OPTIONS': { 57 | 'context_processors': [ 58 | 'django.template.context_processors.debug', 59 | 'django.template.context_processors.request', 60 | 'django.contrib.auth.context_processors.auth', 61 | 'django.contrib.messages.context_processors.messages', 62 | ], 63 | }, 64 | }, 65 | ] 66 | -------------------------------------------------------------------------------- /tests/test_admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.core.urlresolvers import reverse 3 | from django.test import TestCase 4 | from rest_framework_api_key.models import APIKey 5 | from rest_framework_api_key.helpers import generate_key 6 | 7 | 8 | class LoggedInAdminTestCase(TestCase): 9 | 10 | USERNAME = "martymcfly" 11 | EMAIL = "marty@mcfly.com" 12 | PASSWORD = "password" 13 | 14 | def setUp(self): 15 | super(LoggedInAdminTestCase, self).setUp() 16 | 17 | self.user = User.objects.create_superuser(self.USERNAME, self.EMAIL, self.PASSWORD) 18 | self.assertTrue(self.user.is_active) 19 | self.assertTrue(self.user.is_superuser) 20 | self.client.login(username=self.USERNAME, password=self.PASSWORD) 21 | 22 | 23 | class APIAuthenticatedTestCase(TestCase): 24 | 25 | APP_NAME = 'Project Tests' 26 | 27 | def setUp(self): 28 | self.app_key = APIKey.objects.create(name=self.APP_NAME, key=generate_key()) 29 | self.header = {'HTTP_API_KEY': self.app_key.key} 30 | 31 | 32 | class AdminTestCase(LoggedInAdminTestCase, APIAuthenticatedTestCase): 33 | def setUp(self): 34 | super(AdminTestCase, self).setUp() 35 | self.add_app_url = reverse('admin:rest_framework_api_key_apikey_add') 36 | 37 | def test_admin_create_app_access(self): 38 | response = self.client.get(self.add_app_url) 39 | self.assertEqual(response.status_code, 200) 40 | 41 | def test_admin_create_app(self): 42 | self.assertEqual(APIKey.objects.all().count(), 1) 43 | 44 | response = self.client.post(self.add_app_url, data={"name": "Hello"}, follow=True) 45 | 46 | self.assertEqual(response.status_code, 200) 47 | self.assertContains(response, "Please note it since you will not be able to see it again.") 48 | self.assertContains(response, "was added successfully.") 49 | 50 | self.assertEqual(APIKey.objects.all().count(), 2) 51 | 52 | def test_admin_change_app(self): 53 | self.change_app_url = reverse('admin:rest_framework_api_key_apikey_change', args=(self.app_key.id,)) 54 | 55 | response = self.client.get(self.change_app_url,) 56 | 57 | self.assertEqual(response.status_code, 200) 58 | self.assertContains(response, "Hidden") 59 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from rest_framework_api_key.models import APIKey 3 | 4 | 5 | class ModelTestCase(TestCase): 6 | 7 | def setUp(self): 8 | super(ModelTestCase, self).setUp() 9 | 10 | def test_admin_create_object(self): 11 | self.assertEqual(APIKey.objects.all().count(), 0) 12 | APIKey.objects.create(name="Hello World", key="helloworld") 13 | self.assertEqual(APIKey.objects.all().count(), 1) 14 | -------------------------------------------------------------------------------- /tests/test_views.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from tests.test_admin import APIAuthenticatedTestCase 3 | 4 | 5 | class APICategoriesEndpoint(APIAuthenticatedTestCase): 6 | 7 | def test_get_view_authorized(self): 8 | 9 | response = self.client.get(reverse("test-view"), **self.header) 10 | 11 | self.assertEqual(response.status_code, 200) 12 | self.assertEqual(response.data["msg"], "Hello World!") 13 | 14 | def test_get_view_unauthorized(self): 15 | 16 | response = self.client.get(reverse("test-view")) 17 | 18 | self.assertEqual(response.status_code, 403) 19 | self.assertEqual(response.data["detail"], "Authentication credentials were not provided.") 20 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.contrib import admin 3 | from tests.views import TestView 4 | 5 | 6 | urlpatterns = [ 7 | 8 | url(r'^admin/', include(admin.site.urls)), 9 | url(r'^test/$', TestView.as_view(), name="test-view"), 10 | 11 | ] 12 | -------------------------------------------------------------------------------- /tests/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import status 2 | from rest_framework.response import Response 3 | from rest_framework.views import APIView 4 | 5 | 6 | class TestView(APIView): 7 | """ 8 | A dummy view used only for testing. 9 | """ 10 | 11 | def get(self, request): 12 | return Response({"msg": "Hello World!"}, status=status.HTTP_200_OK) 13 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE=tests.settings 3 | 4 | [tox] 5 | envlist = 6 | {py27,py34,py35}-django18, 7 | {py27,py34,py35}-django19, 8 | {py27,py34,py35}-django110, 9 | {py27,py34,py35}-django{master} 10 | 11 | [testenv] 12 | commands = python runtests.py 13 | setenv = 14 | PYTHONDONTWRITEBYTECODE=1 15 | PYTHONWARNINGS=once 16 | deps = 17 | django18: Django==1.8.14 18 | django19: Django==1.9.9 19 | django110: Django==1.10 20 | djangomaster: https://github.com/django/django/archive/master.tar.gz 21 | -rrequirements/requirements-testing.txt 22 | basepython = 23 | py35: python3.5 24 | py34: python3.4 25 | py27: python2.7 26 | --------------------------------------------------------------------------------