@proteinjs/db 1.44.0 → 1.46.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/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/generated/test/index.js +1 -1
- package/dist/generated/test/index.js.map +1 -1
- package/dist/src/Table.d.ts +8 -0
- package/dist/src/Table.d.ts.map +1 -1
- package/dist/src/Table.js.map +1 -1
- package/dist/src/auth/TableAuth.d.ts +11 -1
- package/dist/src/auth/TableAuth.d.ts.map +1 -1
- package/dist/src/auth/TableAuth.js +39 -13
- package/dist/src/auth/TableAuth.js.map +1 -1
- package/dist/test/TableAuthIdentity.test.d.ts +2 -0
- package/dist/test/TableAuthIdentity.test.d.ts.map +1 -0
- package/dist/test/TableAuthIdentity.test.js +110 -0
- package/dist/test/TableAuthIdentity.test.js.map +1 -0
- package/generated/index.ts +1 -1
- package/generated/test/index.ts +1 -1
- package/package.json +2 -2
- package/src/Table.ts +8 -0
- package/src/auth/TableAuth.ts +45 -20
- package/test/TableAuthIdentity.test.ts +115 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/db",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.46.0",
|
|
4
4
|
"main": "./dist/generated/index.js",
|
|
5
5
|
"types": "./dist/generated/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"ts-jest": "29.1.1",
|
|
67
67
|
"typescript": "5.2.2"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "93bcd1aaf8d85abf75c20478e324b6b5323baf2f"
|
|
70
70
|
}
|
package/src/Table.ts
CHANGED
|
@@ -139,6 +139,14 @@ export abstract class Table<T extends Record> implements Loadable, CustomSeriali
|
|
|
139
139
|
* migration ledger reads most-recently-run first, never-run rows last.
|
|
140
140
|
*/
|
|
141
141
|
sort?: SortCriteria<T>[];
|
|
142
|
+
/**
|
|
143
|
+
* A table whose rows have their OWN page instead of the generic record form: the record table
|
|
144
|
+
* links each row here (replacing the generic form link), draws no New button and no selection
|
|
145
|
+
* Delete button, and the generic form page replace-navigates stale
|
|
146
|
+
* `/record/form?table=<name>&record=<id>` URLs to this link. Built from the id alone so the
|
|
147
|
+
* redirect never needs a row load.
|
|
148
|
+
*/
|
|
149
|
+
recordLink?: (row: Pick<T, 'id'>) => string;
|
|
142
150
|
};
|
|
143
151
|
};
|
|
144
152
|
public auth?: {
|
package/src/auth/TableAuth.ts
CHANGED
|
@@ -67,27 +67,19 @@ export class TableAuth {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Non-throwing read of ONE declared identity against the CURRENT user — the same grammar the
|
|
72
|
+
* table doors speak (`'public' | 'authenticated' | roles[] | { permission }`), for declared
|
|
73
|
+
* UI affordances that carry their own grant (a record form's declared panels, `Table.auth.ui`).
|
|
74
|
+
* An UNDECLARED identity is the default-deny made explicit: admin only. Fails closed (false)
|
|
75
|
+
* when no user can be resolved, like `canPerform`.
|
|
76
|
+
*/
|
|
77
|
+
identityAllows(identity: Identity | undefined): boolean {
|
|
78
|
+
try {
|
|
79
|
+
return identity === undefined ? UserAuth.hasRole('admin') : this.allows(identity);
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
78
82
|
}
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
tableAuth.all === 'public' ||
|
|
82
|
-
tableAuth[operation] === 'public' ||
|
|
83
|
-
(tableAuth.all === 'authenticated' && UserAuth.isLoggedIn()) ||
|
|
84
|
-
(tableAuth[operation] === 'authenticated' && UserAuth.isLoggedIn()) ||
|
|
85
|
-
(Array.isArray(tableAuth.all) && UserAuth.hasRoles(tableAuth.all, 'at least one')) ||
|
|
86
|
-
(Array.isArray(tableAuth[operation]) && UserAuth.hasRoles(tableAuth[operation] as string[], 'at least one')) ||
|
|
87
|
-
(isPermissionIdentity(tableAuth.all) && UserAuth.hasPermission(tableAuth.all.permission)) ||
|
|
88
|
-
(isPermissionIdentity(tableAuth[operation]) &&
|
|
89
|
-
UserAuth.hasPermission((tableAuth[operation] as PermissionIdentity).permission))
|
|
90
|
-
);
|
|
91
83
|
}
|
|
92
84
|
|
|
93
85
|
canQuery(table: Table<any>, api: 'db' | 'service' = 'db'): void {
|
|
@@ -113,4 +105,37 @@ export class TableAuth {
|
|
|
113
105
|
throw new TableAuthError(`User is not authorized to delete records from table: ${table.name}`);
|
|
114
106
|
}
|
|
115
107
|
}
|
|
108
|
+
|
|
109
|
+
private canAccess(
|
|
110
|
+
table: Table<any>,
|
|
111
|
+
api: 'db' | 'service',
|
|
112
|
+
operation: 'query' | 'insert' | 'update' | 'delete'
|
|
113
|
+
): boolean {
|
|
114
|
+
const tableAuth = table.auth ? table.auth[api] : undefined;
|
|
115
|
+
if (!tableAuth || Object.keys(tableAuth).length == 0) {
|
|
116
|
+
return UserAuth.hasRole('admin');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Within a declared block an undeclared operation stays closed to everyone (the block is
|
|
120
|
+
// the whole declaration) — `allows` reads undefined as false, so the two doors are exactly
|
|
121
|
+
// the ones the table wrote down.
|
|
122
|
+
return this.allows(tableAuth.all) || this.allows(tableAuth[operation]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** One identity against the current user; undefined (nothing declared) is not a grant. */
|
|
126
|
+
private allows(identity: Identity | undefined): boolean {
|
|
127
|
+
if (identity === 'public') {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
if (identity === 'authenticated') {
|
|
131
|
+
return UserAuth.isLoggedIn();
|
|
132
|
+
}
|
|
133
|
+
if (Array.isArray(identity)) {
|
|
134
|
+
return UserAuth.hasRoles(identity, 'at least one');
|
|
135
|
+
}
|
|
136
|
+
if (isPermissionIdentity(identity)) {
|
|
137
|
+
return UserAuth.hasPermission(identity.permission);
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
116
141
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { UserAuth } from '@proteinjs/user-auth';
|
|
2
|
+
import { Table } from '../src/Table';
|
|
3
|
+
import { withRecordColumns, Record } from '../src/Record';
|
|
4
|
+
import { StringColumn } from '../src/Columns';
|
|
5
|
+
import { TableAuth } from '../src/auth/TableAuth';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `TableAuth.identityAllows` — the one identity grammar (`'public' | 'authenticated' | roles[] |
|
|
9
|
+
* { permission }`) read for a DECLARED UI affordance that carries its own grant (a record form's
|
|
10
|
+
* declared panels, `Table.auth.ui`). Outcomes pinned:
|
|
11
|
+
* - each identity form resolves against the current user; 'admin' is break-glass throughout;
|
|
12
|
+
* - an UNDECLARED identity is admin-only (default-deny made explicit), never open;
|
|
13
|
+
* - it fails CLOSED when no user can be resolved;
|
|
14
|
+
* - the operation doors (`canPerform`) are byte-for-byte the pre-refactor semantics: within a
|
|
15
|
+
* declared block an undeclared operation stays closed to everyone, admin included.
|
|
16
|
+
*
|
|
17
|
+
* `UserAuth` reads from a static repo; tests stub it directly per identity — no server needed.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
interface Doc extends Record {
|
|
21
|
+
title: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class QueryOnlyTable extends Table<Doc> {
|
|
25
|
+
public name = 'identity_query_only_test';
|
|
26
|
+
public auth: Table<Doc>['auth'] = {
|
|
27
|
+
db: { query: 'authenticated' },
|
|
28
|
+
};
|
|
29
|
+
public columns = withRecordColumns<Doc>({ title: new StringColumn('title') });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const setUser = (roles: string[], email = 'user@test.local') => {
|
|
33
|
+
(UserAuth as any).userRepo = { getUser: () => ({ email, roles }) };
|
|
34
|
+
};
|
|
35
|
+
const setGuest = () => {
|
|
36
|
+
(UserAuth as any).userRepo = { getUser: () => ({ email: 'guest', roles: [] }) };
|
|
37
|
+
};
|
|
38
|
+
const setMapping = (mapping: { [permission: string]: string[] }) => {
|
|
39
|
+
(UserAuth as any).permissionRolesMapping = { getRoles: (permission: string) => mapping[permission] };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
describe('TableAuth.identityAllows', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
setMapping({ usage: ['usage'] });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
(UserAuth as any).userRepo = undefined;
|
|
49
|
+
(UserAuth as any).permissionRolesMapping = undefined;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("'public' admits anyone, including a guest", () => {
|
|
53
|
+
setGuest();
|
|
54
|
+
expect(new TableAuth().identityAllows('public')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("'authenticated' admits a signed-in user and refuses a guest", () => {
|
|
58
|
+
setUser([]);
|
|
59
|
+
expect(new TableAuth().identityAllows('authenticated')).toBe(true);
|
|
60
|
+
setGuest();
|
|
61
|
+
expect(new TableAuth().identityAllows('authenticated')).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('a roles list admits a holder of at least one role', () => {
|
|
65
|
+
setUser(['ops']);
|
|
66
|
+
expect(new TableAuth().identityAllows(['users', 'ops'])).toBe(true);
|
|
67
|
+
setUser(['dev']);
|
|
68
|
+
expect(new TableAuth().identityAllows(['users', 'ops'])).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('a permission identity resolves through the consumer mapping; admin is break-glass', () => {
|
|
72
|
+
setUser(['usage']);
|
|
73
|
+
expect(new TableAuth().identityAllows({ permission: 'usage' })).toBe(true);
|
|
74
|
+
setUser(['users']);
|
|
75
|
+
expect(new TableAuth().identityAllows({ permission: 'usage' })).toBe(false);
|
|
76
|
+
setUser(['admin']);
|
|
77
|
+
expect(new TableAuth().identityAllows({ permission: 'usage' })).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('an undeclared identity is admin-only, never open', () => {
|
|
81
|
+
setUser(['usage', 'users', 'ops']);
|
|
82
|
+
expect(new TableAuth().identityAllows(undefined)).toBe(false);
|
|
83
|
+
setUser(['admin']);
|
|
84
|
+
expect(new TableAuth().identityAllows(undefined)).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('fails closed when no user can be resolved', () => {
|
|
88
|
+
(UserAuth as any).userRepo = {
|
|
89
|
+
getUser: () => {
|
|
90
|
+
throw new Error('no session');
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
expect(new TableAuth().identityAllows('authenticated')).toBe(false);
|
|
94
|
+
expect(new TableAuth().identityAllows({ permission: 'usage' })).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('TableAuth operation doors keep their declared-block semantics', () => {
|
|
99
|
+
afterEach(() => {
|
|
100
|
+
(UserAuth as any).userRepo = undefined;
|
|
101
|
+
(UserAuth as any).permissionRolesMapping = undefined;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('a declared operation opens to its identity', () => {
|
|
105
|
+
setUser([]);
|
|
106
|
+
expect(new TableAuth().canPerform(new QueryOnlyTable(), 'query')).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('within a declared block an undeclared operation stays closed — admin included', () => {
|
|
110
|
+
setUser(['admin']);
|
|
111
|
+
expect(new TableAuth().canPerform(new QueryOnlyTable(), 'insert')).toBe(false);
|
|
112
|
+
setUser([]);
|
|
113
|
+
expect(new TableAuth().canPerform(new QueryOnlyTable(), 'insert')).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|