pax_global_header 0000666 0000000 0000000 00000000064 13643027634 0014522 g ustar 00root root 0000000 0000000 52 comment=1388d1cc92b4f8214f645c4e894bd2f350c71e68
djangorestframework-api-key-2.0.0/ 0000775 0000000 0000000 00000000000 13643027634 0017134 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/.github/ 0000775 0000000 0000000 00000000000 13643027634 0020474 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 13643027634 0022657 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/.github/ISSUE_TEMPLATE/bug_report.md 0000664 0000000 0000000 00000001264 13643027634 0025354 0 ustar 00root root 0000000 0000000 ---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Linux]
- Version: [e.g. 0.2.2]
- Python Version: [e.g. 3.7]
- Django Version: [e.g. 2.1]
- DRF Version: [e.g. 3.8.2]
**Additional context**
Add any other context about the problem here.
djangorestframework-api-key-2.0.0/.github/ISSUE_TEMPLATE/discussion.md 0000664 0000000 0000000 00000000214 13643027634 0025361 0 ustar 00root root 0000000 0000000 ---
name: Discussion/Other
about: Discuss something that is not a bug report or a feature request.
---
It's free form! Just start typing.
djangorestframework-api-key-2.0.0/.github/ISSUE_TEMPLATE/feature_request.md 0000664 0000000 0000000 00000001060 13643027634 0026401 0 ustar 00root root 0000000 0000000 ---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
djangorestframework-api-key-2.0.0/.gitignore 0000664 0000000 0000000 00000002312 13643027634 0021122 0 ustar 00root root 0000000 0000000 # Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
# databases
*.sqlite3
djangorestframework-api-key-2.0.0/CHANGELOG.md 0000664 0000000 0000000 00000011125 13643027634 0020745 0 ustar 00root root 0000000 0000000 # Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.0.0 - 2020-04-07
**NOTE**: this release drops compatibility with certain Python and Django versions, but contains no other breaking changes. See [Upgrade to 2.0](https://florimondmanca.github.io/djangorestframework-api-key/upgrade/2.0/) for detailed migration steps.
### Removed
- Dropped support for Django 2.0 and Django 2.1. (Pull #126)
- Dropped support for Python 3.5. (Pull #84)
### Added
- Add support for Django 3.0. (Pull #82)
- Add support for Python 3.8. (Pull #81)
- Add `BaseAPIKeyManager.get_from_key()` to allow retrieving API keys from views. (Pull #93)
- Add type annotations, and partial support for `django-stubs` and `djangorestframework-stubs`. (Pull #88, Pull #122)
## 1.4.1 - 2019-08-24
### Added
- Now ships with type annotations ([PEP 561](https://www.python.org/dev/peps/pep-0561/)). (Pull #73)
## 1.4.0 - 2019-07-16
**NOTE**: this release contains migrations. See [Upgrade to v1.4](https://florimondmanca.github.io/djangorestframework-api-key/upgrade/1.4/) for detailed instructions.
### Added
- The `prefix` and `hashed_key` are now stored in dedicated fields on the `APIKey` model. (Pull #62)
## 1.3.0 - 2019-06-28
**NOTE**: this release contains migrations. In your Django project, run them using:
```python
python manage.py migrate rest_framework_api_key
```
### Added
- Add abstract API key model (`AbstractAPIKey`) and base manager (`BaseAPIKeyManager`). (Pull #36)
- Add base permissions (`BaseHasAPIKey`). (Pull #46)
### Changed
- The `id` field of `APIKey` is now non-`editable`.
- `APIKeyModelAdmin` does not define `fieldsets` anymore. This allows subclasses to benefit from Django's automatic fieldsets. (Pull #52)
### Fixed
- Explicitly use `utf-8` encoding in `setup.py`, which could previously lead to issues when installing on certain systems. (Pull #58)
## 1.2.1 - 2019-06-03
### Fixed
- Fixed a critical bug in `APIKeyModelAdmin` that prevented `rest_framework_api_key` from passing Django system checks. (Pull #39)
## 1.2.0 - 2019-05-29
**NOTE**: this release contains migrations. In your Django project, run them using:
```python
python manage.py migrate rest_framework_api_key
```
### Added
- API keys can now have an optional `expiry_date`. (Pull #33) `HasAPIKey` denies access if the API key has expired, i.e. if `expiry_date`, if set, is in the past.
- It is now possible to search by `prefix` in the API key admin panel.
- The `prefix` is now displayed in the edit view of the API key admin panel.
## 1.1.0 - 2019-05-14
### Added
- Improve documentation on which password hasher is used.
- Add tests against the Argon2, BcryptSHA256 and PBKDF2SHA1 hashers. (Pull #32)
### Fixed
- Fix support for password hashers that generate hashes that contain dots. (Pull #31)
## 1.0.0 - 2019-04-24
**This release is incompatible with 0.x**. See [Upgrade to 1.0](https://florimondmanca.github.io/djangorestframework-api-key/upgrade/1.4/) for migration steps.
### Removed
- Remove `HasAPIKeyOrIsAuthenticated` permission class. You should use bitwise composition now, e.g. `HasAPIKey | IsAuthenticated`.
- Drop the `DRF_API_KEY_*` settings. (Pull #19)
### Changed
- Switch to a new API key generation and validation scheme. Clients must now authorize using a single API key header (Pull #19). The header is `Authorization` by default. It can be customized using the `API_KEY_CUSTOM_HEADER` setting (Pull #26). Use the `name` field to identify clients.
### Added
- Add support for Django 2.2. (Pull #27)
- Add programmatic API key creation using `APIKey.objects.create_key()`. (Pull #19)
### Fixed
- Improved API key storage using Django's password hashing helpers. (Uses the default Django password hasher.) (Pull #19)
## 0.4.0 - 2019-04-21
### Removed
- Drop support for Python 3.4. Only 3.5, 3.6 and 3.7 are supported now.
- Drop support for Django < 2.0. Only 2.0 and 2.1 are supported now.
### Fixed
- `HasAPIKey` now implements `.has_object_permissions()`, which allows to compose it with other permission classes and perform object-level permission checks. (Pull #25)
## 0.3.1 - 2018-11-17
_Initial changelog entry._
### Added
- `APIKey` model.
- `HasAPIKey` and `HasAPIKeyOrIsAuthenticated` permission classes.
- Generate, view and revoke API keys from the Django admin.
- Authenticate requests using the `Api-Token` and `Api-Secret-Key` headers. Customizable via the `DRF_API_KEY_TOKEN_HEADER` and `DRF_API_KEY_SECRET_KEY_HEADER` settings.
djangorestframework-api-key-2.0.0/CONTRIBUTING.md 0000664 0000000 0000000 00000003560 13643027634 0021371 0 ustar 00root root 0000000 0000000 # Contributing
Thanks for your interest in contributing to this project!
Here are a few ways in which you can help:
- Discovered a bug? Please open a [bug report](https://github.com/florimondmanca/djangorestframework-api-key/issues/new?template=bug_report.md).
- Have a feature you'd like to see implemented? Please open a [Feature Request](https://github.com/florimondmanca/djangorestframework-api-key/issues/new?template=feature_request.md).
- For any other contribution, please open a [discussion](https://github.com/florimondmanca/djangorestframework-api-key/issues/new?template=discussion.md).
**NOTE**: for **non-trivial changes** we _highly_ encourage you to **open an issue** first. This will allow maintainers and contributors to confirm that the problem you are trying to solve is well-posed, in the scope of the project, and/or can't be solved with existing features.
### Installation
1. Fork the repository.
1. Clone it on your machine.
1. Install dependencies:
```bash
scripts/install
```
### Tests
Run the tests using:
```bash
scripts/test
```
### Code style
Run code auto-formatting with:
```bash
scripts/lint
```
Run code style checks using:
```bash
scripts/check
```
### Generating migrations
This package includes migrations. To update them in case of changes without setting up a Django project, run:
```bash
scripts/makemigrations
```
### Documentation
Serve the docs site locally (with hot-reload) using:
```bash
scripts/serve
```
Build the documentation using:
```bash
scripts/docs
```
## Notes to maintainers
### Releasing
- Create a PR with the following:
- Bump the package version by editing `__version__.py`.
- Update the changelog with any relevant PRs merged since the last version: bug fixes, new features, changes, deprecations, removals.
- Merge the PR.
- Run `$ scripts/publish` on `master`.
- Tag the commit and push the tag to the remote.
djangorestframework-api-key-2.0.0/LICENSE 0000664 0000000 0000000 00000002060 13643027634 0020137 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2018 Florimond Manca
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
djangorestframework-api-key-2.0.0/MANIFEST.in 0000664 0000000 0000000 00000000101 13643027634 0020662 0 ustar 00root root 0000000 0000000 graft src
include README.md
include CHANGELOG.md
include LICENSE
djangorestframework-api-key-2.0.0/README.md 0000664 0000000 0000000 00000010051 13643027634 0020410 0 ustar 00root root 0000000 0000000 # Django REST Framework API Key
API key permissions for the [Django REST Framework](https://www.django-rest-framework.org).
## Introduction
**Django REST Framework API Key is a library for allowing server-side clients to safely use your API.** These clients are typically third-party backends and services (i.e. _machines_) which do not have a user account but still need to interact with your API in a secure way.
### Features
- βοΈ **Simple to use**: create, view and revoke API keys via the admin site, or use built-in helpers to create API keys programmatically.
- π **As secure as possible**: API keys are treated with the same level of care than user passwords. They are hashed using the default password hasher before being stored in the database, and only visible at creation.
- π¨ **Customizable**: satisfy specific business requirements by building your own customized API key models, permission classes and admin panels.
### Should I use API keys?
There are important security aspects you need to consider before switching to an API key access control scheme. We've listed some of these in [Security caveats](security.md#caveats), including serving your API over HTTPS.
Besides, see [Why and when to use API keys](https://cloud.google.com/endpoints/docs/openapi/when-why-api-key#top_of_page) for hints on whether API keys can fit your use case.
API keys are ideal in the following situations:
- Blocking anonymous traffic.
- Implementing API key-based [throttling](https://www.django-rest-framework.org/api-guide/throttling/). (Note that Django REST Framework already has may built-in utilities for this use case.)
- Identifying usage patterns by logging request information along with the API key.
They can also present enough security for authorizing internal services, such as your API server and an internal frontend application.
> Please note that this package is NOT meant for authentication. You should NOT use this package to identify individual users, either directly or indirectly.
>
> If you need server-to-server authentication, you may want to consider OAuth instead. Libraries such as [django-oauth-toolkit](https://django-oauth-toolkit.readthedocs.io/en/latest/index.html) can help.
## Quickstart
Install with `pip`:
```bash
pip install "djangorestframework-api-key==2.*"
```
_**Note**: It is highly recommended to **pin your dependency** to the latest major version (as depicted above), as breaking changes may and will happen between major releases._
Add the app to your `INSTALLED_APPS`:
```python
# settings.py
INSTALLED_APPS = [
# ...
"rest_framework",
"rest_framework_api_key",
]
```
Run the included migrations:
```bash
python manage.py migrate
```
To learn how to configure permissions and manage API keys, head to the [Documentation](https://florimondmanca.github.io/djangorestframework-api-key).
## Changelog
See [CHANGELOG.md](https://github.com/florimondmanca/djangorestframework-api-key/tree/master/CHANGELOG.md).
## Contributing
See [CONTRIBUTING.md](https://github.com/florimondmanca/djangorestframework-api-key/tree/master/CONTRIBUTING.md).
## License
MIT
djangorestframework-api-key-2.0.0/ci/ 0000775 0000000 0000000 00000000000 13643027634 0017527 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/ci/azure-pipelines.yml 0000664 0000000 0000000 00000004116 13643027634 0023370 0 ustar 00root root 0000000 0000000 trigger:
- master
pr:
- master
resources:
containers:
- container: pg11
image: postgres:11
ports:
- 5432:5432
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: default
variables:
CI: true
PIP_CACHE_DIR: $(Pipeline.Workspace)/.cache/pip
jobs:
- job: Check
steps:
- template: templates/install.yml
parameters:
pythonVersion: "3.8"
- bash: scripts/check
displayName: "Run checks"
- job: Docs
steps:
- template: templates/install.yml
parameters:
pythonVersion: "3.8"
- bash: scripts/docs build
displayName: "Build docs"
- job: Linux
strategy:
matrix:
py3.6-dj2.2:
pythonVersion: "3.6"
djangoVersion: "2.2.*"
py3.7-dj2.2:
pythonVersion: "3.7"
djangoVersion: "2.2.*"
py3.8-dj3.0:
pythonVersion: "3.8"
djangoVersion: "3.0.*"
uploadCoverage: true
steps:
- template: templates/install.yml
parameters:
pythonVersion: $(pythonVersion)
djangoVersion: $(djangoVersion)
- bash: scripts/test
displayName: "Run tests"
- bash: |
if [ -f .coverage ]; then
python -m pip install codecov;
codecov --required;
fi
condition: eq(variables['uploadCoverage'], true)
env:
CODECOV_TOKEN: $(codecovToken)
displayName: "Upload coverage"
- job: Windows
pool:
vmImage: "vs2017-win2016"
steps:
- template: templates/install.yml
parameters:
pythonVersion: "3.7"
djangoVersion: "3.0.*"
- bash: scripts/test
displayName: "Run tests"
- job: Postgres
services:
postgres: pg11
steps:
- template: templates/install.yml
parameters:
pythonVersion: "3.7"
djangoVersion: "3.0.*"
- bash: scripts/test
env:
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/default"
displayName: "Run tests"
djangorestframework-api-key-2.0.0/ci/templates/ 0000775 0000000 0000000 00000000000 13643027634 0021525 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/ci/templates/install.yml 0000664 0000000 0000000 00000001151 13643027634 0023714 0 ustar 00root root 0000000 0000000 parameters:
- name: pythonVersion
type: string
- name: djangoVersion
type: string
default: ""
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: "${{ parameters.pythonVersion }}"
displayName: "Use Python ${{ parameters.pythonVersion }}"
- task: Cache@2
inputs:
key: "pip | $(Agent.OS) | requirements.txt"
restoreKeys: |
pip | $(Agent.OS)
pip
path: $(PIP_CACHE_DIR)
- bash: scripts/install
env:
${{ if parameters.djangoVersion }}:
DJANGO_VERSION: ${{ parameters.djangoVersion }}
displayName: "Install dependencies"
djangorestframework-api-key-2.0.0/docs/ 0000775 0000000 0000000 00000000000 13643027634 0020064 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/docs/guide.md 0000664 0000000 0000000 00000033633 13643027634 0021513 0 ustar 00root root 0000000 0000000 # User Guide
## Getting started
### Installation
Install from PyPI:
```bash
pip install djangorestframework-api-key
```
**Note**: this package requires Python 3.6+, Django 2.0+ and Django REST Framework 3.8+.
### Project setup
Add the app to your `INSTALLED_APPS`:
```py
# settings.py
INSTALLED_APPS = [
# ...
"rest_framework",
"rest_framework_api_key",
]
```
Run the included migrations:
```bash
python manage.py migrate
```
### Setting permissions
The `HasAPIKey` permission class protects a view behind API key authorization.
You can set the permission globally:
```python
# settings.py
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework_api_key.permissions.HasAPIKey",
]
}
```
or on a per-view basis:
```python
# views.py
from rest_framework.views import APIView
from rest_framework_api_key.permissions import HasAPIKey
class UserListView(APIView):
permission_classes = [HasAPIKey]
# ...
```
See also [Setting the permission policy](http://www.django-rest-framework.org/api-guide/permissions/#setting-the-permission-policy) for more information on using permission classes in the Django REST Framework.
!!! tip
You can use the bitwise operators `|` and `&` to compose `HasAPIKey` with other permission classes and achieve more complex authorization behaviour.
For example, to require a valid API key _or_ authentication credentials, use:
```python
from rest_framework.permissions import IsAuthenticated
from rest_framework_api_key.permissions import HasAPIKey
# ...
permission_classes = [HasAPIKey | IsAuthenticated]
```
### Making authorized requests
#### Authorization header
By default, clients must pass their API key via the `Authorization` header. It must be formatted as follows:
```
Authorization: Api-Key ********
```
where `********` refers to the generated API key.
To know under which conditions access is granted, please see [Grant scheme](security.md#grant-scheme).
#### Custom header
You can set the `API_KEY_CUSTOM_HEADER` setting to a non-`None` value to require clients to pass their API key in a custom header instead of the `Authorization` header.
This is useful if you plan to use API keys _AND_ an authentication scheme which already uses the `Authorization` header (e.g. token-based authentication).
For example, if you set:
```python
# settings.py
API_KEY_CUSTOM_HEADER = "HTTP_X_API_KEY"
```
then clients must make authorized requests using:
```
X-Api-Key: ********
```
where `********` refers to the generated API key.
Please refer to [HttpRequest.META](https://docs.djangoproject.com/en/2.2/ref/request-response/#django.http.HttpRequest.META) for more information on headers in Django.
### Creating and managing API keys
#### Admin site
When it is installed, `djangorestframework-api-key` adds an "API Key Permissions" section to the Django admin site where you can create, view and revoke API keys.
#### Programmatic usage
API keys can be created, viewed and revoked programmatically by manipulating the `APIKey` model.
!!! note
The examples below use the [Django shell](https://docs.djangoproject.com/en/2.2/ref/django-admin/#django-admin-shell).
- You can view and query `APIKey` like any other model. For example, to know the total number of API keys:
```python
>>> from rest_framework_api_key.models import APIKey
>>> APIKey.objects.count()
42
```
- If you wish to create an API key programmatically, you'll most likely want a one-time access to its generated key too. To do so, use the `.create_key()` method on the `APIKey` objects manager instead of `.create()`:
```python
>>> from rest_framework_api_key.models import APIKey
>>> api_key, key = APIKey.objects.create_key(name="my-remote-service")
>>> # Proceed with `api_key` and `key`...
```
!!! danger
To prevent leaking API keys, you must only give the `key` **to the client that triggered its generation**. In particular, **do not keep any trace of it on the server**.
- To retrieve an `APIKey` instance based on its generated key (which is not stored in the database) use the `.get_from_key()` method on the `APIKey` objects manager instead of `.get()`. This is useful if you'd like to access an `APIKey` object from a view protected by a `HasAPIKey` permission.
```python
from rest_framework.views import APIView
from rest_framework_api_key.models import APIKey
from rest_framework_api_key.permissions import HasAPIKey
from .models import Project
class ProjectListView(APIView):
permission_classes = [HasAPIKey]
def get(self, request):
"""Retrieve a project based on the request API key."""
key = request.META["HTTP_AUTHORIZATION"]
api_key = APIKey.objects.get_from_key(key)
project = Project.objects.get(api_key=api_key)
```
## Customization
This package provides various customization APIs that allow you to extend its basic behavior.
### API key models
If the built-in `APIKey` model doesn't fit your needs, you can create your own by subclassing `AbstractAPIKey`. This is particularly useful if you need to **store extra information** or **link API keys to other models** using a `ForeignKey` or a `ManyToManyField`.
!!! warning
Associating API keys to users, directly or indirectly, can present a security risk. See also: [Should I use API keys?](/#should-i-use-api-keys).
#### Example
Here's how you could link API keys to an imaginary `Organization` model:
```python
# organizations/models.py
from django.db import models
from rest_framework_api_key.models import AbstractAPIKey
class Organization(models.Model):
name = models.CharField(max_length=128)
active = models.BooleanField(default=True)
class OrganizationAPIKey(AbstractAPIKey):
organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name="api_keys",
)
```
If you need to customize the model's `Meta`, it should inherit from `AbstractAPIKey.Meta`:
```python
class OrganizationAPIKey(AbstractAPIKey):
# ...
class Meta(AbstractAPIKey.Meta):
verbose_name = "Organization API key"
verbose_name_plural = "Organization API keys"
```
#### Migrations
Because `AbstractAPIKey` is an [abstract model](https://docs.djangoproject.com/en/2.2/topics/db/models/#abstract-base-classes), the custom API key model will have its own table in the database.
This means that you need to **generate a migration** and then **apply it** to be able to query the new API key model:
```bash
python manage.py makemigrations
python manage.py migrate
```
!!! important
If `AbstractAPIKey` changes (e.g. because of an update to Django REST Framework API Key), you will need to **generate and apply migrations again** to account for these changes.
#### Managers
The `APIKey` model as well as custom API keys models inherited from `AbstractAPIKey` have a dedicated [manager](https://docs.djangoproject.com/en/2.2/topics/db/managers) which is responsible for implementing `.create_key()` and other important behavior.
As a result, if you want to build a custom API key manager, it should inherit from `BaseAPIKeyManager` instead of Django's `Manager`.
Besides [customization APIs that come with Django's managers](https://docs.djangoproject.com/en/2.2/topics/db/managers/#custom-managers), `BaseAPIKeyManager` gives you one extra hook: you can override `.get_usable_keys()` to customize which set of API keys clients can use in authorized requests.
For example, here's how to restrict usable keys to those of active organizations only:
```python
class OrganizationAPIKeyManager(BaseAPIKeyManager):
def get_usable_keys(self):
return super().get_usable_keys().filter(organization__active=True)
```
!!! check
Note the call to the parent implementation using `super()` here. This is because `.get_usable_keys()` has some default behavior, including making sure that revoked API keys cannot be used.
!!! tip
You don't need to use a custom model to use a custom manager β it can be used on the built-in `APIKey` model as well.
#### Admin panel
If you'd like to view and manage your custom API key model via the [Django admin site](https://docs.djangoproject.com/en/2.2/ref/contrib/admin/), you can create and register a subclass of `APIKeyModelAdmin`:
```python
# organizations/admin.py
from django.contrib import admin
from rest_framework_api_key.admin import APIKeyModelAdmin
from .models import OrganizationAPIKey
@admin.register(OrganizationAPIKey)
class OrganizationAPIKeyModelAdmin(APIKeyModelAdmin):
pass
```
You can also customize any of the default attributes defined in `APIKeyModelAdmin`. For example, to display the organization's name in the list view, and allow searching `OrganizationAPIKey` instances by organization name while keeping the original search behavior, you can write:
```python
list_display = [*APIKeyModelAdmin.list_display, "organization__name"]
search_fields = [*APIKeyModelAdmin.search_fields, "organization__name"]
```
!!! question "How can I display API keys on the detail page of a related model instance?"
In theory, this could be done using Django's [`InlineModelAdmin`](https://docs.djangoproject.com/en/2.2/ref/contrib/admin/#inlinemodeladmin-objects).
However, due to the limitations of inlines, this cannot be easily achieved while correctly saving and displaying the generated key in the detail page of the related model.
As an alternative, you can use the `.list_filter` class attribute to filter API keys by an identifying field on the related model. In the examples above, you could use `organization__name` to filter API keys by organization.
### Permission classes
The built-in `HasAPIKey` permission class only checks against the built-in `APIKey` model. This means that if you use a custom API key model, you need to create a **custom permission class** for your application to validate API keys against it.
You can do so by subclassing `BaseHasAPIKey` and specifying the `.model` class attribute:
```python
# organizations/permissions.py
from rest_framework_api_key.permissions import BaseHasAPIKey
from .models import OrganizationAPIKey
class HasOrganizationAPIKey(BaseHasAPIKey):
model = OrganizationAPIKey
```
You can then use `HasOrganizationAPIKey` as described in [Setting permissions](#setting-permissions).
!!! tip
If you need to customize `.has_permission()` or `.has_object_permission()`, feel free to read the [source code](https://github.com/florimondmanca/djangorestframework-api-key/blob/master/rest_framework_api_key/permissions.py).
#### API key parsing
By default, API key permission classes retrieve the API key from the `Authorization` header or a custom header, as described in [Making authorized requests](#making-authorized-requests).
You can override this behavior by redefining the `.get_key()` method on your custom permission class. It accepts the [HttpRequest](https://docs.djangoproject.com/en/2.2/ref/request-response/#httprequest-objects) object as unique argument and should return the API key as an `str` if one was found, or `None` otherwise.
For example, here's how you could retrieve the API key from a cookie:
```python
class HasOrganizationAPIKey(BaseHasAPIKey):
# ...
def get_key(self, request):
return request.COOKIES.get("api_key")
```
If your custom key parsing algorithm is complex, you may want to define it as a separate component. To do so, build a class which implements the `.get()` method with the same signature as `.get_key()`, and set it as the `.key_parser`:
```python
class CookieKeyParser:
def get(self, request):
cookie_name = getattr(settings, "API_KEY_COOKIE_NAME", "api_key")
return request.COOKIES.get(cookie_name)
class HasOrganizationAPIKey(BaseHasAPIKey):
# ...
key_parser = CookieKeyParser()
```
### Key generation
!!! warning
**This is an advanced topic**. Customizing the key generation algorithm must be done with care to prevent security issues.
If you proceed, it is best to customize key generation **with a clean database state**, that is **before running initial migrations**, and more importantly **before any API key is created**.
This package ships with a key generation algorithm based on Django's password hashing infrastructure (see also [Security](security.md)).
The `.key_generator` attribute on `BaseAPIKeyManager` allows you to customize key generation.
For example, you can customize the length of the prefix and secret key using:
```python
from rest_framework_api_key.models import BaseAPIKeyManager
from rest_framework_api_key.crypto import KeyGenerator
class OrganizationAPIKeyManager(BaseAPIKeyManager):
key_generator = KeyGenerator(prefix_length=8, secret_key_length=32) # Default values
class OrganizationAPIKey(AbstractAPIKey):
objects = OrganizationAPIKeyManager()
# ...
```
If you want to replace the key generation algorithm entirely, you can create your own `KeyGenerator` class. It must implement the `.generate()` and `.verify()` methods. At this point, it's probably best to read the [source code](https://github.com/florimondmanca/djangorestframework-api-key/blob/master/rest_framework_api_key/crypto.py) for the built-in `KeyGenerator`.
!!! check
If the signature of your `.generate()` method is different from the built-in one, you'll need to override `.assign_key()` in your custom API key manager as well.
Likewise, if `.verify()` must accept anything else than the `key` and `hashed_key`, you'll need to override `.is_valid()` on your custom API key model.
See [models.py](https://github.com/florimondmanca/djangorestframework-api-key/blob/master/rest_framework_api_key/models.py) for the source code of `BaseAPIKeyManager`.
## Typing support
This package provides type information starting with version 2.0, making it suitable for usage with type checkers such as `mypy`.
For the best experience, you may want to install packages such as [`django-stubs`](https://github.com/typeddjango/django-stubs) and [`djangorestframework-stubs`](https://github.com/typeddjango/djangorestframework-stubs). Note however that a seamless integration with these packages is not guaranteed yet.
djangorestframework-api-key-2.0.0/docs/index.md 0000664 0000000 0000000 00000010052 13643027634 0021513 0 ustar 00root root 0000000 0000000
## Introduction
**Django REST Framework API Key is a powerful library for allowing server-side clients to safely use your API.** These clients are typically third-party backends and services (i.e. _machines_) which do not have a user account but still need to interact with your API in a secure way.
### Features
- βοΈ **Simple to use**: create, view and revoke API keys via the admin site, or use built-in helpers to create API keys programmatically.
- π **As secure as possible**: API keys are treated with the same level of care than user passwords. They are hashed using the default password hasher before being stored in the database, and only visible at creation.
- π¨ **Customizable**: satisfy specific business requirements by building your own customized API key models, permission classes and admin panels.
### Should I use API keys?
There are important security aspects you need to consider before switching to an API key access control scheme. We've listed some of these in [Security caveats](security.md#caveats), including serving your API over HTTPS.
Besides, see [Why and when to use API keys](https://cloud.google.com/endpoints/docs/openapi/when-why-api-key#top_of_page) for hints on whether API keys can fit your use case.
API keys are ideal in the following situations:
- Blocking anonymous traffic.
- Implementing API key-based [throttling](https://www.django-rest-framework.org/api-guide/throttling/). (Note that Django REST Framework already has may built-in utilities for this use case.)
- Identifying usage patterns by logging request information along with the API key.
They can also present enough security for authorizing internal services, such as your API server and an internal frontend application.
!!! warning
Please note that this package is NOT meant for authentication. You should NOT use this package to identify individual users, either directly or indirectly.
If you need server-to-server authentication, you may want to consider OAuth instead. Libraries such as [django-oauth-toolkit](https://django-oauth-toolkit.readthedocs.io/en/latest/index.html) can help.
## Quickstart
Install with `pip`:
```bash
pip install "djangorestframework-api-key==2.*"
```
!!! important
It is highly recommended to **pin your dependency** to the latest major version (as depicted above), as breaking changes may and will happen between major releases.
Add the app to your `INSTALLED_APPS`:
```python
# settings.py
INSTALLED_APPS = [
# ...
"rest_framework",
"rest_framework_api_key",
]
```
Run the included migrations:
```bash
python manage.py migrate
```
To learn how to configure permissions and manage API keys, head to the [User Guide](guide.md).
djangorestframework-api-key-2.0.0/docs/security.md 0000664 0000000 0000000 00000004666 13643027634 0022271 0 ustar 00root root 0000000 0000000 # Security
## Implementation details
### Key generation scheme
An API key is composed of two items:
- A prefix `P`, which is a generated string of 8 characters.
- A secret key `SK`, which is a generated string of 32 characters.
The generated key that clients use to [make authorized requests](#making-authorized-requests) is `GK = P.SK`. It is treated with the same level of care than passwords:
- Only a hashed version is stored in the database. The hash is computed using the default password hasher. [^1]
- The generated key is shown only once to the client upon API key creation.
[^1]: All hashers provided by Django should be supported. This package is tested against the [default list of `PASSWORD_HASHERS`](https://docs.djangoproject.com/en/2.2/ref/settings/#std:setting-PASSWORD_HASHERS). See also [How Django stores passwords](https://docs.djangoproject.com/en/2.2/topics/auth/passwords/#how-django-stores-passwords) for more information.
### Grant scheme
Access is granted if and only if all of the following is true:
1. The configured API key header is present and correctly formatted. [^3]
2. A usable API key with the prefix of the given key exists in the database. [^4]
3. The hash of the given key matches that of the API key.
[^3]: To customize this behavior, see [API key parsing](guide.md#api-key-parsing).
[^4]: Only unrevoked keys are usable by default, but this can be customized with a [custom manager](guide.md#managers).
## Caveats
[API keys β Security](https://nordicapis.com/why-api-keys-are-not-enough/): depending on your situation, you should probably not use API keys only to authorize your clients.
Besides, you do NOT recommend using this package for authentication, i.e. retrieving user information from API keys.
Inded, **using API keys shifts the responsability of Information Security on your clients**. This induces risks, especially if detaining an API key gives access to confidential information or write operations. For example, an attacker could impersonate clients if they let their API keys leak.
As a best practice, you should apply the _Principle of Least Privilege_: allow only those who require resources to access those specific resources. In other words: **if your client needs to access an endpoint, add API permissions on that endpoint only** instead of the whole API.
Besides, it is highly recommended to serve the API over **HTTPS** to ensure the confidentiality of API keys passed in requests.
Act responsibly!
djangorestframework-api-key-2.0.0/docs/upgrade/ 0000775 0000000 0000000 00000000000 13643027634 0021513 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/docs/upgrade/1.0.md 0000664 0000000 0000000 00000002221 13643027634 0022330 0 ustar 00root root 0000000 0000000 # Upgrading to 1.0
## Introduction
**The 1.0 release is incompatible with 0.x**. It introduces a new and more transparent API key generation and validation scheme which allows to pass it in a single header, instead of two previously.
This document lists the steps necessary to upgrade from 0.x to 1.0.
## Steps
### 1. Backup existing API keys
Unfortunately, it is not possible to provide a migration that would preserve existing API keys. This is because the cryptographic generation and validation methods have changed fundamentally, which means that new keys cannot be inferred from existing ones.
As a result, **all existing API keys will be destroyed** during Step 1/ described below. You may want to backup the existing data so that you can regenerate keys notify clients once migration is complete.
### 2. Reset migrations
**This step will destroy existing API keys**. It is necessary for the new migrations to run correctly.
```bash
python manage.py migrate rest_framework_api_key zero
```
### 3. Upgrade
```bash
pip install "djangorestframework-api-key==1.0.*"
```
### 4. Run migrations
```bash
python manage.py migrate rest_framework_api_key
```
djangorestframework-api-key-2.0.0/docs/upgrade/1.4.md 0000664 0000000 0000000 00000002004 13643027634 0022333 0 ustar 00root root 0000000 0000000 # Upgrading to 1.4
## Introduction
The 1.4 release includes a migration that adds and populates the `prefix` and `hashed_key` fields to API keys.
This document lists the steps necessary to upgrade from 1.3.x to 1.4.
## Steps
### 1. Migrate the built-in API key model
The `APIKey` model can be migrated using the migration shipped with this package:
```bash
python manage.py migrate rest_framework_api_key
```
### 2. Migrate custom API key models (if applicable)
If you have a custom API key model deriving from `AbstractAPIKey`, you need to **manually add the migration** to your application.
- Copy the migration script below to your app's `migrations/` directory. Be sure to modify `APP_NAME`, `MODEL_NAME` and `DEPENDENCIES` as seems fit. You can name the migration script `xxxx_prefix_hashed_key.py` (replace `xxxx` with the next available migration ID).
```python
--8<-- "src/rest_framework_api_key/migrations/0004_prefix_hashed_key.py"
```
- Apply the migration:
```bash
python manage.py migrate
```
djangorestframework-api-key-2.0.0/docs/upgrade/2.0.md 0000664 0000000 0000000 00000001531 13643027634 0022334 0 ustar 00root root 0000000 0000000 # Upgrading to 2.0
## Introduction
This document lists the steps necessary to upgrade from 1.4.x to 2.0.
The full list of changes can be found in the [Changelog](https://github.com/florimondmanca/djangorestframework-api-key/blob/master/CHANGELOG.md).
## Steps
### 1. Upgrade your Python to 3.6+
The 2.0 release dropped support for Python 3.5.
Before upgrading, make sure you are running on Python 3.6 or above.
### 2. Upgrade your Django to 2.2+
To 2.0 release dropped official support for Django 2.0 and Django 2.1, which have both reached EOL (see [Django: Supported Versions](https://www.djangoproject.com/download/#supported-versions)). Django 2.2 is still supported, and support for Django 3.0 was added.
While we there are no internal changes due to this change, you should make sure you are running on Django 2.2 or above before upgrading.
djangorestframework-api-key-2.0.0/mkdocs.yml 0000664 0000000 0000000 00000002021 13643027634 0021132 0 ustar 00root root 0000000 0000000 site_name: Django REST Framework API Key
site_url: https://florimondmanca.github.io/djangorestframework-api-key
copyright: Β© 2018-present Florimond Manca
theme:
name: "material"
palette:
primary: "red"
accent: "red"
repo_name: florimondmanca/djangorestframework-api-key
repo_url: https://github.com/florimondmanca/djangorestframework-api-key
nav:
- Home: index.md
- User Guide: guide.md
- Security: security.md
- Upgrade Guides:
- "2.0": upgrade/2.0.md
- "1.4": upgrade/1.4.md
- "1.0": upgrade/1.0.md
- Contributing: https://github.com/florimondmanca/djangorestframework-api-key/tree/master/CONTRIBUTING.md
- Changelog: https://github.com/florimondmanca/djangorestframework-api-key/tree/master/CHANGELOG.md
- License: https://github.com/florimondmanca/djangorestframework-api-key/tree/master/LICENSE
markdown_extensions:
- admonition
- footnotes
- pymdownx.superfences
- pymdownx.snippets:
check_paths: true
- codehilite:
guess_lang: false
- toc:
permalink: true
djangorestframework-api-key-2.0.0/requirements.txt 0000664 0000000 0000000 00000000544 13643027634 0022423 0 ustar 00root root 0000000 0000000 -e .
# Django environment.
django[argon2,bcrypt]==3.0.*
djangorestframework==3.10.*
dj-database-url
django-dotenv
# PostgreSQL testing.
psycopg2-binary
# Tooling.
autoflake
black
flake8
flake8-bugbear
flake8-comprehensions
isort
mkdocs==1.*
mkdocs-material==4.*
pymdown-extensions==6.*
mypy
pytest==5.*
pytest-django==3.5.*
pytest-cov
seed-isort-config
djangorestframework-api-key-2.0.0/scripts/ 0000775 0000000 0000000 00000000000 13643027634 0020623 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/scripts/check 0000775 0000000 0000000 00000000600 13643027634 0021622 0 ustar 00root root 0000000 0000000 #!/bin/sh -e
. scripts/env
export SOURCE_FILES="src/rest_framework_api_key test_project/ tests/conftest.py"
set -x
${PREFIX}black --check --diff --target-version=py36 $SOURCE_FILES
${PREFIX}flake8 $SOURCE_FILES
${PREFIX}mypy $SOURCE_FILES
${PREFIX}isort --check --diff --project=djangorestframework-api-key --recursive $SOURCE_FILES
${PREFIX}python scripts/makemigrations --check
djangorestframework-api-key-2.0.0/scripts/clean 0000775 0000000 0000000 00000000670 13643027634 0021636 0 ustar 00root root 0000000 0000000 #!/bin/sh -e
if [ -d 'dist' ] ; then
rm -r dist
fi
if [ -d 'build' ] ; then
rm -r build
fi
if [ -d 'site' ] ; then
rm -r site
fi
if [ -d 'htmlcov' ] ; then
rm -r htmlcov
fi
if [ -d "src/djangorestframework_api_key.egg-info" ] ; then
rm -r src/djangorestframework_api_key.egg-info
fi
find src/rest_framework_api_key -type f -name "*.py[co]" -delete
find src/rest_framework_api_key -type d -name __pycache__ -delete
djangorestframework-api-key-2.0.0/scripts/docs 0000775 0000000 0000000 00000000402 13643027634 0021475 0 ustar 00root root 0000000 0000000 #!/bin/sh -e
. scripts/env
COMMAND=$1
USAGE="USAGE: scripts/docs (build|serve)"
if [ ! $COMMAND ]; then
echo $USAGE
exit 1
fi
if [ "$COMMAND" != "build" ] && [ "$COMMAND" != "serve" ]; then
echo $USAGE
exit 1
fi
set -x
${PREFIX}mkdocs $COMMAND
djangorestframework-api-key-2.0.0/scripts/env 0000775 0000000 0000000 00000000461 13643027634 0021342 0 ustar 00root root 0000000 0000000 #!/bin/sh -e
_set_prefix() {
export PREFIX=""
if [ -d "venv" ]; then
export PREFIX="venv/bin/"
if [[ "$OSTYPE" == "cygwin" || $OSTYPE == "msys" ]]; then
# Windows.
export PREFIX="venv/Scripts/"
fi
fi
}
set_env() {
_set_prefix
}
set_env
djangorestframework-api-key-2.0.0/scripts/install 0000775 0000000 0000000 00000000726 13643027634 0022224 0 ustar 00root root 0000000 0000000 #!/bin/sh -e
if [ -z $CI ] ; then
echo "Creating virtual environment in 'venv/'..."
python -m venv venv
fi
. scripts/env
set -x
${PREFIX}python -m pip install -U pip
${PREFIX}python -m pip install -r requirements.txt
if [ ! -z $DJANGO_VERSION ]; then
${PREFIX}python -m pip install django==$DJANGO_VERSION
fi
set +x
echo
echo "Success!"
if [ -z $CI ]; then
echo "You can now activate your virtual environment using:"
echo "source ${PREFIX}activate"
fi
djangorestframework-api-key-2.0.0/scripts/lint 0000775 0000000 0000000 00000000554 13643027634 0021523 0 ustar 00root root 0000000 0000000 #!/bin/sh -e
. scripts/env
export SOURCE_FILES="src/rest_framework_api_key test_project tests/conftest.py"
set -x
${PREFIX}autoflake --in-place --recursive $SOURCE_FILES
${PREFIX}seed-isort-config --application-directories=src
${PREFIX}isort --project=rest_framework_api_key --recursive --apply $SOURCE_FILES
${PREFIX}black --target-version=py36 $SOURCE_FILES
djangorestframework-api-key-2.0.0/scripts/makemigrations 0000775 0000000 0000000 00000001172 13643027634 0023564 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""A stand-alone equivalent of `python manage.py makemigrations`."""
import pathlib
import sys
import django
from django.conf import settings
from django.core.management import call_command
root = pathlib.Path(__file__).parent.parent
sys.path.append(str(root))
if __name__ == "__main__":
APP = "rest_framework_api_key"
settings.configure(INSTALLED_APPS=[APP, "test_project.heroes"])
django.setup()
# For available options, see:
# https://docs.djangoproject.com/en/3.0/ref/django-admin/#makemigrations
options = sys.argv[1:]
call_command("makemigrations", *options, APP, "heroes")
djangorestframework-api-key-2.0.0/scripts/publish 0000775 0000000 0000000 00000001435 13643027634 0022222 0 ustar 00root root 0000000 0000000 #!/bin/sh -e
. scripts/env
export PACKAGE="rest_framework_api_key"
export VERSION=`cat src/${PACKAGE}/__version__.py | grep __version__ | sed "s/__version__ = //" | sed "s/'//g"`
if ! command -v "${PREFIX}twine" &>/dev/null ; then
echo "Unable to find the 'twine' command."
echo "Install from PyPI, using '${PREFIX}pip install twine'."
exit 1
fi
if ! ${PREFIX}pip show wheel &>/dev/null ; then
echo "Unable to find the 'wheel' command."
echo "Install from PyPI, using '${PREFIX}pip install wheel'."
exit 1
fi
scripts/clean
${PREFIX}python setup.py sdist bdist_wheel
${PREFIX}twine upload dist/*
${PREFIX}mkdocs gh-deploy
echo "You probably want to also tag the version now:"
echo "git tag -a ${VERSION} -m 'version ${VERSION}'"
echo "git push --tags"
scripts/clean
djangorestframework-api-key-2.0.0/scripts/test 0000775 0000000 0000000 00000000142 13643027634 0021525 0 ustar 00root root 0000000 0000000 #!/bin/sh -e
. scripts/env
set -x
if [ -z $CI ]; then
scripts/check
fi
${PREFIX}pytest $@
djangorestframework-api-key-2.0.0/setup.cfg 0000664 0000000 0000000 00000001037 13643027634 0020756 0 ustar 00root root 0000000 0000000 [flake8]
ignore = W503, E203, B305
max-line-length = 88
[mypy]
disallow_untyped_defs = True
ignore_missing_imports = True
[tool:isort]
combine_as_imports = True
force_grid_wrap = 0
include_trailing_comma = True
known_first_party = rest_framework_api_key,tests
known_third_party = dj_database_url,django,dotenv,pytest,rest_framework,setuptools,test_project
line_length = 88
multi_line_output = 3
[tool:pytest]
testpaths = tests
addopts =
-rxXs
--cov=rest_framework_api_key
--cov=tests
--cov-report=term-missing
--cov-fail-under=80
djangorestframework-api-key-2.0.0/setup.py 0000664 0000000 0000000 00000003443 13643027634 0020652 0 ustar 00root root 0000000 0000000 import re
from pathlib import Path
from setuptools import find_packages, setup
def get_version(package: str) -> str:
version = (Path("src") / package / "__version__.py").read_text()
match = re.search("__version__ = ['\"]([^'\"]+)['\"]", version)
assert match is not None
return match.group(1)
def get_long_description() -> str:
with open("README.md", encoding="utf8") as readme:
with open("CHANGELOG.md", encoding="utf8") as changelog:
return readme.read() + "\n\n" + changelog.read()
setup(
name="djangorestframework-api-key",
version=get_version("rest_framework_api_key"),
description="API key permissions for the Django REST Framework",
long_description=get_long_description(),
long_description_content_type="text/markdown",
url="http://github.com/florimondmanca/djangorestframework-api-key",
project_urls={
"Documentation": "https://florimondmanca.github.io/djangorestframework-api-key/"
},
author="Florimond Manca",
author_email="florimond.manca@gmail.com",
packages=find_packages("src"),
package_dir={"": "src"},
include_package_data=True,
zip_safe=False,
install_requires=[],
python_requires=">=3.6",
license="MIT",
classifiers=[
"Development Status :: 4 - Beta",
"Operating System :: OS Independent",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Environment :: Web Environment",
"Topic :: Software Development :: Libraries :: Python Modules",
"Framework :: Django",
"Framework :: Django :: 2.2",
"Framework :: Django :: 3.0",
],
)
djangorestframework-api-key-2.0.0/src/ 0000775 0000000 0000000 00000000000 13643027634 0017723 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/src/rest_framework_api_key/ 0000775 0000000 0000000 00000000000 13643027634 0024456 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/src/rest_framework_api_key/__init__.py 0000664 0000000 0000000 00000000244 13643027634 0026567 0 ustar 00root root 0000000 0000000 from .__version__ import __version__
default_app_config = "rest_framework_api_key.apps.RestFrameworkApiKeyConfig"
__all__ = ["__version__", "default_app_config"]
djangorestframework-api-key-2.0.0/src/rest_framework_api_key/__version__.py 0000664 0000000 0000000 00000000026 13643027634 0027307 0 ustar 00root root 0000000 0000000 __version__ = "2.0.0"
djangorestframework-api-key-2.0.0/src/rest_framework_api_key/admin.py 0000664 0000000 0000000 00000003047 13643027634 0026124 0 ustar 00root root 0000000 0000000 import typing
from django.contrib import admin, messages
from django.db import models
from django.http.request import HttpRequest
from .models import AbstractAPIKey, APIKey
class APIKeyModelAdmin(admin.ModelAdmin):
model: typing.Type[AbstractAPIKey]
list_display = (
"prefix",
"name",
"created",
"expiry_date",
"_has_expired",
"revoked",
)
list_filter = ("created",)
search_fields = ("name", "prefix")
def get_readonly_fields(
self, request: HttpRequest, obj: models.Model = None
) -> typing.Tuple[str, ...]:
obj = typing.cast(AbstractAPIKey, obj)
fields: typing.Tuple[str, ...]
fields = ("prefix",)
if obj is not None and obj.revoked:
fields = fields + ("name", "revoked", "expiry_date")
return fields
def save_model(
self,
request: HttpRequest,
obj: AbstractAPIKey,
form: typing.Any = None,
change: bool = False,
) -> None:
created = not obj.pk
if created:
key = self.model.objects.assign_key(obj)
obj.save()
message = (
"The API key for {} is: {}. ".format(obj.name, key)
+ "Please store it somewhere safe: "
+ "you will not be able to see it again."
)
messages.add_message(request, messages.WARNING, message)
else:
obj.save()
admin.site.register(APIKey, APIKeyModelAdmin)
APIKeyAdmin = APIKeyModelAdmin # Compatibility with <1.3
djangorestframework-api-key-2.0.0/src/rest_framework_api_key/apps.py 0000664 0000000 0000000 00000000235 13643027634 0025773 0 ustar 00root root 0000000 0000000 from django.apps import AppConfig
class RestFrameworkApiKeyConfig(AppConfig):
name = "rest_framework_api_key"
verbose_name = "API Key Permissions"
djangorestframework-api-key-2.0.0/src/rest_framework_api_key/crypto.py 0000664 0000000 0000000 00000002214 13643027634 0026347 0 ustar 00root root 0000000 0000000 import typing
from django.contrib.auth.hashers import check_password, make_password
from django.utils.crypto import get_random_string
def concatenate(left: str, right: str) -> str:
return "{}.{}".format(left, right)
def split(concatenated: str) -> typing.Tuple[str, str]:
left, _, right = concatenated.partition(".")
return left, right
class KeyGenerator:
def __init__(self, prefix_length: int = 8, secret_key_length: int = 32):
self.prefix_length = prefix_length
self.secret_key_length = secret_key_length
def get_prefix(self) -> str:
return get_random_string(self.prefix_length)
def get_secret_key(self) -> str:
return get_random_string(self.secret_key_length)
def hash(self, value: str) -> str:
return make_password(value)
def generate(self) -> typing.Tuple[str, str, str]:
prefix = self.get_prefix()
secret_key = self.get_secret_key()
key = concatenate(prefix, secret_key)
hashed_key = self.hash(key)
return key, prefix, hashed_key
def verify(self, key: str, hashed_key: str) -> bool:
return check_password(key, hashed_key)
djangorestframework-api-key-2.0.0/src/rest_framework_api_key/migrations/ 0000775 0000000 0000000 00000000000 13643027634 0026632 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/src/rest_framework_api_key/migrations/0001_initial.py 0000664 0000000 0000000 00000002306 13643027634 0031276 0 ustar 00root root 0000000 0000000 # Generated by Django 2.1.7 on 2019-04-03 16:11
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [] # type: ignore
operations = [
migrations.CreateModel(
name="APIKey",
fields=[
(
"id",
models.CharField(
max_length=100, primary_key=True, serialize=False, unique=True
),
),
("created", models.DateTimeField(auto_now_add=True, db_index=True)),
("name", models.CharField(default=None, max_length=50)),
(
"revoked",
models.BooleanField(
blank=True,
default=False,
help_text=(
"If the API key is revoked, clients cannot use it anymore."
),
),
),
],
options={
"verbose_name": "API key",
"verbose_name_plural": "API keys",
"ordering": ("-created",),
},
)
]
djangorestframework-api-key-2.0.0/src/rest_framework_api_key/migrations/0002_auto_20190529_2243.py 0000664 0000000 0000000 00000002457 13643027634 0032272 0 ustar 00root root 0000000 0000000 # Generated by Django 2.2.1 on 2019-05-29 22:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("rest_framework_api_key", "0001_initial")]
operations = [
migrations.AddField(
model_name="apikey",
name="expiry_date",
field=models.DateTimeField(
blank=True,
help_text="Once API key expires, clients cannot use it anymore.",
null=True,
verbose_name="Expires",
),
),
migrations.AlterField(
model_name="apikey",
name="name",
field=models.CharField(
default=None,
help_text=(
"A free-form name for the API key. Need not be unique. "
"50 characters max."
),
max_length=50,
),
),
migrations.AlterField(
model_name="apikey",
name="revoked",
field=models.BooleanField(
blank=True,
default=False,
help_text=(
"If the API key is revoked, clients cannot use it anymore. "
"(This cannot be undone.)"
),
),
),
]
djangorestframework-api-key-2.0.0/src/rest_framework_api_key/migrations/0003_auto_20190623_1952.py 0000664 0000000 0000000 00000001035 13643027634 0032263 0 ustar 00root root 0000000 0000000 # Generated by Django 2.2.2 on 2019-06-23 19:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("rest_framework_api_key", "0002_auto_20190529_2243")]
operations = [
migrations.AlterField(
model_name="apikey",
name="id",
field=models.CharField(
editable=False,
max_length=100,
primary_key=True,
serialize=False,
unique=True,
),
)
]
djangorestframework-api-key-2.0.0/src/rest_framework_api_key/migrations/0004_prefix_hashed_key.py 0000664 0000000 0000000 00000002607 13643027634 0033335 0 ustar 00root root 0000000 0000000 # Generated by Django 2.2.2 on 2019-06-29 10:38
from django.db import migrations, models
APP_NAME = "rest_framework_api_key"
MODEL_NAME = "apikey"
DEPENDENCIES = [(APP_NAME, "0003_auto_20190623_1952")]
def populate_prefix_hashed_key(apps, schema_editor) -> None: # type: ignore
model = apps.get_model(APP_NAME, MODEL_NAME)
for api_key in model.objects.all():
prefix, _, hashed_key = api_key.id.partition(".")
api_key.prefix = prefix
api_key.hashed_key = hashed_key
api_key.save()
class Migration(migrations.Migration):
dependencies = DEPENDENCIES
operations = [
migrations.AddField(
model_name=MODEL_NAME,
name="hashed_key",
field=models.CharField(max_length=100, null=True),
),
migrations.AddField(
model_name=MODEL_NAME,
name="prefix",
field=models.CharField(max_length=8, unique=True, null=True),
),
migrations.RunPython(populate_prefix_hashed_key, migrations.RunPython.noop),
migrations.AlterField(
model_name=MODEL_NAME,
name="hashed_key",
field=models.CharField(max_length=100, editable=False),
),
migrations.AlterField(
model_name=MODEL_NAME,
name="prefix",
field=models.CharField(max_length=8, unique=True, editable=False),
),
]
djangorestframework-api-key-2.0.0/src/rest_framework_api_key/migrations/__init__.py 0000664 0000000 0000000 00000000000 13643027634 0030731 0 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/src/rest_framework_api_key/models.py 0000664 0000000 0000000 00000010407 13643027634 0026315 0 ustar 00root root 0000000 0000000 import typing
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from .crypto import KeyGenerator, concatenate, split
class BaseAPIKeyManager(models.Manager):
key_generator = KeyGenerator()
def assign_key(self, obj: "AbstractAPIKey") -> str:
try:
key, prefix, hashed_key = self.key_generator.generate()
except ValueError: # Compatibility with < 1.4
generate = typing.cast(
typing.Callable[[], typing.Tuple[str, str]], self.key_generator.generate
)
key, hashed_key = generate()
pk = hashed_key
prefix, hashed_key = split(hashed_key)
else:
pk = concatenate(prefix, hashed_key)
obj.id = pk
obj.prefix = prefix
obj.hashed_key = hashed_key
return key
def create_key(self, **kwargs: typing.Any) -> typing.Tuple["AbstractAPIKey", str]:
# Prevent from manually setting the primary key.
kwargs.pop("id", None)
obj = self.model(**kwargs)
key = self.assign_key(obj)
obj.save()
return obj, key
def get_usable_keys(self) -> models.QuerySet:
return self.filter(revoked=False)
def get_from_key(self, key: str) -> "AbstractAPIKey":
prefix, _, _ = key.partition(".")
queryset = self.get_usable_keys()
try:
api_key = queryset.get(prefix=prefix)
except self.model.DoesNotExist:
raise # For the sake of being explicit.
if not api_key.is_valid(key):
raise self.model.DoesNotExist("Key is not valid.")
else:
return api_key
def is_valid(self, key: str) -> bool:
try:
api_key = self.get_from_key(key)
except self.model.DoesNotExist:
return False
if api_key.has_expired:
return False
return True
class APIKeyManager(BaseAPIKeyManager):
pass
class AbstractAPIKey(models.Model):
objects = APIKeyManager()
id = models.CharField(max_length=100, unique=True, primary_key=True, editable=False)
prefix = models.CharField(max_length=8, unique=True, editable=False)
hashed_key = models.CharField(max_length=100, editable=False)
created = models.DateTimeField(auto_now_add=True, db_index=True)
name = models.CharField(
max_length=50,
blank=False,
default=None,
help_text=(
"A free-form name for the API key. "
"Need not be unique. "
"50 characters max."
),
)
revoked = models.BooleanField(
blank=True,
default=False,
help_text=(
"If the API key is revoked, clients cannot use it anymore. "
"(This cannot be undone.)"
),
)
expiry_date = models.DateTimeField(
blank=True,
null=True,
verbose_name="Expires",
help_text="Once API key expires, clients cannot use it anymore.",
)
class Meta: # noqa
abstract = True
ordering = ("-created",)
verbose_name = "API key"
verbose_name_plural = "API keys"
def __init__(self, *args: typing.Any, **kwargs: typing.Any):
super().__init__(*args, **kwargs)
# Store the initial value of `revoked` to detect changes.
self._initial_revoked = self.revoked
def _has_expired(self) -> bool:
if self.expiry_date is None:
return False
return self.expiry_date < timezone.now()
_has_expired.short_description = "Has expired" # type: ignore
_has_expired.boolean = True # type: ignore
has_expired = property(_has_expired)
def is_valid(self, key: str) -> bool:
return type(self).objects.key_generator.verify(key, self.hashed_key)
def clean(self) -> None:
self._validate_revoked()
def save(self, *args: typing.Any, **kwargs: typing.Any) -> None:
self._validate_revoked()
super().save(*args, **kwargs)
def _validate_revoked(self) -> None:
if self._initial_revoked and not self.revoked:
raise ValidationError(
"The API key has been revoked, which cannot be undone."
)
def __str__(self) -> str:
return str(self.name)
class APIKey(AbstractAPIKey):
pass
djangorestframework-api-key-2.0.0/src/rest_framework_api_key/permissions.py 0000664 0000000 0000000 00000003411 13643027634 0027402 0 ustar 00root root 0000000 0000000 import typing
from django.conf import settings
from django.http import HttpRequest
from rest_framework import permissions
from .models import AbstractAPIKey, APIKey
class KeyParser:
def get(self, request: HttpRequest) -> typing.Optional[str]:
custom_header = getattr(settings, "API_KEY_CUSTOM_HEADER", None)
if custom_header is not None:
return self.get_from_header(request, custom_header)
return self.get_from_authorization(request)
def get_from_authorization(self, request: HttpRequest) -> typing.Optional[str]:
authorization = request.META.get("HTTP_AUTHORIZATION")
if not authorization:
return None
try:
_, key = authorization.split("Api-Key ")
except ValueError:
key = None
return key
def get_from_header(self, request: HttpRequest, name: str) -> typing.Optional[str]:
return request.META.get(name) or None
class BaseHasAPIKey(permissions.BasePermission):
model: typing.Optional[typing.Type[AbstractAPIKey]] = None
key_parser = KeyParser()
def get_key(self, request: HttpRequest) -> typing.Optional[str]:
return self.key_parser.get(request)
def has_permission(self, request: HttpRequest, view: typing.Any) -> bool:
assert self.model is not None, (
"%s must define `.model` with the API key model to use"
% self.__class__.__name__
)
key = self.get_key(request)
if not key:
return False
return self.model.objects.is_valid(key)
def has_object_permission(
self, request: HttpRequest, view: typing.Any, obj: AbstractAPIKey
) -> bool:
return self.has_permission(request, view)
class HasAPIKey(BaseHasAPIKey):
model = APIKey
djangorestframework-api-key-2.0.0/src/rest_framework_api_key/py.typed 0000664 0000000 0000000 00000000000 13643027634 0026143 0 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/test_project/ 0000775 0000000 0000000 00000000000 13643027634 0021641 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/test_project/README.md 0000664 0000000 0000000 00000000604 13643027634 0023120 0 ustar 00root root 0000000 0000000 # project
Test project for Django REST Framework API Key.
## Usage
- Run migrations (creates an SQLite database):
```bash
python test_project/manage.py migrate
```
- Create a superuser to access the admin site:
```bash
python test_project/manage.py createsuperuser
# Enter user information as instructed
```
- Start the server:
```bash
python test_project/manage.py runserver
```
djangorestframework-api-key-2.0.0/test_project/__init__.py 0000664 0000000 0000000 00000000000 13643027634 0023740 0 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/test_project/heroes/ 0000775 0000000 0000000 00000000000 13643027634 0023126 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/test_project/heroes/__init__.py 0000664 0000000 0000000 00000000000 13643027634 0025225 0 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/test_project/heroes/admin.py 0000664 0000000 0000000 00000000440 13643027634 0024566 0 ustar 00root root 0000000 0000000 from django.contrib import admin
from rest_framework_api_key.admin import APIKeyModelAdmin
from .models import Hero, HeroAPIKey
@admin.register(HeroAPIKey)
class HeroAPIKeyModelAdmin(APIKeyModelAdmin):
pass
@admin.register(Hero)
class HeroModelAdmin(admin.ModelAdmin):
pass
djangorestframework-api-key-2.0.0/test_project/heroes/apps.py 0000664 0000000 0000000 00000000127 13643027634 0024443 0 ustar 00root root 0000000 0000000 from django.apps import AppConfig
class HeroesConfig(AppConfig):
name = "heroes"
djangorestframework-api-key-2.0.0/test_project/heroes/migrations/ 0000775 0000000 0000000 00000000000 13643027634 0025302 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/test_project/heroes/migrations/0001_initial.py 0000664 0000000 0000000 00000005723 13643027634 0027754 0 ustar 00root root 0000000 0000000 # Generated by Django 2.2.2 on 2019-06-23 19:53
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
operations = [
migrations.CreateModel(
name="Hero",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=64)),
("retired", models.BooleanField(default=False)),
],
options={"verbose_name_plural": "heroes"},
),
migrations.CreateModel(
name="HeroAPIKey",
fields=[
(
"id",
models.CharField(
editable=False,
max_length=100,
primary_key=True,
serialize=False,
unique=True,
),
),
("created", models.DateTimeField(auto_now_add=True, db_index=True)),
(
"name",
models.CharField(
default=None,
help_text=(
"A free-form name for the API key. Need not be unique. "
"50 characters max."
),
max_length=50,
),
),
(
"revoked",
models.BooleanField(
blank=True,
default=False,
help_text=(
"If the API key is revoked, clients cannot use it anymore. "
"(This cannot be undone.)"
),
),
),
(
"expiry_date",
models.DateTimeField(
blank=True,
help_text=(
"Once API key expires, clients cannot use it anymore."
),
null=True,
verbose_name="Expires",
),
),
(
"hero",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="api_keys",
to="heroes.Hero",
),
),
],
options={
"verbose_name": "Hero API key",
"verbose_name_plural": "Hero API keys",
"ordering": ("-created",),
"abstract": False,
},
),
]
djangorestframework-api-key-2.0.0/test_project/heroes/migrations/0002_prefix_hashed_key.py 0000664 0000000 0000000 00000002550 13643027634 0032000 0 ustar 00root root 0000000 0000000 # Generated by Django 2.2.2 on 2019-06-29 10:38
from django.db import migrations, models
APP_NAME = "heroes"
MODEL_NAME = "heroapikey"
DEPENDENCIES = [(APP_NAME, "0001_initial")]
def populate_prefix_hashed_key(apps, schema_editor): # type: ignore
model = apps.get_model(APP_NAME, MODEL_NAME)
for api_key in model.objects.all():
prefix, _, hashed_key = api_key.id.partition(".")
api_key.prefix = prefix
api_key.hashed_key = hashed_key
api_key.save()
class Migration(migrations.Migration):
dependencies = DEPENDENCIES
operations = [
migrations.AddField(
model_name=MODEL_NAME,
name="hashed_key",
field=models.CharField(max_length=100, null=True),
),
migrations.AddField(
model_name=MODEL_NAME,
name="prefix",
field=models.CharField(max_length=8, unique=True, null=True),
),
migrations.RunPython(populate_prefix_hashed_key, migrations.RunPython.noop),
migrations.AlterField(
model_name=MODEL_NAME,
name="hashed_key",
field=models.CharField(max_length=100, editable=False),
),
migrations.AlterField(
model_name=MODEL_NAME,
name="prefix",
field=models.CharField(max_length=8, unique=True, editable=False),
),
]
djangorestframework-api-key-2.0.0/test_project/heroes/migrations/__init__.py 0000664 0000000 0000000 00000000000 13643027634 0027401 0 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/test_project/heroes/models.py 0000664 0000000 0000000 00000001454 13643027634 0024767 0 ustar 00root root 0000000 0000000 from django.db import models
from rest_framework_api_key.models import AbstractAPIKey, BaseAPIKeyManager
class Hero(models.Model):
objects = models.Manager()
name = models.CharField(max_length=64)
retired = models.BooleanField(default=False)
class Meta:
verbose_name_plural = "heroes"
def __str__(self) -> str:
return self.name
class HeroAPIKeyManager(BaseAPIKeyManager):
def get_usable_keys(self) -> models.QuerySet:
return super().get_usable_keys().filter(hero__retired=False)
class HeroAPIKey(AbstractAPIKey):
objects = HeroAPIKeyManager()
hero = models.ForeignKey(Hero, on_delete=models.CASCADE, related_name="api_keys")
class Meta(AbstractAPIKey.Meta):
verbose_name = "Hero API key"
verbose_name_plural = "Hero API keys"
djangorestframework-api-key-2.0.0/test_project/heroes/permissions.py 0000664 0000000 0000000 00000000232 13643027634 0026050 0 ustar 00root root 0000000 0000000 from rest_framework_api_key.permissions import BaseHasAPIKey
from .models import HeroAPIKey
class HasHeroAPIKey(BaseHasAPIKey):
model = HeroAPIKey
djangorestframework-api-key-2.0.0/test_project/manage.py 0000775 0000000 0000000 00000001277 13643027634 0023455 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
import os
import pathlib
import sys
import dotenv
root = pathlib.Path(__file__).parent.parent
if __name__ == "__main__":
dotenv.read_dotenv(str(root / ".env"))
sys.path.append(str(root))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.project.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
djangorestframework-api-key-2.0.0/test_project/project/ 0000775 0000000 0000000 00000000000 13643027634 0023307 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/test_project/project/__init__.py 0000664 0000000 0000000 00000000000 13643027634 0025406 0 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/test_project/project/settings.py 0000664 0000000 0000000 00000004241 13643027634 0025522 0 ustar 00root root 0000000 0000000 import os
import dj_database_url
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = "*(q7fo7duyzqhv$qmiym5#z%w^qqh67a9rqk$jr4n1))nvnql^"
DEBUG = True
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"rest_framework_api_key",
"test_project.heroes",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "project.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]
},
}
]
WSGI_APPLICATION = "project.wsgi.application"
# Database
DATABASES = {"default": dj_database_url.config(default="sqlite://db.sqlite3")}
# Password validation
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" # noqa: E501
},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# Internationalization
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
STATIC_URL = "/static/"
djangorestframework-api-key-2.0.0/test_project/project/urls.py 0000664 0000000 0000000 00000000157 13643027634 0024651 0 ustar 00root root 0000000 0000000 from django.contrib import admin
from django.urls import path
urlpatterns = [path("admin/", admin.site.urls)]
djangorestframework-api-key-2.0.0/test_project/project/wsgi.py 0000664 0000000 0000000 00000000247 13643027634 0024635 0 ustar 00root root 0000000 0000000 import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
application = get_wsgi_application()
djangorestframework-api-key-2.0.0/tests/ 0000775 0000000 0000000 00000000000 13643027634 0020276 5 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/tests/__init__.py 0000664 0000000 0000000 00000000000 13643027634 0022375 0 ustar 00root root 0000000 0000000 djangorestframework-api-key-2.0.0/tests/compat.py 0000664 0000000 0000000 00000000313 13643027634 0022130 0 ustar 00root root 0000000 0000000 import sys
if sys.version_info < (3, 7):
from contextlib import contextmanager
@contextmanager
def nullcontext():
yield
else:
from contextlib import nullcontext # noqa: F401
djangorestframework-api-key-2.0.0/tests/conftest.py 0000664 0000000 0000000 00000010363 13643027634 0022500 0 ustar 00root root 0000000 0000000 import typing
from pathlib import Path
import dj_database_url
import dotenv
import pytest
from django.conf import settings
from django.http import HttpRequest
from django.test import override_settings
from .compat import nullcontext
if typing.TYPE_CHECKING:
from django.contrib.auth.base_user import AbstractBaseUser
def pytest_configure() -> None:
dotenv.read_dotenv(str(Path(__file__).parent.parent / ".env"))
settings.configure(
**{
"SECRET_KEY": "abcd",
"INSTALLED_APPS": [
# Mandatory
"django.contrib.contenttypes",
# Permissions
"django.contrib.auth",
# Admin
"django.contrib.admin",
"django.contrib.messages",
"django.contrib.sessions",
# Project
"rest_framework",
"rest_framework_api_key",
"test_project.heroes",
],
"TEMPLATES": [
# Admin
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"OPTIONS": {
"context_processors": [
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]
},
}
],
"MIDDLEWARE": [
# Admin
"django.contrib.messages.middleware.MessageMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
],
"ROOT_URLCONF": "test_project.project.urls",
"DATABASES": {
"default": dj_database_url.config(default="sqlite://:memory:")
},
}
)
def _create_user() -> "AbstractBaseUser":
from django.contrib.auth import get_user_model
User = get_user_model()
return User.objects.create_user(username="foo", password="bar")
@pytest.fixture(
name="key_header_config",
params=[
{"header": "HTTP_AUTHORIZATION", "default": "Api-Key {key}"},
{
"header": "HTTP_X_API_KEY",
"default": "{key}",
"set_custom_header_setting": True,
},
],
)
def fixture_key_header_config(request: typing.Any) -> typing.Iterator[dict]:
config: dict = request.param
ctx: typing.ContextManager[None]
if config.get("set_custom_header_setting"):
ctx = override_settings(API_KEY_CUSTOM_HEADER=config["header"]) # type: ignore
else:
ctx = nullcontext()
with ctx:
yield config
@pytest.fixture(name="build_create_request")
def fixture_build_create_request(key_header_config: dict) -> typing.Callable:
from rest_framework.test import APIRequestFactory, force_authenticate
from rest_framework_api_key.models import AbstractAPIKey
def build_create_request(model: typing.Type[AbstractAPIKey]) -> typing.Callable:
request_factory = APIRequestFactory()
_MISSING = object()
def create_request(
authenticated: bool = False, **kwargs: typing.Any,
) -> HttpRequest:
headers = {}
authorization = kwargs.pop("authorization", _MISSING)
if authorization is not None:
kwargs.setdefault("name", "test")
_, key = model.objects.create_key(**kwargs)
if callable(authorization):
authorization = authorization(key)
if authorization is _MISSING:
authorization = key_header_config["default"]
headers[key_header_config["header"]] = authorization.format(key=key)
request = request_factory.get("/test/", **headers)
if authenticated:
user = _create_user()
force_authenticate(request, user)
return request
return create_request
return build_create_request
@pytest.fixture(name="create_request")
def fixture_create_request(build_create_request: typing.Callable) -> typing.Callable:
from rest_framework_api_key.models import APIKey
return build_create_request(APIKey)
djangorestframework-api-key-2.0.0/tests/dateutils.py 0000664 0000000 0000000 00000000206 13643027634 0022644 0 ustar 00root root 0000000 0000000 from datetime import datetime, timedelta
NOW = datetime.now()
TOMORROW = NOW + timedelta(days=1)
YESTERDAY = NOW - timedelta(days=1)
djangorestframework-api-key-2.0.0/tests/test_admin.py 0000664 0000000 0000000 00000003177 13643027634 0023007 0 ustar 00root root 0000000 0000000 import pytest
from django.contrib.admin import site
from django.contrib.messages import get_messages
from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import RequestFactory
from django.http.request import HttpRequest
from rest_framework_api_key.admin import APIKeyModelAdmin
from rest_framework_api_key.models import APIKey
from test_project.heroes.admin import HeroAPIKeyModelAdmin
from test_project.heroes.models import Hero, HeroAPIKey
def build_admin_request(rf: RequestFactory) -> HttpRequest:
request = rf.post("/")
# NOTE: all middleware must be instantiated before
# any middleware can process the request.
sessions = SessionMiddleware()
messages = MessageMiddleware()
sessions.process_request(request)
messages.process_request(request)
return request
@pytest.mark.django_db
def test_admin_create_api_key(rf: RequestFactory) -> None:
request = build_admin_request(rf)
admin = APIKeyModelAdmin(APIKey, site)
api_key = APIKey(name="test")
assert not api_key.pk
admin.save_model(request, obj=api_key)
assert api_key.pk
messages = get_messages(request)
assert len(messages) == 1
@pytest.mark.django_db
def test_admin_create_custom_api_key(rf: RequestFactory) -> None:
request = build_admin_request(rf)
admin = HeroAPIKeyModelAdmin(HeroAPIKey, site)
api_key = HeroAPIKey(name="test", hero=Hero.objects.create())
assert not api_key.pk
admin.save_model(request, obj=api_key)
assert api_key.pk
messages = get_messages(request)
assert len(messages) == 1
djangorestframework-api-key-2.0.0/tests/test_checks.py 0000664 0000000 0000000 00000000163 13643027634 0023147 0 ustar 00root root 0000000 0000000 from django.core.management import call_command
def test_system_checks_pass() -> None:
call_command("check")
djangorestframework-api-key-2.0.0/tests/test_legacy_key_generator.py 0000664 0000000 0000000 00000002501 13643027634 0026067 0 ustar 00root root 0000000 0000000 from typing import Tuple
import pytest
from django.contrib.auth.hashers import make_password
from django.utils.crypto import get_random_string
from rest_framework_api_key.crypto import KeyGenerator
from rest_framework_api_key.models import APIKey, BaseAPIKeyManager
pytestmark = pytest.mark.django_db
class LegacyKeyGenerator(KeyGenerator):
"""
Pre-1.4 key generator.
The key generator interface was updated in v1.4 via:
https://github.com/florimondmanca/djangorestframework-api-key/pull/62
We must ensure that custom key generators created based on the pre-1.4 interface
continue to work in 1.x.
"""
def generate(self) -> Tuple[str, str]: # type: ignore
# NOTE: this method should replicate the behavior before #62, and
# have no dependencies on the current `rest_framework_api_key` package.
prefix = get_random_string(8)
secret_key = get_random_string(32)
key = prefix + "." + secret_key
key_id = prefix + "." + make_password(key)
return key, key_id
def test_manager_with_legacy_key_generator() -> None:
class Manager(BaseAPIKeyManager):
key_generator = LegacyKeyGenerator()
manager = Manager()
manager.model = APIKey
api_key, generated_key = manager.create_key(name="test")
assert api_key.is_valid(generated_key)
djangorestframework-api-key-2.0.0/tests/test_model.py 0000664 0000000 0000000 00000004757 13643027634 0023024 0 ustar 00root root 0000000 0000000 import string
import pytest
from django.core.exceptions import ValidationError
from django.db.utils import IntegrityError
from rest_framework_api_key.models import APIKey
from test_project.heroes.models import HeroAPIKey, Hero
from .dateutils import NOW, TOMORROW, YESTERDAY
pytestmark = pytest.mark.django_db
def test_key_generation():
api_key, generated_key = APIKey.objects.create_key(name="test")
prefix = api_key.prefix
hashed_key = api_key.hashed_key
assert prefix and hashed_key
charset = set(string.ascii_letters + string.digits + ".")
assert all(c in charset for c in generated_key)
# The generated key must be validβ¦
assert api_key.is_valid(generated_key) is True
# But not the hashed key.
assert api_key.is_valid(hashed_key) is False
def test_name_is_required():
with pytest.raises(IntegrityError):
APIKey.objects.create()
def test_cannot_unrevoke():
api_key, _ = APIKey.objects.create_key(name="test", revoked=True)
# Try to unrevoke the API key programmatically.
api_key.revoked = False
with pytest.raises(ValidationError):
api_key.save()
with pytest.raises(ValidationError):
api_key.clean()
@pytest.mark.parametrize(
"expiry_date, has_expired",
[(None, False), (NOW, True), (TOMORROW, False), (YESTERDAY, True)],
)
def test_has_expired(expiry_date, has_expired):
api_key, _ = APIKey.objects.create_key(name="test", expiry_date=expiry_date)
assert api_key.has_expired is has_expired
def test_custom_api_key_model():
hero = Hero.objects.create()
hero_api_key, generated_key = HeroAPIKey.objects.create_key(name="test", hero=hero)
assert hero_api_key.is_valid(generated_key)
assert hero_api_key.hero.id == hero.id
assert hero.api_keys.first() == hero_api_key
@pytest.mark.django_db
def test_api_key_manager_get_from_key():
api_key, generated_key = APIKey.objects.create_key(name="test")
retrieved_key = APIKey.objects.get_from_key(generated_key)
assert retrieved_key == api_key
@pytest.mark.django_db
def test_api_key_manager_get_from_key_missing_key():
with pytest.raises(APIKey.DoesNotExist):
APIKey.objects.get_from_key("foobar")
@pytest.mark.django_db
def test_api_key_manager_get_from_key_invalid_key():
api_key, generated_key = APIKey.objects.create_key(name="test")
prefix, _, _ = generated_key.partition(".")
invalid_key = f"{prefix}.foobar"
with pytest.raises(APIKey.DoesNotExist):
APIKey.objects.get_from_key(invalid_key)
djangorestframework-api-key-2.0.0/tests/test_permissions.py 0000664 0000000 0000000 00000005641 13643027634 0024270 0 ustar 00root root 0000000 0000000 from datetime import datetime, timedelta
import pytest
from django.conf.global_settings import PASSWORD_HASHERS
from django.test import override_settings
from rest_framework import generics, permissions
from rest_framework.response import Response
from .utils import create_view_with_permissions
from rest_framework_api_key.permissions import HasAPIKey
pytestmark = pytest.mark.django_db
@pytest.fixture(name="view")
def fixture_view():
return create_view_with_permissions(HasAPIKey)
def test_if_valid_api_key_then_permission_granted(create_request, view):
request = create_request()
response = view(request)
assert response.status_code == 200
@pytest.mark.parametrize("hasher", PASSWORD_HASHERS)
def test_hashers(create_request, view, hasher):
with override_settings(PASSWORD_HASHERS=[hasher]):
test_if_valid_api_key_then_permission_granted(create_request, view)
def test_if_no_api_key_then_permission_denied(create_request, view):
request = create_request(authorization=None)
response = view(request)
assert response.status_code == 403
def _scramble_prefix(key: str) -> str:
prefix, _, secret_key = key.partition(".")
truncated_prefix = prefix[:-1]
return truncated_prefix + "." + secret_key
@pytest.mark.parametrize(
"modifier",
[
lambda key: "",
lambda key: "abcd",
lambda key: "foo.bar",
lambda key: " " + key,
str.upper,
str.lower,
_scramble_prefix,
],
)
def test_if_invalid_api_key_then_permission_denied(
create_request, view, key_header_config, modifier
):
def get_authorization(key):
return key_header_config["default"].format(key=modifier(key))
request = create_request(authorization=get_authorization)
response = view(request)
assert response.status_code == 403
def test_if_revoked_then_permission_denied(create_request, view):
request = create_request(revoked=True)
response = view(request)
assert response.status_code == 403
NOW = datetime.now()
TOMORROW = NOW + timedelta(days=1)
TWO_DAYS_AGO = NOW - timedelta(days=2)
@pytest.mark.parametrize("expiry_date, ok", [(TOMORROW, True), (TWO_DAYS_AGO, False)])
def test_expiry_date(create_request, view, expiry_date, ok):
request = create_request(expiry_date=expiry_date)
response = view(request)
status_code = 200 if ok else 403
assert response.status_code == status_code
def test_object_permission(create_request):
class DenyObject(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return False
class View(generics.GenericAPIView):
permission_classes = [HasAPIKey | DenyObject]
def get(self, request):
self.check_object_permissions(request, object())
return Response()
view = View.as_view()
request = create_request(authorization=None)
response = view(request)
assert response.status_code == 403
djangorestframework-api-key-2.0.0/tests/test_permissions_combination.py 0000664 0000000 0000000 00000001464 13643027634 0026651 0 ustar 00root root 0000000 0000000 import pytest
from rest_framework.permissions import IsAuthenticated
from rest_framework_api_key.permissions import HasAPIKey
from .utils import create_view_with_permissions
pytestmark = pytest.mark.django_db
@pytest.fixture(name="view")
def fixture_view():
return create_view_with_permissions(HasAPIKey | IsAuthenticated)
def test_if_authenticated_and_no_api_key_then_permission_granted(create_request, view):
request = create_request(authenticated=True, authorization=None)
response = view(request)
assert response.status_code == 200, response.data
def test_if_authenticated_and_revoked_api_key_then_permission_granted(
create_request, view
):
request = create_request(authenticated=True, revoked=True)
response = view(request)
assert response.status_code == 200, response.data
djangorestframework-api-key-2.0.0/tests/test_permissions_custom.py 0000664 0000000 0000000 00000002050 13643027634 0025651 0 ustar 00root root 0000000 0000000 import pytest
from test_project.heroes.models import HeroAPIKey, Hero
from test_project.heroes.permissions import HasHeroAPIKey
from .utils import create_view_with_permissions
pytestmark = pytest.mark.django_db
@pytest.fixture(name="view")
def fixture_view():
return create_view_with_permissions(HasHeroAPIKey)
@pytest.fixture(name="create_hero_request")
def fixture_create_hero_request(build_create_request):
return build_create_request(HeroAPIKey)
def test_non_hero_api_key_denied(create_request, view):
request = create_request()
response = view(request)
assert response.status_code == 403
def test_hero_api_key_granted(create_hero_request, view):
hero = Hero.objects.create()
hero_request = create_hero_request(hero=hero)
response = view(hero_request)
assert response.status_code == 200
def test_retired_hero_denied(create_hero_request, view):
hero = Hero.objects.create(retired=True)
hero_request = create_hero_request(hero=hero)
response = view(hero_request)
assert response.status_code == 403
djangorestframework-api-key-2.0.0/tests/utils.py 0000664 0000000 0000000 00000000643 13643027634 0022013 0 ustar 00root root 0000000 0000000 import typing
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.permissions import BasePermission
def create_view_with_permissions(
*classes: typing.Type[BasePermission],
) -> typing.Callable:
@api_view()
@permission_classes(classes)
def view(*args: typing.Any) -> Response:
return Response()
return view