Skip to content

Commit 168fba3

Browse files
committed
enforce site-scoped effective TargetedInstanceCreation on instance, machine, and vpc actions
1 parent faa903a commit 168fba3

8 files changed

Lines changed: 187 additions & 13 deletions

File tree

rest-api/api/pkg/api/handler/instance.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,18 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error {
861861
return cutil.NewAPIError(http.StatusBadRequest, fmt.Sprintf("Machine specified in request does not belong to Site: %s", site.Name), nil)
862862
}
863863

864+
// Site-scoped gate: the targetedInstanceCreation capability must be
865+
// effective for this Machine's Site (ceiling + per-site opt-in).
866+
enabledForSite, derr := common.EffectiveTargetedInstanceCreation(ctx, tx, cih.dbSession, tenant, machine.SiteID)
867+
if derr != nil {
868+
logger.Error().Err(derr).Msg("error checking effective targeted instance creation for Machine's Site")
869+
return cutil.NewAPIError(http.StatusInternalServerError, "Failed to verify capability for Machine's Site", nil)
870+
}
871+
if !enabledForSite {
872+
logger.Warn().Msg("tenant does not have targeted instance creation enabled for the Machine's Site")
873+
return cutil.NewAPIError(http.StatusForbidden, "Tenant does not have capability to create Instances using specific Machine ID for this Site", nil)
874+
}
875+
864876
// Validate Machine availability. Note: allowUnhealthyMachine also bypasses
865877
// the Ready status check, not just health - consider renaming the parameter later.
866878
if apiRequest.AllowUnhealthyMachine != nil {
@@ -4741,16 +4753,37 @@ func (dih DeleteInstanceHandler) Handle(c echo.Context) error {
47414753
// requires the tenant to carry the TargetedInstanceCreation
47424754
// capability. By the time `ToProto` runs the request is safe
47434755
// to trust.
4756+
// Prepare the delete/release request workflow object
4757+
releaseInstanceRequest := apiRequest.ToProto(instance)
4758+
4759+
// This is for enhanced break-fix flow:
4760+
if apiRequest.MachineHealthIssue != nil {
4761+
releaseInstanceRequest.Issue = &cwssaws.Issue{
4762+
Category: cwssaws.IssueCategory(model.MachineIssueCategoriesFromAPIToProtobuf[apiRequest.MachineHealthIssue.Category]),
4763+
}
4764+
if apiRequest.MachineHealthIssue.Summary != nil {
4765+
releaseInstanceRequest.Issue.Summary = *apiRequest.MachineHealthIssue.Summary
4766+
}
4767+
if apiRequest.MachineHealthIssue.Details != nil {
4768+
releaseInstanceRequest.Issue.Details = *apiRequest.MachineHealthIssue.Details
4769+
}
4770+
}
4771+
4772+
// if caller attempt to set IsRepairTenant then the tenant must have the
4773+
// targetedInstanceCreation capability effective for the Instance's Site.
47444774
if apiRequest.IsRepairTenant != nil && *apiRequest.IsRepairTenant {
4745-
if !common.TenantHasTargetedInstanceCreation(instance.Tenant) {
4746-
logger.Warn().Msg("tenant does not have capability to set IsRepairTenant")
4775+
enabledForSite, derr := common.EffectiveTargetedInstanceCreation(ctx, tx, dih.dbSession, instance.Tenant, instance.SiteID)
4776+
if derr != nil {
4777+
logger.Error().Err(derr).Msg("error checking effective targeted instance creation for Instance's Site")
4778+
return cutil.NewAPIError(http.StatusInternalServerError, "Failed to verify capability for Instance's Site", nil)
4779+
}
4780+
if !enabledForSite {
4781+
logger.Warn().Msg("tenant does not have capability to set IsRepairTenant for the Instance's Site")
47474782
return cutil.NewAPIError(http.StatusForbidden, "Tenant does not have capability to set IsRepairTenant", nil)
47484783
}
47494784
}
47504785

4751-
// Prepare the delete/release request workflow object
4752-
releaseInstanceRequest := apiRequest.ToProto(instance)
4753-
4786+
// Trigger Site workflow to delete instance
47544787
workflowOptions := temporalClient.StartWorkflowOptions{
47554788
ID: "instance-delete-" + instance.ID.String(),
47564789
TaskQueue: queue.SiteTaskQueue,

rest-api/api/pkg/api/handler/instance_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ func testInstanceSetupSchema(t *testing.T, dbSession *cdb.Session) {
132132
// create NetworkSecurityGroup table
133133
err = dbSession.DB.ResetModel(context.Background(), (*cdbm.NetworkSecurityGroup)(nil))
134134
assert.Nil(t, err)
135+
// create TenantSiteCapabilityAssociation table
136+
err = dbSession.DB.ResetModel(context.Background(), (*cdbm.TenantSiteCapabilityAssociation)(nil))
137+
assert.Nil(t, err)
135138
}
136139

137140
func testInstanceSiteBuildInfrastructureProvider(t *testing.T, dbSession *cdb.Session, name string, org string, user *cdbm.User) *cdbm.InfrastructureProvider {
@@ -692,6 +695,11 @@ func TestCreateInstanceHandler_Handle(t *testing.T) {
692695
ts1 := testBuildTenantSiteAssociation(t, dbSession, tnOrg, tn1.ID, st1.ID, tnu1.ID)
693696
assert.NotNil(t, ts1)
694697

698+
// Enable the TargetedInstanceCreation capability for the privileged tenant on its sites
699+
// so the site-scoped effective check passes for targeted (MachineID) instance creation.
700+
cdbm.TestBuildTenantSiteCapabilityAssociation(t, dbSession, tn1, st1, true, tnu1)
701+
cdbm.TestBuildTenantSiteCapabilityAssociation(t, dbSession, tn1, st2, true, tnu1)
702+
695703
al1 := testInstanceSiteBuildAllocation(t, dbSession, st1, tn1, "test-allocation-1", ipu)
696704
assert.NotNil(t, al1)
697705

rest-api/api/pkg/api/handler/machine.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -757,8 +757,13 @@ func (umh UpdateMachineHandler) Handle(c echo.Context) error {
757757

758758
// Validate if Tenant is allowed to update Machine
759759
if tenant != nil {
760-
// Check if Tenant is privileged
761-
if common.TenantHasTargetedInstanceCreation(tenant) {
760+
// Check if the capability is effective for the Machine's Site (ceiling + per-site opt-in)
761+
enabledForSite, serr := common.EffectiveTargetedInstanceCreation(ctx, nil, umh.dbSession, tenant, machine.SiteID)
762+
if serr != nil {
763+
logger.Error().Err(serr).Msg("error checking effective targeted instance creation for Machine's Site")
764+
return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Error verifying capability for Machine's Site, DB error", nil)
765+
}
766+
if enabledForSite {
762767
// Check if privileged Tenant has an account with Infrastructure Provider
763768
taDAO := cdbm.NewTenantAccountDAO(umh.dbSession)
764769
_, taCount, serr := taDAO.GetAll(ctx, nil, cdbm.TenantAccountFilterInput{

rest-api/api/pkg/api/handler/machine_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,9 @@ func TestMachineHandler_Update(t *testing.T) {
16321632
tenant2 := testMachineBuildTenant(t, dbSession, tnOrg2, "testTenant2")
16331633
_ = testMachineUpdateTenantCapability(t, dbSession, tenant2)
16341634
_ = common.TestBuildTenantAccount(t, dbSession, ip, &tenant2.ID, tnOrg2, cdbm.TenantAccountStatusReady, tnu2)
1635+
// Enable the TargetedInstanceCreation capability for the privileged tenant on the Machine's Site
1636+
// so the site-scoped effective check passes during Machine update.
1637+
cdbm.TestBuildTenantSiteCapabilityAssociation(t, dbSession, tenant2, site, true, tnu2)
16351638

16361639
instanceType1 := testMachineBuildInstanceType(t, dbSession, ip, site, "testInstanceType1")
16371640
instanceType2 := testMachineBuildInstanceType(t, dbSession, ip, site, "testInstanceType2")

rest-api/api/pkg/api/handler/util/common/common.go

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,14 +1283,47 @@ func IsTenant(ctx context.Context, logger zerolog.Logger, dbSession *cdb.Session
12831283
// "ceiling"). It is nil-safe so callers don't have to repeat the
12841284
// tenant/Config nil checks.
12851285
//
1286-
// This is the single chokepoint for the tenant-level capability read. A
1287-
// later phase will introduce a site-scoped EffectiveTargetedInstanceCreation
1288-
// that composes this ceiling with per-site association rows and the provider
1289-
// gate; until then this preserves the existing tenant-global behavior.
1286+
// This is the single chokepoint for the tenant-level (ceiling) capability read.
1287+
// It still governs broad discovery/listing paths (e.g. expanding a privileged
1288+
// tenant's visible Sites). Site-scoped capability *actions* should instead use
1289+
// EffectiveTargetedInstanceCreation, which layers the per-site opt-in on top of
1290+
// this ceiling.
12901291
func TenantHasTargetedInstanceCreation(tenant *cdbm.Tenant) bool {
12911292
return tenant != nil && tenant.Config != nil && tenant.Config.TargetedInstanceCreation
12921293
}
12931294

1295+
// EffectiveTargetedInstanceCreation reports whether the TargetedInstanceCreation
1296+
// capability is effective for a Tenant on a specific Site. It composes the
1297+
// tenant-level ceiling (TenantHasTargetedInstanceCreation) with the per-site
1298+
// opt-in stored in TenantSiteCapabilityAssociation:
1299+
//
1300+
// effective = ceiling AND association_enabled(tenant, site)
1301+
//
1302+
// The provider gate (a Ready TenantAccount with the Site's Infrastructure
1303+
// Provider) is intentionally NOT enforced here. Call sites that require it
1304+
// already perform that check where the API mandates it, so keeping it out of
1305+
// this helper avoids double-enforcement and keeps the helper composable.
1306+
//
1307+
// The default when no association row exists is strict (false). The Phase-1
1308+
// conservative backfill materialized enabled rows for previously-implicit
1309+
// Sites, so existing privileged tenants are unaffected.
1310+
func EffectiveTargetedInstanceCreation(ctx context.Context, tx *cdb.Tx, dbSession *cdb.Session, tenant *cdbm.Tenant, siteID uuid.UUID) (bool, error) {
1311+
if !TenantHasTargetedInstanceCreation(tenant) {
1312+
return false, nil
1313+
}
1314+
1315+
tscaDAO := cdbm.NewTenantSiteCapabilityAssociationDAO(dbSession)
1316+
tsca, err := tscaDAO.GetByTenantIDAndSiteID(ctx, tx, tenant.ID, siteID, nil)
1317+
if err != nil {
1318+
if errors.Is(err, cdb.ErrDoesNotExist) {
1319+
return false, nil
1320+
}
1321+
return false, err
1322+
}
1323+
1324+
return tsca.TargetedInstanceCreation, nil
1325+
}
1326+
12941327
// IsProviderOrTenant ensures that user is authorized to act as a Provider Admin or/and Tenant Admin for the org.
12951328
// if authorized it returns the tenant otherwise a relevant error.
12961329
func IsProviderOrTenant(ctx context.Context, logger zerolog.Logger, dbSession *cdb.Session, org string, user *cdbm.User, allowViewerRole bool, requirePrivilegedTenant bool) (infrastructureProvider *cdbm.InfrastructureProvider, tenant *cdbm.Tenant, apiError *cutil.APIError) {

rest-api/api/pkg/api/handler/util/common/common_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2630,3 +2630,80 @@ func TestTenantHasTargetedInstanceCreation(t *testing.T) {
26302630
})
26312631
}
26322632
}
2633+
2634+
func TestEffectiveTargetedInstanceCreation(t *testing.T) {
2635+
ctx := context.Background()
2636+
dbSession := testCommonInitDB(t)
2637+
defer dbSession.Close()
2638+
2639+
testCommonSetupSchema(t, dbSession)
2640+
err := dbSession.DB.ResetModel(ctx, (*cdbm.TenantSiteCapabilityAssociation)(nil))
2641+
assert.Nil(t, err)
2642+
2643+
org := "test-eff-org"
2644+
user := testCommonBuildUser(t, dbSession, uuid.NewString(), []string{org}, []string{authz.TenantAdminRole})
2645+
ip := testCommonBuildInfrastructureProvider(t, dbSession, "Test Provider", org, user)
2646+
site := testCommonBuildSite(t, dbSession, ip, "Test Site", user)
2647+
2648+
// Tenant with the ceiling enabled.
2649+
tnDAO := cdbm.NewTenantDAO(dbSession)
2650+
tenantWithCeiling, err := tnDAO.CreateFromParams(ctx, nil, "ceiling-tenant", cdb.GetStrPtr("Ceiling Tenant"), org, nil,
2651+
&cdbm.TenantConfig{TargetedInstanceCreation: true}, user)
2652+
assert.Nil(t, err)
2653+
// Tenant without the ceiling.
2654+
tenantNoCeiling := testCommonBuildTenant(t, dbSession, "no-ceiling-tenant", org+"-2", user)
2655+
2656+
// Association row for the ceiling tenant on the site, enabled.
2657+
cdbm.TestBuildTenantSiteCapabilityAssociation(t, dbSession, tenantWithCeiling, site, true, user)
2658+
// A disabled association on a second site.
2659+
site2 := testCommonBuildSite(t, dbSession, ip, "Test Site 2", user)
2660+
cdbm.TestBuildTenantSiteCapabilityAssociation(t, dbSession, tenantWithCeiling, site2, false, user)
2661+
// A site with no association row at all.
2662+
site3 := testCommonBuildSite(t, dbSession, ip, "Test Site 3", user)
2663+
2664+
tests := []struct {
2665+
name string
2666+
tenant *cdbm.Tenant
2667+
siteID uuid.UUID
2668+
expected bool
2669+
}{
2670+
{
2671+
name: "nil tenant",
2672+
tenant: nil,
2673+
siteID: site.ID,
2674+
expected: false,
2675+
},
2676+
{
2677+
name: "ceiling disabled",
2678+
tenant: tenantNoCeiling,
2679+
siteID: site.ID,
2680+
expected: false,
2681+
},
2682+
{
2683+
name: "ceiling enabled and association enabled",
2684+
tenant: tenantWithCeiling,
2685+
siteID: site.ID,
2686+
expected: true,
2687+
},
2688+
{
2689+
name: "ceiling enabled but association disabled",
2690+
tenant: tenantWithCeiling,
2691+
siteID: site2.ID,
2692+
expected: false,
2693+
},
2694+
{
2695+
name: "ceiling enabled but no association row",
2696+
tenant: tenantWithCeiling,
2697+
siteID: site3.ID,
2698+
expected: false,
2699+
},
2700+
}
2701+
2702+
for _, tc := range tests {
2703+
t.Run(tc.name, func(t *testing.T) {
2704+
got, gerr := EffectiveTargetedInstanceCreation(ctx, nil, dbSession, tc.tenant, tc.siteID)
2705+
assert.Nil(t, gerr)
2706+
assert.Equal(t, tc.expected, got)
2707+
})
2708+
}
2709+
}

rest-api/api/pkg/api/handler/vpc.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,14 @@ func (cvh CreateVPCHandler) Handle(c echo.Context) error {
238238
var routingProfile *string
239239
if apiRequest.RoutingProfile != nil {
240240
// For now, we gate on TargetedInstanceCreation permission,
241-
// Which implies a "privileged tenant"
242-
if !common.TenantHasTargetedInstanceCreation(tenant) {
241+
// Which implies a "privileged tenant". The capability must be effective
242+
// for the VPC's Site (ceiling + per-site opt-in).
243+
enabledForSite, derr := common.EffectiveTargetedInstanceCreation(ctx, nil, cvh.dbSession, tenant, site.ID)
244+
if derr != nil {
245+
logger.Error().Err(derr).Msg("error checking effective targeted instance creation for Site")
246+
return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to verify privileges for Site", nil)
247+
}
248+
if !enabledForSite {
243249
logger.Warn().Msg("tenant does not have sufficient privileges to set `routingProfile`")
244250
return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "Tenant does not have sufficient privileges to set `routingProfile`", nil)
245251
}

rest-api/api/pkg/api/handler/vpc_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ func testVPCSetupSchema(t *testing.T, dbSession *cdb.Session) {
9191
// create VPC table
9292
err = dbSession.DB.ResetModel(context.Background(), (*cdbm.Vpc)(nil))
9393
assert.Nil(t, err)
94+
// create TenantSiteCapabilityAssociation table
95+
err = dbSession.DB.ResetModel(context.Background(), (*cdbm.TenantSiteCapabilityAssociation)(nil))
96+
assert.Nil(t, err)
9497
}
9598

9699
func testVPCSiteBuildInfrastructureProvider(t *testing.T, dbSession *cdb.Session, name string, org string, user *cdbm.User) *cdbm.InfrastructureProvider {
@@ -359,6 +362,12 @@ func TestCreateVPCHandler_Handle(t *testing.T) {
359362
ts1t3 := testBuildTenantSiteAssociation(t, dbSession, tnOrg3, tn3.ID, st1.ID, tnu3.ID)
360363
assert.NotNil(t, ts1t3)
361364

365+
// Enable the TargetedInstanceCreation capability for the privileged tenant on its sites
366+
// so the site-scoped effective check passes for routingProfile.
367+
cdbm.TestBuildTenantSiteCapabilityAssociation(t, dbSession, tn, st1, true, tnu)
368+
cdbm.TestBuildTenantSiteCapabilityAssociation(t, dbSession, tn, st2, true, tnu)
369+
cdbm.TestBuildTenantSiteCapabilityAssociation(t, dbSession, tn, st3, true, tnu)
370+
362371
// NSG for tenant 1 on site 1
363372
nsgTenant1Site1 := testBuildNetworkSecurityGroup(t, dbSession, "test-nsg-1", tn, st1, cdbm.NetworkSecurityGroupStatusReady)
364373
assert.NotNil(t, nsgTenant1Site1)

0 commit comments

Comments
 (0)