mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 19:26:38 +00:00
Merge branch 'main' of https://github.com/Infisical/infisical
This commit is contained in:
Generated
+6
-6
@@ -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
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -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 };
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { getLogger } from '../utils/logger';
|
||||||
|
import { initDatabaseHelper } from '../helpers/database';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle database actions
|
||||||
|
*/
|
||||||
|
class DatabaseService {
|
||||||
|
static async initDatabase(MONGO_URL: string) {
|
||||||
|
return await initDatabaseHelper({
|
||||||
|
mongoURL: MONGO_URL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DatabaseService;
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
import { getLogger } from '../utils/logger';
|
|
||||||
|
|
||||||
export const initDatabase = (MONGO_URL: string) => {
|
|
||||||
mongoose
|
|
||||||
.connect(MONGO_URL)
|
|
||||||
.then(() => getLogger("database").info("Database connection established"))
|
|
||||||
.catch((e) => getLogger("database").error(`Unable to establish Database connection due to the error.\n${e}`));
|
|
||||||
return mongoose.connection;
|
|
||||||
};
|
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import DatabaseService from './DatabaseService';
|
||||||
import postHogClient from './PostHogClient';
|
import postHogClient from './PostHogClient';
|
||||||
import BotService from './BotService';
|
import BotService from './BotService';
|
||||||
import EventService from './EventService';
|
import EventService from './EventService';
|
||||||
import IntegrationService from './IntegrationService';
|
import IntegrationService from './IntegrationService';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
DatabaseService,
|
||||||
postHogClient,
|
postHogClient,
|
||||||
BotService,
|
BotService,
|
||||||
EventService,
|
EventService,
|
||||||
|
|||||||
Vendored
+1
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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=
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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{}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+38
-14
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
Generated
+1925
-2171
File diff suppressed because it is too large
Load Diff
@@ -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
Reference in New Issue
Block a user