diff --git a/backend/src/app.ts b/backend/src/app.ts index 500aafe77..7a263acee 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -34,7 +34,7 @@ import { stripe as stripeRouter, integration as integrationRouter, integrationAuth as integrationAuthRouter, - apiKey as apiKeyRouter + serviceTokenData as serviceTokenDataRouter } from './routes'; import { getLogger } from './utils/logger'; @@ -86,7 +86,7 @@ app.use('/api/v1/password', passwordRouter); app.use('/api/v1/stripe', stripeRouter); app.use('/api/v1/integration', integrationRouter); app.use('/api/v1/integration-auth', integrationAuthRouter); -app.use('/api/v1/api-key', apiKeyRouter); +app.use('/api/v1/service-token-data', serviceTokenDataRouter); //* Handle unrouted requests and respond with proper error message as well as status code app.use((req, res, next)=>{ diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 3fb475099..652b3c7b2 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,6 +1,7 @@ const PORT = process.env.PORT || 4000; const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400'; const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; +const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10; const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!; const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d'; @@ -47,6 +48,7 @@ export { PORT, EMAIL_TOKEN_LIFETIME, ENCRYPTION_KEY, + SALT_ROUNDS, JWT_AUTH_LIFETIME, JWT_AUTH_SECRET, JWT_REFRESH_LIFETIME, diff --git a/backend/src/controllers/workspaceController.ts b/backend/src/controllers/workspaceController.ts index 6f3e4bd11..a843ed9d6 100644 --- a/backend/src/controllers/workspaceController.ts +++ b/backend/src/controllers/workspaceController.ts @@ -8,6 +8,7 @@ import { IntegrationAuth, IUser, ServiceToken, + ServiceTokenData, } from '../models'; import { createWorkspace as create, @@ -334,4 +335,31 @@ export const getWorkspaceServiceTokens = async ( return res.status(200).send({ serviceTokens }); +} + +export const getWorkspaceServiceTokenData = async ( + req: Request, + res: Response +) => { + let serviceTokenData; + try { + const { workspaceId } = req.query; + + serviceTokenData = await ServiceTokenData + .find({ + workspace: workspaceId + }) + .select('+encryptedKey +iv +tag'); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace service token data' + }); + } + + return res.status(200).send({ + serviceTokenData + }); } \ No newline at end of file diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index 7fcba66e1..57ddbaeb7 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -6,6 +6,7 @@ import requireOrganizationAuth from './requireOrganizationAuth'; import requireIntegrationAuth from './requireIntegrationAuth'; import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth'; import requireServiceTokenAuth from './requireServiceTokenAuth'; +import requireServiceTokenDataAuth from './requireServiceTokenDataAuth'; import validateRequest from './validateRequest'; export { @@ -17,5 +18,6 @@ export { requireIntegrationAuth, requireIntegrationAuthorizationAuth, requireServiceTokenAuth, + requireServiceTokenDataAuth, validateRequest }; diff --git a/backend/src/middleware/requireServiceTokenAuth.ts b/backend/src/middleware/requireServiceTokenAuth.ts index 94e8363ff..904f4d38e 100644 --- a/backend/src/middleware/requireServiceTokenAuth.ts +++ b/backend/src/middleware/requireServiceTokenAuth.ts @@ -4,6 +4,7 @@ import { ServiceToken } from '../models'; import { JWT_SERVICE_SECRET } from '../config'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +// TODO: deprecate declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { userId: string; diff --git a/backend/src/middleware/requireAPIKeyDataAuth.ts b/backend/src/middleware/requireServiceTokenDataAuth.ts similarity index 60% rename from backend/src/middleware/requireAPIKeyDataAuth.ts rename to backend/src/middleware/requireServiceTokenDataAuth.ts index 8dafb5a9c..1f2e33d7a 100644 --- a/backend/src/middleware/requireAPIKeyDataAuth.ts +++ b/backend/src/middleware/requireServiceTokenDataAuth.ts @@ -1,11 +1,11 @@ import { Request, Response, NextFunction } from 'express'; -import { APIKeyData } from '../models'; +import { ServiceToken, ServiceTokenData } from '../models'; import { validateMembership } from '../helpers/membership'; import { AccountNotFoundError } from '../utils/errors'; type req = 'params' | 'body' | 'query'; -const requireAPIKeyDataAuth = ({ +const requireServiceTokenDataAuth = ({ acceptedRoles, acceptedStatuses, location = 'params' @@ -16,25 +16,25 @@ const requireAPIKeyDataAuth = ({ }) => { return async (req: Request, res: Response, next: NextFunction) => { - // req.user + const serviceTokenData = await ServiceTokenData + .findById(req[location].serviceTokenDataId) + .select('+encryptedKey +iv +tag'); - const apiKeyData = await APIKeyData.findById(req[location].apiKeyDataId); - - if (!apiKeyData) { - return next(AccountNotFoundError({message: 'Failed to locate API Key data'})); + if (!serviceTokenData) { + return next(AccountNotFoundError({message: 'Failed to locate service token data'})); } await validateMembership({ userId: req.user._id.toString(), - workspaceId: apiKeyData?.workspace.toString(), + workspaceId: serviceTokenData.workspace.toString(), acceptedRoles, acceptedStatuses }); - req.apiKeyData = '' // ?? + req.serviceTokenData = serviceTokenData; next(); } } -export default requireAPIKeyDataAuth; \ No newline at end of file +export default requireServiceTokenDataAuth; \ No newline at end of file diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index fd9578523..8e934d511 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -14,7 +14,7 @@ import Token, { IToken } from './token'; import User, { IUser } from './user'; import UserAction, { IUserAction } from './userAction'; import Workspace, { IWorkspace } from './workspace'; -import APIKeyData, { IAPIKeyData } from './apiKeyData'; +import ServiceTokenData, { IServiceTokenData } from './serviceTokenData '; export { BackupPrivateKey, @@ -49,6 +49,6 @@ export { IUserAction, Workspace, IWorkspace, - APIKeyData, - IAPIKeyData, + ServiceTokenData, + IServiceTokenData }; diff --git a/backend/src/models/serviceToken.ts b/backend/src/models/serviceToken.ts index 73f705fc8..b5a2f4ec9 100644 --- a/backend/src/models/serviceToken.ts +++ b/backend/src/models/serviceToken.ts @@ -1,6 +1,7 @@ import { Schema, model, Types } from 'mongoose'; import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; +// TODO: deprecate export interface IServiceToken { _id: Types.ObjectId; name: string; diff --git a/backend/src/models/apiKeyData.ts b/backend/src/models/serviceTokenData .ts similarity index 57% rename from backend/src/models/apiKeyData.ts rename to backend/src/models/serviceTokenData .ts index 8b064bf08..01bac8d58 100644 --- a/backend/src/models/apiKeyData.ts +++ b/backend/src/models/serviceTokenData .ts @@ -1,36 +1,33 @@ import { Schema, model, Types } from 'mongoose'; import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; -export interface IAPIKeyData { +export interface IServiceTokenData { name: string; - workspaces: { - workspace: Types.ObjectId, - environments: string[] - }[]; + workspace: Types.ObjectId; + environment: string; // TODO: adapt to upcoming environment id expiresAt: Date; prefix: string; - apiKeyHash: string; + serviceTokenHash: string; encryptedKey: string; iv: string; tag: string; } -const apiKeyDataSchema = new Schema( +const serviceTokenDataSchema = new Schema( { name: { type: String, required: true }, - workspaces: [{ - workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace' - }, - environments: [{ - type: String, - enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD] - }] - }], + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + environment: { // TODO: adapt to upcoming environment id + type: String, + required: true + }, expiresAt: { type: Date }, @@ -38,7 +35,7 @@ const apiKeyDataSchema = new Schema( type: String, required: true }, - apiKeyHash: { + serviceTokenHash: { type: String, unique: true, required: true @@ -61,6 +58,6 @@ const apiKeyDataSchema = new Schema( } ); -const APIKeyData = model('APIKeyData', apiKeyDataSchema); +const ServiceTokenData = model('ServiceTokenData', serviceTokenDataSchema); -export default APIKeyData; +export default ServiceTokenData; diff --git a/backend/src/routes/apiKey.ts b/backend/src/routes/apiKey.ts deleted file mode 100644 index a40c852ac..000000000 --- a/backend/src/routes/apiKey.ts +++ /dev/null @@ -1,127 +0,0 @@ -import express from 'express'; -const router = express.Router(); -import { - requireAuth -} from '../middleware'; -import { - APIKeyData -} from '../models'; -import { param, body, query } from 'express-validator'; -import crypto from 'crypto'; -import bcrypt from 'bcrypt'; -import * as Sentry from '@sentry/node'; - -// TODO: middleware -router.post( - '/', - requireAuth, - body('name').exists().trim(), - body('workspace'), - body('environment'), - body('encryptedKey'), - body('iv'), - body('tag'), - body('expiresAt'), - async (req, res) => { - let apiKey, apiKeyData; - try { - const { - name, - workspace, - environment, - encryptedKey, - iv, - tag, - expiresAt - } = req.body; - - // create 38-char API key with first 6-char being the prefix - apiKey = crypto.randomBytes(19).toString('hex'); - - const saltRounds = 10; // TODO: add as config envar - const apiKeyHash = await bcrypt.hash(apiKey, saltRounds); - - apiKeyData = await new APIKeyData({ - name, - workspace, - environment, - expiresAt, - prefix: apiKey.substring(0, 6), - apiKeyHash, - encryptedKey, - iv, - tag - }).save(); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to create workspace API Key' - }); - } - - return res.status(200).send({ - apiKey, - apiKeyData - }); - } -); - -// TODO: middleware -router.get( - '/', - requireAuth, - query('workspaceId').exists().trim(), - async (req, res) => { - let apiKeyData; - try { - const { workspaceId } = req.query; - - apiKeyData = await APIKeyData.find({ - workspace: workspaceId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace API Key data' - }); - } - - return res.status(200).send({ - apiKeyData - }); - } -); - -// TODO: middleware -router.delete( - ':apiKeyDataId', - requireAuth, - // TODO: requireAPIKeyDataAuth, - param('apiKeyDataId').exists().trim(), - async (req, res) => { - let apiKeyData; - try { - const { apiKeyDataId } = req.params; - - apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete API key data' - }); - } - - return res.status(200).send({ - apiKeyData - }); - } -); - -// INFISICAL TOKEN = . - -export default router; \ No newline at end of file diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index bc0ff776d..89b02ebc9 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -15,7 +15,7 @@ import password from './password'; import stripe from './stripe'; import integration from './integration'; import integrationAuth from './integrationAuth'; -import apiKey from './apiKey'; +import serviceTokenData from './serviceTokenData'; export { signup, @@ -35,5 +35,5 @@ export { stripe, integration, integrationAuth, - apiKey + serviceTokenData }; diff --git a/backend/src/routes/serviceTokenData.ts b/backend/src/routes/serviceTokenData.ts new file mode 100644 index 000000000..903e72771 --- /dev/null +++ b/backend/src/routes/serviceTokenData.ts @@ -0,0 +1,144 @@ +import express from 'express'; +const router = express.Router(); +import crypto from 'crypto'; +import bcrypt from 'bcrypt'; +import * as Sentry from '@sentry/node'; +import { + requireAuth, + requireWorkspaceAuth, + requireServiceTokenDataAuth, + validateRequest +} from '../middleware'; +import { + ServiceTokenData +} from '../models'; +import { param, body, query } from 'express-validator'; +import { + SALT_ROUNDS +} from '../config'; +import { + ADMIN, + MEMBER, + COMPLETED, + GRANTED +} from '../variables'; + +// TODO: move logic into separate controller (probably after pull with latest routing) + +/** + * 2 different concepts that we should distinguish between: + * - API key (user) - allows user to perform queries and mutations on whatever + * their account could access (better than JWT because it has ACL and scoping). + * - Service token (bound to a workspace and environment). + */ + +/** + * Service token flow? + * 1. Post service token data details including project key encrypted under on cient-side. + * 2. Construct on client-side as =. + * 3. Need for CLI to be able to get back service token details + */ + +router.post( + '/', + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED], + location: 'body' + }), + requireAuth, + body('name').exists().trim(), + body('workspace'), + body('environment'), + body('encryptedKey'), + body('iv'), + body('tag'), + body('expiresAt'), + validateRequest, + async (req, res) => { + let serviceToken, serviceTokenData; + try { + const { + name, + workspace, + environment, + encryptedKey, + iv, + tag, + expiresAt + } = req.body; + + // create 38-char service token with first 6-char being the prefix + serviceToken = crypto.randomBytes(19).toString('hex'); + + const serviceTokenHash = await bcrypt.hash(serviceToken, SALT_ROUNDS); + + serviceTokenData = await new ServiceTokenData({ + name, + workspace, + environment, + expiresAt, + prefix: serviceToken.substring(0, 6), + serviceTokenHash, + encryptedKey, + iv, + tag + }).save(); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to create service token data' + }); + } + + return res.status(200).send({ + serviceToken, + serviceTokenData + }); + } +); + +// TODO: CLI has to get service token details without needing a JWT +router.get( + '/:serviceTokenDataId', + requireAuth, + requireServiceTokenDataAuth, + param('serviceTokenDataId').exists().trim(), + validateRequest, + async (req, res) => { + return ({ + serviceTokenData: req.serviceTokenData + }); + } +); + +router.delete( + '/:serviceTokenDataId', + requireAuth, + requireServiceTokenDataAuth, + param('serviceTokenDataId').exists().trim(), + validateRequest, + async (req, res) => { + let serviceTokenData; + try { + const { serviceTokenDataId } = req.params; + + serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to delete service token data' + }); + } + + return res.status(200).send({ + serviceTokenData + }); + } +); + +export default router; \ No newline at end of file diff --git a/backend/src/routes/workspace.ts b/backend/src/routes/workspace.ts index acd2aaf8b..3551841c7 100644 --- a/backend/src/routes/workspace.ts +++ b/backend/src/routes/workspace.ts @@ -119,7 +119,7 @@ router.get( ); router.get( - '/:workspaceId/service-tokens', + '/:workspaceId/service-tokens', // deprecate requireAuth, requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -130,4 +130,16 @@ router.get( workspaceController.getWorkspaceServiceTokens ); +router.get( + '/:workspaceId/service-token-data', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspaceServiceTokenData +); + export default router; diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index 319562fd0..a019be1e9 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -15,6 +15,7 @@ declare global { bot: any; serviceToken: any; accessToken: any; + serviceTokenData: any; query?: any; } }