@sasjs/api 0.2.0 → 0.3.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/README.md +28 -8
- package/build/browser.js +810 -4
- package/build/cjs/corpus/drive.js +20 -0
- package/build/cjs/corpus/identity.js +268 -0
- package/build/cjs/corpus/runner.js +11 -2
- package/build/cjs/drive/service.js +8 -0
- package/build/cjs/host/dispatch.js +57 -2
- package/build/cjs/identity/fixture.js +118 -0
- package/build/cjs/identity/memory-store.js +179 -0
- package/build/cjs/identity/service.js +142 -0
- package/build/cjs/identity/types.js +11 -0
- package/build/cjs/index.js +11 -1
- package/build/cjs/routes/generated.js +72 -0
- package/build/cjs/routes/index.js +6 -0
- package/build/cjs/routes/overlay.js +38 -0
- package/build/esm/corpus/drive.d.ts +14 -0
- package/build/esm/corpus/drive.d.ts.map +1 -1
- package/build/esm/corpus/drive.js +20 -0
- package/build/esm/corpus/identity.d.ts +17 -0
- package/build/esm/corpus/identity.d.ts.map +1 -0
- package/build/esm/corpus/identity.js +265 -0
- package/build/esm/corpus/runner.d.ts +6 -1
- package/build/esm/corpus/runner.d.ts.map +1 -1
- package/build/esm/corpus/runner.js +11 -2
- package/build/esm/drive/service.d.ts.map +1 -1
- package/build/esm/drive/service.js +9 -1
- package/build/esm/host/dispatch.d.ts +16 -0
- package/build/esm/host/dispatch.d.ts.map +1 -1
- package/build/esm/host/dispatch.js +57 -2
- package/build/esm/identity/fixture.d.ts +84 -0
- package/build/esm/identity/fixture.d.ts.map +1 -0
- package/build/esm/identity/fixture.js +114 -0
- package/build/esm/identity/memory-store.d.ts +51 -0
- package/build/esm/identity/memory-store.d.ts.map +1 -0
- package/build/esm/identity/memory-store.js +175 -0
- package/build/esm/identity/service.d.ts +48 -0
- package/build/esm/identity/service.d.ts.map +1 -0
- package/build/esm/identity/service.js +137 -0
- package/build/esm/identity/types.d.ts +79 -0
- package/build/esm/identity/types.d.ts.map +1 -0
- package/build/esm/identity/types.js +10 -0
- package/build/esm/index.d.ts +7 -0
- package/build/esm/index.d.ts.map +1 -1
- package/build/esm/index.js +4 -0
- package/build/esm/routes/generated.d.ts.map +1 -1
- package/build/esm/routes/generated.js +72 -0
- package/build/esm/routes/index.d.ts.map +1 -1
- package/build/esm/routes/index.js +6 -0
- package/build/esm/routes/overlay.d.ts +9 -0
- package/build/esm/routes/overlay.d.ts.map +1 -1
- package/build/esm/routes/overlay.js +38 -0
- package/build/esm/types.d.ts +7 -0
- package/build/esm/types.d.ts.map +1 -1
- package/package.json +9 -3
|
@@ -238,6 +238,26 @@ exports.DRIVE_CORPUS = [
|
|
|
238
238
|
request: { body: { oldPath: '/data/nope.csv', newPath: '/data/x.csv' } },
|
|
239
239
|
expect: { status: 404, error: 'No file/folder found for provided path.' }
|
|
240
240
|
},
|
|
241
|
+
{
|
|
242
|
+
id: 'rename-file-to-blocked-extension',
|
|
243
|
+
route: 'POST /SASjsApi/drive/rename',
|
|
244
|
+
description: 'refuses a rename that would produce a name the file routes refuse',
|
|
245
|
+
seed: { '/data/report.csv': 'a,b' },
|
|
246
|
+
request: {
|
|
247
|
+
body: { oldPath: '/data/report.csv', newPath: '/data/report.sh' }
|
|
248
|
+
},
|
|
249
|
+
expect: { status: 400, error: 'Invalid file extension' }
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: 'rename-folder-to-dotted-name',
|
|
253
|
+
route: 'POST /SASjsApi/drive/rename',
|
|
254
|
+
description: 'renames a folder whose new name carries a blocked suffix',
|
|
255
|
+
seed: { '/data/reports/a.csv': 'a' },
|
|
256
|
+
request: {
|
|
257
|
+
body: { oldPath: '/data/reports', newPath: '/data/reports.sh' }
|
|
258
|
+
},
|
|
259
|
+
expect: { status: 200, body: { status: 'success' } }
|
|
260
|
+
},
|
|
241
261
|
{
|
|
242
262
|
id: 'rename-folder-into-itself',
|
|
243
263
|
route: 'POST /SASjsApi/drive/rename',
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IDENTITY_CORPUS = exports.DEACTIVATED = exports.SIGNED_IN = void 0;
|
|
4
|
+
const fixture_js_1 = require("../identity/fixture.js");
|
|
5
|
+
/** The default fixture, plus a caller that is already signed in. */
|
|
6
|
+
exports.SIGNED_IN = {
|
|
7
|
+
...fixture_js_1.DEFAULT_IDENTITY,
|
|
8
|
+
tokens: [
|
|
9
|
+
{
|
|
10
|
+
username: 'user2',
|
|
11
|
+
clientId: 'web_app',
|
|
12
|
+
accessToken: 'seed-access',
|
|
13
|
+
refreshToken: 'seed-refresh'
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
username: 'admin',
|
|
17
|
+
clientId: 'removed_client',
|
|
18
|
+
accessToken: 'seed-access-orphan',
|
|
19
|
+
refreshToken: 'seed-refresh-orphan'
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
codes: [
|
|
23
|
+
{ code: 'seed-code', username: 'user2', clientId: 'web_app' },
|
|
24
|
+
{ code: 'seed-orphan-code', username: 'user2', clientId: 'removed_client' },
|
|
25
|
+
// the same shape with a pair already issued: the exchange must still refuse,
|
|
26
|
+
// rather than handing back tokens that belong to a client that is gone
|
|
27
|
+
{
|
|
28
|
+
code: 'seed-orphan-pair-code',
|
|
29
|
+
username: 'admin',
|
|
30
|
+
clientId: 'removed_client'
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
};
|
|
34
|
+
/** The same identity, with the restricted user switched off. */
|
|
35
|
+
exports.DEACTIVATED = {
|
|
36
|
+
...exports.SIGNED_IN,
|
|
37
|
+
users: exports.SIGNED_IN.users.map((user) => user.username === 'user2' ? { ...user, isActive: false } : user)
|
|
38
|
+
};
|
|
39
|
+
/** The user the seeded session belongs to, without the host's own identifier. */
|
|
40
|
+
const user2 = {
|
|
41
|
+
username: 'user2',
|
|
42
|
+
displayName: 'Demo User',
|
|
43
|
+
isAdmin: false,
|
|
44
|
+
needsToUpdatePassword: false
|
|
45
|
+
};
|
|
46
|
+
exports.IDENTITY_CORPUS = [
|
|
47
|
+
{
|
|
48
|
+
id: 'login',
|
|
49
|
+
route: 'POST /SASLogon/login',
|
|
50
|
+
description: 'signs a seeded user in',
|
|
51
|
+
request: { body: { username: 'admin', password: 'admin123' } },
|
|
52
|
+
expect: {
|
|
53
|
+
status: 200,
|
|
54
|
+
partial: {
|
|
55
|
+
loggedIn: true,
|
|
56
|
+
user: {
|
|
57
|
+
username: 'admin',
|
|
58
|
+
displayName: 'Demo Admin',
|
|
59
|
+
isAdmin: true,
|
|
60
|
+
needsToUpdatePassword: false
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'login-missing-username',
|
|
67
|
+
route: 'POST /SASLogon/login',
|
|
68
|
+
description: 'refuses a sign-in with no username as 400, naming the field',
|
|
69
|
+
request: { body: { password: 'admin123' } },
|
|
70
|
+
expect: { status: 400, error: '"username" is required' }
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'login-deactivated',
|
|
74
|
+
route: 'POST /SASLogon/login',
|
|
75
|
+
description: 'refuses a deactivated account, naming it',
|
|
76
|
+
identity: exports.DEACTIVATED,
|
|
77
|
+
request: { body: { username: 'user2', password: 'user123' } },
|
|
78
|
+
expect: { status: 401, error: 'This account is not active.' }
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'login-wrong-password',
|
|
82
|
+
route: 'POST /SASLogon/login',
|
|
83
|
+
description: 'refuses a wrong password as 401, naming it',
|
|
84
|
+
request: { body: { username: 'admin', password: 'wrong123' } },
|
|
85
|
+
expect: { status: 401, error: 'Invalid Password.' }
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'login-unknown-user',
|
|
89
|
+
route: 'POST /SASLogon/login',
|
|
90
|
+
description: 'refuses an unknown username as 401, naming it',
|
|
91
|
+
request: { body: { username: 'nobody', password: 'admin123' } },
|
|
92
|
+
expect: { status: 401, error: 'Username is not found.' }
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'authorize',
|
|
96
|
+
route: 'POST /SASLogon/authorize',
|
|
97
|
+
description: 'issues a code to a signed-in caller',
|
|
98
|
+
identity: exports.SIGNED_IN,
|
|
99
|
+
request: {
|
|
100
|
+
token: 'seed-access',
|
|
101
|
+
body: { clientId: 'web_app' }
|
|
102
|
+
},
|
|
103
|
+
expect: { status: 200, keys: ['code'] }
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'authorize-missing-client',
|
|
107
|
+
route: 'POST /SASLogon/authorize',
|
|
108
|
+
description: 'refuses an authorize with no clientId as 400, naming the field',
|
|
109
|
+
identity: exports.SIGNED_IN,
|
|
110
|
+
request: { token: 'seed-access', body: {} },
|
|
111
|
+
expect: { status: 400, error: '"clientId" is required' }
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'authorize-without-session',
|
|
115
|
+
route: 'POST /SASLogon/authorize',
|
|
116
|
+
description: 'refuses with 401 when no caller is signed in',
|
|
117
|
+
request: { body: { clientId: 'web_app' } },
|
|
118
|
+
expect: { status: 401, error: 'Unauthorized' }
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'authorize-unknown-client',
|
|
122
|
+
route: 'POST /SASLogon/authorize',
|
|
123
|
+
description: 'refuses an unknown client with 403',
|
|
124
|
+
identity: exports.SIGNED_IN,
|
|
125
|
+
request: { token: 'seed-access', body: { clientId: 'nope' } },
|
|
126
|
+
expect: { status: 403, error: 'Invalid clientId.' }
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'exchange-code',
|
|
130
|
+
route: 'POST /SASjsApi/auth/token',
|
|
131
|
+
description: 'exchanges a code for a token pair',
|
|
132
|
+
identity: exports.SIGNED_IN,
|
|
133
|
+
request: { body: { clientId: 'web_app', code: 'seed-code' } },
|
|
134
|
+
expect: { status: 200, keys: ['accessToken', 'refreshToken'] }
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'exchange-missing-client',
|
|
138
|
+
route: 'POST /SASjsApi/auth/token',
|
|
139
|
+
description: 'refuses an exchange with no clientId as 400, naming the field',
|
|
140
|
+
identity: exports.SIGNED_IN,
|
|
141
|
+
request: { body: { code: 'seed-code' } },
|
|
142
|
+
expect: { status: 400, error: '"clientId" is required' }
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'exchange-code-unknown',
|
|
146
|
+
route: 'POST /SASjsApi/auth/token',
|
|
147
|
+
description: 'refuses a code the host never issued',
|
|
148
|
+
request: { body: { clientId: 'web_app', code: 'not-a-code' } },
|
|
149
|
+
expect: { status: 401, error: 'Invalid Auth Code' }
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: 'exchange-code-other-client',
|
|
153
|
+
route: 'POST /SASjsApi/auth/token',
|
|
154
|
+
description: 'refuses a code presented for another client, as a bad code',
|
|
155
|
+
identity: exports.SIGNED_IN,
|
|
156
|
+
request: { body: { clientId: 'sasjs', code: 'seed-code' } },
|
|
157
|
+
expect: { status: 401, error: 'Invalid Auth Code' }
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 'exchange-code-client-gone',
|
|
161
|
+
route: 'POST /SASjsApi/auth/token',
|
|
162
|
+
description: 'refuses a valid code whose client no longer exists',
|
|
163
|
+
identity: exports.SIGNED_IN,
|
|
164
|
+
request: { body: { clientId: 'removed_client', code: 'seed-orphan-code' } },
|
|
165
|
+
expect: { status: 401, error: 'Invalid clientId.' }
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'exchange-code-client-gone-with-pair',
|
|
169
|
+
route: 'POST /SASjsApi/auth/token',
|
|
170
|
+
description: 'refuses an exchange whose client is gone even when a pair was issued before it was deleted',
|
|
171
|
+
identity: exports.SIGNED_IN,
|
|
172
|
+
request: {
|
|
173
|
+
body: { clientId: 'removed_client', code: 'seed-orphan-pair-code' }
|
|
174
|
+
},
|
|
175
|
+
expect: { status: 401, error: 'Invalid clientId.' }
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: 'exchange-code-deactivated',
|
|
179
|
+
route: 'POST /SASjsApi/auth/token',
|
|
180
|
+
description: 'refuses an exchange for an account that has been deactivated',
|
|
181
|
+
identity: exports.DEACTIVATED,
|
|
182
|
+
request: { body: { clientId: 'web_app', code: 'seed-code' } },
|
|
183
|
+
expect: { status: 401, error: 'Unauthorized' }
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 'refresh',
|
|
187
|
+
route: 'POST /SASjsApi/auth/refresh',
|
|
188
|
+
description: 'mints a new pair from a refresh token',
|
|
189
|
+
identity: exports.SIGNED_IN,
|
|
190
|
+
request: { token: 'seed-refresh' },
|
|
191
|
+
expect: { status: 200, keys: ['accessToken', 'refreshToken'] }
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: 'refresh-invalid',
|
|
195
|
+
route: 'POST /SASjsApi/auth/refresh',
|
|
196
|
+
description: 'refuses a refresh token it did not issue',
|
|
197
|
+
request: { token: 'not-a-token' },
|
|
198
|
+
expect: { status: 401, error: 'Unauthorized' }
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 'refresh-without-token',
|
|
202
|
+
route: 'POST /SASjsApi/auth/refresh',
|
|
203
|
+
description: 'refuses a refresh with no token at all',
|
|
204
|
+
request: {},
|
|
205
|
+
expect: { status: 401, error: 'Unauthorized' }
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: 'refresh-client-gone',
|
|
209
|
+
route: 'POST /SASjsApi/auth/refresh',
|
|
210
|
+
description: 'refuses a refresh whose client no longer exists',
|
|
211
|
+
identity: exports.SIGNED_IN,
|
|
212
|
+
request: { token: 'seed-refresh-orphan' },
|
|
213
|
+
expect: { status: 401, error: 'Invalid clientId.' }
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'refresh-deactivated',
|
|
217
|
+
route: 'POST /SASjsApi/auth/refresh',
|
|
218
|
+
description: 'refuses a refresh for an account that has been deactivated, as the server does on every request',
|
|
219
|
+
identity: exports.DEACTIVATED,
|
|
220
|
+
request: { token: 'seed-refresh' },
|
|
221
|
+
expect: { status: 401, error: 'Unauthorized' }
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 'logout',
|
|
225
|
+
route: 'POST /SASjsApi/auth/logout',
|
|
226
|
+
description: 'revokes the caller pair and answers 204',
|
|
227
|
+
identity: exports.SIGNED_IN,
|
|
228
|
+
request: { token: 'seed-access' },
|
|
229
|
+
expect: { status: 204 }
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 'logout-without-token',
|
|
233
|
+
route: 'POST /SASjsApi/auth/logout',
|
|
234
|
+
description: 'refuses a logout with no valid token',
|
|
235
|
+
request: {},
|
|
236
|
+
expect: { status: 401, error: 'Unauthorized' }
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
id: 'session',
|
|
240
|
+
route: 'GET /SASjsApi/session',
|
|
241
|
+
description: 'reports the caller the token names',
|
|
242
|
+
identity: exports.SIGNED_IN,
|
|
243
|
+
request: { token: 'seed-access' },
|
|
244
|
+
expect: { status: 200, partial: user2 }
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: 'session-deactivated',
|
|
248
|
+
route: 'GET /SASjsApi/session',
|
|
249
|
+
description: 'stops resolving a token whose account has been deactivated',
|
|
250
|
+
identity: exports.DEACTIVATED,
|
|
251
|
+
request: { token: 'seed-access' },
|
|
252
|
+
expect: { status: 401, error: 'Unauthorized' }
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: 'session-without-token',
|
|
256
|
+
route: 'GET /SASjsApi/session',
|
|
257
|
+
description: 'refuses the session read with no caller',
|
|
258
|
+
request: {},
|
|
259
|
+
expect: { status: 401, error: 'Unauthorized' }
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: 'sign-out',
|
|
263
|
+
route: 'GET /SASLogon/logout',
|
|
264
|
+
description: 'answers the body the browser sign-out returns',
|
|
265
|
+
request: {},
|
|
266
|
+
expect: { status: 200, contents: 'OK!' }
|
|
267
|
+
}
|
|
268
|
+
];
|
|
@@ -4,10 +4,19 @@ exports.runCase = exports.servicesFor = void 0;
|
|
|
4
4
|
const dispatch_js_1 = require("../host/dispatch.js");
|
|
5
5
|
const service_js_1 = require("../drive/service.js");
|
|
6
6
|
const memory_store_js_1 = require("../drive/memory-store.js");
|
|
7
|
+
const service_js_2 = require("../identity/service.js");
|
|
8
|
+
const memory_store_js_2 = require("../identity/memory-store.js");
|
|
9
|
+
const fixture_js_1 = require("../identity/fixture.js");
|
|
7
10
|
const types_js_1 = require("../types.js");
|
|
8
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Services over fresh stores, seeded with the case's files and identity.
|
|
13
|
+
*
|
|
14
|
+
* The identity store is built for every case, drive or not, so a case that starts
|
|
15
|
+
* mid-flow can declare the tokens and codes it begins with.
|
|
16
|
+
*/
|
|
9
17
|
const servicesFor = (testCase) => ({
|
|
10
|
-
drive: (0, service_js_1.createDriveService)(new memory_store_js_1.MemoryDriveStore(testCase.seed || {}))
|
|
18
|
+
drive: (0, service_js_1.createDriveService)(new memory_store_js_1.MemoryDriveStore(testCase.seed || {})),
|
|
19
|
+
identity: (0, service_js_2.createIdentityService)(new memory_store_js_2.MemoryIdentityStore(testCase.identity || fixture_js_1.DEFAULT_IDENTITY))
|
|
11
20
|
});
|
|
12
21
|
exports.servicesFor = servicesFor;
|
|
13
22
|
/**
|
|
@@ -124,6 +124,14 @@ const createDriveService = (store) => {
|
|
|
124
124
|
throw new types_js_1.ApiError(409, stat.directory
|
|
125
125
|
? 'Folder with new name already exists.'
|
|
126
126
|
: 'File with new name already exists.');
|
|
127
|
+
// the extension rule is the drive's file-path rule, so a rename may not
|
|
128
|
+
// be the way around it: without this a file could be renamed into a name
|
|
129
|
+
// that GET, PATCH and DELETE then refuse, leaving an entry the API that
|
|
130
|
+
// created it can no longer read or remove. A folder is not subject to it
|
|
131
|
+
// (nothing puts a folder through the file-path schema), so only a file
|
|
132
|
+
// move is checked.
|
|
133
|
+
if (!stat.directory && (0, paths_js_1.hasBlockedExtension)(to))
|
|
134
|
+
throw new types_js_1.ApiError(400, 'Invalid file extension');
|
|
127
135
|
await store.move(from, to);
|
|
128
136
|
return { status: 'success' };
|
|
129
137
|
}
|
|
@@ -42,8 +42,56 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
42
42
|
if (!route)
|
|
43
43
|
throw new types_js_1.ApiError(404, `No such operation: ${routeId}`);
|
|
44
44
|
validate(route, request);
|
|
45
|
-
const { drive } = services;
|
|
45
|
+
const { drive, identity } = services;
|
|
46
46
|
const query = request.query || {};
|
|
47
|
+
const body = request.body || {};
|
|
48
|
+
/** The caller, from the request's own subject or the token it carried. */
|
|
49
|
+
const caller = async () => {
|
|
50
|
+
if (request.subject)
|
|
51
|
+
return request.subject;
|
|
52
|
+
return identity?.authenticate(request.token);
|
|
53
|
+
};
|
|
54
|
+
if (routeId.startsWith('POST /SASjsApi/auth/') ||
|
|
55
|
+
routeId.startsWith('POST /SASLogon/') ||
|
|
56
|
+
routeId === 'GET /SASLogon/logout' ||
|
|
57
|
+
routeId === 'GET /SASjsApi/session') {
|
|
58
|
+
if (!identity)
|
|
59
|
+
throw new types_js_1.ApiError(501, `This host has no identity service, so it cannot answer ${routeId}.`);
|
|
60
|
+
switch (routeId) {
|
|
61
|
+
case 'POST /SASjsApi/auth/token':
|
|
62
|
+
return {
|
|
63
|
+
status: 200,
|
|
64
|
+
body: await identity.token({
|
|
65
|
+
clientId: String(body.clientId ?? ''),
|
|
66
|
+
code: String(body.code ?? '')
|
|
67
|
+
})
|
|
68
|
+
};
|
|
69
|
+
case 'POST /SASjsApi/auth/refresh':
|
|
70
|
+
return { status: 200, body: await identity.refresh(request.token) };
|
|
71
|
+
case 'POST /SASjsApi/auth/logout':
|
|
72
|
+
await identity.logout(await caller());
|
|
73
|
+
return { status: 204 };
|
|
74
|
+
case 'POST /SASLogon/login':
|
|
75
|
+
return {
|
|
76
|
+
status: 200,
|
|
77
|
+
body: await identity.login({
|
|
78
|
+
username: String(body.username ?? ''),
|
|
79
|
+
password: String(body.password ?? '')
|
|
80
|
+
})
|
|
81
|
+
};
|
|
82
|
+
case 'POST /SASLogon/authorize':
|
|
83
|
+
return {
|
|
84
|
+
status: 200,
|
|
85
|
+
body: {
|
|
86
|
+
code: await identity.authorize(await caller(), String(body.clientId ?? ''))
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
case 'GET /SASLogon/logout':
|
|
90
|
+
return { status: 200, contents: await identity.logoutSession() };
|
|
91
|
+
case 'GET /SASjsApi/session':
|
|
92
|
+
return { status: 200, body: await identity.session(await caller()) };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
47
95
|
switch (routeId) {
|
|
48
96
|
case 'GET /SASjsApi/drive/file': {
|
|
49
97
|
const file = await drive.read(String(query._filePath || ''));
|
|
@@ -98,5 +146,12 @@ exports.HANDLED_ROUTES = [
|
|
|
98
146
|
'POST /SASjsApi/drive/folder',
|
|
99
147
|
'DELETE /SASjsApi/drive/folder',
|
|
100
148
|
'POST /SASjsApi/drive/rename',
|
|
101
|
-
'GET /SASjsApi/drive/filetree'
|
|
149
|
+
'GET /SASjsApi/drive/filetree',
|
|
150
|
+
'POST /SASjsApi/auth/token',
|
|
151
|
+
'POST /SASjsApi/auth/refresh',
|
|
152
|
+
'POST /SASjsApi/auth/logout',
|
|
153
|
+
'POST /SASLogon/login',
|
|
154
|
+
'POST /SASLogon/authorize',
|
|
155
|
+
'GET /SASLogon/logout',
|
|
156
|
+
'GET /SASjsApi/session'
|
|
102
157
|
];
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The identity a browser build starts from, and the format it travels in.
|
|
4
|
+
*
|
|
5
|
+
* A shipped fixture is what makes a mock build work with no configuration: it
|
|
6
|
+
* is small, legible, and declared to be fake. Passwords are plain text because
|
|
7
|
+
* nothing verifies them beyond an equality check - there is no boundary here,
|
|
8
|
+
* and pretending otherwise by shipping a hash would suggest one.
|
|
9
|
+
*
|
|
10
|
+
* `format` is stamped the way the drive's snapshot is: a store written by an
|
|
11
|
+
* older build is refused with a named reason rather than misread.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.parseIdentity = exports.DEFAULT_IDENTITY = exports.IDENTITY_FORMAT = void 0;
|
|
15
|
+
exports.IDENTITY_FORMAT = 1;
|
|
16
|
+
const SECONDS_IN_A_DAY = 86400;
|
|
17
|
+
exports.DEFAULT_IDENTITY = {
|
|
18
|
+
format: exports.IDENTITY_FORMAT,
|
|
19
|
+
users: [
|
|
20
|
+
{
|
|
21
|
+
uid: 'user-admin',
|
|
22
|
+
username: 'admin',
|
|
23
|
+
displayName: 'Demo Admin',
|
|
24
|
+
// long enough to pass the API's own password rule (six characters), so a
|
|
25
|
+
// demo can sign in against a real server as well as a mock
|
|
26
|
+
password: 'admin123',
|
|
27
|
+
isAdmin: true,
|
|
28
|
+
isActive: true,
|
|
29
|
+
needsToUpdatePassword: false
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
uid: 'user-restricted',
|
|
33
|
+
username: 'user2',
|
|
34
|
+
displayName: 'Demo User',
|
|
35
|
+
password: 'user123',
|
|
36
|
+
isAdmin: false,
|
|
37
|
+
isActive: true,
|
|
38
|
+
needsToUpdatePassword: false
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
groups: [
|
|
42
|
+
{
|
|
43
|
+
uid: 'group-data-controller',
|
|
44
|
+
name: 'DCGroup',
|
|
45
|
+
description: 'Data Controller users',
|
|
46
|
+
isActive: true,
|
|
47
|
+
users: ['user2']
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
uid: 'group-admins',
|
|
51
|
+
name: 'Admins',
|
|
52
|
+
description: 'Everything the API offers',
|
|
53
|
+
isActive: true,
|
|
54
|
+
users: ['admin']
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
permissions: [
|
|
58
|
+
{
|
|
59
|
+
uid: 'rule-routes',
|
|
60
|
+
path: '/SASjsApi/info',
|
|
61
|
+
type: 'Route',
|
|
62
|
+
setting: 'Grant',
|
|
63
|
+
group: 'group-data-controller'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
uid: 'rule-execute',
|
|
67
|
+
path: '/SASjsApi/stp/execute',
|
|
68
|
+
type: 'Route',
|
|
69
|
+
setting: 'Grant',
|
|
70
|
+
group: 'group-data-controller'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
uid: 'rule-code',
|
|
74
|
+
path: '/SASjsApi/code/execute',
|
|
75
|
+
type: 'Route',
|
|
76
|
+
setting: 'Deny',
|
|
77
|
+
user: 'user-restricted'
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
clients: [
|
|
81
|
+
{
|
|
82
|
+
clientId: 'web_app',
|
|
83
|
+
clientSecret: 'web_app',
|
|
84
|
+
accessTokenExpiration: SECONDS_IN_A_DAY,
|
|
85
|
+
refreshTokenExpiration: SECONDS_IN_A_DAY * 30
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
clientId: 'sasjs',
|
|
89
|
+
clientSecret: 'sasjs',
|
|
90
|
+
accessTokenExpiration: 3600,
|
|
91
|
+
refreshTokenExpiration: SECONDS_IN_A_DAY
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Parses a fixture a build wrote, refusing one this package cannot read.
|
|
97
|
+
*
|
|
98
|
+
* The checks name what is wrong and what to do about it: a fixture is hand-
|
|
99
|
+
* editable, so "invalid identity" would send the reader to the type rather than
|
|
100
|
+
* to the line.
|
|
101
|
+
*/
|
|
102
|
+
const parseIdentity = (raw) => {
|
|
103
|
+
const fixture = raw;
|
|
104
|
+
if (!fixture || typeof fixture !== 'object')
|
|
105
|
+
throw new Error('The identity fixture is not an object.');
|
|
106
|
+
if (fixture.format !== exports.IDENTITY_FORMAT)
|
|
107
|
+
throw new Error(`The identity fixture states format ${String(fixture.format)}, and this build reads format ${exports.IDENTITY_FORMAT}. Rebuild the site, or replace mock-identity.json with one this version wrote.`);
|
|
108
|
+
for (const key of ['users', 'groups', 'permissions', 'clients'])
|
|
109
|
+
if (!Array.isArray(fixture[key]))
|
|
110
|
+
throw new Error(`The identity fixture has no \`${key}\` array.`);
|
|
111
|
+
for (const token of fixture.tokens || [])
|
|
112
|
+
if (!token.accessToken || !token.refreshToken)
|
|
113
|
+
throw new Error(`The identity fixture declares a half pair for ${token.username}/${token.clientId}: a seeded token needs both an accessToken and a refreshToken, because a token only authenticates while the subject's pair holds it.`);
|
|
114
|
+
if (!fixture.users.some((user) => user.isAdmin))
|
|
115
|
+
throw new Error('The identity fixture declares no administrator: a demo with nobody able to administer it cannot exercise the admin routes.');
|
|
116
|
+
return fixture;
|
|
117
|
+
};
|
|
118
|
+
exports.parseIdentity = parseIdentity;
|