diff --git a/pkg/server/api/console/v1/cookie_banner_resolvers.go b/pkg/server/api/console/v1/cookie_banner_resolvers.go index 220b79c56..ea9c53d00 100644 --- a/pkg/server/api/console/v1/cookie_banner_resolvers.go +++ b/pkg/server/api/console/v1/cookie_banner_resolvers.go @@ -14,6 +14,7 @@ import ( "go.gearno.de/kit/log" "go.probo.inc/probo/pkg/cookiebanner" "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/probo" "go.probo.inc/probo/pkg/server/api/authn" @@ -210,6 +211,25 @@ func (r *cookieBannerResolver) TrackerPatterns(ctx context.Context, obj *types.C if filter != nil { coredataFilter = coredata.NewTrackerPatternFilter(nil, filter.CookieCategoryID, nil) coredataFilter = coredataFilter.WithQuery(filter.Query).WithSource(filter.Source).WithTrackerType(filter.TrackerType) + + if filter.ThirdPartyID != nil { + switch filter.ThirdPartyID.EntityType() { + case coredata.ThirdPartyEntityType: + coredataFilter = coredataFilter.WithThirdPartyID(filter.ThirdPartyID) + case coredata.CommonThirdPartyEntityType: + ids, err := r.cookieBanner.LoadCommonTrackerPatternIDsByCommonThirdPartyID(ctx, *filter.ThirdPartyID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot resolve common third party tracker patterns", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + if ids == nil { + ids = []gid.GID{} + } + coredataFilter = coredataFilter.WithCommonTrackerPatternIDs(ids) + default: + return nil, gqlutils.Invalidf(ctx, "thirdPartyId must reference a ThirdParty or CommonThirdParty") + } + } } patterns, err := r.cookieBanner.ListTrackerPatternsForBanner(ctx, scope, obj.ID, cursor, coredataFilter) @@ -223,6 +243,101 @@ func (r *cookieBannerResolver) TrackerPatterns(ctx context.Context, obj *types.C return types.NewTrackerPatternConnectionWithFilter(p, r, obj.ID, filter), nil } +// LinkedThirdParties is the resolver for the linkedThirdParties field. +// +// Aggregates the deduped union of third parties linked to the banner's +// tracker patterns: the org-scoped ThirdParty values reached through +// the direct foreign key, plus the global CommonThirdParty values +// reached indirectly through CommonTrackerPattern. The two sources are +// independent, so a tracker pattern that has both ThirdPartyID and +// CommonTrackerPatternID contributes the org-scoped link only — the +// commonThirdParty resolver follows the same priority and we want the +// banner-level filter to mirror it. +func (r *cookieBannerResolver) LinkedThirdParties(ctx context.Context, obj *types.CookieBanner) ([]types.TrackerPatternThirdPartyLink, error) { + if _, err := r.authorize(ctx, obj.ID, probo.ActionTrackerPatternList); err != nil { + return nil, err + } + + scope := coredata.NewScopeFromObjectID(obj.ID) + + thirdPartyIDs, err := r.cookieBanner.LoadDistinctThirdPartyIDsByCookieBannerID(ctx, scope, obj.ID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot list banner third party links", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + commonPatternIDs, err := r.cookieBanner.LoadDistinctCommonTrackerPatternIDsByCookieBannerID(ctx, scope, obj.ID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot list banner common tracker pattern links", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + out := make([]types.TrackerPatternThirdPartyLink, 0, len(thirdPartyIDs)+len(commonPatternIDs)) + loaders := dataloader.FromContext(ctx) + + if len(thirdPartyIDs) > 0 { + if _, err := r.authorize(ctx, obj.ID, probo.ActionThirdPartyGet); err != nil { + return nil, err + } + + tps, loadErr := loaders.ThirdParty.LoadAll(ctx, thirdPartyIDs) + var loadErrs dataloadgen.ErrorSlice + if loadErr != nil && !errors.As(loadErr, &loadErrs) { + r.logger.ErrorCtx(ctx, "cannot get third parties", log.Error(loadErr)) + return nil, gqlutils.Internal(ctx) + } + for i, tp := range tps { + if loadErrs != nil && loadErrs[i] != nil { + if errors.Is(loadErrs[i], coredata.ErrResourceNotFound) || errors.Is(loadErrs[i], dataloadgen.ErrNotFound) { + continue + } + r.logger.ErrorCtx(ctx, "cannot get third party", log.Error(loadErrs[i])) + return nil, gqlutils.Internal(ctx) + } + out = append(out, types.NewThirdParty(tp)) + } + } + + if len(commonPatternIDs) > 0 { + identity := authn.IdentityFromContext(ctx) + if _, err := r.authorize(ctx, identity.ID, probo.ActionCommonThirdPartyGet); err != nil { + return nil, err + } + + patterns, err := r.cookieBanner.GetCommonTrackerPatternsByIDs(ctx, commonPatternIDs...) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot get common tracker patterns", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + seen := make(map[gid.GID]struct{}, len(patterns)) + commonThirdPartyIDs := make([]gid.GID, 0, len(patterns)) + for _, p := range patterns { + if p.CommonThirdPartyID == nil { + continue + } + if _, ok := seen[*p.CommonThirdPartyID]; ok { + continue + } + seen[*p.CommonThirdPartyID] = struct{}{} + commonThirdPartyIDs = append(commonThirdPartyIDs, *p.CommonThirdPartyID) + } + + if len(commonThirdPartyIDs) > 0 { + parties, err := r.thirdParty.GetCommonThirdPartiesByIDs(ctx, commonThirdPartyIDs...) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot get common third parties", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + for _, p := range parties { + out = append(out, types.NewCommonThirdParty(p)) + } + } + } + + return out, nil +} + // UncategorisedTrackerResources is the resolver for the uncategorisedTrackerResources field. func (r *cookieBannerResolver) UncategorisedTrackerResources(ctx context.Context, obj *types.CookieBanner, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.TrackerResourceOrderBy, filter *types.TrackerResourceFilter) (*types.TrackerResourceConnection, error) { if _, err := r.authorize(ctx, obj.ID, probo.ActionTrackerResourceList); err != nil { diff --git a/pkg/server/api/console/v1/graphql/cookie_banner.graphql b/pkg/server/api/console/v1/graphql/cookie_banner.graphql index cce17b609..4ac8bebd5 100644 --- a/pkg/server/api/console/v1/graphql/cookie_banner.graphql +++ b/pkg/server/api/console/v1/graphql/cookie_banner.graphql @@ -167,6 +167,15 @@ type CookieBanner implements Node { filter: TrackerPatternFilter ): TrackerPatternConnection @goField(forceResolver: true) + """ + The deduped union of third parties referenced by the banner's + tracker patterns, both org-scoped (ThirdParty) and global + (CommonThirdParty). Powers the third-party filter combobox on the + trackers list page. + """ + linkedThirdParties: [TrackerPatternThirdPartyLink!]! + @goField(forceResolver: true) + uncategorisedTrackerResources( first: Int after: CursorKey @@ -318,6 +327,14 @@ type TrackerPatternConnection pageInfo: PageInfo! } +""" +A third party associated with a banner's tracker patterns. Either the +tenant-managed ThirdParty (when a pattern was promoted to the org +catalog) or the global CommonThirdParty linked through a common +tracker pattern. +""" +union TrackerPatternThirdPartyLink = ThirdParty | CommonThirdParty + type TrackerPatternEdge { cursor: CursorKey! node: TrackerPattern! @@ -339,6 +356,14 @@ input TrackerPatternFilter source: CookieSource trackerType: TrackerType cookieCategoryId: ID + """ + Filter to tracker patterns linked to a single third party. Accepts + either an org-scoped ThirdParty GID (matched against third_party_id) + or a global CommonThirdParty GID (matched against the indirect link + through common_tracker_patterns). The dispatch happens at the + resolver based on the GID entity-type prefix. + """ + thirdPartyId: ID } type DetectedTracker implements Node { diff --git a/pkg/server/api/console/v1/types/common_third_party.go b/pkg/server/api/console/v1/types/common_third_party.go index aa9d6c791..50826a62e 100644 --- a/pkg/server/api/console/v1/types/common_third_party.go +++ b/pkg/server/api/console/v1/types/common_third_party.go @@ -37,6 +37,8 @@ type CommonThirdParty struct { LogoFileID *gid.GID `json:"logoFileId,omitempty"` } +func (CommonThirdParty) IsTrackerPatternThirdPartyLink() {} + func NewCommonThirdParty(c *coredata.CommonThirdParty) *CommonThirdParty { return &CommonThirdParty{ ID: c.ID, diff --git a/pkg/server/api/console/v1/types/tracker_pattern.go b/pkg/server/api/console/v1/types/tracker_pattern.go index bc6ee0652..1280a8299 100644 --- a/pkg/server/api/console/v1/types/tracker_pattern.go +++ b/pkg/server/api/console/v1/types/tracker_pattern.go @@ -73,6 +73,7 @@ type ( Source *coredata.CookieSource TrackerType *coredata.TrackerType CookieCategoryID *gid.GID + ThirdPartyID *gid.GID } )