summaryrefslogtreecommitdiff
path: root/internal/controlplane/access.go
diff options
context:
space:
mode:
authorChia <Chia@93.nz>2026-08-05 00:26:25 +1200
committerChia <Chia@93.nz>2026-08-05 00:33:31 +1200
commit1a3d7f9a8a181df48f0e911cbe17a3fad3ab9ac9 (patch)
tree8c92e1e7326fc67ed077a0a878697f1be14b43da /internal/controlplane/access.go
parent5b651488b081b65fda8a323f228e139adb79a35d (diff)
add some scriptsmain
Diffstat (limited to 'internal/controlplane/access.go')
-rw-r--r--internal/controlplane/access.go156
1 files changed, 156 insertions, 0 deletions
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
+}