Metadata-Version: 2.4
Name: pkg-auth
Version: 3.0.0
Summary: Clean-architecture auth core for multiple Python frameworks
Author-email: Fritill <info@fritill.ae>
License: MIT
Project-URL: Homepage, https://github.com/fritill-team/fri_pkg_auth
Project-URL: Repository, https://github.com/fritill-team/fri_pkg_auth
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.28.1
Requires-Dist: pyjwt[crypto]>=2.10.1
Requires-Dist: requests>=2.32.3
Provides-Extra: dev
Requires-Dist: pytest<9.0.0,>=8.0.0; extra == "dev"
Requires-Dist: pytest-asyncio<1.0.0,>=0.23.0; extra == "dev"
Requires-Dist: mypy<2.0.0,>=1.11.0; extra == "dev"
Requires-Dist: types-requests<3.0.0.0,>=2.32.0.0; extra == "dev"
Requires-Dist: types-PyJWT<2.0.0,>=1.7.0; extra == "dev"
Requires-Dist: testcontainers[postgres,redis]>=4.0; extra == "dev"
Provides-Extra: acl-sqlalchemy
Requires-Dist: sqlalchemy<3.0,>=2.0; extra == "acl-sqlalchemy"
Requires-Dist: asyncpg>=0.29; extra == "acl-sqlalchemy"
Requires-Dist: alembic>=1.13; extra == "acl-sqlalchemy"
Provides-Extra: acl-django
Requires-Dist: django>=4.2; extra == "acl-django"
Requires-Dist: psycopg[binary]>=3.1; extra == "acl-django"
Provides-Extra: cache-redis
Requires-Dist: redis>=5.0; extra == "cache-redis"
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.115; extra == "fastapi"
Requires-Dist: starlette>=0.37; extra == "fastapi"
Provides-Extra: django
Requires-Dist: django>=4.2; extra == "django"
Provides-Extra: strawberry
Requires-Dist: strawberry-graphql>=0.255; extra == "strawberry"
Provides-Extra: all
Requires-Dist: django>=6.0; extra == "all"
Requires-Dist: fastapi>=0.115; extra == "all"
Requires-Dist: starlette>=0.37; extra == "all"
Requires-Dist: django>=4.2; extra == "all"
Requires-Dist: psycopg[binary]>=3.1; extra == "all"
Requires-Dist: strawberry-graphql>=0.255; extra == "all"
Requires-Dist: sqlalchemy<3.0,>=2.0; extra == "all"
Requires-Dist: asyncpg>=0.29; extra == "all"
Requires-Dist: alembic>=1.13; extra == "all"
Requires-Dist: redis>=5.0; extra == "all"

# pkg-auth

Clean-architecture **identity + ACL** for multi-framework Python services. Handles JWT authentication (via Keycloak) and database-backed authorization (users, organizations, roles, permissions, memberships) in a single package with first-class support for **FastAPI**, **Django**, and **Strawberry GraphQL**.

> **v1.0 is a breaking change from v0.x.** The old claim-based authorization model (`AccessContext`, `AccessRights`, `require_permissions`) is replaced by a real ACL database. See [`docs/MIGRATION_v1.md`](docs/MIGRATION_v1.md) for the upgrade guide.

## Install

```bash
# Core (identity only — no DB deps)
pip install pkg-auth

# With ACL + FastAPI (most common for itqadem services)
pip install pkg-auth[acl-sqlalchemy,fastapi]

# With ACL + Django
pip install pkg-auth[acl-django,django]

# With optional Redis cache
pip install pkg-auth[cache-redis]
```

## Quickstart (FastAPI)

```python
from fastapi import Depends, FastAPI
from pkg_auth.authentication import IdentityContext
from pkg_auth.authorization import AuthContext
from pkg_auth.integrations.fastapi import (
    create_authentication,
    make_get_auth_context,
    require_permission,
)

# --- Wire authentication + authorization ---

auth = create_authentication(
    keycloak_base_url="https://auth.example.com",
    realm="itqadem",
    audience="courses-service",
)

# Mode B (consumer — the common case): pass resolve_user_use_case.
# Mode A (source-of-truth): pass sync_user_use_case instead. Exactly
# one of the two is required; passing both raises ValueError.
get_auth_context = make_get_auth_context(
    get_identity=auth.get_identity,
    resolve_user_use_case=resolve_user,   # or: sync_user_use_case=sync_user (Mode A)
    resolve_use_case=resolve,
    organization_repo=org_repo,
)

app = FastAPI()

# --- Use in routes ---

@app.get("/courses/{id}")
async def get_course(
    id: str,
    bundle: tuple[IdentityContext, AuthContext] = Depends(
        require_permission("course:view", get_auth_context=get_auth_context)
    ),
):
    identity, auth_ctx = bundle
    return {"course_id": id, "role": str(auth_ctx.role_name)}
```

See [`examples/itqadem_courses_app`](examples/itqadem_courses_app) for a complete working example.

## Architecture

```
pkg_auth/
  authentication/             JWT validation → IdentityContext (identity only)
  authorization/              Full ACL (users, orgs, roles, perms, memberships)
    domain/                   Pure entities, ports (Protocol), exceptions
    application/use_cases/    Business logic (13 use cases)
    adapters/
      sqlalchemy/             Canonical schema + Alembic migration + repos
      django_orm/             Mirror models (managed=False) + repos
      cache/                  InMemoryTTLCache / RedisCache + decorator
  integrations/
    fastapi/                  Deps + require_permission + exception handlers
    django/                   Middleware + decorators
    strawberry/               Context getter + permission classes
  admin/                      Keycloak admin client (user provisioning)
```

**Layering rules**: domain has zero external imports; application imports only domain; adapters import their framework; integrations import everything.

## Documentation

- [Authorization model](docs/Authorization.md) — schema, permission catalog, roles, memberships
- [Caching](docs/Caching.md) — InMemoryTTLCache, RedisCache, invalidation contract
- [FastAPI Integration](docs/FastAPI.md)
- [Django Integration](docs/Django.md)
- [Strawberry Integration](docs/Strawberry.md)
- [Keycloak Admin](docs/Keycloak-Admin.md)
- [Migration from v0.x](docs/MIGRATION_v1.md)
