mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-21 02:20:03 +00:00
234 lines
12 KiB
Go
234 lines
12 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"`
|
|
// AdapterName routes the binary to a CredentialAdapter at exec time.
|
|
// NULL/empty → passthrough adapter (legacy env-vars injection only).
|
|
// Non-empty (e.g. "git") → typed adapter resolved via tools.LookupAdapter.
|
|
AdapterName *string `json:"adapter_name,omitempty" db:"adapter_name"`
|
|
UserEnv []byte `json:"-" db:"-"` // per-user encrypted env (populated by LookupByBinary LEFT JOIN)
|
|
// UserCredentialType + UserHostScope mirror the joined user-credential row
|
|
// (populated by LookupByBinary). NULL when the user has no credential or the
|
|
// credential is legacy env-only.
|
|
UserCredentialType *string `json:"-" db:"-"`
|
|
UserHostScope *string `json:"-" db:"-"`
|
|
// CredentialEnv + metadata carry the effective typed credential selected
|
|
// for runtime injection. Source can be "user", "context", "agent", or "".
|
|
CredentialEnv []byte `json:"-" db:"-"`
|
|
CredentialType *string `json:"-" db:"-"`
|
|
CredentialHostScope *string `json:"-" db:"-"`
|
|
CredentialSource string `json:"credential_source,omitempty" db:"-"`
|
|
CredentialSubjectID string `json:"credential_subject_id,omitempty" db:"-"`
|
|
// 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:"-"`
|
|
}
|
|
|
|
// SetEffectiveCredential records the credential material selected for runtime
|
|
// injection. The legacy User* fields remain populated for older call sites that
|
|
// still synthesize adapter credentials from the binary row.
|
|
func (b *SecureCLIBinary) SetEffectiveCredential(env []byte, credentialType, hostScope *string, source, subjectID string) {
|
|
if b == nil {
|
|
return
|
|
}
|
|
b.CredentialEnv = env
|
|
b.CredentialType = credentialType
|
|
b.CredentialHostScope = hostScope
|
|
b.CredentialSource = source
|
|
b.CredentialSubjectID = subjectID
|
|
if source == "user" || source == "context" {
|
|
b.UserEnv = env
|
|
b.UserCredentialType = credentialType
|
|
b.UserHostScope = hostScope
|
|
}
|
|
}
|
|
|
|
// 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"`
|
|
// CredentialType selects the wire shape carried in EncryptedEnv.
|
|
// NULL/empty → legacy env-vars map. Future: 'pat', 'ssh_key', 'pg_password_file'.
|
|
CredentialType *string `json:"credential_type,omitempty" db:"credential_type"`
|
|
// HostScope binds the credential to a specific hostname (e.g. 'github.com').
|
|
// Required when CredentialType ∈ {'pat','ssh_key'}; NULL for legacy env creds.
|
|
HostScope *string `json:"host_scope,omitempty" db:"host_scope"`
|
|
}
|
|
|
|
// SecureCLIAgentCredential holds per-agent encrypted env overrides for a binary.
|
|
type SecureCLIAgentCredential struct {
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
BinaryID uuid.UUID `json:"binary_id" db:"binary_id"`
|
|
AgentID uuid.UUID `json:"agent_id" db:"agent_id"`
|
|
AgentKey string `json:"agent_key,omitempty" db:"-"`
|
|
Name string `json:"name,omitempty" db:"-"`
|
|
Metadata json.RawMessage `json:"metadata,omitempty" db:"metadata"`
|
|
CreatedBy string `json:"created_by" db:"created_by"`
|
|
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"`
|
|
// CredentialType selects the wire shape carried in EncryptedEnv.
|
|
CredentialType *string `json:"credential_type,omitempty" db:"credential_type"`
|
|
// HostScope binds the credential to a specific hostname.
|
|
HostScope *string `json:"host_scope,omitempty" db:"host_scope"`
|
|
}
|
|
|
|
// 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
|
|
// SetUserCredentialsTyped writes a typed user credential (PAT, SSH key, etc.).
|
|
// credentialType and hostScope are nil for legacy env-vars credentials —
|
|
// in that case behavior is identical to SetUserCredentials.
|
|
SetUserCredentialsTyped(ctx context.Context, binaryID uuid.UUID, userID string, encryptedEnv []byte, credentialType, hostScope *string) error
|
|
DeleteUserCredentials(ctx context.Context, binaryID uuid.UUID, userID string) error
|
|
ListUserCredentials(ctx context.Context, binaryID uuid.UUID) ([]SecureCLIUserCredential, error)
|
|
}
|
|
|
|
// SecureCLIAgentCredentialStore manages per-agent credential material for
|
|
// secure CLI binaries. It intentionally does not grant binary access.
|
|
type SecureCLIAgentCredentialStore interface {
|
|
BinaryExists(ctx context.Context, binaryID uuid.UUID) (bool, error)
|
|
AgentExists(ctx context.Context, agentID uuid.UUID) (bool, error)
|
|
GetAgentCredentials(ctx context.Context, binaryID uuid.UUID, agentID uuid.UUID) (*SecureCLIAgentCredential, error)
|
|
SetAgentCredentials(ctx context.Context, binaryID uuid.UUID, agentID uuid.UUID, encryptedEnv []byte, createdBy string) error
|
|
SetAgentCredentialsTyped(ctx context.Context, binaryID uuid.UUID, agentID uuid.UUID, encryptedEnv []byte, credentialType, hostScope *string, createdBy string) error
|
|
DeleteAgentCredentials(ctx context.Context, binaryID uuid.UUID, agentID uuid.UUID) error
|
|
ListAgentCredentials(ctx context.Context, binaryID uuid.UUID) ([]SecureCLIAgentCredential, 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
|
|
}
|