Skip to content

Commit

Permalink
Merge pull request #83 from 0x416e746f6e/feat/access-token-auth
Browse files Browse the repository at this point in the history
feat: implement `access_token` auth method
  • Loading branch information
amstuta authored Jun 13, 2024
2 parents 3b83768 + bff1e65 commit 6817886
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 9 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
30 changes: 24 additions & 6 deletions ovh/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")
}
Expand All @@ -125,19 +129,33 @@ 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")
}
if (c.AppKey != "") != (c.AppSecret != "") {
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
Expand Down
4 changes: 2 additions & 2 deletions ovh/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
19 changes: 19 additions & 0 deletions ovh/ovh.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
}

Expand Down
20 changes: 20 additions & 0 deletions ovh/ovh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 6817886

Please sign in to comment.