@strapi/admin 4.6.2 → 4.7.0-exp.3d6a31eb083e9d44afcf98f68c107fb7567e5720
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/admin/src/content-manager/components/CollectionTypeFormWrapper/index.js +0 -2
- package/admin/src/hooks/useRegenerate/index.js +2 -2
- package/admin/src/hooks/useSettingsMenu/utils/defaultGlobalLinks.js +7 -0
- package/admin/src/pages/HomePage/CloudBox.js +83 -0
- package/admin/src/pages/HomePage/ContentBlocks.js +2 -0
- package/admin/src/pages/HomePage/assets/strapi-cloud-background.png +0 -0
- package/admin/src/pages/HomePage/assets/strapi-cloud-flags.svg +1 -0
- package/admin/src/pages/HomePage/assets/strapi-cloud-icon.svg +1 -0
- package/admin/src/pages/SettingsPage/{pages/ApiTokens/EditView/components → components/Tokens}/FormHead/index.js +36 -19
- package/admin/src/pages/SettingsPage/components/Tokens/FormiTokenContainer/LifeSpanInput.js +95 -0
- package/admin/src/pages/SettingsPage/components/Tokens/LifeSpanInput/index.js +97 -0
- package/admin/src/pages/SettingsPage/components/Tokens/Regenerate/index.js +73 -0
- package/admin/src/pages/SettingsPage/{pages/ApiTokens/ListView/DynamicTable → components/Tokens/Table}/DeleteButton/index.js +19 -6
- package/admin/src/pages/SettingsPage/components/Tokens/Table/index.js +145 -0
- package/admin/src/pages/SettingsPage/{pages/ApiTokens/EditView/components/ContentBox → components/Tokens/TokenBox}/index.js +19 -16
- package/admin/src/pages/SettingsPage/components/Tokens/TokenDescription/index.js +51 -0
- package/admin/src/pages/SettingsPage/components/Tokens/TokenName/index.js +46 -0
- package/admin/src/pages/SettingsPage/components/Tokens/TokenTypeSelect/index.js +69 -0
- package/admin/src/pages/SettingsPage/components/Tokens/constants.js +2 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/CollapsableContentType/index.js +3 -1
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/FormApiTokenContainer/index.js +53 -150
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/Regenerate/index.js +5 -1
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/index.js +46 -17
- package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/index.js +16 -16
- package/admin/src/pages/SettingsPage/pages/TransferTokens/EditView/components/FormTransferTokenContainer/index.js +101 -0
- package/admin/src/pages/SettingsPage/pages/TransferTokens/EditView/components/LoadingView/index.js +48 -0
- package/admin/src/pages/SettingsPage/pages/TransferTokens/EditView/index.js +219 -0
- package/admin/src/pages/SettingsPage/pages/TransferTokens/EditView/utils/getDateOfExpiration.js +16 -0
- package/admin/src/pages/SettingsPage/pages/TransferTokens/EditView/utils/index.js +4 -0
- package/admin/src/pages/SettingsPage/pages/TransferTokens/EditView/utils/schema.js +10 -0
- package/admin/src/pages/SettingsPage/pages/TransferTokens/ListView/index.js +194 -0
- package/admin/src/pages/SettingsPage/pages/TransferTokens/ListView/utils/tableHeaders.js +48 -0
- package/admin/src/pages/SettingsPage/pages/TransferTokens/ProtectedCreateView/index.js +14 -0
- package/admin/src/pages/SettingsPage/pages/TransferTokens/ProtectedEditView/index.js +14 -0
- package/admin/src/pages/SettingsPage/pages/TransferTokens/ProtectedListView/index.js +12 -0
- package/admin/src/pages/SettingsPage/utils/defaultRoutes.js +33 -0
- package/admin/src/permissions/defaultPermissions.js +8 -0
- package/admin/src/translations/en.json +15 -0
- package/build/27d16aefee06412db90a.png +0 -0
- package/build/4049.16583eee.chunk.js +1 -0
- package/build/4649.b7e84a29.chunk.js +30 -0
- package/build/7259.3f04094f.chunk.js +1 -0
- package/build/{Admin-authenticatedApp.dd16edad.chunk.js → Admin-authenticatedApp.368164a1.chunk.js} +6 -6
- package/build/Admin_homePage.1f10437f.chunk.js +78 -0
- package/build/{Admin_settingsPage.3cd54156.chunk.js → Admin_settingsPage.5a329b58.chunk.js} +25 -25
- package/build/{admin-app.3a084127.chunk.js → admin-app.df9adf93.chunk.js} +26 -26
- package/build/{api-tokens-create-page.a31c7fba.chunk.js → api-tokens-create-page.4328b852.chunk.js} +1 -1
- package/build/{api-tokens-edit-page.64fef287.chunk.js → api-tokens-edit-page.bce5050f.chunk.js} +1 -1
- package/build/api-tokens-list-page.149903c8.chunk.js +16 -0
- package/build/bb3108f7fd1e6179bde1.svg +1 -0
- package/build/bb4d0d527bdfb161bc5a.svg +1 -0
- package/build/{content-manager.d04b738f.chunk.js → content-manager.6ed87531.chunk.js} +1 -1
- package/build/en-json.8e5451b1.chunk.js +1 -0
- package/build/index.html +1 -1
- package/build/{main.9c01de7f.js → main.8009bfe8.js} +1 -0
- package/build/runtime~main.725b20df.js +2 -0
- package/build/transfer-tokens-create-page.a1f14bb1.chunk.js +1 -0
- package/build/transfer-tokens-edit-page.00ee1c74.chunk.js +1 -0
- package/build/transfer-tokens-list-page.1e15926d.chunk.js +16 -0
- package/package.json +9 -9
- package/server/bootstrap.js +2 -0
- package/server/config/admin-actions.js +48 -0
- package/server/content-types/index.js +2 -0
- package/server/content-types/transfer-token-permission.js +36 -0
- package/server/content-types/transfer-token.js +66 -0
- package/server/controllers/api-token.js +4 -5
- package/server/controllers/index.js +1 -0
- package/server/controllers/transfer/index.js +13 -0
- package/server/controllers/transfer/runner.js +24 -0
- package/server/controllers/transfer/token.js +131 -0
- package/server/register.js +2 -9
- package/server/routes/index.js +2 -0
- package/server/routes/transfer.js +95 -0
- package/server/services/api-token.js +2 -3
- package/server/services/constants.js +6 -0
- package/server/services/index.js +1 -0
- package/server/services/transfer/index.js +6 -0
- package/server/services/transfer/permission.js +22 -0
- package/server/services/transfer/token.js +409 -0
- package/server/strategies/api-token.js +4 -2
- package/server/strategies/data-transfer.js +107 -0
- package/server/strategies/index.js +1 -0
- package/server/utils/index.d.ts +2 -0
- package/server/validation/api-tokens.js +1 -6
- package/server/validation/transfer/index.js +5 -0
- package/server/validation/transfer/token.js +34 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/FormBody/index.js +0 -77
- package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/index.js +0 -110
- package/build/1341.5d48c79b.chunk.js +0 -1
- package/build/4318.9b1ac9bc.chunk.js +0 -30
- package/build/Admin_homePage.4b878f04.chunk.js +0 -72
- package/build/api-tokens-list-page.370459ba.chunk.js +0 -16
- package/build/en-json.9cada7f3.chunk.js +0 -1
- package/build/runtime~main.bb1389c9.js +0 -2
- package/admin/src/pages/SettingsPage/{pages/ApiTokens/ListView/DynamicTable → components/Tokens/Table}/DefaultButton/index.js +1 -1
- /package/admin/src/pages/SettingsPage/{pages/ApiTokens/ListView/DynamicTable → components/Tokens/Table}/ReadButton/index.js +0 -0
- /package/admin/src/pages/SettingsPage/{pages/ApiTokens/ListView/DynamicTable → components/Tokens/Table}/UpdateButton/index.js +0 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { map, isArray, omit, uniq, isNil, difference, isEmpty } = require('lodash/fp');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
errors: { ValidationError, NotFoundError },
|
|
8
|
+
} = require('@strapi/utils');
|
|
9
|
+
|
|
10
|
+
const constants = require('../constants');
|
|
11
|
+
|
|
12
|
+
const TRANSFER_TOKEN_UID = 'admin::transfer-token';
|
|
13
|
+
const TRANSFER_TOKEN_PERMISSION_UID = 'admin::transfer-token-permission';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef TransferToken
|
|
17
|
+
*
|
|
18
|
+
* @property {number|string} id
|
|
19
|
+
* @property {string} name
|
|
20
|
+
* @property {string} description
|
|
21
|
+
* @property {string} accessKey
|
|
22
|
+
* @property {number} lastUsedAt
|
|
23
|
+
* @property {number} lifespan
|
|
24
|
+
* @property {number} expiresAt
|
|
25
|
+
* @property {(number[]|TransferTokenPermission[])} permissions
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef TransferTokenPermission
|
|
30
|
+
*
|
|
31
|
+
* @property {number|string} id
|
|
32
|
+
* @property {string} action
|
|
33
|
+
* @property {TransferToken|number} token
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** @constant {Array<string>} */
|
|
37
|
+
const SELECT_FIELDS = [
|
|
38
|
+
'id',
|
|
39
|
+
'name',
|
|
40
|
+
'description',
|
|
41
|
+
'lastUsedAt',
|
|
42
|
+
'lifespan',
|
|
43
|
+
'expiresAt',
|
|
44
|
+
'createdAt',
|
|
45
|
+
'updatedAt',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/** @constant {Array<string>} */
|
|
49
|
+
const POPULATE_FIELDS = ['permissions'];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Return a list of all tokens and their permissions
|
|
53
|
+
*
|
|
54
|
+
* @returns {Promise<Omit<TransferToken, 'accessKey'>[]>}
|
|
55
|
+
*/
|
|
56
|
+
const list = async () => {
|
|
57
|
+
const tokens = await strapi.query(TRANSFER_TOKEN_UID).findMany({
|
|
58
|
+
select: SELECT_FIELDS,
|
|
59
|
+
populate: POPULATE_FIELDS,
|
|
60
|
+
orderBy: { name: 'ASC' },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!tokens) return tokens;
|
|
64
|
+
return tokens.map((token) => flattenTokenPermissions(token));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a token and its permissions
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} attributes
|
|
71
|
+
* @param {string} attributes.name
|
|
72
|
+
* @param {string} attributes.description
|
|
73
|
+
* @param {number} attributes.lifespan
|
|
74
|
+
* @param {string[]} attributes.permissions
|
|
75
|
+
*
|
|
76
|
+
* @returns {Promise<TransferToken>}
|
|
77
|
+
*/
|
|
78
|
+
const create = async (attributes) => {
|
|
79
|
+
const accessKey = crypto.randomBytes(128).toString('hex');
|
|
80
|
+
|
|
81
|
+
assertTokenPermissionsValidity(attributes);
|
|
82
|
+
assertValidLifespan(attributes);
|
|
83
|
+
|
|
84
|
+
const result = await strapi.db.transaction(async () => {
|
|
85
|
+
const transferToken = await strapi.query(TRANSFER_TOKEN_UID).create({
|
|
86
|
+
select: SELECT_FIELDS,
|
|
87
|
+
populate: POPULATE_FIELDS,
|
|
88
|
+
data: {
|
|
89
|
+
...omit('permissions', attributes),
|
|
90
|
+
accessKey: hash(accessKey),
|
|
91
|
+
...getExpirationFields(attributes.lifespan),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await Promise.all(
|
|
96
|
+
uniq(attributes.permissions).map((action) =>
|
|
97
|
+
strapi
|
|
98
|
+
.query(TRANSFER_TOKEN_PERMISSION_UID)
|
|
99
|
+
.create({ data: { action, token: transferToken } })
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const currentPermissions = await strapi.entityService.load(
|
|
104
|
+
TRANSFER_TOKEN_UID,
|
|
105
|
+
transferToken,
|
|
106
|
+
'permissions'
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (currentPermissions) {
|
|
110
|
+
Object.assign(transferToken, { permissions: map('action', currentPermissions) });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return transferToken;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return { ...result, accessKey };
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Update a token and its permissions
|
|
121
|
+
*
|
|
122
|
+
* @param {string|number} id
|
|
123
|
+
* @param {Object} attributes
|
|
124
|
+
* @param {string} attributes.name
|
|
125
|
+
* @param {number} attributes.lastUsedAt
|
|
126
|
+
* @param {string[]} attributes.permissions
|
|
127
|
+
* @param {string} attributes.description
|
|
128
|
+
*
|
|
129
|
+
* @returns {Promise<Omit<TransferToken, 'accessKey'>>}
|
|
130
|
+
*/
|
|
131
|
+
const update = async (id, attributes) => {
|
|
132
|
+
// retrieve token without permissions
|
|
133
|
+
const originalToken = await strapi.query(TRANSFER_TOKEN_UID).findOne({ where: { id } });
|
|
134
|
+
|
|
135
|
+
if (!originalToken) {
|
|
136
|
+
throw new NotFoundError('Token not found');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
assertTokenPermissionsValidity(attributes);
|
|
140
|
+
assertValidLifespan(attributes);
|
|
141
|
+
|
|
142
|
+
return strapi.db.transaction(async () => {
|
|
143
|
+
const updatedToken = await strapi.query(TRANSFER_TOKEN_UID).update({
|
|
144
|
+
select: SELECT_FIELDS,
|
|
145
|
+
where: { id },
|
|
146
|
+
data: {
|
|
147
|
+
...omit('permissions', attributes),
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const currentPermissionsResult = await strapi.entityService.load(
|
|
152
|
+
TRANSFER_TOKEN_UID,
|
|
153
|
+
updatedToken,
|
|
154
|
+
'permissions'
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const currentPermissions = map('action', currentPermissionsResult || []);
|
|
158
|
+
const newPermissions = uniq(attributes.permissions);
|
|
159
|
+
|
|
160
|
+
const actionsToDelete = difference(currentPermissions, newPermissions);
|
|
161
|
+
const actionsToAdd = difference(newPermissions, currentPermissions);
|
|
162
|
+
|
|
163
|
+
// TODO: improve efficiency here
|
|
164
|
+
// method using a loop -- works but very inefficient
|
|
165
|
+
await Promise.all(
|
|
166
|
+
actionsToDelete.map((action) =>
|
|
167
|
+
strapi.query(TRANSFER_TOKEN_PERMISSION_UID).delete({
|
|
168
|
+
where: { action, token: id },
|
|
169
|
+
})
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// TODO: improve efficiency here
|
|
174
|
+
// using a loop -- works but very inefficient
|
|
175
|
+
await Promise.all(
|
|
176
|
+
actionsToAdd.map((action) =>
|
|
177
|
+
strapi.query(TRANSFER_TOKEN_PERMISSION_UID).create({
|
|
178
|
+
data: { action, token: id },
|
|
179
|
+
})
|
|
180
|
+
)
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// retrieve permissions
|
|
184
|
+
const permissionsFromDb = await strapi.entityService.load(
|
|
185
|
+
TRANSFER_TOKEN_UID,
|
|
186
|
+
updatedToken,
|
|
187
|
+
'permissions'
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
...updatedToken,
|
|
192
|
+
permissions: permissionsFromDb ? permissionsFromDb.map((p) => p.action) : undefined,
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Revoke (delete) a token
|
|
199
|
+
*
|
|
200
|
+
* @param {string|number} id
|
|
201
|
+
*
|
|
202
|
+
* @returns {Promise<Omit<TransferToken, 'accessKey'>>}
|
|
203
|
+
*/
|
|
204
|
+
const revoke = async (id) => {
|
|
205
|
+
return strapi.db.transaction(async () =>
|
|
206
|
+
strapi
|
|
207
|
+
.query(TRANSFER_TOKEN_UID)
|
|
208
|
+
.delete({ select: SELECT_FIELDS, populate: POPULATE_FIELDS, where: { id } })
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get a token
|
|
214
|
+
*
|
|
215
|
+
* @param {Object} whereParams
|
|
216
|
+
* @param {string|number} whereParams.id
|
|
217
|
+
* @param {string} whereParams.name
|
|
218
|
+
* @param {number} whereParams.lastUsedAt
|
|
219
|
+
* @param {string} whereParams.description
|
|
220
|
+
* @param {string} whereParams.accessKey
|
|
221
|
+
*
|
|
222
|
+
* @returns {Promise<Omit<TransferToken, 'accessKey'> | null>}
|
|
223
|
+
*/
|
|
224
|
+
const getBy = async (whereParams = {}) => {
|
|
225
|
+
if (Object.keys(whereParams).length === 0) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const token = await strapi
|
|
230
|
+
.query(TRANSFER_TOKEN_UID)
|
|
231
|
+
.findOne({ select: SELECT_FIELDS, populate: POPULATE_FIELDS, where: whereParams });
|
|
232
|
+
|
|
233
|
+
if (!token) return token;
|
|
234
|
+
return flattenTokenPermissions(token);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Retrieve a token by id
|
|
239
|
+
*
|
|
240
|
+
* @param {string|number} id
|
|
241
|
+
*
|
|
242
|
+
* @returns {Promise<Omit<TransferToken, 'accessKey'>>}
|
|
243
|
+
*/
|
|
244
|
+
const getById = async (id) => {
|
|
245
|
+
return getBy({ id });
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Retrieve a token by name
|
|
250
|
+
*
|
|
251
|
+
* @param {string} name
|
|
252
|
+
*
|
|
253
|
+
* ^@returns {Promise<Omit<TransferToken, 'accessKey'>>}
|
|
254
|
+
*/
|
|
255
|
+
const getByName = async (name) => {
|
|
256
|
+
return getBy({ name });
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check if token exists
|
|
261
|
+
*
|
|
262
|
+
* @param {Object} whereParams
|
|
263
|
+
* @param {string|number} whereParams.id
|
|
264
|
+
* @param {string} whereParams.name
|
|
265
|
+
* @param {number} whereParams.lastUsedAt
|
|
266
|
+
* @param {string} whereParams.description
|
|
267
|
+
* @param {string} whereParams.accessKey
|
|
268
|
+
*
|
|
269
|
+
* @returns {Promise<boolean>}
|
|
270
|
+
*/
|
|
271
|
+
const exists = async (whereParams = {}) => {
|
|
272
|
+
const transferToken = await getBy(whereParams);
|
|
273
|
+
|
|
274
|
+
return !!transferToken;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {string|number} id
|
|
279
|
+
*
|
|
280
|
+
* @returns {Promise<TransferToken>}
|
|
281
|
+
*/
|
|
282
|
+
const regenerate = async (id) => {
|
|
283
|
+
const accessKey = crypto.randomBytes(128).toString('hex');
|
|
284
|
+
const transferToken = await strapi.db.transaction(async () =>
|
|
285
|
+
strapi.query(TRANSFER_TOKEN_UID).update({
|
|
286
|
+
select: ['id', 'accessKey'],
|
|
287
|
+
where: { id },
|
|
288
|
+
data: {
|
|
289
|
+
accessKey: hash(accessKey),
|
|
290
|
+
},
|
|
291
|
+
})
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
if (!transferToken) {
|
|
295
|
+
throw new NotFoundError('The provided token id does not exist');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
...transferToken,
|
|
300
|
+
accessKey,
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* @param {number} lifespan
|
|
306
|
+
*
|
|
307
|
+
* @returns { { lifespan: null | number, expiresAt: null | number } }
|
|
308
|
+
*/
|
|
309
|
+
const getExpirationFields = (lifespan) => {
|
|
310
|
+
// it must be nil or a finite number >= 0
|
|
311
|
+
const isValidNumber = Number.isFinite(lifespan) && lifespan > 0;
|
|
312
|
+
if (!isValidNumber && !isNil(lifespan)) {
|
|
313
|
+
throw new ValidationError('lifespan must be a positive number or null');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
lifespan: lifespan || null,
|
|
318
|
+
expiresAt: lifespan ? Date.now() + lifespan : null,
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Return a secure sha512 hash of an accessKey
|
|
324
|
+
*
|
|
325
|
+
* @param {string} accessKey
|
|
326
|
+
*
|
|
327
|
+
* @returns {string}
|
|
328
|
+
*/
|
|
329
|
+
const hash = (accessKey) => {
|
|
330
|
+
return crypto
|
|
331
|
+
.createHmac('sha512', strapi.config.get('admin.transfer.token.salt'))
|
|
332
|
+
.update(accessKey)
|
|
333
|
+
.digest('hex');
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @returns {void}
|
|
338
|
+
*/
|
|
339
|
+
const checkSaltIsDefined = () => {
|
|
340
|
+
if (!strapi.config.get('admin.transfer.token.salt')) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
`Missing transfer.token.salt. Please set transfer.token.salt in config/admin.js (ex: you can generate one using Node with \`crypto.randomBytes(16).toString('base64')\`).
|
|
343
|
+
For security reasons, prefer storing the secret in an environment variable and read it in config/admin.js. See https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/environment.html#configuration-using-environment-variables.`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Flatten a token's database permissions objects to an array of strings
|
|
350
|
+
*
|
|
351
|
+
* @param {TransferToken} token
|
|
352
|
+
*
|
|
353
|
+
* @returns {TransferToken}
|
|
354
|
+
*/
|
|
355
|
+
const flattenTokenPermissions = (token) => {
|
|
356
|
+
if (!token) return token;
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
...token,
|
|
360
|
+
permissions: isArray(token.permissions) ? map('action', token.permissions) : token.permissions,
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Assert that a token's permissions are valid
|
|
366
|
+
*
|
|
367
|
+
* @param {TransferToken} token
|
|
368
|
+
*/
|
|
369
|
+
const assertTokenPermissionsValidity = (attributes) => {
|
|
370
|
+
const permissionService = strapi.admin.services.transfer.permission;
|
|
371
|
+
const validPermissions = permissionService.providers.action.keys();
|
|
372
|
+
const invalidPermissions = difference(attributes.permissions, validPermissions);
|
|
373
|
+
|
|
374
|
+
if (!isEmpty(invalidPermissions)) {
|
|
375
|
+
throw new ValidationError(`Unknown permissions provided: ${invalidPermissions.join(', ')}`);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Assert that a token's lifespan is valid
|
|
381
|
+
*
|
|
382
|
+
* @param {TransferToken} token
|
|
383
|
+
*/
|
|
384
|
+
const assertValidLifespan = ({ lifespan }) => {
|
|
385
|
+
if (isNil(lifespan)) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!Object.values(constants.TRANSFER_TOKEN_LIFESPANS).includes(lifespan)) {
|
|
390
|
+
throw new ValidationError(
|
|
391
|
+
`lifespan must be one of the following values:
|
|
392
|
+
${Object.values(constants.TRANSFER_TOKEN_LIFESPANS).join(', ')}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
module.exports = {
|
|
398
|
+
create,
|
|
399
|
+
list,
|
|
400
|
+
exists,
|
|
401
|
+
getBy,
|
|
402
|
+
getById,
|
|
403
|
+
getByName,
|
|
404
|
+
update,
|
|
405
|
+
revoke,
|
|
406
|
+
regenerate,
|
|
407
|
+
hash,
|
|
408
|
+
checkSaltIsDefined,
|
|
409
|
+
};
|
|
@@ -24,7 +24,8 @@ const extractToken = (ctx) => {
|
|
|
24
24
|
/**
|
|
25
25
|
* Authenticate the validity of the token
|
|
26
26
|
*
|
|
27
|
-
* @type {import('.').AuthenticateFunction}
|
|
27
|
+
* @type {import('.').AuthenticateFunction}
|
|
28
|
+
*/
|
|
28
29
|
const authenticate = async (ctx) => {
|
|
29
30
|
const apiTokenService = getService('api-token');
|
|
30
31
|
const token = extractToken(ctx);
|
|
@@ -72,7 +73,8 @@ const authenticate = async (ctx) => {
|
|
|
72
73
|
/**
|
|
73
74
|
* Verify the token has the required abilities for the requested scope
|
|
74
75
|
*
|
|
75
|
-
* @type {import('.').VerifyFunction}
|
|
76
|
+
* @type {import('.').VerifyFunction}
|
|
77
|
+
*/
|
|
76
78
|
const verify = (auth, config) => {
|
|
77
79
|
const { credentials: apiToken, ability } = auth;
|
|
78
80
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { UnauthorizedError, ForbiddenError } = require('@strapi/utils/lib/errors');
|
|
4
|
+
const { castArray, isNil } = require('lodash/fp');
|
|
5
|
+
|
|
6
|
+
const { getService } = require('../utils');
|
|
7
|
+
|
|
8
|
+
const extractToken = (ctx) => {
|
|
9
|
+
if (ctx.request && ctx.request.header && ctx.request.header.authorization) {
|
|
10
|
+
const parts = ctx.request.header.authorization.split(/\s+/);
|
|
11
|
+
|
|
12
|
+
if (parts[0].toLowerCase() !== 'bearer' || parts.length !== 2) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return parts[1];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Authenticate the validity of the token
|
|
24
|
+
*
|
|
25
|
+
* @type {import('.').AuthenticateFunction}
|
|
26
|
+
*/
|
|
27
|
+
const authenticate = async (ctx) => {
|
|
28
|
+
const { token: tokenService } = getService('transfer');
|
|
29
|
+
const token = extractToken(ctx);
|
|
30
|
+
|
|
31
|
+
if (!token) {
|
|
32
|
+
return { authenticated: false };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const transferToken = await tokenService.getBy({ accessKey: tokenService.hash(token) });
|
|
36
|
+
|
|
37
|
+
// Check if the token exists
|
|
38
|
+
if (!transferToken) {
|
|
39
|
+
return { authenticated: false };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if the token has expired
|
|
43
|
+
const currentDate = new Date();
|
|
44
|
+
|
|
45
|
+
if (!isNil(transferToken.expiresAt)) {
|
|
46
|
+
const expirationDate = new Date(transferToken.expiresAt);
|
|
47
|
+
|
|
48
|
+
if (expirationDate < currentDate) {
|
|
49
|
+
return { authenticated: false, error: new UnauthorizedError('Token expired') };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Update token metadata
|
|
54
|
+
await strapi.query('admin::transfer-token').update({
|
|
55
|
+
where: { id: transferToken.id },
|
|
56
|
+
data: { lastUsedAt: currentDate },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Generate an ability based on the token permissions
|
|
60
|
+
const ability = await getService('transfer').permission.engine.generateAbility(
|
|
61
|
+
transferToken.permissions.map((action) => ({ action }))
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return { authenticated: true, ability, credentials: transferToken };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Verify the token has the required abilities for the requested scope
|
|
69
|
+
*
|
|
70
|
+
* @type {import('.').VerifyFunction}
|
|
71
|
+
*/
|
|
72
|
+
const verify = async (auth, config = {}) => {
|
|
73
|
+
const { credentials: transferToken, ability } = auth;
|
|
74
|
+
|
|
75
|
+
if (!transferToken) {
|
|
76
|
+
throw new UnauthorizedError('Token not found');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const currentDate = new Date();
|
|
80
|
+
|
|
81
|
+
if (!isNil(transferToken.expiresAt)) {
|
|
82
|
+
const expirationDate = new Date(transferToken.expiresAt);
|
|
83
|
+
// token has expired
|
|
84
|
+
if (expirationDate < currentDate) {
|
|
85
|
+
throw new UnauthorizedError('Token expired');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!ability) {
|
|
90
|
+
throw new ForbiddenError();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const scopes = castArray(config.scope ?? []);
|
|
94
|
+
|
|
95
|
+
const isAllowed = scopes.every((scope) => ability.can(scope));
|
|
96
|
+
|
|
97
|
+
if (!isAllowed) {
|
|
98
|
+
throw new ForbiddenError();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** @type {import('.').AuthStrategy} */
|
|
103
|
+
module.exports = {
|
|
104
|
+
name: 'data-transfer',
|
|
105
|
+
authenticate,
|
|
106
|
+
verify,
|
|
107
|
+
};
|
package/server/utils/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import * as token from '../services/token';
|
|
|
7
7
|
import * as auth from '../services/auth';
|
|
8
8
|
import * as apiToken from '../services/api-token';
|
|
9
9
|
import * as projectSettings from '../services/project-settings';
|
|
10
|
+
import * as transfer from '../services/transfer';
|
|
10
11
|
|
|
11
12
|
type S = {
|
|
12
13
|
role: typeof role;
|
|
@@ -18,6 +19,7 @@ type S = {
|
|
|
18
19
|
metrics: typeof metrics;
|
|
19
20
|
'api-token': typeof apiToken;
|
|
20
21
|
'project-settings': typeof projectSettings;
|
|
22
|
+
transfer: typeof transfer;
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
export function getService<T extends keyof S>(name: T): S[T];
|
|
@@ -10,12 +10,7 @@ const apiTokenCreationSchema = yup
|
|
|
10
10
|
description: yup.string().optional(),
|
|
11
11
|
type: yup.string().oneOf(Object.values(constants.API_TOKEN_TYPE)).required(),
|
|
12
12
|
permissions: yup.array().of(yup.string()).nullable(),
|
|
13
|
-
lifespan: yup
|
|
14
|
-
.number()
|
|
15
|
-
.integer()
|
|
16
|
-
.min(1)
|
|
17
|
-
.oneOf(Object.values(constants.API_TOKEN_LIFESPANS))
|
|
18
|
-
.nullable(),
|
|
13
|
+
lifespan: yup.number().min(1).oneOf(Object.values(constants.API_TOKEN_LIFESPANS)).nullable(),
|
|
19
14
|
})
|
|
20
15
|
.noUnknown()
|
|
21
16
|
.strict();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { yup, validateYupSchema } = require('@strapi/utils');
|
|
4
|
+
const constants = require('../../services/constants');
|
|
5
|
+
|
|
6
|
+
const transferTokenCreationSchema = yup
|
|
7
|
+
.object()
|
|
8
|
+
.shape({
|
|
9
|
+
name: yup.string().min(1).required(),
|
|
10
|
+
description: yup.string().optional(),
|
|
11
|
+
permissions: yup.array().of(yup.string()).nullable(),
|
|
12
|
+
lifespan: yup
|
|
13
|
+
.number()
|
|
14
|
+
.min(1)
|
|
15
|
+
.oneOf(Object.values(constants.TRANSFER_TOKEN_LIFESPANS))
|
|
16
|
+
.nullable(),
|
|
17
|
+
})
|
|
18
|
+
.noUnknown()
|
|
19
|
+
.strict();
|
|
20
|
+
|
|
21
|
+
const transferTokenUpdateSchema = yup
|
|
22
|
+
.object()
|
|
23
|
+
.shape({
|
|
24
|
+
name: yup.string().min(1).notNull(),
|
|
25
|
+
description: yup.string().nullable(),
|
|
26
|
+
permissions: yup.array().of(yup.string()).nullable(),
|
|
27
|
+
})
|
|
28
|
+
.noUnknown()
|
|
29
|
+
.strict();
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
validateTransferTokenCreationInput: validateYupSchema(transferTokenCreationSchema),
|
|
33
|
+
validateTransferTokenUpdateInput: validateYupSchema(transferTokenUpdateSchema),
|
|
34
|
+
};
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import PropTypes from 'prop-types';
|
|
3
|
-
import { ContentLayout, Stack } from '@strapi/design-system';
|
|
4
|
-
import HeaderContentBox from '../ContentBox';
|
|
5
|
-
import FormApiTokenContainer from '../FormApiTokenContainer';
|
|
6
|
-
import Permissions from '../Permissions';
|
|
7
|
-
|
|
8
|
-
const FormBody = ({
|
|
9
|
-
apiToken,
|
|
10
|
-
errors,
|
|
11
|
-
onChange,
|
|
12
|
-
canEditInputs,
|
|
13
|
-
isCreating,
|
|
14
|
-
values,
|
|
15
|
-
onDispatch,
|
|
16
|
-
setHasChangedPermissions,
|
|
17
|
-
}) => {
|
|
18
|
-
return (
|
|
19
|
-
<ContentLayout>
|
|
20
|
-
<Stack spacing={6}>
|
|
21
|
-
{Boolean(apiToken?.name) && <HeaderContentBox apiToken={apiToken?.accessKey} />}
|
|
22
|
-
<FormApiTokenContainer
|
|
23
|
-
errors={errors}
|
|
24
|
-
onChange={onChange}
|
|
25
|
-
canEditInputs={canEditInputs}
|
|
26
|
-
isCreating={isCreating}
|
|
27
|
-
values={values}
|
|
28
|
-
apiToken={apiToken}
|
|
29
|
-
onDispatch={onDispatch}
|
|
30
|
-
setHasChangedPermissions={setHasChangedPermissions}
|
|
31
|
-
/>
|
|
32
|
-
<Permissions
|
|
33
|
-
disabled={
|
|
34
|
-
!canEditInputs || values?.type === 'read-only' || values?.type === 'full-access'
|
|
35
|
-
}
|
|
36
|
-
/>
|
|
37
|
-
</Stack>
|
|
38
|
-
</ContentLayout>
|
|
39
|
-
);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
FormBody.propTypes = {
|
|
43
|
-
errors: PropTypes.shape({
|
|
44
|
-
name: PropTypes.string,
|
|
45
|
-
description: PropTypes.string,
|
|
46
|
-
lifespan: PropTypes.string,
|
|
47
|
-
type: PropTypes.string,
|
|
48
|
-
}),
|
|
49
|
-
apiToken: PropTypes.shape({
|
|
50
|
-
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
51
|
-
type: PropTypes.string,
|
|
52
|
-
lifespan: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
53
|
-
name: PropTypes.string,
|
|
54
|
-
accessKey: PropTypes.string,
|
|
55
|
-
permissions: PropTypes.array,
|
|
56
|
-
description: PropTypes.string,
|
|
57
|
-
createdAt: PropTypes.string,
|
|
58
|
-
}),
|
|
59
|
-
onChange: PropTypes.func.isRequired,
|
|
60
|
-
canEditInputs: PropTypes.bool.isRequired,
|
|
61
|
-
isCreating: PropTypes.bool.isRequired,
|
|
62
|
-
values: PropTypes.shape({
|
|
63
|
-
name: PropTypes.string,
|
|
64
|
-
description: PropTypes.string,
|
|
65
|
-
lifespan: PropTypes.string,
|
|
66
|
-
type: PropTypes.string,
|
|
67
|
-
}).isRequired,
|
|
68
|
-
onDispatch: PropTypes.func.isRequired,
|
|
69
|
-
setHasChangedPermissions: PropTypes.func.isRequired,
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
FormBody.defaultProps = {
|
|
73
|
-
errors: {},
|
|
74
|
-
apiToken: {},
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export default FormBody;
|