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:
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user