Decouple each campaign from the live access-review sources it was started with by introducing a per-campaign source snapshot table (access_review_campaign_sources). The snapshot captures the source name, category, and connector at start time, so a review remains coherent even after the underlying source is edited or deleted. Fetch tracking becomes an append-only log (access_review_campaign_source_fetch_attempts) that preserves every attempt with its own status and error rather than overwriting a single row. Rename the shared access-review tables and enums to use a consistent access_review_ prefix throughout: access_entries → access_review_entries access_sources → access_review_sources access_source_category → access_review_source_category access_entry_* → access_review_entry_* The same rename propagates to every coredata type, service, GraphQL schema, MCP specification, CLI command, frontend component, and e2e test. The accessreview package gains dedicated actions.go and policies.go files for its own IAM policy set, mirroring the agentrun package pattern. Signed-off-by: Bryan Frimin <bryan@probo.com>
1057 lines
36 KiB
Go
1057 lines
36 KiB
Go
package console_v1
|
|
|
|
// This file will be automatically regenerated based on the schema, any resolver
|
|
// implementations
|
|
// will be copied through when generating and any unknown code will be moved to the end.
|
|
// Code generated by github.com/99designs/gqlgen version v0.17.90
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/vikstrous/dataloadgen"
|
|
"go.gearno.de/kit/log"
|
|
"go.probo.inc/probo/pkg/accessreview"
|
|
"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"
|
|
"go.probo.inc/probo/pkg/server/api/console/v1/dataloader"
|
|
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
|
|
"go.probo.inc/probo/pkg/server/api/console/v1/types"
|
|
"go.probo.inc/probo/pkg/server/gqlutils"
|
|
)
|
|
|
|
// Organization is the resolver for the organization field.
|
|
func (r *accessReviewCampaignResolver) Organization(ctx context.Context, obj *types.AccessReviewCampaign) (*types.Organization, error) {
|
|
if _, err := r.authorize(ctx, obj.ID, probo.ActionOrganizationGet); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
loaders := dataloader.FromContext(ctx)
|
|
|
|
organization, err := loaders.Organization.Load(ctx, obj.Organization.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 load organization", log.Error(err))
|
|
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
return types.NewOrganization(organization), nil
|
|
}
|
|
|
|
// Sources is the resolver for the sources field.
|
|
func (r *accessReviewCampaignResolver) Sources(ctx context.Context, obj *types.AccessReviewCampaign) ([]*types.AccessReviewCampaignSource, error) {
|
|
scope, err := r.authorize(ctx, obj.ID, accessreview.ActionCampaignGet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
campaignSources, err := r.accessReview.ListCampaignSources(ctx, scope, obj.ID)
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot list campaign sources: %w", err))
|
|
}
|
|
|
|
attempts, err := r.accessReview.ListLatestFetchAttempts(ctx, scope, obj.ID)
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot list latest fetch attempts: %w", err))
|
|
}
|
|
|
|
attemptByCampaignSourceID := make(map[gid.GID]*coredata.AccessReviewCampaignSourceFetchAttempt, len(attempts))
|
|
for _, attempt := range attempts {
|
|
attemptByCampaignSourceID[attempt.AccessReviewCampaignSourceID] = attempt
|
|
}
|
|
|
|
result := make([]*types.AccessReviewCampaignSource, len(campaignSources))
|
|
for i, campaignSource := range campaignSources {
|
|
result[i] = types.NewAccessReviewCampaignSource(campaignSource, attemptByCampaignSourceID[campaignSource.ID])
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Entries is the resolver for the entries field.
|
|
func (r *accessReviewCampaignResolver) Entries(ctx context.Context, obj *types.AccessReviewCampaign, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AccessReviewEntryOrder, campaignSourceID *gid.GID, filter *coredata.AccessReviewEntryFilter) (*types.AccessReviewEntryConnection, error) {
|
|
scope, err := r.authorize(ctx, obj.ID, accessreview.ActionEntryList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pageOrderBy := page.OrderBy[coredata.AccessReviewEntryOrderField]{
|
|
Field: coredata.AccessReviewEntryOrderFieldCreatedAt,
|
|
Direction: page.OrderDirectionDesc,
|
|
}
|
|
|
|
if orderBy != nil {
|
|
pageOrderBy = page.OrderBy[coredata.AccessReviewEntryOrderField]{
|
|
Field: orderBy.Field,
|
|
Direction: orderBy.Direction,
|
|
}
|
|
}
|
|
|
|
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
|
|
|
var (
|
|
p *page.Page[*coredata.AccessReviewEntry, coredata.AccessReviewEntryOrderField]
|
|
)
|
|
|
|
if campaignSourceID != nil {
|
|
p, err = r.accessReview.ListEntriesForCampaignIDAndSourceID(ctx, scope, obj.ID, *campaignSourceID, cursor, filter)
|
|
} else {
|
|
p, err = r.accessReview.ListEntriesForCampaignID(ctx, scope, obj.ID, cursor, filter)
|
|
}
|
|
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot list access entries: %w", err))
|
|
}
|
|
|
|
return types.NewAccessReviewEntryConnection(p, r, obj.ID, campaignSourceID, filter), nil
|
|
}
|
|
|
|
// PendingEntryCount is the resolver for the pendingEntryCount field.
|
|
func (r *accessReviewCampaignResolver) PendingEntryCount(ctx context.Context, obj *types.AccessReviewCampaign) (int, error) {
|
|
scope, err := r.authorize(ctx, obj.ID, accessreview.ActionEntryList)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
count, err := r.accessReview.CountPendingEntriesForCampaignID(ctx, scope, obj.ID)
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot count pending access entries: %w", err))
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// Statistics is the resolver for the statistics field.
|
|
func (r *accessReviewCampaignResolver) Statistics(ctx context.Context, obj *types.AccessReviewCampaign) (*types.AccessReviewStatistics, error) {
|
|
scope, err := r.authorize(ctx, obj.ID, accessreview.ActionEntryList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats, err := r.accessReview.CampaignStatistics(ctx, scope, obj.ID)
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot get campaign statistics: %w", err))
|
|
}
|
|
|
|
return types.NewAccessReviewStatistics(stats), nil
|
|
}
|
|
|
|
// Permission is the resolver for the permission field.
|
|
func (r *accessReviewCampaignResolver) Permission(ctx context.Context, obj *types.AccessReviewCampaign, action string) (bool, error) {
|
|
return r.Resolver.Permission(ctx, obj, action)
|
|
}
|
|
|
|
// TotalCount is the resolver for the totalCount field.
|
|
func (r *accessReviewCampaignConnectionResolver) TotalCount(ctx context.Context, obj *types.AccessReviewCampaignConnection) (int, error) {
|
|
scope, err := r.authorize(ctx, obj.ParentID, accessreview.ActionCampaignList)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
switch obj.Resolver.(type) {
|
|
case *organizationResolver:
|
|
count, err := r.accessReview.CountCampaignsForOrganizationID(ctx, scope, obj.ParentID)
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot count access review campaigns: %w", err))
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
|
|
}
|
|
|
|
// Source is the resolver for the source field.
|
|
func (r *accessReviewCampaignSourceResolver) Source(ctx context.Context, obj *types.AccessReviewCampaignSource) (*types.AccessReviewSource, error) {
|
|
if obj.SourceID == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
scope, err := r.authorize(ctx, obj.CampaignID, accessreview.ActionCampaignGet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
source, err := r.accessReview.GetSource(ctx, scope, *obj.SourceID)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return nil, nil
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot get access review source: %w", err))
|
|
}
|
|
|
|
return types.NewAccessReviewSource(source), nil
|
|
}
|
|
|
|
// FetchAttempts is the resolver for the fetchAttempts field.
|
|
func (r *accessReviewCampaignSourceResolver) FetchAttempts(ctx context.Context, obj *types.AccessReviewCampaignSource) ([]*types.AccessReviewCampaignSourceFetchAttempt, error) {
|
|
scope, err := r.authorize(ctx, obj.CampaignID, accessreview.ActionCampaignGet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
attempts, err := r.accessReview.ListFetchAttempts(ctx, scope, obj.ID)
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot list fetch attempts: %w", err))
|
|
}
|
|
|
|
result := make([]*types.AccessReviewCampaignSourceFetchAttempt, len(attempts))
|
|
for i, attempt := range attempts {
|
|
result[i] = types.NewAccessReviewCampaignSourceFetchAttempt(attempt)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Entries is the resolver for the entries field.
|
|
func (r *accessReviewCampaignSourceResolver) Entries(ctx context.Context, obj *types.AccessReviewCampaignSource, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AccessReviewEntryOrder, filter *coredata.AccessReviewEntryFilter) (*types.AccessReviewEntryConnection, error) {
|
|
scope, err := r.authorize(ctx, obj.CampaignID, accessreview.ActionEntryList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pageOrderBy := page.OrderBy[coredata.AccessReviewEntryOrderField]{
|
|
Field: coredata.AccessReviewEntryOrderFieldCreatedAt,
|
|
Direction: page.OrderDirectionDesc,
|
|
}
|
|
|
|
if orderBy != nil {
|
|
pageOrderBy = page.OrderBy[coredata.AccessReviewEntryOrderField]{
|
|
Field: orderBy.Field,
|
|
Direction: orderBy.Direction,
|
|
}
|
|
}
|
|
|
|
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
|
|
|
p, err := r.accessReview.ListEntriesForCampaignIDAndSourceID(ctx, scope, obj.CampaignID, obj.ID, cursor, filter)
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot list access entries: %w", err))
|
|
}
|
|
|
|
sourceID := obj.ID
|
|
|
|
return types.NewAccessReviewEntryConnection(p, r, obj.CampaignID, &sourceID, filter), nil
|
|
}
|
|
|
|
// Statistics is the resolver for the statistics field.
|
|
func (r *accessReviewCampaignSourceResolver) Statistics(ctx context.Context, obj *types.AccessReviewCampaignSource) (*types.AccessReviewStatistics, error) {
|
|
scope, err := r.authorize(ctx, obj.CampaignID, accessreview.ActionEntryList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats, err := r.accessReview.CampaignSourceStatistics(ctx, scope, obj.CampaignID, obj.ID)
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot get source statistics: %w", err))
|
|
}
|
|
|
|
return types.NewAccessReviewStatistics(stats), nil
|
|
}
|
|
|
|
// Campaign is the resolver for the campaign field.
|
|
func (r *accessReviewEntryResolver) Campaign(ctx context.Context, obj *types.AccessReviewEntry) (*types.AccessReviewCampaign, error) {
|
|
scope, err := r.authorize(ctx, obj.Campaign.ID, accessreview.ActionCampaignGet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
campaign, err := r.accessReview.GetCampaign(ctx, scope, obj.Campaign.ID)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return nil, gqlutils.NotFound(ctx, err)
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot get access review campaign: %w", err))
|
|
}
|
|
|
|
return types.NewAccessReviewCampaign(campaign), nil
|
|
}
|
|
|
|
// AccessReviewSource is the resolver for the accessReviewSource field.
|
|
func (r *accessReviewEntryResolver) AccessReviewSource(ctx context.Context, obj *types.AccessReviewEntry) (*types.AccessReviewSource, error) {
|
|
scope, err := r.authorize(ctx, obj.ID, accessreview.ActionEntryGet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entry, err := r.accessReview.GetEntry(ctx, scope, obj.ID)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return nil, gqlutils.NotFound(ctx, err)
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot get access review entry: %w", err))
|
|
}
|
|
|
|
campaignSource, err := r.accessReview.GetCampaignSource(ctx, scope, entry.AccessReviewCampaignSourceID)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return nil, nil
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot get campaign source: %w", err))
|
|
}
|
|
|
|
if campaignSource.AccessReviewSourceID == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
source, err := r.accessReview.GetSource(ctx, scope, *campaignSource.AccessReviewSourceID)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return nil, nil
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot get access review source: %w", err))
|
|
}
|
|
|
|
return types.NewAccessReviewSource(source), nil
|
|
}
|
|
|
|
// DecisionHistory is the resolver for the decisionHistory field.
|
|
func (r *accessReviewEntryResolver) DecisionHistory(ctx context.Context, obj *types.AccessReviewEntry) ([]*types.AccessReviewEntryDecisionHistoryEntry, error) {
|
|
scope, err := r.authorize(ctx, obj.ID, accessreview.ActionEntryGet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
histories, err := r.accessReview.EntryDecisionHistory(ctx, scope, obj.ID)
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot get decision history: %w", err))
|
|
}
|
|
|
|
result := make([]*types.AccessReviewEntryDecisionHistoryEntry, len(histories))
|
|
for i, h := range histories {
|
|
result[i] = types.NewAccessReviewEntryDecisionHistoryEntry(h)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Permission is the resolver for the permission field.
|
|
func (r *accessReviewEntryResolver) Permission(ctx context.Context, obj *types.AccessReviewEntry, action string) (bool, error) {
|
|
return r.Resolver.Permission(ctx, obj, action)
|
|
}
|
|
|
|
// TotalCount is the resolver for the totalCount field.
|
|
func (r *accessReviewEntryConnectionResolver) TotalCount(ctx context.Context, obj *types.AccessReviewEntryConnection) (int, error) {
|
|
scope, err := r.authorize(ctx, obj.ParentID, accessreview.ActionEntryList)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
switch obj.Resolver.(type) {
|
|
case *accessReviewCampaignResolver:
|
|
if obj.SourceID != nil {
|
|
count, err := r.accessReview.CountEntriesForCampaignIDAndSourceID(ctx, scope, obj.ParentID, *obj.SourceID, obj.Filter)
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot count access entries: %w", err))
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
count, err := r.accessReview.CountEntriesForCampaignID(ctx, scope, obj.ParentID, obj.Filter)
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot count access entries: %w", err))
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
|
|
}
|
|
|
|
// Organization is the resolver for the organization field.
|
|
func (r *accessReviewSourceResolver) Organization(ctx context.Context, obj *types.AccessReviewSource) (*types.Organization, error) {
|
|
if _, err := r.authorize(ctx, obj.ID, probo.ActionOrganizationGet); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
loaders := dataloader.FromContext(ctx)
|
|
|
|
organization, err := loaders.Organization.Load(ctx, obj.Organization.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 load organization", log.Error(err))
|
|
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
return types.NewOrganization(organization), nil
|
|
}
|
|
|
|
// Connector is the resolver for the connector field.
|
|
func (r *accessReviewSourceResolver) Connector(ctx context.Context, obj *types.AccessReviewSource) (*types.Connector, error) {
|
|
if obj.ConnectorID == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
scope, err := r.authorize(ctx, obj.ID, accessreview.ActionSourceGet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
connector, err := r.probo.Connectors.Get(ctx, scope, *obj.ConnectorID)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return nil, nil
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot get connector: %w", err))
|
|
}
|
|
|
|
return types.NewConnector(connector), nil
|
|
}
|
|
|
|
// ProviderOrganizations is the resolver for the providerOrganizations field.
|
|
func (r *accessReviewSourceResolver) ProviderOrganizations(ctx context.Context, obj *types.AccessReviewSource) ([]*types.ProviderOrganization, error) {
|
|
scope, err := r.authorize(ctx, obj.ID, accessreview.ActionSourceGet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if obj.ConnectorID == nil {
|
|
return []*types.ProviderOrganization{}, nil
|
|
}
|
|
|
|
httpClient, dbConnector, err := r.accessReview.ConnectorHTTPClient(ctx, scope, *obj.ConnectorID)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return []*types.ProviderOrganization{}, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("cannot get connector HTTP client: %w", err)
|
|
}
|
|
|
|
cfg, ok := providerOrgConfigs[dbConnector.Provider]
|
|
if !ok || cfg.ListOrgs == nil {
|
|
return []*types.ProviderOrganization{}, nil
|
|
}
|
|
|
|
orgs, err := cfg.ListOrgs(ctx, httpClient)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]*types.ProviderOrganization, len(orgs))
|
|
for i, o := range orgs {
|
|
result[i] = &types.ProviderOrganization{Slug: o.Slug, DisplayName: o.DisplayName}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// NeedsConfiguration is the resolver for the needsConfiguration field.
|
|
//
|
|
// True when the provider has a picker UI (NeedsPicker) AND the user has
|
|
// not yet picked an org. 2-auto providers (PagerDuty, Vercel) always
|
|
// return false: the identifier is captured during the OAuth callback,
|
|
// not via a follow-up configure mutation.
|
|
func (r *accessReviewSourceResolver) NeedsConfiguration(ctx context.Context, obj *types.AccessReviewSource) (bool, error) {
|
|
scope, err := r.authorize(ctx, obj.ID, accessreview.ActionSourceGet)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if obj.ConnectorID == nil {
|
|
return false, nil
|
|
}
|
|
|
|
dbConnector, err := r.probo.Connectors.Get(ctx, scope, *obj.ConnectorID)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return false, nil
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot get connector: %w", err))
|
|
}
|
|
|
|
cfg, ok := providerOrgConfigs[dbConnector.Provider]
|
|
if !ok || !cfg.NeedsPicker {
|
|
return false, nil
|
|
}
|
|
|
|
return cfg.SelectedSlug(dbConnector) == "", nil
|
|
}
|
|
|
|
// ConnectionStatus is the resolver for the connectionStatus field.
|
|
func (r *accessReviewSourceResolver) ConnectionStatus(ctx context.Context, obj *types.AccessReviewSource) (types.AccessReviewSourceConnectionStatus, error) {
|
|
if obj.ConnectorID == nil {
|
|
return types.AccessReviewSourceConnectionStatusNotApplicable, nil
|
|
}
|
|
|
|
scope, err := r.authorize(ctx, obj.ID, accessreview.ActionSourceGet)
|
|
if err != nil {
|
|
return types.AccessReviewSourceConnectionStatusNotApplicable, err
|
|
}
|
|
|
|
httpClient, dbConnector, err := r.accessReview.ConnectorHTTPClient(ctx, scope, *obj.ConnectorID)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return types.AccessReviewSourceConnectionStatusNotApplicable, nil
|
|
}
|
|
|
|
return types.AccessReviewSourceConnectionStatusDisconnected, nil
|
|
}
|
|
|
|
if dbConnector.Protocol != coredata.ConnectorProtocolOAuth2 {
|
|
return types.AccessReviewSourceConnectionStatusConnected, nil
|
|
}
|
|
|
|
// Creating an HTTP client may succeed even with an expired token
|
|
// (e.g. no refresh token available). Make a lightweight probe
|
|
// request to verify the token is actually valid.
|
|
probeURL := r.providerRegistry.ProbeURL(string(dbConnector.Provider))
|
|
if err := probeConnection(ctx, httpClient, probeURL); err != nil {
|
|
return types.AccessReviewSourceConnectionStatusDisconnected, nil
|
|
}
|
|
|
|
return types.AccessReviewSourceConnectionStatusConnected, nil
|
|
}
|
|
|
|
// SelectedOrganization is the resolver for the selectedOrganization field.
|
|
func (r *accessReviewSourceResolver) SelectedOrganization(ctx context.Context, obj *types.AccessReviewSource) (*string, error) {
|
|
scope, err := r.authorize(ctx, obj.ID, accessreview.ActionSourceGet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if obj.ConnectorID == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
dbConnector, err := r.probo.Connectors.Get(ctx, scope, *obj.ConnectorID)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return nil, nil
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot get connector: %w", err))
|
|
}
|
|
|
|
cfg, ok := providerOrgConfigs[dbConnector.Provider]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
slug := cfg.SelectedSlug(dbConnector)
|
|
if slug == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
return &slug, nil
|
|
}
|
|
|
|
// Permission is the resolver for the permission field.
|
|
func (r *accessReviewSourceResolver) Permission(ctx context.Context, obj *types.AccessReviewSource, action string) (bool, error) {
|
|
return r.Resolver.Permission(ctx, obj, action)
|
|
}
|
|
|
|
// TotalCount is the resolver for the totalCount field.
|
|
func (r *accessReviewSourceConnectionResolver) TotalCount(ctx context.Context, obj *types.AccessReviewSourceConnection) (int, error) {
|
|
scope, err := r.authorize(ctx, obj.ParentID, accessreview.ActionSourceList)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
switch obj.Resolver.(type) {
|
|
case *organizationResolver:
|
|
count, err := r.accessReview.CountSourcesForOrganizationID(ctx, scope, obj.ParentID)
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot count access sources: %w", err))
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
|
|
}
|
|
|
|
// CreateAccessReviewSource is the resolver for the createAccessReviewSource field.
|
|
func (r *mutationResolver) CreateAccessReviewSource(ctx context.Context, input types.CreateAccessReviewSourceInput) (*types.CreateAccessReviewSourcePayload, error) {
|
|
scope, err := r.authorize(ctx, input.OrganizationID, accessreview.ActionSourceCreate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
source, err := r.accessReview.CreateSource(ctx, scope, accessreview.CreateAccessReviewSourceRequest{
|
|
OrganizationID: input.OrganizationID,
|
|
ConnectorID: input.ConnectorID,
|
|
Name: input.Name,
|
|
Category: coredata.AccessReviewSourceCategorySaaS,
|
|
CsvData: input.CSVData,
|
|
})
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot create access source: %w", err))
|
|
}
|
|
|
|
return &types.CreateAccessReviewSourcePayload{
|
|
AccessReviewSourceEdge: types.NewAccessReviewSourceEdge(source, coredata.AccessReviewSourceOrderFieldCreatedAt),
|
|
}, nil
|
|
}
|
|
|
|
// UpdateAccessReviewSource is the resolver for the updateAccessReviewSource field.
|
|
func (r *mutationResolver) UpdateAccessReviewSource(ctx context.Context, input types.UpdateAccessReviewSourceInput) (*types.UpdateAccessReviewSourcePayload, error) {
|
|
scope, err := r.authorize(ctx, input.AccessReviewSourceID, accessreview.ActionSourceUpdate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req := accessreview.UpdateAccessReviewSourceRequest{
|
|
AccessReviewSourceID: input.AccessReviewSourceID,
|
|
}
|
|
|
|
if input.Name.IsSet() {
|
|
req.Name = input.Name.Value()
|
|
}
|
|
|
|
if input.ConnectorID.IsSet() {
|
|
req.ConnectorID = gqlutils.UnwrapOmittable(input.ConnectorID)
|
|
}
|
|
|
|
if input.CSVData.IsSet() {
|
|
req.CsvData = gqlutils.UnwrapOmittable(input.CSVData)
|
|
}
|
|
|
|
source, err := r.accessReview.UpdateSource(ctx, scope, req)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return nil, gqlutils.NotFound(ctx, err)
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot update access source: %w", err))
|
|
}
|
|
|
|
return &types.UpdateAccessReviewSourcePayload{
|
|
AccessReviewSource: types.NewAccessReviewSource(source),
|
|
}, nil
|
|
}
|
|
|
|
// DeleteAccessReviewSource is the resolver for the deleteAccessReviewSource field.
|
|
func (r *mutationResolver) DeleteAccessReviewSource(ctx context.Context, input types.DeleteAccessReviewSourceInput) (*types.DeleteAccessReviewSourcePayload, error) {
|
|
scope, err := r.authorize(ctx, input.AccessReviewSourceID, accessreview.ActionSourceDelete)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := r.accessReview.DeleteSource(ctx, scope, input.AccessReviewSourceID); err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return nil, gqlutils.NotFound(ctx, err)
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot delete access source: %w", err))
|
|
}
|
|
|
|
return &types.DeleteAccessReviewSourcePayload{
|
|
DeletedAccessReviewSourceID: input.AccessReviewSourceID,
|
|
}, nil
|
|
}
|
|
|
|
// ConfigureAccessReviewSource is the resolver for the configureAccessReviewSource field.
|
|
func (r *mutationResolver) ConfigureAccessReviewSource(ctx context.Context, input types.ConfigureAccessReviewSourceInput) (*types.ConfigureAccessReviewSourcePayload, error) {
|
|
scope, err := r.authorize(ctx, input.AccessReviewSourceID, accessreview.ActionSourceUpdate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
source, err := r.accessReview.ConfigureAccessReviewSource(
|
|
ctx,
|
|
scope,
|
|
accessreview.ConfigureAccessReviewSourceRequest{
|
|
AccessReviewSourceID: input.AccessReviewSourceID,
|
|
OrganizationSlug: input.OrganizationSlug,
|
|
},
|
|
)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return nil, gqlutils.NotFound(ctx, err)
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot configure access source: %w", err))
|
|
}
|
|
|
|
return &types.ConfigureAccessReviewSourcePayload{
|
|
AccessReviewSource: types.NewAccessReviewSource(source),
|
|
}, nil
|
|
}
|
|
|
|
// 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)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var description string
|
|
if input.Description != nil {
|
|
description = *input.Description
|
|
}
|
|
|
|
campaign, err := r.accessReview.CreateCampaign(ctx, scope, accessreview.CreateAccessReviewCampaignRequest{
|
|
OrganizationID: input.OrganizationID,
|
|
Name: input.Name,
|
|
Description: description,
|
|
FrameworkControls: input.FrameworkControls,
|
|
AccessReviewSourceIDs: input.AccessReviewSourceIds,
|
|
})
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot create access review campaign: %w", err))
|
|
}
|
|
|
|
return &types.CreateAccessReviewCampaignPayload{
|
|
AccessReviewCampaignEdge: types.NewAccessReviewCampaignEdge(campaign, coredata.AccessReviewCampaignOrderFieldCreatedAt),
|
|
}, nil
|
|
}
|
|
|
|
// UpdateAccessReviewCampaign is the resolver for the updateAccessReviewCampaign field.
|
|
func (r *mutationResolver) UpdateAccessReviewCampaign(ctx context.Context, input types.UpdateAccessReviewCampaignInput) (*types.UpdateAccessReviewCampaignPayload, error) {
|
|
scope, err := r.authorize(ctx, input.AccessReviewCampaignID, accessreview.ActionCampaignUpdate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req := accessreview.UpdateAccessReviewCampaignRequest{
|
|
CampaignID: input.AccessReviewCampaignID,
|
|
}
|
|
|
|
if input.Name.IsSet() {
|
|
req.Name = input.Name.Value()
|
|
}
|
|
|
|
if input.Description.IsSet() {
|
|
req.Description = input.Description.Value()
|
|
}
|
|
|
|
if input.FrameworkControls.IsSet() {
|
|
controls := input.FrameworkControls.Value()
|
|
req.FrameworkControls = &controls
|
|
}
|
|
|
|
campaign, err := r.accessReview.UpdateCampaign(ctx, scope, req)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return nil, gqlutils.NotFound(ctx, err)
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot update access review campaign: %w", err))
|
|
}
|
|
|
|
return &types.UpdateAccessReviewCampaignPayload{
|
|
AccessReviewCampaign: types.NewAccessReviewCampaign(campaign),
|
|
}, nil
|
|
}
|
|
|
|
// DeleteAccessReviewCampaign is the resolver for the deleteAccessReviewCampaign field.
|
|
func (r *mutationResolver) DeleteAccessReviewCampaign(ctx context.Context, input types.DeleteAccessReviewCampaignInput) (*types.DeleteAccessReviewCampaignPayload, error) {
|
|
scope, err := r.authorize(ctx, input.AccessReviewCampaignID, accessreview.ActionCampaignDelete)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := r.accessReview.DeleteCampaign(ctx, scope, input.AccessReviewCampaignID); err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return nil, gqlutils.NotFound(ctx, err)
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot delete access review campaign: %w", err))
|
|
}
|
|
|
|
return &types.DeleteAccessReviewCampaignPayload{
|
|
DeletedAccessReviewCampaignID: input.AccessReviewCampaignID,
|
|
}, nil
|
|
}
|
|
|
|
// StartAccessReviewCampaign is the resolver for the startAccessReviewCampaign field.
|
|
func (r *mutationResolver) StartAccessReviewCampaign(ctx context.Context, input types.StartAccessReviewCampaignInput) (*types.StartAccessReviewCampaignPayload, error) {
|
|
scope, err := r.authorize(ctx, input.AccessReviewCampaignID, accessreview.ActionCampaignStart)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
campaign, err := r.accessReview.StartCampaign(ctx, scope, input.AccessReviewCampaignID)
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot start access review campaign: %w", err))
|
|
}
|
|
|
|
return &types.StartAccessReviewCampaignPayload{
|
|
AccessReviewCampaign: types.NewAccessReviewCampaign(campaign),
|
|
}, nil
|
|
}
|
|
|
|
// CloseAccessReviewCampaign is the resolver for the closeAccessReviewCampaign field.
|
|
func (r *mutationResolver) CloseAccessReviewCampaign(ctx context.Context, input types.CloseAccessReviewCampaignInput) (*types.CloseAccessReviewCampaignPayload, error) {
|
|
scope, err := r.authorize(ctx, input.AccessReviewCampaignID, accessreview.ActionCampaignClose)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
campaign, err := r.accessReview.CloseCampaign(ctx, scope, input.AccessReviewCampaignID)
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot close access review campaign: %w", err))
|
|
}
|
|
|
|
return &types.CloseAccessReviewCampaignPayload{
|
|
AccessReviewCampaign: types.NewAccessReviewCampaign(campaign),
|
|
}, nil
|
|
}
|
|
|
|
// CancelAccessReviewCampaign is the resolver for the cancelAccessReviewCampaign field.
|
|
func (r *mutationResolver) CancelAccessReviewCampaign(ctx context.Context, input types.CancelAccessReviewCampaignInput) (*types.CancelAccessReviewCampaignPayload, error) {
|
|
scope, err := r.authorize(ctx, input.AccessReviewCampaignID, accessreview.ActionCampaignCancel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
campaign, err := r.accessReview.CancelCampaign(ctx, scope, input.AccessReviewCampaignID)
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot cancel access review campaign: %w", err))
|
|
}
|
|
|
|
return &types.CancelAccessReviewCampaignPayload{
|
|
AccessReviewCampaign: types.NewAccessReviewCampaign(campaign),
|
|
}, nil
|
|
}
|
|
|
|
// AddAccessReviewCampaignSource is the resolver for the addAccessReviewCampaignSource field.
|
|
func (r *mutationResolver) AddAccessReviewCampaignSource(ctx context.Context, input types.AddAccessReviewCampaignSourceInput) (*types.AddAccessReviewCampaignSourcePayload, error) {
|
|
scope, err := r.authorize(ctx, input.AccessReviewCampaignID, accessreview.ActionCampaignAddSource)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
campaign, err := r.accessReview.AddCampaignSource(ctx, scope, accessreview.AddCampaignSourceRequest{
|
|
CampaignID: input.AccessReviewCampaignID,
|
|
AccessReviewSourceID: input.AccessReviewSourceID,
|
|
})
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot add scope source to access review campaign: %w", err))
|
|
}
|
|
|
|
return &types.AddAccessReviewCampaignSourcePayload{
|
|
AccessReviewCampaign: types.NewAccessReviewCampaign(campaign),
|
|
}, nil
|
|
}
|
|
|
|
// RemoveAccessReviewCampaignSource is the resolver for the removeAccessReviewCampaignSource field.
|
|
func (r *mutationResolver) RemoveAccessReviewCampaignSource(ctx context.Context, input types.RemoveAccessReviewCampaignSourceInput) (*types.RemoveAccessReviewCampaignSourcePayload, error) {
|
|
scope, err := r.authorize(ctx, input.AccessReviewCampaignID, accessreview.ActionCampaignRemoveSource)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
campaign, err := r.accessReview.RemoveCampaignSource(ctx, scope, accessreview.RemoveCampaignSourceRequest{
|
|
CampaignID: input.AccessReviewCampaignID,
|
|
AccessReviewSourceID: input.AccessReviewSourceID,
|
|
})
|
|
if err != nil {
|
|
panic(fmt.Errorf("cannot remove scope source from access review campaign: %w", err))
|
|
}
|
|
|
|
return &types.RemoveAccessReviewCampaignSourcePayload{
|
|
AccessReviewCampaign: types.NewAccessReviewCampaign(campaign),
|
|
}, nil
|
|
}
|
|
|
|
// RecordAccessReviewEntryDecision is the resolver for the recordAccessReviewEntryDecision field.
|
|
func (r *mutationResolver) RecordAccessReviewEntryDecision(ctx context.Context, input types.RecordAccessReviewEntryDecisionInput) (*types.RecordAccessReviewEntryDecisionPayload, error) {
|
|
scope, err := r.authorize(ctx, input.AccessReviewEntryID, accessreview.ActionEntryDecide)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Resolve the profile ID from the session's identity.
|
|
// The profile may not exist for every identity, in which
|
|
// case decided_by will be left nil.
|
|
identity := authn.IdentityFromContext(ctx)
|
|
if identity == nil {
|
|
return nil, fmt.Errorf("no identity in context")
|
|
}
|
|
|
|
req := accessreview.RecordAccessReviewEntryDecisionRequest{
|
|
EntryID: input.AccessReviewEntryID,
|
|
Decision: input.Decision,
|
|
DecisionNote: input.DecisionNote,
|
|
}
|
|
|
|
organizationID, err := r.accessReview.ResolveEntryOrganizationID(ctx, input.AccessReviewEntryID)
|
|
if err == nil {
|
|
profile, err := r.iam.OrganizationService.GetProfileForIdentityAndOrganization(ctx, identity.ID, organizationID)
|
|
if err == nil {
|
|
req.DecidedByID = &profile.ID
|
|
}
|
|
}
|
|
|
|
entry, err := r.accessReview.RecordDecision(ctx, scope, req)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return nil, gqlutils.NotFound(ctx, err)
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot record access entry decision: %w", err))
|
|
}
|
|
|
|
return &types.RecordAccessReviewEntryDecisionPayload{
|
|
AccessReviewEntry: types.NewAccessReviewEntry(entry),
|
|
}, nil
|
|
}
|
|
|
|
// RecordAccessReviewEntryDecisions is the resolver for the recordAccessReviewEntryDecisions field.
|
|
func (r *mutationResolver) RecordAccessReviewEntryDecisions(ctx context.Context, input types.RecordAccessReviewEntryDecisionsInput) (*types.RecordAccessReviewEntryDecisionsPayload, error) {
|
|
if len(input.Decisions) == 0 {
|
|
return &types.RecordAccessReviewEntryDecisionsPayload{
|
|
AccessReviewEntries: []*types.AccessReviewEntry{},
|
|
}, nil
|
|
}
|
|
|
|
const maxBatchSize = 100
|
|
if len(input.Decisions) > maxBatchSize {
|
|
return nil, fmt.Errorf("cannot record decisions: batch size %d exceeds maximum of %d", len(input.Decisions), maxBatchSize)
|
|
}
|
|
|
|
// Authorize each entry individually to prevent cross-org bypass.
|
|
for _, d := range input.Decisions {
|
|
_, err := r.authorize(ctx, d.AccessReviewEntryID, accessreview.ActionEntryDecide)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
identity := authn.IdentityFromContext(ctx)
|
|
if identity == nil {
|
|
return nil, fmt.Errorf("no identity in context")
|
|
}
|
|
|
|
tenantID := input.Decisions[0].AccessReviewEntryID.TenantID()
|
|
scope := coredata.NewScope(tenantID)
|
|
|
|
// Cache profile lookups per organization so we resolve the correct
|
|
// decidedByID for each entry even when a batch spans multiple orgs.
|
|
profileCache := make(map[gid.GID]*gid.GID)
|
|
|
|
decisions := make([]accessreview.RecordAccessReviewEntryDecisionRequest, len(input.Decisions))
|
|
for i, d := range input.Decisions {
|
|
var decidedByID *gid.GID
|
|
|
|
organizationID, err := r.accessReview.ResolveEntryOrganizationID(ctx, d.AccessReviewEntryID)
|
|
if err == nil {
|
|
if cached, ok := profileCache[organizationID]; ok {
|
|
decidedByID = cached
|
|
} else {
|
|
profile, err := r.iam.OrganizationService.GetProfileForIdentityAndOrganization(ctx, identity.ID, organizationID)
|
|
if err == nil {
|
|
decidedByID = &profile.ID
|
|
}
|
|
|
|
profileCache[organizationID] = decidedByID
|
|
}
|
|
}
|
|
|
|
decisions[i] = accessreview.RecordAccessReviewEntryDecisionRequest{
|
|
EntryID: d.AccessReviewEntryID,
|
|
Decision: d.Decision,
|
|
DecisionNote: d.DecisionNote,
|
|
DecidedByID: decidedByID,
|
|
}
|
|
}
|
|
|
|
entries, err := r.accessReview.RecordDecisions(ctx, scope, decisions)
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return nil, gqlutils.NotFound(ctx, err)
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot record access entry decisions: %w", err))
|
|
}
|
|
|
|
accessEntries := make([]*types.AccessReviewEntry, len(entries))
|
|
for i, e := range entries {
|
|
accessEntries[i] = types.NewAccessReviewEntry(e)
|
|
}
|
|
|
|
return &types.RecordAccessReviewEntryDecisionsPayload{
|
|
AccessReviewEntries: accessEntries,
|
|
}, nil
|
|
}
|
|
|
|
// FlagAccessReviewEntry is the resolver for the flagAccessReviewEntry field.
|
|
func (r *mutationResolver) FlagAccessReviewEntry(ctx context.Context, input types.FlagAccessReviewEntryInput) (*types.FlagAccessReviewEntryPayload, error) {
|
|
scope, err := r.authorize(ctx, input.AccessReviewEntryID, accessreview.ActionEntryFlag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entry, err := r.accessReview.FlagEntry(ctx, scope, accessreview.FlagAccessReviewEntryRequest{
|
|
EntryID: input.AccessReviewEntryID,
|
|
Flags: input.Flags,
|
|
FlagReasons: input.FlagReasons,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return nil, gqlutils.NotFound(ctx, err)
|
|
}
|
|
|
|
panic(fmt.Errorf("cannot flag access entry: %w", err))
|
|
}
|
|
|
|
return &types.FlagAccessReviewEntryPayload{
|
|
AccessReviewEntry: types.NewAccessReviewEntry(entry),
|
|
}, nil
|
|
}
|
|
|
|
// AccessReviewCampaign returns schema.AccessReviewCampaignResolver implementation.
|
|
func (r *Resolver) AccessReviewCampaign() schema.AccessReviewCampaignResolver {
|
|
return &accessReviewCampaignResolver{r}
|
|
}
|
|
|
|
// AccessReviewCampaignConnection returns schema.AccessReviewCampaignConnectionResolver implementation.
|
|
func (r *Resolver) AccessReviewCampaignConnection() schema.AccessReviewCampaignConnectionResolver {
|
|
return &accessReviewCampaignConnectionResolver{r}
|
|
}
|
|
|
|
// AccessReviewCampaignSource returns schema.AccessReviewCampaignSourceResolver implementation.
|
|
func (r *Resolver) AccessReviewCampaignSource() schema.AccessReviewCampaignSourceResolver {
|
|
return &accessReviewCampaignSourceResolver{r}
|
|
}
|
|
|
|
// AccessReviewEntry returns schema.AccessReviewEntryResolver implementation.
|
|
func (r *Resolver) AccessReviewEntry() schema.AccessReviewEntryResolver {
|
|
return &accessReviewEntryResolver{r}
|
|
}
|
|
|
|
// AccessReviewEntryConnection returns schema.AccessReviewEntryConnectionResolver implementation.
|
|
func (r *Resolver) AccessReviewEntryConnection() schema.AccessReviewEntryConnectionResolver {
|
|
return &accessReviewEntryConnectionResolver{r}
|
|
}
|
|
|
|
// AccessReviewSource returns schema.AccessReviewSourceResolver implementation.
|
|
func (r *Resolver) AccessReviewSource() schema.AccessReviewSourceResolver {
|
|
return &accessReviewSourceResolver{r}
|
|
}
|
|
|
|
// AccessReviewSourceConnection returns schema.AccessReviewSourceConnectionResolver implementation.
|
|
func (r *Resolver) AccessReviewSourceConnection() schema.AccessReviewSourceConnectionResolver {
|
|
return &accessReviewSourceConnectionResolver{r}
|
|
}
|
|
|
|
type accessReviewCampaignResolver struct{ *Resolver }
|
|
type accessReviewCampaignConnectionResolver struct{ *Resolver }
|
|
type accessReviewCampaignSourceResolver struct{ *Resolver }
|
|
type accessReviewEntryResolver struct{ *Resolver }
|
|
type accessReviewEntryConnectionResolver struct{ *Resolver }
|
|
type accessReviewSourceResolver struct{ *Resolver }
|
|
type accessReviewSourceConnectionResolver struct{ *Resolver }
|