2021-08-26 14:37:55 +02:00
'use strict' ;
const crypto = require ( 'crypto' ) ;
2022-08-05 12:31:16 +02:00
const { map , omit , differenceBy } = require ( 'lodash/fp' ) ;
2022-08-09 10:28:42 +02:00
const { ValidationError } = 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-08 22:50:36 +02:00
if ( attributes . type !== constants . API _TOKEN _TYPE . CUSTOM && 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-08 22:50:36 +02:00
if ( attributes . type === constants . API _TOKEN _TYPE . CUSTOM && ! 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 } ;
// If this is a custom type token, create and link the associated permissions
2022-08-08 17:06:38 +02:00
if ( attributes . type === constants . API _TOKEN _TYPE . CUSTOM ) {
2022-08-09 10:27:34 +02:00
const permissionsCount = await strapi
2022-08-05 12:31:16 +02:00
. query ( 'admin::token-permission' )
. createMany ( { data : attributes . permissions . map ( action => ( { action , token : apiToken . id } ) ) } ) ;
2022-08-05 12:01:36 +02:00
2022-08-09 10:27:34 +02:00
// TODO: should we select the permissions again to ensure it worked?
if ( permissionsCount ) {
Object . assign ( result , { permissions : attributes . permissions } ) ;
}
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-05 12:31:16 +02:00
const oldToken = await strapi . query ( 'admin::api-token' ) . findOne ( { where : { id } } ) ;
if ( ! oldToken ) {
throw new Error ( 'Token not found' ) ;
}
assertCustomTokenPermissionsValidity ( { ... 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
if ( token . type === 'custom' ) {
const permissionsToDelete = differenceBy ( 'action' , token . permissions , attributes . permissions ) ;
const permissionsToCreate = differenceBy ( 'action' , attributes . permissions , token . permissions ) ;
await strapi
. query ( 'admin::token-permission' )
. deleteMany ( { where : { action : map ( 'action' , permissionsToDelete ) } } ) ;
await strapi
. query ( 'admin::token-permission' )
. createMany ( { data : permissionsToCreate . map ( ( { action } ) => ( { action , token : id } ) ) } ) ;
}
const permissions = await strapi . entityService . load ( 'admin::api-token' , token , 'permissions' ) ;
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
} ;