Replace third-party owners with administrators
Some checks failed
github / Analyze (go) (push) Has been cancelled
github / Analyze (actions) (push) Has been cancelled
github / Analyze (javascript-typescript) (push) Has been cancelled
make / build-apps (push) Has been cancelled
make / probod binary (darwin/amd64) (push) Has been cancelled
make / probod binary (freebsd/amd64) (push) Has been cancelled
make / probod binary (linux/amd64) (push) Has been cancelled
make / probod binary (openbsd/amd64) (push) Has been cancelled
make / probod binary (windows/amd64) (push) Has been cancelled
make / probod binary (darwin/arm64) (push) Has been cancelled
make / probod binary (freebsd/arm64) (push) Has been cancelled
make / probod binary (linux/arm64) (push) Has been cancelled
make / probod binary (openbsd/arm64) (push) Has been cancelled
make / probo-agent (darwin/amd64) (push) Has been cancelled
make / probo-agent (freebsd/amd64) (push) Has been cancelled
make / probo-agent (linux/amd64) (push) Has been cancelled
make / probo-agent (windows/amd64) (push) Has been cancelled
make / probo-agent (darwin/arm64) (push) Has been cancelled
make / probo-agent (freebsd/arm64) (push) Has been cancelled
make / probo-agent (linux/arm64) (push) Has been cancelled
make / probo-agent (windows/arm64) (push) Has been cancelled
make / docker (amd64) (push) Has been cancelled
make / docker (arm64) (push) Has been cancelled
make / snapshot-scan (push) Has been cancelled
make / build-probod (push) Has been cancelled
make / build-probo-agent (push) Has been cancelled
make / lint-go (push) Has been cancelled
make / lint-js (push) Has been cancelled
make / lint-swift (push) Has been cancelled
make / lint-shell (push) Has been cancelled
make / test (push) Has been cancelled
make / test-e2e (push) Has been cancelled
trufflehog / scan (push) Has been cancelled

Migrate business and security owners into a shared administrators list across GraphQL, MCP, CLI, n8n, and the console.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
This commit is contained in:
Sacha Al Himdani
2026-07-27 12:24:43 +02:00
parent 084726dbad
commit 6a4f124adb
31 changed files with 808 additions and 601 deletions

View File

@@ -58,21 +58,22 @@ type (
}
Loaders struct {
Organization *dataloadgen.Loader[gid.GID, *coredata.Organization]
Framework *dataloadgen.Loader[gid.GID, *coredata.Framework]
Control *dataloadgen.Loader[gid.GID, *coredata.Control]
ThirdParty *dataloadgen.Loader[gid.GID, *coredata.ThirdParty]
Document *dataloadgen.Loader[gid.GID, *coredata.Document]
Profile *dataloadgen.Loader[gid.GID, *coredata.MembershipProfile]
Risk *dataloadgen.Loader[gid.GID, *coredata.Risk]
Measure *dataloadgen.Loader[gid.GID, *coredata.Measure]
Task *dataloadgen.Loader[gid.GID, *coredata.Task]
File *dataloadgen.Loader[gid.GID, *coredata.File]
CookieBanner *dataloadgen.Loader[gid.GID, *coredata.CookieBanner]
CookieCategory *dataloadgen.Loader[gid.GID, *coredata.CookieCategory]
CommonTrackerPattern *dataloadgen.Loader[gid.GID, *coredata.CommonTrackerPattern]
CommonThirdParty *dataloadgen.Loader[gid.GID, *coredata.CommonThirdParty]
Authorize *dataloadgen.Loader[AuthorizeKey, AuthorizeResult]
Organization *dataloadgen.Loader[gid.GID, *coredata.Organization]
Framework *dataloadgen.Loader[gid.GID, *coredata.Framework]
Control *dataloadgen.Loader[gid.GID, *coredata.Control]
ThirdParty *dataloadgen.Loader[gid.GID, *coredata.ThirdParty]
Document *dataloadgen.Loader[gid.GID, *coredata.Document]
Profile *dataloadgen.Loader[gid.GID, *coredata.MembershipProfile]
Risk *dataloadgen.Loader[gid.GID, *coredata.Risk]
Measure *dataloadgen.Loader[gid.GID, *coredata.Measure]
Task *dataloadgen.Loader[gid.GID, *coredata.Task]
File *dataloadgen.Loader[gid.GID, *coredata.File]
CookieBanner *dataloadgen.Loader[gid.GID, *coredata.CookieBanner]
CookieCategory *dataloadgen.Loader[gid.GID, *coredata.CookieCategory]
CommonTrackerPattern *dataloadgen.Loader[gid.GID, *coredata.CommonTrackerPattern]
CommonThirdParty *dataloadgen.Loader[gid.GID, *coredata.CommonThirdParty]
ThirdPartyAdministratorIDs *dataloadgen.Loader[gid.GID, []gid.GID]
Authorize *dataloadgen.Loader[AuthorizeKey, AuthorizeResult]
}
batchFetcher struct {
@@ -109,20 +110,21 @@ func NewMiddleware(proboSvc *probo.Service, iamSvc *iam.Service, cookieBannerSvc
func (f *batchFetcher) newLoaders() *Loaders {
return &Loaders{
Organization: dataloadgen.NewMappedLoader(f.fetchOrganizations),
Framework: dataloadgen.NewMappedLoader(f.fetchFrameworks),
Control: dataloadgen.NewMappedLoader(f.fetchControls),
ThirdParty: dataloadgen.NewMappedLoader(f.fetchThirdParties),
Document: dataloadgen.NewMappedLoader(f.fetchDocuments),
Profile: dataloadgen.NewMappedLoader(f.fetchProfiles),
Risk: dataloadgen.NewMappedLoader(f.fetchRisks),
Measure: dataloadgen.NewMappedLoader(f.fetchMeasures),
Task: dataloadgen.NewMappedLoader(f.fetchTasks),
File: dataloadgen.NewMappedLoader(f.fetchFiles),
CookieBanner: dataloadgen.NewMappedLoader(f.fetchCookieBanners),
CookieCategory: dataloadgen.NewMappedLoader(f.fetchCookieCategories),
CommonTrackerPattern: dataloadgen.NewMappedLoader(f.fetchCommonTrackerPatterns),
CommonThirdParty: dataloadgen.NewMappedLoader(f.fetchCommonThirdParties),
Organization: dataloadgen.NewMappedLoader(f.fetchOrganizations),
Framework: dataloadgen.NewMappedLoader(f.fetchFrameworks),
Control: dataloadgen.NewMappedLoader(f.fetchControls),
ThirdParty: dataloadgen.NewMappedLoader(f.fetchThirdParties),
Document: dataloadgen.NewMappedLoader(f.fetchDocuments),
Profile: dataloadgen.NewMappedLoader(f.fetchProfiles),
Risk: dataloadgen.NewMappedLoader(f.fetchRisks),
Measure: dataloadgen.NewMappedLoader(f.fetchMeasures),
Task: dataloadgen.NewMappedLoader(f.fetchTasks),
File: dataloadgen.NewMappedLoader(f.fetchFiles),
CookieBanner: dataloadgen.NewMappedLoader(f.fetchCookieBanners),
CookieCategory: dataloadgen.NewMappedLoader(f.fetchCookieCategories),
CommonTrackerPattern: dataloadgen.NewMappedLoader(f.fetchCommonTrackerPatterns),
CommonThirdParty: dataloadgen.NewMappedLoader(f.fetchCommonThirdParties),
ThirdPartyAdministratorIDs: dataloadgen.NewMappedLoader(f.fetchThirdPartyAdministratorIDs),
Authorize: dataloadgen.NewMappedLoader(
f.fetchAuthorizes,
dataloadgen.WithoutCache(),
@@ -350,6 +352,26 @@ func (f *batchFetcher) fetchCommonThirdParties(ctx context.Context, keys []gid.G
return result, nil
}
func (f *batchFetcher) fetchThirdPartyAdministratorIDs(ctx context.Context, keys []gid.GID) (map[gid.GID][]gid.GID, error) {
scope := coredata.NewScopeFromObjectID(keys[0])
administratorIDsByThirdPartyID, err := f.probo.ThirdParties.MapAdministratorIDsForThirdPartyIDs(ctx, scope, keys)
if err != nil {
return nil, fmt.Errorf("cannot batch load third party administrator ids: %w", err)
}
result := make(map[gid.GID][]gid.GID, len(keys))
for _, id := range keys {
if ids, ok := administratorIDsByThirdPartyID[id]; ok {
result[id] = ids
} else {
result[id] = []gid.GID{}
}
}
return result, nil
}
// fetchAuthorizes evaluates the batch with a single AuthorizeMulti call and
// surfaces per-key denials via dataloadgen.MappedFetchError. When
// AuthorizeMulti cannot evaluate the batch as a whole (e.g. mixed

View File

@@ -285,8 +285,7 @@ type ThirdParty implements Node {
filter: MeasureFilter
): MeasureConnection! @goField(forceResolver: true)
businessOwner: Profile @goField(forceResolver: true)
securityOwner: Profile @goField(forceResolver: true)
administrators: [Profile!]! @goField(forceResolver: true)
statusPageUrl: String
termsOfServiceUrl: String
@@ -540,8 +539,7 @@ input CreateThirdPartyInput {
trustPageUrl: String
statusPageUrl: String
termsOfServiceUrl: String
businessOwnerId: ID
securityOwnerId: ID
administratorIds: [ID!]
parentThirdPartyId: ID
}
@@ -564,8 +562,7 @@ input UpdateThirdPartyInput {
countries: [CountryCode!]
securityPageUrl: String @goField(omittable: true)
trustPageUrl: String @goField(omittable: true)
businessOwnerId: ID @goField(omittable: true)
securityOwnerId: ID @goField(omittable: true)
administratorIds: [ID!]
showOnCompliancePortal: Boolean
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/vikstrous/dataloadgen"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/probo"
@@ -58,8 +59,7 @@ func (r *mutationResolver) CreateThirdParty(ctx context.Context, input types.Cre
Certifications: input.Certifications,
SecurityPageURL: input.SecurityPageURL,
TrustPageURL: input.TrustPageURL,
BusinessOwnerID: input.BusinessOwnerID,
SecurityOwnerID: input.SecurityOwnerID,
AdministratorIDs: input.AdministratorIds,
Countries: input.Countries,
ParentThirdPartyID: input.ParentThirdPartyID,
},
@@ -120,6 +120,11 @@ func (r *mutationResolver) UpdateThirdParty(ctx context.Context, input types.Upd
return nil, err
}
var administratorIDs *[]gid.GID
if input.AdministratorIds != nil {
administratorIDs = &input.AdministratorIds
}
thirdParty, err := r.probo.ThirdParties.Update(
ctx, scope,
probo.UpdateThirdPartyRequest{
@@ -140,8 +145,7 @@ func (r *mutationResolver) UpdateThirdParty(ctx context.Context, input types.Upd
WebsiteURL: gqlutils.UnwrapOmittable(input.WebsiteURL),
Category: input.Category,
Certifications: input.Certifications,
BusinessOwnerID: gqlutils.UnwrapOmittable(input.BusinessOwnerID),
SecurityOwnerID: gqlutils.UnwrapOmittable(input.SecurityOwnerID),
AdministratorIDs: administratorIDs,
ShowOnCompliancePortal: input.ShowOnCompliancePortal,
Countries: input.Countries,
},
@@ -855,56 +859,45 @@ func (r *thirdPartyResolver) Measures(ctx context.Context, obj *types.ThirdParty
return types.NewMeasureConnection(page, r, obj.ID, measureFilter), nil
}
// BusinessOwner is the resolver for the businessOwner field.
func (r *thirdPartyResolver) BusinessOwner(ctx context.Context, obj *types.ThirdParty) (*types.Profile, error) {
if obj.BusinessOwner == nil {
return nil, nil
}
if _, err := r.authorize(ctx, obj.BusinessOwner.ID, iam.ActionMembershipProfileGet); err != nil {
// Administrators is the resolver for the administrators field.
func (r *thirdPartyResolver) Administrators(ctx context.Context, obj *types.ThirdParty) ([]*types.Profile, error) {
if _, err := r.authorize(ctx, obj.ID, probo.ActionThirdPartyGet); err != nil {
return nil, err
}
loaders := dataloader.FromContext(ctx)
businessOwner, err := loaders.Profile.Load(ctx, obj.BusinessOwner.ID)
administratorIDs, err := loaders.ThirdPartyAdministratorIDs.Load(ctx, obj.ID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get business owner", log.Error(err))
r.logger.ErrorCtx(ctx, "cannot get third party administrator ids", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewProfile(businessOwner), nil
}
// SecurityOwner is the resolver for the securityOwner field.
func (r *thirdPartyResolver) SecurityOwner(ctx context.Context, obj *types.ThirdParty) (*types.Profile, error) {
if obj.SecurityOwner == nil {
return nil, nil
if len(administratorIDs) == 0 {
return []*types.Profile{}, nil
}
if _, err := r.authorize(ctx, obj.SecurityOwner.ID, iam.ActionMembershipProfileGet); err != nil {
if _, err := r.batchAuthorize(ctx, iam.ActionMembershipProfileGet, administratorIDs); err != nil {
return nil, err
}
loaders := dataloader.FromContext(ctx)
securityOwner, err := loaders.Profile.Load(ctx, obj.SecurityOwner.ID)
profiles, err := loaders.Profile.LoadAll(ctx, administratorIDs)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get security owner", log.Error(err))
r.logger.ErrorCtx(ctx, "cannot get third party administrators", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewProfile(securityOwner), nil
result := make([]*types.Profile, len(profiles))
for i, p := range profiles {
result[i] = types.NewProfile(p)
}
return result, nil
}
// ParentThirdParty is the resolver for the parentThirdParty field.

View File

@@ -98,18 +98,6 @@ func NewThirdParty(v *coredata.ThirdParty) *ThirdParty {
CreatedAt: v.CreatedAt,
}
if v.BusinessOwnerID != nil {
object.BusinessOwner = &Profile{
ID: *v.BusinessOwnerID,
}
}
if v.SecurityOwnerID != nil {
object.SecurityOwner = &Profile{
ID: *v.SecurityOwnerID,
}
}
if v.ParentThirdPartyID != nil {
object.ParentThirdParty = &ThirdParty{
ID: *v.ParentThirdPartyID,