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

     1  package repo
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/kyma-incubator/compass/components/director/pkg/log"
     9  
    10  	"github.com/kyma-incubator/compass/components/director/pkg/resource"
    11  
    12  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    13  
    14  	"github.com/kyma-incubator/compass/components/director/pkg/persistence"
    15  
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  // Deleter is an interface for deleting tenant scoped entities with either externally managed tenant accesses (m2m table or view) or embedded tenant in them.
    20  type Deleter interface {
    21  	DeleteOne(ctx context.Context, resourceType resource.Type, tenant string, conditions Conditions) error
    22  	DeleteMany(ctx context.Context, resourceType resource.Type, tenant string, conditions Conditions) error
    23  }
    24  
    25  // DeleterGlobal is an interface for deleting global entities.
    26  type DeleterGlobal interface {
    27  	DeleteOneGlobal(ctx context.Context, conditions Conditions) error
    28  	DeleteManyGlobal(ctx context.Context, conditions Conditions) error
    29  }
    30  
    31  // DeleterConditionTree deletes tenant scoped entities matching the provided condition tree with tenant isolation.
    32  type DeleterConditionTree interface {
    33  	DeleteConditionTree(ctx context.Context, resourceType resource.Type, tenant string, conditionTree *ConditionTree) error
    34  }
    35  
    36  type universalDeleter struct {
    37  	tableName    string
    38  	resourceType resource.Type
    39  	tenantColumn *string
    40  }
    41  
    42  // NewDeleter is a constructor for Deleter about entities with externally managed tenant accesses (m2m table or view)
    43  func NewDeleter(tableName string) Deleter {
    44  	return &universalDeleter{tableName: tableName}
    45  }
    46  
    47  // NewDeleterConditionTree is a constructor for Deleter about entities with externally managed tenant accesses (m2m table or view)
    48  func NewDeleterConditionTree(tableName string) DeleterConditionTree {
    49  	return &universalDeleter{tableName: tableName}
    50  }
    51  
    52  // NewDeleterConditionTreeWithEmbeddedTenant is a constructor for Deleter about entities with externally managed tenant accesses (m2m table or view)
    53  func NewDeleterConditionTreeWithEmbeddedTenant(tableName string, tenantColumn string) DeleterConditionTree {
    54  	return &universalDeleter{tableName: tableName, tenantColumn: &tenantColumn}
    55  }
    56  
    57  // NewDeleterWithEmbeddedTenant is a constructor for Deleter about entities with tenant embedded in them.
    58  func NewDeleterWithEmbeddedTenant(tableName string, tenantColumn string) Deleter {
    59  	return &universalDeleter{tableName: tableName, tenantColumn: &tenantColumn}
    60  }
    61  
    62  // NewDeleterGlobal is a constructor for DeleterGlobal about global entities.
    63  func NewDeleterGlobal(resourceType resource.Type, tableName string) DeleterGlobal {
    64  	return &universalDeleter{tableName: tableName, resourceType: resourceType}
    65  }
    66  
    67  // DeleteOne deletes exactly one entity from the database if the calling tenant has owner access to it. It returns an error if more than one entity matches the provided conditions.
    68  // If the tenantColumn is configured the isolation is based on equal condition on tenantColumn.
    69  func (g *universalDeleter) DeleteOne(ctx context.Context, resourceType resource.Type, tenant string, conditions Conditions) error {
    70  	if tenant == "" {
    71  		return apperrors.NewTenantRequiredError()
    72  	}
    73  
    74  	if g.tenantColumn != nil {
    75  		conditions = append(Conditions{NewEqualCondition(*g.tenantColumn, tenant)}, conditions...)
    76  		return g.delete(ctx, resourceType, conditions, true)
    77  	}
    78  
    79  	tenantIsolation, err := NewTenantIsolationCondition(resourceType, tenant, true)
    80  	if err != nil {
    81  		return err
    82  	}
    83  
    84  	conditions = append(conditions, tenantIsolation)
    85  
    86  	return g.delete(ctx, resourceType, conditions, true)
    87  }
    88  
    89  // DeleteMany deletes all the entities that match the provided conditions from the database if the calling tenant has owner access to them.
    90  // If the tenantColumn is configured the isolation is based on equal condition on tenantColumn.
    91  func (g *universalDeleter) DeleteMany(ctx context.Context, resourceType resource.Type, tenant string, conditions Conditions) error {
    92  	if tenant == "" {
    93  		return apperrors.NewTenantRequiredError()
    94  	}
    95  
    96  	if g.tenantColumn != nil {
    97  		conditions = append(Conditions{NewEqualCondition(*g.tenantColumn, tenant)}, conditions...)
    98  		return g.delete(ctx, resourceType, conditions, false)
    99  	}
   100  
   101  	tenantIsolation, err := NewTenantIsolationCondition(resourceType, tenant, true)
   102  	if err != nil {
   103  		return err
   104  	}
   105  
   106  	conditions = append(conditions, tenantIsolation)
   107  
   108  	return g.delete(ctx, resourceType, conditions, false)
   109  }
   110  
   111  // DeleteOneGlobal deletes exactly one entity from the database. It returns an error if more than one entity matches the provided conditions.
   112  func (g *universalDeleter) DeleteOneGlobal(ctx context.Context, conditions Conditions) error {
   113  	return g.delete(ctx, g.resourceType, conditions, true)
   114  }
   115  
   116  // DeleteManyGlobal deletes all the entities that match the provided conditions from the database.
   117  func (g *universalDeleter) DeleteManyGlobal(ctx context.Context, conditions Conditions) error {
   118  	return g.delete(ctx, g.resourceType, conditions, false)
   119  }
   120  
   121  // DeleteConditionTree lists tenant scoped entities matching the provided condition tree with tenant isolation.
   122  // If the tenantColumn is configured the isolation is based on equal condition on tenantColumn.
   123  // If the tenantColumn is not configured an entity with externally managed tenant accesses in m2m table / view is assumed.
   124  func (g *universalDeleter) DeleteConditionTree(ctx context.Context, resourceType resource.Type, tenant string, conditionTree *ConditionTree) error {
   125  	return g.treeConditionDeleterWithTenantScope(ctx, resourceType, tenant, NoLock, conditionTree)
   126  }
   127  
   128  func (g *universalDeleter) treeConditionDeleterWithTenantScope(ctx context.Context, resourceType resource.Type, tenant string, lockClause string, conditionTree *ConditionTree) error {
   129  	if tenant == "" {
   130  		return apperrors.NewTenantRequiredError()
   131  	}
   132  
   133  	if g.tenantColumn != nil {
   134  		conditions := And(&ConditionTree{Operand: NewEqualCondition(*g.tenantColumn, tenant)}, conditionTree)
   135  		return g.deleteWithConditionTree(ctx, resourceType, lockClause, conditions)
   136  	}
   137  
   138  	tenantIsolation, err := NewTenantIsolationCondition(resourceType, tenant, true)
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	conditions := And(&ConditionTree{Operand: tenantIsolation}, conditionTree)
   144  
   145  	return g.deleteWithConditionTree(ctx, resourceType, lockClause, conditions)
   146  }
   147  
   148  func (g *universalDeleter) deleteWithConditionTree(ctx context.Context, resourceType resource.Type, lockClause string, conditionTree *ConditionTree) error {
   149  	persist, err := persistence.FromCtx(ctx)
   150  	if err != nil {
   151  		return err
   152  	}
   153  
   154  	query, args := buildDeleteQueryFromTree(g.tableName, conditionTree, lockClause, true)
   155  
   156  	log.C(ctx).Debugf("Executing DB query: %s", query)
   157  	_, err = persist.ExecContext(ctx, query, args...)
   158  
   159  	return persistence.MapSQLError(ctx, err, resourceType, resource.List, "while fetching list of objects from '%s' table", g.tableName)
   160  }
   161  
   162  func buildDeleteQueryFromTree(tableName string, conditions *ConditionTree, lockClause string, isRebindingNeeded bool) (string, []interface{}) {
   163  	var stmtBuilder strings.Builder
   164  
   165  	stmtBuilder.WriteString(fmt.Sprintf("DELETE FROM %s", tableName))
   166  	var allArgs []interface{}
   167  	if conditions != nil {
   168  		stmtBuilder.WriteString(" WHERE ")
   169  		var subquery string
   170  		subquery, allArgs = conditions.BuildSubquery()
   171  		stmtBuilder.WriteString(subquery)
   172  	}
   173  
   174  	writeLockClause(&stmtBuilder, lockClause)
   175  
   176  	if isRebindingNeeded {
   177  		return getQueryFromBuilder(stmtBuilder), allArgs
   178  	}
   179  	return stmtBuilder.String(), allArgs
   180  }
   181  
   182  func (g *universalDeleter) delete(ctx context.Context, resourceType resource.Type, conditions Conditions, requireSingleRemoval bool) error {
   183  	persist, err := persistence.FromCtx(ctx)
   184  	if err != nil {
   185  		return err
   186  	}
   187  
   188  	var stmtBuilder strings.Builder
   189  
   190  	stmtBuilder.WriteString(fmt.Sprintf("DELETE FROM %s", g.tableName))
   191  
   192  	if len(conditions) > 0 {
   193  		stmtBuilder.WriteString(" WHERE")
   194  	}
   195  
   196  	err = writeEnumeratedConditions(&stmtBuilder, conditions)
   197  	if err != nil {
   198  		return errors.Wrap(err, "while writing enumerated conditions")
   199  	}
   200  	allArgs := getAllArgs(conditions)
   201  
   202  	query := getQueryFromBuilder(stmtBuilder)
   203  	log.C(ctx).Debugf("Executing DB query: %s", query)
   204  	res, err := persist.ExecContext(ctx, query, allArgs...)
   205  	if err = persistence.MapSQLError(ctx, err, resourceType, resource.Delete, "while deleting object from '%s' table", g.tableName); err != nil {
   206  		return err
   207  	}
   208  
   209  	isTenantScopedDelete := g.tenantColumn != nil
   210  
   211  	if requireSingleRemoval {
   212  		affected, err := res.RowsAffected()
   213  		if err != nil {
   214  			return errors.Wrap(err, "while checking affected rows")
   215  		}
   216  		if (affected == 0 && hasTenantIsolationCondition(conditions)) || (affected == 0 && isTenantScopedDelete) {
   217  			return apperrors.NewUnauthorizedError(apperrors.ShouldBeOwnerMsg)
   218  		}
   219  		if affected != 1 {
   220  			return apperrors.NewInternalError("delete should remove single row, but removed %d rows", affected)
   221  		}
   222  	}
   223  
   224  	return nil
   225  }
   226  
   227  func hasTenantIsolationCondition(conditions Conditions) bool {
   228  	for _, cond := range conditions {
   229  		if _, ok := cond.(*tenantIsolationCondition); ok {
   230  			return true
   231  		}
   232  	}
   233  	return false
   234  }
   235  
   236  // IDs keeps IDs retrieved from the Compass storage.
   237  type IDs []string
   238  
   239  // Len returns the length of the IDs
   240  func (i IDs) Len() int {
   241  	return len(i)
   242  }