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

     1  package destination
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"strings"
    12  
    13  	"github.com/kyma-incubator/compass/components/director/pkg/tenant"
    14  	"github.com/tidwall/sjson"
    15  
    16  	"github.com/kyma-incubator/compass/components/director/internal/domain/client"
    17  	"github.com/kyma-incubator/compass/components/director/internal/domain/destination/destinationcreator"
    18  	"github.com/kyma-incubator/compass/components/director/internal/domain/formationconstraint/operators"
    19  	"github.com/kyma-incubator/compass/components/director/internal/model"
    20  	"github.com/kyma-incubator/compass/components/director/pkg/correlation"
    21  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    22  	"github.com/kyma-incubator/compass/components/director/pkg/persistence"
    23  	"github.com/pkg/errors"
    24  )
    25  
    26  const (
    27  	clientUserHeaderKey        = "CLIENT_USER"
    28  	contentTypeHeaderKey       = "Content-Type"
    29  	contentTypeApplicationJSON = "application/json;charset=UTF-8"
    30  	globalSubaccountLabelKey   = "global_subaccount_id"
    31  	regionLabelKey             = "region"
    32  	javaKeyStoreFileExtension  = ".jks"
    33  )
    34  
    35  //go:generate mockery --exported --name=applicationRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    36  type applicationRepository interface {
    37  	ListByScenariosNoPaging(ctx context.Context, tenant string, scenarios []string) ([]*model.Application, error)
    38  	GetByID(ctx context.Context, tenant, id string) (*model.Application, error)
    39  	OwnerExists(ctx context.Context, tenant, id string) (bool, error)
    40  }
    41  
    42  //go:generate mockery --exported --name=runtimeRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    43  type runtimeRepository interface {
    44  	OwnerExists(ctx context.Context, tenant, id string) (bool, error)
    45  }
    46  
    47  //go:generate mockery --exported --name=runtimeCtxRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    48  type runtimeCtxRepository interface {
    49  	GetByID(ctx context.Context, tenant, id string) (*model.RuntimeContext, error)
    50  }
    51  
    52  //go:generate mockery --exported --name=labelRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    53  type labelRepository interface {
    54  	GetByKey(ctx context.Context, tenant string, objectType model.LabelableObject, objectID, key string) (*model.Label, error)
    55  	ListForObject(ctx context.Context, tenant string, objectType model.LabelableObject, objectID string) (map[string]*model.Label, error)
    56  	ListForGlobalObject(ctx context.Context, objectType model.LabelableObject, objectID string) (map[string]*model.Label, error)
    57  }
    58  
    59  //go:generate mockery --exported --name=destinationRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    60  type destinationRepository interface {
    61  	DeleteByDestinationNameAndAssignmentID(ctx context.Context, destinationName, formationAssignmentID, tenantID string) error
    62  	ListByTenantIDAndAssignmentID(ctx context.Context, tenantID, formationAssignmentID string) ([]*model.Destination, error)
    63  	UpsertWithEmbeddedTenant(ctx context.Context, destination *model.Destination) error
    64  }
    65  
    66  //go:generate mockery --exported --name=tenantRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    67  type tenantRepository interface {
    68  	GetByExternalTenant(ctx context.Context, externalTenant string) (*model.BusinessTenantMapping, error)
    69  }
    70  
    71  // UIDService generates UUIDs for new entities
    72  //
    73  //go:generate mockery --name=UIDService --output=automock --outpkg=automock --case=underscore --disable-version-string
    74  type UIDService interface {
    75  	Generate() string
    76  }
    77  
    78  // Service consists of a service-level operations related to the destination entity
    79  type Service struct {
    80  	mtlsHTTPClient        *http.Client
    81  	destinationCreatorCfg *destinationcreator.Config
    82  	transact              persistence.Transactioner
    83  	applicationRepository applicationRepository
    84  	runtimeRepository     runtimeRepository
    85  	runtimeCtxRepository  runtimeCtxRepository
    86  	labelRepo             labelRepository
    87  	destinationRepo       destinationRepository
    88  	tenantRepo            tenantRepository
    89  	uidSvc                UIDService
    90  }
    91  
    92  // NewService creates a new Service
    93  func NewService(mtlsHTTPClient *http.Client, destinationCreatorCfg *destinationcreator.Config, transact persistence.Transactioner, applicationRepository applicationRepository, runtimeRepository runtimeRepository, runtimeCtxRepository runtimeCtxRepository, labelRepo labelRepository, destinationRepository destinationRepository, tenantRepository tenantRepository, uidSvc UIDService) *Service {
    94  	return &Service{
    95  		mtlsHTTPClient:        mtlsHTTPClient,
    96  		destinationCreatorCfg: destinationCreatorCfg,
    97  		transact:              transact,
    98  		applicationRepository: applicationRepository,
    99  		runtimeRepository:     runtimeRepository,
   100  		runtimeCtxRepository:  runtimeCtxRepository,
   101  		labelRepo:             labelRepo,
   102  		destinationRepo:       destinationRepository,
   103  		tenantRepo:            tenantRepository,
   104  		uidSvc:                uidSvc,
   105  	}
   106  }
   107  
   108  // CreateBasicCredentialDestinations is responsible to create a basic destination resource in the DB as well as in the remote destination service
   109  func (s *Service) CreateBasicCredentialDestinations(ctx context.Context, destinationDetails operators.Destination, basicAuthenticationCredentials operators.BasicAuthentication, formationAssignment *model.FormationAssignment, correlationIDs []string) (defaultStatusCode int, err error) {
   110  	subaccountID, err := s.validateDestinationSubaccount(ctx, destinationDetails.SubaccountID, formationAssignment)
   111  	if err != nil {
   112  		return defaultStatusCode, err
   113  	}
   114  
   115  	region, err := s.getRegionLabel(ctx, subaccountID)
   116  	if err != nil {
   117  		return defaultStatusCode, err
   118  	}
   119  
   120  	strURL, err := buildDestinationURL(s.destinationCreatorCfg.DestinationAPIConfig, region, subaccountID, "", false)
   121  	if err != nil {
   122  		return defaultStatusCode, errors.Wrapf(err, "while building destination URL")
   123  	}
   124  
   125  	reqBody, err := s.prepareBasicRequestBody(ctx, destinationDetails, basicAuthenticationCredentials, formationAssignment, correlationIDs)
   126  	if err != nil {
   127  		return defaultStatusCode, err
   128  	}
   129  
   130  	destinationName := destinationDetails.Name
   131  	log.C(ctx).Infof("Creating inbound basic destination with name: %q, subaccount ID: %q and assignment ID: %q in the destination service", destinationName, subaccountID, formationAssignment.ID)
   132  	_, statusCode, err := s.executeCreateRequest(ctx, strURL, reqBody, destinationName)
   133  	if err != nil {
   134  		return defaultStatusCode, errors.Wrapf(err, "while creating inbound basic destination with name: %q in the destination service", destinationName)
   135  	}
   136  
   137  	if statusCode == http.StatusConflict {
   138  		return statusCode, nil
   139  	}
   140  
   141  	if transactionErr := s.transaction(ctx, func(ctxWithTransact context.Context) error {
   142  		t, err := s.tenantRepo.GetByExternalTenant(ctx, subaccountID)
   143  		if err != nil {
   144  			return errors.Wrapf(err, "while getting tenant by external ID: %q", subaccountID)
   145  		}
   146  
   147  		destModel := &model.Destination{
   148  			ID:                    s.uidSvc.Generate(),
   149  			Name:                  reqBody.Name,
   150  			Type:                  reqBody.Type,
   151  			URL:                   reqBody.URL,
   152  			Authentication:        reqBody.AuthenticationType,
   153  			SubaccountID:          t.ID,
   154  			FormationAssignmentID: &formationAssignment.ID,
   155  		}
   156  
   157  		if err = s.destinationRepo.UpsertWithEmbeddedTenant(ctx, destModel); err != nil {
   158  			return errors.Wrapf(err, "while upserting basic destination with name: %q and assignment ID: %q in the DB", destinationName, formationAssignment.ID)
   159  		}
   160  		return nil
   161  	}); transactionErr != nil {
   162  		return defaultStatusCode, transactionErr
   163  	}
   164  
   165  	return statusCode, nil
   166  }
   167  
   168  // CreateDesignTimeDestinations is responsible to create so-called design time(destinationcreator.AuthTypeNoAuth) destination resource in the DB as well as in the remote destination service
   169  func (s *Service) CreateDesignTimeDestinations(ctx context.Context, destinationDetails operators.Destination, formationAssignment *model.FormationAssignment) (defaultStatusCode int, err error) {
   170  	subaccountID, err := s.validateDestinationSubaccount(ctx, destinationDetails.SubaccountID, formationAssignment)
   171  	if err != nil {
   172  		return defaultStatusCode, err
   173  	}
   174  
   175  	region, err := s.getRegionLabel(ctx, subaccountID)
   176  	if err != nil {
   177  		return defaultStatusCode, err
   178  	}
   179  
   180  	strURL, err := buildDestinationURL(s.destinationCreatorCfg.DestinationAPIConfig, region, subaccountID, "", false)
   181  	if err != nil {
   182  		return defaultStatusCode, errors.Wrapf(err, "while building destination URL")
   183  	}
   184  
   185  	destinationName := destinationDetails.Name
   186  	destReqBody := &destinationcreator.NoAuthRequestBody{
   187  		BaseDestinationRequestBody: destinationcreator.BaseDestinationRequestBody{
   188  			Name:                 destinationDetails.Name,
   189  			URL:                  destinationDetails.URL,
   190  			Type:                 destinationDetails.Type,
   191  			ProxyType:            destinationDetails.ProxyType,
   192  			AuthenticationType:   destinationDetails.Authentication,
   193  			AdditionalProperties: destinationDetails.AdditionalProperties,
   194  		},
   195  	}
   196  
   197  	if err := destReqBody.Validate(); err != nil {
   198  		return defaultStatusCode, errors.Wrapf(err, "while validating no authentication destination request body")
   199  	}
   200  
   201  	log.C(ctx).Infof("Creating design time destination with name: %q, subaccount ID: %q and assignment ID: %q in the destination service", destinationName, subaccountID, formationAssignment.ID)
   202  	_, statusCode, err := s.executeCreateRequest(ctx, strURL, destReqBody, destinationName)
   203  	if err != nil {
   204  		return defaultStatusCode, errors.Wrapf(err, "while creating design time destination with name: %q in the destination service", destinationName)
   205  	}
   206  
   207  	if statusCode == http.StatusConflict {
   208  		return statusCode, nil
   209  	}
   210  
   211  	t, err := s.tenantRepo.GetByExternalTenant(ctx, subaccountID)
   212  	if err != nil {
   213  		return defaultStatusCode, errors.Wrapf(err, "while getting tenant by external ID: %q", subaccountID)
   214  	}
   215  
   216  	destModel := &model.Destination{
   217  		ID:                    s.uidSvc.Generate(),
   218  		Name:                  destReqBody.Name,
   219  		Type:                  destReqBody.Type,
   220  		URL:                   destReqBody.URL,
   221  		Authentication:        destReqBody.AuthenticationType,
   222  		SubaccountID:          t.ID,
   223  		FormationAssignmentID: &formationAssignment.ID,
   224  	}
   225  
   226  	if transactionErr := s.transaction(ctx, func(ctxWithTransact context.Context) error {
   227  		if err = s.destinationRepo.UpsertWithEmbeddedTenant(ctx, destModel); err != nil {
   228  			return errors.Wrapf(err, "while upserting basic destination with name: %q and assignment ID: %q in the DB", destinationName, formationAssignment.ID)
   229  		}
   230  		return nil
   231  	}); transactionErr != nil {
   232  		return defaultStatusCode, transactionErr
   233  	}
   234  
   235  	return statusCode, nil
   236  }
   237  
   238  // CreateSAMLAssertionDestination is responsible to create SAML assertion destination resource in the DB as well as in the remote destination service
   239  func (s *Service) CreateSAMLAssertionDestination(ctx context.Context, destinationDetails operators.Destination, samlAuthCreds *operators.SAMLAssertionAuthentication, formationAssignment *model.FormationAssignment, correlationIDs []string) (defaultStatusCode int, err error) {
   240  	subaccountID, err := s.validateDestinationSubaccount(ctx, destinationDetails.SubaccountID, formationAssignment)
   241  	if err != nil {
   242  		return defaultStatusCode, err
   243  	}
   244  
   245  	region, err := s.getRegionLabel(ctx, subaccountID)
   246  	if err != nil {
   247  		return defaultStatusCode, err
   248  	}
   249  
   250  	strURL, err := buildDestinationURL(s.destinationCreatorCfg.DestinationAPIConfig, region, subaccountID, "", false)
   251  	if err != nil {
   252  		return defaultStatusCode, errors.Wrapf(err, "while building destination URL")
   253  	}
   254  
   255  	destinationName := destinationDetails.Name
   256  	destReqBody := &destinationcreator.SAMLAssertionRequestBody{
   257  		BaseDestinationRequestBody: destinationcreator.BaseDestinationRequestBody{
   258  			Name:               destinationDetails.Name,
   259  			URL:                samlAuthCreds.URL,
   260  			Type:               destinationcreator.TypeHTTP,
   261  			ProxyType:          destinationcreator.ProxyTypeInternet,
   262  			AuthenticationType: destinationcreator.AuthTypeSAMLAssertion,
   263  		},
   264  		KeyStoreLocation: destinationDetails.Name + javaKeyStoreFileExtension,
   265  	}
   266  
   267  	if destinationDetails.Type != "" {
   268  		destReqBody.Type = destinationDetails.Type
   269  	}
   270  
   271  	if destinationDetails.ProxyType != "" {
   272  		destReqBody.ProxyType = destinationDetails.ProxyType
   273  	}
   274  
   275  	if destinationDetails.Authentication != "" && destinationDetails.Authentication != destinationcreator.AuthTypeSAMLAssertion {
   276  		return defaultStatusCode, errors.Errorf("The provided authentication type: %s in the destination details is invalid. It should be %s", destinationDetails.Authentication, destinationcreator.AuthTypeSAMLAssertion)
   277  	}
   278  
   279  	enrichedProperties, err := enrichDestinationAdditionalPropertiesWithCorrelationIDs(s.destinationCreatorCfg, correlationIDs, destinationDetails.AdditionalProperties)
   280  	if err != nil {
   281  		return defaultStatusCode, err
   282  	}
   283  	destReqBody.AdditionalProperties = enrichedProperties
   284  
   285  	app, err := s.applicationRepository.GetByID(ctx, formationAssignment.TenantID, formationAssignment.Target)
   286  	if err != nil {
   287  		return defaultStatusCode, errors.Wrapf(err, "while getting application with ID: %q", formationAssignment.Target)
   288  	}
   289  	if app.BaseURL != nil {
   290  		destReqBody.Audience = *app.BaseURL
   291  	}
   292  
   293  	if err := destReqBody.Validate(); err != nil {
   294  		return defaultStatusCode, errors.Wrapf(err, "while validating SAML assertion destination request body")
   295  	}
   296  
   297  	log.C(ctx).Infof("Creating SAML assertion destination with name: %q, subaccount ID: %q and assignment ID: %q in the destination service", destinationName, subaccountID, formationAssignment.ID)
   298  	_, statusCode, err := s.executeCreateRequest(ctx, strURL, destReqBody, destinationName)
   299  	if err != nil {
   300  		return defaultStatusCode, errors.Wrapf(err, "while creating design time destination with name: %q in the destination service", destinationName)
   301  	}
   302  
   303  	if statusCode == http.StatusConflict {
   304  		return statusCode, nil
   305  	}
   306  
   307  	t, err := s.tenantRepo.GetByExternalTenant(ctx, subaccountID)
   308  	if err != nil {
   309  		return defaultStatusCode, errors.Wrapf(err, "while getting tenant by external ID: %q", subaccountID)
   310  	}
   311  
   312  	destModel := &model.Destination{
   313  		ID:                    s.uidSvc.Generate(),
   314  		Name:                  destReqBody.Name,
   315  		Type:                  destReqBody.Type,
   316  		URL:                   destReqBody.URL,
   317  		Authentication:        destReqBody.AuthenticationType,
   318  		SubaccountID:          t.ID,
   319  		FormationAssignmentID: &formationAssignment.ID,
   320  	}
   321  
   322  	if transactionErr := s.transaction(ctx, func(ctxWithTransact context.Context) error {
   323  		if err = s.destinationRepo.UpsertWithEmbeddedTenant(ctx, destModel); err != nil {
   324  			return errors.Wrapf(err, "while upserting basic destination with name: %q and assignment ID: %q in the DB", destinationName, formationAssignment.ID)
   325  		}
   326  		return nil
   327  	}); transactionErr != nil {
   328  		return defaultStatusCode, transactionErr
   329  	}
   330  
   331  	return statusCode, nil
   332  }
   333  
   334  // CreateCertificateInDestinationService is responsible to create certificate resource in the remote destination service
   335  func (s *Service) CreateCertificateInDestinationService(ctx context.Context, destinationDetails operators.Destination, formationAssignment *model.FormationAssignment) (defaultCertData destinationcreator.CertificateResponse, defaultStatusCode int, err error) {
   336  	subaccountID, err := s.validateDestinationSubaccount(ctx, destinationDetails.SubaccountID, formationAssignment)
   337  	if err != nil {
   338  		return defaultCertData, defaultStatusCode, err
   339  	}
   340  
   341  	region, err := s.getRegionLabel(ctx, subaccountID)
   342  	if err != nil {
   343  		return defaultCertData, defaultStatusCode, err
   344  	}
   345  
   346  	strURL, err := buildCertificateURL(s.destinationCreatorCfg.CertificateAPIConfig, region, subaccountID, "", false)
   347  	if err != nil {
   348  		return defaultCertData, defaultStatusCode, errors.Wrapf(err, "while building certificate URL")
   349  	}
   350  
   351  	certName := destinationDetails.Name
   352  	certReqBody := &destinationcreator.CertificateRequestBody{Name: certName}
   353  
   354  	if err := certReqBody.Validate(); err != nil {
   355  		return defaultCertData, defaultStatusCode, errors.Wrapf(err, "while validating certificate request body")
   356  	}
   357  
   358  	log.C(ctx).Infof("Creating certificate with name: %q for subaccount with ID: %q in the destination service for SAML destination", certName, subaccountID)
   359  	respBody, statusCode, err := s.executeCreateRequest(ctx, strURL, certReqBody, certName)
   360  	if err != nil {
   361  		return defaultCertData, defaultStatusCode, errors.Wrapf(err, "while creating certificate with name: %q for subaccount with ID: %q in the destination service", certName, subaccountID)
   362  	}
   363  
   364  	if statusCode == http.StatusConflict {
   365  		return defaultCertData, statusCode, nil
   366  	}
   367  
   368  	var certResp destinationcreator.CertificateResponse
   369  	err = json.Unmarshal(respBody, &certResp)
   370  	if err != nil {
   371  		return defaultCertData, defaultStatusCode, err
   372  	}
   373  
   374  	return certResp, defaultStatusCode, nil
   375  }
   376  
   377  // DeleteDestinations is responsible to delete all type of destinations associated with the given `formationAssignment` from the DB as well as from the remote destination service
   378  func (s *Service) DeleteDestinations(ctx context.Context, formationAssignment *model.FormationAssignment) error {
   379  	externalDestSubaccountID, err := s.getConsumerTenant(ctx, formationAssignment)
   380  	if err != nil {
   381  		return err
   382  	}
   383  
   384  	formationAssignmentID := formationAssignment.ID
   385  
   386  	t, err := s.tenantRepo.GetByExternalTenant(ctx, externalDestSubaccountID)
   387  	if err != nil {
   388  		return errors.Wrapf(err, "while getting tenant by external ID: %q", externalDestSubaccountID)
   389  	}
   390  
   391  	destinations, err := s.destinationRepo.ListByTenantIDAndAssignmentID(ctx, t.ID, formationAssignmentID)
   392  	if err != nil {
   393  		return err
   394  	}
   395  
   396  	log.C(ctx).Infof("There is/are %d destination(s) in the DB", len(destinations))
   397  	if len(destinations) == 0 {
   398  		return nil
   399  	}
   400  
   401  	for _, destination := range destinations {
   402  		if destination.Authentication == destinationcreator.AuthTypeSAMLAssertion {
   403  			if err := s.DeleteCertificateFromDestinationService(ctx, destination.Name, externalDestSubaccountID, formationAssignment); err != nil {
   404  				return errors.Wrapf(err, "while deleting SAML assertion certificate with name: %q", destination.Name)
   405  			}
   406  		}
   407  		if err := s.DeleteDestinationFromDestinationService(ctx, destination.Name, externalDestSubaccountID, formationAssignment); err != nil {
   408  			return err
   409  		}
   410  
   411  		if transactionErr := s.transaction(ctx, func(ctxWithTransact context.Context) error {
   412  			if err := s.destinationRepo.DeleteByDestinationNameAndAssignmentID(ctx, destination.Name, formationAssignmentID, t.ID); err != nil {
   413  				return errors.Wrapf(err, "while deleting destination(s) by name: %q, internal tenant ID: %q and assignment ID: %q from the DB", destination.Name, t.ID, formationAssignmentID)
   414  			}
   415  			return nil
   416  		}); transactionErr != nil {
   417  			return transactionErr
   418  		}
   419  	}
   420  
   421  	return nil
   422  }
   423  
   424  // DeleteDestinationFromDestinationService is responsible to delete destination resource from the remote destination service
   425  func (s *Service) DeleteDestinationFromDestinationService(ctx context.Context, destinationName, externalDestSubaccountID string, formationAssignment *model.FormationAssignment) error {
   426  	subaccountID, err := s.validateDestinationSubaccount(ctx, externalDestSubaccountID, formationAssignment)
   427  	if err != nil {
   428  		return err
   429  	}
   430  
   431  	region, err := s.getRegionLabel(ctx, subaccountID)
   432  	if err != nil {
   433  		return err
   434  	}
   435  
   436  	strURL, err := buildDestinationURL(s.destinationCreatorCfg.DestinationAPIConfig, region, subaccountID, destinationName, true)
   437  	if err != nil {
   438  		return errors.Wrapf(err, "while building destination URL")
   439  	}
   440  
   441  	log.C(ctx).Infof("Deleting destination with name: %q and subaccount ID: %q from destination service", destinationName, subaccountID)
   442  	err = s.executeDeleteRequest(ctx, strURL, destinationName, subaccountID)
   443  	if err != nil {
   444  		return err
   445  	}
   446  
   447  	return nil
   448  }
   449  
   450  // DeleteCertificateFromDestinationService is responsible to delete certificate resource from the remote destination service
   451  func (s *Service) DeleteCertificateFromDestinationService(ctx context.Context, certificateName, externalDestSubaccountID string, formationAssignment *model.FormationAssignment) error {
   452  	subaccountID, err := s.validateDestinationSubaccount(ctx, externalDestSubaccountID, formationAssignment)
   453  	if err != nil {
   454  		return err
   455  	}
   456  
   457  	region, err := s.getRegionLabel(ctx, subaccountID)
   458  	if err != nil {
   459  		return err
   460  	}
   461  
   462  	strURL, err := buildCertificateURL(s.destinationCreatorCfg.CertificateAPIConfig, region, subaccountID, certificateName, true)
   463  	if err != nil {
   464  		return errors.Wrapf(err, "while building certificate URL")
   465  	}
   466  
   467  	log.C(ctx).Infof("Deleting SAML assertion certificate with name: %q and subaccount ID: %q from destination service", certificateName, subaccountID)
   468  	err = s.executeDeleteRequest(ctx, strURL, certificateName, subaccountID)
   469  	if err != nil {
   470  		return err
   471  	}
   472  
   473  	return nil
   474  }
   475  
   476  // EnrichAssignmentConfigWithCertificateData is responsible to enrich the assignment configuration with the created certificate resource for the SAML assertion destination
   477  func (s *Service) EnrichAssignmentConfigWithCertificateData(assignmentConfig json.RawMessage, certData destinationcreator.CertificateResponse, destinationIndex int) (json.RawMessage, error) {
   478  	certAPIConfig := s.destinationCreatorCfg.CertificateAPIConfig
   479  	configStr := string(assignmentConfig)
   480  
   481  	path := fmt.Sprintf("credentials.inboundCommunication.samlAssertion.destinations.%d.%s", destinationIndex, certAPIConfig.FileNameKey)
   482  	configStr, err := sjson.Set(configStr, path, certData.FileName)
   483  	if err != nil {
   484  		return nil, errors.Wrapf(err, "while enriching SAML assertion destination with certificate %q key", certAPIConfig.FileNameKey)
   485  	}
   486  
   487  	path = fmt.Sprintf("credentials.inboundCommunication.samlAssertion.destinations.%d.%s", destinationIndex, certAPIConfig.CommonNameKey)
   488  	configStr, err = sjson.Set(configStr, path, certData.CommonName)
   489  	if err != nil {
   490  		return nil, errors.Wrapf(err, "while enriching SAML assertion destination with certificate %q key", certAPIConfig.CommonNameKey)
   491  	}
   492  
   493  	path = fmt.Sprintf("credentials.inboundCommunication.samlAssertion.destinations.%d.%s", destinationIndex, certAPIConfig.CertificateChainKey)
   494  	configStr, err = sjson.Set(configStr, path, certData.CertificateChain)
   495  	if err != nil {
   496  		return nil, errors.Wrapf(err, "while enriching SAML assertion destination with %q key", certAPIConfig.CertificateChainKey)
   497  	}
   498  
   499  	return json.RawMessage(configStr), nil
   500  }
   501  
   502  func (s *Service) validateDestinationSubaccount(ctx context.Context, externalDestSubaccountID string, formationAssignment *model.FormationAssignment) (string, error) {
   503  	var subaccountID string
   504  	if externalDestSubaccountID == "" {
   505  		consumerSubaccountID, err := s.getConsumerTenant(ctx, formationAssignment)
   506  		if err != nil {
   507  			return "", err
   508  		}
   509  		subaccountID = consumerSubaccountID
   510  
   511  		log.C(ctx).Infof("There was no subaccount ID provided in the destination but the consumer: %q is validated successfully", subaccountID)
   512  		return subaccountID, nil
   513  	}
   514  
   515  	if externalDestSubaccountID != "" {
   516  		consumerSubaccountID, err := s.getConsumerTenant(ctx, formationAssignment)
   517  		if err != nil {
   518  			log.C(ctx).Warnf("Couldn't validate the if the provided destination subaccount ID: %q is a consumer subaccount. Validating if it's a provider one...", externalDestSubaccountID)
   519  		}
   520  
   521  		if consumerSubaccountID != "" && externalDestSubaccountID == consumerSubaccountID {
   522  			log.C(ctx).Infof("Successfully validated the provided destination subaccount ID: %q is a consumer subaccount", externalDestSubaccountID)
   523  			return consumerSubaccountID, nil
   524  		}
   525  
   526  		switch formationAssignment.TargetType {
   527  		case model.FormationAssignmentTypeApplication:
   528  			if err := s.validateAppTemplateProviderSubaccount(ctx, formationAssignment, externalDestSubaccountID); err != nil {
   529  				return "", err
   530  			}
   531  		case model.FormationAssignmentTypeRuntime:
   532  			if err := s.validateRuntimeProviderSubaccount(ctx, formationAssignment.Target, externalDestSubaccountID); err != nil {
   533  				return "", err
   534  			}
   535  		case model.FormationAssignmentTypeRuntimeContext:
   536  			if err := s.validateRuntimeContextProviderSubaccount(ctx, formationAssignment, externalDestSubaccountID); err != nil {
   537  				return "", err
   538  			}
   539  		default:
   540  			return "", errors.Errorf("Unknown formation assignment type: %q", formationAssignment.TargetType)
   541  		}
   542  
   543  		subaccountID = externalDestSubaccountID
   544  	}
   545  
   546  	return subaccountID, nil
   547  }
   548  
   549  func (s *Service) getConsumerTenant(ctx context.Context, formationAssignment *model.FormationAssignment) (string, error) {
   550  	labelableObjType, err := determineLabelableObjectType(formationAssignment.TargetType)
   551  	if err != nil {
   552  		return "", err
   553  	}
   554  
   555  	labels, err := s.labelRepo.ListForObject(ctx, formationAssignment.TenantID, labelableObjType, formationAssignment.Target)
   556  	if err != nil {
   557  		return "", errors.Wrapf(err, "while getting labels for %s with ID: %q", formationAssignment.TargetType, formationAssignment.Target)
   558  	}
   559  
   560  	globalSubaccIDLbl, globalSubaccIDExists := labels[globalSubaccountLabelKey]
   561  	if !globalSubaccIDExists {
   562  		return "", errors.Errorf("%q label does not exists for: %q with ID: %q", globalSubaccountLabelKey, formationAssignment.TargetType, formationAssignment.Target)
   563  	}
   564  
   565  	globalSubaccIDLblValue, ok := globalSubaccIDLbl.Value.(string)
   566  	if !ok {
   567  		return "", errors.Errorf("unexpected type of %q label, expect: string, got: %T", globalSubaccountLabelKey, globalSubaccIDLbl.Value)
   568  	}
   569  
   570  	return globalSubaccIDLblValue, nil
   571  }
   572  
   573  func (s *Service) getRegionLabel(ctx context.Context, tenantID string) (string, error) {
   574  	t, err := s.tenantRepo.GetByExternalTenant(ctx, tenantID)
   575  	if err != nil {
   576  		return "", errors.Wrapf(err, "while getting tenant by external ID: %q", tenantID)
   577  	}
   578  
   579  	regionLbl, err := s.labelRepo.GetByKey(ctx, t.ID, model.TenantLabelableObject, tenantID, regionLabelKey)
   580  	if err != nil {
   581  		return "", err
   582  	}
   583  
   584  	region, ok := regionLbl.Value.(string)
   585  	if !ok {
   586  		return "", errors.Errorf("unexpected type of %q label, expect: string, got: %T", regionLabelKey, regionLbl.Value)
   587  	}
   588  	return region, nil
   589  }
   590  
   591  func (s *Service) validateAppTemplateProviderSubaccount(ctx context.Context, formationAssignment *model.FormationAssignment, externalDestSubaccountID string) error {
   592  	app, err := s.applicationRepository.GetByID(ctx, formationAssignment.TenantID, formationAssignment.Target)
   593  	if err != nil {
   594  		return err
   595  	}
   596  
   597  	if app.ApplicationTemplateID == nil || *app.ApplicationTemplateID == "" {
   598  		return errors.Errorf("The application template ID for application ID: %q should not be empty", app.ID)
   599  	}
   600  
   601  	labels, err := s.labelRepo.ListForGlobalObject(ctx, model.AppTemplateLabelableObject, *app.ApplicationTemplateID)
   602  	if err != nil {
   603  		return errors.Wrapf(err, "while getting labels for application template with ID: %q", *app.ApplicationTemplateID)
   604  	}
   605  
   606  	subaccountLbl, subaccountLblExists := labels[globalSubaccountLabelKey]
   607  
   608  	if !subaccountLblExists {
   609  		return errors.Errorf("%q label should exist as part of the provider application template with ID: %q", globalSubaccountLabelKey, *app.ApplicationTemplateID)
   610  	}
   611  
   612  	subaccountLblValue, ok := subaccountLbl.Value.(string)
   613  	if !ok {
   614  		return errors.Errorf("unexpected type of %q label, expect: string, got: %T", globalSubaccountLabelKey, subaccountLbl.Value)
   615  	}
   616  
   617  	if externalDestSubaccountID != subaccountLblValue {
   618  		return errors.Errorf("The provided destination subaccount is different from the owner subaccount of the application template with ID: %q", *app.ApplicationTemplateID)
   619  	}
   620  
   621  	log.C(ctx).Infof("Successfully validated that the provided destination subaccount: %q is a provider one - the owner of the application template", externalDestSubaccountID)
   622  
   623  	return nil
   624  }
   625  
   626  func (s *Service) validateRuntimeProviderSubaccount(ctx context.Context, runtimeID, externalDestSubaccountID string) error {
   627  	t, err := s.tenantRepo.GetByExternalTenant(ctx, externalDestSubaccountID)
   628  	if err != nil {
   629  		return errors.Wrapf(err, "while getting tenant by external ID: %q", externalDestSubaccountID)
   630  	}
   631  
   632  	if t.Type != tenant.Subaccount {
   633  		return errors.Errorf("The provided destination external tenant ID: %q has invalid type, expected: %q, got: %q", externalDestSubaccountID, tenant.Subaccount, t.Type)
   634  	}
   635  
   636  	exists, err := s.runtimeRepository.OwnerExists(ctx, t.ID, runtimeID)
   637  	if err != nil {
   638  		return err
   639  	}
   640  
   641  	if !exists {
   642  		return errors.Errorf("The provided destination external subaccount: %q is not provider of the runtime with ID: %q", externalDestSubaccountID, runtimeID)
   643  	}
   644  
   645  	log.C(ctx).Infof("Successfully validated that the provided destination external subaccount: %q is a provider one - the owner of the runtime", externalDestSubaccountID)
   646  
   647  	return nil
   648  }
   649  
   650  func (s *Service) validateRuntimeContextProviderSubaccount(ctx context.Context, formationAssignment *model.FormationAssignment, externalDestSubaccountID string) error {
   651  	rtmCtxID, err := s.runtimeCtxRepository.GetByID(ctx, formationAssignment.TenantID, formationAssignment.Target)
   652  	if err != nil {
   653  		return err
   654  	}
   655  
   656  	return s.validateRuntimeProviderSubaccount(ctx, rtmCtxID.RuntimeID, externalDestSubaccountID)
   657  }
   658  
   659  func (s *Service) transaction(ctx context.Context, dbCall func(ctxWithTransact context.Context) error) error {
   660  	tx, err := s.transact.Begin()
   661  	if err != nil {
   662  		log.C(ctx).WithError(err).Error("Failed to begin DB transaction")
   663  		return err
   664  	}
   665  	defer s.transact.RollbackUnlessCommitted(ctx, tx)
   666  
   667  	ctx = persistence.SaveToContext(ctx, tx)
   668  
   669  	if err = dbCall(ctx); err != nil {
   670  		return err
   671  	}
   672  
   673  	if err = tx.Commit(); err != nil {
   674  		log.C(ctx).WithError(err).Error("Failed to commit database transaction")
   675  		return err
   676  	}
   677  	return nil
   678  }
   679  
   680  func (s *Service) executeCreateRequest(ctx context.Context, url string, reqBody interface{}, entityName string) (defaultRespBody []byte, defaultStatusCode int, err error) {
   681  	reqBodyBytes, err := json.Marshal(reqBody)
   682  	if err != nil {
   683  		return defaultRespBody, defaultStatusCode, errors.Wrapf(err, "while marshalling request body")
   684  	}
   685  
   686  	clientUser, err := client.LoadFromContext(ctx)
   687  	if err != nil || clientUser == "" {
   688  		log.C(ctx).Warn("Unable to provide client_user. Using correlation ID as client_user header...")
   689  		clientUser = correlation.CorrelationIDFromContext(ctx)
   690  		if clientUser == "" {
   691  			log.C(ctx).Warnf("The correlation ID is empty, generating random UUID and using it as client user")
   692  			clientUser = s.uidSvc.Generate()
   693  		}
   694  	}
   695  
   696  	req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBodyBytes))
   697  	if err != nil {
   698  		return defaultRespBody, defaultStatusCode, errors.Wrap(err, "while preparing destination service creation request")
   699  	}
   700  	req.Header.Set(clientUserHeaderKey, clientUser)
   701  	req.Header.Set(contentTypeHeaderKey, contentTypeApplicationJSON)
   702  
   703  	resp, err := s.mtlsHTTPClient.Do(req)
   704  	if err != nil {
   705  		return defaultRespBody, defaultStatusCode, err
   706  	}
   707  	defer closeResponseBody(ctx, resp)
   708  
   709  	body, err := io.ReadAll(resp.Body)
   710  	if err != nil {
   711  		return defaultRespBody, defaultStatusCode, errors.Errorf("Failed to read response body: %v", err)
   712  	}
   713  
   714  	if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusConflict {
   715  		return body, defaultStatusCode, errors.Errorf("Failed to create entity with name: %q, status: %d, body: %s", entityName, resp.StatusCode, body)
   716  	}
   717  
   718  	if resp.StatusCode == http.StatusConflict {
   719  		log.C(ctx).Infof("The entity with name: %q already exists in the destination service. Returning conflict status code...", entityName)
   720  		return body, http.StatusConflict, nil
   721  	}
   722  	log.C(ctx).Infof("Successfully created entity with name: %q in the destination service", entityName)
   723  
   724  	return body, http.StatusCreated, nil
   725  }
   726  
   727  func (s *Service) executeDeleteRequest(ctx context.Context, url string, entityName, subaccountID string) error {
   728  	clientUser, err := client.LoadFromContext(ctx)
   729  	if err != nil || clientUser == "" {
   730  		log.C(ctx).Warn("unable to provide client_user. Using correlation ID as client_user header...")
   731  		clientUser = correlation.CorrelationIDFromContext(ctx)
   732  	}
   733  
   734  	req, err := http.NewRequest(http.MethodDelete, url, nil)
   735  	if err != nil {
   736  		return errors.Wrap(err, "while preparing destination service deletion request")
   737  	}
   738  	req.Header.Set(clientUserHeaderKey, clientUser)
   739  	req.Header.Set(contentTypeHeaderKey, contentTypeApplicationJSON)
   740  
   741  	resp, err := s.mtlsHTTPClient.Do(req)
   742  	if err != nil {
   743  		return err
   744  	}
   745  	defer closeResponseBody(ctx, resp)
   746  
   747  	body, err := io.ReadAll(resp.Body)
   748  	if err != nil {
   749  		return errors.Errorf("Failed to read destination delete response body: %v", err)
   750  	}
   751  
   752  	if resp.StatusCode != http.StatusNoContent {
   753  		return errors.Errorf("Failed to delete entity with name: %q from destination service, status: %d, body: %s", entityName, resp.StatusCode, body)
   754  	}
   755  
   756  	log.C(ctx).Infof("Successfully deleted entity with name: %q and subaccount ID: %q from destination service", entityName, subaccountID)
   757  
   758  	return nil
   759  }
   760  
   761  func (s *Service) prepareBasicRequestBody(ctx context.Context, destinationDetails operators.Destination, basicAuthenticationCredentials operators.BasicAuthentication, formationAssignment *model.FormationAssignment, correlationIDs []string) (*destinationcreator.BasicRequestBody, error) {
   762  	reqBody := &destinationcreator.BasicRequestBody{
   763  		BaseDestinationRequestBody: destinationcreator.BaseDestinationRequestBody{
   764  			Name:               destinationDetails.Name,
   765  			URL:                "",
   766  			Type:               destinationcreator.TypeHTTP,
   767  			ProxyType:          destinationcreator.ProxyTypeInternet,
   768  			AuthenticationType: destinationcreator.AuthTypeBasic,
   769  		},
   770  		User:     basicAuthenticationCredentials.Username,
   771  		Password: basicAuthenticationCredentials.Password,
   772  	}
   773  
   774  	enrichedProperties, err := enrichDestinationAdditionalPropertiesWithCorrelationIDs(s.destinationCreatorCfg, correlationIDs, destinationDetails.AdditionalProperties)
   775  	if err != nil {
   776  		return nil, err
   777  	}
   778  	reqBody.AdditionalProperties = enrichedProperties
   779  
   780  	if destinationDetails.URL != "" {
   781  		reqBody.URL = destinationDetails.URL
   782  	}
   783  
   784  	if destinationDetails.URL == "" && basicAuthenticationCredentials.URL != "" {
   785  		reqBody.URL = basicAuthenticationCredentials.URL
   786  	}
   787  
   788  	if destinationDetails.URL == "" && basicAuthenticationCredentials.URL == "" {
   789  		app, err := s.applicationRepository.GetByID(ctx, formationAssignment.TenantID, formationAssignment.Target)
   790  		if err != nil {
   791  			return nil, err
   792  		}
   793  		if app.BaseURL != nil {
   794  			reqBody.URL = *app.BaseURL
   795  		}
   796  	}
   797  
   798  	if destinationDetails.Type != "" {
   799  		reqBody.Type = destinationDetails.Type
   800  	}
   801  
   802  	if destinationDetails.ProxyType != "" {
   803  		reqBody.ProxyType = destinationDetails.ProxyType
   804  	}
   805  
   806  	if destinationDetails.Authentication != "" && destinationDetails.Authentication != destinationcreator.AuthTypeBasic {
   807  		return nil, errors.Errorf("The provided authentication type: %s in the destination details is invalid. It should be %s", destinationDetails.Authentication, destinationcreator.AuthTypeBasic)
   808  	}
   809  
   810  	if err := reqBody.Validate(); err != nil {
   811  		return nil, errors.Wrapf(err, "while validating basic destination request body")
   812  	}
   813  
   814  	return reqBody, nil
   815  }
   816  
   817  func enrichDestinationAdditionalPropertiesWithCorrelationIDs(destinationCreatorCfg *destinationcreator.Config, correlationIDs []string, destinationAdditionalProperties json.RawMessage) (json.RawMessage, error) {
   818  	joinedCorrelationIDs := strings.Join(correlationIDs, ",")
   819  	additionalProps := string(destinationAdditionalProperties)
   820  	additionalProps, err := sjson.Set(additionalProps, destinationCreatorCfg.CorrelationIDsKey, joinedCorrelationIDs)
   821  	if err != nil {
   822  		return nil, errors.Wrapf(err, "while setting the correlation IDs as additional properties of the destination")
   823  	}
   824  
   825  	return json.RawMessage(additionalProps), nil
   826  }
   827  
   828  func determineLabelableObjectType(assignmentType model.FormationAssignmentType) (model.LabelableObject, error) {
   829  	switch assignmentType {
   830  	case model.FormationAssignmentTypeApplication:
   831  		return model.ApplicationLabelableObject, nil
   832  	case model.FormationAssignmentTypeRuntime:
   833  		return model.RuntimeLabelableObject, nil
   834  	case model.FormationAssignmentTypeRuntimeContext:
   835  		return model.RuntimeContextLabelableObject, nil
   836  	default:
   837  		return "", errors.Errorf("Couldn't determine the label-able object type from assignment type: %q", assignmentType)
   838  	}
   839  }
   840  
   841  func closeResponseBody(ctx context.Context, resp *http.Response) {
   842  	if err := resp.Body.Close(); err != nil {
   843  		log.C(ctx).Errorf("An error has occurred while closing response body: %v", err)
   844  	}
   845  }
   846  
   847  func buildDestinationURL(destinationCfg *destinationcreator.DestinationAPIConfig, region, subaccountID, destinationName string, isDeleteRequest bool) (string, error) {
   848  	return buildURL(destinationCfg.BaseURL, destinationCfg.Path, destinationCfg.RegionParam, destinationCfg.SubaccountIDParam, destinationCfg.DestinationNameParam, region, subaccountID, destinationName, isDeleteRequest)
   849  }
   850  
   851  func buildCertificateURL(certificateCfg *destinationcreator.CertificateAPIConfig, region, subaccountID, certificateName string, isDeleteRequest bool) (string, error) {
   852  	return buildURL(certificateCfg.BaseURL, certificateCfg.Path, certificateCfg.RegionParam, certificateCfg.SubaccountIDParam, certificateCfg.CertificateNameParam, region, subaccountID, certificateName, isDeleteRequest)
   853  }
   854  
   855  func buildURL(baseURL, path, regionParam, subaccountIDParam, entityNameParam, region, subaccountID, entityName string, isDeleteRequest bool) (string, error) {
   856  	if region == "" || subaccountID == "" {
   857  		return "", errors.Errorf("The provided region and/or subaccount for the URL couldn't be empty")
   858  	}
   859  
   860  	base, err := url.Parse(baseURL)
   861  	if err != nil {
   862  		return "", err
   863  	}
   864  
   865  	regionalEndpoint := strings.Replace(path, fmt.Sprintf("{%s}", regionParam), region, 1)
   866  	regionalEndpoint = strings.Replace(regionalEndpoint, fmt.Sprintf("{%s}", subaccountIDParam), subaccountID, 1)
   867  
   868  	if isDeleteRequest {
   869  		if entityName == "" {
   870  			return "", errors.Errorf("The entity name should not be empty in case of %s request", http.MethodDelete)
   871  		}
   872  		regionalEndpoint += fmt.Sprintf("/{%s}", entityNameParam)
   873  		regionalEndpoint = strings.Replace(regionalEndpoint, fmt.Sprintf("{%s}", entityNameParam), entityName, 1)
   874  	}
   875  
   876  	// Path params
   877  	base.Path += regionalEndpoint
   878  
   879  	return base.String(), nil
   880  }