|
2 | 2 | * @vitest-environment node |
3 | 3 | */ |
4 | 4 |
|
| 5 | +import { serializePrincipal } from '@sim/auth/principal' |
5 | 6 | import { resetEnvMock } from '@sim/testing' |
6 | | -import { decodeJwt } from 'jose' |
| 7 | +import { decodeJwt, SignJWT } from 'jose' |
7 | 8 | import { afterAll, describe, expect, it, vi } from 'vitest' |
| 9 | +import { env } from '@/lib/core/config/env' |
8 | 10 |
|
9 | 11 | vi.unmock('@/lib/auth/internal') |
10 | 12 |
|
@@ -135,7 +137,32 @@ describe('internal executor delegation claims', () => { |
135 | 137 | }) |
136 | 138 | }) |
137 | 139 |
|
138 | | - it('rejects laundering actorless or external principals into a Sim user subject', async () => { |
| 140 | + it('round-trips an authenticated chat subject without inventing a Sim user', async () => { |
| 141 | + const token = await generateInternalDelegationToken({ |
| 142 | + workflowId: 'workflow-1', |
| 143 | + principal: { |
| 144 | + kind: 'system', |
| 145 | + serviceId: 'chat', |
| 146 | + workspaceId: 'workspace-1', |
| 147 | + workflowId: 'workflow-1', |
| 148 | + subject: { kind: 'authenticated_email', email: 'person@example.com' }, |
| 149 | + }, |
| 150 | + }) |
| 151 | + |
| 152 | + await expect(verifyInternalDelegationToken(token)).resolves.toMatchObject({ |
| 153 | + workflowId: 'workflow-1', |
| 154 | + principal: { |
| 155 | + kind: 'system', |
| 156 | + serviceId: 'chat', |
| 157 | + workspaceId: 'workspace-1', |
| 158 | + workflowId: 'workflow-1', |
| 159 | + subject: { kind: 'authenticated_email', email: 'person@example.com' }, |
| 160 | + }, |
| 161 | + }) |
| 162 | + expect(decodeJwt(token).sub).toBeUndefined() |
| 163 | + }) |
| 164 | + |
| 165 | + it('rejects laundering actorless or non-Sim principals into a Sim user subject', async () => { |
139 | 166 | await expect( |
140 | 167 | generateInternalDelegationToken({ |
141 | 168 | subjectUserId: 'billing-owner', |
@@ -167,7 +194,49 @@ describe('internal executor delegation claims', () => { |
167 | 194 | }, |
168 | 195 | }, |
169 | 196 | }) |
170 | | - ).rejects.toThrow('External workflow subjects cannot be represented as Sim users') |
| 197 | + ).rejects.toThrow('Non-Sim workflow subjects cannot be represented as Sim users') |
| 198 | + |
| 199 | + await expect( |
| 200 | + generateInternalDelegationToken({ |
| 201 | + subjectUserId: 'unrelated-user', |
| 202 | + workflowId: 'workflow-1', |
| 203 | + principal: { |
| 204 | + kind: 'system', |
| 205 | + serviceId: 'chat', |
| 206 | + workspaceId: 'workspace-1', |
| 207 | + workflowId: 'workflow-1', |
| 208 | + subject: { kind: 'authenticated_email', email: 'person@example.com' }, |
| 209 | + }, |
| 210 | + }) |
| 211 | + ).rejects.toThrow('Non-Sim workflow subjects cannot be represented as Sim users') |
| 212 | + }) |
| 213 | + |
| 214 | + it('rejects a signed delegation that pairs a non-Sim principal with a Sim user subject', async () => { |
| 215 | + const issuedAt = Math.floor(Date.now() / 1000) |
| 216 | + const token = await new SignJWT({ |
| 217 | + type: 'internal_delegation', |
| 218 | + serviceId: 'executor', |
| 219 | + workflowId: 'workflow-1', |
| 220 | + principal: serializePrincipal({ |
| 221 | + kind: 'system', |
| 222 | + serviceId: 'chat', |
| 223 | + workspaceId: 'workspace-1', |
| 224 | + workflowId: 'workflow-1', |
| 225 | + subject: { kind: 'authenticated_email', email: 'person@example.com' }, |
| 226 | + }), |
| 227 | + }) |
| 228 | + .setProtectedHeader({ alg: 'HS256' }) |
| 229 | + .setJti('delegation-1') |
| 230 | + .setSubject('unrelated-user') |
| 231 | + .setIssuedAt(issuedAt) |
| 232 | + .setExpirationTime(issuedAt + 5 * 60) |
| 233 | + .setIssuer('sim-internal') |
| 234 | + .setAudience('sim-api') |
| 235 | + .sign(new TextEncoder().encode(env.INTERNAL_API_SECRET)) |
| 236 | + |
| 237 | + await expect(verifyInternalDelegationToken(token)).rejects.toBeInstanceOf( |
| 238 | + InvalidInternalDelegationTokenError |
| 239 | + ) |
171 | 240 | }) |
172 | 241 |
|
173 | 242 | it('derives issued-at and expiry from one timestamp', async () => { |
|
0 commit comments