Skip to content

Act as Another Principal

Every request a client sends carries one bearer token, and the token is the principal: it decides what the request may do and who a write is attributed to. Most programs need one. Some need several at once:

  • a service that writes on behalf of the user who made the request;
  • an agent that should only ever read the repository it was given;
  • a job that holds a broad token and hands a narrow one to the code it runs.

You could construct a second WarmHubClient, but that opens a second connection pool. client.with_access_token(token) returns a client that authenticates as token and shares the original client’s pool.

from warmhub import WarmHubClient
client = WarmHubClient.from_env()
reader = client.with_access_token(other_token)
row = reader.repository("acme/sensors").things.get("probe-1")

The call makes no request. The derived client copies the parent’s backend URL, client identity, and client flags, and takes the new token. Everything else about it is an ordinary client, with the same namespaces and repository handles. AsyncWarmHubClient.with_access_token works the same way.

Example: a read-only token for one repository

Section titled “Example: a read-only token for one repository”

In production, the second token usually comes from elsewhere: the user you are acting for, a secret store, or your own identity provider. Here the parent client mints it, so the example needs nothing but the token you started with.

client.token.create takes the request as a mapping. A scope entry binds a resource (org/repo) to permissions; this one grants repo:read on a single repository. Scopes only narrow: a token never holds more than the user who created it. See Personal access tokens for every permission.

from warmhub import Add, WarmHubClient, WarmHubError
client = WarmHubClient.from_env()
sensors = client.repository("acme/sensors")
minted = client.token.create(
{
"name": "sensors-reader",
"scopes": [{"resource": "acme/sensors", "permissions": ["repo:read"]}],
}
)
reader = client.with_access_token(minted.token)
as_reader = reader.repository("acme/sensors")
print(as_reader.things.get("probe-1").data)

minted.token is the only time the token value is returned. token.list and token.get describe a token afterwards but never return its value.

The derived client does not know what its token allows. It sends the request and the server decides. A write with the read-only token is refused with FORBIDDEN:

op = Add(name="probe-2", shapes=["Reading"], data={"temp_celsius": 19.8, "bay_id": "bay-a"})
try:
as_reader.apply("write as the reader", [op])
except WarmHubError as err:
print(err.code, err) # FORBIDDEN Missing scope: repo:write
sensors.apply("write as the parent", [op]) # succeeds

Branch on err.code, not on the message text. The same write through the parent succeeds: each request runs as exactly the principal you sent it with.

with_access_token — and the access_token= constructor argument — also accept a zero-argument callable instead of a string. The client calls it before every request, which suits a token that rotates: a short-lived token refreshed from a secret store, or the current user in a request handler.

def current_token() -> str:
return token_store.get("sensors-reader")
rotating = client.with_access_token(current_token)

WarmHubClient takes a callable that returns a string. AsyncWarmHubClient also accepts one that returns an awaitable.

The parent owns the pool; derived clients borrow it.

  • client.owns_transport is True; reader.owns_transport is False.
  • Closing a derived client does not close the pool. The parent keeps working.
  • Closing the parent closes the pool, and every client derived from it then fails. Close derived clients first, or keep the parent open for as long as they are in use.
reader.close()
sensors.things.get("probe-1") # the parent still works

A token you create for one task is a credential you are responsible for. Revoke it by name when the task ends. Any later request that presents it is refused with UNAUTHENTICATED.

client.token.revoke(minted.name)