dce-reactkit 3.11.3 → 3.12.1-beta-cross-server.1
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 +20 -0
- package/dist/cjs/index.js +281 -140
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/helpers/dataSigner.d.ts +38 -0
- package/dist/cjs/types/helpers/genRouteHandler.d.ts +10 -10
- package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/index.d.ts +15 -13
- package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +0 -6
- package/dist/cjs/types/index.d.ts +2 -1
- package/dist/cjs/types/server/initCrossServerCredentialCollection.d.ts +8 -0
- package/dist/cjs/types/server/initServer.d.ts +11 -0
- package/dist/cjs/types/types/CrossServerCredential.d.ts +11 -0
- package/dist/cjs/types/types/ReactKitErrorCode.d.ts +13 -1
- package/dist/esm/index.js +280 -140
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types/helpers/dataSigner.d.ts +38 -0
- package/dist/esm/types/helpers/genRouteHandler.d.ts +10 -10
- package/dist/esm/types/helpers/visitEndpointOnAnotherServer/index.d.ts +15 -13
- package/dist/esm/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +0 -6
- package/dist/esm/types/index.d.ts +2 -1
- package/dist/esm/types/server/initCrossServerCredentialCollection.d.ts +8 -0
- package/dist/esm/types/server/initServer.d.ts +11 -0
- package/dist/esm/types/types/CrossServerCredential.d.ts +11 -0
- package/dist/esm/types/types/ReactKitErrorCode.d.ts +13 -1
- package/dist/index.d.ts +54 -25
- package/genEncodedSecret.ts +28 -0
- package/package.json +5 -2
- package/src/components/IntelliTable.tsx +1 -1
- package/src/components/ItemPicker/NestableItemList.tsx +0 -1
- package/src/helpers/dataSigner.ts +232 -0
- package/src/helpers/genRouteHandler.ts +55 -95
- package/src/helpers/visitEndpointOnAnotherServer/index.ts +42 -41
- package/src/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.ts +3 -5
- package/src/index.ts +2 -0
- package/src/server/initCrossServerCredentialCollection.ts +16 -0
- package/src/server/initServer.ts +18 -0
- package/src/types/CrossServerCredential.ts +16 -0
- package/src/types/ReactKitErrorCode.tsx +13 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Import crypto
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
|
|
4
|
+
// Import jwt
|
|
5
|
+
import jwt from 'jwt-simple';
|
|
6
|
+
|
|
7
|
+
// Import shared helpers
|
|
8
|
+
import { internalGetCrossServerCredentialCollection } from '../server/initServer';
|
|
9
|
+
|
|
10
|
+
// Import shared types
|
|
11
|
+
import ErrorWithCode from '../errors/ErrorWithCode';
|
|
12
|
+
import ReactKitErrorCode from '../types/ReactKitErrorCode';
|
|
13
|
+
import CrossServerCredential from '../types/CrossServerCredential';
|
|
14
|
+
|
|
15
|
+
// Import shared constants
|
|
16
|
+
import MINUTE_IN_MS from '../constants/MINUTE_IN_MS';
|
|
17
|
+
|
|
18
|
+
/*------------------------------------------------------------------------*/
|
|
19
|
+
/* ------------------------------ Encoding ------------------------------ */
|
|
20
|
+
/*------------------------------------------------------------------------*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validate a JWT token
|
|
24
|
+
* @author Gabe Abrams
|
|
25
|
+
* @param encodedSecret the encoded secret
|
|
26
|
+
* @return the secret
|
|
27
|
+
*/
|
|
28
|
+
const decodeCredentialSecret = (encodedSecret: string): string => {
|
|
29
|
+
// Get the credential encoding salt
|
|
30
|
+
const credentialEncodingSalt = process.env.REACTKIT_CRED_ENCODING_SALT;
|
|
31
|
+
if (!credentialEncodingSalt) {
|
|
32
|
+
throw new ErrorWithCode(
|
|
33
|
+
'Cannot decode a cross-server credential secret because the credential encoding salt was not found in env.',
|
|
34
|
+
ReactKitErrorCode.CrossServerNoCredentialEncodingSalt,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Decode the secret
|
|
39
|
+
return jwt.decode(encodedSecret, credentialEncodingSalt);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/*------------------------------------------------------------------------*/
|
|
43
|
+
/* ------------------------------- Signing ------------------------------ */
|
|
44
|
+
/*------------------------------------------------------------------------*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Sign using sha 256
|
|
48
|
+
* @author Gabe Abrams
|
|
49
|
+
* @param opts object containing all arguments
|
|
50
|
+
* @param opts.pack the pack to sign
|
|
51
|
+
* @param opts.secret the reactkit secret to sign with
|
|
52
|
+
* @return the signed hash
|
|
53
|
+
*/
|
|
54
|
+
const genSignature = (
|
|
55
|
+
opts: {
|
|
56
|
+
pack: string,
|
|
57
|
+
secret: string,
|
|
58
|
+
},
|
|
59
|
+
): string => {
|
|
60
|
+
// Generate signature
|
|
61
|
+
return (
|
|
62
|
+
createHash('sha256')
|
|
63
|
+
.update(JSON.stringify({ pack: opts.pack, secret: opts.secret }))
|
|
64
|
+
.digest('base64')
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sign data with a private reactkit key, package it into a signed data pack
|
|
70
|
+
* @author Gabe Abrams
|
|
71
|
+
* @param opts object containing all arguments
|
|
72
|
+
* @param opts.method the method to sign
|
|
73
|
+
* @param opts.path the http request path
|
|
74
|
+
* @param opts.params the data in the body to sign
|
|
75
|
+
* @param opts.key the reactkit key to sign with
|
|
76
|
+
* @param opts.secret the reactkit secret to sign with
|
|
77
|
+
* @return the signed data
|
|
78
|
+
*/
|
|
79
|
+
export const createSignedPack = (
|
|
80
|
+
opts: {
|
|
81
|
+
method: string,
|
|
82
|
+
path: string,
|
|
83
|
+
params: { [key: string]: any },
|
|
84
|
+
key: string,
|
|
85
|
+
secret: string,
|
|
86
|
+
},
|
|
87
|
+
): string => {
|
|
88
|
+
// Create a timestamp
|
|
89
|
+
const timestamp = Date.now();
|
|
90
|
+
|
|
91
|
+
// Create the pack
|
|
92
|
+
const pack = JSON.stringify({
|
|
93
|
+
method: opts.method,
|
|
94
|
+
path: opts.path,
|
|
95
|
+
params: opts.params,
|
|
96
|
+
key: opts.key,
|
|
97
|
+
timestamp,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Generate signature
|
|
101
|
+
const signature = genSignature({
|
|
102
|
+
pack,
|
|
103
|
+
secret: opts.secret,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Create a signed pack
|
|
107
|
+
const signedPack = encodeURIComponent(JSON.stringify({
|
|
108
|
+
pack,
|
|
109
|
+
signature,
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
// Return the signed pack
|
|
113
|
+
return signedPack;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse signed pack. Throws an error if invalid
|
|
118
|
+
* @author Gabe Abrams
|
|
119
|
+
* @param opts object containing all arguments
|
|
120
|
+
* @param opts.method the method of the data validate
|
|
121
|
+
* @param opts.path the http request path to validate
|
|
122
|
+
* @param opts.scope the name of the scope to validate
|
|
123
|
+
* @param opts.signedPack the signed data pack to validate
|
|
124
|
+
* @returns parsed and validated params
|
|
125
|
+
*/
|
|
126
|
+
export const parseSignedPack = async (
|
|
127
|
+
opts: {
|
|
128
|
+
method: string,
|
|
129
|
+
path: string,
|
|
130
|
+
scope: string,
|
|
131
|
+
signedPack: string,
|
|
132
|
+
},
|
|
133
|
+
): Promise<{ [key: string]: any }> => {
|
|
134
|
+
// Extract signature
|
|
135
|
+
let pack: string;
|
|
136
|
+
let signature: string;
|
|
137
|
+
let method: string;
|
|
138
|
+
let path: string;
|
|
139
|
+
let key: string;
|
|
140
|
+
let timestamp: number;
|
|
141
|
+
let params: { [k: string]: any };
|
|
142
|
+
try {
|
|
143
|
+
({
|
|
144
|
+
pack,
|
|
145
|
+
signature,
|
|
146
|
+
} = JSON.parse(decodeURIComponent(opts.signedPack)));
|
|
147
|
+
|
|
148
|
+
// Unpack
|
|
149
|
+
({
|
|
150
|
+
method,
|
|
151
|
+
path,
|
|
152
|
+
params,
|
|
153
|
+
key,
|
|
154
|
+
timestamp,
|
|
155
|
+
} = JSON.parse(pack));
|
|
156
|
+
} catch (err) {
|
|
157
|
+
throw new ErrorWithCode(
|
|
158
|
+
'Could not validate a cross-server request because the request could not be parsed.',
|
|
159
|
+
ReactKitErrorCode.PackUnparseable,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Make sure the method and path match
|
|
164
|
+
if (method !== opts.method) {
|
|
165
|
+
throw new ErrorWithCode(
|
|
166
|
+
'Could not validate a cross-server request because the method did not match.',
|
|
167
|
+
ReactKitErrorCode.PackInvalidMethod,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (path !== opts.path) {
|
|
171
|
+
throw new ErrorWithCode(
|
|
172
|
+
'Could not validate a cross-server request because the path did not match.',
|
|
173
|
+
ReactKitErrorCode.PackInvalidPath,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Make sure the timestamp was recent enough
|
|
178
|
+
const elapsedMs = Math.abs(Date.now() - timestamp);
|
|
179
|
+
if (elapsedMs < MINUTE_IN_MS) {
|
|
180
|
+
throw new ErrorWithCode(
|
|
181
|
+
'Could not validate a cross-server request because the request was too old.',
|
|
182
|
+
ReactKitErrorCode.PackInvalidTimestamp,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Get the cross-server credential collection
|
|
187
|
+
const crossServerCredentialCollection = internalGetCrossServerCredentialCollection();
|
|
188
|
+
if (!crossServerCredentialCollection) {
|
|
189
|
+
throw new ErrorWithCode(
|
|
190
|
+
'Could not validate a cross-server request because the cross-server credential collection was not ready in time.',
|
|
191
|
+
ReactKitErrorCode.PackInvalidCollection,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Get the cross-server credential
|
|
196
|
+
const crossServerCredential: CrossServerCredential = await crossServerCredentialCollection.find({ key });
|
|
197
|
+
if (!crossServerCredential) {
|
|
198
|
+
throw new ErrorWithCode(
|
|
199
|
+
'Could not validate a cross-server request because the credential was not found.',
|
|
200
|
+
ReactKitErrorCode.PackInvalidCredential,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Make sure the scope is included
|
|
205
|
+
const allowedScopes = crossServerCredential.scopes;
|
|
206
|
+
if (!allowedScopes.includes(opts.scope)) {
|
|
207
|
+
throw new ErrorWithCode(
|
|
208
|
+
'Could not validate a cross-server request because the scope was not included.',
|
|
209
|
+
ReactKitErrorCode.PackInvalidScope,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Decode the secret
|
|
214
|
+
const secret = decodeCredentialSecret(crossServerCredential.encodedeSecret);
|
|
215
|
+
|
|
216
|
+
// Generate signature
|
|
217
|
+
const expectedSignature = genSignature({
|
|
218
|
+
pack,
|
|
219
|
+
secret,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Make sure the signature matches
|
|
223
|
+
if (signature !== expectedSignature) {
|
|
224
|
+
throw new ErrorWithCode(
|
|
225
|
+
'Could not validate a cross-server request because the signature did not match.',
|
|
226
|
+
ReactKitErrorCode.PackInvalidSignature,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Return body
|
|
231
|
+
return params ?? {};
|
|
232
|
+
};
|
|
@@ -17,6 +17,7 @@ import genErrorPage from '../html/genErrorPage';
|
|
|
17
17
|
import genInfoPage from '../html/genInfoPage';
|
|
18
18
|
import parseUserAgent from './parseUserAgent';
|
|
19
19
|
import getTimeInfoInET from './getTimeInfoInET';
|
|
20
|
+
import { parseSignedPack } from './dataSigner';
|
|
20
21
|
|
|
21
22
|
// Import shared types
|
|
22
23
|
import LogFunction from '../types/LogFunction';
|
|
@@ -29,6 +30,7 @@ import LogSourceSpecificInfo from '../types/Log/LogSourceSpecificInfo';
|
|
|
29
30
|
import LogBuiltInMetadata from '../types/LogBuiltInMetadata';
|
|
30
31
|
import LogAction from '../types/LogAction';
|
|
31
32
|
import LogLevel from '../types/LogLevel';
|
|
33
|
+
import ErrorWithCode from '../errors/ErrorWithCode';
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* Generate an express API route handler
|
|
@@ -37,14 +39,15 @@ import LogLevel from '../types/LogLevel';
|
|
|
37
39
|
* @param opts.paramTypes map containing the types for each parameter that is
|
|
38
40
|
* included in the request (map: param name => type)
|
|
39
41
|
* @param opts.handler function that processes the request
|
|
40
|
-
* @param [opts.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
42
|
+
* @param [opts.crossServerScope] the scope associated with this endpoint.
|
|
43
|
+
* If defined, this is a cross-server endpoint, which will never
|
|
44
|
+
* have any launch data, will never check Canvas roles or launch status, and will
|
|
45
|
+
* instead use scopes and reactkit credentials to sign and validate requests.
|
|
46
|
+
* Never start the path with /api/ttm or /api/admin if the endpoint is a cross-server
|
|
47
|
+
* endpoint because those roles will not be validated
|
|
48
|
+
* @param [opts.skipSessionCheck=true if crossServerScope defined] if true, skip
|
|
49
|
+
* the session check (allow users to not be logged in and launched via LTI).
|
|
50
|
+
* If crossServerScope is defined, this is always true
|
|
48
51
|
* @param [opts.unhandledErrorMessagePrefix] if included, when an error that
|
|
49
52
|
* is not of type ErrorWithCode is thrown, the client will receive an error
|
|
50
53
|
* where the error message is prefixed with this string. For example,
|
|
@@ -107,106 +110,67 @@ const genRouteHandler = (
|
|
|
107
110
|
logServerEvent: LogFunction,
|
|
108
111
|
},
|
|
109
112
|
) => any,
|
|
113
|
+
crossServerScope?: string,
|
|
110
114
|
skipSessionCheck?: boolean,
|
|
111
|
-
allowedHosts?: string[] | string,
|
|
112
|
-
bannedHosts?: string[] | string,
|
|
113
115
|
unhandledErrorMessagePrefix?: string,
|
|
114
116
|
},
|
|
115
117
|
) => {
|
|
116
118
|
// Return a route handler
|
|
117
119
|
return async (req: any, res: any, next: () => void) => {
|
|
120
|
+
/*----------------------------------------*/
|
|
121
|
+
/* ------------- Preparation ------------ */
|
|
122
|
+
/*----------------------------------------*/
|
|
123
|
+
|
|
118
124
|
// Output params
|
|
119
125
|
const output: { [k in string]: any } = {};
|
|
120
126
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
// Determine cross server scopes
|
|
128
|
+
let crossServerScope: string | null = null;
|
|
129
|
+
if (opts.crossServerScope) {
|
|
130
|
+
crossServerScope = opts.crossServerScope ?? null;
|
|
131
|
+
}
|
|
124
132
|
|
|
125
|
-
//
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
||
|
|
129
|
-
|| req.headers.referer,
|
|
133
|
+
// Determine whether we're skipping the session check
|
|
134
|
+
const skipSessionCheck = !!(
|
|
135
|
+
opts.skipSessionCheck
|
|
136
|
+
|| crossServerScope
|
|
130
137
|
);
|
|
131
|
-
const originHostname = (
|
|
132
|
-
originURL
|
|
133
|
-
// Remove protocol
|
|
134
|
-
.replace(/(^\w+:|^)\/\//, '')
|
|
135
|
-
// Remove port
|
|
136
|
-
.replace(/:\d+$/, '')
|
|
137
|
-
);
|
|
138
|
-
const serverHostname = String(req.hostname);
|
|
139
|
-
|
|
140
|
-
// Check allowed
|
|
141
|
-
if (opts.allowedHosts) {
|
|
142
|
-
// Only accept requests from allowed hosts
|
|
143
|
-
const allowedArray = (
|
|
144
|
-
Array.isArray(opts.allowedHosts)
|
|
145
|
-
? opts.allowedHosts
|
|
146
|
-
: [opts.allowedHosts]
|
|
147
|
-
);
|
|
148
138
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
139
|
+
// Get body from everywhere it can come from
|
|
140
|
+
let requestBody: {
|
|
141
|
+
[k: string]: any,
|
|
142
|
+
} = {
|
|
143
|
+
...req.body,
|
|
144
|
+
...req.query,
|
|
145
|
+
...req.params,
|
|
146
|
+
};
|
|
154
147
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// Check end of hostname
|
|
159
|
-
return originHostname.endsWith(allowedHost.substring(1));
|
|
160
|
-
}
|
|
148
|
+
/*----------------------------------------*/
|
|
149
|
+
/* ------- Cross-Server Validation ------ */
|
|
150
|
+
/*----------------------------------------*/
|
|
161
151
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
152
|
+
if (crossServerScope) {
|
|
153
|
+
// Get the signed pack
|
|
154
|
+
const { signedPack } = requestBody;
|
|
165
155
|
|
|
166
|
-
// If
|
|
167
|
-
if (!
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
message: 'You are not allowed to access this endpoint.',
|
|
172
|
-
code: ReactKitErrorCode.HostNotAllowed,
|
|
173
|
-
status: 403,
|
|
174
|
-
},
|
|
156
|
+
// If no pack, throw error
|
|
157
|
+
if (!signedPack || typeof signedPack !== 'string') {
|
|
158
|
+
throw new ErrorWithCode(
|
|
159
|
+
'Could not process a cross server request because there was no valid signed pack.',
|
|
160
|
+
ReactKitErrorCode.CrossServerNoPack,
|
|
175
161
|
);
|
|
176
162
|
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Check banned
|
|
180
|
-
if (opts.bannedHosts) {
|
|
181
|
-
// Do not allow requests from banned hosts
|
|
182
|
-
const bannedArray = (
|
|
183
|
-
Array.isArray(opts.bannedHosts)
|
|
184
|
-
? opts.bannedHosts
|
|
185
|
-
: [opts.bannedHosts]
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
// Check if current host is banned
|
|
189
|
-
const banned = bannedArray.some((bannedHost) => {
|
|
190
|
-
if (bannedHost.startsWith('*')) {
|
|
191
|
-
// Check end of hostname
|
|
192
|
-
return originHostname.endsWith(bannedHost.substring(1));
|
|
193
|
-
}
|
|
194
163
|
|
|
195
|
-
|
|
196
|
-
|
|
164
|
+
// Validate the request
|
|
165
|
+
const crossServerParams = await parseSignedPack({
|
|
166
|
+
method: req.method ?? 'GET',
|
|
167
|
+
path: req.path,
|
|
168
|
+
scope: crossServerScope,
|
|
169
|
+
signedPack,
|
|
197
170
|
});
|
|
198
171
|
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
return handleError(
|
|
202
|
-
res,
|
|
203
|
-
{
|
|
204
|
-
message: 'You are not allowed to access this endpoint.',
|
|
205
|
-
code: ReactKitErrorCode.HostBanned,
|
|
206
|
-
status: 403,
|
|
207
|
-
},
|
|
208
|
-
);
|
|
209
|
-
}
|
|
172
|
+
// Replace body with params from the pack
|
|
173
|
+
requestBody = crossServerParams ?? {};
|
|
210
174
|
}
|
|
211
175
|
|
|
212
176
|
/*----------------------------------------*/
|
|
@@ -219,11 +183,7 @@ const genRouteHandler = (
|
|
|
219
183
|
const [name, type] = paramList[i];
|
|
220
184
|
|
|
221
185
|
// Find the value as a string
|
|
222
|
-
const value =
|
|
223
|
-
req.params[name]
|
|
224
|
-
|| req.query[name]
|
|
225
|
-
|| req.body[name]
|
|
226
|
-
);
|
|
186
|
+
const value = requestBody[name];
|
|
227
187
|
|
|
228
188
|
// Parse
|
|
229
189
|
if (type === ParamType.Boolean || type === ParamType.BooleanOptional) {
|
|
@@ -406,7 +366,7 @@ const genRouteHandler = (
|
|
|
406
366
|
// Not launched
|
|
407
367
|
(!launched || !launchInfo)
|
|
408
368
|
// Not skipping the session check
|
|
409
|
-
&& !
|
|
369
|
+
&& !skipSessionCheck
|
|
410
370
|
) {
|
|
411
371
|
return handleError(
|
|
412
372
|
res,
|
|
@@ -437,7 +397,7 @@ const genRouteHandler = (
|
|
|
437
397
|
)
|
|
438
398
|
)
|
|
439
399
|
// Not skipping the session check
|
|
440
|
-
&& !
|
|
400
|
+
&& !skipSessionCheck
|
|
441
401
|
) {
|
|
442
402
|
return handleError(
|
|
443
403
|
res,
|
|
@@ -1,71 +1,72 @@
|
|
|
1
|
-
// Import
|
|
2
|
-
import
|
|
1
|
+
// Import data signer
|
|
2
|
+
import { createSignedPack } from '../dataSigner';
|
|
3
3
|
|
|
4
4
|
// Import shared types
|
|
5
|
+
import ErrorWithCode from '../../errors/ErrorWithCode';
|
|
5
6
|
import ReactKitErrorCode from '../../types/ReactKitErrorCode';
|
|
6
|
-
|
|
7
|
-
// Import other helpers
|
|
8
7
|
import sendServerToServerRequest from './sendServerToServerRequest';
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
12
|
-
* dce-reactkit [for server only]
|
|
10
|
+
* Visit an endpoint on another server
|
|
13
11
|
* @author Gabe Abrams
|
|
14
12
|
* @param opts object containing all arguments
|
|
15
|
-
* @param opts.
|
|
16
|
-
* @param opts.path
|
|
17
|
-
* @param
|
|
18
|
-
* @param [opts.
|
|
19
|
-
*
|
|
20
|
-
* @
|
|
13
|
+
* @param opts.method the method of the endpoint
|
|
14
|
+
* @param opts.path the path of the other server's endpoint
|
|
15
|
+
* @param opts.host the host of the other server
|
|
16
|
+
* @param [opts.key=process.env.REACTKIT_CROSS_SERVER_CREDENTIAL_KEY] reactkit cross-server
|
|
17
|
+
* credential key
|
|
18
|
+
* @param [opts.secret=process.env.REACTKIT_CROSS_SERVER_CREDENTIAL_SECRET reactkit cross-server
|
|
19
|
+
* credential secret
|
|
20
|
+
* @param [opts.params={}] query/body parameters to include
|
|
21
|
+
* @param [opts.responseType=JSON] the response type from the other server
|
|
21
22
|
*/
|
|
22
23
|
const visitEndpointOnAnotherServer = async (
|
|
23
24
|
opts: {
|
|
24
|
-
|
|
25
|
+
method: 'GET' | 'POST' | 'DELETE' | 'PUT',
|
|
25
26
|
path: string,
|
|
26
|
-
|
|
27
|
+
host: string,
|
|
28
|
+
key?: string,
|
|
29
|
+
secret?: string,
|
|
27
30
|
params?: { [key in string]: any },
|
|
28
|
-
|
|
31
|
+
responseType?: 'JSON' | 'Text',
|
|
29
32
|
},
|
|
30
33
|
): Promise<any> => {
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
params = Object.fromEntries(
|
|
35
|
-
Object
|
|
36
|
-
.entries(opts.params)
|
|
37
|
-
.filter(([, value]) => {
|
|
38
|
-
return value !== undefined;
|
|
39
|
-
}),
|
|
40
|
-
);
|
|
41
|
-
}
|
|
34
|
+
// Get cross-server credentials
|
|
35
|
+
const key = opts.key ?? process.env.REACTKIT_CROSS_SERVER_KEY;
|
|
36
|
+
const secret = opts.secret ?? process.env.REACTKIT_CROSS_SERVER_SECRET;
|
|
42
37
|
|
|
43
|
-
//
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.map(([key, value]) => {
|
|
49
|
-
if (Array.isArray(value) || typeof value === 'object') {
|
|
50
|
-
return [key, JSON.stringify(value)];
|
|
51
|
-
}
|
|
52
|
-
return [key, value];
|
|
53
|
-
}),
|
|
38
|
+
// Throw error if no credentials
|
|
39
|
+
if (!key || !secret) {
|
|
40
|
+
throw new ErrorWithCode(
|
|
41
|
+
'Cannot send cross-server signed request because either or both the key and secret were not included or found in env.',
|
|
42
|
+
ReactKitErrorCode.CrossServerNoCredentialsToSignWith,
|
|
54
43
|
);
|
|
55
44
|
}
|
|
56
45
|
|
|
46
|
+
// Create signed pack
|
|
47
|
+
const signedPack = createSignedPack({
|
|
48
|
+
method: opts.method,
|
|
49
|
+
path: opts.path,
|
|
50
|
+
params: opts.params ?? {},
|
|
51
|
+
key,
|
|
52
|
+
secret,
|
|
53
|
+
});
|
|
54
|
+
|
|
57
55
|
// Send the request
|
|
58
56
|
const response = await sendServerToServerRequest({
|
|
59
|
-
host: opts.host,
|
|
60
57
|
path: opts.path,
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
host: opts.host,
|
|
59
|
+
method: opts.method,
|
|
60
|
+
params: {
|
|
61
|
+
signedPack,
|
|
62
|
+
},
|
|
63
|
+
responseType: opts.responseType,
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
// Check for failure
|
|
66
67
|
if (!response || !response.body) {
|
|
67
68
|
throw new ErrorWithCode(
|
|
68
|
-
'We didn\'t get a response from the server. Please check your internet connection.',
|
|
69
|
+
'We didn\'t get a response from the other server. Please check your internet connection.',
|
|
69
70
|
ReactKitErrorCode.NoResponse,
|
|
70
71
|
);
|
|
71
72
|
}
|
|
@@ -15,9 +15,6 @@ import ErrorWithCode from '../../errors/ErrorWithCode';
|
|
|
15
15
|
* @param [opts.host] host to send request to
|
|
16
16
|
* @param [opts.method=GET] http method to use
|
|
17
17
|
* @param [opts.params] body/data to include in the request
|
|
18
|
-
* @param [opts.headers] headers to include in the request
|
|
19
|
-
* @param [opts.sendCrossDomainCredentials=true if in development mode] if true,
|
|
20
|
-
* send cross-domain credentials even if not in dev mode
|
|
21
18
|
* @param [opts.responseType=JSON] expected response type
|
|
22
19
|
* @returns { body, status, headers } on success
|
|
23
20
|
*/
|
|
@@ -27,7 +24,6 @@ const sendServerToServerRequest = async (
|
|
|
27
24
|
host?: string,
|
|
28
25
|
method?: ('GET' | 'POST' | 'PUT' | 'DELETE'),
|
|
29
26
|
params?: { [k in string]: any },
|
|
30
|
-
headers?: { [k in string]: any },
|
|
31
27
|
responseType?: 'Text' | 'JSON',
|
|
32
28
|
},
|
|
33
29
|
): Promise<{
|
|
@@ -70,7 +66,9 @@ const sendServerToServerRequest = async (
|
|
|
70
66
|
}
|
|
71
67
|
|
|
72
68
|
// Update headers
|
|
73
|
-
const headers
|
|
69
|
+
const headers: {
|
|
70
|
+
[k: string]: any,
|
|
71
|
+
} = {};
|
|
74
72
|
let data: string | null | { [k: string]: any } | undefined = null;
|
|
75
73
|
if (!headers['Content-Type']) {
|
|
76
74
|
// Form encoded
|
package/src/index.ts
CHANGED
|
@@ -113,6 +113,7 @@ import LogMetadataType from './types/LogMetadataType';
|
|
|
113
113
|
import LogFunction from './types/LogFunction';
|
|
114
114
|
import IntelliTableColumn from './types/IntelliTableColumn';
|
|
115
115
|
import DropdownItemType from './types/DropdownItemType';
|
|
116
|
+
import CrossServerCredential from './types/CrossServerCredential';
|
|
116
117
|
|
|
117
118
|
// Component-specific-types
|
|
118
119
|
import PickableItem from './components/ItemPicker/types/PickableItem';
|
|
@@ -234,6 +235,7 @@ export {
|
|
|
234
235
|
LogFunction,
|
|
235
236
|
IntelliTableColumn,
|
|
236
237
|
DropdownItemType,
|
|
238
|
+
CrossServerCredential,
|
|
237
239
|
// Component-specific-types
|
|
238
240
|
PickableItem,
|
|
239
241
|
DBEntry,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialize a cross-server credential collection given the dce-mango Collection class
|
|
3
|
+
* @author Gabe Abrams
|
|
4
|
+
* @param Collection the Collection class from dce-mango
|
|
5
|
+
* @returns initialized logCollection
|
|
6
|
+
*/
|
|
7
|
+
const initCrossServerCredentialCollection = (Collection: any) => {
|
|
8
|
+
return new Collection(
|
|
9
|
+
'CrossServerCredential',
|
|
10
|
+
{
|
|
11
|
+
uniqueIndexKey: 'key',
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default initCrossServerCredentialCollection;
|
package/src/server/initServer.ts
CHANGED
|
@@ -27,6 +27,9 @@ let _cacclGetLaunchInfo: GetLaunchInfoFunction;
|
|
|
27
27
|
// Stored copy of dce-mango log collection
|
|
28
28
|
let _logCollection: any;
|
|
29
29
|
|
|
30
|
+
// Stored copy of dce-mango cross-server credential collection
|
|
31
|
+
let _crossServerCredentialCollection: any;
|
|
32
|
+
|
|
30
33
|
/*------------------------------------------------------------------------*/
|
|
31
34
|
/* Helpers */
|
|
32
35
|
/*------------------------------------------------------------------------*/
|
|
@@ -58,6 +61,16 @@ export const internalGetLogCollection = () => {
|
|
|
58
61
|
return _logCollection ?? null;
|
|
59
62
|
};
|
|
60
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Get cross-server credential collection
|
|
66
|
+
* @author Gabe Abrams
|
|
67
|
+
* @return cross-server credential collection if one was included during launch or null
|
|
68
|
+
* if we don't have a cross-server credential collection (yet)
|
|
69
|
+
*/
|
|
70
|
+
export const internalGetCrossServerCredentialCollection = () => {
|
|
71
|
+
return _crossServerCredentialCollection ?? null;
|
|
72
|
+
};
|
|
73
|
+
|
|
61
74
|
/*------------------------------------------------------------------------*/
|
|
62
75
|
/* Main */
|
|
63
76
|
/*------------------------------------------------------------------------*/
|
|
@@ -78,6 +91,9 @@ export const internalGetLogCollection = () => {
|
|
|
78
91
|
* userIds are allowed to review logs. If a dce-mango collection, only
|
|
79
92
|
* Canvas admins with entries in that collection ({ userId, ...}) are allowed
|
|
80
93
|
* to review logs
|
|
94
|
+
* @param [opts.crossServerCredentialCollection] mongo collection from dce-mango to use for
|
|
95
|
+
* storing cross-server credentials. If none is included, cross-server credentials
|
|
96
|
+
* are not supported
|
|
81
97
|
*/
|
|
82
98
|
const initServer = (
|
|
83
99
|
opts: {
|
|
@@ -85,10 +101,12 @@ const initServer = (
|
|
|
85
101
|
getLaunchInfo: GetLaunchInfoFunction,
|
|
86
102
|
logCollection?: any,
|
|
87
103
|
logReviewAdmins?: (number[] | any),
|
|
104
|
+
crossServerCredentialCollection?: any,
|
|
88
105
|
},
|
|
89
106
|
) => {
|
|
90
107
|
_cacclGetLaunchInfo = opts.getLaunchInfo;
|
|
91
108
|
_logCollection = opts.logCollection;
|
|
109
|
+
_crossServerCredentialCollection = opts.crossServerCredentialCollection;
|
|
92
110
|
|
|
93
111
|
/*----------------------------------------*/
|
|
94
112
|
/* Logging */
|