mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
23 call sites across hanzo-iam and hanzo-tools-iam reached IAM over the legacy
verb surface — get-user, get-users, get-application(s), get-organization(s),
get-providers, get-role(s), get-user-count, get-user-roles, and the
oauth/access_token spelling. Those routes are being removed: HIP-0111 forbids
them, and a capability with an RFC uses its RFC.
Two things had to change together, which is why this adds routes.py rather than
editing the literals in place. The verb surface answered {status, msg, data} at
HTTP 200 even for a miss; the native surface returns the object at the top level,
uses real status codes, and NAMES its lists — {"users": [...]}, not
{"data": [...]}.
Swapping only the path is the dangerous half-migration, and hanzo-tools-iam shows
why: it did `users = data if isinstance(data, list) else []`, so against a native
response it would have reported ZERO users for a healthy org. Not an error — an
empty list. Both halves changed there.
routes.unwrap reads either shape so a fleet mid-rollout keeps working, and still
RAISES on the legacy error envelope because callers depended on that rather than
on a falsy return. It is deliberately temporary: once every server serves native
only, the envelope branch is dead and goes with it. Only a body whose keys are
exactly the envelope's is unwrapped, so a native row with its own `data` column
survives.
These admin methods had no coverage while they called the verb surface — the
reason migrating them blind was the risk. tests/test_routes.py pins it: no route
may contain "/get-", the token endpoint must be the one discovery advertises,
every list route must declare its key (a missing one unwraps to [] and reads as
empty), and neither client may carry a path literal. Falsified — restore a verb
in the table or in a client and the suite names it.
99 pass. tests/test_fastapi.py cannot collect in this environment
(starlette/httpx version clash, unrelated to this change); hanzo-tools-iam's
suite cannot either (`from mcp.server import FastMCP` against the installed MCP
SDK) — both verified pre-existing by reproducing them with these changes stashed.
Rebased onto upstream's IAM_ROUTE_PREFIX refactor rather than merged: that
refactor centralised the prefix but kept the verbs, so the two changes compose.
Co-authored-by: Hanzo Dev <[email protected]>
hanzoai-iam
Identity and Access Management SDK for the Hanzo ecosystem. Organization-aware multi-tenant IAM for the Hanzo ecosystem.
The PyPI distribution is hanzoai-iam; the import path is hanzo_iam.
Installation
pip install hanzoai-iam
With FastAPI integration:
pip install hanzoai-iam[fastapi]
With KMS support for certificate management:
pip install hanzoai-iam[kms]
Quick Start
from hanzo_iam import IAMClient, IAMConfig
config = IAMConfig(
endpoint="https://iam.hanzo.ai",
client_id="your-client-id",
client_secret="your-client-secret",
org_name="HANZO",
)
client = IAMClient(config)
# Get authorization URL for user login
auth_url = client.get_auth_url(redirect_uri="https://yourapp.com/callback")
# Exchange code for tokens
tokens = client.get_token(code="auth-code-from-callback")
# Get user info
user = client.get_user_info(access_token=tokens.access_token)
Organizations
| Organization | Endpoint | Description |
|---|---|---|
| HANZO | https://iam.hanzo.ai | Hanzo AI platform |
| ZOO | https://iam.zoo.dev | Zoo Labs Foundation |
| LUX | https://iam.lux.network | Lux blockchain network |
| PARS | https://iam.pars.dev | Pars development platform |
Environment Variables
One canonical prefix — IAM_*. No upstream-brand aliases, no per-org variants.
# Required
IAM_ENDPOINT=https://hanzo.id
IAM_CLIENT_ID=your-client-id
IAM_CLIENT_SECRET=your-client-secret
# Optional (defaults shown)
IAM_ORG=hanzo
IAM_APP=app
IAM_CERT=path/to/cert.pem # PEM file or inline PEM content
FastAPI Integration
from fastapi import FastAPI, Depends
from hanzo_iam.fastapi import IAMAuth, get_current_user
from hanzo_iam import IAMConfig, User
app = FastAPI()
config = IAMConfig.from_env()
auth = IAMAuth(config)
@app.get("/protected")
async def protected_route(user: User = Depends(auth.require_user)):
return {"user": user.name, "org": user.owner}
@app.get("/optional")
async def optional_auth(user: User | None = Depends(auth.optional_user)):
if user:
return {"message": f"Hello, {user.name}"}
return {"message": "Hello, anonymous"}
Client Credentials Flow
For service-to-service authentication:
from hanzo_iam import IAMClient, IAMConfig
config = IAMConfig(
endpoint="https://iam.hanzo.ai",
client_id="service-client-id",
client_secret="service-client-secret",
org_name="HANZO",
)
client = IAMClient(config)
# Get service token
tokens = client.get_client_credentials_token()
# Use token for API calls
headers = {"Authorization": f"Bearer {tokens.access_token}"}
Async Client
import asyncio
from hanzo_iam import AsyncIAMClient, IAMConfig
async def main():
config = IAMConfig.from_env()
client = AsyncIAMClient(config)
# All methods are async
tokens = await client.get_client_credentials_token()
user = await client.get_user_info(tokens.access_token)
print(f"Authenticated as: {user.name}")
asyncio.run(main())
API Reference
| Method | Description |
|---|---|
get_auth_url(redirect_uri, state, scope) |
Generate OAuth authorization URL |
get_token(code) |
Exchange authorization code for tokens |
refresh_token(refresh_token) |
Refresh access token |
get_client_credentials_token() |
Get token via client credentials flow |
get_user_info(access_token) |
Get user info from access token |
parse_jwt(token) |
Parse and validate JWT claims |
get_user(user_id) |
Get user by ID |
get_users() |
List all users in organization |
create_user(user) |
Create new user |
update_user(user) |
Update existing user |
delete_user(user_id) |
Delete user |
get_organizations() |
List organizations |
get_organization(name) |
Get organization by name |
License
Apache-2.0