-
Notifications
You must be signed in to change notification settings - Fork 126
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(automation_tokens): Add support for automation-tokens (#547)
* feat(automation_tokens): add support for automation-tokens * tidy up comments
- Loading branch information
1 parent
bd8b494
commit 8283d41
Showing
6 changed files
with
513 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
package fastly | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"time" | ||
) | ||
|
||
// AutomationTokenRole is used to match possible automation token roles. | ||
type AutomationTokenRole string | ||
|
||
const ( | ||
// BillingRole allows view access to basic information about service configurations, | ||
// invoices, and account billing history. | ||
BillingRole AutomationTokenRole = "billing" | ||
// EngineerRole allows creating services and managing their configurations. | ||
EngineerRole AutomationTokenRole = "engineer" | ||
// UserRole allows view access to basic information about service configurations, | ||
// and controls. | ||
UserRole AutomationTokenRole = "user" | ||
) | ||
|
||
// AutomationTokenPaginator is used for pagination on AutomationToken endpoints. | ||
// as they return JSONAPI data. | ||
type AutomationTokenPaginator struct { | ||
Data []*AutomationToken `mapstructure:"data"` | ||
Meta AutomationTokenPaginatorMeta `mapstructure:"meta"` | ||
} | ||
|
||
// AutomationTokenPaginatorMeta represents the metadata for an AutomationTokenPaginator. | ||
type AutomationTokenPaginatorMeta struct { | ||
CurrentPage int `mapstructure:"current_page"` | ||
PerPage int `mapstructure:"per_page"` | ||
RecordCount int `mapstructure:"record_count"` | ||
TotalPages int `mapstructure:"total_pages"` | ||
} | ||
|
||
// AutomationToken represents an API token which allows non-human clients to | ||
// authenticate requests to the Fastly API. | ||
type AutomationToken struct { | ||
AccessToken *string `mapstructure:"access_token"` | ||
CreatedAt *time.Time `mapstructure:"created_at"` | ||
ExpiresAt *time.Time `mapstructure:"expires_at"` | ||
IP *string `mapstructure:"ip"` | ||
LastUsedAt *time.Time `mapstructure:"last_used_at"` | ||
Name *string `mapstructure:"name"` | ||
Role *AutomationTokenRole `mapstructure:"role"` | ||
Scope *TokenScope `mapstructure:"scope"` | ||
Services []string `mapstructure:"services"` | ||
TLSAccess *bool `mapstructure:"tls_access"` | ||
TokenID *string `mapstructure:"id"` | ||
UserID *string `mapstructure:"user_id"` | ||
CustomerID *string `mapstructure:"customer_id"` | ||
} | ||
|
||
// GetAutomationTokensInput is used as input to the GetAutomationTokens function. | ||
type GetAutomationTokensInput struct { | ||
// Page is the current page. | ||
Page *int | ||
// PerPage is the number of records per page. | ||
PerPage *int | ||
} | ||
|
||
// GetAutomationTokens retrieves all resources. | ||
func (c *Client) GetAutomationTokens(i *GetAutomationTokensInput) *ListPaginator[AutomationTokenPaginator] { | ||
input := ListOpts{} | ||
if i.Page != nil { | ||
input.Page = *i.Page | ||
} | ||
if i.PerPage != nil { | ||
input.PerPage = *i.PerPage | ||
} | ||
return NewPaginator[AutomationTokenPaginator](c, input, "/automation-tokens") | ||
} | ||
|
||
// ListAutomationTokens retrieves all resources. | ||
func (c *Client) ListAutomationTokens() ([]*AutomationToken, error) { | ||
p := c.GetAutomationTokens(&GetAutomationTokensInput{}) | ||
var results []*AutomationToken | ||
for p.HasNext() { | ||
data, err := p.GetNext() | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get next page (remaining: %d): %s", p.Remaining(), err) | ||
} | ||
|
||
for _, t := range data { | ||
results = append(results, t.Data...) | ||
} | ||
} | ||
return results, nil | ||
} | ||
|
||
// GetAutomationTokenInput is used as input to the GetAutomationToken function. | ||
type GetAutomationTokenInput struct { | ||
// TokenID is an alphanumeric string identifying the token (required). | ||
TokenID string | ||
} | ||
|
||
// GetAutomationToken retrieves a specific resource by ID. | ||
func (c *Client) GetAutomationToken(i *GetAutomationTokenInput) (*AutomationToken, error) { | ||
if i.TokenID == "" { | ||
return nil, ErrMissingTokenID | ||
} | ||
|
||
path := ToSafeURL("automation-tokens", i.TokenID) | ||
|
||
resp, err := c.Get(path, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer resp.Body.Close() | ||
|
||
var t *AutomationToken | ||
if err := decodeBodyMap(resp.Body, &t); err != nil { | ||
return nil, err | ||
} | ||
return t, nil | ||
} | ||
|
||
// CreateAutomationTokenInput is used as input to the CreateAutomationToken function. | ||
type CreateAutomationTokenInput struct { | ||
// ExpiresAt is a time-stamp (UTC) of when the token will expire. | ||
ExpiresAt time.Time `json:"expires_at" url:"expires_at,omitempty"` | ||
// Name is the name of the token. | ||
Name string `json:"name" url:"name,omitempty"` | ||
// Password is the token password. | ||
Password *string `json:"-" url:"password,omitempty"` | ||
// Role is the role on the token (billing, engineer, user). | ||
Role AutomationTokenRole `json:"role" url:"role,omitempty"` | ||
// Scope is a space-delimited list of authorization scope (global, purge_select, purge_all, global). | ||
Scope *TokenScope `json:"scope,omitempty" url:"scope,omitempty"` | ||
// Services is a list of alphanumeric strings identifying services. | ||
// If no services are specified, the token will have access to all services on the account. | ||
Services []string `json:"services" url:"services,omitempty,brackets"` | ||
// Username is the email of the user the token is assigned to. | ||
Username *string `json:"-" url:"username,omitempty"` | ||
// TLSAccess indicates whether TLS access is enabled for the token. | ||
TLSAccess bool `json:"tls_access" url:"tls_access,omitempty"` | ||
} | ||
|
||
// CreateAutomationToken creates a new resource. | ||
// | ||
// Requires sudo capability for the token being used. | ||
func (c *Client) CreateAutomationToken(i *CreateAutomationTokenInput) (*AutomationToken, error) { | ||
_, err := c.PostForm("/sudo", i, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
resp, err := c.PostJSON("/automation-tokens", i, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer resp.Body.Close() | ||
|
||
var t *AutomationToken | ||
if err := decodeBodyMap(resp.Body, &t); err != nil { | ||
return nil, err | ||
} | ||
return t, nil | ||
} | ||
|
||
// DeleteAutomationTokenInput is used as input to the DeleteAutomationToken function. | ||
type DeleteAutomationTokenInput struct { | ||
// TokenID is an alphanumeric string identifying a token (required). | ||
TokenID string | ||
} | ||
|
||
// DeleteAutomationToken deletes the specified resource. | ||
func (c *Client) DeleteAutomationToken(i *DeleteAutomationTokenInput) error { | ||
if i.TokenID == "" { | ||
return ErrMissingTokenID | ||
} | ||
|
||
path := ToSafeURL("tokens", i.TokenID) | ||
|
||
resp, err := c.Delete(path, nil) | ||
if err != nil { | ||
return err | ||
} | ||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode != http.StatusNoContent { | ||
return ErrNotOK | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package fastly | ||
|
||
import ( | ||
"testing" | ||
) | ||
|
||
func TestClient_ListAutomationTokens(t *testing.T) { | ||
t.Parallel() | ||
|
||
var tokens []*AutomationToken | ||
var err error | ||
record(t, "automation_tokens/list", func(c *Client) { | ||
tokens, err = c.ListAutomationTokens() | ||
}) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if len(tokens) < 1 { | ||
t.Errorf("bad tokens: %v", tokens) | ||
} | ||
} | ||
|
||
func TestClient_GetAutomationToken(t *testing.T) { | ||
t.Parallel() | ||
|
||
input := &GetAutomationTokenInput{ | ||
TokenID: "XXXXXXXXXXXXXXXXXXXXXX", | ||
} | ||
|
||
var token *AutomationToken | ||
var err error | ||
record(t, "automation_tokens/get", func(c *Client) { | ||
token, err = c.GetAutomationToken(input) | ||
}) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
t.Logf("%+v", token) | ||
} | ||
|
||
func TestClient_CreateAutomationToken(t *testing.T) { | ||
t.Parallel() | ||
|
||
input := &CreateAutomationTokenInput{ | ||
Name: "my-test-token", | ||
Role: EngineerRole, | ||
Scope: ToPointer(GlobalScope), | ||
Username: ToPointer("XXXXXXXXXXXXXXXXXXXXXX"), | ||
Password: ToPointer("XXXXXXXXXXXXXXXXXXXXXX"), | ||
} | ||
|
||
var token *AutomationToken | ||
var err error | ||
record(t, "automation_tokens/create", func(c *Client) { | ||
token, err = c.CreateAutomationToken(input) | ||
}) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if *token.Name != input.Name { | ||
t.Errorf("returned invalid name, got %s, want %s", *token.Name, input.Name) | ||
} | ||
if *token.Scope != *input.Scope { | ||
t.Errorf("returned invalid scope, got %s, want %s", *token.Scope, *input.Scope) | ||
} | ||
} | ||
|
||
func TestClient_DeleteAutomationToken(t *testing.T) { | ||
t.Parallel() | ||
|
||
input := &DeleteAutomationTokenInput{ | ||
TokenID: "XXXXXXXXXXXXXXXXXXXXXX", | ||
} | ||
|
||
var err error | ||
record(t, "automation_tokens/delete", func(c *Client) { | ||
err = c.DeleteAutomationToken(input) | ||
}) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
--- | ||
version: 1 | ||
interactions: | ||
- request: | ||
body: name=my-test-token&password=XXXXXXXXXXXXXXXXXXXXXX&role=engineer&scope=global&username=XXXXXXXXXXXXXXXXXXXXXX | ||
form: | ||
name: | ||
- my-test-token | ||
password: | ||
- XXXXXXXXXXXXXXXXXXXXXX | ||
role: | ||
- engineer | ||
scope: | ||
- global | ||
username: | ||
- XXXXXXXXXXXXXXXXXXXXXX | ||
headers: | ||
Content-Type: | ||
- application/x-www-form-urlencoded | ||
User-Agent: | ||
- FastlyGo/9.8.0 (+github.com/fastly/go-fastly; go1.22.2) | ||
url: https://api.fastly.com/sudo | ||
method: POST | ||
response: | ||
body: '{"expiry_time":"2024-09-21T04:01:00+00:00"}' | ||
headers: | ||
Accept-Ranges: | ||
- bytes | ||
Cache-Control: | ||
- no-store | ||
Content-Type: | ||
- application/json | ||
Date: | ||
- Sat, 21 Sep 2024 03:56:00 GMT | ||
Fastly-Ratelimit-Remaining: | ||
- "975" | ||
Fastly-Ratelimit-Reset: | ||
- "1726891200" | ||
Pragma: | ||
- no-cache | ||
Server: | ||
- control-gateway | ||
Status: | ||
- 200 OK | ||
Strict-Transport-Security: | ||
- max-age=31536000 | ||
Vary: | ||
- Accept-Encoding | ||
Via: | ||
- 1.1 varnish, 1.1 varnish | ||
X-Cache: | ||
- MISS, MISS | ||
X-Cache-Hits: | ||
- 0, 0 | ||
X-Served-By: | ||
- cache-chi-kigq8000058-CHI, cache-per12629-PER | ||
X-Timer: | ||
- S1726890960.858190,VS0,VE534 | ||
status: 200 OK | ||
code: 200 | ||
duration: "" | ||
- request: | ||
body: '{"expires_at":"0001-01-01T00:00:00Z","name":"my-test-token","role":"engineer","scope":"global","services":null,"tls_access":false}' | ||
form: {} | ||
headers: | ||
Accept: | ||
- application/json | ||
Content-Type: | ||
- application/json | ||
User-Agent: | ||
- FastlyGo/9.8.0 (+github.com/fastly/go-fastly; go1.22.2) | ||
url: https://api.fastly.com/automation-tokens | ||
method: POST | ||
response: | ||
body: | | ||
{"id":"XXXXXXXXXXXXXXXXXXXXXX","services":[],"name":"my-test-token","role":"engineer","access_token":"XXXXXXXXXXXXXXXXXXXXXX","scope":"global","ip":"","created_at":"2024-09-21T03:56:00Z","last_used_at":"0001-01-01T00:00:00Z","expires_at":null,"user_agent":""} | ||
headers: | ||
Accept-Ranges: | ||
- bytes | ||
Cache-Control: | ||
- no-store | ||
Content-Length: | ||
- "270" | ||
Content-Type: | ||
- application/json | ||
Date: | ||
- Sat, 21 Sep 2024 03:56:01 GMT | ||
Fastly-Ratelimit-Remaining: | ||
- "993" | ||
Fastly-Ratelimit-Reset: | ||
- "1726891200" | ||
Pragma: | ||
- no-cache | ||
Server: | ||
- control-gateway | ||
Strict-Transport-Security: | ||
- max-age=31536000 | ||
Via: | ||
- 1.1 varnish, 1.1 varnish | ||
X-Cache: | ||
- MISS, MISS | ||
X-Cache-Hits: | ||
- 0, 0 | ||
X-Served-By: | ||
- cache-chi-kigq8000082-CHI, cache-per12629-PER | ||
X-Timer: | ||
- S1726890960.404951,VS0,VE753 | ||
status: 200 OK | ||
code: 200 | ||
duration: "" |
Oops, something went wrong.