Skip to content

Commit aee5e72

Browse files
authored
chore: validate dependent attestation in referrer endpoint (#745)
Signed-off-by: Miguel Martinez Trivino <[email protected]>
1 parent 1ffdc13 commit aee5e72

File tree

6 files changed

+121
-20
lines changed

6 files changed

+121
-20
lines changed

app/controlplane/internal/biz/referrer.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ type ReferrerRepo interface {
7272
// For example if sha:deadbeef represents an attestation, the result will contain the attestation + materials associated to it
7373
// OrgIDs represent an allowList of organizations where the referrers should be looked for
7474
GetFromRoot(ctx context.Context, digest string, orgIDS []uuid.UUID, filters ...GetFromRootFilter) (*StoredReferrer, error)
75+
// Check if a given referrer by digest exist.
76+
// The query can be scoped further down if needed by providing the kind or visibility status
77+
Exist(ctx context.Context, digest string, filters ...GetFromRootFilter) (bool, error)
7578
}
7679

7780
type Referrer struct {
@@ -132,7 +135,7 @@ func (s *ReferrerUseCase) ExtractAndPersist(ctx context.Context, att *dsse.Envel
132135
return NewErrNotFound("workflow")
133136
}
134137

135-
referrers, err := extractReferrers(att)
138+
referrers, err := extractReferrers(att, s.repo)
136139
if err != nil {
137140
return fmt.Errorf("extracting referrers: %w", err)
138141
}
@@ -247,7 +250,7 @@ func (r *Referrer) MapID() string {
247250
// 3 - and the subjects (some of them)
248251
// 4 - creating link between the attestation and the materials/subjects as needed
249252
// see tests for examples
250-
func extractReferrers(att *dsse.Envelope) ([]*Referrer, error) {
253+
func extractReferrers(att *dsse.Envelope, repo ReferrerRepo) ([]*Referrer, error) {
251254
_, h, err := attestation.JSONEnvelopeWithDigest(att)
252255
if err != nil {
253256
return nil, fmt.Errorf("marshaling attestation: %w", err)
@@ -290,6 +293,17 @@ func extractReferrers(att *dsse.Envelope) ([]*Referrer, error) {
290293
continue
291294
}
292295

296+
// If we are inserting an attestation as a dependent, we want to make sure it already exists
297+
// stored in the system. This is so we can ensure that the attestations nodes are created through
298+
// an attestation process, not as a referenced provided by the user
299+
if material.Type == referrerAttestationType {
300+
if exists, err := repo.Exist(context.Background(), material.Hash.String(), WithKind(referrerAttestationType)); err != nil {
301+
return nil, fmt.Errorf("checking if attestation exists: %w", err)
302+
} else if !exists {
303+
return nil, fmt.Errorf("attestation material does not exist %q", material.Hash.String())
304+
}
305+
}
306+
293307
// Create its referrer entry if it doesn't exist yet
294308
// the reason it might exist is because you might be attaching the same material twice
295309
// i.e the same SBOM twice, in that case we don't want to create a new referrer

app/controlplane/internal/biz/referrer_integration_test.go

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ import (
2424
"github.com/chainloop-dev/chainloop/app/controlplane/internal/biz"
2525
"github.com/chainloop-dev/chainloop/app/controlplane/internal/biz/testhelpers"
2626
"github.com/chainloop-dev/chainloop/app/controlplane/internal/conf"
27+
"github.com/chainloop-dev/chainloop/internal/credentials"
28+
creds "github.com/chainloop-dev/chainloop/internal/credentials/mocks"
2729
"github.com/google/uuid"
2830
"github.com/secure-systems-lab/go-securesystemslib/dsse"
31+
"github.com/stretchr/testify/mock"
2932
"github.com/stretchr/testify/require"
3033
"github.com/stretchr/testify/suite"
3134
)
@@ -108,12 +111,39 @@ func (s *referrerIntegrationTestSuite) TestGetFromRootInPublicSharedIndex() {
108111
})
109112
}
110113

114+
func (s *referrerIntegrationTestSuite) TestExtractAndPersistsDependentAttestation() {
115+
envelope := testEnvelope(s.T(), "testdata/attestations/with-dependent-attestation.json")
116+
ctx := context.Background()
117+
118+
const (
119+
wantReferrerAtt = "sha256:950c7b4c65447a3b86b6f769515005e7c44a67c8193bff790750eadf13207fbb"
120+
wantDependentAtt = "sha256:2dc17f7c933d20e06b49250a582a3d19bdfbadba9c4e5f3f856af6f261db79d4"
121+
)
122+
123+
s.Run("creation fails because the dependent attestation doesn't exist yet", func() {
124+
err := s.Referrer.ExtractAndPersist(ctx, envelope, s.workflow1.ID.String())
125+
s.ErrorContains(err, "attestation material does not exist")
126+
})
127+
128+
s.Run("if the dependent attestation exists we ingest it", func() {
129+
// We store the dependent attestation
130+
dependentAtt := testEnvelope(s.T(), "testdata/attestations/dependent-attestation.json")
131+
err := s.Referrer.ExtractAndPersist(ctx, dependentAtt, s.workflow1.ID.String())
132+
require.NoError(s.T(), err)
133+
134+
err = s.Referrer.ExtractAndPersist(ctx, envelope, s.workflow1.ID.String())
135+
s.NoError(err)
136+
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerAtt, "ATTESTATION", s.user.ID)
137+
s.NoError(err)
138+
// It has a commit and an attestation
139+
require.Len(s.T(), got.References, 2)
140+
s.Equal(wantDependentAtt, got.References[1].Digest)
141+
})
142+
}
143+
111144
func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
112145
// Load attestation
113-
attJSON, err := os.ReadFile("testdata/attestations/with-git-subject.json")
114-
require.NoError(s.T(), err)
115-
var envelope *dsse.Envelope
116-
require.NoError(s.T(), json.Unmarshal(attJSON, &envelope))
146+
envelope := testEnvelope(s.T(), "testdata/attestations/with-git-subject.json")
117147

118148
wantReferrerAtt := &biz.Referrer{
119149
Digest: "sha256:de36d470d792499b1489fc0e6623300fc8822b8f0d2981bb5ec563f8dde723c7",
@@ -218,7 +248,7 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
218248
})
219249

220250
s.T().Run("but another workflow can be attached", func(t *testing.T) {
221-
err = s.Referrer.ExtractAndPersist(ctx, envelope, s.workflow2.ID.String())
251+
err := s.Referrer.ExtractAndPersist(ctx, envelope, s.workflow2.ID.String())
222252
s.NoError(err)
223253
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerAtt.Digest, "", s.user.ID)
224254
s.NoError(err)
@@ -232,12 +262,12 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
232262
got, err = s.Referrer.GetFromRootUser(ctx, wantReferrerAtt.Digest, "", s.user.ID)
233263
s.NoError(err)
234264
require.Len(t, got.OrgIDs, 2)
235-
s.Equal([]uuid.UUID{s.org1UUID, s.org2UUID}, got.OrgIDs)
236-
s.Equal([]uuid.UUID{s.workflow1.ID, s.workflow2.ID}, got.WorkflowIDs)
265+
s.Equal([]uuid.UUID{s.org2UUID, s.org1UUID}, got.OrgIDs)
266+
s.Equal([]uuid.UUID{s.workflow2.ID, s.workflow1.ID}, got.WorkflowIDs)
237267
})
238268

239269
s.T().Run("and now user2 has access to it since it has access to workflow2 in org2", func(t *testing.T) {
240-
err = s.Referrer.ExtractAndPersist(ctx, envelope, s.workflow2.ID.String())
270+
err := s.Referrer.ExtractAndPersist(ctx, envelope, s.workflow2.ID.String())
241271
s.NoError(err)
242272
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerAtt.Digest, "", s.user2.ID)
243273
s.NoError(err)
@@ -274,19 +304,14 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
274304
})
275305

276306
s.T().Run("it should NOT fail storing the attestation with the same material twice with different types", func(t *testing.T) {
277-
attJSON, err = os.ReadFile("testdata/attestations/with-duplicated-sha.json")
278-
require.NoError(s.T(), err)
279-
require.NoError(s.T(), json.Unmarshal(attJSON, &envelope))
307+
envelope := testEnvelope(s.T(), "testdata/attestations/with-duplicated-sha.json")
280308

281309
err := s.Referrer.ExtractAndPersist(ctx, envelope, s.workflow1.ID.String())
282310
s.NoError(err)
283311
})
284312

285313
s.T().Run("it should fail on retrieval if we have stored two referrers with same digest (for two different types)", func(t *testing.T) {
286-
// this attestation contains a material with same digest than the container image from git-subject.json
287-
attJSON, err = os.ReadFile("testdata/attestations/same-digest-than-git-subject.json")
288-
require.NoError(s.T(), err)
289-
require.NoError(s.T(), json.Unmarshal(attJSON, &envelope))
314+
envelope := testEnvelope(s.T(), "testdata/attestations/same-digest-than-git-subject.json")
290315

291316
// storing will not fail since it's the a different artifact type
292317
err := s.Referrer.ExtractAndPersist(ctx, envelope, s.workflow1.ID.String())
@@ -329,7 +354,7 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
329354
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerAtt.Digest, "", s.user.ID)
330355
s.NoError(err)
331356
s.False(got.InPublicWorkflow)
332-
s.Equal([]uuid.UUID{s.workflow1.ID, s.workflow2.ID}, got.WorkflowIDs)
357+
s.Equal([]uuid.UUID{s.workflow2.ID, s.workflow1.ID}, got.WorkflowIDs)
333358
for _, r := range got.References {
334359
s.False(r.InPublicWorkflow)
335360
}
@@ -356,11 +381,15 @@ type referrerIntegrationTestSuite struct {
356381
org1UUID, org2UUID uuid.UUID
357382
user, user2 *biz.User
358383
sharedEnabledUC *biz.ReferrerUseCase
384+
run *biz.WorkflowRun
359385
}
360386

361387
func (s *referrerIntegrationTestSuite) SetupTest() {
362-
s.TestingUseCases = testhelpers.NewTestingUseCases(s.T())
388+
credsWriter := creds.NewReaderWriter(s.T())
363389
ctx := context.Background()
390+
credsWriter.On("SaveCredentials", ctx, mock.Anything, &credentials.OCIKeypair{Repo: "repo", Username: "username", Password: "pass"}).Return("stored-OCI-secret", nil)
391+
392+
s.TestingUseCases = testhelpers.NewTestingUseCases(s.T(), testhelpers.WithCredsReaderWriter(credsWriter))
364393

365394
var err error
366395
s.org1, err = s.Organization.CreateWithRandomName(ctx)
@@ -398,6 +427,24 @@ func (s *referrerIntegrationTestSuite) SetupTest() {
398427
AllowedOrgs: []string{s.org1.ID},
399428
}, nil)
400429
require.NoError(s.T(), err)
430+
431+
// Robot account
432+
robotAccount, err := s.RobotAccount.Create(ctx, "name", s.org1.ID, s.workflow1.ID.String())
433+
require.NoError(s.T(), err)
434+
435+
// Find contract revision
436+
contractVersion, err := s.WorkflowContract.Describe(ctx, s.org1.ID, s.workflow1.ContractID.String(), 0)
437+
require.NoError(s.T(), err)
438+
439+
casBackend, err := s.CASBackend.CreateOrUpdate(ctx, s.org1.ID, "repo", "username", "pass", backendType, true)
440+
require.NoError(s.T(), err)
441+
442+
s.run, err = s.WorkflowRun.Create(ctx,
443+
&biz.WorkflowRunCreateOpts{
444+
WorkflowID: s.workflow1.ID.String(), RobotaccountID: robotAccount.ID.String(), ContractRevision: contractVersion, CASBackendID: casBackend.ID,
445+
})
446+
447+
require.NoError(s.T(), err)
401448
}
402449

403450
func TestReferrerIntegration(t *testing.T) {

app/controlplane/internal/biz/referrer_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ func (s *referrerTestSuite) TestExtractReferrers() {
331331
var envelope *dsse.Envelope
332332
require.NoError(s.T(), json.Unmarshal(attJSON, &envelope))
333333

334-
got, err := extractReferrers(envelope)
334+
got, err := extractReferrers(envelope, nil)
335335
if tc.expectErr {
336336
s.Error(err)
337337
return
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"payloadType": "application/vnd.in-toto+json",
3+
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiY2hhaW5sb29wLndvcmtmbG93LnRlc3QyIiwiZGlnZXN0Ijp7InNoYTI1NiI6IjE1MTAwMzA1OWJmOTRiMTViZTMzNTBiZTc5MGM5MGEyYzE5MzBjOWU0Y2ZkYzQwOTEyYmMxY2UzM2Y3YzFmZGQifX0seyJuYW1lIjoiZ2l0LmhlYWQiLCJkaWdlc3QiOnsic2hhMSI6IjM1MTBkZDE1YzJiYTRmMWFkMDk0YjVlNTI3ZjNjYTAxNjVhNjk2MDAifSwiYW5ub3RhdGlvbnMiOnsiYXV0aG9yLmVtYWlsIjoibWlndWVsQGNoYWlubG9vcC5kZXYiLCJhdXRob3IubmFtZSI6Ik1pZ3VlbCBNYXJ0aW5leiBUcml2aW5vIiwiZGF0ZSI6IjIwMjQtMDQtMzBUMDY6Mjg6NDZaIiwibWVzc2FnZSI6ImZlYXQoY2xpKTogYWRkIGpzb24gb3V0cHV0IHRvIGF0dGVzdGF0aW9uIHB1c2hcblxuU2lnbmVkLW9mZi1ieTogTWlndWVsIE1hcnRpbmV6IFRyaXZpbm8gPG1pZ3VlbEBjaGFpbmxvb3AuZGV2PlxuIiwicmVtb3RlcyI6W3sibmFtZSI6Im9yaWdpbiIsInVybCI6ImdpdEBnaXRodWIuY29tOm1pZ21hcnRyaS9jaGFpbmxvb3AuZ2l0In0seyJuYW1lIjoidXBzdHJlYW0iLCJ1cmwiOiJnaXRAZ2l0aHViLmNvbTpjaGFpbmxvb3AtZGV2L2NoYWlubG9vcC5naXQifSx7Im5hbWUiOiJ0ZXN0LXRva2VuIiwidXJsIjoiaHR0cHM6Ly9naXRsYWItY2ktdG9rZW46Z2xjYnQtNjVfNVg2dUR6SlZSeDlyU3pnZFdES1pAZ2l0aHViLmNvbS9jaGFpbmxvb3AtZGV2L2NoYWlubG9vcC5naXQifV19fV0sInByZWRpY2F0ZVR5cGUiOiJjaGFpbmxvb3AuZGV2L2F0dGVzdGF0aW9uL3YwLjIiLCJwcmVkaWNhdGUiOnsiYnVpbGRUeXBlIjoiY2hhaW5sb29wLmRldi93b3JrZmxvd3J1bi92MC4xIiwiYnVpbGRlciI6eyJpZCI6ImNoYWlubG9vcC5kZXYvY2xpL2RldkBzaGEyNTY6MmE0ZGE4MGU5Y2IzZDJkZTU5YmVjZTAwNmEwOWZlOGNlN2RiZmJmYWQ3MDQ2MTcyMDIzYzY5N2VjMTc4MDcyMCJ9LCJtZXRhZGF0YSI6eyJmaW5pc2hlZEF0IjoiMjAyNC0wNC0zMFQwNjozMjoxOC41NjEwOTA2MzhaIiwiaW5pdGlhbGl6ZWRBdCI6IjIwMjQtMDQtMzBUMDY6MzI6MTEuNjkyOTUyOTk0WiIsIm5hbWUiOiJ0ZXN0MiIsIm9yZ2FuaXphdGlvbiI6ImZvbyIsInByb2plY3QiOiJiYXIiLCJ0ZWFtIjoiIiwid29ya2Zsb3dJRCI6IjY1MmRkMmExLTE0NjItNGFhMC05OTVmLTFlMjg2NWFkMjY3NiIsIndvcmtmbG93UnVuSUQiOiJiYzE2NzhlYy1iMGIzLTQxYWYtYmVhZC1lNzViYzBjNTRiMmMifSwicnVubmVyVHlwZSI6IlJVTk5FUl9UWVBFX1VOU1BFQ0lGSUVEIn19",
4+
"signatures": [
5+
{
6+
"keyid": "",
7+
"sig": "MEUCIDGBc8J4tpGaSyTcdHecnZOa725Tja4tozwtXNr6jSz7AiEA8xOts32aCmDs3TaUTyV8Tiv461gblt+ysfCT2OH9DFU="
8+
}
9+
]
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"payloadType": "application/vnd.in-toto+json",
3+
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCAic3ViamVjdCI6W3sibmFtZSI6ImNoYWlubG9vcC53b3JrZmxvdy50ZXN0MiIsICJkaWdlc3QiOnsic2hhMjU2IjoiNjA4N2M2NWFkMzczYzU1NzA5ZmM3OTM0NGM4OTcyZDAwNjk1ODM4ODkzYWE4NWZiMzU3MTcxZDk5NjY4MjcxOCJ9fSwgeyJuYW1lIjoiZ2l0LmhlYWQiLCAiZGlnZXN0Ijp7InNoYTEiOiI2NGNjZTZiYWQ4NGI0NzI1MjUyMjc4MjdlMzNlOTNhZmI0YzUyNzhhIn0sICJhbm5vdGF0aW9ucyI6eyJhdXRob3IuZW1haWwiOiJqYXZpZXJAY2hhaW5sb29wLmRldiIsICJhdXRob3IubmFtZSI6IkphdmllciBSb2Ryw61ndWV6IiwgImRhdGUiOiIyMDI0LTA0LTMwVDE0OjM0OjU5WiIsICJtZXNzYWdlIjoiZml4KGNpKTogT25seSBydW4gY29udHJhY3Qgc3luYyBvbiBjaGFuZ2VzIGluIGNvbnRyYWN0cyBwYXRoICgjNzM4KVxuXG5TaWduZWQtb2ZmLWJ5OiBKYXZpZXIgUm9kcmlndWV6IDxqYXZpZXJAY2hhaW5sb29wLmRldj4iLCAicmVtb3RlcyI6W3sibmFtZSI6InVwc3RyZWFtIiwgInVybCI6ImdpdEBnaXRodWIuY29tOmNoYWlubG9vcC1kZXYvY2hhaW5sb29wLmdpdCJ9LCB7Im5hbWUiOiJ0ZXN0LXRva2VuIiwgInVybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9jaGFpbmxvb3AtZGV2L2NoYWlubG9vcC5naXQifSwgeyJuYW1lIjoib3JpZ2luIiwgInVybCI6ImdpdEBnaXRodWIuY29tOm1pZ21hcnRyaS9jaGFpbmxvb3AuZ2l0In1dfX1dLCAicHJlZGljYXRlVHlwZSI6ImNoYWlubG9vcC5kZXYvYXR0ZXN0YXRpb24vdjAuMiIsICJwcmVkaWNhdGUiOnsiYnVpbGRUeXBlIjoiY2hhaW5sb29wLmRldi93b3JrZmxvd3J1bi92MC4xIiwgImJ1aWxkZXIiOnsiaWQiOiJjaGFpbmxvb3AuZGV2L2NsaS9kZXZAc2hhMjU2OmYxY2NhMmEyYTAyYWVjZmZkYzljZTdiYzBmNTY4MDQ3M2Y2ZjNmNTQxZDE2ZTk1MGRkMmNiMjYxZDU0MGM3NDAifSwgIm1hdGVyaWFscyI6W3siYW5ub3RhdGlvbnMiOnsiY2hhaW5sb29wLm1hdGVyaWFsLmNhcyI6dHJ1ZSwgImNoYWlubG9vcC5tYXRlcmlhbC5uYW1lIjoiZGVwbG95bWVudCIsICJjaGFpbmxvb3AubWF0ZXJpYWwudHlwZSI6IkFUVEVTVEFUSU9OIn0sICJkaWdlc3QiOnsic2hhMjU2IjoiMmRjMTdmN2M5MzNkMjBlMDZiNDkyNTBhNTgyYTNkMTliZGZiYWRiYTljNGU1ZjNmODU2YWY2ZjI2MWRiNzlkNCJ9LCAibmFtZSI6InRlc3QyLWJjMTY3OGVjLWIwYjMtNDFhZi1iZWFkLWU3NWJjMGM1NGIyYy1hdHRlc3RhdGlvbi5qc29uIn1dLCAibWV0YWRhdGEiOnsiZmluaXNoZWRBdCI6IjIwMjQtMDUtMDNUMDk6NDQ6MDguMjY2ODM3MjYyWiIsICJpbml0aWFsaXplZEF0IjoiMjAyNC0wNS0wM1QwOTo0Mzo1Ny40MzI2MjQ1MTRaIiwgIm5hbWUiOiJ0ZXN0MiIsICJvcmdhbml6YXRpb24iOiJmb28iLCAicHJvamVjdCI6ImJhciIsICJ0ZWFtIjoiIiwgIndvcmtmbG93SUQiOiI2NTJkZDJhMS0xNDYyLTRhYTAtOTk1Zi0xZTI4NjVhZDI2NzYiLCAid29ya2Zsb3dSdW5JRCI6ImM5NmMzZWZiLWMyOTgtNGQxOC04OGM1LTc4YWRkZWFjNGE3NSJ9LCAicnVubmVyVHlwZSI6IlJVTk5FUl9UWVBFX1VOU1BFQ0lGSUVEIn19",
4+
"signatures": [
5+
{
6+
"keyid": "",
7+
"sig": "MEQCIAVsfX0IwxTll5kYtFg8dsVZWMUzFusHNgUpNfD8RMiqAiACJCCjo1hz/z3TUaw9IYsWaTkzk6YFtJClOyp0bFDanw=="
8+
}
9+
]
10+
}

app/controlplane/internal/data/referrer.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,26 @@ func (r *ReferrerRepo) Save(ctx context.Context, referrers []*biz.Referrer, work
116116
return nil
117117
}
118118

119+
// Check if a given referrer by digest exist. The query can be scoped further down if needed by providing the kind or visibility status
120+
func (r *ReferrerRepo) Exist(ctx context.Context, digest string, filters ...biz.GetFromRootFilter) (bool, error) {
121+
opts := &biz.GetFromRootFilters{}
122+
for _, f := range filters {
123+
f(opts)
124+
}
125+
126+
query := r.data.db.Referrer.Query().Where(referrer.DigestEQ(digest))
127+
// We might be filtering by the rootKind, this will prevent ambiguity
128+
if opts.RootKind != nil {
129+
query = query.Where(referrer.Kind(*opts.RootKind))
130+
}
131+
132+
if opts.Public != nil {
133+
query = query.WithWorkflows(func(q *ent.WorkflowQuery) { q.Where(workflow.PublicEQ(*opts.Public)) })
134+
}
135+
136+
return query.Exist(ctx)
137+
}
138+
119139
func (r *ReferrerRepo) GetFromRoot(ctx context.Context, digest string, orgIDs []uuid.UUID, filters ...biz.GetFromRootFilter) (*biz.StoredReferrer, error) {
120140
opts := &biz.GetFromRootFilters{}
121141
for _, f := range filters {

0 commit comments

Comments
 (0)