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 }