From 247cae14fa6cc8c28dae32f90a6e95d3820453d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sibiril?= <81782+aureliensibiril@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:53:25 +0200 Subject: [PATCH] Expose OAuth2 scopes via GraphQL fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add per-context fields so the frontend can read scopes from the type that owns each connection: - ConnectorProviderInfo.oauth2Scopes: access review providers - AccessSource.oauth2Scopes: access review reconnect flow - Organization.slackOAuth2Scopes (console): compliance page Slack - Organization.googleWorkspaceOAuth2Scopes (connect): SCIM bridge Resolvers delegate to the constants declared in each owning Go module. The Google Workspace field lives on Organization, not on SCIMConfiguration, so the Connect button can read it before any SCIM configuration exists. Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com> --- pkg/server/api/connect/v1/schema.graphql | 1 + pkg/server/api/connect/v1/v1_resolver.go | 6 ++++ pkg/server/api/console/v1/schema.graphql | 3 ++ pkg/server/api/console/v1/v1_resolver.go | 36 ++++++++++++++++++++++++ 4 files changed, 46 insertions(+) diff --git a/pkg/server/api/connect/v1/schema.graphql b/pkg/server/api/connect/v1/schema.graphql index 4980573f6..6e4d9d4a0 100644 --- a/pkg/server/api/connect/v1/schema.graphql +++ b/pkg/server/api/connect/v1/schema.graphql @@ -250,6 +250,7 @@ type Organization implements Node { ): SAMLConfigurationConnection @goField(forceResolver: true) scimConfiguration: SCIMConfiguration @goField(forceResolver: true) + googleWorkspaceOAuth2Scopes: [String!]! @goField(forceResolver: true) auditLogEntries( first: Int diff --git a/pkg/server/api/connect/v1/v1_resolver.go b/pkg/server/api/connect/v1/v1_resolver.go index 23d6b6b6e..49457e800 100644 --- a/pkg/server/api/connect/v1/v1_resolver.go +++ b/pkg/server/api/connect/v1/v1_resolver.go @@ -18,6 +18,7 @@ import ( "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/iam" + "go.probo.inc/probo/pkg/iam/scim/bridge/provider/googleworkspace" "go.probo.inc/probo/pkg/mail" "go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/server/api/authn" @@ -1333,6 +1334,11 @@ func (r *organizationResolver) ScimConfiguration(ctx context.Context, obj *types return types.NewSCIMConfiguration(config), nil } +// GoogleWorkspaceOAuth2Scopes is the resolver for the googleWorkspaceOAuth2Scopes field. +func (r *organizationResolver) GoogleWorkspaceOAuth2Scopes(ctx context.Context, obj *types.Organization) ([]string, error) { + return googleworkspace.OAuth2Scopes, nil +} + // AuditLogEntries is the resolver for the auditLogEntries field. func (r *organizationResolver) AuditLogEntries(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AuditLogEntryOrderBy, filter *types.AuditLogEntryFilter) (*types.AuditLogEntryConnection, error) { if err := r.authorize(ctx, obj.ID, iam.ActionAuditLogEntryList); err != nil { diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 45960d734..badc3ce95 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -1908,6 +1908,7 @@ type Organization implements Node { last: Int before: CursorKey ): SlackConnectionConnection! @goField(forceResolver: true) + slackOAuth2Scopes: [String!]! @goField(forceResolver: true) connectors(filter: ConnectorFilter): [Connector!]! @goField(forceResolver: true) connectorProviderInfos: [ConnectorProviderInfo!]! @goField(forceResolver: true) @@ -2166,6 +2167,7 @@ type ConnectorProviderInfo { oauthConfigured: Boolean! apiKeySupported: Boolean! clientCredentialsSupported: Boolean! + oauth2Scopes: [String!]! extraSettings: [ConnectorProviderSettingInfo!]! } @@ -6802,6 +6804,7 @@ type AccessSource implements Node { needsConfiguration: Boolean! @goField(forceResolver: true) connectionStatus: AccessSourceConnectionStatus! @goField(forceResolver: true) selectedOrganization: String @goField(forceResolver: true) + oauth2Scopes: [String!]! @goField(forceResolver: true) createdAt: Datetime! updatedAt: Datetime! diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index fd9d25d06..4c9559be3 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -18,6 +18,7 @@ import ( "github.com/vikstrous/dataloadgen" "go.gearno.de/kit/log" "go.probo.inc/probo/pkg/accessreview" + "go.probo.inc/probo/pkg/accessreview/drivers" "go.probo.inc/probo/pkg/connector" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" @@ -31,6 +32,7 @@ import ( "go.probo.inc/probo/pkg/server/api/console/v1/types" "go.probo.inc/probo/pkg/server/gqlutils" "go.probo.inc/probo/pkg/server/gqlutils/types/cursor" + "go.probo.inc/probo/pkg/slack" "go.probo.inc/probo/pkg/validator" ) @@ -520,6 +522,30 @@ func (r *accessSourceResolver) SelectedOrganization(ctx context.Context, obj *ty return nil, nil } +// Oauth2Scopes is the resolver for the oauth2Scopes field. +func (r *accessSourceResolver) Oauth2Scopes(ctx context.Context, obj *types.AccessSource) ([]string, error) { + if obj.ConnectorID == nil { + return []string{}, nil + } + + prb := r.ProboService(ctx, obj.ID.TenantID()) + + dbConnector, err := prb.Connectors.Get(ctx, *obj.ConnectorID) + if err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return []string{}, nil + } + r.logger.ErrorCtx(ctx, "cannot get connector for oauth2 scopes", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + scopes := drivers.ProviderOAuth2Scopes(dbConnector.Provider) + if scopes == nil { + return []string{}, nil + } + return scopes, nil +} + // Permission is the resolver for the permission field. func (r *accessSourceResolver) Permission(ctx context.Context, obj *types.AccessSource, action string) (bool, error) { return r.Resolver.Permission(ctx, obj, action) @@ -8280,6 +8306,11 @@ func (r *organizationResolver) SlackConnections(ctx context.Context, obj *types. return types.NewSlackConnectionConnection(page), nil } +// SlackOAuth2Scopes is the resolver for the slackOAuth2Scopes field. +func (r *organizationResolver) SlackOAuth2Scopes(ctx context.Context, obj *types.Organization) ([]string, error) { + return slack.OAuth2Scopes, nil +} + // Connectors is the resolver for the connectors field. func (r *organizationResolver) Connectors(ctx context.Context, obj *types.Organization, filter *types.ConnectorFilter) ([]*types.Connector, error) { if err := r.authorize(ctx, obj.ID, probo.ActionConnectorList); err != nil { @@ -8320,12 +8351,17 @@ func (r *organizationResolver) ConnectorProviderInfos(ctx context.Context, obj * var infos []*types.ConnectorProviderInfo for _, provider := range coredata.ConnectorProviders() { _, oauthErr := r.connectorRegistry.Get(string(provider)) + scopes := drivers.ProviderOAuth2Scopes(provider) + if scopes == nil { + scopes = []string{} + } info := &types.ConnectorProviderInfo{ Provider: provider, DisplayName: providerDisplayName(provider), OauthConfigured: oauthErr == nil, APIKeySupported: providerSupportsAPIKey(provider), ClientCredentialsSupported: providerSupportsClientCredentials(provider), + Oauth2Scopes: scopes, ExtraSettings: providerExtraSettings(provider), } infos = append(infos, info)