From a5a29187fea9f555ea43fae16ca65066c452d330 Mon Sep 17 00:00:00 2001 From: Jarred Trainor Date: Sun, 1 Sep 2019 05:32:20 -0700 Subject: [PATCH] cloudflare: add support for API tokens (#937) --- cmd/zz_gen_cmd_dnshelp.go | 4 +- docs/content/dns/zz_gen_cloudflare.md | 24 +++++++++++- providers/dns/cloudflare/cloudflare.go | 25 +++++++++++-- providers/dns/cloudflare/cloudflare.toml | 24 +++++++++++- providers/dns/cloudflare/cloudflare_test.go | 41 +++++++++++++++++---- 5 files changed, 101 insertions(+), 17 deletions(-) diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index c3383c6b..7e5c45ee 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -220,7 +220,9 @@ func displayDNSHelp(name string) error { ew.writeln(`Credentials:`) ew.writeln(` - "CF_API_EMAIL": Account email`) ew.writeln(` - "CF_API_KEY": API key`) - ew.writeln(` - "CLOUDFLARE_API_KEY": Alias to CLOUDFLARE_API_KEY`) + ew.writeln(` - "CF_API_TOKEN": API token`) + ew.writeln(` - "CLOUDFLARE_API_KEY": Alias to CF_API_KEY`) + ew.writeln(` - "CLOUDFLARE_API_TOKEN": Alias to CF_API_TOKEN`) ew.writeln(` - "CLOUDFLARE_EMAIL": Alias to CF_API_EMAIL`) ew.writeln() diff --git a/docs/content/dns/zz_gen_cloudflare.md b/docs/content/dns/zz_gen_cloudflare.md index a030af98..0805abcb 100644 --- a/docs/content/dns/zz_gen_cloudflare.md +++ b/docs/content/dns/zz_gen_cloudflare.md @@ -24,6 +24,11 @@ Here is an example bash command using the Cloudflare provider: CLOUDFLARE_EMAIL=foo@bar.com \ CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ lego --dns cloudflare --domains my.domain.com --email my@email.com run + +# or + +CLOUDFLARE_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ +lego --dns cloudflare --domains my.domain.com --email my@email.com run ``` @@ -35,7 +40,9 @@ lego --dns cloudflare --domains my.domain.com --email my@email.com run |-----------------------|-------------| | `CF_API_EMAIL` | Account email | | `CF_API_KEY` | API key | -| `CLOUDFLARE_API_KEY` | Alias to CLOUDFLARE_API_KEY | +| `CF_API_TOKEN` | API token | +| `CLOUDFLARE_API_KEY` | Alias to CF_API_KEY | +| `CLOUDFLARE_API_TOKEN` | Alias to CF_API_TOKEN | | `CLOUDFLARE_EMAIL` | Alias to CF_API_EMAIL | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. @@ -54,7 +61,20 @@ More information [here](/lego/dns/#configuration-and-credentials). The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here](/lego/dns/#configuration-and-credentials). -The Global API Key needs to be used, not the Origin CA Key. +## Description + +You may use `CF_API_EMAIL` and `CF_API_KEY` to authenticate, or `CF_API_TOKEN`. + +### API keys + +If using API keys (`CF_API_EMAIL` and `CF_API_KEY`), the Global API Key needs to be used, not the Origin CA Key. + +### API tokens + +If using [API tokens](https://api.cloudflare.com/#getting-started-endpoints) (`CF_API_TOKEN`), the following permissions are required: + +* `Zone:Read` +* `DNS:Edit` diff --git a/providers/dns/cloudflare/cloudflare.go b/providers/dns/cloudflare/cloudflare.go index d809e3f4..c840a522 100644 --- a/providers/dns/cloudflare/cloudflare.go +++ b/providers/dns/cloudflare/cloudflare.go @@ -21,6 +21,7 @@ const ( type Config struct { AuthEmail string AuthKey string + AuthToken string TTL int PropagationTimeout time.Duration PollingInterval time.Duration @@ -47,18 +48,26 @@ type DNSProvider struct { // NewDNSProvider returns a DNSProvider instance configured for Cloudflare. // Credentials must be passed in the environment variables: -// CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY. +// CLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY, CLOUDFLARE_API_TOKEN. func NewDNSProvider() (*DNSProvider, error) { values, err := env.GetWithFallback( []string{"CLOUDFLARE_EMAIL", "CF_API_EMAIL"}, - []string{"CLOUDFLARE_API_KEY", "CF_API_KEY"}) + []string{"CLOUDFLARE_API_KEY", "CF_API_KEY"}, + ) if err != nil { - return nil, fmt.Errorf("cloudflare: %v", err) + var errT error + values, errT = env.GetWithFallback( + []string{"CLOUDFLARE_API_TOKEN", "CF_API_TOKEN"}, + ) + if errT != nil { + return nil, fmt.Errorf("cloudflare: %v or %v", err, errT) + } } config := NewDefaultConfig() config.AuthEmail = values["CLOUDFLARE_EMAIL"] config.AuthKey = values["CLOUDFLARE_API_KEY"] + config.AuthToken = values["CLOUDFLARE_API_TOKEN"] return NewDNSProviderConfig(config) } @@ -73,7 +82,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, fmt.Errorf("cloudflare: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } - client, err := cloudflare.New(config.AuthKey, config.AuthEmail, cloudflare.HTTPClient(config.HTTPClient)) + client, err := getClient(config) if err != nil { return nil, err } @@ -81,6 +90,14 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return &DNSProvider{client: client, config: config}, nil } +func getClient(config *Config) (*cloudflare.API, error) { + if config.AuthToken == "" { + return cloudflare.New(config.AuthKey, config.AuthEmail, cloudflare.HTTPClient(config.HTTPClient)) + } + + return cloudflare.NewWithAPIToken(config.AuthToken, cloudflare.HTTPClient(config.HTTPClient)) +} + // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { diff --git a/providers/dns/cloudflare/cloudflare.toml b/providers/dns/cloudflare/cloudflare.toml index d4e0a0fd..0c1bc7f5 100644 --- a/providers/dns/cloudflare/cloudflare.toml +++ b/providers/dns/cloudflare/cloudflare.toml @@ -8,18 +8,38 @@ Example = ''' CLOUDFLARE_EMAIL=foo@bar.com \ CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ lego --dns cloudflare --domains my.domain.com --email my@email.com run + +# or + +CLOUDFLARE_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ +lego --dns cloudflare --domains my.domain.com --email my@email.com run ''' Additional = ''' -The Global API Key needs to be used, not the Origin CA Key. +## Description + +You may use `CF_API_EMAIL` and `CF_API_KEY` to authenticate, or `CF_API_TOKEN`. + +### API keys + +If using API keys (`CF_API_EMAIL` and `CF_API_KEY`), the Global API Key needs to be used, not the Origin CA Key. + +### API tokens + +If using [API tokens](https://api.cloudflare.com/#getting-started-endpoints) (`CF_API_TOKEN`), the following permissions are required: + +* `Zone:Read` +* `DNS:Edit` ''' [Configuration] [Configuration.Credentials] CF_API_EMAIL = "Account email" CF_API_KEY = "API key" + CF_API_TOKEN = "API token" CLOUDFLARE_EMAIL = "Alias to CF_API_EMAIL" - CLOUDFLARE_API_KEY = "Alias to CLOUDFLARE_API_KEY" + CLOUDFLARE_API_KEY = "Alias to CF_API_KEY" + CLOUDFLARE_API_TOKEN = "Alias to CF_API_TOKEN" [Configuration.Additional] CLOUDFLARE_POLLING_INTERVAL = "Time between DNS propagation check" CLOUDFLARE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" diff --git a/providers/dns/cloudflare/cloudflare_test.go b/providers/dns/cloudflare/cloudflare_test.go index 68710732..7c040492 100644 --- a/providers/dns/cloudflare/cloudflare_test.go +++ b/providers/dns/cloudflare/cloudflare_test.go @@ -11,7 +11,8 @@ import ( var envTest = tester.NewEnvTest( "CLOUDFLARE_EMAIL", - "CLOUDFLARE_API_KEY"). + "CLOUDFLARE_API_KEY", + "CLOUDFLARE_API_TOKEN"). WithDomain("CLOUDFLARE_DOMAIN") func TestNewDNSProvider(t *testing.T) { @@ -21,19 +22,26 @@ func TestNewDNSProvider(t *testing.T) { expected string }{ { - desc: "success", + desc: "success email, API key", envVars: map[string]string{ "CLOUDFLARE_EMAIL": "test@example.com", "CLOUDFLARE_API_KEY": "123", }, }, + { + desc: "success API token", + envVars: map[string]string{ + "CLOUDFLARE_API_TOKEN": "012345abcdef", + }, + }, { desc: "missing credentials", envVars: map[string]string{ - "CLOUDFLARE_EMAIL": "", - "CLOUDFLARE_API_KEY": "", + "CLOUDFLARE_EMAIL": "", + "CLOUDFLARE_API_KEY": "", + "CLOUDFLARE_API_TOKEN": "", }, - expected: "cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL,CLOUDFLARE_API_KEY", + expected: "cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL,CLOUDFLARE_API_KEY or some credentials information are missing: CLOUDFLARE_API_TOKEN", }, { desc: "missing email", @@ -41,7 +49,7 @@ func TestNewDNSProvider(t *testing.T) { "CLOUDFLARE_EMAIL": "", "CLOUDFLARE_API_KEY": "key", }, - expected: "cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL", + expected: "cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL or some credentials information are missing: CLOUDFLARE_API_TOKEN", }, { desc: "missing api key", @@ -49,7 +57,7 @@ func TestNewDNSProvider(t *testing.T) { "CLOUDFLARE_EMAIL": "awesome@possum.com", "CLOUDFLARE_API_KEY": "", }, - expected: "cloudflare: some credentials information are missing: CLOUDFLARE_API_KEY", + expected: "cloudflare: some credentials information are missing: CLOUDFLARE_API_KEY or some credentials information are missing: CLOUDFLARE_API_TOKEN", }, } @@ -79,10 +87,21 @@ func TestNewDNSProviderConfig(t *testing.T) { desc string authEmail string authKey string + authToken string expected string }{ { - desc: "success", + desc: "success with email and api key", + authEmail: "test@example.com", + authKey: "123", + }, + { + desc: "success with api token", + authToken: "012345abcdef", + }, + { + desc: "prefer api token", + authToken: "012345abcdef", authEmail: "test@example.com", authKey: "123", }, @@ -100,6 +119,11 @@ func TestNewDNSProviderConfig(t *testing.T) { authEmail: "test@example.com", expected: "invalid credentials: key & email must not be empty", }, + { + desc: "missing api token, fallback to api key/email", + authToken: "", + expected: "invalid credentials: key & email must not be empty", + }, } for _, test := range testCases { @@ -107,6 +131,7 @@ func TestNewDNSProviderConfig(t *testing.T) { config := NewDefaultConfig() config.AuthEmail = test.authEmail config.AuthKey = test.authKey + config.AuthToken = test.authToken p, err := NewDNSProviderConfig(config)