All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- 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>
127 lines
3.6 KiB
Go
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)
|
|
}
|