mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
fix(composio): accept nested connected account auth config
This commit is contained in:
@@ -671,7 +671,11 @@ func (s *Service) verifyAccountOwnership(ctx context.Context, connectedAccountID
|
||||
// config the connect link used. An empty expected value (state missing it,
|
||||
// or a resolver gap) is rejected rather than skipped — skipping is the
|
||||
// fail-open hole that let a cross-toolkit account id be bound here.
|
||||
if expectedAuthConfigID == "" || acct.AuthConfigID != expectedAuthConfigID {
|
||||
accountAuthConfigID := acct.AuthConfigID
|
||||
if accountAuthConfigID == "" {
|
||||
accountAuthConfigID = acct.AuthConfig.ID
|
||||
}
|
||||
if expectedAuthConfigID == "" || accountAuthConfigID != expectedAuthConfigID {
|
||||
return ErrAccountVerification
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -34,11 +34,12 @@ type fakeSDK struct {
|
||||
// ListConnectedAccounts echoes the requested id with acctUserID /
|
||||
// acctAuthConfigID so success-path tests can opt in to a matching account;
|
||||
// acctMissing returns no items, listAccountsErr forces a transport error.
|
||||
acctUserID string
|
||||
acctAuthConfigID string
|
||||
acctMissing bool
|
||||
listAccountsErr error
|
||||
lastListAccounts sdk.ListConnectedAccountsRequest
|
||||
acctUserID string
|
||||
acctAuthConfigID string
|
||||
acctNestedAuthConfigID string
|
||||
acctMissing bool
|
||||
listAccountsErr error
|
||||
lastListAccounts sdk.ListConnectedAccountsRequest
|
||||
// auth-config resolution (BeginConnect / ListToolkits connectable flag).
|
||||
// authConfigs nil => a default single notion→ac_notion ENABLED config so
|
||||
// existing connect tests keep resolving; set explicitly to override.
|
||||
@@ -77,6 +78,7 @@ func (f *fakeSDK) ListConnectedAccounts(_ context.Context, req sdk.ListConnected
|
||||
ID: id,
|
||||
UserID: f.acctUserID,
|
||||
AuthConfigID: f.acctAuthConfigID,
|
||||
AuthConfig: sdk.AuthConfigRef{ID: f.acctNestedAuthConfigID},
|
||||
}}}, nil
|
||||
}
|
||||
|
||||
@@ -405,6 +407,29 @@ func TestCompleteCallback_SuccessAndIdempotent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteCallback_AcceptsNestedAuthConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := newFakeStore()
|
||||
userID := mintUUID(30)
|
||||
// Composio v3.1 returns connected-account auth config under auth_config.id,
|
||||
// not always as a top-level auth_config_id.
|
||||
sdkFake := &fakeSDK{acctUserID: util.UUIDToString(userID), acctNestedAuthConfigID: "ac_notion"}
|
||||
svc := newTestService(t, sdkFake, store)
|
||||
state, _ := signState(testSecret, stateClaims{
|
||||
UserID: util.UUIDToString(userID),
|
||||
ToolkitSlug: "notion",
|
||||
AuthConfigID: "ac_notion",
|
||||
Exp: time.Unix(1_700_000_000, 0).Add(time.Minute).Unix(),
|
||||
})
|
||||
|
||||
if _, err := svc.CompleteCallback(context.Background(), state, "success", "ca_nested"); err != nil {
|
||||
t.Fatalf("CompleteCallback: %v", err)
|
||||
}
|
||||
if len(store.rows) != 1 {
|
||||
t.Fatalf("expected 1 row, got %d", len(store.rows))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteCallback_NonSuccessNoRow(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := newFakeStore()
|
||||
|
||||
@@ -288,6 +288,48 @@ func TestListConnectedAccounts_QueryString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListConnectedAccounts_ParsesNestedAuthConfig(t *testing.T) {
|
||||
c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(t, w, http.StatusOK, map[string]any{
|
||||
"items": []map[string]any{
|
||||
{
|
||||
"id": "ca_nested",
|
||||
"user_id": "u_1",
|
||||
"auth_config": map[string]any{
|
||||
"id": "ac_nested",
|
||||
"auth_scheme": "OAUTH2",
|
||||
"is_composio_managed": true,
|
||||
},
|
||||
"toolkit": map[string]any{"slug": "notion"},
|
||||
"status": "ACTIVE",
|
||||
},
|
||||
{
|
||||
"id": "ca_top_level",
|
||||
"user_id": "u_1",
|
||||
"auth_config_id": "ac_top_level",
|
||||
"auth_config": map[string]any{"id": "ac_nested_ignored"},
|
||||
"toolkit": map[string]any{"slug": "gmail"},
|
||||
"status": "ACTIVE",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
resp, err := c.ListConnectedAccounts(context.Background(), composio.ListConnectedAccountsRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListConnectedAccounts: %v", err)
|
||||
}
|
||||
if got := resp.Items[0].AuthConfig.ID; got != "ac_nested" {
|
||||
t.Errorf("nested auth config id = %q, want ac_nested", got)
|
||||
}
|
||||
if got := resp.Items[0].AuthConfig.AuthScheme; got != "OAUTH2" {
|
||||
t.Errorf("nested auth scheme = %q, want OAUTH2", got)
|
||||
}
|
||||
if got := resp.Items[1].AuthConfigID; got != "ac_top_level" {
|
||||
t.Errorf("top-level auth config id = %q, want ac_top_level", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeConnection_Success(t *testing.T) {
|
||||
c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/connected_accounts/ca_42/revoke" {
|
||||
|
||||
@@ -91,6 +91,7 @@ type ConnectedAccount struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
AuthConfigID string `json:"auth_config_id"`
|
||||
AuthConfig AuthConfigRef `json:"auth_config"`
|
||||
Toolkit Toolkit `json:"toolkit"`
|
||||
Status string `json:"status"`
|
||||
StatusReason string `json:"status_reason,omitempty"`
|
||||
@@ -100,6 +101,15 @@ type ConnectedAccount struct {
|
||||
Extra map[string]any `json:"-"`
|
||||
}
|
||||
|
||||
// AuthConfigRef is the nested auth_config object Composio returns on connected
|
||||
// accounts. Some responses also include the older top-level auth_config_id.
|
||||
type AuthConfigRef struct {
|
||||
ID string `json:"id"`
|
||||
AuthScheme string `json:"auth_scheme,omitempty"`
|
||||
IsComposioManaged bool `json:"is_composio_managed,omitempty"`
|
||||
IsDisabled bool `json:"is_disabled,omitempty"`
|
||||
}
|
||||
|
||||
// ListConnectedAccountsResponse is the typed paginated response.
|
||||
type ListConnectedAccountsResponse struct {
|
||||
Items []ConnectedAccount `json:"items"`
|
||||
|
||||
Reference in New Issue
Block a user