Files
goclaw/internal/store/secure_cli_store.go
T

158 lines
7.9 KiB
Go

package store
import (
"context"
"encoding/json"
"time"
"github.com/google/uuid"
)
// AgentGrantSummary is the lightweight per-grant item returned in the List response.
// It exposes env_set (bool: has override) but NEVER the encrypted bytes.
type AgentGrantSummary struct {
GrantID uuid.UUID `json:"grant_id"`
AgentID uuid.UUID `json:"agent_id"`
AgentKey string `json:"agent_key"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
EnvSet bool `json:"env_set"` // true when encrypted_env IS NOT NULL — projection only, never the blob
}
// SecureCLIBinary represents a CLI binary with auto-injected credentials.
// Credentials are encrypted at rest and injected into child processes via Direct Exec Mode.
type SecureCLIBinary struct {
BaseModel
BinaryName string `json:"binary_name" db:"binary_name"`
BinaryPath *string `json:"binary_path,omitempty" db:"binary_path"`
Description string `json:"description" db:"description"`
EncryptedEnv []byte `json:"-" db:"encrypted_env"` // AES-256-GCM encrypted JSON — never serialized to API
DenyArgs json.RawMessage `json:"deny_args" db:"deny_args"` // regex patterns for blocked subcommands
DenyVerbose json.RawMessage `json:"deny_verbose" db:"deny_verbose"` // blocked verbose/debug flags
TimeoutSeconds int `json:"timeout_seconds" db:"timeout_seconds"`
Tips string `json:"tips" db:"tips"` // hint injected into TOOLS.md context
IsGlobal bool `json:"is_global" db:"is_global"`
Enabled bool `json:"enabled" db:"enabled"`
CreatedBy string `json:"created_by" db:"created_by"`
UserEnv []byte `json:"-" db:"-"` // per-user encrypted env (populated by LookupByBinary LEFT JOIN)
// EnvKeys is set by HTTP handlers only (names from decrypted env, no values); not a DB column.
EnvKeys []string `json:"env_keys,omitempty" db:"-"`
// Env is set by HTTP handlers only. Sensitive values are masked; value entries are visible.
Env map[string]SecureCLIEnvResponseEntry `json:"env,omitempty" db:"-"`
// AgentGrantsSummary is populated by List only — lightweight per-grant summary (no env bytes).
AgentGrantsSummary []AgentGrantSummary `json:"agent_grants_summary" db:"-"`
}
// MergeGrantOverrides applies agent grant overrides onto a binary config.
// Non-nil grant fields replace binary defaults; nil fields keep binary values.
func (b *SecureCLIBinary) MergeGrantOverrides(g *SecureCLIAgentGrant) {
if g == nil {
return
}
if g.DenyArgs != nil {
b.DenyArgs = *g.DenyArgs
}
if g.DenyVerbose != nil {
b.DenyVerbose = *g.DenyVerbose
}
if g.TimeoutSeconds != nil {
b.TimeoutSeconds = *g.TimeoutSeconds
}
if g.Tips != nil {
b.Tips = *g.Tips
}
// Grant env fully replaces binary default env when non-empty.
if len(g.EncryptedEnv) > 0 {
b.EncryptedEnv = g.EncryptedEnv
}
}
// SecureCLIUserCredential holds per-user encrypted env overrides for a binary.
type SecureCLIUserCredential struct {
ID uuid.UUID `json:"id" db:"id"`
BinaryID uuid.UUID `json:"binary_id" db:"binary_id"`
UserID string `json:"user_id" db:"user_id"`
Metadata json.RawMessage `json:"metadata,omitempty" db:"metadata"`
CreatedAt string `json:"created_at" db:"created_at"`
UpdatedAt string `json:"updated_at" db:"updated_at"`
// EncryptedEnv is decrypted JSON — never serialized to API.
EncryptedEnv []byte `json:"-" db:"encrypted_env"`
}
// SecureCLIAgentGrant represents a per-agent grant with optional setting overrides.
type SecureCLIAgentGrant struct {
BaseModel
BinaryID uuid.UUID `json:"binary_id" db:"binary_id"`
AgentID uuid.UUID `json:"agent_id" db:"agent_id"`
DenyArgs *json.RawMessage `json:"deny_args,omitempty" db:"deny_args"`
DenyVerbose *json.RawMessage `json:"deny_verbose,omitempty" db:"deny_verbose"`
TimeoutSeconds *int `json:"timeout_seconds,omitempty" db:"timeout_seconds"`
Tips *string `json:"tips,omitempty" db:"tips"`
Enabled bool `json:"enabled" db:"enabled"`
// EncryptedEnv holds per-grant AES-256-GCM encrypted env vars. NULL means no override.
// Never serialized to API — HTTP layer exposes env_keys + env_set only.
EncryptedEnv []byte `json:"-" db:"encrypted_env"`
// EnvKeys is populated by HTTP handlers only (sorted key names, no values). Not a DB column.
EnvKeys []string `json:"env_keys,omitempty" db:"-"`
// Env is populated by HTTP handlers only for sanitized responses.
Env map[string]SecureCLIEnvResponseEntry `json:"env,omitempty" db:"-"`
// EnvSet indicates whether this grant has an env override. Not a DB column.
EnvSet bool `json:"env_set" db:"-"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// SecureCLIStore manages secure CLI binary credential configurations.
type SecureCLIStore interface {
Create(ctx context.Context, b *SecureCLIBinary) error
Get(ctx context.Context, id uuid.UUID) (*SecureCLIBinary, error)
Update(ctx context.Context, id uuid.UUID, updates map[string]any) error
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context) ([]SecureCLIBinary, error)
// LookupByBinary finds the credential config for a binary name.
// If agentID is provided, checks grant authorization and merges overrides.
// If userID is non-empty, also fetches per-user env overrides via LEFT JOIN.
LookupByBinary(ctx context.Context, binaryName string, agentID *uuid.UUID, userID string) (*SecureCLIBinary, error)
// ListEnabled returns all enabled configs (for TOOLS.md context generation).
ListEnabled(ctx context.Context) ([]SecureCLIBinary, error)
// ListForAgent returns all CLIs accessible by an agent (global + granted),
// with grant overrides merged into the returned configs.
ListForAgent(ctx context.Context, agentID uuid.UUID) ([]SecureCLIBinary, error)
// IsRegisteredBinary reports whether a binary with the given name is
// registered and enabled for the tenant in ctx AND requires a grant
// (is_global = false). Used by the shell exec gate to hard-deny
// execution of credentialed binaries when the calling agent has no
// grant. is_global = true binaries are open to all agents and MUST
// NOT be reported as gate-needing. Tenant-scoped unless IsCrossTenant(ctx).
// Returns (false, nil) when name is empty or tenant context is missing.
IsRegisteredBinary(ctx context.Context, binaryName string) (bool, error)
// --- Per-user credential management ---
GetUserCredentials(ctx context.Context, binaryID uuid.UUID, userID string) (*SecureCLIUserCredential, error)
SetUserCredentials(ctx context.Context, binaryID uuid.UUID, userID string, encryptedEnv []byte) error
DeleteUserCredentials(ctx context.Context, binaryID uuid.UUID, userID string) error
ListUserCredentials(ctx context.Context, binaryID uuid.UUID) ([]SecureCLIUserCredential, error)
}
// SecureCLIAgentGrantStore manages per-agent grants for secure CLI binaries.
type SecureCLIAgentGrantStore interface {
BinaryExists(ctx context.Context, binaryID uuid.UUID) (bool, error)
AgentExists(ctx context.Context, agentID uuid.UUID) (bool, error)
Create(ctx context.Context, g *SecureCLIAgentGrant) error
Get(ctx context.Context, id uuid.UUID) (*SecureCLIAgentGrant, error)
Update(ctx context.Context, id uuid.UUID, updates map[string]any) error
Delete(ctx context.Context, id uuid.UUID) error
ListByBinary(ctx context.Context, binaryID uuid.UUID) ([]SecureCLIAgentGrant, error)
ListByAgent(ctx context.Context, agentID uuid.UUID) ([]SecureCLIAgentGrant, error)
// UpdateGrantEnv sets the encrypted env override for a grant.
// encryptedEnv must be the plaintext JSON bytes — the store layer encrypts with AES-256-GCM.
// Pass nil to clear the env override. Fails closed if encryption key is missing.
UpdateGrantEnv(ctx context.Context, grantID uuid.UUID, plaintextEnv []byte) error
}