Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"loguru",
"lrange",
"maxdepth",
"mcls",
"modindex",
"Multiprocess",
"noindex",
Expand Down
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Security
-->

## [Unreleased] - YYYY-MM-DD

### Added
- `SQLStorage` - SQL based storage module.
- `StorageModule` and `ComputeModule` now automatically translate exceptions raised in their methods into the method's expected result body.
- Full class and method docstrings for `StorageModule` and `ComputeModule`, including success and error return examples, call examples with the full config body, and notes on the automatic exception translation.
- `ComputeModule` docstring notes that a compute module must handle all errors returned from storage.
- Class docstrings for `InProcessCompute` and `DictStorage`.

### Changed
- `ComputeModule` and `StorageModule` base classes now inherit from ABC.
- `DictStorage` now stores storage latch `created_at` as an ISO 8601 string instead of a `datetime` object.

### Deprecated

### Removed
- `NotImplementedError` since base classes now use auto checks from ABC.

### Fixed
- `InProcessCompute` request and batch request validation
- `get_context_def` / `get_resource_def` now use their own config instead of `get_identity_def`.
- The non-list (`get_*`) identity lookup in `validate_request` now populates the identity lookup and returns the correct identity error message.
- `validate_batch_request` no longer raises `KeyError` on the non-list identity lookup path and no longer silently succeeds for an unregistered root definition in the list path.

### Security


## [0.1.0a6] - 2026-08-27

Support for Authzee spec 0.5.0.
Expand Down
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Authzee is a highly expressive grant-based authorization engine. Check out the [
- [Full Example](#full-example)
- [Development](#development)
- [Compute and Storage Module Development](#compute-and-storage-module-development)
- [Return Values and Error Handling](#return-values-and-error-handling)
- [Configuration](#configuration)
- [Module Caching](#module-caching)


Expand All @@ -46,9 +48,9 @@ pip install authzee[jmespath,sql-storage]
Extra dependencies available/needed:

- `jmespath` - needed if using the built in jmespath execute functions
- `sql-storage` - needed for `SQLStorage` class
- `dev` - development dependencies
- `sql` - needed for `SQLStorage` class
- `all` - for all extra dependencies except for `dev`
- `dev` - development dependencies


## Tutorial
Expand Down Expand Up @@ -509,6 +511,33 @@ The compute and storage modules are meant to be that - modular!

You should be able to build custom ones based off of the base classes `ComputeModule` and `StorageModule`. Note that all underlying methods must be async.

#### Return Values and Error Handling

Every method on a compute or storage module returns a result body (a `dict`) rather than raising on failure.

- On success, populate the result fields and set `error` to `None`.
- On a handled failure, return the result body with its non-`error` fields set to safe defaults and `error` set to an error object (`{"error_type": ..., "message": ...}`). For example:
- A `GenericResult` method returns `{"error": <error>}`.
- A single-item method (like `get_context_def`) returns the item as `None` alongside the error, e.g. `{"context_def": None, "error": <error>}`.
- A page method (like `list_grants`) returns an empty list and a `None` page reference alongside the error, e.g. `{"grants": [], "next_page_ref": None, "error": <error>}`.

You do not have to wrap every method body in a try/except. `ComputeModule` and `StorageModule` use metaclasses (`_ComputeMeta` / `_StorageMeta`) that wrap every method so any raised exception is automatically caught and translated into that method's expected result body, with `error` populated and `error_type` set to `"compute"` or `"storage"` depending on where the exception originated. You can simply raise on unexpected failures and rely on this translation.

A compute module retrieves definitions and grants from a storage module. Since storage methods return errors in their result body rather than raising, a compute module **must** check the `error` field of every storage result it receives and handle it - typically by short-circuiting and returning its own result body with that error propagated (its `error_type` will already be `"storage"`, identifying where the failure originated). Do not ignore storage errors or assume storage calls always succeed.

See the `ComputeModule` and `StorageModule` class and method docstrings for per-method return shapes and success/error examples.

#### Configuration

Every method receives a per-call `config` (a `dict`). A module does **not** have to provide or honor a value for every key its config type allows - a config type describes every option that *could* apply to that call across all module implementations, so any key a given module does not understand can simply be ignored. But a module **should** utilize the config keys that map to behavior it actually implements (for example `page_size` and `use_cache` on list methods, or `use_cache` on get methods), so callers can tune those behaviors.

Each base class documents the config it does **not** use:

- `StorageModule` does not use the nested `get_*` / `use_list_*` / `list_*` sub-configs inside the put and delete definition configs and the repeal config (`PutContextDefConfig`, `DeleteContextDefConfig`, `PutIdentityDefConfig`, `DeleteIdentityDefConfig`, `PutResourceDefConfig`, `DeleteResourceDefConfig`, `RepealConfig`). A storage module acts on the target directly by type or UUID; those nested sub-configs describe an optional "look the target up first" step that belongs to the orchestration layer, not storage.
- `ComputeModule` does not use the storage-only definition/grant persistence and retrieval configs (get/put/delete definition configs, `GetGrantConfig`, `EnactConfig`, `RepealConfig`, and the standalone `List*Config` types) or the storage latch configs, since a compute module has no corresponding operation. When a compute operation does trigger a storage call (such as listing grants during an audit or authorize), it uses the sub-config embedded in the compute config it received rather than a standalone top-level config.

See the `ComputeModule` and `StorageModule` class docstrings for the full, exact list of unused config.

### Module Caching

Caching for validating a request or batch request should be self contained within the compute model per request. Besides that, it is up to the storage module to control caching for storage calls.
12 changes: 5 additions & 7 deletions clr.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
from cleer import cleer_default_config, Cleer
"""cleer config"""

from cleer import Cleer, cleer_default_config


clr = Cleer(
config=cleer_default_config(
python_packages=[
"authzee"
]
)
)
config=cleer_default_config(python_packages=["authzee"])
)
2 changes: 1 addition & 1 deletion full_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
from authzee import (
AuditResultPage,
Authzee,
authzee_specification_version,
BatchAuditResultPage,
DictStorage,
InProcessCompute,
authzee_specification_version,
jmespath_execute,
paginator
)
Expand Down
76 changes: 58 additions & 18 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,50 @@
"""noxfile"""

import sys

import nox


nox.options.sessions = [
"build-docs",
"unit-tests-versions"
]


@nox.session(name="build-docs")
def build_docs(session: nox.Session):
"""Build the documentation.
"""
if "--no-venv" not in sys.argv:
dev_venv_setup(session=session)

session.run("rm", "-rf", "./docs/_build/",
session.run(
"rm",
"-rf",
"./docs/_build/",
external=True
)
session.run("sphinx-build", "-b", "html", "./docs", "./docs/_build/html/")
session.run(
"sphinx-build",
"-b",
"html",
"./docs",
"./docs/_build/html/"
)


@nox.session(
name="docs-server",
venv_backend="none"
)
@nox.session(name="docs-server", venv_backend="none")
def docs_server(session: nox.Session):
"""Run a local server for the docs at http://localhost:7999/index.html
"""
session.run("python", "-m", "http.server", "-d", "docs/_build/html/", "7999")
session.run(
"python",
"-m",
"http.server",
"-d",
"docs/_build/html/",
"7999"
)


@nox.session(name="publish")
Expand All @@ -37,23 +53,41 @@ def publish(session: nox.Session):
"""
dev_venv_setup(session=session)
session.run(
"rm", "-rf", "./build/", "./dist/",
"rm",
"-rf",
"./build/",
"./dist/",
external=True
)
session.run("python", "-m", "build", "--sdist", "--wheel")
session.run("twine", "upload", "dist/*", "--repository", "authzee")
session.run(
"python",
"-m",
"build",
"--sdist",
"--wheel"
)
session.run(
"twine",
"upload",
"dist/*",
"--repository",
"authzee"
)


@nox.session(
name="unit-tests",
python=False
)
@nox.session(name="unit-tests", python=False)
def unit_tests(session: nox.Session):
"""Run tests with current python version and generate html coverage report.
"""
session.run("coverage", "erase")
session.run("pytest", "-vvv",
"--cov=src/authzee", "--cov-report", "html", "--cov-report", "term",
session.run(
"pytest",
"-vvv",
"--cov=src/authzee",
"--cov-report",
"html",
"--cov-report",
"term",
"tests/unit"
)

Expand All @@ -72,10 +106,16 @@ def unit_tests_versions(session: nox.Session):
"""
dev_venv_setup(session=session)
session.run("coverage", "erase")
session.run("pytest", "-vvv", "--cov=src/authzee", "--cov-report", "term-missing", "tests/unit")
session.run(
"pytest",
"-vvv",
"--cov=src/authzee",
"--cov-report",
"term-missing",
"tests/unit"
)


def dev_venv_setup(session: nox.Session):
session.install("-U", "pip", "build")
session.install("-e", ".[dev,all]")

4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ dependencies = [

[project.optional-dependencies]
jmespath = ["jmespath"]
all = ["authzee[jmespath]"]
sql = ["SQLAlchemy"]
all = ["authzee[jmespath,sql]"]
dev = [
"aiosqlite",
"build",
"coverage",
"moto[s3,server]",
Expand Down
2 changes: 1 addition & 1 deletion src/authzee/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
or [](authzee.authzee_async.AuthzeeAsync) for asyncio support!
"""

__version__ = "0.1.0a6"
__version__ = "0.1.0a7"

__all__ = [
"Authzee",
Expand Down
63 changes: 63 additions & 0 deletions src/authzee/_module_meta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Compute and Storage Module Base Meta class and method handlers"""

__all__ = []

from abc import ABCMeta
import functools
from typing import Any, Callable

from authzee.types import GenericResult


def _generic_result_handler(func, error_type):
@functools.wraps(func)
async def wrapper(self, *args, **kwargs) -> GenericResult:
try:
return await func(self, *args, **kwargs)

except Exception as exc:
return {
"error": {
"error_type": error_type,
"message": f"[{exc.__class__.__qualname__}] {exc}"
}
}

return wrapper


def _make_result_handler(default_fields: dict[str, Any]) -> Callable:
def handler(func, error_type):
@functools.wraps(func)
async def wrapper(self, *args, **kwargs):
try:
return await func(self, *args, **kwargs)

except Exception as exc:
result = dict(default_fields)
result['error'] = {
"error_type": error_type,
"message": f"[{exc.__class__.__qualname__}] {exc}"
}

return result

return wrapper

return handler


class _ModuleMeta(ABCMeta):
_error_type: str = "unknown"
_handler_map: dict[str, Callable] = {}


def __new__(mcls, name: str, bases, namespace: dict[str, Any]):
for attr_name, attr_value in namespace.items():
if (
attr_name in mcls._handler_map
and getattr(attr_value, "__isabstractmethod__", False) is False
):
namespace[attr_name] = mcls._handler_map[attr_name](attr_value, mcls._error_type)

return super().__new__(mcls, name, bases, namespace)
Loading
Loading