From c19b4896d2fb6ed3204d4a616956179595713ae4 Mon Sep 17 00:00:00 2001 From: Will Robertson <6219869+aliask@users.noreply.github.com> Date: Thu, 30 May 2024 08:57:14 +1000 Subject: [PATCH] Split API key into two parts per #4 --- README.md | 8 +++++--- client.go | 44 +++++++++++++++++++++++++++++++++++++------- provider.go | 18 ++++++++++++------ 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fe9185e..b1b3cfa 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,15 @@ This package implements the [libdns interfaces](https://github.com/libdns/libdns This package supports API **token** authentication. -You will need to create a token with the following permissions: +You will need to create two tokens with the following permissions: -- Zone / Zone / Read -- Zone / DNS / Edit +1. Zone / Zone / Read +2. Applicable Zone / DNS / Edit The first permission is needed to get the zone ID, and the second permission is obviously necessary to edit the DNS records. If you're only using the `GetRecords()` method, you can change the second permission to Read to guarantee no changes will be made. +By using two API keys it allows for having a DNS Edit key which only allows editing a single Zone. + To clarify, do NOT use API keys, which are globally-scoped: ![Don't use API keys](https://user-images.githubusercontent.com/1128849/81196485-556aca00-8f7c-11ea-9e13-c6a8a966f689.png) diff --git a/client.go b/client.go index dd73e30..a020e0c 100644 --- a/client.go +++ b/client.go @@ -29,7 +29,7 @@ func (p *Provider) createRecord(ctx context.Context, zoneInfo cfZone, record lib req.Header.Set("Content-Type", "application/json") var result cfDNSRecord - _, err = p.doAPIRequest(req, &result) + _, err = p.doDNSAPIRequest(req, &result) if err != nil { return cfDNSRecord{}, err } @@ -54,7 +54,7 @@ func (p *Provider) updateRecord(ctx context.Context, oldRec, newRec cfDNSRecord) req.Header.Set("Content-Type", "application/json") var result cfDNSRecord - _, err = p.doAPIRequest(req, &result) + _, err = p.doDNSAPIRequest(req, &result) return result, err } @@ -73,7 +73,7 @@ func (p *Provider) getDNSRecords(ctx context.Context, zoneInfo cfZone, rec libdn } var results []cfDNSRecord - _, err = p.doAPIRequest(req, &results) + _, err = p.doDNSAPIRequest(req, &results) return results, err } @@ -99,7 +99,7 @@ func (p *Provider) getZoneInfo(ctx context.Context, zoneName string) (cfZone, er } var zones []cfZone - _, err = p.doAPIRequest(req, &zones) + _, err = p.doZoneAPIRequest(req, &zones) if err != nil { return cfZone{}, err } @@ -113,13 +113,43 @@ func (p *Provider) getZoneInfo(ctx context.Context, zoneName string) (cfZone, er return zones[0], nil } -// doAPIRequest authenticates the request req and does the round trip. It returns -// the decoded response from Cloudflare if successful; otherwise it returns an +func (p *Provider) doZoneAPIRequest(req *http.Request, result interface{}) (cfResponse, error) { + var token string + if p.APIToken != "" { + token = p.APIToken + } else if p.ZoneToken != "" { + token = p.ZoneToken + } else { + return cfResponse{}, fmt.Errorf("you must set either api_token or zone_token") + } + + req.Header.Set("Authorization", "Bearer " + token) + + return p.doAPIRequest(req, result) +} + +func (p *Provider) doDNSAPIRequest(req *http.Request, result interface{}) (cfResponse, error) { + var token string + if p.APIToken != "" { + token = p.APIToken + } else if p.DNSToken != "" { + token = p.DNSToken + } else { + return cfResponse{}, fmt.Errorf("you must set either api_token or dns_token") + } + + req.Header.Set("Authorization", "Bearer " + token) + + return p.doAPIRequest(req, result) +} + +// doAPIRequest does the round trip, using the authentication set in the above functions. +// It returns the decoded response from Cloudflare if successful; otherwise it returns an // error including error information from the API if applicable. If result is a // non-nil pointer, the result field from the API response will be decoded into // it for convenience. func (p *Provider) doAPIRequest(req *http.Request, result interface{}) (cfResponse, error) { - req.Header.Set("Authorization", "Bearer "+p.APIToken) + resp, err := http.DefaultClient.Do(req) if err != nil { diff --git a/provider.go b/provider.go index 413cd31..0bd386e 100644 --- a/provider.go +++ b/provider.go @@ -12,12 +12,18 @@ import ( // Provider implements the libdns interfaces for Cloudflare. // TODO: Support pagination and retries, handle rate limits. type Provider struct { - // API token is used for authentication. Make sure to use a - // scoped API **token**, NOT a global API **key**. It will - // need two permissions: Zone-Zone-Read and Zone-DNS-Edit, - // unless you are only using `GetRecords()`, in which case - // the second can be changed to Read. - APIToken string `json:"api_token,omitempty"` + // API tokens are used for authentication. Make sure to use + // two scoped API **tokens**, NOT a global API **key**. + // Key 1: + // - Zone-Zone-Read (All Zones) + // Key 2: + // - Zone-DNS-Edit (Relevant Zone) + // However, if you are only using `GetRecords()`, in which + // case the second can be changed to Read. + // This prevents having a single key which can edit ALL Zones + APIToken string `json:"api_token,omitempty"` // Backwards compatibility + ZoneToken string `json:"zone_token,omitempty"` + DNSToken string `json:"dns_token,omitempty"` zones map[string]cfZone zonesMu sync.Mutex