Expose OAuth2 scopes via GraphQL fields

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>
This commit is contained in:
Aurélien Sibiril
2026-04-07 14:53:25 +02:00
parent 56c042b7ed
commit 247cae14fa
4 changed files with 46 additions and 0 deletions

View File

@@ -250,6 +250,7 @@ type Organization implements Node {
): SAMLConfigurationConnection @goField(forceResolver: true) ): SAMLConfigurationConnection @goField(forceResolver: true)
scimConfiguration: SCIMConfiguration @goField(forceResolver: true) scimConfiguration: SCIMConfiguration @goField(forceResolver: true)
googleWorkspaceOAuth2Scopes: [String!]! @goField(forceResolver: true)
auditLogEntries( auditLogEntries(
first: Int first: Int

View File

@@ -18,6 +18,7 @@ import (
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam" "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/mail"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/server/api/authn" "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 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. // 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) { 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 { if err := r.authorize(ctx, obj.ID, iam.ActionAuditLogEntryList); err != nil {

View File

@@ -1908,6 +1908,7 @@ type Organization implements Node {
last: Int last: Int
before: CursorKey before: CursorKey
): SlackConnectionConnection! @goField(forceResolver: true) ): SlackConnectionConnection! @goField(forceResolver: true)
slackOAuth2Scopes: [String!]! @goField(forceResolver: true)
connectors(filter: ConnectorFilter): [Connector!]! @goField(forceResolver: true) connectors(filter: ConnectorFilter): [Connector!]! @goField(forceResolver: true)
connectorProviderInfos: [ConnectorProviderInfo!]! @goField(forceResolver: true) connectorProviderInfos: [ConnectorProviderInfo!]! @goField(forceResolver: true)
@@ -2166,6 +2167,7 @@ type ConnectorProviderInfo {
oauthConfigured: Boolean! oauthConfigured: Boolean!
apiKeySupported: Boolean! apiKeySupported: Boolean!
clientCredentialsSupported: Boolean! clientCredentialsSupported: Boolean!
oauth2Scopes: [String!]!
extraSettings: [ConnectorProviderSettingInfo!]! extraSettings: [ConnectorProviderSettingInfo!]!
} }
@@ -6802,6 +6804,7 @@ type AccessSource implements Node {
needsConfiguration: Boolean! @goField(forceResolver: true) needsConfiguration: Boolean! @goField(forceResolver: true)
connectionStatus: AccessSourceConnectionStatus! @goField(forceResolver: true) connectionStatus: AccessSourceConnectionStatus! @goField(forceResolver: true)
selectedOrganization: String @goField(forceResolver: true) selectedOrganization: String @goField(forceResolver: true)
oauth2Scopes: [String!]! @goField(forceResolver: true)
createdAt: Datetime! createdAt: Datetime!
updatedAt: Datetime! updatedAt: Datetime!

View File

@@ -18,6 +18,7 @@ import (
"github.com/vikstrous/dataloadgen" "github.com/vikstrous/dataloadgen"
"go.gearno.de/kit/log" "go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/accessreview" "go.probo.inc/probo/pkg/accessreview"
"go.probo.inc/probo/pkg/accessreview/drivers"
"go.probo.inc/probo/pkg/connector" "go.probo.inc/probo/pkg/connector"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "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/api/console/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils" "go.probo.inc/probo/pkg/server/gqlutils"
"go.probo.inc/probo/pkg/server/gqlutils/types/cursor" "go.probo.inc/probo/pkg/server/gqlutils/types/cursor"
"go.probo.inc/probo/pkg/slack"
"go.probo.inc/probo/pkg/validator" "go.probo.inc/probo/pkg/validator"
) )
@@ -520,6 +522,30 @@ func (r *accessSourceResolver) SelectedOrganization(ctx context.Context, obj *ty
return nil, nil 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. // Permission is the resolver for the permission field.
func (r *accessSourceResolver) Permission(ctx context.Context, obj *types.AccessSource, action string) (bool, error) { func (r *accessSourceResolver) Permission(ctx context.Context, obj *types.AccessSource, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action) 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 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. // Connectors is the resolver for the connectors field.
func (r *organizationResolver) Connectors(ctx context.Context, obj *types.Organization, filter *types.ConnectorFilter) ([]*types.Connector, error) { 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 { 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 var infos []*types.ConnectorProviderInfo
for _, provider := range coredata.ConnectorProviders() { for _, provider := range coredata.ConnectorProviders() {
_, oauthErr := r.connectorRegistry.Get(string(provider)) _, oauthErr := r.connectorRegistry.Get(string(provider))
scopes := drivers.ProviderOAuth2Scopes(provider)
if scopes == nil {
scopes = []string{}
}
info := &types.ConnectorProviderInfo{ info := &types.ConnectorProviderInfo{
Provider: provider, Provider: provider,
DisplayName: providerDisplayName(provider), DisplayName: providerDisplayName(provider),
OauthConfigured: oauthErr == nil, OauthConfigured: oauthErr == nil,
APIKeySupported: providerSupportsAPIKey(provider), APIKeySupported: providerSupportsAPIKey(provider),
ClientCredentialsSupported: providerSupportsClientCredentials(provider), ClientCredentialsSupported: providerSupportsClientCredentials(provider),
Oauth2Scopes: scopes,
ExtraSettings: providerExtraSettings(provider), ExtraSettings: providerExtraSettings(provider),
} }
infos = append(infos, info) infos = append(infos, info)