Resource-scoped API key and group permissions for Django REST Framework. Built for startups moving fast.
Project description
DRF Scoped Permissions
Resource-scoped API key and group permissions for Django REST Framework.
What is this?
A Django package that adds scope-based permissions to your API keys and user groups. Instead of API keys having full access to everything, you can limit them to specific resources and actions.
Works with API keys (for service-to-service auth), user groups (for regular users), and JWT tokens.
Features
- Scope-based permissions for API keys and user groups
- Automatic scope discovery from your viewsets
- Django admin integration with checkboxes
- Works alongside existing authentication
- Backward compatible - keys without scopes still work
- Built on
djangorestframework-api-key
Installation
pip install drf-scoped-permissions
Requirements
- Python 3.11+
- Django 4.2+
- Django REST Framework 3.14+
Quick Start
1. Add to INSTALLED_APPS
# settings.py
INSTALLED_APPS = [
# ...
'rest_framework',
'rest_framework_api_key',
'drf_scoped_permissions',
]
2. Run Migrations
python manage.py migrate drf_scoped_permissions
3. Use in Your Views
# views.py
from rest_framework import viewsets
from drf_scoped_permissions.permissions import HasAPIKeyOrGroupScope
class PostViewSet(viewsets.ModelViewSet):
permission_classes = [HasAPIKeyOrGroupScope]
scope_resource = 'posts' # Auto-generates: posts.read, posts.write, posts.delete
queryset = Post.objects.all()
serializer_class = PostSerializer
4. Create API Keys in Django Admin
- Go to Django Admin → API Keys → Scoped API keys
- Click "Add Scoped API Key"
- Select scopes (checkboxes are auto-generated from your viewsets)
- Save and copy the generated key
5. Use the API Key
curl -H "Authorization: Api-Key YOUR_API_KEY" \
http://localhost:8000/api/posts/
How It Works
Scope Format
Scopes follow the pattern: resource.action
posts.read- Read access to posts (GET, HEAD, OPTIONS)posts.write- Write access to posts (POST, PUT, PATCH)posts.delete- Delete access to posts (DELETE)posts.publish- Custom action access (custom @action methods)
Auto-Discovery
Scopes are automatically discovered from your viewsets:
# This viewset automatically creates:
# - posts.read (from list/retrieve)
# - posts.write (from create/update/partial_update)
# - posts.delete (from destroy)
# - posts.publish (from custom action)
class PostViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
# Custom action
pass
Resource Name Resolution
The resource name used in scopes is resolved consistently across discovery and permission checking:
scope_resourceattribute (explicit) - takes priority- Class name with
ViewSetstripped, lowercased (fallback)
class PostViewSet(viewsets.ModelViewSet):
scope_resource = 'posts' # → posts.read, posts.write, etc.
class UserProfileViewSet(viewsets.ModelViewSet):
scope_resource = 'profiles' # → profiles.read (not "userprofile")
class OrderViewSet(viewsets.ModelViewSet):
pass # No scope_resource → falls back to "order" from class name
This ensures the admin, management commands, and runtime permission checks all use the same resource name.
Usage Examples
API Keys (Service Accounts)
from drf_scoped_permissions.models import ScopedAPIKey
# Create API key with limited scopes
api_key, key = ScopedAPIKey.objects.create_key(
name="Mobile App Backend",
scopes=["posts.read", "posts.write", "comments.read"]
)
print(f"API Key: {key}") # Give this to your service
User Groups (Human Users)
from django.contrib.auth.models import Group
from drf_scoped_permissions.models import ScopedGroup
# Create group with scopes
editors = Group.objects.create(name='Editors')
ScopedGroup.objects.create(
group=editors,
scopes=["posts.read", "posts.write", "comments.read", "comments.write"]
)
# Add users to group
user.groups.add(editors)
JWT Tokens
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'drf_scoped_permissions.permissions.HasAPIKeyOrGroupScope',
],
}
# Scopes from user's groups are automatically checked
# You can also include scopes in JWT claims (see Advanced Usage)
Explicit Scope Requirements
class AnalyticsViewSet(viewsets.ViewSet):
permission_classes = [HasAPIKeyOrGroupScope]
required_scope = 'analytics.export' # Explicit scope requirement
def list(self, request):
return Response({'data': 'analytics'})
Custom Actions
class PostViewSet(viewsets.ModelViewSet):
permission_classes = [HasAPIKeyOrGroupScope]
scope_resource = 'posts'
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
# Requires 'posts.publish' scope
post = self.get_object()
post.published = True
post.save()
return Response({'status': 'published'})
Advanced Usage
Backward Compatibility
API keys without scopes have unrestricted access (legacy mode):
# Old API key with no scopes
api_key = ScopedAPIKey.objects.create(name="Legacy Key")
# Scopes: [] (empty) → Full access to everything
Including Scopes in JWT Tokens
# serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
# Add user scopes to token
from drf_scoped_permissions.utils import get_user_scopes
token['scopes'] = list(get_user_scopes(user))
return token
Management Commands
List available scopes:
python manage.py list_scopes
Output:
Available API Scopes:
[Blog]
posts:
- posts.read
- posts.write
- posts.delete
- posts.publish
[Comments]
comments:
- comments.read
- comments.write
- comments.delete
Scopes are grouped by Django app for readability.
Migrate legacy API keys:
# Preview migration
python manage.py migrate_api_keys --dry-run
# Run migration
python manage.py migrate_api_keys
Custom Permission Class
from drf_scoped_permissions.permissions import HasAPIKeyOrGroupScope
class CustomScopePermission(HasAPIKeyOrGroupScope):
def get_required_scope(self, request, view):
# Custom logic for determining required scope
if view.action == 'special_action':
return 'posts.special'
return super().get_required_scope(request, view)
Configuration
Settings
# settings.py
# Track when API keys are last used (default: False)
# When enabled, updates last_used_at on every authenticated request
# Note: This adds a database write per request - enable only if needed
SCOPED_PERMISSIONS_TRACK_LAST_USED = True
Django Admin
The package provides a user-friendly admin interface:
API Keys Admin
- Create/revoke API keys
- Select scopes via organized checkboxes (grouped by Django app, then by resource)
- View masked keys for security
- Track creation date and last used
Groups Admin
- Extended Django Groups with scope management
- Same checkbox interface as API keys
- Scopes automatically apply to all users in group
Security Considerations
API Key Storage
- Keys are hashed using Django's password hashers
- Only shown once upon creation
- Stored securely in database
Best Practices
- ✅ Use HTTPS in production
- ✅ Rotate API keys regularly
- ✅ Use minimal scopes (principle of least privilege)
- ✅ Monitor API key usage via
last_used_at - ✅ Revoke unused keys
Not Recommended For
- ❌ User authentication (use Django auth + sessions)
- ❌ Public API keys (they should be server-side only)
- ❌ Mobile app auth (use OAuth2 or JWT)
Testing
from django.test import TestCase
from drf_scoped_permissions.models import ScopedAPIKey
class APITestCase(TestCase):
def test_api_key_scopes(self):
api_key, key = ScopedAPIKey.objects.create_key(
name="Test Key",
scopes=["posts.read"]
)
response = self.client.get(
'/api/posts/',
HTTP_AUTHORIZATION=f'Api-Key {key}'
)
self.assertEqual(response.status_code, 200)
Migration Guide
From djangorestframework-api-key
If you're already using djangorestframework-api-key:
- Install
drf-scoped-permissions - Run migrations:
python manage.py migrate drf_scoped_permissions - Migrate existing API keys:
# Preview what will be migrated
python manage.py migrate_api_keys --dry-run
# Run the migration
python manage.py migrate_api_keys
- Migrated keys have empty scopes (unrestricted access) - same as before
- Add scopes via Django admin when ready
- Once verified, delete legacy keys:
from rest_framework_api_key.models import APIKey
APIKey.objects.all().delete()
From Manual Implementation
- Replace your custom models with
ScopedAPIKeyandScopedGroup - Replace permission classes with
HasAPIKeyOrGroupScope - Update admin to use provided admin classes
- Remove custom scope discovery code (now automatic)
Examples
See the examples directory for complete example projects:
- Basic Setup - Minimal configuration
- Microservices - Service-to-service authentication
- Multi-tenant - Scoping by organization
- JWT Integration - Token-based auth with scopes
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
Development Setup
git clone https://github.com/yourusername/drf-scoped-permissions.git
cd drf-scoped-permissions
pip install -e ".[dev]"
make install-hooks # installs pre-commit hook (ruff auto-fix + mypy)
pytest
Changelog
See CHANGELOG.md for version history.
License
MIT License - see LICENSE file for details.
Credits
Built on top of the excellent djangorestframework-api-key package.
Support
- 📖 Documentation
- 🐛 Report Issues
- 💬 Discussions
- 📧 Email: hello@frankapps.com
About Frankapps
We help startups ship faster with battle-tested Django tools and consulting.
- 🛠️ Open Source Tools - Production-ready packages like this one
- 🚀 Startup Consulting - Django/React architecture and best practices
- 📚 Technical Content - Guides on building scalable APIs
Need help with your Django project? We specialize in helping startups build robust APIs quickly. Get in touch →
Similar Projects
- djangorestframework-api-key - API keys without scopes
- django-oauth-toolkit - OAuth2 with scopes (more complex)
- drf-access-policy - Declarative policies (no API key integration)
DRF Scoped Permissions combines the simplicity of API keys with the flexibility of scoped permissions.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file drf_scoped_permissions-0.3.0.tar.gz.
File metadata
- Download URL: drf_scoped_permissions-0.3.0.tar.gz
- Upload date:
- Size: 31.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aebca53282ee4172be8e2a4229ff5b8299425b9f6b5f2cc9bc5e345bd734859e
|
|
| MD5 |
67e25860fe53ad8d336bcd106bffcac6
|
|
| BLAKE2b-256 |
5482dfa60d1917a65aad0c9fa266084e60f5246cda6f0bbb8cff73d2c830cef3
|
Provenance
The following attestation bundles were made for drf_scoped_permissions-0.3.0.tar.gz:
Publisher:
publish.yml on frankapps-io/drf-scoped-permissions
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
drf_scoped_permissions-0.3.0.tar.gz -
Subject digest:
aebca53282ee4172be8e2a4229ff5b8299425b9f6b5f2cc9bc5e345bd734859e - Sigstore transparency entry: 927315779
- Sigstore integration time:
-
Permalink:
frankapps-io/drf-scoped-permissions@60cbb4f3b055ce8e8c314b2fac8b3fc59699f513 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/frankapps-io
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@60cbb4f3b055ce8e8c314b2fac8b3fc59699f513 -
Trigger Event:
push
-
Statement type:
File details
Details for the file drf_scoped_permissions-0.3.0-py3-none-any.whl.
File metadata
- Download URL: drf_scoped_permissions-0.3.0-py3-none-any.whl
- Upload date:
- Size: 21.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1a7488a69dfa1eb33481475c895bdb793d568d94098e8397abd304d3496529aa
|
|
| MD5 |
e2f1dff898fbd7816d1bc085f93dd6f7
|
|
| BLAKE2b-256 |
e2956fa5f8aa38a304ca333171e3c4fad741551ecefbf38b8b5fb97844e209aa
|
Provenance
The following attestation bundles were made for drf_scoped_permissions-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on frankapps-io/drf-scoped-permissions
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
drf_scoped_permissions-0.3.0-py3-none-any.whl -
Subject digest:
1a7488a69dfa1eb33481475c895bdb793d568d94098e8397abd304d3496529aa - Sigstore transparency entry: 927315786
- Sigstore integration time:
-
Permalink:
frankapps-io/drf-scoped-permissions@60cbb4f3b055ce8e8c314b2fac8b3fc59699f513 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/frankapps-io
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@60cbb4f3b055ce8e8c314b2fac8b3fc59699f513 -
Trigger Event:
push
-
Statement type: