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 }