Skip to content

Commit

Permalink
feat(fastly/objectstorage/accesskeys): adds crud operations for acces…
Browse files Browse the repository at this point in the history
…s keys in object storage
  • Loading branch information
anthony-gomez-fastly committed Feb 20, 2025
1 parent a83e0ea commit 9e8cbcd
Show file tree
Hide file tree
Showing 16 changed files with 575 additions and 2 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## Unreleased

[Full Changelog](https://github.com/fastly/go-fastly/compare/v9.13.1...)

**Enhancements:**

- feat(fastly/objectstorage): adds crud operations for access keys in object storage [#612](https://github.com/fastly/go-fastly/pull/612)

**Bug fixes:**

**Dependencies:**

## [v9.13.1](https://github.com/fastly/go-fastly/releases/tag/v9.13.1) (2025-02-14)

[Full Changelog](https://github.com/fastly/go-fastly/compare/v9.13.0...v9.13.1)
Expand Down
4 changes: 2 additions & 2 deletions fastly/computeacls/api_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type ComputeACL struct {

// ComputeACLs is the API response structure for the list compute ACLs operation.
type ComputeACLs struct {
// Data is the list of returned cumpute ACLs.
// Data is the list of returned compute ACLs.
Data []ComputeACL `json:"data"`
// Meta is the information for total compute ACLs.
Meta MetaACLs `json:"meta"`
Expand All @@ -32,7 +32,7 @@ type ComputeACLEntry struct {

// ComputeACLEntries is the API response structure for the list compute ACL entries operation.
type ComputeACLEntries struct {
// Entries is the list of returned cumpute ACL entries.
// Entries is the list of returned compute ACL entries.
Entries []ComputeACLEntry
// Meta is the information for pagination.
Meta MetaEntries `json:"meta"`
Expand Down
12 changes: 12 additions & 0 deletions fastly/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ var ErrMissingType = NewFieldError("Type")
// requires a "CustomerID" key, but one was not set.
var ErrMissingCustomerID = NewFieldError("CustomerID")

// ErrMissingAccessKeyID is an error that is returned when an input struct
// requires a "AccessKeyID" key, but one was not set.
var ErrMissingAccessKeyID = NewFieldError("AccessKeyID")

// ErrMissingBuckets is an error that is returned when an input struct
// requires a "Buckets" key, but one was not set.
var ErrMissingBuckets = NewFieldError("Buckets")

// ErrMissingDescription is an error that is returned when an input struct
// requires a "Description" key, but one was not set.
var ErrMissingDescription = NewFieldError("Description")

// ErrMissingDictionaryID is an error that is returned when an input struct
// requires a "DictionaryID" key, but one was not set.
var ErrMissingDictionaryID = NewFieldError("DictionaryID")
Expand Down
43 changes: 43 additions & 0 deletions fastly/objectstorage/accesskeys/api_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package accesskeys

import (
"encoding/json"
"fmt"

"github.com/fastly/go-fastly/v9/fastly"
)

// CreateInput specifies the information needed for the Create() function to
// perform the operation.
type CreateInput struct {
// Description is a description of the access key (required).
Description *string `json:"description"`
// Permission is the permissions the access key will have (required).
Permission *string `json:"permission"`
// Buckets are the buckets the access key will have (optional).
Buckets *[]string `json:"buckets"`
}

// Create creates a new Object Storage Access Key.
func Create(c *fastly.Client, i *CreateInput) (*AccessKey, error) {
if i.Description == nil {
return nil, fastly.ErrMissingDescription
}

if i.Permission == nil {
return nil, fastly.ErrMissingPermission
}

resp, err := c.PostJSON("/resources/object-storage/access-keys", i, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var accessKey *AccessKey
if err := json.NewDecoder(resp.Body).Decode(&accessKey); err != nil {
return nil, fmt.Errorf("failed to decode json response: %w", err)
}

return accessKey, nil
}
34 changes: 34 additions & 0 deletions fastly/objectstorage/accesskeys/api_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package accesskeys

import (
"net/http"

"github.com/fastly/go-fastly/v9/fastly"
)

// DeleteInput specifies the information needed for the Delete() function to
// perform the operation.
type DeleteInput struct {
// AccessKeyID is an AccessKey Identifier (required).
AccessKeyID *string
}

// DeleteAccessKey deletes an access key
func Delete(c *fastly.Client, i *DeleteInput) error {
if i.AccessKeyID == nil {
return fastly.ErrMissingAccessKeyID
}

path := fastly.ToSafeURL("resources", "object-storage", "access-keys", *i.AccessKeyID)

resp, err := c.Delete(path, nil)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusNoContent {
return fastly.NewHTTPError(resp)
}
return nil
}
37 changes: 37 additions & 0 deletions fastly/objectstorage/accesskeys/api_get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package accesskeys

import (
"encoding/json"
"fmt"

"github.com/fastly/go-fastly/v9/fastly"
)

// GetInput specifies the information needed for the Get() function to perform
// the operation.
type GetInput struct {
// AccessKeyID is an AccessKey Identifier (required).
AccessKeyID *string
}

// GetAccessKey finds an access key with the given ID if the user has the correct permisssions
func Get(c *fastly.Client, i *GetInput) (*AccessKey, error) {
if i.AccessKeyID == nil {
return nil, fastly.ErrMissingAccessKeyID
}

path := fastly.ToSafeURL("resources", "object-storage", "access-keys", *i.AccessKeyID)

resp, err := c.Get(path, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var entry *AccessKey
if err := json.NewDecoder(resp.Body).Decode(&entry); err != nil {
return nil, fmt.Errorf("failed to decode json response: %w", err)
}

return entry, nil
}
25 changes: 25 additions & 0 deletions fastly/objectstorage/accesskeys/api_list_access_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package accesskeys

import (
"encoding/json"
"fmt"

"github.com/fastly/go-fastly/v9/fastly"
)

// ListAccessKeys retrieves all access keys within object storage that this Fastly api key has access to.
func ListAccessKeys(c *fastly.Client) (*AccessKeys, error) {
resp, err := c.Get("/resources/object-storage/access-keys", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var accessKeys *AccessKeys

if err := json.NewDecoder(resp.Body).Decode(&accessKeys); err != nil {
return nil, fmt.Errorf("failed to decode json response: %w", err)
}

return accessKeys, nil
}
25 changes: 25 additions & 0 deletions fastly/objectstorage/accesskeys/api_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package accesskeys

// AccessKey is the API response structure for the create and describe operations.
type AccessKey struct {
// AccessKey is an AccessKey identifier.
AccessKeyID string `json:"access_key"`
// Description is human readable description for the access key.
Description string `json:"description"`
// Permission is the permissions the key has.
Permission string `json:"permission"`
// Buckets is the list of buckets associated with the access key.
Buckets []string `json:"buckets"`
// CreatedAt is the timestamp associated with the creation of the access key.
CreatedAt string `json:"created_at"`
}

// AccessKeys is the API response structure for the list access keys operation.
type AccessKeys struct {
// Data is the list of returned AccessKeys.
Data []AccessKey `json:"data"`
// Meta is additional information about the request
Meta MetaAccessKeys `json:"meta"`
}

type MetaAccessKeys struct{}
161 changes: 161 additions & 0 deletions fastly/objectstorage/accesskeys/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package accesskeys

import (
"testing"

"github.com/fastly/go-fastly/v9/fastly"
)

func TestClient_AccessKey(t *testing.T) {
t.Parallel()

TestAccessKeyDescription := "THIS IS A TEST ACCESS KEY"
TestAccessKeyPermission := "read-write-objects"
TestAccessKeyBuckets := []string{"test-bucket"}

var accessKeys *AccessKeys
var err error

// List all AccessKeys.
fastly.Record(t, "list_accesskeys", func(c *fastly.Client) {
accessKeys, err = ListAccessKeys(c)
})
if err != nil {
t.Fatal(err)
}

// Make sure the test AccessKey we're going to create isn't among them.
for _, ak := range accessKeys.Data {
if ak.Description == TestAccessKeyDescription &&
ak.Permission == TestAccessKeyPermission &&
len(ak.Buckets) == len(TestAccessKeyBuckets) &&
ak.Buckets[0] == TestAccessKeyBuckets[0] {
t.Errorf("found test AccessKey %q, aborting", ak.AccessKeyID)
}
}

// Create a AccessKey for testing.
var accessKey *AccessKey
fastly.Record(t, "create_AccessKey", func(c *fastly.Client) {
accessKey, err = Create(c, &CreateInput{
Description: fastly.ToPointer(TestAccessKeyDescription),
Permission: fastly.ToPointer(TestAccessKeyPermission),
Buckets: fastly.ToPointer(TestAccessKeyBuckets),
})
})
if err != nil {
t.Fatal(err)
}
if accessKey.Description != TestAccessKeyDescription {
t.Errorf("unexpected AccessKey name: got %q, expected %q", accessKey.Description, TestAccessKeyDescription)
}
if accessKey.Permission != TestAccessKeyPermission {
t.Errorf("unexpected AccessKey permission: got %q, expected %q", accessKey.Permission, TestAccessKeyPermission)
}
if len(accessKey.Buckets) != len(TestAccessKeyBuckets) {
t.Errorf("unexpected AccessKey buckets length: got %q, expected %q", len(accessKey.Buckets), len(TestAccessKeyBuckets))
}
if accessKey.Buckets[0] != TestAccessKeyBuckets[0] {
t.Errorf("unexpected AccessKey bucket: got %q, expected %q", accessKey.Buckets[0], TestAccessKeyBuckets[0])
}

// Ensure we delete the test AccessKey at the end.
defer func() {
fastly.Record(t, "delete_AccessKey", func(c *fastly.Client) {
err = Delete(c, &DeleteInput{
AccessKeyID: fastly.ToPointer(accessKey.AccessKeyID),
})
})
if err != nil {
t.Errorf("error during AccessKey cleanup: %v", err)
}
}()

// Get the test AccessKey.
var ac *AccessKey
fastly.Record(t, "get_AccessKey", func(c *fastly.Client) {
ac, err = Get(c, &GetInput{
AccessKeyID: fastly.ToPointer(accessKey.AccessKeyID),
})
})
if err != nil {
t.Fatal(err)
}
if ac.Description != accessKey.Description {
t.Errorf("unexpected AccessKey Description: got %q, expected %q", ac.Description, accessKey.Description)
}
if ac.Permission != accessKey.Permission {
t.Errorf("unexpected AccessKey Permissions: got %q, expected %q", ac.Permission, accessKey.Permission)
}
if len(ac.Buckets) != len(accessKey.Buckets) {
t.Errorf("unexpected AccessKey Buckets length: got %q, expected %q", len(ac.Buckets), len(accessKey.Buckets))
}
if ac.Buckets[0] != accessKey.Buckets[0] {
t.Errorf("unexpected AccessKey Buckets contents: got %q, expected %q", ac.Buckets[0], accessKey.Buckets[0])
}

// List all entries of the test AccessKey and compare it to the input.
var actualAccessKeys *AccessKeys
fastly.Record(t, "list_accesskeys_with_new", func(c *fastly.Client) {
actualAccessKeys, err = ListAccessKeys(c)
})
if err != nil {
t.Errorf("error fetching list of AccessKey entries: %v", err)
}

actualNumberOfAccessKeyEntries := len(actualAccessKeys.Data)
expectedNumberOfAccessKeyEntries := len(accessKeys.Data)
// This checks the original number of access keys fetched in the creation check vs the number fetched after adding the test key
if actualNumberOfAccessKeyEntries != expectedNumberOfAccessKeyEntries+1 {
t.Errorf("incorrect number of AccessKeys returned, expected: %d, got %d", expectedNumberOfAccessKeyEntries, actualNumberOfAccessKeyEntries)
}

// Make sure the test AccessKey we've created is among them.
var newKeyPresent = false
for _, rak := range actualAccessKeys.Data {
if rak.Description == TestAccessKeyDescription &&
rak.Permission == TestAccessKeyPermission &&
len(rak.Buckets) == len(TestAccessKeyBuckets) &&
rak.Buckets[0] == TestAccessKeyBuckets[0] {
newKeyPresent = true
}
}
if !newKeyPresent {
t.Errorf("missing test AccessKey %q, aborting", accessKey.AccessKeyID)
}
}

func TestClient_Create_validation(t *testing.T) {
_, err := Create(fastly.TestClient, &CreateInput{
Description: nil,
})
if err != fastly.ErrMissingDescription {
t.Errorf("expected ErrMissingDescription: got %s", err)
}

_, err = Create(fastly.TestClient, &CreateInput{
Description: fastly.ToPointer("description"),
Permission: nil,
})
if err != fastly.ErrMissingPermission {
t.Errorf("expected ErrMissingPermission: got %s", err)
}
}

func TestClient_Get_validation(t *testing.T) {
_, err := Get(fastly.TestClient, &GetInput{
AccessKeyID: nil,
})
if err != fastly.ErrMissingAccessKeyID {
t.Errorf("expected ErrMissingAccessKeyID: got %s", err)
}
}

func TestClient_Delete_validation(t *testing.T) {
err := Delete(fastly.TestClient, &DeleteInput{
AccessKeyID: nil,
})
if err != fastly.ErrMissingAccessKeyID {
t.Errorf("expected ErrMissingAccessKeyID: got %s", err)
}
}
3 changes: 3 additions & 0 deletions fastly/objectstorage/accesskeys/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package accesskeys contains subpackages which offer various operations to
// configure access keys.
package accesskeys
Loading

0 comments on commit 9e8cbcd

Please sign in to comment.