2021-08-26 14:37:55 +02:00
'use strict' ;
const crypto = require ( 'crypto' ) ;
2022-08-12 09:52:47 +02:00
const { omit , difference , isEmpty , map , isArray } = require ( 'lodash/fp' ) ;
2022-08-10 10:55:49 +02:00
const { ValidationError , NotFoundError } = require ( '@strapi/utils' ) . errors ;
2022-08-18 12:20:45 +02:00
const constants = require ( './constants' ) ;
2021-08-26 14:37:55 +02:00
2021-08-30 14:00:53 +02:00
/ * *
2022-08-05 12:01:36 +02:00
* @ typedef { 'read-only' | 'full-access' | 'custom' } TokenType
2021-08-30 14:00:53 +02:00
* /
2021-08-27 09:44:29 +02:00
/ * *
* @ typedef ApiToken
*
2021-08-30 14:00:53 +02:00
* @ property { number | string } id
2021-08-27 09:44:29 +02:00
* @ property { string } name
2022-08-19 16:41:39 +02:00
* @ property { string } description
2021-08-27 09:44:29 +02:00
* @ property { string } accessKey
2022-08-19 16:36:28 +02:00
* @ property { number } lastUsedAt
2021-08-30 14:00:53 +02:00
* @ property { TokenType } type
2022-08-19 16:41:39 +02:00
* @ property { ( number | ApiTokenPermission ) [ ] } permissions
2022-08-05 12:01:36 +02:00
* /
/ * *
* @ typedef ApiTokenPermission
*
* @ property { number | string } id
* @ property { string } action
2022-08-19 16:41:39 +02:00
* @ property { ApiToken | number } token
2021-08-27 09:44:29 +02:00
* /
2021-09-02 10:47:06 +02:00
/** @constant {Array<string>} */
2022-08-19 16:36:28 +02:00
const SELECT _FIELDS = [ 'id' , 'name' , 'description' , 'lastUsedAt' , 'type' , 'createdAt' , 'updatedAt' ] ;
2021-09-02 10:47:06 +02:00
2022-08-05 12:01:36 +02:00
/** @constant {Array<string>} */
const POPULATE _FIELDS = [ 'permissions' ] ;
2022-08-19 16:53:51 +02:00
/ * *
* Assert that a token ' s permissions attribute is valid for its type
*
* @ param { ApiToken } token
* /
2022-08-18 12:20:45 +02:00
const assertCustomTokenPermissionsValidity = ( attributes ) => {
2022-08-05 12:01:36 +02:00
// Ensure non-custom tokens doesn't have permissions
2022-08-10 10:55:49 +02:00
if ( attributes . type !== constants . API _TOKEN _TYPE . CUSTOM && ! isEmpty ( attributes . permissions ) ) {
2022-08-11 10:48:40 +02:00
throw new ValidationError ( 'Non-custom tokens should not reference permissions' ) ;
2022-08-05 12:01:36 +02:00
}
// Custom type tokens should always have permissions attached to them
2022-08-09 10:49:44 +02:00
if ( attributes . type === constants . API _TOKEN _TYPE . CUSTOM && isEmpty ( attributes . permissions ) ) {
2022-08-11 10:48:40 +02:00
throw new ValidationError ( 'Missing permissions attribute for custom token' ) ;
2022-08-05 12:01:36 +02:00
}
} ;
2021-08-26 14:37:55 +02:00
/ * *
2022-08-19 16:53:51 +02:00
* Flatten a token ' s database permissions objects to an array of strings
*
2022-08-19 16:45:16 +02:00
* @ param { ApiToken } token
*
* @ returns { ApiToken }
* /
const flattenTokenPermissions = ( token ) => {
2022-08-19 16:42:55 +02:00
if ( ! token ) return token ;
return {
... token ,
permissions : isArray ( token . permissions ) ? map ( 'action' , token . permissions ) : token . permissions ,
} ;
} ;
/ * *
2022-08-19 16:53:51 +02:00
* Get a token
*
2022-08-19 16:42:55 +02:00
* @ param { Object } whereParams
* @ param { string | number } whereParams . id
* @ param { string } whereParams . name
* @ param { number } whereParams . lastUsedAt
* @ param { string } whereParams . description
* @ param { string } whereParams . accessKey
*
* @ returns { Promise < Omit < ApiToken , 'accessKey' > | null > }
* /
const getBy = async ( whereParams = { } ) => {
if ( Object . keys ( whereParams ) . length === 0 ) {
return null ;
}
const token = await strapi
. query ( 'admin::api-token' )
. findOne ( { select : SELECT _FIELDS , populate : POPULATE _FIELDS , where : whereParams } ) ;
if ( ! token ) return token ;
2022-08-19 16:45:16 +02:00
return flattenTokenPermissions ( token ) ;
2022-08-19 16:42:55 +02:00
} ;
2021-08-26 14:37:55 +02:00
/ * *
2022-08-19 16:53:51 +02:00
* Check if token exists
*
2021-08-27 16:23:19 +02:00
* @ param { Object } whereParams
2022-08-19 16:41:39 +02:00
* @ param { string | number } whereParams . id
* @ param { string } whereParams . name
* @ param { number } whereParams . lastUsedAt
* @ param { string } whereParams . description
* @ param { string } whereParams . accessKey
2021-08-26 14:37:55 +02:00
*
2021-08-27 10:30:18 +02:00
* @ returns { Promise < boolean > }
2021-08-26 14:37:55 +02:00
* /
2021-08-27 16:23:19 +02:00
const exists = async ( whereParams = { } ) => {
2021-09-16 14:36:54 +02:00
const apiToken = await getBy ( whereParams ) ;
2021-08-27 16:23:19 +02:00
return ! ! apiToken ;
} ;
/ * *
2022-08-19 16:53:51 +02:00
* Return a secure sha512 hash of an accessKey
*
2021-08-27 16:23:19 +02:00
* @ param { string } accessKey
*
* @ returns { string }
* /
2022-08-08 23:33:39 +02:00
const hash = ( accessKey ) => {
2021-08-27 16:23:19 +02:00
return crypto
2021-10-26 12:18:53 +02:00
. createHmac ( 'sha512' , strapi . config . get ( 'admin.apiToken.salt' ) )
2021-08-27 17:06:05 +02:00
. update ( accessKey )
2021-08-27 16:23:19 +02:00
. digest ( 'hex' ) ;
2021-08-26 14:37:55 +02:00
} ;
/ * *
2022-08-19 16:53:51 +02:00
* Create a token and its permissions
*
2021-08-26 14:37:55 +02:00
* @ param { Object } attributes
2021-08-30 14:00:53 +02:00
* @ param { TokenType } attributes . type
2021-08-26 14:37:55 +02:00
* @ param { string } attributes . name
2022-08-19 16:41:39 +02:00
* @ param { string [ ] } attributes . permissions
* @ param { string } attributes . description
2021-08-26 14:37:55 +02:00
*
2021-08-27 09:44:29 +02:00
* @ returns { Promise < ApiToken > }
2021-08-26 14:37:55 +02:00
* /
2022-08-08 23:33:39 +02:00
const create = async ( attributes ) => {
2021-08-26 14:37:55 +02:00
const accessKey = crypto . randomBytes ( 128 ) . toString ( 'hex' ) ;
2022-08-05 12:01:36 +02:00
assertCustomTokenPermissionsValidity ( attributes ) ;
// Create the token
2021-08-27 16:23:19 +02:00
const apiToken = await strapi . query ( 'admin::api-token' ) . create ( {
2021-09-02 10:47:06 +02:00
select : SELECT _FIELDS ,
2022-08-05 12:31:16 +02:00
populate : POPULATE _FIELDS ,
2021-08-26 14:37:55 +02:00
data : {
2022-08-05 12:01:36 +02:00
... omit ( 'permissions' , attributes ) ,
2021-08-27 16:23:19 +02:00
accessKey : hash ( accessKey ) ,
2021-08-26 14:37:55 +02:00
} ,
} ) ;
2021-08-27 16:23:19 +02:00
2022-08-05 12:01:36 +02:00
const result = { ... apiToken , accessKey } ;
2022-08-10 17:35:15 +02:00
// If this is a custom type token, create and the related permissions
2022-08-08 17:06:38 +02:00
if ( attributes . type === constants . API _TOKEN _TYPE . CUSTOM ) {
2022-08-11 10:48:40 +02:00
// TODO: createMany doesn't seem to create relation properly, implement a better way rather than a ton of queries
2022-08-10 17:35:15 +02:00
// const permissionsCount = await strapi.query('admin::token-permission').createMany({
// populate: POPULATE_FIELDS,
// data: attributes.permissions.map(action => ({ action, token: apiToken })),
// });
2022-08-18 11:53:30 +02:00
await Promise . all (
2022-08-18 12:20:45 +02:00
attributes . permissions . map ( ( action ) =>
2022-08-10 17:35:15 +02:00
strapi . query ( 'admin::token-permission' ) . create ( {
data : { action , token : apiToken } ,
} )
2022-08-18 11:53:30 +02:00
)
) ;
2022-08-10 17:35:15 +02:00
const currentPermissions = await strapi . entityService . load (
'admin::api-token' ,
apiToken ,
'permissions'
) ;
2022-08-05 12:01:36 +02:00
2022-08-10 17:35:15 +02:00
if ( currentPermissions ) {
Object . assign ( result , { permissions : map ( 'action' , currentPermissions ) } ) ;
2022-08-09 10:27:34 +02:00
}
2022-08-05 12:01:36 +02:00
}
return result ;
2021-08-27 16:23:19 +02:00
} ;
2022-08-18 13:31:02 +02:00
/ * *
* @ param { string | number } id
*
* @ returns { Promise < ApiToken > }
* /
const regenerate = async ( id ) => {
const accessKey = crypto . randomBytes ( 128 ) . toString ( 'hex' ) ;
2022-08-18 14:03:59 +02:00
const apiToken = await strapi . query ( 'admin::api-token' ) . update ( {
2022-08-18 13:31:02 +02:00
select : [ 'id' , 'accessKey' ] ,
2022-08-18 14:03:59 +02:00
where : { id } ,
2022-08-18 13:31:02 +02:00
data : {
accessKey : hash ( accessKey ) ,
} ,
} ) ;
2022-08-22 12:33:00 +02:00
if ( ! apiToken ) {
throw new NotFoundError ( 'The provided token id does not exist' ) ;
}
2022-08-18 13:31:02 +02:00
return {
... apiToken ,
accessKey ,
} ;
} ;
2021-08-27 16:23:19 +02:00
/ * *
* @ returns { void }
* /
2022-01-24 18:13:27 +01:00
const checkSaltIsDefined = ( ) => {
if ( ! strapi . config . get ( 'admin.apiToken.salt' ) ) {
2022-02-14 14:57:15 +01:00
// TODO V5: stop reading API_TOKEN_SALT
if ( process . env . API _TOKEN _SALT ) {
2022-03-04 15:48:49 +01:00
process . emitWarning ( ` [deprecated] In future versions, Strapi will stop reading directly from the environment variable API_TOKEN_SALT. Please set apiToken.salt in config/admin.js instead.
For security reasons , keep storing the secret in an environment variable and use env ( ) to read it in config / admin . js ( ex : \ ` apiToken: { salt: env('API_TOKEN_SALT') } \` ). See https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/environment.html#configuration-using-environment-variables. ` ) ;
2022-02-14 14:57:15 +01:00
strapi . config . set ( 'admin.apiToken.salt' , process . env . API _TOKEN _SALT ) ;
} else {
throw new Error (
2022-03-18 17:55:22 +01:00
` Missing apiToken.salt. Please set apiToken.salt in config/admin.js (ex: you can generate one using Node with \` crypto.randomBytes(16).toString('base64') \` ).
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.`
2022-02-14 14:57:15 +01:00
) ;
}
2021-08-27 16:23:19 +02:00
}
2021-08-26 14:37:55 +02:00
} ;
2021-08-27 08:14:36 +02:00
/ * *
2022-08-19 16:53:51 +02:00
* Return a list of all tokens and their permissions
*
2021-09-02 10:47:06 +02:00
* @ returns { Promise < Omit < ApiToken , 'accessKey' >> }
2021-08-27 08:14:36 +02:00
* /
const list = async ( ) => {
2022-08-12 09:52:47 +02:00
const tokens = await strapi . query ( 'admin::api-token' ) . findMany ( {
2021-09-02 10:47:06 +02:00
select : SELECT _FIELDS ,
2022-08-05 12:01:36 +02:00
populate : POPULATE _FIELDS ,
2021-08-27 08:39:08 +02:00
orderBy : { name : 'ASC' } ,
} ) ;
2022-08-12 09:52:47 +02:00
if ( ! tokens ) return tokens ;
2022-08-19 16:45:16 +02:00
return tokens . map ( ( token ) => flattenTokenPermissions ( token ) ) ;
2021-08-27 08:14:36 +02:00
} ;
2021-08-31 15:31:54 +02:00
/ * *
2022-08-19 16:53:51 +02:00
* Revoke ( delete ) a token
*
2021-08-31 15:31:54 +02:00
* @ param { string | number } id
*
2021-09-02 10:47:06 +02:00
* @ returns { Promise < Omit < ApiToken , 'accessKey' >> }
2021-08-31 15:31:54 +02:00
* /
2022-08-08 23:33:39 +02:00
const revoke = async ( id ) => {
2022-08-05 12:01:36 +02:00
return strapi
. query ( 'admin::api-token' )
. delete ( { select : SELECT _FIELDS , populate : POPULATE _FIELDS , where : { id } } ) ;
2021-08-31 15:31:54 +02:00
} ;
2021-09-02 11:56:14 +02:00
/ * *
2022-08-19 16:53:51 +02:00
* Retrieve a token by id
*
2021-09-02 11:56:14 +02:00
* @ param { string | number } id
*
* @ returns { Promise < Omit < ApiToken , 'accessKey' >> }
* /
2022-08-08 23:33:39 +02:00
const getById = async ( id ) => {
2021-09-16 14:36:54 +02:00
return getBy ( { id } ) ;
2021-09-02 11:56:14 +02:00
} ;
2021-09-08 14:38:43 +02:00
/ * *
2022-08-19 16:53:51 +02:00
* Retrieve a token by name
*
2021-09-08 14:38:43 +02:00
* @ param { string } name
*
* @ returns { Promise < Omit < ApiToken , 'accessKey' >> }
* /
2022-08-08 23:33:39 +02:00
const getByName = async ( name ) => {
2021-09-16 14:36:54 +02:00
return getBy ( { name } ) ;
2021-09-08 14:38:43 +02:00
} ;
2021-09-06 13:30:52 +02:00
/ * *
2022-08-19 16:53:51 +02:00
* Update a token and its permissions
*
2021-09-06 13:30:52 +02:00
* @ param { string | number } id
* @ param { Object } attributes
* @ param { TokenType } attributes . type
* @ param { string } attributes . name
2022-08-19 16:36:28 +02:00
* @ param { number } attributes . lastUsedAt
2022-08-19 16:41:39 +02:00
* @ param { string [ ] } attributes . permissions
* @ param { string } attributes . description
2021-09-06 13:30:52 +02:00
*
* @ returns { Promise < Omit < ApiToken , 'accessKey' >> }
* /
const update = async ( id , attributes ) => {
2022-08-10 10:55:49 +02:00
// retrieve token without permissions
2022-08-11 10:48:40 +02:00
const originalToken = await strapi . query ( 'admin::api-token' ) . findOne ( { where : { id } } ) ;
2022-08-05 12:31:16 +02:00
2022-08-11 10:48:40 +02:00
if ( ! originalToken ) {
2022-08-10 10:55:49 +02:00
throw new NotFoundError ( 'Token not found' ) ;
2022-08-05 12:31:16 +02:00
}
2022-08-18 10:22:09 +02:00
const changingTypeToCustom =
attributes . type === constants . API _TOKEN _TYPE . custom &&
originalToken . type !== constants . API _TOKEN _TYPE . custom ;
// if we're updating the permissions on any token type, or changing from non-custom to custom, ensure they're still valid
// if neither type nor permissions are changing, we don't need to validate again or else we can't allow partial update
if ( attributes . permissions || changingTypeToCustom ) {
assertCustomTokenPermissionsValidity ( {
... originalToken ,
... attributes ,
type : attributes . type || originalToken . type ,
} ) ;
}
2022-08-05 12:01:36 +02:00
2022-08-11 10:48:40 +02:00
const updatedToken = await strapi . query ( 'admin::api-token' ) . update ( {
2022-08-05 12:01:36 +02:00
select : SELECT _FIELDS ,
populate : POPULATE _FIELDS ,
where : { id } ,
data : omit ( 'permissions' , attributes ) ,
} ) ;
2022-08-05 12:31:16 +02:00
2022-08-18 10:22:09 +02:00
// custom tokens need to have their permissions updated as well
if ( updatedToken . type === constants . API _TOKEN _TYPE . CUSTOM && attributes . permissions ) {
2022-08-11 11:04:12 +02:00
const currentPermissionsResult =
( await strapi . entityService . load ( 'admin::api-token' , updatedToken , 'permissions' ) ) || [ ] ;
2022-08-10 17:35:15 +02:00
2022-08-11 11:04:12 +02:00
const actionsToDelete = difference (
map ( 'action' , currentPermissionsResult ) ,
2022-08-10 17:35:15 +02:00
attributes . permissions
) ;
2022-08-11 11:04:12 +02:00
const actionsToAdd = difference ( attributes . permissions , originalToken . permissions ) ;
2022-08-10 17:35:15 +02:00
2022-08-11 18:04:59 +02:00
// TODO: improve efficiency here
// method using a loop -- works but very inefficient
2022-08-18 11:53:30 +02:00
await Promise . all (
2022-08-18 12:20:45 +02:00
actionsToDelete . map ( ( action ) =>
2022-08-11 13:00:21 +02:00
strapi . query ( 'admin::token-permission' ) . delete ( {
where : { action , token : id } ,
} )
2022-08-18 11:53:30 +02:00
)
) ;
2022-08-10 17:35:15 +02:00
2022-08-11 18:04:59 +02:00
// method using deleteMany -- leaves relations in _links table!
2022-08-10 17:35:15 +02:00
// await strapi
// .query('admin::token-permission')
2022-08-11 18:04:59 +02:00
// .deleteMany({ where: { action: map('action', permissionsToDelete), token: id } });
// TODO: improve efficiency here
// using a loop -- works but very inefficient
2022-08-18 11:53:30 +02:00
await Promise . all (
2022-08-18 12:20:45 +02:00
actionsToAdd . map ( ( action ) =>
2022-08-10 17:35:15 +02:00
strapi . query ( 'admin::token-permission' ) . create ( {
data : { action , token : id } ,
} )
2022-08-18 11:53:30 +02:00
)
) ;
2022-08-11 18:04:59 +02:00
// method using createMany -- doesn't create relations in _links table!
// await strapi
// .query('admin::token-permission')
// .createMany({ data: actionsToAdd.map(action => ({ action, token: id })) });
// method attempting to use entityService -- can't create new items in entityservice, permissions need to already exist
// await strapi.entityService.update('admin::api-token', originalToken.id, {
// data: {
// permissions: [
// actionsToAdd.map(action => {
// return { action };
// }),
// ],
// },
// populate: POPULATE_FIELDS,
// });
// method attempting to createMany permissions, then update token with those permissions -- createMany doesn't return the ids, and we can't query for them
2022-08-10 17:35:15 +02:00
}
2022-08-11 10:48:40 +02:00
// if type is not custom, make sure any old permissions get removed
2022-08-18 10:22:09 +02:00
else if ( updatedToken . type !== constants . API _TOKEN _TYPE . CUSTOM ) {
2022-08-11 10:48:40 +02:00
await strapi . query ( 'admin::token-permission' ) . delete ( {
where : { token : id } ,
} ) ;
2022-08-09 17:48:52 +02:00
}
2022-08-05 12:31:16 +02:00
2022-08-11 10:48:40 +02:00
// retrieve permissions
const permissionsFromDb = await strapi . entityService . load (
'admin::api-token' ,
updatedToken ,
'permissions'
) ;
return {
... updatedToken ,
2022-08-18 12:20:45 +02:00
permissions : permissionsFromDb ? permissionsFromDb . map ( ( p ) => p . action ) : undefined ,
2022-08-11 10:48:40 +02:00
} ;
2021-09-06 13:30:52 +02:00
} ;
2021-08-26 14:37:55 +02:00
module . exports = {
create ,
2022-08-18 13:31:02 +02:00
regenerate ,
2021-08-26 14:37:55 +02:00
exists ,
2022-01-24 18:13:27 +01:00
checkSaltIsDefined ,
2021-08-27 16:23:19 +02:00
hash ,
2021-08-27 08:14:36 +02:00
list ,
2021-08-31 15:31:54 +02:00
revoke ,
2021-09-06 15:14:45 +02:00
getById ,
2021-09-06 13:30:52 +02:00
update ,
2021-09-08 14:38:43 +02:00
getByName ,
2021-09-16 14:36:54 +02:00
getBy ,
2021-08-26 14:37:55 +02:00
} ;