mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 12:22:44 +00:00
274 lines
8.5 KiB
Go
274 lines
8.5 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
type fakeSecureCLIGrantStore struct {
|
|
binaries map[uuid.UUID]bool
|
|
agents map[uuid.UUID]bool
|
|
grants map[uuid.UUID]*store.SecureCLIAgentGrant
|
|
|
|
createCalled bool
|
|
updateCalled bool
|
|
deleteCalled bool
|
|
}
|
|
|
|
func (s *fakeSecureCLIGrantStore) BinaryExists(_ context.Context, id uuid.UUID) (bool, error) {
|
|
return s.binaries[id], nil
|
|
}
|
|
|
|
func (s *fakeSecureCLIGrantStore) AgentExists(_ context.Context, id uuid.UUID) (bool, error) {
|
|
return s.agents[id], nil
|
|
}
|
|
|
|
func (s *fakeSecureCLIGrantStore) Create(_ context.Context, g *store.SecureCLIAgentGrant) error {
|
|
s.createCalled = true
|
|
if g.ID == uuid.Nil {
|
|
g.ID = store.GenNewID()
|
|
}
|
|
s.grants[g.ID] = g
|
|
return nil
|
|
}
|
|
|
|
func (s *fakeSecureCLIGrantStore) Get(_ context.Context, id uuid.UUID) (*store.SecureCLIAgentGrant, error) {
|
|
if g := s.grants[id]; g != nil {
|
|
cp := *g
|
|
return &cp, nil
|
|
}
|
|
return nil, sql.ErrNoRows
|
|
}
|
|
|
|
func (s *fakeSecureCLIGrantStore) Update(context.Context, uuid.UUID, map[string]any) error {
|
|
s.updateCalled = true
|
|
return nil
|
|
}
|
|
|
|
func (s *fakeSecureCLIGrantStore) Delete(context.Context, uuid.UUID) error {
|
|
s.deleteCalled = true
|
|
return nil
|
|
}
|
|
|
|
func (s *fakeSecureCLIGrantStore) ListByBinary(_ context.Context, binaryID uuid.UUID) ([]store.SecureCLIAgentGrant, error) {
|
|
grants := make([]store.SecureCLIAgentGrant, 0, len(s.grants))
|
|
for _, grant := range s.grants {
|
|
if grant == nil || grant.BinaryID != binaryID {
|
|
continue
|
|
}
|
|
cp := *grant
|
|
grants = append(grants, cp)
|
|
}
|
|
return grants, nil
|
|
}
|
|
|
|
func (s *fakeSecureCLIGrantStore) ListByAgent(context.Context, uuid.UUID) ([]store.SecureCLIAgentGrant, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *fakeSecureCLIGrantStore) UpdateGrantEnv(context.Context, uuid.UUID, []byte) error {
|
|
s.updateCalled = true
|
|
return nil
|
|
}
|
|
|
|
func requestWithGrantPath(method string, body io.Reader, binaryID, grantID uuid.UUID) (*httptest.ResponseRecorder, *http.Request) {
|
|
req := httptest.NewRequest(method, "/v1/cli-credentials/"+binaryID.String()+"/agent-grants/"+grantID.String(), body)
|
|
req.SetPathValue("id", binaryID.String())
|
|
req.SetPathValue("grantId", grantID.String())
|
|
ctx := store.WithTenantID(req.Context(), uuid.MustParse("0193a5b0-7000-7000-8000-000000000002"))
|
|
ctx = store.WithRole(ctx, store.RoleOwner)
|
|
ctx = store.WithUserID(ctx, "admin@example.com")
|
|
return httptest.NewRecorder(), req.WithContext(ctx)
|
|
}
|
|
|
|
func requestWithBinaryPath(body io.Reader, binaryID uuid.UUID) (*httptest.ResponseRecorder, *http.Request) {
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/cli-credentials/"+binaryID.String()+"/agent-grants", body)
|
|
req.SetPathValue("id", binaryID.String())
|
|
ctx := store.WithTenantID(req.Context(), uuid.MustParse("0193a5b0-7000-7000-8000-000000000002"))
|
|
ctx = store.WithRole(ctx, store.RoleOwner)
|
|
ctx = store.WithUserID(ctx, "admin@example.com")
|
|
return httptest.NewRecorder(), req.WithContext(ctx)
|
|
}
|
|
|
|
func TestSecureCLIGrantNestedRoutesRejectWrongBinaryParent(t *testing.T) {
|
|
realBinaryID := uuid.New()
|
|
pathBinaryID := uuid.New()
|
|
grantID := uuid.New()
|
|
fake := &fakeSecureCLIGrantStore{
|
|
grants: map[uuid.UUID]*store.SecureCLIAgentGrant{
|
|
grantID: {
|
|
BaseModel: store.BaseModel{ID: grantID},
|
|
BinaryID: realBinaryID,
|
|
AgentID: uuid.New(),
|
|
Enabled: true,
|
|
EncryptedEnv: []byte(`{"TOKEN":"value"}`),
|
|
},
|
|
},
|
|
}
|
|
h := NewSecureCLIGrantHandler(fake, nil, nil)
|
|
|
|
tests := []struct {
|
|
name string
|
|
method string
|
|
body string
|
|
call func(http.ResponseWriter, *http.Request)
|
|
}{
|
|
{name: "get", method: http.MethodGet, call: h.handleGet},
|
|
{name: "update", method: http.MethodPut, body: `{"enabled":false}`, call: h.handleUpdate},
|
|
{name: "delete", method: http.MethodDelete, call: h.handleDelete},
|
|
{name: "reveal", method: http.MethodPost, call: h.handleRevealEnv},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
fake.updateCalled = false
|
|
fake.deleteCalled = false
|
|
rr, req := requestWithGrantPath(tt.method, strings.NewReader(tt.body), pathBinaryID, grantID)
|
|
tt.call(rr, req)
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404 for wrong binary parent, got %d body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
if fake.updateCalled {
|
|
t.Fatal("wrong-parent request must not update grant or env")
|
|
}
|
|
if fake.deleteCalled {
|
|
t.Fatal("wrong-parent request must not delete grant")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSecureCLIGrantCreateValidatesBinaryAndAgentScope(t *testing.T) {
|
|
binaryID := uuid.New()
|
|
agentID := uuid.New()
|
|
|
|
tests := []struct {
|
|
name string
|
|
binaryOK bool
|
|
agentOK bool
|
|
wantStatus int
|
|
wantCreate bool
|
|
}{
|
|
{name: "missing binary", binaryOK: false, agentOK: true, wantStatus: http.StatusNotFound},
|
|
{name: "missing agent", binaryOK: true, agentOK: false, wantStatus: http.StatusNotFound},
|
|
{name: "valid scope", binaryOK: true, agentOK: true, wantStatus: http.StatusCreated, wantCreate: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
fake := &fakeSecureCLIGrantStore{
|
|
binaries: map[uuid.UUID]bool{binaryID: tt.binaryOK},
|
|
agents: map[uuid.UUID]bool{agentID: tt.agentOK},
|
|
grants: map[uuid.UUID]*store.SecureCLIAgentGrant{},
|
|
}
|
|
h := NewSecureCLIGrantHandler(fake, nil, nil)
|
|
rr, req := requestWithBinaryPath(strings.NewReader(`{"agent_id":"`+agentID.String()+`","enabled":true}`), binaryID)
|
|
|
|
h.handleCreate(rr, req)
|
|
|
|
if rr.Code != tt.wantStatus {
|
|
t.Fatalf("expected status %d, got %d body=%s", tt.wantStatus, rr.Code, rr.Body.String())
|
|
}
|
|
if fake.createCalled != tt.wantCreate {
|
|
t.Fatalf("createCalled=%v, want %v", fake.createCalled, tt.wantCreate)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateAndSerializeEnvVarsRejectsGoClawGatewayToken(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
envJSON, ok := validateAndSerializeEnvVars(rr, "en", json.RawMessage(`{
|
|
"GOCLAW_GATEWAY_TOKEN": {"kind":"sensitive","value":"test-secret-token"}
|
|
}`))
|
|
|
|
if ok || envJSON != nil {
|
|
t.Fatalf("expected GOCLAW_GATEWAY_TOKEN to be rejected by public env validator")
|
|
}
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
if !strings.Contains(rr.Body.String(), "GOCLAW_GATEWAY_TOKEN") {
|
|
t.Fatalf("expected rejected key in response, got %s", rr.Body.String())
|
|
}
|
|
if strings.Contains(rr.Body.String(), "test-secret-token") {
|
|
t.Fatalf("secret value leaked in validation error: %s", rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSecureCLIGrantUpdateRejectsInvalidEnvVarsBeforeScalarUpdate(t *testing.T) {
|
|
binaryID := uuid.New()
|
|
grantID := uuid.New()
|
|
fake := &fakeSecureCLIGrantStore{
|
|
grants: map[uuid.UUID]*store.SecureCLIAgentGrant{
|
|
grantID: {
|
|
BaseModel: store.BaseModel{ID: grantID},
|
|
BinaryID: binaryID,
|
|
AgentID: uuid.New(),
|
|
Enabled: true,
|
|
},
|
|
},
|
|
}
|
|
h := NewSecureCLIGrantHandler(fake, nil, nil)
|
|
rr, req := requestWithGrantPath(http.MethodPut, strings.NewReader(`{"enabled":false,"env_vars":123}`), binaryID, grantID)
|
|
|
|
h.handleUpdate(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
if fake.updateCalled {
|
|
t.Fatal("invalid env_vars request must not persist scalar grant updates")
|
|
}
|
|
}
|
|
|
|
func TestSecureCLIGrantGetSanitizesMixedEnv(t *testing.T) {
|
|
binaryID := uuid.New()
|
|
grantID := uuid.New()
|
|
fake := &fakeSecureCLIGrantStore{
|
|
grants: map[uuid.UUID]*store.SecureCLIAgentGrant{
|
|
grantID: {
|
|
BaseModel: store.BaseModel{ID: grantID},
|
|
BinaryID: binaryID,
|
|
AgentID: uuid.New(),
|
|
Enabled: true,
|
|
EncryptedEnv: []byte(`{"TOKEN":"secret-token","PUBLIC_BASE_URL":{"kind":"value","value":"https://goclaw.sh"}}`),
|
|
},
|
|
},
|
|
}
|
|
h := NewSecureCLIGrantHandler(fake, nil, nil)
|
|
rr, req := requestWithGrantPath(http.MethodGet, nil, binaryID, grantID)
|
|
|
|
h.handleGet(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
if strings.Contains(rr.Body.String(), "secret-token") {
|
|
t.Fatalf("sensitive grant env leaked in response: %s", rr.Body.String())
|
|
}
|
|
var got struct {
|
|
Env map[string]store.SecureCLIEnvResponseEntry `json:"env"`
|
|
}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !got.Env["TOKEN"].Masked || got.Env["TOKEN"].Value != nil {
|
|
t.Fatalf("TOKEN not masked: %#v", got.Env["TOKEN"])
|
|
}
|
|
if got.Env["PUBLIC_BASE_URL"].Value == nil || *got.Env["PUBLIC_BASE_URL"].Value != "https://goclaw.sh" {
|
|
t.Fatalf("PUBLIC_BASE_URL not returned: %#v", got.Env["PUBLIC_BASE_URL"])
|
|
}
|
|
}
|