From 1a3d7f9a8a181df48f0e911cbe17a3fad3ab9ac9 Mon Sep 17 00:00:00 2001 From: Chia Date: Wed, 5 Aug 2026 00:26:25 +1200 Subject: add some scripts --- internal/controlplane/access.go | 156 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 internal/controlplane/access.go (limited to 'internal/controlplane/access.go') diff --git a/internal/controlplane/access.go b/internal/controlplane/access.go new file mode 100644 index 0000000..ec2d177 --- /dev/null +++ b/internal/controlplane/access.go @@ -0,0 +1,156 @@ +package controlplane + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "net/mail" + "strings" + + "github.com/jackc/pgx/v5" +) + +var ErrConsoleUnauthorized = errors.New("invalid console token") + +const ( + RolePlatformAdmin = "platform_admin" + RolePlatformViewer = "platform_viewer" + RoleTenantAdmin = "tenant_admin" + RoleTenantBilling = "tenant_billing" + RoleTenantDeveloper = "tenant_developer" + RoleTenantViewer = "tenant_viewer" +) + +func (a ConsoleActor) IsPlatform() bool { return strings.HasPrefix(a.Role, "platform_") } + +func (a ConsoleActor) Can(permission string) bool { + if a.Role == RolePlatformAdmin { + return true + } + read := strings.HasSuffix(permission, ".read") + switch a.Role { + case RolePlatformViewer: + return read + case RoleTenantAdmin: + switch permission { + case "overview.read", "tenants.read", "projects.read", "projects.write", "keys.read", "keys.write", + "billing.read", "billing.topup", "usage.read", "audit.read", "limits.read", "users.read", "users.write": + return true + } + return false + case RoleTenantBilling: + return permission == "overview.read" || permission == "billing.read" || permission == "billing.topup" || permission == "usage.read" || permission == "audit.read" + case RoleTenantDeveloper: + return permission == "overview.read" || permission == "tenants.read" || permission == "projects.read" || permission == "keys.read" || permission == "keys.write" || permission == "usage.read" || permission == "limits.read" + case RoleTenantViewer: + return permission == "overview.read" || permission == "tenants.read" || permission == "projects.read" || permission == "keys.read" || permission == "billing.read" || permission == "usage.read" || permission == "limits.read" || permission == "audit.read" + default: + return false + } +} + +func (a ConsoleActor) Permissions() []string { + all := []string{"overview.read", "tenants.read", "tenants.write", "projects.read", "projects.write", "keys.read", "keys.write", "platform.read", "platform.write", "billing.read", "billing.topup", "billing.adjust", "usage.read", "limits.read", "limits.write", "users.read", "users.write", "audit.read"} + result := make([]string, 0, len(all)) + for _, permission := range all { + if a.Can(permission) { + result = append(result, permission) + } + } + return result +} + +func (s *Store) AuthenticateConsoleToken(ctx context.Context, raw string) (ConsoleActor, error) { + hash := sha256.Sum256([]byte(raw)) + var actor ConsoleActor + err := s.db.QueryRow(ctx, ` + UPDATE console_users SET last_used_at = now() + WHERE token_hash = $1 AND status = 'active' + RETURNING id::text, COALESCE(tenant_id::text, ''), email, display_name, role`, hash[:], + ).Scan(&actor.ID, &actor.TenantID, &actor.Email, &actor.DisplayName, &actor.Role) + if errors.Is(err, pgx.ErrNoRows) { + return ConsoleActor{}, ErrConsoleUnauthorized + } + if err != nil { + return ConsoleActor{}, fmt.Errorf("authenticate console token: %w", err) + } + return actor, nil +} + +func (s *Store) ListConsoleUsers(ctx context.Context, tenantID string) ([]ConsoleUser, error) { + query := `SELECT id::text, COALESCE(tenant_id::text, ''), email, display_name, role, token_prefix, status, last_used_at, created_at FROM console_users` + args := []any{} + if tenantID != "" { + query += ` WHERE tenant_id = $1` + args = append(args, tenantID) + } + query += ` ORDER BY created_at DESC` + rows, err := s.db.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("query console users: %w", err) + } + defer rows.Close() + result := make([]ConsoleUser, 0) + for rows.Next() { + var item ConsoleUser + if err := rows.Scan(&item.ID, &item.TenantID, &item.Email, &item.DisplayName, &item.Role, &item.TokenPrefix, &item.Status, &item.LastUsedAt, &item.CreatedAt); err != nil { + return nil, fmt.Errorf("scan console user: %w", err) + } + result = append(result, item) + } + return result, rows.Err() +} + +func (s *Store) CreateConsoleUser(ctx context.Context, input CreateConsoleUserInput) (CreatedConsoleUser, error) { + input.Email = strings.ToLower(strings.TrimSpace(input.Email)) + input.DisplayName = strings.TrimSpace(input.DisplayName) + input.TenantID = strings.TrimSpace(input.TenantID) + address, addressErr := mail.ParseAddress(input.Email) + if addressErr != nil || address.Address != input.Email || input.DisplayName == "" { + return CreatedConsoleUser{}, errors.New("console user requires a valid email and display_name") + } + platform := input.Role == RolePlatformAdmin || input.Role == RolePlatformViewer + tenant := input.Role == RoleTenantAdmin || input.Role == RoleTenantBilling || input.Role == RoleTenantDeveloper || input.Role == RoleTenantViewer + if (!platform && !tenant) || (platform && input.TenantID != "") || (tenant && input.TenantID == "") { + return CreatedConsoleUser{}, errors.New("console user role and tenant_id are inconsistent") + } + random := make([]byte, 32) + if _, err := rand.Read(random); err != nil { + return CreatedConsoleUser{}, fmt.Errorf("generate console token: %w", err) + } + raw := "cu-aigw-" + base64.RawURLEncoding.EncodeToString(random) + hash := sha256.Sum256([]byte(raw)) + prefix := raw[:min(18, len(raw))] + "..." + var result CreatedConsoleUser + err := s.db.QueryRow(ctx, ` + INSERT INTO console_users (tenant_id, email, display_name, role, token_prefix, token_hash) + VALUES (NULLIF($1,'')::uuid,$2,$3,$4,$5,$6) + RETURNING id::text, COALESCE(tenant_id::text, ''), email, display_name, role, token_prefix, status, last_used_at, created_at`, + input.TenantID, input.Email, input.DisplayName, input.Role, prefix, hash[:], + ).Scan(&result.ID, &result.TenantID, &result.Email, &result.DisplayName, &result.Role, &result.TokenPrefix, &result.Status, &result.LastUsedAt, &result.CreatedAt) + if err != nil { + return CreatedConsoleUser{}, fmt.Errorf("create console user: %w", err) + } + result.Token = raw + return result, nil +} + +func (s *Store) RevokeConsoleUser(ctx context.Context, id, tenantID string) error { + query := `UPDATE console_users SET status='revoked', revoked_at=now() WHERE id=$1 AND status='active'` + args := []any{id} + if tenantID != "" { + query += ` AND tenant_id=$2` + args = append(args, tenantID) + } + result, err := s.db.Exec(ctx, query, args...) + if err != nil { + return err + } + if result.RowsAffected() == 0 { + return ErrNotFound + } + return nil +} -- cgit v1.2.3