Compare commits

...

2 commits

Author SHA1 Message Date
Igor Zornik 79a19a7faf
Merge 2c3185c7c5 into acd338259d 2024-05-06 15:44:15 +02:00
Idix acd338259d
ovh: add OAuth2 authentication (#2173)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2024-05-06 15:43:25 +02:00
7 changed files with 330 additions and 69 deletions

View file

@ -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()

View file

@ -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
```
@ -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.
@ -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

2
go.mod
View file

@ -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

4
go.sum
View file

@ -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=

View file

@ -14,16 +14,14 @@ 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"
@ -31,6 +29,19 @@ const (
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"`
@ -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{
@ -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)
}
@ -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)
}
@ -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
}

View file

@ -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 = '''
@ -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"

View file

@ -14,7 +14,9 @@ var envTest = tester.NewEnvTest(
EnvEndpoint,
EnvApplicationKey,
EnvApplicationSecret,
EnvConsumerKey).
EnvConsumerKey,
EnvClientID,
EnvClientSecret).
WithDomain(envDomain)
func TestNewDNSProvider(t *testing.T) {
@ -24,7 +26,7 @@ func TestNewDNSProvider(t *testing.T) {
expected string
}{
{
desc: "success",
desc: "application key: success",
envVars: map[string]string{
EnvEndpoint: "ovh-eu",
EnvApplicationKey: "B",
@ -33,17 +35,7 @@ func TestNewDNSProvider(t *testing.T) {
},
},
{
desc: "missing credentials",
envVars: map[string]string{
EnvEndpoint: "",
EnvApplicationKey: "",
EnvApplicationSecret: "",
EnvConsumerKey: "",
},
expected: "ovh: some credentials information are missing: OVH_ENDPOINT,OVH_APPLICATION_KEY,OVH_APPLICATION_SECRET,OVH_CONSUMER_KEY",
},
{
desc: "missing endpoint",
desc: "application key: missing endpoint",
envVars: map[string]string{
EnvEndpoint: "",
EnvApplicationKey: "B",
@ -53,17 +45,17 @@ func TestNewDNSProvider(t *testing.T) {
expected: "ovh: some credentials information are missing: OVH_ENDPOINT",
},
{
desc: "missing invalid endpoint",
desc: "application key: missing invalid endpoint",
envVars: map[string]string{
EnvEndpoint: "foobar",
EnvApplicationKey: "B",
EnvApplicationSecret: "C",
EnvConsumerKey: "D",
},
expected: "ovh: unknown endpoint 'foobar', consider checking 'Endpoints' list of using an URL",
expected: "ovh: new client: unknown endpoint 'foobar', consider checking 'Endpoints' list or using an URL",
},
{
desc: "missing application key",
desc: "application key: missing application key",
envVars: map[string]string{
EnvEndpoint: "ovh-eu",
EnvApplicationKey: "",
@ -73,7 +65,7 @@ func TestNewDNSProvider(t *testing.T) {
expected: "ovh: some credentials information are missing: OVH_APPLICATION_KEY",
},
{
desc: "missing application secret",
desc: "application key: missing application secret",
envVars: map[string]string{
EnvEndpoint: "ovh-eu",
EnvApplicationKey: "B",
@ -83,7 +75,7 @@ func TestNewDNSProvider(t *testing.T) {
expected: "ovh: some credentials information are missing: OVH_APPLICATION_SECRET",
},
{
desc: "missing consumer key",
desc: "application key: missing consumer key",
envVars: map[string]string{
EnvEndpoint: "ovh-eu",
EnvApplicationKey: "B",
@ -92,6 +84,56 @@ func TestNewDNSProvider(t *testing.T) {
},
expected: "ovh: some credentials information are missing: OVH_CONSUMER_KEY",
},
{
desc: "oauth2: success",
envVars: map[string]string{
EnvEndpoint: "ovh-eu",
EnvClientID: "E",
EnvClientSecret: "F",
},
},
{
desc: "oauth2: missing client secret",
envVars: map[string]string{
EnvEndpoint: "ovh-eu",
EnvClientID: "E",
EnvClientSecret: "",
},
expected: "ovh: some credentials information are missing: OVH_CLIENT_SECRET",
},
{
desc: "oauth2: missing client ID",
envVars: map[string]string{
EnvEndpoint: "ovh-eu",
EnvClientID: "",
EnvClientSecret: "F",
},
expected: "ovh: some credentials information are missing: OVH_CLIENT_ID",
},
{
desc: "missing credentials",
envVars: map[string]string{
EnvEndpoint: "",
EnvApplicationKey: "",
EnvApplicationSecret: "",
EnvConsumerKey: "",
EnvClientID: "",
EnvClientSecret: "",
},
expected: "ovh: some credentials information are missing: OVH_ENDPOINT,OVH_APPLICATION_KEY,OVH_APPLICATION_SECRET,OVH_CONSUMER_KEY",
},
{
desc: "mixed auth",
envVars: map[string]string{
EnvEndpoint: "ovh-eu",
EnvApplicationKey: "B",
EnvApplicationSecret: "C",
EnvConsumerKey: "D",
EnvClientID: "E",
EnvClientSecret: "F",
},
expected: "ovh: can't use both OVH_APPLICATION_KEY and OVH_CLIENT_ID at the same time",
},
}
for _, test := range testCases {
@ -123,61 +165,111 @@ func TestNewDNSProviderConfig(t *testing.T) {
applicationKey string
applicationSecret string
consumerKey string
clientID string
clientSecret string
expected string
}{
{
desc: "success",
desc: "application key: success",
apiEndpoint: "ovh-eu",
applicationKey: "B",
applicationSecret: "C",
consumerKey: "D",
},
{
desc: "missing credentials",
expected: "ovh: credentials missing",
},
{
desc: "missing api endpoint",
desc: "application key: missing api endpoint",
apiEndpoint: "",
applicationKey: "B",
applicationSecret: "C",
consumerKey: "D",
expected: "ovh: credentials missing",
expected: "ovh: credentials are missing",
},
{
desc: "missing invalid api endpoint",
desc: "application key: invalid api endpoint",
apiEndpoint: "foobar",
applicationKey: "B",
applicationSecret: "C",
consumerKey: "D",
expected: "ovh: unknown endpoint 'foobar', consider checking 'Endpoints' list of using an URL",
expected: "ovh: new client: unknown endpoint 'foobar', consider checking 'Endpoints' list or using an URL",
},
{
desc: "missing application key",
desc: "application key: missing application key",
apiEndpoint: "ovh-eu",
applicationKey: "",
applicationSecret: "C",
consumerKey: "D",
expected: "ovh: credentials missing",
expected: "ovh: credentials are missing",
},
{
desc: "missing application secret",
desc: "application key: missing application secret",
apiEndpoint: "ovh-eu",
applicationKey: "B",
applicationSecret: "",
consumerKey: "D",
expected: "ovh: credentials missing",
expected: "ovh: credentials are missing",
},
{
desc: "missing consumer key",
desc: "application key: missing consumer key",
apiEndpoint: "ovh-eu",
applicationKey: "B",
applicationSecret: "C",
consumerKey: "",
expected: "ovh: credentials missing",
expected: "ovh: credentials are missing",
},
{
desc: "oauth2: success",
apiEndpoint: "ovh-eu",
clientID: "B",
clientSecret: "C",
},
{
desc: "oauth2: missing api endpoint",
apiEndpoint: "",
clientID: "B",
clientSecret: "C",
expected: "ovh: credentials are missing",
},
{
desc: "oauth2: invalid api endpoint",
apiEndpoint: "foobar",
clientID: "B",
clientSecret: "C",
expected: "ovh: new OAuth2 client: unknown endpoint 'foobar', consider checking 'Endpoints' list or using an URL",
},
{
desc: "oauth2: missing client id",
apiEndpoint: "ovh-eu",
clientID: "",
clientSecret: "C",
expected: "ovh: credentials are missing",
},
{
desc: "oauth2: missing client secret",
apiEndpoint: "ovh-eu",
clientID: "B",
clientSecret: "",
expected: "ovh: credentials are missing",
},
{
desc: "missing credentials",
expected: "ovh: credentials are missing",
},
{
desc: "mixed auth",
apiEndpoint: "ovh-eu",
applicationKey: "B",
applicationSecret: "C",
consumerKey: "D",
clientID: "B",
clientSecret: "C",
expected: "ovh: can't use both authentication systems (ApplicationKey and OAuth2)",
},
}
// The OVH client use the same env vars than lego, so it requires to clean them.
defer envTest.RestoreEnv()
envTest.ClearEnv()
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig()
@ -186,6 +278,13 @@ func TestNewDNSProviderConfig(t *testing.T) {
config.ApplicationSecret = test.applicationSecret
config.ConsumerKey = test.consumerKey
if test.clientID != "" || test.clientSecret != "" {
config.OAuth2Config = &OAuth2Config{
ClientID: test.clientID,
ClientSecret: test.clientSecret,
}
}
p, err := NewDNSProviderConfig(config)
if test.expected == "" {