@proteinjs/db 1.50.1 → 1.51.0
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 +22 -0
- package/dist/generated/test/index.js +1 -1
- package/dist/src/auth/TableAuth.d.ts +9 -2
- package/dist/src/auth/TableAuth.d.ts.map +1 -1
- package/dist/src/auth/TableAuth.js +26 -11
- package/dist/src/auth/TableAuth.js.map +1 -1
- package/dist/src/services/DbService.d.ts +7 -0
- package/dist/src/services/DbService.d.ts.map +1 -1
- package/dist/src/services/DbService.js +13 -1
- package/dist/src/services/DbService.js.map +1 -1
- package/dist/test/DbServiceRetryDeclaration.test.d.ts +2 -0
- package/dist/test/DbServiceRetryDeclaration.test.d.ts.map +1 -0
- package/dist/test/DbServiceRetryDeclaration.test.js +139 -0
- package/dist/test/DbServiceRetryDeclaration.test.js.map +1 -0
- package/dist/test/TableAuthIdentity.test.js +134 -8
- package/dist/test/TableAuthIdentity.test.js.map +1 -1
- package/generated/test/index.ts +1 -1
- package/package.json +3 -3
- package/src/auth/TableAuth.ts +26 -11
- package/src/services/DbService.ts +13 -1
- package/test/DbServiceRetryDeclaration.test.ts +105 -0
- package/test/TableAuthIdentity.test.ts +129 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/db",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.51.0",
|
|
4
4
|
"main": "./dist/generated/index.js",
|
|
5
5
|
"types": "./dist/generated/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@proteinjs/reflection": "^1.2.0",
|
|
47
47
|
"@proteinjs/serializer": "^1.1.10",
|
|
48
48
|
"@proteinjs/server-api": "^3.0.11",
|
|
49
|
-
"@proteinjs/service": "^1.
|
|
49
|
+
"@proteinjs/service": "^1.8.0",
|
|
50
50
|
"@proteinjs/user-auth": "^1.3.0",
|
|
51
51
|
"@proteinjs/util": "^1.6.1",
|
|
52
52
|
"moment": "2.29.4",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"ts-jest": "29.1.1",
|
|
67
67
|
"typescript": "5.2.2"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "1128b1e6374c7f52cd5de172620b619b8f92e119"
|
|
70
70
|
}
|
package/src/auth/TableAuth.ts
CHANGED
|
@@ -138,26 +138,41 @@ export class TableAuth {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
// ...except that a db door the block leaves UNDECLARED mirrors the door the table's SERVICE
|
|
141
|
-
// block declares for the same operation
|
|
142
|
-
// (TableServiceAuth), then the inner Db's
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
141
|
+
// block declares for the same operation — and only where that door admits the admin role
|
|
142
|
+
// ALONE. The RPC runs both doors — the service gate (TableServiceAuth), then the inner Db's
|
|
143
|
+
// db-api re-check as the calling user — so an ['admin'] service write door with no db mirror
|
|
144
|
+
// admitted the admin at the gate and refused them behind it, in the gate's own words ("db
|
|
145
|
+
// doors mirror service doors" was a rule tables hand-copied; the owner derives it). Any wider
|
|
146
|
+
// service door — a permission, a non-admin role, 'authenticated', 'public' — leaves the
|
|
147
|
+
// undeclared db door shut: role privilege and the reach of a permission are different things,
|
|
148
|
+
// so a table that means a wider identity to write through the db api declares that door at
|
|
149
|
+
// the db block itself. One-way: a service block that omits the door keeps the lock (the
|
|
150
|
+
// session / audit-trail pattern — no write door on either api, break-glass included), and a
|
|
151
|
+
// table with no service block keeps its db block as written.
|
|
148
152
|
return api === 'db' && tableAuth.all === undefined && tableAuth[operation] === undefined
|
|
149
|
-
? this.
|
|
153
|
+
? this.mirrorsAdminOnlyServiceDoor(table, operation)
|
|
150
154
|
: false;
|
|
151
155
|
}
|
|
152
156
|
|
|
153
|
-
/**
|
|
154
|
-
|
|
157
|
+
/**
|
|
158
|
+
* The mirror: the service block's door for `operation` (its `all` and its own entry, as the RPC
|
|
159
|
+
* gate reads them) where every identity it declares is exactly `['admin']` admits the admin
|
|
160
|
+
* here too; a door declared any wider, or not at all, mirrors nothing. A table with no service
|
|
161
|
+
* block declares no door to mirror.
|
|
162
|
+
*/
|
|
163
|
+
private mirrorsAdminOnlyServiceDoor(table: Table<any>, operation: 'query' | 'insert' | 'update' | 'delete'): boolean {
|
|
155
164
|
const serviceAuth = table.auth?.service;
|
|
156
165
|
if (!serviceAuth || Object.keys(serviceAuth).length == 0) {
|
|
157
166
|
return false;
|
|
158
167
|
}
|
|
159
168
|
|
|
160
|
-
|
|
169
|
+
const door = [serviceAuth.all, serviceAuth[operation]].filter((identity) => identity !== undefined);
|
|
170
|
+
return door.length > 0 && door.every((identity) => this.isAdminOnly(identity)) && UserAuth.hasRole('admin');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** A roles list naming the admin role and nothing else. */
|
|
174
|
+
private isAdminOnly(identity: Identity | undefined): boolean {
|
|
175
|
+
return Array.isArray(identity) && identity.length === 1 && identity[0] === 'admin';
|
|
161
176
|
}
|
|
162
177
|
|
|
163
178
|
/** One identity against the current user; undefined (nothing declared) is not a grant. */
|
|
@@ -5,7 +5,19 @@ import { QueryBuilder } from '@proteinjs/db-query';
|
|
|
5
5
|
import { ArrayMembershipUpdate } from '../reference/ArrayMembershipOps';
|
|
6
6
|
import { PreservedPath } from '../UpdatePreserving';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The db service rides the service client's retry policy by declaration alone, no logic of its
|
|
10
|
+
* own: the reads (`query`, `get`, `getRowCount`, `tableExists`) are declared `'read'` — bounded at
|
|
11
|
+
* the first contact and redelivered under the client's series when a delivery produced no
|
|
12
|
+
* response — and every write stays undeclared: one delivery, never redelivered (a redelivered
|
|
13
|
+
* `insert` would insert twice). The policy's numbers and exceptions are @proteinjs/service's.
|
|
14
|
+
*/
|
|
15
|
+
export const getDbService = serviceFactory<DbService>('@proteinjs/db/DbService', undefined, {
|
|
16
|
+
query: 'read',
|
|
17
|
+
get: 'read',
|
|
18
|
+
getRowCount: 'read',
|
|
19
|
+
tableExists: 'read',
|
|
20
|
+
});
|
|
9
21
|
|
|
10
22
|
export type Query<T> = ObjectQuery<T> | QueryBuilder<T>;
|
|
11
23
|
export type ObjectQuery<T> = Partial<{ [P in keyof T]: any }>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Interface, Method, SourceRepository, TypeAliasDeclaration } from '@proteinjs/reflection';
|
|
2
|
+
import { READ_CONTACT_TIMEOUT_MS, REDELIVERY_BASE_MS } from '@proteinjs/service';
|
|
3
|
+
import { getDbService } from '../src/services/DbService';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The db service rides the service client's retry policy by declaration alone: its reads (`query`,
|
|
7
|
+
* `get`, `getRowCount`, `tableExists`) are declared `'read'` on the factory — bounded at the first
|
|
8
|
+
* contact and redelivered as fresh requests under the client's series when a delivery produced no
|
|
9
|
+
* response — and its writes (`insert`, `update`, `updateArrayMembership`, `updatePreserving`,
|
|
10
|
+
* `delete`) stay undeclared: one delivery, no bound, never redelivered. Nothing here is the db
|
|
11
|
+
* package's own logic — the policy, its numbers and its exceptions are @proteinjs/service's; this
|
|
12
|
+
* suite pins the declaration. Read as OUTCOMES off the transport: the requests that left and their
|
|
13
|
+
* watchdog signals, under fake timers.
|
|
14
|
+
*
|
|
15
|
+
* The service interface is seeded into the source repository directly (this package's own test pattern —
|
|
16
|
+
* DbDefaultDriverDuplicateModuleState seeds the object cache the same way), so the real factory
|
|
17
|
+
* builds the real clients without the generated source graph.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
type RepositoryInternals = { flattenedSourceGraph: { interfaces: { [qualifiedName: string]: Interface } } };
|
|
21
|
+
|
|
22
|
+
const READS = ['query', 'get', 'getRowCount', 'tableExists'];
|
|
23
|
+
const WRITES = ['insert', 'update', 'updateArrayMembership', 'updatePreserving', 'delete'];
|
|
24
|
+
|
|
25
|
+
beforeAll(() => {
|
|
26
|
+
// Node's Request rejects the relative service paths a browser resolves against the page origin.
|
|
27
|
+
global.Request = class {
|
|
28
|
+
constructor(
|
|
29
|
+
public url: string,
|
|
30
|
+
public init: any
|
|
31
|
+
) {}
|
|
32
|
+
} as any;
|
|
33
|
+
const returnType = { name: 'Promise<any>' } as unknown as TypeAliasDeclaration;
|
|
34
|
+
const methods = [...READS, ...WRITES].map(
|
|
35
|
+
(name) => new Method(name, returnType, true, false, false, false, 'public', [])
|
|
36
|
+
);
|
|
37
|
+
(SourceRepository.get() as unknown as RepositoryInternals).flattenedSourceGraph.interfaces[
|
|
38
|
+
'@proteinjs/db/DbService'
|
|
39
|
+
] = new Interface('@proteinjs/db', 'DbService', [], methods);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** A transport that never answers: its promise settles only when the request is abandoned (its signal aborts). */
|
|
43
|
+
const neverAnswers = () =>
|
|
44
|
+
jest.fn(
|
|
45
|
+
(request: any) =>
|
|
46
|
+
new Promise((unused, reject) => {
|
|
47
|
+
request.init.signal?.addEventListener('abort', () =>
|
|
48
|
+
reject(Object.assign(new Error('The operation was aborted.'), { name: 'AbortError' }))
|
|
49
|
+
);
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const sentRequests = (): any[] => (global.fetch as jest.Mock).mock.calls.map(([request]) => request);
|
|
54
|
+
|
|
55
|
+
/** The call's outcome, readable without awaiting it — a pending call must read as pending. */
|
|
56
|
+
const track = (promise: Promise<any>) => {
|
|
57
|
+
const outcome: { state: 'pending' | 'resolved' | 'rejected' } = { state: 'pending' };
|
|
58
|
+
promise.then(
|
|
59
|
+
() => {
|
|
60
|
+
outcome.state = 'resolved';
|
|
61
|
+
},
|
|
62
|
+
() => {
|
|
63
|
+
outcome.state = 'rejected';
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
return outcome;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const aTable = { name: 'a_table' } as any;
|
|
70
|
+
|
|
71
|
+
describe('the db service under the client retry policy', () => {
|
|
72
|
+
beforeEach(() => jest.useFakeTimers());
|
|
73
|
+
afterEach(() => jest.useRealTimers());
|
|
74
|
+
|
|
75
|
+
it.each(READS)(
|
|
76
|
+
'%s is a declared read: bounded at the first contact, redelivered as a fresh request',
|
|
77
|
+
async (name) => {
|
|
78
|
+
global.fetch = neverAnswers() as any;
|
|
79
|
+
const service = getDbService() as any;
|
|
80
|
+
const outcome = track(service[name](aTable, {}));
|
|
81
|
+
|
|
82
|
+
await jest.advanceTimersByTimeAsync(READ_CONTACT_TIMEOUT_MS);
|
|
83
|
+
expect(sentRequests()).toHaveLength(1);
|
|
84
|
+
expect(sentRequests()[0].url).toBe(`/service/@proteinjs/db/DbService/${name}`);
|
|
85
|
+
expect(sentRequests()[0].init.signal.aborted).toBe(true);
|
|
86
|
+
|
|
87
|
+
await jest.advanceTimersByTimeAsync(REDELIVERY_BASE_MS);
|
|
88
|
+
expect(sentRequests()).toHaveLength(2);
|
|
89
|
+
expect(sentRequests()[1].init.signal.aborted).toBe(false);
|
|
90
|
+
expect(outcome.state).toBe('pending');
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
it.each(WRITES)('%s is undeclared: one delivery, no bound, never redelivered', async (name) => {
|
|
95
|
+
global.fetch = neverAnswers() as any;
|
|
96
|
+
const service = getDbService() as any;
|
|
97
|
+
const outcome = track(service[name](aTable, {}));
|
|
98
|
+
|
|
99
|
+
await jest.advanceTimersByTimeAsync(READ_CONTACT_TIMEOUT_MS * 4);
|
|
100
|
+
expect(sentRequests()).toHaveLength(1);
|
|
101
|
+
expect(sentRequests()[0].url).toBe(`/service/@proteinjs/db/DbService/${name}`);
|
|
102
|
+
expect(sentRequests()[0].init.signal).toBeUndefined();
|
|
103
|
+
expect(outcome.state).toBe('pending');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -13,7 +13,8 @@ import { TableAuth } from '../src/auth/TableAuth';
|
|
|
13
13
|
* - it fails CLOSED when no user can be resolved;
|
|
14
14
|
* - the operation doors (`canPerform`) keep the declared-block semantics: within a declared block
|
|
15
15
|
* an undeclared operation stays closed to everyone, admin included — unless the table's SERVICE
|
|
16
|
-
* block names that door, which the db door then mirrors (the last
|
|
16
|
+
* block names that door for the admin role alone, which the db door then mirrors (the last two
|
|
17
|
+
* describes).
|
|
17
18
|
*
|
|
18
19
|
* `UserAuth` reads from a static repo; tests stub it directly per identity — no server needed.
|
|
19
20
|
*/
|
|
@@ -118,13 +119,15 @@ describe('TableAuth operation doors keep their declared-block semantics', () =>
|
|
|
118
119
|
/**
|
|
119
120
|
* The RPC runs BOTH doors — the service gate (`TableServiceAuth`) and then the inner `Db`'s
|
|
120
121
|
* db-api re-check as the calling user — so a db door a table leaves UNDECLARED mirrors the door
|
|
121
|
-
* its service block declares for the same operation
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
122
|
+
* its service block declares for the same operation, where that door admits the admin role ALONE
|
|
123
|
+
* (declared exactly `['admin']`). "db doors mirror service doors" was a rule tables hand-copied;
|
|
124
|
+
* the owner derives it now: consumer tables opened `['admin']` writes on the service api over a
|
|
125
|
+
* query-only db block, and every one refused the admin at the inner check in the gate's own words
|
|
126
|
+
* — the gate admitted them, the re-check behind it did not, two verdicts for one caller. One-way:
|
|
127
|
+
* a service block that omits the door keeps the lock (the session / audit-trail pattern — no
|
|
128
|
+
* write door on either api, break-glass included), and a table with no service block keeps its
|
|
129
|
+
* db block as written (the pin above). A service door declared any wider than the admin role is
|
|
130
|
+
* the next describe's: it mirrors nothing.
|
|
128
131
|
*/
|
|
129
132
|
class ReadOpenWriteAdminTable extends Table<Doc> {
|
|
130
133
|
public name = 'identity_read_open_write_admin_test';
|
|
@@ -144,7 +147,7 @@ class LockedLedgerTable extends Table<Doc> {
|
|
|
144
147
|
public columns = withRecordColumns<Doc>({ title: new StringColumn('title') });
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
describe(
|
|
150
|
+
describe("TableAuth — an undeclared db door mirrors the declared ['admin'] service door", () => {
|
|
148
151
|
beforeEach(() => {
|
|
149
152
|
setMapping({ ledger: ['ledger'] });
|
|
150
153
|
});
|
|
@@ -206,3 +209,120 @@ describe('TableAuth — an undeclared db door mirrors the declared service door'
|
|
|
206
209
|
);
|
|
207
210
|
});
|
|
208
211
|
});
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Role privilege and the reach of a permission are different things: the mirror carries the
|
|
215
|
+
* admin's break-glass across the two apis, never a wider identity. The service block here opens
|
|
216
|
+
* three writes to identities wider than the admin role — a permission, a non-admin role, every
|
|
217
|
+
* signed-in user — over a query-only db block, and none of them is mirrored: the gate admits the
|
|
218
|
+
* holder and the db door behind it stays shut, exactly as before the mirror existed. A table that
|
|
219
|
+
* means a wider identity to write through the db api declares that door at the db block itself.
|
|
220
|
+
*/
|
|
221
|
+
class WiderServiceDoorsTable extends Table<Doc> {
|
|
222
|
+
public name = 'identity_wider_service_doors_test';
|
|
223
|
+
public auth: Table<Doc>['auth'] = {
|
|
224
|
+
db: { query: { permission: 'ledger' } },
|
|
225
|
+
service: {
|
|
226
|
+
query: { permission: 'ledger' },
|
|
227
|
+
update: { permission: 'ledger' },
|
|
228
|
+
insert: ['ledger'],
|
|
229
|
+
delete: 'authenticated',
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
public columns = withRecordColumns<Doc>({ title: new StringColumn('title') });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A service write door the admin role shares with another role: not the admin role alone. */
|
|
236
|
+
class SharedRoleDoorTable extends Table<Doc> {
|
|
237
|
+
public name = 'identity_shared_role_door_test';
|
|
238
|
+
public auth: Table<Doc>['auth'] = {
|
|
239
|
+
db: { query: 'authenticated' },
|
|
240
|
+
service: { query: 'authenticated', update: ['admin', 'ledger'] },
|
|
241
|
+
};
|
|
242
|
+
public columns = withRecordColumns<Doc>({ title: new StringColumn('title') });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The admin-only door named by `all`: the same door, read the way the gate reads it. */
|
|
246
|
+
class AllAdminServiceTable extends Table<Doc> {
|
|
247
|
+
public name = 'identity_all_admin_service_test';
|
|
248
|
+
public auth: Table<Doc>['auth'] = {
|
|
249
|
+
db: { query: 'authenticated' },
|
|
250
|
+
service: { all: ['admin'] },
|
|
251
|
+
};
|
|
252
|
+
public columns = withRecordColumns<Doc>({ title: new StringColumn('title') });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** An `['admin']` entry beside an `all` that widens the door to every signed-in user: not admin alone. */
|
|
256
|
+
class WidenedByAllTable extends Table<Doc> {
|
|
257
|
+
public name = 'identity_widened_by_all_test';
|
|
258
|
+
public auth: Table<Doc>['auth'] = {
|
|
259
|
+
db: { query: 'authenticated' },
|
|
260
|
+
service: { all: 'authenticated', update: ['admin'] },
|
|
261
|
+
};
|
|
262
|
+
public columns = withRecordColumns<Doc>({ title: new StringColumn('title') });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
describe('TableAuth — the mirror reaches only a service door that admits the admin role alone', () => {
|
|
266
|
+
beforeEach(() => {
|
|
267
|
+
setMapping({ ledger: ['ledger'] });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
afterEach(() => {
|
|
271
|
+
(UserAuth as any).userRepo = undefined;
|
|
272
|
+
(UserAuth as any).permissionRolesMapping = undefined;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('a permission-declared service write with no db door stays shut for the permission holder — the gate admits, the db door does not', () => {
|
|
276
|
+
setUser(['ledger']);
|
|
277
|
+
const table = new WiderServiceDoorsTable();
|
|
278
|
+
expect(new TableAuth().canPerform(table, 'update', 'service')).toBe(true);
|
|
279
|
+
expect(new TableAuth().canPerform(table, 'update', 'db')).toBe(false);
|
|
280
|
+
expect(() => new TableAuth().canUpdate(table)).toThrow(
|
|
281
|
+
'User is not authorized to update records in table: identity_wider_service_doors_test'
|
|
282
|
+
);
|
|
283
|
+
expect(() => new TableAuth().canQuery(table)).not.toThrow();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("a non-admin role door and an 'authenticated' door are as shut on the db api as the permission door", () => {
|
|
287
|
+
setUser(['ledger']);
|
|
288
|
+
const table = new WiderServiceDoorsTable();
|
|
289
|
+
expect(new TableAuth().canPerform(table, 'insert', 'service')).toBe(true);
|
|
290
|
+
expect(new TableAuth().canPerform(table, 'insert', 'db')).toBe(false);
|
|
291
|
+
expect(new TableAuth().canPerform(table, 'delete', 'service')).toBe(true);
|
|
292
|
+
expect(new TableAuth().canPerform(table, 'delete', 'db')).toBe(false);
|
|
293
|
+
expect(() => new TableAuth().canInsert(table)).toThrow(
|
|
294
|
+
'User is not authorized to insert records into table: identity_wider_service_doors_test'
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('a wider service door opens nothing on the db api for the admin either — shut as it was before the mirror', () => {
|
|
299
|
+
setUser(['admin']);
|
|
300
|
+
const table = new WiderServiceDoorsTable();
|
|
301
|
+
expect(new TableAuth().canPerform(table, 'update', 'db')).toBe(false);
|
|
302
|
+
expect(new TableAuth().canPerform(table, 'insert', 'db')).toBe(false);
|
|
303
|
+
expect(new TableAuth().canPerform(table, 'delete', 'db')).toBe(false);
|
|
304
|
+
expect(() => new TableAuth().canUpdate(table)).toThrow(
|
|
305
|
+
'User is not authorized to update records in table: identity_wider_service_doors_test'
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('a door the admin role shares with another role is not the admin role alone: shut on the db api for both', () => {
|
|
310
|
+
const table = new SharedRoleDoorTable();
|
|
311
|
+
setUser(['admin']);
|
|
312
|
+
expect(new TableAuth().canPerform(table, 'update', 'service')).toBe(true);
|
|
313
|
+
expect(new TableAuth().canPerform(table, 'update', 'db')).toBe(false);
|
|
314
|
+
setUser(['ledger']);
|
|
315
|
+
expect(new TableAuth().canPerform(table, 'update', 'service')).toBe(true);
|
|
316
|
+
expect(new TableAuth().canPerform(table, 'update', 'db')).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("the admin-only door may be named by `all`; an `all` that widens the door beside an ['admin'] entry mirrors nothing", () => {
|
|
320
|
+
setUser(['admin']);
|
|
321
|
+
expect(new TableAuth().canPerform(new AllAdminServiceTable(), 'update', 'db')).toBe(true);
|
|
322
|
+
expect(() => new TableAuth().canUpdate(new AllAdminServiceTable())).not.toThrow();
|
|
323
|
+
expect(new TableAuth().canPerform(new WidenedByAllTable(), 'update', 'service')).toBe(true);
|
|
324
|
+
expect(new TableAuth().canPerform(new WidenedByAllTable(), 'update', 'db')).toBe(false);
|
|
325
|
+
setUser(['ledger']);
|
|
326
|
+
expect(new TableAuth().canPerform(new AllAdminServiceTable(), 'update', 'db')).toBe(false);
|
|
327
|
+
});
|
|
328
|
+
});
|