Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c5a2692
feat(azure/compute): CalculateExchange and DoExchange client operations
cristim Jul 27, 2026
dffe476
feat(api): Azure RI exchange compatible-offerings and execute endpoints
cristim Jul 27, 2026
8ac5b09
test(api): Azure exchange handler coverage
cristim Jul 27, 2026
c2fc81f
fix(api): reject currency-blind MaxPurchaseAmount cap on Azure execute
cristim Jul 27, 2026
8f583c2
fix(api): drop dead action parameter from requirePermissionConstraints
cristim Jul 27, 2026
99326fc
fix(azure/compute): refuse exchange results with a failed terminal st…
cristim Jul 27, 2026
45aac2e
fix(api): derive azure exchange billing scope from the authorized sub…
cristim Jul 27, 2026
7a5053e
test(api): split azure exchange tests out and fix a misplaced doc com…
cristim Jul 27, 2026
da5be12
fix(api): check Azure exchange subscription scope before building client
cristim Jul 27, 2026
c0c0858
fix(azure/compute): never drop a message-less exchange policy error
cristim Jul 27, 2026
ecbcfad
test(api): pin the azure exchange authz set and commit-path failures
cristim Jul 27, 2026
72c0126
docs(api): document the azure exchange sources/targets item bounds
cristim Jul 27, 2026
2010218
test(api): guard the offerings endpoint against the enumeration oracle
cristim Jul 27, 2026
6e86a96
docs(api): give the azure exchange sources/targets real schemas
cristim Jul 27, 2026
319bfb2
docs(azure/compute): correct the documented reservation term set
cristim Jul 27, 2026
e7b124a
fix(api): scope azure exchange sources to the authorized subscription
cristim Jul 27, 2026
1d01d6d
style(api): fix lint on the source-ownership gate
cristim Jul 27, 2026
55f98da
fix(auth/api): require every requested region to be permitted
cristim Jul 28, 2026
879bc1b
fix(api/azure): bound RI exchange sources by the Regions constraint
cristim Jul 28, 2026
849679b
fix(api/azure): check exchange source ownership before the constraints
cristim Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions internal/api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,11 @@ type Handler struct {
targetOfferingsEC2Factory func(aws.Config) targetOfferingsEC2Client

// Optional Azure exchange client factory injected by tests. When nil
// (the production default), buildAzureExchangeClient uses
// azidentity.NewDefaultAzureCredential to construct a real
// armreservations-backed client.
// (the production default), buildAzureExchangeClient resolves the
// registered CloudAccount's per-subscription credentials and constructs
// a real armreservations-backed client. The azureExchangeClient
// interface covers listing exchangeable reservations plus pricing
// (CalculateExchange) and committing (ExecuteExchange) an exchange.
azureExchangeFactory func(subscriptionID string) azureExchangeClient

// Optional marketplace EC2 client factory injected by tests. When nil
Expand Down Expand Up @@ -460,6 +462,15 @@ func (h *Handler) authorizeAPIKeyAny(ctx context.Context, apiKey string, verbs [
// AccountIDs constraint still matches via the empty-permission-side rule.
const unattributedAccountConstraint = "unattributed"

// requirePermissionConstraintsAction is the action every current caller of
// requirePermissionConstraints checks (execute:purchases, execute:ri-exchange
// for both AWS and Azure). Every constraint-gated operation today is an
// irreversible execute; hardcoded rather than threaded as a parameter since
// a parameter with only one real value across all call sites is dead
// flexibility (a genuinely new action should add a real parameter back,
// not resurrect an unused one).
const requirePermissionConstraintsAction = "execute"

// requirePermissionConstraints re-checks an already-authenticated session
// against request-derived permission constraint sets, so the Constraints
// (MaxPurchaseAmount, Providers, Services, Regions, AccountIDs) configured on
Expand All @@ -477,7 +488,7 @@ const unattributedAccountConstraint = "unattributed"
// prevents a CI key with MaxPurchaseAmount=$100 from spending up to the
// owning user's full group limit by inheriting the broader group permissions
// (adversarial-review F2).
func (h *Handler) requirePermissionConstraints(ctx context.Context, session *Session, action, resource string, constraintSets []auth.PermissionConstraints) error {
func (h *Handler) requirePermissionConstraints(ctx context.Context, session *Session, resource string, constraintSets []auth.PermissionConstraints) error {
if session == nil {
return fmt.Errorf("internal error: nil session passed to requirePermissionConstraints")
}
Expand All @@ -490,21 +501,21 @@ func (h *Handler) requirePermissionConstraints(ctx context.Context, session *Ses
// User API key: evaluate constraints against the key's effective permissions,
// not the owning user's full group permissions.
if session.UserAPIKeyID != "" {
has, err := h.auth.HasAPIKeyPermissionForConstraintsAPI(ctx, session.UserAPIKeyID, session.UserID, action, resource, constraintSets)
has, err := h.auth.HasAPIKeyPermissionForConstraintsAPI(ctx, session.UserAPIKeyID, session.UserID, requirePermissionConstraintsAction, resource, constraintSets)
if err != nil {
return fmt.Errorf("permission constraint check failed: %w", err)
}
if !has {
return NewClientError(403, fmt.Sprintf("permission denied: this request exceeds the constraints configured on your %s permission for %s", action, resource))
return NewClientError(403, fmt.Sprintf("permission denied: this request exceeds the constraints configured on your %s permission for %s", requirePermissionConstraintsAction, resource))
}
return nil
}
has, err := h.auth.HasPermissionForConstraintsAPI(ctx, session.UserID, action, resource, constraintSets)
has, err := h.auth.HasPermissionForConstraintsAPI(ctx, session.UserID, requirePermissionConstraintsAction, resource, constraintSets)
if err != nil {
return fmt.Errorf("permission constraint check failed: %w", err)
}
if !has {
return NewClientError(403, fmt.Sprintf("permission denied: this request exceeds the constraints configured on your %s permission for %s", action, resource))
return NewClientError(403, fmt.Sprintf("permission denied: this request exceeds the constraints configured on your %s permission for %s", requirePermissionConstraintsAction, resource))
}
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion internal/api/handler_purchases.go
Original file line number Diff line number Diff line change
Expand Up @@ -2098,7 +2098,7 @@ func (h *Handler) enforcePurchaseConstraints(ctx context.Context, session *Sessi
if err := requireNonZeroCommitment(constraintSets); err != nil {
return err
}
return h.requirePermissionConstraints(ctx, session, "execute", "purchases", constraintSets)
return h.requirePermissionConstraints(ctx, session, "purchases", constraintSets)
}

// purchaseConstraintSets builds one auth.PermissionConstraints per
Expand Down
Loading
Loading