|
1 | 1 | /** |
2 | 2 | * @vitest-environment node |
3 | 3 | */ |
4 | | -import { describe, expect, it } from 'vitest' |
| 4 | +import { flattenMockConditions } from '@sim/testing' |
| 5 | +import { encryptionMock, encryptionMockFns } from '@sim/testing/mocks/encryption.mock' |
| 6 | +import { beforeEach, describe, expect, it, vi } from 'vitest' |
| 7 | + |
| 8 | +vi.mock('@/lib/core/security/encryption', () => encryptionMock) |
| 9 | + |
5 | 10 | import { |
| 11 | + assertWorkloadIdentitySubjectUnclaimed, |
6 | 12 | bindWorkloadIdentitySubject, |
7 | 13 | issueWorkloadIdentitySubject, |
8 | 14 | verifyWorkloadIdentitySubject, |
9 | 15 | WorkloadIdentitySubjectError, |
10 | 16 | } from '@/lib/credentials/workload-identity/subject' |
| 17 | +import type { DbTransaction } from '@/lib/db/types' |
11 | 18 |
|
12 | 19 | const owner = { workspaceId: 'ws-1' } |
13 | 20 | const now = Date.UTC(2026, 10, 6) |
@@ -170,3 +177,112 @@ describe('binding a subject to a saved credential', () => { |
170 | 177 | ).toThrow(WorkloadIdentitySubjectError) |
171 | 178 | }) |
172 | 179 | }) |
| 180 | + |
| 181 | +describe('keyless subject uniqueness across providers', () => { |
| 182 | + const SUBJECT = 'sim:workspace:ws-1:credential:abcdef0123456789' |
| 183 | + |
| 184 | + interface StoredCredential { |
| 185 | + id: string |
| 186 | + providerId: string |
| 187 | + secret: object |
| 188 | + } |
| 189 | + |
| 190 | + /** |
| 191 | + * A transaction whose credential query returns only the rows its provider |
| 192 | + * filter admits, so a provider missing from the scan is never checked. |
| 193 | + */ |
| 194 | + function transactionWith(rows: StoredCredential[]): DbTransaction { |
| 195 | + encryptionMockFns.mockDecryptSecret.mockImplementation(async (encrypted: string) => ({ |
| 196 | + decrypted: JSON.stringify(rows.find((row) => row.id === encrypted)?.secret), |
| 197 | + })) |
| 198 | + const tx = { |
| 199 | + execute: async () => [], |
| 200 | + select: () => ({ |
| 201 | + from: () => ({ |
| 202 | + where: async (condition: unknown) => { |
| 203 | + const providerFilter = flattenMockConditions(condition).find( |
| 204 | + (node) => node.type === 'inArray' |
| 205 | + ) |
| 206 | + const admitted = Array.isArray(providerFilter?.values) ? providerFilter.values : [] |
| 207 | + return rows |
| 208 | + .filter((row) => admitted.includes(row.providerId)) |
| 209 | + .map((row) => ({ |
| 210 | + id: row.id, |
| 211 | + displayName: row.id, |
| 212 | + encryptedServiceAccountKey: row.id, |
| 213 | + })) |
| 214 | + }, |
| 215 | + }), |
| 216 | + }), |
| 217 | + } |
| 218 | + return tx as unknown as DbTransaction |
| 219 | + } |
| 220 | + |
| 221 | + function claim(rows: StoredCredential[]) { |
| 222 | + return assertWorkloadIdentitySubjectUnclaimed(transactionWith(rows), { |
| 223 | + subject: SUBJECT, |
| 224 | + scope: { kind: 'workspace', workspaceId: 'ws-1' }, |
| 225 | + }) |
| 226 | + } |
| 227 | + |
| 228 | + beforeEach(() => { |
| 229 | + encryptionMockFns.mockDecryptSecret.mockReset() |
| 230 | + }) |
| 231 | + |
| 232 | + it.each([ |
| 233 | + { |
| 234 | + providerId: 'snowflake-service-account', |
| 235 | + secret: { |
| 236 | + type: 'snowflake_workload_identity_v1', |
| 237 | + config: { |
| 238 | + authType: 'workload_identity', |
| 239 | + host: 'org-acct.snowflakecomputing.com', |
| 240 | + subject: SUBJECT, |
| 241 | + }, |
| 242 | + }, |
| 243 | + }, |
| 244 | + { |
| 245 | + providerId: 'microsoft-app-service-account', |
| 246 | + secret: { |
| 247 | + type: 'microsoft_app_credential_v1', |
| 248 | + config: { authType: 'federated', tenantId: 't', clientId: 'c', subject: SUBJECT }, |
| 249 | + }, |
| 250 | + }, |
| 251 | + { |
| 252 | + providerId: 'google-service-account', |
| 253 | + secret: { |
| 254 | + type: 'google_workload_identity_v1', |
| 255 | + config: { projectNumber: '1', poolId: 'pool', providerId: 'prov', subject: SUBJECT }, |
| 256 | + }, |
| 257 | + }, |
| 258 | + ])( |
| 259 | + 'refuses a subject a live $providerId credential already holds', |
| 260 | + async ({ providerId, secret }) => { |
| 261 | + await expect(claim([{ id: 'held', providerId, secret }])).rejects.toThrow( |
| 262 | + /Another credential already uses this trust setup/ |
| 263 | + ) |
| 264 | + } |
| 265 | + ) |
| 266 | + |
| 267 | + it('is not blocked by key-based Google and Snowflake credentials, which hold no subject', async () => { |
| 268 | + await expect( |
| 269 | + claim([ |
| 270 | + { |
| 271 | + id: 'json-key', |
| 272 | + providerId: 'google-service-account', |
| 273 | + secret: { type: 'service_account', client_email: 'sa@p.iam.gserviceaccount.com' }, |
| 274 | + }, |
| 275 | + { |
| 276 | + id: 'pat', |
| 277 | + providerId: 'snowflake-service-account', |
| 278 | + secret: { type: 'token_service_account', apiToken: 'pat', domain: 'x' }, |
| 279 | + }, |
| 280 | + { |
| 281 | + id: 'client-secret', |
| 282 | + providerId: 'microsoft-app-service-account', |
| 283 | + secret: { type: 'microsoft_app_credential_v1', config: { authType: 'client_secret' } }, |
| 284 | + }, |
| 285 | + ]) |
| 286 | + ).resolves.toBeUndefined() |
| 287 | + }) |
| 288 | +}) |
0 commit comments