Skip to content

Commit 40293ed

Browse files
authored
signedrequests: Pipe context in signing requests (#6344)
Without this, our traces are often disconnected for requests that use request signing.
1 parent 2eaf335 commit 40293ed

2 files changed

Lines changed: 116 additions & 18 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/parca-dev/parca
33
go 1.25.0
44

55
require (
6+
cloud.google.com/go/compute/metadata v0.9.0
67
cloud.google.com/go/storage v1.61.3
78
github.com/ClickHouse/clickhouse-go/v2 v2.43.0
89
github.com/alecthomas/kong v0.9.0
@@ -74,7 +75,6 @@ require (
7475
cloud.google.com/go v0.123.0 // indirect
7576
cloud.google.com/go/auth v0.20.0 // indirect
7677
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
77-
cloud.google.com/go/compute/metadata v0.9.0 // indirect
7878
cloud.google.com/go/iam v1.5.3 // indirect
7979
cloud.google.com/go/monitoring v1.24.3 // indirect
8080
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect

pkg/signedrequests/gcs.go

Lines changed: 115 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,41 @@ package signedrequests
1515

1616
import (
1717
"context"
18+
"encoding/base64"
19+
"encoding/json"
1820
"errors"
1921
"fmt"
2022
"io"
2123
"strconv"
2224
"time"
2325

26+
"cloud.google.com/go/compute/metadata"
2427
"cloud.google.com/go/storage"
2528
"github.com/thanos-io/objstore/providers/gcs"
2629
"golang.org/x/oauth2/google"
30+
iamcredentials "google.golang.org/api/iamcredentials/v1"
2731
"google.golang.org/api/option"
2832
"gopkg.in/yaml.v3"
2933
)
3034

3135
type GCSClient struct {
3236
bucket *storage.BucketHandle
3337
closer io.Closer
38+
39+
// googleAccessID is the service account email used to sign URLs. Detected
40+
// once at construction time; passing it explicitly into SignedURLOptions
41+
// lets us also override SignBytes, which is what enables trace-context
42+
// propagation into the IAM SignBlob call.
43+
googleAccessID string
44+
45+
// privateKey is set when the service account JSON contains one, enabling
46+
// local signing without a round-trip to IAM.
47+
privateKey []byte
48+
49+
// iamService is used for remote signing via IAM SignBlob when no private
50+
// key is available. It is context-aware, so spans emitted by its HTTP
51+
// transport are parented to the caller's trace.
52+
iamService *iamcredentials.Service
3453
}
3554

3655
func NewGCSClient(ctx context.Context, conf []byte) (*GCSClient, error) {
@@ -47,28 +66,107 @@ func NewGCSBucketWithConfig(ctx context.Context, gc gcs.Config) (*GCSClient, err
4766
return nil, errors.New("missing Google Cloud Storage bucket name for stored blocks")
4867
}
4968

50-
var opts []option.ClientOption
51-
52-
// If ServiceAccount is provided, use them in GCS client, otherwise fallback to Google default logic.
69+
var (
70+
creds *google.Credentials
71+
err error
72+
)
5373
if gc.ServiceAccount != "" {
54-
credentials, err := google.CredentialsFromJSONWithType(ctx, []byte(gc.ServiceAccount), google.ServiceAccount, storage.ScopeFullControl)
55-
if err != nil {
56-
return nil, fmt.Errorf("create credentials from JSON: %w", err)
57-
}
58-
opts = append(opts, option.WithCredentials(credentials))
74+
creds, err = google.CredentialsFromJSONWithType(ctx, []byte(gc.ServiceAccount), google.ServiceAccount, storage.ScopeFullControl)
75+
} else {
76+
creds, err = google.FindDefaultCredentials(ctx, storage.ScopeFullControl)
77+
}
78+
if err != nil {
79+
return nil, fmt.Errorf("load credentials: %w", err)
5980
}
6081

61-
opts = append(opts, option.WithUserAgent("parca"))
82+
opts := []option.ClientOption{
83+
option.WithCredentials(creds),
84+
option.WithUserAgent("parca"),
85+
}
6286

6387
gcsClient, err := storage.NewClient(ctx, opts...)
6488
if err != nil {
6589
return nil, err
6690
}
6791

68-
return &GCSClient{
69-
bucket: gcsClient.Bucket(gc.Bucket),
70-
closer: gcsClient,
71-
}, nil
92+
googleAccessID, privateKey, err := signingIdentity(ctx, creds)
93+
if err != nil {
94+
return nil, fmt.Errorf("resolve signing identity: %w", err)
95+
}
96+
97+
c := &GCSClient{
98+
bucket: gcsClient.Bucket(gc.Bucket),
99+
closer: gcsClient,
100+
googleAccessID: googleAccessID,
101+
privateKey: privateKey,
102+
}
103+
104+
if len(privateKey) == 0 {
105+
iamService, err := iamcredentials.NewService(ctx, opts...)
106+
if err != nil {
107+
return nil, fmt.Errorf("create iamcredentials service: %w", err)
108+
}
109+
c.iamService = iamService
110+
}
111+
112+
return c, nil
113+
}
114+
115+
// signingIdentity returns the service account email and (optionally) a private
116+
// key for URL signing. Mirrors storage.BucketHandle.detectDefaultGoogleAccessID,
117+
// which is unexported — we need this surfaced because overriding SignBytes (to
118+
// thread context through to IAM) means the library no longer fills in
119+
// GoogleAccessID for our closure.
120+
func signingIdentity(ctx context.Context, creds *google.Credentials) (string, []byte, error) {
121+
if len(creds.JSON) > 0 {
122+
var sa struct {
123+
ClientEmail string `json:"client_email"`
124+
PrivateKey string `json:"private_key"`
125+
}
126+
if err := json.Unmarshal(creds.JSON, &sa); err == nil && sa.ClientEmail != "" {
127+
return sa.ClientEmail, []byte(sa.PrivateKey), nil
128+
}
129+
}
130+
131+
if !metadata.OnGCE() {
132+
return "", nil, errors.New("could not resolve service account email from credentials JSON and not running on GCE")
133+
}
134+
email, err := metadata.EmailWithContext(ctx, "default")
135+
if err != nil {
136+
return "", nil, fmt.Errorf("read service account email from GCE metadata: %w", err)
137+
}
138+
if email == "" {
139+
return "", nil, errors.New("empty service account email from GCE metadata")
140+
}
141+
return email, nil, nil
142+
}
143+
144+
// signBytesWithContext returns a SignBytes function that calls IAM SignBlob
145+
// with the given context. Using our context here means the HTTP span emitted
146+
// by the IAM client inherits the caller's trace.
147+
func (c *GCSClient) signBytesWithContext(ctx context.Context) func([]byte) ([]byte, error) {
148+
return func(in []byte) ([]byte, error) {
149+
resp, err := c.iamService.Projects.ServiceAccounts.SignBlob(
150+
fmt.Sprintf("projects/-/serviceAccounts/%s", c.googleAccessID),
151+
&iamcredentials.SignBlobRequest{
152+
Payload: base64.StdEncoding.EncodeToString(in),
153+
},
154+
).Context(ctx).Do()
155+
if err != nil {
156+
return nil, fmt.Errorf("iam sign blob: %w", err)
157+
}
158+
return base64.StdEncoding.DecodeString(resp.SignedBlob)
159+
}
160+
}
161+
162+
func (c *GCSClient) signedURLOptions(ctx context.Context, base *storage.SignedURLOptions) *storage.SignedURLOptions {
163+
base.GoogleAccessID = c.googleAccessID
164+
if len(c.privateKey) > 0 {
165+
base.PrivateKey = c.privateKey
166+
} else {
167+
base.SignBytes = c.signBytesWithContext(ctx)
168+
}
169+
return base
72170
}
73171

74172
func (c *GCSClient) Close() error {
@@ -81,22 +179,22 @@ func (c *GCSClient) SignedPUT(
81179
size int64,
82180
expiry time.Time,
83181
) (string, error) {
84-
return c.bucket.SignedURL(objectKey, &storage.SignedURLOptions{
182+
return c.bucket.SignedURL(objectKey, c.signedURLOptions(ctx, &storage.SignedURLOptions{
85183
Method: "PUT",
86184
Expires: expiry,
87185
Headers: []string{
88186
"X-Upload-Content-Length:" + strconv.FormatInt(size, 10),
89187
},
90-
})
188+
}))
91189
}
92190

93191
func (c *GCSClient) SignedGET(
94192
ctx context.Context,
95193
objectKey string,
96194
expiry time.Time,
97195
) (string, error) {
98-
return c.bucket.SignedURL(objectKey, &storage.SignedURLOptions{
196+
return c.bucket.SignedURL(objectKey, c.signedURLOptions(ctx, &storage.SignedURLOptions{
99197
Method: "GET",
100198
Expires: expiry,
101-
})
199+
}))
102200
}

0 commit comments

Comments
 (0)