Skip to content
9 changes: 1 addition & 8 deletions rest-api/api/pkg/api/handler/dpureprovision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,12 @@ func TestReprovisionMachineDpuHandlerAllowsPrivilegedTenant(t *testing.T) {
tenantOrg := "test-tenant-org"
tenantUser := common.TestBuildUser(t, fixture.DBSession, "test-tenant-starfleet-id", tenantOrg, []string{authz.TenantAdminRole})
tenant := common.TestBuildTenant(t, fixture.DBSession, "test-tenant", tenantOrg, tenantUser)
tenant, err := cdbm.NewTenantDAO(fixture.DBSession).Update(ctx, nil, cdbm.TenantUpdateInput{
TenantID: tenant.ID,
Config: &cdbm.TenantConfig{
TargetedInstanceCreation: true,
},
})
require.NoError(t, err)

machine, err := cdbm.NewMachineDAO(fixture.DBSession).GetByID(ctx, nil, fixture.MachineID, []string{}, false)
require.NoError(t, err)
provider, err := cdbm.NewInfrastructureProviderDAO(fixture.DBSession).GetByID(ctx, nil, machine.InfrastructureProviderID, []string{})
require.NoError(t, err)
common.TestBuildTenantAccount(t, fixture.DBSession, provider, &tenant.ID, tenant.Org, cdbm.TenantAccountStatusReady, tenantUser)
common.TestBuildTenantAccountWithTargetedInstanceCreation(t, fixture.DBSession, provider, &tenant.ID, tenant.Org, cdbm.TenantAccountStatusReady, tenantUser)

fixture.Org = tenantOrg
fixture.User = tenantUser
Expand Down
53 changes: 24 additions & 29 deletions rest-api/api/pkg/api/handler/expectedmachine.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,15 @@ func ValidateProviderOrTenantSiteAccess(ctx context.Context, logger zerolog.Logg

hasAccess = tsCount > 0

// Check if Tenant is privileged
if !hasAccess && tenant.Config.TargetedInstanceCreation {
// Check if privileged tenant has an account with the Site's Infrastructure Provider
taDAO := cdbm.NewTenantAccountDAO(dbSession)
_, taCount, err := taDAO.GetAll(ctx, nil, cdbm.TenantAccountFilterInput{
InfrastructureProviderID: &site.InfrastructureProviderID,
TenantIDs: []uuid.UUID{tenant.ID},
}, paginator.PageInput{}, []string{})
// Privileged tenants may access Sites via a Ready TenantAccount with
// TargetedInstanceCreation even without an explicit TenantSite row.
if !hasAccess {
enabled, err := common.TenantHasTargetedInstanceCreation(ctx, nil, dbSession, tenant, site)
if err != nil {
logger.Error().Err(err).Msg("error retrieving Tenant Account for Site")
return false, cutil.NewAPIError(http.StatusInternalServerError, "Failed to retrieve Tenant's Account with Site's Provider due to DB error", nil)
logger.Error().Err(err).Msg("error resolving TargetedInstanceCreation for Tenant/Site")
return false, cutil.NewAPIError(http.StatusInternalServerError, "Failed to resolve Tenant capability for Site due to DB error", nil)
}

hasAccess = taCount > 0
hasAccess = enabled
}
}

Expand Down Expand Up @@ -317,13 +312,19 @@ func (gaemh GetAllExpectedMachineHandler) Handle(c echo.Context) error {
return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve current user", nil)
}

// ensure our user is a provider for the org
infrastructureProvider, tenant, apiError := common.IsProviderOrTenant(ctx, logger, gaemh.dbSession, org, dbUser, true, true)
// ensure our user is a provider or tenant for the org. We do not request the
// privileged-tenant pre-gate (requirePrivilegedTenant=false): a Tenant's
// visibility is scoped below to the Sites it has TenantSite access to (and
// only when TargetedInstanceCreation is enabled), so a non-privileged Tenant
// receives an empty list rather than a spurious 403.
infrastructureProvider, tenant, apiError := common.IsProviderOrTenant(ctx, logger, gaemh.dbSession, org, dbUser, true, false)
if apiError != nil {
return cutil.NewAPIErrorResponse(c, apiError.Code, apiError.Message, apiError.Data)
}

filterInput := cdbm.ExpectedMachineFilterInput{}
// Initialize SiteIDs to a non-nil empty slice so an unscoped caller (e.g. a
// non-privileged Tenant) matches no Sites instead of every Site.
filterInput := cdbm.ExpectedMachineFilterInput{SiteIDs: []uuid.UUID{}}

if infrastructureProvider != nil {
// Get all Sites for the org's Infrastructure Provider
Expand All @@ -346,20 +347,14 @@ func (gaemh GetAllExpectedMachineHandler) Handle(c echo.Context) error {
}

if tenant != nil {
// Check if Tenant is privileged
if tenant.Config.TargetedInstanceCreation {
// Get IDs for all Sites the privileged Tenant has an access with
tenantSiteDAO := cdbm.NewTenantSiteDAO(gaemh.dbSession)
tenantSites, _, err := tenantSiteDAO.GetAll(ctx, nil, cdbm.TenantSiteFilterInput{TenantIDs: []uuid.UUID{tenant.ID}}, paginator.PageInput{Limit: cutil.GetPtr(math.MaxInt)}, nil)
if err != nil {
logger.Error().Err(err).Msg("error retrieving Tenant Sites from DB")
return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenant Sites due to DB error", nil)
}

for _, tenantSite := range tenantSites {
filterInput.SiteIDs = append(filterInput.SiteIDs, tenantSite.SiteID)
}
// Get the Sites where the Tenant has effective TargetedInstanceCreation,
// honoring per-site TenantSite.config overrides.
privilegedSiteIDs, err := common.GetPrivilegedAccessSiteIDsForTenant(ctx, nil, gaemh.dbSession, tenant)
if err != nil {
logger.Error().Err(err).Msg("error resolving privileged Site access for Tenant")
return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to resolve Tenant capability due to DB error", nil)
}
filterInput.SiteIDs = append(filterInput.SiteIDs, privilegedSiteIDs...)
}

siteIDStr := c.QueryParam("siteId")
Expand All @@ -383,7 +378,7 @@ func (gaemh GetAllExpectedMachineHandler) Handle(c echo.Context) error {
}

if !isAssociated && tenant != nil {
// We've already populated the filter with Providers the Tenant has an account with
// filterInput.SiteIDs already holds the Tenant's effective privileged Sites.
isAssociated = slices.Contains(filterInput.SiteIDs, site.ID)
}

Expand Down
57 changes: 42 additions & 15 deletions rest-api/api/pkg/api/handler/expectedmachine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,10 +527,7 @@ func TestGetAllExpectedMachineHandler_Handle(t *testing.T) {
Name: "privileged-tenant",
Org: privilegedTenantOrg,
OrgDisplayName: cutil.GetPtr("Privileged Tenant Org"),
Config: &cdbm.TenantConfig{
TargetedInstanceCreation: true,
},
CreatedBy: privilegedTenantUserID,
CreatedBy: privilegedTenantUserID,
}
_, err = dbSession.DB.NewInsert().Model(privilegedTenant).Exec(ctx)
assert.Nil(t, err)
Expand All @@ -546,6 +543,18 @@ func TestGetAllExpectedMachineHandler_Handle(t *testing.T) {
assert.Nil(t, err)

privilegedTenantUser := createMockUser(privilegedTenantOrg, authz.TenantAdminRole)
_, err = dbSession.DB.NewInsert().Model(&cdbm.TenantAccount{
ID: uuid.New(),
AccountNumber: common.GenerateAccountNumber(),
TenantID: &privilegedTenant.ID,
TenantOrg: privilegedTenantOrg,
InfrastructureProviderID: infraProv.ID,
InfrastructureProviderOrg: org,
Status: cdbm.TenantAccountStatusReady,
Config: cdbm.TenantAccountConfig{TargetedInstanceCreation: true},
CreatedBy: privilegedTenantUserID,
}).Exec(ctx)
assert.Nil(t, err)

// Dual-role org: same org acts as both Infrastructure Provider and privileged Tenant
dualRoleOrg := "dual-role-org"
Expand All @@ -563,10 +572,7 @@ func TestGetAllExpectedMachineHandler_Handle(t *testing.T) {
Name: "dual-role-tenant",
Org: dualRoleOrg,
OrgDisplayName: cutil.GetPtr("Dual Role Org"),
Config: &cdbm.TenantConfig{
TargetedInstanceCreation: true,
},
CreatedBy: dualRoleUserID,
CreatedBy: dualRoleUserID,
}
_, err = dbSession.DB.NewInsert().Model(dualRoleTenant).Exec(ctx)
assert.Nil(t, err)
Expand Down Expand Up @@ -612,6 +618,31 @@ func TestGetAllExpectedMachineHandler_Handle(t *testing.T) {
_, err = dbSession.DB.NewInsert().Model(dualRoleExternalTenantSite).Exec(ctx)
assert.Nil(t, err)

_, err = dbSession.DB.NewInsert().Model(&cdbm.TenantAccount{
ID: uuid.New(),
AccountNumber: common.GenerateAccountNumber(),
TenantID: &dualRoleTenant.ID,
TenantOrg: dualRoleOrg,
InfrastructureProviderID: dualRoleIP.ID,
InfrastructureProviderOrg: dualRoleOrg,
Status: cdbm.TenantAccountStatusReady,
Config: cdbm.TenantAccountConfig{TargetedInstanceCreation: true},
CreatedBy: dualRoleUserID,
}).Exec(ctx)
assert.Nil(t, err)
_, err = dbSession.DB.NewInsert().Model(&cdbm.TenantAccount{
ID: uuid.New(),
AccountNumber: common.GenerateAccountNumber(),
TenantID: &dualRoleTenant.ID,
TenantOrg: dualRoleOrg,
InfrastructureProviderID: unmanagedIP.ID,
InfrastructureProviderOrg: unmanagedIP.Org,
Status: cdbm.TenantAccountStatusReady,
Config: cdbm.TenantAccountConfig{TargetedInstanceCreation: true},
CreatedBy: dualRoleUserID,
}).Exec(ctx)
assert.Nil(t, err)

dualRoleEM, err := emDAO.Create(ctx, nil, cdbm.ExpectedMachineCreateInput{
ExpectedMachineID: uuid.New(),
SiteID: dualRoleSite.ID,
Expand Down Expand Up @@ -1541,9 +1572,6 @@ func TestTenantWithTargetedInstanceCreationCapability(t *testing.T) {
Name: "test-tenant",
Org: tenantOrg,
OrgDisplayName: cutil.GetPtr("Test Tenant"),
Config: &cdbm.TenantConfig{
TargetedInstanceCreation: true,
},
}
_, err = dbSession.DB.NewInsert().Model(tenant).Exec(ctx)
assert.Nil(t, err)
Expand All @@ -1565,14 +1593,16 @@ func TestTenantWithTargetedInstanceCreationCapability(t *testing.T) {
_, err = dbSession.DB.NewInsert().Model(tenantUser).Exec(ctx)
assert.Nil(t, err)

// Create TenantAccount linking tenant to infrastructure provider
// Create TenantAccount linking tenant to infrastructure provider. Privilege
// (TargetedInstanceCreation) is resolved from the Ready TenantAccount config.
tenantAccount := &cdbm.TenantAccount{
ID: uuid.New(),
AccountNumber: "TA-12345",
TenantID: &tenant.ID,
TenantOrg: tenantOrg,
InfrastructureProviderID: ip.ID,
Status: cdbm.TenantAccountStatusReady,
Config: cdbm.TenantAccountConfig{TargetedInstanceCreation: true},
CreatedBy: tenantUser.ID,
}
_, err = dbSession.DB.NewInsert().Model(tenantAccount).Exec(ctx)
Expand All @@ -1595,9 +1625,6 @@ func TestTenantWithTargetedInstanceCreationCapability(t *testing.T) {
Name: "test-tenant-no-cap",
Org: tenantOrg2,
OrgDisplayName: cutil.GetPtr("Test Tenant No Cap"),
Config: &cdbm.TenantConfig{
TargetedInstanceCreation: false, // No capability
},
}
_, err = dbSession.DB.NewInsert().Model(tenant2).Exec(ctx)
assert.Nil(t, err)
Expand Down
19 changes: 16 additions & 3 deletions rest-api/api/pkg/api/handler/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,12 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error {

// Begin validating Machine ID
if apiRequest.MachineID != nil {
if tenant.Config == nil || !tenant.Config.TargetedInstanceCreation {
privilegedAccess, derr := common.TenantHasTargetedInstanceCreation(ctx, tx, cih.dbSession, tenant, site)
if derr != nil {
logger.Error().Err(derr).Msg("error checking effective targeted instance creation for Site")
return cutil.NewAPIError(http.StatusInternalServerError, "Failed to verify capability for Site", nil)
}
if !privilegedAccess {
logger.Warn().Msg("tenant does not have capability to create instances from specific machine")
return cutil.NewAPIError(http.StatusForbidden, "Tenant does not have capability to create Instances using specific Machine ID", nil)
}
Expand Down Expand Up @@ -4900,8 +4905,16 @@ func (dih DeleteInstanceHandler) Handle(c echo.Context) error {
// capability. By the time `ToProto` runs the request is safe
// to trust.
if apiRequest.IsRepairTenant != nil && *apiRequest.IsRepairTenant {
if instance.Tenant.Config == nil || !instance.Tenant.Config.TargetedInstanceCreation {
logger.Warn().Msg("tenant does not have capability to set IsRepairTenant")
enabledForSite, derr := common.TenantHasTargetedInstanceCreation(ctx, tx, dih.dbSession, instance.Tenant, &cdbm.Site{
ID: instance.SiteID,
InfrastructureProviderID: instance.InfrastructureProviderID,
})
if derr != nil {
logger.Error().Err(derr).Msg("error checking effective targeted instance creation for Instance's Site")
return cutil.NewAPIError(http.StatusInternalServerError, "Failed to verify capability for Instance's Site", nil)
}
if !enabledForSite {
logger.Warn().Msg("tenant does not have capability to set IsRepairTenant for the Instance's Site")
return cutil.NewAPIError(http.StatusForbidden, "Tenant does not have capability to set IsRepairTenant", nil)
}
}
Expand Down
19 changes: 3 additions & 16 deletions rest-api/api/pkg/api/handler/instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,21 +196,6 @@ func testInstanceBuildTenant(t *testing.T, dbSession *cdb.Session, name string,

return tn
}
func testInstanceUpdateTenantCapability(t *testing.T, dbSession *cdb.Session, tn *cdbm.Tenant) *cdbm.Tenant {
tncfg := cdbm.TenantConfig{
TargetedInstanceCreation: true,
}

tnDAO := cdbm.NewTenantDAO(dbSession)
tn, err := tnDAO.Update(context.Background(), nil, cdbm.TenantUpdateInput{
TenantID: tn.ID,
Config: &tncfg,
})
assert.Nil(t, err)

return tn
}

func testInstanceBuildUser(t *testing.T, dbSession *cdb.Session, starfleetID string, org string, roles []string) *cdbm.User {
uDAO := cdbm.NewUserDAO(dbSession)

Expand Down Expand Up @@ -738,7 +723,9 @@ func TestCreateInstanceHandler_Handle(t *testing.T) {
// Tenant 1
tnu1 := testInstanceBuildUser(t, dbSession, "test-starfleet-id-2", tnOrg, tnOrgRoles)
tn1 := testInstanceBuildTenant(t, dbSession, "test-tenant", tnOrg, tnu1)
tn1 = testInstanceUpdateTenantCapability(t, dbSession, tn1)
// Privilege is resolved from a Ready TenantAccount config, so enable
// TargetedInstanceCreation on tn1's account with the site's provider.
_ = common.TestBuildTenantAccountWithTargetedInstanceCreation(t, dbSession, ip, &tn1.ID, tnOrg, cdbm.TenantAccountStatusReady, tnu1)

ts1 := testBuildTenantSiteAssociation(t, dbSession, tnOrg, tn1.ID, st1.ID, tnu1.ID)
assert.NotNil(t, ts1)
Expand Down
2 changes: 1 addition & 1 deletion rest-api/api/pkg/api/handler/instancebatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func TestBatchCreateInstanceHandler_Handle(t *testing.T) {
// Tenant 1
tnu1 := testInstanceBuildUser(t, dbSession, "test-starfleet-id-2", tnOrg, tnOrgRoles)
tn1 := testInstanceBuildTenant(t, dbSession, "test-tenant-1", tnOrg, tnu1)
tn1 = testInstanceUpdateTenantCapability(t, dbSession, tn1)
_ = common.TestBuildTenantAccountWithTargetedInstanceCreation(t, dbSession, ip, &tn1.ID, tnOrg, cdbm.TenantAccountStatusReady, tnu1)

// Tenant-Site association
ts1 := testBuildTenantSiteAssociation(t, dbSession, tnOrg, tn1.ID, st1.ID, tnu1.ID)
Expand Down
Loading
Loading