// Copyright (c) 2026 Probo Inc . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY // AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. package console_v1 import ( "encoding/json" "fmt" "net/url" "go.probo.inc/probo/pkg/accessreview/drivers" "go.probo.inc/probo/pkg/connector" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/server/api/console/v1/types" ) // These helpers live outside connector_resolvers.go because that file is // regenerated by gqlgen, which does not preserve standalone functions. // apiKeyConnectorSettings marshals the provider-specific extra settings // for an API-key connector from the typed gqlgen input into the JSON // blob persisted on coredata.Connector.RawSettings. It returns (nil, // nil) for providers without extra settings. // // Returned errors are surfaced verbatim to the client via // gqlutils.Invalid: they must contain only field names and structural // information, never user-supplied values. func apiKeyConnectorSettings(input types.CreateAPIKeyConnectorInput) (json.RawMessage, error) { switch input.Provider { case coredata.ConnectorProviderTally: if input.TallyOrganizationID == nil || *input.TallyOrganizationID == "" { return nil, fmt.Errorf("cannot create tally connector: tallyOrganizationId is required") } return json.Marshal(&coredata.TallyConnectorSettings{OrganizationID: *input.TallyOrganizationID}) case coredata.ConnectorProviderSentry: if input.SentryOrganizationSlug == nil || *input.SentryOrganizationSlug == "" { return nil, fmt.Errorf("cannot create sentry connector: sentryOrganizationSlug is required") } return json.Marshal(&coredata.SentryConnectorSettings{OrganizationSlug: *input.SentryOrganizationSlug}) case coredata.ConnectorProviderSupabase: if input.SupabaseOrganizationSlug == nil || *input.SupabaseOrganizationSlug == "" { return nil, fmt.Errorf("cannot create supabase connector: supabaseOrganizationSlug is required") } return json.Marshal(&coredata.SupabaseConnectorSettings{OrganizationSlug: *input.SupabaseOrganizationSlug}) case coredata.ConnectorProviderGitHub: if input.GithubOrganization == nil || *input.GithubOrganization == "" { return nil, fmt.Errorf("cannot create github connector: githubOrganization is required") } return json.Marshal(&coredata.GitHubConnectorSettings{Organization: *input.GithubOrganization}) case coredata.ConnectorProviderGrafana: if input.GrafanaBaseURL == nil || *input.GrafanaBaseURL == "" { return nil, fmt.Errorf("cannot create grafana connector: grafanaBaseUrl is required") } u, err := url.Parse(*input.GrafanaBaseURL) if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { return nil, fmt.Errorf("cannot create grafana connector: grafanaBaseUrl must be an http(s) URL") } return json.Marshal(&coredata.GrafanaConnectorSettings{BaseURL: *input.GrafanaBaseURL}) case coredata.ConnectorProviderSigNoz: if input.SignozBaseURL == nil || *input.SignozBaseURL == "" { return nil, fmt.Errorf("cannot create signoz connector: signozBaseUrl is required") } u, err := url.Parse(*input.SignozBaseURL) if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { return nil, fmt.Errorf("cannot create signoz connector: signozBaseUrl must be an http(s) URL") } return json.Marshal(&coredata.SigNozConnectorSettings{BaseURL: *input.SignozBaseURL}) case coredata.ConnectorProviderOnePassword: if input.OnePasswordScimBridgeURL == nil || *input.OnePasswordScimBridgeURL == "" { return nil, fmt.Errorf("cannot create 1password connector: onePasswordScimBridgeURL is required") } u, err := url.Parse(*input.OnePasswordScimBridgeURL) if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { return nil, fmt.Errorf("cannot create 1password connector: onePasswordScimBridgeURL must be an http(s) URL") } return json.Marshal(&coredata.OnePasswordConnectorSettings{SCIMBridgeURL: *input.OnePasswordScimBridgeURL}) case coredata.ConnectorProviderMetabase: if input.MetabaseInstanceURL == nil || *input.MetabaseInstanceURL == "" { return nil, fmt.Errorf("cannot create metabase connector: metabaseInstanceUrl is required") } u, err := url.Parse(*input.MetabaseInstanceURL) if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { return nil, fmt.Errorf("cannot create metabase connector: metabaseInstanceUrl must be an http(s) URL") } return json.Marshal(&coredata.MetabaseConnectorSettings{InstanceURL: *input.MetabaseInstanceURL}) case coredata.ConnectorProviderPostHog: region := "" if input.PosthogRegion != nil { region = *input.PosthogRegion } instanceURL := "" if input.PosthogInstanceURL != nil { instanceURL = *input.PosthogInstanceURL } // Cloud (region) and self-hosted (instance URL) are mutually // exclusive; exactly one identifies the connection's data host. switch { case region != "" && instanceURL != "": return nil, fmt.Errorf("cannot create posthog connector: set either posthogRegion or posthogInstanceUrl, not both") case region != "": baseURL, ok := drivers.PostHogRegionBaseURL(region) if !ok { return nil, fmt.Errorf("cannot create posthog connector: posthogRegion must be US or EU") } return json.Marshal(&coredata.PostHogConnectorSettings{BaseURL: baseURL}) case instanceURL != "": u, err := url.Parse(instanceURL) if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { return nil, fmt.Errorf("cannot create posthog connector: posthogInstanceUrl must be an http(s) URL") } return json.Marshal(&coredata.PostHogConnectorSettings{BaseURL: instanceURL}) default: return nil, fmt.Errorf("cannot create posthog connector: posthogRegion or posthogInstanceUrl is required") } case coredata.ConnectorProviderOkta: if input.OktaDomain == nil || *input.OktaDomain == "" { return nil, fmt.Errorf("cannot create okta connector: oktaDomain is required") } // NormalizeOktaDomain strips scheme/path and validates the host; the // stored value is the bare org domain (e.g. "acme.okta.com"). Use a // static error message — this string is surfaced verbatim to the // client and must never echo the operator-supplied input. domain, err := connector.NormalizeOktaDomain(*input.OktaDomain) if err != nil { return nil, fmt.Errorf("cannot create okta connector: oktaDomain must be a valid Okta domain (e.g. acme.okta.com)") } return json.Marshal(&coredata.OktaConnectorSettings{Domain: domain}) case coredata.ConnectorProviderBetterStack: if input.BetterStackTeamName == nil || *input.BetterStackTeamName == "" { return nil, fmt.Errorf("cannot create better stack connector: betterStackTeamName is required") } return json.Marshal(&coredata.BetterStackConnectorSettings{TeamName: *input.BetterStackTeamName}) case coredata.ConnectorProviderQovery: if input.QoveryOrganizationID == nil || *input.QoveryOrganizationID == "" { return nil, fmt.Errorf("cannot create qovery connector: qoveryOrganizationId is required") } return json.Marshal(&coredata.QoveryConnectorSettings{OrganizationID: *input.QoveryOrganizationID}) case coredata.ConnectorProviderRender: if input.RenderWorkspaceID == nil || *input.RenderWorkspaceID == "" { return nil, fmt.Errorf("cannot create render connector: renderWorkspaceId is required") } return json.Marshal(&coredata.RenderConnectorSettings{OwnerID: *input.RenderWorkspaceID}) case coredata.ConnectorProviderNeon: if input.NeonOrganizationID == nil || *input.NeonOrganizationID == "" { return nil, fmt.Errorf("cannot create neon connector: neonOrganizationId is required") } return json.Marshal(&coredata.NeonConnectorSettings{OrganizationID: *input.NeonOrganizationID}) } return nil, nil } // clientCredentialsConnectorSettings marshals the provider-specific // extra settings for a client-credentials connector. See // apiKeyConnectorSettings for the error contract. func clientCredentialsConnectorSettings(input types.CreateClientCredentialsConnectorInput) (json.RawMessage, error) { switch input.Provider { case coredata.ConnectorProviderOnePassword: if input.OnePasswordAccountID == nil || *input.OnePasswordAccountID == "" || input.OnePasswordRegion == nil || *input.OnePasswordRegion == "" { return nil, fmt.Errorf("cannot create 1password connector: onePasswordAccountId and onePasswordRegion are required") } return json.Marshal(&coredata.OnePasswordUsersAPISettings{ AccountID: *input.OnePasswordAccountID, Region: *input.OnePasswordRegion, }) } return nil, nil }