diff --git a/pkg/server/api/console/v1/access_review_campaign_resolvers.go b/pkg/server/api/console/v1/access_review_campaign_resolvers.go index 46b41bdb9..5c786e01a 100644 --- a/pkg/server/api/console/v1/access_review_campaign_resolvers.go +++ b/pkg/server/api/console/v1/access_review_campaign_resolvers.go @@ -628,6 +628,8 @@ func (r *mutationResolver) CreateAccessReviewSource(ctx context.Context, input t return nil, gqlutils.Internal(ctx) } + r.autoSelectDefaultOrganization(ctx, scope, source) + return &types.CreateAccessReviewSourcePayload{ AccessReviewSourceEdge: types.NewAccessReviewSourceEdge(source, coredata.AccessReviewSourceOrderFieldCreatedAt), }, nil @@ -660,6 +662,13 @@ func (r *mutationResolver) UpdateAccessReviewSource(ctx context.Context, input t return nil, gqlutils.Internal(ctx) } + // A connector was just (re)linked: default its org so the source is usable + // right away. Skipped on name/CSV-only updates to avoid a needless + // provider round-trip. + if input.ConnectorID.IsSet() { + r.autoSelectDefaultOrganization(ctx, scope, source) + } + return &types.UpdateAccessReviewSourcePayload{ AccessReviewSource: types.NewAccessReviewSource(source), }, nil @@ -717,6 +726,80 @@ func (r *mutationResolver) ConfigureAccessReviewSource(ctx context.Context, inpu }, nil } +// autoSelectDefaultOrganization picks the first workspace/org the connector can +// see for a freshly linked picker-provider source that has none selected yet. +// Without it a connected source stays "needs configuration" until the user +// completes the picker step; if they skip it, the first campaign silently +// resolves no users (the driver requires an org). Defaulting to the first +// available makes the source immediately usable; the picker UI stays available +// to switch when several are listed. +// +// Best-effort: any failure (provider unreachable, nothing listed) leaves the +// source in its existing "needs configuration" state, where the picker is the +// fallback. It never fails the create/update mutation that triggered it. +func (r *mutationResolver) autoSelectDefaultOrganization( + ctx context.Context, + scope coredata.Scoper, + source *coredata.AccessReviewSource, +) { + if source == nil || source.ConnectorID == nil { + return + } + + httpClient, dbConnector, err := r.accessReview.ConnectorHTTPClient(ctx, scope, *source.ConnectorID) + if err != nil { + // A missing connector is not an error worth logging: the picker + // simply never surfaces a default. + if !errors.Is(err, coredata.ErrResourceNotFound) { + r.logger.WarnCtx(ctx, "cannot load connector for default organization", log.Error(err)) + } + + return + } + + cfg, ok := providerOrgConfigs[dbConnector.Provider] + if !ok || !cfg.NeedsPicker || cfg.ListOrgs == nil { + return + } + + // Never override an org the user (or an earlier default) already picked. + if cfg.SelectedSlug(dbConnector) != "" { + return + } + + orgs, err := cfg.ListOrgs(ctx, httpClient) + if err != nil { + r.logger.WarnCtx( + ctx, + "cannot list provider organizations for default selection", + log.String("provider", dbConnector.Provider.String()), + log.Error(err), + ) + + return + } + + if len(orgs) == 0 { + return + } + + if _, err := r.accessReview.ConfigureAccessReviewSource( + ctx, + scope, + accessreview.ConfigureAccessReviewSourceRequest{ + AccessReviewSourceID: source.ID, + OrganizationSlug: orgs[0].Slug, + }, + ); err != nil { + r.logger.WarnCtx( + ctx, + "cannot apply default provider organization", + log.String("provider", dbConnector.Provider.String()), + log.Error(err), + ) + } +} + // CreateAccessReviewCampaign is the resolver for the createAccessReviewCampaign field. func (r *mutationResolver) CreateAccessReviewCampaign(ctx context.Context, input types.CreateAccessReviewCampaignInput) (*types.CreateAccessReviewCampaignPayload, error) { scope, err := r.authorize(ctx, input.OrganizationID, accessreview.ActionCampaignCreate)