-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy path4-patch-one-acl-check.spec.ts
More file actions
264 lines (238 loc) · 10.2 KB
/
4-patch-one-acl-check.spec.ts
File metadata and controls
264 lines (238 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
/**
* ACL: PATCH One Resource - Update Permission and Field-Level Security
*
* This test suite verifies ACL enforcement for updating resources. It tests complex
* permission scenarios including field-level restrictions and conditional value validation.
*
* 1. Admin Role: Full update access without conditions
* - Can update any resource with any field values
*
* 2. Moderator Role: Complex field and value restrictions
* - CANNOT update published articles (status='published') - returns 403 Forbidden
* - CANNOT update 'title' field in non-published articles - returns 403 Forbidden
* - CANNOT set status to 'published' - returns 403 Forbidden
* - CAN set status to 'review' for non-published articles
* - Field-level and value-level ACL enforced
*
* 3. User Role: Owner-based conditional update access
* a) coAuthor scenario:
* - CANNOT add new coAuthorIds - returns 403 Forbidden
* - CANNOT modify coAuthorIds while keeping themselves - returns 403 Forbidden
* - CAN remove themselves from coAuthorIds
* b) Author scenario:
* - CAN update own articles
* - Row-level security enforced (only own resources)
*/
import {
ArticleAcl,
ArticleStatus,
ContextTestAcl,
UserRole,
UsersAcl,
} from '@nestjs-json-api/microorm-database/entity';
import { JsonSdkPromise } from '@klerick/json-api-nestjs-sdk';
import { AxiosError } from 'axios';
import { creatSdk } from '../utils/run-application';
import { AbilityBuilder, CheckFieldAndInclude } from '../utils/acl/acl';
describe('ACL: PATCH One Resource (Update Operations)', () => {
let contextTestAcl = new ContextTestAcl();
let usersAcl: UsersAcl[];
let articleAcl: ArticleAcl[];
contextTestAcl.aclRules = { rules: [] };
contextTestAcl.context = {};
let jsonSdk: JsonSdkPromise;
beforeEach(async () => {
jsonSdk = creatSdk();
contextTestAcl = await jsonSdk.jsonApiSdkService.postOne(contextTestAcl);
usersAcl = await jsonSdk.jsonApiSdkService.getAll(UsersAcl, {
include: ['profile'],
});
articleAcl = await jsonSdk.jsonApiSdkService.getAll(ArticleAcl, {
include: ['author', 'editor'],
});
});
afterEach(async () => {
await jsonSdk.jsonApiSdkService.deleteOne(contextTestAcl);
});
describe('Admin Role: Full Update Access Without Restrictions', () => {
let articleForUpdate: ArticleAcl;
beforeEach(async () => {
const adminUser = usersAcl.find((user) => user.login === 'admin');
if (!adminUser) throw new Error('Daphne user not found');
const posibleArticle = articleAcl.find(
(i) => i.author.login !== 'bob' && i.author.login !== 'alice'
);
if (!posibleArticle) throw new Error('article not found');
articleForUpdate = posibleArticle;
contextTestAcl.context = { currentUser: adminUser };
contextTestAcl.aclRules.rules = new AbilityBuilder(
CheckFieldAndInclude
).permissionsFor(UserRole.admin).rules as any;
await jsonSdk.jsonApiSdkService.patchOne(contextTestAcl);
});
it('should update any article with any field values (no ACL restrictions)', async () => {
articleForUpdate.title = 'new title';
await jsonSdk.jsonApiSdkService.patchOne(articleForUpdate);
});
});
describe('Moderator Role: Field and Value-Level Restrictions', () => {
let articlePublishForUpdate: ArticleAcl;
let articleNoPublishForUpdate: ArticleAcl;
beforeEach(async () => {
const moderatorUser = usersAcl.find((user) => user.login === 'moderator');
const posiblePublishArticle = articleAcl.find(
(item) => item.status === 'published'
);
if (!posiblePublishArticle) throw new Error('article not found');
articlePublishForUpdate = posiblePublishArticle;
const posibleNpPublishArticle = articleAcl.find(
(item) => item.status !== 'published' && item.status !== 'review'
);
if (!posibleNpPublishArticle) throw new Error('article not found');
articleNoPublishForUpdate = posibleNpPublishArticle;
if (!moderatorUser) throw new Error('Sheila user not found');
contextTestAcl.context = { currentUser: moderatorUser };
contextTestAcl.aclRules.rules = new AbilityBuilder(
CheckFieldAndInclude
).permissionsFor(UserRole.moderator).rules as any;
await jsonSdk.jsonApiSdkService.patchOne(contextTestAcl);
});
it('should return 403 Forbidden when attempting to update published article', async () => {
try {
await jsonSdk.jsonApiSdkService.patchOne(articlePublishForUpdate);
assert.fail('should be error');
} catch (e) {
expect(e).toBeInstanceOf(AxiosError);
expect((e as AxiosError).response?.status).toBe(403);
}
});
it('should return 403 Forbidden when attempting to update restricted field (title) in non-published article', async () => {
const oldTitle = articleNoPublishForUpdate.title;
try {
articleNoPublishForUpdate.title = 'new title';
await jsonSdk.jsonApiSdkService.patchOne(articleNoPublishForUpdate);
assert.fail('should be error');
} catch (e) {
expect(e).toBeInstanceOf(AxiosError);
expect((e as AxiosError).response?.status).toBe(403);
articleNoPublishForUpdate.title = oldTitle;
}
});
it('should return 403 Forbidden when attempting to set status to forbidden value (published)', async () => {
try {
articleNoPublishForUpdate.status = ArticleStatus.PUBLISHED;
await jsonSdk.jsonApiSdkService.patchOne(articleNoPublishForUpdate);
assert.fail('should be error');
} catch (e) {
expect(e).toBeInstanceOf(AxiosError);
expect((e as AxiosError).response?.status).toBe(403);
}
});
it('should update non-published article with allowed field (status) and allowed value (review)', async () => {
articleNoPublishForUpdate.status = ArticleStatus.REVIEW;
// @ts-ignore
delete articleNoPublishForUpdate.author;
await jsonSdk.jsonApiSdkService.patchOne(articleNoPublishForUpdate);
});
});
describe('User Role: Owner-Based Conditional Update Access', () => {
let aliceUser: UsersAcl;
let bobUser: UsersAcl;
let articleForUpdate: ArticleAcl;
let articleForUpdateAlice: ArticleAcl;
beforeEach(async () => {
const posibleAliceUser = usersAcl.find((user) => user.login === 'alice');
if (!posibleAliceUser) throw new Error('bob user not found');
aliceUser = posibleAliceUser;
const posibleBobUser = usersAcl.find((user) => user.login === 'bob');
if (!posibleBobUser) throw new Error('Bob user not found');
bobUser = posibleBobUser;
const listAliceArticleForUpdate = await jsonSdk.jsonApiSdkService.getAll(
ArticleAcl,
{
filter: {
author: { id: { eq: aliceUser.id.toString() } },
},
include: ['author'],
}
);
const posibleAliceArticle = listAliceArticleForUpdate.at(0);
if (!posibleAliceArticle) throw new Error('article not found');
articleForUpdateAlice = posibleAliceArticle;
});
describe('coAuthor Scenario: Can Only Remove Self from coAuthorIds', () => {
beforeEach(async () => {
contextTestAcl.context = { currentUser: bobUser };
contextTestAcl.aclRules.rules = new AbilityBuilder(
CheckFieldAndInclude
).permissionsFor(UserRole.user).rules as any;
await jsonSdk.jsonApiSdkService.patchOne(contextTestAcl);
const listArticleForUpdate = await jsonSdk.jsonApiSdkService.getAll(
ArticleAcl,
{
filter: {
author: { id: { eq: aliceUser.id.toString() } },
target: { coAuthorIds: { some: [bobUser.id.toString()] } },
},
include: ['author'],
}
);
const posibleArticle = listArticleForUpdate.at(0);
if (!posibleArticle) throw new Error('article not found');
articleForUpdate = posibleArticle;
});
it('should return 403 Forbidden when adding new coAuthorIds while keeping self', async () => {
const save = articleForUpdate.coAuthorIds;
try {
articleForUpdate.coAuthorIds = [...articleForUpdate.coAuthorIds, 6];
// @ts-ignore
delete articleForUpdate.author;
await jsonSdk.jsonApiSdkService.patchOne(articleForUpdate);
assert.fail('should be error');
} catch (e) {
expect(e).toBeInstanceOf(AxiosError);
expect((e as AxiosError).response?.status).toBe(403);
articleForUpdate.coAuthorIds = save;
}
});
it('should return 403 Forbidden when modifying coAuthorIds with new ids even after removing self', async () => {
const save = articleForUpdate.coAuthorIds;
try {
articleForUpdate.coAuthorIds = [
...articleForUpdate.coAuthorIds.filter((i) => i !== bobUser.id),
6,
];
// @ts-ignore
delete articleForUpdate.author;
await jsonSdk.jsonApiSdkService.patchOne(articleForUpdate);
assert.fail('should be error');
} catch (e) {
expect(e).toBeInstanceOf(AxiosError);
expect((e as AxiosError).response?.status).toBe(403);
articleForUpdate.coAuthorIds = save;
}
});
it('should update article when coAuthor removes only themselves from coAuthorIds', async () => {
articleForUpdate.coAuthorIds = articleForUpdate.coAuthorIds.filter(
(i) => i !== bobUser.id
);
// @ts-ignore
delete articleForUpdate.author;
await jsonSdk.jsonApiSdkService.patchOne(articleForUpdate);
});
});
describe('Author Scenario: Can Update Own Articles', () => {
beforeEach(async () => {
contextTestAcl.context = { currentUser: aliceUser };
contextTestAcl.aclRules.rules = new AbilityBuilder(
CheckFieldAndInclude
).permissionsFor(UserRole.user).rules as any;
await jsonSdk.jsonApiSdkService.patchOne(contextTestAcl);
});
it('should update own article (alice updating alice article)', async () => {
articleForUpdateAlice.title = 'new Title';
await jsonSdk.jsonApiSdkService.patchOne(articleForUpdateAlice);
});
});
});
});