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 }