@things-factory/auth-base 10.1.111 → 10.1.116
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-server/ai-entry/access-assistant.d.ts +8 -0
- package/dist-server/ai-entry/access-assistant.js +9 -1
- package/dist-server/ai-entry/access-assistant.js.map +1 -1
- package/dist-server/service/appliance/appliance-query.js +4 -2
- package/dist-server/service/appliance/appliance-query.js.map +1 -1
- package/dist-server/service/application/application-query.js +2 -1
- package/dist-server/service/application/application-query.js.map +1 -1
- package/dist-server/service/auth-provider/auth-provider-query.js +2 -1
- package/dist-server/service/auth-provider/auth-provider-query.js.map +1 -1
- package/dist-server/service/index.d.ts +2 -1
- package/dist-server/service/index.js +4 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/invitation/invitation-query.js +2 -1
- package/dist-server/service/invitation/invitation-query.js.map +1 -1
- package/dist-server/service/login-history/login-history-query.js +8 -4
- package/dist-server/service/login-history/login-history-query.js.map +1 -1
- package/dist-server/service/partner/partner-query.js +2 -1
- package/dist-server/service/partner/partner-query.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/tests/access-entry-targets-db.test.ts +36 -0
- package/tests/list-inheritance-db.test.ts +149 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/auth-base",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.116",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "dist-client/index.js",
|
|
6
6
|
"things-factory": true,
|
|
@@ -48,5 +48,5 @@
|
|
|
48
48
|
"passport-jwt": "^4.0.0",
|
|
49
49
|
"passport-local": "^1.0.0"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "49fa935cc703be70200eef4d29f473de98f871ce"
|
|
52
52
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { parse } from 'graphql'
|
|
11
11
|
|
|
12
12
|
import { clearEntryTargets, declareEntryTarget } from '../../ai-assistant/server/service/data-entry/entry-target'
|
|
13
|
+
import { applyEntryProposal } from '../../ai-assistant/server/service/data-entry/entry-apply'
|
|
13
14
|
|
|
14
15
|
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
15
16
|
import { loadCompiled } from './compiled'
|
|
@@ -22,6 +23,8 @@ const { Privilege } = loadCompiled('service/privilege/privilege')
|
|
|
22
23
|
const { User } = loadCompiled('service/user/user')
|
|
23
24
|
const access = loadCompiled('controllers/access-request')
|
|
24
25
|
const targets = loadCompiled('ai-entry/access-entry-targets')
|
|
26
|
+
const { judgePermission } = loadCompiled('utils/check-permission')
|
|
27
|
+
const { AccessRequest } = loadCompiled('service/access-request/access-request')
|
|
25
28
|
|
|
26
29
|
const rootFields = (document: string) =>
|
|
27
30
|
((parse(document).definitions[0] as any).selectionSet.selections as any[]).map(one => one.name.value)
|
|
@@ -31,6 +34,10 @@ const row = (values: Record<string, unknown>, index = 0) => ({ index, values, so
|
|
|
31
34
|
describe('the declarations', () => {
|
|
32
35
|
afterEach(() => clearEntryTargets())
|
|
33
36
|
|
|
37
|
+
it('the built public AUTH entry exports the same refusal formatter used by AI entry', () => {
|
|
38
|
+
expect(loadCompiled('index').privilegeRejection).toBe(loadCompiled('service/privilege/privilege-rejection').privilegeRejection)
|
|
39
|
+
})
|
|
40
|
+
|
|
34
41
|
it('are accepted by the assistant as declared', () => {
|
|
35
42
|
for (const target of targets.accessEntryTargets()) expect(() => declareEntryTarget(target)).not.toThrow()
|
|
36
43
|
})
|
|
@@ -110,6 +117,35 @@ describe('the card says what the screen says', () => {
|
|
|
110
117
|
|
|
111
118
|
const state = (user: any) => ({ domain, user })
|
|
112
119
|
|
|
120
|
+
it.each(['GRANT', 'DENY'])('a stored AI decision applies %s through the same controller and schedules the result after commit', async decision => {
|
|
121
|
+
const request = await access.requestAccess({ category: 'warehouse', privilege: 'mutation', reason: 'inbound' }, { state: { ...state(kim), afterCommit: [] } } as any)
|
|
122
|
+
const target = targets.accessDecisionEntryTarget()
|
|
123
|
+
const notify = jest.fn().mockResolvedValue(undefined)
|
|
124
|
+
const afterCommit: any[] = []
|
|
125
|
+
const context = { state: { ...state(park), notify, afterCommit }, t: (key: string) => key }
|
|
126
|
+
const execute = jest.fn(async (document, variables) => {
|
|
127
|
+
expect(document).toBe(target.mutation)
|
|
128
|
+
const decided = await access.decideAccessRequest(variables.decision, context)
|
|
129
|
+
return { data: { decideAccessRequest: decided } }
|
|
130
|
+
})
|
|
131
|
+
const proposal = { key: 'ai-decision', kind: 'entry', target: target.key, rows: [row({ request: { id: request.id }, decision, ...(decision === 'GRANT' ? { role: { id: inbound.id } } : { reason: 'IT only' }) })] }
|
|
132
|
+
const outcome = await applyEntryProposal(proposal.key, {
|
|
133
|
+
messages: [{ id: 'm', proposals: [proposal] }], answered: false, state: context.state,
|
|
134
|
+
passesDoor: async door => {
|
|
135
|
+
expect(door).toBe('Mutation.decideAccessRequest')
|
|
136
|
+
return (await judgePermission({ category: 'user', privilege: 'mutation', owner: true }, park, domain)).permitted
|
|
137
|
+
},
|
|
138
|
+
execute
|
|
139
|
+
}, key => key === target.key ? target : undefined)
|
|
140
|
+
expect(outcome).toMatchObject({ verdict: 'accepted', applied: 1, missed: 0 })
|
|
141
|
+
expect(execute).toHaveBeenCalledTimes(1)
|
|
142
|
+
expect(notify).not.toHaveBeenCalled()
|
|
143
|
+
for (const step of afterCommit) await step.run()
|
|
144
|
+
expect(notify).toHaveBeenCalledWith(expect.objectContaining({ receivers: [kim.id], url: 'my-access-requests', title: `access-request.notify.${decision === 'GRANT' ? 'granted' : 'denied'}.title` }))
|
|
145
|
+
expect((await getRepository(AccessRequest).findOneBy({ id: request.id })).state).toBe(decision === 'GRANT' ? 'GRANTED' : 'DENIED')
|
|
146
|
+
expect(await User.hasPrivilege('mutation', 'warehouse', domain, kim)).toBe(decision === 'GRANT')
|
|
147
|
+
})
|
|
148
|
+
|
|
113
149
|
it('★ the card\'s findings are reviewDecision\'s, for grant and for deny', async () => {
|
|
114
150
|
const request = await access.requestAccess({ category: 'warehouse', privilege: 'mutation', reason: 'inbound' }, { state: { ...state(kim), afterCommit: [] } } as any)
|
|
115
151
|
const rows = [
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A list reads this domain's rows only - whatever `inherited` the caller sent (2026-10-02).
|
|
3
|
+
*
|
|
4
|
+
* `getQueryBuilderFromListParams` honours `inherited: Include | Only`, so a list that hands the client's ListParam
|
|
5
|
+
* through lets a child domain read its parent's rows. For these lists a parent's rows are another tenant's
|
|
6
|
+
* configuration or people, and no screen asks for them: the query pins `inherited` to None. The same pin as
|
|
7
|
+
* `accessRequests` (access-request-db.test.ts).
|
|
8
|
+
*
|
|
9
|
+
* Roles are not here - the role screens ask for the parent's roles on purpose (assignable scope, docs/design/auth-rebuild.md).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
13
|
+
import { loadCompiled } from './compiled'
|
|
14
|
+
|
|
15
|
+
const { User } = loadCompiled('service/user/user')
|
|
16
|
+
const { AuthProvider } = loadCompiled('service/auth-provider/auth-provider')
|
|
17
|
+
const { AuthProviderQuery } = loadCompiled('service/auth-provider/auth-provider-query')
|
|
18
|
+
const { Appliance } = loadCompiled('service/appliance/appliance')
|
|
19
|
+
const { ApplianceQuery } = loadCompiled('service/appliance/appliance-query')
|
|
20
|
+
const { Application } = loadCompiled('service/application/application')
|
|
21
|
+
const { ApplicationQuery } = loadCompiled('service/application/application-query')
|
|
22
|
+
const { Invitation } = loadCompiled('service/invitation/invitation')
|
|
23
|
+
const { InvitationStatus } = loadCompiled('utils/invitation-state')
|
|
24
|
+
const { InvitationQuery } = loadCompiled('service/invitation/invitation-query')
|
|
25
|
+
const { Partner } = loadCompiled('service/partner/partner')
|
|
26
|
+
const { PartnerQuery } = loadCompiled('service/partner/partner-query')
|
|
27
|
+
const { LoginHistory } = loadCompiled('service/login-history/login-history')
|
|
28
|
+
const { LoginHistoryQuery } = loadCompiled('service/login-history/login-history-query')
|
|
29
|
+
|
|
30
|
+
let parent: any
|
|
31
|
+
let child: any
|
|
32
|
+
let partnerDomain: any
|
|
33
|
+
let parentUser: any
|
|
34
|
+
let childUser: any
|
|
35
|
+
|
|
36
|
+
async function makeUser(name: string, domains: any[]) {
|
|
37
|
+
return await getRepository(User).save({
|
|
38
|
+
username: name,
|
|
39
|
+
email: `${name}@example.com`,
|
|
40
|
+
name,
|
|
41
|
+
salt: User.generateSalt(),
|
|
42
|
+
status: 'activated',
|
|
43
|
+
domains
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const from = (domain: any, user: any) => ({ state: { domain, user } }) as any
|
|
48
|
+
const ONLY = { inherited: 'Only' } as any
|
|
49
|
+
|
|
50
|
+
beforeAll(async () => {
|
|
51
|
+
await openAuthDatabase()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
afterAll(async () => {
|
|
55
|
+
await closeAuthDatabase()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
beforeEach(async () => {
|
|
59
|
+
await resetAuthDatabase()
|
|
60
|
+
|
|
61
|
+
const savedParent = await getRepository(Domain).save({ name: 'Plant A', subdomain: 'plant-a' })
|
|
62
|
+
parent = await getRepository(Domain).findOneBy({ id: savedParent.id })
|
|
63
|
+
const savedChild = await getRepository(Domain).save({ name: 'Plant A-1', subdomain: 'plant-a-1', parent })
|
|
64
|
+
child = await getRepository(Domain).findOneBy({ id: savedChild.id })
|
|
65
|
+
partnerDomain = await getRepository(Domain).save({ name: 'Vendor', subdomain: 'vendor' })
|
|
66
|
+
|
|
67
|
+
parentUser = await makeUser('parent-admin', [parent])
|
|
68
|
+
childUser = await makeUser('child-admin', [child])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
/*
|
|
72
|
+
* Each list: the parent reads its own row (so an empty answer below is the pin, not a broken fixture), and the
|
|
73
|
+
* child asking for the parent's with `inherited: Only` reads nothing.
|
|
74
|
+
*/
|
|
75
|
+
const LISTS: [string, () => Promise<any>, (params: any, context: any) => Promise<any>][] = [
|
|
76
|
+
[
|
|
77
|
+
'auth providers',
|
|
78
|
+
() => getRepository(AuthProvider).save({ domain: parent, type: 'keycloak', clientSecret: 'parent-secret' }),
|
|
79
|
+
(params, context) => new AuthProviderQuery().authProviders(params, context)
|
|
80
|
+
],
|
|
81
|
+
[
|
|
82
|
+
'appliances',
|
|
83
|
+
() => getRepository(Appliance).save({ domain: parent, name: 'gate', serialNo: 'gate-SN', brand: 'Acme', model: 'X1' }),
|
|
84
|
+
(params, context) => new ApplianceQuery().appliances(params, context)
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
'edges',
|
|
88
|
+
() => getRepository(Appliance).save({ domain: parent, name: 'edge', serialNo: 'edge-SN', brand: 'Acme', model: 'X1' }),
|
|
89
|
+
(params, context) => new ApplianceQuery().edges(params, context)
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
'applications',
|
|
93
|
+
() =>
|
|
94
|
+
getRepository(Application).save({
|
|
95
|
+
domain: parent,
|
|
96
|
+
name: 'billing',
|
|
97
|
+
email: 'billing@acme.z',
|
|
98
|
+
url: 'https://billing.example.com',
|
|
99
|
+
redirectUrl: 'https://billing.example.com/callback',
|
|
100
|
+
status: 0,
|
|
101
|
+
type: 'OTHERS',
|
|
102
|
+
appSecret: 'parent-app-secret'
|
|
103
|
+
}),
|
|
104
|
+
(params, context) => new ApplicationQuery().applications(params, context)
|
|
105
|
+
],
|
|
106
|
+
[
|
|
107
|
+
'invitations',
|
|
108
|
+
() =>
|
|
109
|
+
getRepository(Invitation).save({
|
|
110
|
+
email: 'lee@acme.z',
|
|
111
|
+
domain: parent,
|
|
112
|
+
status: InvitationStatus.PENDING,
|
|
113
|
+
token: 'parent-token',
|
|
114
|
+
expiresAt: new Date(Date.now() + 3600_000)
|
|
115
|
+
}),
|
|
116
|
+
(params, context) => new InvitationQuery().invitations(params, context)
|
|
117
|
+
],
|
|
118
|
+
[
|
|
119
|
+
'partners',
|
|
120
|
+
() => getRepository(Partner).save({ domain: parent, partnerDomain, requestedAt: new Date() }),
|
|
121
|
+
(params, context) => new PartnerQuery().partners(params, context)
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
'login histories',
|
|
125
|
+
() => LoginHistory.stamp(parent, parentUser, '10.0.0.1'),
|
|
126
|
+
(params, context) => new LoginHistoryQuery().loginHistories(params, context)
|
|
127
|
+
]
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
describe.each(LISTS)('%s', (_name, seed, list) => {
|
|
131
|
+
it("★ the parent reads its own row - the fixture is real", async () => {
|
|
132
|
+
await seed()
|
|
133
|
+
const answer = await list({}, from(parent, parentUser))
|
|
134
|
+
expect(answer.items).toHaveLength(1)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("★ a child domain asking for the parent's with inherited: Only reads nothing", async () => {
|
|
138
|
+
await seed()
|
|
139
|
+
const answer = await list(ONLY, from(child, childUser))
|
|
140
|
+
expect(answer.items).toEqual([])
|
|
141
|
+
expect(answer.total).toBe(0)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it("a child domain asking with inherited: Include reads nothing of the parent's", async () => {
|
|
145
|
+
await seed()
|
|
146
|
+
const answer = await list({ inherited: 'Include' }, from(child, childUser))
|
|
147
|
+
expect(answer.items).toEqual([])
|
|
148
|
+
})
|
|
149
|
+
})
|