Promote connector provider infos to a root-level access-review drivers query

Move connectorProviderInfos from Organization to a new root query field
accessReviewDrivers, backed by a deployment-scoped policy so any
authenticated identity can list it without an org-scoped permission check.
Delete the now-unused helper file and update the frontend and e2e tests.

Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
Bryan Frimin
2026-06-09 09:12:43 +02:00
parent 29b72ebc3b
commit 5b79b52e23
8 changed files with 141 additions and 134 deletions

View File

@@ -487,8 +487,9 @@ const (
ActionCookieConsentRecordList = "core:cookie-consent-record:list"
// CommonThirdParty actions (global catalog, no organization scope).
ActionCommonThirdPartyGet = "core:common-third-party:get"
ActionCommonThirdPartyList = "core:common-third-party:list"
ActionCommonThirdPartyGet = "core:common-third-party:get"
ActionCommonThirdPartyList = "core:common-third-party:list"
ActionAccessReviewDriverCatalogList = "core:access-review-driver-catalog:list"
// ElectronicSignature actions (tenant-scoped via the related document
// version signature / trust center access).

View File

@@ -199,6 +199,18 @@ var CommonThirdPartyCatalogPolicy = policy.NewPolicy(
).WithSID("read-common-third-party-catalog"),
).WithDescription("Allows every authenticated user to read the global common third-party catalog")
// AccessReviewDriverCatalogPolicy grants every authenticated identity
// read access to the global access-review driver catalog. The catalog
// is deployment-scoped and has no organization scoping, so the allow
// has no condition.
var AccessReviewDriverCatalogPolicy = policy.NewPolicy(
"probo:access-review-driver-catalog",
"Probo Access Review Driver Catalog",
policy.Allow(
ActionAccessReviewDriverCatalogList,
).WithSID("read-access-review-driver-catalog"),
).WithDescription("Allows every authenticated user to read the global access-review driver catalog")
// EmployeePolicy defines permissions for employee role.
var EmployeePolicy = policy.NewPolicy(
"probo:employee",
@@ -232,5 +244,6 @@ func ProboPolicySet() *iam.PolicySet {
AddRolePolicy("VIEWER", ViewerPolicy).
AddRolePolicy("AUDITOR", AuditorPolicy).
AddRolePolicy("EMPLOYEE", EmployeePolicy).
AddIdentityScopedPolicy(CommonThirdPartyCatalogPolicy)
AddIdentityScopedPolicy(CommonThirdPartyCatalogPolicy).
AddIdentityScopedPolicy(AccessReviewDriverCatalogPolicy)
}

View File

@@ -9,6 +9,8 @@ import (
"context"
"errors"
"fmt"
"slices"
"strings"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/agentrun"
@@ -539,6 +541,73 @@ func (r *queryResolver) CommonThirdParties(ctx context.Context, name string) ([]
return result, nil
}
// AccessReviewDrivers is the resolver for the accessReviewDrivers field.
func (r *queryResolver) AccessReviewDrivers(ctx context.Context) ([]*types.ConnectorProviderInfo, error) {
identity := authn.IdentityFromContext(ctx)
if _, err := r.authorize(ctx, identity.ID, probo.ActionAccessReviewDriverCatalogList); err != nil {
return nil, err
}
registrations := r.providerRegistry.All()
infos := make([]*types.ConnectorProviderInfo, 0, len(registrations))
for _, reg := range registrations {
if reg == nil || reg.NewDriver == nil {
continue
}
provider := reg.Provider
_, oauthErr := r.connectorRegistry.Get(string(provider))
oauthConfigured := oauthErr == nil
apiKeySupported := reg.SupportsAPIKey
clientCredentialsSupported := reg.SupportsClientCredentials
// Skip providers that cannot be connected in this deployment: no
// OAuth client credentials configured and no key-based fallback
// (API key or client credentials) supported.
if !oauthConfigured && !apiKeySupported && !clientCredentialsSupported {
continue
}
scopes := r.providerRegistry.ProviderOAuth2Scopes(provider)
if scopes == nil {
scopes = []string{}
}
extraSettings := make([]*types.ConnectorProviderSettingInfo, 0, len(reg.ExtraSettings))
for _, setting := range reg.ExtraSettings {
extraSettings = append(
extraSettings,
&types.ConnectorProviderSettingInfo{
Key: setting.Key,
Label: setting.Label,
Required: setting.Required,
},
)
}
infos = append(infos, &types.ConnectorProviderInfo{
Provider: provider,
DisplayName: reg.DisplayName,
OauthConfigured: oauthConfigured,
APIKeySupported: apiKeySupported,
ClientCredentialsSupported: clientCredentialsSupported,
Oauth2Scopes: scopes,
ExtraSettings: extraSettings,
})
}
slices.SortFunc(
infos,
func(a, b *types.ConnectorProviderInfo) int {
return strings.Compare(a.DisplayName, b.DisplayName)
},
)
return infos, nil
}
// Mutation returns schema.MutationResolver implementation.
func (r *Resolver) Mutation() schema.MutationResolver { return &mutationResolver{r} }

View File

@@ -26,6 +26,7 @@ type Query {
node(id: ID!): Node!
viewer: Viewer!
commonThirdParties(name: String!): [CommonThirdParty!]!
accessReviewDrivers: [ConnectorProviderInfo!]! @goField(forceResolver: true)
}
type Mutation

View File

@@ -182,7 +182,6 @@ type Organization implements Node {
): SlackConnectionConnection! @goField(forceResolver: true)
slackOAuth2Scopes: [String!]! @goField(forceResolver: true)
connectors(filter: ConnectorFilter): [Connector!]! @goField(forceResolver: true)
connectorProviderInfos: [ConnectorProviderInfo!]! @goField(forceResolver: true)
controls(
first: Int

View File

@@ -540,48 +540,6 @@ func (r *organizationResolver) Connectors(ctx context.Context, obj *types.Organi
return types.NewConnectors(connectors), nil
}
// ConnectorProviderInfos is the resolver for the connectorProviderInfos field.
func (r *organizationResolver) ConnectorProviderInfos(ctx context.Context, obj *types.Organization) ([]*types.ConnectorProviderInfo, error) {
if _, err := r.authorize(ctx, obj.ID, probo.ActionConnectorList); err != nil {
return nil, err
}
var infos []*types.ConnectorProviderInfo
for _, p := range coredata.ConnectorProviders() {
_, oauthErr := r.connectorRegistry.Get(string(p))
oauthConfigured := oauthErr == nil
apiKeySupported := r.providerSupportsAPIKey(p)
clientCredentialsSupported := r.providerSupportsClientCredentials(p)
// Skip providers that cannot be connected in this deployment: no
// OAuth client credentials configured and no key-based fallback
// (API key or client credentials) supported. Surfacing them would
// render dead entries the operator has no way to use.
if !oauthConfigured && !apiKeySupported && !clientCredentialsSupported {
continue
}
scopes := r.providerRegistry.ProviderOAuth2Scopes(p)
if scopes == nil {
scopes = []string{}
}
info := &types.ConnectorProviderInfo{
Provider: p,
DisplayName: r.providerDisplayName(p),
OauthConfigured: oauthConfigured,
APIKeySupported: apiKeySupported,
ClientCredentialsSupported: clientCredentialsSupported,
Oauth2Scopes: scopes,
ExtraSettings: r.providerExtraSettings(p),
}
infos = append(infos, info)
}
return infos, nil
}
// Controls is the resolver for the controls field.
func (r *organizationResolver) Controls(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) (*types.ControlConnection, error) {
scope, err := r.authorize(ctx, obj.ID, probo.ActionControlList)