Claude Config API (v0.6): - Add CRUD endpoints for commands, skills, and agents - Commands/skills/agents stored in /workspace/.claude/ (per-project, in git) - Credentials shared via PVC at /root/.claude/ (shared across pods) - Use base64 encoding for file writes (prevents shell injection) - Add content size limits (1MB max) Security Hardening: - Add sanitize package for command/prompt validation - Add rate limiting middleware (token bucket algorithm) - Add concurrent command limiting - Add input sanitization to all command handlers - Gitignore secrets.yaml and credentials.yaml - Add *.example templates for secrets Testing Infrastructure: - Add testutil package with mocks and fixtures - Add unit tests for auth package (63% coverage) - Add unit tests for executor (47% coverage) - Add handler integration tests (40% coverage) - Add 100% coverage for sanitize, cmdlimit packages - Add 96% coverage for ratelimit package Infrastructure: - Shared Claude credentials PVC (ReadWriteMany) - Reduced workspace PVC size from 20Gi to 5Gi - Add init container cleanup before git clone - Document Longhorn RWX requirements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
156 lines
4.1 KiB
Go
156 lines
4.1 KiB
Go
package auth
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestScopeIsValid(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
scope Scope
|
|
want bool
|
|
}{
|
|
{"projects:read", ScopeProjectsRead, true},
|
|
{"projects:execute", ScopeProjectsExecute, true},
|
|
{"keys:read", ScopeKeysRead, true},
|
|
{"keys:write", ScopeKeysWrite, true},
|
|
{"admin", ScopeAdmin, true},
|
|
{"invalid", Scope("invalid"), false},
|
|
{"empty", Scope(""), false},
|
|
{"similar", Scope("projects:write"), false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := tt.scope.IsValid(); got != tt.want {
|
|
t.Errorf("Scope(%q).IsValid() = %v, want %v", tt.scope, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScopesFromStrings(t *testing.T) {
|
|
input := []string{"projects:read", "keys:write"}
|
|
got := ScopesFromStrings(input)
|
|
|
|
if len(got) != 2 {
|
|
t.Fatalf("ScopesFromStrings() returned %d scopes, want 2", len(got))
|
|
}
|
|
|
|
if got[0] != ScopeProjectsRead {
|
|
t.Errorf("got[0] = %v, want %v", got[0], ScopeProjectsRead)
|
|
}
|
|
|
|
if got[1] != ScopeKeysWrite {
|
|
t.Errorf("got[1] = %v, want %v", got[1], ScopeKeysWrite)
|
|
}
|
|
}
|
|
|
|
func TestScopesToStrings(t *testing.T) {
|
|
input := []Scope{ScopeProjectsRead, ScopeKeysWrite}
|
|
got := ScopesToStrings(input)
|
|
|
|
if len(got) != 2 {
|
|
t.Fatalf("ScopesToStrings() returned %d strings, want 2", len(got))
|
|
}
|
|
|
|
if got[0] != "projects:read" {
|
|
t.Errorf("got[0] = %q, want %q", got[0], "projects:read")
|
|
}
|
|
|
|
if got[1] != "keys:write" {
|
|
t.Errorf("got[1] = %q, want %q", got[1], "keys:write")
|
|
}
|
|
}
|
|
|
|
func TestValidateScopes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
scopes []Scope
|
|
want bool
|
|
}{
|
|
{"all valid", []Scope{ScopeProjectsRead, ScopeKeysWrite}, true},
|
|
{"single valid", []Scope{ScopeAdmin}, true},
|
|
{"empty", []Scope{}, true},
|
|
{"one invalid", []Scope{ScopeProjectsRead, Scope("invalid")}, false},
|
|
{"all invalid", []Scope{Scope("foo"), Scope("bar")}, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := ValidateScopes(tt.scopes); got != tt.want {
|
|
t.Errorf("ValidateScopes() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHasScope(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
scopes []Scope
|
|
required Scope
|
|
want bool
|
|
}{
|
|
{"has exact scope", []Scope{ScopeProjectsRead, ScopeKeysRead}, ScopeProjectsRead, true},
|
|
{"admin grants all", []Scope{ScopeAdmin}, ScopeProjectsRead, true},
|
|
{"admin grants keys", []Scope{ScopeAdmin}, ScopeKeysWrite, true},
|
|
{"missing scope", []Scope{ScopeProjectsRead}, ScopeKeysWrite, false},
|
|
{"empty scopes", []Scope{}, ScopeProjectsRead, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := HasScope(tt.scopes, tt.required); got != tt.want {
|
|
t.Errorf("HasScope() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHasAnyScope(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
scopes []Scope
|
|
required []Scope
|
|
want bool
|
|
}{
|
|
{"has first", []Scope{ScopeProjectsRead}, []Scope{ScopeProjectsRead, ScopeKeysRead}, true},
|
|
{"has second", []Scope{ScopeKeysRead}, []Scope{ScopeProjectsRead, ScopeKeysRead}, true},
|
|
{"has neither", []Scope{ScopeKeysWrite}, []Scope{ScopeProjectsRead, ScopeKeysRead}, false},
|
|
{"admin grants any", []Scope{ScopeAdmin}, []Scope{ScopeProjectsRead, ScopeKeysRead}, true},
|
|
{"empty required", []Scope{ScopeProjectsRead}, []Scope{}, false},
|
|
{"empty scopes", []Scope{}, []Scope{ScopeProjectsRead}, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := HasAnyScope(tt.scopes, tt.required...); got != tt.want {
|
|
t.Errorf("HasAnyScope() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHasProjectAccess(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
allowed []string
|
|
project string
|
|
want bool
|
|
}{
|
|
{"nil allows all", nil, "any-project", true},
|
|
{"in list", []string{"proj-a", "proj-b"}, "proj-a", true},
|
|
{"not in list", []string{"proj-a", "proj-b"}, "proj-c", false},
|
|
{"empty list denies", []string{}, "proj-a", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := HasProjectAccess(tt.allowed, tt.project); got != tt.want {
|
|
t.Errorf("HasProjectAccess() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|