From 49ee43684489f0acfdd5ad507b7a08ef47882a4d Mon Sep 17 00:00:00 2001 From: Parham Armani Date: Fri, 22 May 2026 10:00:13 -0700 Subject: [PATCH] feat: Infer Provider/Tenant from caller's org across Tenant Account handlers Signed-off-by: Parham Armani --- api/pkg/api/handler/tenantaccount.go | 340 ++++++------------ api/pkg/api/handler/tenantaccount_test.go | 116 +----- api/pkg/api/model/tenantaccount.go | 3 +- api/pkg/api/model/tenantaccount_test.go | 5 +- cli/tui/commands.go | 7 +- db/pkg/db/model/tenantaccount.go | 10 + docs/index.html | 54 ++- openapi/spec.yaml | 37 +- sdk/standard/api_tenant_account.go | 30 +- .../model_tenant_account_create_request.go | 43 ++- 10 files changed, 224 insertions(+), 421 deletions(-) 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 @@

Typical API Call Flow for Tenant

" class="sc-iJuXkV sc-cBNeAB iNuSsz dyntKg">

Kubernetes Cluster

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tenant/instance-type/stats

Response samples

Content type
application/json
[
  • {
    }
]

Tenant Account

Tenant Account connects a Tenant with an Infrastructure Provider. It represents/contains any information pertaining to their relationship.

-

Retrieve all Tenant Accounts

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

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.

Authorizations:
JWTBearerToken
path Parameters
org
required
string

Name of the Org

-
query Parameters
infrastructureProviderId
string <uuid>

Filter TenantAccounts by Infrastructure Provider ID

-
tenantId
string <uuid>

Filter TenantAccounts by Tenant ID

+
query Parameters
infrastructureProviderId
string <uuid>
Deprecated

Filter TenantAccounts by Infrastructure Provider ID. Deprecated: Infrastructure Provider is now inferred from the org's membership.

+
tenantId
string <uuid>

Filter TenantAccounts by Tenant ID (Provider role only; for Tenant role the tenant is inferred from org membership and this param is ignored).

query
string

Search string to filter Tenant Accounts by account number, tenant org, or tenant org display name

includeRelation
string
Enum: "InfrastructureProvider" "Tenant"
Typical API Call Flow for Tenant

Response samples

Content type
application/json
[
  • {
    }
]

Create Tenant Account

Create a Tenant Account.

-

Org must have an Infrastructure Provider entity and its ID must match the Infrastructure Provider ID in request data. User must have authorization role with PROVIDER_ADMIN suffix

+

Org must have an Infrastructure Provider entity. User must have authorization role with PROVIDER_ADMIN suffix. The Infrastructure Provider is inferred from the caller's org; the deprecated infrastructureProviderId request body field is optional and, when provided, must match the org's Infrastructure Provider.

Infrastructure Provider can create a Tenant Account by specifying the Tenant's UUID or Tenant's org name. This will set the status of the Tenant Account to "Invited". Then the Tenant can view this account information and are able to confirm/accept the account by updating the Tenant Account.

Authorizations:
JWTBearerToken
path Parameters
org
required
string

Name of the Org

-
Request Body schema: application/json
infrastructureProviderId
required
string <uuid>
tenantOrg
required
string non-empty ^[A-Za-z0-9-_]+$
Request Body schema: application/json
infrastructureProviderId
string <uuid>
Deprecated

Deprecated; inferred from the caller's org Infrastructure Provider +when omitted. When provided, the value must match the org's +Infrastructure Provider — mismatched values are rejected with 400.

+
tenantOrg
required
string non-empty ^[A-Za-z0-9_-]+$

Must be a valid Org name

Responses

Request samples

Content type
application/json
{
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "tenantOrg": "rf43bbtnb9c5"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "infrastructureProviderOrg": "xskkpgqpeakn",
  • "tenantId": null,
  • "tenantOrg": "rf43bbtnb9c5",
  • "tenantContact": {
    },
  • "allocationCount": 0,
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Tenant Account

Retrieve a Tenant Account by ID

-

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.

+
https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tenant/account

Request samples

Content type
application/json
{
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "tenantOrg": "rf43bbtnb9c5"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "infrastructureProviderOrg": "xskkpgqpeakn",
  • "tenantId": null,
  • "tenantOrg": "rf43bbtnb9c5",
  • "tenantContact": {
    },
  • "allocationCount": 0,
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Tenant Account

Retrieve a Tenant Account by ID.

+

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.

Authorizations:
JWTBearerToken
path Parameters
org
required
string

Name of the Org

accountId
required
string <uuid>

ID of the Tenant Account

-
query Parameters
infrastructureProviderId
string <uuid>

Filter Tenant Accounts by Infrastructure Provider ID

-
tenantId
string <uuid>

Filter Tenant Accounts by Tenant ID

+
query Parameters
infrastructureProviderId
string <uuid>
Deprecated

Filter Tenant Accounts by Infrastructure Provider ID. Deprecated: Infrastructure Provider is now inferred from the org's membership.

+
tenantId
string <uuid>
Deprecated

Filter Tenant Accounts by Tenant ID. Deprecated: Tenant is now inferred from the org's membership.

includeRelation
string
Enum: "InfrastructureProvider" "Tenant"

Related entity to expand

Responses