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

     1  package resync
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    15  	"github.com/kyma-incubator/compass/components/director/pkg/cert"
    16  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    17  	"github.com/kyma-incubator/compass/components/director/pkg/oauth"
    18  	bndlErrors "github.com/pkg/errors"
    19  	"golang.org/x/oauth2"
    20  	"golang.org/x/oauth2/clientcredentials"
    21  )
    22  
    23  // OAuth2Config is the auth configuration used by Tenant Events API clients.
    24  type OAuth2Config struct {
    25  	X509Config
    26  	ClientID           string
    27  	ClientSecret       string
    28  	OAuthTokenEndpoint string
    29  	TokenPath          string
    30  	SkipSSLValidation  bool
    31  }
    32  
    33  // AuthProviderConfig  is the configuration of the authentication secret used by tenants aggregator.
    34  // The auth secret contains auth details for different regions. Each region reads its auth config from the secret file by given a specific key.
    35  type AuthProviderConfig struct {
    36  	AuthMappingConfig
    37  
    38  	SecretFilePath    string `envconfig:"FILE_PATH" default:"/tmp/keyConfig"`
    39  	TokenPath         string `envconfig:"TOKEN_PATH" required:"true"`
    40  	SkipSSLValidation bool   `envconfig:"OAUTH_SKIP_SSL_VALIDATION" default:"false"`
    41  }
    42  
    43  // AuthMappingConfig is the mapping configuration between auth details and their paths in the auth secret file.
    44  type AuthMappingConfig struct {
    45  	ClientIDPath      string `envconfig:"CLIENT_ID_PATH" required:"true"`
    46  	ClientSecretPath  string `envconfig:"CLIENT_SECRET_PATH"`
    47  	TokenEndpointPath string `envconfig:"TOKEN_ENDPOINT_PATH" required:"true"`
    48  	CertPath          string `envconfig:"CERT_PATH"`
    49  	KeyPath           string `envconfig:"CERT_KEY_PATH"`
    50  }
    51  
    52  // Validate checks if the configuration is considered valid against the given auth mode.
    53  // The configuration is considered valid if it contains all needed auth details for the given mode.
    54  func (c OAuth2Config) Validate(oauthMode oauth.AuthMode) error {
    55  	missingProperties := make([]string, 0)
    56  	if len(c.ClientID) == 0 {
    57  		missingProperties = append(missingProperties, "ClientID")
    58  	}
    59  	if len(c.OAuthTokenEndpoint) == 0 {
    60  		missingProperties = append(missingProperties, "OAuthTokenEndpoint")
    61  	}
    62  
    63  	switch oauthMode {
    64  	case oauth.Standard:
    65  		if len(c.ClientSecret) == 0 {
    66  			missingProperties = append(missingProperties, "ClientSecret")
    67  		}
    68  	case oauth.Mtls:
    69  		if len(c.Cert) == 0 {
    70  			missingProperties = append(missingProperties, "Certificate")
    71  		}
    72  		if len(c.Key) == 0 {
    73  			missingProperties = append(missingProperties, "CertificateKey")
    74  		}
    75  	}
    76  
    77  	if len(missingProperties) > 0 {
    78  		return fmt.Errorf("missing API Client Auth config properties: %s", strings.Join(missingProperties, ","))
    79  	}
    80  
    81  	return nil
    82  }
    83  
    84  // X509Config is X509 configuration for getting an OAuth token via mtls
    85  // same as struct in pkg/oauth but with different envconfig
    86  type X509Config struct {
    87  	Cert string
    88  	Key  string
    89  }
    90  
    91  // ParseCertificate parses the TLS certificate contained in the X509Config
    92  func (c *X509Config) ParseCertificate() (*tls.Certificate, error) {
    93  	return cert.ParseCertificate(c.Cert, c.Key)
    94  }
    95  
    96  // APIEndpointsConfig missing godoc
    97  type APIEndpointsConfig struct {
    98  	EndpointTenantCreated     string `envconfig:"ENDPOINT_TENANT_CREATED"`
    99  	EndpointTenantDeleted     string `envconfig:"ENDPOINT_TENANT_DELETED"`
   100  	EndpointTenantUpdated     string `envconfig:"ENDPOINT_TENANT_UPDATED"`
   101  	EndpointSubaccountCreated string `envconfig:"ENDPOINT_SUBACCOUNT_CREATED"`
   102  	EndpointSubaccountDeleted string `envconfig:"ENDPOINT_SUBACCOUNT_DELETED"`
   103  	EndpointSubaccountUpdated string `envconfig:"ENDPOINT_SUBACCOUNT_UPDATED"`
   104  	EndpointSubaccountMoved   string `envconfig:"ENDPOINT_SUBACCOUNT_MOVED"`
   105  }
   106  
   107  func (c APIEndpointsConfig) isUnassignedOptionalProperty(eventsType EventsType) bool {
   108  	return eventsType == MovedSubaccountType && len(c.EndpointSubaccountMoved) == 0
   109  }
   110  
   111  // QueryParams describes the key and the corresponding value for query parameters when requesting the service
   112  type QueryParams map[string]string
   113  
   114  // Client implements the communication with the service
   115  type Client struct {
   116  	config     ClientConfig
   117  	httpClient *http.Client
   118  }
   119  
   120  // ClientConfig is the client specific configuration of the Events API
   121  type ClientConfig struct {
   122  	TenantProvider      string
   123  	APIConfig           APIEndpointsConfig
   124  	FieldMapping        TenantFieldMapping
   125  	MovedSAFieldMapping MovedSubaccountsFieldMapping
   126  }
   127  
   128  // NewClient missing godoc
   129  func NewClient(oAuth2Config OAuth2Config, authMode oauth.AuthMode, clientConfig ClientConfig, timeout time.Duration) (*Client, error) {
   130  	ctx := context.Background()
   131  	cfg := clientcredentials.Config{
   132  		ClientID:     oAuth2Config.ClientID,
   133  		ClientSecret: oAuth2Config.ClientSecret,
   134  		TokenURL:     oAuth2Config.OAuthTokenEndpoint + oAuth2Config.TokenPath,
   135  	}
   136  
   137  	switch authMode {
   138  	case oauth.Standard:
   139  		// do nothing
   140  	case oauth.Mtls:
   141  		cert, err := oAuth2Config.X509Config.ParseCertificate()
   142  		if nil != err {
   143  			return nil, err
   144  		}
   145  
   146  		// When the auth style is InParams, the TokenSource
   147  		// will not add the clientSecret if it's empty
   148  		cfg.AuthStyle = oauth2.AuthStyleInParams
   149  		cfg.ClientSecret = ""
   150  
   151  		transport := &http.Transport{
   152  			TLSClientConfig: &tls.Config{
   153  				Certificates:       []tls.Certificate{*cert},
   154  				InsecureSkipVerify: oAuth2Config.SkipSSLValidation,
   155  			},
   156  		}
   157  
   158  		mtlClient := &http.Client{
   159  			Transport: transport,
   160  			Timeout:   timeout,
   161  		}
   162  
   163  		ctx = context.WithValue(ctx, oauth2.HTTPClient, mtlClient)
   164  	default:
   165  		return nil, errors.New("unsupported auth mode:" + string(authMode))
   166  	}
   167  
   168  	httpClient := cfg.Client(ctx)
   169  	httpClient.Timeout = timeout
   170  
   171  	return &Client{
   172  		httpClient: httpClient,
   173  		config:     clientConfig,
   174  	}, nil
   175  }
   176  
   177  // FetchTenantEventsPage missing godoc
   178  func (c *Client) FetchTenantEventsPage(ctx context.Context, eventsType EventsType, additionalQueryParams QueryParams) (*EventsPage, error) {
   179  	if c.config.APIConfig.isUnassignedOptionalProperty(eventsType) {
   180  		log.C(ctx).Warnf("Optional property for event type %s was not set", eventsType)
   181  		return nil, nil
   182  	}
   183  
   184  	endpoint, err := c.getEndpointForEventsType(eventsType)
   185  	if endpoint == "" && err == nil {
   186  		log.C(ctx).Warnf("Endpoint for event %s is not set", eventsType)
   187  		return nil, nil
   188  	}
   189  
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  
   194  	reqURL, err := c.buildRequestURL(endpoint, additionalQueryParams)
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  
   199  	res, err := c.httpClient.Get(reqURL)
   200  	if err != nil {
   201  		return nil, bndlErrors.Wrap(err, "while sending get request")
   202  	}
   203  	defer func() {
   204  		err := res.Body.Close()
   205  		if err != nil {
   206  			log.C(ctx).Warnf("Unable to close response body. Cause: %v", err)
   207  		}
   208  	}()
   209  
   210  	bytes, err := io.ReadAll(res.Body)
   211  	if err != nil {
   212  		return nil, bndlErrors.Wrap(err, "while reading response body")
   213  	}
   214  
   215  	if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNoContent {
   216  		return nil, fmt.Errorf("request to %q returned status code %d and body %q", reqURL, res.StatusCode, bytes)
   217  	}
   218  
   219  	if len(bytes) == 0 {
   220  		return nil, nil
   221  	}
   222  
   223  	return &EventsPage{
   224  		FieldMapping:                 c.config.FieldMapping,
   225  		MovedSubaccountsFieldMapping: c.config.MovedSAFieldMapping,
   226  		ProviderName:                 c.config.TenantProvider,
   227  		Payload:                      bytes,
   228  	}, nil
   229  }
   230  
   231  // SetHTTPClient sets the underlying HTTP client
   232  func (c *Client) SetHTTPClient(client *http.Client) {
   233  	c.httpClient = client
   234  }
   235  
   236  // GetHTTPClient returns the underlying HTTP client
   237  func (c *Client) GetHTTPClient() *http.Client {
   238  	return c.httpClient
   239  }
   240  
   241  func (c *Client) getEndpointForEventsType(eventsType EventsType) (string, error) {
   242  	switch eventsType {
   243  	case CreatedAccountType:
   244  		return c.config.APIConfig.EndpointTenantCreated, nil
   245  	case DeletedAccountType:
   246  		return c.config.APIConfig.EndpointTenantDeleted, nil
   247  	case UpdatedAccountType:
   248  		return c.config.APIConfig.EndpointTenantUpdated, nil
   249  	case CreatedSubaccountType:
   250  		return c.config.APIConfig.EndpointSubaccountCreated, nil
   251  	case DeletedSubaccountType:
   252  		return c.config.APIConfig.EndpointSubaccountDeleted, nil
   253  	case UpdatedSubaccountType:
   254  		return c.config.APIConfig.EndpointSubaccountUpdated, nil
   255  	case MovedSubaccountType:
   256  		return c.config.APIConfig.EndpointSubaccountMoved, nil
   257  	default:
   258  		return "", apperrors.NewInternalError("unknown events type")
   259  	}
   260  }
   261  
   262  func (c *Client) buildRequestURL(endpoint string, queryParams QueryParams) (string, error) {
   263  	u, err := url.Parse(endpoint)
   264  	if err != nil {
   265  		return "", err
   266  	}
   267  
   268  	q, err := url.ParseQuery(u.RawQuery)
   269  	if err != nil {
   270  		return "", err
   271  	}
   272  
   273  	for qKey, qValue := range queryParams {
   274  		q.Add(qKey, qValue)
   275  	}
   276  
   277  	u.RawQuery = q.Encode()
   278  
   279  	return u.String(), nil
   280  }