diff --git a/api/pkg/api/handler/tenantaccount.go b/api/pkg/api/handler/tenantaccount.go index d2d2ce330..e93ab6d6c 100644 --- a/api/pkg/api/handler/tenantaccount.go +++ b/api/pkg/api/handler/tenantaccount.go @@ -15,6 +15,7 @@ import ( "go.opentelemetry.io/otel/attribute" temporalClient "go.temporal.io/sdk/client" + mapset "github.com/deckarep/golang-set/v2" "github.com/google/uuid" "github.com/NVIDIA/infra-controller-rest/api/internal/config" @@ -107,6 +108,11 @@ func (ctah CreateTenantAccountHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to retrieve Infrastructure Provider for current org", nil) } + // Deprecated: infrastructureProviderId in request body. Infer from org when not provided. + if apiRequest.InfrastructureProviderID == "" { + apiRequest.InfrastructureProviderID = ip.ID.String() + } + if ip.ID.String() != apiRequest.InfrastructureProviderID { logger.Warn().Err(err).Msg("infrastructure provider in request does not belong to org") return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Invalid Infrastructure ProviderId in request", nil) @@ -242,9 +248,10 @@ func NewGetAllTenantAccountHandler(dbSession *cdb.Session, tc temporalClient.Cli // @Produce json // @Security ApiKeyAuth // @Param org path string true "Name of NGC organization" -// @Param infrastructureProviderId query string true "ID of InfrastructureProvider" -// @Param tenantId query string true "ID of Tenant" +// @Param infrastructureProviderId query string false "Deprecated: ID of Infrastructure Provider" +// @Param tenantId query string false "Filter TenantAccounts by Tenant ID (Provider role only; for Tenant role the tenant is inferred from org membership and this param is ignored)" // @Param status query string false "Query input for status" +// @Param query query string false "Search query string" // @Param includeRelation query string false "Related entities to include in response e.g. 'InfrastructureProvider', 'Tenant'" // @Param pageNumber query integer false "Page number of results returned" // @Param pageSize query integer false "Number of results per page" @@ -260,20 +267,9 @@ func (gatah GetAllTenantAccountHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve current user", nil) } - // Validate org - ok, err := auth.ValidateOrgMembership(dbUser, org) - if !ok { - if err != nil { - logger.Error().Err(err).Msg("error validating org membership for User in request") - } else { - logger.Warn().Msg("could not validate org membership for user, access denied") - } - return cutil.NewAPIErrorResponse(c, http.StatusForbidden, fmt.Sprintf("Failed to validate membership for org: %s", org), nil) - } - // Validate paginantion request pageRequest := pagination.PageRequest{} - err = c.Bind(&pageRequest) + err := c.Bind(&pageRequest) if err != nil { logger.Warn().Err(err).Msg("error binding pagination request data into API model") return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to parse request pagination data", nil) @@ -286,15 +282,6 @@ func (gatah GetAllTenantAccountHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to validate pagination request data", err) } - // Get and validate query params - qInfrastructureProviderID := c.QueryParam("infrastructureProviderId") - qTenantID := c.QueryParam("tenantId") - if qInfrastructureProviderID == "" && qTenantID == "" { - errStr := "Either infrastructureProviderId or tenantId query param must be specified." - logger.Warn().Msg(errStr) - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, errStr, nil) - } - // Get and validate includeRelation params qParams := c.QueryParams() qIncludeRelations, errMsg := common.GetAndValidateQueryRelations(qParams, cdbm.TenantAccountRelatedEntities) @@ -304,7 +291,7 @@ func (gatah GetAllTenantAccountHandler) Handle(c echo.Context) error { } // Get status from query param - var status *string + var statuses []string statusQuery := c.QueryParam("status") if statusQuery != "" { @@ -314,7 +301,7 @@ func (gatah GetAllTenantAccountHandler) Handle(c echo.Context) error { logger.Warn().Msg(fmt.Sprintf("invalid value in status query: %v", statusQuery)) return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Invalid Status value in query", nil) } - status = &statusQuery + statuses = []string{statusQuery} } searchQuery := common.GetSearchQuery(c) @@ -322,104 +309,66 @@ func (gatah GetAllTenantAccountHandler) Handle(c echo.Context) error { gatah.tracerSpan.SetAttribute(handlerSpan, attribute.String("query", *searchQuery), logger) } - var infrastructureProviderID *uuid.UUID - - // Validate infrastructure provider id if provided - if qInfrastructureProviderID != "" { - id, serr := uuid.Parse(qInfrastructureProviderID) - if serr != nil { - logger.Warn().Err(serr).Msg("error parsing infrastructureProviderId in query into uuid") - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Invalid Infrastructure Provider ID in query", nil) - } - - infrastructureProviderID = &id - } - - // Validate tenant id if provided - var tenantID *uuid.UUID - if qTenantID != "" { - id, serr := uuid.Parse(qTenantID) + // Optional Provider-side tenantId narrowing filter. The Tenant branch + // ignores this and always pins to the caller's own tenant. + var filterTenantIDs []uuid.UUID + if tenantIdQuery := c.QueryParam("tenantId"); tenantIdQuery != "" { + gatah.tracerSpan.SetAttribute(handlerSpan, attribute.String("tenantId", tenantIdQuery), logger) + id, serr := uuid.Parse(tenantIdQuery) if serr != nil { logger.Warn().Err(serr).Msg("error parsing tenantId in query into uuid") - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Invalid Tenant ID in query", nil) - } - - tenantID = &id - } - - // The query params must match _either_ the org's Infrastructure Provider _or_ the org's Tenant - // This allows the cases where: - // - A Tenant associated with the org could be filtering on Infrastructure Provider by providing both param - // - An Infrastructure Provider associated with the org could be filtering on Tenant by providing both param - isAssociated := false - orgInfrastructureProvider, err := common.GetInfrastructureProviderForOrg(ctx, nil, gatah.dbSession, org) - if err != nil { - if err != common.ErrOrgInstrastructureProviderNotFound { - logger.Error().Err(err).Msg("error getting infrastructure provider for org") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Infrastructure Provider for org, DB error", nil) - } - } else if infrastructureProviderID != nil && orgInfrastructureProvider.ID == *infrastructureProviderID { - // Validate role, only Provider Admins are allowed to proceed from here - ok = auth.ValidateUserRoles(dbUser, org, nil, auth.ProviderAdminRole, auth.ProviderViewerRole) - if !ok { - logger.Warn().Msg("user does not have Provider Admin role with org, access denied") - return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "User does not have Provider Admin role with org", nil) - } - - isAssociated = true - } - - // If the Infrastructure Provider in query does not belong to the org, then check if the Tenant in query belongs to the org - if !isAssociated { - orgTenant, err1 := common.GetTenantForOrg(ctx, nil, gatah.dbSession, org) - if err1 != nil { - if err1 != common.ErrOrgTenantNotFound { - logger.Error().Err(err1).Msg("error getting tenant for org") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve tenant for org", nil) - } - } else if tenantID != nil && orgTenant.ID == *tenantID { - // Validate role, only Tenant Admins are allowed to proceed from here - ok = auth.ValidateUserRoles(dbUser, org, nil, auth.TenantAdminRole) - if !ok { - logger.Warn().Msg("user does not have Tenant Admin role with org, access denied") - return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "User does not have Tenant Admin role with org", nil) - } - - isAssociated = true + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Invalid Tenant ID: %s in query", tenantIdQuery), nil) } + filterTenantIDs = []uuid.UUID{id} } - if !isAssociated { - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Either Infrastructure Provider or Tenant in query param must be associated with org", nil) + provider, tenant, apiErr := common.IsProviderOrTenant(ctx, logger, gatah.dbSession, org, dbUser, true, false) + if apiErr != nil { + return cutil.NewAPIErrorResponse(c, apiErr.Code, apiErr.Message, apiErr.Data) } - // Get Tenant Accounts taDAO := cdbm.NewTenantAccountDAO(gatah.dbSession) // default append `TenantContact` qIncludeRelations = append(qIncludeRelations, "TenantContact") - var tenantIDs []uuid.UUID - if tenantID != nil { - tenantIDs = []uuid.UUID{*tenantID} + sharedFilter := cdbm.TenantAccountFilterInput{ + Statuses: statuses, + SearchQuery: searchQuery, } - var statuses []string - if status != nil { - statuses = []string{*status} + mergedTenantAccountIDs := mapset.NewSet[uuid.UUID]() + totalLimitPage := cdbp.PageInput{Limit: cdb.GetIntPtr(cdbp.TotalLimit)} + + if provider != nil { + providerFilter := sharedFilter + providerFilter.InfrastructureProviderID = &provider.ID + providerFilter.TenantIDs = filterTenantIDs + tenantAccountsFromProviderPerspective, _, err := taDAO.GetAll(ctx, nil, providerFilter, totalLimitPage, nil) + if err != nil { + logger.Error().Err(err).Msg("error getting TenantAccounts from Provider perspective") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve TenantAccounts, DB error", nil) + } + for _, tenantAccount := range tenantAccountsFromProviderPerspective { + mergedTenantAccountIDs.Add(tenantAccount.ID) + } } - filter := cdbm.TenantAccountFilterInput{ - InfrastructureProviderID: infrastructureProviderID, - TenantIDs: tenantIDs, - Statuses: statuses, - SearchQuery: searchQuery, + if tenant != nil { + tenantFilter := sharedFilter + tenantFilter.TenantIDs = []uuid.UUID{tenant.ID} + tenantAccountsFromTenantPerspective, _, err := taDAO.GetAll(ctx, nil, tenantFilter, totalLimitPage, nil) + if err != nil { + logger.Error().Err(err).Msg("error getting TenantAccounts from Tenant perspective") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve TenantAccounts, DB error", nil) + } + for _, tenantAccount := range tenantAccountsFromTenantPerspective { + mergedTenantAccountIDs.Add(tenantAccount.ID) + } } - tas, total, err := taDAO.GetAll(ctx, nil, filter, cdbp.PageInput{ - Offset: pageRequest.Offset, - Limit: pageRequest.Limit, - OrderBy: pageRequest.OrderBy, - }, qIncludeRelations) + tas, total, err := taDAO.GetAll(ctx, nil, cdbm.TenantAccountFilterInput{ + TenantAccountIDs: mergedTenantAccountIDs.ToSlice(), + }, pageRequest.ConvertToDB(), qIncludeRelations) if err != nil { logger.Error().Err(err).Msg("error getting TenantAccounts from db") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve TenantAccounts", nil) @@ -443,38 +392,53 @@ func (gatah GetAllTenantAccountHandler) Handle(c echo.Context) error { ssdMap[ssd.EntityID] = append(ssdMap[ssd.EntityID], cssd) } - // Create response apiTas := []*model.APITenantAccount{} aDAO := cdbm.NewAllocationDAO(gatah.dbSession) - // Get Allocation count by InfrastructureProvider/Tenant - var allocationCountByTenantIDMap map[uuid.UUID]int - if len(tas) > 0 { - allocationCountByTenantIDMap = make(map[uuid.UUID]int) - allocationFilter := cdbm.AllocationFilterInput{InfrastructureProviderID: infrastructureProviderID} - if tenantID != nil { - allocationFilter.TenantIDs = append(allocationFilter.TenantIDs, *tenantID) + // Pre-compute Allocation counts in bounded queries instead of one per row. + // Group the page's TAs by their Infrastructure Provider and issue one + // Allocation query per provider, then bucket results by (Provider, Tenant). + // In the common case the merged page comes from one provider (Provider Admin + // view) so this collapses to a single query; for Tenant-Admin and dual-role + // callers with accounts across multiple providers, it is O(providers), not + // O(accounts). + type ipTenantKey struct{ IP, Tenant uuid.UUID } + allocCountByPair := map[ipTenantKey]int{} + tenantsByProvider := map[uuid.UUID]mapset.Set[uuid.UUID]{} + for _, ta := range tas { + if ta.TenantID == nil { + continue } - allocationPage := cdbp.PageInput{Limit: cdb.GetIntPtr(cdbp.TotalLimit)} - als, _, serr := aDAO.GetAll(ctx, nil, allocationFilter, allocationPage, nil) - if serr != nil { - logger.Error().Err(serr).Msg("error retrieving allocation for Tenant from DB") + set, ok := tenantsByProvider[ta.InfrastructureProviderID] + if !ok { + set = mapset.NewSet[uuid.UUID]() + tenantsByProvider[ta.InfrastructureProviderID] = set + } + set.Add(*ta.TenantID) + } + allocPage := cdbp.PageInput{Limit: cdb.GetIntPtr(cdbp.TotalLimit)} + for ipID, tenantSet := range tenantsByProvider { + ipID := ipID + als, _, aerr := aDAO.GetAll(ctx, nil, cdbm.AllocationFilterInput{ + InfrastructureProviderID: &ipID, + TenantIDs: tenantSet.ToSlice(), + }, allocPage, nil) + if aerr != nil { + logger.Error().Err(aerr).Msg("error retrieving allocations for Tenant Accounts from DB") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Allocations to determine total Allocation count for Tenants", nil) } - for _, al := range als { - allocationCountByTenantIDMap[al.TenantID]++ + allocCountByPair[ipTenantKey{IP: al.InfrastructureProviderID, Tenant: al.TenantID}]++ } } for _, ta := range tas { tmpTa := ta - // Check for Allocation for Tenant - total := 0 + allocCount := 0 if tmpTa.TenantID != nil { - total = allocationCountByTenantIDMap[*tmpTa.TenantID] + allocCount = allocCountByPair[ipTenantKey{IP: tmpTa.InfrastructureProviderID, Tenant: *tmpTa.TenantID}] } - apiTa := model.NewAPITenantAccount(&tmpTa, ssdMap[ta.ID.String()], total) + apiTa := model.NewAPITenantAccount(&tmpTa, ssdMap[ta.ID.String()], allocCount) apiTas = append(apiTas, apiTa) } @@ -522,8 +486,8 @@ func NewGetTenantAccountHandler(dbSession *cdb.Session, tc temporalClient.Client // @Security ApiKeyAuth // @Param org path string true "Name of NGC organization" // @Param id path string true "ID of Tenant Account" -// @Param infrastructureProviderId query string true "ID of InfrastructureProvider" -// @Param tenantId query string true "ID of Tenant" +// @Param infrastructureProviderId query string false "Deprecated: ID of Infrastructure Provider" +// @Param tenantId query string false "Deprecated: ID of Tenant" // @Param includeRelation query string false "Related entities to include in response e.g. 'InfrastructureProvider', 'Tenant'" // @Success 200 {object} model.APITenantAccount // @Router /v2/org/{org}/nico/tenant/account/{id} [get] @@ -536,17 +500,6 @@ func (gtah GetTenantAccountHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve current user", nil) } - // Validate org - ok, err := auth.ValidateOrgMembership(dbUser, org) - if !ok { - if err != nil { - logger.Error().Err(err).Msg("error validating org membership for User in request") - } else { - logger.Warn().Msg("could not validate org membership for user, access denied") - } - return cutil.NewAPIErrorResponse(c, http.StatusForbidden, fmt.Sprintf("Failed to validate membership for org: %s", org), nil) - } - // Get tenant account ID from URL param taStrID := c.Param("id") @@ -578,106 +531,29 @@ func (gtah GetTenantAccountHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusNotFound, "Could not retrieve Tenant Account to update", nil) } - // Get and validate query params - qInfrastructureProviderID := c.QueryParam("infrastructureProviderId") - qTenantID := c.QueryParam("tenantId") - - if qInfrastructureProviderID == "" && qTenantID == "" { - // Logging common user request data error can add a lot to system logs - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Either Infrastructure Provider ID or Tenant ID query param must be specified.", nil) + provider, tenant, apiErr := common.IsProviderOrTenant(ctx, logger, gtah.dbSession, org, dbUser, true, false) + if apiErr != nil { + return cutil.NewAPIErrorResponse(c, apiErr.Code, apiErr.Message, apiErr.Data) } - var infrastructureProviderID *uuid.UUID - var tenantID *uuid.UUID - - // Validate infrastructure provider id if provided - if qInfrastructureProviderID != "" { - id, err1 := uuid.Parse(qInfrastructureProviderID) - if err1 != nil { - logger.Warn().Err(err1).Msg("error parsing infrastructureProviderId in query into uuid") - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Invalid Infrastructure Provider ID in query", nil) - } - - // If the Infrastructure Provider ID in query is not the same as the one in the Tenant Account, return an error - if id != ta.InfrastructureProviderID { - return cutil.NewAPIErrorResponse(c, http.StatusNotFound, "Could not find Tenant Account matching Infrastructure Provider in query", nil) - } - - infrastructureProviderID = &id + authorized := (provider != nil && ta.InfrastructureProviderID == provider.ID) || + (tenant != nil && ta.TenantID != nil && *ta.TenantID == tenant.ID) + if !authorized { + return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "Tenant Account is not associated with org", nil) } - // Validate tenant id if provided - if qTenantID != "" { - id, err1 := uuid.Parse(qTenantID) - if err1 != nil { - logger.Warn().Err(err1).Msg("error parsing tenantId in query into uuid") - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Invalid Tenant ID in query", nil) - } - - // If the Tenant in query is not the same as the one in the Tenant Account, return an error - if ta.TenantID == nil || *ta.TenantID != id { - return cutil.NewAPIErrorResponse(c, http.StatusNotFound, "Could not find Tenant Account matching Tenant in query", nil) - } - - tenantID = &id - } - - // The query params must match _either_ the org's Infrastructure Provider _or_ the org's Tenant - // This allows the cases where: - // - A Tenant associated with the org could be filtering on Infrastructure Provider by providing both param - // - An Infrastructure Provider associated with the org could be filtering on Tenant by providing both param - isAssociated := false - orgInfrastructureProvider, err := common.GetInfrastructureProviderForOrg(ctx, nil, gtah.dbSession, org) - if err != nil { - if err != common.ErrOrgInstrastructureProviderNotFound { - logger.Error().Err(err).Msg("error getting infrastructure provider for org") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve infrastructure provider for org, DB error", nil) - } - } else if infrastructureProviderID != nil && orgInfrastructureProvider.ID == *infrastructureProviderID { - // Validate role, only Provider Admins are allowed to proceed from here - ok = auth.ValidateUserRoles(dbUser, org, nil, auth.ProviderAdminRole, auth.ProviderViewerRole) - if !ok { - logger.Warn().Msg("user does not have Provider Admin role with org, access denied") - return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "User does not have Provider Admin role with org", nil) - } - - isAssociated = true - } - - // If the Infrastructure Provider in query does not belong to the org, then check if the Tenant in query belongs to the org - if !isAssociated { - orgTenant, err1 := common.GetTenantForOrg(ctx, nil, gtah.dbSession, org) - if err1 != nil { - if err1 != common.ErrOrgTenantNotFound { - logger.Error().Err(err1).Msg("error getting tenant for org") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenant for org", nil) - } - } else if tenantID != nil && orgTenant.ID == *tenantID { - // Validate role, only Tenant Admins are allowed to proceed from here - ok = auth.ValidateUserRoles(dbUser, org, nil, auth.TenantAdminRole) - if !ok { - logger.Warn().Msg("user does not have Tenant Admin role with org, access denied") - return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "User does not have Tenant Admin role with org", nil) - } - - isAssociated = true - } - } - - if !isAssociated { - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Either Infrastructure Provider or Tenant in query param must be associated with org", nil) - } - - // Check for Allocation aDAO := cdbm.NewAllocationDAO(gtah.dbSession) - allocationFilter := cdbm.AllocationFilterInput{InfrastructureProviderID: infrastructureProviderID} - if tenantID != nil { - allocationFilter.TenantIDs = append(allocationFilter.TenantIDs, *tenantID) - } - total, serr := aDAO.GetCount(ctx, nil, allocationFilter) - if serr != nil { - logger.Error().Err(serr).Msg("error retrieving allocation for Tenant from DB") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Allocations to determine total allocation for tenant account", nil) + total := 0 + if ta.TenantID != nil { + cnt, cerr := aDAO.GetCount(ctx, nil, cdbm.AllocationFilterInput{ + InfrastructureProviderID: &ta.InfrastructureProviderID, + TenantIDs: []uuid.UUID{*ta.TenantID}, + }) + if cerr != nil { + logger.Error().Err(cerr).Msg("error retrieving allocation count for Tenant Account from DB") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Allocations to determine total allocation for tenant account", nil) + } + total = cnt } sdDAO := cdbm.NewStatusDetailDAO(gtah.dbSession) diff --git a/api/pkg/api/handler/tenantaccount_test.go b/api/pkg/api/handler/tenantaccount_test.go index bc771ab36..c270228c2 100644 --- a/api/pkg/api/handler/tenantaccount_test.go +++ b/api/pkg/api/handler/tenantaccount_test.go @@ -256,6 +256,8 @@ func TestTenantAccountHandler_Create(t *testing.T) { assert.Nil(t, err) okBody3, err := json.Marshal(model.APITenantAccountCreateRequest{InfrastructureProviderID: ip.ID.String(), TenantOrg: cdb.GetStrPtr(tnOrg3)}) assert.Nil(t, err) + okBodyInferIP, err := json.Marshal(model.APITenantAccountCreateRequest{TenantOrg: cdb.GetStrPtr("test-tn-org-infer")}) + assert.Nil(t, err) cfg := common.GetTestConfig() tempClient := &tmocks.Client{} @@ -381,6 +383,15 @@ func TestTenantAccountHandler_Create(t *testing.T) { expectedStatus: http.StatusCreated, expectedTenantID: nil, }, + { + name: "success when infrastructureProviderId is omitted (inferred from org)", + reqOrgName: ipOrg1, + reqBody: string(okBodyInferIP), + user: ipu, + expectedErr: false, + expectedStatus: http.StatusCreated, + expectedTenantID: nil, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -793,14 +804,6 @@ func TestTenantAccountHandler_GetByID(t *testing.T) { expectedErr: true, expectedStatus: http.StatusInternalServerError, }, - { - name: "error when infrastructure provider and tenant not specified", - reqOrgName: tnOrg1, - user: tnUser, - taID: ta11.ID.String(), - expectedErr: true, - expectedStatus: http.StatusBadRequest, - }, { name: "error when tenant account id is invalid uuid", reqOrgName: tnOrg1, @@ -817,15 +820,6 @@ func TestTenantAccountHandler_GetByID(t *testing.T) { expectedErr: true, expectedStatus: http.StatusNotFound, }, - { - name: "error when infrastructure provider not valid uuid", - reqOrgName: ipOrg1, - user: ipUser, - taID: ta11.ID.String(), - queryInfrastructureProviderID: cdb.GetStrPtr("non-uuid"), - expectedErr: true, - expectedStatus: http.StatusBadRequest, - }, { name: "error when infrastructure provider not found for org", reqOrgName: ipOrg3, @@ -835,15 +829,6 @@ func TestTenantAccountHandler_GetByID(t *testing.T) { expectedErr: true, expectedStatus: http.StatusBadRequest, }, - { - name: "error when infrastructure provider in url doesnt match org", - reqOrgName: ipOrg1, - user: ipUser, - taID: ta11.ID.String(), - queryInfrastructureProviderID: cdb.GetStrPtr(uuid.New().String()), - expectedErr: true, - expectedStatus: http.StatusNotFound, - }, { name: "error when infrastructure provider in org doesnt match infrastructure provider in tenant account", reqOrgName: ipOrg1, @@ -851,16 +836,7 @@ func TestTenantAccountHandler_GetByID(t *testing.T) { taID: ta21.ID.String(), queryInfrastructureProviderID: cdb.GetStrPtr(ip1.ID.String()), expectedErr: true, - expectedStatus: http.StatusNotFound, - }, - { - name: "error when tenant id not valid uuid", - reqOrgName: ipOrg1, - user: ipUser, - taID: ta11.ID.String(), - queryTenantID: cdb.GetStrPtr("non-uuid"), - expectedErr: true, - expectedStatus: http.StatusBadRequest, + expectedStatus: http.StatusForbidden, }, { name: "error when tenant not found for org", @@ -878,7 +854,7 @@ func TestTenantAccountHandler_GetByID(t *testing.T) { taID: ta11.ID.String(), queryTenantID: cdb.GetStrPtr(tn1.ID.String()), expectedErr: true, - expectedStatus: http.StatusBadRequest, + expectedStatus: http.StatusForbidden, }, { name: "error when tenant in org doesnt match tenant in tenant account", @@ -887,7 +863,7 @@ func TestTenantAccountHandler_GetByID(t *testing.T) { taID: ta12.ID.String(), queryTenantID: cdb.GetStrPtr(tn1.ID.String()), expectedErr: true, - expectedStatus: http.StatusNotFound, + expectedStatus: http.StatusForbidden, }, { name: "error when tenant id is not found", @@ -1087,6 +1063,8 @@ func TestTenantAccountHandler_GetAll(t *testing.T) { st1 := testTenantAccountBuildSite(t, dbSession, ip1, "site1", ipUser) assert.NotNil(t, st1) + st2 := testTenantAccountBuildSite(t, dbSession, ip2, "site2", ipUser) + assert.NotNil(t, st2) tns := []cdbm.Tenant{} tas := []cdbm.TenantAccount{} @@ -1098,6 +1076,8 @@ func TestTenantAccountHandler_GetAll(t *testing.T) { allocation := testTenantAccountBuildAllocation(t, dbSession, st1, tn, "Test Allocation", ipUser) assert.NotNil(t, allocation) + allocation2 := testTenantAccountBuildAllocation(t, dbSession, st2, tn, "Test Allocation 2", ipUser) + assert.NotNil(t, allocation2) ta1 := testTenantAccountBuildTenantAccount(t, dbSession, fmt.Sprintf("test-tenant-account-%02d", i), ip1, tn, tn.Org, cdbm.TenantAccountStatusInvited, ipUser.ID, contactUser1.ID) assert.NotNil(t, ta1) @@ -1165,26 +1145,6 @@ func TestTenantAccountHandler_GetAll(t *testing.T) { expectedErr: true, expectedStatus: http.StatusInternalServerError, }, - { - name: "error when infrastructure provider and tenant not specified", - reqOrgName: tnOrgs[0], - user: tnUser, - queryInfrastructureProviderID: nil, - queryTenantID: nil, - queryIncludeRelations1: nil, - expectedErr: true, - expectedStatus: http.StatusBadRequest, - }, - { - name: "error when infrastructure provider not valid uuid", - reqOrgName: ipOrg1, - user: ipUser, - queryInfrastructureProviderID: cdb.GetStrPtr("non-uuid"), - queryTenantID: nil, - queryIncludeRelations1: nil, - expectedErr: true, - expectedStatus: http.StatusBadRequest, - }, { name: "error when infrastructure provider not found for org", reqOrgName: ipOrg3, @@ -1195,46 +1155,6 @@ func TestTenantAccountHandler_GetAll(t *testing.T) { expectedErr: true, expectedStatus: http.StatusBadRequest, }, - { - name: "error when infrastructure provider in url doesnt match org", - reqOrgName: ipOrg1, - user: ipUser, - queryInfrastructureProviderID: cdb.GetStrPtr(uuid.New().String()), - queryTenantID: nil, - queryIncludeRelations1: nil, - expectedErr: true, - expectedStatus: http.StatusBadRequest, - }, - { - name: "error when tenant id not valid uuid", - reqOrgName: tnOrgs[0], - user: tnUser, - queryInfrastructureProviderID: nil, - queryTenantID: cdb.GetStrPtr("non-uuid"), - queryIncludeRelations1: nil, - expectedErr: true, - expectedStatus: http.StatusBadRequest, - }, - { - name: "error when tenant not found for org", - reqOrgName: tnOrgs[0], - user: tnUser, - queryInfrastructureProviderID: nil, - queryTenantID: cdb.GetStrPtr(tn15.ID.String()), - queryIncludeRelations1: nil, - expectedErr: true, - expectedStatus: http.StatusBadRequest, - }, - { - name: "error when tenant id in url doesnt match org", - reqOrgName: tnOrgs[0], - user: tnUser, - queryInfrastructureProviderID: nil, - queryTenantID: cdb.GetStrPtr(uuid.New().String()), - queryIncludeRelations1: nil, - expectedErr: true, - expectedStatus: http.StatusBadRequest, - }, { name: "success when infrastructure provider id is specified", reqOrgName: ipOrg1, diff --git a/api/pkg/api/model/tenantaccount.go b/api/pkg/api/model/tenantaccount.go index abb4076be..0ce875810 100644 --- a/api/pkg/api/model/tenantaccount.go +++ b/api/pkg/api/model/tenantaccount.go @@ -32,8 +32,7 @@ type APITenantAccountCreateRequest struct { func (tacr APITenantAccountCreateRequest) Validate() error { return validation.ValidateStruct(&tacr, validation.Field(&tacr.InfrastructureProviderID, - validation.Required.Error(validationErrorValueRequired), - validationis.UUID.Error(validationErrorInvalidUUID)), + validation.When(tacr.InfrastructureProviderID != "", validationis.UUID.Error(validationErrorInvalidUUID))), validation.Field(&tacr.TenantID, validation.When(tacr.TenantOrg == nil, validation.Required.Error(validationErrorTenantIDOrOrgRequired)), validationis.UUID.Error(validationErrorInvalidUUID)), diff --git a/api/pkg/api/model/tenantaccount_test.go b/api/pkg/api/model/tenantaccount_test.go index 4062281a6..b1e356264 100644 --- a/api/pkg/api/model/tenantaccount_test.go +++ b/api/pkg/api/model/tenantaccount_test.go @@ -22,10 +22,9 @@ func TestAPITenantAccountCreateRequest_Validate(t *testing.T) { errStr string }{ { - desc: "errors when infrastructureProviderID is not provided", + desc: "ok when infrastructureProviderID is omitted (inferred from org by handler)", obj: APITenantAccountCreateRequest{TenantID: cdb.GetStrPtr(uuid.New().String())}, - expectErr: true, - errStr: "infrastructureProviderId: " + validationErrorValueRequired + ".", + expectErr: false, }, { desc: "errors when infrastructureProviderID is invalid", diff --git a/cli/tui/commands.go b/cli/tui/commands.go index 67aaf3033..37b5f7cd8 100644 --- a/cli/tui/commands.go +++ b/cli/tui/commands.go @@ -2311,17 +2311,12 @@ func cmdTenantAccountList(s *Session, _ []string) error { } func cmdTenantAccountCreate(s *Session, _ []string) error { - infrastructureProviderID, err := PromptText("Infrastructure provider ID", true) - if err != nil { - return err - } tenantOrg, err := PromptText("Tenant org", true) if err != nil { return err } body := map[string]interface{}{ - "infrastructureProviderId": strings.TrimSpace(infrastructureProviderID), - "tenantOrg": strings.TrimSpace(tenantOrg), + "tenantOrg": strings.TrimSpace(tenantOrg), } LogCmd(s, "tenant-account", "create", "--tenant-org", strings.TrimSpace(tenantOrg)) bodyJSON, _ := json.Marshal(body) diff --git a/db/pkg/db/model/tenantaccount.go b/db/pkg/db/model/tenantaccount.go index f0d1f49e9..03dd21b0c 100644 --- a/db/pkg/db/model/tenantaccount.go +++ b/db/pkg/db/model/tenantaccount.go @@ -160,6 +160,7 @@ type TenantAccountFilterInput struct { Statuses []string TenantIDs []uuid.UUID TenantOrgs []string + TenantAccountIDs []uuid.UUID SearchQuery *string } @@ -343,6 +344,15 @@ func (tasd TenantAccountSQLDAO) setQueryWithFilter(filter TenantAccountFilterInp tasd.tracerSpan.SetAttribute(tnaDAOSpan, "status", filter.Statuses) } + if filter.TenantAccountIDs != nil { + if len(filter.TenantAccountIDs) == 1 { + query = query.Where("ta.id = ?", filter.TenantAccountIDs[0]) + } else { + query = query.Where("ta.id IN (?)", bun.In(filter.TenantAccountIDs)) + } + tasd.tracerSpan.SetAttribute(tnaDAOSpan, "id", filter.TenantAccountIDs) + } + searchQuery, searchTokens, ok := db.NormalizeSearchQuery(filter.SearchQuery) if ok { query = query.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery { diff --git a/docs/index.html b/docs/index.html index a8025bddf..36694b0f2 100644 --- a/docs/index.html +++ b/docs/index.html @@ -820,20 +820,16 @@
Kubernetes Cluster
[- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "org": "string",
- "orgDisplayName": "string",
- "instanceTypes": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "allocated": 0,
- "usedMachineStats": {
- "total": 0,
- "initializing": 0,
- "ready": 0,
- "inUse": 0,
- "error": 0,
- "maintenance": 0,
- "unknown": 0
}, - "maxAllocatable": 0,
- "allocations": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "total": 0
}
]
}
]
}
]Tenant Account connects a Tenant with an Infrastructure Provider. It represents/contains any information pertaining to their relationship.
-Retrieve all Tenant Accounts.
-Either infrastructureProviderId or tenantId query param must be specified.
If infrastructureProviderId query param is provided, then org must have an Infrastructure Provider entity and its ID should match the query param value. User must have authorization role with PROVIDER_ADMIN suffix.
If tenantId query param is provided, then org must have a Tenant entity and its ID should match the query param value. User must have authorization role with TENANT_ADMIN suffix.
Retrieve all Tenant Accounts for the org.
+Provider and Tenant roles are inferred from the org's membership. User must have authorization role with PROVIDER_ADMIN, PROVIDER_VIEWER, or TENANT_ADMIN suffix.
| org required | string Name of the Org - |
| infrastructureProviderId | string <uuid> Filter TenantAccounts by Infrastructure Provider ID - |
| tenantId | string <uuid> Filter TenantAccounts by Tenant ID + |