github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/repo/create.go (about) 1 package repo 2 3 import ( 4 "context" 5 "fmt" 6 "strings" 7 "time" 8 9 "github.com/kyma-incubator/compass/components/director/pkg/apperrors" 10 "github.com/kyma-incubator/compass/components/director/pkg/graphql" 11 "github.com/kyma-incubator/compass/components/director/pkg/log" 12 "github.com/kyma-incubator/compass/components/director/pkg/operation" 13 "github.com/pkg/errors" 14 15 "github.com/kyma-incubator/compass/components/director/pkg/resource" 16 17 "github.com/kyma-incubator/compass/components/director/pkg/persistence" 18 ) 19 20 // Creator is an interface for creating entities with externally managed tenant accesses (m2m table or view) 21 type Creator interface { 22 Create(ctx context.Context, resourceType resource.Type, tenant string, dbEntity interface{}) error 23 } 24 25 // CreatorGlobal is an interface for creating global entities without tenant or entities with tenant embedded in them. 26 type CreatorGlobal interface { 27 Create(ctx context.Context, dbEntity interface{}) error 28 } 29 30 type universalCreator struct { 31 tableName string 32 columns []string 33 matcherColumns []string 34 ownerCheckRequired bool 35 } 36 37 // NewCreator is a constructor for Creator about entities with externally managed tenant accesses (m2m table or view) 38 func NewCreator(tableName string, columns []string) Creator { 39 return &universalCreator{ 40 tableName: tableName, 41 columns: columns, 42 ownerCheckRequired: true, 43 } 44 } 45 46 // NewCreatorWithMatchingColumns is a constructor for Creator about entities with externally managed tenant accesses (m2m table or view). 47 // In addition, matcherColumns can be added in order to identify already existing top-level entities and prevent their duplicate creation. 48 func NewCreatorWithMatchingColumns(tableName string, columns []string, matcherColumns []string) Creator { 49 return &universalCreator{ 50 tableName: tableName, 51 columns: columns, 52 matcherColumns: matcherColumns, 53 ownerCheckRequired: true, 54 } 55 } 56 57 // NewCreatorGlobal is a constructor for GlobalCreator about entities without tenant or entities with tenant embedded in them. 58 func NewCreatorGlobal(resourceType resource.Type, tableName string, columns []string) CreatorGlobal { 59 return &globalCreator{ 60 resourceType: resourceType, 61 tableName: tableName, 62 columns: columns, 63 } 64 } 65 66 // Create is a method for creating entities with externally managed tenant accesses (m2m table or view) 67 // In case of top level entity it creates tenant access record in the m2m table as well. 68 // In case of child entity first it checks if the calling tenant has access to the parent entity and then creates the child entity. 69 func (c *universalCreator) Create(ctx context.Context, resourceType resource.Type, tenant string, dbEntity interface{}) error { 70 if dbEntity == nil { 71 return apperrors.NewInternalError("item cannot be nil") 72 } 73 74 var id string 75 if identifiable, ok := dbEntity.(Identifiable); ok { 76 id = identifiable.GetID() 77 } 78 79 if len(id) == 0 { 80 return apperrors.NewInternalError("id cannot be empty, check if the entity implements Identifiable") 81 } 82 83 entity, ok := dbEntity.(Entity) 84 if ok && entity.GetCreatedAt().IsZero() { // This zero check is needed to mock the Create tests 85 now := time.Now() 86 entity.SetCreatedAt(now) 87 entity.SetReady(true) 88 entity.SetError(NewValidNullableString("")) 89 90 if operation.ModeFromCtx(ctx) == graphql.OperationModeAsync { 91 entity.SetReady(false) 92 } 93 94 dbEntity = entity 95 } 96 97 if resourceType.IsTopLevel() { 98 return c.createTopLevelEntity(ctx, id, tenant, dbEntity, resourceType) 99 } 100 101 return c.createChildEntity(ctx, tenant, dbEntity, resourceType) 102 } 103 104 func (c *universalCreator) createTopLevelEntity(ctx context.Context, id string, tenant string, dbEntity interface{}, resourceType resource.Type) error { 105 persist, err := persistence.FromCtx(ctx) 106 if err != nil { 107 return err 108 } 109 110 values := make([]string, 0, len(c.columns)) 111 for _, c := range c.columns { 112 values = append(values, fmt.Sprintf(":%s", c)) 113 } 114 115 stmt := fmt.Sprintf("INSERT INTO %s ( %s ) VALUES ( %s )", c.tableName, strings.Join(c.columns, ", "), strings.Join(values, ", ")) 116 if len(c.matcherColumns) > 0 { 117 stmt = fmt.Sprintf("%s ON CONFLICT ( %s ) DO NOTHING", stmt, strings.Join(c.matcherColumns, ", ")) 118 } 119 120 log.C(ctx).Debugf("Executing DB query: %s", stmt) 121 res, err := persist.NamedExecContext(ctx, stmt, dbEntity) 122 if err != nil { 123 return persistence.MapSQLError(ctx, err, resourceType, resource.Create, "while inserting row to '%s' table", c.tableName) 124 } 125 126 affected, err := res.RowsAffected() 127 if err != nil { 128 return errors.Wrap(err, "while checking affected rows") 129 } 130 131 if affected == 0 { 132 log.C(ctx).Warnf("%s top level entity already exists based on matcher columns [%s]. Returning not-unique error for calling tenant %s...", resourceType, strings.Join(c.matcherColumns, ", "), tenant) 133 return apperrors.NewNotUniqueError(resourceType) 134 } 135 136 m2mTable, ok := resourceType.TenantAccessTable() 137 if !ok { 138 return errors.Errorf("entity %s does not have access table", resourceType) 139 } 140 141 return CreateTenantAccessRecursively(ctx, m2mTable, &TenantAccess{ 142 TenantID: tenant, 143 ResourceID: id, 144 Owner: true, 145 }) 146 } 147 148 func (c *universalCreator) createChildEntity(ctx context.Context, tenant string, dbEntity interface{}, resourceType resource.Type) error { 149 if err := c.checkParentAccess(ctx, tenant, dbEntity, resourceType); err != nil { 150 return apperrors.NewUnauthorizedError(fmt.Sprintf("Tenant %s does not have access to the parent of the currently created %s: %v", tenant, resourceType, err)) 151 } 152 153 persist, err := persistence.FromCtx(ctx) 154 if err != nil { 155 return err 156 } 157 158 values := make([]string, 0, len(c.columns)) 159 for _, c := range c.columns { 160 values = append(values, fmt.Sprintf(":%s", c)) 161 } 162 163 insertStmt := fmt.Sprintf("INSERT INTO %s ( %s ) VALUES ( %s )", c.tableName, strings.Join(c.columns, ", "), strings.Join(values, ", ")) 164 165 log.C(ctx).Infof("Executing DB query: %s", insertStmt) 166 _, err = persist.NamedExecContext(ctx, insertStmt, dbEntity) 167 168 return persistence.MapSQLError(ctx, err, resourceType, resource.Create, "while inserting row to '%s' table", c.tableName) 169 } 170 171 func (c *universalCreator) checkParentAccess(ctx context.Context, tenant string, dbEntity interface{}, resourceType resource.Type) error { 172 var parentID string 173 var parentResourceType resource.Type 174 if childEntity, ok := dbEntity.(ChildEntity); ok { 175 parentResourceType, parentID = childEntity.GetParent(resourceType) 176 } 177 178 if len(parentID) == 0 || len(parentResourceType) == 0 { 179 return errors.Errorf("unknown parent for entity type %s", resourceType) 180 } 181 182 if _, ok := parentResourceType.IgnoredTenantAccessTable(); ok { 183 log.C(ctx).Debugf("parent entity %s does not need a tenant access table", parentResourceType) 184 return nil 185 } 186 187 tenantAccessResourceType := resource.TenantAccess 188 parentAccessTable, ok := parentResourceType.TenantAccessTable() 189 if !ok { 190 log.C(ctx).Infof("parent entity %s does not have access table. Will check if it has table with embedded tenant...", parentResourceType) 191 var ok bool 192 parentAccessTable, ok = parentResourceType.EmbeddedTenantTable() 193 if !ok { 194 return errors.Errorf("parent entity %s does not have access table or table with embedded tenant", parentResourceType) 195 } 196 tenantAccessResourceType = parentResourceType 197 } 198 199 conditions := Conditions{NewEqualCondition(M2MResourceIDColumn, parentID)} 200 if c.ownerCheckRequired && ok { 201 conditions = append(conditions, NewEqualCondition(M2MOwnerColumn, true)) 202 } 203 204 exister := NewExistQuerierWithEmbeddedTenant(parentAccessTable, M2MTenantIDColumn) 205 exists, err := exister.Exists(ctx, tenantAccessResourceType, tenant, conditions) 206 if err != nil { 207 return errors.Wrap(err, "while checking for tenant access") 208 } 209 210 if !exists { 211 return errors.Errorf("tenant %s does not have access to the parent resource %s with ID %s", tenant, parentResourceType, parentID) 212 } 213 214 return nil 215 } 216 217 type globalCreator struct { 218 tableName string 219 resourceType resource.Type 220 columns []string 221 } 222 223 // Create creates a new global entity or entity with embedded tenant in it. 224 func (c *globalCreator) Create(ctx context.Context, dbEntity interface{}) error { 225 if dbEntity == nil { 226 return apperrors.NewInternalError("item cannot be nil") 227 } 228 229 persist, err := persistence.FromCtx(ctx) 230 if err != nil { 231 return err 232 } 233 234 values := make([]string, 0, len(c.columns)) 235 for _, c := range c.columns { 236 values = append(values, fmt.Sprintf(":%s", c)) 237 } 238 239 entity, ok := dbEntity.(Entity) 240 if ok && entity.GetCreatedAt().IsZero() { // This zero check is needed to mock the Create tests 241 now := time.Now() 242 entity.SetCreatedAt(now) 243 entity.SetReady(true) 244 entity.SetError(NewValidNullableString("")) 245 246 if operation.ModeFromCtx(ctx) == graphql.OperationModeAsync { 247 entity.SetReady(false) 248 } 249 250 dbEntity = entity 251 } 252 253 stmt := fmt.Sprintf("INSERT INTO %s ( %s ) VALUES ( %s )", c.tableName, strings.Join(c.columns, ", "), strings.Join(values, ", ")) 254 255 log.C(ctx).Debugf("Executing DB query: %s", stmt) 256 _, err = persist.NamedExecContext(ctx, stmt, dbEntity) 257 258 return persistence.MapSQLError(ctx, err, c.resourceType, resource.Create, "while inserting row to '%s' table", c.tableName) 259 }