Skip to content

Commit 8a74d44

Browse files
authored
perf(controlplane): make referrer discovery independent of the caller's organization count (#3445)
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>
1 parent 56124cb commit 8a74d44

5 files changed

Lines changed: 640 additions & 33 deletions

File tree

‎app/controlplane/pkg/biz/referrer_integration_test.go‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,3 +653,37 @@ func (s *referrerIntegrationTestSuite) SetupTest() {
653653
func TestReferrerIntegration(t *testing.T) {
654654
suite.Run(t, new(referrerIntegrationTestSuite))
655655
}
656+
657+
// Project visibility is not an optional filter: a member restricted to a subset of an
658+
// organization's projects must not reach a referrer outside them, and a grant naming one
659+
// organization must not reach a project living in another.
660+
func (s *referrerIntegrationTestSuite) TestGetFromRootProjectVisibility() {
661+
ctx := context.Background()
662+
envelope, envBytes := testEnvelope(s.T(), "testdata/attestations/with-git-subject.json")
663+
attDigest, _, err := v1.SHA256(bytes.NewReader(envBytes))
664+
require.NoError(s.T(), err)
665+
require.NoError(s.T(), s.Referrer.ExtractAndPersist(ctx, envelope, attDigest, s.workflow1.ID.String()))
666+
digest := attDigest.String()
667+
668+
s.Run("a caller granted the project reaches it", func() {
669+
granted := map[biz.OrgID][]biz.ProjectID{s.org1UUID: {s.workflow1.ProjectID}}
670+
got, _, err := s.Referrer.GetFromRoot(ctx, digest, "", []uuid.UUID{s.org1UUID}, granted, nil)
671+
s.NoError(err)
672+
s.Equal(digest, got.Digest)
673+
})
674+
675+
s.Run("a caller restricted to no project in the organization does not", func() {
676+
noProjects := map[biz.OrgID][]biz.ProjectID{s.org1UUID: {}}
677+
_, _, err := s.Referrer.GetFromRoot(ctx, digest, "", []uuid.UUID{s.org1UUID}, noProjects, nil)
678+
s.Error(err)
679+
})
680+
681+
s.Run("a grant recorded against another organization does not", func() {
682+
crossOrg := map[biz.OrgID][]biz.ProjectID{
683+
s.org1UUID: {}, // restricted here, nothing granted
684+
s.org2UUID: {s.workflow1.ProjectID}, // granted here, but the project lives in org1
685+
}
686+
_, _, err := s.Referrer.GetFromRoot(ctx, digest, "", []uuid.UUID{s.org1UUID, s.org2UUID}, crossOrg, nil)
687+
s.Error(err)
688+
})
689+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// Copyright 2026 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package data
17+
18+
// Internals exposed to the package's external tests. This file is only compiled during tests, so
19+
// it does not widen the package's API. The visibility filter is reached from data_test because
20+
// asserting on it against a real database needs the shared suite in biz/testhelpers, which
21+
// imports this package and therefore cannot be imported from inside it.
22+
var ReferrerVisibleToOrgsForTest = referrerVisibleToOrgs

‎app/controlplane/pkg/data/referrer.go‎

Lines changed: 117 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
"entgo.io/ent/dialect/sql"
2424
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
2525
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent"
26-
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/organization"
2726
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/predicate"
2827
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/project"
2928
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/projectversion"
@@ -97,10 +96,10 @@ func (r *ReferrerRepo) Save(ctx context.Context, referrers []*biz.Referrer, work
9796
for _, parentRef := range referrers {
9897
// This is the current item stored in DB
9998
storedReferrer := storedMap[parentRef.MapID()]
100-
// Iterate on the items it refer to (references)
99+
// Iterate on the items it refers to (references)
101100
var references []uuid.UUID
102101
for _, ref := range parentRef.References {
103-
// amd find it in the DB
102+
// and find it in the DB
104103
storedReference, ok := storedMap[ref.MapID()]
105104
if !ok {
106105
return fmt.Errorf("referrer %v not found", ref)
@@ -162,13 +161,8 @@ func (r *ReferrerRepo) GetFromRoot(ctx context.Context, digest string, orgIDs []
162161
predicateReferrer = append(predicateReferrer, referrer.Kind(*opts.RootKind))
163162
}
164163

165-
// Prepare the workflow query predicate
166-
predicateWF := []predicate.Workflow{
167-
workflow.DeletedAtIsNil(), workflow.HasOrganizationWith(organization.IDIn(orgIDs...)),
168-
}
169-
170-
// Attach the workflow predicate
171-
predicateReferrer = append(predicateReferrer, referrer.HasWorkflowsWith(predicateWF...))
164+
// Attach the visibility predicate
165+
predicateReferrer = append(predicateReferrer, referrerVisibleToOrgs(orgIDs))
172166

173167
// If a project filter is requested, attach it as a subquery predicate. An attestation root
174168
// matches only when its digest is one of the attestation_digests produced by a workflow run
@@ -216,10 +210,8 @@ func (r *ReferrerRepo) GetFromRoot(ctx context.Context, digest string, orgIDs []
216210

217211
// projectScopePredicate returns a predicate matching referrers whose digest is the attestation
218212
// digest of a workflow run in the requested project (and, when non-empty, version), visible to
219-
// the caller. The predicate compiles to a SQL subquery — no digest list is materialized in Go,
220-
// so the cost is independent of how many runs the project has. Postgres plans this as a
221-
// semi-join via the index on workflow_run.attestation_digest, which is what makes the filter
222-
// scale at thousands of runs per project.
213+
// the caller. The predicate compiles to a SQL subquery rather than materializing a digest list in
214+
// Go, so the request carries no per-run data however many runs the project has.
223215
//
224216
// Visibility mirrors isReferrerVisible: a run is included when its workflow's project is in the
225217
// caller's RBAC-visible set. visibleProjectsMap follows the existing convention — an org entry
@@ -262,30 +254,114 @@ func (r *ReferrerRepo) projectScopePredicate(projectName, version string, orgIDs
262254
}
263255
}
264256

257+
// referrerVisibleToOrgs matches a referrer that is attached to at least one live workflow in the
258+
// given organizations.
259+
//
260+
// It is written by hand rather than with referrer.HasWorkflowsWith because the generated form
261+
// renders as `id IN (subquery)`, which describes a set to build rather than a condition to test.
262+
// Correlating the subquery to the referrer row says what is meant: for this referrer, does such a
263+
// workflow exist. The organization is matched on the workflow's own column rather than through a
264+
// nested relation predicate, which Ent renders back into the uncorrelated shape.
265+
func referrerVisibleToOrgs(orgIDs []uuid.UUID) predicate.Referrer {
266+
orgs := make([]any, 0, len(orgIDs))
267+
for _, id := range orgIDs {
268+
orgs = append(orgs, id)
269+
}
270+
271+
return func(s *sql.Selector) {
272+
joinTable := sql.Table(referrer.WorkflowsTable).As("visible_rw")
273+
workflows := sql.Table(referrer.WorkflowsInverseTable).As("visible_wf")
274+
sub := sql.Dialect(s.Dialect()).
275+
Select(joinTable.C(referrer.WorkflowsPrimaryKey[0])).
276+
From(joinTable).
277+
Join(workflows).
278+
On(joinTable.C(referrer.WorkflowsPrimaryKey[1]), workflows.C(workflow.FieldID)).
279+
Where(sql.And(
280+
sql.ColumnsEQ(joinTable.C(referrer.WorkflowsPrimaryKey[0]), s.C(referrer.FieldID)),
281+
sql.IsNull(workflows.C(workflow.FieldDeletedAt)),
282+
sql.In(workflows.C(workflow.FieldOrganizationID), orgs...),
283+
))
284+
s.Where(sql.Exists(sub))
285+
}
286+
}
287+
265288
// projectVisibilityPredicate builds a project predicate that accepts a project iff it belongs to
266289
// one of the allowed orgs AND, when RBAC applies to that org, the project is in the caller's
267290
// visible set. Returns nil when no org grants any project visibility, so callers can treat that
268291
// as "nothing is visible".
292+
//
293+
// A caller reaches a project one of two ways, so the predicate has at most two branches:
294+
// every project of an org whose role carries no project restriction, or an individually
295+
// granted project in an org whose role does. Each branch is emitted once for the whole set
296+
// of orgs rather than once per org, which keeps the filter a constant size: a disjunction
297+
// that grows with the caller's org count stops the planner from using the root's index and
298+
// turns the surrounding referrer query into a full scan.
299+
//
300+
// The second branch matches (org, project) pairs rather than the two sets independently.
301+
// Intersecting the sets would also admit a project that belongs to one of the caller's
302+
// restricted orgs and happens to be granted in another, which is not a grant the caller
303+
// holds. Pairs cannot express that, and they keep the branch a single condition.
269304
func projectVisibilityPredicate(orgIDs []uuid.UUID, visibleProjectsMap map[uuid.UUID][]uuid.UUID) predicate.Project {
270-
perOrg := make([]predicate.Project, 0, len(orgIDs))
305+
unrestrictedOrgs := make([]uuid.UUID, 0, len(orgIDs))
306+
grants := make([]projectGrant, 0, len(orgIDs))
271307
for _, orgID := range orgIDs {
272-
visible, hasRBAC := visibleProjectsMap[orgID]
273-
if !hasRBAC {
274-
perOrg = append(perOrg, project.HasOrganizationWith(organization.ID(orgID)))
308+
visible, restricted := visibleProjectsMap[orgID]
309+
if !restricted {
310+
unrestrictedOrgs = append(unrestrictedOrgs, orgID)
275311
continue
276312
}
277-
if len(visible) == 0 {
278-
continue // RBAC applies but no project is visible in this org
313+
// Restricted: nothing in this org is visible beyond the projects granted in it,
314+
// so an org with no grants contributes nothing.
315+
for _, projectID := range visible {
316+
grants = append(grants, projectGrant{orgID: orgID, projectID: projectID})
279317
}
280-
perOrg = append(perOrg, project.And(
281-
project.HasOrganizationWith(organization.ID(orgID)),
282-
project.IDIn(visible...),
283-
))
284318
}
285-
if len(perOrg) == 0 {
319+
320+
predicates := make([]predicate.Project, 0, 2)
321+
if len(unrestrictedOrgs) > 0 {
322+
// The org is a column on the project row, so this needs no subquery.
323+
predicates = append(predicates, project.OrganizationIDIn(unrestrictedOrgs...))
324+
}
325+
if len(grants) > 0 {
326+
predicates = append(predicates, projectGrantsPredicate(grants))
327+
}
328+
329+
switch len(predicates) {
330+
case 0:
286331
return nil
332+
case 1:
333+
return predicates[0]
334+
default:
335+
return project.Or(predicates...)
336+
}
337+
}
338+
339+
// projectGrant is a project the caller may see and the org the grant was recorded under.
340+
type projectGrant struct {
341+
orgID, projectID uuid.UUID
342+
}
343+
344+
// projectGrantsPredicate matches a project iff it is one of the granted projects AND sits in the
345+
// org that grant was recorded under, as a single (organization_id, id) IN ((..), (..)) condition.
346+
func projectGrantsPredicate(grants []projectGrant) predicate.Project {
347+
return func(s *sql.Selector) {
348+
s.Where(sql.P().Append(func(b *sql.Builder) {
349+
b.Wrap(func(nb *sql.Builder) {
350+
nb.IdentComma(s.C(project.FieldOrganizationID), s.C(project.FieldID))
351+
})
352+
b.WriteString(" IN ")
353+
b.Wrap(func(nb *sql.Builder) {
354+
for i, g := range grants {
355+
if i > 0 {
356+
nb.Comma()
357+
}
358+
nb.Wrap(func(vb *sql.Builder) {
359+
vb.Args(g.orgID, g.projectID)
360+
})
361+
}
362+
})
363+
}))
287364
}
288-
return project.Or(perOrg...)
289365
}
290366

291367
// max number of recursive levels to traverse
@@ -331,12 +407,8 @@ func (r *ReferrerRepo) doGet(ctx context.Context, root *ent.Referrer, allowedOrg
331407
// and by the visibility if needed
332408
predicateReferrer := []predicate.Referrer{}
333409

334-
predicateWF := []predicate.Workflow{
335-
workflow.DeletedAtIsNil(), workflow.HasOrganizationWith(organization.IDIn(allowedOrgs...)),
336-
}
337-
338-
// Attach the workflow predicate
339-
predicateReferrer = append(predicateReferrer, referrer.HasWorkflowsWith(predicateWF...))
410+
// Attach the visibility predicate
411+
predicateReferrer = append(predicateReferrer, referrerVisibleToOrgs(allowedOrgs))
340412

341413
// When scoping to a project, attestation references must belong to that project (optionally
342414
// narrowed to a version). Non-attestation references (materials/subjects) are kept as-is:
@@ -358,6 +430,18 @@ func (r *ReferrerRepo) doGet(ctx context.Context, root *ent.Referrer, allowedOrg
358430

359431
// Sort references by creation date and ID in descending order for deterministic pagination
360432
q := root.QueryReferences().Where(predicateReferrer...).WithWorkflows().
433+
// Ent adds a DISTINCT to any traversal by default, because an edge query can multiply
434+
// rows. This one cannot: the join table is keyed on (referrer_id, referred_by_id), so a
435+
// fixed root matches each referrer at most once, and the selected columns include the
436+
// primary key, so there is nothing to collapse. Every predicate above is a semi-join
437+
// (EXISTS, IN) or a scalar comparison.
438+
//
439+
// Any predicate added here must stay join-free. A join would return a reference twice,
440+
// which consumes a slot in the page and leaves the cursor on the wrong row, so references
441+
// are skipped rather than repeated. sql.OrPredicates applies each predicate to this same
442+
// selector, so a joining predicate inside an Or leaks its join here while its WHERE is
443+
// OR-ed away.
444+
Unique(false).
361445
Order(referrer.ByCreatedAt(sql.OrderDesc())).
362446
Order(referrer.ByID(sql.OrderDesc())).
363447
Limit(p.Limit + 1) // fetch limit+1 to detect next page
@@ -407,7 +491,7 @@ func (r *ReferrerRepo) doGet(ctx context.Context, root *ent.Referrer, allowedOrg
407491
Where(
408492
referrer.KindEQ(biz.ReferrerAttestationType),
409493
projectPred,
410-
referrer.HasWorkflowsWith(predicateWF...),
494+
referrerVisibleToOrgs(allowedOrgs),
411495
).
412496
Exist(ctx)
413497
if err != nil {

0 commit comments

Comments
 (0)