Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Add UndeployAll() using label selectors to clean up monorepo components on project deletion (replaces name-based Undeploy in DeleteProject and the direct undeploy handler) - Add ResourceGC background worker that periodically finds K8s resources whose project label has no matching DB record, deletes after 1h safety window - Widen deployer client type from *kubernetes.Clientset to kubernetes.Interface for testability - UndeployAll accumulates errors via errors.Join instead of failing fast - Add checkout/checkin sidecar dev flow: temporary git tokens, branch checkout, review on checkin with cleanup workers - Add interactive sessions: pod binding, command execution, SSE streaming, ephemeral preview URLs with session cleanup workers - Add GET /workers/pool endpoint for aggregate capacity and queue depth - Add sessions:read and sessions:execute auth scopes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
240 lines
6.3 KiB
Go
240 lines
6.3 KiB
Go
package domain
|
|
|
|
import (
|
|
"net"
|
|
"time"
|
|
)
|
|
|
|
// APIKeyID is a strongly-typed identifier for API keys.
|
|
type APIKeyID string
|
|
|
|
// Scope represents a permission scope for API keys.
|
|
type Scope string
|
|
|
|
const (
|
|
ScopeProjectsRead Scope = "projects:read"
|
|
ScopeProjectsExecute Scope = "projects:execute"
|
|
ScopeKeysRead Scope = "keys:read"
|
|
ScopeKeysWrite Scope = "keys:write"
|
|
ScopeAuditRead Scope = "audit:read"
|
|
ScopeQueueRead Scope = "queue:read"
|
|
ScopeQueueWrite Scope = "queue:write"
|
|
ScopeWebhookRead Scope = "webhook:read"
|
|
ScopeWebhookWrite Scope = "webhook:write"
|
|
ScopeWorkersRead Scope = "workers:read"
|
|
ScopeWorkersWrite Scope = "workers:write"
|
|
ScopeBuildRead Scope = "build:read"
|
|
ScopeBuildWrite Scope = "build:write"
|
|
ScopeVerifyRead Scope = "verify:read"
|
|
ScopeVerifyWrite Scope = "verify:write"
|
|
ScopeSessionsRead Scope = "sessions:read"
|
|
ScopeSessionsExecute Scope = "sessions:execute"
|
|
ScopeAdmin Scope = "admin"
|
|
)
|
|
|
|
// AllScopes is the list of all valid scopes.
|
|
var AllScopes = []Scope{
|
|
ScopeProjectsRead,
|
|
ScopeProjectsExecute,
|
|
ScopeKeysRead,
|
|
ScopeKeysWrite,
|
|
ScopeAuditRead,
|
|
ScopeQueueRead,
|
|
ScopeQueueWrite,
|
|
ScopeWebhookRead,
|
|
ScopeWebhookWrite,
|
|
ScopeWorkersRead,
|
|
ScopeWorkersWrite,
|
|
ScopeBuildRead,
|
|
ScopeBuildWrite,
|
|
ScopeVerifyRead,
|
|
ScopeVerifyWrite,
|
|
ScopeSessionsRead,
|
|
ScopeSessionsExecute,
|
|
ScopeAdmin,
|
|
}
|
|
|
|
// ScopeDescriptions provides human-readable descriptions.
|
|
var ScopeDescriptions = map[Scope]string{
|
|
ScopeProjectsRead: "List and view project details",
|
|
ScopeProjectsExecute: "Execute commands (claude, shell, git) on projects",
|
|
ScopeKeysRead: "List API keys (metadata only, not secrets)",
|
|
ScopeKeysWrite: "Create and revoke API keys",
|
|
ScopeAuditRead: "View audit logs for command executions",
|
|
ScopeQueueRead: "View queued commands and queue status",
|
|
ScopeQueueWrite: "Enqueue and cancel queued commands",
|
|
ScopeWebhookRead: "View webhooks and delivery history",
|
|
ScopeWebhookWrite: "Create, update, and delete webhooks",
|
|
ScopeWorkersRead: "View workers and worker status",
|
|
ScopeWorkersWrite: "Manage workers (drain, register)",
|
|
ScopeBuildRead: "View build status and history",
|
|
ScopeBuildWrite: "Start and manage builds",
|
|
ScopeVerifyRead: "View verify tasks and capture results",
|
|
ScopeVerifyWrite: "Submit and cancel verify tasks",
|
|
ScopeSessionsRead: "View interactive development sessions",
|
|
ScopeSessionsExecute: "Create and end interactive development sessions",
|
|
ScopeAdmin: "Full administrative access (includes all scopes)",
|
|
}
|
|
|
|
// IsValid checks if a scope is valid.
|
|
func (s Scope) IsValid() bool {
|
|
for _, scope := range AllScopes {
|
|
if scope == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// String returns the scope as a string.
|
|
func (s Scope) String() string {
|
|
return string(s)
|
|
}
|
|
|
|
// ScopesFromStrings converts string slice to Scope slice.
|
|
func ScopesFromStrings(ss []string) []Scope {
|
|
scopes := make([]Scope, len(ss))
|
|
for i, s := range ss {
|
|
scopes[i] = Scope(s)
|
|
}
|
|
return scopes
|
|
}
|
|
|
|
// ScopesToStrings converts Scope slice to string slice.
|
|
func ScopesToStrings(scopes []Scope) []string {
|
|
ss := make([]string, len(scopes))
|
|
for i, s := range scopes {
|
|
ss[i] = string(s)
|
|
}
|
|
return ss
|
|
}
|
|
|
|
// ValidateScopes checks if all scopes are valid.
|
|
func ValidateScopes(scopes []Scope) bool {
|
|
for _, s := range scopes {
|
|
if !s.IsValid() {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// HasScope checks if a scope list contains a required scope.
|
|
// Admin scope grants access to everything.
|
|
func HasScope(scopes []Scope, required Scope) bool {
|
|
for _, s := range scopes {
|
|
if s == ScopeAdmin || s == required {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// HasAnyScope checks if a scope list contains any of the required scopes.
|
|
func HasAnyScope(scopes []Scope, required ...Scope) bool {
|
|
for _, r := range required {
|
|
if HasScope(scopes, r) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// APIKey represents an API key for authentication.
|
|
type APIKey struct {
|
|
ID APIKeyID
|
|
Name string
|
|
KeyPrefix string // First 8 chars of key for identification
|
|
Scopes []Scope
|
|
ProjectIDs []ProjectID // nil = access to all projects
|
|
AllowedIPs []string // CIDR notation, e.g., ["192.168.1.0/24", "10.0.0.0/8"]; nil = no restriction
|
|
CreatedAt time.Time
|
|
ExpiresAt *time.Time
|
|
LastUsedAt *time.Time
|
|
RevokedAt *time.Time
|
|
CreatedBy string
|
|
}
|
|
|
|
// IsExpired returns true if the key has expired.
|
|
func (k *APIKey) IsExpired() bool {
|
|
if k.ExpiresAt == nil {
|
|
return false
|
|
}
|
|
return time.Now().After(*k.ExpiresAt)
|
|
}
|
|
|
|
// IsRevoked returns true if the key has been revoked.
|
|
func (k *APIKey) IsRevoked() bool {
|
|
return k.RevokedAt != nil
|
|
}
|
|
|
|
// IsActive returns true if the key is valid for use.
|
|
func (k *APIKey) IsActive() bool {
|
|
return !k.IsRevoked() && !k.IsExpired()
|
|
}
|
|
|
|
// HasScope returns true if the key has the specified scope.
|
|
func (k *APIKey) HasScope(scope Scope) bool {
|
|
// Admin scope grants all permissions
|
|
for _, s := range k.Scopes {
|
|
if s == ScopeAdmin || s == scope {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// HasAnyScope returns true if the key has any of the specified scopes.
|
|
func (k *APIKey) HasAnyScope(scopes ...Scope) bool {
|
|
for _, scope := range scopes {
|
|
if k.HasScope(scope) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// HasProjectAccess returns true if the key can access the given project.
|
|
func (k *APIKey) HasProjectAccess(projectID ProjectID) bool {
|
|
// Admin or nil project list means access to all projects
|
|
if k.HasScope(ScopeAdmin) || k.ProjectIDs == nil {
|
|
return true
|
|
}
|
|
for _, pid := range k.ProjectIDs {
|
|
if pid == projectID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsIPAllowed checks if the given IP address is allowed by the key's IP restrictions.
|
|
// Returns true if no IP restrictions are set or if the IP matches any allowed CIDR.
|
|
func (k *APIKey) IsIPAllowed(clientIP string) bool {
|
|
// No restrictions means all IPs are allowed
|
|
if len(k.AllowedIPs) == 0 {
|
|
return true
|
|
}
|
|
|
|
ip := net.ParseIP(clientIP)
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
|
|
for _, cidr := range k.AllowedIPs {
|
|
_, network, err := net.ParseCIDR(cidr)
|
|
if err != nil {
|
|
// If not a CIDR, try parsing as single IP
|
|
allowedIP := net.ParseIP(cidr)
|
|
if allowedIP != nil && allowedIP.Equal(ip) {
|
|
return true
|
|
}
|
|
continue
|
|
}
|
|
if network.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|