Skip to content

Commit c6d0554

Browse files
authored
feat(client): Add support for impersonation. (#724)
A new 'impersonation' package provides a function which can be used to construct a context.Context object containing a Fastly CID. If that context is passed to any of go-fastly's API functions which generate HTTP requests to the Fastly API endpoint, the contained CID will be included in the request. As noted in the package documentation, impersonation is only available to Fastly employees, it is not usable by customers. All Submissions: * [X] Have you followed the guidelines in our Contributing document? * [X] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/fastly/go-fastly/pulls) for the same update/change? <!-- You can erase any parts of this template not applicable to your Pull Request. --> ### New Feature Submissions: * [X] Does your submission pass tests? ### Changes to Core Features: * [X] Have you added an explanation of what your changes do and why you'd like us to include them? * [X] Have you written new tests for your core changes, as applicable? * [X] Have you successfully run tests with your changes locally?
1 parent b0e79e0 commit c6d0554

File tree

7 files changed

+103
-3
lines changed

7 files changed

+103
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
### Enhancements:
1010

11+
- feat(client): Add support for impersonation. [#724](https://github.com/fastly/go-fastly/pull/724)
12+
1113
### Bug fixes:
1214

1315
### Dependencies:
@@ -29,6 +31,7 @@
2931
- feat(ngwaf/v1/signals): moves signals up to the base level and adds support for account level signals ([#722](https://github.com/fastly/go-fastly/pull/722))
3032

3133
### Enhancements:
34+
3235
- feat(ngwaf): add support for alerts ([#714](https://github.com/fastly/go-fastly/pull/714))
3336
- feat(ngwaf/v1/workspaces/thresholds): adds CRUD support for NGWAF Thresholds ([#713](https://github.com/fastly/go-fastly/pull/713))
3437
- feat(tls_custom_certificate): Add support for allow_untrusted_root attribute ([#596](https://github.com/fastly/go-fastly/pull/596))
@@ -38,6 +41,7 @@
3841
- fix(client): Implement new mechanism for serializing mutating requests. ([#715](https://github.com/fastly/go-fastly/pull/715))
3942

4043
### Dependencies:
44+
4145
- build(deps): `golang.org/x/crypto` from 0.39.0 to 0.40.0 ([#720](https://github.com/fastly/go-fastly/pull/720))
4246
- build(deps): `golang.org/x/sys` from 0.33.0 to 0.34.0 ([#720](https://github.com/fastly/go-fastly/pull/720))
4347

fastly/client.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"github.com/google/jsonapi"
2424
"github.com/hashicorp/go-cleanhttp"
2525
"github.com/mitchellh/mapstructure"
26+
27+
"github.com/fastly/go-fastly/v11/fastly/impersonation"
2628
)
2729

2830
// APIKeyEnvVar is the name of the environment variable where the Fastly API
@@ -313,6 +315,10 @@ func (c *Client) DeleteJSONAPIBulk(ctx context.Context, p string, i any, ro Requ
313315
// Request makes an HTTP request against the HTTPClient using the given verb,
314316
// Path, and request options.
315317
func (c *Client) Request(ctx context.Context, verb, p string, ro RequestOptions) (*http.Response, error) {
318+
if id, ok := impersonation.CustomerIDFromContext(ctx); ok {
319+
ro.Params[impersonation.QueryParam] = id
320+
}
321+
316322
req, err := c.RawRequest(ctx, verb, p, ro)
317323
if err != nil {
318324
return nil, err

fastly/client_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import (
66
"net/url"
77
"strings"
88
"testing"
9+
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/fastly/go-fastly/v11/fastly/impersonation"
913
)
1014

1115
func TestClient_RawRequest(t *testing.T) {
@@ -55,3 +59,31 @@ func TestClient_RawRequest(t *testing.T) {
5559
}
5660
}
5761
}
62+
63+
type impersonationRoundTripper struct {
64+
rawQuery string
65+
}
66+
67+
func (irt *impersonationRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
68+
irt.rawQuery = req.URL.RawQuery
69+
return &http.Response{StatusCode: http.StatusOK}, nil
70+
}
71+
72+
func TestClient_Impersonation(t *testing.T) {
73+
const testCustomerID = "1234ABCD"
74+
75+
require := require.New(t)
76+
t.Parallel()
77+
78+
c, err := NewClient("nokey")
79+
require.NoError(err)
80+
81+
irt := &impersonationRoundTripper{}
82+
ro := CreateRequestOptions()
83+
c.HTTPClient = &http.Client{Transport: irt}
84+
85+
_, err = c.Request(impersonation.NewContextForCustomerID(context.TODO(), testCustomerID), http.MethodGet, "/test", ro)
86+
require.NoError(err)
87+
88+
require.Equal(impersonation.QueryParam+"="+testCustomerID, irt.rawQuery, "unexpected query parameter")
89+
}

fastly/impersonation/doc.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Package impersonation provides a method for using a
2+
// [context.Context] object to provide a Fastly CID that will be used
3+
// for API requests made using the context. This method is used by
4+
// Fastlyans to issue API requests 'on behalf of' (impersonating) a
5+
// Fastly customer.
6+
//
7+
// Note: This impersonation mechanism is only usable by Fastly employees.
8+
package impersonation

fastly/impersonation/impersonation.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package impersonation
2+
3+
import (
4+
"context"
5+
)
6+
7+
const QueryParam = "customer_id"
8+
9+
type impersonationKey struct{}
10+
11+
var key impersonationKey
12+
13+
// NewContextForCustomerID returns a [context.Context] which contains
14+
// the customer ID specified in the id parameter.
15+
//
16+
// This [context.Context] should be passed to the various request
17+
// methods of [fastly.Client] so that those request methods can use
18+
// the included customer ID to impersonate the customer when the
19+
// request is made.
20+
func NewContextForCustomerID(ctx context.Context, id string) context.Context {
21+
return context.WithValue(ctx, key, id)
22+
}
23+
24+
// CustomerIDFromContext returns the customer ID contained within the
25+
// provided [context.Context]. If the provided [context.Context] does
26+
// not contain a customer ID, then `false` is returned along with an
27+
// empty string.
28+
func CustomerIDFromContext(ctx context.Context) (string, bool) {
29+
id, ok := ctx.Value(key).(string)
30+
return id, ok
31+
}

fastly/purge.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"net/http"
66
"net/url"
77
"strings"
8+
9+
"github.com/fastly/go-fastly/v11/fastly/impersonation"
810
)
911

1012
// Purge is a response from a purge request.
@@ -44,6 +46,10 @@ func (c *Client) Purge(ctx context.Context, i *PurgeInput) (*Purge, error) {
4446
return nil, err
4547
}
4648

49+
if id, ok := impersonation.CustomerIDFromContext(ctx); ok {
50+
requestOptions.Params[impersonation.QueryParam] = id
51+
}
52+
4753
resp, err := c.Post(ctx, "purge/"+i.URL, requestOptions)
4854
if err != nil {
4955
return nil, err
@@ -99,6 +105,10 @@ func (c *Client) PurgeKey(ctx context.Context, i *PurgeKeyInput) (*Purge, error)
99105
requestOptions := CreateRequestOptions()
100106
requestOptions.Parallel = true
101107

108+
if id, ok := impersonation.CustomerIDFromContext(ctx); ok {
109+
requestOptions.Params[impersonation.QueryParam] = id
110+
}
111+
102112
req, err := c.RawRequest(ctx, http.MethodPost, path, requestOptions)
103113
if err != nil {
104114
return nil, err
@@ -145,6 +155,10 @@ func (c *Client) PurgeKeys(ctx context.Context, i *PurgeKeysInput) (map[string]s
145155
requestOptions := CreateRequestOptions()
146156
requestOptions.Parallel = true
147157

158+
if id, ok := impersonation.CustomerIDFromContext(ctx); ok {
159+
requestOptions.Params[impersonation.QueryParam] = id
160+
}
161+
148162
req, err := c.RawRequest(ctx, http.MethodPost, path, requestOptions)
149163
if err != nil {
150164
return nil, err
@@ -183,7 +197,12 @@ func (c *Client) PurgeAll(ctx context.Context, i *PurgeAllInput) (*Purge, error)
183197

184198
path := ToSafeURL("service", i.ServiceID, "purge_all")
185199

186-
req, err := c.RawRequest(ctx, http.MethodPost, path, CreateRequestOptions())
200+
requestOptions := CreateRequestOptions()
201+
if id, ok := impersonation.CustomerIDFromContext(ctx); ok {
202+
requestOptions.Params[impersonation.QueryParam] = id
203+
}
204+
205+
req, err := c.RawRequest(ctx, http.MethodPost, path, requestOptions)
187206
if err != nil {
188207
return nil, err
189208
}

fastly/resource_locking.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ type ResourceLockManager struct {
2020
locks map[string]weak.Pointer[sync.Mutex]
2121
}
2222

23-
// NewResourceLockManager creates constructs a [ResourceLockManager]
24-
// with an empty map.
23+
// NewResourceLockManager constructs a [ResourceLockManager] with an
24+
// empty map.
2525
func NewResourceLockManager() *ResourceLockManager {
2626
return &ResourceLockManager{
2727
locks: make(map[string]weak.Pointer[sync.Mutex]),

0 commit comments

Comments
 (0)