cloudns: Add subuser support (#1098)

This commit is contained in:
Daniel Pfankuchen 2020-03-25 09:34:23 +01:00 committed by GitHub
parent 692dd913ea
commit 6b6ab3fd51
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 112 additions and 20 deletions

View file

@ -305,6 +305,7 @@ func displayDNSHelp(name string) error {
ew.writeln(` - "CLOUDNS_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "CLOUDNS_HTTP_TIMEOUT": API request timeout`)
ew.writeln(` - "CLOUDNS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "CLOUDNS_POLLING_INTERVAL": Time between DNS propagation check`)
ew.writeln(` - "CLOUDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "CLOUDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
ew.writeln(` - "CLOUDNS_SUB_AUTH_ID": The API sub user ID`)
ew.writeln(` - "CLOUDNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "CLOUDNS_TTL": The TTL of the TXT record used for the DNS challenge`)
ew.writeln() ew.writeln()

View file

@ -43,6 +43,7 @@ More information [here](/lego/dns/#configuration-and-credentials).
| `CLOUDNS_HTTP_TIMEOUT` | API request timeout | | `CLOUDNS_HTTP_TIMEOUT` | API request timeout |
| `CLOUDNS_POLLING_INTERVAL` | Time between DNS propagation check | | `CLOUDNS_POLLING_INTERVAL` | Time between DNS propagation check |
| `CLOUDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `CLOUDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
| `CLOUDNS_SUB_AUTH_ID` | The API sub user ID |
| `CLOUDNS_TTL` | The TTL of the TXT record used for the DNS challenge | | `CLOUDNS_TTL` | The TTL of the TXT record used for the DNS challenge |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.

View file

@ -17,6 +17,7 @@ const (
envNamespace = "CLOUDNS_" envNamespace = "CLOUDNS_"
EnvAuthID = envNamespace + "AUTH_ID" EnvAuthID = envNamespace + "AUTH_ID"
EnvSubAuthID = envNamespace + "SUB_AUTH_ID"
EnvAuthPassword = envNamespace + "AUTH_PASSWORD" EnvAuthPassword = envNamespace + "AUTH_PASSWORD"
EnvTTL = envNamespace + "TTL" EnvTTL = envNamespace + "TTL"
@ -28,6 +29,7 @@ const (
// Config is used to configure the creation of the DNSProvider // Config is used to configure the creation of the DNSProvider
type Config struct { type Config struct {
AuthID string AuthID string
SubAuthID string
AuthPassword string AuthPassword string
PropagationTimeout time.Duration PropagationTimeout time.Duration
PollingInterval time.Duration PollingInterval time.Duration
@ -57,13 +59,24 @@ type DNSProvider struct {
// Credentials must be passed in the environment variables: // Credentials must be passed in the environment variables:
// CLOUDNS_AUTH_ID and CLOUDNS_AUTH_PASSWORD. // CLOUDNS_AUTH_ID and CLOUDNS_AUTH_PASSWORD.
func NewDNSProvider() (*DNSProvider, error) { func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvAuthID, EnvAuthPassword) var subAuthID string
authID := env.GetOrFile(EnvAuthID)
if authID == "" {
subAuthID = env.GetOrFile(EnvSubAuthID)
}
if authID == "" && subAuthID == "" {
return nil, fmt.Errorf("ClouDNS: some credentials information are missing: %s or %s", EnvAuthID, EnvSubAuthID)
}
values, err := env.Get(EnvAuthPassword)
if err != nil { if err != nil {
return nil, fmt.Errorf("ClouDNS: %w", err) return nil, fmt.Errorf("ClouDNS: %w", err)
} }
config := NewDefaultConfig() config := NewDefaultConfig()
config.AuthID = values[EnvAuthID] config.AuthID = authID
config.SubAuthID = subAuthID
config.AuthPassword = values[EnvAuthPassword] config.AuthPassword = values[EnvAuthPassword]
return NewDNSProviderConfig(config) return NewDNSProviderConfig(config)
@ -75,7 +88,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return nil, errors.New("ClouDNS: the configuration of the DNS provider is nil") return nil, errors.New("ClouDNS: the configuration of the DNS provider is nil")
} }
client, err := internal.NewClient(config.AuthID, config.AuthPassword) client, err := internal.NewClient(config.AuthID, config.SubAuthID, config.AuthPassword)
if err != nil { if err != nil {
return nil, fmt.Errorf("ClouDNS: %w", err) return nil, fmt.Errorf("ClouDNS: %w", err)
} }

View file

@ -11,6 +11,7 @@ Example = ''''''
CLOUDNS_AUTH_ID = "The API user ID" CLOUDNS_AUTH_ID = "The API user ID"
CLOUDNS_AUTH_PASSWORD = "The password for API user ID" CLOUDNS_AUTH_PASSWORD = "The password for API user ID"
[Configuration.Additional] [Configuration.Additional]
CLOUDNS_SUB_AUTH_ID = "The API sub user ID"
CLOUDNS_POLLING_INTERVAL = "Time between DNS propagation check" CLOUDNS_POLLING_INTERVAL = "Time between DNS propagation check"
CLOUDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" CLOUDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
CLOUDNS_TTL = "The TTL of the TXT record used for the DNS challenge" CLOUDNS_TTL = "The TTL of the TXT record used for the DNS challenge"

View file

@ -12,6 +12,7 @@ const envDomain = envNamespace + "DOMAIN"
var envTest = tester.NewEnvTest( var envTest = tester.NewEnvTest(
EnvAuthID, EnvAuthID,
EnvSubAuthID,
EnvAuthPassword). EnvAuthPassword).
WithDomain(envDomain) WithDomain(envDomain)
@ -22,9 +23,18 @@ func TestNewDNSProvider(t *testing.T) {
expected string expected string
}{ }{
{ {
desc: "success", desc: "success auth-id",
envVars: map[string]string{ envVars: map[string]string{
EnvAuthID: "123", EnvAuthID: "123",
EnvSubAuthID: "",
EnvAuthPassword: "456",
},
},
{
desc: "success sub-auth-id",
envVars: map[string]string{
EnvAuthID: "",
EnvSubAuthID: "123",
EnvAuthPassword: "456", EnvAuthPassword: "456",
}, },
}, },
@ -32,22 +42,34 @@ func TestNewDNSProvider(t *testing.T) {
desc: "missing credentials", desc: "missing credentials",
envVars: map[string]string{ envVars: map[string]string{
EnvAuthID: "", EnvAuthID: "",
EnvSubAuthID: "",
EnvAuthPassword: "", EnvAuthPassword: "",
}, },
expected: "ClouDNS: some credentials information are missing: CLOUDNS_AUTH_ID,CLOUDNS_AUTH_PASSWORD", expected: "ClouDNS: some credentials information are missing: CLOUDNS_AUTH_ID or CLOUDNS_SUB_AUTH_ID",
}, },
{ {
desc: "missing auth-id", desc: "missing auth-id",
envVars: map[string]string{ envVars: map[string]string{
EnvAuthID: "", EnvAuthID: "",
EnvSubAuthID: "",
EnvAuthPassword: "456", EnvAuthPassword: "456",
}, },
expected: "ClouDNS: some credentials information are missing: CLOUDNS_AUTH_ID", expected: "ClouDNS: some credentials information are missing: CLOUDNS_AUTH_ID or CLOUDNS_SUB_AUTH_ID",
},
{
desc: "missing sub-auth-id",
envVars: map[string]string{
EnvAuthID: "",
EnvSubAuthID: "",
EnvAuthPassword: "456",
},
expected: "ClouDNS: some credentials information are missing: CLOUDNS_AUTH_ID or CLOUDNS_SUB_AUTH_ID",
}, },
{ {
desc: "missing auth-password", desc: "missing auth-password",
envVars: map[string]string{ envVars: map[string]string{
EnvAuthID: "123", EnvAuthID: "123",
EnvSubAuthID: "",
EnvAuthPassword: "", EnvAuthPassword: "",
}, },
expected: "ClouDNS: some credentials information are missing: CLOUDNS_AUTH_PASSWORD", expected: "ClouDNS: some credentials information are missing: CLOUDNS_AUTH_PASSWORD",
@ -79,22 +101,39 @@ func TestNewDNSProviderConfig(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
authID string authID string
subAuthID string
authPassword string authPassword string
expected string expected string
}{ }{
{ {
desc: "success", desc: "success auth-id",
authID: "123", authID: "123",
subAuthID: "",
authPassword: "456",
},
{
desc: "success sub-auth-id",
authID: "",
subAuthID: "123",
authPassword: "456", authPassword: "456",
}, },
{ {
desc: "missing credentials", desc: "missing credentials",
expected: "ClouDNS: credentials missing: authID", expected: "ClouDNS: credentials missing: authID or subAuthID",
}, },
{ {
desc: "missing auth-id", desc: "missing auth-id",
authID: "",
subAuthID: "",
authPassword: "456", authPassword: "456",
expected: "ClouDNS: credentials missing: authID", expected: "ClouDNS: credentials missing: authID or subAuthID",
},
{
desc: "missing sub-auth-id",
authID: "",
subAuthID: "",
authPassword: "456",
expected: "ClouDNS: credentials missing: authID or subAuthID",
}, },
{ {
desc: "missing auth-password", desc: "missing auth-password",
@ -107,6 +146,7 @@ func TestNewDNSProviderConfig(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig() config := NewDefaultConfig()
config.AuthID = test.authID config.AuthID = test.authID
config.SubAuthID = test.subAuthID
config.AuthPassword = test.authPassword config.AuthPassword = test.authPassword
p, err := NewDNSProviderConfig(config) p, err := NewDNSProviderConfig(config)

View file

@ -41,9 +41,9 @@ type TXTRecord struct {
type TXTRecords map[string]TXTRecord type TXTRecords map[string]TXTRecord
// NewClient creates a ClouDNS client // NewClient creates a ClouDNS client
func NewClient(authID string, authPassword string) (*Client, error) { func NewClient(authID string, subAuthID string, authPassword string) (*Client, error) {
if authID == "" { if authID == "" && subAuthID == "" {
return nil, errors.New("credentials missing: authID") return nil, errors.New("credentials missing: authID or subAuthID")
} }
if authPassword == "" { if authPassword == "" {
@ -57,6 +57,7 @@ func NewClient(authID string, authPassword string) (*Client, error) {
return &Client{ return &Client{
authID: authID, authID: authID,
subAuthID: subAuthID,
authPassword: authPassword, authPassword: authPassword,
HTTPClient: &http.Client{}, HTTPClient: &http.Client{},
BaseURL: baseURL, BaseURL: baseURL,
@ -66,6 +67,7 @@ func NewClient(authID string, authPassword string) (*Client, error) {
// Client ClouDNS client // Client ClouDNS client
type Client struct { type Client struct {
authID string authID string
subAuthID string
authPassword string authPassword string
HTTPClient *http.Client HTTPClient *http.Client
BaseURL *url.URL BaseURL *url.URL
@ -229,8 +231,15 @@ func (c *Client) doRequest(method string, url *url.URL) (json.RawMessage, error)
func (c *Client) buildRequest(method string, url *url.URL) (*http.Request, error) { func (c *Client) buildRequest(method string, url *url.URL) (*http.Request, error) {
q := url.Query() q := url.Query()
if c.subAuthID != "" {
q.Add("sub-auth-id", c.subAuthID)
} else {
q.Add("auth-id", c.authID) q.Add("auth-id", c.authID)
}
q.Add("auth-password", c.authPassword) q.Add("auth-password", c.authPassword)
url.RawQuery = q.Encode() url.RawQuery = q.Encode()
req, err := http.NewRequest(method, url.String(), nil) req, err := http.NewRequest(method, url.String(), nil)

View file

@ -62,7 +62,7 @@ func TestClientGetZone(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
server := httptest.NewServer(handlerMock(http.MethodGet, test.apiResponse)) server := httptest.NewServer(handlerMock(http.MethodGet, test.apiResponse))
client, _ := NewClient("myAuthID", "myAuthPassword") client, _ := NewClient("myAuthID", "", "myAuthPassword")
mockBaseURL, _ := url.Parse(fmt.Sprintf("%s/", server.URL)) mockBaseURL, _ := url.Parse(fmt.Sprintf("%s/", server.URL))
client.BaseURL = mockBaseURL client.BaseURL = mockBaseURL
@ -140,7 +140,9 @@ func TestClientFindTxtRecord(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
server := httptest.NewServer(handlerMock(http.MethodGet, test.apiResponse)) server := httptest.NewServer(handlerMock(http.MethodGet, test.apiResponse))
client, _ := NewClient("myAuthID", "myAuthPassword") client, err := NewClient("myAuthID", "", "myAuthPassword")
require.NoError(t, err)
mockBaseURL, _ := url.Parse(fmt.Sprintf("%s/", server.URL)) mockBaseURL, _ := url.Parse(fmt.Sprintf("%s/", server.URL))
client.BaseURL = mockBaseURL client.BaseURL = mockBaseURL
@ -164,6 +166,8 @@ func TestClientAddTxtRecord(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
authID string
subAuthID string
zone *Zone zone *Zone
authFQDN string authFQDN string
value string value string
@ -173,6 +177,7 @@ func TestClientAddTxtRecord(t *testing.T) {
}{ }{
{ {
desc: "sub-zone", desc: "sub-zone",
authID: "myAuthID",
zone: &Zone{ zone: &Zone{
Name: "bar.com", Name: "bar.com",
Type: "master", Type: "master",
@ -188,7 +193,8 @@ func TestClientAddTxtRecord(t *testing.T) {
}, },
}, },
{ {
desc: "main zone", desc: "main zone (authID)",
authID: "myAuthID",
zone: &Zone{ zone: &Zone{
Name: "bar.com", Name: "bar.com",
Type: "master", Type: "master",
@ -203,8 +209,27 @@ func TestClientAddTxtRecord(t *testing.T) {
Query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=60`, Query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=60`,
}, },
}, },
{
desc: "main zone (subAuthID)",
authID: "myAuthID",
subAuthID: "mySubAuthID",
zone: &Zone{
Name: "bar.com",
Type: "master",
Zone: "domain",
Status: "1",
},
authFQDN: "_acme-challenge.bar.com.",
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
ttl: 60,
apiResponse: []byte(`{"status":"Success","statusDescription":"The record was added successfully."}`),
expected: expected{
Query: `auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&sub-auth-id=mySubAuthID&ttl=60`,
},
},
{ {
desc: "invalid status", desc: "invalid status",
authID: "myAuthID",
zone: &Zone{ zone: &Zone{
Name: "bar.com", Name: "bar.com",
Type: "master", Type: "master",
@ -231,11 +256,13 @@ func TestClientAddTxtRecord(t *testing.T) {
handlerMock(http.MethodPost, test.apiResponse).ServeHTTP(rw, req) handlerMock(http.MethodPost, test.apiResponse).ServeHTTP(rw, req)
})) }))
client, _ := NewClient("myAuthID", "myAuthPassword") client, err := NewClient(test.authID, test.subAuthID, "myAuthPassword")
require.NoError(t, err)
mockBaseURL, _ := url.Parse(fmt.Sprintf("%s/", server.URL)) mockBaseURL, _ := url.Parse(fmt.Sprintf("%s/", server.URL))
client.BaseURL = mockBaseURL client.BaseURL = mockBaseURL
err := client.AddTxtRecord(test.zone.Name, test.authFQDN, test.value, test.ttl) err = client.AddTxtRecord(test.zone.Name, test.authFQDN, test.value, test.ttl)
if test.expected.Error != "" { if test.expected.Error != "" {
require.EqualError(t, err, test.expected.Error) require.EqualError(t, err, test.expected.Error)