From 143ee8a8785aeedfd68c2015c6b2091bf9413641 Mon Sep 17 00:00:00 2001 From: favonia Date: Fri, 27 Sep 2024 06:23:38 -0500 Subject: [PATCH 1/3] feat(config): accept CLOUDFLARE_* and all compatible token settings --- internal/config/config_read_test.go | 6 +- internal/config/env_auth.go | 131 ++++++++++++++++++++----- internal/config/env_auth_test.go | 144 +++++++++++++--------------- internal/pp/hint.go | 29 +++--- 4 files changed, 194 insertions(+), 116 deletions(-) diff --git a/internal/config/config_read_test.go b/internal/config/config_read_test.go index 8ee6e446..45d71e2f 100644 --- a/internal/config/config_read_test.go +++ b/internal/config/config_read_test.go @@ -19,6 +19,7 @@ import ( func unsetAll(t *testing.T) { t.Helper() unset(t, + "CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_TOKEN_FILE", "CF_API_TOKEN", "CF_API_TOKEN_FILE", "CF_ACCOUNT_ID", "IP4_PROVIDER", "IP6_PROVIDER", "DOMAINS", "IP4_DOMAINS", "IP6_DOMAINS", "WAF_LISTS", @@ -43,7 +44,7 @@ func TestReadEnvWithOnlyToken(t *testing.T) { mockCtrl := gomock.NewController(t) unsetAll(t) - store(t, "CF_API_TOKEN", "deadbeaf") + store(t, "CLOUDFLARE_API_TOKEN", "deadbeaf") var cfg config.Config mockPP := mocks.NewMockPP(mockCtrl) @@ -79,7 +80,8 @@ func TestReadEnvEmpty(t *testing.T) { mockPP.EXPECT().IsShowing(pp.Info).Return(true), mockPP.EXPECT().Infof(pp.EmojiEnvVars, "Reading settings . . ."), mockPP.EXPECT().Indent().Return(innerMockPP), - innerMockPP.EXPECT().Noticef(pp.EmojiUserError, "Needs either CF_API_TOKEN or CF_API_TOKEN_FILE"), + innerMockPP.EXPECT().Noticef(pp.EmojiUserError, + "Needs either %s or %s", "CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_TOKEN_FILE"), ) ok := cfg.ReadEnv(mockPP) require.False(t, ok) diff --git a/internal/config/env_auth.go b/internal/config/env_auth.go index 864d1908..68eda3f9 100644 --- a/internal/config/env_auth.go +++ b/internal/config/env_auth.go @@ -10,48 +10,131 @@ import ( var oauthBearerRegex = regexp.MustCompile(`^[-a-zA-Z0-9._~+/]+=*$`) -func readAuthToken(ppfmt pp.PP) (string, bool) { - var ( - token = Getenv("CF_API_TOKEN") - tokenFile = Getenv("CF_API_TOKEN_FILE") - ) - var ok bool +// Keys of environment variables. +const ( + TokenKey1 string = "CLOUDFLARE_API_TOKEN" //nolint:gosec + TokenKey2 string = "CF_API_TOKEN" //nolint:gosec + TokenFileKey1 string = "CLOUDFLARE_API_TOKEN_FILE" //nolint:gosec + TokenFileKey2 string = "CF_API_TOKEN_FILE" //nolint:gosec +) + +// HintAuthTokenNewPrefix contains the hint about the transition from +// CF_* to CLOUDFLARE_*. +const HintAuthTokenNewPrefix string = "Cloudflare is transitioning its tools to use the prefix CLOUDFLARE instead of CF. To align with this change, it is recommended to use CLOUDFLARE_API_TOKEN (or CLOUDFLARE_API_TOKEN_FILE) instead of CF_API_TOKEN (or CF_API_TOKEN_FILE) moving forward. All options will be fully supported until version 2.0." //nolint:lll,gosec - // foolproof checks +func readPlainAuthTokens(ppfmt pp.PP) (string, string, bool) { + token1 := Getenv(TokenKey1) + token2 := Getenv(TokenKey2) + + var token, tokenKey string + switch { + case token1 == "" && token2 == "": + return "", "", true + case token1 != "" && token2 != "" && token1 != token2: + ppfmt.Noticef(pp.EmojiUserError, "The values of %s and %s do not match", TokenKey1, TokenKey2) + return "", "", false + case token1 != "": + token, tokenKey = token1, TokenKey1 + case token2 != "": + ppfmt.Hintf(pp.HintAuthTokenNewPrefix, HintAuthTokenNewPrefix) + token, tokenKey = token2, TokenKey2 + } + + // foolproof check: the sample value in README if token == "YOUR-CLOUDFLARE-API-TOKEN" { - ppfmt.Noticef(pp.EmojiUserError, "You need to provide a real API token as CF_API_TOKEN") + ppfmt.Noticef(pp.EmojiUserError, "You need to provide a real API token as %s", tokenKey) + return "", "", false + } + + return token, tokenKey, true +} + +func readAuthTokenFile(ppfmt pp.PP, key string) (string, bool) { + tokenFile := Getenv(key) + if tokenFile == "" { + return "", true + } + + token, ok := file.ReadString(ppfmt, tokenFile) + if !ok { + return "", false + } + + if token == "" { + ppfmt.Noticef(pp.EmojiUserError, "The file specified by %s does not contain an API token", key) + return "", false + } + + return token, true +} + +func readAuthTokenFiles(ppfmt pp.PP) (string, string, bool) { + token1, ok := readAuthTokenFile(ppfmt, TokenFileKey1) + if !ok { + return "", "", false + } + + token2, ok := readAuthTokenFile(ppfmt, TokenFileKey2) + if !ok { + return "", "", false + } + + switch { + case token1 != "" && token2 != "" && token1 != token2: + ppfmt.Noticef(pp.EmojiUserError, + "The files specified by %s and %s contain conflicting tokens", TokenFileKey1, TokenFileKey2) + return "", "", false + case token1 != "": + return token1, TokenFileKey1, true + case token2 != "": + ppfmt.Hintf(pp.HintAuthTokenNewPrefix, HintAuthTokenNewPrefix) + return token2, TokenFileKey2, true + default: + return "", "", true + } +} + +func readAuthToken(ppfmt pp.PP) (string, bool) { + //default: + // ppfmt.Noticef(pp.EmojiUserError, "Needs either CF_API_TOKEN or CF_API_TOKEN_FILE") + // return "", false + //} + + tokenPlain, tokenPlainKey, ok := readPlainAuthTokens(ppfmt) + if !ok { + return "", false + } + + tokenFile, tokenFileKey, ok := readAuthTokenFiles(ppfmt) + if !ok { return "", false } + var token string switch { - case token != "" && tokenFile != "": - ppfmt.Noticef(pp.EmojiUserError, "Cannot have both CF_API_TOKEN and CF_API_TOKEN_FILE set") + case tokenPlain != "" && tokenFile != "" && tokenPlain != tokenFile: + ppfmt.Noticef(pp.EmojiUserError, + "The value of %s does not match the token found in the file specified by %s", tokenPlainKey, tokenFileKey) return "", false - case token != "": + case tokenPlain != "": + token = tokenPlain case tokenFile != "": - token, ok = file.ReadString(ppfmt, tokenFile) - if !ok { - return "", false - } - - if token == "" { - ppfmt.Noticef(pp.EmojiUserError, "The token in the file specified by CF_API_TOKEN_FILE is empty") - return "", false - } + token = tokenFile default: - ppfmt.Noticef(pp.EmojiUserError, "Needs either CF_API_TOKEN or CF_API_TOKEN_FILE") + ppfmt.Noticef(pp.EmojiUserError, "Needs either %s or %s", TokenKey1, TokenFileKey1) return "", false } if !oauthBearerRegex.MatchString(token) { - ppfmt.Noticef(pp.EmojiUserWarning, "The API token does not look like a valid OAuth2 bearer token") + ppfmt.Noticef(pp.EmojiUserWarning, + "The API token appears to be invalid. It does not follow the OAuth2 bearer token format.") } return token, true } -// ReadAuth reads environment variables CF_API_TOKEN, CF_API_TOKEN_FILE, and CF_ACCOUNT_ID -// and creates an [api.CloudflareAuth]. +// ReadAuth reads environment variables CLOUDFLARE_API_TOKEN, CLOUDFLARE_API_TOKEN_FILE, +// CF_API_TOKEN, CF_API_TOKEN_FILE, and CF_ACCOUNT_ID and creates an [api.CloudflareAuth]. func ReadAuth(ppfmt pp.PP, field *api.Auth) bool { token, ok := readAuthToken(ppfmt) if !ok { diff --git a/internal/config/env_auth_test.go b/internal/config/env_auth_test.go index 5bc357f2..6f8ff35c 100644 --- a/internal/config/env_auth_test.go +++ b/internal/config/env_auth_test.go @@ -15,103 +15,90 @@ import ( "github.com/favonia/cloudflare-ddns/internal/pp" ) -//nolint:paralleltest // environment vars are global -func TestReadAuth(t *testing.T) { - unset(t, "CF_API_TOKEN", "CF_API_TOKEN_FILE", "CF_ACCOUNT_ID") +func useMemFS(memfs fstest.MapFS) { + file.FS = memfs +} +//nolint:paralleltest // environment vars and file system are global +func TestReadAuth(t *testing.T) { for name, tc := range map[string]struct { - token string - account string - ok bool - prepareMockPP func(*mocks.MockPP) + mapFS map[string]string + token1 string + token2 string + fileToken1Path string + fileToken2Path string + account string + ok bool + expected string + prepareMockPP func(*mocks.MockPP) }{ - "success": {"123456789", "", true, nil}, + "success": { + map[string]string{"token.txt": "hello"}, + "123456789", "", "", "", "", + true, "123456789", nil, + }, "empty-token": { - "", "account", false, + map[string]string{}, + "", "", "", "", "account", + false, "", func(m *mocks.MockPP) { - m.EXPECT().Noticef(pp.EmojiUserError, "Needs either CF_API_TOKEN or CF_API_TOKEN_FILE") + m.EXPECT().Noticef(pp.EmojiUserError, + "Needs either %s or %s", "CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_TOKEN_FILE") }, }, "invalid": { - "!!!", "", true, + map[string]string{}, + "!!!", "", "", "", "", + true, "!!!", func(m *mocks.MockPP) { - m.EXPECT().Noticef(pp.EmojiUserWarning, "The API token does not look like a valid OAuth2 bearer token") + m.EXPECT().Noticef(pp.EmojiUserWarning, + "The API token appears to be invalid. It does not follow the OAuth2 bearer token format.") }, }, "account": { - "123456789", "secret account", true, + map[string]string{}, + "123456789", "", "", "", "secret account", + true, "123456789", func(m *mocks.MockPP) { m.EXPECT().Noticef(pp.EmojiUserWarning, "CF_ACCOUNT_ID is ignored since 1.14.0") }, }, "copycat": { - "YOUR-CLOUDFLARE-API-TOKEN", "", false, + map[string]string{}, + "YOUR-CLOUDFLARE-API-TOKEN", "", "", "", "", + false, "", func(m *mocks.MockPP) { - m.EXPECT().Noticef(pp.EmojiUserError, "You need to provide a real API token as CF_API_TOKEN") + m.EXPECT().Noticef(pp.EmojiUserError, + "You need to provide a real API token as %s", "CLOUDFLARE_API_TOKEN") }, }, - } { - t.Run(name, func(t *testing.T) { - mockCtrl := gomock.NewController(t) - - store(t, "CF_API_TOKEN", tc.token) - store(t, "CF_ACCOUNT_ID", tc.account) - - mockPP := mocks.NewMockPP(mockCtrl) - if tc.prepareMockPP != nil { - tc.prepareMockPP(mockPP) - } - - var field api.Auth - ok := config.ReadAuth(mockPP, &field) - require.Equal(t, tc.ok, ok) - if tc.ok { - require.Equal(t, &api.CloudflareAuth{Token: tc.token, BaseURL: ""}, field) - } else { - require.Nil(t, field) - } - }) - } -} - -func useMemFS(memfs fstest.MapFS) { - file.FS = memfs -} - -//nolint:paralleltest // environment vars and file system are global -func TestReadAuthWithFile(t *testing.T) { - unset(t, "CF_API_TOKEN", "CF_API_TOKEN_FILE", "CF_ACCOUNT_ID") - - for name, tc := range map[string]struct { - token string - tokenFile string - actualPath string - actualContent string - expected string - ok bool - prepareMockPP func(*mocks.MockPP) - }{ - "ok": {"", "test.txt", "test.txt", "hello", "hello", true, nil}, - "both": { - "123456789", "test.txt", "test.txt", "hello", "", false, - func(m *mocks.MockPP) { - m.EXPECT().Noticef(pp.EmojiUserError, "Cannot have both CF_API_TOKEN and CF_API_TOKEN_FILE set") - }, + "file/success": { + map[string]string{"token.txt": "hello"}, + "", "", "token.txt", "", "", + true, "hello", nil, }, - "wrong.path": { - "", "wrong.txt", "actual.txt", "hello", "", false, + "file/wrong.path": { + map[string]string{}, + "", "", "wrong.txt", "", "", + false, "", func(m *mocks.MockPP) { m.EXPECT().Noticef(pp.EmojiUserError, "Failed to read %q: %v", "wrong.txt", gomock.Any()) }, }, - "empty": { - "", "test.txt", "test.txt", "", "", false, + "file/empty": { + map[string]string{"empty.txt": ""}, + "", "", "empty.txt", "", "", + false, "", func(m *mocks.MockPP) { - m.EXPECT().Noticef(pp.EmojiUserError, "The token in the file specified by CF_API_TOKEN_FILE is empty") + m.EXPECT().Noticef(pp.EmojiUserError, + "The file specified by %s does not contain an API token", + "CLOUDFLARE_API_TOKEN_FILE") }, }, - "invalid path": { - "", "dir", "dir/test.txt", "hello", "", false, + "file/invalid-path": { + map[string]string{"dir/file.txt": ""}, + "", "", "dir", "", "", + false, "", func(m *mocks.MockPP) { m.EXPECT().Noticef(pp.EmojiUserError, "Failed to read %q: %v", "dir", gomock.Any()) }, @@ -120,17 +107,22 @@ func TestReadAuthWithFile(t *testing.T) { t.Run(name, func(t *testing.T) { mockCtrl := gomock.NewController(t) - store(t, "CF_API_TOKEN", tc.token) - store(t, "CF_API_TOKEN_FILE", tc.tokenFile) + store(t, "CLOUDFLARE_API_TOKEN", tc.token1) + store(t, "CLOUDFLARE_API_TOKEN_FILE", tc.fileToken1Path) + store(t, "CF_API_TOKEN", tc.token2) + store(t, "CF_API_TOKEN_FILE", tc.fileToken2Path) + store(t, "CF_ACCOUNT_ID", tc.account) - useMemFS(fstest.MapFS{ - tc.actualPath: &fstest.MapFile{ - Data: []byte(tc.actualContent), + mapFS := fstest.MapFS{} + for path, content := range tc.mapFS { + mapFS[path] = &fstest.MapFile{ + Data: []byte(content), Mode: 0o644, ModTime: time.Unix(1234, 5678), Sys: nil, - }, - }) + } + } + useMemFS(mapFS) var field api.Auth mockPP := mocks.NewMockPP(mockCtrl) diff --git a/internal/pp/hint.go b/internal/pp/hint.go index bd533b3a..cf9368cc 100644 --- a/internal/pp/hint.go +++ b/internal/pp/hint.go @@ -5,18 +5,19 @@ type Hint int // All the registered hints. const ( - HintUpdateDockerTemplate Hint = iota - HintIP4DetectionFails - HintIP6DetectionFails - HintIP4MappedIP6Address - HintDetectionTimeouts - HintUpdateTimeouts - HintRecordPermission - HintWAFListPermission - HintMismatchedRecordAttributes - HintMismatchedWAFListAttributes - Hint1111Blockage - HintExperimentalShoutrrr // introduced in 1.12.0 - HintExperimentalWAF // introduced in 1.14.0 - HintExperimentalLocalWithInterface // introduced in 1.15.0 + HintUpdateDockerTemplate Hint = iota // PUID or PGID was used + HintAuthTokenNewPrefix // "CF_*" to "CLOUDFLARE_*" + HintIP4DetectionFails // How to turn off IPv4 + HintIP6DetectionFails // How to set up IPv6 or turn it off + HintIP4MappedIP6Address // IPv4-mapped IPv6 addresses are bad for AAAA records + HintDetectionTimeouts // Longer detection timeout + HintUpdateTimeouts // Longer update timeout + HintRecordPermission // Permissions to update DNS tokens + HintWAFListPermission // Permissions to update WAF lists + HintMismatchedRecordAttributes // Attributes of DNS records have been changed + HintMismatchedWAFListAttributes // Attributes of WAF lists have been changed + Hint1111Blockage // 1.1.1.1 is blocked + HintExperimentalShoutrrr // New feature introduced in 1.12.0 on 2024/6/28 + HintExperimentalWAF // New feature introduced in 1.14.0 on 2024/8/25 + HintExperimentalLocalWithInterface // New feature introduced in 1.15.0 ) From cd1d1ff4af2434c360e9f397f776f57ec0a0b8b0 Mon Sep 17 00:00:00 2001 From: favonia Date: Fri, 27 Sep 2024 22:05:34 -0500 Subject: [PATCH 2/3] docs(README): document new convention --- README.markdown | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/README.markdown b/README.markdown index 5bd2dfe4..a0c05814 100644 --- a/README.markdown +++ b/README.markdown @@ -82,7 +82,7 @@ _(Click to expand the following items.)_ ```bash docker run \ --network host \ - -e CF_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN \ + -e CLOUDFLARE_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN \ -e DOMAINS=example.org,www.example.org,example.io \ -e PROXIED=true \ favonia/cloudflare-ddns:latest @@ -95,7 +95,7 @@ docker run \ You need the [Go tool](https://golang.org/doc/install) to run the updater from its source. ```bash -CF_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN \ +CLOUDFLARE_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN \ DOMAINS=example.org,www.example.org,example.io \ PROXIED=true \ go run github.com/favonia/cloudflare-ddns/cmd/ddns@latest @@ -132,7 +132,7 @@ services: security_opt: [no-new-privileges:true] # Another protection to restrict superuser privileges (optional but recommended) environment: - - CF_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN + - CLOUDFLARE_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN # Your Cloudflare API token - DOMAINS=example.org,www.example.org,example.io # Your domains (separated by commas) @@ -143,15 +143,14 @@ services: _(Click to expand the following important tips.)_
-🔑 CF_API_TOKEN is your Cloudflare API token +🔑 CLOUDFLARE_API_TOKEN is your Cloudflare API token -The value of `CF_API_TOKEN` should be an API **token** (_not_ an API key), which can be obtained from the [API Tokens page](https://dash.cloudflare.com/profile/api-tokens). (The less secure API key authentication is deliberately _not_ supported.) +The value of `CLOUDFLARE_API_TOKEN` should be an API **token** (_not_ an API key), which can be obtained from the [API Tokens page](https://dash.cloudflare.com/profile/api-tokens). The less secure API key authentication is deliberately _not_ supported. - To update only DNS records, use the **Edit zone DNS** template to create a token. - To update only WAF lists, choose **Create Custom Token** and then add the **Account - Account Filter Lists - Edit** permission to create a token. -- To update DNS records _and_ WAF lists, use the **Edit zone DNS** template and then add the **Account - Account Filter Lists - Edit** permission when creating the token. - -You can also adjust the permissions of existing tokens at any time! +- To update _both_ DNS records _and_ WAF lists, use the **Edit zone DNS** template and then add the **Account - Account Filter Lists - Edit** permission when creating the token. +- You can adjust the permissions of existing tokens at any time!
@@ -259,15 +258,20 @@ _(Click to expand the following items.)_
🔑 The Cloudflare API token -> Exactly one of the following variables should be set. +> Starting with version 1.15.0, the updater supports environment variables that begin with `CLOUDFLARE_*`. Multiple environment variables can be used at the same time, provided they all specify the same token. -| Name | Meaning | -| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| `CF_API_TOKEN` | The [Cloudflare API token](https://dash.cloudflare.com/profile/api-tokens) to access the Cloudflare API | -| `CF_API_TOKEN_FILE` | A path to a file that contains the [Cloudflare API token](https://dash.cloudflare.com/profile/api-tokens) to access the Cloudflare API | +| Name | Meaning | +| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `CLOUDFLARE_API_TOKEN` | The [Cloudflare API token](https://dash.cloudflare.com/profile/api-tokens) to access the Cloudflare API | +| `CLOUDFLARE_API_TOKEN_FILE` | A path to a file that contains the [Cloudflare API token](https://dash.cloudflare.com/profile/api-tokens) to access the Cloudflare API | +| `CF_API_TOKEN` (will be deprecated in version 2.0) | Same as `CLOUDFLARE_API_TOKEN` | +| `CF_API_TOKEN_FILE` (will be deprecated version in 2.0) | Same as `CLOUDFLARE_API_TOKEN_FILE` | -- 🔑 To update DNS records, the updater needs the **Account - Account Filter Lists - Edit** permission. -- 🔑 To manipulate WAF lists, the updater needs the **Zone - DNS - Edit** permission. +> 🚂 Cloudflare is updating its tools to use environment variables starting with `CLOUDFLARE_*` instead of `CF_*`. It is recommended to align your setting to align with this new convention. However, the updater will fully support both `CLOUDFLARE_*` and `CF_*` environment variables until version 2.0. +> +> 🔑 To update DNS records, the updater needs the **Account - Account Filter Lists - Edit** permission. +> +> 🔑 To manipulate WAF lists, the updater needs the **Zone - DNS - Edit** permission.
@@ -423,8 +427,8 @@ _(Click to expand the following items.)_ | Old Parameter | | Note | | -------------------------------------- | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `API_KEY=key` | ✔️ | Use `CF_API_TOKEN=key` | -| `API_KEY_FILE=file` | ✔️ | Use `CF_API_TOKEN_FILE=file` | +| `API_KEY=key` | ✔️ | Use `CLOUDFLARE_API_TOKEN=key` | +| `API_KEY_FILE=file` | ✔️ | Use `CLOUDFLARE_API_TOKEN_FILE=file` | | `ZONE=example.org` and `SUBDOMAIN=sub` | ✔️ | Use `DOMAINS=sub.example.org` directly | | `PROXIED=true` | ✔️ | Same (`PROXIED=true`) | | `RRTYPE=A` | ✔️ | Both IPv4 and IPv6 are enabled by default; use `IP6_PROVIDER=none` to disable IPv6 | @@ -441,7 +445,7 @@ _(Click to expand the following items.)_ | Old JSON Key | | Note | | ------------------------------------- | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `cloudflare.authentication.api_token` | ✔️ | Use `CF_API_TOKEN=key` | +| `cloudflare.authentication.api_token` | ✔️ | Use `CLOUDFLARE_API_TOKEN=key` | | `cloudflare.authentication.api_key` | ❌ | Please use the newer, more secure [API tokens](https://dash.cloudflare.com/profile/api-tokens) | | `cloudflare.zone_id` | ✔️ | Not needed; automatically retrieved from the server | | `cloudflare.subdomains[].name` | ✔️ | Use `DOMAINS` with [**fully qualified domain names (FQDNs)**](https://en.wikipedia.org/wiki/Fully_qualified_domain_name) directly; for example, if your zone is `example.org` and your subdomain is `sub`, use `DOMAINS=sub.example.org` | From 65f5516d93ab8be2d1dbc2ffa7ca4fb47a4e99e2 Mon Sep 17 00:00:00 2001 From: favonia Date: Fri, 27 Sep 2024 22:30:56 -0500 Subject: [PATCH 3/3] test(config): improve coverage --- internal/config/env_auth.go | 15 +++--- internal/config/env_auth_test.go | 91 ++++++++++++++++++++++++++++---- 2 files changed, 87 insertions(+), 19 deletions(-) diff --git a/internal/config/env_auth.go b/internal/config/env_auth.go index 68eda3f9..b0e4e330 100644 --- a/internal/config/env_auth.go +++ b/internal/config/env_auth.go @@ -31,7 +31,8 @@ func readPlainAuthTokens(ppfmt pp.PP) (string, string, bool) { case token1 == "" && token2 == "": return "", "", true case token1 != "" && token2 != "" && token1 != token2: - ppfmt.Noticef(pp.EmojiUserError, "The values of %s and %s do not match", TokenKey1, TokenKey2) + ppfmt.Noticef(pp.EmojiUserError, + "The values of %s and %s do not match; they must specify the same token", TokenKey1, TokenKey2) return "", "", false case token1 != "": token, tokenKey = token1, TokenKey1 @@ -82,7 +83,7 @@ func readAuthTokenFiles(ppfmt pp.PP) (string, string, bool) { switch { case token1 != "" && token2 != "" && token1 != token2: ppfmt.Noticef(pp.EmojiUserError, - "The files specified by %s and %s contain conflicting tokens", TokenFileKey1, TokenFileKey2) + "The files specified by %s and %s have conflicting tokens; their content must match", TokenFileKey1, TokenFileKey2) return "", "", false case token1 != "": return token1, TokenFileKey1, true @@ -95,11 +96,6 @@ func readAuthTokenFiles(ppfmt pp.PP) (string, string, bool) { } func readAuthToken(ppfmt pp.PP) (string, bool) { - //default: - // ppfmt.Noticef(pp.EmojiUserError, "Needs either CF_API_TOKEN or CF_API_TOKEN_FILE") - // return "", false - //} - tokenPlain, tokenPlainKey, ok := readPlainAuthTokens(ppfmt) if !ok { return "", false @@ -114,7 +110,8 @@ func readAuthToken(ppfmt pp.PP) (string, bool) { switch { case tokenPlain != "" && tokenFile != "" && tokenPlain != tokenFile: ppfmt.Noticef(pp.EmojiUserError, - "The value of %s does not match the token found in the file specified by %s", tokenPlainKey, tokenFileKey) + "The value of %s does not match the token found in the file specified by %s; they must specify the same token", + tokenPlainKey, tokenFileKey) return "", false case tokenPlain != "": token = tokenPlain @@ -127,7 +124,7 @@ func readAuthToken(ppfmt pp.PP) (string, bool) { if !oauthBearerRegex.MatchString(token) { ppfmt.Noticef(pp.EmojiUserWarning, - "The API token appears to be invalid. It does not follow the OAuth2 bearer token format.") + "The API token appears to be invalid; it does not follow the OAuth2 bearer token format") } return token, true diff --git a/internal/config/env_auth_test.go b/internal/config/env_auth_test.go index 6f8ff35c..a469cd24 100644 --- a/internal/config/env_auth_test.go +++ b/internal/config/env_auth_test.go @@ -37,22 +37,45 @@ func TestReadAuth(t *testing.T) { "123456789", "", "", "", "", true, "123456789", nil, }, - "empty-token": { + "empty": { map[string]string{}, - "", "", "", "", "account", + "", "", "", "", "", false, "", func(m *mocks.MockPP) { m.EXPECT().Noticef(pp.EmojiUserError, "Needs either %s or %s", "CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_TOKEN_FILE") }, }, + "conflicting": { + map[string]string{}, + "token1", "token2", "", "", "", + false, "", + func(m *mocks.MockPP) { + m.EXPECT().Noticef(pp.EmojiUserError, + "The values of %s and %s do not match; they must specify the same token", + "CLOUDFLARE_API_TOKEN", "CF_API_TOKEN") + }, + }, + "old": { + map[string]string{}, + "", "token2", "", "", "", + true, "token2", + func(m *mocks.MockPP) { + m.EXPECT().Hintf(pp.HintAuthTokenNewPrefix, config.HintAuthTokenNewPrefix) + }, + }, + "old/same": { + map[string]string{}, + "token", "token", "", "", "", + true, "token", nil, + }, "invalid": { map[string]string{}, "!!!", "", "", "", "", true, "!!!", func(m *mocks.MockPP) { m.EXPECT().Noticef(pp.EmojiUserWarning, - "The API token appears to be invalid. It does not follow the OAuth2 bearer token format.") + "The API token appears to be invalid; it does not follow the OAuth2 bearer token format") }, }, "account": { @@ -77,6 +100,56 @@ func TestReadAuth(t *testing.T) { "", "", "token.txt", "", "", true, "hello", nil, }, + "file/empty": { + map[string]string{"empty.txt": ""}, + "", "", "empty.txt", "", "", + false, "", + func(m *mocks.MockPP) { + m.EXPECT().Noticef(pp.EmojiUserError, + "The file specified by %s does not contain an API token", + "CLOUDFLARE_API_TOKEN_FILE") + }, + }, + "file/conflicting": { + map[string]string{"token1.txt": "hello1", "token2.txt": "hello2"}, + "", "", "token1.txt", "token2.txt", "", + false, "", + func(m *mocks.MockPP) { + m.EXPECT().Noticef(pp.EmojiUserError, + "The files specified by %s and %s have conflicting tokens; their content must match", + "CLOUDFLARE_API_TOKEN_FILE", "CF_API_TOKEN_FILE") + }, + }, + "file/conflicting/non-file": { + map[string]string{"token.txt": "file"}, + "plain", "", "token.txt", "", "", + false, "", + func(m *mocks.MockPP) { + m.EXPECT().Noticef(pp.EmojiUserError, + "The value of %s does not match the token found in the file specified by %s; "+ + "they must specify the same token", + "CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_TOKEN_FILE", + ) + }, + }, + "file/same/non-file": { + map[string]string{"token.txt": "token"}, + "token", "", "token.txt", "", "", + true, "token", nil, + }, + "file/old": { + map[string]string{"token.txt": "hello"}, + "", "", "", "token.txt", "", + true, "hello", + func(m *mocks.MockPP) { + m.EXPECT().Hintf(pp.HintAuthTokenNewPrefix, config.HintAuthTokenNewPrefix) + }, + }, + "file/old/same": { + map[string]string{"token1.txt": "hello", "token2.txt": "hello"}, + "", "", "token1.txt", "token2.txt", "", + true, "hello", nil, + }, "file/wrong.path": { map[string]string{}, "", "", "wrong.txt", "", "", @@ -85,17 +158,15 @@ func TestReadAuth(t *testing.T) { m.EXPECT().Noticef(pp.EmojiUserError, "Failed to read %q: %v", "wrong.txt", gomock.Any()) }, }, - "file/empty": { - map[string]string{"empty.txt": ""}, - "", "", "empty.txt", "", "", + "file/wrong.path/2": { + map[string]string{}, + "", "", "", "wrong.txt", "", false, "", func(m *mocks.MockPP) { - m.EXPECT().Noticef(pp.EmojiUserError, - "The file specified by %s does not contain an API token", - "CLOUDFLARE_API_TOKEN_FILE") + m.EXPECT().Noticef(pp.EmojiUserError, "Failed to read %q: %v", "wrong.txt", gomock.Any()) }, }, - "file/invalid-path": { + "file/invalid-directory": { map[string]string{"dir/file.txt": ""}, "", "", "dir", "", "", false, "",