mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 05:26:43 +00:00
Finish v1 audit logs, secret versioning, version all unversioned secrets
This commit is contained in:
+3
-1
@@ -13,7 +13,8 @@ import { apiLimiter } from './helpers/rateLimiter';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
workspace as eeWorkspaceRouter,
|
workspace as eeWorkspaceRouter,
|
||||||
secret as eeSecretRouter
|
secret as eeSecretRouter,
|
||||||
|
action as eeActionRouter
|
||||||
} from './ee/routes/v1';
|
} from './ee/routes/v1';
|
||||||
import {
|
import {
|
||||||
signup as v1SignupRouter,
|
signup as v1SignupRouter,
|
||||||
@@ -69,6 +70,7 @@ if (NODE_ENV === 'production') {
|
|||||||
// (EE) routes
|
// (EE) routes
|
||||||
app.use('/api/v1/secret', eeSecretRouter);
|
app.use('/api/v1/secret', eeSecretRouter);
|
||||||
app.use('/api/v1/workspace', eeWorkspaceRouter);
|
app.use('/api/v1/workspace', eeWorkspaceRouter);
|
||||||
|
app.use('/api/v1/action', eeActionRouter);
|
||||||
|
|
||||||
// v1 routes
|
// v1 routes
|
||||||
app.use('/api/v1/signup', v1SignupRouter);
|
app.use('/api/v1/signup', v1SignupRouter);
|
||||||
|
|||||||
@@ -369,7 +369,6 @@ export const getWorkspaceServiceTokens = async (
|
|||||||
*/
|
*/
|
||||||
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
||||||
const { keys, environment, channel } = req.body;
|
const { keys, environment, channel } = req.body;
|
||||||
@@ -400,7 +399,6 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
|||||||
keys
|
keys
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: 'secrets pushed',
|
event: 'secrets pushed',
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Action } from '../../models';
|
import { Action, SecretVersion } from '../../models';
|
||||||
|
import { ActionNotFoundError } from '../../../utils/errors';
|
||||||
|
|
||||||
export const getAction = (req: Request, res: Response) => {
|
export const getAction = async (req: Request, res: Response) => {
|
||||||
let action;
|
let action;
|
||||||
// try {
|
try {
|
||||||
// const { actionId } = req.params;
|
const { actionId } = req.params;
|
||||||
|
|
||||||
// action = await Action.findById(actionId);
|
action = await Action
|
||||||
|
.findById(actionId)
|
||||||
|
.populate([
|
||||||
|
'payload.secretVersions.oldSecretVersion',
|
||||||
|
'payload.secretVersions.newSecretVersion'
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!action) throw ActionNotFoundError({
|
||||||
// } catch (err) {
|
message: 'Failed to find action'
|
||||||
|
});
|
||||||
|
|
||||||
// }
|
} catch (err) {
|
||||||
|
throw ActionNotFoundError({
|
||||||
|
message: 'Failed to find action'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
action
|
action
|
||||||
|
|||||||
@@ -41,14 +41,23 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
|
|||||||
let logs
|
let logs
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
const { userId, actionNames } = req.query;
|
||||||
|
|
||||||
const offset: number = parseInt(req.query.offset as string);
|
const offset: number = parseInt(req.query.offset as string);
|
||||||
const limit: number = parseInt(req.query.limit as string);
|
const limit: number = parseInt(req.query.limit as string);
|
||||||
const filters: any = req.query.filters || {};
|
|
||||||
|
|
||||||
filters.workspace = workspaceId;
|
logs = await Log.find({
|
||||||
|
workspace: workspaceId,
|
||||||
logs = await Log.find(filters)
|
...( userId ? { user: userId } : {}),
|
||||||
|
...(
|
||||||
|
actionNames
|
||||||
|
? {
|
||||||
|
actionNames: {
|
||||||
|
$in: actionNames
|
||||||
|
}
|
||||||
|
} : {}
|
||||||
|
)
|
||||||
|
})
|
||||||
.skip(offset)
|
.skip(offset)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.populate('actions')
|
.populate('actions')
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { Secret } from '../../models';
|
||||||
|
import { SecretVersion, Action } from '../models';
|
||||||
|
import { ACTION_UPDATE_SECRETS } from '../../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an (audit) action for secrets including
|
||||||
|
* add, delete, update, and read actions.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.name - name of action
|
||||||
|
* @param {ObjectId[]} obj.secretIds - ids of relevant secrets
|
||||||
|
* @returns {Action} action - new action
|
||||||
|
*/
|
||||||
|
const createActionSecretHelper = async ({
|
||||||
|
name,
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
secretIds
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
secretIds: Types.ObjectId[];
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
let action;
|
||||||
|
let latestSecretVersions;
|
||||||
|
try {
|
||||||
|
if (name === ACTION_UPDATE_SECRETS) {
|
||||||
|
// case: action is updating secrets
|
||||||
|
// -> add old and new secret versions
|
||||||
|
|
||||||
|
// TODO: make query more efficient
|
||||||
|
latestSecretVersions = (await SecretVersion.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
secret: {
|
||||||
|
$in: secretIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$sort: { version: -1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$secret",
|
||||||
|
versions: { $push: "$$ROOT" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 0,
|
||||||
|
secret: "$_id",
|
||||||
|
versions: { $slice: ["$versions", 2] },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]))
|
||||||
|
.map((s) => ({
|
||||||
|
oldSecretVersion: s.versions[0]._id,
|
||||||
|
newSecretVersion: s.versions[1]._id
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// case: action is adding, deleting, or reading secrets
|
||||||
|
// -> add new secret versions
|
||||||
|
latestSecretVersions = (await SecretVersion.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
secret: {
|
||||||
|
$in: secretIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: '$secret',
|
||||||
|
version: { $max: '$version' },
|
||||||
|
versionId: { $max: '$_id' } // secret version id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$sort: { version: -1 }
|
||||||
|
}
|
||||||
|
])
|
||||||
|
.exec())
|
||||||
|
.map((s) => ({
|
||||||
|
newSecretVersion: s.versionId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
action = await new Action({
|
||||||
|
name,
|
||||||
|
user: userId,
|
||||||
|
workspace: workspaceId,
|
||||||
|
payload: {
|
||||||
|
secretVersions: latestSecretVersions
|
||||||
|
}
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to create action');
|
||||||
|
}
|
||||||
|
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createActionSecretHelper };
|
||||||
@@ -22,6 +22,7 @@ const createLogHelper = async ({
|
|||||||
log = await new Log({
|
log = await new Log({
|
||||||
user: userId,
|
user: userId,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
|
actionNames: actions.map((a) => a.name),
|
||||||
actions,
|
actions,
|
||||||
channel,
|
channel,
|
||||||
ipAddress
|
ipAddress
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import {
|
import {
|
||||||
Secret
|
Secret
|
||||||
@@ -59,16 +60,40 @@ const addSecretVersionsHelper = async ({
|
|||||||
}: {
|
}: {
|
||||||
secretVersions: ISecretVersion[]
|
secretVersions: ISecretVersion[]
|
||||||
}) => {
|
}) => {
|
||||||
|
let newSecretVersions;
|
||||||
try {
|
try {
|
||||||
await SecretVersion.insertMany(secretVersions);
|
newSecretVersions = await SecretVersion.insertMany(secretVersions);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to add secret versions');
|
throw new Error('Failed to add secret versions');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return newSecretVersions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const markDeletedSecretVersionsHelper = async ({
|
||||||
|
secretIds
|
||||||
|
}: {
|
||||||
|
secretIds: Types.ObjectId[];
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
await SecretVersion.updateMany({
|
||||||
|
secret: { $in: secretIds }
|
||||||
|
}, {
|
||||||
|
isDeleted: true
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to mark secret versions as deleted');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
takeSecretSnapshotHelper,
|
takeSecretSnapshotHelper,
|
||||||
addSecretVersionsHelper
|
addSecretVersionsHelper,
|
||||||
|
markDeletedSecretVersionsHelper
|
||||||
}
|
}
|
||||||
@@ -26,8 +26,14 @@ const actionSchema = new Schema<IAction>(
|
|||||||
},
|
},
|
||||||
payload: {
|
payload: {
|
||||||
secretVersions: [{
|
secretVersions: [{
|
||||||
type: Schema.Types.ObjectId,
|
oldSecretVersion: {
|
||||||
ref: 'SecretVersion'
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'SecretVersion'
|
||||||
|
},
|
||||||
|
newSecretVersion: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'SecretVersion'
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
ACTION_ADD_SECRETS,
|
||||||
|
ACTION_UPDATE_SECRETS,
|
||||||
|
ACTION_READ_SECRETS,
|
||||||
|
ACTION_DELETE_SECRETS
|
||||||
|
} from '../../variables';
|
||||||
|
|
||||||
export interface ILog {
|
export interface ILog {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
user?: Types.ObjectId;
|
user?: Types.ObjectId;
|
||||||
workspace?: Types.ObjectId;
|
workspace?: Types.ObjectId;
|
||||||
|
actionNames: string[];
|
||||||
actions: Types.ObjectId[];
|
actions: Types.ObjectId[];
|
||||||
channel: string;
|
channel: string;
|
||||||
ipAddress?: string;
|
ipAddress?: string;
|
||||||
@@ -19,9 +26,20 @@ const logSchema = new Schema<ILog>(
|
|||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Workspace'
|
ref: 'Workspace'
|
||||||
},
|
},
|
||||||
|
actionNames: {
|
||||||
|
type: [String],
|
||||||
|
enum: [
|
||||||
|
ACTION_ADD_SECRETS,
|
||||||
|
ACTION_UPDATE_SECRETS,
|
||||||
|
ACTION_READ_SECRETS,
|
||||||
|
ACTION_DELETE_SECRETS
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
actions: [{
|
actions: [{
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Action'
|
ref: 'Action',
|
||||||
|
required: true
|
||||||
}],
|
}],
|
||||||
channel: {
|
channel: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ router.get(
|
|||||||
param('workspaceId').exists().trim(),
|
param('workspaceId').exists().trim(),
|
||||||
query('offset').exists().isInt(),
|
query('offset').exists().isInt(),
|
||||||
query('limit').exists().isInt(),
|
query('limit').exists().isInt(),
|
||||||
query('filters').exists(),
|
|
||||||
validateRequest,
|
validateRequest,
|
||||||
workspaceController.getWorkspaceLogs
|
workspaceController.getWorkspaceLogs
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
|
Log,
|
||||||
Action,
|
Action,
|
||||||
IAction
|
IAction
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
createLogHelper
|
createLogHelper
|
||||||
} from '../helpers/log';
|
} from '../helpers/log';
|
||||||
|
import {
|
||||||
|
createActionSecretHelper
|
||||||
|
} from '../helpers/action';
|
||||||
import EELicenseService from './EELicenseService';
|
import EELicenseService from './EELicenseService';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,6 +24,7 @@ class EELogService {
|
|||||||
* @param {Action} obj.actions - actions to include in log
|
* @param {Action} obj.actions - actions to include in log
|
||||||
* @param {String} obj.channel - channel (web/cli/auto) associated with the log
|
* @param {String} obj.channel - channel (web/cli/auto) associated with the log
|
||||||
* @param {String} obj.ipAddress - ip address associated with the log
|
* @param {String} obj.ipAddress - ip address associated with the log
|
||||||
|
* @returns {Log} log - new audit log
|
||||||
*/
|
*/
|
||||||
static async createLog({
|
static async createLog({
|
||||||
userId,
|
userId,
|
||||||
@@ -33,7 +39,7 @@ class EELogService {
|
|||||||
channel: string;
|
channel: string;
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
}) {
|
}) {
|
||||||
if (!EELicenseService.isLicenseValid) return;
|
if (!EELicenseService.isLicenseValid) return null;
|
||||||
return await createLogHelper({
|
return await createLogHelper({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -42,6 +48,34 @@ class EELogService {
|
|||||||
ipAddress
|
ipAddress
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an (audit) action for secrets including
|
||||||
|
* add, delete, update, and read actions.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.name - name of action
|
||||||
|
* @param {ObjectId[]} obj.secretIds - secret ids
|
||||||
|
* @returns {Action} action - new action
|
||||||
|
*/
|
||||||
|
static async createActionSecret({
|
||||||
|
name,
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
secretIds
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
secretIds: Types.ObjectId[];
|
||||||
|
}) {
|
||||||
|
if (!EELicenseService.isLicenseValid) return null;
|
||||||
|
return await createActionSecretHelper({
|
||||||
|
name,
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
secretIds
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EELogService;
|
export default EELogService;
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
import { ISecretVersion } from '../models';
|
import { ISecretVersion } from '../models';
|
||||||
import {
|
import {
|
||||||
takeSecretSnapshotHelper,
|
takeSecretSnapshotHelper,
|
||||||
addSecretVersionsHelper
|
addSecretVersionsHelper,
|
||||||
|
markDeletedSecretVersionsHelper
|
||||||
} from '../helpers/secret';
|
} from '../helpers/secret';
|
||||||
import EELicenseService from './EELicenseService';
|
import EELicenseService from './EELicenseService';
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ class EESecretService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds secret versions [secretVersions] to the SecretVersion collection.
|
* Add secret versions [secretVersions] to the SecretVersion collection.
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {SecretVersion} obj.secretVersions
|
* @param {SecretVersion} obj.secretVersions
|
||||||
*/
|
*/
|
||||||
@@ -38,10 +40,27 @@ class EESecretService {
|
|||||||
secretVersions: ISecretVersion[];
|
secretVersions: ISecretVersion[];
|
||||||
}) {
|
}) {
|
||||||
if (!EELicenseService.isLicenseValid) return;
|
if (!EELicenseService.isLicenseValid) return;
|
||||||
await addSecretVersionsHelper({
|
return await addSecretVersionsHelper({
|
||||||
secretVersions
|
secretVersions
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark secret versions associated with secrets with ids [secretIds]
|
||||||
|
* as deleted.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {ObjectId[]} obj.secretIds - secret ids
|
||||||
|
*/
|
||||||
|
static async markDeletedSecretVersions({
|
||||||
|
secretIds
|
||||||
|
}: {
|
||||||
|
secretIds: Types.ObjectId[];
|
||||||
|
}) {
|
||||||
|
if (!EELicenseService.isLicenseValid) return;
|
||||||
|
await markDeletedSecretVersionsHelper({
|
||||||
|
secretIds
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EESecretService;
|
export default EESecretService;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { ISecret, Secret } from '../models';
|
||||||
|
import { EESecretService } from '../ee/services';
|
||||||
|
import { getLogger } from '../utils/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize database connection
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.mongoURL - mongo connection string
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const initDatabaseHelper = async ({
|
||||||
|
mongoURL
|
||||||
|
}: {
|
||||||
|
mongoURL: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
await mongoose.connect(mongoURL);
|
||||||
|
getLogger("database").info("Database connection established");
|
||||||
|
|
||||||
|
await prepareDatabase();
|
||||||
|
} catch (err) {
|
||||||
|
getLogger("database").error(`Unable to establish Database connection due to the error.\n${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mongoose.connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare database by:
|
||||||
|
* - Setting unversioned secrets to version 1
|
||||||
|
* - Initializing secret versions for unversioned secrets
|
||||||
|
*/
|
||||||
|
const prepareDatabase = async () => {
|
||||||
|
try {
|
||||||
|
// set previously unversioned secrets in Secret to version 1
|
||||||
|
await Secret.updateMany(
|
||||||
|
{ version: { $exists: false } },
|
||||||
|
{ $set: { version: 1 } }
|
||||||
|
);
|
||||||
|
|
||||||
|
// initialize secret versions for unversioned secrets
|
||||||
|
const unversionedSecrets: ISecret[] = await Secret.aggregate([
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: 'secretversions',
|
||||||
|
localField: '_id',
|
||||||
|
foreignField: 'secret',
|
||||||
|
as: 'versions',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
versions: { $size: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (unversionedSecrets.length > 0) {
|
||||||
|
await EESecretService.addSecretVersions({
|
||||||
|
secretVersions: unversionedSecrets.map((s, idx) => ({
|
||||||
|
...s,
|
||||||
|
secret: s._id,
|
||||||
|
version: s.version ? s.version : 1,
|
||||||
|
isDeleted: false,
|
||||||
|
workspace: s.workspace,
|
||||||
|
environment: s.environment
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
getLogger('database').error('Failed to prepare database');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
initDatabaseHelper
|
||||||
|
}
|
||||||
+42
-157
@@ -285,6 +285,9 @@ const v1PushSecrets = async ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: optimize this route.
|
||||||
|
// TODO: ensure that it's possible to query for and filter logs
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Push secrets for user with id [userId] to workspace
|
* Push secrets for user with id [userId] to workspace
|
||||||
* with id [workspaceId] with environment [environment]. Follow steps:
|
* with id [workspaceId] with environment [environment]. Follow steps:
|
||||||
@@ -341,42 +344,19 @@ const v1PushSecrets = async ({
|
|||||||
await Secret.deleteMany({
|
await Secret.deleteMany({
|
||||||
_id: { $in: toDelete }
|
_id: { $in: toDelete }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await EESecretService.markDeletedSecretVersions({
|
||||||
|
secretIds: toDelete
|
||||||
|
});
|
||||||
|
|
||||||
await SecretVersion.updateMany({
|
const deleteAction = await EELogService.createActionSecret({
|
||||||
secret: { $in: toDelete }
|
name: ACTION_DELETE_SECRETS,
|
||||||
}, {
|
userId,
|
||||||
isDeleted: true
|
workspaceId,
|
||||||
}, {
|
secretIds: toDelete
|
||||||
new: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// add audit log for deleted secrets
|
deleteAction && actions.push(deleteAction);
|
||||||
const deletedLatestSecretVersions = (await SecretVersion.aggregate([
|
|
||||||
{
|
|
||||||
$match: { secret: { $in: toDelete } }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: '$secret',
|
|
||||||
version: { $max: '$version' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$sort: { version: -1 }
|
|
||||||
}
|
|
||||||
])
|
|
||||||
.exec())
|
|
||||||
.map((s) => s._id);
|
|
||||||
|
|
||||||
const deleteAction = await new Action({
|
|
||||||
name: ACTION_DELETE_SECRETS,
|
|
||||||
user: new Types.ObjectId(userId),
|
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
|
||||||
payload: {
|
|
||||||
secretVersions: deletedLatestSecretVersions
|
|
||||||
}
|
|
||||||
}).save();
|
|
||||||
actions.push(deleteAction);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toUpdate = oldSecrets
|
const toUpdate = oldSecrets
|
||||||
@@ -459,10 +439,6 @@ const v1PushSecrets = async ({
|
|||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
secretValueHash,
|
secretValueHash,
|
||||||
secretCommentCiphertext,
|
|
||||||
secretCommentIV,
|
|
||||||
secretCommentTag,
|
|
||||||
secretCommentHash,
|
|
||||||
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
||||||
|
|
||||||
return ({
|
return ({
|
||||||
@@ -481,34 +457,14 @@ const v1PushSecrets = async ({
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
// add audit log for updated secrets
|
const updateAction = await EELogService.createActionSecret({
|
||||||
const updatedLatestSecretVersions = (await SecretVersion.aggregate([
|
|
||||||
{
|
|
||||||
$match: { secret: { $in: toUpdate.map((u) => u._id) } }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: '$secret',
|
|
||||||
version: { $max: '$version' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$sort: { version: -1 }
|
|
||||||
}
|
|
||||||
])
|
|
||||||
.exec())
|
|
||||||
.map((s) => s._id);
|
|
||||||
|
|
||||||
const updateAction = await new Action({
|
|
||||||
name: ACTION_UPDATE_SECRETS,
|
name: ACTION_UPDATE_SECRETS,
|
||||||
user: new Types.ObjectId(userId),
|
userId,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspaceId,
|
||||||
payload: {
|
secretIds: toUpdate.map((u) => u._id)
|
||||||
secretVersions: updatedLatestSecretVersions
|
});
|
||||||
}
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
actions.push(updateAction);
|
updateAction && actions.push(updateAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle adding new secrets
|
// handle adding new secrets
|
||||||
@@ -517,45 +473,14 @@ const v1PushSecrets = async ({
|
|||||||
if (toAdd.length > 0) {
|
if (toAdd.length > 0) {
|
||||||
// add secrets
|
// add secrets
|
||||||
const newSecrets = await Secret.insertMany(
|
const newSecrets = await Secret.insertMany(
|
||||||
toAdd.map(({
|
toAdd.map((s, idx) => ({
|
||||||
secretKeyCiphertext,
|
...s,
|
||||||
secretKeyIV,
|
version: 1,
|
||||||
secretKeyTag,
|
workspace: workspaceId,
|
||||||
secretKeyHash,
|
type: toAdd[idx].type,
|
||||||
secretValueCiphertext,
|
environment,
|
||||||
secretValueIV,
|
...( toAdd[idx].type === 'personal' ? { user: userId } : {})
|
||||||
secretValueTag,
|
}))
|
||||||
secretValueHash,
|
|
||||||
secretCommentCiphertext,
|
|
||||||
secretCommentIV,
|
|
||||||
secretCommentTag,
|
|
||||||
secretCommentHash,
|
|
||||||
}, idx) => {
|
|
||||||
const obj: any = {
|
|
||||||
version: 1,
|
|
||||||
workspace: workspaceId,
|
|
||||||
type: toAdd[idx].type,
|
|
||||||
environment,
|
|
||||||
secretKeyCiphertext,
|
|
||||||
secretKeyIV,
|
|
||||||
secretKeyTag,
|
|
||||||
secretKeyHash,
|
|
||||||
secretValueCiphertext,
|
|
||||||
secretValueIV,
|
|
||||||
secretValueTag,
|
|
||||||
secretValueHash,
|
|
||||||
secretCommentCiphertext,
|
|
||||||
secretCommentIV,
|
|
||||||
secretCommentTag,
|
|
||||||
secretCommentHash
|
|
||||||
};
|
|
||||||
|
|
||||||
if (toAdd[idx].type === 'personal') {
|
|
||||||
obj['user' as keyof typeof obj] = userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// (EE) add secret versions for new secrets
|
// (EE) add secret versions for new secrets
|
||||||
@@ -584,35 +509,14 @@ const v1PushSecrets = async ({
|
|||||||
secretValueHash
|
secretValueHash
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
// add audit log for new secrets
|
|
||||||
const newLatestSecretVersions = (await SecretVersion.aggregate([
|
|
||||||
{
|
|
||||||
$match: { secret: { $in: newSecrets.map((n) => n._id) } }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: '$secret',
|
|
||||||
version: { $max: '$version' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$sort: { version: -1 }
|
|
||||||
}
|
|
||||||
])
|
|
||||||
.exec())
|
|
||||||
.map((s) => s._id);
|
|
||||||
|
|
||||||
const addAction = await new Action({
|
const addAction = await EELogService.createActionSecret({
|
||||||
name: ACTION_ADD_SECRETS,
|
name: ACTION_ADD_SECRETS,
|
||||||
user: new Types.ObjectId(userId),
|
userId,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspaceId,
|
||||||
payload: {
|
secretIds: newSecrets.map((n) => n._id)
|
||||||
secretVersions: newLatestSecretVersions
|
});
|
||||||
}
|
addAction && actions.push(addAction);
|
||||||
}).save();
|
|
||||||
|
|
||||||
actions.push(addAction);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
@@ -620,6 +524,7 @@ const v1PushSecrets = async ({
|
|||||||
workspaceId
|
workspaceId
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// (EE) create (audit) log
|
||||||
if (actions.length > 0) {
|
if (actions.length > 0) {
|
||||||
await EELogService.createLog({
|
await EELogService.createLog({
|
||||||
userId,
|
userId,
|
||||||
@@ -637,7 +542,7 @@ const v1PushSecrets = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pull secrets for user with id [userId] for workspace
|
* Get secrets for user with id [userId] for workspace
|
||||||
* with id [workspaceId] with environment [environment]
|
* with id [workspaceId] with environment [environment]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.userId -id of user to pull secrets for
|
* @param {String} obj.userId -id of user to pull secrets for
|
||||||
@@ -704,7 +609,7 @@ const pullSecrets = async ({
|
|||||||
channel: string;
|
channel: string;
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
}): Promise<ISecret[]> => {
|
}): Promise<ISecret[]> => {
|
||||||
let secrets: any; // TODO: FIX any
|
let secrets: any;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
secrets = await getSecrets({
|
secrets = await getSecrets({
|
||||||
@@ -712,35 +617,15 @@ const pullSecrets = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
environment
|
environment
|
||||||
})
|
})
|
||||||
|
|
||||||
// add audit log for new secrets
|
|
||||||
const readLatestSecretVersions = (await SecretVersion.aggregate([
|
|
||||||
{
|
|
||||||
$match: { secret: { $in: secrets.map((n: any) => n._id) } }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: '$secret',
|
|
||||||
version: { $max: '$version' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$sort: { version: -1 }
|
|
||||||
}
|
|
||||||
])
|
|
||||||
.exec())
|
|
||||||
.map((s) => s._id);
|
|
||||||
|
|
||||||
const readAction = await new Action({
|
const readAction = await EELogService.createActionSecret({
|
||||||
name: ACTION_READ_SECRETS,
|
name: ACTION_READ_SECRETS,
|
||||||
user: new Types.ObjectId(userId),
|
userId,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspaceId,
|
||||||
payload: {
|
secretIds: secrets.map((n: any) => n._id)
|
||||||
secretVersions: readLatestSecretVersions
|
});
|
||||||
}
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
await EELogService.createLog({
|
readAction && await EELogService.createLog({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actions: [readAction],
|
actions: [readAction],
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ dotenv.config();
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config';
|
import { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config';
|
||||||
import { server } from './app';
|
import { server } from './app';
|
||||||
import { initDatabase } from './services/database';
|
import { DatabaseService } from './services';
|
||||||
import { setUpHealthEndpoint } from './services/health';
|
import { setUpHealthEndpoint } from './services/health';
|
||||||
import { initSmtp } from './services/smtp';
|
import { initSmtp } from './services/smtp';
|
||||||
import { setTransporter } from './helpers/nodemailer';
|
import { setTransporter } from './helpers/nodemailer';
|
||||||
|
|
||||||
initDatabase(MONGO_URL);
|
DatabaseService.initDatabase(MONGO_URL);
|
||||||
|
|
||||||
setUpHealthEndpoint(server);
|
setUpHealthEndpoint(server);
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ const userSchema = new Schema<IUser>(
|
|||||||
},
|
},
|
||||||
refreshVersion: {
|
refreshVersion: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0,
|
||||||
|
select: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { getLogger } from '../utils/logger';
|
||||||
|
import { initDatabaseHelper } from '../helpers/database';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle database actions
|
||||||
|
*/
|
||||||
|
class DatabaseService {
|
||||||
|
static async initDatabase(MONGO_URL: string) {
|
||||||
|
return await initDatabaseHelper({
|
||||||
|
mongoURL: MONGO_URL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DatabaseService;
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
import { getLogger } from '../utils/logger';
|
|
||||||
|
|
||||||
export const initDatabase = (MONGO_URL: string) => {
|
|
||||||
mongoose
|
|
||||||
.connect(MONGO_URL)
|
|
||||||
.then(() => getLogger("database").info("Database connection established"))
|
|
||||||
.catch((e) => getLogger("database").error(`Unable to establish Database connection due to the error.\n${e}`));
|
|
||||||
return mongoose.connection;
|
|
||||||
};
|
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import DatabaseService from './DatabaseService';
|
||||||
import postHogClient from './PostHogClient';
|
import postHogClient from './PostHogClient';
|
||||||
import BotService from './BotService';
|
import BotService from './BotService';
|
||||||
import EventService from './EventService';
|
import EventService from './EventService';
|
||||||
import IntegrationService from './IntegrationService';
|
import IntegrationService from './IntegrationService';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
DatabaseService,
|
||||||
postHogClient,
|
postHogClient,
|
||||||
BotService,
|
BotService,
|
||||||
EventService,
|
EventService,
|
||||||
|
|||||||
Reference in New Issue
Block a user