2023-02-17 11:15:36 +01:00
import {
ApiTokenSchema ,
apiTokenSchema ,
ApiTokensSchema ,
apiTokensSchema ,
createRequestSchema ,
createResponseSchema ,
emptyResponse ,
resourceCreatedResponseSchema ,
} from '../../../openapi' ;
2023-05-19 09:07:23 +02:00
import { getStandardResponses } from '../../../openapi/util/standard-responses' ;
2023-02-17 11:15:36 +01:00
import User from '../../../types/user' ;
import {
ADMIN ,
CREATE_PROJECT_API_TOKEN ,
DELETE_PROJECT_API_TOKEN ,
IUnleashConfig ,
IUnleashServices ,
READ_PROJECT_API_TOKEN ,
serializeDates ,
} from '../../../types' ;
import { ApiTokenType , IApiToken } from '../../../types/models/api-token' ;
import {
AccessService ,
ApiTokenService ,
OpenApiService ,
ProxyService ,
} from '../../../services' ;
import { extractUsername } from '../../../util' ;
import { IAuthRequest } from '../../unleash-types' ;
import Controller from '../../controller' ;
import { Logger } from '../../../logger' ;
import { Response } from 'express' ;
import { timingSafeEqual } from 'crypto' ;
2023-05-09 11:22:21 +02:00
import { createApiToken } from '../../../schema/api-token-schema' ;
2023-02-17 11:15:36 +01:00
interface ProjectTokenParam {
token : string ;
projectId : string ;
}
const PATH = '/:projectId/api-tokens' ;
const PATH_TOKEN = ` ${ PATH } /:token ` ;
export class ProjectApiTokenController extends Controller {
private apiTokenService : ApiTokenService ;
private accessService : AccessService ;
private proxyService : ProxyService ;
private openApiService : OpenApiService ;
private logger : Logger ;
constructor (
config : IUnleashConfig ,
{
apiTokenService ,
accessService ,
proxyService ,
openApiService ,
} : Pick <
IUnleashServices ,
| 'apiTokenService'
| 'accessService'
| 'proxyService'
| 'openApiService'
> ,
) {
super ( config ) ;
this . apiTokenService = apiTokenService ;
this . accessService = accessService ;
this . proxyService = proxyService ;
this . openApiService = openApiService ;
this . logger = config . getLogger ( 'project-api-token-controller.js' ) ;
this . route ( {
method : 'get' ,
path : PATH ,
handler : this.getProjectApiTokens ,
permission : READ_PROJECT_API_TOKEN ,
middleware : [
openApiService . validPath ( {
tags : [ 'Projects' ] ,
operationId : 'getProjectApiTokens' ,
2023-05-19 09:07:23 +02:00
summary : 'Get api tokens for project.' ,
description :
'Returns the [project API tokens](https://docs.getunleash.io/how-to/how-to-create-project-api-tokens) that have been created for this project.' ,
2023-02-17 11:15:36 +01:00
responses : {
200 : createResponseSchema ( 'apiTokensSchema' ) ,
2023-05-19 09:07:23 +02:00
. . . getStandardResponses ( 401 , 403 , 404 ) ,
2023-02-17 11:15:36 +01:00
} ,
} ) ,
] ,
} ) ;
this . route ( {
method : 'post' ,
path : PATH ,
handler : this.createProjectApiToken ,
permission : CREATE_PROJECT_API_TOKEN ,
middleware : [
openApiService . validPath ( {
tags : [ 'Projects' ] ,
operationId : 'createProjectApiToken' ,
requestBody : createRequestSchema ( 'createApiTokenSchema' ) ,
2023-05-19 09:07:23 +02:00
summary : 'Create a project API token.' ,
description :
'Endpoint that allows creation of [project API tokens](https://docs.getunleash.io/reference/api-tokens-and-client-keys#api-token-visibility) for the specified project.' ,
2023-02-17 11:15:36 +01:00
responses : {
201 : resourceCreatedResponseSchema ( 'apiTokenSchema' ) ,
2023-05-19 09:07:23 +02:00
. . . getStandardResponses ( 400 , 401 , 403 ) ,
2023-02-17 11:15:36 +01:00
} ,
} ) ,
] ,
} ) ;
this . route ( {
method : 'delete' ,
path : PATH_TOKEN ,
handler : this.deleteProjectApiToken ,
acceptAnyContentType : true ,
permission : DELETE_PROJECT_API_TOKEN ,
middleware : [
openApiService . validPath ( {
tags : [ 'Projects' ] ,
operationId : 'deleteProjectApiToken' ,
2023-05-19 09:07:23 +02:00
summary : 'Delete a project API token.' ,
description : ` This operation deletes the API token specified in the request URL. If the token doesn't exist, returns an OK response (status code 200). ` ,
2023-02-17 11:15:36 +01:00
responses : {
200 : emptyResponse ,
2023-05-19 09:07:23 +02:00
. . . getStandardResponses ( 401 , 403 ) ,
2023-02-17 11:15:36 +01:00
} ,
} ) ,
] ,
} ) ;
}
async getProjectApiTokens (
req : IAuthRequest ,
res : Response < ApiTokensSchema > ,
) : Promise < void > {
const { user } = req ;
const { projectId } = req . params ;
const projectTokens = await this . accessibleTokens ( user , projectId ) ;
this . openApiService . respondWithValidation (
200 ,
res ,
apiTokensSchema . $id ,
{ tokens : serializeDates ( projectTokens ) } ,
) ;
}
async createProjectApiToken (
req : IAuthRequest ,
res : Response < ApiTokenSchema > ,
) : Promise < any > {
2023-05-09 11:22:21 +02:00
const createToken = await createApiToken . validateAsync ( req . body ) ;
2023-02-17 11:15:36 +01:00
const { projectId } = req . params ;
if ( ! createToken . project ) {
createToken . project = projectId ;
}
if (
createToken . projects . length === 1 &&
createToken . projects [ 0 ] === projectId
) {
const token = await this . apiTokenService . createApiToken (
createToken ,
extractUsername ( req ) ,
) ;
this . openApiService . respondWithValidation (
201 ,
res ,
apiTokenSchema . $id ,
serializeDates ( token ) ,
{ location : ` api-tokens ` } ,
) ;
} else {
res . statusMessage =
'Project level tokens can only be created for one project' ;
res . status ( 400 ) ;
}
}
async deleteProjectApiToken (
req : IAuthRequest < ProjectTokenParam > ,
res : Response ,
) : Promise < void > {
const { user } = req ;
const { projectId , token } = req . params ;
const storedToken = ( await this . accessibleTokens ( user , projectId ) ) . find (
( currentToken ) = > this . tokenEquals ( currentToken . secret , token ) ,
) ;
if (
storedToken &&
( storedToken . project === projectId ||
( storedToken . projects . length === 1 &&
storedToken . project [ 0 ] === projectId ) )
) {
await this . apiTokenService . delete ( token , extractUsername ( req ) ) ;
2023-04-04 09:32:35 +02:00
await this . proxyService . deleteClientForProxyToken ( token ) ;
2023-02-17 11:15:36 +01:00
res . status ( 200 ) . end ( ) ;
}
}
private tokenEquals ( token1 : string , token2 : string ) {
return (
token1 . length === token2 . length &&
timingSafeEqual ( Buffer . from ( token1 ) , Buffer . from ( token2 ) )
) ;
}
private async accessibleTokens (
user : User ,
project : string ,
) : Promise < IApiToken [ ] > {
const allTokens = await this . apiTokenService . getAllTokens ( ) ;
if ( user . isAPI && user . permissions . includes ( ADMIN ) ) {
return allTokens ;
}
return allTokens . filter (
( token ) = >
token . type !== ApiTokenType . ADMIN &&
( token . project === project || token . projects . includes ( project ) ) ,
) ;
}
}