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 }