Skip to content

About

Python SDK for the saas accounts API: generated protobuf bindings + gateway-bound facade. Consumed by solutions; never vendored.

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Latest commit

 

History

21 Commits

Folders and files

Repository files navigation

saas-sdk-python

The Python SDK for the saas accounts API — the Python twin of saas-sdk-go. A versioned, published client that solutions depend on instead of regenerating their own bindings.

Everything ships under a single top-level package, saas_sdk, so installing it never claims a generic name (saas, buf, datasource) in the consumer's import namespace.

Packages:

  • saas_sdk._gen — the generated protobuf message bindings (*_pb2), from the accounts public proto at the ref recorded in SOURCE.txt. Only message types are generated; the runtime owns the Connect transport, so there are no client/server stubs. The bindings embed only the accounts protos this SDK exposes (datasource.proto, work_contexts.proto) — the buf.validate / saas.policy custom options, the google.api HTTP annotations, and their shared descriptors are stripped during generation, so this SDK never registers a shared proto into the global descriptor pool (which would collide with a sibling saas SDK). The google.protobuf well-known types stay runtime types.

  • saas_sdk.datasource — a gateway-bound facade over those types. It names the Connect procedure and routes the call through the solution runtime's Gateway.unary(procedure, request, response_type) seam, so a solution connects a GitHub datasource collection in a few lines:

    from saas_sdk import datasource
    
    ds = datasource.new(gateway)
    source = ds.add_github_source(
        org_id=org,
        repo="codefly-dev/module-saas-starter",
        paths=["docs"],
        collection="handbook",
        access_token=token,
    )
    ds.sync(org, source.id)

    gateway is any value exposing unary(procedure, request, response_type) — which solution_runtime.Gateway already satisfies. The runtime stays datasource-agnostic and takes no dependency on this SDK.

  • saas_sdk.work_context — both halves of the x-codefly-work-context feature. Mint side, a delegated caller mints a short-lived context at turn start and stamps it on outgoing calls. Callee side, a service verifies a presented context. See below.

Work Context

A Work Context is a signed, delegated-authority capability carried in the x-codefly-work-context header. saas_sdk.work_context carries the two sides a solution needs: the mint-side client to the accounts authority, and the callee-side verifier. Only the signer itself stays authority-side in sdk-go.

The wire format is not a JWT. A token is base64url(JSON payload).base64url(Ed25519 sig), the payload is a fixed snake_case JSON object, and key_id lives inside the payload. Field order, uint64-as-decimal-string encoding, scope canonicalization, base64url, and Ed25519 signing are pinned byte-for-byte to sdk-go: the wire golden in tests/fixtures/work_context_wire_golden.json is byte-identical to sdk-go's TestWorkContextWireGolden, and CI in both repos verifies the same token.

Minting (delegated caller)

A solution acting on a user's behalf mints a context at turn start and stamps it on every outgoing call, so a downstream service can verify the delegated authority:

from saas_sdk import work_context

wc = work_context.new(gateway)
parent = wc.start_task(
    bearer=user_bearer, org_id=org, task_id=task, session_id=session,
    audience="accounts",
    scopes=[work_context.pb.WorkContextScope(resource_kind="evidence", actions=["read"])],
)
ctx = wc.exchange_audience(bearer=user_bearer, parent=parent, audience="evidence", scopes=scopes)
work_context.attach(headers, ctx)   # per outgoing request

Every mint RPC is owner-bound, so the user's bearer is passed on each call rather than relying on the gateway's ambient service identity; the mint client therefore needs a gateway whose unary takes that bearer. A context lives 5 minutes by default and at most 15 (DEFAULT_TTL / MAX_TTL); renew extends work running past the cap. Mint failures raise WorkContextMintError, distinct from a verification failure.

Verifying (callee)

from saas_sdk import work_context as wc

verifier = wc.JWKSVerifier(
    "https://accounts.codefly.dev/v1/auth/.well-known/jwks.json",
)
verifier.refresh()  # optional: fail closed at boot if key discovery is down

token = wc.token_from_headers(request.headers)
claims = verifier.verify(token, wc.Expectations(
    issuer="https://accounts.codefly.dev/work-context",
    audience="warden.evidence",
))
wc.require_scope(claims, wc.ScopeRequirement("repository", "write", "repo-warden"))

JWKSVerifier discovers Ed25519 keys through the published JWKS endpoint, caches them, and — rotation-aware — refreshes once per cache generation on an unknown key_id. Verifier is the same check against a fixed set of keys. Verification enforces the two-segment shape, the signature, structural validation (including the proto's min_len constraints on optional fields and monotonic scope attenuation), the time window, and the caller's expectations, raising WorkContextError (or WorkContextDenied from require_scope) on any failure. When the key set itself cannot be fetched or read, JWKSVerifier raises WorkContextUnavailable, a WorkContextError subclass, so a consumer can report an authority outage as unavailable rather than as an invalid capability. Replay policy is reported on the claims; enforcing single-use consumption still requires a durable replay store the caller owns.

Current authorization revision

saas_sdk.authorization_revision implements Accounts' current revision protocol for already verified WorkContext claims. It projects the owner and every actor's original scopes, including resource IDs and revision. The consumer keeps signature/lifetime verification, action/resource policy and public error mapping.

from saas_sdk.authorization_revision import MeshGRPCClient, RevisionDenied, RevisionUnavailable

# The host:port Codefly resolved for accounts' private `rest` endpoint.
client = MeshGRPCClient(resolved_accounts_rest_address, current_internal_credential)
try:
    await client.check(verified_claims)
finally:
    await client.aclose()

A current host serves this RPC as cleartext HTTP/2 (h2c) multiplexed onto accounts' private rest endpoint, and leaves in-cluster transport security to the service mesh (module-saas-starter module/INTERNAL_TRANSPORT.md). MeshGRPCClient dials that resolved host:port and adds no TLS; it refuses a URL, a path or credentials in the address. Use it only where the mesh enforces mutual TLS on the hop. GRPCClient("https://…", …) remains for a deployment that terminates server TLS on accounts itself.

Install saas-sdk-python[revision-grpc] for either gRPC client. Where an endpoint explicitly serves Connect, install [revision-connect] and select ConnectClient; selecting that compatibility transport is a composition choice, never an automatic fallback. revision_request(claims) needs neither networking extra and returns the canonical generated protobuf request.

Each check loads the current internal service credential through an async callback and makes one query. It does not cache authorization, retry, inherit environment proxies, follow redirects or forward caller bearer tokens, cookies or Work Context headers. One three-second budget covers credential loading, connection and the complete response; caller cancellation propagates. The callback and any injected fixture transport must cooperate with cancellation. tls_context selects verified trust roots; TLS verification cannot be disabled. Connect's explicit transport injection exists for fixtures and is trusted to implement its own transport security; production should use the default transport.

Only gRPC PermissionDenied/FailedPrecondition, or their coherent Connect status/code pairs, raise RevisionDenied. Credential, transport and malformed response failures raise RevisionUnavailable. Responses must be the canonical empty message, and error strings omit remote details and credentials. Neither error grants authority; consumers explicitly map them onto their public API.

The projection fixture at tests/fixtures/authorization_revision_projection.json is shared byte-for-byte with the Go SDK's authorizationrevision/testdata/projection.json. Owner/actor scope projection is therefore verified independently of transport and consumer policy.

Versioning

This SDK's release version tracks the saas-starter module version (module/module.package.codefly.yaml), recorded in SOURCE.txt alongside the proto ref the bindings were generated from. The protobuf runtime floor is >=5.29.3 (the gencode line the bindings are built with), so the SDK installs alongside consumers still on protobuf 5.x.

Regenerating

The proto source of truth is module-saas-starter/module/services/accounts/proto. From a checkout of that repo:

scripts/generate.sh <module-saas-starter>/module/services/accounts/proto

That builds a descriptor image for datasource.proto, strips the custom options and shared-proto dependencies (scripts/strip_options.py), and regenerates src/saas_sdk/_gen/datasource_pb2.py. Never hand-edit _gen/.

Consuming

from saas_sdk import datasource                 # the facade
from saas_sdk._gen import datasource_pb2         # the message types

About

Python SDK for the saas accounts API: generated protobuf bindings + gateway-bound facade. Consumed by solutions; never vendored.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages