better/compute
IAM integration

Actions and resources

The compute:* actions and compute-* resource types Better Compute adds to Better IAM, the method-to-action table, list filtering and cross-tenant refusal.

Every protected method asks Better IAM one question: may this caller perform this action on this resource in this ? Actions are written compute:<area>:<verb>, such as compute:reservations:create. Resources are written <type>/<id>, such as compute-pool/{poolId}. Both come from compute's catalog, which the bridge installs into every Better IAM host through bridge.plugin(), and they are the names you write policies against.

Resource types

All of compute's resource types are app-owned: nothing is ever registered in Better IAM. When IAM needs a resource, it calls compute's , which answers from the memory-resident . The prefix is compute- by default; typePrefix changes it (it must match ^[a-z][a-z0-9-]{0,31}$) and must be the same in betterCompute() and createComputeIamBridge().

TypeWhat it isParentRelationsDeclared attributes
compute-fleetThe platform fleet. A scope type whose only id is global.
compute-tenantA tenant's compute scope. A scope type whose id is the tenant id.
compute-projectA member, admin
compute-siteA boot and failure domainregion, iamRegion, kind
compute-poolA scheduling and policy domainviewer, user, operator, adminregion, class, gpuModel, gpuMemoryGb, sharing
compute-nodeA machinecompute-pooloperatorregion, gpuModel, gpuCount, lifecycle, trust, dedicatedTo
compute-reservationA GPU reservation, decided while it is still a compute-poolregion, gpuModel, gpuCount, durationHours, binding, projectId, spot
compute-instanceA VPS instancecompute-poolvcpu, memoryGiB, gpus, imageId, imageVerified, projectId
compute-imageAn OS or ramdisk imagevisibility, verified
compute-profileA provisioning profile
compute-provider-accountAn account with an external capacity provider
compute-sandboxA sandbox (v0.5): a VPS instance with a template and a lifetime, decided while it is still a draftcompute-pooltemplateId, vcpu, memoryGiB, gpus, ttlSeconds, warm, and fromSnapshot on a restore
compute-sandbox-templateA tenant's sandbox template (v0.5): pool, flavor, image, runtime, TTL bounds, warm pool
compute-sandbox-snapshotA sandbox's snapshot (v0.5): disk and memory, tenant-private, owned by its sandbox's owner whoever took it; may outlive its sandboxsandboxId, templateId, includeMemory
compute-sandbox-volumeA tenant's persistent sandbox volume (v1.1, agent memory): a disk on one host of a pool that its sandboxes attach across runs, owned by whoever created it; decided before it exists on createcompute-poolsizeGiB (pending on resize)

Besides a row's attributes, every resolved resource carries ownerId when the row has an owner, and the prefixed parentType and parentId of its parent. Better IAM turns the parent into resource.parentRelations, so a policy on a reservation can test the caller's relations on its pool. The attributes each row actually carries today are listed under Resource attributes.

Scope types

Three types exist only as scopes, so a decision about "everything in this tenant" or "the whole fleet" has a resource to name:

  • compute-fleet/global resolves only in the root tenant. Fleet-wide methods are decided in the root tenant, whatever tenantId their input names.
  • compute-tenant/{t} resolves only when its id is the decision's tenant.
  • compute-project/{id} resolves in the project's owner tenant.

How the resolver answers

The resolver runs inside Better IAM's locked decision transaction, so a hit must do no I/O:

  • It echoes { tenantId, type, id } exactly. Better IAM refuses anything else as RESOURCE_MISMATCH.
  • A row missing from memory costs one primary-key read, bounded to 20 ms. A row that does not exist is NOT_FOUND, which Better IAM does not audit and which aborts a whole authorizeMany batch.
  • A lookup that times out, or a projection that is not loaded yet, denies (ACCESS_DENIED).
  • Types without compute's prefix go to the hostFallback you passed to bridge.resolveResource(), or fail with RESOURCE_RESOLVER_REQUIRED.

Each process loads the projection at start and then tails authz_changes about once a second. Staleness can cause a spurious denial, never a wrong mutation, because compute re-validates inside its own transaction.

Admissions never decide on a stale copy. After an admission's first transaction commits the draft (or the changed row, as in reservations.extend), the envelope calls the optional IamPort.refreshResource(tenantId, resource) before iam.require. The embedded port from bridge.embedded() implements it with AuthzProjection.refresh(type, id, tenantId), which re-reads that one row; on a miss or a failed read it drops the row, so the decision takes the bounded miss path above and fails closed. Before this, reservations.extend decided on the row cached at creation, so a BoundedExtend-style policy capped at 72 hours could be passed by one extra hour within the tail interval. tests/server-example-policies.test.ts runs the example policies against real drafts to hold this.

In remote mode the IAM host decides on its replica's copy, so pass refresh: { url, secret, sql? } to bridge.remote() (or remoteIam(), which it wraps): the port then posts a signed { tenantId, type, id, lsn } to the IAM host's refresh handler (computeIamHost().refreshHandler), which waits at most 2 seconds until its replica has replayed compute's WAL position from after transaction 1 and re-reads the row. Any failure fails the admission with UNAVAILABLE (fail closed). Without refresh the remote port has no refreshResource, remote admissions can decide on a row up to the replica lag plus 1 second old, and better-compute doctor warns iam.refresh.

The remote port forwards only identity and client-context headers with the caller's credential (authorization, cookie when there is no bearer, origin, user-agent, x-request-id), never the client's own x-forwarded-for, x-real-ip or forwarded (e7092bf, security review 14 L3): behind a proxy that appends to them, a caller could choose the request.sourceIp its policy conditions see. The address IAM sees is Credential.sourceIp, which the host verified and the port sends as x-forwarded-for. compute.nodeHandler sets it from the socket's peer address; a fetch host passes it as compute.handler(request, { peer: { address } }) (d269738), from the socket or from the hop a proxy it trusts reported. Without it IAM sees no source address. See hosting the API.

Actions

The core catalog declares 56 actions. Most areas have a read action and a manage, create or finer verb. Where an action appears on a scope type as well as on an item type, the scope form gates list and create calls and the item form gates calls on one resource:

ActionResource types that carry it
compute:fleet:admincompute-fleet
compute:sites:readcompute-tenant, compute-site
compute:sites:managecompute-fleet, compute-tenant, compute-site
compute:pools:readcompute-tenant, compute-pool
compute:pools:createcompute-fleet, compute-tenant
compute:pools:managecompute-pool
compute:pools:sharecompute-pool
compute:capacity:readcompute-tenant, compute-pool
compute:quotas:readcompute-tenant
compute:quotas:managecompute-fleet
compute:join-tokens:create, compute:join-tokens:read, compute:join-tokens:revokecompute-pool
compute:flavors:read, compute:flavors:managecompute-pool
compute:nodes:readcompute-tenant, compute-node
compute:nodes:provisioncompute-pool, compute-node
compute:nodes:update, compute:nodes:cordon, compute:nodes:drain, compute:nodes:power, compute:nodes:exec, compute:nodes:console, compute:nodes:ssh-root, compute:nodes:delete, compute:nodes:revoke-identitycompute-node
compute:workflows:read, compute:workflows:managecompute-node
compute:reservations:readcompute-tenant, compute-reservation
compute:reservations:create, compute:reservations:extend, compute:reservations:releasecompute-reservation
compute:instances:readcompute-tenant, compute-instance
compute:instances:create, compute:instances:update, compute:instances:exec, compute:instances:deletecompute-instance
compute:projects:read, compute:projects:managecompute-tenant, compute-project
compute:usage:readcompute-tenant, compute-project
compute:events:read, compute:webhooks:managecompute-tenant
compute:secrets:managecompute-fleet, compute-tenant
compute:drivers:readcompute-fleet, compute-tenant
compute:profiles:read, compute:profiles:managecompute-tenant, compute-profile
compute:images:readcompute-tenant, compute-image
compute:images:publishcompute-fleet, compute-tenant, compute-image
compute:providers:readcompute-tenant, compute-provider-account
compute:providers:managecompute-fleet, compute-tenant, compute-provider-account
compute:sandboxes:read, compute:sandboxes:managecompute-tenant, compute-sandbox-template, compute-sandbox-volume (read also on compute-sandbox and compute-sandbox-snapshot)
compute:sandboxes:create, compute:sandboxes:update, compute:sandboxes:exec, compute:sandboxes:deletecompute-sandbox (delete also on compute-sandbox-snapshot)
compute:batches:read, compute:batches:create, compute:batches:approvecompute-tenant (v1.0 destructive-batch quorum)

Adding actions

Modules and plugins extend the catalog with a catalog fragment, which createComputeIamBridge() merges into the types above. The built-in modules add none: the core catalog covers all of their methods. The merge enforces Better IAM's limits and fails with INVALID_CONFIG on a breach:

  • An action matches ^compute:[a-z][a-z0-9-]*(:[a-z][a-z0-9-]*)+$. A plugin's actions must start with compute:<pluginId>:.
  • A type has at most 64 attributes and 32 relations, and its name at most 64 characters. Attribute types are string, number or boolean.
  • Two fragments may not disagree on a type's parent or on an attribute's type. A parent must be in the catalog.
acme-autoinstall.ts
import { definePlugin } from 'better-compute/core';

export const acmeAutoinstall = () =>
  definePlugin({
    id: 'acme-autoinstall',
    catalog: {
      // Merged into compute-node; plugin actions live under compute:<id>:.
      resourceTypes: { node: { actions: ['compute:acme-autoinstall:render'] } },
    },
    // api(context) { ...methods that declare action 'compute:acme-autoinstall:render' }
  });

Pass the plugin to both betterCompute({ plugins }) and createComputeIamBridge({ plugins }), so the API and every IAM host agree. initialize() checks the method registry against the merged catalog: a method whose action is missing (uncatalogued) or that disagrees with its method-table row (mismatch) fails with INVALID_CONFIG, and a non-plugin method with no row (undeclared) is a catalog.undeclared warning in better-compute doctor. See Modules and Methods.

Method-to-action table

This table is rendered from the live method registry. The normative source is PLAN.md §9.2, kept as apiSurface in @better-compute/core: scripts/authz-matrix.mjs generates one test per row, and tests/iam-bridge-matrix.test.ts checks every action and type pair against a real Better IAM carrying the bridge, allowing each only when granted. It decides through compute's own IamPort: bridge.embedded under pnpm test, and bridge.remote over the IAM's HTTP API under pnpm test:iam-remote.

  • Decided on is the resource type of the one decision. Its id comes from the input: the pool for compute-pool, the tenant for compute-tenant, global for compute-fleet.
  • or means the method decides on one of two resources depending on its input. For example, sites.create decides on compute-fleet for shared and dedicated sites and on compute-tenant for a tenant's custom site, and usage.list decides on compute-project when the input names a project.
  • + items marks a list that follows the list recipe.
  • planned marks a method this release does not serve yet; it arrives in the phase shown.

Platform and tenancy

MethodActionDecided onPhase
system.whoaminone (public)nothingv0.1
system.versionnone (public)nothingv0.1
system.selfCheckcompute:fleet:admincompute-fleetv0.1
projects.createcompute:projects:managecompute-tenantv0.1
projects.getcompute:projects:readcompute-projectv0.1
projects.listcompute:projects:readcompute-tenant + itemsv0.1
projects.archivecompute:projects:managecompute-projectv0.2
sites.listcompute:sites:readcompute-tenant + itemsv0.1
sites.getcompute:sites:readcompute-sitev0.1
sites.createcompute:sites:managecompute-fleet or compute-tenantv0.1
sites.updatecompute:sites:managecompute-sitev0.2
sites.deletecompute:sites:managecompute-sitev0.2
siteTokens.createcompute:sites:managecompute-sitev0.2
siteTokens.revokecompute:sites:managecompute-sitev0.2
secrets.putcompute:secrets:managecompute-tenant or compute-fleetv0.2
secrets.deletecompute:secrets:managecompute-tenant or compute-fleetv0.2
drivers.listcompute:drivers:readcompute-tenantv0.2

Nodes and access

MethodActionDecided onPhase
joinTokens.createcompute:join-tokens:createcompute-poolv0.1
joinTokens.listcompute:join-tokens:readcompute-poolv0.1
joinTokens.revokecompute:join-tokens:revokecompute-poolv0.1
joinRequests.listcompute:join-tokens:readcompute-poolv0.2
joinRequests.approvecompute:join-tokens:createcompute-poolv0.2
joinRequests.rejectcompute:join-tokens:createcompute-poolv0.2
nodes.listcompute:nodes:readcompute-tenant + itemsv0.1
nodes.getcompute:nodes:readcompute-nodev0.1
nodes.eventscompute:nodes:readcompute-nodev0.1
nodes.cordoncompute:nodes:cordoncompute-nodev0.1
nodes.uncordoncompute:nodes:cordoncompute-nodev0.1
nodes.draincompute:nodes:draincompute-nodev0.1
nodes.labelcompute:nodes:updatecompute-nodev0.1
nodes.taintcompute:nodes:updatecompute-nodev0.1
nodes.adoptcompute:nodes:provisioncompute-nodev0.1
nodes.revokeIdentitycompute:nodes:revoke-identitycompute-nodev0.1
nodes.deletecompute:nodes:deletecompute-nodev0.2
nodes.issueSshCertcompute:nodes:ssh-rootcompute-nodev0.3
nodes.revokeSshCertcompute:nodes:ssh-rootcompute-nodev0.3
nodes.listSshCertscompute:nodes:readcompute-nodev0.3
inventory.getcompute:nodes:readcompute-nodev0.1
inventory.historycompute:nodes:readcompute-nodev0.2
inventory.diffcompute:nodes:readcompute-nodev0.2
commands.runcompute:nodes:execcompute-nodev0.1
commands.getcompute:nodes:readcompute-nodev0.1
commands.listcompute:nodes:readcompute-nodev0.1
commands.cancelcompute:nodes:execcompute-nodev0.2
commandBatches.createcompute:batches:createcompute-tenantv1.0
commandBatches.get, list, scopecompute:batches:readcompute-tenantv1.0
commandBatches.approve, cancelcompute:batches:approve (on top of the approver signatures, never instead of them)compute-tenantv1.0
tunnels.opencompute:instances:execcompute-instance or compute:nodes:console on compute-nodev0.3

Capacity

MethodActionDecided onPhase
pools.listcompute:pools:readcompute-tenant + itemsv0.1
pools.getcompute:pools:readcompute-poolv0.1
pools.createcompute:pools:createcompute-fleet or compute-tenantv0.1
pools.capacitycompute:capacity:readcompute-poolv0.1
pools.offercompute:pools:sharecompute-poolv0.1
pools.unoffercompute:pools:sharecompute-poolv0.1
pools.updatecompute:pools:managecompute-poolv0.2
pools.archivecompute:pools:managecompute-poolv0.2
capacity.summarycompute:capacity:readcompute-tenantv0.1
capacity.byPoolcompute:capacity:readcompute-poolv0.2
capacity.previewcompute:capacity:readcompute-poolv0.2
capacity.timelinecompute:capacity:readcompute-poolv0.2
reservations.createcompute:reservations:createcompute-reservation (the draft)v0.1
reservations.getcompute:reservations:readcompute-reservationv0.1
reservations.listcompute:reservations:readcompute-tenant + itemsv0.1
reservations.releasecompute:reservations:releasecompute-reservationv0.1
reservations.cancelcompute:reservations:releasecompute-reservationv0.2
reservations.extendcompute:reservations:extendcompute-reservationv0.1
reservations.renewcompute:reservations:extendcompute-reservationv0.1
quotas.getcompute:quotas:readcompute-tenantv0.1
quotas.setcompute:quotas:managecompute-fleetv0.2

Usage and events

MethodActionDecided onPhase
usage.listcompute:usage:readcompute-tenant or compute-projectv0.1
usage.exportcompute:usage:readcompute-tenant or compute-projectv0.2
events.listcompute:events:readcompute-tenantv0.1
webhooks.createcompute:webhooks:managecompute-tenantv0.3
webhooks.listcompute:webhooks:managecompute-tenantv0.3
webhooks.getcompute:webhooks:managecompute-tenantv0.3
webhooks.updatecompute:webhooks:managecompute-tenantv0.3
webhooks.rotateSecretcompute:webhooks:managecompute-tenantv0.3
webhooks.deletecompute:webhooks:managecompute-tenantv0.3
webhooks.pingcompute:webhooks:managecompute-tenantv0.3
webhooks.deliveriescompute:webhooks:managecompute-tenantv0.3
webhooks.redelivercompute:webhooks:managecompute-tenantv0.3
webhooks.eventTypescompute:webhooks:managecompute-tenantserved, no §9.2 row

Provisioning and bare metal

MethodActionDecided onPhase
profiles.createcompute:profiles:managecompute-tenantv0.2
profiles.getcompute:profiles:readcompute-profilev0.2
profiles.validatecompute:profiles:readcompute-profilev0.2
profiles.rendercompute:profiles:readcompute-profilev0.2
profiles.listcompute:profiles:readcompute-tenant + itemsv0.2
profiles.updatecompute:profiles:managecompute-profilev0.2
profiles.deletecompute:profiles:managecompute-profilev0.2
images.registercompute:images:publishcompute-tenant or compute-fleetv0.2
images.getcompute:images:readcompute-imagev0.2
images.listcompute:images:readcompute-tenant + itemsv0.2
images.promotecompute:images:publishcompute-imagev0.2
workflows.getcompute:workflows:readcompute-nodev0.2
workflows.listcompute:workflows:readcompute-nodev0.2
workflows.abortcompute:workflows:managecompute-nodev0.2
workflows.retrycompute:workflows:managecompute-nodev0.2
workflows.pausecompute:workflows:managecompute-nodev0.2
workflows.resumecompute:workflows:managecompute-nodev0.2
metal.enrollcompute:nodes:provisioncompute-poolv0.2
metal.managecompute:nodes:provisioncompute-nodev0.2
metal.inspectcompute:nodes:provisioncompute-nodev0.2
metal.providecompute:nodes:provisioncompute-nodev0.2
metal.deploycompute:nodes:provisioncompute-nodev0.2
metal.undeploycompute:nodes:provisioncompute-nodev0.2
metal.rescuecompute:nodes:provisioncompute-nodev0.2
metal.unrescuecompute:nodes:provisioncompute-nodev0.2
metal.abortcompute:nodes:provisioncompute-nodev0.2
metal.recovercompute:nodes:provisioncompute-nodev0.2
metal.retirecompute:nodes:provisioncompute-nodev0.2
metal.powercompute:nodes:powercompute-nodev0.2
metal.bulkcompute:nodes:provisioncompute-pool, then each nodev1.0
metal.discoveredcompute:sites:readcompute-site (the site's owning tenant only)v1.0
metal.confirmMaccompute:nodes:provisioncompute-nodev1.0

VPS

MethodActionDecided onPhase
instances.createcompute:instances:create, and compute:secrets:manage when userData is a secret:// referencecompute-instance (the draft); the secrets decision on compute-tenant, plus compute-fleet in the root tenantv0.3
instances.listcompute:instances:readcompute-tenant + itemsv0.3
instances.getcompute:instances:readcompute-instancev0.3
instances.startcompute:instances:updatecompute-instancev0.3
instances.stopcompute:instances:updatecompute-instancev0.3
instances.resizecompute:instances:updatecompute-instancev0.3
instances.deletecompute:instances:deletecompute-instancev0.3
flavors.listcompute:flavors:readcompute-poolv0.3
flavors.createcompute:flavors:managecompute-poolv0.3
flavors.updatecompute:flavors:managecompute-poolv0.3
flavors.deletecompute:flavors:managecompute-poolv0.3
providerAccounts.createcompute:providers:managecompute-fleet or compute-tenantv0.3
providerAccounts.listcompute:providers:readcompute-tenant + itemsv0.3
providerAccounts.getcompute:providers:readcompute-provider-accountv0.3
providerAccounts.offerscompute:providers:readcompute-provider-accountv0.3
providerAccounts.updatecompute:providers:managecompute-provider-accountv0.3
providerAccounts.deletecompute:providers:managecompute-provider-accountv0.3
sandboxes.createcompute:sandboxes:createcompute-sandbox (the draft)v0.5
sandboxes.listcompute:sandboxes:readcompute-tenant + itemsv0.5
sandboxes.getcompute:sandboxes:readcompute-sandboxv0.5
sandboxes.extendcompute:sandboxes:updatecompute-sandboxv0.5
sandboxes.snapshotcompute:sandboxes:updatecompute-sandboxv0.5
sandboxes.exec (hosted by the tunnels module)compute:sandboxes:execcompute-sandboxv0.5
sandboxes.deletecompute:sandboxes:deletecompute-sandboxv0.5
sandboxTemplates.createcompute:sandboxes:manage, and compute:secrets:manage when it names secrets; for each volume it names, compute:sandboxes:manage (read-write mount) or compute:sandboxes:read (read-only)compute-tenant (the secrets decision also on compute-fleet in the root tenant; the volume decisions on each compute-sandbox-volume, 77bf9f5)v0.5
sandboxTemplates.listcompute:sandboxes:readcompute-tenant + itemsv0.5
sandboxTemplates.getcompute:sandboxes:readcompute-sandbox-templatev0.5
sandboxTemplates.updatecompute:sandboxes:manage, and compute:secrets:manage when the resulting template names secrets and the update changes them or what the guest runs; the per-volume decisions for a new volumes list, or for the current one when the update changes what the guest runscompute-sandbox-template (the secrets decision on compute-tenant, plus compute-fleet in the root tenant; the volume decisions on each compute-sandbox-volume)v0.5
sandboxTemplates.deletecompute:sandboxes:managecompute-sandbox-templatev0.5
sandboxSnapshots.listcompute:sandboxes:readcompute-tenant + itemsv0.5
sandboxSnapshots.getcompute:sandboxes:readcompute-sandbox-snapshotv0.5
sandboxSnapshots.deletecompute:sandboxes:deletecompute-sandbox-snapshotv0.5
sandboxVolumes.createcompute:sandboxes:manage, with the requested sizeGiBcompute-sandbox-volume (the volume it would create, under its pool; 6cb30e9, was compute-tenant)v1.0
sandboxVolumes.listcompute:sandboxes:readcompute-tenant (scope mine by default, or all)v1.0
sandboxVolumes.getcompute:sandboxes:readcompute-sandbox-volumev1.0
sandboxVolumes.resizecompute:sandboxes:manage, with the new sizeGiB pendingcompute-sandbox-volumev1.0
sandboxVolumes.deletecompute:sandboxes:managecompute-sandbox-volumev1.0

Fleet and rollouts

MethodActionDecided onPhase
fleet.exportcompute:fleet:admincompute-fleetv0.2
fleet.plancompute:fleet:admincompute-fleetv0.2
fleet.applycompute:fleet:admincompute-fleetv0.2
fleet.statuscompute:fleet:admincompute-fleetv0.2
releases.registercompute:fleet:admincompute-fleetv0.4
releases.listcompute:fleet:admincompute-fleetv0.4
rollouts.createcompute:fleet:admincompute-fleetv0.4
rollouts.listcompute:fleet:admincompute-fleetv0.4
rollouts.getcompute:fleet:admincompute-fleetv0.4
rollouts.pausecompute:fleet:admincompute-fleetv0.4
rollouts.resumecompute:fleet:admincompute-fleetv0.4
rollouts.abortcompute:fleet:admincompute-fleetv0.4

How a call is decided

The envelope in @better-compute/server runs the same steps for every method, over HTTP (POST /api/compute/<group>/<method>), the typed client and compute.api.<group>.<method>(credential, input):

  1. Public methods (system.whoami, system.version) validate their input and run.
  2. A call without a credential is UNAUTHENTICATED, with no IAM call.
  3. The input is validated (INVALID_INPUT) before any IAM call.
  4. IamPort.authenticate identifies the caller. The answer is cached for 30 seconds.
  5. The tenant pre-check below runs.
  6. Methods with an admission (request-as-resource creates, reservations.extend, and creates that take an idempotency key) run prepare in a first transaction: the idempotency lookup and the draft and projection rows, never an IAM call. A replay by the same caller returns the stored result without a decision. Otherwise the one decision is require on the resource prepare returned.
  7. Every other mutation makes one require, never cached. A read makes one authorize, whose allows are cached for 10 seconds per session.
  8. The handler runs in one transaction, read-only for reads. An allowed mutation also writes an audit_events row with its admission context in that transaction.

IAM is never called inside a compute transaction. Denials are opaque: Better IAM reports every denial as ACCESS_DENIED, including a host without the plugin answering UNKNOWN_ACTION, and compute adds nothing to it.

Cross-tenant refusal

A caller whose tenant is neither the input's tenantId nor the root tenant gets ACCESS_DENIED before compute reads or writes its database and before any IAM decision. For fleet-wide methods, the decision tenant is the root tenant, so only root-tenant callers pass.

The check was added because Better IAM 0.1 resolved the resource before it checked the tenant: without it, an outsider probing ids got NOT_FOUND for ids that do not exist and ACCESS_DENIED for ids that do, an existence oracle, and every probe appended a denial to the victim tenant's audit chain. Better IAM 0.2, which Better Compute now requires, checks the tenant first itself: a foreign tenant is denied (TENANT_MISMATCH, masked as ACCESS_DENIED) before any resolver runs, absent and present ids get the same answer, and the denial is audited on the caller's own tenant with the target in targetTenantId (pinned by tests/iam-contract.test.ts). Compute keeps its own check anyway: the answer is the same in every case, and an outsider costs no database access and no IAM decision. tests/server-admission.test.ts asserts that such a call makes one IAM call (authenticate) and no database access at all.

Root-tenant callers continue to IAM, which allows them into another tenant only through ROOT_OVERRIDE: a root-tenant person with root authority, in a user session with MFA. Better IAM audits every override. Inside your own tenant, an id that is not in your view resolves to NOT_FOUND, which reveals nothing beyond the tenant.

List methods and item filtering

The methods marked + items return only the rows the caller may read, without one IAM round trip per row:

  1. Coarse gate. The envelope's one decision on the scope, compute-tenant/{t} or compute-project/{id}.
  2. SQL pre-filter. The query is scoped to the tenant and to rows the projection holds in that tenant's view.
  3. Owner-scoped kinds. reservations.list and instances.list take scope: 'mine' | 'all', and mine is the default: the coarse decision plus owner_identity_id = caller in SQL, with no per-item checks.
  4. Everything else, including scope: 'all', pools.list, nodes.list, sites.list, projects.list, profiles.list, images.list and providerAccounts.list: after the page is read, every item is decided with the method's action on its own resource. Allows already in the read-decision cache are reused; the rest go in authorizeMany batches of at most 50.
// The caller's own reservations: one decision on compute-tenant, no per-item checks.
const mine = await compute.api.reservations.list(credential, { tenantId });

// Every reservation the caller may read in the tenant: per-item checks in batches of 50.
const all = await compute.api.reservations.list(credential, { tenantId, scope: 'all' });

A batch never degrades into a partial list. When the resolver does not know an item yet (NOT_FOUND aborts the whole batch), compute waits 100 ms for the projection to catch up, reloads the page and retries once. Any other batch error fails the request, and an answer whose length does not match the batch is INTERNAL_ERROR.

Every denied item costs an audit write: Better IAM appends a denial to the tenant's audit chain, under its global lock. Write policies so that list pages mostly allow, and prefer scope: 'mine' where it answers the question.

Caches

CacheKeyLifetimeHolds
Authenticationssha256 of the token, the Authorization header and the IAM cookie30 sThe Caller
Read decisionsSession, tenant, action, type and id10 sAllows only

Both caches hold at most 10 000 entries, least recently used first out. Mutations are never cached. The server exposes invalidation as compute.caches: dropIdentity(identityId), dropSession(sessionId), dropDecisions(), clear() and stats(). Every drop also discards answers that were in flight when it happened, so a revocation cannot be overwritten by an older allow. Wire these to IAM events as shown in IAM integration.

Was this page helpful?

Better Compute is created by Sean Filimon

Last updated

On this page