cf-static-credstore-plugin 0.1.5

CredStore plugin with static secret mapping for development and testing
Documentation

Static CredStore Plugin

CredStore storage-backend plugin that serves pre-configured secrets from YAML configuration. Designed for development, testing, and fixed-credential deployments where a full secrets vault is unnecessary.

Overview

The cf-static-credstore-plugin module provides:

  • Static secret mapping — secrets defined in YAML config, loaded and validated at init
  • Four sharing scopes — Private, Tenant, Shared (tenant-scoped), and Global
  • O(1) lookup — secrets are pre-indexed into separate HashMaps per scope
  • Deterministic precedence — lookup order: Private → Tenant → Shared → Global
  • Strict config validation — invalid keys, duplicate entries, and contradictory field combinations are rejected at startup

The plugin registers itself via the types registry as a CredStorePluginClientV1 implementation and is discovered by the credstore gateway module.

Configuration

Add the plugin section under your module configuration:

static-credstore-plugin:
  config:
    vendor: "hyperspot"   # GTS vendor name (default: "hyperspot")
    priority: 100          # Plugin priority, lower = higher (default: 100)
    secrets:
      # Private secret — only accessible by this specific user in this tenant
      - tenant_id: "11111111-1111-1111-1111-111111111111"
        owner_id: "22222222-2222-2222-2222-222222222222"
        key: "my-api-key"
        value: "sk-secret-123"

      # Tenant secret — accessible by any user within the tenant
      - tenant_id: "11111111-1111-1111-1111-111111111111"
        key: "team-api-key"
        value: "sk-team-456"

      # Shared secret — tenant-scoped, visible to descendant tenants via gateway walk-up
      - tenant_id: "11111111-1111-1111-1111-111111111111"
        key: "org-api-key"
        value: "sk-org-789"
        sharing: "shared"

      # Global secret — accessible by any tenant and any user (fallback)
      - key: "platform-api-key"
        value: "sk-global-000"

Secret fields

Field Type Required Description
tenant_id UUID No Tenant scope. None → global secret.
owner_id UUID No Subject scope. Only valid for private sharing. Requires tenant_id.
key string Yes Secret reference key. Must match SecretRef format (alphanumeric, -, _).
value string Yes Plaintext secret value (converted to bytes at init).
sharing SharingMode No Explicit sharing mode. When omitted, inferred from tenant_id/owner_id.

Sharing mode inference

When sharing is omitted, the mode is inferred automatically:

tenant_id owner_id Inferred mode
None shared (global)
Some None tenant
Some Some private

You can override the default with an explicit sharing value (e.g. set sharing: "shared" on a tenant-scoped secret to make it visible to descendant tenants).

Validation rules

The plugin rejects invalid configurations at startup with a descriptive error:

  • Invalid keykey must be a valid SecretRef (alphanumeric, -, _)
  • Nil UUIDstenant_id and owner_id must not be 00000000-0000-0000-0000-000000000000
  • owner_id without tenant_id — global secrets cannot have an owner
  • owner_id on non-Private secretowner_id is only valid when resolved sharing is private
  • private without owner_id — explicit sharing: "private" requires owner_id
  • Global with non-Shared modetenant_id: None only allows shared (or inferred shared)
  • Duplicate keys — within the same scope (same tenant + sharing mode), keys must be unique

Lookup precedence

When a secret is requested, the plugin checks maps in this order:

  1. Private — keyed by (tenant_id, owner_id, key), matched against SecurityContext
  2. Tenant — keyed by (tenant_id, key), any subject in the tenant
  3. Shared — keyed by (tenant_id, key), tenant-scoped but visible to descendants
  4. Global — keyed by key only, fallback for any caller

The first match wins. This means a Private secret shadows a Tenant secret with the same key for the matching user, while other users in the same tenant still see the Tenant-level value.

SecretMetadata::owner_id resolution

For Private secrets, owner_id comes from the config. For Tenant, Shared, and Global secrets, owner_id is not stored — the plugin fills it from SecurityContext::subject_id() of the caller at lookup time.

Architecture

module.rs          ModKit module — init, config loading, GTS registration
config.rs          YAML config model + resolve_sharing() + validation docs
domain/
  service.rs       Service — from_config() builder + get() lookup
  client.rs        CredStorePluginClientV1 impl (maps SecretEntry → SecretMetadata)
  mod.rs           Re-exports

Init sequence

  1. Load StaticCredStorePluginConfig from module config
  2. Service::from_config() — validate all entries, build lookup maps
  3. Register GTS plugin instance in types-registry
  4. Store Arc<Service> in module state
  5. Register CredStorePluginClientV1 scoped client in ClientHub

Testing

cargo test -p cf-static-credstore-plugin

The test suite covers:

  • Lookup per scope (private, tenant, shared, global)
  • Precedence across all four scopes
  • Owner/tenant isolation
  • Config validation (all rejection rules)
  • Sharing mode inference and explicit overrides
  • SecretMetadata owner resolution from SecurityContext

License

Apache-2.0