Skip to content

feat(cloudflare): add Cloudflare API token lease backend#335

Merged
jdx merged 20 commits intomainfrom
feat/cloudflare-lease-backend
Mar 8, 2026
Merged

feat(cloudflare): add Cloudflare API token lease backend#335
jdx merged 20 commits intomainfrom
feat/cloudflare-lease-backend

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Mar 8, 2026

Summary

  • Adds a new cloudflare lease backend that creates short-lived, scoped Cloudflare API tokens via the Cloudflare API Tokens API
  • A parent token with API Tokens: Edit permission creates child tokens scoped to specified permission policies with automatic expiry
  • Supports explicit revocation (token deletion), {account_id} placeholder substitution in resource keys, and configurable env var name
  • Adds backend documentation (docs/leases/cloudflare.md) and updates the lease guide with a Cloudflare example

Test plan

  • Verify cargo check passes (confirmed locally)
  • Manual test with a real Cloudflare account: create a parent token with "API Tokens: Edit", configure a cloudflare lease, run fnox lease create, verify scoped token works and expires
  • Verify fnox lease revoke deletes the token via the Cloudflare API
  • Verify prerequisite check reports missing CLOUDFLARE_API_TOKEN
  • Verify JSON schema generation includes the new Cloudflare variant

🤖 Generated with Claude Code


Note

Medium Risk
Introduces a new external-API-backed credential issuer (Cloudflare token create/delete) and changes lease creation error handling, which could affect lease provisioning flows and failure modes.

Overview
Adds a new cloudflare lease backend that vends short-lived, scoped Cloudflare API tokens (user- or account-owned), supports optional explicit policies (or inheritance from the parent token), {account_id} substitution, configurable env var output, and revocation via token deletion.

Updates configuration/schema and docs to expose the new backend, and refactors prerequisite/interactive env-var guidance into per-backend helpers.

Strengthens fnox lease create by hard-failing when secrets required by a backend fail to resolve (instead of continuing and later reporting a generic “token not found”), reducing confusing auth error paths.

Written by Cursor Bugbot for commit 2dbc9e3. This will update automatically on new commits. Configure here.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the credential leasing capabilities by integrating a new Cloudflare API token lease backend. This feature allows users to securely provision temporary, finely-scoped API tokens for Cloudflare services, improving security posture by minimizing the lifetime and permissions of access credentials. It streamlines the process of managing Cloudflare access within automated workflows.

Highlights

  • New Cloudflare Lease Backend: Introduced a new cloudflare lease backend for creating short-lived, scoped Cloudflare API tokens.
  • Scoped Token Generation: Implemented the ability for a parent token with 'API Tokens: Edit' permission to generate child tokens with specific permissions and automatic expiry.
  • Enhanced Token Management: Added support for explicit token revocation, dynamic {account_id} placeholder substitution in resource keys, and custom environment variable naming for the generated token.
  • Documentation: Included comprehensive documentation for the new Cloudflare lease backend and updated the general lease guide with an example.
Changelog
  • docs/guide/leases.md
    • Added a detailed example for configuring and using the Cloudflare lease backend.
    • Updated the lease backend comparison table to include the new Cloudflare entry.
  • docs/leases/cloudflare.md
    • Created new documentation detailing the Cloudflare lease backend's configuration, prerequisites, credentials produced, limits, and examples.
  • src/lease_backends/cloudflare.rs
    • Implemented the CloudflareBackend struct and its LeaseBackend trait methods for creating and revoking Cloudflare API tokens.
    • Included logic for handling API requests, error responses, and substituting account_id placeholders in policies.
  • src/lease_backends/mod.rs
    • Added the cloudflare module to the lease_backends module.
    • Extended LeaseBackendConfig enum with a Cloudflare variant.
    • Implemented prerequisite checks and backend instantiation for the Cloudflare lease type.
    • Added a default environment variable name for Cloudflare tokens.
Activity
  • No human activity (comments, reviews, etc.) has been recorded on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new lease backend for Cloudflare API tokens, allowing for the creation of short-lived, scoped tokens. The implementation includes token creation, revocation, and configuration options for policies and account IDs. The changes are well-documented with a dedicated page for the new backend and an example in the main lease guide. The code is well-structured and includes good error handling. I have one suggestion to simplify the JSON serialization logic by leveraging serde's derived implementations, which will improve code maintainability.

Comment thread src/lease_backends/cloudflare.rs Outdated
Comment on lines +83 to +90
"permission_groups": p.permission_groups.iter().map(|pg| {
let mut m = serde_json::Map::new();
m.insert("id".to_string(), serde_json::Value::String(pg.id.clone()));
if let Some(name) = &pg.name {
m.insert("name".to_string(), serde_json::Value::String(name.clone()));
}
serde_json::Value::Object(m)
}).collect::<Vec<_>>(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The manual JSON construction for permission_groups is unnecessary because the CloudflarePermissionGroup struct derives serde::Serialize. You can simplify this by letting serde_json handle the serialization directly within the json! macro. This improves readability and maintainability.

                    "permission_groups": p.permission_groups,

@jdx jdx force-pushed the feat/cloudflare-lease-backend branch from d65c122 to fdc4a5a Compare March 8, 2026 21:18
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 8, 2026

Greptile Summary

This PR adds a cloudflare lease backend that creates short-lived, scoped Cloudflare API tokens via the Cloudflare API Tokens API. It also refactors per-backend prerequisite/env-var helpers into their respective modules, adds an early hard-fail in fnox lease create when required secrets fail to resolve, and removes an unused test helper from source_registry.rs.

The implementation is well-structured and addresses most of the common pitfalls in this class of API-backed credential backend (enum-based effect validation, character-aware token name truncation, saturating cast for duration, explicit guards for empty policies and empty permission_groups, and proper {account_id} placeholder detection). Two issues remain:

  • Empty resources map not validated per policybuild_api_policies validates that permission_groups is non-empty for each policy but has no equivalent check for resources. An empty resources map produces a policy with "resources": {} that Cloudflare will reject with an opaque API error rather than a clear configuration message.
  • Default env_var collides with the parent auth token — The default output variable CLOUDFLARE_API_TOKEN is the same variable get_api_token() reads to authenticate. Users who eval $(fnox lease create --format env cf) will silently overwrite their parent token with the child token, causing authorization failures for any subsequent revocation or lease-creation commands in that shell session.

Confidence Score: 3/5

  • Safe to merge after addressing the empty resources validation gap; the default env-var collision is a usability concern worth fixing before broad adoption.
  • The core implementation is solid — enum-validated effect field, char-aware name truncation, saturating duration cast, and guards for empty policies/permission_groups/unresolved placeholders are all present. The two remaining issues (missing resources emptiness check and the parent-token-shadowing footgun from the default env_var) are meaningful but neither is a data-loss or security vulnerability. The early hard-fail on unresolved secrets in lease create is a good defensive improvement.
  • src/lease_backends/cloudflare.rs (empty resources validation in build_api_policies) and src/lease_backends/mod.rs / docs/leases/cloudflare.md (default env_var collision with parent auth token).

Important Files Changed

Filename Overview
src/lease_backends/cloudflare.rs New Cloudflare backend implementing token creation/revocation. Well-structured with most previous review issues addressed (enum-based effect, char-aware truncation, saturating cast, empty policies/groups validation, placeholder guard). Missing validation for empty resources map per policy, which would produce an opaque Cloudflare API error.
src/lease_backends/mod.rs Adds Cloudflare variant to LeaseBackendConfig enum and wires prerequisite/env-var/backend-creation dispatch. Default env_var = "CLOUDFLARE_API_TOKEN" collides with the parent auth token variable, which can cause silent failures when lease credentials are eval'd into the shell.
src/commands/lease.rs Added early hard-fail when required secrets fail to resolve before reaching backend. Interactive prompting via required_env_vars is plumbed correctly. The unsafe { std::env::set_var } in interactive mode is pre-existing and noted with a TODO.
src/source_registry.rs Removed the test-only clear() helper that was no longer referenced. No functional impact.
docs/leases/cloudflare.md New documentation for the Cloudflare lease backend with configuration examples. The eval $(fnox lease create --format env cf) example could silently overwrite the parent CLOUDFLARE_API_TOKEN, which is risky without an explicit warning.

Sequence Diagram

sequenceDiagram
    participant User
    participant fnox as fnox CLI
    participant Ledger as LeaseLedger
    participant CF as Cloudflare API

    User->>fnox: fnox lease create cf
    fnox->>fnox: resolve_secrets_batch()
    fnox->>fnox: check_prerequisites() - verify parent token present
    alt policies configured in fnox.toml
        fnox->>fnox: build_api_policies(policies, account_id)
    else no policies - inherit from parent
        fnox->>CF: GET tokens/verify
        CF-->>fnox: parent token_id
        fnox->>CF: GET tokens/{token_id}
        CF-->>fnox: policies (API Tokens perms stripped)
    end
    fnox->>CF: POST tokens<br/>with name, policies, expires_on
    CF-->>fnox: child token id and value
    fnox->>Ledger: record lease (lease_id = CF token UUID)
    fnox-->>User: child token credential

    User->>fnox: fnox lease revoke {lease_id}
    fnox->>Ledger: find record by CF UUID
    fnox->>CF: DELETE tokens/{lease_id}
    CF-->>fnox: 200 OK or 404 treated as success
    fnox->>Ledger: mark_revoked(lease_id)
    fnox-->>User: Lease revoked
Loading

Comments Outside Diff (2)

  1. src/lease_backends/cloudflare.rs, line 129-135 (link)

    Empty resources map per policy is not validated

    build_api_policies correctly guards against empty permission_groups (line 130) but has no equivalent check for empty resources. A user can configure:

    [[leases.cf.policies]]
    effect = "allow"
    permission_groups = [{ id = "abc123" }]
    # resources omitted → deserializes as empty IndexMap

    This generates "resources": {} in the API body. Cloudflare's API requires at least one resource scope per policy and will reject this with a validation error, leaving the user with an opaque API-level message rather than a clear configuration hint.

    Adding a parallel check alongside the permission_groups one would catch this early:

    if p.permission_groups.is_empty() {
        return Err(FnoxError::Config(
            "Cloudflare backend: each policy must have at least one permission group."
                .to_string(),
        ));
    }
    if p.resources.is_empty() {
        return Err(FnoxError::Config(
            "Cloudflare backend: each policy must have at least one resource scope.".to_string(),
        ));
    }
  2. src/lease_backends/mod.rs, line 62-64 (link)

    Default env_var shadows the parent authentication token

    The default output env_var is "CLOUDFLARE_API_TOKEN" — the exact same variable that get_api_token() reads to authenticate as the parent token. Any user who applies the lease credentials to their shell (e.g., eval $(fnox lease create --format env cf)) will overwrite their parent token with the child token. Since the child token typically lacks "API Tokens: Edit" permission, all subsequent fnox operations in that shell session (revocation, creating new leases) will fail with an authorization error.

    The documentation example in docs/leases/cloudflare.md encourages this eval pattern. Consider:

    1. Choosing a different default var name (e.g., CLOUDFLARE_LEASE_TOKEN) that does not collide with the authentication credential, or
    2. Adding a prominent documentation warning that users must preserve their parent CLOUDFLARE_API_TOKEN before evaling the output if they intend to run further fnox commands in the same shell.

Fix All in Claude Code

Last reviewed commit: 2dbc9e3

Comment thread src/lease_backends/cloudflare.rs Outdated
Comment thread src/lease_backends/mod.rs Outdated
Comment thread src/lease_backends/cloudflare.rs Outdated
Comment thread src/lease_backends/cloudflare.rs
Comment thread src/lease_backends/mod.rs Outdated
Comment thread src/lease_backends/cloudflare.rs
Comment thread src/lease_backends/cloudflare.rs Outdated
Comment thread src/lease_backends/cloudflare.rs
Comment thread src/lease_backends/cloudflare.rs Outdated
@jdx
Copy link
Copy Markdown
Owner Author

jdx commented Mar 8, 2026

bugbot run

Comment thread src/lease_backends/mod.rs Outdated
Comment thread src/lease_backends/cloudflare.rs Outdated
Comment thread src/lease_backends/cloudflare.rs Outdated
Comment thread docs/public/schema.json Outdated
Comment thread src/lease_backends/cloudflare.rs Outdated
Comment thread src/lease_backends/cloudflare.rs Outdated
Comment thread src/lease_backends/cloudflare.rs
@jdx
Copy link
Copy Markdown
Owner Author

jdx commented Mar 8, 2026

bugbot run

Comment thread src/lease_backends/cloudflare.rs Outdated
@jdx jdx force-pushed the feat/cloudflare-lease-backend branch from 1ebe16f to 8f4ef10 Compare March 8, 2026 22:31
Comment thread src/lease_backends/cloudflare.rs
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment thread src/lease_backends/mod.rs Outdated
jdx and others added 12 commits March 8, 2026 19:11
Creates short-lived, scoped Cloudflare API tokens using the Cloudflare
API Tokens API. A parent token with 'API Tokens: Edit' permission
creates child tokens that are scoped to specified permission policies
and automatically expire. Supports explicit revocation via token deletion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use CloudflarePolicyEffect enum instead of String for effect field
- Change resources map values from serde_json::Value to String
- Use serde derive for permission_groups serialization
- Fix empty errors array producing blank error messages
- Truncate token name to Cloudflare's 100-char limit
- Validate policies is non-empty before API call
- Remove not_before field (Cloudflare defaults to immediate validity)
- Saturate u64→i64 cast for duration
- Mention CF_API_TOKEN in required_env_vars hint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When `policies` is omitted from the config, the Cloudflare backend now
fetches the parent token's policies via the API (GET /user/tokens/verify
then GET /user/tokens/{id}) and uses them for the child token. This
makes the minimal config just `type = "cloudflare"`.

Changed `policies` from `Vec` (defaulting to empty) to `Option<Vec>` so
the intent is explicit: `None` = inherit, `Some([...])` = override.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…id} placeholders

When `account_id` is set, the backend now uses the account-owned tokens
API (`/accounts/{id}/tokens`) instead of `/user/tokens`. Account tokens
are better for CI/CD since they aren't tied to individual users.

Also adds a guard that errors early if a resource key contains the
`{account_id}` placeholder but `account_id` is not configured, instead
of sending the literal placeholder string to the API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds an explicit `token_type` config field ("user" or "account") instead
of implicitly choosing based on `account_id` presence. When set to
"account", `account_id` is required and the backend uses the
account-owned tokens API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cloudflare forbids sub-tokens from having token management permissions.
When inheriting policies from the parent token, filter out any "API
Tokens" permission groups to avoid the "sub-token is not allowed to have
permissions to manage other tokens" error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jdx and others added 6 commits March 8, 2026 19:11
- Always use /user/tokens for verify/details (account endpoint doesn't exist)
- Sanitize inherited policies: only extract effect, resources, and
  permission group id (drop server-generated fields like status, created_on)
- Fix UTF-8 byte-boundary panic on token name truncation (use char-based)
- Validate empty policies and per-policy empty permission_groups
- Add CF_API_TOKEN to required_env_vars for secret resolution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests share a global SOURCES static and run in parallel. Calling clear()
in each test creates a race where one test's clear() can wipe another
test's registered data. Since each test uses unique paths, clear() is
unnecessary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The verify and token details endpoints didn't check HTTP status codes
before extracting JSON fields. Invalid/expired tokens (401/403) produced
misleading "missing result.id" errors instead of proper auth errors.

Extracted cf_api_call helper that checks status and returns proper
ProviderAuthFailed for 401/403 responses.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a secret required by a lease backend fails to resolve (e.g. FIDO2
PIN error), resolve_secrets_batch respects if_missing (default: "warn")
and swallows the error. This produced a misleading "token not found"
from check_prerequisites instead of surfacing the actual auth failure.

Now checks if any required secrets resolved to None and fails early
with a clear error message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Account-scoped tokens must verify via /accounts/{id}/tokens/verify,
not /user/tokens/verify. The greptile review incorrectly claimed the
account verify endpoint doesn't exist — it does. Reverts to passing
tokens_path to fetch_parent_policies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CF_API_TOKEN is a runtime fallback alias, not an independently required
var. Listing both caused the hard-fail check to fire if only one was
defined as a managed secret and it failed to resolve.

Also fix CI build: move the hard-fail check into create_single where
backend_config exists, passing resolved_secrets through run_all.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jdx jdx force-pushed the feat/cloudflare-lease-backend branch from fdc5d4a to 8b91bc7 Compare March 8, 2026 23:13
…ckends

Each backend module now owns its prerequisite checks and env var
definitions. mod.rs dispatches to them instead of embedding env var
names directly. This centralizes backend-specific knowledge in the
backend modules where it belongs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jdx jdx enabled auto-merge (squash) March 8, 2026 23:26
@jdx jdx disabled auto-merge March 8, 2026 23:26
…single

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jdx jdx merged commit 73b792a into main Mar 8, 2026
16 checks passed
@jdx jdx deleted the feat/cloudflare-lease-backend branch March 8, 2026 23:41
jdx pushed a commit that referenced this pull request Mar 9, 2026
### 🚀 Features

- **(cloudflare)** add Cloudflare API token lease backend by
[@jdx](https://github.com/jdx) in
[#335](#335)
- **(fido2)** bump demand to v2, mask PIN during typing by
[@jdx](https://github.com/jdx) in
[#334](#334)
- **(init)** add -f as short alias for --force by
[@jdx](https://github.com/jdx) in
[#329](#329)
- **(lease)** add --all flag, default to creating all leases by
[@jdx](https://github.com/jdx) in
[#337](#337)
- **(lease)** add GitHub App installation token lease backend by
[@jdx](https://github.com/jdx) in
[#342](#342)

### 🐛 Bug Fixes

- **(config)** fix directory locations to follow XDG spec by
[@jdx](https://github.com/jdx) in
[#336](#336)
- **(exec)** use unix exec and exit silently on subprocess failure by
[@jdx](https://github.com/jdx) in
[#339](#339)
- **(fido2)** remove duplicate touch prompt by
[@jdx](https://github.com/jdx) in
[#332](#332)
- **(set)** write to lowest-priority existing config file by
[@jdx](https://github.com/jdx) in
[#331](#331)
- **(tui)** skip providers requiring interactive auth by
[@jdx](https://github.com/jdx) in
[#333](#333)

### 🛡️ Security

- **(ci)** retry lint step to handle transient pkl fetch failures by
[@jdx](https://github.com/jdx) in
[#341](#341)
- **(mcp)** add MCP server for secret-gated AI agent access by
[@jdx](https://github.com/jdx) in
[#343](#343)
- add guide for fnox sync by [@jdx](https://github.com/jdx) in
[#328](#328)

### 🔍 Other Changes

- share Rust cache across CI jobs by [@jdx](https://github.com/jdx) in
[#340](#340)
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.

1 participant