Begin service token data refactor

This commit is contained in:
Tuan Dang
2022-12-30 23:57:21 +03:00
parent addf04d54d
commit 01d969190b
14 changed files with 226 additions and 165 deletions
+2 -2
View File
@@ -34,7 +34,7 @@ import {
stripe as stripeRouter, stripe as stripeRouter,
integration as integrationRouter, integration as integrationRouter,
integrationAuth as integrationAuthRouter, integrationAuth as integrationAuthRouter,
apiKey as apiKeyRouter serviceTokenData as serviceTokenDataRouter
} from './routes'; } from './routes';
import { getLogger } from './utils/logger'; 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/stripe', stripeRouter);
app.use('/api/v1/integration', integrationRouter); app.use('/api/v1/integration', integrationRouter);
app.use('/api/v1/integration-auth', integrationAuthRouter); 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 //* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next)=>{ app.use((req, res, next)=>{
+2
View File
@@ -1,6 +1,7 @@
const PORT = process.env.PORT || 4000; const PORT = process.env.PORT || 4000;
const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400'; const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400';
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; 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_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!; const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!;
const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d'; const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d';
@@ -47,6 +48,7 @@ export {
PORT, PORT,
EMAIL_TOKEN_LIFETIME, EMAIL_TOKEN_LIFETIME,
ENCRYPTION_KEY, ENCRYPTION_KEY,
SALT_ROUNDS,
JWT_AUTH_LIFETIME, JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET, JWT_AUTH_SECRET,
JWT_REFRESH_LIFETIME, JWT_REFRESH_LIFETIME,
@@ -8,6 +8,7 @@ import {
IntegrationAuth, IntegrationAuth,
IUser, IUser,
ServiceToken, ServiceToken,
ServiceTokenData,
} from '../models'; } from '../models';
import { import {
createWorkspace as create, createWorkspace as create,
@@ -334,4 +335,31 @@ export const getWorkspaceServiceTokens = async (
return res.status(200).send({ return res.status(200).send({
serviceTokens 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
});
} }
+2
View File
@@ -6,6 +6,7 @@ import requireOrganizationAuth from './requireOrganizationAuth';
import requireIntegrationAuth from './requireIntegrationAuth'; import requireIntegrationAuth from './requireIntegrationAuth';
import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth'; import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth';
import requireServiceTokenAuth from './requireServiceTokenAuth'; import requireServiceTokenAuth from './requireServiceTokenAuth';
import requireServiceTokenDataAuth from './requireServiceTokenDataAuth';
import validateRequest from './validateRequest'; import validateRequest from './validateRequest';
export { export {
@@ -17,5 +18,6 @@ export {
requireIntegrationAuth, requireIntegrationAuth,
requireIntegrationAuthorizationAuth, requireIntegrationAuthorizationAuth,
requireServiceTokenAuth, requireServiceTokenAuth,
requireServiceTokenDataAuth,
validateRequest validateRequest
}; };
@@ -4,6 +4,7 @@ import { ServiceToken } from '../models';
import { JWT_SERVICE_SECRET } from '../config'; import { JWT_SERVICE_SECRET } from '../config';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
// TODO: deprecate
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string; userId: string;
@@ -1,11 +1,11 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { APIKeyData } from '../models'; import { ServiceToken, ServiceTokenData } from '../models';
import { validateMembership } from '../helpers/membership'; import { validateMembership } from '../helpers/membership';
import { AccountNotFoundError } from '../utils/errors'; import { AccountNotFoundError } from '../utils/errors';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
const requireAPIKeyDataAuth = ({ const requireServiceTokenDataAuth = ({
acceptedRoles, acceptedRoles,
acceptedStatuses, acceptedStatuses,
location = 'params' location = 'params'
@@ -16,25 +16,25 @@ const requireAPIKeyDataAuth = ({
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { 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 (!serviceTokenData) {
return next(AccountNotFoundError({message: 'Failed to locate service token data'}));
if (!apiKeyData) {
return next(AccountNotFoundError({message: 'Failed to locate API Key data'}));
} }
await validateMembership({ await validateMembership({
userId: req.user._id.toString(), userId: req.user._id.toString(),
workspaceId: apiKeyData?.workspace.toString(), workspaceId: serviceTokenData.workspace.toString(),
acceptedRoles, acceptedRoles,
acceptedStatuses acceptedStatuses
}); });
req.apiKeyData = '' // ?? req.serviceTokenData = serviceTokenData;
next(); next();
} }
} }
export default requireAPIKeyDataAuth; export default requireServiceTokenDataAuth;
+3 -3
View File
@@ -14,7 +14,7 @@ import Token, { IToken } from './token';
import User, { IUser } from './user'; import User, { IUser } from './user';
import UserAction, { IUserAction } from './userAction'; import UserAction, { IUserAction } from './userAction';
import Workspace, { IWorkspace } from './workspace'; import Workspace, { IWorkspace } from './workspace';
import APIKeyData, { IAPIKeyData } from './apiKeyData'; import ServiceTokenData, { IServiceTokenData } from './serviceTokenData ';
export { export {
BackupPrivateKey, BackupPrivateKey,
@@ -49,6 +49,6 @@ export {
IUserAction, IUserAction,
Workspace, Workspace,
IWorkspace, IWorkspace,
APIKeyData, ServiceTokenData,
IAPIKeyData, IServiceTokenData
}; };
+1
View File
@@ -1,6 +1,7 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables';
// TODO: deprecate
export interface IServiceToken { export interface IServiceToken {
_id: Types.ObjectId; _id: Types.ObjectId;
name: string; name: string;
@@ -1,36 +1,33 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables';
export interface IAPIKeyData { export interface IServiceTokenData {
name: string; name: string;
workspaces: { workspace: Types.ObjectId;
workspace: Types.ObjectId, environment: string; // TODO: adapt to upcoming environment id
environments: string[]
}[];
expiresAt: Date; expiresAt: Date;
prefix: string; prefix: string;
apiKeyHash: string; serviceTokenHash: string;
encryptedKey: string; encryptedKey: string;
iv: string; iv: string;
tag: string; tag: string;
} }
const apiKeyDataSchema = new Schema<IAPIKeyData>( const serviceTokenDataSchema = new Schema<IServiceTokenData>(
{ {
name: { name: {
type: String, type: String,
required: true required: true
}, },
workspaces: [{ workspace: {
workspace: { type: Schema.Types.ObjectId,
type: Schema.Types.ObjectId, ref: 'Workspace',
ref: 'Workspace' required: true
}, },
environments: [{ environment: { // TODO: adapt to upcoming environment id
type: String, type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD] required: true
}] },
}],
expiresAt: { expiresAt: {
type: Date type: Date
}, },
@@ -38,7 +35,7 @@ const apiKeyDataSchema = new Schema<IAPIKeyData>(
type: String, type: String,
required: true required: true
}, },
apiKeyHash: { serviceTokenHash: {
type: String, type: String,
unique: true, unique: true,
required: true required: true
@@ -61,6 +58,6 @@ const apiKeyDataSchema = new Schema<IAPIKeyData>(
} }
); );
const APIKeyData = model<IAPIKeyData>('APIKeyData', apiKeyDataSchema); const ServiceTokenData = model<IServiceTokenData>('ServiceTokenData', serviceTokenDataSchema);
export default APIKeyData; export default ServiceTokenData;
-127
View File
@@ -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 = <API_KEY>.<KEY>
export default router;
+2 -2
View File
@@ -15,7 +15,7 @@ import password from './password';
import stripe from './stripe'; import stripe from './stripe';
import integration from './integration'; import integration from './integration';
import integrationAuth from './integrationAuth'; import integrationAuth from './integrationAuth';
import apiKey from './apiKey'; import serviceTokenData from './serviceTokenData';
export { export {
signup, signup,
@@ -35,5 +35,5 @@ export {
stripe, stripe,
integration, integration,
integrationAuth, integrationAuth,
apiKey serviceTokenData
}; };
+144
View File
@@ -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 <symmetric_key> on cient-side.
* 2. Construct <infisical_token> on client-side as <infisical_token>=<service_token>.<symmetric_key>
* 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;
+13 -1
View File
@@ -119,7 +119,7 @@ router.get(
); );
router.get( router.get(
'/:workspaceId/service-tokens', '/:workspaceId/service-tokens', // deprecate
requireAuth, requireAuth,
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -130,4 +130,16 @@ router.get(
workspaceController.getWorkspaceServiceTokens 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; export default router;
+1
View File
@@ -15,6 +15,7 @@ declare global {
bot: any; bot: any;
serviceToken: any; serviceToken: any;
accessToken: any; accessToken: any;
serviceTokenData: any;
query?: any; query?: any;
} }
} }