rdev/internal/auth/service.go
jordan 4f01015132
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
feat: implement project access enforcement and management API
- Fix no-op RequireProjectAccess middleware to enforce project_ids
- Apply project access middleware to all project-scoped routes
- Filter GET /projects by allowed project IDs for restricted keys
- Add GET /me endpoint with key identity, scopes, and project access info
- Add PATCH /keys/{id} for partial key updates (name, scopes, project_ids, allowed_ips, expires_in)
- Add GET/POST/DELETE /projects/{id}/access for project-centric access management
- Auto-grant creating key access when using POST /project/create-and-build
- Accept grant_to_key_ids in create-and-build to grant multiple keys on project creation
- Move newProvisionerWithDeps test helper from production code to test file

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 15:38:37 -07:00

127 lines
3.6 KiB
Go

package auth
import (
"context"
"fmt"
"time"
"github.com/orchard9/rdev/internal/domain"
"github.com/orchard9/rdev/internal/port"
"github.com/orchard9/rdev/internal/service"
)
// APIKey is an alias for domain.APIKey.
// All API key behavior (IsExpired, IsRevoked, etc.) lives in domain/apikey.go.
type APIKey = domain.APIKey
// Error sentinels — delegate to domain errors.
// Consumers should migrate to domain.ErrXxx over time.
var (
ErrKeyNotFound = domain.ErrKeyNotFound
ErrKeyRevoked = domain.ErrKeyRevoked
ErrKeyExpired = domain.ErrKeyExpired
ErrIPNotAllowed = domain.ErrIPNotAllowed
)
// CreateKeyRequest is the input for creating a new key.
type CreateKeyRequest struct {
Name string
Scopes []Scope
ProjectIDs []string // nil = all projects
AllowedIPs []string // CIDR notation; nil = no restriction
ExpiresIn time.Duration // 0 = never
CreatedBy string
}
// CreateKeyResponse is the output of creating a new key.
type CreateKeyResponse struct {
Key *APIKey
Secret string // Full key, shown only once
}
// Service handles API key operations.
// It wraps service.APIKeyService to provide the same interface as before
// while delegating to the hexagonal service layer.
type Service struct {
svc *service.APIKeyService
adminKey string
}
// NewService creates a new auth service.
// Accepts a service.APIKeyService (hexagonal) instead of raw *sql.DB.
func NewService(svc *service.APIKeyService, adminKey string) *Service {
return &Service{
svc: svc,
adminKey: adminKey,
}
}
// IsAdminKey checks if the provided key is the super admin key.
func (s *Service) IsAdminKey(key string) bool {
return s.adminKey != "" && key == s.adminKey
}
// Create generates a new API key.
func (s *Service) Create(ctx context.Context, req CreateKeyRequest) (*CreateKeyResponse, error) {
// Validate scopes
if !ValidateScopes(req.Scopes) {
return nil, fmt.Errorf("invalid scopes")
}
// Convert []string ProjectIDs to []domain.ProjectID
var projectIDs []domain.ProjectID
if req.ProjectIDs != nil {
projectIDs = make([]domain.ProjectID, len(req.ProjectIDs))
for i, p := range req.ProjectIDs {
projectIDs[i] = domain.ProjectID(p)
}
}
result, err := s.svc.Create(ctx, service.CreateKeyRequest{
Name: req.Name,
Scopes: req.Scopes,
ProjectIDs: projectIDs,
AllowedIPs: req.AllowedIPs,
ExpiresIn: req.ExpiresIn,
CreatedBy: req.CreatedBy,
})
if err != nil {
return nil, err
}
return &CreateKeyResponse{
Key: result.Key,
Secret: result.Secret,
}, nil
}
// Validate checks if a key is valid and returns the key details.
func (s *Service) Validate(ctx context.Context, key string) (*APIKey, error) {
return s.svc.Validate(ctx, key)
}
// List returns all API keys (without secrets).
func (s *Service) List(ctx context.Context) ([]*APIKey, error) {
return s.svc.List(ctx)
}
// Get returns a single API key by ID.
func (s *Service) Get(ctx context.Context, id string) (*APIKey, error) {
return s.svc.Get(ctx, domain.APIKeyID(id))
}
// Revoke marks an API key as revoked.
func (s *Service) Revoke(ctx context.Context, id string) error {
return s.svc.Revoke(ctx, domain.APIKeyID(id))
}
// Update applies a partial update to an API key.
func (s *Service) Update(ctx context.Context, id string, update port.APIKeyUpdate) error {
return s.svc.Update(ctx, domain.APIKeyID(id), update)
}
// ListByProjectID returns all active keys that have the given project ID in their project_ids.
func (s *Service) ListByProjectID(ctx context.Context, projectID domain.ProjectID) ([]*APIKey, error) {
return s.svc.ListByProjectID(ctx, projectID)
}