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  }