-
Notifications
You must be signed in to change notification settings - Fork 24
/
casmapping.go
239 lines (198 loc) · 7.38 KB
/
casmapping.go
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
//
// Copyright 2023 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 biz
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/chainloop-dev/chainloop/internal/attestation/renderer/chainloop"
"github.com/chainloop-dev/chainloop/internal/servicelogger"
"github.com/go-kratos/kratos/v2/log"
cr_v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/uuid"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
)
type CASMapping struct {
ID, OrgID, WorkflowRunID uuid.UUID
CASBackend *CASBackend
Digest string
CreatedAt *time.Time
// A public mapping means that the material/attestation can be downloaded by anyone
Public bool
}
type CASMappingRepo interface {
Create(ctx context.Context, digest string, casBackendID, workflowRunID uuid.UUID) (*CASMapping, error)
// List all the CAS mappings for the given digest
FindByDigest(ctx context.Context, digest string) ([]*CASMapping, error)
}
type CASMappingUseCase struct {
repo CASMappingRepo
membershipRepo MembershipRepo
logger *log.Helper
}
func NewCASMappingUseCase(repo CASMappingRepo, mRepo MembershipRepo, logger log.Logger) *CASMappingUseCase {
return &CASMappingUseCase{repo, mRepo, servicelogger.ScopedHelper(logger, "cas-mapping-usecase")}
}
func (uc *CASMappingUseCase) Create(ctx context.Context, digest string, casBackendID, workflowRunID string) (*CASMapping, error) {
casBackendUUID, err := uuid.Parse(casBackendID)
if err != nil {
return nil, NewErrInvalidUUID(err)
}
workflowRunUUID, err := uuid.Parse(workflowRunID)
if err != nil {
return nil, NewErrInvalidUUID(err)
}
// parse the digest to make sure is a valid sha256 sum
if _, err = cr_v1.NewHash(digest); err != nil {
return nil, NewErrValidation(fmt.Errorf("invalid digest format: %w", err))
}
return uc.repo.Create(ctx, digest, casBackendUUID, workflowRunUUID)
}
func (uc *CASMappingUseCase) FindByDigest(ctx context.Context, digest string) ([]*CASMapping, error) {
return uc.repo.FindByDigest(ctx, digest)
}
// FindCASMappingForDownloadByUser returns the CASMapping appropriate for the given digest and user
// This means, in order
// 1 - Any mapping that points to an organization which the user is member of
// 1.1 If there are multiple mappings, it will pick the default one or the first one
// 2 - Any mapping that is public
func (uc *CASMappingUseCase) FindCASMappingForDownloadByUser(ctx context.Context, digest string, userID string) (*CASMapping, error) {
uc.logger.Infow("msg", "finding cas mapping for download", "digest", digest, "user", userID)
userUUID, err := uuid.Parse(userID)
if err != nil {
return nil, NewErrInvalidUUID(err)
}
// Load organizations for the given user
memberships, err := uc.membershipRepo.FindByUser(ctx, userUUID)
if err != nil {
return nil, fmt.Errorf("failed to list memberships: %w", err)
}
userOrgs := make([]string, 0, len(memberships))
for _, m := range memberships {
userOrgs = append(userOrgs, m.OrganizationID.String())
}
return uc.FindCASMappingForDownloadByOrg(ctx, digest, userOrgs)
}
func (uc *CASMappingUseCase) FindCASMappingForDownloadByOrg(ctx context.Context, digest string, orgs []string) (*CASMapping, error) {
if _, err := cr_v1.NewHash(digest); err != nil {
return nil, NewErrValidation(fmt.Errorf("invalid digest format: %w", err))
}
if len(orgs) == 0 {
return nil, NewErrValidationStr("no organizations provided")
}
// 1 - All CAS mappings for the given digest
mappings, err := uc.repo.FindByDigest(ctx, digest)
if err != nil {
return nil, fmt.Errorf("failed to list cas mappings: %w", err)
}
uc.logger.Debugw("msg", fmt.Sprintf("found %d entries globally", len(mappings)), "digest", digest, "orgs", orgs)
if len(mappings) == 0 {
return nil, NewErrNotFound("digest not found in any mapping")
}
// 2 - CAS mappings associated with the given list of orgs
orgMappings, err := filterByOrgs(mappings, orgs)
if err != nil {
return nil, fmt.Errorf("failed to load mappings associated to an user: %w", err)
} else if len(orgMappings) > 0 {
result := defaultOrFirst(orgMappings)
uc.logger.Infow("msg", "mapping found!", "digest", digest, "orgs", orgs, "casBackend", result.CASBackend.ID, "default", result.CASBackend.Default, "public", result.Public)
return result, nil
}
// 3 - mappings that are public
publicMappings := filterByPublic(mappings)
// The user has not access to neither proprietary nor public mappings
if len(publicMappings) == 0 {
uc.logger.Warnw("msg", "digest exist but user does not have access to it", "digest", digest, "orgs", orgs)
return nil, NewErrUnauthorized(errors.New("unauthorized access to the artifact"))
}
// Pick the appropriate mapping from multiple ones
result := defaultOrFirst(publicMappings)
uc.logger.Infow("msg", "mapping found!", "digest", digest, "orgs", orgs, "casBackend", result.CASBackend.ID, "default", result.CASBackend.Default, "public", result.Public)
return result, nil
}
// Extract only the mappings associated with a list of orgs
func filterByOrgs(mappings []*CASMapping, orgs []string) ([]*CASMapping, error) {
result := make([]*CASMapping, 0)
for _, mapping := range mappings {
for _, o := range orgs {
if mapping.OrgID.String() == o {
result = append(result, mapping)
}
}
}
return result, nil
}
func filterByPublic(mappings []*CASMapping) []*CASMapping {
result := make([]*CASMapping, 0)
for _, mapping := range mappings {
if mapping.Public {
result = append(result, mapping)
}
}
return result
}
func defaultOrFirst(mappings []*CASMapping) *CASMapping {
if len(mappings) == 0 {
return nil
}
result := mappings[0]
for _, mapping := range mappings {
if mapping.CASBackend.Default {
result = mapping
break
}
}
return result
}
type CASMappingLookupRef struct {
Name, Digest string
}
// LookupCASItemsInAttestation returns a list of references to the materials that have been uploaded to CAS
// as well as the attestation digest itself
func (uc *CASMappingUseCase) LookupDigestsInAttestation(att *dsse.Envelope) ([]*CASMappingLookupRef, error) {
// Calculate the attestation hash
jsonAtt, err := json.Marshal(att)
if err != nil {
return nil, fmt.Errorf("marshaling attestation: %w", err)
}
// Extract the materials that have been uploaded too
predicate, err := chainloop.ExtractPredicate(att)
if err != nil {
return nil, fmt.Errorf("extracting predicate: %w", err)
}
// Calculate the attestation hash
h, _, err := cr_v1.SHA256(bytes.NewBuffer(jsonAtt))
if err != nil {
return nil, fmt.Errorf("calculating attestation hash: %w", err)
}
references := []*CASMappingLookupRef{
{
Name: "attestation",
Digest: h.String(),
},
}
for _, material := range predicate.GetMaterials() {
if material.UploadedToCAS {
references = append(references, &CASMappingLookupRef{
Name: material.Name,
Digest: material.Hash.String(),
})
}
}
return references, nil
}