diff --git a/README.md b/README.md index 3adf3b2..0de4731 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 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 +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 @@ -354,9 +369,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..6102f06 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 one of the following: application_key/application_secret, 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..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 a client_id/client_secret`) + 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) { @@ -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) {