github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/domain/onetimetoken/service.go (about) 1 package onetimetoken 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "net/http" 9 "time" 10 11 "github.com/kyma-incubator/compass/components/director/pkg/correlation" 12 13 "github.com/kyma-incubator/compass/components/director/internal/domain/scenariogroups" 14 15 pkgadapters "github.com/kyma-incubator/compass/components/director/pkg/adapters" 16 17 pkgmodel "github.com/kyma-incubator/compass/components/director/pkg/model" 18 19 "github.com/kyma-incubator/compass/components/director/internal/domain/tenant" 20 tenantpkg "github.com/kyma-incubator/compass/components/director/pkg/tenant" 21 22 "github.com/avast/retry-go/v4" 23 "github.com/kyma-incubator/compass/components/director/internal/domain/client" 24 "github.com/kyma-incubator/compass/components/director/internal/model" 25 "github.com/kyma-incubator/compass/components/director/internal/tokens" 26 "github.com/kyma-incubator/compass/components/director/pkg/graphql" 27 "github.com/kyma-incubator/compass/components/director/pkg/log" 28 "github.com/kyma-incubator/compass/components/director/pkg/pairing" 29 directorTime "github.com/kyma-incubator/compass/components/director/pkg/time" 30 "github.com/pkg/errors" 31 ) 32 33 // SystemAuthService missing godoc 34 // 35 //go:generate mockery --name=SystemAuthService --output=automock --outpkg=automock --case=underscore --disable-version-string 36 type SystemAuthService interface { 37 Create(ctx context.Context, objectType pkgmodel.SystemAuthReferenceObjectType, objectID string, authInput *model.AuthInput) (string, error) 38 GetByToken(ctx context.Context, token string) (*pkgmodel.SystemAuth, error) 39 GetGlobal(ctx context.Context, authID string) (*pkgmodel.SystemAuth, error) 40 Update(ctx context.Context, item *pkgmodel.SystemAuth) error 41 } 42 43 // ApplicationConverter missing godoc 44 // 45 //go:generate mockery --name=ApplicationConverter --output=automock --outpkg=automock --case=underscore --disable-version-string 46 type ApplicationConverter interface { 47 ToGraphQL(in *model.Application) *graphql.Application 48 } 49 50 // ApplicationService missing godoc 51 // 52 //go:generate mockery --name=ApplicationService --output=automock --outpkg=automock --case=underscore --disable-version-string 53 type ApplicationService interface { 54 Get(ctx context.Context, id string) (*model.Application, error) 55 ListLabels(ctx context.Context, applicationID string) (map[string]*model.Label, error) 56 } 57 58 // ExternalTenantsService missing godoc 59 // 60 //go:generate mockery --name=ExternalTenantsService --output=automock --outpkg=automock --case=underscore --disable-version-string 61 type ExternalTenantsService interface { 62 GetTenantByID(ctx context.Context, id string) (*model.BusinessTenantMapping, error) 63 } 64 65 // HTTPDoer missing godoc 66 // 67 //go:generate mockery --name=HTTPDoer --output=automock --outpkg=automock --case=underscore --disable-version-string 68 type HTTPDoer interface { 69 Do(req *http.Request) (*http.Response, error) 70 } 71 72 type service struct { 73 connectorURL string 74 legacyConnectorURL string 75 suggestTokenHeaderKey string 76 csrTokenExpiration time.Duration 77 appTokenExpiration time.Duration 78 runtimeTokenExpiration time.Duration 79 sysAuthSvc SystemAuthService 80 pairingAdapters *pkgadapters.Adapters 81 appSvc ApplicationService 82 appConverter ApplicationConverter 83 extTenantsSvc ExternalTenantsService 84 doer HTTPDoer 85 tokenGenerator TokenGenerator 86 timeService directorTime.Service 87 } 88 89 // NewTokenService missing godoc 90 func NewTokenService(sysAuthSvc SystemAuthService, appSvc ApplicationService, appConverter ApplicationConverter, extTenantsSvc ExternalTenantsService, doer HTTPDoer, tokenGenerator TokenGenerator, config Config, pairingAdapters *pkgadapters.Adapters, timeService directorTime.Service) *service { 91 return &service{ 92 connectorURL: config.ConnectorURL, 93 legacyConnectorURL: config.LegacyConnectorURL, 94 suggestTokenHeaderKey: config.SuggestTokenHeaderKey, 95 csrTokenExpiration: config.CSRExpiration, 96 appTokenExpiration: config.ApplicationExpiration, 97 runtimeTokenExpiration: config.RuntimeExpiration, 98 sysAuthSvc: sysAuthSvc, 99 pairingAdapters: pairingAdapters, 100 appSvc: appSvc, 101 appConverter: appConverter, 102 extTenantsSvc: extTenantsSvc, 103 doer: doer, 104 tokenGenerator: tokenGenerator, 105 timeService: timeService, 106 } 107 } 108 109 // GenerateOneTimeToken missing godoc 110 func (s *service) GenerateOneTimeToken(ctx context.Context, objectID string, tokenType pkgmodel.SystemAuthReferenceObjectType) (*model.OneTimeToken, error) { 111 token, suggestedToken, err := s.getToken(ctx, objectID, tokenType) 112 if err != nil { 113 return nil, err 114 } 115 if err := s.saveToken(ctx, objectID, tokenType, token); err != nil { 116 return nil, err 117 } 118 if suggestedToken != "" { 119 token.Token = suggestedToken 120 } 121 122 return token, nil 123 } 124 125 // RegenerateOneTimeToken missing godoc 126 func (s *service) RegenerateOneTimeToken(ctx context.Context, sysAuthID string) (*model.OneTimeToken, error) { 127 sysAuth, err := s.sysAuthSvc.GetGlobal(ctx, sysAuthID) 128 if err != nil { 129 return nil, err 130 } 131 objectID, err := sysAuth.GetReferenceObjectID() 132 if err != nil { 133 return nil, err 134 } 135 if sysAuth.Value == nil { 136 sysAuth.Value = &model.Auth{} 137 } 138 tokenType, err := sysAuth.GetReferenceObjectType() 139 if err != nil { 140 return nil, err 141 } 142 oneTimeToken, _, err := s.getToken(ctx, objectID, tokenType) 143 if err != nil { 144 return nil, err 145 } 146 147 sysAuth.Value.OneTimeToken = oneTimeToken 148 if err := s.sysAuthSvc.Update(ctx, sysAuth); err != nil { 149 return nil, err 150 } 151 152 return oneTimeToken, nil 153 } 154 155 func (s *service) getToken(ctx context.Context, objectID string, tokenType pkgmodel.SystemAuthReferenceObjectType) (*model.OneTimeToken, string, error) { 156 if tokenType == pkgmodel.ApplicationReference { 157 log.C(ctx).Infof("Getting one time token for %s with ID: %s...", tokenType, objectID) 158 return s.getAppToken(ctx, objectID) 159 } else { 160 token, err := s.createToken(ctx, tokenType, objectID, nil) 161 return token, "", err 162 } 163 } 164 165 func (s *service) createToken(ctx context.Context, tokenType pkgmodel.SystemAuthReferenceObjectType, objectID string, oneTimeToken *model.OneTimeToken) (*model.OneTimeToken, error) { 166 var err error 167 if oneTimeToken == nil { 168 log.C(ctx).Infof("Creating one time token for %s with ID: %s...", tokenType, objectID) 169 oneTimeToken, err = s.getNewToken() 170 if err != nil { 171 return nil, errors.Wrapf(err, "while generating onetime token for %s", tokenType) 172 } 173 } 174 175 log.C(ctx).Infof("Updating one time token with metadata for %s with ID: %s...", tokenType, objectID) 176 177 switch tokenType { 178 case pkgmodel.ApplicationReference: 179 oneTimeToken.Type = tokens.ApplicationToken 180 case pkgmodel.RuntimeReference: 181 oneTimeToken.Type = tokens.RuntimeToken 182 } 183 oneTimeToken.CreatedAt = s.timeService.Now() 184 oneTimeToken.Used = false 185 oneTimeToken.UsedAt = time.Time{} 186 expiresAfter, err := s.getExpirationDurationForToken(oneTimeToken.Type) 187 if err != nil { 188 return nil, err 189 } 190 oneTimeToken.ExpiresAt = oneTimeToken.CreatedAt.Add(expiresAfter) 191 192 oneTimeToken.ScenarioGroups = scenariogroups.LoadFromContext(ctx) 193 194 return oneTimeToken, nil 195 } 196 197 func (s *service) saveToken(ctx context.Context, objectID string, tokenType pkgmodel.SystemAuthReferenceObjectType, oneTimeToken *model.OneTimeToken) error { 198 if _, err := s.sysAuthSvc.Create(ctx, tokenType, objectID, &model.AuthInput{OneTimeToken: oneTimeToken}); err != nil { 199 return errors.Wrap(err, "while creating System Auth") 200 } 201 return nil 202 } 203 204 func (s *service) getAppToken(ctx context.Context, id string) (*model.OneTimeToken, string, error) { 205 var ( 206 oneTimeToken *model.OneTimeToken 207 err error 208 ) 209 210 app, err := s.appSvc.Get(ctx, id) 211 if err != nil { 212 return nil, "", errors.Wrapf(err, "while getting application [id: %s]", id) 213 } 214 215 if app.IntegrationSystemID != nil { 216 intSystemToAdapterMapping := map[string]string{} 217 if s.pairingAdapters != nil { 218 intSystemToAdapterMapping = s.pairingAdapters.Get() 219 if intSystemToAdapterMapping == nil { 220 log.C(ctx).Error("pairing adapter configuration mapping cannot be nil") 221 return nil, "", errors.Errorf("pairing adapter configuration mapping cannot be nil") 222 } 223 } 224 225 if adapterURL, ok := intSystemToAdapterMapping[*app.IntegrationSystemID]; ok { 226 log.C(ctx).Infof("Getting one time token for application with name: %s and ID: %s from pairing adapter...", app.Name, app.ID) 227 oneTimeToken, err = s.getTokenFromAdapter(ctx, adapterURL, *app) 228 if err != nil { 229 return nil, "", errors.Wrapf(err, "while getting one time token for application from adapter with URL %s", adapterURL) 230 } 231 } 232 log.C(ctx).Warnf("Could not find any adapter for the given integration system ID: %s", *app.IntegrationSystemID) 233 } 234 235 oneTimeToken, err = s.createToken(ctx, pkgmodel.ApplicationReference, id, oneTimeToken) 236 if err != nil { 237 return nil, "", err 238 } 239 240 suggestedAppTokenString := s.getSuggestedTokenForApp(ctx, app, oneTimeToken) 241 return oneTimeToken, suggestedAppTokenString, nil 242 } 243 244 func (s *service) getTokenFromAdapter(ctx context.Context, adapterURL string, app model.Application) (*model.OneTimeToken, error) { 245 tntCtx, err := tenant.LoadTenantPairFromContext(ctx) 246 if err != nil { 247 return nil, err 248 } 249 250 tnt, err := s.extTenantsSvc.GetTenantByID(ctx, tntCtx.InternalID) 251 if err != nil { 252 return nil, errors.Wrapf(err, "while getting tenant with internal ID %q", tntCtx.InternalID) 253 } 254 255 extTenant := tnt.ExternalTenant 256 if tnt.Type == tenantpkg.Subaccount { 257 if tnt, err = s.extTenantsSvc.GetTenantByID(ctx, tnt.Parent); err != nil { 258 return nil, errors.Wrapf(err, "while getting parent tenant with internal tenant %q", tnt.Parent) 259 } 260 extTenant = tnt.ExternalTenant 261 } 262 263 clientUser, err := client.LoadFromContext(ctx) 264 if err != nil || clientUser == "" { 265 log.C(ctx).Warnf("unable to provide client_user for internal tenant [%s] with corresponding external tenant [%s]. Using correlation ID as client_user header...", tntCtx.InternalID, extTenant) 266 clientUser = correlation.CorrelationIDFromContext(ctx) 267 } 268 269 scenarioGroups := scenariogroups.LoadFromContext(ctx) 270 271 graphqlApp := s.appConverter.ToGraphQL(&app) 272 data := pairing.RequestData{ 273 Application: *graphqlApp, 274 Tenant: extTenant, 275 ClientUser: clientUser, 276 ScenarioGroups: scenarioGroups, 277 } 278 279 asJSON, err := json.Marshal(data) 280 if err != nil { 281 return nil, errors.Wrap(err, "while marshaling data for adapter") 282 } 283 284 log.C(ctx).Infof("Getting one time token from pairing adapter with URL: %s", adapterURL) 285 var externalToken string 286 err = retry.Do(func() error { 287 buf := bytes.NewBuffer(asJSON) 288 req, err := http.NewRequestWithContext(ctx, http.MethodPost, adapterURL, buf) 289 if err != nil { 290 return errors.Wrap(err, "while creating request") 291 } 292 293 resp, err := s.doer.Do(req) 294 if err != nil { 295 return errors.Wrap(err, "while executing request") 296 } 297 298 defer func() { 299 err := resp.Body.Close() 300 if err != nil { 301 log.C(ctx).Warnf("Got error on closing response body: [%v]", err) 302 } 303 }() 304 305 if resp.StatusCode != http.StatusOK { 306 return fmt.Errorf("wrong status code, got [%d], expected [%d]", resp.StatusCode, http.StatusOK) 307 } 308 309 responseBody := pairing.ResponseData{} 310 if err := json.NewDecoder(resp.Body).Decode(&responseBody); err != nil { 311 return errors.Wrap(err, "while decoding response from Adapter") 312 } 313 314 externalToken = responseBody.Token 315 return nil 316 }, retry.Attempts(3)) 317 if err != nil { 318 return nil, errors.Wrapf(err, "while calling adapter [%s] for application [%s] with integration system [%s]", adapterURL, app.ID, *app.IntegrationSystemID) 319 } 320 return &model.OneTimeToken{ 321 Token: externalToken, 322 }, nil 323 } 324 325 func (s *service) getNewToken() (*model.OneTimeToken, error) { 326 tokenString, err := s.tokenGenerator.NewToken() 327 if err != nil { 328 return nil, err 329 } 330 return &model.OneTimeToken{ 331 Token: tokenString, 332 ConnectorURL: s.connectorURL, 333 }, nil 334 } 335 336 // getSuggestedTokenForApp returns the token that an application would use, depending on its type - if the application belongs to an integration 337 // system, then it would use the token as is, if the application is considered "legacy" - an application which still uses the "connectivity-adapter" - 338 // then it would use the legacy connector URL, then any new applications which are not managed by an integration system, would use the base64 encoded JSON 339 // containing the token, and the connector URL 340 func (s *service) getSuggestedTokenForApp(ctx context.Context, app *model.Application, oneTimeToken *model.OneTimeToken) string { 341 if !tokenSuggestionEnabled(ctx, s.suggestTokenHeaderKey) { 342 return oneTimeToken.Token 343 } 344 345 if app.IntegrationSystemID != nil { 346 log.C(ctx).Infof("Application with ID %s belongs to an integration system, will use the actual token", app.ID) 347 return oneTimeToken.Token 348 } 349 350 appLabels, err := s.appSvc.ListLabels(ctx, app.ID) 351 if err != nil { 352 log.C(ctx).WithError(err).Errorf("Failed to check if application with ID %s is of legacy type, will use the actual token: %v", app.ID, err) 353 return oneTimeToken.Token 354 } 355 356 if label, ok := appLabels["legacy"]; ok { 357 if isLegacy, ok := (label.Value).(bool); ok && isLegacy { 358 suggestedToken, err := legacyConnectorURLWithToken(s.legacyConnectorURL, oneTimeToken.Token) 359 if err != nil { 360 log.C(ctx).WithError(err).Errorf("Failed to obtain legacy connector URL with token for application with ID %s, will use the actual token: %v", app.ID, err) 361 return oneTimeToken.Token 362 } 363 364 log.C(ctx).Infof("Application with ID %s is of legacy type, will use legacy token URL", app.ID) 365 return suggestedToken 366 } 367 } 368 369 rawEnc, err := rawEncoded(&graphql.TokenWithURL{ 370 Token: oneTimeToken.Token, 371 ConnectorURL: oneTimeToken.ConnectorURL, 372 Used: oneTimeToken.Used, 373 Type: graphql.OneTimeTokenTypeApplication, 374 ExpiresAt: (*graphql.Timestamp)(&oneTimeToken.ExpiresAt), 375 CreatedAt: (*graphql.Timestamp)(&oneTimeToken.CreatedAt), 376 UsedAt: (*graphql.Timestamp)(&oneTimeToken.UsedAt), 377 ScenarioGroups: oneTimeToken.ScenarioGroups, 378 }) 379 if err != nil { 380 log.C(ctx).WithError(err).Errorf("Failed to generade raw encoded one time token for application, will continue with actual token: %v", err) 381 return oneTimeToken.Token 382 } 383 384 return *rawEnc 385 } 386 387 func (s *service) getExpirationDurationForToken(tokenType tokens.TokenType) (time.Duration, error) { 388 switch tokenType { 389 case tokens.ApplicationToken: 390 return s.appTokenExpiration, nil 391 case tokens.RuntimeToken: 392 return s.runtimeTokenExpiration, nil 393 default: 394 return time.Duration(0), errors.Errorf("%s is no valid token type", tokenType) 395 } 396 } 397 398 func (s *service) IsTokenValid(systemAuth *pkgmodel.SystemAuth) (bool, error) { 399 if systemAuth.Value == nil { 400 return false, errors.Errorf("System Auth value for auth id %s is missing", systemAuth.ID) 401 } 402 403 if systemAuth.Value.OneTimeToken == nil { 404 return false, errors.Errorf("One Time Token for system auth id %s is missing", systemAuth.ID) 405 } 406 407 if systemAuth.Value.OneTimeToken.Used { 408 return false, errors.Errorf("One Time Token for system auth id %s has been used", systemAuth.ID) 409 } 410 411 expirationTime, err := s.getExpirationDurationForToken(systemAuth.Value.OneTimeToken.Type) 412 if err != nil { 413 return false, errors.Wrapf(err, "one-time token for system auth id %s has no valid expiration type", systemAuth.ID) 414 } 415 416 isExpired := systemAuth.Value.OneTimeToken.CreatedAt.Add(expirationTime).Before(s.timeService.Now()) 417 if isExpired { 418 return false, errors.Errorf("One Time Token with validity %s for system auth with ID %s has expired", expirationTime.String(), systemAuth.ID) 419 } 420 421 return true, nil 422 }