package pg import ( "context" "database/sql" "encoding/json" "errors" "fmt" "time" "github.com/google/uuid" "github.com/nextlevelbuilder/goclaw/internal/crypto" "github.com/nextlevelbuilder/goclaw/internal/store" ) func resolveContextChannelInstanceID(ctx context.Context, db *sql.DB, scope store.ChannelContextScope) (uuid.UUID, error) { if scope.ChannelInstanceID != uuid.Nil { return scope.ChannelInstanceID, nil } if scope.ChannelInstanceName == "" { return uuid.Nil, sql.ErrNoRows } var id uuid.UUID err := db.QueryRowContext(ctx, `SELECT id FROM channel_instances WHERE tenant_id = $1 AND name = $2`, tenantIDForInsert(ctx), scope.ChannelInstanceName, ).Scan(&id) return id, err } func validateChannelScope(scopeType, scopeKey string) error { switch scopeType { case store.ChannelScopeTypeChannel: return nil case store.ChannelScopeTypeGroup, store.ChannelScopeTypeUser, store.ChannelScopeTypeRole: if scopeKey == "" { return fmt.Errorf("scope_key required for %s scope", scopeType) } return nil default: return fmt.Errorf("invalid scope_type: %s", scopeType) } } func rawPtrOrNull(raw *json.RawMessage) any { if raw == nil || len(*raw) == 0 { return nil } return *raw } func jsonOrEmptyObject(raw json.RawMessage) any { if len(raw) == 0 { return json.RawMessage(`{}`) } return raw } func (s *PGMCPServerStore) UpsertContextGrant(ctx context.Context, g *store.MCPContextGrant) error { if err := validateChannelScope(g.ScopeType, g.ScopeKey); err != nil { return err } if err := store.ValidateUserID(g.GrantedBy); err != nil { return err } if g.ID == uuid.Nil { g.ID = store.GenNewID() } now := time.Now() g.CreatedAt = now g.UpdatedAt = now _, err := s.db.ExecContext(ctx, `INSERT INTO mcp_context_grants (id, tenant_id, channel_instance_id, scope_type, scope_key, server_id, enabled, tool_allow, tool_deny, config_overrides, granted_by, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT (tenant_id, channel_instance_id, scope_type, scope_key, server_id) DO UPDATE SET enabled = EXCLUDED.enabled, tool_allow = EXCLUDED.tool_allow, tool_deny = EXCLUDED.tool_deny, config_overrides = EXCLUDED.config_overrides, granted_by = EXCLUDED.granted_by, updated_at = EXCLUDED.updated_at`, g.ID, tenantIDForInsert(ctx), g.ChannelInstanceID, g.ScopeType, g.ScopeKey, g.ServerID, g.Enabled, jsonOrNull(g.ToolAllow), jsonOrNull(g.ToolDeny), jsonOrNull(g.ConfigOverrides), g.GrantedBy, now, now, ) return err } func (s *PGMCPServerStore) DeleteContextGrant(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string, serverID uuid.UUID) error { _, err := s.db.ExecContext(ctx, `DELETE FROM mcp_context_grants WHERE tenant_id = $1 AND channel_instance_id = $2 AND scope_type = $3 AND scope_key = $4 AND server_id = $5`, tenantIDForInsert(ctx), channelInstanceID, scopeType, scopeKey, serverID, ) return err } func (s *PGMCPServerStore) ListContextGrants(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string) ([]store.MCPContextGrant, error) { rows, err := s.db.QueryContext(ctx, `SELECT id, channel_instance_id, scope_type, scope_key, server_id, enabled, COALESCE(tool_allow, 'null'::jsonb), COALESCE(tool_deny, 'null'::jsonb), COALESCE(config_overrides, 'null'::jsonb), granted_by, created_at, updated_at FROM mcp_context_grants WHERE tenant_id = $1 AND channel_instance_id = $2 AND scope_type = $3 AND scope_key = $4 ORDER BY created_at`, tenantIDForInsert(ctx), channelInstanceID, scopeType, scopeKey, ) if err != nil { return nil, err } defer rows.Close() result := make([]store.MCPContextGrant, 0) for rows.Next() { var g store.MCPContextGrant if err := rows.Scan(&g.ID, &g.ChannelInstanceID, &g.ScopeType, &g.ScopeKey, &g.ServerID, &g.Enabled, &g.ToolAllow, &g.ToolDeny, &g.ConfigOverrides, &g.GrantedBy, &g.CreatedAt, &g.UpdatedAt); err != nil { return nil, err } result = append(result, g) } return result, rows.Err() } func (s *PGMCPServerStore) ListContextGrantsForScope(ctx context.Context, scope store.ChannelContextScope) ([]store.MCPContextGrant, error) { id, err := resolveContextChannelInstanceID(ctx, s.db, scope) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, err } return s.ListContextGrants(ctx, id, scope.ScopeType, scope.ScopeKey) } func (s *PGMCPServerStore) SetContextCredentials(ctx context.Context, creds *store.MCPContextCredentials) error { if err := validateChannelScope(creds.ScopeType, creds.ScopeKey); err != nil { return err } if err := store.ValidateUserID(creds.CreatedBy); err != nil { return err } if creds.ID == uuid.Nil { creds.ID = store.GenNewID() } var apiKey sql.NullString if creds.APIKey != "" && s.encKey != "" { enc, err := crypto.Encrypt(creds.APIKey, s.encKey) if err != nil { return fmt.Errorf("encrypt mcp context api_key: %w", err) } apiKey = sql.NullString{String: enc, Valid: true} } else if creds.APIKey != "" { apiKey = sql.NullString{String: creds.APIKey, Valid: true} } var headersEnc, envEnc []byte if len(creds.Headers) > 0 { raw, _ := json.Marshal(creds.Headers) headersEnc = s.encryptJSONB(raw) } if len(creds.Env) > 0 { raw, _ := json.Marshal(creds.Env) envEnc = s.encryptJSONB(raw) } now := time.Now() _, err := s.db.ExecContext(ctx, `INSERT INTO mcp_context_credentials (id, tenant_id, channel_instance_id, scope_type, scope_key, server_id, api_key, headers, env, created_by, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT (tenant_id, channel_instance_id, scope_type, scope_key, server_id) DO UPDATE SET api_key = EXCLUDED.api_key, headers = EXCLUDED.headers, env = EXCLUDED.env, created_by = EXCLUDED.created_by, updated_at = EXCLUDED.updated_at`, creds.ID, tenantIDForInsert(ctx), creds.ChannelInstanceID, creds.ScopeType, creds.ScopeKey, creds.ServerID, apiKey, headersEnc, envEnc, creds.CreatedBy, now, now, ) return err } func (s *PGMCPServerStore) GetContextCredentials(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string, serverID uuid.UUID) (*store.MCPContextCredentials, error) { row := s.db.QueryRowContext(ctx, `SELECT id, channel_instance_id, scope_type, scope_key, server_id, api_key, headers, env, created_by, created_at, updated_at FROM mcp_context_credentials WHERE tenant_id = $1 AND channel_instance_id = $2 AND scope_type = $3 AND scope_key = $4 AND server_id = $5`, tenantIDForInsert(ctx), channelInstanceID, scopeType, scopeKey, serverID, ) creds, err := s.scanContextCredentials(row) if errors.Is(err, sql.ErrNoRows) { return nil, nil } return creds, err } func (s *PGMCPServerStore) GetContextCredentialsForScope(ctx context.Context, scope store.ChannelContextScope, serverID uuid.UUID) (*store.MCPContextCredentials, error) { id, err := resolveContextChannelInstanceID(ctx, s.db, scope) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, err } return s.GetContextCredentials(ctx, id, scope.ScopeType, scope.ScopeKey, serverID) } func (s *PGMCPServerStore) DeleteContextCredentials(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string, serverID uuid.UUID) error { _, err := s.db.ExecContext(ctx, `DELETE FROM mcp_context_credentials WHERE tenant_id = $1 AND channel_instance_id = $2 AND scope_type = $3 AND scope_key = $4 AND server_id = $5`, tenantIDForInsert(ctx), channelInstanceID, scopeType, scopeKey, serverID, ) return err } func (s *PGMCPServerStore) ListContextCredentials(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string) ([]store.MCPContextCredentials, error) { rows, err := s.db.QueryContext(ctx, `SELECT id, channel_instance_id, scope_type, scope_key, server_id, api_key, headers, env, created_by, created_at, updated_at FROM mcp_context_credentials WHERE tenant_id = $1 AND channel_instance_id = $2 AND scope_type = $3 AND scope_key = $4 ORDER BY created_at`, tenantIDForInsert(ctx), channelInstanceID, scopeType, scopeKey, ) if err != nil { return nil, err } defer rows.Close() result := make([]store.MCPContextCredentials, 0) for rows.Next() { creds, err := s.scanContextCredentials(rows) if err != nil { return nil, err } result = append(result, *creds) } return result, rows.Err() } func (s *PGMCPServerStore) ListContextCredentialsForScope(ctx context.Context, scope store.ChannelContextScope) ([]store.MCPContextCredentials, error) { id, err := resolveContextChannelInstanceID(ctx, s.db, scope) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, err } return s.ListContextCredentials(ctx, id, scope.ScopeType, scope.ScopeKey) } type contextCredentialScanner interface { Scan(dest ...any) error } func (s *PGMCPServerStore) scanContextCredentials(row contextCredentialScanner) (*store.MCPContextCredentials, error) { var creds store.MCPContextCredentials var apiKey sql.NullString var headersEnc, envEnc []byte if err := row.Scan(&creds.ID, &creds.ChannelInstanceID, &creds.ScopeType, &creds.ScopeKey, &creds.ServerID, &apiKey, &headersEnc, &envEnc, &creds.CreatedBy, &creds.CreatedAt, &creds.UpdatedAt); err != nil { return nil, err } if apiKey.Valid && apiKey.String != "" && s.encKey != "" { if dec, err := crypto.Decrypt(apiKey.String, s.encKey); err == nil { creds.APIKey = dec } } else if apiKey.Valid { creds.APIKey = apiKey.String } if len(headersEnc) > 0 { _ = json.Unmarshal(s.decryptJSONB(headersEnc), &creds.Headers) } if len(envEnc) > 0 { _ = json.Unmarshal(s.decryptJSONB(envEnc), &creds.Env) } return &creds, nil } func (s *PGSecureCLIStore) UpsertContextGrant(ctx context.Context, g *store.SecureCLIContextGrant) error { if err := validateChannelScope(g.ScopeType, g.ScopeKey); err != nil { return err } if err := store.ValidateUserID(g.GrantedBy); err != nil { return err } if g.ID == uuid.Nil { g.ID = store.GenNewID() } var envBytes []byte if len(g.EncryptedEnv) > 0 && s.encKey != "" { encrypted, err := crypto.Encrypt(string(g.EncryptedEnv), s.encKey) if err != nil { return fmt.Errorf("encrypt context grant env: %w", err) } envBytes = []byte(encrypted) } else { envBytes = g.EncryptedEnv } now := time.Now() _, err := s.db.ExecContext(ctx, `INSERT INTO secure_cli_context_grants (id, tenant_id, channel_instance_id, scope_type, scope_key, binary_id, deny_args, deny_verbose, timeout_seconds, tips, encrypted_env, enabled, granted_by, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT (tenant_id, channel_instance_id, scope_type, scope_key, binary_id) DO UPDATE SET deny_args = EXCLUDED.deny_args, deny_verbose = EXCLUDED.deny_verbose, timeout_seconds = EXCLUDED.timeout_seconds, tips = EXCLUDED.tips, encrypted_env = EXCLUDED.encrypted_env, enabled = EXCLUDED.enabled, granted_by = EXCLUDED.granted_by, updated_at = EXCLUDED.updated_at`, g.ID, tenantIDForInsert(ctx), g.ChannelInstanceID, g.ScopeType, g.ScopeKey, g.BinaryID, rawPtrOrNull(g.DenyArgs), rawPtrOrNull(g.DenyVerbose), g.TimeoutSeconds, g.Tips, envBytes, g.Enabled, g.GrantedBy, now, now, ) return err } func (s *PGSecureCLIStore) DeleteContextGrant(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string, binaryID uuid.UUID) error { _, err := s.db.ExecContext(ctx, `DELETE FROM secure_cli_context_grants WHERE tenant_id = $1 AND channel_instance_id = $2 AND scope_type = $3 AND scope_key = $4 AND binary_id = $5`, tenantIDForInsert(ctx), channelInstanceID, scopeType, scopeKey, binaryID, ) return err } func (s *PGSecureCLIStore) ListContextGrants(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string) ([]store.SecureCLIContextGrant, error) { rows, err := s.db.QueryContext(ctx, `SELECT id, channel_instance_id, scope_type, scope_key, binary_id, deny_args, deny_verbose, timeout_seconds, tips, encrypted_env, enabled, granted_by, created_at, updated_at FROM secure_cli_context_grants WHERE tenant_id = $1 AND channel_instance_id = $2 AND scope_type = $3 AND scope_key = $4 ORDER BY created_at`, tenantIDForInsert(ctx), channelInstanceID, scopeType, scopeKey, ) if err != nil { return nil, err } defer rows.Close() result := make([]store.SecureCLIContextGrant, 0) for rows.Next() { g, err := s.scanSecureCLIContextGrant(rows) if err != nil { return nil, err } result = append(result, *g) } return result, rows.Err() } func (s *PGSecureCLIStore) ListContextGrantsForScope(ctx context.Context, scope store.ChannelContextScope) ([]store.SecureCLIContextGrant, error) { id, err := resolveContextChannelInstanceID(ctx, s.db, scope) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, err } return s.ListContextGrants(ctx, id, scope.ScopeType, scope.ScopeKey) } func (s *PGSecureCLIStore) scanSecureCLIContextGrant(row contextCredentialScanner) (*store.SecureCLIContextGrant, error) { var g store.SecureCLIContextGrant var denyArgs, denyVerbose *[]byte if err := row.Scan(&g.ID, &g.ChannelInstanceID, &g.ScopeType, &g.ScopeKey, &g.BinaryID, &denyArgs, &denyVerbose, &g.TimeoutSeconds, &g.Tips, &g.EncryptedEnv, &g.Enabled, &g.GrantedBy, &g.CreatedAt, &g.UpdatedAt); err != nil { return nil, err } if denyArgs != nil { raw := json.RawMessage(*denyArgs) g.DenyArgs = &raw } if denyVerbose != nil { raw := json.RawMessage(*denyVerbose) g.DenyVerbose = &raw } if len(g.EncryptedEnv) > 0 && s.encKey != "" { if decrypted, err := crypto.Decrypt(string(g.EncryptedEnv), s.encKey); err == nil { g.EncryptedEnv = []byte(decrypted) } } g.EnvSet = len(g.EncryptedEnv) > 0 return &g, nil } func (s *PGSecureCLIStore) SetContextCredentials(ctx context.Context, creds *store.SecureCLIContextCredentials) error { if err := validateChannelScope(creds.ScopeType, creds.ScopeKey); err != nil { return err } if err := store.ValidateUserID(creds.CreatedBy); err != nil { return err } if creds.ID == uuid.Nil { creds.ID = store.GenNewID() } var envBytes []byte if len(creds.EncryptedEnv) > 0 && s.encKey != "" { encrypted, err := crypto.Encrypt(string(creds.EncryptedEnv), s.encKey) if err != nil { return fmt.Errorf("encrypt context credential env: %w", err) } envBytes = []byte(encrypted) } else { envBytes = creds.EncryptedEnv } now := time.Now() _, err := s.db.ExecContext(ctx, `INSERT INTO secure_cli_context_credentials (id, tenant_id, channel_instance_id, scope_type, scope_key, binary_id, encrypted_env, metadata, credential_type, host_scope, created_by, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT (tenant_id, channel_instance_id, scope_type, scope_key, binary_id) DO UPDATE SET encrypted_env = EXCLUDED.encrypted_env, metadata = EXCLUDED.metadata, credential_type = EXCLUDED.credential_type, host_scope = EXCLUDED.host_scope, created_by = EXCLUDED.created_by, updated_at = EXCLUDED.updated_at`, creds.ID, tenantIDForInsert(ctx), creds.ChannelInstanceID, creds.ScopeType, creds.ScopeKey, creds.BinaryID, envBytes, jsonOrEmptyObject(creds.Metadata), creds.CredentialType, creds.HostScope, creds.CreatedBy, now, now, ) return err } func (s *PGSecureCLIStore) GetContextCredentials(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string, binaryID uuid.UUID) (*store.SecureCLIContextCredentials, error) { row := s.db.QueryRowContext(ctx, `SELECT id, channel_instance_id, scope_type, scope_key, binary_id, encrypted_env, COALESCE(metadata, '{}'::jsonb), credential_type, host_scope, created_by, created_at, updated_at FROM secure_cli_context_credentials WHERE tenant_id = $1 AND channel_instance_id = $2 AND scope_type = $3 AND scope_key = $4 AND binary_id = $5`, tenantIDForInsert(ctx), channelInstanceID, scopeType, scopeKey, binaryID, ) creds, err := s.scanSecureCLIContextCredentials(row) if errors.Is(err, sql.ErrNoRows) { return nil, nil } return creds, err } func (s *PGSecureCLIStore) GetContextCredentialsForScope(ctx context.Context, scope store.ChannelContextScope, binaryID uuid.UUID) (*store.SecureCLIContextCredentials, error) { id, err := resolveContextChannelInstanceID(ctx, s.db, scope) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, err } return s.GetContextCredentials(ctx, id, scope.ScopeType, scope.ScopeKey, binaryID) } func (s *PGSecureCLIStore) DeleteContextCredentials(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string, binaryID uuid.UUID) error { _, err := s.db.ExecContext(ctx, `DELETE FROM secure_cli_context_credentials WHERE tenant_id = $1 AND channel_instance_id = $2 AND scope_type = $3 AND scope_key = $4 AND binary_id = $5`, tenantIDForInsert(ctx), channelInstanceID, scopeType, scopeKey, binaryID, ) return err } func (s *PGSecureCLIStore) ListContextCredentials(ctx context.Context, channelInstanceID uuid.UUID, scopeType, scopeKey string) ([]store.SecureCLIContextCredentials, error) { rows, err := s.db.QueryContext(ctx, `SELECT id, channel_instance_id, scope_type, scope_key, binary_id, encrypted_env, COALESCE(metadata, '{}'::jsonb), credential_type, host_scope, created_by, created_at, updated_at FROM secure_cli_context_credentials WHERE tenant_id = $1 AND channel_instance_id = $2 AND scope_type = $3 AND scope_key = $4 ORDER BY created_at`, tenantIDForInsert(ctx), channelInstanceID, scopeType, scopeKey, ) if err != nil { return nil, err } defer rows.Close() result := make([]store.SecureCLIContextCredentials, 0) for rows.Next() { creds, err := s.scanSecureCLIContextCredentials(rows) if err != nil { return nil, err } result = append(result, *creds) } return result, rows.Err() } func (s *PGSecureCLIStore) ListContextCredentialsForScope(ctx context.Context, scope store.ChannelContextScope) ([]store.SecureCLIContextCredentials, error) { id, err := resolveContextChannelInstanceID(ctx, s.db, scope) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, err } return s.ListContextCredentials(ctx, id, scope.ScopeType, scope.ScopeKey) } func (s *PGSecureCLIStore) scanSecureCLIContextCredentials(row contextCredentialScanner) (*store.SecureCLIContextCredentials, error) { var creds store.SecureCLIContextCredentials if err := row.Scan(&creds.ID, &creds.ChannelInstanceID, &creds.ScopeType, &creds.ScopeKey, &creds.BinaryID, &creds.EncryptedEnv, &creds.Metadata, &creds.CredentialType, &creds.HostScope, &creds.CreatedBy, &creds.CreatedAt, &creds.UpdatedAt); err != nil { return nil, err } if len(creds.EncryptedEnv) > 0 && s.encKey != "" { if decrypted, err := crypto.Decrypt(string(creds.EncryptedEnv), s.encKey); err == nil { creds.EncryptedEnv = []byte(decrypted) } } return &creds, nil }