Add Scaleway, Yousign, Railway and Crisp access-review connectors
Four API-key, single-tenant (Pattern 3) connectors:
- Scaleway: secret key in the X-Auth-Token header plus an Organization ID
setting; GET /iam/v1alpha1/users (owner/member, status, two-factor),
per-connection BuildProbeURL.
- Yousign: Bearer API key; GET /v3/users (admin/owner/member, is_active);
production host with a static probe.
- Railway: Bearer account token; GraphQL me{workspaces{members}} aggregated
and deduplicated across workspaces; custom probe, since Railway returns
HTTP 200 with an errors body on a rejected token.
- Crisp: plugin token as HTTP Basic (identifier:key) plus a Website ID
setting and the X-Crisp-Tier header; GET /v1/website/{id}/operators/list,
custom probe and name resolver.
Scaleway and Crisp carry a required extra setting, so the console add-source
dialog maps organizationId/websiteId onto their scalewayOrganizationId and
crispWebsiteId API-key inputs; without that mapping the value is silently
dropped and the create is rejected.
Cassette-backed driver tests plus unit tests for the cross-workspace
deduplication, the probe contracts and the role/MFA helpers.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
@@ -33,6 +33,7 @@ func NewBuiltinRegistry() *Registry {
|
||||
clickhouseRegistration(),
|
||||
clickupRegistration(),
|
||||
cloudflareRegistration(),
|
||||
crispRegistration(),
|
||||
cursorRegistration(),
|
||||
datadogRegistration(),
|
||||
deepgramRegistration(),
|
||||
@@ -62,8 +63,10 @@ func NewBuiltinRegistry() *Registry {
|
||||
pagerdutyRegistration(),
|
||||
pylonRegistration(),
|
||||
qoveryRegistration(),
|
||||
railwayRegistration(),
|
||||
renderRegistration(),
|
||||
resendRegistration(),
|
||||
scalewayRegistration(),
|
||||
sendgridRegistration(),
|
||||
sentryRegistration(),
|
||||
signozRegistration(),
|
||||
@@ -72,6 +75,7 @@ func NewBuiltinRegistry() *Registry {
|
||||
tailscaleRegistration(),
|
||||
tallyRegistration(),
|
||||
vercelRegistration(),
|
||||
yousignRegistration(),
|
||||
zendeskRegistration(),
|
||||
} {
|
||||
if err := r.Register(reg); err != nil {
|
||||
|
||||
68
pkg/connector/provider/crisp.go
Normal file
68
pkg/connector/provider/crisp.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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 provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/accessreview/drivers"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
)
|
||||
|
||||
func crispRegistration() *Registration {
|
||||
return &Registration{
|
||||
Provider: coredata.ConnectorProviderCrisp,
|
||||
DisplayName: "Crisp",
|
||||
SupportsAPIKey: true,
|
||||
// Crisp authenticates with a plugin token presented as HTTP Basic, the
|
||||
// credential being the verbatim "identifier:key" pair.
|
||||
// APIKeyBasicAuthUserPass base64-encodes it (the empty-password
|
||||
// APIKeyBasicAuth cannot carry the key). A plugin token can serve
|
||||
// several websites, so the reviewed website is captured via
|
||||
// ExtraSettings. Every request also needs the non-auth X-Crisp-Tier
|
||||
// header (set by the driver/probe/name resolver), so the probe is a
|
||||
// custom closure.
|
||||
APIKeyBasicAuthUserPass: true,
|
||||
ExtraSettings: []ExtraSetting{
|
||||
{Key: "websiteId", Label: "Website ID", Required: true},
|
||||
},
|
||||
Probe: probeCrisp,
|
||||
NewDriver: func(_ context.Context, c *http.Client, conn *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
|
||||
s, err := coredata.ConnectorSettings[coredata.CrispConnectorSettings](conn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read crisp connector settings: %w", err)
|
||||
}
|
||||
|
||||
if s.WebsiteID == "" {
|
||||
return nil, fmt.Errorf("cannot create crisp driver: website_id is required")
|
||||
}
|
||||
|
||||
return drivers.NewCrispDriver(c, s.WebsiteID), nil
|
||||
},
|
||||
NewNameResolver: func(ctx context.Context, c *http.Client, conn *coredata.Connector, logger *log.Logger) drivers.NameResolver {
|
||||
s, err := coredata.ConnectorSettings[coredata.CrispConnectorSettings](conn)
|
||||
if err != nil {
|
||||
logger.ErrorCtx(ctx, "cannot read crisp connector settings", log.Error(err))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return drivers.NewCrispNameResolver(c, s.WebsiteID)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ const (
|
||||
openRouterMembersProbeURL = "https://openrouter.ai/api/v1/organization/members?limit=1"
|
||||
linearGraphQLEndpoint = "https://api.linear.app/graphql"
|
||||
mondayGraphQLEndpoint = "https://api.monday.com/v2"
|
||||
railwayGraphQLEndpoint = "https://backboard.railway.com/graphql/v2"
|
||||
posthogOrganizationPath = "/api/organizations/@current/"
|
||||
posthogUSBaseURL = "https://us.posthog.com"
|
||||
posthogEUBaseURL = "https://eu.posthog.com"
|
||||
@@ -233,6 +234,29 @@ func buildNeonProbeURL(conn *coredata.Connector) (string, error) {
|
||||
return endpoint + "?" + q.Encode(), nil
|
||||
}
|
||||
|
||||
func buildScalewayProbeURL(conn *coredata.Connector) (string, error) {
|
||||
s, err := coredata.ConnectorSettings[coredata.ScalewayConnectorSettings](conn)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot read scaleway connector settings: %w", err)
|
||||
}
|
||||
|
||||
if s.OrganizationID == "" {
|
||||
return "", fmt.Errorf("missing scaleway organization_id")
|
||||
}
|
||||
|
||||
endpoint := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.scaleway.com",
|
||||
Path: "/iam/v1alpha1/users",
|
||||
RawQuery: url.Values{
|
||||
"organization_id": {s.OrganizationID},
|
||||
"page_size": {"1"},
|
||||
}.Encode(),
|
||||
}
|
||||
|
||||
return endpoint.String(), nil
|
||||
}
|
||||
|
||||
func buildRenderProbeURL(conn *coredata.Connector) (string, error) {
|
||||
s, err := coredata.ConnectorSettings[coredata.RenderConnectorSettings](conn)
|
||||
if err != nil {
|
||||
@@ -404,6 +428,99 @@ func probeMonday(
|
||||
)
|
||||
}
|
||||
|
||||
// probeRailway verifies a Railway account token. Railway returns HTTP 200 with
|
||||
// a populated errors array (and data.me null) for a rejected token rather than
|
||||
// 401/403, so the generic probe would falsely pass — this closure inspects the
|
||||
// response body instead.
|
||||
func probeRailway(
|
||||
ctx context.Context,
|
||||
httpClient *http.Client,
|
||||
_ *coredata.Connector,
|
||||
) error {
|
||||
body, err := json.Marshal(map[string]string{"query": "query { me { id } }"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot marshal railway probe request: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, railwayGraphQLEndpoint, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create railway probe request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("railway probe request failed: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
|
||||
return fmt.Errorf("credential rejected: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var parsed struct {
|
||||
Data struct {
|
||||
Me *struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"me"`
|
||||
} `json:"data"`
|
||||
Errors []json.RawMessage `json:"errors"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
|
||||
return fmt.Errorf("cannot decode railway probe response: %w", err)
|
||||
}
|
||||
|
||||
if len(parsed.Errors) > 0 || parsed.Data.Me == nil {
|
||||
return fmt.Errorf("credential rejected: railway returned no authenticated account")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// probeCrisp verifies a Crisp plugin token against the configured website.
|
||||
// Every Crisp request needs the non-auth X-Crisp-Tier header, which the default
|
||||
// probeGET does not set, so this closure builds the request itself; the Basic
|
||||
// credential is attached by the connection transport. Beyond the usual 401/403,
|
||||
// it treats 404 as a rejection too: a valid token whose website_id is wrong or
|
||||
// unbound returns 404 on operators/list — a permanent misconfiguration that
|
||||
// would otherwise pass the probe and fail every later access review, so it
|
||||
// surfaces at connection time instead.
|
||||
func probeCrisp(
|
||||
ctx context.Context,
|
||||
httpClient *http.Client,
|
||||
conn *coredata.Connector,
|
||||
) error {
|
||||
s, err := coredata.ConnectorSettings[coredata.CrispConnectorSettings](conn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read crisp connector settings: %w", err)
|
||||
}
|
||||
|
||||
if s.WebsiteID == "" {
|
||||
return fmt.Errorf("missing crisp website_id")
|
||||
}
|
||||
|
||||
endpoint, err := url.JoinPath("https://api.crisp.chat/v1", "website", url.PathEscape(s.WebsiteID), "operators", "list")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build crisp probe URL: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create crisp probe request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("X-Crisp-Tier", "plugin")
|
||||
|
||||
return doProbeRequest(httpClient, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func probeAnthropic(
|
||||
ctx context.Context,
|
||||
httpClient *http.Client,
|
||||
|
||||
@@ -16,7 +16,9 @@ package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -112,6 +114,23 @@ func TestBuildPostHogProbeURL(t *testing.T) {
|
||||
assert.Equal(t, "https://us.posthog.com/api/organizations/@current/", probeURL)
|
||||
}
|
||||
|
||||
func TestBuildScalewayProbeURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := &coredata.Connector{Provider: coredata.ConnectorProviderScaleway}
|
||||
require.NoError(t, conn.SetSettings(&coredata.ScalewayConnectorSettings{
|
||||
OrganizationID: "11111111-2222-3333-4444-555555555555",
|
||||
}))
|
||||
|
||||
probeURL, err := buildScalewayProbeURL(conn)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"https://api.scaleway.com/iam/v1alpha1/users?organization_id=11111111-2222-3333-4444-555555555555&page_size=1",
|
||||
probeURL,
|
||||
)
|
||||
}
|
||||
|
||||
func TestProbeOpenRouter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -197,3 +216,101 @@ func TestProbeHeroku(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeRailway(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Railway returns HTTP 200 with a populated errors array (data.me null) for
|
||||
// a rejected token instead of 401/403, so the probe must inspect the body —
|
||||
// the generic 401/403-only contract would falsely accept a dead token.
|
||||
cases := []struct {
|
||||
name string
|
||||
status int
|
||||
body string
|
||||
wantReject bool
|
||||
}{
|
||||
{"valid token", http.StatusOK, `{"data":{"me":{"id":"u-1"}}}`, false},
|
||||
{"rejected token (200 + errors)", http.StatusOK, `{"errors":[{"message":"Not Authorized"}],"data":null}`, true},
|
||||
{"null me", http.StatusOK, `{"data":{"me":null}}`, true},
|
||||
{"unauthorized status", http.StatusUnauthorized, ``, true},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var gotURL, gotContentType string
|
||||
|
||||
client := &http.Client{Transport: probeRoundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
gotURL = r.URL.String()
|
||||
gotContentType = r.Header.Get("Content-Type")
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: tc.status,
|
||||
Body: io.NopCloser(strings.NewReader(tc.body)),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})}
|
||||
|
||||
err := probeRailway(context.Background(), client, &coredata.Connector{Provider: coredata.ConnectorProviderRailway})
|
||||
|
||||
assert.Equal(t, "https://backboard.railway.com/graphql/v2", gotURL)
|
||||
assert.Equal(t, "application/json", gotContentType)
|
||||
|
||||
if tc.wantReject {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeCrisp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// probeCrisp must send the non-auth X-Crisp-Tier header (the generic
|
||||
// probeGET does not) and hit the configured website's operators/list
|
||||
// endpoint; 401/403 mean a rejected credential, and 404 means a valid token
|
||||
// pointed at a wrong/unbound website_id — a permanent misconfiguration that
|
||||
// must be rejected at connect time rather than fail every later review.
|
||||
cases := []struct {
|
||||
name string
|
||||
status int
|
||||
wantReject bool
|
||||
}{
|
||||
{"valid token", http.StatusOK, false},
|
||||
{"revoked token", http.StatusUnauthorized, true},
|
||||
{"forbidden token", http.StatusForbidden, true},
|
||||
{"wrong or unbound website (404)", http.StatusNotFound, true},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := &coredata.Connector{Provider: coredata.ConnectorProviderCrisp}
|
||||
require.NoError(t, conn.SetSettings(&coredata.CrispConnectorSettings{WebsiteID: "abc-123"}))
|
||||
|
||||
var gotURL, gotTier string
|
||||
|
||||
client := &http.Client{Transport: probeRoundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
gotURL = r.URL.String()
|
||||
gotTier = r.Header.Get("X-Crisp-Tier")
|
||||
|
||||
return &http.Response{StatusCode: tc.status, Body: http.NoBody, Header: make(http.Header)}, nil
|
||||
})}
|
||||
|
||||
err := probeCrisp(context.Background(), client, conn)
|
||||
|
||||
assert.Equal(t, "https://api.crisp.chat/v1/website/abc-123/operators/list", gotURL)
|
||||
assert.Equal(t, "plugin", gotTier)
|
||||
|
||||
if tc.wantReject {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
44
pkg/connector/provider/railway.go
Normal file
44
pkg/connector/provider/railway.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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 provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/accessreview/drivers"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
)
|
||||
|
||||
func railwayRegistration() *Registration {
|
||||
return &Registration{
|
||||
Provider: coredata.ConnectorProviderRailway,
|
||||
DisplayName: "Railway",
|
||||
SupportsAPIKey: true,
|
||||
// Railway authenticates with an account API token as Authorization:
|
||||
// Bearer. A single GraphQL call resolves the account's workspaces and
|
||||
// their members, so there is nothing to pick (Pattern 3). Railway
|
||||
// returns HTTP 200 with an errors body for a rejected token, so the
|
||||
// probe must inspect the body — hence a custom Probe.
|
||||
Probe: probeRailway,
|
||||
NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
|
||||
return drivers.NewRailwayDriver(c), nil
|
||||
},
|
||||
NewNameResolver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) drivers.NameResolver {
|
||||
return drivers.NewRailwayNameResolver(c)
|
||||
},
|
||||
}
|
||||
}
|
||||
59
pkg/connector/provider/scaleway.go
Normal file
59
pkg/connector/provider/scaleway.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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 provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/accessreview/drivers"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
)
|
||||
|
||||
func scalewayRegistration() *Registration {
|
||||
return &Registration{
|
||||
Provider: coredata.ConnectorProviderScaleway,
|
||||
DisplayName: "Scaleway",
|
||||
SupportsAPIKey: true,
|
||||
// Scaleway authenticates with the secret key in the X-Auth-Token header
|
||||
// rather than Authorization: Bearer. APIKeyHeader makes the
|
||||
// APIKeyConnection send that header and omit Authorization. The key is
|
||||
// bound to one Organization, but GET /iam/v1alpha1/users requires the
|
||||
// organization_id explicitly, so it is captured via ExtraSettings rather
|
||||
// than discovered — hence no picker and a BuildProbeURL.
|
||||
APIKeyHeader: "X-Auth-Token",
|
||||
ExtraSettings: []ExtraSetting{
|
||||
{Key: "organizationId", Label: "Organization ID", Required: true},
|
||||
},
|
||||
BuildProbeURL: buildScalewayProbeURL,
|
||||
// No NewNameResolver: Scaleway exposes no read-only endpoint that maps
|
||||
// an Organization UUID to its display name, so the source keeps its
|
||||
// generic name.
|
||||
NewDriver: func(_ context.Context, c *http.Client, conn *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
|
||||
s, err := coredata.ConnectorSettings[coredata.ScalewayConnectorSettings](conn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read scaleway connector settings: %w", err)
|
||||
}
|
||||
|
||||
if s.OrganizationID == "" {
|
||||
return nil, fmt.Errorf("cannot create scaleway driver: organization_id is required")
|
||||
}
|
||||
|
||||
return drivers.NewScalewayDriver(c, s.OrganizationID), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
48
pkg/connector/provider/yousign.go
Normal file
48
pkg/connector/provider/yousign.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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 provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/accessreview/drivers"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
)
|
||||
|
||||
func yousignRegistration() *Registration {
|
||||
return &Registration{
|
||||
Provider: coredata.ConnectorProviderYousign,
|
||||
DisplayName: "Yousign",
|
||||
SupportsAPIKey: true,
|
||||
// Yousign authenticates with an API key as Authorization: Bearer. The
|
||||
// key is bound to one organization, so GET /v3/users returns everyone
|
||||
// with nothing to pick (Pattern 3). The connector targets Yousign
|
||||
// production; the sandbox runs on a separate host and is not a reviewed
|
||||
// environment.
|
||||
//
|
||||
// ProbeURL lets the connection-status check confirm the key with a
|
||||
// lightweight GET; the transport attaches the Bearer credential and a
|
||||
// dead key returns 401/403.
|
||||
//
|
||||
// No NewNameResolver: Yousign v3 exposes no organization-name endpoint,
|
||||
// so the source keeps its generic name.
|
||||
ProbeURL: "https://api.yousign.app/v3/users?limit=1",
|
||||
NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
|
||||
return drivers.NewYousignDriver(c), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user