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  }