package middleware import ( "context" "errors" "fmt" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/geo-platform/tenant-api/internal/shared/auth" "github.com/geo-platform/tenant-api/internal/shared/response" ) const WorkspaceIDHeader = "X-Workspace-ID" func WorkspaceScope(pool *pgxpool.Pool) gin.HandlerFunc { return func(c *gin.Context) { actor, ok := auth.ActorFromCtx(c.Request.Context()) if !ok || actor.TenantID == 0 || actor.UserID == 0 || actor.PrimaryWorkspaceID == 0 { response.Error(c, response.ErrUnauthorized(40105, "workspace_scope_missing", "workspace context is required")) c.Abort() return } workspaceID, err := requestedWorkspaceID(c.GetHeader(WorkspaceIDHeader), actor.PrimaryWorkspaceID) if err != nil { response.Error(c, response.ErrBadRequest(40032, "invalid_workspace_id", "workspace id must be a positive number")) c.Abort() return } if workspaceID != actor.PrimaryWorkspaceID { if pool == nil { response.Error(c, response.ErrInternal(50012, "workspace_scope_unavailable", "workspace scope validation is unavailable")) c.Abort() return } if err := validateWorkspaceMembership(c.Request.Context(), pool, actor.TenantID, workspaceID, actor.UserID); err != nil { if errors.Is(err, pgx.ErrNoRows) { response.Error(c, response.ErrForbidden(40312, "workspace_forbidden", "workspace is not available for current user")) } else { response.Error(c, response.ErrInternal(50012, "workspace_scope_lookup_failed", "failed to validate workspace scope")) } c.Abort() return } } c.Request = c.Request.WithContext(auth.WithCurrentWorkspaceID(c.Request.Context(), workspaceID)) c.Next() } } func requestedWorkspaceID(raw string, fallback int64) (int64, error) { trimmed := strings.TrimSpace(raw) if trimmed == "" { return fallback, nil } workspaceID, err := strconv.ParseInt(trimmed, 10, 64) if err != nil { return 0, err } if workspaceID <= 0 { return 0, fmt.Errorf("workspace id must be positive") } return workspaceID, nil } func validateWorkspaceMembership(ctx context.Context, pool *pgxpool.Pool, tenantID, workspaceID, userID int64) error { var exists int return pool.QueryRow(ctx, ` SELECT 1 FROM workspace_memberships WHERE tenant_id = $1 AND workspace_id = $2 AND user_id = $3 LIMIT 1 `, tenantID, workspaceID, userID).Scan(&exists) }