mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 14:22:57 +00:00
Squash merge PR #115 after resolving changelog and SQLite migration-map conflicts with current dev. Renumbered channel-context PostgreSQL migration to 000075 and bumped PG required schema to 75 plus SQLite schema to 44 so it follows the run timeline migration. Local checks passed: go test ./..., go build ./..., go build -tags sqliteonly ./..., go vet ./..., and pnpm -C ui/web build. PR CI run 26705617311 passed release-versioning, go, and web.
531 lines
19 KiB
Go
531 lines
19 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
|
)
|
|
|
|
type channelContextPath struct {
|
|
Instance *store.ChannelInstanceData
|
|
ScopeType string
|
|
ScopeKey string
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) resolveContextPath(w http.ResponseWriter, r *http.Request) (channelContextPath, bool) {
|
|
inst, ok := h.resolveInstance(w, r)
|
|
if !ok {
|
|
return channelContextPath{}, false
|
|
}
|
|
scopeType := r.PathValue("scopeType")
|
|
scopeKey := r.PathValue("scopeKey")
|
|
if !validateHTTPChannelScope(scopeType, scopeKey) {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidRequest, "invalid scope"))
|
|
return channelContextPath{}, false
|
|
}
|
|
return channelContextPath{Instance: inst, ScopeType: scopeType, ScopeKey: scopeKey}, true
|
|
}
|
|
|
|
func validateHTTPChannelScope(scopeType, scopeKey string) bool {
|
|
scope := store.ChannelContextScope{ChannelInstanceName: "placeholder", ScopeType: scopeType, ScopeKey: scopeKey}
|
|
return scope.Valid()
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) requireContextAdmin(w http.ResponseWriter, r *http.Request) bool {
|
|
return requireTenantAdmin(w, r, h.tenantStore)
|
|
}
|
|
|
|
type contextMCPGrantRequest struct {
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
ToolAllow json.RawMessage `json:"tool_allow,omitempty"`
|
|
ToolDeny json.RawMessage `json:"tool_deny,omitempty"`
|
|
ConfigOverrides json.RawMessage `json:"config_overrides,omitempty"`
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) handleListContextMCPGrants(w http.ResponseWriter, r *http.Request) {
|
|
if h.mcpContextStore == nil {
|
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "MCP context store is not available"})
|
|
return
|
|
}
|
|
if !h.requireContextAdmin(w, r) {
|
|
return
|
|
}
|
|
path, ok := h.resolveContextPath(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
grants, err := h.mcpContextStore.ListContextGrants(r.Context(), path.Instance.ID, path.ScopeType, path.ScopeKey)
|
|
if err != nil {
|
|
slog.Error("channel_context.mcp_grants.list", "error", err)
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeError(w, http.StatusInternalServerError, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToList, "MCP context grants"))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"grants": grants})
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) handleUpsertContextMCPGrant(w http.ResponseWriter, r *http.Request) {
|
|
if h.mcpContextStore == nil {
|
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "MCP context store is not available"})
|
|
return
|
|
}
|
|
if !h.requireContextAdmin(w, r) {
|
|
return
|
|
}
|
|
path, ok := h.resolveContextPath(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
locale := store.LocaleFromContext(r.Context())
|
|
serverID, err := uuid.Parse(r.PathValue("serverID"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "server"))
|
|
return
|
|
}
|
|
var req contextMCPGrantRequest
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON))
|
|
return
|
|
}
|
|
enabled := true
|
|
if req.Enabled != nil {
|
|
enabled = *req.Enabled
|
|
}
|
|
grant := &store.MCPContextGrant{
|
|
ChannelInstanceID: path.Instance.ID,
|
|
ScopeType: path.ScopeType,
|
|
ScopeKey: path.ScopeKey,
|
|
ServerID: serverID,
|
|
Enabled: enabled,
|
|
ToolAllow: req.ToolAllow,
|
|
ToolDeny: req.ToolDeny,
|
|
ConfigOverrides: req.ConfigOverrides,
|
|
GrantedBy: store.UserIDFromContext(r.Context()),
|
|
}
|
|
if err := h.mcpContextStore.UpsertContextGrant(r.Context(), grant); err != nil {
|
|
slog.Error("channel_context.mcp_grants.upsert", "error", err)
|
|
writeError(w, http.StatusInternalServerError, protocol.ErrInternal, err.Error())
|
|
return
|
|
}
|
|
emitAudit(h.msgBus, r, "channel_context.mcp_grant_upserted", "mcp_server", serverID.String())
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "updated"})
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) handleDeleteContextMCPGrant(w http.ResponseWriter, r *http.Request) {
|
|
if h.mcpContextStore == nil {
|
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "MCP context store is not available"})
|
|
return
|
|
}
|
|
if !h.requireContextAdmin(w, r) {
|
|
return
|
|
}
|
|
path, ok := h.resolveContextPath(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
locale := store.LocaleFromContext(r.Context())
|
|
serverID, err := uuid.Parse(r.PathValue("serverID"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "server"))
|
|
return
|
|
}
|
|
if err := h.mcpContextStore.DeleteContextGrant(r.Context(), path.Instance.ID, path.ScopeType, path.ScopeKey, serverID); err != nil {
|
|
slog.Error("channel_context.mcp_grants.delete", "error", err)
|
|
writeError(w, http.StatusInternalServerError, protocol.ErrInternal, err.Error())
|
|
return
|
|
}
|
|
emitAudit(h.msgBus, r, "channel_context.mcp_grant_deleted", "mcp_server", serverID.String())
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "deleted"})
|
|
}
|
|
|
|
type contextMCPCredentialSummary struct {
|
|
ChannelInstanceID uuid.UUID `json:"channel_instance_id"`
|
|
ScopeType string `json:"scope_type"`
|
|
ScopeKey string `json:"scope_key"`
|
|
ServerID uuid.UUID `json:"server_id"`
|
|
HasAPIKey bool `json:"has_api_key"`
|
|
HasHeaders bool `json:"has_headers"`
|
|
HasEnv bool `json:"has_env"`
|
|
CreatedBy string `json:"created_by"`
|
|
}
|
|
|
|
func summarizeMCPCredentials(creds store.MCPContextCredentials) contextMCPCredentialSummary {
|
|
return contextMCPCredentialSummary{
|
|
ChannelInstanceID: creds.ChannelInstanceID,
|
|
ScopeType: creds.ScopeType,
|
|
ScopeKey: creds.ScopeKey,
|
|
ServerID: creds.ServerID,
|
|
HasAPIKey: creds.APIKey != "",
|
|
HasHeaders: len(creds.Headers) > 0,
|
|
HasEnv: len(creds.Env) > 0,
|
|
CreatedBy: creds.CreatedBy,
|
|
}
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) handleListContextMCPCredentials(w http.ResponseWriter, r *http.Request) {
|
|
if h.mcpContextStore == nil {
|
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "MCP context store is not available"})
|
|
return
|
|
}
|
|
if !h.requireContextAdmin(w, r) {
|
|
return
|
|
}
|
|
path, ok := h.resolveContextPath(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
creds, err := h.mcpContextStore.ListContextCredentials(r.Context(), path.Instance.ID, path.ScopeType, path.ScopeKey)
|
|
if err != nil {
|
|
slog.Error("channel_context.mcp_credentials.list", "error", err)
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeError(w, http.StatusInternalServerError, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToList, "MCP context credentials"))
|
|
return
|
|
}
|
|
out := make([]contextMCPCredentialSummary, 0, len(creds))
|
|
for _, c := range creds {
|
|
out = append(out, summarizeMCPCredentials(c))
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"credentials": out})
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) handleSetContextMCPCredentials(w http.ResponseWriter, r *http.Request) {
|
|
if h.mcpContextStore == nil {
|
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "MCP context store is not available"})
|
|
return
|
|
}
|
|
if !h.requireContextAdmin(w, r) {
|
|
return
|
|
}
|
|
path, ok := h.resolveContextPath(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
locale := store.LocaleFromContext(r.Context())
|
|
serverID, err := uuid.Parse(r.PathValue("serverID"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "server"))
|
|
return
|
|
}
|
|
var creds store.MCPContextCredentials
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&creds); err != nil {
|
|
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON))
|
|
return
|
|
}
|
|
creds.ChannelInstanceID = path.Instance.ID
|
|
creds.ScopeType = path.ScopeType
|
|
creds.ScopeKey = path.ScopeKey
|
|
creds.ServerID = serverID
|
|
creds.CreatedBy = store.UserIDFromContext(r.Context())
|
|
if err := h.mcpContextStore.SetContextCredentials(r.Context(), &creds); err != nil {
|
|
slog.Error("channel_context.mcp_credentials.set", "error", err)
|
|
writeError(w, http.StatusInternalServerError, protocol.ErrInternal, err.Error())
|
|
return
|
|
}
|
|
emitAudit(h.msgBus, r, "channel_context.mcp_credentials_set", "mcp_server", serverID.String())
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "updated"})
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) handleDeleteContextMCPCredentials(w http.ResponseWriter, r *http.Request) {
|
|
if h.mcpContextStore == nil {
|
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "MCP context store is not available"})
|
|
return
|
|
}
|
|
if !h.requireContextAdmin(w, r) {
|
|
return
|
|
}
|
|
path, ok := h.resolveContextPath(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
locale := store.LocaleFromContext(r.Context())
|
|
serverID, err := uuid.Parse(r.PathValue("serverID"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "server"))
|
|
return
|
|
}
|
|
if err := h.mcpContextStore.DeleteContextCredentials(r.Context(), path.Instance.ID, path.ScopeType, path.ScopeKey, serverID); err != nil {
|
|
slog.Error("channel_context.mcp_credentials.delete", "error", err)
|
|
writeError(w, http.StatusInternalServerError, protocol.ErrInternal, err.Error())
|
|
return
|
|
}
|
|
emitAudit(h.msgBus, r, "channel_context.mcp_credentials_deleted", "mcp_server", serverID.String())
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "deleted"})
|
|
}
|
|
|
|
type contextCLIGrantRequest struct {
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
EnvVars json.RawMessage `json:"env_vars,omitempty"`
|
|
DenyArgs *json.RawMessage `json:"deny_args,omitempty"`
|
|
DenyVerbose *json.RawMessage `json:"deny_verbose,omitempty"`
|
|
TimeoutSeconds *int `json:"timeout_seconds,omitempty"`
|
|
Tips *string `json:"tips,omitempty"`
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) handleListContextCLIGrants(w http.ResponseWriter, r *http.Request) {
|
|
if h.cliContextStore == nil {
|
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "Secure CLI context store is not available"})
|
|
return
|
|
}
|
|
if !h.requireContextAdmin(w, r) {
|
|
return
|
|
}
|
|
path, ok := h.resolveContextPath(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
grants, err := h.cliContextStore.ListContextGrants(r.Context(), path.Instance.ID, path.ScopeType, path.ScopeKey)
|
|
if err != nil {
|
|
slog.Error("channel_context.cli_grants.list", "error", err)
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeError(w, http.StatusInternalServerError, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToList, "CLI context grants"))
|
|
return
|
|
}
|
|
for i := range grants {
|
|
populateContextGrantEnvFields(&grants[i])
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"grants": grants})
|
|
}
|
|
|
|
func populateContextGrantEnvFields(g *store.SecureCLIContextGrant) {
|
|
if len(g.EncryptedEnv) == 0 {
|
|
g.EnvKeys = []string{}
|
|
g.Env = nil
|
|
g.EnvSet = false
|
|
return
|
|
}
|
|
g.EnvKeys = store.SecureCLIEnvKeys(g.EncryptedEnv)
|
|
g.Env = store.SanitizeSecureCLIEnvJSON(g.EncryptedEnv)
|
|
g.EnvSet = len(g.EnvKeys) > 0
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) handleUpsertContextCLIGrant(w http.ResponseWriter, r *http.Request) {
|
|
if h.cliContextStore == nil {
|
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "Secure CLI context store is not available"})
|
|
return
|
|
}
|
|
if !h.requireContextAdmin(w, r) {
|
|
return
|
|
}
|
|
path, ok := h.resolveContextPath(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
locale := store.LocaleFromContext(r.Context())
|
|
binaryID, err := uuid.Parse(r.PathValue("binaryID"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "credential"))
|
|
return
|
|
}
|
|
var req contextCLIGrantRequest
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON))
|
|
return
|
|
}
|
|
enabled := true
|
|
if req.Enabled != nil {
|
|
enabled = *req.Enabled
|
|
}
|
|
var envJSON []byte
|
|
if len(req.EnvVars) > 0 {
|
|
var ok bool
|
|
envJSON, ok = validateAndSerializeEnvVars(w, locale, req.EnvVars)
|
|
if !ok {
|
|
return
|
|
}
|
|
}
|
|
grant := &store.SecureCLIContextGrant{
|
|
ChannelInstanceID: path.Instance.ID,
|
|
ScopeType: path.ScopeType,
|
|
ScopeKey: path.ScopeKey,
|
|
BinaryID: binaryID,
|
|
DenyArgs: req.DenyArgs,
|
|
DenyVerbose: req.DenyVerbose,
|
|
TimeoutSeconds: req.TimeoutSeconds,
|
|
Tips: req.Tips,
|
|
EncryptedEnv: envJSON,
|
|
Enabled: enabled,
|
|
GrantedBy: store.UserIDFromContext(r.Context()),
|
|
}
|
|
if err := h.cliContextStore.UpsertContextGrant(r.Context(), grant); err != nil {
|
|
slog.Error("channel_context.cli_grants.upsert", "error", err)
|
|
writeError(w, http.StatusInternalServerError, protocol.ErrInternal, err.Error())
|
|
return
|
|
}
|
|
emitAudit(h.msgBus, r, "channel_context.cli_grant_upserted", "secure_cli", binaryID.String())
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "updated"})
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) handleDeleteContextCLIGrant(w http.ResponseWriter, r *http.Request) {
|
|
if h.cliContextStore == nil {
|
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "Secure CLI context store is not available"})
|
|
return
|
|
}
|
|
if !h.requireContextAdmin(w, r) {
|
|
return
|
|
}
|
|
path, ok := h.resolveContextPath(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
locale := store.LocaleFromContext(r.Context())
|
|
binaryID, err := uuid.Parse(r.PathValue("binaryID"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "credential"))
|
|
return
|
|
}
|
|
if err := h.cliContextStore.DeleteContextGrant(r.Context(), path.Instance.ID, path.ScopeType, path.ScopeKey, binaryID); err != nil {
|
|
slog.Error("channel_context.cli_grants.delete", "error", err)
|
|
writeError(w, http.StatusInternalServerError, protocol.ErrInternal, err.Error())
|
|
return
|
|
}
|
|
emitAudit(h.msgBus, r, "channel_context.cli_grant_deleted", "secure_cli", binaryID.String())
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "deleted"})
|
|
}
|
|
|
|
type contextCLICredentialRequest struct {
|
|
EnvVars json.RawMessage `json:"env_vars"`
|
|
Metadata json.RawMessage `json:"metadata,omitempty"`
|
|
CredentialType *string `json:"credential_type,omitempty"`
|
|
HostScope *string `json:"host_scope,omitempty"`
|
|
}
|
|
|
|
type contextCLICredentialSummary struct {
|
|
ChannelInstanceID uuid.UUID `json:"channel_instance_id"`
|
|
ScopeType string `json:"scope_type"`
|
|
ScopeKey string `json:"scope_key"`
|
|
BinaryID uuid.UUID `json:"binary_id"`
|
|
EnvSet bool `json:"env_set"`
|
|
EnvKeys []string `json:"env_keys"`
|
|
Metadata json.RawMessage `json:"metadata,omitempty"`
|
|
CredentialType *string `json:"credential_type,omitempty"`
|
|
HostScope *string `json:"host_scope,omitempty"`
|
|
CreatedBy string `json:"created_by"`
|
|
}
|
|
|
|
func summarizeCLICredentials(creds store.SecureCLIContextCredentials) contextCLICredentialSummary {
|
|
keys := store.SecureCLIEnvKeys(creds.EncryptedEnv)
|
|
return contextCLICredentialSummary{
|
|
ChannelInstanceID: creds.ChannelInstanceID,
|
|
ScopeType: creds.ScopeType,
|
|
ScopeKey: creds.ScopeKey,
|
|
BinaryID: creds.BinaryID,
|
|
EnvSet: len(keys) > 0,
|
|
EnvKeys: keys,
|
|
Metadata: creds.Metadata,
|
|
CredentialType: creds.CredentialType,
|
|
HostScope: creds.HostScope,
|
|
CreatedBy: creds.CreatedBy,
|
|
}
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) handleListContextCLICredentials(w http.ResponseWriter, r *http.Request) {
|
|
if h.cliContextStore == nil {
|
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "Secure CLI context store is not available"})
|
|
return
|
|
}
|
|
if !h.requireContextAdmin(w, r) {
|
|
return
|
|
}
|
|
path, ok := h.resolveContextPath(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
creds, err := h.cliContextStore.ListContextCredentials(r.Context(), path.Instance.ID, path.ScopeType, path.ScopeKey)
|
|
if err != nil {
|
|
slog.Error("channel_context.cli_credentials.list", "error", err)
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeError(w, http.StatusInternalServerError, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToList, "CLI context credentials"))
|
|
return
|
|
}
|
|
out := make([]contextCLICredentialSummary, 0, len(creds))
|
|
for _, c := range creds {
|
|
out = append(out, summarizeCLICredentials(c))
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"credentials": out})
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) handleSetContextCLICredentials(w http.ResponseWriter, r *http.Request) {
|
|
if h.cliContextStore == nil {
|
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "Secure CLI context store is not available"})
|
|
return
|
|
}
|
|
if !h.requireContextAdmin(w, r) {
|
|
return
|
|
}
|
|
path, ok := h.resolveContextPath(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
locale := store.LocaleFromContext(r.Context())
|
|
binaryID, err := uuid.Parse(r.PathValue("binaryID"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "credential"))
|
|
return
|
|
}
|
|
var req contextCLICredentialRequest
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON))
|
|
return
|
|
}
|
|
if len(req.EnvVars) == 0 {
|
|
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "env_vars"))
|
|
return
|
|
}
|
|
envJSON, ok := validateAndSerializeEnvVars(w, locale, req.EnvVars)
|
|
if !ok {
|
|
return
|
|
}
|
|
creds := &store.SecureCLIContextCredentials{
|
|
ChannelInstanceID: path.Instance.ID,
|
|
ScopeType: path.ScopeType,
|
|
ScopeKey: path.ScopeKey,
|
|
BinaryID: binaryID,
|
|
EncryptedEnv: envJSON,
|
|
Metadata: req.Metadata,
|
|
CredentialType: req.CredentialType,
|
|
HostScope: req.HostScope,
|
|
CreatedBy: store.UserIDFromContext(r.Context()),
|
|
}
|
|
if err := h.cliContextStore.SetContextCredentials(r.Context(), creds); err != nil {
|
|
slog.Error("channel_context.cli_credentials.set", "error", err)
|
|
writeError(w, http.StatusInternalServerError, protocol.ErrInternal, err.Error())
|
|
return
|
|
}
|
|
emitAudit(h.msgBus, r, "channel_context.cli_credentials_set", "secure_cli", binaryID.String())
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "updated"})
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) handleDeleteContextCLICredentials(w http.ResponseWriter, r *http.Request) {
|
|
if h.cliContextStore == nil {
|
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "Secure CLI context store is not available"})
|
|
return
|
|
}
|
|
if !h.requireContextAdmin(w, r) {
|
|
return
|
|
}
|
|
path, ok := h.resolveContextPath(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
locale := store.LocaleFromContext(r.Context())
|
|
binaryID, err := uuid.Parse(r.PathValue("binaryID"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "credential"))
|
|
return
|
|
}
|
|
if err := h.cliContextStore.DeleteContextCredentials(r.Context(), path.Instance.ID, path.ScopeType, path.ScopeKey, binaryID); err != nil {
|
|
slog.Error("channel_context.cli_credentials.delete", "error", err)
|
|
writeError(w, http.StatusInternalServerError, protocol.ErrInternal, err.Error())
|
|
return
|
|
}
|
|
emitAudit(h.msgBus, r, "channel_context.cli_credentials_deleted", "secure_cli", binaryID.String())
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "deleted"})
|
|
}
|