Skip to content

Commit

Permalink
feat(api): check account IDs (#868)
Browse files Browse the repository at this point in the history
  • Loading branch information
favonia authored Aug 12, 2024
1 parent f0b2945 commit a021805
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 67 deletions.
6 changes: 3 additions & 3 deletions cmd/ddns/ddns.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,10 @@ func realMain() int { //nolint:funlen
} else {
if c.UpdateCron != nil { // no need to do sanity check if it's a one-time update
if ok, certain := s.SanityCheck(ctxWithSignals, ppfmt); !ok && certain {
monitor.FailureAll(ctx, ppfmt, c.Monitors, "Invalid Cloudflare API token")
monitor.FailureAll(ctx, ppfmt, c.Monitors, "Invalid Cloudflare API token or account ID")
notifier.SendAll(ctx, ppfmt, c.Notifiers,
"The Cloudflare API token is invalid. "+
"Please check the value of CF_API_TOKEN or CF_API_TOKEN_FILE.",
"The Cloudflare API token or account ID is invalid. "+
"Please check the values of CF_API_TOKEN, CF_ACCOUNT_ID, and CF_API_TOKEN_FILE.",
)
return 1
}
Expand Down
67 changes: 57 additions & 10 deletions internal/api/cloudflare.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,11 @@ func (h CloudflareHandle) SanityCheckToken(ctx context.Context, ppfmt pp.PP) (bo
// 401:1000:"Invalid API Token"

switch {
case errors.As(err, &requestError),
errors.As(err, &requestError) && requestError.InternalErrorCodeIs(6111), //nolint:mnd
errors.As(err, &authorizationError) && authorizationError.InternalErrorCodeIs(1000): //nolint:mnd

case errors.As(err, &requestError), errors.As(err, &authorizationError):
ppfmt.Errorf(pp.EmojiUserError,
"The Cloudflare API token is invalid; "+
"please check the value of CF_API_TOKEN or CF_API_TOKEN_FILE")
goto certainlyBad
return false, true

default:
// We will try again later.
Expand All @@ -169,7 +166,7 @@ func (h CloudflareHandle) SanityCheckToken(ctx context.Context, ppfmt pp.PP) (bo
case "active":
case "disabled", "expired":
ppfmt.Errorf(pp.EmojiUserError, "The Cloudflare API token is %s", res.Status)
goto certainlyBad
return false, true
default:
ppfmt.Warningf(pp.EmojiImpossible,
"The Cloudflare API token is in an undocumented state %q; please report this at %s",
Expand All @@ -187,13 +184,63 @@ func (h CloudflareHandle) SanityCheckToken(ctx context.Context, ppfmt pp.PP) (bo

h.cache.sanityCheck.Set(sanityCheckToken, true, ttlcache.DefaultTTL)
return true, true

certainlyBad:
return false, true
}

// SanityCheck verifies both the Cloudflare API token and account ID.
// It returns false only when the token or the account ID is certainly bad.
func (h CloudflareHandle) SanityCheck(ctx context.Context, ppfmt pp.PP) (bool, bool) {
return h.SanityCheckToken(ctx, ppfmt)
tokenOK, tokenCertain := h.SanityCheckToken(ctx, ppfmt)

if !tokenOK {
return false, tokenCertain
}

// If the account ID is empty, nothing to check other than the token!
if h.accountID == "" {
return true, tokenCertain
}

if valid := h.cache.sanityCheck.Get(sanityCheckAccount); valid != nil {
return valid.Value(), tokenCertain
}

quickCtx, cancel := context.WithTimeoutCause(ctx, time.Second, errTimeout)
defer cancel()

// Checking the account ID
_, _, err := h.cf.Account(quickCtx, h.accountID)
if err != nil {
if quickCtx.Err() != nil {
return true, false
}

var requestError *cloudflare.RequestError
var notFoundError *cloudflare.NotFoundError

// known ambiguous cases
// 403:9109:"Unauthorized to access requested resource": this might actually be okay

// known error messages
// 403:9109:"Invalid account identifier"
// 400:7003:"Could not route to ..., perhaps your object identifier is invalid?"
// 403:7003:"Invalid account identifier"
// 404:7003:"Could not route to ..., perhaps your object identifier is invalid?"

switch {
case errors.As(err, &requestError), errors.As(err, &notFoundError):
ppfmt.Errorf(pp.EmojiUserError,
"The Cloudflare account ID is invalid; "+
"please check the value of CF_ACCOUNT_ID")
return false, true

default:
// We will try again later.
return true, false
}
}

h.skipSanityCheckToken()
h.cache.sanityCheck.Set(sanityCheckAccount, true, ttlcache.DefaultTTL)

return true, true
}
3 changes: 2 additions & 1 deletion internal/api/cloudflare_records_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,8 @@ func TestZoneOfDomain(t *testing.T) {
t.Parallel()
mockCtrl := gomock.NewController(t)
mockPP := mocks.NewMockPP(mockCtrl)
mux, h, ok := newHandle(t, mockPP, tc.accountID, http.StatusOK, mockVerifyToken())
mux, h, ok := newHandle(t, mockPP, tc.accountID,
http.StatusOK, mockTokenVerifyResponse, http.StatusOK, mockAccountResponse)
require.True(t, ok)
zh := newZonesHandler(t, mux, tc.accountID, tc.zoneStatuses)

Expand Down
Loading

0 comments on commit a021805

Please sign in to comment.