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
35 changes: 34 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,39 @@ jobs:
- name: Type-check
run: mypy src
- name: Test
run: pytest -q
run: pytest -q -m "not live"
- name: Build wheel
run: pip install build && python -m build --wheel

live:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install
run: pip install -e ".[dev,grpc]"
# Authorizer v2 is configured via CLI flags (not env vars), so the server
# is started with `docker run` rather than a `services:` block. gRPC needs
# its own port (9091) and --grpc-insecure for plaintext CI testing.
- name: Start Authorizer
run: |
docker run -d --name authorizer \
-p 8080:8080 -p 9091:9091 \
quay.io/authorizer/authorizer:2.3.0 \
--database-type=sqlite --database-url=test.db \
--jwt-type=HS256 --jwt-secret=test \
--admin-secret=admin --client-id=ci-client --client-secret=secret \
--grpc-insecure=true
for i in $(seq 1 30); do
curl -fsS http://localhost:8080/health && break || sleep 2
done
- name: Live integration tests
env:
AUTHORIZER_TEST_URL: http://localhost:8080
AUTHORIZER_TEST_GRPC: localhost:9091
AUTHORIZER_TEST_CLIENT_ID: ci-client
AUTHORIZER_ADMIN_SECRET: admin
AUTHORIZER_PROTOCOLS: graphql,rest,grpc
run: pytest tests/integration -m live -q
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pip install authorizer-py
| `authorizer_url` | Yes | Base URL of your Authorizer instance (no trailing slash) |
| `redirect_url` | No | Default redirect URL used by magic-link and forgot-password flows |
| `extra_headers` | No | Additional headers sent on every request (e.g. custom `Origin`) |
| `protocol` | No | Transport: `"graphql"` (default), `"rest"`, or `"grpc"` |
| `grpc_endpoint` | No | gRPC target `host:port`. The server's gRPC listener runs on a **separate port** (default `9091`), not the HTTP URL's port. When unset, the host is derived from `authorizer_url` and port `9091` is used. Only used when `protocol="grpc"`. |

**Sync:**

Expand Down Expand Up @@ -115,6 +117,31 @@ print("can view:", accessible.objects)
client.close()
```

## gRPC transport

Set `protocol="grpc"` to call the server over gRPC. This requires the optional
gRPC dependencies:

```bash
pip install 'authorizer-py[grpc]'
```

The server's gRPC listener runs on a **separate port** (default `9091`), not the
HTTP URL's port (`8080`). When `grpc_endpoint` is unset, the host is taken from
`authorizer_url` and port `9091` is used; pass `grpc_endpoint` to dial a custom
target explicitly:

```python
from authorizer import AuthorizerClient

client = AuthorizerClient(
client_id="YOUR_CLIENT_ID",
authorizer_url="https://your-instance.authorizer.dev",
protocol="grpc",
grpc_endpoint="your-instance.authorizer.dev:9091", # optional; defaults to host:9091
)
```

## License

Apache-2.0 — see [LICENSE](LICENSE) for details.
154 changes: 154 additions & 0 deletions examples/manual_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""Manual end-to-end smoke test for the Authorizer Python SDK.

Exercises the public client (meta/signup/login/profile) and the admin client
(users/webhooks/FGA) over the protocol you pick.

Setup + run (defaults shown):

python -m venv .venv
./.venv/bin/pip install -e ".[grpc]" # drop [grpc] if you only use graphql/rest

AUTHORIZER_URL=http://localhost:8080 \
CLIENT_ID=test-client \
ADMIN_SECRET=admin \
PROTOCOL=graphql \ # graphql | rest | grpc
./.venv/bin/python examples/manual_test.py

gRPC listens on its own port (default :9091); override with GRPC_ENDPOINT=host:port.
For plaintext gRPC the server must run with --grpc-insecure=true.
"""

from __future__ import annotations

import os
import time

from authorizer import (
AddWebhookRequest,
AuthorizerAdminClient,
AuthorizerClient,
FgaReadTuplesRequest,
FgaTupleInput,
FgaWriteModelRequest,
FgaWriteTuplesRequest,
LoginRequest,
SignUpRequest,
WebhookRequest,
)
from authorizer.exceptions import AuthorizerError

URL = os.getenv("AUTHORIZER_URL", "http://localhost:8080")
CLIENT_ID = os.getenv("CLIENT_ID", "test-client")
ADMIN_SECRET = os.getenv("ADMIN_SECRET", "admin")
PROTOCOL = os.getenv("PROTOCOL", "graphql") # graphql | rest | grpc
GRPC_ENDPOINT = os.getenv("GRPC_ENDPOINT", "")

FGA_MODEL = """model
schema 1.1
type user
type document
relations
define viewer: [user]"""


def step(label: str, fn) -> None:
"""Run fn(), print result; never abort so the whole flow runs."""
try:
result = fn()
print(f"✓ {label:<22} {result}")
except AuthorizerError as exc: # noqa: BLE001 - demo wants every call attempted
print(f"✗ {label:<22} error: {exc}")
except Exception as exc: # noqa: BLE001
print(f"✗ {label:<22} error: {exc}")


def main() -> None:
print(f"== Authorizer Python SDK manual test ==\nurl={URL} protocol={PROTOCOL}\n")

client = AuthorizerClient(
client_id=CLIENT_ID,
authorizer_url=URL,
protocol=PROTOCOL,
grpc_endpoint=GRPC_ENDPOINT,
)

step("get_meta_data", lambda: client.get_meta_data())

email = f"py-manual-{time.time_ns()}@example.com"
step(
"signup",
lambda: client.signup(
SignUpRequest(email=email, password="Test@12345", confirm_password="Test@12345")
),
)

auth = None

def _login():
nonlocal auth
auth = client.login(LoginRequest(email=email, password="Test@12345"))
return f"access_token={'set' if auth.access_token else 'none'}"

step("login", _login)

if auth and auth.access_token:
step(
"get_profile",
lambda: client.get_profile({"Authorization": f"Bearer {auth.access_token}"}),
)

# ---- Admin client (auth via x-authorizer-admin-secret) ----
print("\n-- admin --")
admin = AuthorizerAdminClient(
authorizer_url=URL,
admin_secret=ADMIN_SECRET,
protocol=PROTOCOL,
grpc_endpoint=GRPC_ENDPOINT,
)

step("users", lambda: f"{len(admin.users().users)} user(s)")

webhook_endpoint = "https://example.com/webhook"
step(
"add_webhook",
lambda: admin.add_webhook(
AddWebhookRequest(
event_name="user.login",
endpoint=webhook_endpoint,
enabled=True,
)
),
)

def _list_and_clean():
resp = admin.webhooks()
# Clean up by endpoint: the server appends a "-<timestamp>" suffix to
# event_name (not a stable key); endpoint is stored verbatim.
deleted = 0
for w in resp.webhooks:
if w.endpoint == webhook_endpoint:
admin.delete_webhook(WebhookRequest(id=w.id))
deleted += 1
return f"{len(resp.webhooks)} webhook(s); deleted {deleted}"

step("webhooks + cleanup", _list_and_clean)

# ---- FGA admin ----
print("\n-- fga admin --")
step("fga_write_model", lambda: admin.fga_write_model(FgaWriteModelRequest(dsl=FGA_MODEL)))
fga_object = f"document:{time.time_ns()}" # unique so re-runs don't collide
step(
"fga_write_tuples",
lambda: admin.fga_write_tuples(
FgaWriteTuplesRequest(
tuples=[FgaTupleInput(user="user:alice", relation="viewer", object=fga_object)]
)
),
)
step("fga_read_tuples", lambda: f"{len(admin.fga_read_tuples(FgaReadTuplesRequest()).tuples)} tuple(s)")

print("\ndone.")


if __name__ == "__main__":
main()
32 changes: 30 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,22 @@ classifiers = [
"Programming Language :: Python :: 3.13",
"Typing :: Typed",
]
dependencies = ["httpx>=0.24,<1"]
# protobuf is a base dependency: the REST protocol parses grpc-gateway responses
# with protojson (int64-as-string + single-field wrappers) using the vendored
# proto message types. grpcio is only needed for the optional ``grpc`` protocol.
dependencies = ["httpx>=0.24,<1", "protobuf>=4"]

[project.optional-dependencies]
dev = ["pytest>=7", "pytest-asyncio>=0.23", "respx>=0.20", "ruff>=0.5", "mypy>=1.8"]
grpc = ["grpcio>=1.60", "protobuf>=4"]
dev = [
"pytest>=7",
"pytest-asyncio>=0.23",
"respx>=0.20",
"ruff>=0.5",
"mypy>=1.8",
"grpcio>=1.60",
"protobuf>=4",
]

[project.urls]
Homepage = "https://authorizer.dev"
Expand All @@ -39,6 +51,8 @@ packages = ["src/authorizer"]
[tool.ruff]
line-length = 100
target-version = "py39"
# Vendored generated gRPC/protobuf stubs — never lint.
extend-exclude = ["src/authorizer/_grpc"]

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "W"]
Expand All @@ -47,7 +61,21 @@ select = ["E", "F", "I", "UP", "B", "W"]
python_version = "3.9"
strict = true
warn_unused_ignores = true
# Vendored generated gRPC/protobuf stubs — not type-checked.
exclude = ["src/authorizer/_grpc/"]

[[tool.mypy.overrides]]
module = "authorizer._grpc.*"
ignore_errors = true
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = ["grpc", "grpc.*", "google.protobuf.*"]
ignore_missing_imports = true

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
markers = [
"live: integration tests that hit a real Authorizer server (deselect with -m 'not live')",
]
Loading
Loading