Files
probo/pkg/server/api/console/v1/base_resolvers.go
Aurélien Sibiril 37121e7bac Verify Crisp website ownership before connecting
Crisp is a managed (Model B) connector: Probo holds one plugin token
server-side and each connection carries only a Website ID. Nothing
stops one organization from entering another organization's Website
ID, so prove control of the website before creating the connection.

Probo derives a per-(organization, website) verification code as an
HMAC over the token secret and exposes it through a new
crispVerificationCode query. The customer pastes it into the Probo
plugin's per-website settings; at connect time the resolver reads the
setting back through the managed plugin token and requires a
constant-time match before any row is written. The managed key and
plugin ID come from bootstrap, so the connector stays hidden until the
deployment configures them.

The settings fetch is injected so the create-time gate's branch wiring
is unit-tested (mismatch and not-subscribed reject, internal errors
stay generic, a matching code passes), and the managed-versus-client
key resolution is covered too.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-11 16:50:32 +02:00

658 lines
22 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.93
import (
"context"
"errors"
"slices"
"strings"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/accessreview"
"go.probo.inc/probo/pkg/agentrun"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/server/api/authn"
"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"
)
// Node is the resolver for the node field.
func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error) {
var (
loadNode func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error)
action string
)
switch id.EntityType() {
case coredata.OrganizationEntityType:
action = probo.ActionOrganizationGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
organization, err := r.probo.Organizations.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewOrganization(organization), nil
}
case coredata.ThirdPartyEntityType:
action = probo.ActionThirdPartyGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
thirdParty, err := r.probo.ThirdParties.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewThirdParty(thirdParty), nil
}
case coredata.FrameworkEntityType:
action = probo.ActionFrameworkGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
framework, err := r.probo.Frameworks.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewFramework(framework), nil
}
case coredata.MeasureEntityType:
action = probo.ActionMeasureGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
measure, err := r.probo.Measures.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewMeasure(measure), nil
}
case coredata.TaskEntityType:
action = probo.ActionTaskGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
task, err := r.probo.Tasks.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewTask(task), nil
}
case coredata.EvidenceEntityType:
action = probo.ActionEvidenceList
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
evidence, err := r.probo.Evidences.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewEvidence(evidence), nil
}
case coredata.DocumentEntityType:
action = probo.ActionDocumentGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
document, err := r.probo.Documents.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewDocument(document), nil
}
case coredata.ControlEntityType:
action = probo.ActionControlList
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
control, err := r.probo.Controls.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewControl(control), nil
}
case coredata.ApplicabilityStatementEntityType:
action = probo.ActionApplicabilityStatementGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
applicabilityStatement, err := r.probo.StatementsOfApplicability.GetApplicabilityStatement(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewApplicabilityStatement(applicabilityStatement), nil
}
case coredata.RiskEntityType:
action = probo.ActionRiskGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
risk, err := r.probo.Risks.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewRisk(risk), nil
}
case coredata.RiskAssessmentEntityType:
action = probo.ActionRiskAssessmentGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
ra, err := r.riskManagement.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewRiskAssessment(ra), nil
}
case coredata.RiskAssessmentNodeEntityType:
action = probo.ActionRiskAssessmentNodeGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
n, err := r.riskManagement.GetNode(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewRiskAssessmentNode(n), nil
}
case coredata.RiskAssessmentProcessEntityType:
action = probo.ActionRiskAssessmentProcessGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
p, err := r.riskManagement.GetProcess(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewRiskAssessmentProcess(p), nil
}
case coredata.RiskAssessmentThreatEntityType:
action = probo.ActionRiskAssessmentThreatGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
t, err := r.riskManagement.GetThreat(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewRiskAssessmentThreat(t), nil
}
case coredata.RiskAssessmentScopeEntityType:
action = probo.ActionRiskAssessmentScopeGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
s, err := r.riskManagement.GetScope(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewRiskAssessmentScope(s), nil
}
case coredata.RiskAssessmentBoundaryEntityType:
action = probo.ActionRiskAssessmentBoundaryGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
b, err := r.riskManagement.GetBoundary(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewRiskAssessmentBoundary(b), nil
}
case coredata.RiskAssessmentScenarioEntityType:
action = probo.ActionRiskAssessmentScenarioGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
s, err := r.riskManagement.GetScenario(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewRiskAssessmentScenario(s), nil
}
case coredata.ThirdPartyComplianceReportEntityType:
action = probo.ActionThirdPartyComplianceReportGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
thirdPartyComplianceReport, err := r.probo.ThirdPartyComplianceReports.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewThirdPartyComplianceReport(thirdPartyComplianceReport), nil
}
case coredata.ThirdPartyContactEntityType:
action = probo.ActionThirdPartyContactGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
thirdPartyContact, err := r.probo.ThirdPartyContacts.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewThirdPartyContact(thirdPartyContact), nil
}
case coredata.ThirdPartyServiceEntityType:
action = probo.ActionThirdPartyServiceGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
thirdPartyService, err := r.probo.ThirdPartyServices.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewThirdPartyService(thirdPartyService), nil
}
case coredata.DocumentVersionEntityType:
action = probo.ActionDocumentVersionList
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
documentVersion, err := r.probo.Documents.GetVersion(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewDocumentVersion(documentVersion), nil
}
case coredata.DocumentVersionSignatureEntityType:
action = probo.ActionDocumentVersionSignatureList
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
documentVersionSignature, err := r.probo.Documents.GetVersionSignature(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewDocumentVersionSignature(documentVersionSignature), nil
}
case coredata.AssetEntityType:
action = probo.ActionAssetList
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
asset, err := r.probo.Assets.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewAsset(asset), nil
}
case coredata.DatumEntityType:
action = probo.ActionDatumList
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
datum, err := r.probo.Data.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewDatum(datum), nil
}
case coredata.AuditEntityType:
action = probo.ActionAuditList
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
audit, err := r.probo.Audits.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewAudit(audit), nil
}
case coredata.FindingEntityType:
action = probo.ActionFindingList
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
finding, err := r.probo.Findings.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewFinding(finding), nil
}
case coredata.ObligationEntityType:
action = probo.ActionObligationList
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
obligation, err := r.probo.Obligations.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewObligation(obligation), nil
}
case coredata.ProcessingActivityEntityType:
action = probo.ActionProcessingActivityList
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
processingActivity, err := r.probo.ProcessingActivities.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewProcessingActivity(processingActivity), nil
}
case coredata.DataProtectionImpactAssessmentEntityType:
// TODO: add action
// action = probo.ActionDataProtectionImpactAssessmentGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
dpia, err := r.probo.DataProtectionImpactAssessments.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewDataProtectionImpactAssessment(dpia), nil
}
case coredata.TransferImpactAssessmentEntityType:
// TODO: add action
//action = probo.ActionTransferImpactAssessmentGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
tia, err := r.probo.TransferImpactAssessments.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewTransferImpactAssessment(tia), nil
}
case coredata.TrustCenterEntityType:
action = probo.ActionTrustCenterGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
trustCenter, err := r.probo.TrustCenters.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewTrustCenter(trustCenter), nil
}
case coredata.TrustCenterAccessEntityType:
action = probo.ActionTrustCenterAccessGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
trustCenterAccess, err := r.probo.TrustCenterAccesses.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewTrustCenterAccess(trustCenterAccess), nil
}
case coredata.RightsRequestEntityType:
action = probo.ActionRightsRequestGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
rightsRequest, err := r.probo.RightsRequests.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewRightsRequest(rightsRequest), nil
}
case coredata.StatementOfApplicabilityEntityType:
action = probo.ActionStatementOfApplicabilityGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
statementOfApplicability, err := r.probo.StatementsOfApplicability.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewStatementOfApplicability(statementOfApplicability), nil
}
case coredata.WebhookSubscriptionEntityType:
action = probo.ActionWebhookSubscriptionGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
wc, err := r.probo.WebhookSubscriptions.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewWebhookSubscription(wc), nil
}
case coredata.AgentRunEntityType:
action = agentrun.ActionAgentRunGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
run, err := r.agentRun.Get(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewAgentRun(run), nil
}
case coredata.AccessReviewCampaignEntityType:
action = accessreview.ActionCampaignGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
campaign, err := r.accessReview.GetCampaign(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewAccessReviewCampaign(campaign), nil
}
case coredata.AccessReviewCampaignSourceEntityType:
action = accessreview.ActionCampaignGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
campaignSource, err := r.accessReview.GetCampaignSource(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewAccessReviewCampaignSource(campaignSource), nil
}
case coredata.AccessReviewSourceEntityType:
action = accessreview.ActionSourceGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
source, err := r.accessReview.GetSource(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewAccessReviewSource(source), nil
}
case coredata.AccessReviewEntryEntityType:
action = accessreview.ActionEntryGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
entry, err := r.accessReview.GetEntry(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewAccessReviewEntry(entry), nil
}
case coredata.CookieBannerEntityType:
action = probo.ActionCookieBannerGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
banner, err := r.cookieBanner.GetCookieBanner(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewCookieBanner(banner), nil
}
case coredata.CookieCategoryEntityType:
action = probo.ActionCookieCategoryGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
category, err := r.cookieBanner.GetCookieCategory(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewCookieCategory(category), nil
}
case coredata.CookieConsentRecordEntityType:
action = probo.ActionCookieConsentRecordList
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
record, err := r.cookieBanner.GetCookieConsentRecord(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewCookieConsentRecord(record), nil
}
case coredata.TrackerPatternEntityType:
action = probo.ActionTrackerPatternGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
pattern, err := r.cookieBanner.GetTrackerPattern(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewTrackerPatternNode(pattern), nil
}
case coredata.CookieBannerVersionEntityType:
action = probo.ActionCookieBannerVersionGet
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
version, err := r.cookieBanner.GetCookieBannerVersion(ctx, scope, id)
if err != nil {
return nil, err
}
return &types.CookieBannerVersion{
ID: version.ID,
Version: version.Version,
State: string(version.State),
CreatedAt: version.CreatedAt,
UpdatedAt: version.UpdatedAt,
}, nil
}
default:
}
scope, err := r.authorize(ctx, id, action)
if err != nil {
return nil, err
}
node, err := loadNode(ctx, scope, id)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot load node", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return node, nil
}
// Viewer is the resolver for the viewer field.
func (r *queryResolver) Viewer(ctx context.Context) (*types.Viewer, error) {
identity := authn.IdentityFromContext(ctx)
session := authn.SessionFromContext(ctx)
apiKey := authn.APIKeyFromContext(ctx)
var viewerID gid.GID
if session != nil {
viewerID = session.ID
} else if apiKey != nil {
viewerID = apiKey.ID
} else {
viewerID = identity.ID
}
return &types.Viewer{ID: viewerID}, nil
}
// CommonThirdParties is the resolver for the commonThirdParties field.
func (r *queryResolver) CommonThirdParties(ctx context.Context, name string) ([]*types.CommonThirdParty, error) {
identity := authn.IdentityFromContext(ctx)
if _, err := r.authorize(ctx, identity.ID, probo.ActionCommonThirdPartyList); err != nil {
return nil, err
}
parties, err := r.thirdParty.Search(ctx, name)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot search common third parties", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
result := make([]*types.CommonThirdParty, len(parties))
for i, p := range parties {
result[i] = types.NewCommonThirdParty(p)
}
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, accessreview.ActionDriverCatalogList); 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
// ManagedAPIKey (Model B, e.g. Crisp) providers are connectable only
// once the operator configures the Probo-held key; until then they
// stay hidden, so such a provider ships deactivated.
_, hasManaged := r.providerRegistry.ManagedAPIKey(provider)
apiKeyManaged := reg.ManagedAPIKey && hasManaged
// Skip providers that cannot be connected in this deployment: no
// OAuth client credentials configured and no key-based fallback
// (API key, managed API key, or client credentials) supported.
if !oauthConfigured && !apiKeySupported && !clientCredentialsSupported && !apiKeyManaged {
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,
APIKeyManaged: apiKeyManaged,
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
}
// CrispVerificationCode is the resolver for the crispVerificationCode field. It
// returns the deterministic ownership-verification code the customer must paste
// into the Probo plugin's per-website settings in their Crisp dashboard before
// connecting that website. Authorized against the organization with the same
// action as the create mutation because the code is organization-bound. This is
// a UI-only helper; MCP/CLI/n8n are intentionally not extended.
func (r *queryResolver) CrispVerificationCode(ctx context.Context, organizationID gid.GID, websiteID string) (string, error) {
if _, err := r.authorize(ctx, organizationID, probo.ActionConnectorCreate); err != nil {
return "", err
}
websiteID = strings.TrimSpace(websiteID)
if websiteID == "" {
return "", gqlutils.Invalidf(ctx, "websiteId is required")
}
return computeCrispVerificationCode(r.tokenSecret, organizationID.String(), websiteID), nil
}
// Mutation returns schema.MutationResolver implementation.
func (r *Resolver) Mutation() schema.MutationResolver { return &mutationResolver{r} }
// Query returns schema.QueryResolver implementation.
func (r *Resolver) Query() schema.QueryResolver { return &queryResolver{r} }
type (
mutationResolver struct{ *Resolver }
queryResolver struct{ *Resolver }
)