Skip to content

Commit

Permalink
ovh: add OAuth2 authentication (#2173)
Browse files Browse the repository at this point in the history
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
  • Loading branch information
Idix and ldez authored May 6, 2024
1 parent 42aa57e commit acd3382
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 69 deletions.
8 changes: 5 additions & 3 deletions cmd/zz_gen_cmd_dnshelp.go
Original file line number Diff line number Diff line change
Expand Up @@ -2099,9 +2099,11 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln()

ew.writeln(`Credentials:`)
ew.writeln(` - "OVH_APPLICATION_KEY": Application key`)
ew.writeln(` - "OVH_APPLICATION_SECRET": Application secret`)
ew.writeln(` - "OVH_CONSUMER_KEY": Consumer key`)
ew.writeln(` - "OVH_APPLICATION_KEY": Application key (Application Key authentication)`)
ew.writeln(` - "OVH_APPLICATION_SECRET": Application secret (Application Key authentication)`)
ew.writeln(` - "OVH_CLIENT_ID": Client ID (OAuth2)`)
ew.writeln(` - "OVH_CLIENT_SECRET": Client secret (OAuth2)`)
ew.writeln(` - "OVH_CONSUMER_KEY": Consumer key (Application Key authentication)`)
ew.writeln(` - "OVH_ENDPOINT": Endpoint URL (ovh-eu or ovh-ca)`)
ew.writeln()

Expand Down
33 changes: 30 additions & 3 deletions docs/content/dns/zz_gen_ovh.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,20 @@ Configuration for [OVH](https://www.ovh.com/).
Here is an example bash command using the OVH provider:

```bash
# Application Key authentication:

OVH_APPLICATION_KEY=1234567898765432 \
OVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \
OVH_CONSUMER_KEY=256vfsd347245sdfg \
OVH_ENDPOINT=ovh-eu \
lego --email you@example.com --dns ovh --domains my.example.org run

# Or OAuth2:

OVH_CLIENT_ID=yyy \
OVH_CLIENT_SECRET=xxx \
OVH_ENDPOINT=ovh-eu \
lego --email you@example.com --dns ovh --domains my.example.org run
```


Expand All @@ -40,9 +49,11 @@ lego --email you@example.com --dns ovh --domains my.example.org run

| Environment Variable Name | Description |
|-----------------------|-------------|
| `OVH_APPLICATION_KEY` | Application key |
| `OVH_APPLICATION_SECRET` | Application secret |
| `OVH_CONSUMER_KEY` | Consumer key |
| `OVH_APPLICATION_KEY` | Application key (Application Key authentication) |
| `OVH_APPLICATION_SECRET` | Application secret (Application Key authentication) |
| `OVH_CLIENT_ID` | Client ID (OAuth2) |
| `OVH_CLIENT_SECRET` | Client secret (OAuth2) |
| `OVH_CONSUMER_KEY` | Consumer key (Application Key authentication) |
| `OVH_ENDPOINT` | Endpoint URL (ovh-eu or ovh-ca) |

The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
Expand Down Expand Up @@ -82,6 +93,22 @@ When requesting the consumer key, the following configuration can be used to def
}
```

## OAuth2 Client Credentials

Another method for authentication is by using OAuth2 client credentials.

An IAM policy and service account can be created by following the [OVH guide](https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343).

Following IAM policies need to be authorized for the affected domain:

* dnsZone:apiovh:record/create
* dnsZone:apiovh:record/delete
* dnsZone:apiovh:refresh

## Important Note

Both authentication methods cannot be used at the same time.



## More information
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ require (
github.com/nrdcg/porkbun v0.3.0
github.com/nzdjb/go-metaname v1.0.0
github.com/oracle/oci-go-sdk/v65 v65.63.1
github.com/ovh/go-ovh v1.4.3
github.com/ovh/go-ovh v1.5.1
github.com/pquerna/otp v1.4.0
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2
github.com/sacloud/api-client-go v0.2.10
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -524,8 +524,8 @@ github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:Ff
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/oracle/oci-go-sdk/v65 v65.63.1 h1:dYL7sk9L1+C9LCmoq+zjPMNteuJJfk54YExq/4pV9xQ=
github.com/oracle/oci-go-sdk/v65 v65.63.1/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0=
github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY=
github.com/ovh/go-ovh v1.5.1 h1:P8O+7H+NQuFK9P/j4sFW5C0fvSS2DnHYGPwdVCp45wI=
github.com/ovh/go-ovh v1.5.1/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
Expand Down
154 changes: 130 additions & 24 deletions providers/dns/ovh/ovh.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,34 @@ import (
)

// OVH API reference: https://eu.api.ovh.com/
// Create a Token: https://eu.api.ovh.com/createToken/
// Create a Token: https://eu.api.ovh.com/createToken/
// Create a OAuth2 client: https://eu.api.ovh.com/console-preview/?section=%2Fme&branch=v1#post-/me/api/oauth2/client

// Environment variables names.
const (
envNamespace = "OVH_"

EnvEndpoint = envNamespace + "ENDPOINT"
EnvApplicationKey = envNamespace + "APPLICATION_KEY"
EnvApplicationSecret = envNamespace + "APPLICATION_SECRET"
EnvConsumerKey = envNamespace + "CONSUMER_KEY"
EnvEndpoint = envNamespace + "ENDPOINT"

EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)

// Authenticate using application key.
const (
EnvApplicationKey = envNamespace + "APPLICATION_KEY"
EnvApplicationSecret = envNamespace + "APPLICATION_SECRET"
EnvConsumerKey = envNamespace + "CONSUMER_KEY"
)

// Authenticate using OAuth2 client.
const (
EnvClientID = envNamespace + "CLIENT_ID"
EnvClientSecret = envNamespace + "CLIENT_SECRET"
)

// Record a DNS record.
type Record struct {
ID int64 `json:"id,omitempty"`
Expand All @@ -41,18 +52,32 @@ type Record struct {
Zone string `json:"zone,omitempty"`
}

// OAuth2Config the OAuth2 specific configuration.
type OAuth2Config struct {
ClientID string
ClientSecret string
}

// Config is used to configure the creation of the DNSProvider.
type Config struct {
APIEndpoint string
ApplicationKey string
ApplicationSecret string
ConsumerKey string
APIEndpoint string

ApplicationKey string
ApplicationSecret string
ConsumerKey string

OAuth2Config *OAuth2Config

PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}

func (c *Config) hasAppKeyAuth() bool {
return c.ApplicationKey != "" || c.ApplicationSecret != "" || c.ConsumerKey != ""
}

// NewDefaultConfig returns a default configuration for the DNSProvider.
func NewDefaultConfig() *Config {
return &Config{
Expand All @@ -77,17 +102,11 @@ type DNSProvider struct {
// Credentials must be passed in the environment variables:
// OVH_ENDPOINT (must be either "ovh-eu" or "ovh-ca"), OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvEndpoint, EnvApplicationKey, EnvApplicationSecret, EnvConsumerKey)
config, err := createConfigFromEnvVars()
if err != nil {
return nil, fmt.Errorf("ovh: %w", err)
}

config := NewDefaultConfig()
config.APIEndpoint = values[EnvEndpoint]
config.ApplicationKey = values[EnvApplicationKey]
config.ApplicationSecret = values[EnvApplicationSecret]
config.ConsumerKey = values[EnvConsumerKey]

return NewDNSProviderConfig(config)
}

Expand All @@ -97,16 +116,11 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return nil, errors.New("ovh: the configuration of the DNS provider is nil")
}

if config.APIEndpoint == "" || config.ApplicationKey == "" || config.ApplicationSecret == "" || config.ConsumerKey == "" {
return nil, errors.New("ovh: credentials missing")
if config.OAuth2Config != nil && config.hasAppKeyAuth() {
return nil, errors.New("ovh: can't use both authentication systems (ApplicationKey and OAuth2)")
}

client, err := ovh.NewClient(
config.APIEndpoint,
config.ApplicationKey,
config.ApplicationSecret,
config.ConsumerKey,
)
client, err := newClient(config)
if err != nil {
return nil, fmt.Errorf("ovh: %w", err)
}
Expand Down Expand Up @@ -207,3 +221,95 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}

func createConfigFromEnvVars() (*Config, error) {
firstAppKeyEnvVar := findFirstValuedEnvVar(EnvApplicationKey, EnvApplicationSecret, EnvConsumerKey)
firstOAuth2EnvVar := findFirstValuedEnvVar(EnvClientID, EnvClientSecret)

if firstAppKeyEnvVar != "" && firstOAuth2EnvVar != "" {
return nil, fmt.Errorf("can't use both %s and %s at the same time", firstAppKeyEnvVar, firstOAuth2EnvVar)
}

config := NewDefaultConfig()

if firstOAuth2EnvVar != "" {
values, err := env.Get(EnvEndpoint, EnvClientID, EnvClientSecret)
if err != nil {
return nil, err
}

config.APIEndpoint = values[EnvEndpoint]
config.OAuth2Config = &OAuth2Config{
ClientID: values[EnvClientID],
ClientSecret: values[EnvClientSecret],
}

return config, nil
}

values, err := env.Get(EnvEndpoint, EnvApplicationKey, EnvApplicationSecret, EnvConsumerKey)
if err != nil {
return nil, err
}

config.APIEndpoint = values[EnvEndpoint]

config.ApplicationKey = values[EnvApplicationKey]
config.ApplicationSecret = values[EnvApplicationSecret]
config.ConsumerKey = values[EnvConsumerKey]

return config, nil
}

func findFirstValuedEnvVar(envVars ...string) string {
for _, envVar := range envVars {
if env.GetOrFile(envVar) != "" {
return envVar
}
}

return ""
}

func newClient(config *Config) (*ovh.Client, error) {
if config.OAuth2Config == nil {
return newClientApplicationKey(config)
}

return newClientOAuth2(config)
}

func newClientApplicationKey(config *Config) (*ovh.Client, error) {
if config.APIEndpoint == "" || config.ApplicationKey == "" || config.ApplicationSecret == "" || config.ConsumerKey == "" {
return nil, errors.New("credentials are missing")
}

client, err := ovh.NewClient(
config.APIEndpoint,
config.ApplicationKey,
config.ApplicationSecret,
config.ConsumerKey,
)
if err != nil {
return nil, fmt.Errorf("new client: %w", err)
}

return client, nil
}

func newClientOAuth2(config *Config) (*ovh.Client, error) {
if config.APIEndpoint == "" || config.OAuth2Config.ClientID == "" || config.OAuth2Config.ClientSecret == "" {
return nil, errors.New("credentials are missing")
}

client, err := ovh.NewOAuth2Client(
config.APIEndpoint,
config.OAuth2Config.ClientID,
config.OAuth2Config.ClientSecret,
)
if err != nil {
return nil, fmt.Errorf("new OAuth2 client: %w", err)
}

return client, nil
}
33 changes: 30 additions & 3 deletions providers/dns/ovh/ovh.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@ Code = "ovh"
Since = "v0.4.0"

Example = '''
# Application Key authentication:
OVH_APPLICATION_KEY=1234567898765432 \
OVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \
OVH_CONSUMER_KEY=256vfsd347245sdfg \
OVH_ENDPOINT=ovh-eu \
lego --email you@example.com --dns ovh --domains my.example.org run
# Or OAuth2:
OVH_CLIENT_ID=yyy \
OVH_CLIENT_SECRET=xxx \
OVH_ENDPOINT=ovh-eu \
lego --email you@example.com --dns ovh --domains my.example.org run
'''

Additional = '''
Expand All @@ -33,14 +42,32 @@ When requesting the consumer key, the following configuration can be used to def
]
}
```
## OAuth2 Client Credentials
Another method for authentication is by using OAuth2 client credentials.
An IAM policy and service account can be created by following the [OVH guide](https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343).
Following IAM policies need to be authorized for the affected domain:
* dnsZone:apiovh:record/create
* dnsZone:apiovh:record/delete
* dnsZone:apiovh:refresh
## Important Note
Both authentication methods cannot be used at the same time.
'''

[Configuration]
[Configuration.Credentials]
OVH_ENDPOINT = "Endpoint URL (ovh-eu or ovh-ca)"
OVH_APPLICATION_KEY = "Application key"
OVH_APPLICATION_SECRET = "Application secret"
OVH_CONSUMER_KEY = "Consumer key"
OVH_APPLICATION_KEY = "Application key (Application Key authentication)"
OVH_APPLICATION_SECRET = "Application secret (Application Key authentication)"
OVH_CONSUMER_KEY = "Consumer key (Application Key authentication)"
OVH_CLIENT_ID = "Client ID (OAuth2)"
OVH_CLIENT_SECRET = "Client secret (OAuth2)"
[Configuration.Additional]
OVH_POLLING_INTERVAL = "Time between DNS propagation check"
OVH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
Expand Down
Loading

0 comments on commit acd3382

Please sign in to comment.