github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/domain/tenant/repository.go (about)

     1  package tenant
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"math"
     8  	"strings"
     9  	"text/template"
    10  
    11  	"github.com/jmoiron/sqlx"
    12  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    13  
    14  	"github.com/kyma-incubator/compass/components/director/pkg/tenant"
    15  
    16  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    17  
    18  	"github.com/kyma-incubator/compass/components/director/pkg/resource"
    19  
    20  	"github.com/kyma-incubator/compass/components/director/pkg/persistence"
    21  	"github.com/kyma-incubator/compass/components/director/pkg/str"
    22  
    23  	"github.com/pkg/errors"
    24  
    25  	"github.com/kyma-incubator/compass/components/director/internal/model"
    26  	"github.com/kyma-incubator/compass/components/director/internal/repo"
    27  )
    28  
    29  const (
    30  	tableName                      string = `public.business_tenant_mappings`
    31  	labelDefinitionsTableName      string = `public.label_definitions`
    32  	labelDefinitionsTenantIDColumn string = `tenant_id`
    33  
    34  	maxParameterChunkSize int = 50000 // max parameters size in PostgreSQL is 65535
    35  )
    36  
    37  var (
    38  	idColumn                  = "id"
    39  	idColumnCasted            = "id::text"
    40  	externalNameColumn        = "external_name"
    41  	externalTenantColumn      = "external_tenant"
    42  	parentColumn              = "parent"
    43  	typeColumn                = "type"
    44  	providerNameColumn        = "provider_name"
    45  	statusColumn              = "status"
    46  	initializedComputedColumn = "initialized"
    47  
    48  	insertColumns      = []string{idColumn, externalNameColumn, externalTenantColumn, parentColumn, typeColumn, providerNameColumn, statusColumn}
    49  	conflictingColumns = []string{externalTenantColumn}
    50  	updateColumns      = []string{externalNameColumn}
    51  	searchColumns      = []string{idColumnCasted, externalNameColumn, externalTenantColumn}
    52  )
    53  
    54  // Converter converts tenants between the model.BusinessTenantMapping service-layer representation of a tenant and the repo-layer representation tenant.Entity.
    55  //
    56  //go:generate mockery --name=Converter --output=automock --outpkg=automock --case=underscore --disable-version-string
    57  type Converter interface {
    58  	ToEntity(in *model.BusinessTenantMapping) *tenant.Entity
    59  	FromEntity(in *tenant.Entity) *model.BusinessTenantMapping
    60  }
    61  
    62  type pgRepository struct {
    63  	upserter              repo.UpserterGlobal
    64  	unsafeCreator         repo.UnsafeCreator
    65  	existQuerierGlobal    repo.ExistQuerierGlobal
    66  	singleGetterGlobal    repo.SingleGetterGlobal
    67  	pageableQuerierGlobal repo.PageableQuerierGlobal
    68  	listerGlobal          repo.ListerGlobal
    69  	updaterGlobal         repo.UpdaterGlobal
    70  	deleterGlobal         repo.DeleterGlobal
    71  
    72  	conv Converter
    73  }
    74  
    75  // NewRepository returns a new entity responsible for repo-layer tenant operations. All of its methods require persistence.PersistenceOp it the provided context.
    76  func NewRepository(conv Converter) *pgRepository {
    77  	return &pgRepository{
    78  		upserter:              repo.NewUpserterGlobal(resource.Tenant, tableName, insertColumns, conflictingColumns, updateColumns),
    79  		unsafeCreator:         repo.NewUnsafeCreator(resource.Tenant, tableName, insertColumns, conflictingColumns),
    80  		existQuerierGlobal:    repo.NewExistQuerierGlobal(resource.Tenant, tableName),
    81  		singleGetterGlobal:    repo.NewSingleGetterGlobal(resource.Tenant, tableName, insertColumns),
    82  		pageableQuerierGlobal: repo.NewPageableQuerierGlobal(resource.Tenant, tableName, insertColumns),
    83  		listerGlobal:          repo.NewListerGlobal(resource.Tenant, tableName, insertColumns),
    84  		updaterGlobal:         repo.NewUpdaterGlobal(resource.Tenant, tableName, []string{externalNameColumn, externalTenantColumn, parentColumn, typeColumn, providerNameColumn, statusColumn}, []string{idColumn}),
    85  		deleterGlobal:         repo.NewDeleterGlobal(resource.Tenant, tableName),
    86  		conv:                  conv,
    87  	}
    88  }
    89  
    90  // UnsafeCreate adds a new tenant in the Compass DB in case it does not exist. If it already exists, no action is taken.
    91  // It is not guaranteed that the provided tenant ID is the same as the tenant ID in the database.
    92  func (r *pgRepository) UnsafeCreate(ctx context.Context, item model.BusinessTenantMapping) error {
    93  	return r.unsafeCreator.UnsafeCreate(ctx, r.conv.ToEntity(&item))
    94  }
    95  
    96  // Upsert adds the provided tenant into the Compass storage if it does not exist, or updates it if it does.
    97  func (r *pgRepository) Upsert(ctx context.Context, item model.BusinessTenantMapping) error {
    98  	return r.upserter.UpsertGlobal(ctx, r.conv.ToEntity(&item))
    99  }
   100  
   101  // Get retrieves the active tenant with matching internal ID from the Compass storage.
   102  func (r *pgRepository) Get(ctx context.Context, id string) (*model.BusinessTenantMapping, error) {
   103  	var entity tenant.Entity
   104  	conditions := repo.Conditions{
   105  		repo.NewEqualCondition(idColumn, id),
   106  		repo.NewNotEqualCondition(statusColumn, string(tenant.Inactive))}
   107  	if err := r.singleGetterGlobal.GetGlobal(ctx, conditions, repo.NoOrderBy, &entity); err != nil {
   108  		return nil, err
   109  	}
   110  
   111  	return r.conv.FromEntity(&entity), nil
   112  }
   113  
   114  // GetByExternalTenant retrieves the active tenant with matching external ID from the Compass storage.
   115  func (r *pgRepository) GetByExternalTenant(ctx context.Context, externalTenant string) (*model.BusinessTenantMapping, error) {
   116  	var entity tenant.Entity
   117  	conditions := repo.Conditions{
   118  		repo.NewEqualCondition(externalTenantColumn, externalTenant),
   119  		repo.NewNotEqualCondition(statusColumn, string(tenant.Inactive))}
   120  	if err := r.singleGetterGlobal.GetGlobal(ctx, conditions, repo.NoOrderBy, &entity); err != nil {
   121  		return nil, err
   122  	}
   123  	return r.conv.FromEntity(&entity), nil
   124  }
   125  
   126  // Exists checks if tenant with the provided internal ID exists in the Compass storage.
   127  func (r *pgRepository) Exists(ctx context.Context, id string) (bool, error) {
   128  	return r.existQuerierGlobal.ExistsGlobal(ctx, repo.Conditions{repo.NewEqualCondition(idColumn, id)})
   129  }
   130  
   131  // ExistsByExternalTenant checks if tenant with the provided external ID exists in the Compass storage.
   132  func (r *pgRepository) ExistsByExternalTenant(ctx context.Context, externalTenant string) (bool, error) {
   133  	return r.existQuerierGlobal.ExistsGlobal(ctx, repo.Conditions{repo.NewEqualCondition(externalTenantColumn, externalTenant)})
   134  }
   135  
   136  // List retrieves all tenants from the Compass storage.
   137  func (r *pgRepository) List(ctx context.Context) ([]*model.BusinessTenantMapping, error) {
   138  	var entityCollection tenant.EntityCollection
   139  
   140  	persist, err := persistence.FromCtx(ctx)
   141  	if err != nil {
   142  		return nil, errors.Wrap(err, "while fetching persistence from context")
   143  	}
   144  
   145  	prefixedFields := strings.Join(str.PrefixStrings(insertColumns, "t."), ", ")
   146  	query := fmt.Sprintf(`SELECT DISTINCT %s, ld.%s IS NOT NULL AS %s
   147  			FROM %s t LEFT JOIN %s ld ON t.%s=ld.%s
   148  			WHERE t.%s = $1
   149  			ORDER BY %s DESC, t.%s ASC`, prefixedFields, labelDefinitionsTenantIDColumn, initializedComputedColumn, tableName, labelDefinitionsTableName, idColumn, labelDefinitionsTenantIDColumn, statusColumn, initializedComputedColumn, externalNameColumn)
   150  
   151  	err = persist.SelectContext(ctx, &entityCollection, query, tenant.Active)
   152  	if err != nil {
   153  		return nil, errors.Wrap(err, "while listing tenants from DB")
   154  	}
   155  
   156  	return r.multipleFromEntities(entityCollection), nil
   157  }
   158  
   159  // ListPageBySearchTerm retrieves a page of tenants from the Compass storage filtered by a search term.
   160  func (r *pgRepository) ListPageBySearchTerm(ctx context.Context, searchTerm string, pageSize int, cursor string) (*model.BusinessTenantMappingPage, error) {
   161  	searchTermRegex := fmt.Sprintf("%%%s%%", searchTerm)
   162  
   163  	var entityCollection tenant.EntityCollection
   164  	likeConditions := make([]repo.Condition, 0, len(searchColumns))
   165  	for _, searchColumn := range searchColumns {
   166  		likeConditions = append(likeConditions, repo.NewLikeCondition(searchColumn, searchTermRegex))
   167  	}
   168  
   169  	conditions := repo.And(
   170  		&repo.ConditionTree{Operand: repo.NewEqualCondition(statusColumn, tenant.Active)},
   171  		repo.Or(repo.ConditionTreesFromConditions(likeConditions)...))
   172  
   173  	page, totalCount, err := r.pageableQuerierGlobal.ListGlobalWithAdditionalConditions(ctx, pageSize, cursor, externalNameColumn, &entityCollection, conditions)
   174  	if err != nil {
   175  		return nil, errors.Wrap(err, "while listing tenants from DB")
   176  	}
   177  
   178  	items := r.multipleFromEntities(entityCollection)
   179  
   180  	return &model.BusinessTenantMappingPage{
   181  		Data:       items,
   182  		TotalCount: totalCount,
   183  		PageInfo:   page,
   184  	}, nil
   185  }
   186  
   187  // ListByExternalTenants retrieves all tenants with matching external ID from the Compass storage in chunks.
   188  func (r *pgRepository) ListByExternalTenants(ctx context.Context, externalTenantIDs []string) ([]*model.BusinessTenantMapping, error) {
   189  	tenants := make([]*model.BusinessTenantMapping, 0)
   190  
   191  	for len(externalTenantIDs) > 0 {
   192  		chunkSize := int(math.Min(float64(len(externalTenantIDs)), float64(maxParameterChunkSize)))
   193  		tenantsChunk := externalTenantIDs[:chunkSize]
   194  		tenantsFromDB, err := r.listByExternalTenantIDs(ctx, tenantsChunk)
   195  		if err != nil {
   196  			return nil, err
   197  		}
   198  		externalTenantIDs = externalTenantIDs[chunkSize:]
   199  		tenants = append(tenants, tenantsFromDB...)
   200  	}
   201  
   202  	return tenants, nil
   203  }
   204  
   205  func (r *pgRepository) listByExternalTenantIDs(ctx context.Context, externalTenant []string) ([]*model.BusinessTenantMapping, error) {
   206  	var entityCollection tenant.EntityCollection
   207  
   208  	conditions := repo.Conditions{
   209  		repo.NewInConditionForStringValues(externalTenantColumn, externalTenant)}
   210  
   211  	if err := r.listerGlobal.ListGlobal(ctx, &entityCollection, conditions...); err != nil {
   212  		return nil, err
   213  	}
   214  
   215  	return r.multipleFromEntities(entityCollection), nil
   216  }
   217  
   218  // Update updates the values of tenant with matching internal, and external IDs.
   219  func (r *pgRepository) Update(ctx context.Context, model *model.BusinessTenantMapping) error {
   220  	if model == nil {
   221  		return apperrors.NewInternalError("model can not be empty")
   222  	}
   223  
   224  	tntFromDB, err := r.Get(ctx, model.ID)
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	entity := r.conv.ToEntity(model)
   230  
   231  	if err := r.updaterGlobal.UpdateSingleGlobal(ctx, entity); err != nil {
   232  		return err
   233  	}
   234  
   235  	if tntFromDB.Parent != model.Parent {
   236  		for topLevelEntity := range resource.TopLevelEntities {
   237  			if _, ok := topLevelEntity.IgnoredTenantAccessTable(); ok {
   238  				log.C(ctx).Debugf("top level entity %s does not need a tenant access table", topLevelEntity)
   239  				continue
   240  			}
   241  
   242  			m2mTable, ok := topLevelEntity.TenantAccessTable()
   243  			if !ok {
   244  				return errors.Errorf("top level entity %s does not have tenant access table", topLevelEntity)
   245  			}
   246  
   247  			tenantAccesses := repo.TenantAccessCollection{}
   248  
   249  			tenantAccessLister := repo.NewListerGlobal(resource.TenantAccess, m2mTable, repo.M2MColumns)
   250  			if err := tenantAccessLister.ListGlobal(ctx, &tenantAccesses, repo.NewEqualCondition(repo.M2MTenantIDColumn, model.ID), repo.NewEqualCondition(repo.M2MOwnerColumn, true)); err != nil {
   251  				return errors.Wrapf(err, "while listing tenant access records for tenant with id %s", model.ID)
   252  			}
   253  
   254  			for _, ta := range tenantAccesses {
   255  				tenantAccess := &repo.TenantAccess{
   256  					TenantID:   model.Parent,
   257  					ResourceID: ta.ResourceID,
   258  					Owner:      true,
   259  				}
   260  				if err := repo.CreateTenantAccessRecursively(ctx, m2mTable, tenantAccess); err != nil {
   261  					return errors.Wrapf(err, "while creating tenant acccess record for resource %s for parent %s of tenant %s", ta.ResourceID, model.Parent, model.ID)
   262  				}
   263  			}
   264  
   265  			if len(tntFromDB.Parent) > 0 && len(tenantAccesses) > 0 {
   266  				resourceIDs := make([]string, 0, len(tenantAccesses))
   267  				for _, ta := range tenantAccesses {
   268  					resourceIDs = append(resourceIDs, ta.ResourceID)
   269  				}
   270  
   271  				if err := repo.DeleteTenantAccessRecursively(ctx, m2mTable, tntFromDB.Parent, resourceIDs); err != nil {
   272  					return errors.Wrapf(err, "while deleting tenant accesses for the old parent %s of the tenant %s", tntFromDB.Parent, model.ID)
   273  				}
   274  			}
   275  		}
   276  	}
   277  
   278  	return nil
   279  }
   280  
   281  // DeleteByExternalTenant removes a tenant with matching external ID from the Compass storage.
   282  // It also deletes all the accesses for resources that the tenant is owning for its parents.
   283  func (r *pgRepository) DeleteByExternalTenant(ctx context.Context, externalTenant string) error {
   284  	tnt, err := r.GetByExternalTenant(ctx, externalTenant)
   285  	if err != nil {
   286  		if apperrors.IsNotFoundError(err) {
   287  			return nil
   288  		}
   289  		return err
   290  	}
   291  
   292  	for topLevelEntity, topLevelEntityTable := range resource.TopLevelEntities {
   293  		if _, ok := topLevelEntity.IgnoredTenantAccessTable(); ok {
   294  			log.C(ctx).Debugf("top level entity %s does not need a tenant access table", topLevelEntity)
   295  			continue
   296  		}
   297  
   298  		m2mTable, ok := topLevelEntity.TenantAccessTable()
   299  		if !ok {
   300  			return errors.Errorf("top level entity %s does not have tenant access table", topLevelEntity)
   301  		}
   302  
   303  		tenantAccesses := repo.TenantAccessCollection{}
   304  
   305  		tenantAccessLister := repo.NewListerGlobal(resource.TenantAccess, m2mTable, repo.M2MColumns)
   306  		if err := tenantAccessLister.ListGlobal(ctx, &tenantAccesses, repo.NewEqualCondition(repo.M2MTenantIDColumn, tnt.ID), repo.NewEqualCondition(repo.M2MOwnerColumn, true)); err != nil {
   307  			return errors.Wrapf(err, "while listing tenant access records for tenant with id %s", tnt.ID)
   308  		}
   309  
   310  		if len(tenantAccesses) > 0 {
   311  			resourceIDs := make([]string, 0, len(tenantAccesses))
   312  			for _, ta := range tenantAccesses {
   313  				resourceIDs = append(resourceIDs, ta.ResourceID)
   314  			}
   315  
   316  			deleter := repo.NewDeleterGlobal(topLevelEntity, topLevelEntityTable)
   317  			if err := deleter.DeleteManyGlobal(ctx, repo.Conditions{repo.NewInConditionForStringValues("id", resourceIDs)}); err != nil {
   318  				return errors.Wrapf(err, "while deleting resources owned by tenant %s", tnt.ID)
   319  			}
   320  		}
   321  	}
   322  
   323  	conditions := repo.Conditions{
   324  		repo.NewEqualCondition(externalTenantColumn, externalTenant),
   325  	}
   326  
   327  	return r.deleterGlobal.DeleteManyGlobal(ctx, conditions)
   328  }
   329  
   330  // GetLowestOwnerForResource returns the lowest tenant in the hierarchy that is owner of a given resource.
   331  func (r *pgRepository) GetLowestOwnerForResource(ctx context.Context, resourceType resource.Type, objectID string) (string, error) {
   332  	rawStmt := `(SELECT {{ .m2mTenantID }} FROM {{ .m2mTable }} ta WHERE ta.{{ .m2mID }} = ? AND ta.{{ .owner }} = true` +
   333  		` AND (NOT EXISTS(SELECT 1 FROM {{ .tenantsTable }} WHERE {{ .parent }} = ta.{{ .m2mTenantID }})` + // the tenant has no children
   334  		` OR (NOT EXISTS(SELECT 1 FROM {{ .m2mTable }} ta2` +
   335  		` WHERE ta2.{{ .m2mID }} = ? AND ta2.{{ .owner }} = true AND` +
   336  		` ta2.{{ .m2mTenantID }} IN (SELECT {{ .id }} FROM {{ .tenantsTable }} WHERE {{ .parent }} = ta.{{ .m2mTenantID }})))))` // there is no child that has owner access
   337  
   338  	t, err := template.New("").Parse(rawStmt)
   339  	if err != nil {
   340  		return "", err
   341  	}
   342  
   343  	m2mTable, ok := resourceType.TenantAccessTable()
   344  	if !ok {
   345  		return "", errors.Errorf("No tenant access table for %s", resourceType)
   346  	}
   347  
   348  	data := map[string]string{
   349  		"m2mTenantID":  repo.M2MTenantIDColumn,
   350  		"m2mTable":     m2mTable,
   351  		"m2mID":        repo.M2MResourceIDColumn,
   352  		"owner":        repo.M2MOwnerColumn,
   353  		"tenantsTable": tableName,
   354  		"parent":       parentColumn,
   355  		"id":           idColumn,
   356  	}
   357  
   358  	res := new(bytes.Buffer)
   359  	if err = t.Execute(res, data); err != nil {
   360  		return "", errors.Wrapf(err, "while executing template")
   361  	}
   362  
   363  	stmt := res.String()
   364  	stmt = sqlx.Rebind(sqlx.DOLLAR, stmt)
   365  
   366  	persist, err := persistence.FromCtx(ctx)
   367  	if err != nil {
   368  		return "", err
   369  	}
   370  
   371  	log.C(ctx).Debugf("Executing DB query: %s", stmt)
   372  
   373  	dest := struct {
   374  		TenantID string `db:"tenant_id"`
   375  	}{}
   376  
   377  	if err := persist.GetContext(ctx, &dest, stmt, objectID, objectID); err != nil {
   378  		return "", persistence.MapSQLError(ctx, err, resource.TenantAccess, resource.Get, "while getting lowest tenant from %s table for resource %s with id %s", m2mTable, resourceType, objectID)
   379  	}
   380  
   381  	return dest.TenantID, nil
   382  }
   383  
   384  // GetCustomerIDParentRecursively gets the top parent external ID (customer_id) for a given tenant
   385  func (r *pgRepository) GetCustomerIDParentRecursively(ctx context.Context, tenantID string) (string, error) {
   386  	recursiveQuery := `WITH RECURSIVE parents AS
   387                     (SELECT t1.id, t1.parent, t1.external_tenant, t1.type
   388                      FROM business_tenant_mappings t1
   389                      WHERE id = $1
   390                      UNION ALL
   391                      SELECT t2.id, t2.parent, t2.external_tenant, t2.type
   392                      FROM business_tenant_mappings t2
   393                               INNER JOIN parents p on p.parent = t2.id)
   394  			SELECT external_tenant, type FROM parents WHERE parent is null`
   395  
   396  	persist, err := persistence.FromCtx(ctx)
   397  	if err != nil {
   398  		return "", err
   399  	}
   400  
   401  	log.C(ctx).Debugf("Executing DB query: %s", recursiveQuery)
   402  
   403  	dest := struct {
   404  		ExternalCustomerTenant string `db:"external_tenant"`
   405  		Type                   string `db:"type"`
   406  	}{}
   407  
   408  	if err := persist.GetContext(ctx, &dest, recursiveQuery, tenantID); err != nil {
   409  		return "", persistence.MapSQLError(ctx, err, resource.Tenant, resource.Get, "while getting parent external customer ID for internal tenant: %q", tenantID)
   410  	}
   411  
   412  	if dest.Type != tenant.TypeToStr(tenant.Customer) {
   413  		return "", nil
   414  	}
   415  
   416  	if dest.ExternalCustomerTenant == "" {
   417  		return "", errors.Errorf("external parent customer ID for internal tenant ID: %s can not be empty", tenantID)
   418  	}
   419  
   420  	return dest.ExternalCustomerTenant, nil
   421  }
   422  
   423  func (r *pgRepository) ListBySubscribedRuntimes(ctx context.Context) ([]*model.BusinessTenantMapping, error) {
   424  	var entityCollection tenant.EntityCollection
   425  
   426  	conditions := repo.Conditions{
   427  		repo.NewInConditionForSubQuery(
   428  			idColumn, "SELECT DISTINCT tenant_id from tenant_runtime_contexts", []interface{}{}),
   429  		repo.NewEqualCondition(typeColumn, tenant.Subaccount),
   430  	}
   431  
   432  	if err := r.listerGlobal.ListGlobal(ctx, &entityCollection, conditions...); err != nil {
   433  		return nil, err
   434  	}
   435  
   436  	return r.multipleFromEntities(entityCollection), nil
   437  }
   438  
   439  // ListByParentAndType list tenants by parent ID and tenant.Type
   440  func (r *pgRepository) ListByParentAndType(ctx context.Context, parentID string, tenantType tenant.Type) ([]*model.BusinessTenantMapping, error) {
   441  	var entityCollection tenant.EntityCollection
   442  
   443  	conditions := repo.Conditions{
   444  		repo.NewEqualCondition(parentColumn, parentID),
   445  		repo.NewEqualCondition(typeColumn, tenantType),
   446  	}
   447  
   448  	if err := r.listerGlobal.ListGlobal(ctx, &entityCollection, conditions...); err != nil {
   449  		return nil, err
   450  	}
   451  
   452  	return r.multipleFromEntities(entityCollection), nil
   453  }
   454  
   455  // ListByType list tenants by tenant.Type
   456  func (r *pgRepository) ListByType(ctx context.Context, tenantType tenant.Type) ([]*model.BusinessTenantMapping, error) {
   457  	var entityCollection tenant.EntityCollection
   458  
   459  	conditions := repo.Conditions{
   460  		repo.NewEqualCondition(typeColumn, tenantType),
   461  	}
   462  
   463  	if err := r.listerGlobal.ListGlobal(ctx, &entityCollection, conditions...); err != nil {
   464  		return nil, err
   465  	}
   466  
   467  	return r.multipleFromEntities(entityCollection), nil
   468  }
   469  
   470  func (r *pgRepository) multipleFromEntities(entities tenant.EntityCollection) []*model.BusinessTenantMapping {
   471  	items := make([]*model.BusinessTenantMapping, 0, len(entities))
   472  
   473  	for _, entity := range entities {
   474  		tmModel := r.conv.FromEntity(&entity)
   475  		items = append(items, tmModel)
   476  	}
   477  
   478  	return items
   479  }