This commit is contained in:
Tuan Dang
2023-01-06 13:09:32 +07:00
111 changed files with 6121 additions and 3887 deletions
+6 -6
View File
@@ -6687,9 +6687,9 @@
"dev": true "dev": true
}, },
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.2", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true, "dev": true,
"bin": { "bin": {
"json5": "lib/cli.js" "json5": "lib/cli.js"
@@ -17198,9 +17198,9 @@
"dev": true "dev": true
}, },
"json5": { "json5": {
"version": "2.2.1", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true "dev": true
}, },
"jsonwebtoken": { "jsonwebtoken": {
+5 -1
View File
@@ -13,7 +13,9 @@ import { apiLimiter } from './helpers/rateLimiter';
import { import {
workspace as eeWorkspaceRouter, workspace as eeWorkspaceRouter,
secret as eeSecretRouter secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter,
action as eeActionRouter
} from './ee/routes/v1'; } from './ee/routes/v1';
import { import {
signup as v1SignupRouter, signup as v1SignupRouter,
@@ -70,7 +72,9 @@ if (NODE_ENV === 'production') {
// (EE) routes // (EE) routes
app.use('/api/v1/secret', eeSecretRouter); app.use('/api/v1/secret', eeSecretRouter);
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
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);
@@ -123,7 +123,9 @@ export const pullSecrets = async (req: Request, res: Response) => {
secrets = await pull({ secrets = await pull({
userId: req.user._id.toString(), userId: req.user._id.toString(),
workspaceId, workspaceId,
environment environment,
channel: channel ? channel : 'cli',
ipAddress: req.ip
}); });
key = await Key.findOne({ key = await Key.findOne({
@@ -188,7 +190,9 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
secrets = await pull({ secrets = await pull({
userId: req.serviceToken.user._id.toString(), userId: req.serviceToken.user._id.toString(),
workspaceId, workspaceId,
environment environment,
channel: 'cli',
ipAddress: req.ip
}); });
key = { key = {
@@ -2,7 +2,7 @@ import to from "await-to-js";
import { Request, Response } from "express"; import { Request, Response } from "express";
import mongoose, { Types } from "mongoose"; import mongoose, { Types } from "mongoose";
import Secret, { ISecret } from "../../models/secret"; import Secret, { ISecret } from "../../models/secret";
import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCreate, SanitizedSecretModify } from "../../types/secret/types"; import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCreate, SanitizedSecretModify } from "../../types/secret";
const { ValidationError } = mongoose.Error; const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
import { AnyBulkWriteOperation } from 'mongodb'; import { AnyBulkWriteOperation } from 'mongodb';
@@ -11,10 +11,6 @@ import {
ServiceToken, ServiceToken,
ServiceTokenData ServiceTokenData
} from '../../models'; } from '../../models';
import {
createWorkspace as create,
deleteWorkspace as deleteWork
} from '../../helpers/workspace';
import { import {
v2PushSecrets as push, v2PushSecrets as push,
pullSecrets as pull, pullSecrets as pull,
@@ -50,7 +46,6 @@ interface V2PushSecret {
*/ */
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;
@@ -70,7 +65,9 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
environment, environment,
secrets secrets,
channel: channel ? channel : 'cli',
ipAddress: req.ip
}); });
await pushKeys({ await pushKeys({
@@ -136,7 +133,9 @@ export const pullSecrets = async (req: Request, res: Response) => {
secrets = await pull({ secrets = await pull({
userId, userId,
workspaceId, workspaceId,
environment environment,
channel: channel ? channel : 'cli',
ipAddress: req.ip
}); });
if (channel !== 'cli') { if (channel !== 'cli') {
@@ -196,7 +195,7 @@ export const getWorkspaceServiceTokenData = async (
) => { ) => {
let serviceTokenData; let serviceTokenData;
try { try {
const { workspaceId } = req.query; const { workspaceId } = req.params;
serviceTokenData = await ServiceTokenData serviceTokenData = await ServiceTokenData
.find({ .find({
@@ -0,0 +1,31 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Action, SecretVersion } from '../../models';
import { ActionNotFoundError } from '../../../utils/errors';
export const getAction = async (req: Request, res: Response) => {
let action;
try {
const { actionId } = req.params;
action = await Action
.findById(actionId)
.populate([
'payload.secretVersions.oldSecretVersion',
'payload.secretVersions.newSecretVersion'
]);
if (!action) throw ActionNotFoundError({
message: 'Failed to find action'
});
} catch (err) {
throw ActionNotFoundError({
message: 'Failed to find action'
});
}
return res.status(200).send({
action
});
}
+5 -1
View File
@@ -1,9 +1,13 @@
import * as stripeController from './stripeController'; import * as stripeController from './stripeController';
import * as secretController from './secretController'; import * as secretController from './secretController';
import * as secretSnapshotController from './secretSnapshotController';
import * as workspaceController from './workspaceController'; import * as workspaceController from './workspaceController';
import * as actionController from './actionController';
export { export {
stripeController, stripeController,
secretController, secretController,
workspaceController secretSnapshotController,
workspaceController,
actionController
} }
@@ -18,6 +18,7 @@ import { SecretVersion } from '../../models';
secretVersions = await SecretVersion.find({ secretVersions = await SecretVersion.find({
secret: secretId secret: secretId
}) })
.sort({ createdAt: -1 })
.skip(offset) .skip(offset)
.limit(limit); .limit(limit);
@@ -0,0 +1,27 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { SecretSnapshot } from '../../models';
export const getSecretSnapshot = async (req: Request, res: Response) => {
let secretSnapshot;
try {
const { secretSnapshotId } = req.params;
secretSnapshot = await SecretSnapshot
.findById(secretSnapshotId)
.populate('secretVersions');
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get secret snapshot'
});
}
return res.status(200).send({
secretSnapshot
});
}
@@ -1,6 +1,9 @@
import { Request, Response } from 'express'; import e, { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { SecretSnapshot } from '../../models'; import {
SecretSnapshot,
Log
} from '../../models';
/** /**
* Return secret snapshots for workspace with id [workspaceId] * Return secret snapshots for workspace with id [workspaceId]
@@ -18,6 +21,7 @@ import { SecretSnapshot } from '../../models';
secretSnapshots = await SecretSnapshot.find({ secretSnapshots = await SecretSnapshot.find({
workspace: workspaceId workspace: workspaceId
}) })
.sort({ createdAt: -1 })
.skip(offset) .skip(offset)
.limit(limit); .limit(limit);
@@ -32,4 +36,77 @@ import { SecretSnapshot } from '../../models';
return res.status(200).send({ return res.status(200).send({
secretSnapshots secretSnapshots
}); });
}
/**
* Return count of secret snapshots for workspace with id [workspaceId]
* @param req
* @param res
*/
export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => {
let count;
try {
const { workspaceId } = req.params;
count = await SecretSnapshot.countDocuments({
workspace: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to count number of secret snapshots'
});
}
return res.status(200).send({
count
});
}
/**
* Return (audit) logs for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceLogs = async (req: Request, res: Response) => {
let logs
try {
const { workspaceId } = req.params;
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
const sortBy: string = req.query.sortBy as string;
const userId: string = req.query.userId as string;
const actionNames: string = req.query.actionNames as string;
logs = await Log.find({
workspace: workspaceId,
...( userId ? { user: userId } : {}),
...(
actionNames
? {
actionNames: {
$in: actionNames.split(',')
}
} : {}
)
})
.sort({ createdAt: sortBy === 'recent' ? -1 : 1 })
.skip(offset)
.limit(limit)
.populate('actions')
.populate('user');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace logs'
});
}
return res.status(200).send({
logs
});
} }
+112
View File
@@ -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 };
+41
View File
@@ -0,0 +1,41 @@
import * as Sentry from '@sentry/node';
import {
Log,
IAction
} from '../models';
const createLogHelper = async ({
userId,
workspaceId,
actions,
channel,
ipAddress
}: {
userId: string;
workspaceId: string;
actions: IAction[];
channel: string;
ipAddress: string;
}) => {
let log;
try {
log = await new Log({
user: userId,
workspace: workspaceId,
actionNames: actions.map((a) => a.name),
actions,
channel,
ipAddress
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create log');
}
return log;
}
export {
createLogHelper
}
+117 -22
View File
@@ -1,6 +1,8 @@
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { import {
Secret Secret,
ISecret
} from '../../models'; } from '../../models';
import { import {
SecretSnapshot, SecretSnapshot,
@@ -9,66 +11,159 @@ import {
} from '../models'; } from '../models';
/** /**
* Save a copy of the current state of secrets in workspace with id * Save a secret snapshot that is a copy of the current state of secrets in workspace with id
* [workspaceId] under a new snapshot with incremented version under the * [workspaceId] under a new snapshot with incremented version under the
* secretsnapshots collection. * secretsnapshots collection.
* @param {Object} obj * @param {Object} obj
* @param {String} obj.workspaceId * @param {String} obj.workspaceId
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
*/ */
const takeSecretSnapshotHelper = async ({ const takeSecretSnapshotHelper = async ({
workspaceId workspaceId
}: { }: {
workspaceId: string; workspaceId: string;
}) => { }) => {
let secretSnapshot;
try { try {
const secrets = await Secret.find({ const secretIds = (await Secret.find({
workspace: workspaceId workspace: workspaceId
}); }, '_id')).map((s) => s._id);
const 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) => s.versionId);
const latestSecretSnapshot = await SecretSnapshot.findOne({ const latestSecretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId workspace: workspaceId
}).sort({ version: -1 }); }).sort({ version: -1 });
if (!latestSecretSnapshot) { secretSnapshot = await new SecretSnapshot({
// case: no snapshots exist for workspace -> create first snapshot
await new SecretSnapshot({
workspace: workspaceId,
version: 1,
secrets
}).save();
return;
}
// case: snapshots exist for workspace
await new SecretSnapshot({
workspace: workspaceId, workspace: workspaceId,
version: latestSecretSnapshot.version + 1, version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
secrets secretVersions: latestSecretVersions
}).save(); }).save();
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to take a secret snapshot'); throw new Error('Failed to take a secret snapshot');
} }
return secretSnapshot;
} }
/**
* Add secret versions [secretVersions] to the SecretVersion collection.
* @param {Object} obj
* @param {Object[]} obj.secretVersions
* @returns {SecretVersion[]} newSecretVersions - new secret versions
*/
const addSecretVersionsHelper = async ({ const addSecretVersionsHelper = async ({
secretVersions secretVersions
}: { }: {
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');
}
}
/**
* Initialize secret versioning by setting previously unversioned
* secrets to version 1 and begin populating secret versions.
*/
const initSecretVersioningHelper = async () => {
try {
await Secret.updateMany(
{ version: { $exists: false } },
{ $set: { version: 1 } }
);
const unversionedSecrets: ISecret[] = await Secret.aggregate([
{
$lookup: {
from: 'secretversions',
localField: '_id',
foreignField: 'secret',
as: 'versions',
},
},
{
$match: {
versions: { $size: 0 },
},
},
]);
if (unversionedSecrets.length > 0) {
await addSecretVersionsHelper({
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) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to ensure that secrets are versioned');
}
} }
export { export {
takeSecretSnapshotHelper, takeSecretSnapshotHelper,
addSecretVersionsHelper addSecretVersionsHelper,
markDeletedSecretVersionsHelper,
initSecretVersioningHelper
} }
+7
View File
@@ -0,0 +1,7 @@
import requireLicenseAuth from './requireLicenseAuth';
import requireSecretSnapshotAuth from './requireSecretSnapshotAuth';
export {
requireLicenseAuth,
requireSecretSnapshotAuth
}
@@ -0,0 +1,47 @@
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError, SecretSnapshotNotFoundError } from '../../utils/errors';
import { SecretSnapshot } from '../models';
import {
validateMembership
} from '../../helpers/membership';
/**
* Validate if user on request has proper membership for secret snapshot
* @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
*/
const requireSecretSnapshotAuth = ({
acceptedRoles,
}: {
acceptedRoles: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { secretSnapshotId } = req.params;
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
if (!secretSnapshot) {
return next(SecretSnapshotNotFoundError({
message: 'Failed to find secret snapshot'
}));
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: secretSnapshot.workspace.toString(),
acceptedRoles
});
req.secretSnapshot = secretSnapshot as any;
next();
} catch (err) {
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret snapshot' }));
}
}
}
export default requireSecretSnapshotAuth;
+46
View File
@@ -0,0 +1,46 @@
import { Schema, model, Types } from 'mongoose';
export interface IAction {
name: string;
user?: Types.ObjectId,
workspace?: Types.ObjectId,
payload: {
secretVersions?: Types.ObjectId[]
}
}
const actionSchema = new Schema<IAction>(
{
name: {
type: String,
required: true
},
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace'
},
payload: {
secretVersions: [{
oldSecretVersion: {
type: Schema.Types.ObjectId,
ref: 'SecretVersion'
},
newSecretVersion: {
type: Schema.Types.ObjectId,
ref: 'SecretVersion'
}
}]
}
}, {
timestamps: true
}
);
const Action = model<IAction>('Action', actionSchema);
export default Action;
+9 -3
View File
@@ -1,9 +1,15 @@
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot"; import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot';
import SecretVersion, { ISecretVersion } from "./secretVersion"; import SecretVersion, { ISecretVersion } from './secretVersion';
import Log, { ILog } from './log';
import Action, { IAction } from './action';
export { export {
SecretSnapshot, SecretSnapshot,
ISecretSnapshot, ISecretSnapshot,
SecretVersion, SecretVersion,
ISecretVersion ISecretVersion,
Log,
ILog,
Action,
IAction
} }
+59
View File
@@ -0,0 +1,59 @@
import { Schema, model, Types } from 'mongoose';
import {
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS,
ACTION_DELETE_SECRETS
} from '../../variables';
export interface ILog {
_id: Types.ObjectId;
user?: Types.ObjectId;
workspace?: Types.ObjectId;
actionNames: string[];
actions: Types.ObjectId[];
channel: string;
ipAddress?: string;
}
const logSchema = new Schema<ILog>(
{
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace'
},
actionNames: {
type: [String],
enum: [
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS,
ACTION_DELETE_SECRETS
],
required: true
},
actions: [{
type: Schema.Types.ObjectId,
ref: 'Action',
required: true
}],
channel: {
type: String,
enum: ['web', 'cli', 'auto'],
required: true
},
ipAddress: {
type: String
}
}, {
timestamps: true
}
);
const Log = model<ILog>('Log', logSchema);
export default Log;
+5 -81
View File
@@ -1,31 +1,9 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import {
SECRET_SHARED,
SECRET_PERSONAL,
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD
} from '../../variables';
export interface ISecretSnapshot { export interface ISecretSnapshot {
workspace: Types.ObjectId; workspace: Types.ObjectId;
version: number; version: number;
secrets: { secretVersions: Types.ObjectId[];
version: number;
workspace: Types.ObjectId;
type: string;
user: Types.ObjectId;
environment: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
}[]
} }
const secretSnapshotSchema = new Schema<ISecretSnapshot>( const secretSnapshotSchema = new Schema<ISecretSnapshot>(
@@ -39,64 +17,10 @@ const secretSnapshotSchema = new Schema<ISecretSnapshot>(
type: Number, type: Number,
required: true required: true
}, },
secrets: [{ secretVersions: [{
version: { type: Schema.Types.ObjectId,
type: Number, ref: 'SecretVersion',
default: 1, required: true
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
type: {
type: String,
enum: [SECRET_SHARED, SECRET_PERSONAL],
required: true
},
user: {
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: 'User'
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
secretKeyCiphertext: {
type: String,
required: true
},
secretKeyIV: {
type: String, // symmetric
required: true
},
secretKeyTag: {
type: String, // symmetric
required: true
},
secretKeyHash: {
type: String,
required: true
},
secretValueCiphertext: {
type: String,
required: true
},
secretValueIV: {
type: String, // symmetric
required: true
},
secretValueTag: {
type: String, // symmetric
required: true
},
secretValueHash: {
type: String,
required: true
}
}] }]
}, },
{ {
+41
View File
@@ -1,9 +1,30 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import {
SECRET_SHARED,
SECRET_PERSONAL,
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD
} from '../../variables';
/**
* TODO:
* 1. Modify SecretVersion to also contain XX
* - type
* - user
* - environment
* 2. Modify SecretSnapshot to point to arrays of SecretVersion
*/
export interface ISecretVersion { export interface ISecretVersion {
_id?: Types.ObjectId; _id?: Types.ObjectId;
secret: Types.ObjectId; secret: Types.ObjectId;
version: number; version: number;
workspace: Types.ObjectId; // new
type: string; // new
user: Types.ObjectId; // new
environment: string; // new
isDeleted: boolean; isDeleted: boolean;
secretKeyCiphertext: string; secretKeyCiphertext: string;
secretKeyIV: string; secretKeyIV: string;
@@ -27,6 +48,26 @@ const secretVersionSchema = new Schema<ISecretVersion>(
default: 1, default: 1,
required: true required: true
}, },
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
type: {
type: String,
enum: [SECRET_SHARED, SECRET_PERSONAL],
required: true
},
user: {
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: 'User'
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
isDeleted: { isDeleted: {
type: Boolean, type: Boolean,
default: false, default: false,
+17
View File
@@ -0,0 +1,17 @@
import express from 'express';
const router = express.Router();
import {
validateRequest
} from '../../../middleware';
import { param } from 'express-validator';
import { actionController } from '../../controllers/v1';
// TODO: put into action controller
router.get(
'/:actionId',
param('actionId').exists().trim(),
validateRequest,
actionController.getAction
);
export default router;
+5 -1
View File
@@ -1,7 +1,11 @@
import secret from './secret'; import secret from './secret';
import secretSnapshot from './secretSnapshot';
import workspace from './workspace'; import workspace from './workspace';
import action from './action';
export { export {
secret, secret,
workspace secretSnapshot,
workspace,
action
} }
@@ -0,0 +1,27 @@
import express from 'express';
const router = express.Router();
import {
requireSecretSnapshotAuth
} from '../../middleware';
import {
requireAuth,
validateRequest
} from '../../../middleware';
import { param } from 'express-validator';
import { ADMIN, MEMBER } from '../../../variables';
import { secretSnapshotController } from '../../controllers/v1';
router.get(
'/:secretSnapshotId',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireSecretSnapshotAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('secretSnapshotId').exists().trim(),
validateRequest,
secretSnapshotController.getSecretSnapshot
);
export default router;
+31
View File
@@ -24,4 +24,35 @@ router.get(
workspaceController.getWorkspaceSecretSnapshots workspaceController.getWorkspaceSecretSnapshots
); );
router.get(
'/:workspaceId/secret-snapshots/count',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspaceSecretSnapshotsCount
);
router.get(
'/:workspaceId/logs',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().trim(),
query('offset').exists().isInt(),
query('limit').exists().isInt(),
query('sortBy'),
query('userId'),
query('actionNames'),
validateRequest,
workspaceController.getWorkspaceLogs
);
export default router; export default router;
+81
View File
@@ -0,0 +1,81 @@
import { Types } from 'mongoose';
import {
Log,
Action,
IAction
} from '../models';
import {
createLogHelper
} from '../helpers/log';
import {
createActionSecretHelper
} from '../helpers/action';
import EELicenseService from './EELicenseService';
/**
* Class to handle Enterprise Edition log actions
*/
class EELogService {
/**
* Create an (audit) log
* @param {Object} obj
* @param {String} obj.userId - id of user associated with the log
* @param {String} obj.workspaceId - id of workspace associated with the 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.ipAddress - ip address associated with the log
* @returns {Log} log - new audit log
*/
static async createLog({
userId,
workspaceId,
actions,
channel,
ipAddress
}: {
userId: string;
workspaceId: string;
actions: IAction[];
channel: string;
ipAddress: string;
}) {
if (!EELicenseService.isLicenseValid) return null;
return await createLogHelper({
userId,
workspaceId,
actions,
channel,
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;
+37 -6
View File
@@ -1,7 +1,10 @@
import { Types } from 'mongoose';
import { ISecretVersion } from '../models'; import { ISecretVersion } from '../models';
import { import {
takeSecretSnapshotHelper, takeSecretSnapshotHelper,
addSecretVersionsHelper addSecretVersionsHelper,
markDeletedSecretVersionsHelper,
initSecretVersioningHelper
} from '../helpers/secret'; } from '../helpers/secret';
import EELicenseService from './EELicenseService'; import EELicenseService from './EELicenseService';
@@ -11,12 +14,13 @@ import EELicenseService from './EELicenseService';
class EESecretService { class EESecretService {
/** /**
* Save a copy of the current state of secrets in workspace with id * Save a secret snapshot that is a copy of the current state of secrets in workspace with id
* [workspaceId] under a new snapshot with incremented version under the * [workspaceId] under a new snapshot with incremented version under the
* SecretSnapshot collection. * SecretSnapshot collection.
* Requires a valid license key [licenseKey] * Requires a valid license key [licenseKey]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.workspaceId * @param {String} obj.workspaceId
* @returns {SecretSnapshot} secretSnapshot - new secret snpashot
*/ */
static async takeSecretSnapshot({ static async takeSecretSnapshot({
workspaceId workspaceId
@@ -24,13 +28,14 @@ class EESecretService {
workspaceId: string; workspaceId: string;
}) { }) {
if (!EELicenseService.isLicenseValid) return; if (!EELicenseService.isLicenseValid) return;
await takeSecretSnapshotHelper({ workspaceId }); return await takeSecretSnapshotHelper({ workspaceId });
} }
/** /**
* 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 {Object[]} obj.secretVersions
* @returns {SecretVersion[]} newSecretVersions - new secret versions
*/ */
static async addSecretVersions({ static async addSecretVersions({
secretVersions secretVersions
@@ -38,10 +43,36 @@ 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
});
}
/**
* Initialize secret versioning by setting previously unversioned
* secrets to version 1 and begin populating secret versions.
*/
static async initSecretVersioning() {
if (!EELicenseService.isLicenseValid) return;
await initSecretVersioningHelper();
}
} }
export default EESecretService; export default EESecretService;
+3 -1
View File
@@ -1,7 +1,9 @@
import EELicenseService from "./EELicenseService"; import EELicenseService from "./EELicenseService";
import EESecretService from "./EESecretService"; import EESecretService from "./EESecretService";
import EELogService from "./EELogService";
export { export {
EELicenseService, EELicenseService,
EESecretService EESecretService,
EELogService
} }
View File
+25 -2
View File
@@ -1,4 +1,7 @@
import { EVENT_PUSH_SECRETS } from '../variables'; import {
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS
} from '../variables';
interface PushSecret { interface PushSecret {
ciphertextKey: string; ciphertextKey: string;
@@ -19,7 +22,7 @@ interface PushSecret {
* @returns * @returns
*/ */
const eventPushSecrets = ({ const eventPushSecrets = ({
workspaceId, workspaceId
}: { }: {
workspaceId: string; workspaceId: string;
}) => { }) => {
@@ -32,6 +35,26 @@ const eventPushSecrets = ({
}); });
} }
/**
* Return event for pulling secrets
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace to pull secrets from
* @returns
*/
const eventPullSecrets = ({
workspaceId,
}: {
workspaceId: string;
}) => {
return ({
name: EVENT_PULL_SECRETS,
workspaceId,
payload: {
}
});
}
export { export {
eventPushSecrets eventPushSecrets
} }
+31
View File
@@ -0,0 +1,31 @@
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 EESecretService.initSecretVersioning();
} catch (err) {
getLogger("database").error(`Unable to establish Database connection due to the error.\n${err}`);
}
return mongoose.connection;
}
export {
initDatabaseHelper
}
+215 -169
View File
@@ -1,19 +1,24 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { import {
Secret, Secret,
ISecret, ISecret,
} from '../models'; } from '../models';
import { import {
EESecretService EESecretService,
EELogService
} from '../ee/services'; } from '../ee/services';
import { import {
SecretVersion IAction
} from '../ee/models'; } from '../ee/models';
import { import {
takeSecretSnapshotHelper SECRET_SHARED,
} from '../ee/helpers/secret'; SECRET_PERSONAL,
import { decryptSymmetric } from '../utils/crypto'; ACTION_ADD_SECRETS,
import { SECRET_SHARED, SECRET_PERSONAL } from '../variables'; ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS
} from '../variables';
interface V1PushSecret { interface V1PushSecret {
ciphertextKey: string; ciphertextKey: string;
@@ -51,8 +56,6 @@ interface Update {
[index: string]: any; [index: string]: any;
} }
type DecryptSecretType = 'text' | 'object' | 'expanded';
/** /**
* 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:
@@ -68,7 +71,7 @@ const v1PushSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment, environment,
secrets secrets,
}: { }: {
userId: string; userId: string;
workspaceId: string; workspaceId: string;
@@ -78,7 +81,7 @@ const v1PushSecrets = async ({
// TODO: clean up function and fix up types // TODO: clean up function and fix up types
try { try {
// construct useful data structures // construct useful data structures
const oldSecrets = await pullSecrets({ const oldSecrets = await getSecrets({
userId, userId,
workspaceId, workspaceId,
environment environment
@@ -101,11 +104,9 @@ const v1PushSecrets = async ({
await Secret.deleteMany({ await Secret.deleteMany({
_id: { $in: toDelete } _id: { $in: toDelete }
}); });
await SecretVersion.updateMany({ await EESecretService.markDeletedSecretVersions({
secret: { $in: toDelete } secretIds: toDelete
}, {
isDeleted: true
}); });
} }
@@ -188,6 +189,10 @@ const v1PushSecrets = async ({
return ({ return ({
secret: _id, secret: _id,
version: version ? version + 1 : 1, version: version ? version + 1 : 1,
workspace: new Types.ObjectId(workspaceId),
type: newSecret.type,
user: new Types.ObjectId(userId),
environment,
isDeleted: false, isDeleted: false,
secretKeyCiphertext: newSecret.ciphertextKey, secretKeyCiphertext: newSecret.ciphertextKey,
secretKeyIV: newSecret.ivKey, secretKeyIV: newSecret.ivKey,
@@ -239,6 +244,11 @@ const v1PushSecrets = async ({
EESecretService.addSecretVersions({ EESecretService.addSecretVersions({
secretVersions: newSecrets.map(({ secretVersions: newSecrets.map(({
_id, _id,
version,
workspace,
type,
user,
environment,
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
secretKeyTag, secretKeyTag,
@@ -249,7 +259,11 @@ const v1PushSecrets = async ({
secretValueHash secretValueHash
}) => ({ }) => ({
secret: _id, secret: _id,
version: 1, version,
workspace,
type,
user,
environment,
isDeleted: false, isDeleted: false,
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
@@ -284,22 +298,30 @@ const v1PushSecrets = async ({
* @param {String} obj.workspaceId - id of workspace to push to * @param {String} obj.workspaceId - id of workspace to push to
* @param {String} obj.environment - environment for secrets * @param {String} obj.environment - environment for secrets
* @param {Object[]} obj.secrets - secrets to push * @param {Object[]} obj.secrets - secrets to push
* @param {String} obj.channel - channel (web/cli/auto)
* @param {String} obj.ipAddress - ip address of request to push secrets
*/ */
const v2PushSecrets = async ({ const v2PushSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment, environment,
secrets secrets,
channel,
ipAddress
}: { }: {
userId: string; userId: string;
workspaceId: string; workspaceId: string;
environment: string; environment: string;
secrets: V2PushSecret[]; secrets: V2PushSecret[];
channel: string;
ipAddress: string;
}): Promise<void> => { }): Promise<void> => {
// TODO: clean up function and fix up types // TODO: clean up function and fix up types
try { try {
const actions: IAction[] = [];
// construct useful data structures // construct useful data structures
const oldSecrets = await pullSecrets({ const oldSecrets = await getSecrets({
userId, userId,
workspaceId, workspaceId,
environment environment
@@ -322,12 +344,19 @@ const v1PushSecrets = async ({
await Secret.deleteMany({ await Secret.deleteMany({
_id: { $in: toDelete } _id: { $in: toDelete }
}); });
await SecretVersion.updateMany({ await EESecretService.markDeletedSecretVersions({
secret: { $in: toDelete } secretIds: toDelete
}, {
isDeleted: true
}); });
const deleteAction = await EELogService.createActionSecret({
name: ACTION_DELETE_SECRETS,
userId,
workspaceId,
secretIds: toDelete
});
deleteAction && actions.push(deleteAction);
} }
const toUpdate = oldSecrets const toUpdate = oldSecrets
@@ -348,118 +377,10 @@ const v1PushSecrets = async ({
return false; return false;
}); });
const operations = toUpdate if (toUpdate.length > 0) {
.map((s) => { const operations = toUpdate
const { .map((s) => {
secretValueCiphertext, const {
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
const update: Update = {
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
}
if (!s.version) {
// case: (legacy) secret was not versioned
update.version = 1;
} else {
update['$inc'] = {
version: 1
}
}
if (s.type === SECRET_PERSONAL) {
// attach user associated with the personal secret
update['user'] = userId;
}
return {
updateOne: {
filter: {
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
},
update
}
};
});
await Secret.bulkWrite(operations as any);
// (EE) add secret versions for updated secrets
await EESecretService.addSecretVersions({
secretVersions: toUpdate.map((s) => {
const {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
return ({
secret: s._id,
version: s.version ? s.version + 1 : 1,
isDeleted: false,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
})
})
});
// handle adding new secrets
const toAdd = secrets.filter((s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj));
if (toAdd.length > 0) {
// add secrets
const newSecrets = await Secret.insertMany(
toAdd.map(({
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
}, idx) => {
const obj: any = {
version: 1,
workspace: workspaceId,
type: toAdd[idx].type,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
@@ -467,49 +388,120 @@ const v1PushSecrets = async ({
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
secretCommentHash secretCommentHash,
}; } = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
if (toAdd[idx].type === 'personal') { const update: Update = {
obj['user' as keyof typeof obj] = userId; secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
} }
return obj; if (!s.version) {
}) // case: (legacy) secret was not versioned
update.version = 1;
} else {
update['$inc'] = {
version: 1
}
}
if (s.type === SECRET_PERSONAL) {
// attach user associated with the personal secret
update['user'] = userId;
}
return {
updateOne: {
filter: {
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
},
update
}
};
});
await Secret.bulkWrite(operations as any);
// (EE) add secret versions for updated secrets
await EESecretService.addSecretVersions({
secretVersions: toUpdate.map((s) => {
return ({
...newSecretsObj[`${s.type}-${s.secretKeyHash}`],
secret: s._id,
version: s.version ? s.version + 1 : 1,
workspace: new Types.ObjectId(workspaceId),
user: s.user,
environment: s.environment,
isDeleted: false
})
})
});
const updateAction = await EELogService.createActionSecret({
name: ACTION_UPDATE_SECRETS,
userId,
workspaceId,
secretIds: toUpdate.map((u) => u._id)
});
updateAction && actions.push(updateAction);
}
// handle adding new secrets
const toAdd = secrets.filter((s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj));
if (toAdd.length > 0) {
// add secrets
const newSecrets = await Secret.insertMany(
toAdd.map((s, idx) => ({
...s,
version: 1,
workspace: workspaceId,
type: toAdd[idx].type,
environment,
...( toAdd[idx].type === 'personal' ? { user: userId } : {})
}))
); );
// (EE) add secret versions for new secrets // (EE) add secret versions for new secrets
EESecretService.addSecretVersions({ EESecretService.addSecretVersions({
secretVersions: newSecrets.map(({ secretVersions: newSecrets.map((secretDocument) => {
_id, return {
secretKeyCiphertext, ...secretDocument.toObject(),
secretKeyIV, secret: secretDocument._id,
secretKeyTag, isDeleted: false
secretKeyHash, }})
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
}) => ({
secret: _id,
version: 1,
isDeleted: false,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
}))
}); });
const addAction = await EELogService.createActionSecret({
name: ACTION_ADD_SECRETS,
userId,
workspaceId,
secretIds: newSecrets.map((n) => n._id)
});
addAction && actions.push(addAction);
} }
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId workspaceId
}) })
// (EE) create (audit) log
if (actions.length > 0) {
await EELogService.createLog({
userId,
workspaceId,
actions,
channel,
ipAddress
});
}
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
@@ -518,15 +510,14 @@ 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
* @param {String} obj.workspaceId - id of workspace to pull from * @param {String} obj.workspaceId - id of workspace to pull from
* @param {String} obj.environment - environment for secrets * @param {String} obj.environment - environment for secrets
*
*/ */
const pullSecrets = async ({ const getSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment environment
@@ -563,9 +554,64 @@ const pullSecrets = async ({
return secrets; return secrets;
}; };
/**
* Pull secrets for user with id [userId] for workspace
* with id [workspaceId] with environment [environment]
* @param {Object} obj
* @param {String} obj.userId -id of user to pull secrets for
* @param {String} obj.workspaceId - id of workspace to pull from
* @param {String} obj.environment - environment for secrets
* @param {String} obj.channel - channel (web/cli/auto)
* @param {String} obj.ipAddress - ip address of request to push secrets
*/
const pullSecrets = async ({
userId,
workspaceId,
environment,
channel,
ipAddress
}: {
userId: string;
workspaceId: string;
environment: string;
channel: string;
ipAddress: string;
}): Promise<ISecret[]> => {
let secrets: any;
try {
secrets = await getSecrets({
userId,
workspaceId,
environment
})
const readAction = await EELogService.createActionSecret({
name: ACTION_READ_SECRETS,
userId,
workspaceId,
secretIds: secrets.map((n: any) => n._id)
});
readAction && await EELogService.createLog({
userId,
workspaceId,
actions: [readAction],
channel,
ipAddress
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to pull shared and personal secrets');
}
return secrets;
};
/** /**
* Reformat output of pullSecrets() to be compatible with how existing * Reformat output of pullSecrets() to be compatible with how existing
* clients handle secrets * web client handle secrets
* @param {Object} obj * @param {Object} obj
* @param {Object} obj.secrets * @param {Object} obj.secrets
*/ */
+2 -2
View File
@@ -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);
+3 -2
View File
@@ -11,7 +11,7 @@ export interface IUser {
tag?: string; tag?: string;
salt?: string; salt?: string;
verifier?: string; verifier?: string;
refreshVersion?: Number; refreshVersion?: number;
} }
const userSchema = new Schema<IUser>( const userSchema = new Schema<IUser>(
@@ -52,7 +52,8 @@ const userSchema = new Schema<IUser>(
}, },
refreshVersion: { refreshVersion: {
type: Number, type: Number,
default: 0 default: 0,
select: false
} }
}, },
{ {
+1
View File
@@ -4,6 +4,7 @@ import { requireAuth, validateRequest } from '../../middleware';
import { body, query } from 'express-validator'; import { body, query } from 'express-validator';
import { userActionController } from '../../controllers/v1'; import { userActionController } from '../../controllers/v1';
// note: [userAction] will be deprecated in /v2 in favor of [action]
router.post( router.post(
'/', '/',
requireAuth({ requireAuth({
+1 -1
View File
@@ -2,7 +2,7 @@ import express, { Request, Response } from 'express';
import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware'; import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware';
import { body, param, query } from 'express-validator'; import { body, param, query } from 'express-validator';
import { ADMIN, MEMBER } from '../../variables'; import { ADMIN, MEMBER } from '../../variables';
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret/types'; import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
import { secretController } from '../../controllers/v2'; import { secretController } from '../../controllers/v2';
import { fetchAllSecrets } from '../../controllers/v2/secretController'; import { fetchAllSecrets } from '../../controllers/v2/secretController';
+16
View File
@@ -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;
-10
View File
@@ -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;
};
+2
View File
@@ -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,
+1
View File
@@ -13,6 +13,7 @@ declare global {
integrationAuth: any; integrationAuth: any;
bot: any; bot: any;
secret: any; secret: any;
secretSnapshot: any;
serviceToken: any; serviceToken: any;
accessToken: any; accessToken: any;
serviceTokenData: any; serviceTokenData: any;
+10
View File
@@ -123,6 +123,16 @@ export const SecretNotFoundError = (error?: Partial<RequestErrorContext>) => new
stack: error?.stack stack: error?.stack
}); });
//* ----->[SECRET SNAPSHOT ERRORS]<-----
export const SecretSnapshotNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 404,
type: error?.type ?? 'secret_snapshot_not_found_error',
message: error?.message ?? 'The requested secret snapshot was not found',
context: error?.context,
stack: error?.stack
});
//* ----->[ACTION ERRORS]<----- //* ----->[ACTION ERRORS]<-----
export const ActionNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({ export const ActionNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR, logLevel: error?.logLevel ?? LogLevel.ERROR,
+11
View File
@@ -0,0 +1,11 @@
const ACTION_ADD_SECRETS = 'addSecrets';
const ACTION_DELETE_SECRETS = 'deleteSecrets';
const ACTION_UPDATE_SECRETS = 'updateSecrets';
const ACTION_READ_SECRETS = 'readSecrets';
export {
ACTION_ADD_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS
}
+10
View File
@@ -31,6 +31,12 @@ import {
} from './organization'; } from './organization';
import { SECRET_SHARED, SECRET_PERSONAL } from './secret'; import { SECRET_SHARED, SECRET_PERSONAL } from './secret';
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event'; import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event';
import {
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS
} from './action';
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp'; import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp';
import { PLAN_STARTER, PLAN_PRO } from './stripe'; import { PLAN_STARTER, PLAN_PRO } from './stripe';
@@ -63,6 +69,10 @@ export {
INTEGRATION_GITHUB_API_URL, INTEGRATION_GITHUB_API_URL,
EVENT_PUSH_SECRETS, EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS, EVENT_PULL_SECRETS,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS,
INTEGRATION_OPTIONS, INTEGRATION_OPTIONS,
SMTP_HOST_SENDGRID, SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN, SMTP_HOST_MAILGUN,
+3 -3
View File
@@ -48,7 +48,7 @@ const INTEGRATION_OPTIONS = [
name: 'Vercel', name: 'Vercel',
slug: 'vercel', slug: 'vercel',
image: 'Vercel', image: 'Vercel',
isAvailable: true, isAvailable: false,
type: 'vercel', type: 'vercel',
clientId: '', clientId: '',
clientSlug: CLIENT_SLUG_VERCEL, clientSlug: CLIENT_SLUG_VERCEL,
@@ -58,7 +58,7 @@ const INTEGRATION_OPTIONS = [
name: 'Netlify', name: 'Netlify',
slug: 'netlify', slug: 'netlify',
image: 'Netlify', image: 'Netlify',
isAvailable: true, isAvailable: false,
type: 'oauth2', type: 'oauth2',
clientId: CLIENT_ID_NETLIFY, clientId: CLIENT_ID_NETLIFY,
docsLink: '' docsLink: ''
@@ -67,7 +67,7 @@ const INTEGRATION_OPTIONS = [
name: 'GitHub', name: 'GitHub',
slug: 'github', slug: 'github',
image: 'GitHub', image: 'GitHub',
isAvailable: true, isAvailable: false,
type: 'oauth2', type: 'oauth2',
clientId: CLIENT_ID_GITHUB, clientId: CLIENT_ID_GITHUB,
docsLink: '' docsLink: ''
+3 -2
View File
@@ -13,7 +13,6 @@ require (
require ( require (
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/Luzifer/go-openssl/v4 v4.1.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
github.com/chzyer/readline v1.5.1 // indirect github.com/chzyer/readline v1.5.1 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect github.com/danieljoos/wincred v1.1.2 // indirect
@@ -22,6 +21,8 @@ require (
github.com/go-openapi/strfmt v0.21.3 // indirect github.com/go-openapi/strfmt v0.21.3 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/mapstructure v1.3.3 // indirect github.com/mitchellh/mapstructure v1.3.3 // indirect
github.com/mtibben/percent v0.2.1 // indirect github.com/mtibben/percent v0.2.1 // indirect
@@ -35,7 +36,7 @@ require (
) )
require ( require (
github.com/Luzifer/go-openssl v2.0.0+incompatible github.com/fatih/color v1.13.0
github.com/go-resty/resty/v2 v2.7.0 github.com/go-resty/resty/v2 v2.7.0
github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/jedib0t/go-pretty v4.3.0+incompatible
+10 -10
View File
@@ -2,10 +2,6 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMb
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk=
github.com/Luzifer/go-openssl v2.0.0+incompatible h1:EpNNxrPDji4rRzE0KeOeIeV7pHyKe8zF9oNnAXy4mBY=
github.com/Luzifer/go-openssl v2.0.0+incompatible/go.mod h1:t2qnLjT8WQ3usGU1R8uAqjY4T7CK7eMg9vhQ3l9Ue/Y=
github.com/Luzifer/go-openssl/v4 v4.1.0 h1:8qi3Z6f8Aflwub/Cs4FVSmKUEg/lC8GlODbR2TyZ+nM=
github.com/Luzifer/go-openssl/v4 v4.1.0/go.mod h1:3i1T3Pe6eQK19d86WhuQzjLyMwBaNmGmt3ZceWpWVa4=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -26,6 +22,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM=
github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8= github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8=
github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o=
@@ -53,6 +51,11 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8= github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
@@ -100,23 +103,21 @@ github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgk
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg= go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg=
go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -125,7 +126,6 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+136
View File
@@ -0,0 +1,136 @@
package api
import (
"fmt"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/go-resty/resty/v2"
)
func CallBatchModifySecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchModifySecretsByWorkspaceAndEnvRequest) error {
endpoint := fmt.Sprintf("%v/v2/secret/batch-modify/workspace/%v/environment/%v", config.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName)
response, err := httpClient.
R().
SetBody(request).
Patch(endpoint)
if err != nil {
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallBatchCreateSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchCreateSecretsByWorkspaceAndEnvRequest) error {
endpoint := fmt.Sprintf("%v/v2/secret/batch-create/workspace/%v/environment/%v", config.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName)
response, err := httpClient.
R().
SetBody(request).
Post(endpoint)
if err != nil {
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchDeleteSecretsBySecretIdsRequest) error {
endpoint := fmt.Sprintf("%v/v2/secret/batch/workspace/%v/environment/%v", config.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName)
response, err := httpClient.
R().
SetBody(request).
Delete(endpoint)
if err != nil {
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncryptedWorkspaceKeyRequest) (GetEncryptedWorkspaceKeyResponse, error) {
endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", config.INFISICAL_URL, request.WorkspaceId)
var result GetEncryptedWorkspaceKeyResponse
response, err := httpClient.
R().
SetResult(&result).
Get(endpoint)
if err != nil {
return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response: [response=%s]", response)
}
return result, nil
}
func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDetailsResponse, error) {
var tokenDetailsResponse GetServiceTokenDetailsResponse
response, err := httpClient.
R().
SetResult(&tokenDetailsResponse).
Get(fmt.Sprintf("%v/v2/service-token", config.INFISICAL_URL))
if err != nil {
return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unsuccessful response: [response=%s]", response)
}
return tokenDetailsResponse, nil
}
func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Request) (GetEncryptedSecretsV2Response, error) {
var secretsResponse GetEncryptedSecretsV2Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetQueryParam("environment", request.EnvironmentName).
Get(fmt.Sprintf("%v/v2/secret/workspace/%v", config.INFISICAL_URL, request.WorkspaceId))
if err != nil {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unsuccessful response: [response=%s]", response)
}
return secretsResponse, nil
}
func CallGetAllWorkSpacesUserBelongsTo(httpClient *resty.Client) (GetWorkSpacesResponse, error) {
var workSpacesResponse GetWorkSpacesResponse
response, err := httpClient.
R().
SetResult(&workSpacesResponse).
Get(fmt.Sprintf("%v/v1/workspace", config.INFISICAL_URL))
if err != nil {
return GetWorkSpacesResponse{}, err
}
if response.StatusCode() > 299 {
return GetWorkSpacesResponse{}, fmt.Errorf("CallGetAllWorkSpacesUserBelongsTo: Unsuccessful response: [response=%v]", response)
}
return workSpacesResponse, nil
}
@@ -1,4 +1,4 @@
package models package api
import "time" import "time"
@@ -119,14 +119,13 @@ type PullSecretsByInfisicalTokenResponse struct {
} }
type GetWorkSpacesResponse struct { type GetWorkSpacesResponse struct {
Workspaces []Workspace `json:"workspaces"` Workspaces []struct {
} ID string `json:"_id"`
type Workspace struct { Name string `json:"name"`
ID string `json:"_id"` Plan string `json:"plan,omitempty"`
Name string `json:"name"` V int `json:"__v"`
Plan string `json:"plan,omitempty"` Organization string `json:"organization,omitempty"`
V int `json:"__v"` } `json:"workspaces"`
Organization string `json:"organization,omitempty"`
} }
type Secret struct { type Secret struct {
@@ -169,30 +168,68 @@ type GetEncryptedWorkspaceKeyRequest struct {
} }
type GetEncryptedWorkspaceKeyResponse struct { type GetEncryptedWorkspaceKeyResponse struct {
LatestKey struct { ID string `json:"_id"`
ID string `json:"_id"` EncryptedKey string `json:"encryptedKey"`
EncryptedKey string `json:"encryptedKey"` Nonce string `json:"nonce"`
Nonce string `json:"nonce"` Sender struct {
Sender struct { ID string `json:"_id"`
ID string `json:"_id"` Email string `json:"email"`
Email string `json:"email"` RefreshVersion int `json:"refreshVersion"`
RefreshVersion int `json:"refreshVersion"` CreatedAt time.Time `json:"createdAt"`
CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"`
UpdatedAt time.Time `json:"updatedAt"` V int `json:"__v"`
V int `json:"__v"` FirstName string `json:"firstName"`
FirstName string `json:"firstName"` LastName string `json:"lastName"`
LastName string `json:"lastName"` PublicKey string `json:"publicKey"`
PublicKey string `json:"publicKey"` } `json:"sender"`
} `json:"sender"` Receiver string `json:"receiver"`
Receiver string `json:"receiver"` Workspace string `json:"workspace"`
Workspace string `json:"workspace"` V int `json:"__v"`
V int `json:"__v"` CreatedAt time.Time `json:"createdAt"`
CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"latestKey"`
} }
type GetSecretsByWorkspaceIdAndEnvironmentRequest struct { type GetSecretsByWorkspaceIdAndEnvironmentRequest struct {
EnvironmentName string `json:"environmentName"` EnvironmentName string `json:"environmentName"`
WorkspaceId string `json:"workspaceId"` WorkspaceId string `json:"workspaceId"`
} }
type GetEncryptedSecretsV2Request struct {
EnvironmentName string `json:"environmentName"`
WorkspaceId string `json:"workspaceId"`
}
type GetEncryptedSecretsV2Response []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretKeyHash string `json:"secretKeyHash"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretValueHash string `json:"secretValueHash"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
SecretCommentHash string `json:"secretCommentHash"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
User string `json:"user,omitempty"`
}
type GetServiceTokenDetailsResponse struct {
ID string `json:"_id"`
Name string `json:"name"`
Workspace string `json:"workspace"`
Environment string `json:"environment"`
User string `json:"user"`
EncryptedKey string `json:"encryptedKey"`
Iv string `json:"iv"`
Tag string `json:"tag"`
}
+15 -28
View File
@@ -29,58 +29,46 @@ var exportCmd = &cobra.Command{
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
Example: "infisical export --env=prod --format=json > secrets.json", Example: "infisical export --env=prod --format=json > secrets.json",
Args: cobra.NoArgs, Args: cobra.NoArgs,
PreRun: toggleDebug, PreRun: func(cmd *cobra.Command, args []string) {
toggleDebug(cmd, args)
util.RequireLogin()
util.RequireLocalWorkspaceFile()
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
envName, err := cmd.Flags().GetString("env") envName, err := cmd.Flags().GetString("env")
if err != nil { if err != nil {
log.Errorln("Unable to parse the environment flag") util.HandleError(err)
log.Debugln(err)
return
} }
shouldExpandSecrets, err := cmd.Flags().GetBool("expand") shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
if err != nil { if err != nil {
log.Errorln("Unable to parse the substitute flag") util.HandleError(err)
log.Debugln(err)
return
}
projectId, err := cmd.Flags().GetString("projectId")
if err != nil {
log.Errorln("Unable to parse the project id flag")
log.Debugln(err)
return
} }
format, err := cmd.Flags().GetString("format") format, err := cmd.Flags().GetString("format")
if err != nil { if err != nil {
log.Errorln("Unable to parse the format flag") util.HandleError(err)
log.Debugln(err)
return
} }
envsFromApi, err := util.GetAllEnvironmentVariables(projectId, envName) secrets, err := util.GetAllEnvironmentVariables(envName)
if err != nil { if err != nil {
log.Errorln("Something went wrong when pulling secrets using your Infisical token. Double check the token, project id or environment name (dev, prod, ect.)") util.HandleError(err, "Unable to fetch secrets")
log.Debugln(err)
return
} }
var output string var output string
if shouldExpandSecrets { if shouldExpandSecrets {
substitutions := util.SubstituteSecrets(envsFromApi) substitutions := util.SubstituteSecrets(secrets)
output, err = formatEnvs(substitutions, format) output, err = formatEnvs(substitutions, format)
if err != nil { if err != nil {
log.Errorln(err) util.HandleError(err)
return
} }
} else { } else {
output, err = formatEnvs(envsFromApi, format) output, err = formatEnvs(secrets, format)
if err != nil { if err != nil {
log.Errorln(err) util.HandleError(err)
return
} }
} }
fmt.Print(output) fmt.Print(output)
}, },
} }
@@ -88,7 +76,6 @@ var exportCmd = &cobra.Command{
func init() { func init() {
rootCmd.AddCommand(exportCmd) rootCmd.AddCommand(exportCmd)
exportCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") exportCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
exportCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from")
exportCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") exportCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)") exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)")
} }
+17 -30
View File
@@ -5,10 +5,13 @@ package cmd
import ( import (
"encoding/json" "encoding/json"
"fmt"
"os" "os"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/util"
"github.com/go-resty/resty/v2"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -21,21 +24,10 @@ var initCmd = &cobra.Command{
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
Example: "infisical init", Example: "infisical init",
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
PreRun: toggleDebug, PreRun: func(cmd *cobra.Command, args []string) {
util.RequireLogin()
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// check if user is logged
hasUserLoggedInbefore, loggedInUserEmail, err := util.IsUserLoggedIn()
if err != nil {
log.Info("Unexpected issue occurred while checking login status. To see more details, add flag --debug")
log.Debugln(err)
return
}
if !hasUserLoggedInbefore {
log.Infoln("No logged in user. To login, please run command [infisical login]")
return
}
if util.WorkspaceConfigFileExistsInCurrentPath() { if util.WorkspaceConfigFileExistsInCurrentPath() {
shouldOverride, err := shouldOverrideWorkspacePrompt() shouldOverride, err := shouldOverrideWorkspacePrompt()
if err != nil { if err != nil {
@@ -49,23 +41,22 @@ var initCmd = &cobra.Command{
} }
} }
userCreds, err := util.GetUserCredsFromKeyRing(loggedInUserEmail) userCreds, err := util.GetCurrentLoggedInUserDetails()
if err != nil { if err != nil {
log.Infoln("Unable to get user creds from key ring") util.HandleError(err, "Unable to get your login details")
log.Debug(err)
return
} }
workspaces, err := util.GetWorkSpacesFromAPI(userCreds) httpClient := resty.New()
httpClient.SetAuthToken(userCreds.UserCredentials.JTWToken)
workspaceResponse, err := api.CallGetAllWorkSpacesUserBelongsTo(httpClient)
if err != nil { if err != nil {
log.Errorln("Unable to pull your projects. To see more logs add the --debug flag to this command") util.HandleError(err, "Unable to pull projects that belong to you")
log.Debugln("Unable to get your projects because:", err)
return
} }
workspaces := workspaceResponse.Workspaces
if len(workspaces) == 0 { if len(workspaces) == 0 {
log.Infoln("You don't have any projects created in Infisical. You must first create a project at https://infisical.com") message := fmt.Sprintf("You don't have any projects created in Infisical. You must first create a project at %s", util.INFISICAL_TOKEN_NAME)
return util.PrintMessageAndExit(message)
} }
var workspaceNames []string var workspaceNames []string
@@ -81,16 +72,12 @@ var initCmd = &cobra.Command{
index, _, err := prompt.Run() index, _, err := prompt.Run()
if err != nil { if err != nil {
log.Errorln("Unable to parse your response") util.HandleError(err)
log.Debug(err)
return
} }
err = writeWorkspaceFile(workspaces[index]) err = writeWorkspaceFile(workspaces[index])
if err != nil { if err != nil {
log.Errorln("Something went wrong when creating your workspace file") util.HandleError(err)
log.Debug("Error while writing your workspace file:", err)
return
} }
}, },
} }
+22 -30
View File
@@ -11,6 +11,9 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/Infisical/infisical-merge/packages/crypto"
"github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/srp" "github.com/Infisical/infisical-merge/packages/srp"
"github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/util"
@@ -27,18 +30,15 @@ var loginCmd = &cobra.Command{
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
PreRun: toggleDebug, PreRun: toggleDebug,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
hasUserLoggedInbefore, currentLoggedInUserEmail, err := util.IsUserLoggedIn() currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
if err != nil { if err != nil {
log.Debugln("Unable to get current logged in user.", err) util.HandleError(err)
} }
if hasUserLoggedInbefore { if currentLoggedInUserDetails.IsUserLoggedIn {
shouldOverride, err := shouldOverrideLoginPrompt(currentLoggedInUserEmail) shouldOverride, err := shouldOverrideLoginPrompt(currentLoggedInUserDetails.UserCredentials.Email)
if err != nil { if err != nil {
log.Errorln("Unable to parse your answer") util.HandleError(err)
log.Debug(err)
return
} }
if !shouldOverride { if !shouldOverride {
@@ -48,14 +48,12 @@ var loginCmd = &cobra.Command{
email, password, err := askForLoginCredentials() email, password, err := askForLoginCredentials()
if err != nil { if err != nil {
log.Errorln("Unable to parse email and password for authentication") util.HandleError(err, "Unable to parse email and password for authentication")
log.Debugln(err)
return
} }
userCredentials, err := getFreshUserCredentials(email, password) userCredentials, err := getFreshUserCredentials(email, password)
if err != nil { if err != nil {
log.Errorln("Unable to authenticate with the provided credentials, please try again") log.Infoln("Unable to authenticate with the provided credentials, please try again")
log.Debugln(err) log.Debugln(err)
return return
} }
@@ -63,24 +61,20 @@ var loginCmd = &cobra.Command{
encryptedPrivateKey, _ := base64.StdEncoding.DecodeString(userCredentials.EncryptedPrivateKey) encryptedPrivateKey, _ := base64.StdEncoding.DecodeString(userCredentials.EncryptedPrivateKey)
tag, err := base64.StdEncoding.DecodeString(userCredentials.Tag) tag, err := base64.StdEncoding.DecodeString(userCredentials.Tag)
if err != nil { if err != nil {
log.Errorln("Unable to decode the auth tag") util.HandleError(err)
log.Debugln(err)
} }
IV, err := base64.StdEncoding.DecodeString(userCredentials.IV) IV, err := base64.StdEncoding.DecodeString(userCredentials.IV)
if err != nil { if err != nil {
log.Errorln("Unable to decode the IV/Nonce") util.HandleError(err)
log.Debugln(err)
} }
paddedPassword := fmt.Sprintf("%032s", password) paddedPassword := fmt.Sprintf("%032s", password)
key := []byte(paddedPassword) key := []byte(paddedPassword)
decryptedPrivateKey, err := util.DecryptSymmetric(key, encryptedPrivateKey, tag, IV) decryptedPrivateKey, err := crypto.DecryptSymmetric(key, encryptedPrivateKey, tag, IV)
if err != nil || len(decryptedPrivateKey) == 0 { if err != nil || len(decryptedPrivateKey) == 0 {
log.Errorln("There was an issue decrypting your keys") util.HandleError(err)
log.Debugln(err)
return
} }
userCredentialsToBeStored := &models.UserCredentials{ userCredentialsToBeStored := &models.UserCredentials{
@@ -100,9 +94,7 @@ var loginCmd = &cobra.Command{
err = util.WriteInitalConfig(userCredentialsToBeStored) err = util.WriteInitalConfig(userCredentialsToBeStored)
if err != nil { if err != nil {
log.Errorln("Unable to write write to Infisical Config file. Please try again") util.HandleError(err, "Unable to write write to Infisical Config file. Please try again")
log.Debugln(err)
return
} }
log.Infoln("Nice! You are loggin as:", email) log.Infoln("Nice! You are loggin as:", email)
@@ -156,7 +148,7 @@ func askForLoginCredentials() (email string, password string, err error) {
return userEmail, userPassword, nil return userEmail, userPassword, nil
} }
func getFreshUserCredentials(email string, password string) (*models.LoginTwoResponse, error) { func getFreshUserCredentials(email string, password string) (*api.LoginTwoResponse, error) {
log.Debugln("getFreshUserCredentials:", "email", email, "password", password) log.Debugln("getFreshUserCredentials:", "email", email, "password", password)
httpClient := resty.New() httpClient := resty.New()
httpClient.SetRetryCount(5) httpClient.SetRetryCount(5)
@@ -167,18 +159,18 @@ func getFreshUserCredentials(email string, password string) (*models.LoginTwoRes
srpA := hex.EncodeToString(srpClient.ComputeA()) srpA := hex.EncodeToString(srpClient.ComputeA())
// ** Login one // ** Login one
loginOneRequest := models.LoginOneRequest{ loginOneRequest := api.LoginOneRequest{
Email: email, Email: email,
ClientPublicKey: srpA, ClientPublicKey: srpA,
} }
var loginOneResponseResult models.LoginOneResponse var loginOneResponseResult api.LoginOneResponse
loginOneResponse, err := httpClient. loginOneResponse, err := httpClient.
R(). R().
SetBody(loginOneRequest). SetBody(loginOneRequest).
SetResult(&loginOneResponseResult). SetResult(&loginOneResponseResult).
Post(fmt.Sprintf("%v/v1/auth/login1", util.INFISICAL_URL)) Post(fmt.Sprintf("%v/v1/auth/login1", config.INFISICAL_URL))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -204,17 +196,17 @@ func getFreshUserCredentials(email string, password string) (*models.LoginTwoRes
srpM1 := srpClient.ComputeM1() srpM1 := srpClient.ComputeM1()
LoginTwoRequest := models.LoginTwoRequest{ LoginTwoRequest := api.LoginTwoRequest{
Email: email, Email: email,
ClientProof: hex.EncodeToString(srpM1), ClientProof: hex.EncodeToString(srpM1),
} }
var loginTwoResponseResult models.LoginTwoResponse var loginTwoResponseResult api.LoginTwoResponse
loginTwoResponse, err := httpClient. loginTwoResponse, err := httpClient.
R(). R().
SetBody(LoginTwoRequest). SetBody(LoginTwoRequest).
SetResult(&loginTwoResponseResult). SetResult(&loginTwoResponseResult).
Post(fmt.Sprintf("%v/v1/auth/login2", util.INFISICAL_URL)) Post(fmt.Sprintf("%v/v1/auth/login2", config.INFISICAL_URL))
if err != nil { if err != nil {
return nil, err return nil, err
+3 -3
View File
@@ -6,7 +6,7 @@ package cmd
import ( import (
"os" "os"
"github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/config"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{
Short: "Infisical CLI is used to inject environment variables into any process", Short: "Infisical CLI is used to inject environment variables into any process",
Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`, Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`,
CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true}, CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true},
Version: "0.1.16", Version: "0.2.0",
} }
// Execute adds all child commands to the root command and sets flags appropriately. // Execute adds all child commands to the root command and sets flags appropriately.
@@ -30,7 +30,7 @@ func Execute() {
func init() { func init() {
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
rootCmd.PersistentFlags().BoolVarP(&debugLogging, "debug", "d", false, "Enable verbose logging") rootCmd.PersistentFlags().BoolVarP(&debugLogging, "debug", "d", false, "Enable verbose logging")
rootCmd.PersistentFlags().StringVar(&util.INFISICAL_URL, "domain", "https://app.infisical.com/api", "Point the CLI to your own backend") rootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", "https://app.infisical.com/api", "Point the CLI to your own backend")
// rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { // rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
// } // }
} }
+16 -29
View File
@@ -55,36 +55,26 @@ var runCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
envName, err := cmd.Flags().GetString("env") envName, err := cmd.Flags().GetString("env")
if err != nil { if err != nil {
log.Errorln("Unable to parse the environment flag") util.HandleError(err, "Unable to parse flag")
log.Debugln(err) }
return
if !util.IsSecretEnvironmentValid(envName) {
util.PrintMessageAndExit("Invalid environment name passed. Environment names can only be prod, dev, test or staging")
} }
secretOverriding, err := cmd.Flags().GetBool("secret-overriding") secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
if err != nil { if err != nil {
log.Errorln("Unable to parse the secret-overriding flag") util.HandleError(err, "Unable to parse flag")
log.Debugln(err)
return
} }
shouldExpandSecrets, err := cmd.Flags().GetBool("expand") shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
if err != nil { if err != nil {
log.Errorln("Unable to parse the substitute flag") util.HandleError(err, "Unable to parse flag")
log.Debugln(err)
return
} }
projectId, err := cmd.Flags().GetString("projectId") secrets, err := util.GetAllEnvironmentVariables(envName)
if err != nil { if err != nil {
log.Errorln("Unable to parse the project id flag") util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
log.Debugln(err)
return
}
secrets, err := util.GetAllEnvironmentVariables(projectId, envName)
if err != nil {
log.Debugln(err)
return
} }
if shouldExpandSecrets { if shouldExpandSecrets {
@@ -97,29 +87,26 @@ var runCmd = &cobra.Command{
if cmd.Flags().Changed("command") { if cmd.Flags().Changed("command") {
command := cmd.Flag("command").Value.String() command := cmd.Flag("command").Value.String()
err = executeMultipleCommandWithEnvs(command, secrets) err = executeMultipleCommandWithEnvs(command, secrets)
if err != nil { if err != nil {
log.Errorf("Something went wrong when executing your command [error=%s]", err) util.HandleError(err, "Unable to execute your chained command")
return
} }
} else { } else {
err = executeSingleCommandWithEnvs(args, secrets) err = executeSingleCommandWithEnvs(args, secrets)
if err != nil { if err != nil {
log.Errorf("Something went wrong when executing your command [error=%s]", err) util.HandleError(err, "Unable to execute your single command")
return
} }
return
} }
}, },
} }
func init() { func init() {
rootCmd.AddCommand(runCmd) rootCmd.AddCommand(runCmd)
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
runCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from")
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets with the same name over shared secrets") runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
} }
@@ -130,7 +117,7 @@ func executeSingleCommandWithEnvs(args []string, secrets []models.SingleEnvironm
numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets)) numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets))
log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected) log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected)
log.Debugf("executing command: %s %s \n", command, strings.Join(argsForCommand, " ")) log.Debugf("executing command: %s %s \n", command, strings.Join(argsForCommand, " "))
log.Debugln("Secrets injected:", secrets) log.Debugf("Secrets injected: %v", secrets)
cmd := exec.Command(command, argsForCommand...) cmd := exec.Command(command, argsForCommand...)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
@@ -158,7 +145,7 @@ func executeMultipleCommandWithEnvs(fullCommand string, secrets []models.SingleE
numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets)) numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets))
log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected) log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected)
log.Debugf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand) log.Debugf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand)
log.Debugln("Secrets injected:", secrets) log.Debugf("Secrets injected: %v", secrets)
return execCmd(cmd) return execCmd(cmd)
} }
+54 -123
View File
@@ -11,7 +11,8 @@ import (
"crypto/sha256" "crypto/sha256"
"github.com/Infisical/infisical-merge/packages/http" "github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/crypto"
"github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/util"
"github.com/Infisical/infisical-merge/packages/visualize" "github.com/Infisical/infisical-merge/packages/visualize"
@@ -30,35 +31,23 @@ var secretsCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env") environmentName, err := cmd.Flags().GetString("env")
if err != nil { if err != nil {
log.Errorln("Unable to parse the environment name flag") util.HandleError(err)
log.Debugln(err)
return
} }
shouldExpandSecrets, err := cmd.Flags().GetBool("expand") shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
if err != nil { if err != nil {
log.Errorln("Unable to parse the substitute flag") util.HandleError(err)
log.Debugln(err)
return
} }
workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() secrets, err := util.GetAllEnvironmentVariables(environmentName)
if !workspaceFileExists { if err != nil {
log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") util.HandleError(err)
return
} }
secrets, err := util.GetAllEnvironmentVariables("", environmentName)
if shouldExpandSecrets { if shouldExpandSecrets {
secrets = util.SubstituteSecrets(secrets) secrets = util.SubstituteSecrets(secrets)
} }
if err != nil {
log.Debugln(err)
return
}
visualize.PrintAllSecretDetails(secrets) visualize.PrintAllSecretDetails(secrets)
}, },
} }
@@ -81,85 +70,50 @@ var secretsSetCmd = &cobra.Command{
PreRun: toggleDebug, PreRun: toggleDebug,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// secretType, err := cmd.Flags().GetString("type")
// if err != nil {
// log.Errorln("Unable to parse the secret type flag")
// log.Debugln(err)
// return
// }
// if !util.IsSecretTypeValid(secretType) {
// log.Errorf("secret type can only be `personal` or `shared`. You have entered [%v]", secretType)
// return
// }
environmentName, err := cmd.Flags().GetString("env") environmentName, err := cmd.Flags().GetString("env")
if err != nil { if err != nil {
log.Errorln("Unable to parse the environment name flag") util.HandleError(err, "Unable to parse flag")
log.Debugln(err)
return
} }
if !util.IsSecretEnvironmentValid(environmentName) { if !util.IsSecretEnvironmentValid(environmentName) {
log.Errorln("You have entered a invalid environment name. Environment names can only be prod, dev, test or staging") util.PrintMessageAndExit("You have entered a invalid environment name", "Environment names can only be prod, dev, test or staging")
return
}
workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath()
if !workspaceFileExists {
log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]")
return
} }
workspaceFile, err := util.GetWorkSpaceFromFile() workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil { if err != nil {
log.Error(err) util.HandleError(err, "Unable to get your local config details")
return
} }
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
if err != nil { if err != nil {
log.Error(err) util.HandleError(err, "Unable to authenticate")
return
}
if !loggedInUserDetails.IsUserLoggedIn {
log.Error("You are not logged in yet. Please run [infisical login] then try again")
return
}
if loggedInUserDetails.IsUserLoggedIn && loggedInUserDetails.LoginExpired {
log.Error("Your login has expired. Please run [infisical login] then try again")
return
} }
httpClient := resty.New(). httpClient := resty.New().
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
request := models.GetEncryptedWorkspaceKeyRequest{ request := api.GetEncryptedWorkspaceKeyRequest{
WorkspaceId: workspaceFile.WorkspaceId, WorkspaceId: workspaceFile.WorkspaceId,
} }
workspaceKeyResponse, err := http.CallGetEncryptedWorkspaceKey(httpClient, request) workspaceKeyResponse, err := api.CallGetEncryptedWorkspaceKey(httpClient, request)
if err != nil { if err != nil {
log.Errorf("unable to get your encrypted workspace key. [err=%v]", err) util.HandleError(err, "unable to get your encrypted workspace key")
return
} }
encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.EncryptedKey) encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey)
encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.Sender.PublicKey) encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey)
encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.Nonce) encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce)
currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(loggedInUserDetails.UserCredentials.PrivateKey) currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(loggedInUserDetails.UserCredentials.PrivateKey)
// decrypt workspace key // decrypt workspace key
plainTextEncryptionKey := util.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
// pull current secrets // pull current secrets
secrets, err := util.GetAllEnvironmentVariables("", environmentName) secrets, err := util.GetAllEnvironmentVariables(environmentName)
if err != nil { if err != nil {
log.Error("unable to retrieve secrets. Run with -d to see full logs") util.HandleError(err, "unable to retrieve secrets")
log.Debug(err)
} }
type SecretSetOperation struct { type SecretSetOperation struct {
@@ -168,8 +122,8 @@ var secretsSetCmd = &cobra.Command{
SecretOperation string SecretOperation string
} }
secretsToCreate := []models.Secret{} secretsToCreate := []api.Secret{}
secretsToModify := []models.Secret{} secretsToModify := []api.Secret{}
secretOperations := []SecretSetOperation{} secretOperations := []SecretSetOperation{}
secretByKey := getSecretsByKeys(secrets) secretByKey := getSecretsByKeys(secrets)
@@ -177,13 +131,11 @@ var secretsSetCmd = &cobra.Command{
for _, arg := range args { for _, arg := range args {
splitKeyValueFromArg := strings.SplitN(arg, "=", 2) splitKeyValueFromArg := strings.SplitN(arg, "=", 2)
if splitKeyValueFromArg[0] == "" || splitKeyValueFromArg[1] == "" { if splitKeyValueFromArg[0] == "" || splitKeyValueFromArg[1] == "" {
log.Error("ensure that each secret has a none empty key and value. Modify the input and try again") util.PrintMessageAndExit("ensure that each secret has a none empty key and value. Modify the input and try again")
return
} }
if unicode.IsNumber(rune(splitKeyValueFromArg[0][0])) { if unicode.IsNumber(rune(splitKeyValueFromArg[0][0])) {
log.Error("keys of secrets cannot start with a number. Modify the key name(s) and try again") util.PrintMessageAndExit("keys of secrets cannot start with a number. Modify the key name(s) and try again")
return
} }
// Key and value from argument // Key and value from argument
@@ -191,20 +143,20 @@ var secretsSetCmd = &cobra.Command{
value := splitKeyValueFromArg[1] value := splitKeyValueFromArg[1]
hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key)))
encryptedKey, err := util.EncryptSymmetric([]byte(key), []byte(plainTextEncryptionKey)) encryptedKey, err := crypto.EncryptSymmetric([]byte(key), []byte(plainTextEncryptionKey))
if err != nil { if err != nil {
log.Errorf("unable to encrypt your secrets [err=%v]", err) util.HandleError(err, "unable to encrypt your secrets")
} }
hashedValue := fmt.Sprintf("%x", sha256.Sum256([]byte(value))) hashedValue := fmt.Sprintf("%x", sha256.Sum256([]byte(value)))
encryptedValue, err := util.EncryptSymmetric([]byte(value), []byte(plainTextEncryptionKey)) encryptedValue, err := crypto.EncryptSymmetric([]byte(value), []byte(plainTextEncryptionKey))
if err != nil { if err != nil {
log.Errorf("unable to encrypt your secrets [err=%v]", err) util.HandleError(err, "unable to encrypt your secrets")
} }
if existingSecret, ok := secretByKey[key]; ok { if existingSecret, ok := secretByKey[key]; ok {
// case: secret exists in project so it needs to be modified // case: secret exists in project so it needs to be modified
encryptedSecretDetails := models.Secret{ encryptedSecretDetails := api.Secret{
ID: existingSecret.ID, ID: existingSecret.ID,
SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText),
SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce),
@@ -231,7 +183,7 @@ var secretsSetCmd = &cobra.Command{
} else { } else {
// case: secret doesn't exist in project so it needs to be created // case: secret doesn't exist in project so it needs to be created
encryptedSecretDetails := models.Secret{ encryptedSecretDetails := api.Secret{
SecretKeyCiphertext: base64.StdEncoding.EncodeToString(encryptedKey.CipherText), SecretKeyCiphertext: base64.StdEncoding.EncodeToString(encryptedKey.CipherText),
SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce), SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce),
SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag), SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag),
@@ -252,29 +204,29 @@ var secretsSetCmd = &cobra.Command{
} }
if len(secretsToCreate) > 0 { if len(secretsToCreate) > 0 {
batchCreateRequest := models.BatchCreateSecretsByWorkspaceAndEnvRequest{ batchCreateRequest := api.BatchCreateSecretsByWorkspaceAndEnvRequest{
WorkspaceId: workspaceFile.WorkspaceId, WorkspaceId: workspaceFile.WorkspaceId,
EnvironmentName: environmentName, EnvironmentName: environmentName,
Secrets: secretsToCreate, Secrets: secretsToCreate,
} }
err = http.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest) err = api.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest)
if err != nil { if err != nil {
log.Errorf("Unable to process new secret creations because %v", err) util.HandleError(err, "Unable to process new secret creations")
return return
} }
} }
if len(secretsToModify) > 0 { if len(secretsToModify) > 0 {
batchModifyRequest := models.BatchModifySecretsByWorkspaceAndEnvRequest{ batchModifyRequest := api.BatchModifySecretsByWorkspaceAndEnvRequest{
WorkspaceId: workspaceFile.WorkspaceId, WorkspaceId: workspaceFile.WorkspaceId,
EnvironmentName: environmentName, EnvironmentName: environmentName,
Secrets: secretsToModify, Secrets: secretsToModify,
} }
err = http.CallBatchModifySecretsByWorkspaceAndEnv(httpClient, batchModifyRequest) err = api.CallBatchModifySecretsByWorkspaceAndEnv(httpClient, batchModifyRequest)
if err != nil { if err != nil {
log.Errorf("Unable to process the modifications to your secrets because %v", err) util.HandleError(err, "Unable to process the modifications to your secrets")
return return
} }
} }
@@ -307,36 +259,17 @@ var secretsDeleteCmd = &cobra.Command{
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
if err != nil { if err != nil {
log.Error(err) util.HandleError(err, "Unable to authenticate")
return
}
if !loggedInUserDetails.IsUserLoggedIn {
log.Error("You are not logged in yet. Please run [infisical login] then try again")
return
}
if loggedInUserDetails.IsUserLoggedIn && loggedInUserDetails.LoginExpired {
log.Error("Your login has expired. Please run [infisical login] then try again")
return
}
workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath()
if !workspaceFileExists {
log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]")
return
} }
workspaceFile, err := util.GetWorkSpaceFromFile() workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil { if err != nil {
log.Error(err) util.HandleError(err, "Unable to get local project details")
return
} }
secrets, err := util.GetAllEnvironmentVariables("", environmentName) secrets, err := util.GetAllEnvironmentVariables(environmentName)
if err != nil { if err != nil {
log.Error("Unable to retrieve secrets. Run with -d to see full logs") util.HandleError(err, "Unable to fetch secrets")
log.Debug(err)
} }
secretByKey := getSecretsByKeys(secrets) secretByKey := getSecretsByKeys(secrets)
@@ -352,11 +285,11 @@ var secretsDeleteCmd = &cobra.Command{
} }
if len(invalidSecretNamesThatDoNotExist) != 0 { if len(invalidSecretNamesThatDoNotExist) != 0 {
log.Errorf("secret name(s) [%v] does not exist in your project. To see which secrets exist run [infisical secrets]", strings.Join(invalidSecretNamesThatDoNotExist, ", ")) message := fmt.Sprintf("secret name(s) [%v] does not exist in your project. To see which secrets exist run [infisical secrets]", strings.Join(invalidSecretNamesThatDoNotExist, ", "))
return util.PrintMessageAndExit(message)
} }
request := models.BatchDeleteSecretsBySecretIdsRequest{ request := api.BatchDeleteSecretsBySecretIdsRequest{
WorkspaceId: workspaceFile.WorkspaceId, WorkspaceId: workspaceFile.WorkspaceId,
EnvironmentName: environmentName, EnvironmentName: environmentName,
SecretIds: validSecretIdsToDelete, SecretIds: validSecretIdsToDelete,
@@ -366,45 +299,43 @@ var secretsDeleteCmd = &cobra.Command{
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
err = http.CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient, request) err = api.CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient, request)
if err != nil { if err != nil {
log.Errorf("Unable to complete your request because %v", err) util.HandleError(err, "Unable to complete your batch delete request")
return
} }
log.Infof("secret name(s) [%v] have been deleted from your project", strings.Join(args, ", ")) fmt.Printf("secret name(s) [%v] have been deleted from your project \n", strings.Join(args, ", "))
}, },
} }
func init() { func init() {
secretsCmd.AddCommand(secretsGetCmd) secretsCmd.AddCommand(secretsGetCmd)
// secretsSetCmd.Flags().String("type", "shared", "Used to set the type for secrets")
secretsCmd.AddCommand(secretsSetCmd) secretsCmd.AddCommand(secretsSetCmd)
secretsCmd.AddCommand(secretsDeleteCmd) secretsCmd.AddCommand(secretsDeleteCmd)
secretsCmd.PersistentFlags().String("env", "dev", "Used to define the environment name on which actions should be taken on") secretsCmd.PersistentFlags().String("env", "dev", "Used to define the environment name on which actions should be taken on")
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
}
rootCmd.AddCommand(secretsCmd) rootCmd.AddCommand(secretsCmd)
} }
func getSecretsByNames(cmd *cobra.Command, args []string) { func getSecretsByNames(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env") environmentName, err := cmd.Flags().GetString("env")
if err != nil { if err != nil {
log.Errorln("Unable to parse the environment name flag") util.HandleError(err, "Unable to parse flag")
log.Debugln(err)
return
} }
workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath()
if !workspaceFileExists { if !workspaceFileExists {
log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") util.HandleError(err, "Unable to parse flag")
return
} }
secrets, err := util.GetAllEnvironmentVariables("", environmentName) secrets, err := util.GetAllEnvironmentVariables(environmentName)
if err != nil { if err != nil {
log.Error("Unable to retrieve secrets. Run with -d to see full logs") util.HandleError(err, "To fetch all secrets")
log.Debug(err)
} }
requestedSecrets := []models.SingleEnvironmentVariable{} requestedSecrets := []models.SingleEnvironmentVariable{}
+3
View File
@@ -0,0 +1,3 @@
package config
var INFISICAL_URL = "http://localhost:8080/api"
@@ -1,4 +1,4 @@
package util package crypto
import ( import (
"crypto/aes" "crypto/aes"
-102
View File
@@ -1,102 +0,0 @@
package http
import (
"fmt"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/go-resty/resty/v2"
)
func CallBatchModifySecretsByWorkspaceAndEnv(httpClient *resty.Client, request models.BatchModifySecretsByWorkspaceAndEnvRequest) error {
endpoint := fmt.Sprintf("%v/v2/secret/batch-modify/workspace/%v/environment/%v", util.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName)
response, err := httpClient.
R().
SetBody(request).
Patch(endpoint)
if err != nil {
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallBatchCreateSecretsByWorkspaceAndEnv(httpClient *resty.Client, request models.BatchCreateSecretsByWorkspaceAndEnvRequest) error {
endpoint := fmt.Sprintf("%v/v2/secret/batch-create/workspace/%v/environment/%v", util.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName)
response, err := httpClient.
R().
SetBody(request).
Post(endpoint)
if err != nil {
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient *resty.Client, request models.BatchDeleteSecretsBySecretIdsRequest) error {
endpoint := fmt.Sprintf("%v/v2/secret/batch/workspace/%v/environment/%v", util.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName)
response, err := httpClient.
R().
SetBody(request).
Delete(endpoint)
if err != nil {
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request models.GetEncryptedWorkspaceKeyRequest) (models.GetEncryptedWorkspaceKeyResponse, error) {
endpoint := fmt.Sprintf("%v/v1/key/%v/latest", util.INFISICAL_URL, request.WorkspaceId)
var result models.GetEncryptedWorkspaceKeyResponse
response, err := httpClient.
R().
SetResult(&result).
Get(endpoint)
if err != nil {
return models.GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return models.GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response: [response=%s]", response)
}
return result, nil
}
func CallGetEncryptedSecretsByWorkspaceIdAndEnv(httpClient resty.Client, request models.GetSecretsByWorkspaceIdAndEnvironmentRequest) (models.PullSecretsResponse, error) {
var pullSecretsRequestResponse models.PullSecretsResponse
response, err := httpClient.
R().
SetQueryParam("environment", request.EnvironmentName).
SetQueryParam("channel", "cli").
SetResult(&pullSecretsRequestResponse).
Get(fmt.Sprintf("%v/v1/secret/%v", util.INFISICAL_URL, request.WorkspaceId))
if err != nil {
return models.PullSecretsResponse{}, fmt.Errorf("CallGetEncryptedSecretsByWorkspaceIdAndEnv: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return models.PullSecretsResponse{}, fmt.Errorf("CallGetEncryptedSecretsByWorkspaceIdAndEnv: Unsuccessful response: [response=%s]", response)
}
return pullSecretsRequestResponse, nil
}
+8
View File
@@ -21,6 +21,14 @@ type SingleEnvironmentVariable struct {
ID string `json:"_id"` ID string `json:"_id"`
} }
type Workspace struct {
ID string `json:"_id"`
Name string `json:"name"`
Plan string `json:"plan,omitempty"`
V int `json:"__v"`
Organization string `json:"organization,omitempty"`
}
type WorkspaceConfigFile struct { type WorkspaceConfigFile struct {
WorkspaceId string `json:"workspaceId"` WorkspaceId string `json:"workspaceId"`
} }
+1 -12
View File
@@ -5,17 +5,6 @@ import (
"os" "os"
) )
const (
CONFIG_FILE_NAME = "infisical-config.json"
CONFIG_FOLDER_NAME = ".infisical"
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
SECRET_TYPE_PERSONAL = "personal"
SECRET_TYPE_SHARED = "shared"
)
var INFISICAL_URL = "https://app.infisical.com/api"
func GetHomeDir() (string, error) { func GetHomeDir() (string, error) {
directory, err := os.UserHomeDir() directory, err := os.UserHomeDir()
return directory, err return directory, err
@@ -25,7 +14,7 @@ func GetHomeDir() (string, error) {
func WriteToFile(fileName string, dataToWrite []byte, filePerm os.FileMode) error { func WriteToFile(fileName string, dataToWrite []byte, filePerm os.FileMode) error {
err := os.WriteFile(fileName, dataToWrite, filePerm) err := os.WriteFile(fileName, dataToWrite, filePerm)
if err != nil { if err != nil {
return fmt.Errorf("Unable to wrote to file", err) return fmt.Errorf("unable to wrote to file [err=%v]", err)
} }
return nil return nil
+13
View File
@@ -0,0 +1,13 @@
package util
const (
CONFIG_FILE_NAME = "infisical-config.json"
CONFIG_FOLDER_NAME = ".infisical"
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
SECRET_TYPE_PERSONAL = "personal"
SECRET_TYPE_SHARED = "shared"
KEYRING_SERVICE_NAME = "infisical"
PERSONAL_SECRET_TYPE_NAME = "personal"
SHARED_SECRET_TYPE_NAME = "shared"
)
+2 -45
View File
@@ -5,20 +5,17 @@ import (
"fmt" "fmt"
"github.com/99designs/keyring" "github.com/99designs/keyring"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/models"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
) )
const SERVICE_NAME = "infisical"
type LoggedInUserDetails struct { type LoggedInUserDetails struct {
IsUserLoggedIn bool IsUserLoggedIn bool
LoginExpired bool LoginExpired bool
UserCredentials models.UserCredentials UserCredentials models.UserCredentials
} }
// To do: what happens if the user doesn't have a keyring in their system?
func StoreUserCredsInKeyRing(userCred *models.UserCredentials) error { func StoreUserCredsInKeyRing(userCred *models.UserCredentials) error {
userCredMarshalled, err := json.Marshal(userCred) userCredMarshalled, err := json.Marshal(userCred)
if err != nil { if err != nil {
@@ -69,46 +66,6 @@ func GetUserCredsFromKeyRing(userEmail string) (credentials models.UserCredentia
return userCredentials, err return userCredentials, err
} }
func IsUserLoggedIn() (hasUserLoggedIn bool, theUsersEmail string, err error) {
if ConfigFileExists() {
configFile, err := GetConfigFile()
if err != nil {
return false, "", fmt.Errorf("IsUserLoggedIn: unable to get logged in user from config file [err=%s]", err)
}
if configFile.LoggedInUserEmail == "" {
return false, "", nil
}
userCreds, err := GetUserCredsFromKeyRing(configFile.LoggedInUserEmail)
if err != nil {
return false, "", err
}
// check to to see if the JWT is still valid
httpClient := resty.New().
SetAuthToken(userCreds.JTWToken).
SetHeader("Accept", "application/json")
response, err := httpClient.
R().
Post(fmt.Sprintf("%v/v1/auth/checkAuth", INFISICAL_URL))
if err != nil {
return false, "", err
}
if response.StatusCode() > 299 {
log.Infoln("Login expired, please login again.")
return false, "", fmt.Errorf("GetUserCredsFromKeyRing: Login expired, please login again.")
}
return true, configFile.LoggedInUserEmail, nil
} else {
return false, "", nil
}
}
func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
if ConfigFileExists() { if ConfigFileExists() {
configFile, err := GetConfigFile() configFile, err := GetConfigFile()
@@ -132,7 +89,7 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
response, err := httpClient. response, err := httpClient.
R(). R().
Post(fmt.Sprintf("%v/v1/auth/checkAuth", INFISICAL_URL)) Post(fmt.Sprintf("%v/v1/auth/checkAuth", config.INFISICAL_URL))
if err != nil { if err != nil {
return LoggedInUserDetails{}, err return LoggedInUserDetails{}, err
+38
View File
@@ -0,0 +1,38 @@
package util
import (
"fmt"
"os"
"github.com/fatih/color"
)
func HandleError(err error, messages ...string) {
PrintErrorAndExit(1, err, messages...)
}
func PrintErrorAndExit(exitCode int, err error, messages ...string) {
printError(err)
if len(messages) > 0 {
for _, message := range messages {
fmt.Println(message)
}
}
os.Exit(exitCode)
}
func PrintMessageAndExit(messages ...string) {
if len(messages) > 0 {
for _, message := range messages {
fmt.Println(message)
}
}
os.Exit(1)
}
func printError(e error) {
color.Red("Hmm, we ran into an error: %v", e)
}
+100
View File
@@ -0,0 +1,100 @@
package util
import (
"encoding/base64"
"fmt"
"os"
)
type DecodedSymmetricEncryptionDetails = struct {
Cipher []byte
IV []byte
Tag []byte
Key []byte
}
func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) {
cipherx, err := base64.StdEncoding.DecodeString(cipher)
if err != nil {
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode cipher text [err=%v]", err)
}
keyx, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode key [err=%v]", err)
}
IVx, err := base64.StdEncoding.DecodeString(IV)
if err != nil {
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode IV [err=%v]", err)
}
tagx, err := base64.StdEncoding.DecodeString(tag)
if err != nil {
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode tag [err=%v]", err)
}
return DecodedSymmetricEncryptionDetails{
Key: keyx,
Cipher: cipherx,
IV: IVx,
Tag: tagx,
}, nil
}
func IsSecretEnvironmentValid(env string) bool {
if env == "prod" || env == "dev" || env == "test" || env == "staging" {
return true
}
return false
}
func IsSecretTypeValid(s string) bool {
if s == "personal" || s == "shared" {
return true
}
return false
}
func RequireLogin() {
currentUserDetails, err := GetCurrentLoggedInUserDetails()
if err != nil {
HandleError(err, "unable to retrieve your login details")
}
if !currentUserDetails.IsUserLoggedIn {
PrintMessageAndExit("You must be logged in to run this command. To login, run [infisical login]")
}
if currentUserDetails.LoginExpired {
PrintMessageAndExit("Your login expired, please login in again. To login, run [infisical login]")
}
if currentUserDetails.UserCredentials.Email == "" && currentUserDetails.UserCredentials.JTWToken == "" && currentUserDetails.UserCredentials.PrivateKey == "" {
PrintMessageAndExit("One or more of your login details is empty. Please try logging in again via by running [infisical login]")
}
}
func RequireServiceToken() {
serviceToken := os.Getenv(INFISICAL_TOKEN_NAME)
if serviceToken == "" {
PrintMessageAndExit("No service token is found in your terminal")
}
}
func RequireLocalWorkspaceFile() {
workspaceFileExists := WorkspaceConfigFileExistsInCurrentPath()
if !workspaceFileExists {
PrintMessageAndExit("It looks you have not yet connected this project to Infisical", "To do so, run [infisical init] then run your command again")
}
workspaceFile, err := GetWorkSpaceFromFile()
if err != nil {
HandleError(err, "Unable to read your project configuration, please try initializing this project again.", "Run [infisical init]")
}
if workspaceFile.WorkspaceId == "" {
PrintMessageAndExit("Your project id is missing in your local config file. Please add it or run again [infisical init]")
}
}
+136 -257
View File
@@ -2,284 +2,127 @@ package util
import ( import (
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
"strings" "strings"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/crypto"
"github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/models"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/go-resty/resty/v2"
) )
const PERSONAL_SECRET_TYPE_NAME = "personal" func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.SingleEnvironmentVariable, error) {
const SHARED_SECRET_TYPE_NAME = "shared" serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 {
func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, workspace models.WorkspaceConfigFile, userCreds models.UserCredentials) (listOfSecrets []models.SingleEnvironmentVariable, err error) { return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
var pullSecretsRequestResponse models.PullSecretsResponse
response, err := httpClient.
R().
SetQueryParam("environment", envName).
SetQueryParam("channel", "cli").
SetResult(&pullSecretsRequestResponse).
Get(fmt.Sprintf("%v/v1/secret/%v", INFISICAL_URL, workspace.WorkspaceId)) // need to change workspace id
if err != nil {
return nil, err
} }
if response.StatusCode() > 299 { serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
return nil, fmt.Errorf(response.Status())
}
// Get workspace key httpClient := resty.New()
workspaceKey, err := base64.StdEncoding.DecodeString(pullSecretsRequestResponse.Key.EncryptedKey) httpClient.SetAuthToken(serviceToken).
if err != nil {
return nil, err
}
nonce, err := base64.StdEncoding.DecodeString(pullSecretsRequestResponse.Key.Nonce)
if err != nil {
return nil, err
}
senderPublicKey, err := base64.StdEncoding.DecodeString(pullSecretsRequestResponse.Key.Sender.PublicKey)
if err != nil {
return nil, err
}
currentUsersPrivateKey, err := base64.StdEncoding.DecodeString(userCreds.PrivateKey)
if err != nil {
return nil, err
}
// log.Debugln("workspaceKey", workspaceKey, "nonce", nonce, "senderPublicKey", senderPublicKey, "currentUsersPrivateKey", currentUsersPrivateKey)
workspaceKeyInBytes := DecryptAsymmetric(workspaceKey, nonce, senderPublicKey, currentUsersPrivateKey)
var listOfEnv []models.SingleEnvironmentVariable
for _, secret := range pullSecretsRequestResponse.Secrets {
key_iv, _ := base64.StdEncoding.DecodeString(secret.SecretKeyIV)
key_tag, _ := base64.StdEncoding.DecodeString(secret.SecretKeyTag)
key_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretKeyCiphertext)
plainTextKey, err := DecryptSymmetric(workspaceKeyInBytes, key_ciphertext, key_tag, key_iv)
if err != nil {
return nil, err
}
value_iv, _ := base64.StdEncoding.DecodeString(secret.SecretValueIV)
value_tag, _ := base64.StdEncoding.DecodeString(secret.SecretValueTag)
value_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretValueCiphertext)
plainTextValue, err := DecryptSymmetric(workspaceKeyInBytes, value_ciphertext, value_tag, value_iv)
if err != nil {
return nil, err
}
env := models.SingleEnvironmentVariable{
Key: string(plainTextKey),
Value: string(plainTextValue),
Type: string(secret.Type),
ID: secret.ID,
}
listOfEnv = append(listOfEnv, env)
}
return listOfEnv, nil
}
func GetSecretsFromAPIUsingCurrentLoggedInUser(envName string, userCreds models.UserCredentials) ([]models.SingleEnvironmentVariable, error) {
log.Debugln("GetSecretsFromAPIUsingCurrentLoggedInUser", "envName", envName, "userCreds", userCreds)
// check if user has configured a workspace
workspaces, err := GetAllWorkSpaceConfigsStartingFromCurrentPath()
if err != nil {
return nil, fmt.Errorf("Unable to read workspace file(s):", err)
}
// create http client
httpClient := resty.New().
SetAuthToken(userCreds.JTWToken).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
secrets := []models.SingleEnvironmentVariable{} serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
for _, workspace := range workspaces { if err != nil {
secretsFromAPI, err := getSecretsByWorkspaceIdAndEnvName(*httpClient, envName, workspace, userCreds) return nil, fmt.Errorf("unable to get service token details. [err=%v]", err)
if err != nil {
return nil, fmt.Errorf("GetSecretsFromAPIUsingCurrentLoggedInUser: Unable to get secrets by workspace id and env name")
}
secrets = append(secrets, secretsFromAPI...)
} }
return secrets, nil encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
WorkspaceId: serviceTokenDetails.Workspace,
EnvironmentName: serviceTokenDetails.Environment,
})
if err != nil {
return nil, err
}
decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag)
if err != nil {
return nil, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err)
}
plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV)
if err != nil {
return nil, fmt.Errorf("unable to decrypt the required workspace key")
}
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets)
if err != nil {
return nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
}
return plainTextSecrets, nil
} }
func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, projectId string) ([]models.SingleEnvironmentVariable, error) { func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string) ([]models.SingleEnvironmentVariable, error) {
if infisicalToken == "" || projectId == "" || envName == "" { httpClient := resty.New()
return nil, errors.New("infisical token, project id and or environment name cannot be empty") httpClient.SetAuthToken(JTWToken).
}
splitToken := strings.Split(infisicalToken, ",")
JTWToken := splitToken[0]
temPrivateKey := splitToken[1]
// create http client
httpClient := resty.New().
SetAuthToken(JTWToken).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
var pullSecretsByInfisicalTokenResponse models.PullSecretsByInfisicalTokenResponse request := api.GetEncryptedWorkspaceKeyRequest{
response, err := httpClient. WorkspaceId: workspaceId,
R(). }
SetQueryParam("environment", envName).
SetQueryParam("channel", "cli"). workspaceKeyResponse, err := api.CallGetEncryptedWorkspaceKey(httpClient, request)
SetResult(&pullSecretsByInfisicalTokenResponse). if err != nil {
Get(fmt.Sprintf("%v/v1/secret/%v/service-token", INFISICAL_URL, projectId)) return nil, fmt.Errorf("unable to get your encrypted workspace key. [err=%v]", err)
}
encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey)
encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey)
encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce)
currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(receiversPrivateKey)
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
WorkspaceId: workspaceId,
EnvironmentName: environmentName,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
if response.StatusCode() > 299 { plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets)
return nil, fmt.Errorf(response.Status())
}
// Get workspace key
workspaceKey, err := base64.StdEncoding.DecodeString(pullSecretsByInfisicalTokenResponse.Key.EncryptedKey)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
} }
nonce, err := base64.StdEncoding.DecodeString(pullSecretsByInfisicalTokenResponse.Key.Nonce) return plainTextSecrets, nil
if err != nil {
return nil, err
}
senderPublicKey, err := base64.StdEncoding.DecodeString(pullSecretsByInfisicalTokenResponse.Key.Sender.PublicKey)
if err != nil {
return nil, err
}
currentUsersPrivateKey, err := base64.StdEncoding.DecodeString(temPrivateKey)
if err != nil {
return nil, err
}
// workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey))
workspaceKeyInBytes := DecryptAsymmetric(workspaceKey, nonce, senderPublicKey, currentUsersPrivateKey)
var listOfEnv []models.SingleEnvironmentVariable
for _, secret := range pullSecretsByInfisicalTokenResponse.Secrets {
key_iv, _ := base64.StdEncoding.DecodeString(secret.SecretKey.Iv)
key_tag, _ := base64.StdEncoding.DecodeString(secret.SecretKey.Tag)
key_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretKey.Ciphertext)
plainTextKey, err := DecryptSymmetric(workspaceKeyInBytes, key_ciphertext, key_tag, key_iv)
if err != nil {
return nil, err
}
value_iv, _ := base64.StdEncoding.DecodeString(secret.SecretValue.Iv)
value_tag, _ := base64.StdEncoding.DecodeString(secret.SecretValue.Tag)
value_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretValue.Ciphertext)
plainTextValue, err := DecryptSymmetric(workspaceKeyInBytes, value_ciphertext, value_tag, value_iv)
if err != nil {
return nil, err
}
env := models.SingleEnvironmentVariable{
Key: string(plainTextKey),
Value: string(plainTextValue),
Type: string(secret.Type),
ID: secret.ID,
}
listOfEnv = append(listOfEnv, env)
}
return listOfEnv, nil
} }
func GetAllEnvironmentVariables(projectId string, envName string) ([]models.SingleEnvironmentVariable, error) { func GetAllEnvironmentVariables(envName string) ([]models.SingleEnvironmentVariable, error) {
infisicalToken := os.Getenv(INFISICAL_TOKEN_NAME) infisicalToken := os.Getenv(INFISICAL_TOKEN_NAME)
if infisicalToken == "" { if infisicalToken == "" {
hasUserLoggedInbefore, loggedInUserEmail, err := IsUserLoggedIn() RequireLocalWorkspaceFile()
RequireLogin()
log.Debug("Trying to fetch secrets using logged in details")
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
if err != nil { if err != nil {
log.Info("Unexpected issue occurred while checking login status. To see more details, add flag --debug")
log.Debugln(err)
return nil, err return nil, err
} }
if !hasUserLoggedInbefore { workspaceFile, err := GetWorkSpaceFromFile()
log.Infoln("No logged in user. To login, please run command [infisical login]")
return nil, fmt.Errorf("user not logged in")
}
userCreds, err := GetUserCredsFromKeyRing(loggedInUserEmail)
if err != nil { if err != nil {
log.Infoln("Unable to get user creds from key ring")
log.Debug(err)
return nil, err return nil, err
} }
// TODO: Should be based on flag. I.e only get all workspaces if desired, otherwise only get the one in the current root of project secrets, err := GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, envName)
workspaceConfigs, err := GetAllWorkSpaceConfigsStartingFromCurrentPath() return secrets, err
if err != nil {
return nil, fmt.Errorf("unable to check if you have a %s file in your current directory", INFISICAL_WORKSPACE_CONFIG_FILE_NAME)
}
if len(workspaceConfigs) == 0 {
log.Infoln("Your local project is not connected to a Infisical project yet. Run command [infisical init]")
return nil, fmt.Errorf("project not initialized")
}
envsFromApi, err := GetSecretsFromAPIUsingCurrentLoggedInUser(envName, userCreds)
if err != nil {
log.Errorln("Something went wrong when pulling secrets using your logged in credentials. If the issue persists, double check your project id/try logging in again.")
log.Debugln(err)
return nil, err
}
return envsFromApi, nil
} else { } else {
envsFromApi, err := GetSecretsFromAPIUsingInfisicalToken(infisicalToken, envName, projectId) log.Debug("Trying to fetch secrets using service token")
if err != nil { return GetPlainTextSecretsViaServiceToken(infisicalToken)
log.Errorln("Something went wrong when pulling secrets using your Infisical token. Double check the token, project id or environment name (dev, prod, ect.)")
log.Debugln(err)
return nil, err
}
return envsFromApi, nil
} }
} }
func GetWorkSpacesFromAPI(userCreds models.UserCredentials) (workspaces []models.Workspace, err error) {
// create http client
httpClient := resty.New().
SetAuthToken(userCreds.JTWToken).
SetHeader("Accept", "application/json")
var getWorkSpacesResponse models.GetWorkSpacesResponse
response, err := httpClient.
R().
SetResult(&getWorkSpacesResponse).
Get(fmt.Sprintf("%v/v1/workspace", INFISICAL_URL))
if err != nil {
return nil, err
}
if response.StatusCode() > 299 {
return nil, fmt.Errorf("ops, unsuccessful response code. [response=%v]", response)
}
return getWorkSpacesResponse.Workspaces, nil
}
func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string { func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string {
if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found { if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found {
return value return value
@@ -351,6 +194,8 @@ func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.Sing
return expandedSecrets return expandedSecrets
} }
//
// if two secrets with the same name are found, the one that has type `personal` will be in the returned list // if two secrets with the same name are found, the one that has type `personal` will be in the returned list
func OverrideWithPersonalSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable { func OverrideWithPersonalSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable {
personalSecret := make(map[string]models.SingleEnvironmentVariable) personalSecret := make(map[string]models.SingleEnvironmentVariable)
@@ -359,46 +204,80 @@ func OverrideWithPersonalSecrets(secrets []models.SingleEnvironmentVariable) []m
for _, secret := range secrets { for _, secret := range secrets {
if secret.Type == PERSONAL_SECRET_TYPE_NAME { if secret.Type == PERSONAL_SECRET_TYPE_NAME {
personalSecret[secret.Key] = models.SingleEnvironmentVariable{ personalSecret[secret.Key] = secret
Key: secret.Key,
Value: secret.Value,
Type: secret.Type,
}
} }
if secret.Type == SHARED_SECRET_TYPE_NAME { if secret.Type == SHARED_SECRET_TYPE_NAME {
sharedSecret[secret.Key] = models.SingleEnvironmentVariable{ sharedSecret[secret.Key] = secret
Key: secret.Key,
Value: secret.Value,
Type: secret.Type,
}
} }
} }
for _, secret := range secrets { for _, secret := range sharedSecret {
personalValue, personalExists := personalSecret[secret.Key] personalValue, personalExists := personalSecret[secret.Key]
sharedValue, sharedExists := sharedSecret[secret.Key] if personalExists {
if personalExists && sharedExists || personalExists && !sharedExists {
secretsToReturn = append(secretsToReturn, personalValue) secretsToReturn = append(secretsToReturn, personalValue)
} else { } else {
secretsToReturn = append(secretsToReturn, sharedValue) secretsToReturn = append(secretsToReturn, secret)
} }
} }
return secretsToReturn return secretsToReturn
} }
func IsSecretEnvironmentValid(env string) bool { func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2Response) ([]models.SingleEnvironmentVariable, error) {
if env == "prod" || env == "dev" || env == "test" || env == "staging" { plainTextSecrets := []models.SingleEnvironmentVariable{}
return true for _, secret := range encryptedSecrets {
} // Decrypt key
return false key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV)
} if err != nil {
return nil, fmt.Errorf("unable to decode secret IV for secret key")
}
func IsSecretTypeValid(s string) bool { key_tag, err := base64.StdEncoding.DecodeString(secret.SecretKeyTag)
if s == "personal" || s == "shared" { if err != nil {
return true return nil, fmt.Errorf("unable to decode secret authentication tag for secret key")
}
key_ciphertext, err := base64.StdEncoding.DecodeString(secret.SecretKeyCiphertext)
if err != nil {
return nil, fmt.Errorf("unable to decode secret cipher text for secret key")
}
plainTextKey, err := crypto.DecryptSymmetric(key, key_ciphertext, key_tag, key_iv)
if err != nil {
return nil, fmt.Errorf("unable to symmetrically decrypt secret key")
}
// Decrypt value
value_iv, err := base64.StdEncoding.DecodeString(secret.SecretValueIV)
if err != nil {
return nil, fmt.Errorf("unable to decode secret IV for secret value")
}
value_tag, err := base64.StdEncoding.DecodeString(secret.SecretValueTag)
if err != nil {
return nil, fmt.Errorf("unable to decode secret authentication tag for secret value")
}
value_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretValueCiphertext)
if err != nil {
return nil, fmt.Errorf("unable to decode secret cipher text for secret key")
}
plainTextValue, err := crypto.DecryptSymmetric(key, value_ciphertext, value_tag, value_iv)
if err != nil {
return nil, fmt.Errorf("unable to symmetrically decrypt secret value")
}
plainTextSecret := models.SingleEnvironmentVariable{
Key: string(plainTextKey),
Value: string(plainTextValue),
Type: string(secret.Type),
ID: secret.ID,
}
plainTextSecrets = append(plainTextSecrets, plainTextSecret)
} }
return false
return plainTextSecrets, nil
} }
+6 -6
View File
@@ -29,13 +29,13 @@ func GetKeyRing() (keyring.Keyring, error) {
keyringInstanceConfig := keyring.Config{ keyringInstanceConfig := keyring.Config{
FilePasswordFunc: fileKeyringPassphrasePrompt, FilePasswordFunc: fileKeyringPassphrasePrompt,
ServiceName: SERVICE_NAME, ServiceName: KEYRING_SERVICE_NAME,
LibSecretCollectionName: SERVICE_NAME, LibSecretCollectionName: KEYRING_SERVICE_NAME,
KWalletAppID: SERVICE_NAME, KWalletAppID: KEYRING_SERVICE_NAME,
KWalletFolder: SERVICE_NAME, KWalletFolder: KEYRING_SERVICE_NAME,
KeychainTrustApplication: true, KeychainTrustApplication: true,
WinCredPrefix: SERVICE_NAME, WinCredPrefix: KEYRING_SERVICE_NAME,
FileDir: fmt.Sprintf("~/%s-file-vault", SERVICE_NAME), FileDir: fmt.Sprintf("~/%s-file-vault", KEYRING_SERVICE_NAME),
KeychainAccessibleWhenUnlocked: true, KeychainAccessibleWhenUnlocked: true,
} }
@@ -0,0 +1,9 @@
---
title: "Activity Logs"
---
Activity logs record all actions going through Infisical including CRUD operations applied to environment variables. They help answer questions like:
- Who added or updated environment variables recently?
- Did Bob read environment variables last week (if at all)?
- What IP address was used for that action?
@@ -4,11 +4,10 @@ title: "Integrations"
Integrations allow environment variables to be synced across your entire infrastructure from local development to CI/CD and production. Integrations allow environment variables to be synced across your entire infrastructure from local development to CI/CD and production.
We're still early with integrations, but expect more soon. We're still early with integrations, but expect more soon.
<Card title="View integrations documentation" icon="link" href="/integrations/overview"> <Card title="View integrations" icon="link" href="/integrations/overview">
View all available integrations and their guide View all available integrations and their guides
</Card> </Card>
![integrations](../../images/project-integrations.png) ![integrations](../../images/project-integrations.png)
@@ -0,0 +1,5 @@
---
title: "Point-in-Time Recovery"
---
Point-in-time (PIT) recovery allows environment variables to be rolled back to any point in time. It's powered by snapshots that get captured after mutations to environment variables.
@@ -0,0 +1,5 @@
---
title: "Secret Versioning"
---
Secret versioning allows an individual environment variable to be rolled back without touching other project environment variables.
+7 -7
View File
@@ -4,14 +4,14 @@ title: "Features"
This is a non-exhaustive list of features that Infisical offers: This is a non-exhaustive list of features that Infisical offers:
## Web UI ## Platform
The Web UI is used to manage teams and environment variables. - Provision members access to organizations and projects.
- Manage secrets by adding, deleting, updating them across environments; search, sort, hide/un-hide, export/import them.
- Provision access to organizations and projects. - Sync secrets to platforms via integrations to platforms like GitHub, Vercel, and Netlify.
- Add/delete/update, scope, search, sort, hide-unhide environment variables. - Rollback secrets to any point in time.
- Separate environment variables by environment. - Rollback each secrets to any version.
- Import environment variables via drag-and-drop, export them as a .env file. - Track actions through activity logs.
## CLI ## CLI
+3
View File
@@ -85,6 +85,9 @@
"getting-started/dashboard/organization", "getting-started/dashboard/organization",
"getting-started/dashboard/project", "getting-started/dashboard/project",
"getting-started/dashboard/integrations", "getting-started/dashboard/integrations",
"getting-started/dashboard/pit-recovery",
"getting-started/dashboard/versioning",
"getting-started/dashboard/audit-logs",
"getting-started/dashboard/token" "getting-started/dashboard/token"
] ]
}, },
-16
View File
@@ -1,16 +0,0 @@
import posthog from 'posthog-js';
import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from '../utilities/config';
export const initPostHog = () => {
if (typeof window !== 'undefined') {
// eslint-disable-next-line
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
posthog.init(POSTHOG_API_KEY, {
api_host: POSTHOG_HOST
});
}
}
return posthog;
};
+16 -9
View File
@@ -5,14 +5,21 @@ import posthog from 'posthog-js';
import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from '../utilities/config'; import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from '../utilities/config';
export const initPostHog = () => { export const initPostHog = () => {
if (typeof window !== 'undefined') { try {
// @ts-ignore if (typeof window !== 'undefined') {
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) { // @ts-ignore
posthog.init(POSTHOG_API_KEY, { if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
api_host: POSTHOG_HOST console.log("Outside of posthog", "POSTHOG_API_KEY", POSTHOG_API_KEY, "POSTHOG_HOST", POSTHOG_HOST)
}); posthog.init(POSTHOG_API_KEY, {
} api_host: POSTHOG_HOST
} });
}
return posthog; console.log("Outside of posthog")
}
return posthog;
} catch (e) {
console.log("posthog err", e)
}
}; };
+106
View File
@@ -0,0 +1,106 @@
import React from 'react';
import { Fragment } from 'react';
import { useTranslation } from "next-i18next";
import {
faAngleDown,
faEye,
faPlus,
faShuffle,
faTrash,
faX
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Listbox, Transition } from '@headlessui/react';
interface ListBoxProps {
selected: string;
select: (event: string) => void;
}
const eventOptions = [
{
name: 'addSecrets',
icon: faPlus
},
{
name: 'readSecrets',
icon: faEye
},
{
name: 'updateSecrets',
icon: faShuffle
},
{
name: 'deleteSecrets',
icon: faTrash
}
];
/**
* This is the component that we use for the event picker in the activity logs tab.
* @param {object} obj
* @param {string} obj.selected - the event that is currently selected
* @param {function} obj.select - an action that happens when an item is selected
*/
export default function EventFilter({
selected,
select
}: ListBoxProps): JSX.Element {
const { t } = useTranslation();
return (
<Listbox value={t("activity:event." + selected)} onChange={select}>
<div className="relative">
<Listbox.Button className="bg-mineshaft-800 hover:bg-mineshaft-700 duration-200 cursor-pointer rounded-md h-10 flex items-center justify-between pl-4 pr-2 w-52 text-bunker-200 text-sm">
{selected != '' ? (
<p className="select-none text-bunker-100">{t("activity:event." + selected)}</p>
) : (
<p className="select-none">Select an event</p>
)}
{selected != '' ? (
<FontAwesomeIcon
icon={faX}
className="pl-2 w-2 p-2"
onClick={() => select('')}
/>
) : (
<FontAwesomeIcon icon={faAngleDown} className="pl-4 pr-2" />
)}
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="border border-mineshaft-700 z-50 w-52 p-1 absolute mt-1 max-h-60 overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{eventOptions.map((event, id) => {
return (
<Listbox.Option
key={id}
className={`px-4 h-10 flex items-center text-sm cursor-pointer hover:bg-mineshaft-700 text-bunker-200 rounded-md ${
selected == t("activity:event." + event.name) && 'bg-mineshaft-700'
}`}
value={event.name}
>
{({ selected }) => (
<>
<span
className={`block truncate ${
selected ? 'font-semibold' : 'font-normal'
}`}
>
<FontAwesomeIcon icon={event.icon} className="pr-4" />{' '}
{t("activity:event." + event.name)}
</span>
</>
)}
</Listbox.Option>
);
})}
</Listbox.Options>
</Transition>
</div>
</Listbox>
);
}
+11 -6
View File
@@ -6,10 +6,12 @@ import { useRouter } from "next/router";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import { import {
faBookOpen, faBookOpen,
faFileLines,
faGear, faGear,
faKey, faKey,
faMobile, faMobile,
faPlug, faPlug,
faTimeline,
faUser, faUser,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
@@ -119,7 +121,7 @@ export default function Layout({ children }: LayoutProps) {
} }
}); });
} }
router.push("/dashboard/" + newWorkspaceId + "?Development"); router.push("/dashboard/" + newWorkspaceId);
setIsOpen(false); setIsOpen(false);
setNewWorkspaceName(""); setNewWorkspaceName("");
} else { } else {
@@ -139,8 +141,7 @@ export default function Layout({ children }: LayoutProps) {
{ {
href: href:
"/dashboard/" + "/dashboard/" +
workspaceMapping[workspaceSelected as any] + workspaceMapping[workspaceSelected as any],
"?Development",
title: t("nav:menu.secrets"), title: t("nav:menu.secrets"),
emoji: <FontAwesomeIcon icon={faKey} />, emoji: <FontAwesomeIcon icon={faKey} />,
}, },
@@ -154,6 +155,11 @@ export default function Layout({ children }: LayoutProps) {
title: t("nav:menu.integrations"), title: t("nav:menu.integrations"),
emoji: <FontAwesomeIcon icon={faPlug} />, emoji: <FontAwesomeIcon icon={faPlug} />,
}, },
{
href: '/activity/' + workspaceMapping[workspaceSelected as any],
title: 'Activity Logs',
emoji: <FontAwesomeIcon icon={faFileLines} />
},
{ {
href: "/settings/project/" + workspaceMapping[workspaceSelected as any], href: "/settings/project/" + workspaceMapping[workspaceSelected as any],
title: t("nav:menu.project-settings"), title: t("nav:menu.project-settings"),
@@ -192,7 +198,7 @@ export default function Layout({ children }: LayoutProps) {
.map((workspace: { _id: string }) => workspace._id) .map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId) .includes(intendedWorkspaceId)
) { ) {
router.push("/dashboard/" + userWorkspaces[0]._id + "?Development"); router.push("/dashboard/" + userWorkspaces[0]._id);
} else { } else {
setWorkspaceList( setWorkspaceList(
userWorkspaces.map((workspace: any) => workspace.name) userWorkspaces.map((workspace: any) => workspace.name)
@@ -235,8 +241,7 @@ export default function Layout({ children }: LayoutProps) {
) { ) {
router.push( router.push(
"/dashboard/" + "/dashboard/" +
workspaceMapping[workspaceSelected as any] + workspaceMapping[workspaceSelected as any]
"?Development"
); );
localStorage.setItem( localStorage.setItem(
"projectData.id", "projectData.id",
+13 -13
View File
@@ -1,12 +1,12 @@
import React from "react"; import React from 'react';
import { Fragment } from "react"; import { Fragment } from 'react';
import { import {
faAngleDown, faAngleDown,
faCheck, faCheck,
faPlus, faPlus
} from "@fortawesome/free-solid-svg-icons"; } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Listbox, Transition } from "@headlessui/react"; import { Listbox, Transition } from '@headlessui/react';
interface ListBoxProps { interface ListBoxProps {
selected: string; selected: string;
@@ -34,20 +34,20 @@ export default function ListBox({
data, data,
text, text,
buttonAction, buttonAction,
isFull, isFull
}: ListBoxProps): JSX.Element { }: ListBoxProps): JSX.Element {
return ( return (
<Listbox value={selected} onChange={onChange}> <Listbox value={selected} onChange={onChange}>
<div className="relative"> <div className="relative">
<Listbox.Button <Listbox.Button
className={`text-gray-400 relative ${ className={`text-gray-400 relative ${
isFull ? "w-full" : "w-52" isFull ? 'w-full' : 'w-52'
} cursor-default rounded-md bg-white/[0.07] hover:bg-white/[0.11] duration-200 py-2.5 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm`} } cursor-default rounded-md bg-white/[0.07] hover:bg-white/[0.11] duration-200 py-2.5 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm`}
> >
<div className="flex flex-row"> <div className="flex flex-row">
{text} {text}
<span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300"> <span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300">
{" "} {' '}
{selected} {selected}
</span> </span>
</div> </div>
@@ -70,11 +70,11 @@ export default function ListBox({
key={personIdx} key={personIdx}
className={({ active, selected }) => className={({ active, selected }) =>
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${ `my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${
selected ? "bg-white/10 text-gray-400 font-bold" : "" selected ? 'bg-white/10 text-gray-400 font-bold' : ''
} ${ } ${
active && !selected active && !selected
? "bg-white/5 text-mineshaft-200 cursor-pointer" ? 'bg-white/5 text-mineshaft-200 cursor-pointer'
: "text-gray-400" : 'text-gray-400'
} ` } `
} }
value={person} value={person}
@@ -83,7 +83,7 @@ export default function ListBox({
<> <>
<span <span
className={`block truncate text-primary${ className={`block truncate text-primary${
selected ? "font-medium" : "font-normal" selected ? 'font-medium' : 'font-normal'
}`} }`}
> >
{person} {person}
+1 -1
View File
@@ -115,7 +115,7 @@ export default function Button(props: ButtonProps): JSX.Element {
<FontAwesomeIcon <FontAwesomeIcon
icon={props.icon} icon={props.icon}
className={`flex my-auto font-extrabold ${ className={`flex my-auto font-extrabold ${
props.size == "icon-sm" ? "text-sm" : "text-md" props.size == "icon-sm" ? "text-sm" : "text-sm"
} ${(props.text || props.textDisabled) && "mr-2"}`} } ${(props.text || props.textDisabled) && "mr-2"}`}
/> />
)} )}
@@ -12,6 +12,7 @@ import { envMapping } from "../../../public/data/frequentConstants";
import { import {
decryptAssymmetric, decryptAssymmetric,
encryptAssymmetric, encryptAssymmetric,
encryptSymmetric,
} from "../../utilities/cryptography/crypto"; } from "../../utilities/cryptography/crypto";
import Button from "../buttons/Button"; import Button from "../buttons/Button";
import InputField from "../InputField"; import InputField from "../InputField";
@@ -25,11 +26,15 @@ const expiryMapping = {
"12 months": 31104000, "12 months": 31104000,
}; };
const crypto = require('crypto');
const AddServiceTokenDialog = ({ const AddServiceTokenDialog = ({
isOpen, isOpen,
closeModal, closeModal,
workspaceId, workspaceId,
workspaceName, workspaceName,
serviceTokens,
setServiceTokens
}) => { }) => {
const [serviceToken, setServiceToken] = useState(""); const [serviceToken, setServiceToken] = useState("");
const [serviceTokenName, setServiceTokenName] = useState(""); const [serviceTokenName, setServiceTokenName] = useState("");
@@ -48,16 +53,14 @@ const AddServiceTokenDialog = ({
privateKey: localStorage.getItem("PRIVATE_KEY"), privateKey: localStorage.getItem("PRIVATE_KEY"),
}); });
// generate new public/private key pair const randomBytes = crypto.randomBytes(16).toString('hex');
const pair = nacl.box.keyPair(); const {
const publicKey = nacl.util.encodeBase64(pair.publicKey); ciphertext,
const privateKey = nacl.util.encodeBase64(pair.secretKey); iv,
tag,
// encrypt workspace key under newly-generated public key } = encryptSymmetric({
const { ciphertext: encryptedKey, nonce } = encryptAssymmetric({
plaintext: key, plaintext: key,
publicKey, key: randomBytes,
privateKey,
}); });
let newServiceToken = await addServiceToken({ let newServiceToken = await addServiceToken({
@@ -65,13 +68,15 @@ const AddServiceTokenDialog = ({
workspaceId, workspaceId,
environment: envMapping[serviceTokenEnv], environment: envMapping[serviceTokenEnv],
expiresIn: expiryMapping[serviceTokenExpiresIn], expiresIn: expiryMapping[serviceTokenExpiresIn],
publicKey, encryptedKey: ciphertext,
encryptedKey, iv,
nonce, tag
}); });
console.log('newServiceToken', newServiceToken);
const serviceToken = newServiceToken + "," + privateKey; setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
setServiceToken(serviceToken); setServiceToken(newServiceToken.serviceToken + "." + randomBytes);
}; };
function copyToClipboard() { function copyToClipboard() {
@@ -161,7 +166,7 @@ const AddServiceTokenDialog = ({
"Production", "Production",
"Testing", "Testing",
]} ]}
width="full" isFull={true}
text={`${t("common:environment")}: `} text={`${t("common:environment")}: `}
/> />
</div> </div>
@@ -176,7 +181,7 @@ const AddServiceTokenDialog = ({
"6 months", "6 months",
"12 months", "12 months",
]} ]}
width="full" isFull={true}
text={`${t("common:expired-in")}: `} text={`${t("common:expired-in")}: `}
/> />
</div> </div>
@@ -211,7 +216,7 @@ const AddServiceTokenDialog = ({
</div> </div>
</div> </div>
<div className="w-full"> <div className="w-full">
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-44"> <div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20">
<input <input
type="text" type="text"
value={serviceToken} value={serviceToken}
@@ -236,7 +241,7 @@ const AddServiceTokenDialog = ({
)} )}
</button> </button>
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm"> <span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm">
{t("common.click-to-copy")} {t("common:click-to-copy")}
</span> </span>
</div> </div>
</div> </div>
@@ -1,36 +1,53 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { faX } from '@fortawesome/free-solid-svg-icons'; import { faX } from '@fortawesome/free-solid-svg-icons';
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
import { reverseEnvMapping } from '../../../public/data/frequentConstants'; import { reverseEnvMapping } from '../../../public/data/frequentConstants';
import guidGenerator from '../../utilities/randomId'; import guidGenerator from '../../utilities/randomId';
import Button from '../buttons/Button'; import Button from '../buttons/Button';
interface TokenProps {
_id: string;
name: string;
environment: string;
expiresAt: string;
}
interface ServiceTokensProps {
data: TokenProps[];
workspaceName: string;
setServiceTokens: (value: TokenProps[]) => void;
}
/** /**
* This is the component that we utilize for the user table - in future, can reuse it for some other purposes too. * This is the component that we utilize for the service token table
* #TODO: add the possibility of choosing and doing operations on multiple users. * #TODO: add the possibility of choosing and doing operations on multiple users.
* @param {*} props * @param {object} obj
* @param {any[]} obj.data - current state of the service token table
* @param {string} obj.workspaceName - name of the current project
* @param {function} obj.setServiceTokens - updating the state of the service token table
* @returns * @returns
*/ */
const ServiceTokenTable = ({ data, workspaceName }) => { const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTokensProps) => {
const router = useRouter(); const { createNotification } = useNotificationContext();
return ( return (
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1"> <div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
<div className="absolute rounded-t-md w-full h-12 bg-white/5"></div> <div className="absolute rounded-t-md w-full h-12 bg-white/5"></div>
<table className="w-full my-1"> <table className="w-full my-1">
<thead className="text-bunker-300"> <thead className="text-bunker-300 text-sm font-light">
<tr> <tr>
<th className="text-left pl-6 pt-2.5 pb-2">Token name</th> <th className="text-left pl-6 pt-2.5 pb-2">TOKEN NAME</th>
<th className="text-left pl-6 pt-2.5 pb-2">Project</th> <th className="text-left pl-6 pt-2.5 pb-2">PROJECT</th>
<th className="text-left pl-6 pt-2.5 pb-2">Environment</th> <th className="text-left pl-6 pt-2.5 pb-2">ENVIRONMENT</th>
<th className="text-left pl-6 pt-2.5 pb-2">Valid until</th> <th className="text-left pl-6 pt-2.5 pb-2">VAILD UNTIL</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data?.length > 0 ? ( {data?.length > 0 ? (
data.map((row, index) => { data?.map((row) => {
return ( return (
<tr <tr
key={guidGenerator()} key={guidGenerator()}
@@ -51,7 +68,14 @@ const ServiceTokenTable = ({ data, workspaceName }) => {
<td className="py-2 border-mineshaft-700 border-t"> <td className="py-2 border-mineshaft-700 border-t">
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center"> <div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
<Button <Button
onButtonPressed={() => {}} onButtonPressed={() => {
deleteServiceToken({ serviceTokenId: row._id} );
setServiceTokens(data.filter(token => token._id != row._id));
createNotification({
text: `'${row.name}' token has been revoked.`,
type: 'error'
});
}}
color="red" color="red"
size="icon-sm" size="icon-sm"
icon={faX} icon={faX}
@@ -63,7 +87,7 @@ const ServiceTokenTable = ({ data, workspaceName }) => {
}) })
) : ( ) : (
<tr> <tr>
<td colSpan="4" className="text-center pt-7 pb-4 text-bunker-400"> <td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">
No service tokens yet No service tokens yet
</td> </td>
</tr> </tr>
@@ -36,7 +36,7 @@ const Notification = ({
return ( return (
<div <div
className="relative w-full flex items-center justify-between px-4 py-6 rounded-md border border-bunker-500 pointer-events-auto bg-bunker-500" className="relative w-full flex items-center justify-between px-4 py-4 rounded-md border border-bunker-500 pointer-events-auto bg-bunker-500"
role="alert" role="alert"
> >
{notification.type === 'error' && ( {notification.type === 'error' && (
@@ -56,7 +56,7 @@ const Notification = ({
onClick={() => clearNotification(notification.text)} onClick={() => clearNotification(notification.text)}
> >
<FontAwesomeIcon <FontAwesomeIcon
className="text-white w-4 h-3 hover:text-red" className="text-white pl-2 w-4 h-3 hover:text-red"
icon={faX} icon={faX}
/> />
</button> </button>
@@ -38,7 +38,7 @@ const NotificationProvider = ({ children }: NotificationProviderProps) => {
const createNotification = ({ const createNotification = ({
text, text,
type = 'success', type = 'success',
timeoutMs = 5000 timeoutMs = 4000
}: Notification) => { }: Notification) => {
const doesNotifExist = notifications.some((notif) => notif.text === text); const doesNotifExist = notifications.some((notif) => notif.text === text);
@@ -53,7 +53,7 @@ const DashboardInputField = ({
return ( return (
<div className="flex-col w-full"> <div className="flex-col w-full">
<div <div
className={`group relative flex flex-col justify-center w-full max-w-2xl border ${ className={`group relative flex flex-col justify-center w-full border ${
error ? 'border-red' : 'border-mineshaft-500' error ? 'border-red' : 'border-mineshaft-500'
} rounded-md`} } rounded-md`}
> >
@@ -85,7 +85,7 @@ const DashboardInputField = ({
return ( return (
<div className="flex-col w-full"> <div className="flex-col w-full">
<div <div
className={`group relative whitespace-pre flex flex-col justify-center w-full max-w-2xl border border-mineshaft-500 rounded-md`} className={`group relative whitespace-pre flex flex-col justify-center w-full border border-mineshaft-500 rounded-md`}
> >
{override == true && <div className='bg-primary-300 absolute top-[0.1rem] right-[0.1rem] z-10 w-min text-xxs px-1 text-black opacity-80 rounded-md'>Override enabled</div>} {override == true && <div className='bg-primary-300 absolute top-[0.1rem] right-[0.1rem] z-10 w-min text-xxs px-1 text-black opacity-80 rounded-md'>Override enabled</div>}
<input <input
@@ -108,9 +108,9 @@ const DashboardInputField = ({
} ${ } ${
override ? 'text-primary-300' : 'text-gray-400' override ? 'text-primary-300' : 'text-gray-400'
} }
absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture max-w-2xl overflow-x-scroll bg-bunker-800 h-9 rounded-md text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`} absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture overflow-x-scroll bg-bunker-800 h-9 rounded-md text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
> >
{value.split(REGEX).map((word, id) => { {value?.split(REGEX).map((word, id) => {
if (word.match(REGEX) !== null) { if (word.match(REGEX) !== null) {
return ( return (
<span className="ph-no-capture text-yellow" key={id}> <span className="ph-no-capture text-yellow" key={id}>
@@ -139,7 +139,7 @@ const DashboardInputField = ({
})} })}
</div> </div>
{blurred && ( {blurred && (
<div className="absolute flex flex-row items-center z-20 peer pr-2 bg-bunker-800 group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible h-9 w-full max-w-2xl rounded-md text-gray-400/50 text-clip"> <div className="absolute flex flex-row items-center z-20 peer pr-2 bg-bunker-800 group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible h-9 w-full rounded-md text-gray-400/50 text-clip">
<div className="px-2 flex flex-row items-center overflow-x-scroll no-scrollbar no-scrollbar::-webkit-scrollbar"> <div className="px-2 flex flex-row items-center overflow-x-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
{value.split('').map(() => ( {value.split('').map(() => (
<FontAwesomeIcon <FontAwesomeIcon
+11 -20
View File
@@ -15,40 +15,40 @@ interface SecretDataProps {
interface KeyPairProps { interface KeyPairProps {
keyPair: SecretDataProps; keyPair: SecretDataProps;
deleteRow: (id: string) => void;
modifyKey: (value: string, position: number) => void; modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void; modifyValue: (value: string, position: number) => void;
isBlurred: boolean; isBlurred: boolean;
isDuplicate: boolean; isDuplicate: boolean;
toggleSidebar: (id: string) => void; toggleSidebar: (id: string) => void;
sidebarSecretId: string; sidebarSecretId: string;
isSnapshot: boolean;
} }
/** /**
* This component represent a single row for an environemnt variable on the dashboard * This component represent a single row for an environemnt variable on the dashboard
* @param {object} obj * @param {object} obj
* @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private) * @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
* @param {function} obj.deleteRow - a function to delete a certain keyPair
* @param {function} obj.modifyKey - modify the key of a certain environment variable * @param {function} obj.modifyKey - modify the key of a certain environment variable
* @param {function} obj.modifyValue - modify the value of a certain environment variable * @param {function} obj.modifyValue - modify the value of a certain environment variable
* @param {boolean} obj.isBlurred - if the blurring setting is turned on * @param {boolean} obj.isBlurred - if the blurring setting is turned on
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard * @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
* @param {function} obj.toggleSidebar - open/close/switch sidebar * @param {function} obj.toggleSidebar - open/close/switch sidebar
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed * @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
* @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar
* @returns * @returns
*/ */
const KeyPair = ({ const KeyPair = ({
keyPair, keyPair,
deleteRow,
modifyKey, modifyKey,
modifyValue, modifyValue,
isBlurred, isBlurred,
isDuplicate, isDuplicate,
toggleSidebar, toggleSidebar,
sidebarSecretId sidebarSecretId,
isSnapshot
}: KeyPairProps) => { }: KeyPairProps) => {
return ( return (
<div className={`mx-1 flex flex-col items-center ml-1 ${keyPair.id == sidebarSecretId && "bg-mineshaft-500 duration-200"} rounded-md`}> <div className={`mx-1 flex flex-col items-center ml-1 ${isSnapshot && "pointer-events-none"} ${keyPair.id == sidebarSecretId && "bg-mineshaft-500 duration-200"} rounded-md`}>
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1"> <div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
{keyPair.type == "personal" && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200"> {keyPair.type == "personal" && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
<div className='w-1 h-1 rounded-full bg-primary z-40'></div> <div className='w-1 h-1 rounded-full bg-primary z-40'></div>
@@ -57,7 +57,7 @@ const KeyPair = ({
</span> </span>
</div>} </div>}
<div className="min-w-xl w-96"> <div className="min-w-xl w-96">
<div className="flex pr-1 items-center rounded-lg mt-4 md:mt-0 max-h-16"> <div className="flex pr-1.5 items-center rounded-lg mt-4 md:mt-0 max-h-16">
<DashboardInputField <DashboardInputField
onChangeHandler={modifyKey} onChangeHandler={modifyKey}
type="varName" type="varName"
@@ -67,8 +67,8 @@ const KeyPair = ({
/> />
</div> </div>
</div> </div>
<div className="w-full min-w-5xl"> <div className="w-full min-w-xl">
<div className="flex min-w-7xl items-center pl-1 pr-1.5 rounded-lg mt-4 md:mt-0 max-h-10 "> <div className={`flex min-w-xl items-center ${!isSnapshot && "pr-1.5"} rounded-lg mt-4 md:mt-0 max-h-10`}>
<DashboardInputField <DashboardInputField
onChangeHandler={modifyValue} onChangeHandler={modifyValue}
type="value" type="value"
@@ -79,24 +79,15 @@ const KeyPair = ({
/> />
</div> </div>
</div> </div>
<div onClick={() => toggleSidebar(keyPair.id)} className="cursor-pointer w-9 h-9 bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200"> {!isSnapshot && <div onClick={() => toggleSidebar(keyPair.id)} className="cursor-pointer w-[2.35rem] h-[2.35rem] bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200">
<FontAwesomeIcon <FontAwesomeIcon
className="text-gray-300 px-2.5 text-lg mt-0.5" className="text-gray-300 px-2.5 text-lg mt-0.5"
icon={faEllipsis} icon={faEllipsis}
/> />
</div> </div>}
<div className="w-2"></div>
<div className="bg-[#9B3535] hover:bg-red rounded-md duration-200">
<Button
onButtonPressed={() => deleteRow(keyPair.id)}
color="none"
size="icon-sm"
icon={faX}
/>
</div>
</div> </div>
</div> </div>
); );
}; };
export default React.memo(KeyPair); export default KeyPair;
+87 -72
View File
@@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import Image from 'next/image';
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import { faX } from '@fortawesome/free-solid-svg-icons'; import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -40,6 +41,7 @@ interface SideBarProps {
savePush: () => void; savePush: () => void;
sharedToHide: string[]; sharedToHide: string[];
setSharedToHide: (values: string[]) => void; setSharedToHide: (values: string[]) => void;
deleteRow: any;
} }
/** /**
@@ -54,6 +56,7 @@ interface SideBarProps {
* @param {function} obj.savePush - save changes andp ush secrets * @param {function} obj.savePush - save changes andp ush secrets
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden. * @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually * @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
* @param {function} obj.deleteRow - a function to delete a certain keyPair
* @returns the sidebar with 'secret's settings' * @returns the sidebar with 'secret's settings'
*/ */
const SideBar = ({ const SideBar = ({
@@ -67,93 +70,97 @@ const SideBar = ({
buttonReady, buttonReady,
savePush, savePush,
sharedToHide, sharedToHide,
setSharedToHide setSharedToHide,
deleteRow
}: SideBarProps) => { }: SideBarProps) => {
const [isLoading, setIsLoading] = useState(false);
const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal")); const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal"));
const { t } = useTranslation(); const { t } = useTranslation();
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between'> return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between'>
<div className='h-min overflow-y-auto'> {isLoading ? (
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center"> <div className="flex items-center justify-center h-full">
<p className="font-semibold text-lg text-bunker-200">{t("dashboard:sidebar.secret")}</p> <Image
<div className='p-1' onClick={() => toggleSidebar("None")}> src="/images/loading/loading.gif"
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/> height={60}
width={100}
alt="infisical loading indicator"
></Image>
</div>
) : (
<div className='h-min overflow-y-auto'>
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
<p className="font-semibold text-lg text-bunker-200">{t("dashboard:sidebar.secret")}</p>
<div className='p-1' onClick={() => toggleSidebar("None")}>
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
</div>
</div> </div>
</div> <div className='mt-4 px-4 pointer-events-none'>
<div className='mt-4 px-4 pointer-events-none'> <p className='text-sm text-bunker-300'>{t("dashboard:sidebar.key")}</p>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.key")}</p> <DashboardInputField
<DashboardInputField onChangeHandler={modifyKey}
onChangeHandler={modifyKey} type="varName"
type="varName" position={data[0]?.pos}
position={data[0]?.pos} value={data[0]?.key}
value={data[0]?.key} isDuplicate={false}
isDuplicate={false} blurred={false}
blurred={false}
/>
</div>
{data.filter(secret => secret.type == "shared")[0]?.value
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.value")}</p>
<DashboardInputField
onChangeHandler={modifyValue}
type="value"
position={data.filter(secret => secret.type == "shared")[0]?.pos}
value={data.filter(secret => secret.type == "shared")[0]?.value}
isDuplicate={false}
blurred={true}
/>
<div className='absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50'>
<GenerateSecretMenu modifyValue={modifyValue} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
</div>
</div>
: <div className='px-4 text-sm text-bunker-300 pt-4'>
<span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1'>{t("common:note")}:</span>
{t("dashboard:sidebar.personal-explanation")}
</div>}
<div className='mt-4 px-4'>
{data.filter(secret => secret.type == "shared")[0]?.value &&
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.override")}</p>
<Toggle
enabled={overrideEnabled}
setEnabled={setOverrideEnabled}
addOverride={addOverride}
keyName={data[0]?.key}
value={data[0]?.value}
pos={data[0]?.pos}
id={data[0]?.id}
comment={data[0]?.comment}
deleteOverride={deleteOverride}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
/> />
</div>} </div>
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}> {data.filter(secret => secret.type == "shared")[0]?.value
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.value")}</p>
<DashboardInputField <DashboardInputField
onChangeHandler={modifyValue} onChangeHandler={modifyValue}
type="value" type="value"
position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos} position={data.filter(secret => secret.type == "shared")[0]?.pos}
value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.value : data[0]?.value} value={data.filter(secret => secret.type == "shared")[0]?.value}
isDuplicate={false} isDuplicate={false}
blurred={true} blurred={true}
/> />
<div className='absolute right-[0.57rem] top-[0.3rem] z-50'> <div className='absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50'>
<GenerateSecretMenu modifyValue={modifyValue} position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos} /> <GenerateSecretMenu modifyValue={modifyValue} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
</div> </div>
</div> </div>
: <div className='px-4 text-sm text-bunker-300 pt-4'>
<span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1'>{t("common:note")}:</span>
{t("dashboard:sidebar.personal-explanation")}
</div>}
<div className='mt-4 px-4'>
{data.filter(secret => secret.type == "shared")[0]?.value &&
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.override")}</p>
<Toggle
enabled={overrideEnabled}
setEnabled={setOverrideEnabled}
addOverride={addOverride}
keyName={data[0]?.key}
value={data[0]?.value}
pos={data[0]?.pos}
id={data[0]?.id}
comment={data[0]?.comment}
deleteOverride={deleteOverride}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
/>
</div>}
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
<DashboardInputField
onChangeHandler={modifyValue}
type="value"
position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos}
value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.value : data[0]?.value}
isDuplicate={false}
blurred={true}
/>
<div className='absolute right-[0.57rem] top-[0.3rem] z-50'>
<GenerateSecretMenu modifyValue={modifyValue} position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos} />
</div>
</div>
</div>
<SecretVersionList secretId={data[0]?.id} />
<CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
</div> </div>
{/* <div className={`relative mt-4 px-4 opacity-80 duration-200`}> )}
<p className='text-sm text-bunker-200'>Group</p>
<ListBox
selected={"Database Secrets"}
onChange={() => {}}
data={["Group1"]}
isFull={true}
/>
</div> */}
<SecretVersionList secretId={data[0]?.id} />
<CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
</div>
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}> <div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
<Button <Button
text={String(t("common:save-changes"))} text={String(t("common:save-changes"))}
@@ -163,6 +170,14 @@ const SideBar = ({
active={buttonReady} active={buttonReady}
textDisabled="Saved" textDisabled="Saved"
/> />
<div className="bg-[#9B3535] opacity-70 hover:opacity-100 w-[4.5rem] h-[2.5rem] rounded-md duration-200 ml-2">
<Button
text={String(t("Delete"))}
onButtonPressed={() => deleteRow({ ids: overrideEnabled ? data.map(secret => secret.id) : [data.filter(secret => secret.type == "shared")[0]?.id], secretName: data[0]?.key })}
color="red"
size="md"
/>
</div>
</div> </div>
</div> </div>
}; };
@@ -10,7 +10,7 @@ class Capturer {
capture(item) { capture(item) {
if (ENV == "production" && TELEMETRY_CAPTURING_ENABLED) { if (ENV == "production" && TELEMETRY_CAPTURING_ENABLED) {
try { try {
api.capture(item); this.api.capture(item);
} catch (error) { } catch (error) {
console.error("PostHog", error); console.error("PostHog", error);
} }
@@ -20,7 +20,7 @@ class Capturer {
identify(id) { identify(id) {
if (ENV == "production" && TELEMETRY_CAPTURING_ENABLED) { if (ENV == "production" && TELEMETRY_CAPTURING_ENABLED) {
try { try {
api.identify(id); this.api.identify(id);
} catch (error) { } catch (error) {
console.error("PostHog", error); console.error("PostHog", error);
} }
+32
View File
@@ -0,0 +1,32 @@
import SecurityClient from '~/utilities/SecurityClient';
interface workspaceProps {
actionId: string;
}
/**
* This function fetches the data for a certain action performed by a user
* @param {object} obj
* @param {string} obj.actionId - id of an action for which we are trying to get data
* @returns
*/
const getActionData = async ({ actionId }: workspaceProps) => {
return SecurityClient.fetchCall(
'/api/v1/action/' + actionId, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
).then(async (res) => {
console.log(188, res)
if (res && res.status == 200) {
return (await res.json()).action;
} else {
console.log('Failed to get the info about an action');
}
});
};
export default getActionData;
+72
View File
@@ -0,0 +1,72 @@
import SecurityClient from '~/utilities/SecurityClient';
interface workspaceProps {
workspaceId: string;
offset: number;
limit: number;
userId: string;
actionNames: string;
}
/**
* This function fetches the activity logs for a certain project
* @param {object} obj
* @param {string} obj.workspaceId - workspace id for which we are trying to get project log
* @param {object} obj.offset - teh starting point of logs that we want to pull
* @param {object} obj.limit - how many logs will we output
* @param {object} obj.userId - optional userId filter - will only query logs for that user
* @param {string} obj.actionNames - optional actionNames filter - will only query logs for those actions
* @returns
*/
const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames }: workspaceProps) => {
let payload;
if (userId != "" && actionNames != '') {
payload = {
offset: String(offset),
limit: String(limit),
sortBy: 'recent',
userId: JSON.stringify(userId),
actionNames: actionNames
}
} else if (userId != "") {
payload = {
offset: String(offset),
limit: String(limit),
sortBy: 'recent',
userId: JSON.stringify(userId)
}
} else if (actionNames != "") {
payload = {
offset: String(offset),
limit: String(limit),
sortBy: 'recent',
actionNames: actionNames
}
} else {
payload = {
offset: String(offset),
limit: String(limit),
sortBy: 'recent'
}
}
return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + '/logs?' +
new URLSearchParams(payload),
{
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json()).logs;
} else {
console.log('Failed to get project logs');
}
});
};
export default getProjectLogs;
@@ -0,0 +1,39 @@
import SecurityClient from '~/utilities/SecurityClient';
interface workspaceProps {
workspaceId: string;
offset: number;
limit: number;
}
/**
* This function fetches the secret snapshots for a certain project
* @param {object} obj
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
* @param {object} obj.offset - teh starting point of snapshots that we want to pull
* @param {object} obj.limit - how many snapshots will we output
* @returns
*/
const getProjectSecretShanpshots = async ({ workspaceId, offset, limit }: workspaceProps) => {
return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + '/secret-snapshots?' +
new URLSearchParams({
offset: String(offset),
limit: String(limit)
}), {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json()).secretSnapshots;
} else {
console.log('Failed to get project secret snapshots');
}
});
};
export default getProjectSecretShanpshots;
@@ -0,0 +1,31 @@
import SecurityClient from '~/utilities/SecurityClient';
interface workspaceProps {
workspaceId: string;
}
/**
* This function fetches the count of secret snapshots for a certain project
* @param {object} obj
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
* @returns
*/
const getProjectSercetSnapshotsCount = async ({ workspaceId }: workspaceProps) => {
return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + '/secret-snapshots/count', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json()).count;
} else {
console.log('Failed to get the count of project secret snapshots');
}
});
};
export default getProjectSercetSnapshotsCount;
@@ -0,0 +1,31 @@
import SecurityClient from '~/utilities/SecurityClient';
interface SnapshotProps {
secretSnapshotId: string;
}
/**
* This function fetches the secrets for a certain secret snapshot
* @param {object} obj
* @param {string} obj.secretSnapshotId - snapshot id for which we are trying to get secrets
* @returns
*/
const getSecretSnapshotData = async ({ secretSnapshotId }: SnapshotProps) => {
return SecurityClient.fetchCall(
'/api/v1/secret-snapshot/' + secretSnapshotId, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json()).secretSnapshot;
} else {
console.log('Failed to get the secrets of a certain snapshot');
}
});
};
export default getSecretSnapshotData;
+2 -2
View File
@@ -17,7 +17,7 @@ interface secretVersionProps {
*/ */
const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps) => { const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps) => {
return SecurityClient.fetchCall( return SecurityClient.fetchCall(
'/api/v1/secret/' + secretId + '/secret-versions?'+ '/api/v1/secret/' + secretId + '/secret-versions?' +
new URLSearchParams({ new URLSearchParams({
offset: String(offset), offset: String(offset),
limit: String(limit) limit: String(limit)
@@ -32,7 +32,7 @@ const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps
if (res && res.status == 200) { if (res && res.status == 200) {
return await res.json(); return await res.json();
} else { } else {
console.log('Failed to get project secrets'); console.log('Failed to get secret version history');
} }
}); });
}; };
+185
View File
@@ -0,0 +1,185 @@
import { useEffect, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import getActionData from "ee/api/secrets/GetActionData";
import patienceDiff from 'ee/utilities/findTextDifferences';
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import DashboardInputField from '../../components/dashboard/DashboardInputField';
const {
decryptAssymmetric,
decryptSymmetric
} = require('../../components/utilities/cryptography/crypto');
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
interface SideBarProps {
toggleSidebar: (value: string) => void;
currentAction: string;
}
interface SecretProps {
secret: string;
secretKeyCiphertext: string;
secretKeyHash: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueHash: string;
secretValueIV: string;
secretValueTag: string;
}
interface DecryptedSecretProps {
newSecretVersion: {
key: string;
value: string;
}
oldSecretVersion: {
key: string;
value: string;
}
}
interface ActionProps {
name: string;
}
/**
* @param {object} obj
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
* @param {string} obj.currentAction - the action id for which a sidebar is being displayed
* @returns the sidebar with the payload of user activity logs
*/
const ActivitySideBar = ({
toggleSidebar,
currentAction
}: SideBarProps) => {
const { t } = useTranslation();
const router = useRouter();
const [actionData, setActionData] = useState<DecryptedSecretProps[]>();
const [actionMetaData, setActionMetaData] = useState<ActionProps>();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const getLogData = async () => {
setIsLoading(true);
const tempActionData = await getActionData({ actionId: currentAction });
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
// #TODO: make this a separate function and reuse across the app
let decryptedLatestKey: string;
if (latestKey) {
// assymmetrically decrypt symmetric key with local private key
decryptedLatestKey = decryptAssymmetric({
ciphertext: latestKey.latestKey.encryptedKey,
nonce: latestKey.latestKey.nonce,
publicKey: latestKey.latestKey.sender.publicKey,
privateKey: String(PRIVATE_KEY)
});
}
const decryptedSecretVersions = tempActionData.payload.secretVersions.map((encryptedSecretVersion: {
newSecretVersion?: SecretProps;
oldSecretVersion?: SecretProps;
}) => {
return {
newSecretVersion: {
key: decryptSymmetric({
ciphertext: encryptedSecretVersion.newSecretVersion!.secretKeyCiphertext,
iv: encryptedSecretVersion.newSecretVersion!.secretKeyIV,
tag: encryptedSecretVersion.newSecretVersion!.secretKeyTag,
key: decryptedLatestKey
}),
value: decryptSymmetric({
ciphertext: encryptedSecretVersion.newSecretVersion!.secretValueCiphertext,
iv: encryptedSecretVersion.newSecretVersion!.secretValueIV,
tag: encryptedSecretVersion.newSecretVersion!.secretValueTag,
key: decryptedLatestKey
})
},
oldSecretVersion: {
key: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext
? decryptSymmetric({
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext,
iv: encryptedSecretVersion.oldSecretVersion?.secretKeyIV,
tag: encryptedSecretVersion.oldSecretVersion?.secretKeyTag,
key: decryptedLatestKey
}): undefined,
value: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext
? decryptSymmetric({
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext,
iv: encryptedSecretVersion.oldSecretVersion?.secretValueIV,
tag: encryptedSecretVersion.oldSecretVersion?.secretValueTag,
key: decryptedLatestKey
}): undefined
}
}
})
setActionData(decryptedSecretVersions);
setActionMetaData({name: tempActionData.name});
setIsLoading(false);
}
getLogData();
}, [currentAction]);
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between`}>
{isLoading ? (
<div className="flex items-center justify-center h-full mb-8">
<Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="infisical loading indicator"
></Image>
</div>
) : (
<div className='h-min overflow-y-auto'>
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
<p className="font-semibold text-lg text-bunker-200">{t("activity:event." + actionMetaData?.name)}</p>
<div className='p-1' onClick={() => toggleSidebar("")}>
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
</div>
</div>
<div className='flex flex-col px-4'>
{(actionMetaData?.name == 'readSecrets'
|| actionMetaData?.name == 'addSecrets'
|| actionMetaData?.name == 'deleteSecrets') && actionData?.map((item, id) =>
<div key={id}>
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
<DashboardInputField
key={id}
onChangeHandler={() => {}}
type="value"
position={1}
value={item.newSecretVersion.value}
isDuplicate={false}
blurred={false}
/>
</div>
)}
{actionMetaData?.name == 'updateSecrets' && actionData?.map((item, id) =>
<>
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
<div className='text-bunker-100 font-mono rounded-md overflow-hidden'>
<div className='bg-red/30 px-2'>- {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.bIndex != -1 && <span key={id} className={`${character.aIndex == -1 && "bg-red-700/80"}`}>{character.line}</span>)}</div>
<div className='bg-green-500/30 px-2'>+ {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.aIndex != -1 && <span key={id} className={`${character.bIndex == -1 && "bg-green-700/80"}`}>{character.line}</span>)}</div>
</div>
</>
)}
</div>
</div>
)}
</div>
};
export default ActivitySideBar;
+129
View File
@@ -0,0 +1,129 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { useTranslation } from "next-i18next";
import {
faAngleDown,
faAngleRight,
faUpRightFromSquare
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import timeSince from 'ee/utilities/timeSince';
import guidGenerator from '../../components/utilities/randomId';
interface PayloadProps {
_id: string;
name: string;
secretVersions: string[];
}
interface logData {
_id: string;
channel: string;
createdAt: string;
ipAddress: string;
user: string;
payload: PayloadProps[];
}
/**
* This is a single row of the activity table
* @param obj
* @param {logData} obj.row - data for a certain event
* @param {function} obj.toggleSidebar - open and close sidebar that displays data for a specific event
* @returns
*/
const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar: (value: string) => void; }) => {
const [payloadOpened, setPayloadOpened] = useState(false);
const { t } = useTranslation();
return (
<>
<tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full text-sm">
<td
onClick={() => setPayloadOpened(!payloadOpened)}
className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer"
>
<FontAwesomeIcon
icon={payloadOpened ? faAngleDown : faAngleRight}
className={`mt-2.5 ml-6 text-bunker-100 hover:bg-mineshaft-700 ${
payloadOpened && 'bg-mineshaft-500'
} p-1 duration-100 h-4 w-4 rounded-md`}
/>
</td>
<td className="py-3 border-mineshaft-700 border-t text-gray-300">
{row.payload?.map(action => String(action.secretVersions.length) + " " + t("activity:event." + action.name)).join(" and ")}
</td>
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
{row.user}
</td>
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
{row.channel}
</td>
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
{timeSince(new Date(row.createdAt))}
</td>
</tr>
{payloadOpened &&
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
<td></td>
<td>Timestamp</td>
<td>{row.createdAt}</td>
</tr>}
{payloadOpened &&
row.payload?.map((action, index) =>
<tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
<td></td>
<td className="">{t("activity:event." + action.name)}</td>
<td className="text-primary-300 cursor-pointer hover:text-primary duration-200" onClick={() => toggleSidebar(action._id)}>
{action.secretVersions.length + (action.secretVersions.length != 1 ? " secrets" : " secret")}
<FontAwesomeIcon icon={faUpRightFromSquare} className="ml-2 mb-0.5 font-light w-3 h-3"/>
</td>
</tr>)}
{payloadOpened &&
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
<td></td>
<td>IP Address</td>
<td>{row.ipAddress}</td>
</tr>}
</>
);
};
/**
* This is the table for activity logs (one of the tabs)
* @param {object} obj
* @param {logData} obj.data - data for user activity logs
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
* @returns
*/
const ActivityTable = ({ data, toggleSidebar }: { data: logData[], toggleSidebar: (value: string) => void; }) => {
return (
<div className="w-full px-6 mt-8">
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative">
<div className="absolute rounded-t-md w-full h-[3rem] bg-white/5"></div>
<table className="w-full my-1">
<thead className="text-bunker-300">
<tr className='text-sm'>
<th className="text-left pl-6 pt-2.5 pb-3"></th>
<th className="text-left font-semibold pt-2.5 pb-3">EVENT</th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">USER</th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">SOURCE</th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">TIME</th>
<th></th>
</tr>
</thead>
<tbody>
{data?.map((row, index) => {
return <ActivityLogsRow key={index} row={row} toggleSidebar={toggleSidebar} />;
})}
</tbody>
</table>
</div>
</div>
);
};
export default ActivityTable;
@@ -0,0 +1,158 @@
import { useEffect, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import getProjectSecretShanpshots from "ee/api/secrets/GetProjectSercetShanpshots";
import getSecretSnapshotData from "ee/api/secrets/GetSecretSnapshotData";
import timeSince from "ee/utilities/timeSince";
import Button from "~/components/basic/buttons/Button";
import { decryptAssymmetric, decryptSymmetric } from "~/components/utilities/cryptography/crypto";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
interface SideBarProps {
toggleSidebar: (value: boolean) => void;
setSnapshotData: (value: any) => void;
chosenSnapshot: string;
}
interface SnaphotProps {
_id: string;
createdAt: string;
secretVersions: string[];
}
interface EncrypetedSecretVersionListProps {
_id: string;
createdAt: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
environment: string;
type: "personal" | "shared";
}
/**
* @param {object} obj
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
* @param {function} obj.setSnapshotData - state manager for snapshot data
* @param {string} obj.chosenSnaphshot - the snapshot id which is currently selected
*
*
* @returns the sidebar with the options for point-in-time recovery (commits)
*/
const PITRecoverySidebar = ({
toggleSidebar,
setSnapshotData,
chosenSnapshot
}: SideBarProps) => {
const { t } = useTranslation();
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [secretSnapshotsMetadata, setSecretSnapshotsMetadata] = useState<SnaphotProps[]>([]);
const [currentOffset, setCurrentOffset] = useState(0);
const currentLimit = 15;
const loadMoreSnapshots = () => {
setCurrentOffset(currentOffset + currentLimit);
}
useEffect(() => {
const getLogData = async () => {
setIsLoading(true);
const results = await getProjectSecretShanpshots({ workspaceId: String(router.query.id), limit: currentLimit, offset: currentOffset })
setSecretSnapshotsMetadata(secretSnapshotsMetadata.concat(results));
setIsLoading(false);
}
getLogData();
}, [currentOffset]);
const exploreSnapshot = async ({ snapshotId }: { snapshotId: string; }) => {
const secretSnapshotData = await getSecretSnapshotData({ secretSnapshotId: snapshotId });
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
let decryptedLatestKey: string;
if (latestKey) {
// assymmetrically decrypt symmetric key with local private key
decryptedLatestKey = decryptAssymmetric({
ciphertext: latestKey.latestKey.encryptedKey,
nonce: latestKey.latestKey.nonce,
publicKey: latestKey.latestKey.sender.publicKey,
privateKey: String(PRIVATE_KEY)
});
}
const decryptedSecretVersions = secretSnapshotData.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => {
return {
id: encryptedSecretVersion._id,
pos: pos,
type: encryptedSecretVersion.type,
environment: encryptedSecretVersion.environment,
key: decryptSymmetric({
ciphertext: encryptedSecretVersion.secretKeyCiphertext,
iv: encryptedSecretVersion.secretKeyIV,
tag: encryptedSecretVersion.secretKeyTag,
key: decryptedLatestKey
}),
value: decryptSymmetric({
ciphertext: encryptedSecretVersion.secretValueCiphertext,
iv: encryptedSecretVersion.secretValueIV,
tag: encryptedSecretVersion.secretValueTag,
key: decryptedLatestKey
})
}
})
setSnapshotData({ id: secretSnapshotData._id, createdAt: secretSnapshotData.createdAt, secretVersions: decryptedSecretVersions })
}
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between`}>
{isLoading ? (
<div className="flex items-center justify-center h-full mb-8">
<Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="infisical loading indicator"
></Image>
</div>
) : (
<div className='h-min overflow-y-auto'>
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
<p className="font-semibold text-lg text-bunker-200">{t("Point-in-time Recovery")}</p>
<div className='p-1' onClick={() => toggleSidebar(false)}>
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
</div>
</div>
<div className='flex flex-col px-2 py-2'>
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) => <div key={snapshot._id} className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "bg-primary text-black" : "bg-mineshaft-700"} py-3 px-4 mb-2 rounded-md flex flex-row justify-between items-center`}>
<div className="flex flex-row items-start">
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800" : "text-bunker-200"} text-sm mr-1.5`}>{timeSince(new Date(snapshot.createdAt))}</div>
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-900" : "text-bunker-300"} text-sm `}>{" - " + snapshot.secretVersions.length + " Secrets"}</div>
</div>
<div
onClick={() => exploreSnapshot({ snapshotId: snapshot._id })}
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800 pointer-events-none" : "text-bunker-200 hover:text-primary duration-200 cursor-pointer"} text-sm`}>
{id == 0 ? "Current Version" : chosenSnapshot == snapshot._id ? "Currently Viewing" : "Explore"}
</div>
</div>)}
<div className='flex justify-center w-full mb-14'>
<div className='items-center w-40'>
<Button text="View More" textDisabled="End of History" active={secretSnapshotsMetadata.length % 15 == 0 ? true : false} onButtonPressed={loadMoreSnapshots} size="md" color="mineshaft"/>
</div>
</div>
</div>
</div>
)}
</div>
};
export default PITRecoverySidebar;
+41 -26
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons'; import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons';
@@ -22,15 +23,18 @@ interface EncrypetedSecretVersionListProps {
/** /**
* @param {string} secretId - the id of a secret for which are querying version history
* @returns a list of versions for a specific secret * @returns a list of versions for a specific secret
*/ */
const SecretVersionList = ({ secretId }: { secretId: string; }) => { const SecretVersionList = ({ secretId }: { secretId: string; }) => {
const router = useRouter(); const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const [secretVersions, setSecretVersions] = useState<DecryptedSecretVersionListProps[]>([{createdAt: "123", value: "124"}]); const [secretVersions, setSecretVersions] = useState<DecryptedSecretVersionListProps[]>([]);
useEffect(() => { useEffect(() => {
const getSecretVersionHistory = async () => { const getSecretVersionHistory = async () => {
setIsLoading(true);
try { try {
const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10}); const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10});
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) }) const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
@@ -61,43 +65,54 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => {
}) })
setSecretVersions(decryptedSecretVersions); setSecretVersions(decryptedSecretVersions);
setIsLoading(false);
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} }
}; };
getSecretVersionHistory(); getSecretVersionHistory();
}, []); }, [secretId]);
return <div className='w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none'> return <div className='w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none'>
<p className=''>{t("dashboard:sidebar.version-history")}</p> <p className=''>{t("dashboard:sidebar.version-history")}</p>
<div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none'> <div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full'>
<div className='h-48 overflow-y-auto overflow-x-none'> {isLoading ? (
{secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt)) <div className="flex items-center justify-center h-full">
.map((version: DecryptedSecretVersionListProps, index: number) => <Image
<div key={index} className='flex flex-row'> src="/images/loading/loading.gif"
<div className='pr-1 flex flex-col items-center'> height={60}
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div> width={100}
<div className='w-0 h-full border-l mt-1'></div> alt="infisical loading indicator"
</div> ></Image>
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'> </div>
<div className='pr-2 pt-1'> ) : (
{(new Date(version.createdAt)).toLocaleDateString('en-US', { <div className='h-48 overflow-y-auto overflow-x-none'>
year: 'numeric', {secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
month: '2-digit', .map((version: DecryptedSecretVersionListProps, index: number) =>
day: '2-digit', <div key={index} className='flex flex-row'>
hour: '2-digit', <div className='pr-1 flex flex-col items-center'>
minute: '2-digit', <div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
second: '2-digit' <div className='w-0 h-full border-l mt-1'></div>
})} </div>
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
<div className='pr-2 pt-1'>
{(new Date(version.createdAt)).toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
</div>
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
</div>
</div> </div>
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div> )}
{/* <div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Updated by:</span>{version.user}</p></div> */}
</div>
</div> </div>
)} )}
</div> </div>
</div> </div>
</div>
}; };
export default SecretVersionList; export default SecretVersionList;
@@ -0,0 +1,346 @@
/**
*
* @param textOld - old secret
* @param textNew - new (updated) secret
* @param diffPlusFlag - a flag for whether we want to detect moving segments
* - doesn't work in some examples (e.g., when we have a full reverse ordering of the text)
* @returns
*/
function patienceDiff(textOld: string[], textNew: string[], diffPlusFlag?: boolean) {
/**
* findUnique finds all unique values in arr[lo..hi], inclusive. This
* function is used in preparation for determining the longest common
* subsequence. Specifically, it first reduces the array range in question
* to unique values.
* @param chars - an array of characters
* @param lo
* @param hi
* @returns - an ordered Map, with the arr[i] value as the Map key and the
* array index i as the Map value.
*/
function findUnique(chars: string[], lo: number, hi: number) {
const characterMap = new Map();
for (let i=lo; i<=hi; i++) {
const character = chars[i];
if (characterMap.has(character)) {
characterMap.get(character).count++;
characterMap.get(character).index = i;
} else {
characterMap.set(character, { count: 1, index: i });
}
}
characterMap.forEach((val, key, map) => {
if (val.count !== 1) {
map.delete(key);
} else {
map.set(key, val.index);
}
});
return characterMap;
}
/**
* @param aArray
* @param aLo
* @param aHi
* @param bArray
* @param bLo
* @param bHi
* @returns an ordered Map, with the Map key as the common line between aArray
* and bArray, with the Map value as an object containing the array indexes of
* the matching unique lines.
*
*/
function uniqueCommon(aArray: string[], aLo: number, aHi: number, bArray: string[], bLo: number, bHi: number) {
const ma = findUnique(aArray, aLo, aHi);
const mb = findUnique(bArray, bLo, bHi);
ma.forEach((val, key, map) => {
if (mb.has(key)) {
map.set(key, {
indexA: val,
indexB: mb.get(key)
});
} else {
map.delete(key);
}
});
return ma;
}
/**
* longestCommonSubsequence takes an ordered Map from the function uniqueCommon
* and determines the Longest Common Subsequence (LCS).
* @param abMap
* @returns an ordered array of objects containing the array indexes of the
* matching lines for a LCS.
*/
function longestCommonSubsequence(abMap: Map<number, { indexA: number, indexB: number, prev?: number }>) {
const ja: any = [];
// First, walk the list creating the jagged array.
abMap.forEach((val, key, map) => {
let i = 0;
while (ja[i] && ja[i][ja[i].length - 1].indexB < val.indexB) {
i++;
}
if (!ja[i]) {
ja[i] = [];
}
if (0 < i) {
val.prev = ja[i-1][ja[i - 1].length - 1];
}
ja[i].push(val);
});
// Now, pull out the longest common subsequence.
let lcs: any[] = [];
if (0 < ja.length) {
const n = ja.length - 1;
lcs = [ja[n][ja[n].length - 1]];
while (lcs[lcs.length - 1].prev) {
lcs.push(lcs[lcs.length - 1].prev);
}
}
return lcs.reverse();
}
// "result" is the array used to accumulate the textOld that are deleted, the
// lines that are shared between textOld and textNew, and the textNew that were
// inserted.
const result: any[] = [];
let deleted = 0;
let inserted = 0;
// aMove and bMove will contain the lines that don't match, and will be returned
// for possible searching of lines that moved.
const aMove: any[] = [];
const aMoveIndex: any[] = [];
const bMove: any[] = [];
const bMoveIndex: any[] = [];
/**
* addToResult simply pushes the latest value onto the "result" array. This
* array captures the diff of the line, aIndex, and bIndex from the textOld
* and textNew array.
* @param aIndex
* @param bIndex
*/
function addToResult(aIndex: number, bIndex: number) {
if (bIndex < 0) {
aMove.push(textOld[aIndex]);
aMoveIndex.push(result.length);
deleted++;
} else if (aIndex < 0) {
bMove.push(textNew[bIndex]);
bMoveIndex.push(result.length);
inserted++;
}
result.push({
line: 0 <= aIndex ? textOld[aIndex] : textNew[bIndex],
aIndex: aIndex,
bIndex: bIndex,
});
}
/**
* addSubMatch handles the lines between a pair of entries in the LCS. Thus,
* this function might recursively call recurseLCS to further match the lines
* between textOld and textNew.
* @param aLo
* @param aHi
* @param bLo
* @param bHi
*/
function addSubMatch(aLo: number, aHi: number, bLo: number, bHi: number) {
// Match any lines at the beginning of textOld and textNew.
while (aLo <= aHi && bLo <= bHi && textOld[aLo] === textNew[bLo]) {
addToResult(aLo++, bLo++);
}
// Match any lines at the end of textOld and textNew, but don't place them
// in the "result" array just yet, as the lines between these matches at
// the beginning and the end need to be analyzed first.
const aHiTemp = aHi;
while (aLo <= aHi && bLo <= bHi && textOld[aHi] === textNew[bHi]) {
aHi--;
bHi--;
}
// Now, check to determine with the remaining lines in the subsequence
// whether there are any unique common lines between textOld and textNew.
//
// If not, add the subsequence to the result (all textOld having been
// deleted, and all textNew having been inserted).
//
// If there are unique common lines between textOld and textNew, then let's
// recursively perform the patience diff on the subsequence.
const uniqueCommonMap = uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi);
if (uniqueCommonMap.size === 0) {
while (aLo <= aHi) {
addToResult(aLo++, -1);
}
while (bLo <= bHi) {
addToResult(-1, bLo++);
}
} else {
recurseLCS(aLo, aHi, bLo, bHi, uniqueCommonMap);
}
// Finally, let's add the matches at the end to the result.
while (aHi < aHiTemp) {
addToResult(++aHi, ++bHi);
}
}
/**
* recurseLCS finds the longest common subsequence (LCS) between the arrays
* textOld[aLo..aHi] and textNew[bLo..bHi] inclusive. Then for each subsequence
* recursively performs another LCS search (via addSubMatch), until there are
* none found, at which point the subsequence is dumped to the result.
* @param aLo
* @param aHi
* @param bLo
* @param bHi
* @param uniqueCommonMap
*/
function recurseLCS(aLo: number, aHi: number, bLo: number, bHi: number, uniqueCommonMap?: any) {
const x = longestCommonSubsequence(uniqueCommonMap || uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi));
if (x.length === 0) {
addSubMatch(aLo, aHi, bLo, bHi);
} else {
if (aLo < x[0].indexA || bLo < x[0].indexB) {
addSubMatch(aLo, x[0].indexA - 1, bLo, x[0].indexB - 1);
}
let i;
for (i = 0; i < x.length - 1; i++) {
addSubMatch(x[i].indexA, x[i+1].indexA - 1, x[i].indexB, x[i+1].indexB - 1);
}
if (x[i].indexA <= aHi || x[i].indexB <= bHi) {
addSubMatch(x[i].indexA, aHi, x[i].indexB, bHi);
}
}
}
recurseLCS(0, textOld.length - 1, 0, textNew.length - 1);
if (diffPlusFlag) {
return {
lines: result,
lineCountDeleted: deleted,
lineCountInserted: inserted,
lineCountMoved: 0,
aMove: aMove,
aMoveIndex: aMoveIndex,
bMove: bMove,
bMoveIndex: bMoveIndex,
};
}
return {
lines: result,
lineCountDeleted: deleted,
lineCountInserted: inserted,
lineCountMoved: 0,
};
}
/**
* use: patienceDiffPlus( textOld[], textNew[] )
*
* where:
* textOld[] contains the original text lines.
* textNew[] contains the new text lines.
*
* returns an object with the following properties:
* lines[] with properties of:
* line containing the line of text from textOld or textNew.
* aIndex referencing the index in aLine[].
* bIndex referencing the index in textNew[].
* (Note: The line is text from either textOld or textNew, with aIndex and bIndex
* referencing the original index. If aIndex === -1 then the line is new from textNew,
* and if bIndex === -1 then the line is old from textOld.)
* moved is true if the line was moved from elsewhere in textOld[] or textNew[].
* lineCountDeleted is the number of lines from textOld[] not appearing in textNew[].
* lineCountInserted is the number of lines from textNew[] not appearing in textOld[].
* lineCountMoved is the number of lines that moved.
*/
function patienceDiffPlus(textOld: string[], textNew: string[]) {
const difference = patienceDiff(textOld, textNew, true);
let aMoveNext = difference.aMove;
let aMoveIndexNext = difference.aMoveIndex;
let bMoveNext = difference.bMove;
let bMoveIndexNext = difference.bMoveIndex;
delete difference.aMove;
delete difference.aMoveIndex;
delete difference.bMove;
delete difference.bMoveIndex;
let lastLineCountMoved;
do {
const aMove = aMoveNext;
const aMoveIndex = aMoveIndexNext;
const bMove = bMoveNext;
const bMoveIndex = bMoveIndexNext;
aMoveNext = [];
aMoveIndexNext = [];
bMoveNext = [];
bMoveIndexNext = [];
const subDiff = patienceDiff(aMove!, bMove!);
lastLineCountMoved = difference.lineCountMoved;
subDiff.lines.forEach((v, i) => {
if (0 <= v.aIndex && 0 <= v.bIndex) {
difference.lines[aMoveIndex![v.aIndex]].moved = true;
difference.lines[bMoveIndex![v.bIndex]].aIndex = aMoveIndex![v.aIndex];
difference.lines[bMoveIndex![v.bIndex]].moved = true;
difference.lineCountInserted--;
difference.lineCountDeleted--;
difference.lineCountMoved++;
} else if (v.bIndex < 0) {
aMoveNext!.push(aMove![v.aIndex]);
aMoveIndexNext!.push(aMoveIndex![v.aIndex]);
} else {
bMoveNext!.push(bMove![v.bIndex]);
bMoveIndexNext!.push(bMoveIndex![v.bIndex]);
}
});
} while (0 < difference.lineCountMoved - lastLineCountMoved);
return difference;
}
export default patienceDiff;
+35
View File
@@ -0,0 +1,35 @@
/**
* Time since a certain date
* @param {Date} date - the timestamp got which we want to understand how long ago it happened
* @returns {String} text - how much time has passed since a certain timestamp
*/
function timeSince(date: Date) {
const seconds = Math.floor(
((new Date() as any) - (date as any)) / 1000
) as number;
let interval = seconds / 31536000;
if (interval > 1) {
return Math.floor(interval) + ' years ago';
}
interval = seconds / 2592000;
if (interval > 1) {
return Math.floor(interval) + ' months ago';
}
interval = seconds / 86400;
if (interval > 1) {
return Math.floor(interval) + ' days ago';
}
interval = seconds / 3600;
if (interval > 1) {
return Math.floor(interval) + ' hours ago';
}
interval = seconds / 60;
if (interval > 1) {
return Math.floor(interval) + ' minutes ago';
}
return Math.floor(seconds) + ' seconds ago';
}
export default timeSince;
+1925 -2171
View File
File diff suppressed because it is too large Load Diff
+145
View File
@@ -0,0 +1,145 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { useTranslation } from "next-i18next";
import ActivitySideBar from 'ee/components/ActivitySideBar';
import Button from '~/components/basic/buttons/Button';
import EventFilter from '~/components/basic/EventFilter';
import NavHeader from '~/components/navigation/NavHeader';
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
import getProjectLogs from '../../ee/api/secrets/GetProjectLogs';
import ActivityTable from '../../ee/components/ActivityTable';
interface logData {
_id: string;
channel: string;
createdAt: string;
ipAddress: string;
user: {
email: string;
};
actions: {
_id: string;
name: string;
payload: {
secretVersions: string[];
}
}[]
}
interface PayloadProps {
_id: string;
name: string;
secretVersions: string[];
}
interface logDataPoint {
_id: string;
channel: string;
createdAt: string;
ipAddress: string;
user: string;
payload: PayloadProps[];
}
/**
* This is the tab that includes all of the user activity logs
*/
export default function Activity() {
const router = useRouter();
const [eventChosen, setEventChosen] = useState('');
const [logsData, setLogsData] = useState<logDataPoint[]>([]);
const [currentOffset, setCurrentOffset] = useState(0);
const currentLimit = 10;
const [currentSidebarAction, toggleSidebar] = useState<string>()
const { t } = useTranslation();
// this use effect updates the data in case of a new filter being added
useEffect(() => {
setCurrentOffset(0);
const getLogData = async () => {
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: 0, limit: currentLimit, userId: "", actionNames: eventChosen })
setLogsData(tempLogsData.map((log: logData) => {
return {
_id: log._id,
channel: log.channel,
createdAt: log.createdAt,
ipAddress: log.ipAddress,
user: log.user.email,
payload: log.actions.map(action => {
return {
_id: action._id,
name: action.name,
secretVersions: action.payload.secretVersions
}
})
}
}))
}
getLogData();
}, [eventChosen]);
// this use effect adds more data in case 'View More' button is clicked
useEffect(() => {
const getLogData = async () => {
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: currentOffset, limit: currentLimit, userId: "", actionNames: eventChosen })
setLogsData(logsData.concat(tempLogsData.map((log: logData) => {
return {
_id: log._id,
channel: log.channel,
createdAt: log.createdAt,
ipAddress: log.ipAddress,
user: log.user.email,
payload: log.actions.map(action => {
return {
_id: action._id,
name: action.name,
secretVersions: action.payload.secretVersions
}
})
}
})))
}
getLogData();
}, [currentLimit, currentOffset]);
const loadMoreLogs = () => {
setCurrentOffset(currentOffset + currentLimit);
}
return (
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll h-screen">
<NavHeader pageName="Project Activity" isProjectRelated={true} />
{currentSidebarAction && <ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />}
<div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2">
<div className="flex flex-row justify-start items-center text-3xl">
<p className="font-semibold mr-4 text-bunker-100">Activity Logs</p>
</div>
<p className="mr-4 text-base text-gray-400">
Event history for this Infisical project.
</p>
</div>
<div className="px-6 h-8 mt-2">
<EventFilter
selected={eventChosen}
select={setEventChosen}
/>
</div>
<ActivityTable
data={logsData}
toggleSidebar={toggleSidebar}
/>
<div className='flex justify-center w-full mb-6'>
<div className='items-center w-60'>
<Button text="View More" textDisabled="End of History" active={logsData.length % 10 == 0 ? true : false} onButtonPressed={loadMoreLogs} size="md" color="mineshaft"/>
</div>
</div>
</div>
);
}
Activity.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(["activity"]);
@@ -5,14 +5,21 @@ interface Props {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
expiresIn: number; expiresIn: number;
publicKey: string;
encryptedKey: string; encryptedKey: string;
nonce: string; iv: string;
tag: string;
} }
/** /**
* This route gets service tokens for a specific user in a project * This route gets service tokens for a specific user in a project
* @param {*} param0 * @param {object} obj
* @param {string} obj.name - name of the service token
* @param {string} obj.workspaceId - workspace for which we are issuing the token
* @param {string} obj.environment - environment for which we are issuing the token
* @param {string} obj.expiresIn - how soon the service token expires in ms
* @param {string} obj.encryptedKey - encrypted project key through random symmetric encryption
* @param {string} obj.iv - obtained through symmetric encryption
* @param {string} obj.tag - obtained through symmetric encryption
* @returns * @returns
*/ */
const addServiceToken = ({ const addServiceToken = ({
@@ -20,11 +27,11 @@ const addServiceToken = ({
workspaceId, workspaceId,
environment, environment,
expiresIn, expiresIn,
publicKey,
encryptedKey, encryptedKey,
nonce iv,
tag
}: Props) => { }: Props) => {
return SecurityClient.fetchCall('/api/v1/service-token/', { return SecurityClient.fetchCall('/api/v2/service-token/', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -34,13 +41,13 @@ const addServiceToken = ({
workspaceId, workspaceId,
environment, environment,
expiresIn, expiresIn,
publicKey,
encryptedKey, encryptedKey,
nonce iv,
tag
}) })
}).then(async (res) => { }).then(async (res) => {
if (res && res.status == 200) { if (res && res.status == 200) {
return (await res.json()).token; return (await res.json());
} else { } else {
console.log('Failed to add service tokens'); console.log('Failed to add service tokens');
} }

Some files were not shown because too many files have changed in this diff Show More