mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-13 04:11:23 +00:00
75c570e951
- Secure CLI credential injection via AES-256-GCM encrypted env vars - API key management with fine-grained RBAC scopes - resolveAuth/requireAuth middleware across all 25+ HTTP handlers - In-memory API key cache with TTL, negative caching, pubsub invalidation - Sandbox-first execution (fails if unavailable, no silent fallback) - Credential scrubbing, constant-time token comparison, Admin-only CLI creds - SQL migration 000020: secure_cli_binaries + api_keys tables - 14 unit tests for cache and RBAC with race detector Closes #197
150 lines
4.8 KiB
Go
150 lines
4.8 KiB
Go
package methods
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/crypto"
|
|
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
|
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
|
"github.com/nextlevelbuilder/goclaw/internal/permissions"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
|
)
|
|
|
|
// APIKeysMethods handles api_keys.list, api_keys.create, api_keys.revoke.
|
|
type APIKeysMethods struct {
|
|
apiKeys store.APIKeyStore
|
|
}
|
|
|
|
// NewAPIKeysMethods creates a new API keys method handler.
|
|
func NewAPIKeysMethods(apiKeys store.APIKeyStore) *APIKeysMethods {
|
|
return &APIKeysMethods{apiKeys: apiKeys}
|
|
}
|
|
|
|
// Register registers API key management RPC methods.
|
|
func (m *APIKeysMethods) Register(router *gateway.MethodRouter) {
|
|
router.Register(protocol.MethodAPIKeysList, m.handleList)
|
|
router.Register(protocol.MethodAPIKeysCreate, m.handleCreate)
|
|
router.Register(protocol.MethodAPIKeysRevoke, m.handleRevoke)
|
|
}
|
|
|
|
func (m *APIKeysMethods) handleList(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
locale := store.LocaleFromContext(ctx)
|
|
keys, err := m.apiKeys.List(ctx)
|
|
if err != nil {
|
|
slog.Error("api_keys.list failed", "error", err)
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToList, "API keys")))
|
|
return
|
|
}
|
|
if keys == nil {
|
|
keys = []store.APIKeyData{}
|
|
}
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, keys))
|
|
}
|
|
|
|
func (m *APIKeysMethods) handleCreate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
locale := store.LocaleFromContext(ctx)
|
|
|
|
var params struct {
|
|
Name string `json:"name"`
|
|
Scopes []string `json:"scopes"`
|
|
ExpiresIn *int `json:"expires_in"` // seconds; nil = never
|
|
}
|
|
if req.Params != nil {
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
|
return
|
|
}
|
|
}
|
|
|
|
if params.Name == "" {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "name")))
|
|
return
|
|
}
|
|
if len(params.Scopes) == 0 {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "scopes")))
|
|
return
|
|
}
|
|
|
|
// Validate scopes
|
|
for _, s := range params.Scopes {
|
|
if !permissions.ValidScope(s) {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidRequest, "invalid scope: "+s)))
|
|
return
|
|
}
|
|
}
|
|
|
|
raw, hash, prefix, err := crypto.GenerateAPIKey()
|
|
if err != nil {
|
|
slog.Error("api_keys.generate failed", "error", err)
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgInternalError, "key generation")))
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
key := &store.APIKeyData{
|
|
ID: store.GenNewID(),
|
|
Name: params.Name,
|
|
Prefix: prefix,
|
|
KeyHash: hash,
|
|
Scopes: params.Scopes,
|
|
CreatedBy: store.UserIDFromContext(ctx),
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
if params.ExpiresIn != nil && *params.ExpiresIn > 0 {
|
|
exp := now.Add(time.Duration(*params.ExpiresIn) * time.Second)
|
|
key.ExpiresAt = &exp
|
|
}
|
|
|
|
if err := m.apiKeys.Create(ctx, key); err != nil {
|
|
slog.Error("api_keys.create failed", "error", err)
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToCreate, "API key", "internal error")))
|
|
return
|
|
}
|
|
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
|
"id": key.ID,
|
|
"name": key.Name,
|
|
"prefix": key.Prefix,
|
|
"key": raw,
|
|
"scopes": key.Scopes,
|
|
"expires_at": key.ExpiresAt,
|
|
"created_at": key.CreatedAt,
|
|
}))
|
|
}
|
|
|
|
func (m *APIKeysMethods) handleRevoke(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
locale := store.LocaleFromContext(ctx)
|
|
|
|
var params struct {
|
|
ID string `json:"id"`
|
|
}
|
|
if req.Params != nil {
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
|
return
|
|
}
|
|
}
|
|
|
|
id, err := uuid.Parse(params.ID)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "API key")))
|
|
return
|
|
}
|
|
|
|
if err := m.apiKeys.Revoke(ctx, id); err != nil {
|
|
slog.Error("api_keys.revoke failed", "error", err, "id", params.ID)
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgNotFound, "API key", params.ID)))
|
|
return
|
|
}
|
|
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]string{"status": "revoked"}))
|
|
}
|