2021-08-26 14:37:55 +02:00
'use strict' ;
const crypto = require ( 'crypto' ) ;
2022-08-10 17:35:15 +02:00
const { omit , difference , isEmpty , map } = require ( 'lodash/fp' ) ;
2022-08-10 10:55:49 +02:00
const { ValidationError , NotFoundError } = require ( '@strapi/utils' ) . errors ;
2022-08-08 17:06:38 +02:00
const constants = require ( '../services/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
* @ property { string } [ description ]
* @ property { string } accessKey
2021-08-30 14:00:53 +02:00
* @ property { TokenType } type
2022-08-05 12:01:36 +02:00
* @ property { ( number | ApiTokenPermission ) [ ] } [ permissions ]
* /
/ * *
* @ typedef ApiTokenPermission
*
* @ property { number | string } id
* @ property { string } action
* @ property { ApiToken | number } [ token ]
2021-08-27 09:44:29 +02:00
* /
2021-09-02 10:47:06 +02:00
/** @constant {Array<string>} */
2021-10-05 13:13:45 +02:00
const SELECT _FIELDS = [ 'id' , 'name' , 'description' , 'type' , 'createdAt' ] ;
2021-09-02 10:47:06 +02:00
2022-08-05 12:01:36 +02:00
/** @constant {Array<string>} */
const POPULATE _FIELDS = [ 'permissions' ] ;
const assertCustomTokenPermissionsValidity = attributes => {
// 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-09 10:28:42 +02:00
throw new ValidationError ( 'Non-custom tokens should not references 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-09 10:28:42 +02:00
throw new ValidationError ( 'Missing permissions attributes for custom token' ) ;
2022-08-05 12:01:36 +02:00
}
} ;
2021-08-26 14:37:55 +02:00
/ * *
2021-08-27 16:23:19 +02:00
* @ param { Object } whereParams
2021-09-16 14:36:54 +02:00
* @ param { string | number } [ whereParams . id ]
* @ param { string } [ whereParams . name ]
2021-08-27 16:23:19 +02:00
* @ param { string } [ whereParams . description ]
2021-09-16 14:36:54 +02:00
* @ 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 ;
} ;
/ * *
* @ param { string } accessKey
*
* @ returns { string }
* /
const hash = accessKey => {
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
} ;
/ * *
* @ 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-05 12:01:36 +02:00
* @ param { string [ ] } [ attributes . permissions ]
2021-08-26 14:37:55 +02:00
* @ param { string } [ attributes . description ]
*
2021-08-27 09:44:29 +02:00
* @ returns { Promise < ApiToken > }
2021-08-26 14:37:55 +02:00
* /
const create = async attributes => {
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-10 17:35:15 +02:00
// TODO: createMany doesn't seem to create relation properly, figure this out?
// const permissionsCount = await strapi.query('admin::token-permission').createMany({
// populate: POPULATE_FIELDS,
// data: attributes.permissions.map(action => ({ action, token: apiToken })),
// });
let promises = [ ] ;
attributes . permissions . forEach ( action => {
promises . push (
strapi . query ( 'admin::token-permission' ) . create ( {
data : { action , token : apiToken } ,
} )
) ;
} ) ;
await Promise . all ( promises ) ;
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
} ;
/ * *
* @ 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
/ * *
2021-09-02 10:47:06 +02:00
* @ returns { Promise < Omit < ApiToken , 'accessKey' >> }
2021-08-27 08:14:36 +02:00
* /
const list = async ( ) => {
2021-08-30 14:00:53 +02:00
return 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' } ,
} ) ;
2021-08-27 08:14:36 +02:00
} ;
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
* /
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
/ * *
* @ param { string | number } id
*
* @ returns { Promise < Omit < ApiToken , 'accessKey' >> }
* /
2021-09-06 15:14:45 +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
/ * *
* @ param { string } name
*
* @ returns { Promise < Omit < ApiToken , 'accessKey' >> }
* /
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
/ * *
* @ param { string | number } id
* @ param { Object } attributes
* @ param { TokenType } attributes . type
* @ param { string } attributes . name
* @ param { string } [ attributes . description ]
*
* @ returns { Promise < Omit < ApiToken , 'accessKey' >> }
* /
const update = async ( id , attributes ) => {
2022-08-10 10:55:49 +02:00
// retrieve token without permissions
2022-08-05 12:31:16 +02:00
const oldToken = await strapi . query ( 'admin::api-token' ) . findOne ( { where : { id } } ) ;
if ( ! oldToken ) {
2022-08-10 10:55:49 +02:00
throw new NotFoundError ( 'Token not found' ) ;
2022-08-05 12:31:16 +02:00
}
2022-08-10 10:55:49 +02:00
assertCustomTokenPermissionsValidity ( {
... oldToken ,
... attributes ,
type : attributes . type || oldToken . type ,
} ) ;
2022-08-05 12:01:36 +02:00
2022-08-05 12:31:16 +02:00
const token = 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-10 17:35:15 +02:00
const currentPermissions = await strapi . entityService . load (
'admin::api-token' ,
token ,
'permissions'
) ;
2022-08-09 17:48:52 +02:00
let permissions = { } ;
2022-08-09 10:49:44 +02:00
if ( token . type === constants . API _TOKEN _TYPE . CUSTOM ) {
2022-08-10 17:35:15 +02:00
const permissionsToDelete = difference (
map ( 'action' , currentPermissions ) ,
attributes . permissions
) ;
const permissionsToCreate = difference ( attributes . permissions , oldToken . permissions ) ;
// TODO: make deleteMany work with relations
// await strapi
// .query('admin::token-permission')
// .deleteMany({ where: { action: map('action', permissionsToDelete), token: id } });
let promises = [ ] ;
permissionsToDelete . forEach ( action => {
promises . push (
strapi . query ( 'admin::token-permission' ) . delete ( {
where : { action , token : id } ,
} )
) ;
} ) ;
await Promise . all ( promises ) ;
// TODO: make createMany work with relations
// await strapi
// .query('admin::token-permission')
// .createMany({ data: permissionsToCreate.map(action => ({ action, token: id })) });
promises = [ ] ;
permissionsToCreate . forEach ( action => {
promises . push (
strapi . query ( 'admin::token-permission' ) . create ( {
data : { action , token : id } ,
} )
) ;
} ) ;
await Promise . all ( promises ) ;
2022-08-05 12:31:16 +02:00
2022-08-10 17:35:15 +02:00
// retrieve permissions
2022-08-09 17:48:52 +02:00
permissions = {
permissions : await strapi . entityService . load ( 'admin::api-token' , token , 'permissions' ) ,
} ;
2022-08-10 17:35:15 +02:00
}
// TODO: if type is changing from custom, make sure old permissions get removed
else {
//TODO
2022-08-09 17:48:52 +02:00
}
2022-08-05 12:31:16 +02:00
2022-08-09 17:48:52 +02:00
return { ... token , ... permissions } ;
2021-09-06 13:30:52 +02:00
} ;
2021-09-16 14:36:54 +02:00
/ * *
* @ param { Object } whereParams
* @ param { string | number } [ whereParams . id ]
* @ param { string } [ whereParams . name ]
* @ 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 ;
}
2022-08-05 12:01:36 +02:00
return strapi
. query ( 'admin::api-token' )
. findOne ( { select : SELECT _FIELDS , populate : POPULATE _FIELDS , where : whereParams } ) ;
2021-09-16 14:36:54 +02:00
} ;
2021-08-26 14:37:55 +02:00
module . exports = {
create ,
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
} ;