# PyAuthz

**Framework-agnostic authorization for modern Python.**

> *The pyauthz stands between your application and unauthorized access.*

RBAC, ABAC, and Policy-Based Access Control — unified, typed, Pydantic-native, async-first, and auditable out of the box.

## Install

```bash
pip install pyauthz
```

## 30-Second Quickstart

```python
from pyauthz import PyAuthz, Subject, Resource, Role

rp = PyAuthz()
rp.add_role(Role(name="admin", permissions=["*:*"]))

user = Subject(id="alice", roles=["admin"])
doc = Resource(id="doc-1", type="documents")

rp.is_allowed(user, "read", doc)  # True — action auto-expands to "documents:read"
```

Three lines after imports. No policies to define, no config to write. As complexity grows, add explicit policies, conditions, and YAML files.

## Why PyAuthz?

| Feature | Casbin | Oso | PyAuthz |
|---------|--------|-----|---------|
| Framework-agnostic | Yes | Yes | **Yes** |
| Pydantic/Typed | No | No | **Yes** |
| Async-first | Partial | No | **Yes** |
| Embeddable | Yes | **Deprecated** | **Yes** |
| Auditable | No | No | **Yes** |
| Maintained | Yes | **No** | **Yes** |

## Features

- **Progressive complexity** — 3 lines to start, scales to enterprise patterns
- **Pythonic** — Pydantic models, type hints, decorators. No custom DSL.
- **Async-native, sync-friendly** — async engine with synchronous wrappers
- **Auditable** — structured Decision objects with full reasoning chains
- **Policy-as-code** — Python objects and/or YAML files, Git-friendly
- **Deny by default** — closed-by-default is the secure starting point

## Define Policies

### In Python

```python
from pyauthz import Policy, Effect, Condition

admin_access = Policy(
    name="admin-full-access",
    effect=Effect.ALLOW,
    roles=["admin"],
    permissions=["*:*"],
)

own_docs_only = Policy(
    name="owner-edit-documents",
    effect=Effect.ALLOW,
    roles=["editor"],
    permissions=["documents:write", "documents:delete"],
    conditions=[
        Condition(field="resource.owner_id", op="==", value="subject.id"),
    ],
)
```

### In YAML

```yaml
# policies/rbac.yaml
version: 1

roles:
  - name: admin
    permissions: ["*:*"]
  - name: editor
    permissions: [documents:write, documents:read]

policies:
  - name: admin-full-access
    effect: allow
    roles: [admin]
    permissions: ["*:*"]
```

## Check Access

```python
from pyauthz import PyAuthz, Subject, Resource, Denied

rp = PyAuthz()
rp.load_policies("policies/")

user = Subject(id="alice", roles=["editor"], attributes={"department": "engineering"})
doc = Resource(id="doc-123", type="documents", owner_id="alice")

# Simple bool check
if rp.is_allowed(user, "write", doc):
    ...

# Decision with reasoning (raises Denied on deny)
decision = rp.authorize(user, "documents:write", doc)
print(decision.explain())

# Handle denial
try:
    rp.authorize(user, "pii:read", pii_record)
except Denied as e:
    print(e.decision.reasoning)
```

## Async Support

```python
decision = await rp.aauthorize(user, "documents:write", doc)
allowed = await rp.ais_allowed(user, "documents:write", doc)
```

## Audit Logging

```python
from pyauthz import AuditLogger, JsonExporter

audit = AuditLogger(exporters=[JsonExporter(path="logs/authz.jsonl")])
rp.set_audit_logger(audit)
# Every authorize() call now produces structured audit events
```

## Error Handling

PyAuthz follows **fail loud on load, fail safe on evaluate**:

- Invalid policies raise `SchemaError` at load time
- Missing attributes produce safe `DENY` decisions at evaluation time (never crash)
- The `Denied` exception carries the full `Decision` object

## License

Apache 2.0
