rdev/internal/handlers/me.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

100 lines
2.8 KiB
Go

package handlers
import (
"context"
"net/http"
"time"
"github.com/orchard9/rdev/internal/auth"
"github.com/orchard9/rdev/internal/domain"
"github.com/orchard9/rdev/internal/service"
"github.com/orchard9/rdev/pkg/api"
)
// MeHandler handles the /me endpoint.
type MeHandler struct {
authService *auth.Service
projectService *service.ProjectService
}
// NewMeHandler creates a new me handler.
func NewMeHandler(authService *auth.Service, projectService *service.ProjectService) *MeHandler {
return &MeHandler{
authService: authService,
projectService: projectService,
}
}
// Mount registers the /me route.
func (h *MeHandler) Mount(r api.Router) {
r.Get("/me", h.Get)
}
// MeResponse is the JSON response for GET /me.
type MeResponse struct {
ID string `json:"id"`
Name string `json:"name"`
KeyPrefix string `json:"key_prefix"`
Scopes []string `json:"scopes"`
ProjectAccess string `json:"project_access"` // "unrestricted" | "restricted"
Projects []ProjectSummary `json:"projects,omitempty"`
AllowedIPs []string `json:"allowed_ips,omitempty"`
CreatedAt string `json:"created_at"`
ExpiresAt *string `json:"expires_at,omitempty"`
Active bool `json:"active"`
}
// ProjectSummary is a lightweight project view for embedding in /me.
type ProjectSummary struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
}
// Get returns the current key's identity, scopes, and project access.
// GET /me
func (h *MeHandler) Get(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), TimeoutFastLookup)
defer cancel()
apiKey := auth.GetAPIKey(ctx)
if apiKey == nil {
api.WriteUnauthorized(w, r, "Not authenticated")
return
}
resp := MeResponse{
ID: string(apiKey.ID),
Name: apiKey.Name,
KeyPrefix: apiKey.KeyPrefix,
Scopes: auth.ScopesToStrings(apiKey.Scopes),
ProjectAccess: "unrestricted",
AllowedIPs: apiKey.AllowedIPs,
CreatedAt: apiKey.CreatedAt.Format(time.RFC3339),
Active: apiKey.IsActive(),
}
// Populate projects list when key is restricted (non-admin with explicit project_ids)
if apiKey.ProjectIDs != nil && !apiKey.HasScope(domain.ScopeAdmin) {
resp.ProjectAccess = "restricted"
if h.projectService != nil {
projects, _ := h.projectService.List(ctx, apiKey.ProjectIDs)
resp.Projects = make([]ProjectSummary, len(projects))
for i, p := range projects {
resp.Projects[i] = ProjectSummary{
ID: string(p.ID),
Name: p.Name,
Status: string(p.Status),
}
}
}
}
if apiKey.ExpiresAt != nil {
s := apiKey.ExpiresAt.Format(time.RFC3339)
resp.ExpiresAt = &s
}
api.WriteSuccess(w, r, resp)
}