Skip to content

release: 0.7.1 — API tokens with TTL + scope#139

Merged
click0 merged 1 commit intomainfrom
claude/analyze-test-coverage-nCOJW
May 3, 2026
Merged

release: 0.7.1 — API tokens with TTL + scope#139
click0 merged 1 commit intomainfrom
claude/analyze-test-coverage-nCOJW

Conversation

@click0
Copy link
Copy Markdown
Owner

@click0 click0 commented May 3, 2026

Summary

API tokens gain optional expires_at (UTC ISO 8601) and scope (list of path globs) fields — tighten the blast radius of a leaked CI runner token without rewriting the auth flow. Inspired by Proxmox's API token model.

auth:
    tokens:
      - name: ci-runner
        token_hash: "sha256:..."
        role: admin
        expires_at: "2026-12-31T23:59:59Z"
        scope:
          - /api/v1/containers/*
          - /api/v1/exports/*

Scope rules

  • Exact match: /api/v1/host
  • Trailing prefix: /api/v1/containers/* matches any path starting with /api/v1/containers/. The slash is required — the bare prefix /api/v1/containers does NOT match the glob, so granting per-resource access doesn't leak the collection-list endpoint.

Backward compatibility

Pre-0.7.1 configs work unchanged:

  • Missing expires_at0isExpired() returns false → never expires
  • Missing scope[]pathInScope() returns true → no restriction

Existing tokens with just name/token_hash/role behave identically after upgrade.

Implementation

  • lib/auth_pure.{h,cpp}parseIso8601Utc (returns -1 on bad shape, bad month range, non-UTC offset), isExpired (strict-after; expiresAt=0 always false), pathInScope (empty scope → true; exact or /*-suffix), checkBearerAuthFull (combined hash+role+expiry+scope).
  • daemon/config.{h,cpp}AuthToken.expiresAt + AuthToken.scope. YAML parser reads expires_at: and scope: (scalar or sequence).
  • daemon/auth.cppisAuthorized() calls checkBearerAuthFull with req.path and time(nullptr).
  • daemon/crated.conf.sample — documents new fields with ci-runner example.

Tests

tests/unit/auth_pure_test.cpp — 13 new ATF cases on top of the existing 15:

Group Cases
parseIso8601Utc canonical epochs (1970/2000/2026), invalid-shape rejection (2026-13-31, slash separators), +00:00/-00:00 parses as UTC
isExpired 0 = never; strict-after at boundary (exact-second still alive)
pathInScope empty unrestricted; exact match without partial prefix; trailing /* glob; glob requires slash — bare prefix doesn't match
checkBearerAuthFull expired-rejected with matching role+scope; out-of-scope-rejected with matching role; in-scope happy path; backward-compat (expiresAt=0 + empty scope)

Verified locally: 28/28 in auth_pure_test. Cluster-wide expected at 737/737 on FreeBSD CI.

Test plan

  • kyua test auth_pure_test — 28/28
  • Version bumped to 0.7.1
  • crated.conf.sample documents new fields
  • TODO — TTL+scope moved to Done
  • FreeBSD CI: full build + test pass

https://claude.ai/code/session_01X6t6tzVypHye5bDGLxzmZK


Generated by Claude Code

API tokens gain optional `expires_at:` (UTC ISO 8601) and `scope:`
(list of path globs) fields — tighten the blast radius of a leaked
CI runner token without rewriting the auth flow. Inspired by
Proxmox's API token model.

Configuration:

  auth:
    tokens:
      - name: ci-runner
        token_hash: "sha256:..."
        role: admin
        expires_at: "2026-12-31T23:59:59Z"
        scope:
          - /api/v1/containers/*
          - /api/v1/exports/*

Scope rules:
- exact match:    "/api/v1/host"
- trailing /*:    "/api/v1/containers/*" matches any path starting
                  with "/api/v1/containers/" (slash REQUIRED — the
                  bare prefix "/api/v1/containers" does NOT match,
                  so per-resource access doesn't leak the
                  collection-list endpoint).

Backward compatibility: pre-0.7.1 configs unchanged. Missing
expires_at -> 0 -> never expires. Missing scope -> [] -> no
restriction. Existing tokens behave identically after upgrade.

Implementation:
- lib/auth_pure.{h,cpp}: extends with parseIso8601Utc(s) (returns
  -1 on bad shape / bad month range / non-UTC offset; uses timegm),
  isExpired(t, now) (strict-after; expiresAt=0 always false),
  pathInScope(scope, path), checkBearerAuthFull(..., path, now).
- daemon/config.{h,cpp}: AuthToken gains expiresAt + scope.
  YAML parser reads expires_at (rejects malformed) and scope
  (scalar or sequence).
- daemon/auth.cpp: isAuthorized() calls checkBearerAuthFull with
  req.path and time(nullptr).
- daemon/crated.conf.sample: documents new fields with ci-runner
  example.

Tests: +13 new ATF cases in auth_pure_test.cpp (28/28 verified
locally; expect 737/737 cluster-wide on FreeBSD CI). Covers
parseIso8601Utc canonical/invalid/offset-zero, isExpired
strict-after invariant, pathInScope incl. the slash-required
property for trailing globs, and checkBearerAuthFull integration
(expired-rejected, out-of-scope-rejected, happy path,
backward-compat).
@click0 click0 merged commit a455262 into main May 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants