Skip to content

Treetop Permission Backend

This guide covers the Treetop permission backend for Hubuum: what it is, when to use it, and how to set it up.

What is Treetop mode?

Treetop mode delegates all permission authorization decisions to an external Cedar policy server. Instead of querying the local SQL permissions table, Hubuum translates each authorization check into a Cedar authorization request and sends it to your Treetop instance. Treetop evaluates the request against its loaded Cedar policies and returns an Allow or Deny decision.

When to use Treetop

Consider the Treetop backend if you:

  • Want centralized policy authoring across multiple services. If your organization runs several applications that all need to share a common authorization model, Treetop lets you write those policies once and share them.
  • Need richer policy expressions than the SQL boolean grid allows. Cedar supports attribute-based access control (ABAC), conditions, and complex predicates that can't be represented in the simple SQL permission table.
  • Already have a Cedar deployment and want to integrate Hubuum into it.

If none of the above apply, the default Local backend is simpler and requires no external services.

What stays the same / what changes

When you switch to Treetop mode:

Unchanged:

  • Identity: users and groups are still managed in the Hubuum database.
  • Data storage: collections, classes, objects, templates, and relations are stored in the same SQL tables with the same schemas.
  • REST surface: all endpoints accept the same requests and return the same response shapes (with the exceptions noted under "Changed" below).

Changed:

  • Permission DECISIONS: every authorization check is delegated to Treetop instead of the local SQL permissions table.
  • Permission MUTATIONS: the grant/revoke endpoints (POST /api/v1/collections/{id}/permissions, etc.) return 501 Not Implemented. Permissions are managed out-of-band via Treetop's policy upload API.
  • Admin determination: admin status is determined by a Cedar policy on HubuumSystem instead of checking the local admin_groupname config. See "Admin override" below.
  • Relation authorization semantics: the policies emitted by hubuum-admin export-permissions --as cedar use OR-on-endpoints (permission on EITHER endpoint collection is sufficient), while the Local backend uses AND-on-both-endpoints. This is a deliberate divergence — see "Relation policies: OR vs AND semantics" below.
  • Permission GET responses: synthetic, with placeholder id and timestamps. See "Synthetic Permission rows" below.

Configuration

Use Treetop REST 0.1 with this version of Hubuum.

Set the following environment variables to enable Treetop mode:

  • HUBUUM_PERMISSION_BACKEND=treetop — required; selects the Treetop backend.
  • HUBUUM_TREETOP_URL — required; the base URL of your Treetop server (e.g., https://treetop.example.com).
  • HUBUUM_TREETOP_CONNECT_TIMEOUT_MS — optional; connection timeout in milliseconds (default: 5000).
  • HUBUUM_TREETOP_REQUEST_TIMEOUT_MS — optional; request timeout in milliseconds (default: 30000).
  • HUBUUM_TREETOP_ACCEPT_INVALID_CERTS — optional; set to true to accept invalid TLS certificates (development only; DO NOT use in production).
  • HUBUUM_TREETOP_CA_CERT — optional path to a PEM-encoded CA certificate bundle in a regular file no larger than 4 MiB. Every certificate in the bundle is added to the Treetop client's trust store, allowing private PKI without disabling certificate validation. Startup fails if the file cannot be read, is too large, cannot be parsed, or contains no certificates.

Bootstrap workflow

Follow these steps to switch an existing Hubuum deployment to Treetop mode (or to set up a new deployment with Treetop from the start):

  1. Stand up a Treetop server. See the Treetop project documentation for installation and deployment instructions.

  2. Upload the Cedar schema. Upload docs/treetop/schema.json to your Treetop instance. This is the Cedar JSON schema accepted by Treetop and tells it which entity types and actions Hubuum will send. The companion schema.cedarschema file is the human-readable Cedar schema. The schema must be loaded before Hubuum can authorize requests.

  3. Edit and upload the bootstrap policy. Open docs/treetop/bootstrap.cedar and replace REPLACE_ME with your admin group's database id. To find the id, run:

psql -d hubuum -c "SELECT id FROM groups WHERE groupname = 'admin'"

Then upload bootstrap.cedar to your Treetop instance. This policy grants the admin group full access to the system; without it, every request returns 403.

  1. (Optional) Export and upload your existing permissions. If you're migrating from Local mode and want to preserve your existing permission grants, run:
hubuum-admin export-permissions --as cedar > policies.cedar

This generates a Cedar policy bundle that mirrors the current SQL permissions table. Upload policies.cedar to Treetop alongside bootstrap.cedar. (If you're setting up a fresh deployment, skip this step — you'll write your policies from scratch.)

  1. Configure Hubuum and restart. Set HUBUUM_PERMISSION_BACKEND=treetop and HUBUUM_TREETOP_URL=https://your-treetop-instance in your environment, then restart Hubuum. Hubuum checks the Treetop server's /readyz endpoint at startup. If the server is unavailable or not ready, Hubuum exits with status code 6 (EXIT_CODE_PERMISSION_BACKEND_ERROR).

  2. Verify the integration. Run the parity test suite (see "Verifying the integration" below) to confirm Treetop is wired correctly.

Upgrading to Treetop 0.1

Upgrade Treetop REST and its policy configuration before restarting Hubuum with the updated client. Treetop 0.1 removes /api/v1/health; Hubuum now requires a successful /readyz response and rejects an unready policy service at startup. Authorization responses must include the policy version's hash, loaded_at, nullable label_set, and unsigned generation fields.

If you use labels, replace each rule's kind and output with target.resource_type and target.attribute. Set bundle and module manifests to format_version = 2, rebuild the archives, and re-sign them. Follow the upstream migration guide for the complete configuration changes. The local permission backend does not require a Treetop upgrade.

What 501 errors mean

When Treetop mode is enabled, all permission mutation endpoints return 501 Not Implemented:

  • POST /api/v1/collections/{id}/permissions (grant permissions to a group)
  • DELETE /api/v1/collections/{id}/permissions/group/{group_id} (revoke all permissions from a group)
  • PUT /api/v1/collections/{id}/permissions/group/{group_id} (replace permissions for a group)

The 501 response body includes the message:

{
  "error": "permission mutations are managed out-of-band when using the treetop backend"
}

To grant or revoke permissions in Treetop mode, edit your Cedar policies and re-upload them via Treetop's policy upload API. Hubuum does not push policies at runtime; it only queries Treetop for authorization decisions.

Synthetic Permission rows

The GET /api/v1/collections/{id}/permissions/group/{group_id} endpoint returns a Permission object in both Local and Treetop modes. In Treetop mode, the returned object is synthetic: it's constructed on-the-fly from Treetop's authorization responses rather than read from the SQL permissions table.

The synthetic Permission has:

  • A stable id derived from the group id (there is no SQL permission-row id)
  • Stable placeholder timestamps for a single-group response; group-list responses use the group's timestamps so filtering and cursor pagination remain deterministic
  • The boolean permission flags (has_read_collection, has_create_class, etc.) reflect what Treetop allowed at query time

Consumers should not rely on id, created_at, or updated_at for synthetic permissions — they are placeholders for API compatibility only.

Performance and query profile

Local mode retains the SQL visibility-join fast path. Enabling this feature does not add a Treetop call or a group-membership lookup to local list requests. A cursor list normally performs one row query, plus one count query when include_total=true, as before.

Treetop cannot be joined into a PostgreSQL query. Collection, class, object (including computed filtering and sorting), export-template, and direct class/object-relation and task lists enumerate storage candidates in stable cursor order. This also applies to the five resource history lists and structured collection, class, and object search, including its streaming endpoint.

  1. Load the caller's group ids once for candidate authorization.
  2. Fetch at most 128 candidates plus one storage look-ahead row. When totals are skipped, start with the response slots plus the authorized look-ahead. For limit=1 with abundant allowed rows, fetch three rows and authorize two candidates. If the page still needs rows, double the next candidate batch, up to 128, so sparse policies do not require a round trip for each denied row.
  3. Send candidate resources in batches of at most 512 Cedar decisions. Retain only the allowed response page plus one authorized look-ahead row. Advance using the last raw candidate, even when authorization or token scope rejects every row in a storage page.

With include_total=true, Hubuum visits every candidate page to compute the exact authorized total, including rows preceding a response cursor. Candidate and result memory remains bounded by the batch and response limits. With include_total=false, enumeration stops when the page and its look-ahead are allowed, and the total is omitted. History authorizes each stored snapshot's attributes. Direct relation lists resolve endpoint metadata once per candidate batch. Computed-sort candidates retain their resolved types and values for cursor generation. Internal continuations carry validated sort values without encoding public cursor tokens, so an oversized denied value cannot break a valid response or exact-total scan. Public cursor byte limits still apply to client inputs and response cursors. Permission-grid lists use the same typed continuations. Unified text search uses the same growing bounded batches and adapter-owned ranking cursors.

Sparse policies or token scopes may still require a complete scan to fill a page or prove exhaustion, even without totals. These ordinary cursor lists have no new total-work rejection threshold; adding one would reject previously valid queries. Exact totals also remain proportional to all matching candidates. Byte-collated name-and-ID indexes support first-page ordering and continued seeks for catalog names, group and principal names, scoped template and remote target names, event names, and computed-field keys. Both name directions keep the ascending ID tie-breaker. Existing locale-sensitive indexes remain for filters and uniqueness. Other arbitrary sort combinations are not guaranteed to have a matching index.

Apply 2026-09-13-000001_byte_ordered_cursor_indexes before deploying this pagination change. This additive migration builds indexes transactionally and can block writes until it commits. Schedule a quiet period; lock waits are limited to five seconds and each statement to sixty seconds. A timeout rolls back all of its indexes so the migration can be retried.

Structured search retains its 10,000-candidate work limit per request, with bounded pages and early termination when totals are skipped. Related predicates and graph traversal retain their separate existing safety limits; planning them can require more work than filling the final object page. See structured-search limits.

Two event and remote-target visibility paths require a complete authorized collection-id set. Those callers must explicitly request a validated total candidate bound, currently capped at 10,000. Hubuum returns service unavailable instead of retaining or silently truncating a larger set. Deployments with very large candidate sets should monitor authorization batch latency and candidate counts. A policy-aware reverse index or materialized visibility cache would be required to recover SQL-style exact-count complexity for arbitrary Cedar policies.

Relation policies: OR vs AND semantics

The hubuum-admin export-permissions --as cedar command emits relation permits using OR-on-endpoints: a user with permission on EITHER the from_collection_id OR the to_collection_id can act on the relation. This differs from the Local backend, which uses AND-on-both-endpoints (permission required on both).

The divergence is intentional: the OR semantics are simpler to express in Cedar and match the common case where a user managing either endpoint should be able to create/read/update/delete the relation. If you need strict AND semantics, hand-edit the exported policies (or write forbid clauses) to enforce the constraint.

Example of the OR predicate emitted by the exporter (from src/permissions/export.rs):

permit(
    principal in Group::"123",
    action in [Action::"CreateClassRelation", Action::"ReadClassRelation"],
    resource
) when {
    resource is HubuumClassRelation &&
    (resource.from_collection_id == 5 || resource.to_collection_id == 5)
};

To enforce AND semantics, replace the || with &&, or add a forbid clause that denies the relation unless both conditions hold.

Verifying the integration

Hubuum includes a mandatory conformance suite that runs a shared semantic corpus against the local PostgreSQL backend and a digest-pinned real Treetop server. It also exercises batching, pagination, private-CA TLS, service failure and recovery, fail-closed response validation, and diagnostic redaction.

To run the hermetic suite locally after preparing the normal test database:

source .env
./scripts/run-treetop-conformance.sh

The runner owns the test service, strict schema and policies, private CA, and service lifecycle. Missing fixture inputs are fatal; the ignored Rust tests do not silently skip. See test-fixture.md for the exact corpus, failure matrix, immutable service pin, and diagnostics.

CI selects this release-blocking job for changes to Treetop configuration, authorization mappings, token scopes, visibility/search behavior, the fixture, or the harness. It also runs for every release tag. The aggregate CI gate and tag validation both require the job to pass.

Admin override

In Treetop mode, admin status is determined by a Cedar policy instead of the HUBUUM_ADMIN_GROUPNAME environment variable. Hubuum's AdminAccess extractor calls backend.is_admin(principal), which dispatches a Cedar authorization request:

principal: User::"<user_id>" (with groups Group::"<group_id>", ...)
action: ReadCollection
resource: HubuumSystem::"global"

If Treetop returns Allow, the user is an admin. If Deny, the user is not. The bootstrap policy grants this permission to a single group (the one whose id you substitute for REPLACE_ME). You can add more admin groups by adding more permit clauses targeting HubuumSystem.

Further reading

  • schema.json — the Cedar JSON schema accepted by Treetop.
  • schema.cedarschema — the human-readable Cedar schema Hubuum's exporter targets.
  • bootstrap.cedar — minimal policy file to upload before serving traffic.
  • test-fixture.md — the test entities and policies the parity suite expects.
  • ../permissions.md — the on-the-wire permission model that both Local and Treetop backends conform to.
  • src/permissions/treetop/mapping.rs — the runtime entity mappings (source of truth for the schema).
  • src/permissions/export.rs — the Cedar policy exporter.

Task cancellation authorization

CancelTask authorizes POST /api/v1/tasks/{task_id}/cancel independently of ReadTask. Add it to the deployed schema and explicitly grant it in cancellation policies. Scoped tokens remain limited to their own submitted tasks. Internal reindex and schema tasks still require an unscoped administrator, and schema cancellation also requires UpdateClass. See Task API.