Skip to main content

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.

PyPI version Python versions Django versions Coverage Status License

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

  1. Go to Django Admin → API Keys → Scoped API keys
  2. Click "Add Scoped API Key"
  3. Select scopes (checkboxes are auto-generated from your viewsets)
  4. 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:

  1. scope_resource attribute (explicit) - takes priority
  2. Class name with ViewSet stripped, 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:

  1. Install drf-scoped-permissions
  2. Run migrations: python manage.py migrate drf_scoped_permissions
  3. 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
  1. Migrated keys have empty scopes (unrestricted access) - same as before
  2. Add scopes via Django admin when ready
  3. Once verified, delete legacy keys:
from rest_framework_api_key.models import APIKey
APIKey.objects.all().delete()

From Manual Implementation

  1. Replace your custom models with ScopedAPIKey and ScopedGroup
  2. Replace permission classes with HasAPIKeyOrGroupScope
  3. Update admin to use provided admin classes
  4. 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

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

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

drf_scoped_permissions-0.3.0.tar.gz (31.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

drf_scoped_permissions-0.3.0-py3-none-any.whl (21.5 kB view details)

Uploaded Python 3

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

Hashes for drf_scoped_permissions-0.3.0.tar.gz
Algorithm Hash digest
SHA256 aebca53282ee4172be8e2a4229ff5b8299425b9f6b5f2cc9bc5e345bd734859e
MD5 67e25860fe53ad8d336bcd106bffcac6
BLAKE2b-256 5482dfa60d1917a65aad0c9fa266084e60f5246cda6f0bbb8cff73d2c830cef3

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file drf_scoped_permissions-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for drf_scoped_permissions-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1a7488a69dfa1eb33481475c895bdb793d568d94098e8397abd304d3496529aa
MD5 e2f1dff898fbd7816d1bc085f93dd6f7
BLAKE2b-256 e2956fa5f8aa38a304ca333171e3c4fad741551ecefbf38b8b5fb97844e209aa

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page