From 620f0097b6f2fe08837c0336baebea554e9d16d5 Mon Sep 17 00:00:00 2001 From: Anton Bronnikov Date: Wed, 12 Jun 2024 10:46:54 +0300 Subject: [PATCH 1/5] feat: implement `access_token` auth method Signed-off-by: Anton Bronnikov --- README.md | 3 ++- ovh/configuration.go | 30 ++++++++++++++++++++++++------ ovh/configuration_test.go | 4 ++-- ovh/ovh.go | 19 +++++++++++++++++++ ovh/ovh_test.go | 20 ++++++++++++++++++++ 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3adf3b2..0208421 100644 --- a/README.md +++ b/README.md @@ -354,9 +354,10 @@ client.Get("/xdsl/xdsl-yourservice", nil) ### Create a client -- Use ``ovh.NewDefaultClient()`` to create a client unsing endpoint and credentials from config files or environment +- Use ``ovh.NewDefaultClient()`` to create a client using endpoint and credentials from config files or environment - Use ``ovh.NewEndpointClient()`` to create a client for a specific API and use credentials from config files or environment - Use ``ovh.NewOAuth2Client()`` to have full control over their authentication, using OAuth2 authentication method +- Use ``ovh.NewAccessTokenClient()`` to have full control over their authentication, using token that was previously issued by auth/oauth2/token endpoint - Use ``ovh.NewClient()`` to have full control over their authentication, using legacy authentication method ### Query diff --git a/ovh/configuration.go b/ovh/configuration.go index 5983d2f..e4e9d72 100644 --- a/ovh/configuration.go +++ b/ovh/configuration.go @@ -105,6 +105,10 @@ func (c *Client) loadConfig(endpointName string) error { endpointName = getConfigValue(cfg, "default", "endpoint", "ovh-eu") } + if c.AccessToken == "" { + c.AccessToken = getConfigValue(cfg, endpointName, "access_token", "") + } + if c.AppKey == "" { c.AppKey = getConfigValue(cfg, endpointName, "application_key", "") } @@ -125,6 +129,26 @@ func (c *Client) loadConfig(endpointName string) error { c.ClientSecret = getConfigValue(cfg, endpointName, "client_secret", "") } + configuredAuthMethods := []string{} + if c.AppKey != "" || c.AppSecret != "" || c.ConsumerKey != "" { + configuredAuthMethods = append(configuredAuthMethods, "application_key/application_secret") + } + if c.ClientID != "" || c.ClientSecret != "" { + configuredAuthMethods = append(configuredAuthMethods, "client_id/client_secret") + } + if c.AccessToken != "" { + configuredAuthMethods = append(configuredAuthMethods, "access_token") + } + + if len(configuredAuthMethods) > 1 { + return fmt.Errorf("can't use multiple authentication methods: %s", strings.Join(configuredAuthMethods, ", ")) + } + if len(configuredAuthMethods) == 0 { + return errors.New( + "missing authentication information, you need to provide at least an application_key/application_secret, or client_id/client_secret, or access_token", + ) + } + if (c.ClientID != "") != (c.ClientSecret != "") { return errors.New("invalid oauth2 config, both client_id and client_secret must be given") } @@ -132,12 +156,6 @@ func (c *Client) loadConfig(endpointName string) error { return errors.New("invalid authentication config, both application_key and application_secret must be given") } - if c.ClientID != "" && c.AppKey != "" { - return errors.New("can't use both application_key/application_secret and OAuth2 client_id/client_secret") - } else if c.ClientID == "" && c.AppKey == "" { - return errors.New("missing authentication information, you need to provide at least an application_key/application_secret or a client_id/client_secret") - } - // Load real endpoint URL by name. If endpoint contains a '/', consider it as a URL if strings.Contains(endpointName, "/") { c.endpoint = endpointName diff --git a/ovh/configuration_test.go b/ovh/configuration_test.go index c524cf7..1f89467 100644 --- a/ovh/configuration_test.go +++ b/ovh/configuration_test.go @@ -64,7 +64,7 @@ func TestConfigFromNonExistingFile(t *testing.T) { client := Client{} err := client.loadConfig("ovh-eu") - td.CmpString(t, err, `missing authentication information, you need to provide at least an application_key/application_secret or a client_id/client_secret`) + td.CmpString(t, err, `missing authentication information, you need to provide at least an application_key/application_secret, or client_id/client_secret, or access_token`) } func TestConfigFromInvalidINIFile(t *testing.T) { @@ -185,7 +185,7 @@ func TestConfigInvalidBoth(t *testing.T) { client := Client{} err := client.loadConfig("ovh-eu") - td.CmpString(t, err, "can't use both application_key/application_secret and OAuth2 client_id/client_secret") + td.CmpString(t, err, "can't use multiple authentication methods: application_key/application_secret, client_id/client_secret") } func TestConfigOAuth2Invalid(t *testing.T) { diff --git a/ovh/ovh.go b/ovh/ovh.go index 0c47c32..a536340 100644 --- a/ovh/ovh.go +++ b/ovh/ovh.go @@ -60,6 +60,9 @@ var ( // Client represents a client to call the OVH API type Client struct { + // AccessToken is a short-lived access token that we got from auth/oauth2/token endpoint. + AccessToken string + // Self generated tokens. Create one by visiting // https://eu.api.ovh.com/createApp/ // AppKey holds the Application key @@ -141,6 +144,20 @@ func NewOAuth2Client(endpoint, clientID, clientSecret string) (*Client, error) { return &client, nil } +func NewAccessTokenClient(endpoint, accessToken string) (*Client, error) { + client := Client{ + AccessToken: accessToken, + Client: &http.Client{}, + Timeout: DefaultTimeout, + } + + // Get and check the configuration + if err := client.loadConfig(endpoint); err != nil { + return nil, err + } + return &client, nil +} + func (c *Client) Endpoint() string { return c.endpoint } @@ -351,6 +368,8 @@ func (c *Client) NewRequest(method, path string, reqBody interface{}, needAuth b } req.Header.Set("Authorization", "Bearer "+token.AccessToken) + } else if c.AccessToken != "" { + req.Header.Set("Authorization", "Bearer "+c.AccessToken) } } diff --git a/ovh/ovh_test.go b/ovh/ovh_test.go index 48e7fc6..217b813 100644 --- a/ovh/ovh_test.go +++ b/ovh/ovh_test.go @@ -486,6 +486,26 @@ func TestConstructorsOAuth2(t *testing.T) { })) } +func TestConstructorsAccessToken(t *testing.T) { + assert, require := td.AssertRequire(t) + + // Error: missing Endpoint + client, err := NewAccessTokenClient("", "aaaaaaaa") + assert.Nil(client) + assert.String(err, `unknown endpoint '', consider checking 'Endpoints' list or using an URL`) + + // Next: success cases + expected := td.Struct(&Client{ + AccessToken: "aaaaaaaa", + endpoint: "https://eu.api.ovh.com/1.0", + }) + + // Nominal: full constructor + client, err = NewAccessTokenClient("ovh-eu", "aaaaaaaa") + require.CmpNoError(err) + assert.Cmp(client, expected) +} + func (ms *MockSuite) TestVersionInURL(assert, require *td.T) { // Signature checking mocks httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/1.0/call", func(req *http.Request) (*http.Response, error) { From a534dffacfe656be3e9b33719e25ecb6694e2a99 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 12 Jun 2024 18:25:34 +0300 Subject: [PATCH 2/5] review: update error message Co-authored-by: Arthur Amstutz <18141571+amstuta@users.noreply.github.com> Signed-off-by: Anton Bronnikov --- ovh/configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovh/configuration.go b/ovh/configuration.go index e4e9d72..6102f06 100644 --- a/ovh/configuration.go +++ b/ovh/configuration.go @@ -145,7 +145,7 @@ func (c *Client) loadConfig(endpointName string) error { } if len(configuredAuthMethods) == 0 { return errors.New( - "missing authentication information, you need to provide at least an application_key/application_secret, or client_id/client_secret, or access_token", + "missing authentication information, you need to provide one of the following: application_key/application_secret, client_id/client_secret, or access_token", ) } From c0bfeafbb7644c998d27679d563dd61b0db46ce1 Mon Sep 17 00:00:00 2001 From: Anton Bronnikov Date: Wed, 12 Jun 2024 22:27:23 +0300 Subject: [PATCH 3/5] review: add docs for access-token auth method Signed-off-by: Anton Bronnikov --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 0208421..bb834da 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,21 @@ Depending on the API you want to use, you may set the ``endpoint`` to: This lookup mechanism makes it easy to overload credentials for a specific project or user. +### Access Token + +This authentication method is useful when short-lived credentials are necessary. +E.g. oauth2 [plugin](https://github.com/puppetlabs/vault-plugin-secrets-oauthapp) +for hashicorp vault can request an access token that would be used by OVH +terraform provider. Although this token, requested via data-source, would end up +stored in the terraform state-file, that would pose less risk since the token +validity would last for only 1 hour. + +Other applications are of course also possible. + +In order to use the access token with this wrapper either use +`ovh.NewAccessTokenClient` to create the client, or pass the token via +`OVH_ACCESS_TOKEN` environment variable to `ovh.NewDefaultClient`. + ### Application Key/Application Secret If you have completed successfully the __OAuth2__ part, you can continue to From d951cef6691c0ec82fb5d6ab7f79f648504973c5 Mon Sep 17 00:00:00 2001 From: Anton Bronnikov Date: Thu, 13 Jun 2024 11:07:46 +0300 Subject: [PATCH 4/5] fix: update expected error-message Signed-off-by: Anton Bronnikov --- ovh/configuration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovh/configuration_test.go b/ovh/configuration_test.go index 1f89467..5f3140b 100644 --- a/ovh/configuration_test.go +++ b/ovh/configuration_test.go @@ -64,7 +64,7 @@ func TestConfigFromNonExistingFile(t *testing.T) { client := Client{} err := client.loadConfig("ovh-eu") - td.CmpString(t, err, `missing authentication information, you need to provide at least an application_key/application_secret, or client_id/client_secret, or access_token`) + td.CmpString(t, err, `missing authentication information, you need to provide one of the following: application_key/application_secret, client_id/client_secret, or access_token`) } func TestConfigFromInvalidINIFile(t *testing.T) { From bff1e65d6c6269745b618352779d6161394762bd Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 13 Jun 2024 13:19:40 +0300 Subject: [PATCH 5/5] review: update the readme Signed-off-by: Anton Bronnikov Co-authored-by: Adrien Barreau Signed-off-by: Anton Bronnikov --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bb834da..0de4731 100644 --- a/README.md +++ b/README.md @@ -103,9 +103,9 @@ project or user. This authentication method is useful when short-lived credentials are necessary. E.g. oauth2 [plugin](https://github.com/puppetlabs/vault-plugin-secrets-oauthapp) -for hashicorp vault can request an access token that would be used by OVH +for HashiCorp Vault can request an access token that would be used by OVHcloud terraform provider. Although this token, requested via data-source, would end up -stored in the terraform state-file, that would pose less risk since the token +stored in the Terraform state file, that would pose less risk since the token validity would last for only 1 hour. Other applications are of course also possible.