mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-15 23:22:59 +08:00
fix(openai): isolate Codex imports by workspace
This commit is contained in:
@@ -908,14 +908,16 @@ func buildCodexAgentIdentityKeys(accountID string) []string {
|
||||
return []string{"account:" + accountID}
|
||||
}
|
||||
|
||||
// buildCodexStoredIdentityKeys 生成存量账号索引键,保留 user/account 维度,
|
||||
// 让 accessToken-only 账号后续升级为完整 OAuth 时仍能命中并更新原账号。
|
||||
// buildCodexStoredIdentityKeys 生成存量账号索引键,按 workspace/account
|
||||
// 和 user 组合隔离身份,避免同一用户在不同 workspace 的账号误合并。
|
||||
func buildCodexStoredIdentityKeys(accountID, userID, email, accessToken string) []string {
|
||||
keys := make([]string, 0, 3)
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
userID = strings.TrimSpace(userID)
|
||||
accessToken = strings.TrimSpace(accessToken)
|
||||
if userID != "" {
|
||||
if accountID != "" && userID != "" {
|
||||
keys = append(keys, "account_user:"+codexCompositeIdentityKey(accountID, userID))
|
||||
} else if userID != "" {
|
||||
keys = append(keys, "user:"+userID)
|
||||
}
|
||||
if accountID == "" && userID == "" {
|
||||
@@ -932,6 +934,10 @@ func buildCodexStoredIdentityKeys(accountID, userID, email, accessToken string)
|
||||
return keys
|
||||
}
|
||||
|
||||
func codexCompositeIdentityKey(accountID, userID string) string {
|
||||
return fmt.Sprintf("%d:%s%d:%s", len(accountID), accountID, len(userID), userID)
|
||||
}
|
||||
|
||||
func buildCodexAccountIndex(accounts []service.Account) *codexAccountIndex {
|
||||
index := &codexAccountIndex{
|
||||
accountsByKey: map[string][]service.Account{},
|
||||
|
||||
@@ -303,8 +303,8 @@ func TestResolveCodexImportExpiryForNoRefreshTokenUsesEarlierRequestExpiry(t *te
|
||||
|
||||
func TestCodexIdentityKeysPreferStrongIdentifiers(t *testing.T) {
|
||||
keys := buildCodexImportIdentityKeys("acct-1", "user-1", "same@example.com", "token", "refresh")
|
||||
if len(keys) == 0 || keys[0] != "user:user-1" {
|
||||
t.Fatalf("user key should have highest priority when refresh token exists: %v", keys)
|
||||
if len(keys) == 0 || keys[0] != "account_user:6:acct-16:user-1" {
|
||||
t.Fatalf("workspace-scoped user key should have highest priority when refresh token exists: %v", keys)
|
||||
}
|
||||
if keys[len(keys)-1] != "account:acct-1" {
|
||||
t.Fatalf("shared account key should be the last fallback: %v", keys)
|
||||
@@ -465,12 +465,84 @@ func TestCodexAccountIndexKeepsAllCandidatesForSharedAccountKey(t *testing.T) {
|
||||
if got == nil || got.ID != member.ID {
|
||||
t.Fatalf("Find by user key = %v, want member account ID %d", got, member.ID)
|
||||
}
|
||||
if matchedKey != "user:user-2" {
|
||||
t.Fatalf("matched key = %q, want user:user-2", matchedKey)
|
||||
if matchedKey != "account_user:6:team-16:user-2" {
|
||||
t.Fatalf("matched key = %q, want account_user:6:team-16:user-2", matchedKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCodexSessionsSameUserDifferentWorkspacesCreatesSeparateAccounts(t *testing.T) {
|
||||
svc := newCodexImportMemoryAdminService(nil)
|
||||
handler := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
req := CodexSessionImportRequest{SkipDefaultGroupBind: boolPtr(true)}
|
||||
entries := []codexImportEntry{
|
||||
{Index: 1, Value: buildCodexRefreshImportValue(t, "workspace-1", "shared-user", "refresh-1")},
|
||||
{Index: 2, Value: buildCodexRefreshImportValue(t, "workspace-2", "shared-user", "refresh-2")},
|
||||
}
|
||||
|
||||
result, err := handler.importCodexSessions(context.Background(), req, entries)
|
||||
if err != nil {
|
||||
t.Fatalf("importCodexSessions error = %v", err)
|
||||
}
|
||||
if result.Created != 2 || result.Updated != 0 || result.Skipped != 0 || result.Failed != 0 {
|
||||
t.Fatalf("result = %+v, want two created accounts", result)
|
||||
}
|
||||
if len(svc.createdAccounts) != 2 {
|
||||
t.Fatalf("created accounts = %d, want 2", len(svc.createdAccounts))
|
||||
}
|
||||
if got := svc.createdAccounts[0].Credentials["chatgpt_account_id"]; got != "workspace-1" {
|
||||
t.Fatalf("first workspace = %v, want workspace-1", got)
|
||||
}
|
||||
if got := svc.createdAccounts[1].Credentials["chatgpt_account_id"]; got != "workspace-2" {
|
||||
t.Fatalf("second workspace = %v, want workspace-2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexImportIdentityKeySeparatesDelimiterValues(t *testing.T) {
|
||||
left := buildCodexImportIdentityKeys("ab:c", "shared-user", "", "", "refresh")
|
||||
right := buildCodexImportIdentityKeys("ab", "c:shared-user", "", "", "refresh")
|
||||
if left[0] == right[0] {
|
||||
t.Fatalf("composite identity keys collided: %q", left[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCodexSessionsSameWorkspaceAccountUpdatesAndDifferentUserIsolated(t *testing.T) {
|
||||
svc := newCodexImportMemoryAdminService(nil)
|
||||
handler := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
req := CodexSessionImportRequest{SkipDefaultGroupBind: boolPtr(true)}
|
||||
initialEntries := []codexImportEntry{
|
||||
{Index: 1, Value: buildCodexRefreshImportValue(t, "workspace-1", "user-1", "refresh-1")},
|
||||
{Index: 2, Value: buildCodexRefreshImportValue(t, "workspace-1", "user-2", "refresh-2")},
|
||||
}
|
||||
|
||||
initialResult, err := handler.importCodexSessions(context.Background(), req, initialEntries)
|
||||
if err != nil {
|
||||
t.Fatalf("importCodexSessions error = %v", err)
|
||||
}
|
||||
if initialResult.Created != 2 || initialResult.Updated != 0 || initialResult.Skipped != 0 || initialResult.Failed != 0 {
|
||||
t.Fatalf("initial result = %+v, want two created accounts", initialResult)
|
||||
}
|
||||
|
||||
updateResult, err := handler.importCodexSessions(context.Background(), req, []codexImportEntry{
|
||||
{Index: 3, Value: buildCodexRefreshImportValue(t, "workspace-1", "user-1", "refresh-3")},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("importCodexSessions update error = %v", err)
|
||||
}
|
||||
if updateResult.Created != 0 || updateResult.Updated != 1 || updateResult.Skipped != 0 || updateResult.Failed != 0 {
|
||||
t.Fatalf("update result = %+v, want one updated account", updateResult)
|
||||
}
|
||||
if len(svc.createdAccounts) != 2 || len(svc.updatedAccounts) != 1 {
|
||||
t.Fatalf("created = %d, updated = %d, want 2 and 1", len(svc.createdAccounts), len(svc.updatedAccounts))
|
||||
}
|
||||
if svc.updatedAccounts[0].id != 100 {
|
||||
t.Fatalf("updated account ID = %d, want user-1 account ID 100", svc.updatedAccounts[0].id)
|
||||
}
|
||||
if got := svc.updatedAccounts[0].input.Credentials["chatgpt_user_id"]; got != "user-1" {
|
||||
t.Fatalf("updated user = %v, want user-1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexAccountIndexUpsertReplacesSameAccount(t *testing.T) {
|
||||
legacy := service.Account{
|
||||
ID: 40,
|
||||
|
||||
Reference in New Issue
Block a user