@strapi/admin 4.4.0-beta.3 → 4.4.0-beta.4
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/DynamicTable/index.js +2 -2
- package/admin/src/contexts/ApiTokenPermissions/index.js +24 -0
- package/admin/src/hooks/index.js +1 -0
- package/admin/src/hooks/useRegenerate/index.js +34 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/ActionBoundRoutes/index.js +56 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/BoundRoute/getMethodColor.js +41 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/BoundRoute/index.js +72 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/CollapsableContentType/CheckBoxWrapper.js +30 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/CollapsableContentType/index.js +150 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/ContenTypesSection/index.js +37 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/Permissions/index.js +40 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/components/Regenerate/index.js +68 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/index.js +450 -180
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/init.js +13 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/reducer.js +72 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/utils/getDateOfExpiration.js +16 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/utils/index.js +5 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/utils/schema.js +2 -1
- package/admin/src/pages/SettingsPage/pages/ApiTokens/EditView/utils/transformPermissionsData.js +36 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/DefaultButton/index.js +63 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/DeleteButton/index.js +1 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/ReadButton/index.js +19 -0
- package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/UpdateButton/index.js +3 -36
- package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/DynamicTable/index.js +13 -11
- package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/index.js +3 -2
- package/admin/src/pages/SettingsPage/pages/ApiTokens/ListView/utils/tableHeaders.js +8 -8
- package/admin/src/pages/SettingsPage/pages/ApiTokens/ProtectedEditView/index.js +1 -1
- package/admin/src/permissions/defaultPermissions.js +2 -6
- package/admin/src/translations/en.json +17 -0
- package/build/4235.c44d8565.chunk.js +30 -0
- package/build/7379.d246dd38.chunk.js +1 -0
- package/build/{Admin-authenticatedApp.50e41ff2.chunk.js → Admin-authenticatedApp.6ad28580.chunk.js} +1 -1
- package/build/{Admin_homePage.118926e0.chunk.js → Admin_homePage.6d5e3236.chunk.js} +1 -1
- package/build/{Admin_profilePage.9d50ac44.chunk.js → Admin_profilePage.da32abbc.chunk.js} +1 -1
- package/build/{Admin_settingsPage.98a711e5.chunk.js → Admin_settingsPage.fc9c607a.chunk.js} +16 -16
- package/build/admin-app.7b7f9463.chunk.js +112 -0
- package/build/admin-edit-roles-page.4dd6bcb9.chunk.js +1 -0
- package/build/api-tokens-create-page.29cc87b6.chunk.js +1 -0
- package/build/api-tokens-edit-page.c294a88f.chunk.js +1 -0
- package/build/api-tokens-list-page.bb36535f.chunk.js +16 -0
- package/build/{content-manager.2a6f876d.chunk.js → content-manager.5ac9916a.chunk.js} +1 -1
- package/build/en-json.a9918c93.chunk.js +1 -0
- package/build/index.html +1 -1
- package/build/{main.fdc482f3.js → main.c04d580d.js} +1 -1
- package/build/{runtime~main.29105d25.js → runtime~main.3bd4f055.js} +2 -2
- package/build/sso-settings-page.9ceb0140.chunk.js +1 -0
- package/build/{webhook-edit-page.d2ea3351.chunk.js → webhook-edit-page.9e46fc3f.chunk.js} +1 -1
- package/package.json +8 -7
- package/server/bootstrap.js +19 -1
- package/server/config/admin-actions.js +20 -0
- package/server/content-types/api-token-permission.js +36 -0
- package/server/content-types/api-token.js +25 -1
- package/server/content-types/index.js +1 -0
- package/server/controllers/api-token.js +24 -1
- package/server/controllers/content-api.js +15 -0
- package/server/controllers/index.js +1 -0
- package/server/routes/api-tokens.js +11 -0
- package/server/routes/content-api.js +20 -0
- package/server/routes/index.js +2 -0
- package/server/services/api-token.js +310 -29
- package/server/services/constants.js +10 -0
- package/server/services/permission/engine.js +36 -226
- package/server/services/permission.js +4 -1
- package/server/strategies/admin.js +7 -1
- package/server/strategies/api-token.js +71 -11
- package/server/validation/api-tokens.js +12 -2
- package/build/admin-app.8bc3e80f.chunk.js +0 -112
- package/build/admin-edit-roles-page.554ba3fa.chunk.js +0 -1
- package/build/api-tokens-create-page.4c262d6e.chunk.js +0 -1
- package/build/api-tokens-edit-page.10a9d368.chunk.js +0 -1
- package/build/api-tokens-list-page.442c9f3c.chunk.js +0 -15
- package/build/en-json.12bc5a14.chunk.js +0 -1
- package/build/sso-settings-page.445184e0.chunk.js +0 -1
- package/server/services/permission/engine-hooks.js +0 -82
|
@@ -24,6 +24,8 @@ module.exports = {
|
|
|
24
24
|
name: trim(body.name),
|
|
25
25
|
description: trim(body.description),
|
|
26
26
|
type: body.type,
|
|
27
|
+
permissions: body.permissions,
|
|
28
|
+
lifespan: body.lifespan,
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
await validateApiTokenCreationInput(attributes);
|
|
@@ -37,6 +39,21 @@ module.exports = {
|
|
|
37
39
|
ctx.created({ data: apiToken });
|
|
38
40
|
},
|
|
39
41
|
|
|
42
|
+
async regenerate(ctx) {
|
|
43
|
+
const { id } = ctx.params;
|
|
44
|
+
const apiTokenService = getService('api-token');
|
|
45
|
+
|
|
46
|
+
const apiTokenExists = await apiTokenService.getById(id);
|
|
47
|
+
if (!apiTokenExists) {
|
|
48
|
+
ctx.notFound('API Token not found');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const accessToken = await apiTokenService.regenerate(id);
|
|
53
|
+
|
|
54
|
+
ctx.created({ data: accessToken });
|
|
55
|
+
},
|
|
56
|
+
|
|
40
57
|
async list(ctx) {
|
|
41
58
|
const apiTokenService = getService('api-token');
|
|
42
59
|
const apiTokens = await apiTokenService.list();
|
|
@@ -59,7 +76,6 @@ module.exports = {
|
|
|
59
76
|
|
|
60
77
|
if (!apiToken) {
|
|
61
78
|
ctx.notFound('API Token not found');
|
|
62
|
-
|
|
63
79
|
return;
|
|
64
80
|
}
|
|
65
81
|
|
|
@@ -108,4 +124,11 @@ module.exports = {
|
|
|
108
124
|
const apiToken = await apiTokenService.update(id, attributes);
|
|
109
125
|
ctx.send({ data: apiToken });
|
|
110
126
|
},
|
|
127
|
+
|
|
128
|
+
async getLayout(ctx) {
|
|
129
|
+
const apiTokenService = getService('api-token');
|
|
130
|
+
const layout = await apiTokenService.getApiTokenLayout();
|
|
131
|
+
|
|
132
|
+
ctx.send({ data: layout });
|
|
133
|
+
},
|
|
111
134
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
async getPermissions(ctx) {
|
|
5
|
+
const actionsMap = await strapi.contentAPI.permissions.getActionsMap();
|
|
6
|
+
|
|
7
|
+
ctx.send({ data: actionsMap });
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
async getRoutes(ctx) {
|
|
11
|
+
const routesMap = await strapi.contentAPI.getRoutesMap();
|
|
12
|
+
|
|
13
|
+
ctx.send({ data: routesMap });
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -56,4 +56,15 @@ module.exports = [
|
|
|
56
56
|
],
|
|
57
57
|
},
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
method: 'POST',
|
|
61
|
+
path: '/api-tokens/:id/regenerate',
|
|
62
|
+
handler: 'api-token.regenerate',
|
|
63
|
+
config: {
|
|
64
|
+
policies: [
|
|
65
|
+
'admin::isAuthenticatedAdmin',
|
|
66
|
+
{ name: 'admin::hasPermissions', config: { actions: ['admin::api-tokens.regenerate'] } },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
59
70
|
];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = [
|
|
4
|
+
{
|
|
5
|
+
method: 'GET',
|
|
6
|
+
path: '/content-api/permissions',
|
|
7
|
+
handler: 'content-api.getPermissions',
|
|
8
|
+
config: {
|
|
9
|
+
policies: ['admin::isAuthenticatedAdmin'],
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
method: 'GET',
|
|
14
|
+
path: '/content-api/routes',
|
|
15
|
+
handler: 'content-api.getRoutes',
|
|
16
|
+
config: {
|
|
17
|
+
policies: ['admin::isAuthenticatedAdmin'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
];
|
package/server/routes/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const users = require('./users');
|
|
|
7
7
|
const roles = require('./roles');
|
|
8
8
|
const webhooks = require('./webhooks');
|
|
9
9
|
const apiTokens = require('./api-tokens');
|
|
10
|
+
const contentApi = require('./content-api');
|
|
10
11
|
|
|
11
12
|
module.exports = [
|
|
12
13
|
...admin,
|
|
@@ -16,4 +17,5 @@ module.exports = [
|
|
|
16
17
|
...roles,
|
|
17
18
|
...webhooks,
|
|
18
19
|
...apiTokens,
|
|
20
|
+
...contentApi,
|
|
19
21
|
];
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
+
const { isNil } = require('lodash/fp');
|
|
5
|
+
const { omit, difference, isEmpty, map, isArray, uniq } = require('lodash/fp');
|
|
6
|
+
const { ValidationError, NotFoundError } = require('@strapi/utils').errors;
|
|
7
|
+
const constants = require('./constants');
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
|
-
* @typedef {'read-only'|'full-access'} TokenType
|
|
10
|
+
* @typedef {'read-only'|'full-access'|'custom'} TokenType
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
/**
|
|
@@ -11,20 +15,135 @@ const crypto = require('crypto');
|
|
|
11
15
|
*
|
|
12
16
|
* @property {number|string} id
|
|
13
17
|
* @property {string} name
|
|
14
|
-
* @property {string}
|
|
18
|
+
* @property {string} description
|
|
15
19
|
* @property {string} accessKey
|
|
20
|
+
* @property {number} lastUsedAt
|
|
21
|
+
* @property {number} lifespan
|
|
22
|
+
* @property {number} expiresAt
|
|
16
23
|
* @property {TokenType} type
|
|
24
|
+
* @property {(number|ApiTokenPermission)[]} permissions
|
|
17
25
|
*/
|
|
18
26
|
|
|
27
|
+
/**
|
|
28
|
+
* @typedef ApiTokenPermission
|
|
29
|
+
*
|
|
30
|
+
* @property {number|string} id
|
|
31
|
+
* @property {string} action
|
|
32
|
+
* @property {ApiToken|number} token
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** @constant {Array<string>} */
|
|
36
|
+
const SELECT_FIELDS = [
|
|
37
|
+
'id',
|
|
38
|
+
'name',
|
|
39
|
+
'description',
|
|
40
|
+
'lastUsedAt',
|
|
41
|
+
'type',
|
|
42
|
+
'lifespan',
|
|
43
|
+
'expiresAt',
|
|
44
|
+
'createdAt',
|
|
45
|
+
'updatedAt',
|
|
46
|
+
];
|
|
47
|
+
|
|
19
48
|
/** @constant {Array<string>} */
|
|
20
|
-
const
|
|
49
|
+
const POPULATE_FIELDS = ['permissions'];
|
|
50
|
+
|
|
51
|
+
// TODO: we need to ensure the permissions are actually valid registered permissions!
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Assert that a token's permissions attribute is valid for its type
|
|
55
|
+
*
|
|
56
|
+
* @param {ApiToken} token
|
|
57
|
+
*/
|
|
58
|
+
const assertCustomTokenPermissionsValidity = (attributes) => {
|
|
59
|
+
// Ensure non-custom tokens doesn't have permissions
|
|
60
|
+
if (attributes.type !== constants.API_TOKEN_TYPE.CUSTOM && !isEmpty(attributes.permissions)) {
|
|
61
|
+
throw new ValidationError('Non-custom tokens should not reference permissions');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Custom type tokens should always have permissions attached to them
|
|
65
|
+
if (attributes.type === constants.API_TOKEN_TYPE.CUSTOM && !isArray(attributes.permissions)) {
|
|
66
|
+
throw new ValidationError('Missing permissions attribute for custom token');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Permissions provided for a custom type token should be valid/registered permissions UID
|
|
70
|
+
if (attributes.type === constants.API_TOKEN_TYPE.CUSTOM) {
|
|
71
|
+
const validPermissions = strapi.contentAPI.permissions.providers.action.keys();
|
|
72
|
+
const invalidPermissions = difference(attributes.permissions, validPermissions);
|
|
73
|
+
|
|
74
|
+
if (!isEmpty(invalidPermissions)) {
|
|
75
|
+
throw new ValidationError(`Unknown permissions provided: ${invalidPermissions.join(', ')}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Assert that a token's permissions attribute is valid for its type
|
|
82
|
+
*
|
|
83
|
+
* @param {ApiToken} token
|
|
84
|
+
*/
|
|
85
|
+
const assertValidLifespan = ({ lifespan }) => {
|
|
86
|
+
if (isNil(lifespan)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!Object.values(constants.API_TOKEN_LIFESPANS).includes(lifespan)) {
|
|
91
|
+
throw new ValidationError(
|
|
92
|
+
`lifespan must be one of the following values:
|
|
93
|
+
${Object.values(constants.API_TOKEN_LIFESPANS).join(', ')}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Flatten a token's database permissions objects to an array of strings
|
|
100
|
+
*
|
|
101
|
+
* @param {ApiToken} token
|
|
102
|
+
*
|
|
103
|
+
* @returns {ApiToken}
|
|
104
|
+
*/
|
|
105
|
+
const flattenTokenPermissions = (token) => {
|
|
106
|
+
if (!token) return token;
|
|
107
|
+
return {
|
|
108
|
+
...token,
|
|
109
|
+
permissions: isArray(token.permissions) ? map('action', token.permissions) : token.permissions,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
21
112
|
|
|
22
113
|
/**
|
|
114
|
+
* Get a token
|
|
115
|
+
*
|
|
23
116
|
* @param {Object} whereParams
|
|
24
|
-
* @param {string|number}
|
|
25
|
-
* @param {string}
|
|
26
|
-
* @param {
|
|
27
|
-
* @param {string}
|
|
117
|
+
* @param {string|number} whereParams.id
|
|
118
|
+
* @param {string} whereParams.name
|
|
119
|
+
* @param {number} whereParams.lastUsedAt
|
|
120
|
+
* @param {string} whereParams.description
|
|
121
|
+
* @param {string} whereParams.accessKey
|
|
122
|
+
*
|
|
123
|
+
* @returns {Promise<Omit<ApiToken, 'accessKey'> | null>}
|
|
124
|
+
*/
|
|
125
|
+
const getBy = async (whereParams = {}) => {
|
|
126
|
+
if (Object.keys(whereParams).length === 0) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const token = await strapi
|
|
131
|
+
.query('admin::api-token')
|
|
132
|
+
.findOne({ select: SELECT_FIELDS, populate: POPULATE_FIELDS, where: whereParams });
|
|
133
|
+
|
|
134
|
+
if (!token) return token;
|
|
135
|
+
return flattenTokenPermissions(token);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if token exists
|
|
140
|
+
*
|
|
141
|
+
* @param {Object} whereParams
|
|
142
|
+
* @param {string|number} whereParams.id
|
|
143
|
+
* @param {string} whereParams.name
|
|
144
|
+
* @param {number} whereParams.lastUsedAt
|
|
145
|
+
* @param {string} whereParams.description
|
|
146
|
+
* @param {string} whereParams.accessKey
|
|
28
147
|
*
|
|
29
148
|
* @returns {Promise<boolean>}
|
|
30
149
|
*/
|
|
@@ -35,6 +154,8 @@ const exists = async (whereParams = {}) => {
|
|
|
35
154
|
};
|
|
36
155
|
|
|
37
156
|
/**
|
|
157
|
+
* Return a secure sha512 hash of an accessKey
|
|
158
|
+
*
|
|
38
159
|
* @param {string} accessKey
|
|
39
160
|
*
|
|
40
161
|
* @returns {string}
|
|
@@ -47,24 +168,103 @@ const hash = (accessKey) => {
|
|
|
47
168
|
};
|
|
48
169
|
|
|
49
170
|
/**
|
|
171
|
+
* @param {number} lifespan
|
|
172
|
+
*
|
|
173
|
+
* @returns { { lifespan: null | number, expiresAt: null | number } }
|
|
174
|
+
*/
|
|
175
|
+
const getExpirationFields = (lifespan) => {
|
|
176
|
+
// it must be nil or a finite number >= 0
|
|
177
|
+
const isValidNumber = Number.isFinite(lifespan) && lifespan > 0;
|
|
178
|
+
if (!isValidNumber && !isNil(lifespan)) {
|
|
179
|
+
throw new ValidationError('lifespan must be a positive number or null');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
lifespan: lifespan || null,
|
|
184
|
+
expiresAt: lifespan ? Date.now() + lifespan : null,
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Create a token and its permissions
|
|
190
|
+
*
|
|
50
191
|
* @param {Object} attributes
|
|
51
192
|
* @param {TokenType} attributes.type
|
|
52
193
|
* @param {string} attributes.name
|
|
53
|
-
* @param {
|
|
194
|
+
* @param {number} attributes.lifespan
|
|
195
|
+
* @param {string[]} attributes.permissions
|
|
196
|
+
* @param {string} attributes.description
|
|
54
197
|
*
|
|
55
198
|
* @returns {Promise<ApiToken>}
|
|
56
199
|
*/
|
|
57
200
|
const create = async (attributes) => {
|
|
58
201
|
const accessKey = crypto.randomBytes(128).toString('hex');
|
|
59
202
|
|
|
203
|
+
assertCustomTokenPermissionsValidity(attributes);
|
|
204
|
+
assertValidLifespan(attributes);
|
|
205
|
+
|
|
206
|
+
// Create the token
|
|
60
207
|
const apiToken = await strapi.query('admin::api-token').create({
|
|
61
208
|
select: SELECT_FIELDS,
|
|
209
|
+
populate: POPULATE_FIELDS,
|
|
62
210
|
data: {
|
|
63
|
-
...attributes,
|
|
211
|
+
...omit('permissions', attributes),
|
|
64
212
|
accessKey: hash(accessKey),
|
|
213
|
+
...getExpirationFields(attributes.lifespan),
|
|
65
214
|
},
|
|
66
215
|
});
|
|
67
216
|
|
|
217
|
+
const result = { ...apiToken, accessKey };
|
|
218
|
+
|
|
219
|
+
// If this is a custom type token, create and the related permissions
|
|
220
|
+
if (attributes.type === constants.API_TOKEN_TYPE.CUSTOM) {
|
|
221
|
+
// TODO: createMany doesn't seem to create relation properly, implement a better way rather than a ton of queries
|
|
222
|
+
// const permissionsCount = await strapi.query('admin::api-token-permission').createMany({
|
|
223
|
+
// populate: POPULATE_FIELDS,
|
|
224
|
+
// data: attributes.permissions.map(action => ({ action, token: apiToken })),
|
|
225
|
+
// });
|
|
226
|
+
await Promise.all(
|
|
227
|
+
uniq(attributes.permissions).map((action) =>
|
|
228
|
+
strapi.query('admin::api-token-permission').create({
|
|
229
|
+
data: { action, token: apiToken },
|
|
230
|
+
})
|
|
231
|
+
)
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const currentPermissions = await strapi.entityService.load(
|
|
235
|
+
'admin::api-token',
|
|
236
|
+
apiToken,
|
|
237
|
+
'permissions'
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
if (currentPermissions) {
|
|
241
|
+
Object.assign(result, { permissions: map('action', currentPermissions) });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return result;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @param {string|number} id
|
|
250
|
+
*
|
|
251
|
+
* @returns {Promise<ApiToken>}
|
|
252
|
+
*/
|
|
253
|
+
const regenerate = async (id) => {
|
|
254
|
+
const accessKey = crypto.randomBytes(128).toString('hex');
|
|
255
|
+
|
|
256
|
+
const apiToken = await strapi.query('admin::api-token').update({
|
|
257
|
+
select: ['id', 'accessKey'],
|
|
258
|
+
where: { id },
|
|
259
|
+
data: {
|
|
260
|
+
accessKey: hash(accessKey),
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (!apiToken) {
|
|
265
|
+
throw new NotFoundError('The provided token id does not exist');
|
|
266
|
+
}
|
|
267
|
+
|
|
68
268
|
return {
|
|
69
269
|
...apiToken,
|
|
70
270
|
accessKey,
|
|
@@ -92,25 +292,37 @@ For security reasons, prefer storing the secret in an environment variable and r
|
|
|
92
292
|
};
|
|
93
293
|
|
|
94
294
|
/**
|
|
295
|
+
* Return a list of all tokens and their permissions
|
|
296
|
+
*
|
|
95
297
|
* @returns {Promise<Omit<ApiToken, 'accessKey'>>}
|
|
96
298
|
*/
|
|
97
299
|
const list = async () => {
|
|
98
|
-
|
|
300
|
+
const tokens = await strapi.query('admin::api-token').findMany({
|
|
99
301
|
select: SELECT_FIELDS,
|
|
302
|
+
populate: POPULATE_FIELDS,
|
|
100
303
|
orderBy: { name: 'ASC' },
|
|
101
304
|
});
|
|
305
|
+
|
|
306
|
+
if (!tokens) return tokens;
|
|
307
|
+
return tokens.map((token) => flattenTokenPermissions(token));
|
|
102
308
|
};
|
|
103
309
|
|
|
104
310
|
/**
|
|
311
|
+
* Revoke (delete) a token
|
|
312
|
+
*
|
|
105
313
|
* @param {string|number} id
|
|
106
314
|
*
|
|
107
315
|
* @returns {Promise<Omit<ApiToken, 'accessKey'>>}
|
|
108
316
|
*/
|
|
109
317
|
const revoke = async (id) => {
|
|
110
|
-
return strapi
|
|
318
|
+
return strapi
|
|
319
|
+
.query('admin::api-token')
|
|
320
|
+
.delete({ select: SELECT_FIELDS, populate: POPULATE_FIELDS, where: { id } });
|
|
111
321
|
};
|
|
112
322
|
|
|
113
323
|
/**
|
|
324
|
+
* Retrieve a token by id
|
|
325
|
+
*
|
|
114
326
|
* @param {string|number} id
|
|
115
327
|
*
|
|
116
328
|
* @returns {Promise<Omit<ApiToken, 'accessKey'>>}
|
|
@@ -120,6 +332,8 @@ const getById = async (id) => {
|
|
|
120
332
|
};
|
|
121
333
|
|
|
122
334
|
/**
|
|
335
|
+
* Retrieve a token by name
|
|
336
|
+
*
|
|
123
337
|
* @param {string} name
|
|
124
338
|
*
|
|
125
339
|
* @returns {Promise<Omit<ApiToken, 'accessKey'>>}
|
|
@@ -129,39 +343,106 @@ const getByName = async (name) => {
|
|
|
129
343
|
};
|
|
130
344
|
|
|
131
345
|
/**
|
|
346
|
+
* Update a token and its permissions
|
|
347
|
+
*
|
|
132
348
|
* @param {string|number} id
|
|
133
349
|
* @param {Object} attributes
|
|
134
350
|
* @param {TokenType} attributes.type
|
|
135
351
|
* @param {string} attributes.name
|
|
136
|
-
* @param {
|
|
352
|
+
* @param {number} attributes.lastUsedAt
|
|
353
|
+
* @param {string[]} attributes.permissions
|
|
354
|
+
* @param {string} attributes.description
|
|
137
355
|
*
|
|
138
356
|
* @returns {Promise<Omit<ApiToken, 'accessKey'>>}
|
|
139
357
|
*/
|
|
140
358
|
const update = async (id, attributes) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.update({ where: { id }, data: attributes, select: SELECT_FIELDS });
|
|
144
|
-
};
|
|
359
|
+
// retrieve token without permissions
|
|
360
|
+
const originalToken = await strapi.query('admin::api-token').findOne({ where: { id } });
|
|
145
361
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
* @param {string|number} [whereParams.id]
|
|
149
|
-
* @param {string} [whereParams.name]
|
|
150
|
-
* @param {string} [whereParams.description]
|
|
151
|
-
* @param {string} [whereParams.accessKey]
|
|
152
|
-
*
|
|
153
|
-
* @returns {Promise<Omit<ApiToken, 'accessKey'> | null>}
|
|
154
|
-
*/
|
|
155
|
-
const getBy = async (whereParams = {}) => {
|
|
156
|
-
if (Object.keys(whereParams).length === 0) {
|
|
157
|
-
return null;
|
|
362
|
+
if (!originalToken) {
|
|
363
|
+
throw new NotFoundError('Token not found');
|
|
158
364
|
}
|
|
159
365
|
|
|
160
|
-
|
|
366
|
+
const changingTypeToCustom =
|
|
367
|
+
attributes.type === constants.API_TOKEN_TYPE.CUSTOM &&
|
|
368
|
+
originalToken.type !== constants.API_TOKEN_TYPE.CUSTOM;
|
|
369
|
+
|
|
370
|
+
// if we're updating the permissions on any token type, or changing from non-custom to custom, ensure they're still valid
|
|
371
|
+
// if neither type nor permissions are changing, we don't need to validate again or else we can't allow partial update
|
|
372
|
+
if (attributes.permissions || changingTypeToCustom) {
|
|
373
|
+
assertCustomTokenPermissionsValidity({
|
|
374
|
+
...originalToken,
|
|
375
|
+
...attributes,
|
|
376
|
+
type: attributes.type || originalToken.type,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
assertValidLifespan(attributes);
|
|
381
|
+
|
|
382
|
+
const updatedToken = await strapi.query('admin::api-token').update({
|
|
383
|
+
select: SELECT_FIELDS,
|
|
384
|
+
populate: POPULATE_FIELDS,
|
|
385
|
+
where: { id },
|
|
386
|
+
data: omit('permissions', attributes),
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// custom tokens need to have their permissions updated as well
|
|
390
|
+
if (updatedToken.type === constants.API_TOKEN_TYPE.CUSTOM && attributes.permissions) {
|
|
391
|
+
const currentPermissionsResult = await strapi.entityService.load(
|
|
392
|
+
'admin::api-token',
|
|
393
|
+
updatedToken,
|
|
394
|
+
'permissions'
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const currentPermissions = map('action', currentPermissionsResult || []);
|
|
398
|
+
const newPermissions = uniq(attributes.permissions);
|
|
399
|
+
|
|
400
|
+
const actionsToDelete = difference(currentPermissions, newPermissions);
|
|
401
|
+
const actionsToAdd = difference(newPermissions, currentPermissions);
|
|
402
|
+
|
|
403
|
+
// TODO: improve efficiency here
|
|
404
|
+
// method using a loop -- works but very inefficient
|
|
405
|
+
await Promise.all(
|
|
406
|
+
actionsToDelete.map((action) =>
|
|
407
|
+
strapi.query('admin::api-token-permission').delete({
|
|
408
|
+
where: { action, token: id },
|
|
409
|
+
})
|
|
410
|
+
)
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// TODO: improve efficiency here
|
|
414
|
+
// using a loop -- works but very inefficient
|
|
415
|
+
await Promise.all(
|
|
416
|
+
actionsToAdd.map((action) =>
|
|
417
|
+
strapi.query('admin::api-token-permission').create({
|
|
418
|
+
data: { action, token: id },
|
|
419
|
+
})
|
|
420
|
+
)
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
// if type is not custom, make sure any old permissions get removed
|
|
424
|
+
else if (updatedToken.type !== constants.API_TOKEN_TYPE.CUSTOM) {
|
|
425
|
+
await strapi.query('admin::api-token-permission').delete({
|
|
426
|
+
where: { token: id },
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// retrieve permissions
|
|
431
|
+
const permissionsFromDb = await strapi.entityService.load(
|
|
432
|
+
'admin::api-token',
|
|
433
|
+
updatedToken,
|
|
434
|
+
'permissions'
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
...updatedToken,
|
|
439
|
+
permissions: permissionsFromDb ? permissionsFromDb.map((p) => p.action) : undefined,
|
|
440
|
+
};
|
|
161
441
|
};
|
|
162
442
|
|
|
163
443
|
module.exports = {
|
|
164
444
|
create,
|
|
445
|
+
regenerate,
|
|
165
446
|
exists,
|
|
166
447
|
checkSaltIsDefined,
|
|
167
448
|
hash,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const DAY_IN_MS = 24 * 60 * 60 * 1000;
|
|
4
|
+
|
|
3
5
|
module.exports = {
|
|
4
6
|
CONTENT_TYPE_SECTION: 'contentTypes',
|
|
5
7
|
SUPER_ADMIN_CODE: 'strapi-super-admin',
|
|
@@ -13,5 +15,13 @@ module.exports = {
|
|
|
13
15
|
API_TOKEN_TYPE: {
|
|
14
16
|
READ_ONLY: 'read-only',
|
|
15
17
|
FULL_ACCESS: 'full-access',
|
|
18
|
+
CUSTOM: 'custom',
|
|
19
|
+
},
|
|
20
|
+
// The front-end only displays these values
|
|
21
|
+
API_TOKEN_LIFESPANS: {
|
|
22
|
+
UNLIMITED: null,
|
|
23
|
+
DAYS_7: 7 * DAY_IN_MS,
|
|
24
|
+
DAYS_30: 30 * DAY_IN_MS,
|
|
25
|
+
DAYS_90: 90 * DAY_IN_MS,
|
|
16
26
|
},
|
|
17
27
|
};
|