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

     1  package formationmapping
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"net/http"
     7  
     8  	tenantpkg "github.com/kyma-incubator/compass/components/director/pkg/tenant"
     9  
    10  	"github.com/kyma-incubator/compass/components/director/pkg/correlation"
    11  
    12  	"github.com/kyma-incubator/compass/components/director/pkg/graphql"
    13  
    14  	"github.com/kyma-incubator/compass/components/director/internal/domain/formationassignment"
    15  	webhookclient "github.com/kyma-incubator/compass/components/director/pkg/webhook_client"
    16  
    17  	"github.com/gorilla/mux"
    18  	"github.com/kyma-incubator/compass/components/director/internal/domain/tenant"
    19  	"github.com/kyma-incubator/compass/components/director/internal/model"
    20  	"github.com/kyma-incubator/compass/components/director/pkg/consumer"
    21  	"github.com/kyma-incubator/compass/components/director/pkg/httputils"
    22  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    23  	"github.com/kyma-incubator/compass/components/director/pkg/persistence"
    24  	"github.com/pkg/errors"
    25  )
    26  
    27  const (
    28  	// FormationIDParam is formation URL path parameter placeholder
    29  	FormationIDParam = "ucl-formation-id"
    30  	// FormationAssignmentIDParam is formation assignment URL path parameter placeholder
    31  	FormationAssignmentIDParam = "ucl-assignment-id"
    32  )
    33  
    34  // FormationAssignmentService is responsible for the service-layer FormationAssignment operations
    35  //
    36  //go:generate mockery --name=FormationAssignmentService --output=automock --outpkg=automock --case=underscore --disable-version-string
    37  type FormationAssignmentService interface {
    38  	GetGlobalByIDAndFormationID(ctx context.Context, formationAssignmentID, formationID string) (*model.FormationAssignment, error)
    39  	GetReverseBySourceAndTarget(ctx context.Context, formationID, sourceID, targetID string) (*model.FormationAssignment, error)
    40  	ProcessFormationAssignmentPair(ctx context.Context, mappingPair *formationassignment.AssignmentMappingPairWithOperation) (bool, error)
    41  	Delete(ctx context.Context, id string) error
    42  	ListFormationAssignmentsForObjectID(ctx context.Context, formationID, objectID string) ([]*model.FormationAssignment, error)
    43  }
    44  
    45  //go:generate mockery --exported --name=formationAssignmentStatusService --output=automock --outpkg=automock --case=underscore --disable-version-string
    46  type formationAssignmentStatusService interface {
    47  	UpdateWithConstraints(ctx context.Context, fa *model.FormationAssignment, operation model.FormationOperation) error
    48  	SetAssignmentToErrorStateWithConstraints(ctx context.Context, assignment *model.FormationAssignment, errorMessage string, errorCode formationassignment.AssignmentErrorCode, state model.FormationAssignmentState, operation model.FormationOperation) error
    49  	DeleteWithConstraints(ctx context.Context, id string) error
    50  }
    51  
    52  // FormationAssignmentNotificationService represents the formation assignment notification service for generating notifications
    53  //
    54  //go:generate mockery --name=FormationAssignmentNotificationService --output=automock --outpkg=automock --case=underscore --disable-version-string
    55  type FormationAssignmentNotificationService interface {
    56  	GenerateFormationAssignmentNotification(ctx context.Context, formationAssignment *model.FormationAssignment, operation model.FormationOperation) (*webhookclient.FormationAssignmentNotificationRequest, error)
    57  }
    58  
    59  // formationService is responsible for the service-layer Formation operations
    60  //
    61  //go:generate mockery --exported --name=formationService --output=automock --outpkg=automock --case=underscore --disable-version-string
    62  type formationService interface {
    63  	UnassignFormation(ctx context.Context, tnt, objectID string, objectType graphql.FormationObjectType, formation model.Formation) (*model.Formation, error)
    64  	Get(ctx context.Context, id string) (*model.Formation, error)
    65  	GetGlobalByID(ctx context.Context, id string) (*model.Formation, error)
    66  	ResynchronizeFormationNotifications(ctx context.Context, formationID string) (*model.Formation, error)
    67  }
    68  
    69  //go:generate mockery --exported --name=formationStatusService --output=automock --outpkg=automock --case=underscore --disable-version-string
    70  type formationStatusService interface {
    71  	UpdateWithConstraints(ctx context.Context, formation *model.Formation, operation model.FormationOperation) error
    72  	SetFormationToErrorStateWithConstraints(ctx context.Context, formation *model.Formation, errorMessage string, errorCode formationassignment.AssignmentErrorCode, state model.FormationState, operation model.FormationOperation) error
    73  	DeleteFormationEntityAndScenariosWithConstraints(ctx context.Context, tnt string, formation *model.Formation) error
    74  }
    75  
    76  // RuntimeRepository is responsible for the repo-layer runtime operations
    77  //
    78  //go:generate mockery --name=RuntimeRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    79  type RuntimeRepository interface {
    80  	OwnerExists(ctx context.Context, tenant, id string) (bool, error)
    81  }
    82  
    83  // RuntimeContextRepository is responsible for the repo-layer runtime context operations
    84  //
    85  //go:generate mockery --name=RuntimeContextRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    86  type RuntimeContextRepository interface {
    87  	GetByID(ctx context.Context, tenant, id string) (*model.RuntimeContext, error)
    88  }
    89  
    90  // ApplicationRepository is responsible for the repo-layer application operations
    91  //
    92  //go:generate mockery --name=ApplicationRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    93  type ApplicationRepository interface {
    94  	GetByID(ctx context.Context, tenant, id string) (*model.Application, error)
    95  	OwnerExists(ctx context.Context, tenant, id string) (bool, error)
    96  }
    97  
    98  // TenantRepository is responsible for the repo-layer tenant operations
    99  //
   100  //go:generate mockery --name=TenantRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
   101  type TenantRepository interface {
   102  	Get(ctx context.Context, id string) (*model.BusinessTenantMapping, error)
   103  }
   104  
   105  // ApplicationTemplateRepository is responsible for the repo-layer application template operations
   106  //
   107  //go:generate mockery --name=ApplicationTemplateRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
   108  type ApplicationTemplateRepository interface {
   109  	Exists(ctx context.Context, id string) (bool, error)
   110  }
   111  
   112  // LabelRepository is responsible for the repo-layer label operations
   113  //
   114  //go:generate mockery --name=LabelRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
   115  type LabelRepository interface {
   116  	ListForGlobalObject(ctx context.Context, objectType model.LabelableObject, objectID string) (map[string]*model.Label, error)
   117  }
   118  
   119  // FormationRepository is responsible for the repo-layer formation operations
   120  //
   121  //go:generate mockery --name=FormationRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
   122  type FormationRepository interface {
   123  	GetGlobalByID(ctx context.Context, id string) (*model.Formation, error)
   124  }
   125  
   126  // FormationTemplateRepository is responsible for the repo-layer formation template operations
   127  //
   128  //go:generate mockery --name=FormationTemplateRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
   129  type FormationTemplateRepository interface {
   130  	Get(ctx context.Context, id string) (*model.FormationTemplate, error)
   131  }
   132  
   133  // ErrorResponse structure used for the JSON encoded response
   134  type ErrorResponse struct {
   135  	Message string `json:"error"`
   136  }
   137  
   138  // Authenticator struct containing all dependencies to verify the request authenticity
   139  type Authenticator struct {
   140  	transact                   persistence.Transactioner
   141  	faService                  FormationAssignmentService
   142  	runtimeRepo                RuntimeRepository
   143  	runtimeContextRepo         RuntimeContextRepository
   144  	appRepo                    ApplicationRepository
   145  	appTemplateRepo            ApplicationTemplateRepository
   146  	labelRepo                  LabelRepository
   147  	formationRepo              FormationRepository
   148  	formationTemplateRepo      FormationTemplateRepository
   149  	tenantRepo                 TenantRepository
   150  	consumerSubaccountLabelKey string
   151  }
   152  
   153  // NewFormationMappingAuthenticator creates a new Authenticator
   154  func NewFormationMappingAuthenticator(
   155  	transact persistence.Transactioner,
   156  	faService FormationAssignmentService,
   157  	runtimeRepo RuntimeRepository,
   158  	runtimeContextRepo RuntimeContextRepository,
   159  	appRepo ApplicationRepository,
   160  	appTemplateRepo ApplicationTemplateRepository,
   161  	labelRepo LabelRepository,
   162  	formationRepo FormationRepository,
   163  	formationTemplateRepo FormationTemplateRepository,
   164  	tenantRepo TenantRepository,
   165  	consumerSubaccountLabelKey string,
   166  ) *Authenticator {
   167  	return &Authenticator{
   168  		transact:                   transact,
   169  		faService:                  faService,
   170  		runtimeRepo:                runtimeRepo,
   171  		runtimeContextRepo:         runtimeContextRepo,
   172  		appRepo:                    appRepo,
   173  		appTemplateRepo:            appTemplateRepo,
   174  		labelRepo:                  labelRepo,
   175  		formationRepo:              formationRepo,
   176  		formationTemplateRepo:      formationTemplateRepo,
   177  		tenantRepo:                 tenantRepo,
   178  		consumerSubaccountLabelKey: consumerSubaccountLabelKey,
   179  	}
   180  }
   181  
   182  // FormationAssignmentHandler is a handler middleware that executes authorization check for the formation assignments requests reporting status
   183  func (a *Authenticator) FormationAssignmentHandler() func(next http.Handler) http.Handler {
   184  	return func(next http.Handler) http.Handler {
   185  		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   186  			ctx := r.Context()
   187  			correlationID := correlation.CorrelationIDFromContext(ctx)
   188  
   189  			if r.Method != http.MethodPatch {
   190  				w.WriteHeader(http.StatusMethodNotAllowed)
   191  				return
   192  			}
   193  
   194  			routeVars := mux.Vars(r)
   195  			formationID := routeVars[FormationIDParam]
   196  			formationAssignmentID := routeVars[FormationAssignmentIDParam]
   197  
   198  			if formationID == "" || formationAssignmentID == "" {
   199  				log.C(ctx).Errorf("Missing required parameters: %q or/and %q", FormationIDParam, FormationAssignmentIDParam)
   200  				respondWithError(ctx, w, http.StatusBadRequest, errors.New("Not all of the required parameters are provided"))
   201  				return
   202  			}
   203  
   204  			isAuthorized, statusCode, err := a.isFormationAssignmentAuthorized(ctx, formationAssignmentID, formationID)
   205  			if err != nil {
   206  				log.C(ctx).Error(err.Error())
   207  				respondWithError(ctx, w, statusCode, errors.Errorf("An unexpected error occurred while processing the request. X-Request-Id: %s", correlationID))
   208  				return
   209  			}
   210  
   211  			if !isAuthorized {
   212  				httputils.Respond(w, http.StatusUnauthorized)
   213  				return
   214  			}
   215  
   216  			next.ServeHTTP(w, r.WithContext(ctx))
   217  		})
   218  	}
   219  }
   220  
   221  // FormationHandler is a handler middleware that executes authorization check for the formation requests reporting status
   222  func (a *Authenticator) FormationHandler() func(next http.Handler) http.Handler {
   223  	return func(next http.Handler) http.Handler {
   224  		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   225  			ctx := r.Context()
   226  			correlationID := correlation.CorrelationIDFromContext(ctx)
   227  
   228  			if r.Method != http.MethodPatch {
   229  				w.WriteHeader(http.StatusMethodNotAllowed)
   230  				return
   231  			}
   232  
   233  			routeVars := mux.Vars(r)
   234  			formationID := routeVars[FormationIDParam]
   235  
   236  			if formationID == "" {
   237  				log.C(ctx).Errorf("Missing required parameters: %q", FormationIDParam)
   238  				respondWithError(ctx, w, http.StatusBadRequest, errors.New("Not all of the required parameters are provided"))
   239  				return
   240  			}
   241  
   242  			isAuthorized, statusCode, err := a.isFormationAuthorized(ctx, formationID)
   243  			if err != nil {
   244  				log.C(ctx).Error(err.Error())
   245  				respondWithError(ctx, w, statusCode, errors.Errorf("An unexpected error occurred while processing the request. X-Request-Id: %s", correlationID))
   246  				return
   247  			}
   248  
   249  			if !isAuthorized {
   250  				httputils.Respond(w, http.StatusUnauthorized)
   251  				return
   252  			}
   253  
   254  			next.ServeHTTP(w, r.WithContext(ctx))
   255  		})
   256  	}
   257  }
   258  
   259  func (a *Authenticator) isFormationAuthorized(ctx context.Context, formationID string) (bool, int, error) {
   260  	consumerInfo, err := consumer.LoadFromContext(ctx)
   261  	if err != nil {
   262  		return false, http.StatusInternalServerError, errors.Wrap(err, "while fetching consumer info from context")
   263  	}
   264  	consumerID := consumerInfo.ConsumerID
   265  	consumerType := consumerInfo.ConsumerType
   266  	log.C(ctx).Infof("Consumer with ID: %q and type: %q is trying to update formation with ID: %q", consumerID, consumerType, formationID)
   267  
   268  	tx, err := a.transact.Begin()
   269  	if err != nil {
   270  		return false, http.StatusInternalServerError, errors.Wrap(err, "Unable to establish connection with database")
   271  	}
   272  	defer a.transact.RollbackUnlessCommitted(ctx, tx)
   273  	ctx = persistence.SaveToContext(ctx, tx)
   274  
   275  	f, err := a.formationRepo.GetGlobalByID(ctx, formationID)
   276  	if err != nil {
   277  		return false, http.StatusInternalServerError, errors.Wrapf(err, "while getting formation with ID: %q globally", formationID)
   278  	}
   279  
   280  	ft, err := a.formationTemplateRepo.Get(ctx, f.FormationTemplateID)
   281  	if err != nil {
   282  		return false, http.StatusInternalServerError, errors.Wrapf(err, "while getting formation template with ID: %q", f.FormationTemplateID)
   283  	}
   284  
   285  	if err = tx.Commit(); err != nil {
   286  		log.C(ctx).Errorf("An error occurred while closing database transaction: %s", err.Error())
   287  		return false, http.StatusInternalServerError, errors.Wrap(err, "unable to finalize database operation")
   288  	}
   289  
   290  	for _, id := range ft.LeadingProductIDs {
   291  		if id == consumerID {
   292  			log.C(ctx).Infof("Consumer with ID: %q is contained in the leading product IDs list from formation template with ID: %q and name: %q", consumerID, ft.ID, ft.Name)
   293  			return true, http.StatusOK, nil
   294  		}
   295  	}
   296  
   297  	log.C(ctx).Infof("Consumer with ID: %q did not match any of the leading product IDs from formation template with ID: %q and name: %q", consumerID, ft.ID, ft.Name)
   298  	return false, http.StatusUnauthorized, nil
   299  }
   300  
   301  // isFormationAssignmentAuthorized verify through custom logic the caller is authorized to update the formation assignment status
   302  func (a *Authenticator) isFormationAssignmentAuthorized(ctx context.Context, formationAssignmentID, formationID string) (bool, int, error) {
   303  	consumerInfo, err := consumer.LoadFromContext(ctx)
   304  	if err != nil {
   305  		return false, http.StatusInternalServerError, errors.Wrap(err, "while fetching consumer info from context")
   306  	}
   307  	consumerID := consumerInfo.ConsumerID
   308  	consumerType := consumerInfo.ConsumerType
   309  
   310  	tx, err := a.transact.Begin()
   311  	if err != nil {
   312  		return false, http.StatusInternalServerError, errors.Wrap(err, "Unable to establish connection with database")
   313  	}
   314  	defer a.transact.RollbackUnlessCommitted(ctx, tx)
   315  	ctx = persistence.SaveToContext(ctx, tx)
   316  
   317  	fa, err := a.faService.GetGlobalByIDAndFormationID(ctx, formationAssignmentID, formationID)
   318  	if err != nil {
   319  		return false, http.StatusInternalServerError, err
   320  	}
   321  
   322  	if fa.TargetType == model.FormationAssignmentTypeApplication {
   323  		tnt, err := a.tenantRepo.Get(ctx, fa.TenantID)
   324  		if err != nil {
   325  			return false, http.StatusInternalServerError, errors.Wrapf(err, "while getting tenant with ID: %q", fa.TenantID)
   326  		}
   327  
   328  		if consumerType == consumer.BusinessIntegration && tnt.Type == tenantpkg.ResourceGroup {
   329  			if err := tx.Commit(); err != nil {
   330  				return false, http.StatusInternalServerError, errors.Wrap(err, "while closing database transaction")
   331  			}
   332  
   333  			log.C(ctx).Infof("The caller with ID: %s and type: %s is allowed to update formation assignments in tenants of type %s", consumerID, consumerType, tnt.Type)
   334  			return true, http.StatusOK, nil
   335  		}
   336  
   337  		app, err := a.appRepo.GetByID(ctx, fa.TenantID, fa.Target)
   338  		if err != nil {
   339  			return false, http.StatusInternalServerError, errors.Wrapf(err, "while getting application with ID: %q", fa.Target)
   340  		}
   341  		log.C(ctx).Infof("Successfully got application with ID: %q", fa.Target)
   342  
   343  		// If the consumer is integration system validate the formation assignment type is application that can be managed by the integration system caller
   344  		if consumerType == consumer.IntegrationSystem && app.IntegrationSystemID != nil && *app.IntegrationSystemID == consumerID {
   345  			if err := tx.Commit(); err != nil {
   346  				return false, http.StatusInternalServerError, errors.Wrap(err, "while closing database transaction")
   347  			}
   348  
   349  			log.C(ctx).Infof("The caller with ID: %q and type: %q manages the target of the formation assignment with ID: %q and type: %q that is being updated", consumerID, consumerType, fa.Target, fa.TargetType)
   350  			return true, http.StatusOK, nil
   351  		}
   352  
   353  		if app.ApplicationTemplateID != nil && *app.ApplicationTemplateID == consumerID {
   354  			if err := tx.Commit(); err != nil {
   355  				return false, http.StatusInternalServerError, errors.Wrap(err, "while closing database transaction")
   356  			}
   357  
   358  			log.C(ctx).Infof("The caller with ID: %q and type: %q is the parent of the target of the formation assignment with ID: %q and type: %q that is being updated", consumerID, consumerType, fa.Target, fa.TargetType)
   359  			return true, http.StatusOK, nil
   360  		}
   361  
   362  		consumerTenantPair, err := tenant.LoadTenantPairFromContext(ctx)
   363  		if err != nil {
   364  			return false, http.StatusInternalServerError, errors.Wrap(err, "while loading tenant pair from context")
   365  		}
   366  		consumerInternalTenantID := consumerTenantPair.InternalID
   367  		consumerExternalTenantID := consumerTenantPair.ExternalID
   368  
   369  		log.C(ctx).Infof("Tenant with internal ID: %q and external ID: %q for consumer with type: %q is trying to update formation assignment with ID: %q for formation with ID: %q about source: %q and source type: %q, and target: %q and target type: %q", consumerInternalTenantID, consumerExternalTenantID, consumerType, fa.ID, fa.FormationID, fa.Source, fa.SourceType, fa.Target, fa.TargetType)
   370  
   371  		// Verify if the caller has owner access to the target of the formation assignment with type application that is being updated
   372  		exists, err := a.appRepo.OwnerExists(ctx, consumerInternalTenantID, fa.Target)
   373  		if err != nil {
   374  			return false, http.StatusInternalServerError, errors.Wrapf(err, "an error occurred while verifying caller with internal tenant ID: %q has owner access to the target of the formation assignment with ID: %q and type: %q that is being updated", consumerInternalTenantID, fa.Target, fa.TargetType)
   375  		}
   376  
   377  		if exists {
   378  			if err := tx.Commit(); err != nil {
   379  				log.C(ctx).Errorf("An error occurred while closing database transaction: %s", err.Error())
   380  				return false, http.StatusInternalServerError, errors.Wrap(err, "Unable to finalize database operation")
   381  			}
   382  
   383  			log.C(ctx).Infof("The caller with internal tenant ID: %q has owner access to the target of the formation assignment with ID: %q and type: %q that is being updated", consumerInternalTenantID, fa.Target, fa.TargetType)
   384  			return true, http.StatusOK, nil
   385  		}
   386  		log.C(ctx).Warningf("The caller with internal tenant ID: %q has NOT direct owner access to the target of the formation assignment with ID: %q and type: %q that is being updated. Checking if the application is made through subscription...", consumerInternalTenantID, fa.Target, fa.TargetType)
   387  
   388  		// Validate if the application is registered through subscription and the caller has owner access to the application template of that application
   389  		return a.validateSubscriptionProvider(ctx, tx, app.ApplicationTemplateID, consumerExternalTenantID, fa.Target, string(fa.TargetType))
   390  	}
   391  
   392  	consumerTenantPair, err := tenant.LoadTenantPairFromContext(ctx)
   393  	if err != nil {
   394  		return false, http.StatusInternalServerError, errors.Wrap(err, "while loading tenant pair from context")
   395  	}
   396  	consumerInternalTenantID := consumerTenantPair.InternalID
   397  
   398  	log.C(ctx).Infof("Tenant with internal ID: %q and external ID: %q for consumer with type: %q is trying to update formation assignment with ID: %q for formation with ID: %q about source: %q and source type: %q, and target: %q and target type: %q", consumerInternalTenantID, consumerTenantPair.ExternalID, consumerType, fa.ID, fa.FormationID, fa.Source, fa.SourceType, fa.Target, fa.TargetType)
   399  	if fa.TargetType == model.FormationAssignmentTypeRuntime {
   400  		exists, err := a.runtimeRepo.OwnerExists(ctx, consumerInternalTenantID, fa.Target)
   401  		if err != nil {
   402  			return false, http.StatusUnauthorized, errors.Wrapf(err, "while verifying caller with internal tenant ID: %q has owner access to the target of the formation assignment with ID: %q and type: %q that is being updated", consumerInternalTenantID, fa.Target, fa.TargetType)
   403  		}
   404  
   405  		if exists {
   406  			if err := tx.Commit(); err != nil {
   407  				log.C(ctx).Errorf("An error occurred while closing database transaction: %s", err.Error())
   408  				return false, http.StatusInternalServerError, errors.Wrap(err, "Unable to finalize database operation")
   409  			}
   410  
   411  			log.C(ctx).Infof("The caller with internal tenant ID: %q has owner access to the target of the formation assignment with ID: %q and type: %q that is being updated", consumerInternalTenantID, fa.Target, fa.TargetType)
   412  			return true, http.StatusOK, nil
   413  		}
   414  
   415  		return false, http.StatusUnauthorized, nil
   416  	}
   417  
   418  	if fa.TargetType == model.FormationAssignmentTypeRuntimeContext {
   419  		rtmCtx, err := a.runtimeContextRepo.GetByID(ctx, fa.TenantID, fa.Target)
   420  		if err != nil {
   421  			return false, http.StatusInternalServerError, errors.Wrapf(err, "while getting runtime context with ID: %q", fa.Target)
   422  		}
   423  
   424  		exists, err := a.runtimeRepo.OwnerExists(ctx, consumerInternalTenantID, rtmCtx.RuntimeID)
   425  		if err != nil {
   426  			return false, http.StatusUnauthorized, errors.Wrapf(err, "while verifying caller with internal tenant ID: %q has owner access to the target's parent of the formation assignment with ID: %q and type: %q that is being updated", consumerInternalTenantID, fa.Target, fa.TargetType)
   427  		}
   428  
   429  		if exists {
   430  			if err := tx.Commit(); err != nil {
   431  				log.C(ctx).Errorf("An error occurred while closing database transaction: %s", err.Error())
   432  				return false, http.StatusInternalServerError, errors.Wrap(err, "Unable to finalize database operation")
   433  			}
   434  
   435  			log.C(ctx).Infof("The caller with internal tenant ID: %q has owner access to the target's parent of the formation assignment with ID: %q and type: %q that is being updated", consumerInternalTenantID, fa.Target, fa.TargetType)
   436  			return true, http.StatusOK, nil
   437  		}
   438  
   439  		return false, http.StatusUnauthorized, nil
   440  	}
   441  
   442  	if err := tx.Commit(); err != nil {
   443  		log.C(ctx).Errorf("An error occurred while closing database transaction: %s", err.Error())
   444  		return false, http.StatusInternalServerError, errors.Wrap(err, "Unable to finalize database operation")
   445  	}
   446  
   447  	return false, http.StatusUnauthorized, nil
   448  }
   449  
   450  // validateSubscriptionProvider validates if the application is registered through subscription and the caller has owner access to the application template
   451  func (a *Authenticator) validateSubscriptionProvider(ctx context.Context, tx persistence.PersistenceTx, appTemplateID *string, consumerExternalTenantID, faTarget, faTargetType string) (bool, int, error) {
   452  	if appTemplateID == nil || (appTemplateID != nil && *appTemplateID == "") {
   453  		log.C(ctx).Warning("Application template ID should not be nil or empty")
   454  		return false, http.StatusUnauthorized, nil
   455  	}
   456  
   457  	appTemplateExists, err := a.appTemplateRepo.Exists(ctx, *appTemplateID)
   458  	if err != nil {
   459  		return false, http.StatusUnauthorized, errors.Wrapf(err, "while checking application template existence for ID: %q", *appTemplateID)
   460  	}
   461  
   462  	if !appTemplateExists {
   463  		return false, http.StatusUnauthorized, errors.Wrapf(err, "application template with ID: %q doesn't exist", *appTemplateID)
   464  	}
   465  
   466  	labels, err := a.labelRepo.ListForGlobalObject(ctx, model.AppTemplateLabelableObject, *appTemplateID)
   467  	if err != nil {
   468  		return false, http.StatusInternalServerError, errors.Wrapf(err, "while getting labels for application template with ID: %q", *appTemplateID)
   469  	}
   470  
   471  	consumerSubaccountLbl, consumerSubaccountLblExists := labels[a.consumerSubaccountLabelKey]
   472  
   473  	if !consumerSubaccountLblExists {
   474  		return false, http.StatusUnauthorized, errors.Errorf("%q label should exist as part of the provider's application template", a.consumerSubaccountLabelKey)
   475  	}
   476  
   477  	consumerSubaccountLblValue, ok := consumerSubaccountLbl.Value.(string)
   478  	if !ok {
   479  		return false, http.StatusUnauthorized, errors.Errorf("unexpected type of %q label, expect: string, got: %T", a.consumerSubaccountLabelKey, consumerSubaccountLbl.Value)
   480  	}
   481  
   482  	if consumerExternalTenantID == consumerSubaccountLblValue {
   483  		if err := tx.Commit(); err != nil {
   484  			log.C(ctx).Errorf("An error occurred while closing database transaction: %s", err.Error())
   485  			return false, http.StatusInternalServerError, errors.Wrap(err, "Unable to finalize database operation")
   486  		}
   487  
   488  		log.C(ctx).Infof("The caller with external ID: %q has owner access to the target's parent of the formation assignment with ID: %q and type: %q that is being updated", consumerExternalTenantID, faTarget, faTargetType)
   489  		return true, http.StatusOK, nil
   490  	}
   491  
   492  	return false, http.StatusUnauthorized, nil
   493  }
   494  
   495  // respondWithError writes a http response using with the JSON encoded error wrapped in an ErrorResponse struct
   496  func respondWithError(ctx context.Context, w http.ResponseWriter, status int, err error) {
   497  	log.C(ctx).Errorf("Responding with error: %v", err)
   498  	w.Header().Add(httputils.HeaderContentTypeKey, httputils.ContentTypeApplicationJSON)
   499  	w.WriteHeader(status)
   500  	errorResponse := ErrorResponse{Message: err.Error()}
   501  	encodingErr := json.NewEncoder(w).Encode(errorResponse)
   502  	if encodingErr != nil {
   503  		log.C(ctx).WithError(err).Errorf("Failed to encode error response: %v", err)
   504  	}
   505  }