Skip to main content

Production-ready, provider-agnostic authentication library for Django REST Framework applications in the Novah Care ecosystem

Project description

Patronus

Production-ready, provider-agnostic authentication library for Django REST Framework applications in the Novah Care ecosystem.

Features

  • Provider Agnostic: Abstract authentication provider interface with GCIP (Firebase Auth) implementation
  • DRF Compatible: Native Django REST Framework authentication and permission classes
  • Multi-tenant Support: Automatic tenant context injection via middleware
  • Type Safe: Full type annotations with mypy strict mode support
  • Async Ready: Async variants for all provider operations

Requirements

  • Python 3.13+
  • Django 6.0+
  • Django REST Framework 3.14+

Installation

From PyPI (Recommended)

# Install with pip
pip install novah-patronus

# Or with Poetry
poetry add novah-patronus

From GitHub

# Add specific version with Poetry
poetry add git+ssh://git@github.com/Novah-Care/patronus.git@v0.1.0

# Or with pip
pip install git+ssh://git@github.com/Novah-Care/patronus.git@v0.1.0

Or add to your pyproject.toml manually:

[tool.poetry.dependencies]
novah-patronus = { git = "https://github.com/Novah-Care/patronus.git", tag = "v0.1.0" }

From Source (Development)

# Clone the repository
git clone https://github.com/Novah-Care/patronus.git
cd patronus

# Install in editable mode with development dependencies
pip install -e ".[dev]"

Quick Start

1. Configure Django Settings

# settings.py
import os

PATRONUS = {
    # Provider configuration
    "PROVIDER_CLASS": "patronus.providers.gcip.GCIPProvider",

    # Credentials from environment variable (production)
    # JSON strings are auto-parsed, no need for json.loads()
    "PROVIDER_CREDENTIALS": os.environ.get("GCIP_CREDENTIALS"),

    # Or from file path (development)
    # "PROVIDER_CREDENTIALS": "/path/to/service-account.json",

    # Or use application default credentials
    # "PROVIDER_CREDENTIALS": None,

    # Profile loader (implement your own or use mock for testing)
    "PROFILE_LOADER_CLASS": "patronus.profile_loader.MockProfileLoader",
}

# Add Patronus authentication to DRF
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "patronus.PatronusAuthentication",
    ],
}

# Add tenant middleware
MIDDLEWARE = [
    # ... other middleware ...
    "patronus.TenantMiddleware",
]

2. Implement a Profile Loader

# your_app/auth.py
from patronus import ProfileLoader, UserProfile, NoProfileError

class YourProfileLoader(ProfileLoader):
    def load_profile(
        self,
        uid: str,
        email: str | None = None,
        phone_number: str | None = None,
    ) -> UserProfile:
        try:
            user = User.objects.get(identity_provider_uid=uid)
            permissions = user.get_all_permissions()
            return UserProfile(
                company_id=user.company_id,
                permissions=frozenset(permissions),
                profile_type=user.profile_type,
            )
        except User.DoesNotExist:
            raise NoProfileError(f"No profile found for uid: {uid}")

    async def load_profile_async(
        self,
        uid: str,
        email: str | None = None,
        phone_number: str | None = None,
    ) -> UserProfile:
        # Async implementation
        return self.load_profile(uid, email, phone_number)

3. Use Permission Classes

# your_app/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from patronus import HasPermission, HasProfile, IsSameCompany

class PatientListView(APIView):
    permission_classes = [HasPermission("read:patients")]

    def get(self, request):
        # request.user is a NovahUser instance
        return Response({"patients": []})

class PatientDetailView(APIView):
    permission_classes = [HasProfile, IsSameCompany]

    def get(self, request, pk):
        patient = get_object_or_404(Patient, pk=pk)
        self.check_object_permissions(request, patient)
        return Response({"patient": patient.data})

4. Use Tenant Context

from patronus import get_current_company, company_context

def some_service_function():
    company_id = get_current_company()
    if company_id:
        # Filter by tenant
        return Model.objects.filter(company_id=company_id)

# Or use context manager for scoped access
with company_context(some_company_id):
    do_tenant_scoped_work()

API Reference

Authentication

  • PatronusAuthentication: DRF authentication class

User Models

  • NovahUser: Authenticated user with permissions
  • TokenPayload: Raw token data from JWT
  • UserProfile: User profile from database

Context Management

  • get_current_company(): Get current tenant UUID
  • set_current_company(uuid): Set current tenant
  • clear_current_company(): Clear tenant context
  • company_context(uuid): Context manager for scoped access

Permission Classes

  • HasProfile: Require authenticated NovahUser
  • HasPermission(permission): Require specific permission
  • HasAnyPermission(permissions): Require any of listed permissions
  • IsSameCompany: Require same company as resource

Exceptions

  • InvalidTokenError: Token malformed (401)
  • ExpiredTokenError: Token expired (401)
  • RevokedTokenError: Token revoked (401)
  • NoProfileError: No user profile (403)
  • TenantMismatchError: Wrong tenant (403)
  • ProviderError: Provider unavailable (503)

Settings Functions

  • get_settings(): Get Patronus configuration
  • get_provider(): Get configured AuthProvider
  • get_profile_loader(): Get configured ProfileLoader
  • reset_instances(): Reset cached instances (for testing)

Development

Environment Setup

# Clone the repository
git clone https://github.com/Novah-Care/patronus.git
cd patronus

# Create virtual environment
python3.13 -m venv .venv

# Activate virtual environment
# On macOS/Linux:
source .venv/bin/activate

# Upgrade pip
pip install --upgrade pip

# Install package with dev dependencies
pip install -e ".[dev]"

Running Tests

# Run all tests
pytest

# Run tests with verbose output
pytest -v

# Run tests with coverage report
pytest --cov=patronus --cov-report=html

# Run specific test file
pytest tests/test_user.py

# Run tests matching a pattern
pytest -k "test_novah_user"

Code Quality

# Run type checking
mypy src/patronus

# Run linting
ruff check src/patronus tests

# Run linting with auto-fix
ruff check --fix src/patronus tests

# Format code
ruff format src/patronus tests

# Check formatting without changes
ruff format --check src/patronus tests

All-in-One Check (before committing)

# Run all checks
ruff check src/patronus tests && \
ruff format --check src/patronus tests && \
mypy src/patronus && \
pytest --cov=patronus

Project Structure

patronus/
├── src/patronus/
│   ├── __init__.py           # Public API exports
│   ├── authentication.py     # DRF authentication class
│   ├── permissions.py        # DRF permission classes
│   ├── middleware.py         # Tenant middleware
│   ├── context.py            # Tenant context (contextvars)
│   ├── user.py               # NovahUser, TokenPayload, UserProfile
│   ├── exceptions.py         # Exception hierarchy
│   ├── settings.py           # Configuration management
│   ├── profile_loader.py     # ProfileLoader interface
│   ├── decorators.py         # (Phase 2)
│   ├── cache.py              # (Phase 2)
│   └── providers/
│       ├── __init__.py
│       ├── base.py           # AuthProvider ABC
│       └── gcip.py           # GCIP/Firebase implementation
└── tests/
    ├── conftest.py           # Shared fixtures
    └── providers/

Publishing to PyPI

The package is published to PyPI using GitHub Actions with Trusted Publishing (no API tokens needed).

To publish a new version:

  1. Update the version in pyproject.toml
  2. Create a GitHub release with a new tag (e.g., v0.2.0)
  3. The workflow automatically builds and publishes to PyPI

For detailed setup and troubleshooting, see docs/PUBLISHING.md.

OriginGuardMiddleware

Rejects requests that did not transit Cloudflare. Two checks:

  1. Host header must be in ALLOWED_HOSTS (always enforced).
  2. CF-Access-Client-Cert-Verify header must equal SUCCESS (only enforced when ENFORCE_ORIGIN_PULL=True).

The flag allows deploying the middleware with checks disabled, flipping them on once Cloudflare is configured, and flipping off as emergency rollback without redeploying.

Requests whose path starts with any prefix in BYPASS_PATHS skip all checks — intended for Railway's internal health probes which do not transit Cloudflare.

Configuration

# settings.py
PATRONUS_ORIGIN_GUARD = {
    "ENFORCE_ORIGIN_PULL": True,
    "ALLOWED_HOSTS": ["api.novah.care"],
    "BYPASS_PATHS": ["/health/", "/liveness/"],  # Railway internal probes
}

MIDDLEWARE = [
    "apps.core.support.healthcheck.middlewares.LivenessHealthCheckMiddleware",
    "request_id_django_log.middleware.RequestIdDjangoLog",
    "patronus.OriginGuardMiddleware",  # Early rejection, before DB/auth
    # ... rest of middleware
]

Place OriginGuardMiddleware after LivenessHealthCheckMiddleware and the request-id middleware (so rejections still carry a request ID), but before auth/session middleware (no point touching DB for a rejected request).

Rejections are logged at WARNING level under the patronus.origin_guard logger with a structured reason: host_not_allowed, cf_cert_missing, or cf_cert_invalid.

License

MIT License - see LICENSE file for details.

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

novah_patronus-0.3.1.tar.gz (98.8 kB view details)

Uploaded Source

Built Distribution

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

novah_patronus-0.3.1-py3-none-any.whl (24.1 kB view details)

Uploaded Python 3

File details

Details for the file novah_patronus-0.3.1.tar.gz.

File metadata

  • Download URL: novah_patronus-0.3.1.tar.gz
  • Upload date:
  • Size: 98.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for novah_patronus-0.3.1.tar.gz
Algorithm Hash digest
SHA256 70ea80d4009823d65dab0e59c5622ebd120d616776e181d45c4842a13250327e
MD5 d69a5ad58106aa1019ce05055df5925b
BLAKE2b-256 cc77326baa42ca35855554bd2db0d9180272f38e7c7c0857c0143b7b0d44e285

See more details on using hashes here.

Provenance

The following attestation bundles were made for novah_patronus-0.3.1.tar.gz:

Publisher: publish.yml on Novah-Care/patronus

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

File details

Details for the file novah_patronus-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: novah_patronus-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 24.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for novah_patronus-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8adae075ceb290a5321aa802edacdc4450a1e58585c99b7a07dcac3973537f6a
MD5 41f3edb4209a3d0efa0f9080b7da923c
BLAKE2b-256 b18a907d8ca32cc3616673a30bad041f430ebfe75fc320d39040622ce2af30bc

See more details on using hashes here.

Provenance

The following attestation bundles were made for novah_patronus-0.3.1-py3-none-any.whl:

Publisher: publish.yml on Novah-Care/patronus

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