Repository navigation
Expand file tree
/
Copy pathloader.go
More file actions
395 lines (329 loc) · 10.6 KB
/
Copy pathloader.go
File metadata and controls
395 lines (329 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
//
// Copyright 2024-2026 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package policies
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal"
"github.com/chainloop-dev/chainloop/pkg/cache"
crv1 "github.com/google/go-containerregistry/pkg/v1"
"golang.org/x/sync/singleflight"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
const (
fileScheme = "file"
httpScheme = "http"
httpsScheme = "https"
chainloopScheme = "chainloop"
)
// Loader defines the interface for policy loaders from contract attachments
type Loader interface {
Load(context.Context, *v1.PolicyAttachment) (*v1.Policy, *PolicyDescriptor, error)
}
// PolicyDescriptor Represents a policy reference. Used as FQ references.
type PolicyDescriptor struct {
// FQ URI of the policy
URI string
// Policy name (only when it can be resolved by the loader)
Name string
// policy digest
Digest string
// Org name for custom policies (only supported by the remote ChainloopLoader)
OrgName string
}
func (p *PolicyDescriptor) GetURI() string {
if p != nil {
return p.URI
}
return ""
}
func (p *PolicyDescriptor) GetName() string {
if p != nil {
return p.Name
}
return ""
}
func (p *PolicyDescriptor) GetDigest() string {
if p != nil {
return p.Digest
}
return ""
}
func (p *PolicyDescriptor) GetOrgName() string {
if p != nil {
return p.OrgName
}
return ""
}
// EmbeddedLoader returns embedded policies
type EmbeddedLoader struct{}
func (e *EmbeddedLoader) Load(_ context.Context, attachment *v1.PolicyAttachment) (*v1.Policy, *PolicyDescriptor, error) {
return attachment.GetEmbedded(), nil, nil
}
// FileLoader loader loads policies from filesystem and HTTPS references using Cosign's blob package
type FileLoader struct{}
func (l *FileLoader) Load(_ context.Context, attachment *v1.PolicyAttachment) (*v1.Policy, *PolicyDescriptor, error) {
var (
raw []byte
err error
)
// First remove the digest if present
ref, wantDigest := ExtractDigest(attachment.GetRef())
filePath, err := ensureScheme(ref, fileScheme)
if err != nil {
return nil, nil, err
}
raw, err = os.ReadFile(filepath.Clean(filePath))
if err != nil {
return nil, nil, fmt.Errorf("loading policy spec: %w", err)
}
var policy v1.Policy
d, err := unmarshallResource(raw, ref, wantDigest, &policy)
if err != nil {
return nil, nil, fmt.Errorf("unmarshalling policy spec: %w", err)
}
return &policy, d, nil
}
// HTTPSLoader loader loads policies from HTTP or HTTPS references
type HTTPSLoader struct {
// Client sends the request. Defaults to http.DefaultClient.
Client *http.Client
}
func (l *HTTPSLoader) Load(_ context.Context, attachment *v1.PolicyAttachment) (*v1.Policy, *PolicyDescriptor, error) {
ref, wantDigest := ExtractDigest(attachment.GetRef())
// and do not remove the scheme since we need http(s):// to make the request
if _, err := ensureScheme(ref, httpScheme, httpsScheme); err != nil {
return nil, nil, fmt.Errorf("invalid policy reference %q: %w", ref, err)
}
raw, err := fetchRemote(l.Client, ref)
if err != nil {
return nil, nil, err
}
var policy v1.Policy
d, err := unmarshallResource(raw, ref, wantDigest, &policy)
if err != nil {
return nil, nil, fmt.Errorf("unmarshalling policy spec: %w", err)
}
return &policy, d, nil
}
// fetchRemote reads the body of a GET request to ref, sent with client, or
// http.DefaultClient when it is nil. Responses other than 2xx are refused.
// Errors leave out ref, which may carry credentials in its query string.
func fetchRemote(client *http.Client, ref string) ([]byte, error) {
if client == nil {
client = http.DefaultClient
}
// #nosec G107
resp, err := client.Get(ref)
if err != nil {
// url.Error prefixes the full URL to the underlying error
var urlErr *url.Error
if errors.As(err, &urlErr) {
err = urlErr.Err
}
return nil, fmt.Errorf("requesting remote policy: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("requesting remote policy: server returned HTTP %d", resp.StatusCode)
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading remote policy: %w", err)
}
return raw, nil
}
// ChainloopLoader loads policies referenced with chainloop://provider/name URLs
type ChainloopLoader struct {
Client pb.AttestationServiceClient
cache cache.Cache[*policyWithReference]
flight singleflight.Group
}
type policyWithReference struct {
Policy *v1.Policy `json:"policy"`
Reference *PolicyDescriptor `json:"reference"`
}
// remoteLoaderFetchTimeout bounds gRPC calls inside singleflight to prevent
// indefinite blocking. Independent of any caller's context deadline so that
// all singleflight waiters get uniform timeout behavior.
const remoteLoaderFetchTimeout = 30 * time.Second
func NewChainloopLoader(client pb.AttestationServiceClient, c cache.Cache[*policyWithReference]) *ChainloopLoader {
return &ChainloopLoader{Client: client, cache: c}
}
func (c *ChainloopLoader) Load(ctx context.Context, attachment *v1.PolicyAttachment) (*v1.Policy, *PolicyDescriptor, error) {
ref := attachment.GetRef()
// Fast path: check cache
if cached, ok, _ := c.cache.Get(ctx, ref); ok {
return cached.Policy, cached.Reference, nil
}
// Use singleflight to coalesce concurrent fetches for the same ref.
// All operations inside use a fixed-timeout context independent of any
// caller's deadline so that all singleflight waiters get uniform behavior.
result, err, _ := c.flight.Do(ref, func() (interface{}, error) {
sfCtx, cancel := context.WithTimeout(context.Background(), remoteLoaderFetchTimeout)
defer cancel()
// Double-check cache inside singleflight
if cached, ok, _ := c.cache.Get(sfCtx, ref); ok {
return cached, nil
}
if !IsProviderScheme(ref) {
return nil, fmt.Errorf("invalid policy reference %q", ref)
}
providerRef := ProviderParts(ref)
resp, err := c.Client.GetPolicy(sfCtx, &pb.AttestationServiceGetPolicyRequest{
Provider: providerRef.Provider,
PolicyName: providerRef.Name,
OrgName: providerRef.OrgName,
})
if err != nil {
return nil, fmt.Errorf("requesting remote policy (provider: %s, name: %s): %w", providerRef.Provider, providerRef.Name, err)
}
h, err := crv1.NewHash(resp.Reference.GetDigest())
if err != nil {
return nil, fmt.Errorf("parsing digest: %w", err)
}
orgName := providerRef.OrgName
if u, err := url.Parse(resp.Reference.GetUrl()); err == nil {
if orgParam := u.Query().Get("org"); orgParam != "" {
orgName = orgParam
}
}
reference := policyReferenceResourceDescriptor(providerRef.Name, resp.Reference.GetUrl(), orgName, h)
cached := &policyWithReference{Policy: resp.GetPolicy(), Reference: reference}
_ = c.cache.Set(sfCtx, ref, cached)
return cached, nil
})
if err != nil {
return nil, nil, err
}
cached := result.(*policyWithReference)
return cached.Policy, cached.Reference, nil
}
func unmarshallResource(raw []byte, ref string, digest string, dest proto.Message) (*PolicyDescriptor, error) {
jsonContent, err := unmarshal.LoadJSONBytes(raw, filepath.Ext(ref))
if err != nil {
return nil, fmt.Errorf("loading resource spec: %w", err)
}
if err := protojson.Unmarshal(jsonContent, dest); err != nil {
return nil, fmt.Errorf("unmarshalling policy spec: %w", err)
}
// calculate hash of the raw data
h, _, err := crv1.SHA256(bytes.NewBuffer(raw))
if err != nil {
return nil, fmt.Errorf("calculating hash: %w", err)
}
// compare it with the wanted digest if needed
if digest != "" && h.String() != digest {
return nil, fmt.Errorf("digest mismatch: got %s, want %s", h.String(), digest)
}
return policyReferenceResourceDescriptor("", ref, "", h), nil
}
// IsProviderScheme takes a policy reference and returns whether it's referencing to an external provider or not
func IsProviderScheme(ref string) bool {
scheme, _ := RefParts(ref)
return scheme == chainloopScheme || scheme == ""
}
// ProviderRef represents a policy provider reference
type ProviderRef struct {
Provider, OrgName, Name string
}
// ProviderParts returns the provider information for a given reference
func ProviderParts(reference string) *ProviderRef {
var ref, digest string
// first of all, remove the @sha256 suffix to make the parsing easier
withDigest := strings.SplitN(reference, "@sha256:", 2)
if len(withDigest) > 1 {
// it has digest
ref = withDigest[0]
digest = withDigest[1]
} else {
ref = reference
}
parts := strings.SplitN(ref, "://", 2)
var pn []string
if len(parts) == 1 {
pn = strings.SplitN(parts[0], ":", 2)
} else {
// ref might contain the chainloop://protocol
pn = strings.SplitN(parts[1], ":", 2)
}
var (
provider string
orgName string
name = pn[0]
)
if len(pn) == 2 {
provider = pn[0]
name = pn[1]
}
scoped := strings.SplitN(name, "/", 2)
if len(scoped) == 2 {
// the policy is scoped to a specific org
orgName = scoped[0]
name = scoped[1]
}
// return the digest back
if digest != "" {
name = fmt.Sprintf("%s@sha256:%s", name, digest)
}
return &ProviderRef{
Provider: provider,
Name: name,
OrgName: orgName,
}
}
func ensureScheme(ref string, expected ...string) (string, error) {
scheme, id := RefParts(ref)
for _, ex := range expected {
if scheme == ex {
return id, nil
}
}
return "", fmt.Errorf("unexpected policy reference scheme: %q", scheme)
}
func RefParts(ref string) (string, string) {
parts := strings.SplitN(ref, "://", 2)
if len(parts) == 2 {
return parts[0], parts[1]
}
return "", parts[0]
}
func policyReferenceResourceDescriptor(policyName, ref, orgName string, digest crv1.Hash) *PolicyDescriptor {
return &PolicyDescriptor{
URI: ref,
Name: policyName,
Digest: digest.String(),
OrgName: orgName,
}
}
func ExtractDigest(ref string) (string, string) {
parts := strings.SplitN(ref, "@", 2)
if len(parts) == 2 {
return parts[0], parts[1]
}
return parts[0], ""
}