@contember/engine-tenant-api 2.2.0-alpha.7 → 2.2.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/development/src/TenantContainer.cjs +5 -2
- package/dist/development/src/TenantContainer.cjs.map +1 -1
- package/dist/development/src/TenantContainer.js +5 -2
- package/dist/development/src/TenantContainer.js.map +1 -1
- package/dist/development/src/model/commands/apiKey/ProlongApiKeyCommand.cjs +5 -8
- package/dist/development/src/model/commands/apiKey/ProlongApiKeyCommand.cjs.map +1 -1
- package/dist/development/src/model/commands/apiKey/ProlongApiKeyCommand.js +5 -8
- package/dist/development/src/model/commands/apiKey/ProlongApiKeyCommand.js.map +1 -1
- package/dist/development/src/model/commands/authLog/CreateAuthLogEntryCommand.cjs.map +1 -1
- package/dist/development/src/model/commands/authLog/CreateAuthLogEntryCommand.js.map +1 -1
- package/dist/development/src/model/service/IDPSignInManager.cjs +23 -15
- package/dist/development/src/model/service/IDPSignInManager.cjs.map +1 -1
- package/dist/development/src/model/service/IDPSignInManager.js +23 -15
- package/dist/development/src/model/service/IDPSignInManager.js.map +1 -1
- package/dist/development/src/model/service/apiKey/ApiKeyManager.cjs +16 -3
- package/dist/development/src/model/service/apiKey/ApiKeyManager.cjs.map +1 -1
- package/dist/development/src/model/service/apiKey/ApiKeyManager.js +16 -3
- package/dist/development/src/model/service/apiKey/ApiKeyManager.js.map +1 -1
- package/dist/development/src/model/service/idp/IdpSessionRevalidator.cjs +42 -31
- package/dist/development/src/model/service/idp/IdpSessionRevalidator.cjs.map +1 -1
- package/dist/development/src/model/service/idp/IdpSessionRevalidator.js +42 -31
- package/dist/development/src/model/service/idp/IdpSessionRevalidator.js.map +1 -1
- package/dist/production/src/TenantContainer.cjs +5 -2
- package/dist/production/src/TenantContainer.cjs.map +1 -1
- package/dist/production/src/TenantContainer.js +5 -2
- package/dist/production/src/TenantContainer.js.map +1 -1
- package/dist/production/src/model/commands/apiKey/ProlongApiKeyCommand.cjs +5 -8
- package/dist/production/src/model/commands/apiKey/ProlongApiKeyCommand.cjs.map +1 -1
- package/dist/production/src/model/commands/apiKey/ProlongApiKeyCommand.js +5 -8
- package/dist/production/src/model/commands/apiKey/ProlongApiKeyCommand.js.map +1 -1
- package/dist/production/src/model/commands/authLog/CreateAuthLogEntryCommand.cjs.map +1 -1
- package/dist/production/src/model/commands/authLog/CreateAuthLogEntryCommand.js.map +1 -1
- package/dist/production/src/model/service/IDPSignInManager.cjs +23 -15
- package/dist/production/src/model/service/IDPSignInManager.cjs.map +1 -1
- package/dist/production/src/model/service/IDPSignInManager.js +23 -15
- package/dist/production/src/model/service/IDPSignInManager.js.map +1 -1
- package/dist/production/src/model/service/apiKey/ApiKeyManager.cjs +16 -3
- package/dist/production/src/model/service/apiKey/ApiKeyManager.cjs.map +1 -1
- package/dist/production/src/model/service/apiKey/ApiKeyManager.js +16 -3
- package/dist/production/src/model/service/apiKey/ApiKeyManager.js.map +1 -1
- package/dist/production/src/model/service/idp/IdpSessionRevalidator.cjs +42 -31
- package/dist/production/src/model/service/idp/IdpSessionRevalidator.cjs.map +1 -1
- package/dist/production/src/model/service/idp/IdpSessionRevalidator.js +42 -31
- package/dist/production/src/model/service/idp/IdpSessionRevalidator.js.map +1 -1
- package/dist/tests/cases/integration/mocked/sql/sqlTransaction.d.ts +5 -0
- package/dist/tests/cases/integration/mocked/sql/sqlTransaction.d.ts.map +1 -1
- package/dist/tests/cases/unit/apiKey/verifyAndProlongReplica.test.d.ts +2 -0
- package/dist/tests/cases/unit/apiKey/verifyAndProlongReplica.test.d.ts.map +1 -0
- package/dist/tests/src/IdPMock.d.ts.map +1 -1
- package/dist/tests/src/testTenant.d.ts.map +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/dist/types/TenantContainer.d.ts +2 -0
- package/dist/types/TenantContainer.d.ts.map +1 -1
- package/dist/types/model/commands/apiKey/ProlongApiKeyCommand.d.ts +5 -3
- package/dist/types/model/commands/apiKey/ProlongApiKeyCommand.d.ts.map +1 -1
- package/dist/types/model/commands/authLog/CreateAuthLogEntryCommand.d.ts +1 -0
- package/dist/types/model/commands/authLog/CreateAuthLogEntryCommand.d.ts.map +1 -1
- package/dist/types/model/service/IDPSignInManager.d.ts +3 -1
- package/dist/types/model/service/IDPSignInManager.d.ts.map +1 -1
- package/dist/types/model/service/apiKey/ApiKeyManager.d.ts +1 -0
- package/dist/types/model/service/apiKey/ApiKeyManager.d.ts.map +1 -1
- package/dist/types/model/service/idp/IdpSessionRevalidator.d.ts +3 -1
- package/dist/types/model/service/idp/IdpSessionRevalidator.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -12
- package/src/TenantContainer.ts +7 -2
- package/src/model/commands/apiKey/ProlongApiKeyCommand.ts +10 -11
- package/src/model/commands/authLog/CreateAuthLogEntryCommand.ts +1 -0
- package/src/model/service/IDPSignInManager.ts +26 -14
- package/src/model/service/apiKey/ApiKeyManager.ts +24 -3
- package/src/model/service/idp/IdpSessionRevalidator.ts +43 -28
- package/tests/cases/integration/mocked/signInIdpMutation.test.ts +100 -1
- package/tests/cases/integration/mocked/sql/sqlTransaction.ts +16 -0
- package/tests/cases/unit/apiKey/prolongApiKeyCommand.test.ts +19 -9
- package/tests/cases/unit/apiKey/unpersistedApiKeyManager.test.ts +2 -3
- package/tests/cases/unit/apiKey/verifyAndProlongReplica.test.ts +170 -0
- package/tests/cases/unit/idp/idpSessionRevalidator.test.ts +37 -4
- package/tests/src/IdPMock.ts +2 -0
- package/tests/src/testTenant.ts +1 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
ApiKey,
|
|
4
|
+
ApiKeyManager,
|
|
5
|
+
ApiKeyService,
|
|
6
|
+
AuthLogService,
|
|
7
|
+
AuthPolicyResolver,
|
|
8
|
+
computeTokenHash,
|
|
9
|
+
DatabaseContext,
|
|
10
|
+
IDPClaimSyncService,
|
|
11
|
+
IDPHandlerRegistry,
|
|
12
|
+
IdpSessionRevalidator,
|
|
13
|
+
Providers,
|
|
14
|
+
VerifyErrorCode,
|
|
15
|
+
} from '../../../../src/index.js'
|
|
16
|
+
import { Connection } from '@contember/database'
|
|
17
|
+
import { createConnectionMock, ExpectedQuery } from '@contember/database-tester'
|
|
18
|
+
import { createLogger, TestLoggerHandler } from '@contember/logger'
|
|
19
|
+
import { emptySchema } from '@contember/schema-utils'
|
|
20
|
+
|
|
21
|
+
const TOKEN = '0000000000000000000000000000000000000000'
|
|
22
|
+
const NOW = new Date('2026-05-21T12:00:00Z')
|
|
23
|
+
|
|
24
|
+
const providers: Providers = {
|
|
25
|
+
bcrypt: () => Promise.resolve('x'),
|
|
26
|
+
bcryptCompare: () => Promise.resolve(true),
|
|
27
|
+
now: () => NOW,
|
|
28
|
+
randomBytes: () => Promise.resolve(Buffer.alloc(0)),
|
|
29
|
+
uuid: () => '00000000-0000-0000-0000-000000000000',
|
|
30
|
+
decrypt: () => {
|
|
31
|
+
throw new Error('not supported')
|
|
32
|
+
},
|
|
33
|
+
encrypt: () => {
|
|
34
|
+
throw new Error('not supported')
|
|
35
|
+
},
|
|
36
|
+
encryptionEnabled: false,
|
|
37
|
+
hash: () => Buffer.alloc(0),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const apiKeyRow = {
|
|
41
|
+
id: 'api-key-id',
|
|
42
|
+
type: ApiKey.Type.SESSION,
|
|
43
|
+
identity_id: 'identity-id',
|
|
44
|
+
disabled_at: null,
|
|
45
|
+
expires_at: new Date(NOW.getTime() + 30 * 60_000),
|
|
46
|
+
roles: ['editor'],
|
|
47
|
+
expiration: 30,
|
|
48
|
+
person_id: 'person-id',
|
|
49
|
+
last_ip: null,
|
|
50
|
+
last_user_agent: null,
|
|
51
|
+
last_used_at: null,
|
|
52
|
+
trust_forwarded_info: false,
|
|
53
|
+
issued_at: null,
|
|
54
|
+
idle_timeout: null,
|
|
55
|
+
max_expires_at: null,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const selectByToken = (rows: (typeof apiKeyRow)[]): ExpectedQuery => ({
|
|
59
|
+
sql:
|
|
60
|
+
`select "api_key"."id", "api_key"."type", "api_key"."identity_id", "api_key"."disabled_at", "api_key"."expires_at", "identity"."roles", "api_key"."expiration", "person"."id" as "person_id", "api_key"."last_ip", "api_key"."last_user_agent", "api_key"."last_used_at", "api_key"."trust_forwarded_info", "api_key"."issued_at", "api_key"."idle_timeout", "api_key"."max_expires_at" from "tenant"."api_key" inner join "tenant"."identity" as "identity" on "api_key"."identity_id" = "identity"."id" left join "tenant"."person" as "person" on "person"."identity_id" = "identity"."id" where "token_hash" = ?`,
|
|
61
|
+
parameters: [computeTokenHash(TOKEN)],
|
|
62
|
+
response: { rows },
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// The prolong UPDATE runs detached on the primary once a key is accepted.
|
|
66
|
+
const prolongUpdate: ExpectedQuery = {
|
|
67
|
+
sql: `update "tenant"."api_key" set "expires_at" = ? where "id" = ?`,
|
|
68
|
+
parameters: [(val: unknown) => val instanceof Date, 'api-key-id'],
|
|
69
|
+
response: { rowCount: 1 },
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const selectIdpSession = (rows: Record<string, unknown>[]): ExpectedQuery => ({
|
|
73
|
+
sql:
|
|
74
|
+
`select "idp_session"."id", "idp_session"."identity_provider_id", "idp_session"."idp_session_id", "idp_session"."tokens", "idp_session"."tokens_version", "idp_session"."idp_expires_at", "idp_session"."token_obtained_at", "idp_session"."last_validated_at", "idp_session"."created_at", "identity_provider"."type" as "provider_type", "identity_provider"."configuration" as "provider_configuration", "identity_provider"."disabled_at" as "provider_disabled_at" from "tenant"."idp_session" inner join "tenant"."identity_provider" as "identity_provider" on "idp_session"."identity_provider_id" = "identity_provider"."id" where "api_key_id" = ?`,
|
|
75
|
+
parameters: ['api-key-id'],
|
|
76
|
+
response: { rows },
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const disableUpdate: ExpectedQuery = {
|
|
80
|
+
sql: `update "tenant"."api_key" set "disabled_at" = ? where "id" = ?`,
|
|
81
|
+
parameters: [(val: unknown) => val instanceof Date, 'api-key-id'],
|
|
82
|
+
response: { rowCount: 1 },
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const authLogInsert: ExpectedQuery = {
|
|
86
|
+
sql:
|
|
87
|
+
`insert into "tenant"."person_auth_log" ("id", "invoked_by_id", "person_id", "type", "success", "error_code", "identity_provider_id", "metadata", "event_data") values (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
88
|
+
response: { rowCount: 1 },
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const createDbContext = (connection: Connection.ConnectionType) =>
|
|
92
|
+
new DatabaseContext(connection.createClient('tenant', { module: 'tenant' }), providers)
|
|
93
|
+
|
|
94
|
+
const verify = async (primary: Connection.ConnectionType, replica: Connection.ConnectionType, idpSessionRevalidator?: IdpSessionRevalidator) => {
|
|
95
|
+
const manager = new ApiKeyManager(new ApiKeyService(), new AuthPolicyResolver(), new AuthLogService(), undefined, idpSessionRevalidator)
|
|
96
|
+
const response = await manager.verifyAndProlong(createDbContext(primary), createDbContext(replica), TOKEN)
|
|
97
|
+
// Let the setImmediate-scheduled prolong run before the connection mocks are torn down.
|
|
98
|
+
await new Promise(resolve => setImmediate(resolve))
|
|
99
|
+
return response
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
test('a key not yet replicated is found on the primary', async () => {
|
|
103
|
+
const primary = createConnectionMock([selectByToken([apiKeyRow]), prolongUpdate])
|
|
104
|
+
const replica = createConnectionMock([selectByToken([])])
|
|
105
|
+
|
|
106
|
+
const response = await verify(primary, replica)
|
|
107
|
+
expect(response.ok).toBe(true)
|
|
108
|
+
if (response.ok) {
|
|
109
|
+
expect(response.result.apiKeyId).toBe('api-key-id')
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('a key found on the replica does not query the primary', async () => {
|
|
114
|
+
const primary = createConnectionMock([prolongUpdate])
|
|
115
|
+
const replica = createConnectionMock([selectByToken([apiKeyRow])])
|
|
116
|
+
|
|
117
|
+
const response = await verify(primary, replica)
|
|
118
|
+
expect(response.ok).toBe(true)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('a key missing on both the replica and the primary is not found', async () => {
|
|
122
|
+
const primary = createConnectionMock([selectByToken([])])
|
|
123
|
+
const replica = createConnectionMock([selectByToken([])])
|
|
124
|
+
|
|
125
|
+
const response = await verify(primary, replica)
|
|
126
|
+
expect(response.ok).toBe(false)
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
expect(response.error).toBe(VerifyErrorCode.NOT_FOUND)
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('without a replica, a missing key is looked up only once', async () => {
|
|
133
|
+
const connection = createConnectionMock([selectByToken([])])
|
|
134
|
+
|
|
135
|
+
const response = await verify(connection, connection)
|
|
136
|
+
expect(response.ok).toBe(false)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('a key found only on the primary reads its IdP session from the primary', async () => {
|
|
140
|
+
const disabledIdpSession = {
|
|
141
|
+
id: 'idp-session-id',
|
|
142
|
+
identity_provider_id: 'idp-id',
|
|
143
|
+
idp_session_id: null,
|
|
144
|
+
tokens: null,
|
|
145
|
+
tokens_version: null,
|
|
146
|
+
idp_expires_at: null,
|
|
147
|
+
token_obtained_at: NOW,
|
|
148
|
+
last_validated_at: NOW,
|
|
149
|
+
created_at: NOW,
|
|
150
|
+
provider_type: 'oidc',
|
|
151
|
+
provider_configuration: { revalidation: { enabled: true } },
|
|
152
|
+
provider_disabled_at: NOW,
|
|
153
|
+
}
|
|
154
|
+
const primary = createConnectionMock([selectByToken([apiKeyRow]), selectIdpSession([disabledIdpSession]), disableUpdate, authLogInsert])
|
|
155
|
+
const replica = createConnectionMock([selectByToken([]), selectIdpSession([])])
|
|
156
|
+
const logHandler = new TestLoggerHandler()
|
|
157
|
+
const idpSessionRevalidator = new IdpSessionRevalidator(
|
|
158
|
+
new IDPHandlerRegistry(),
|
|
159
|
+
new IDPClaimSyncService({ getSchema: () => Promise.resolve(emptySchema) }),
|
|
160
|
+
createLogger(logHandler),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
const response = await verify(primary, replica, idpSessionRevalidator)
|
|
164
|
+
expect(response.ok).toBe(false)
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
expect(response.error).toBe(VerifyErrorCode.DISABLED)
|
|
167
|
+
}
|
|
168
|
+
// The revocation audit entry is best-effort and only logged on failure.
|
|
169
|
+
expect(logHandler.messages).toEqual([])
|
|
170
|
+
})
|
|
@@ -9,6 +9,7 @@ import { CreateAuthLogEntryCommand } from '../../../../src/model/commands/authLo
|
|
|
9
9
|
import { ProjectBySlugQuery, ProjectMembershipByIdentityQuery } from '../../../../src/model/queries/index.js'
|
|
10
10
|
import { Schema } from '@contember/schema'
|
|
11
11
|
import { emptySchema } from '@contember/schema-utils'
|
|
12
|
+
import { createLogger, Logger, TestLoggerHandler } from '@contember/logger'
|
|
12
13
|
|
|
13
14
|
const NOW = new Date('2026-05-26T12:00:00Z')
|
|
14
15
|
const t = (iso: string) => new Date(`2026-05-26T${iso}Z`)
|
|
@@ -20,8 +21,8 @@ const t = (iso: string) => new Date(`2026-05-26T${iso}Z`)
|
|
|
20
21
|
// happy-path claim tests below grant the plain `editor` role on the existing `demo` project, so they need a
|
|
21
22
|
// resolvable schema that defines that role (otherwise the apply-time backstop would drop the grant).
|
|
22
23
|
const claimSyncSchema: Schema = { ...emptySchema, acl: { roles: { editor: { stages: '*', entities: {}, variables: {} } } } }
|
|
23
|
-
const makeRevalidator = (registry: IDPHandlerRegistry) =>
|
|
24
|
-
new IdpSessionRevalidator(registry, new IDPClaimSyncService({ getSchema: () => Promise.resolve(claimSyncSchema) }))
|
|
24
|
+
const makeRevalidator = (registry: IDPHandlerRegistry, logger: Logger = createLogger(new TestLoggerHandler())) =>
|
|
25
|
+
new IdpSessionRevalidator(registry, new IDPClaimSyncService({ getSchema: () => Promise.resolve(claimSyncSchema) }), logger)
|
|
25
26
|
|
|
26
27
|
const apiKeyRow: any = {
|
|
27
28
|
id: 'api-key-1',
|
|
@@ -48,7 +49,7 @@ const baseRow = (overrides: Partial<IdpSessionRow> = {}): IdpSessionRow => ({
|
|
|
48
49
|
...overrides,
|
|
49
50
|
})
|
|
50
51
|
|
|
51
|
-
const createHarness = (row: IdpSessionRow | null, opts: { claim?: boolean } = {}) => {
|
|
52
|
+
const createHarness = (row: IdpSessionRow | null, opts: { claim?: boolean; failAudit?: boolean } = {}) => {
|
|
52
53
|
const executed: any[] = []
|
|
53
54
|
const commandBus = {
|
|
54
55
|
execute: async (command: any) => {
|
|
@@ -56,6 +57,9 @@ const createHarness = (row: IdpSessionRow | null, opts: { claim?: boolean } = {}
|
|
|
56
57
|
if (command instanceof ClaimIdpRevalidationCommand) {
|
|
57
58
|
return opts.claim ?? true
|
|
58
59
|
}
|
|
60
|
+
if (opts.failAudit && command instanceof CreateAuthLogEntryCommand) {
|
|
61
|
+
throw new Error('audit insert failed')
|
|
62
|
+
}
|
|
59
63
|
if (command instanceof DisableApiKeyCommand) {
|
|
60
64
|
return true
|
|
61
65
|
}
|
|
@@ -155,7 +159,7 @@ describe('IdpSessionRevalidator — lifetime-driven phases', () => {
|
|
|
155
159
|
expect((log as any).data.success).toBe(false)
|
|
156
160
|
expect((log as any).data.errorCode).toBe('invalid_grant')
|
|
157
161
|
// the reason is the action payload → event_data (where the rest of the audit system reads it)
|
|
158
|
-
expect((log as any).data.eventData).toEqual({ reason: 'invalid_grant' })
|
|
162
|
+
expect((log as any).data.eventData).toEqual({ reason: 'invalid_grant', apiKeyId: 'api-key-1' })
|
|
159
163
|
expect((log as any).data.metadata).toBeUndefined()
|
|
160
164
|
})
|
|
161
165
|
|
|
@@ -269,6 +273,8 @@ describe('IdpSessionRevalidator — audit logging', () => {
|
|
|
269
273
|
expect(log).toBeDefined()
|
|
270
274
|
expect((log as any).data.type).toBe('idp_session_revalidated')
|
|
271
275
|
expect((log as any).data.success).toBe(true)
|
|
276
|
+
expect((log as any).data.personTokenId).toBeUndefined()
|
|
277
|
+
expect((log as any).data.eventData).toEqual({ apiKeyId: 'api-key-1' })
|
|
272
278
|
})
|
|
273
279
|
|
|
274
280
|
test('records the originating request IP / user agent on both revoke and revalidated entries', async () => {
|
|
@@ -280,6 +286,8 @@ describe('IdpSessionRevalidator — audit logging', () => {
|
|
|
280
286
|
const revokeLog = revoked.executed.find(c => c instanceof CreateAuthLogEntryCommand) as any
|
|
281
287
|
expect(revokeLog.data.ipAddress).toBe('203.0.113.7')
|
|
282
288
|
expect(revokeLog.data.userAgent).toBe('Mozilla/5.0 test')
|
|
289
|
+
expect(revokeLog.data.personTokenId).toBeUndefined()
|
|
290
|
+
expect(revokeLog.data.eventData).toEqual({ reason: 'invalid_grant', apiKeyId: 'api-key-1' })
|
|
283
291
|
|
|
284
292
|
const rotated = createHarness(baseRow({ session: { tokens: { refresh_token: 'r' }, expiresAt: t('11:30:00') } }))
|
|
285
293
|
await makeRevalidator(
|
|
@@ -304,6 +312,8 @@ describe('IdpSessionRevalidator — audit logging', () => {
|
|
|
304
312
|
expect(log.data.errorCode).toBe('revalidation_error')
|
|
305
313
|
expect(log.data.success).toBe(true) // fail-open marker, not a security failure
|
|
306
314
|
expect(log.data.ipAddress).toBe('203.0.113.7')
|
|
315
|
+
expect(log.data.personTokenId).toBeUndefined()
|
|
316
|
+
expect(log.data.eventData).toEqual({ apiKeyId: 'api-key-1' })
|
|
307
317
|
})
|
|
308
318
|
|
|
309
319
|
test('corrupt stored config (fail open) is audited as idp_session_revalidation_failed / config_invalid', async () => {
|
|
@@ -352,6 +362,27 @@ describe('IdpSessionRevalidator — audit logging', () => {
|
|
|
352
362
|
expect(out).toBe('valid')
|
|
353
363
|
})
|
|
354
364
|
|
|
365
|
+
// person_auth_log.person_token_id references person_token(id), so an api_key id there fails the FK
|
|
366
|
+
// and the insert throws. The key is already disabled at that point: the request must still be
|
|
367
|
+
// answered as revoked, not surface the audit failure (#964).
|
|
368
|
+
for (
|
|
369
|
+
const [name, row] of [
|
|
370
|
+
['blocking revocation', baseRow({ session: { tokens: { refresh_token: 'r' }, expiresAt: t('11:30:00') } })],
|
|
371
|
+
['disabled provider', baseRow({ providerDisabledAt: t('11:30:00') })],
|
|
372
|
+
] as const
|
|
373
|
+
) {
|
|
374
|
+
test(`a failing audit write on ${name} still returns revoked and logs the error`, async () => {
|
|
375
|
+
const h = createHarness(row, { failAudit: true })
|
|
376
|
+
const logs = new TestLoggerHandler()
|
|
377
|
+
const out = await makeRevalidator(registryWith(async () => ({ status: 'revoked', reason: 'invalid_grant' })), createLogger(logs))
|
|
378
|
+
.revalidate(h.dbContext, h.readDbContext, apiKeyRow)
|
|
379
|
+
expect(out).toBe('revoked')
|
|
380
|
+
expect(h.executed.some(c => c instanceof DisableApiKeyCommand)).toBe(true)
|
|
381
|
+
expect(h.executed.some(c => c instanceof CreateAuthLogEntryCommand && (c as any).data.type === 'idp_session_revoked')).toBe(true)
|
|
382
|
+
expect(logs.messages.some(m => m.level.name === 'error' && m.error instanceof Error && m.error.message === 'audit insert failed')).toBe(true)
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
|
|
355
386
|
test('a no-op probe (valid, no rotated session) is NOT logged', async () => {
|
|
356
387
|
// userinfo / introspection return valid without an idpSession; logging every such tick
|
|
357
388
|
// (every minInterval) would flood the audit log, so it must stay silent.
|
|
@@ -461,6 +492,7 @@ describe('IdpSessionRevalidator — A09 claim mapping on refresh', () => {
|
|
|
461
492
|
expect(log).toBeDefined()
|
|
462
493
|
expect(log.data.success).toBe(true)
|
|
463
494
|
expect(log.data.eventData.after.memberships).toEqual([{ project: 'demo', role: 'editor', variables: [] }])
|
|
495
|
+
expect(log.data.personTokenId).toBeUndefined()
|
|
464
496
|
})
|
|
465
497
|
|
|
466
498
|
test('sticky mapping is skipped on refresh (always-only) — no membership write, no audit', async () => {
|
|
@@ -484,6 +516,7 @@ describe('IdpSessionRevalidator — A09 claim mapping on refresh', () => {
|
|
|
484
516
|
const log = h.executed.find(c => c instanceof CreateAuthLogEntryCommand && (c as any).data.type === 'idp_role_mapping_failed') as any
|
|
485
517
|
expect(log).toBeDefined()
|
|
486
518
|
expect(log.data.success).toBe(true)
|
|
519
|
+
expect(log.data.personTokenId).toBeUndefined()
|
|
487
520
|
})
|
|
488
521
|
|
|
489
522
|
test('refresh without fresh claims does not run claim sync', async () => {
|
package/tests/src/IdPMock.ts
CHANGED
|
@@ -17,6 +17,8 @@ export class IdPMock implements IdentityProviderHandler<any> {
|
|
|
17
17
|
// mirroring how real providers spread raw OIDC claims into the IDPResponse — lets tests
|
|
18
18
|
// drive claim-mapping (A09). `claimMapping` itself lives on the IdP config, not here.
|
|
19
19
|
...(configuration.claims ?? {}),
|
|
20
|
+
// Lets tests drive the federated-session branch of sign-in (re-validation state).
|
|
21
|
+
...(configuration.idpSession ? { idpSession: configuration.idpSession } : {}),
|
|
20
22
|
})
|
|
21
23
|
}
|
|
22
24
|
|
package/tests/src/testTenant.ts
CHANGED
|
@@ -143,6 +143,7 @@ export const executeTenantTest = async (test: Test) => {
|
|
|
143
143
|
connection,
|
|
144
144
|
readConnection: connection,
|
|
145
145
|
cryptoProviders: providers,
|
|
146
|
+
logger: createLogger(new JsonStreamLoggerHandler(process.stderr)),
|
|
146
147
|
})
|
|
147
148
|
.replaceService('mailer', () => mailer)
|
|
148
149
|
.setupService('idpRegistry', reg => {
|