@things-factory/auth-base 10.1.4 → 10.1.7
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/CHANGELOG.md +16 -0
- package/dist-client/auth.d.ts +9 -1
- package/dist-client/auth.js +12 -3
- package/dist-client/auth.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/controllers/profile.d.ts +1 -0
- package/dist-server/controllers/signin.js +33 -12
- package/dist-server/controllers/signin.js.map +1 -1
- package/dist-server/controllers/unlock-user.js +21 -2
- package/dist-server/controllers/unlock-user.js.map +1 -1
- package/dist-server/index.js +8 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/router/auth-public-process-router.js +32 -5
- package/dist-server/router/auth-public-process-router.js.map +1 -1
- package/dist-server/service/index.d.ts +3 -1
- package/dist-server/service/index.js +15 -11
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/privilege/privilege-directive.d.ts +15 -0
- package/dist-server/service/privilege/privilege-directive.js +27 -6
- package/dist-server/service/privilege/privilege-directive.js.map +1 -1
- package/dist-server/service/role/index.d.ts +2 -1
- package/dist-server/service/role/index.js +2 -1
- package/dist-server/service/role/index.js.map +1 -1
- package/dist-server/service/role/role-mutation.d.ts +17 -1
- package/dist-server/service/role/role-mutation.js +126 -7
- package/dist-server/service/role/role-mutation.js.map +1 -1
- package/dist-server/service/role/role-privilege-query.d.ts +14 -0
- package/dist-server/service/role/role-privilege-query.js +37 -0
- package/dist-server/service/role/role-privilege-query.js.map +1 -0
- package/dist-server/service/role/role-query.d.ts +61 -1
- package/dist-server/service/role/role-query.js +114 -55
- package/dist-server/service/role/role-query.js.map +1 -1
- package/dist-server/service/role-template/index.d.ts +4 -0
- package/dist-server/service/role-template/index.js +9 -0
- package/dist-server/service/role-template/index.js.map +1 -0
- package/dist-server/service/role-template/role-template-mutation.d.ts +39 -0
- package/dist-server/service/role-template/role-template-mutation.js +124 -0
- package/dist-server/service/role-template/role-template-mutation.js.map +1 -0
- package/dist-server/service/role-template/role-template-query.d.ts +11 -0
- package/dist-server/service/role-template/role-template-query.js +79 -0
- package/dist-server/service/role-template/role-template-query.js.map +1 -0
- package/dist-server/service/role-template/role-template-types.d.ts +51 -0
- package/dist-server/service/role-template/role-template-types.js +71 -0
- package/dist-server/service/role-template/role-template-types.js.map +1 -0
- package/dist-server/service/role-template/role-template.d.ts +110 -0
- package/dist-server/service/role-template/role-template.js +129 -0
- package/dist-server/service/role-template/role-template.js.map +1 -0
- package/dist-server/service/user/user.d.ts +1 -0
- package/dist-server/service/user/user.js +55 -13
- package/dist-server/service/user/user.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/utils/account-lock-rule.d.ts +79 -0
- package/dist-server/utils/account-lock-rule.js +114 -0
- package/dist-server/utils/account-lock-rule.js.map +1 -0
- package/dist-server/utils/lock-duration.d.ts +3 -0
- package/dist-server/utils/lock-duration.js +36 -0
- package/dist-server/utils/lock-duration.js.map +1 -0
- package/package.json +4 -4
- package/tests/account-lock-db.test.ts +216 -0
- package/tests/account-lock-rule.test.ts +141 -0
- package/tests/compiled.ts +41 -0
- package/tests/has-privilege-db.test.ts +198 -0
- package/tests/lock-recovery.test.ts +204 -0
- package/tests/password-hashing.test.ts +131 -0
- package/tests/password-persistence.test.ts +147 -0
- package/tests/permission-gate.test.ts +226 -0
- package/tests/privilege-directive.test.ts +180 -0
- package/tests/role-mutation-db.test.ts +263 -0
- package/tests/role-privileges-db.test.ts +226 -0
- package/tests/role-template-seed-db.test.ts +179 -0
- package/tests/role-template.test.ts +126 -0
- package/tests/signin-policy.test.ts +200 -0
- package/tests/token-issuance.test.ts +240 -0
- package/translations/en.json +161 -1
- package/translations/ko.json +161 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 역할 만들기·고치기·지우기 — **실패했을 때 무엇을 말하나**를 실 DB 로 태운다.
|
|
3
|
+
*
|
|
4
|
+
* 이 셋은 권한 편집 화면이 서는 자리인데, 세 갈래의 실패가 전부 잘못된 말로 나가고 있었다.
|
|
5
|
+
*
|
|
6
|
+
* ① 부모 도메인의 역할을 고치려 하면 `role.name` 에서 TypeError → 500. 화면은 이유를 모른다.
|
|
7
|
+
* 목록은 상속으로 부모 역할까지 보여 주므로(`inherited: Include`) **실제로 자주 닿는다.**
|
|
8
|
+
* ② `privileges` 없는 patch(이름만 고치기)도 `patch.privileges.map` 에서 500.
|
|
9
|
+
* ③ `deleteRole` 의 catch 가 무엇이 났든 「사용 중이라 지울 수 없다」로 401 을 던졌다 —
|
|
10
|
+
* 못 찾은 것도 그렇게 말했다. 관리자는 있지도 않은 사용자를 떼러 갔다.
|
|
11
|
+
*
|
|
12
|
+
* 그리고 `deleteRoles`(복수)는 **도메인을 아예 안 봤다** — id 만 알면 남의 도메인 역할이 지워졌다.
|
|
13
|
+
*
|
|
14
|
+
* 지시자는 여기서 안 돈다(스키마 층이다). 게이트는 `privilege-directive.test.ts` 가 본다.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
18
|
+
import { authDataSource } from './db'
|
|
19
|
+
import { loadCompiled } from './compiled'
|
|
20
|
+
|
|
21
|
+
const { Role } = loadCompiled('service/role/role')
|
|
22
|
+
const { Privilege } = loadCompiled('service/privilege/privilege')
|
|
23
|
+
const { User } = loadCompiled('service/user/user')
|
|
24
|
+
const { RoleMutation } = loadCompiled('service/role/role-mutation')
|
|
25
|
+
|
|
26
|
+
const mutation = new RoleMutation()
|
|
27
|
+
|
|
28
|
+
let parent: any
|
|
29
|
+
let child: any
|
|
30
|
+
let stranger: any
|
|
31
|
+
let actor: any
|
|
32
|
+
let boardQuery: any
|
|
33
|
+
let boardMutation: any
|
|
34
|
+
|
|
35
|
+
async function loadDomain(id: string) {
|
|
36
|
+
return await getRepository(Domain).findOneBy({ id })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 리졸버가 실제로 쓰는 것만 담는다 — `tx`·`domain`·`user`·`t`·`throw`. */
|
|
40
|
+
function context(domain: any) {
|
|
41
|
+
return {
|
|
42
|
+
state: { domain, user: actor, tx: authDataSource().manager },
|
|
43
|
+
t: (key: string, params: any = {}) => `${key}${params.x ? `:${params.x}` : ''}`,
|
|
44
|
+
throw: (status: number, message: string) => {
|
|
45
|
+
throw new Error(`${status} ${message}`)
|
|
46
|
+
}
|
|
47
|
+
} as any
|
|
48
|
+
}
|
|
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 domainRepository = getRepository(Domain)
|
|
62
|
+
const savedParent = await domainRepository.save({ name: 'HQ', subdomain: 'hq' })
|
|
63
|
+
const savedChild = await domainRepository.save({ name: 'Plant A', subdomain: 'plant-a', parent: savedParent })
|
|
64
|
+
const savedStranger = await domainRepository.save({ name: 'Other', subdomain: 'other' })
|
|
65
|
+
|
|
66
|
+
parent = await loadDomain(savedParent.id)
|
|
67
|
+
child = await loadDomain(savedChild.id)
|
|
68
|
+
stranger = await loadDomain(savedStranger.id)
|
|
69
|
+
|
|
70
|
+
const privilegeRepository = getRepository(Privilege)
|
|
71
|
+
boardQuery = await privilegeRepository.save({ name: 'query', category: 'board' })
|
|
72
|
+
boardMutation = await privilegeRepository.save({ name: 'mutation', category: 'board' })
|
|
73
|
+
|
|
74
|
+
const salt = User.generateSalt()
|
|
75
|
+
actor = await getRepository(User).save({
|
|
76
|
+
username: 'admin',
|
|
77
|
+
email: 'admin@example.com',
|
|
78
|
+
name: 'Admin',
|
|
79
|
+
salt,
|
|
80
|
+
status: 'activated',
|
|
81
|
+
domains: [child]
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
async function makeRole(name: string, domain: any, privileges: any[] = [], users: any[] = []) {
|
|
86
|
+
return await getRepository(Role).save({ name, domain, privileges, users })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('고치기', () => {
|
|
90
|
+
it('권한을 갈아 끼운다', async () => {
|
|
91
|
+
const role = await makeRole('viewer', child, [boardQuery])
|
|
92
|
+
|
|
93
|
+
await mutation.updateRole(role.id, { privileges: [{ id: boardMutation.id }] } as any, context(child))
|
|
94
|
+
|
|
95
|
+
const saved = await getRepository(Role).findOne({ where: { id: role.id }, relations: ['privileges'] })
|
|
96
|
+
expect(saved.privileges.map((p: any) => p.id)).toEqual([boardMutation.id])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('`privileges` 를 안 보내면 권한을 건드리지 않는다 — 이름만 고치는 호출이 있다', async () => {
|
|
100
|
+
/* 예전에는 이 호출이 `patch.privileges.map` 에서 TypeError 로 500 이었다. */
|
|
101
|
+
const role = await makeRole('viewer', child, [boardQuery])
|
|
102
|
+
|
|
103
|
+
await mutation.updateRole(role.id, { name: 'reader' } as any, context(child))
|
|
104
|
+
|
|
105
|
+
const saved = await getRepository(Role).findOne({ where: { id: role.id }, relations: ['privileges'] })
|
|
106
|
+
expect(saved.name).toBe('reader')
|
|
107
|
+
expect(saved.privileges.map((p: any) => p.id)).toEqual([boardQuery.id])
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('부모 도메인의 역할은 어디서 고치는지 말하고 거절한다', async () => {
|
|
111
|
+
/*
|
|
112
|
+
* 목록에 보이는 것과 고칠 수 있는 것이 다르다. 예전에는 여기서 TypeError 가 나 500 이었고
|
|
113
|
+
* 화면은 「알 수 없는 오류」만 보여 줬다.
|
|
114
|
+
*/
|
|
115
|
+
const role = await makeRole('shared', parent, [boardQuery])
|
|
116
|
+
|
|
117
|
+
await expect(mutation.updateRole(role.id, { name: 'mine' } as any, context(child))).rejects.toThrow(
|
|
118
|
+
/parent domain/
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('같은 이름이 이미 있으면 거절한다', async () => {
|
|
123
|
+
await makeRole('viewer', child)
|
|
124
|
+
const role = await makeRole('editor', child)
|
|
125
|
+
|
|
126
|
+
await expect(mutation.updateRole(role.id, { name: 'viewer' } as any, context(child))).rejects.toThrow(
|
|
127
|
+
'role name is duplicated'
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('지우기 — 하나', () => {
|
|
133
|
+
it('아무도 안 가진 역할은 지운다', async () => {
|
|
134
|
+
const role = await makeRole('unused', child, [boardQuery])
|
|
135
|
+
|
|
136
|
+
expect(await mutation.deleteRole(role.id, context(child))).toBe(true)
|
|
137
|
+
expect(await getRepository(Role).findOneBy({ id: role.id })).toBeNull()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('가진 사람이 있으면 그 이유로 거절한다', async () => {
|
|
141
|
+
const role = await makeRole('in-use', child, [boardQuery], [actor])
|
|
142
|
+
|
|
143
|
+
await expect(mutation.deleteRole(role.id, context(child))).rejects.toThrow('currently in use')
|
|
144
|
+
expect(await getRepository(Role).findOneBy({ id: role.id })).not.toBeNull()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('부모 도메인의 역할을 「사용 중」이라고 말하지 않는다', async () => {
|
|
148
|
+
/*
|
|
149
|
+
* 이것이 옛 구현의 가장 나쁜 점이었다 — 못 찾은 것을 「사용 중」이라고 말했으므로 관리자는
|
|
150
|
+
* 있지도 않은 사용자를 찾아 떼려 했다.
|
|
151
|
+
*/
|
|
152
|
+
const role = await makeRole('shared', parent)
|
|
153
|
+
|
|
154
|
+
await expect(mutation.deleteRole(role.id, context(child))).rejects.toThrow(/parent domain/)
|
|
155
|
+
await expect(mutation.deleteRole(role.id, context(child))).rejects.not.toThrow(/in use/)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('지우기 — 여럿', () => {
|
|
160
|
+
it('남의 도메인 역할은 id 를 알아도 안 지운다', async () => {
|
|
161
|
+
const mine = await makeRole('mine', child)
|
|
162
|
+
const theirs = await makeRole('theirs', stranger)
|
|
163
|
+
|
|
164
|
+
await mutation.deleteRoles([mine.id, theirs.id], context(child))
|
|
165
|
+
|
|
166
|
+
expect(await getRepository(Role).findOneBy({ id: mine.id })).toBeNull()
|
|
167
|
+
expect(await getRepository(Role).findOneBy({ id: theirs.id })).not.toBeNull()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('사용 중인 역할은 남긴다 — 하나 지우기와 같은 규칙이다', async () => {
|
|
171
|
+
const unused = await makeRole('unused', child)
|
|
172
|
+
const inUse = await makeRole('in-use', child, [], [actor])
|
|
173
|
+
|
|
174
|
+
await mutation.deleteRoles([unused.id, inUse.id], context(child))
|
|
175
|
+
|
|
176
|
+
expect(await getRepository(Role).findOneBy({ id: unused.id })).toBeNull()
|
|
177
|
+
expect(await getRepository(Role).findOneBy({ id: inUse.id })).not.toBeNull()
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 격자에서 여러 줄을 고쳐 한 번에 보내는 길.
|
|
183
|
+
*
|
|
184
|
+
* 목록 화면이 `dirtyRecords` 를 `cuFlag` 와 함께 보낸다. 여기서 지키는 것은 하나가 제일 크다 —
|
|
185
|
+
* **`privileges` 를 안 보낸 patch 가 권한을 비우지 않는 것.** 격자는 이름만 고치는데 그것이
|
|
186
|
+
* 권한을 지우면 그 역할이 아무것도 안 여는 역할이 된다.
|
|
187
|
+
*/
|
|
188
|
+
describe('격자에서 여럿 저장', () => {
|
|
189
|
+
it('`+` 는 만들고 `M` 은 고친다', async () => {
|
|
190
|
+
const existing = await makeRole('viewer', child, [boardQuery])
|
|
191
|
+
|
|
192
|
+
await mutation.updateMultipleRole(
|
|
193
|
+
[
|
|
194
|
+
{ cuFlag: '+', name: 'operator', description: '현장' },
|
|
195
|
+
{ cuFlag: 'M', id: existing.id, name: 'reader' }
|
|
196
|
+
] as any,
|
|
197
|
+
context(child)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
const roles = await getRepository(Role).find({ where: { domain: { id: child.id } } })
|
|
201
|
+
expect(roles.map((r: any) => r.name).sort()).toEqual(['operator', 'reader'])
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('이름만 고쳐도 권한은 그대로다 — 격자는 권한을 안 보낸다', async () => {
|
|
205
|
+
/*
|
|
206
|
+
* 이것이 이 자리의 가장 조용한 결함이 될 수 있었다. 관리자가 격자에서 오타를 고쳤는데
|
|
207
|
+
* 그 역할이 아무것도 안 여는 역할이 되면, 그 역할을 가진 사람들이 다음 로그인에 막힌다.
|
|
208
|
+
*/
|
|
209
|
+
const role = await makeRole('viewer', child, [boardQuery, boardMutation])
|
|
210
|
+
|
|
211
|
+
await mutation.updateMultipleRole([{ cuFlag: 'M', id: role.id, name: 'reader' }] as any, context(child))
|
|
212
|
+
|
|
213
|
+
const saved = await getRepository(Role).findOne({ where: { id: role.id }, relations: ['privileges'] })
|
|
214
|
+
expect(saved.privileges.map((p: any) => p.id).sort()).toEqual([boardMutation.id, boardQuery.id].sort())
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('`cuFlag` 가 없는 줄은 아무것도 하지 않는다 — 짐작해서 만들지 않는다', async () => {
|
|
218
|
+
await mutation.updateMultipleRole([{ name: 'ghost' }] as any, context(child))
|
|
219
|
+
|
|
220
|
+
expect(await getRepository(Role).findOneBy({ name: 'ghost' })).toBeNull()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('부모 도메인의 역할을 고치려 하면 어디서 고치는지 말한다', async () => {
|
|
224
|
+
const role = await makeRole('shared', parent)
|
|
225
|
+
|
|
226
|
+
await expect(
|
|
227
|
+
mutation.updateMultipleRole([{ cuFlag: 'M', id: role.id, name: 'mine' }] as any, context(child))
|
|
228
|
+
).rejects.toThrow(/parent domain/)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('이름 없이 만들려 하면 거절한다', async () => {
|
|
232
|
+
await expect(mutation.updateMultipleRole([{ cuFlag: '+', name: ' ' }] as any, context(child))).rejects.toThrow()
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('같은 이름으로 만들려 하면 거절한다', async () => {
|
|
236
|
+
await makeRole('operator', child)
|
|
237
|
+
|
|
238
|
+
await expect(
|
|
239
|
+
mutation.updateMultipleRole([{ cuFlag: '+', name: 'operator' }] as any, context(child))
|
|
240
|
+
).rejects.toThrow('role name is duplicated')
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
describe('만들기', () => {
|
|
245
|
+
it('내 도메인에 만들고 고른 권한을 붙인다', async () => {
|
|
246
|
+
const created = await mutation.createRole(
|
|
247
|
+
{ name: 'operator', privileges: [{ id: boardQuery.id }] } as any,
|
|
248
|
+
context(child)
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
const saved = await getRepository(Role).findOne({ where: { id: created.id }, relations: ['privileges', 'domain'] })
|
|
252
|
+
expect(saved.domain.id).toBe(child.id)
|
|
253
|
+
expect(saved.privileges.map((p: any) => p.id)).toEqual([boardQuery.id])
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('같은 도메인에 같은 이름은 안 만든다', async () => {
|
|
257
|
+
await makeRole('operator', child)
|
|
258
|
+
|
|
259
|
+
await expect(mutation.createRole({ name: 'operator' } as any, context(child))).rejects.toThrow(
|
|
260
|
+
'role name is duplicated'
|
|
261
|
+
)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rolePrivileges` — 권한 편집 화면이 드는 **한 번의 질의**를 실 DB 로 태운다.
|
|
3
|
+
*
|
|
4
|
+
* 이 질의는 두 가지를 함께 답한다: 「무엇이 있나」와 「이 역할이 무엇을 갖나」. 화면이 그것을
|
|
5
|
+
* 질의 둘로 받으면 짝짓기를 화면이 해야 하는데, 옛 화면은 그 짝을 역할 **이름**으로 맞췄고
|
|
6
|
+
* 부모 도메인에 같은 이름의 역할이 있으면 남의 권한을 그렸다.
|
|
7
|
+
*
|
|
8
|
+
* 그리고 이 질의는 얼마 전까지 **원시 SQL** 이었다. 셋을 동시에 잃고 있었다 —
|
|
9
|
+
* `roleId` 문자열 이어 붙이기, `THEN true`(MSSQL 에 없는 리터럴), 도메인 무시. 아래 시험이
|
|
10
|
+
* 그 셋 중 되돌아올 수 있는 둘을 붙잡는다.
|
|
11
|
+
*
|
|
12
|
+
* ⚠ sqlite 로는 방언 문제를 **못 본다**(`tests/db.ts`). 그것은 원시 SQL 을 안 쓰는 것으로만
|
|
13
|
+
* 막힌다. 그래서 이 시험은 「관계로 읽는다」는 결과를 지키고, 방언은 코드에서 지운다.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
17
|
+
import { loadCompiled } from './compiled'
|
|
18
|
+
|
|
19
|
+
const { Role } = loadCompiled('service/role/role')
|
|
20
|
+
const { Privilege } = loadCompiled('service/privilege/privilege')
|
|
21
|
+
const { User } = loadCompiled('service/user/user')
|
|
22
|
+
const { RoleQuery } = loadCompiled('service/role/role-query')
|
|
23
|
+
|
|
24
|
+
const query = new RoleQuery()
|
|
25
|
+
|
|
26
|
+
let parent: any
|
|
27
|
+
let child: any
|
|
28
|
+
let stranger: any
|
|
29
|
+
let boardQuery: any
|
|
30
|
+
let boardMutation: any
|
|
31
|
+
|
|
32
|
+
/** `parentId` 는 `@RelationId` 라 저장소로 읽은 도메인에만 실린다. */
|
|
33
|
+
async function loadDomain(id: string) {
|
|
34
|
+
return await getRepository(Domain).findOneBy({ id })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 지시자가 없으므로 리졸버 본문을 직접 부른다 — 게이트는 `privilege-directive.test.ts` 가 본다. */
|
|
38
|
+
function ask(roleId: string, domain: any) {
|
|
39
|
+
return query.rolePrivileges(roleId, { state: { domain } } as any)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function askUserRoles(userId: string, domain: any) {
|
|
43
|
+
return query.userRoles(userId, { state: { domain } } as any)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
beforeAll(async () => {
|
|
47
|
+
await openAuthDatabase()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
afterAll(async () => {
|
|
51
|
+
await closeAuthDatabase()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
await resetAuthDatabase()
|
|
56
|
+
|
|
57
|
+
const domainRepository = getRepository(Domain)
|
|
58
|
+
|
|
59
|
+
const savedParent = await domainRepository.save({ name: 'HQ', subdomain: 'hq' })
|
|
60
|
+
const savedChild = await domainRepository.save({ name: 'Plant A', subdomain: 'plant-a', parent: savedParent })
|
|
61
|
+
const savedStranger = await domainRepository.save({ name: 'Other', subdomain: 'other' })
|
|
62
|
+
|
|
63
|
+
parent = await loadDomain(savedParent.id)
|
|
64
|
+
child = await loadDomain(savedChild.id)
|
|
65
|
+
stranger = await loadDomain(savedStranger.id)
|
|
66
|
+
|
|
67
|
+
const privilegeRepository = getRepository(Privilege)
|
|
68
|
+
boardQuery = await privilegeRepository.save({ name: 'query', category: 'board' })
|
|
69
|
+
boardMutation = await privilegeRepository.save({ name: 'mutation', category: 'board' })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('가진 것과 안 가진 것', () => {
|
|
73
|
+
it('전체 권한을 돌려주고, 가진 것에만 표시한다', async () => {
|
|
74
|
+
const role = await getRepository(Role).save({ name: 'viewer', domain: child, privileges: [boardQuery] })
|
|
75
|
+
|
|
76
|
+
const rows = await ask(role.id, child)
|
|
77
|
+
|
|
78
|
+
expect(rows).toHaveLength(2)
|
|
79
|
+
expect(rows.find(row => row.id === boardQuery.id).assigned).toBe(true)
|
|
80
|
+
expect(rows.find(row => row.id === boardMutation.id).assigned).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('아무것도 없는 역할에도 목록을 준다 — 편집을 시작할 수 있어야 한다', async () => {
|
|
84
|
+
const role = await getRepository(Role).save({ name: 'blank', domain: child, privileges: [] })
|
|
85
|
+
|
|
86
|
+
const rows = await ask(role.id, child)
|
|
87
|
+
|
|
88
|
+
expect(rows).toHaveLength(2)
|
|
89
|
+
expect(rows.every(row => row.assigned === false)).toBe(true)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('도메인 범위', () => {
|
|
94
|
+
it('부모 도메인의 역할은 볼 수 있다 — 상속으로 목록에 나오는 것과 같은 범위다', async () => {
|
|
95
|
+
const role = await getRepository(Role).save({ name: 'shared', domain: parent, privileges: [boardMutation] })
|
|
96
|
+
|
|
97
|
+
const rows = await ask(role.id, child)
|
|
98
|
+
|
|
99
|
+
expect(rows.find(row => row.id === boardMutation.id).assigned).toBe(true)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('남의 도메인 역할은 못 본다 — id 를 알아도 답하지 않는다', async () => {
|
|
103
|
+
const role = await getRepository(Role).save({ name: 'theirs', domain: stranger, privileges: [boardMutation] })
|
|
104
|
+
|
|
105
|
+
expect(await ask(role.id, child)).toEqual([])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('자식 도메인의 역할도 못 본다 — 상속은 위로 한 단이고 아래로는 없다', async () => {
|
|
109
|
+
const role = await getRepository(Role).save({ name: 'local', domain: child, privileges: [boardQuery] })
|
|
110
|
+
|
|
111
|
+
expect(await ask(role.id, parent)).toEqual([])
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('없는 역할', () => {
|
|
116
|
+
it('빈 목록을 준다 — 「전체 권한을 아무것도 안 가진 역할」로 보이지 않는다', async () => {
|
|
117
|
+
/*
|
|
118
|
+
* 이 구별이 화면에서 중요하다. 전체 목록을 주면서 `assigned` 를 전부 거짓으로 두면
|
|
119
|
+
* 화면은 「권한이 하나도 없는 역할」을 그리고, 관리자가 저장하면 **실제로** 그렇게 된다.
|
|
120
|
+
*/
|
|
121
|
+
expect(await ask('00000000-0000-0000-0000-000000000000', child)).toEqual([])
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('부러지지 않는다', () => {
|
|
126
|
+
it('id 자리에 SQL 을 넣어도 던지지 않고 빈 목록을 준다', async () => {
|
|
127
|
+
/*
|
|
128
|
+
* 옛 구현은 이 문자열을 SQL 에 그대로 이어 붙였다. `user:query` 를 가진 사람이 닿는
|
|
129
|
+
* 자리이므로 그것으로 충분히 위험했다. 지금은 값으로만 쓰인다.
|
|
130
|
+
*/
|
|
131
|
+
expect(await ask("' OR 1=1 --", child)).toEqual([])
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 목록이 쓰는 **수**.
|
|
137
|
+
*
|
|
138
|
+
* 목록이 「이 역할이 무엇을 여나」를 말하려고 권한 **행**을 받고 있었다 — 역할 20개에
|
|
139
|
+
* 5,731 bytes · 69ms 대 518 bytes · 7ms(UX 레인 실측). 행을 안 받고 수를 받는다.
|
|
140
|
+
*/
|
|
141
|
+
describe('권한 수', () => {
|
|
142
|
+
it('가진 것만 센다', async () => {
|
|
143
|
+
const role = await getRepository(Role).save({ name: 'viewer', domain: child, privileges: [boardQuery] })
|
|
144
|
+
|
|
145
|
+
expect(await query.privilegeCount(role)).toBe(1)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('하나도 없으면 0 이다 — 「모른다」가 아니라 0 이다', async () => {
|
|
149
|
+
const role = await getRepository(Role).save({ name: 'blank', domain: child, privileges: [] })
|
|
150
|
+
|
|
151
|
+
expect(await query.privilegeCount(role)).toBe(0)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('다른 역할의 권한을 세지 않는다', async () => {
|
|
155
|
+
await getRepository(Role).save({ name: 'other', domain: child, privileges: [boardQuery, boardMutation] })
|
|
156
|
+
const mine = await getRepository(Role).save({ name: 'mine', domain: child, privileges: [boardQuery] })
|
|
157
|
+
|
|
158
|
+
expect(await query.privilegeCount(mine)).toBe(1)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 한 사람의 역할 — `rolePrivileges` 와 **같은 결함을 갖고 있던** 자리.
|
|
164
|
+
*
|
|
165
|
+
* 원시 SQL 로 `userId` 를 이어 붙이고, MSSQL 에 없는 `THEN true` 를 쓰고, `FROM roles` 로
|
|
166
|
+
* **도메인을 아예 안 봤다.** 한 자리를 고쳤으면 두 번째 자리도 재야 한다.
|
|
167
|
+
*/
|
|
168
|
+
describe('사용자의 역할', () => {
|
|
169
|
+
async function makeUser(roles: any[]) {
|
|
170
|
+
const salt = User.generateSalt()
|
|
171
|
+
return await getRepository(User).save({
|
|
172
|
+
username: `u-${Math.random().toString(36).slice(2, 8)}`,
|
|
173
|
+
email: `u-${Math.random().toString(36).slice(2, 8)}@example.com`,
|
|
174
|
+
name: 'Kim',
|
|
175
|
+
salt,
|
|
176
|
+
status: 'activated',
|
|
177
|
+
domains: [child],
|
|
178
|
+
roles
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
it('부여할 수 있는 전체를 주고, 가진 것에만 표시한다', async () => {
|
|
183
|
+
const held = await getRepository(Role).save({ name: 'held', domain: child })
|
|
184
|
+
await getRepository(Role).save({ name: 'free', domain: child })
|
|
185
|
+
const user = await makeUser([held])
|
|
186
|
+
|
|
187
|
+
const rows = await askUserRoles(user.id, child)
|
|
188
|
+
|
|
189
|
+
expect(rows).toHaveLength(2)
|
|
190
|
+
expect(rows.find(row => row.id === held.id).assigned).toBe(true)
|
|
191
|
+
expect(rows.find(row => row.name === 'free').assigned).toBe(false)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('부모 도메인의 역할도 준다 — 상속이 부여 범위다', async () => {
|
|
195
|
+
await getRepository(Role).save({ name: 'shared', domain: parent })
|
|
196
|
+
const user = await makeUser([])
|
|
197
|
+
|
|
198
|
+
expect((await askUserRoles(user.id, child)).map((row: any) => row.name)).toEqual(['shared'])
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('남의 도메인 역할은 주지 않는다 — 예전에는 전부 줬다', async () => {
|
|
202
|
+
await getRepository(Role).save({ name: 'theirs', domain: stranger })
|
|
203
|
+
await getRepository(Role).save({ name: 'mine', domain: child })
|
|
204
|
+
const user = await makeUser([])
|
|
205
|
+
|
|
206
|
+
expect((await askUserRoles(user.id, child)).map((row: any) => row.name)).toEqual(['mine'])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('없는 사용자에게도 목록을 준다 — 가진 것이 0 인 것과 같다', async () => {
|
|
210
|
+
await getRepository(Role).save({ name: 'mine', domain: child })
|
|
211
|
+
|
|
212
|
+
const rows = await askUserRoles('00000000-0000-0000-0000-000000000000', child)
|
|
213
|
+
|
|
214
|
+
expect(rows).toHaveLength(1)
|
|
215
|
+
expect(rows[0].assigned).toBe(false)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('id 자리에 SQL 을 넣어도 던지지 않는다', async () => {
|
|
219
|
+
await getRepository(Role).save({ name: 'mine', domain: child })
|
|
220
|
+
|
|
221
|
+
const rows = await askUserRoles("' OR 1=1 --", child)
|
|
222
|
+
|
|
223
|
+
expect(rows).toHaveLength(1)
|
|
224
|
+
expect(rows[0].assigned).toBe(false)
|
|
225
|
+
})
|
|
226
|
+
})
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeding a role from a template — against a real database.
|
|
3
|
+
*
|
|
4
|
+
* `role-template.test.ts` covers what the registry accepts. What it cannot cover is the
|
|
5
|
+
* part that touches rows, and that is where the rules with teeth are:
|
|
6
|
+
*
|
|
7
|
+
* - the plant names the seat, not the template;
|
|
8
|
+
* - an existing name is reused rather than duplicated, because approval lines hold a
|
|
9
|
+
* role's id and a second row would leave them pointing at the one nobody edits;
|
|
10
|
+
* - two templates on one name make the union, and privileges are never taken away;
|
|
11
|
+
* - a privilege this installation does not declare is skipped, never invented.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
15
|
+
import { authDataSource } from './db'
|
|
16
|
+
import { loadCompiled } from './compiled'
|
|
17
|
+
|
|
18
|
+
const { Role } = loadCompiled('service/role/role')
|
|
19
|
+
const { Privilege } = loadCompiled('service/privilege/privilege')
|
|
20
|
+
const { User } = loadCompiled('service/user/user')
|
|
21
|
+
const { RoleTemplateMutation } = loadCompiled('service/role-template/role-template-mutation')
|
|
22
|
+
const { registerRoleTemplate } = loadCompiled('service/role-template/role-template')
|
|
23
|
+
|
|
24
|
+
const mutation = new RoleTemplateMutation()
|
|
25
|
+
|
|
26
|
+
let domain: any
|
|
27
|
+
let actor: any
|
|
28
|
+
let boardQuery: any
|
|
29
|
+
let boardMutation: any
|
|
30
|
+
|
|
31
|
+
function context() {
|
|
32
|
+
return {
|
|
33
|
+
state: { domain, user: actor, tx: authDataSource().manager }
|
|
34
|
+
} as any
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function privilegesOf(id: string): Promise<string[]> {
|
|
38
|
+
const role = await getRepository(Role).findOne({ where: { id }, relations: ['privileges'] })
|
|
39
|
+
|
|
40
|
+
return (role!.privileges || []).map((one: any) => `${one.category}:${one.name}`).sort()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
beforeAll(async () => {
|
|
44
|
+
await openAuthDatabase()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
afterAll(async () => {
|
|
48
|
+
await closeAuthDatabase()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
beforeEach(async () => {
|
|
52
|
+
await resetAuthDatabase()
|
|
53
|
+
;(process as any)['ROLE_TEMPLATES'] = {}
|
|
54
|
+
|
|
55
|
+
const saved = await getRepository(Domain).save({ name: 'Plant A', subdomain: 'plant-a' })
|
|
56
|
+
domain = await getRepository(Domain).findOneBy({ id: saved.id })
|
|
57
|
+
|
|
58
|
+
const privilegeRepository = getRepository(Privilege)
|
|
59
|
+
boardQuery = await privilegeRepository.save({ name: 'query', category: 'board' })
|
|
60
|
+
boardMutation = await privilegeRepository.save({ name: 'mutation', category: 'board' })
|
|
61
|
+
|
|
62
|
+
/* The gate reads this; seeding reads the rows above. Both have to agree. */
|
|
63
|
+
;(process as any).PRIVILEGES = {
|
|
64
|
+
'board query': ['board', 'query'],
|
|
65
|
+
'board mutation': ['board', 'mutation']
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
actor = await getRepository(User).save({
|
|
69
|
+
username: 'admin',
|
|
70
|
+
email: 'admin@example.com',
|
|
71
|
+
name: 'Admin',
|
|
72
|
+
salt: User.generateSalt(),
|
|
73
|
+
status: 'activated',
|
|
74
|
+
domains: [domain]
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('seeding', () => {
|
|
79
|
+
it('creates the role with the privileges the template names', async () => {
|
|
80
|
+
registerRoleTemplate({
|
|
81
|
+
id: 'plant.qi',
|
|
82
|
+
name: 'QUALITY-INSPECTOR',
|
|
83
|
+
grants: [{ category: 'board', axes: ['query'] }]
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const role = await mutation.createRoleFromTemplate('plant.qi', undefined, context())
|
|
87
|
+
|
|
88
|
+
expect(role.name).toBe('QUALITY-INSPECTOR')
|
|
89
|
+
expect(await privilegesOf(role.id)).toEqual(['board:query'])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('lets the plant name the seat itself', async () => {
|
|
93
|
+
registerRoleTemplate({ id: 'plant.qi', name: 'QUALITY-INSPECTOR', grants: [{ category: 'board', axes: ['query'] }] })
|
|
94
|
+
|
|
95
|
+
const role = await mutation.createRoleFromTemplate('plant.qi', '품질검사원', context())
|
|
96
|
+
|
|
97
|
+
expect(role.name).toBe('품질검사원')
|
|
98
|
+
expect(await privilegesOf(role.id)).toEqual(['board:query'])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('makes a second seat from one template when the name differs — day shift and night shift', async () => {
|
|
102
|
+
registerRoleTemplate({ id: 'plant.qi', name: 'QUALITY-INSPECTOR', grants: [{ category: 'board', axes: ['query'] }] })
|
|
103
|
+
|
|
104
|
+
const day = await mutation.createRoleFromTemplate('plant.qi', 'QI-DAY', context())
|
|
105
|
+
const night = await mutation.createRoleFromTemplate('plant.qi', 'QI-NIGHT', context())
|
|
106
|
+
|
|
107
|
+
expect(day.id).not.toBe(night.id)
|
|
108
|
+
expect(await privilegesOf(day.id)).toEqual(['board:query'])
|
|
109
|
+
expect(await privilegesOf(night.id)).toEqual(['board:query'])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('reuses an existing role of that name instead of creating a second one', async () => {
|
|
113
|
+
registerRoleTemplate({ id: 'plant.qi', name: 'QUALITY-INSPECTOR', grants: [{ category: 'board', axes: ['query'] }] })
|
|
114
|
+
|
|
115
|
+
const existing = await getRepository(Role).save({ name: 'QUALITY-INSPECTOR', domain, privileges: [] })
|
|
116
|
+
const role = await mutation.createRoleFromTemplate('plant.qi', undefined, context())
|
|
117
|
+
|
|
118
|
+
expect(role.id).toBe(existing.id)
|
|
119
|
+
expect(await getRepository(Role).count()).toBe(1)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('adds the union when two applications declare the same seat', async () => {
|
|
123
|
+
registerRoleTemplate({ id: 'plant.qi', name: 'QUALITY-INSPECTOR', grants: [{ category: 'board', axes: ['query'] }] })
|
|
124
|
+
registerRoleTemplate({
|
|
125
|
+
id: 'twin.qi',
|
|
126
|
+
name: 'QUALITY-INSPECTOR',
|
|
127
|
+
grants: [{ category: 'board', axes: ['mutation'] }]
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
await mutation.createRoleFromTemplate('plant.qi', undefined, context())
|
|
131
|
+
const role = await mutation.createRoleFromTemplate('twin.qi', undefined, context())
|
|
132
|
+
|
|
133
|
+
expect(await privilegesOf(role.id)).toEqual(['board:mutation', 'board:query'])
|
|
134
|
+
expect(await getRepository(Role).count()).toBe(1)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('leaves privileges the administrator added, and takes nothing away', async () => {
|
|
138
|
+
registerRoleTemplate({ id: 'plant.qi', name: 'QUALITY-INSPECTOR', grants: [{ category: 'board', axes: ['query'] }] })
|
|
139
|
+
|
|
140
|
+
const existing = await getRepository(Role).save({
|
|
141
|
+
name: 'QUALITY-INSPECTOR',
|
|
142
|
+
domain,
|
|
143
|
+
privileges: [boardMutation]
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
await mutation.createRoleFromTemplate('plant.qi', undefined, context())
|
|
147
|
+
|
|
148
|
+
expect(await privilegesOf(existing.id)).toEqual(['board:mutation', 'board:query'])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('skips a privilege this installation does not declare rather than inventing the row', async () => {
|
|
152
|
+
registerRoleTemplate({
|
|
153
|
+
id: 'plant.x',
|
|
154
|
+
name: 'X',
|
|
155
|
+
grants: [
|
|
156
|
+
{ category: 'board', axes: ['query'] },
|
|
157
|
+
{ category: 'board-ai', axes: ['query'] }
|
|
158
|
+
]
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const role = await mutation.createRoleFromTemplate('plant.x', undefined, context())
|
|
162
|
+
|
|
163
|
+
expect(await privilegesOf(role.id)).toEqual(['board:query'])
|
|
164
|
+
expect(await getRepository(Privilege).count()).toBe(2)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('refuses a template this installation does not have', async () => {
|
|
168
|
+
await expect(mutation.createRoleFromTemplate('plant.missing', undefined, context())).rejects.toThrow(
|
|
169
|
+
/no role template/
|
|
170
|
+
)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('seeds the framework-computed administrator without any application declaring it', async () => {
|
|
174
|
+
const role = await mutation.createRoleFromTemplate('framework.administrator', undefined, context())
|
|
175
|
+
|
|
176
|
+
expect(role.name).toBe('ADMINISTRATOR')
|
|
177
|
+
expect(await privilegesOf(role.id)).toEqual(['board:mutation', 'board:query'])
|
|
178
|
+
})
|
|
179
|
+
})
|