@@ -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.
269304func 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