mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'main' of https://github.com/Infisical/infisical
This commit is contained in:
12
backend/package-lock.json
generated
12
backend/package-lock.json
generated
@@ -6687,9 +6687,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz",
|
||||
"integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
@@ -17198,9 +17198,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true
|
||||
},
|
||||
"jsonwebtoken": {
|
||||
|
||||
@@ -13,7 +13,9 @@ import { apiLimiter } from './helpers/rateLimiter';
|
||||
|
||||
import {
|
||||
workspace as eeWorkspaceRouter,
|
||||
secret as eeSecretRouter
|
||||
secret as eeSecretRouter,
|
||||
secretSnapshot as eeSecretSnapshotRouter,
|
||||
action as eeActionRouter
|
||||
} from './ee/routes/v1';
|
||||
import {
|
||||
signup as v1SignupRouter,
|
||||
@@ -70,7 +72,9 @@ if (NODE_ENV === 'production') {
|
||||
|
||||
// (EE) routes
|
||||
app.use('/api/v1/secret', eeSecretRouter);
|
||||
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
|
||||
app.use('/api/v1/workspace', eeWorkspaceRouter);
|
||||
app.use('/api/v1/action', eeActionRouter);
|
||||
|
||||
// v1 routes
|
||||
app.use('/api/v1/signup', v1SignupRouter);
|
||||
|
||||
@@ -123,7 +123,9 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
secrets = await pull({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId,
|
||||
environment
|
||||
environment,
|
||||
channel: channel ? channel : 'cli',
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
key = await Key.findOne({
|
||||
@@ -188,7 +190,9 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
secrets = await pull({
|
||||
userId: req.serviceToken.user._id.toString(),
|
||||
workspaceId,
|
||||
environment
|
||||
environment,
|
||||
channel: 'cli',
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
key = {
|
||||
|
||||
@@ -2,7 +2,7 @@ import to from "await-to-js";
|
||||
import { Request, Response } from "express";
|
||||
import mongoose, { Types } from "mongoose";
|
||||
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;
|
||||
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
|
||||
import { AnyBulkWriteOperation } from 'mongodb';
|
||||
|
||||
@@ -11,10 +11,6 @@ import {
|
||||
ServiceToken,
|
||||
ServiceTokenData
|
||||
} from '../../models';
|
||||
import {
|
||||
createWorkspace as create,
|
||||
deleteWorkspace as deleteWork
|
||||
} from '../../helpers/workspace';
|
||||
import {
|
||||
v2PushSecrets as push,
|
||||
pullSecrets as pull,
|
||||
@@ -50,7 +46,6 @@ interface V2PushSecret {
|
||||
*/
|
||||
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
|
||||
try {
|
||||
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
||||
const { keys, environment, channel } = req.body;
|
||||
@@ -70,7 +65,9 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets
|
||||
secrets,
|
||||
channel: channel ? channel : 'cli',
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
await pushKeys({
|
||||
@@ -136,7 +133,9 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
secrets = await pull({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment
|
||||
environment,
|
||||
channel: channel ? channel : 'cli',
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
if (channel !== 'cli') {
|
||||
@@ -196,7 +195,7 @@ export const getWorkspaceServiceTokenData = async (
|
||||
) => {
|
||||
let serviceTokenData;
|
||||
try {
|
||||
const { workspaceId } = req.query;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
serviceTokenData = await ServiceTokenData
|
||||
.find({
|
||||
|
||||
31
backend/src/ee/controllers/v1/actionController.ts
Normal file
31
backend/src/ee/controllers/v1/actionController.ts
Normal file
@@ -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 secretController from './secretController';
|
||||
import * as secretSnapshotController from './secretSnapshotController';
|
||||
import * as workspaceController from './workspaceController';
|
||||
import * as actionController from './actionController';
|
||||
|
||||
export {
|
||||
stripeController,
|
||||
secretController,
|
||||
workspaceController
|
||||
secretSnapshotController,
|
||||
workspaceController,
|
||||
actionController
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { SecretVersion } from '../../models';
|
||||
secretVersions = await SecretVersion.find({
|
||||
secret: secretId
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(offset)
|
||||
.limit(limit);
|
||||
|
||||
|
||||
27
backend/src/ee/controllers/v1/secretSnapshotController.ts
Normal file
27
backend/src/ee/controllers/v1/secretSnapshotController.ts
Normal file
@@ -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 { SecretSnapshot } from '../../models';
|
||||
import {
|
||||
SecretSnapshot,
|
||||
Log
|
||||
} from '../../models';
|
||||
|
||||
/**
|
||||
* Return secret snapshots for workspace with id [workspaceId]
|
||||
@@ -18,6 +21,7 @@ import { SecretSnapshot } from '../../models';
|
||||
secretSnapshots = await SecretSnapshot.find({
|
||||
workspace: workspaceId
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(offset)
|
||||
.limit(limit);
|
||||
|
||||
@@ -32,4 +36,77 @@ import { SecretSnapshot } from '../../models';
|
||||
return res.status(200).send({
|
||||
secretSnapshots
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return count of secret snapshots for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => {
|
||||
let count;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
count = await SecretSnapshot.countDocuments({
|
||||
workspace: workspaceId
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to count number of secret snapshots'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
count
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return (audit) logs for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceLogs = async (req: Request, res: Response) => {
|
||||
let logs
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
const offset: number = parseInt(req.query.offset as string);
|
||||
const limit: number = parseInt(req.query.limit as string);
|
||||
const sortBy: string = req.query.sortBy as string;
|
||||
const userId: string = req.query.userId as string;
|
||||
const actionNames: string = req.query.actionNames as string;
|
||||
|
||||
logs = await Log.find({
|
||||
workspace: workspaceId,
|
||||
...( userId ? { user: userId } : {}),
|
||||
...(
|
||||
actionNames
|
||||
? {
|
||||
actionNames: {
|
||||
$in: actionNames.split(',')
|
||||
}
|
||||
} : {}
|
||||
)
|
||||
})
|
||||
.sort({ createdAt: sortBy === 'recent' ? -1 : 1 })
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
.populate('actions')
|
||||
.populate('user');
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get workspace logs'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
logs
|
||||
});
|
||||
}
|
||||
112
backend/src/ee/helpers/action.ts
Normal file
112
backend/src/ee/helpers/action.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { Secret } from '../../models';
|
||||
import { SecretVersion, Action } from '../models';
|
||||
import { ACTION_UPDATE_SECRETS } from '../../variables';
|
||||
|
||||
/**
|
||||
* Create an (audit) action for secrets including
|
||||
* add, delete, update, and read actions.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.name - name of action
|
||||
* @param {ObjectId[]} obj.secretIds - ids of relevant secrets
|
||||
* @returns {Action} action - new action
|
||||
*/
|
||||
const createActionSecretHelper = async ({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
}: {
|
||||
name: string;
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
secretIds: Types.ObjectId[];
|
||||
}) => {
|
||||
|
||||
let action;
|
||||
let latestSecretVersions;
|
||||
try {
|
||||
if (name === ACTION_UPDATE_SECRETS) {
|
||||
// case: action is updating secrets
|
||||
// -> add old and new secret versions
|
||||
|
||||
// TODO: make query more efficient
|
||||
latestSecretVersions = (await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
secret: {
|
||||
$in: secretIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { version: -1 },
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$secret",
|
||||
versions: { $push: "$$ROOT" },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
secret: "$_id",
|
||||
versions: { $slice: ["$versions", 2] },
|
||||
},
|
||||
}
|
||||
]))
|
||||
.map((s) => ({
|
||||
oldSecretVersion: s.versions[0]._id,
|
||||
newSecretVersion: s.versions[1]._id
|
||||
}));
|
||||
|
||||
|
||||
} else {
|
||||
// case: action is adding, deleting, or reading secrets
|
||||
// -> add new secret versions
|
||||
latestSecretVersions = (await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
secret: {
|
||||
$in: secretIds
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$secret',
|
||||
version: { $max: '$version' },
|
||||
versionId: { $max: '$_id' } // secret version id
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { version: -1 }
|
||||
}
|
||||
])
|
||||
.exec())
|
||||
.map((s) => ({
|
||||
newSecretVersion: s.versionId
|
||||
}));
|
||||
}
|
||||
|
||||
action = await new Action({
|
||||
name,
|
||||
user: userId,
|
||||
workspace: workspaceId,
|
||||
payload: {
|
||||
secretVersions: latestSecretVersions
|
||||
}
|
||||
}).save();
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create action');
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
export { createActionSecretHelper };
|
||||
41
backend/src/ee/helpers/log.ts
Normal file
41
backend/src/ee/helpers/log.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Log,
|
||||
IAction
|
||||
} from '../models';
|
||||
|
||||
const createLogHelper = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
actions: IAction[];
|
||||
channel: string;
|
||||
ipAddress: string;
|
||||
}) => {
|
||||
let log;
|
||||
try {
|
||||
log = await new Log({
|
||||
user: userId,
|
||||
workspace: workspaceId,
|
||||
actionNames: actions.map((a) => a.name),
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create log');
|
||||
}
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
export {
|
||||
createLogHelper
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Types } from 'mongoose';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Secret
|
||||
Secret,
|
||||
ISecret
|
||||
} from '../../models';
|
||||
import {
|
||||
SecretSnapshot,
|
||||
@@ -9,66 +11,159 @@ import {
|
||||
} 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
|
||||
* secretsnapshots collection.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId
|
||||
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
|
||||
*/
|
||||
const takeSecretSnapshotHelper = async ({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
|
||||
let secretSnapshot;
|
||||
try {
|
||||
const secrets = await Secret.find({
|
||||
const secretIds = (await Secret.find({
|
||||
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({
|
||||
workspace: workspaceId
|
||||
}).sort({ version: -1 });
|
||||
|
||||
if (!latestSecretSnapshot) {
|
||||
// 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({
|
||||
secretSnapshot = await new SecretSnapshot({
|
||||
workspace: workspaceId,
|
||||
version: latestSecretSnapshot.version + 1,
|
||||
secrets
|
||||
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
|
||||
secretVersions: latestSecretVersions
|
||||
}).save();
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
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 ({
|
||||
secretVersions
|
||||
}: {
|
||||
secretVersions: ISecretVersion[]
|
||||
}) => {
|
||||
let newSecretVersions;
|
||||
try {
|
||||
await SecretVersion.insertMany(secretVersions);
|
||||
newSecretVersions = await SecretVersion.insertMany(secretVersions);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
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 {
|
||||
takeSecretSnapshotHelper,
|
||||
addSecretVersionsHelper
|
||||
addSecretVersionsHelper,
|
||||
markDeletedSecretVersionsHelper,
|
||||
initSecretVersioningHelper
|
||||
}
|
||||
7
backend/src/ee/middleware/index.ts
Normal file
7
backend/src/ee/middleware/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import requireLicenseAuth from './requireLicenseAuth';
|
||||
import requireSecretSnapshotAuth from './requireSecretSnapshotAuth';
|
||||
|
||||
export {
|
||||
requireLicenseAuth,
|
||||
requireSecretSnapshotAuth
|
||||
}
|
||||
47
backend/src/ee/middleware/requireSecretSnapshotAuth.ts
Normal file
47
backend/src/ee/middleware/requireSecretSnapshotAuth.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { UnauthorizedRequestError, SecretSnapshotNotFoundError } from '../../utils/errors';
|
||||
import { SecretSnapshot } from '../models';
|
||||
import {
|
||||
validateMembership
|
||||
} from '../../helpers/membership';
|
||||
|
||||
/**
|
||||
* Validate if user on request has proper membership for secret snapshot
|
||||
* @param {Object} obj
|
||||
* @param {String[]} obj.acceptedRoles - accepted workspace roles
|
||||
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
|
||||
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
|
||||
*/
|
||||
const requireSecretSnapshotAuth = ({
|
||||
acceptedRoles,
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { secretSnapshotId } = req.params;
|
||||
|
||||
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
|
||||
|
||||
if (!secretSnapshot) {
|
||||
return next(SecretSnapshotNotFoundError({
|
||||
message: 'Failed to find secret snapshot'
|
||||
}));
|
||||
}
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: secretSnapshot.workspace.toString(),
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
req.secretSnapshot = secretSnapshot as any;
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret snapshot' }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default requireSecretSnapshotAuth;
|
||||
46
backend/src/ee/models/action.ts
Normal file
46
backend/src/ee/models/action.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
|
||||
export interface IAction {
|
||||
name: string;
|
||||
user?: Types.ObjectId,
|
||||
workspace?: Types.ObjectId,
|
||||
payload: {
|
||||
secretVersions?: Types.ObjectId[]
|
||||
}
|
||||
}
|
||||
|
||||
const actionSchema = new Schema<IAction>(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace'
|
||||
},
|
||||
payload: {
|
||||
secretVersions: [{
|
||||
oldSecretVersion: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'SecretVersion'
|
||||
},
|
||||
newSecretVersion: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'SecretVersion'
|
||||
}
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const Action = model<IAction>('Action', actionSchema);
|
||||
|
||||
export default Action;
|
||||
@@ -1,9 +1,15 @@
|
||||
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot";
|
||||
import SecretVersion, { ISecretVersion } from "./secretVersion";
|
||||
import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot';
|
||||
import SecretVersion, { ISecretVersion } from './secretVersion';
|
||||
import Log, { ILog } from './log';
|
||||
import Action, { IAction } from './action';
|
||||
|
||||
export {
|
||||
SecretSnapshot,
|
||||
ISecretSnapshot,
|
||||
SecretVersion,
|
||||
ISecretVersion
|
||||
ISecretVersion,
|
||||
Log,
|
||||
ILog,
|
||||
Action,
|
||||
IAction
|
||||
}
|
||||
59
backend/src/ee/models/log.ts
Normal file
59
backend/src/ee/models/log.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_READ_SECRETS,
|
||||
ACTION_DELETE_SECRETS
|
||||
} from '../../variables';
|
||||
|
||||
export interface ILog {
|
||||
_id: Types.ObjectId;
|
||||
user?: Types.ObjectId;
|
||||
workspace?: Types.ObjectId;
|
||||
actionNames: string[];
|
||||
actions: Types.ObjectId[];
|
||||
channel: string;
|
||||
ipAddress?: string;
|
||||
}
|
||||
|
||||
const logSchema = new Schema<ILog>(
|
||||
{
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace'
|
||||
},
|
||||
actionNames: {
|
||||
type: [String],
|
||||
enum: [
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_READ_SECRETS,
|
||||
ACTION_DELETE_SECRETS
|
||||
],
|
||||
required: true
|
||||
},
|
||||
actions: [{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Action',
|
||||
required: true
|
||||
}],
|
||||
channel: {
|
||||
type: String,
|
||||
enum: ['web', 'cli', 'auto'],
|
||||
required: true
|
||||
},
|
||||
ipAddress: {
|
||||
type: String
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const Log = model<ILog>('Log', logSchema);
|
||||
|
||||
export default Log;
|
||||
@@ -1,31 +1,9 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD
|
||||
} from '../../variables';
|
||||
|
||||
export interface ISecretSnapshot {
|
||||
workspace: Types.ObjectId;
|
||||
version: number;
|
||||
secrets: {
|
||||
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;
|
||||
}[]
|
||||
secretVersions: Types.ObjectId[];
|
||||
}
|
||||
|
||||
const secretSnapshotSchema = new Schema<ISecretSnapshot>(
|
||||
@@ -39,64 +17,10 @@ const secretSnapshotSchema = new Schema<ISecretSnapshot>(
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
secrets: [{
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
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
|
||||
}
|
||||
secretVersions: [{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'SecretVersion',
|
||||
required: true
|
||||
}]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
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 {
|
||||
_id?: Types.ObjectId;
|
||||
secret: Types.ObjectId;
|
||||
version: number;
|
||||
workspace: Types.ObjectId; // new
|
||||
type: string; // new
|
||||
user: Types.ObjectId; // new
|
||||
environment: string; // new
|
||||
isDeleted: boolean;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
@@ -27,6 +48,26 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
default: 1,
|
||||
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: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
||||
17
backend/src/ee/routes/v1/action.ts
Normal file
17
backend/src/ee/routes/v1/action.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
validateRequest
|
||||
} from '../../../middleware';
|
||||
import { param } from 'express-validator';
|
||||
import { actionController } from '../../controllers/v1';
|
||||
|
||||
// TODO: put into action controller
|
||||
router.get(
|
||||
'/:actionId',
|
||||
param('actionId').exists().trim(),
|
||||
validateRequest,
|
||||
actionController.getAction
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -1,7 +1,11 @@
|
||||
import secret from './secret';
|
||||
import secretSnapshot from './secretSnapshot';
|
||||
import workspace from './workspace';
|
||||
import action from './action';
|
||||
|
||||
export {
|
||||
secret,
|
||||
workspace
|
||||
secretSnapshot,
|
||||
workspace,
|
||||
action
|
||||
}
|
||||
27
backend/src/ee/routes/v1/secretSnapshot.ts
Normal file
27
backend/src/ee/routes/v1/secretSnapshot.ts
Normal file
@@ -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
|
||||
);
|
||||
|
||||
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;
|
||||
81
backend/src/ee/services/EELogService.ts
Normal file
81
backend/src/ee/services/EELogService.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Log,
|
||||
Action,
|
||||
IAction
|
||||
} from '../models';
|
||||
import {
|
||||
createLogHelper
|
||||
} from '../helpers/log';
|
||||
import {
|
||||
createActionSecretHelper
|
||||
} from '../helpers/action';
|
||||
import EELicenseService from './EELicenseService';
|
||||
|
||||
/**
|
||||
* Class to handle Enterprise Edition log actions
|
||||
*/
|
||||
class EELogService {
|
||||
/**
|
||||
* Create an (audit) log
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.userId - id of user associated with the log
|
||||
* @param {String} obj.workspaceId - id of workspace associated with the log
|
||||
* @param {Action} obj.actions - actions to include in log
|
||||
* @param {String} obj.channel - channel (web/cli/auto) associated with the log
|
||||
* @param {String} obj.ipAddress - ip address associated with the log
|
||||
* @returns {Log} log - new audit log
|
||||
*/
|
||||
static async createLog({
|
||||
userId,
|
||||
workspaceId,
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
actions: IAction[];
|
||||
channel: string;
|
||||
ipAddress: string;
|
||||
}) {
|
||||
if (!EELicenseService.isLicenseValid) return null;
|
||||
return await createLogHelper({
|
||||
userId,
|
||||
workspaceId,
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an (audit) action for secrets including
|
||||
* add, delete, update, and read actions.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.name - name of action
|
||||
* @param {ObjectId[]} obj.secretIds - secret ids
|
||||
* @returns {Action} action - new action
|
||||
*/
|
||||
static async createActionSecret({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
}: {
|
||||
name: string;
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
secretIds: Types.ObjectId[];
|
||||
}) {
|
||||
if (!EELicenseService.isLicenseValid) return null;
|
||||
return await createActionSecretHelper({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default EELogService;
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Types } from 'mongoose';
|
||||
import { ISecretVersion } from '../models';
|
||||
import {
|
||||
takeSecretSnapshotHelper,
|
||||
addSecretVersionsHelper
|
||||
addSecretVersionsHelper,
|
||||
markDeletedSecretVersionsHelper,
|
||||
initSecretVersioningHelper
|
||||
} from '../helpers/secret';
|
||||
import EELicenseService from './EELicenseService';
|
||||
|
||||
@@ -11,12 +14,13 @@ import EELicenseService from './EELicenseService';
|
||||
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
|
||||
* SecretSnapshot collection.
|
||||
* Requires a valid license key [licenseKey]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId
|
||||
* @returns {SecretSnapshot} secretSnapshot - new secret snpashot
|
||||
*/
|
||||
static async takeSecretSnapshot({
|
||||
workspaceId
|
||||
@@ -24,13 +28,14 @@ class EESecretService {
|
||||
workspaceId: string;
|
||||
}) {
|
||||
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 {SecretVersion} obj.secretVersions
|
||||
* @param {Object[]} obj.secretVersions
|
||||
* @returns {SecretVersion[]} newSecretVersions - new secret versions
|
||||
*/
|
||||
static async addSecretVersions({
|
||||
secretVersions
|
||||
@@ -38,10 +43,36 @@ class EESecretService {
|
||||
secretVersions: ISecretVersion[];
|
||||
}) {
|
||||
if (!EELicenseService.isLicenseValid) return;
|
||||
await addSecretVersionsHelper({
|
||||
return await addSecretVersionsHelper({
|
||||
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;
|
||||
@@ -1,7 +1,9 @@
|
||||
import EELicenseService from "./EELicenseService";
|
||||
import EESecretService from "./EESecretService";
|
||||
import EELogService from "./EELogService";
|
||||
|
||||
export {
|
||||
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 {
|
||||
ciphertextKey: string;
|
||||
@@ -19,7 +22,7 @@ interface PushSecret {
|
||||
* @returns
|
||||
*/
|
||||
const eventPushSecrets = ({
|
||||
workspaceId,
|
||||
workspaceId
|
||||
}: {
|
||||
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 {
|
||||
eventPushSecrets
|
||||
}
|
||||
|
||||
31
backend/src/helpers/database.ts
Normal file
31
backend/src/helpers/database.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { ISecret, Secret } from '../models';
|
||||
import { EESecretService } from '../ee/services';
|
||||
import { getLogger } from '../utils/logger';
|
||||
|
||||
/**
|
||||
* Initialize database connection
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.mongoURL - mongo connection string
|
||||
* @returns
|
||||
*/
|
||||
const initDatabaseHelper = async ({
|
||||
mongoURL
|
||||
}: {
|
||||
mongoURL: string;
|
||||
}) => {
|
||||
try {
|
||||
await mongoose.connect(mongoURL);
|
||||
getLogger("database").info("Database connection established");
|
||||
|
||||
await EESecretService.initSecretVersioning();
|
||||
} catch (err) {
|
||||
getLogger("database").error(`Unable to establish Database connection due to the error.\n${err}`);
|
||||
}
|
||||
|
||||
return mongoose.connection;
|
||||
}
|
||||
|
||||
export {
|
||||
initDatabaseHelper
|
||||
}
|
||||
@@ -1,19 +1,24 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Secret,
|
||||
ISecret,
|
||||
} from '../models';
|
||||
import {
|
||||
EESecretService
|
||||
EESecretService,
|
||||
EELogService
|
||||
} from '../ee/services';
|
||||
import {
|
||||
SecretVersion
|
||||
IAction
|
||||
} from '../ee/models';
|
||||
import {
|
||||
takeSecretSnapshotHelper
|
||||
} from '../ee/helpers/secret';
|
||||
import { decryptSymmetric } from '../utils/crypto';
|
||||
import { SECRET_SHARED, SECRET_PERSONAL } from '../variables';
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_DELETE_SECRETS,
|
||||
ACTION_READ_SECRETS
|
||||
} from '../variables';
|
||||
|
||||
interface V1PushSecret {
|
||||
ciphertextKey: string;
|
||||
@@ -51,8 +56,6 @@ interface Update {
|
||||
[index: string]: any;
|
||||
}
|
||||
|
||||
type DecryptSecretType = 'text' | 'object' | 'expanded';
|
||||
|
||||
/**
|
||||
* Push secrets for user with id [userId] to workspace
|
||||
* with id [workspaceId] with environment [environment]. Follow steps:
|
||||
@@ -68,7 +71,7 @@ const v1PushSecrets = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets
|
||||
secrets,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
@@ -78,7 +81,7 @@ const v1PushSecrets = async ({
|
||||
// TODO: clean up function and fix up types
|
||||
try {
|
||||
// construct useful data structures
|
||||
const oldSecrets = await pullSecrets({
|
||||
const oldSecrets = await getSecrets({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment
|
||||
@@ -101,11 +104,9 @@ const v1PushSecrets = async ({
|
||||
await Secret.deleteMany({
|
||||
_id: { $in: toDelete }
|
||||
});
|
||||
|
||||
await SecretVersion.updateMany({
|
||||
secret: { $in: toDelete }
|
||||
}, {
|
||||
isDeleted: true
|
||||
|
||||
await EESecretService.markDeletedSecretVersions({
|
||||
secretIds: toDelete
|
||||
});
|
||||
}
|
||||
|
||||
@@ -188,6 +189,10 @@ const v1PushSecrets = async ({
|
||||
return ({
|
||||
secret: _id,
|
||||
version: version ? version + 1 : 1,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
type: newSecret.type,
|
||||
user: new Types.ObjectId(userId),
|
||||
environment,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext: newSecret.ciphertextKey,
|
||||
secretKeyIV: newSecret.ivKey,
|
||||
@@ -239,6 +244,11 @@ const v1PushSecrets = async ({
|
||||
EESecretService.addSecretVersions({
|
||||
secretVersions: newSecrets.map(({
|
||||
_id,
|
||||
version,
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
@@ -249,7 +259,11 @@ const v1PushSecrets = async ({
|
||||
secretValueHash
|
||||
}) => ({
|
||||
secret: _id,
|
||||
version: 1,
|
||||
version,
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
@@ -284,22 +298,30 @@ const v1PushSecrets = async ({
|
||||
* @param {String} obj.workspaceId - id of workspace to push to
|
||||
* @param {String} obj.environment - environment for secrets
|
||||
* @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 ({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets
|
||||
secrets,
|
||||
channel,
|
||||
ipAddress
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secrets: V2PushSecret[];
|
||||
channel: string;
|
||||
ipAddress: string;
|
||||
}): Promise<void> => {
|
||||
// TODO: clean up function and fix up types
|
||||
try {
|
||||
const actions: IAction[] = [];
|
||||
|
||||
// construct useful data structures
|
||||
const oldSecrets = await pullSecrets({
|
||||
const oldSecrets = await getSecrets({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment
|
||||
@@ -322,12 +344,19 @@ const v1PushSecrets = async ({
|
||||
await Secret.deleteMany({
|
||||
_id: { $in: toDelete }
|
||||
});
|
||||
|
||||
await SecretVersion.updateMany({
|
||||
secret: { $in: toDelete }
|
||||
}, {
|
||||
isDeleted: true
|
||||
|
||||
await EESecretService.markDeletedSecretVersions({
|
||||
secretIds: toDelete
|
||||
});
|
||||
|
||||
const deleteAction = await EELogService.createActionSecret({
|
||||
name: ACTION_DELETE_SECRETS,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds: toDelete
|
||||
});
|
||||
|
||||
deleteAction && actions.push(deleteAction);
|
||||
}
|
||||
|
||||
const toUpdate = oldSecrets
|
||||
@@ -348,118 +377,10 @@ const v1PushSecrets = async ({
|
||||
return false;
|
||||
});
|
||||
|
||||
const operations = toUpdate
|
||||
.map((s) => {
|
||||
const {
|
||||
secretValueCiphertext,
|
||||
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,
|
||||
if (toUpdate.length > 0) {
|
||||
const operations = toUpdate
|
||||
.map((s) => {
|
||||
const {
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
@@ -467,49 +388,120 @@ const v1PushSecrets = async ({
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash
|
||||
};
|
||||
secretCommentHash,
|
||||
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
||||
|
||||
if (toAdd[idx].type === 'personal') {
|
||||
obj['user' as keyof typeof obj] = userId;
|
||||
const update: Update = {
|
||||
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
|
||||
EESecretService.addSecretVersions({
|
||||
secretVersions: newSecrets.map(({
|
||||
_id,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}) => ({
|
||||
secret: _id,
|
||||
version: 1,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}))
|
||||
secretVersions: newSecrets.map((secretDocument) => {
|
||||
return {
|
||||
...secretDocument.toObject(),
|
||||
secret: secretDocument._id,
|
||||
isDeleted: false
|
||||
}})
|
||||
});
|
||||
|
||||
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
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId
|
||||
})
|
||||
|
||||
// (EE) create (audit) log
|
||||
if (actions.length > 0) {
|
||||
await EELogService.createLog({
|
||||
userId,
|
||||
workspaceId,
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
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]
|
||||
* @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
|
||||
*
|
||||
*/
|
||||
const pullSecrets = async ({
|
||||
const getSecrets = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment
|
||||
@@ -563,9 +554,64 @@ const pullSecrets = async ({
|
||||
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
|
||||
* clients handle secrets
|
||||
* web client handle secrets
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.secrets
|
||||
*/
|
||||
|
||||
@@ -4,12 +4,12 @@ dotenv.config();
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config';
|
||||
import { server } from './app';
|
||||
import { initDatabase } from './services/database';
|
||||
import { DatabaseService } from './services';
|
||||
import { setUpHealthEndpoint } from './services/health';
|
||||
import { initSmtp } from './services/smtp';
|
||||
import { setTransporter } from './helpers/nodemailer';
|
||||
|
||||
initDatabase(MONGO_URL);
|
||||
DatabaseService.initDatabase(MONGO_URL);
|
||||
|
||||
setUpHealthEndpoint(server);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface IUser {
|
||||
tag?: string;
|
||||
salt?: string;
|
||||
verifier?: string;
|
||||
refreshVersion?: Number;
|
||||
refreshVersion?: number;
|
||||
}
|
||||
|
||||
const userSchema = new Schema<IUser>(
|
||||
@@ -52,7 +52,8 @@ const userSchema = new Schema<IUser>(
|
||||
},
|
||||
refreshVersion: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 0,
|
||||
select: false
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { body, query } from 'express-validator';
|
||||
import { userActionController } from '../../controllers/v1';
|
||||
|
||||
// note: [userAction] will be deprecated in /v2 in favor of [action]
|
||||
router.post(
|
||||
'/',
|
||||
requireAuth({
|
||||
|
||||
@@ -2,7 +2,7 @@ import express, { Request, Response } from 'express';
|
||||
import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware';
|
||||
import { body, param, query } from 'express-validator';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret/types';
|
||||
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
|
||||
import { secretController } from '../../controllers/v2';
|
||||
import { fetchAllSecrets } from '../../controllers/v2/secretController';
|
||||
|
||||
|
||||
16
backend/src/services/DatabaseService.ts
Normal file
16
backend/src/services/DatabaseService.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { getLogger } from '../utils/logger';
|
||||
import { initDatabaseHelper } from '../helpers/database';
|
||||
|
||||
/**
|
||||
* Class to handle database actions
|
||||
*/
|
||||
class DatabaseService {
|
||||
static async initDatabase(MONGO_URL: string) {
|
||||
return await initDatabaseHelper({
|
||||
mongoURL: MONGO_URL
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default DatabaseService;
|
||||
@@ -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 BotService from './BotService';
|
||||
import EventService from './EventService';
|
||||
import IntegrationService from './IntegrationService';
|
||||
|
||||
export {
|
||||
DatabaseService,
|
||||
postHogClient,
|
||||
BotService,
|
||||
EventService,
|
||||
|
||||
1
backend/src/types/express/index.d.ts
vendored
1
backend/src/types/express/index.d.ts
vendored
@@ -13,6 +13,7 @@ declare global {
|
||||
integrationAuth: any;
|
||||
bot: any;
|
||||
secret: any;
|
||||
secretSnapshot: any;
|
||||
serviceToken: any;
|
||||
accessToken: any;
|
||||
serviceTokenData: any;
|
||||
|
||||
@@ -123,6 +123,16 @@ export const SecretNotFoundError = (error?: Partial<RequestErrorContext>) => new
|
||||
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]<-----
|
||||
export const ActionNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||
|
||||
11
backend/src/variables/action.ts
Normal file
11
backend/src/variables/action.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
const ACTION_ADD_SECRETS = 'addSecrets';
|
||||
const ACTION_DELETE_SECRETS = 'deleteSecrets';
|
||||
const ACTION_UPDATE_SECRETS = 'updateSecrets';
|
||||
const ACTION_READ_SECRETS = 'readSecrets';
|
||||
|
||||
export {
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_DELETE_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_READ_SECRETS
|
||||
}
|
||||
@@ -31,6 +31,12 @@ import {
|
||||
} from './organization';
|
||||
import { SECRET_SHARED, SECRET_PERSONAL } from './secret';
|
||||
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 { PLAN_STARTER, PLAN_PRO } from './stripe';
|
||||
|
||||
@@ -63,6 +69,10 @@ export {
|
||||
INTEGRATION_GITHUB_API_URL,
|
||||
EVENT_PUSH_SECRETS,
|
||||
EVENT_PULL_SECRETS,
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_DELETE_SECRETS,
|
||||
ACTION_READ_SECRETS,
|
||||
INTEGRATION_OPTIONS,
|
||||
SMTP_HOST_SENDGRID,
|
||||
SMTP_HOST_MAILGUN,
|
||||
|
||||
@@ -48,7 +48,7 @@ const INTEGRATION_OPTIONS = [
|
||||
name: 'Vercel',
|
||||
slug: 'vercel',
|
||||
image: 'Vercel',
|
||||
isAvailable: true,
|
||||
isAvailable: false,
|
||||
type: 'vercel',
|
||||
clientId: '',
|
||||
clientSlug: CLIENT_SLUG_VERCEL,
|
||||
@@ -58,7 +58,7 @@ const INTEGRATION_OPTIONS = [
|
||||
name: 'Netlify',
|
||||
slug: 'netlify',
|
||||
image: 'Netlify',
|
||||
isAvailable: true,
|
||||
isAvailable: false,
|
||||
type: 'oauth2',
|
||||
clientId: CLIENT_ID_NETLIFY,
|
||||
docsLink: ''
|
||||
@@ -67,7 +67,7 @@ const INTEGRATION_OPTIONS = [
|
||||
name: 'GitHub',
|
||||
slug: 'github',
|
||||
image: 'GitHub',
|
||||
isAvailable: true,
|
||||
isAvailable: false,
|
||||
type: 'oauth2',
|
||||
clientId: CLIENT_ID_GITHUB,
|
||||
docsLink: ''
|
||||
|
||||
@@ -13,7 +13,6 @@ require (
|
||||
|
||||
require (
|
||||
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/chzyer/readline v1.5.1 // 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/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // 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/mitchellh/mapstructure v1.3.3 // indirect
|
||||
github.com/mtibben/percent v0.2.1 // indirect
|
||||
@@ -35,7 +36,7 @@ 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/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible
|
||||
|
||||
20
cli/go.sum
20
cli/go.sum
@@ -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/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
|
||||
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/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
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/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/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/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
|
||||
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/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
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/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
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=
|
||||
go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg=
|
||||
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.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||
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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
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/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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-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-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-20220310020820-b874c991c1a5/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.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
136
cli/packages/api/api.go
Normal file
136
cli/packages/api/api.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/config"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
func CallBatchModifySecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchModifySecretsByWorkspaceAndEnvRequest) error {
|
||||
endpoint := fmt.Sprintf("%v/v2/secret/batch-modify/workspace/%v/environment/%v", config.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName)
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetBody(request).
|
||||
Patch(endpoint)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.StatusCode() > 299 {
|
||||
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CallBatchCreateSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchCreateSecretsByWorkspaceAndEnvRequest) error {
|
||||
endpoint := fmt.Sprintf("%v/v2/secret/batch-create/workspace/%v/environment/%v", config.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName)
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetBody(request).
|
||||
Post(endpoint)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.StatusCode() > 299 {
|
||||
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchDeleteSecretsBySecretIdsRequest) error {
|
||||
endpoint := fmt.Sprintf("%v/v2/secret/batch/workspace/%v/environment/%v", config.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName)
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetBody(request).
|
||||
Delete(endpoint)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.StatusCode() > 299 {
|
||||
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncryptedWorkspaceKeyRequest) (GetEncryptedWorkspaceKeyResponse, error) {
|
||||
endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", config.INFISICAL_URL, request.WorkspaceId)
|
||||
var result GetEncryptedWorkspaceKeyResponse
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&result).
|
||||
Get(endpoint)
|
||||
|
||||
if err != nil {
|
||||
return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.StatusCode() > 299 {
|
||||
return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response: [response=%s]", response)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDetailsResponse, error) {
|
||||
var tokenDetailsResponse GetServiceTokenDetailsResponse
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&tokenDetailsResponse).
|
||||
Get(fmt.Sprintf("%v/v2/service-token", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.StatusCode() > 299 {
|
||||
return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unsuccessful response: [response=%s]", response)
|
||||
}
|
||||
|
||||
return tokenDetailsResponse, nil
|
||||
}
|
||||
|
||||
func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Request) (GetEncryptedSecretsV2Response, error) {
|
||||
var secretsResponse GetEncryptedSecretsV2Response
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&secretsResponse).
|
||||
SetQueryParam("environment", request.EnvironmentName).
|
||||
Get(fmt.Sprintf("%v/v2/secret/workspace/%v", config.INFISICAL_URL, request.WorkspaceId))
|
||||
|
||||
if err != nil {
|
||||
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.StatusCode() > 299 {
|
||||
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unsuccessful response: [response=%s]", response)
|
||||
}
|
||||
|
||||
return secretsResponse, nil
|
||||
}
|
||||
|
||||
func CallGetAllWorkSpacesUserBelongsTo(httpClient *resty.Client) (GetWorkSpacesResponse, error) {
|
||||
var workSpacesResponse GetWorkSpacesResponse
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&workSpacesResponse).
|
||||
Get(fmt.Sprintf("%v/v1/workspace", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return GetWorkSpacesResponse{}, err
|
||||
}
|
||||
|
||||
if response.StatusCode() > 299 {
|
||||
return GetWorkSpacesResponse{}, fmt.Errorf("CallGetAllWorkSpacesUserBelongsTo: Unsuccessful response: [response=%v]", response)
|
||||
}
|
||||
|
||||
return workSpacesResponse, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package models
|
||||
package api
|
||||
|
||||
import "time"
|
||||
|
||||
@@ -119,14 +119,13 @@ type PullSecretsByInfisicalTokenResponse struct {
|
||||
}
|
||||
|
||||
type GetWorkSpacesResponse struct {
|
||||
Workspaces []Workspace `json:"workspaces"`
|
||||
}
|
||||
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"`
|
||||
Workspaces []struct {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
Plan string `json:"plan,omitempty"`
|
||||
V int `json:"__v"`
|
||||
Organization string `json:"organization,omitempty"`
|
||||
} `json:"workspaces"`
|
||||
}
|
||||
|
||||
type Secret struct {
|
||||
@@ -169,30 +168,68 @@ type GetEncryptedWorkspaceKeyRequest struct {
|
||||
}
|
||||
|
||||
type GetEncryptedWorkspaceKeyResponse struct {
|
||||
LatestKey struct {
|
||||
ID string `json:"_id"`
|
||||
EncryptedKey string `json:"encryptedKey"`
|
||||
Nonce string `json:"nonce"`
|
||||
Sender struct {
|
||||
ID string `json:"_id"`
|
||||
Email string `json:"email"`
|
||||
RefreshVersion int `json:"refreshVersion"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
V int `json:"__v"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
} `json:"sender"`
|
||||
Receiver string `json:"receiver"`
|
||||
Workspace string `json:"workspace"`
|
||||
V int `json:"__v"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
} `json:"latestKey"`
|
||||
ID string `json:"_id"`
|
||||
EncryptedKey string `json:"encryptedKey"`
|
||||
Nonce string `json:"nonce"`
|
||||
Sender struct {
|
||||
ID string `json:"_id"`
|
||||
Email string `json:"email"`
|
||||
RefreshVersion int `json:"refreshVersion"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
V int `json:"__v"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
} `json:"sender"`
|
||||
Receiver string `json:"receiver"`
|
||||
Workspace string `json:"workspace"`
|
||||
V int `json:"__v"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type GetSecretsByWorkspaceIdAndEnvironmentRequest struct {
|
||||
EnvironmentName string `json:"environmentName"`
|
||||
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"`
|
||||
}
|
||||
@@ -29,58 +29,46 @@ var exportCmd = &cobra.Command{
|
||||
DisableFlagsInUseLine: true,
|
||||
Example: "infisical export --env=prod --format=json > secrets.json",
|
||||
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) {
|
||||
envName, err := cmd.Flags().GetString("env")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the environment flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the substitute flag")
|
||||
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
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
format, err := cmd.Flags().GetString("format")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the format flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
envsFromApi, err := util.GetAllEnvironmentVariables(projectId, envName)
|
||||
secrets, err := util.GetAllEnvironmentVariables(envName)
|
||||
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.)")
|
||||
log.Debugln(err)
|
||||
return
|
||||
util.HandleError(err, "Unable to fetch secrets")
|
||||
}
|
||||
|
||||
var output string
|
||||
if shouldExpandSecrets {
|
||||
substitutions := util.SubstituteSecrets(envsFromApi)
|
||||
substitutions := util.SubstituteSecrets(secrets)
|
||||
output, err = formatEnvs(substitutions, format)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return
|
||||
util.HandleError(err)
|
||||
}
|
||||
} else {
|
||||
output, err = formatEnvs(envsFromApi, format)
|
||||
output, err = formatEnvs(secrets, format)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return
|
||||
util.HandleError(err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Print(output)
|
||||
},
|
||||
}
|
||||
@@ -88,7 +76,6 @@ var exportCmd = &cobra.Command{
|
||||
func init() {
|
||||
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().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().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)")
|
||||
}
|
||||
|
||||
@@ -5,10 +5,13 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/manifoldco/promptui"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -21,21 +24,10 @@ var initCmd = &cobra.Command{
|
||||
DisableFlagsInUseLine: true,
|
||||
Example: "infisical init",
|
||||
Args: cobra.ExactArgs(0),
|
||||
PreRun: toggleDebug,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
util.RequireLogin()
|
||||
},
|
||||
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() {
|
||||
shouldOverride, err := shouldOverrideWorkspacePrompt()
|
||||
if err != nil {
|
||||
@@ -49,23 +41,22 @@ var initCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
|
||||
userCreds, err := util.GetUserCredsFromKeyRing(loggedInUserEmail)
|
||||
userCreds, err := util.GetCurrentLoggedInUserDetails()
|
||||
if err != nil {
|
||||
log.Infoln("Unable to get user creds from key ring")
|
||||
log.Debug(err)
|
||||
return
|
||||
util.HandleError(err, "Unable to get your login details")
|
||||
}
|
||||
|
||||
workspaces, err := util.GetWorkSpacesFromAPI(userCreds)
|
||||
httpClient := resty.New()
|
||||
httpClient.SetAuthToken(userCreds.UserCredentials.JTWToken)
|
||||
workspaceResponse, err := api.CallGetAllWorkSpacesUserBelongsTo(httpClient)
|
||||
if err != nil {
|
||||
log.Errorln("Unable to pull your projects. To see more logs add the --debug flag to this command")
|
||||
log.Debugln("Unable to get your projects because:", err)
|
||||
return
|
||||
util.HandleError(err, "Unable to pull projects that belong to you")
|
||||
}
|
||||
|
||||
workspaces := workspaceResponse.Workspaces
|
||||
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")
|
||||
return
|
||||
message := fmt.Sprintf("You don't have any projects created in Infisical. You must first create a project at %s", util.INFISICAL_TOKEN_NAME)
|
||||
util.PrintMessageAndExit(message)
|
||||
}
|
||||
|
||||
var workspaceNames []string
|
||||
@@ -81,16 +72,12 @@ var initCmd = &cobra.Command{
|
||||
|
||||
index, _, err := prompt.Run()
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse your response")
|
||||
log.Debug(err)
|
||||
return
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
err = writeWorkspaceFile(workspaces[index])
|
||||
if err != nil {
|
||||
log.Errorln("Something went wrong when creating your workspace file")
|
||||
log.Debug("Error while writing your workspace file:", err)
|
||||
return
|
||||
util.HandleError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ import (
|
||||
"fmt"
|
||||
"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/srp"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
@@ -27,18 +30,15 @@ var loginCmd = &cobra.Command{
|
||||
DisableFlagsInUseLine: true,
|
||||
PreRun: toggleDebug,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
hasUserLoggedInbefore, currentLoggedInUserEmail, err := util.IsUserLoggedIn()
|
||||
|
||||
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
||||
if err != nil {
|
||||
log.Debugln("Unable to get current logged in user.", err)
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
if hasUserLoggedInbefore {
|
||||
shouldOverride, err := shouldOverrideLoginPrompt(currentLoggedInUserEmail)
|
||||
if currentLoggedInUserDetails.IsUserLoggedIn {
|
||||
shouldOverride, err := shouldOverrideLoginPrompt(currentLoggedInUserDetails.UserCredentials.Email)
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse your answer")
|
||||
log.Debug(err)
|
||||
return
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
if !shouldOverride {
|
||||
@@ -48,14 +48,12 @@ var loginCmd = &cobra.Command{
|
||||
|
||||
email, password, err := askForLoginCredentials()
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse email and password for authentication")
|
||||
log.Debugln(err)
|
||||
return
|
||||
util.HandleError(err, "Unable to parse email and password for authentication")
|
||||
}
|
||||
|
||||
userCredentials, err := getFreshUserCredentials(email, password)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@@ -63,24 +61,20 @@ var loginCmd = &cobra.Command{
|
||||
encryptedPrivateKey, _ := base64.StdEncoding.DecodeString(userCredentials.EncryptedPrivateKey)
|
||||
tag, err := base64.StdEncoding.DecodeString(userCredentials.Tag)
|
||||
if err != nil {
|
||||
log.Errorln("Unable to decode the auth tag")
|
||||
log.Debugln(err)
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
IV, err := base64.StdEncoding.DecodeString(userCredentials.IV)
|
||||
if err != nil {
|
||||
log.Errorln("Unable to decode the IV/Nonce")
|
||||
log.Debugln(err)
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
paddedPassword := fmt.Sprintf("%032s", password)
|
||||
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 {
|
||||
log.Errorln("There was an issue decrypting your keys")
|
||||
log.Debugln(err)
|
||||
return
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
userCredentialsToBeStored := &models.UserCredentials{
|
||||
@@ -100,9 +94,7 @@ var loginCmd = &cobra.Command{
|
||||
|
||||
err = util.WriteInitalConfig(userCredentialsToBeStored)
|
||||
if err != nil {
|
||||
log.Errorln("Unable to write write to Infisical Config file. Please try again")
|
||||
log.Debugln(err)
|
||||
return
|
||||
util.HandleError(err, "Unable to write write to Infisical Config file. Please try again")
|
||||
}
|
||||
|
||||
log.Infoln("Nice! You are loggin as:", email)
|
||||
@@ -156,7 +148,7 @@ func askForLoginCredentials() (email string, password string, err error) {
|
||||
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)
|
||||
httpClient := resty.New()
|
||||
httpClient.SetRetryCount(5)
|
||||
@@ -167,18 +159,18 @@ func getFreshUserCredentials(email string, password string) (*models.LoginTwoRes
|
||||
srpA := hex.EncodeToString(srpClient.ComputeA())
|
||||
|
||||
// ** Login one
|
||||
loginOneRequest := models.LoginOneRequest{
|
||||
loginOneRequest := api.LoginOneRequest{
|
||||
Email: email,
|
||||
ClientPublicKey: srpA,
|
||||
}
|
||||
|
||||
var loginOneResponseResult models.LoginOneResponse
|
||||
var loginOneResponseResult api.LoginOneResponse
|
||||
|
||||
loginOneResponse, err := httpClient.
|
||||
R().
|
||||
SetBody(loginOneRequest).
|
||||
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 {
|
||||
return nil, err
|
||||
@@ -204,17 +196,17 @@ func getFreshUserCredentials(email string, password string) (*models.LoginTwoRes
|
||||
|
||||
srpM1 := srpClient.ComputeM1()
|
||||
|
||||
LoginTwoRequest := models.LoginTwoRequest{
|
||||
LoginTwoRequest := api.LoginTwoRequest{
|
||||
Email: email,
|
||||
ClientProof: hex.EncodeToString(srpM1),
|
||||
}
|
||||
|
||||
var loginTwoResponseResult models.LoginTwoResponse
|
||||
var loginTwoResponseResult api.LoginTwoResponse
|
||||
loginTwoResponse, err := httpClient.
|
||||
R().
|
||||
SetBody(LoginTwoRequest).
|
||||
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 {
|
||||
return nil, err
|
||||
|
||||
@@ -6,7 +6,7 @@ package cmd
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/Infisical/infisical-merge/packages/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{
|
||||
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.`,
|
||||
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.
|
||||
@@ -30,7 +30,7 @@ func Execute() {
|
||||
func init() {
|
||||
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
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) {
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -55,36 +55,26 @@ var runCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
envName, err := cmd.Flags().GetString("env")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the environment flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
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")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the secret-overriding flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the substitute flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
projectId, err := cmd.Flags().GetString("projectId")
|
||||
secrets, err := util.GetAllEnvironmentVariables(envName)
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the project id flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(projectId, envName)
|
||||
if err != nil {
|
||||
log.Debugln(err)
|
||||
return
|
||||
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
|
||||
}
|
||||
|
||||
if shouldExpandSecrets {
|
||||
@@ -97,29 +87,26 @@ var runCmd = &cobra.Command{
|
||||
|
||||
if cmd.Flags().Changed("command") {
|
||||
command := cmd.Flag("command").Value.String()
|
||||
|
||||
err = executeMultipleCommandWithEnvs(command, secrets)
|
||||
if err != nil {
|
||||
log.Errorf("Something went wrong when executing your command [error=%s]", err)
|
||||
return
|
||||
util.HandleError(err, "Unable to execute your chained command")
|
||||
}
|
||||
|
||||
} else {
|
||||
err = executeSingleCommandWithEnvs(args, secrets)
|
||||
if err != nil {
|
||||
log.Errorf("Something went wrong when executing your command [error=%s]", err)
|
||||
return
|
||||
util.HandleError(err, "Unable to execute your single command")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
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().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("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 ...\")")
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected)
|
||||
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.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))
|
||||
log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
|
||||
"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/util"
|
||||
"github.com/Infisical/infisical-merge/packages/visualize"
|
||||
@@ -30,35 +31,23 @@ var secretsCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
environmentName, err := cmd.Flags().GetString("env")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the environment name flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the substitute flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath()
|
||||
if !workspaceFileExists {
|
||||
log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]")
|
||||
return
|
||||
secrets, err := util.GetAllEnvironmentVariables(environmentName)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables("", environmentName)
|
||||
|
||||
if shouldExpandSecrets {
|
||||
secrets = util.SubstituteSecrets(secrets)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Debugln(err)
|
||||
return
|
||||
}
|
||||
|
||||
visualize.PrintAllSecretDetails(secrets)
|
||||
},
|
||||
}
|
||||
@@ -81,85 +70,50 @@ var secretsSetCmd = &cobra.Command{
|
||||
PreRun: toggleDebug,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
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")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the environment name flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
if !util.IsSecretEnvironmentValid(environmentName) {
|
||||
log.Errorln("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
|
||||
util.PrintMessageAndExit("You have entered a invalid environment name", "Environment names can only be prod, dev, test or staging")
|
||||
}
|
||||
|
||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
util.HandleError(err, "Unable to get your local config details")
|
||||
}
|
||||
|
||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
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
|
||||
util.HandleError(err, "Unable to authenticate")
|
||||
}
|
||||
|
||||
httpClient := resty.New().
|
||||
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
request := models.GetEncryptedWorkspaceKeyRequest{
|
||||
request := api.GetEncryptedWorkspaceKeyRequest{
|
||||
WorkspaceId: workspaceFile.WorkspaceId,
|
||||
}
|
||||
|
||||
workspaceKeyResponse, err := http.CallGetEncryptedWorkspaceKey(httpClient, request)
|
||||
workspaceKeyResponse, err := api.CallGetEncryptedWorkspaceKey(httpClient, request)
|
||||
if err != nil {
|
||||
log.Errorf("unable to get your encrypted workspace key. [err=%v]", err)
|
||||
return
|
||||
util.HandleError(err, "unable to get your encrypted workspace key")
|
||||
}
|
||||
|
||||
encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.EncryptedKey)
|
||||
encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.Sender.PublicKey)
|
||||
encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.Nonce)
|
||||
encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey)
|
||||
encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey)
|
||||
encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce)
|
||||
currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(loggedInUserDetails.UserCredentials.PrivateKey)
|
||||
|
||||
// decrypt workspace key
|
||||
plainTextEncryptionKey := util.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
|
||||
plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
|
||||
|
||||
// pull current secrets
|
||||
secrets, err := util.GetAllEnvironmentVariables("", environmentName)
|
||||
secrets, err := util.GetAllEnvironmentVariables(environmentName)
|
||||
if err != nil {
|
||||
log.Error("unable to retrieve secrets. Run with -d to see full logs")
|
||||
log.Debug(err)
|
||||
util.HandleError(err, "unable to retrieve secrets")
|
||||
}
|
||||
|
||||
type SecretSetOperation struct {
|
||||
@@ -168,8 +122,8 @@ var secretsSetCmd = &cobra.Command{
|
||||
SecretOperation string
|
||||
}
|
||||
|
||||
secretsToCreate := []models.Secret{}
|
||||
secretsToModify := []models.Secret{}
|
||||
secretsToCreate := []api.Secret{}
|
||||
secretsToModify := []api.Secret{}
|
||||
secretOperations := []SecretSetOperation{}
|
||||
|
||||
secretByKey := getSecretsByKeys(secrets)
|
||||
@@ -177,13 +131,11 @@ var secretsSetCmd = &cobra.Command{
|
||||
for _, arg := range args {
|
||||
splitKeyValueFromArg := strings.SplitN(arg, "=", 2)
|
||||
if splitKeyValueFromArg[0] == "" || splitKeyValueFromArg[1] == "" {
|
||||
log.Error("ensure that each secret has a none empty key and value. Modify the input and try again")
|
||||
return
|
||||
util.PrintMessageAndExit("ensure that each secret has a none empty key and value. Modify the input and try again")
|
||||
}
|
||||
|
||||
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")
|
||||
return
|
||||
util.PrintMessageAndExit("keys of secrets cannot start with a number. Modify the key name(s) and try again")
|
||||
}
|
||||
|
||||
// Key and value from argument
|
||||
@@ -191,20 +143,20 @@ var secretsSetCmd = &cobra.Command{
|
||||
value := splitKeyValueFromArg[1]
|
||||
|
||||
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 {
|
||||
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)))
|
||||
encryptedValue, err := util.EncryptSymmetric([]byte(value), []byte(plainTextEncryptionKey))
|
||||
encryptedValue, err := crypto.EncryptSymmetric([]byte(value), []byte(plainTextEncryptionKey))
|
||||
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 {
|
||||
// case: secret exists in project so it needs to be modified
|
||||
encryptedSecretDetails := models.Secret{
|
||||
encryptedSecretDetails := api.Secret{
|
||||
ID: existingSecret.ID,
|
||||
SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText),
|
||||
SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce),
|
||||
@@ -231,7 +183,7 @@ var secretsSetCmd = &cobra.Command{
|
||||
|
||||
} else {
|
||||
// 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),
|
||||
SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce),
|
||||
SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag),
|
||||
@@ -252,29 +204,29 @@ var secretsSetCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
if len(secretsToCreate) > 0 {
|
||||
batchCreateRequest := models.BatchCreateSecretsByWorkspaceAndEnvRequest{
|
||||
batchCreateRequest := api.BatchCreateSecretsByWorkspaceAndEnvRequest{
|
||||
WorkspaceId: workspaceFile.WorkspaceId,
|
||||
EnvironmentName: environmentName,
|
||||
Secrets: secretsToCreate,
|
||||
}
|
||||
|
||||
err = http.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest)
|
||||
err = api.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to process new secret creations because %v", err)
|
||||
util.HandleError(err, "Unable to process new secret creations")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(secretsToModify) > 0 {
|
||||
batchModifyRequest := models.BatchModifySecretsByWorkspaceAndEnvRequest{
|
||||
batchModifyRequest := api.BatchModifySecretsByWorkspaceAndEnvRequest{
|
||||
WorkspaceId: workspaceFile.WorkspaceId,
|
||||
EnvironmentName: environmentName,
|
||||
Secrets: secretsToModify,
|
||||
}
|
||||
|
||||
err = http.CallBatchModifySecretsByWorkspaceAndEnv(httpClient, batchModifyRequest)
|
||||
err = api.CallBatchModifySecretsByWorkspaceAndEnv(httpClient, batchModifyRequest)
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -307,36 +259,17 @@ var secretsDeleteCmd = &cobra.Command{
|
||||
|
||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
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
|
||||
util.HandleError(err, "Unable to authenticate")
|
||||
}
|
||||
|
||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
util.HandleError(err, "Unable to get local project details")
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables("", environmentName)
|
||||
secrets, err := util.GetAllEnvironmentVariables(environmentName)
|
||||
if err != nil {
|
||||
log.Error("Unable to retrieve secrets. Run with -d to see full logs")
|
||||
log.Debug(err)
|
||||
util.HandleError(err, "Unable to fetch secrets")
|
||||
}
|
||||
|
||||
secretByKey := getSecretsByKeys(secrets)
|
||||
@@ -352,11 +285,11 @@ var secretsDeleteCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
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, ", "))
|
||||
return
|
||||
message := fmt.Sprintf("secret name(s) [%v] does not exist in your project. To see which secrets exist run [infisical secrets]", strings.Join(invalidSecretNamesThatDoNotExist, ", "))
|
||||
util.PrintMessageAndExit(message)
|
||||
}
|
||||
|
||||
request := models.BatchDeleteSecretsBySecretIdsRequest{
|
||||
request := api.BatchDeleteSecretsBySecretIdsRequest{
|
||||
WorkspaceId: workspaceFile.WorkspaceId,
|
||||
EnvironmentName: environmentName,
|
||||
SecretIds: validSecretIdsToDelete,
|
||||
@@ -366,45 +299,43 @@ var secretsDeleteCmd = &cobra.Command{
|
||||
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
err = http.CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient, request)
|
||||
err = api.CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient, request)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to complete your request because %v", err)
|
||||
return
|
||||
util.HandleError(err, "Unable to complete your batch delete request")
|
||||
}
|
||||
|
||||
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() {
|
||||
secretsCmd.AddCommand(secretsGetCmd)
|
||||
// secretsSetCmd.Flags().String("type", "shared", "Used to set the type for secrets")
|
||||
secretsCmd.AddCommand(secretsSetCmd)
|
||||
secretsCmd.AddCommand(secretsDeleteCmd)
|
||||
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.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||
util.RequireLogin()
|
||||
util.RequireLocalWorkspaceFile()
|
||||
}
|
||||
rootCmd.AddCommand(secretsCmd)
|
||||
}
|
||||
|
||||
func getSecretsByNames(cmd *cobra.Command, args []string) {
|
||||
environmentName, err := cmd.Flags().GetString("env")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the environment name flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath()
|
||||
if !workspaceFileExists {
|
||||
log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]")
|
||||
return
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables("", environmentName)
|
||||
secrets, err := util.GetAllEnvironmentVariables(environmentName)
|
||||
if err != nil {
|
||||
log.Error("Unable to retrieve secrets. Run with -d to see full logs")
|
||||
log.Debug(err)
|
||||
util.HandleError(err, "To fetch all secrets")
|
||||
}
|
||||
|
||||
requestedSecrets := []models.SingleEnvironmentVariable{}
|
||||
|
||||
3
cli/packages/config/config.go
Normal file
3
cli/packages/config/config.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package config
|
||||
|
||||
var INFISICAL_URL = "http://localhost:8080/api"
|
||||
@@ -1,4 +1,4 @@
|
||||
package util
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"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"`
|
||||
}
|
||||
|
||||
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 {
|
||||
WorkspaceId string `json:"workspaceId"`
|
||||
}
|
||||
|
||||
@@ -5,17 +5,6 @@ import (
|
||||
"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) {
|
||||
directory, err := os.UserHomeDir()
|
||||
return directory, err
|
||||
@@ -25,7 +14,7 @@ func GetHomeDir() (string, error) {
|
||||
func WriteToFile(fileName string, dataToWrite []byte, filePerm os.FileMode) error {
|
||||
err := os.WriteFile(fileName, dataToWrite, filePerm)
|
||||
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
|
||||
|
||||
13
cli/packages/util/constants.go
Normal file
13
cli/packages/util/constants.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package util
|
||||
|
||||
const (
|
||||
CONFIG_FILE_NAME = "infisical-config.json"
|
||||
CONFIG_FOLDER_NAME = ".infisical"
|
||||
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
|
||||
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
|
||||
SECRET_TYPE_PERSONAL = "personal"
|
||||
SECRET_TYPE_SHARED = "shared"
|
||||
KEYRING_SERVICE_NAME = "infisical"
|
||||
PERSONAL_SECRET_TYPE_NAME = "personal"
|
||||
SHARED_SECRET_TYPE_NAME = "shared"
|
||||
)
|
||||
@@ -5,20 +5,17 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/99designs/keyring"
|
||||
"github.com/Infisical/infisical-merge/packages/config"
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const SERVICE_NAME = "infisical"
|
||||
|
||||
type LoggedInUserDetails struct {
|
||||
IsUserLoggedIn bool
|
||||
LoginExpired bool
|
||||
UserCredentials models.UserCredentials
|
||||
}
|
||||
|
||||
// To do: what happens if the user doesn't have a keyring in their system?
|
||||
func StoreUserCredsInKeyRing(userCred *models.UserCredentials) error {
|
||||
userCredMarshalled, err := json.Marshal(userCred)
|
||||
if err != nil {
|
||||
@@ -69,46 +66,6 @@ func GetUserCredsFromKeyRing(userEmail string) (credentials models.UserCredentia
|
||||
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) {
|
||||
if ConfigFileExists() {
|
||||
configFile, err := GetConfigFile()
|
||||
@@ -132,7 +89,7 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
|
||||
|
||||
response, err := httpClient.
|
||||
R().
|
||||
Post(fmt.Sprintf("%v/v1/auth/checkAuth", INFISICAL_URL))
|
||||
Post(fmt.Sprintf("%v/v1/auth/checkAuth", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return LoggedInUserDetails{}, err
|
||||
|
||||
38
cli/packages/util/errors.go
Normal file
38
cli/packages/util/errors.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
func HandleError(err error, messages ...string) {
|
||||
PrintErrorAndExit(1, err, messages...)
|
||||
}
|
||||
|
||||
func PrintErrorAndExit(exitCode int, err error, messages ...string) {
|
||||
printError(err)
|
||||
|
||||
if len(messages) > 0 {
|
||||
for _, message := range messages {
|
||||
fmt.Println(message)
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func PrintMessageAndExit(messages ...string) {
|
||||
if len(messages) > 0 {
|
||||
for _, message := range messages {
|
||||
fmt.Println(message)
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func printError(e error) {
|
||||
color.Red("Hmm, we ran into an error: %v", e)
|
||||
}
|
||||
100
cli/packages/util/helper.go
Normal file
100
cli/packages/util/helper.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type DecodedSymmetricEncryptionDetails = struct {
|
||||
Cipher []byte
|
||||
IV []byte
|
||||
Tag []byte
|
||||
Key []byte
|
||||
}
|
||||
|
||||
func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) {
|
||||
cipherx, err := base64.StdEncoding.DecodeString(cipher)
|
||||
if err != nil {
|
||||
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode cipher text [err=%v]", err)
|
||||
}
|
||||
|
||||
keyx, err := base64.StdEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode key [err=%v]", err)
|
||||
}
|
||||
|
||||
IVx, err := base64.StdEncoding.DecodeString(IV)
|
||||
if err != nil {
|
||||
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode IV [err=%v]", err)
|
||||
}
|
||||
|
||||
tagx, err := base64.StdEncoding.DecodeString(tag)
|
||||
if err != nil {
|
||||
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode tag [err=%v]", err)
|
||||
}
|
||||
|
||||
return DecodedSymmetricEncryptionDetails{
|
||||
Key: keyx,
|
||||
Cipher: cipherx,
|
||||
IV: IVx,
|
||||
Tag: tagx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func IsSecretEnvironmentValid(env string) bool {
|
||||
if env == "prod" || env == "dev" || env == "test" || env == "staging" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsSecretTypeValid(s string) bool {
|
||||
if s == "personal" || s == "shared" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func RequireLogin() {
|
||||
currentUserDetails, err := GetCurrentLoggedInUserDetails()
|
||||
|
||||
if err != nil {
|
||||
HandleError(err, "unable to retrieve your login details")
|
||||
}
|
||||
|
||||
if !currentUserDetails.IsUserLoggedIn {
|
||||
PrintMessageAndExit("You must be logged in to run this command. To login, run [infisical login]")
|
||||
}
|
||||
|
||||
if currentUserDetails.LoginExpired {
|
||||
PrintMessageAndExit("Your login expired, please login in again. To login, run [infisical login]")
|
||||
}
|
||||
|
||||
if currentUserDetails.UserCredentials.Email == "" && currentUserDetails.UserCredentials.JTWToken == "" && currentUserDetails.UserCredentials.PrivateKey == "" {
|
||||
PrintMessageAndExit("One or more of your login details is empty. Please try logging in again via by running [infisical login]")
|
||||
}
|
||||
}
|
||||
|
||||
func RequireServiceToken() {
|
||||
serviceToken := os.Getenv(INFISICAL_TOKEN_NAME)
|
||||
if serviceToken == "" {
|
||||
PrintMessageAndExit("No service token is found in your terminal")
|
||||
}
|
||||
}
|
||||
|
||||
func RequireLocalWorkspaceFile() {
|
||||
workspaceFileExists := WorkspaceConfigFileExistsInCurrentPath()
|
||||
if !workspaceFileExists {
|
||||
PrintMessageAndExit("It looks you have not yet connected this project to Infisical", "To do so, run [infisical init] then run your command again")
|
||||
}
|
||||
|
||||
workspaceFile, err := GetWorkSpaceFromFile()
|
||||
if err != nil {
|
||||
HandleError(err, "Unable to read your project configuration, please try initializing this project again.", "Run [infisical init]")
|
||||
}
|
||||
|
||||
if workspaceFile.WorkspaceId == "" {
|
||||
PrintMessageAndExit("Your project id is missing in your local config file. Please add it or run again [infisical init]")
|
||||
}
|
||||
}
|
||||
@@ -2,284 +2,127 @@ package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/crypto"
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
const PERSONAL_SECRET_TYPE_NAME = "personal"
|
||||
const SHARED_SECRET_TYPE_NAME = "shared"
|
||||
|
||||
func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, workspace models.WorkspaceConfigFile, userCreds models.UserCredentials) (listOfSecrets []models.SingleEnvironmentVariable, err error) {
|
||||
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
|
||||
func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.SingleEnvironmentVariable, error) {
|
||||
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
|
||||
if len(serviceTokenParts) < 4 {
|
||||
return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
||||
}
|
||||
|
||||
if response.StatusCode() > 299 {
|
||||
return nil, fmt.Errorf(response.Status())
|
||||
}
|
||||
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
|
||||
|
||||
// Get workspace key
|
||||
workspaceKey, err := base64.StdEncoding.DecodeString(pullSecretsRequestResponse.Key.EncryptedKey)
|
||||
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).
|
||||
httpClient := resty.New()
|
||||
httpClient.SetAuthToken(serviceToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
secrets := []models.SingleEnvironmentVariable{}
|
||||
for _, workspace := range workspaces {
|
||||
secretsFromAPI, err := getSecretsByWorkspaceIdAndEnvName(*httpClient, envName, workspace, userCreds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetSecretsFromAPIUsingCurrentLoggedInUser: Unable to get secrets by workspace id and env name")
|
||||
}
|
||||
|
||||
secrets = append(secrets, secretsFromAPI...)
|
||||
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get service token details. [err=%v]", err)
|
||||
}
|
||||
|
||||
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) {
|
||||
if infisicalToken == "" || projectId == "" || envName == "" {
|
||||
return nil, errors.New("infisical token, project id and or environment name cannot be empty")
|
||||
}
|
||||
splitToken := strings.Split(infisicalToken, ",")
|
||||
JTWToken := splitToken[0]
|
||||
temPrivateKey := splitToken[1]
|
||||
|
||||
// create http client
|
||||
httpClient := resty.New().
|
||||
SetAuthToken(JTWToken).
|
||||
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string) ([]models.SingleEnvironmentVariable, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient.SetAuthToken(JTWToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
var pullSecretsByInfisicalTokenResponse models.PullSecretsByInfisicalTokenResponse
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetQueryParam("environment", envName).
|
||||
SetQueryParam("channel", "cli").
|
||||
SetResult(&pullSecretsByInfisicalTokenResponse).
|
||||
Get(fmt.Sprintf("%v/v1/secret/%v/service-token", INFISICAL_URL, projectId))
|
||||
request := api.GetEncryptedWorkspaceKeyRequest{
|
||||
WorkspaceId: workspaceId,
|
||||
}
|
||||
|
||||
workspaceKeyResponse, err := api.CallGetEncryptedWorkspaceKey(httpClient, request)
|
||||
if err != nil {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode() > 299 {
|
||||
return nil, fmt.Errorf(response.Status())
|
||||
}
|
||||
|
||||
// Get workspace key
|
||||
workspaceKey, err := base64.StdEncoding.DecodeString(pullSecretsByInfisicalTokenResponse.Key.EncryptedKey)
|
||||
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets)
|
||||
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)
|
||||
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
|
||||
return plainTextSecrets, nil
|
||||
}
|
||||
|
||||
func GetAllEnvironmentVariables(projectId string, envName string) ([]models.SingleEnvironmentVariable, error) {
|
||||
func GetAllEnvironmentVariables(envName string) ([]models.SingleEnvironmentVariable, error) {
|
||||
infisicalToken := os.Getenv(INFISICAL_TOKEN_NAME)
|
||||
|
||||
if infisicalToken == "" {
|
||||
hasUserLoggedInbefore, loggedInUserEmail, err := IsUserLoggedIn()
|
||||
RequireLocalWorkspaceFile()
|
||||
RequireLogin()
|
||||
log.Debug("Trying to fetch secrets using logged in details")
|
||||
|
||||
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
|
||||
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
|
||||
}
|
||||
|
||||
if !hasUserLoggedInbefore {
|
||||
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)
|
||||
workspaceFile, err := GetWorkSpaceFromFile()
|
||||
if err != nil {
|
||||
log.Infoln("Unable to get user creds from key ring")
|
||||
log.Debug(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
|
||||
workspaceConfigs, err := GetAllWorkSpaceConfigsStartingFromCurrentPath()
|
||||
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
|
||||
secrets, err := GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, envName)
|
||||
return secrets, err
|
||||
|
||||
} else {
|
||||
envsFromApi, err := GetSecretsFromAPIUsingInfisicalToken(infisicalToken, envName, projectId)
|
||||
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.)")
|
||||
log.Debugln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return envsFromApi, nil
|
||||
log.Debug("Trying to fetch secrets using service token")
|
||||
return GetPlainTextSecretsViaServiceToken(infisicalToken)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found {
|
||||
return value
|
||||
@@ -351,6 +194,8 @@ func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.Sing
|
||||
return expandedSecrets
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// 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 {
|
||||
personalSecret := make(map[string]models.SingleEnvironmentVariable)
|
||||
@@ -359,46 +204,80 @@ func OverrideWithPersonalSecrets(secrets []models.SingleEnvironmentVariable) []m
|
||||
|
||||
for _, secret := range secrets {
|
||||
if secret.Type == PERSONAL_SECRET_TYPE_NAME {
|
||||
personalSecret[secret.Key] = models.SingleEnvironmentVariable{
|
||||
Key: secret.Key,
|
||||
Value: secret.Value,
|
||||
Type: secret.Type,
|
||||
}
|
||||
personalSecret[secret.Key] = secret
|
||||
}
|
||||
|
||||
if secret.Type == SHARED_SECRET_TYPE_NAME {
|
||||
sharedSecret[secret.Key] = models.SingleEnvironmentVariable{
|
||||
Key: secret.Key,
|
||||
Value: secret.Value,
|
||||
Type: secret.Type,
|
||||
}
|
||||
sharedSecret[secret.Key] = secret
|
||||
}
|
||||
}
|
||||
|
||||
for _, secret := range secrets {
|
||||
for _, secret := range sharedSecret {
|
||||
personalValue, personalExists := personalSecret[secret.Key]
|
||||
sharedValue, sharedExists := sharedSecret[secret.Key]
|
||||
|
||||
if personalExists && sharedExists || personalExists && !sharedExists {
|
||||
if personalExists {
|
||||
secretsToReturn = append(secretsToReturn, personalValue)
|
||||
} else {
|
||||
secretsToReturn = append(secretsToReturn, sharedValue)
|
||||
secretsToReturn = append(secretsToReturn, secret)
|
||||
}
|
||||
}
|
||||
|
||||
return secretsToReturn
|
||||
}
|
||||
|
||||
func IsSecretEnvironmentValid(env string) bool {
|
||||
if env == "prod" || env == "dev" || env == "test" || env == "staging" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2Response) ([]models.SingleEnvironmentVariable, error) {
|
||||
plainTextSecrets := []models.SingleEnvironmentVariable{}
|
||||
for _, secret := range encryptedSecrets {
|
||||
// Decrypt key
|
||||
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 {
|
||||
if s == "personal" || s == "shared" {
|
||||
return true
|
||||
key_tag, err := base64.StdEncoding.DecodeString(secret.SecretKeyTag)
|
||||
if err != nil {
|
||||
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{
|
||||
FilePasswordFunc: fileKeyringPassphrasePrompt,
|
||||
ServiceName: SERVICE_NAME,
|
||||
LibSecretCollectionName: SERVICE_NAME,
|
||||
KWalletAppID: SERVICE_NAME,
|
||||
KWalletFolder: SERVICE_NAME,
|
||||
ServiceName: KEYRING_SERVICE_NAME,
|
||||
LibSecretCollectionName: KEYRING_SERVICE_NAME,
|
||||
KWalletAppID: KEYRING_SERVICE_NAME,
|
||||
KWalletFolder: KEYRING_SERVICE_NAME,
|
||||
KeychainTrustApplication: true,
|
||||
WinCredPrefix: SERVICE_NAME,
|
||||
FileDir: fmt.Sprintf("~/%s-file-vault", SERVICE_NAME),
|
||||
WinCredPrefix: KEYRING_SERVICE_NAME,
|
||||
FileDir: fmt.Sprintf("~/%s-file-vault", KEYRING_SERVICE_NAME),
|
||||
KeychainAccessibleWhenUnlocked: true,
|
||||
}
|
||||
|
||||
|
||||
9
docs/getting-started/dashboard/audit-logs.mdx
Normal file
9
docs/getting-started/dashboard/audit-logs.mdx
Normal file
@@ -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.
|
||||
|
||||
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">
|
||||
View all available integrations and their guide
|
||||
<Card title="View integrations" icon="link" href="/integrations/overview">
|
||||
View all available integrations and their guides
|
||||
</Card>
|
||||
|
||||

|
||||
|
||||
|
||||
5
docs/getting-started/dashboard/pit-recovery.mdx
Normal file
5
docs/getting-started/dashboard/pit-recovery.mdx
Normal file
@@ -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.
|
||||
5
docs/getting-started/dashboard/versioning.mdx
Normal file
5
docs/getting-started/dashboard/versioning.mdx
Normal file
@@ -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:
|
||||
|
||||
## Web UI
|
||||
## Platform
|
||||
|
||||
The Web UI is used to manage teams and environment variables.
|
||||
|
||||
- Provision access to organizations and projects.
|
||||
- Add/delete/update, scope, search, sort, hide-unhide environment variables.
|
||||
- Separate environment variables by environment.
|
||||
- Import environment variables via drag-and-drop, export them as a .env file.
|
||||
- Provision members access to organizations and projects.
|
||||
- Manage secrets by adding, deleting, updating them across environments; search, sort, hide/un-hide, export/import them.
|
||||
- Sync secrets to platforms via integrations to platforms like GitHub, Vercel, and Netlify.
|
||||
- Rollback secrets to any point in time.
|
||||
- Rollback each secrets to any version.
|
||||
- Track actions through activity logs.
|
||||
|
||||
## CLI
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
"getting-started/dashboard/organization",
|
||||
"getting-started/dashboard/project",
|
||||
"getting-started/dashboard/integrations",
|
||||
"getting-started/dashboard/pit-recovery",
|
||||
"getting-started/dashboard/versioning",
|
||||
"getting-started/dashboard/audit-logs",
|
||||
"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';
|
||||
|
||||
export const initPostHog = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
// @ts-ignore
|
||||
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||
posthog.init(POSTHOG_API_KEY, {
|
||||
api_host: POSTHOG_HOST
|
||||
});
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
// @ts-ignore
|
||||
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||
console.log("Outside of posthog", "POSTHOG_API_KEY", POSTHOG_API_KEY, "POSTHOG_HOST", POSTHOG_HOST)
|
||||
posthog.init(POSTHOG_API_KEY, {
|
||||
api_host: POSTHOG_HOST
|
||||
});
|
||||
}
|
||||
|
||||
return posthog;
|
||||
console.log("Outside of posthog")
|
||||
}
|
||||
|
||||
return posthog;
|
||||
} catch (e) {
|
||||
console.log("posthog err", e)
|
||||
}
|
||||
};
|
||||
|
||||
106
frontend/components/basic/EventFilter.tsx
Normal file
106
frontend/components/basic/EventFilter.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import {
|
||||
faAngleDown,
|
||||
faEye,
|
||||
faPlus,
|
||||
faShuffle,
|
||||
faTrash,
|
||||
faX
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Listbox, Transition } from '@headlessui/react';
|
||||
|
||||
interface ListBoxProps {
|
||||
selected: string;
|
||||
select: (event: string) => void;
|
||||
}
|
||||
|
||||
const eventOptions = [
|
||||
{
|
||||
name: 'addSecrets',
|
||||
icon: faPlus
|
||||
},
|
||||
{
|
||||
name: 'readSecrets',
|
||||
icon: faEye
|
||||
},
|
||||
{
|
||||
name: 'updateSecrets',
|
||||
icon: faShuffle
|
||||
},
|
||||
{
|
||||
name: 'deleteSecrets',
|
||||
icon: faTrash
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* This is the component that we use for the event picker in the activity logs tab.
|
||||
* @param {object} obj
|
||||
* @param {string} obj.selected - the event that is currently selected
|
||||
* @param {function} obj.select - an action that happens when an item is selected
|
||||
*/
|
||||
export default function EventFilter({
|
||||
selected,
|
||||
select
|
||||
}: ListBoxProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Listbox value={t("activity:event." + selected)} onChange={select}>
|
||||
<div className="relative">
|
||||
<Listbox.Button className="bg-mineshaft-800 hover:bg-mineshaft-700 duration-200 cursor-pointer rounded-md h-10 flex items-center justify-between pl-4 pr-2 w-52 text-bunker-200 text-sm">
|
||||
{selected != '' ? (
|
||||
<p className="select-none text-bunker-100">{t("activity:event." + selected)}</p>
|
||||
) : (
|
||||
<p className="select-none">Select an event</p>
|
||||
)}
|
||||
{selected != '' ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faX}
|
||||
className="pl-2 w-2 p-2"
|
||||
onClick={() => select('')}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faAngleDown} className="pl-4 pr-2" />
|
||||
)}
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="border border-mineshaft-700 z-50 w-52 p-1 absolute mt-1 max-h-60 overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
{eventOptions.map((event, id) => {
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={id}
|
||||
className={`px-4 h-10 flex items-center text-sm cursor-pointer hover:bg-mineshaft-700 text-bunker-200 rounded-md ${
|
||||
selected == t("activity:event." + event.name) && 'bg-mineshaft-700'
|
||||
}`}
|
||||
value={event.name}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={`block truncate ${
|
||||
selected ? 'font-semibold' : 'font-normal'
|
||||
}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={event.icon} className="pr-4" />{' '}
|
||||
{t("activity:event." + event.name)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
);
|
||||
})}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
);
|
||||
}
|
||||
@@ -6,10 +6,12 @@ import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import {
|
||||
faBookOpen,
|
||||
faFileLines,
|
||||
faGear,
|
||||
faKey,
|
||||
faMobile,
|
||||
faPlug,
|
||||
faTimeline,
|
||||
faUser,
|
||||
} 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);
|
||||
setNewWorkspaceName("");
|
||||
} else {
|
||||
@@ -139,8 +141,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
{
|
||||
href:
|
||||
"/dashboard/" +
|
||||
workspaceMapping[workspaceSelected as any] +
|
||||
"?Development",
|
||||
workspaceMapping[workspaceSelected as any],
|
||||
title: t("nav:menu.secrets"),
|
||||
emoji: <FontAwesomeIcon icon={faKey} />,
|
||||
},
|
||||
@@ -154,6 +155,11 @@ export default function Layout({ children }: LayoutProps) {
|
||||
title: t("nav:menu.integrations"),
|
||||
emoji: <FontAwesomeIcon icon={faPlug} />,
|
||||
},
|
||||
{
|
||||
href: '/activity/' + workspaceMapping[workspaceSelected as any],
|
||||
title: 'Activity Logs',
|
||||
emoji: <FontAwesomeIcon icon={faFileLines} />
|
||||
},
|
||||
{
|
||||
href: "/settings/project/" + workspaceMapping[workspaceSelected as any],
|
||||
title: t("nav:menu.project-settings"),
|
||||
@@ -192,7 +198,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
.map((workspace: { _id: string }) => workspace._id)
|
||||
.includes(intendedWorkspaceId)
|
||||
) {
|
||||
router.push("/dashboard/" + userWorkspaces[0]._id + "?Development");
|
||||
router.push("/dashboard/" + userWorkspaces[0]._id);
|
||||
} else {
|
||||
setWorkspaceList(
|
||||
userWorkspaces.map((workspace: any) => workspace.name)
|
||||
@@ -235,8 +241,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
) {
|
||||
router.push(
|
||||
"/dashboard/" +
|
||||
workspaceMapping[workspaceSelected as any] +
|
||||
"?Development"
|
||||
workspaceMapping[workspaceSelected as any]
|
||||
);
|
||||
localStorage.setItem(
|
||||
"projectData.id",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from "react";
|
||||
import { Fragment } from "react";
|
||||
import React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import {
|
||||
faAngleDown,
|
||||
faCheck,
|
||||
faPlus,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
faPlus
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Listbox, Transition } from '@headlessui/react';
|
||||
|
||||
interface ListBoxProps {
|
||||
selected: string;
|
||||
@@ -34,20 +34,20 @@ export default function ListBox({
|
||||
data,
|
||||
text,
|
||||
buttonAction,
|
||||
isFull,
|
||||
isFull
|
||||
}: ListBoxProps): JSX.Element {
|
||||
return (
|
||||
<Listbox value={selected} onChange={onChange}>
|
||||
<div className="relative">
|
||||
<Listbox.Button
|
||||
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`}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
{text}
|
||||
<span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300">
|
||||
{" "}
|
||||
{' '}
|
||||
{selected}
|
||||
</span>
|
||||
</div>
|
||||
@@ -70,11 +70,11 @@ export default function ListBox({
|
||||
key={personIdx}
|
||||
className={({ active, selected }) =>
|
||||
`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
|
||||
? "bg-white/5 text-mineshaft-200 cursor-pointer"
|
||||
: "text-gray-400"
|
||||
? 'bg-white/5 text-mineshaft-200 cursor-pointer'
|
||||
: 'text-gray-400'
|
||||
} `
|
||||
}
|
||||
value={person}
|
||||
@@ -83,7 +83,7 @@ export default function ListBox({
|
||||
<>
|
||||
<span
|
||||
className={`block truncate text-primary${
|
||||
selected ? "font-medium" : "font-normal"
|
||||
selected ? 'font-medium' : 'font-normal'
|
||||
}`}
|
||||
>
|
||||
{person}
|
||||
|
||||
@@ -115,7 +115,7 @@ export default function Button(props: ButtonProps): JSX.Element {
|
||||
<FontAwesomeIcon
|
||||
icon={props.icon}
|
||||
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"}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { envMapping } from "../../../public/data/frequentConstants";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric,
|
||||
encryptSymmetric,
|
||||
} from "../../utilities/cryptography/crypto";
|
||||
import Button from "../buttons/Button";
|
||||
import InputField from "../InputField";
|
||||
@@ -25,11 +26,15 @@ const expiryMapping = {
|
||||
"12 months": 31104000,
|
||||
};
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
const AddServiceTokenDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
serviceTokens,
|
||||
setServiceTokens
|
||||
}) => {
|
||||
const [serviceToken, setServiceToken] = useState("");
|
||||
const [serviceTokenName, setServiceTokenName] = useState("");
|
||||
@@ -48,16 +53,14 @@ const AddServiceTokenDialog = ({
|
||||
privateKey: localStorage.getItem("PRIVATE_KEY"),
|
||||
});
|
||||
|
||||
// generate new public/private key pair
|
||||
const pair = nacl.box.keyPair();
|
||||
const publicKey = nacl.util.encodeBase64(pair.publicKey);
|
||||
const privateKey = nacl.util.encodeBase64(pair.secretKey);
|
||||
|
||||
// encrypt workspace key under newly-generated public key
|
||||
const { ciphertext: encryptedKey, nonce } = encryptAssymmetric({
|
||||
const randomBytes = crypto.randomBytes(16).toString('hex');
|
||||
const {
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: key,
|
||||
publicKey,
|
||||
privateKey,
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
let newServiceToken = await addServiceToken({
|
||||
@@ -65,13 +68,15 @@ const AddServiceTokenDialog = ({
|
||||
workspaceId,
|
||||
environment: envMapping[serviceTokenEnv],
|
||||
expiresIn: expiryMapping[serviceTokenExpiresIn],
|
||||
publicKey,
|
||||
encryptedKey,
|
||||
nonce,
|
||||
encryptedKey: ciphertext,
|
||||
iv,
|
||||
tag
|
||||
});
|
||||
|
||||
console.log('newServiceToken', newServiceToken);
|
||||
|
||||
const serviceToken = newServiceToken + "," + privateKey;
|
||||
setServiceToken(serviceToken);
|
||||
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
|
||||
setServiceToken(newServiceToken.serviceToken + "." + randomBytes);
|
||||
};
|
||||
|
||||
function copyToClipboard() {
|
||||
@@ -161,7 +166,7 @@ const AddServiceTokenDialog = ({
|
||||
"Production",
|
||||
"Testing",
|
||||
]}
|
||||
width="full"
|
||||
isFull={true}
|
||||
text={`${t("common:environment")}: `}
|
||||
/>
|
||||
</div>
|
||||
@@ -176,7 +181,7 @@ const AddServiceTokenDialog = ({
|
||||
"6 months",
|
||||
"12 months",
|
||||
]}
|
||||
width="full"
|
||||
isFull={true}
|
||||
text={`${t("common:expired-in")}: `}
|
||||
/>
|
||||
</div>
|
||||
@@ -211,7 +216,7 @@ const AddServiceTokenDialog = ({
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
type="text"
|
||||
value={serviceToken}
|
||||
@@ -236,7 +241,7 @@ const AddServiceTokenDialog = ({
|
||||
)}
|
||||
</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">
|
||||
{t("common.click-to-copy")}
|
||||
{t("common:click-to-copy")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,36 +1,53 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
||||
|
||||
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
|
||||
import { reverseEnvMapping } from '../../../public/data/frequentConstants';
|
||||
import guidGenerator from '../../utilities/randomId';
|
||||
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.
|
||||
* @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
|
||||
*/
|
||||
const ServiceTokenTable = ({ data, workspaceName }) => {
|
||||
const router = useRouter();
|
||||
const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTokensProps) => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
return (
|
||||
<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>
|
||||
<table className="w-full my-1">
|
||||
<thead className="text-bunker-300">
|
||||
<thead className="text-bunker-300 text-sm font-light">
|
||||
<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">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">Valid until</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">ENVIRONMENT</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">VAILD UNTIL</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.length > 0 ? (
|
||||
data.map((row, index) => {
|
||||
data?.map((row) => {
|
||||
return (
|
||||
<tr
|
||||
key={guidGenerator()}
|
||||
@@ -51,7 +68,14 @@ const ServiceTokenTable = ({ data, workspaceName }) => {
|
||||
<td className="py-2 border-mineshaft-700 border-t">
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
|
||||
<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"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
@@ -63,7 +87,7 @@ const ServiceTokenTable = ({ data, workspaceName }) => {
|
||||
})
|
||||
) : (
|
||||
<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
|
||||
</td>
|
||||
</tr>
|
||||
@@ -36,7 +36,7 @@ const Notification = ({
|
||||
|
||||
return (
|
||||
<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"
|
||||
>
|
||||
{notification.type === 'error' && (
|
||||
@@ -56,7 +56,7 @@ const Notification = ({
|
||||
onClick={() => clearNotification(notification.text)}
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -38,7 +38,7 @@ const NotificationProvider = ({ children }: NotificationProviderProps) => {
|
||||
const createNotification = ({
|
||||
text,
|
||||
type = 'success',
|
||||
timeoutMs = 5000
|
||||
timeoutMs = 4000
|
||||
}: Notification) => {
|
||||
const doesNotifExist = notifications.some((notif) => notif.text === text);
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ const DashboardInputField = ({
|
||||
return (
|
||||
<div className="flex-col w-full">
|
||||
<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'
|
||||
} rounded-md`}
|
||||
>
|
||||
@@ -85,7 +85,7 @@ const DashboardInputField = ({
|
||||
return (
|
||||
<div className="flex-col w-full">
|
||||
<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>}
|
||||
<input
|
||||
@@ -108,9 +108,9 @@ const DashboardInputField = ({
|
||||
} ${
|
||||
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) {
|
||||
return (
|
||||
<span className="ph-no-capture text-yellow" key={id}>
|
||||
@@ -139,7 +139,7 @@ const DashboardInputField = ({
|
||||
})}
|
||||
</div>
|
||||
{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">
|
||||
{value.split('').map(() => (
|
||||
<FontAwesomeIcon
|
||||
|
||||
@@ -15,40 +15,40 @@ interface SecretDataProps {
|
||||
|
||||
interface KeyPairProps {
|
||||
keyPair: SecretDataProps;
|
||||
deleteRow: (id: string) => void;
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
isBlurred: boolean;
|
||||
isDuplicate: boolean;
|
||||
toggleSidebar: (id: string) => void;
|
||||
sidebarSecretId: string;
|
||||
isSnapshot: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component represent a single row for an environemnt variable on the dashboard
|
||||
* @param {object} obj
|
||||
* @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.modifyValue - modify the value of a certain environment variable
|
||||
* @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 {function} obj.toggleSidebar - open/close/switch sidebar
|
||||
* @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
|
||||
*/
|
||||
const KeyPair = ({
|
||||
keyPair,
|
||||
deleteRow,
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
isBlurred,
|
||||
isDuplicate,
|
||||
toggleSidebar,
|
||||
sidebarSecretId
|
||||
sidebarSecretId,
|
||||
isSnapshot
|
||||
}: KeyPairProps) => {
|
||||
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">
|
||||
{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>
|
||||
@@ -57,7 +57,7 @@ const KeyPair = ({
|
||||
</span>
|
||||
</div>}
|
||||
<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
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
@@ -67,8 +67,8 @@ const KeyPair = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full min-w-5xl">
|
||||
<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="w-full min-w-xl">
|
||||
<div className={`flex min-w-xl items-center ${!isSnapshot && "pr-1.5"} rounded-lg mt-4 md:mt-0 max-h-10`}>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
@@ -79,24 +79,15 @@ const KeyPair = ({
|
||||
/>
|
||||
</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
|
||||
className="text-gray-300 px-2.5 text-lg mt-0.5"
|
||||
icon={faEllipsis}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(KeyPair);
|
||||
export default KeyPair;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
@@ -40,6 +41,7 @@ interface SideBarProps {
|
||||
savePush: () => void;
|
||||
sharedToHide: string[];
|
||||
setSharedToHide: (values: string[]) => void;
|
||||
deleteRow: any;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,6 +56,7 @@ interface SideBarProps {
|
||||
* @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 {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'
|
||||
*/
|
||||
const SideBar = ({
|
||||
@@ -67,93 +70,97 @@ const SideBar = ({
|
||||
buttonReady,
|
||||
savePush,
|
||||
sharedToHide,
|
||||
setSharedToHide
|
||||
setSharedToHide,
|
||||
deleteRow
|
||||
}: SideBarProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal"));
|
||||
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'>
|
||||
<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'/>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<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("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 className='mt-4 px-4 pointer-events-none'>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.key")}</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={data[0]?.pos}
|
||||
value={data[0]?.key}
|
||||
isDuplicate={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 className='mt-4 px-4 pointer-events-none'>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.key")}</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={data[0]?.pos}
|
||||
value={data[0]?.key}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</div>}
|
||||
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||
</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={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}
|
||||
position={data.filter(secret => secret.type == "shared")[0]?.pos}
|
||||
value={data.filter(secret => secret.type == "shared")[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred={true}
|
||||
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 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 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 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]`}>
|
||||
<Button
|
||||
text={String(t("common:save-changes"))}
|
||||
@@ -163,6 +170,14 @@ const SideBar = ({
|
||||
active={buttonReady}
|
||||
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>
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ class Capturer {
|
||||
capture(item) {
|
||||
if (ENV == "production" && TELEMETRY_CAPTURING_ENABLED) {
|
||||
try {
|
||||
api.capture(item);
|
||||
this.api.capture(item);
|
||||
} catch (error) {
|
||||
console.error("PostHog", error);
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class Capturer {
|
||||
identify(id) {
|
||||
if (ENV == "production" && TELEMETRY_CAPTURING_ENABLED) {
|
||||
try {
|
||||
api.identify(id);
|
||||
this.api.identify(id);
|
||||
} catch (error) {
|
||||
console.error("PostHog", error);
|
||||
}
|
||||
|
||||
32
frontend/ee/api/secrets/GetActionData.ts
Normal file
32
frontend/ee/api/secrets/GetActionData.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
actionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the data for a certain action performed by a user
|
||||
* @param {object} obj
|
||||
* @param {string} obj.actionId - id of an action for which we are trying to get data
|
||||
* @returns
|
||||
*/
|
||||
const getActionData = async ({ actionId }: workspaceProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/action/' + actionId, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
console.log(188, res)
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).action;
|
||||
} else {
|
||||
console.log('Failed to get the info about an action');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getActionData;
|
||||
72
frontend/ee/api/secrets/GetProjectLogs.ts
Normal file
72
frontend/ee/api/secrets/GetProjectLogs.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
workspaceId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
userId: string;
|
||||
actionNames: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the activity logs for a certain project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.workspaceId - workspace id for which we are trying to get project log
|
||||
* @param {object} obj.offset - teh starting point of logs that we want to pull
|
||||
* @param {object} obj.limit - how many logs will we output
|
||||
* @param {object} obj.userId - optional userId filter - will only query logs for that user
|
||||
* @param {string} obj.actionNames - optional actionNames filter - will only query logs for those actions
|
||||
* @returns
|
||||
*/
|
||||
const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames }: workspaceProps) => {
|
||||
let payload;
|
||||
if (userId != "" && actionNames != '') {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent',
|
||||
userId: JSON.stringify(userId),
|
||||
actionNames: actionNames
|
||||
}
|
||||
} else if (userId != "") {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent',
|
||||
userId: JSON.stringify(userId)
|
||||
}
|
||||
} else if (actionNames != "") {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent',
|
||||
actionNames: actionNames
|
||||
}
|
||||
} else {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent'
|
||||
}
|
||||
}
|
||||
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + '/logs?' +
|
||||
new URLSearchParams(payload),
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).logs;
|
||||
} else {
|
||||
console.log('Failed to get project logs');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getProjectLogs;
|
||||
39
frontend/ee/api/secrets/GetProjectSercetShanpshots.ts
Normal file
39
frontend/ee/api/secrets/GetProjectSercetShanpshots.ts
Normal file
@@ -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;
|
||||
31
frontend/ee/api/secrets/GetProjectSercetSnapshotsCount.ts
Normal file
31
frontend/ee/api/secrets/GetProjectSercetSnapshotsCount.ts
Normal file
@@ -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;
|
||||
31
frontend/ee/api/secrets/GetSecretSnapshotData.ts
Normal file
31
frontend/ee/api/secrets/GetSecretSnapshotData.ts
Normal file
@@ -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) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/secret/' + secretId + '/secret-versions?'+
|
||||
'/api/v1/secret/' + secretId + '/secret-versions?' +
|
||||
new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(limit)
|
||||
@@ -32,7 +32,7 @@ const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps
|
||||
if (res && res.status == 200) {
|
||||
return await res.json();
|
||||
} else {
|
||||
console.log('Failed to get project secrets');
|
||||
console.log('Failed to get secret version history');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
185
frontend/ee/components/ActivitySideBar.tsx
Normal file
185
frontend/ee/components/ActivitySideBar.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import getActionData from "ee/api/secrets/GetActionData";
|
||||
import patienceDiff from 'ee/utilities/findTextDifferences';
|
||||
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
|
||||
import DashboardInputField from '../../components/dashboard/DashboardInputField';
|
||||
|
||||
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} = require('../../components/utilities/cryptography/crypto');
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
|
||||
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: string) => void;
|
||||
currentAction: string;
|
||||
}
|
||||
|
||||
interface SecretProps {
|
||||
secret: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyHash: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueHash: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
}
|
||||
|
||||
interface DecryptedSecretProps {
|
||||
newSecretVersion: {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
oldSecretVersion: {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface ActionProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} obj
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @param {string} obj.currentAction - the action id for which a sidebar is being displayed
|
||||
* @returns the sidebar with the payload of user activity logs
|
||||
*/
|
||||
const ActivitySideBar = ({
|
||||
toggleSidebar,
|
||||
currentAction
|
||||
}: SideBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [actionData, setActionData] = useState<DecryptedSecretProps[]>();
|
||||
const [actionMetaData, setActionMetaData] = useState<ActionProps>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const tempActionData = await getActionData({ actionId: currentAction });
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
// #TODO: make this a separate function and reuse across the app
|
||||
let decryptedLatestKey: string;
|
||||
if (latestKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
decryptedLatestKey = decryptAssymmetric({
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
privateKey: String(PRIVATE_KEY)
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = tempActionData.payload.secretVersions.map((encryptedSecretVersion: {
|
||||
newSecretVersion?: SecretProps;
|
||||
oldSecretVersion?: SecretProps;
|
||||
}) => {
|
||||
return {
|
||||
newSecretVersion: {
|
||||
key: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.newSecretVersion!.secretKeyCiphertext,
|
||||
iv: encryptedSecretVersion.newSecretVersion!.secretKeyIV,
|
||||
tag: encryptedSecretVersion.newSecretVersion!.secretKeyTag,
|
||||
key: decryptedLatestKey
|
||||
}),
|
||||
value: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.newSecretVersion!.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.newSecretVersion!.secretValueIV,
|
||||
tag: encryptedSecretVersion.newSecretVersion!.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
},
|
||||
oldSecretVersion: {
|
||||
key: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext
|
||||
? decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext,
|
||||
iv: encryptedSecretVersion.oldSecretVersion?.secretKeyIV,
|
||||
tag: encryptedSecretVersion.oldSecretVersion?.secretKeyTag,
|
||||
key: decryptedLatestKey
|
||||
}): undefined,
|
||||
value: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext
|
||||
? decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.oldSecretVersion?.secretValueIV,
|
||||
tag: encryptedSecretVersion.oldSecretVersion?.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
}): undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setActionData(decryptedSecretVersions);
|
||||
setActionMetaData({name: tempActionData.name});
|
||||
setIsLoading(false);
|
||||
}
|
||||
getLogData();
|
||||
}, [currentAction]);
|
||||
|
||||
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between`}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full mb-8">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
></Image>
|
||||
</div>
|
||||
) : (
|
||||
<div className='h-min overflow-y-auto'>
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">{t("activity:event." + actionMetaData?.name)}</p>
|
||||
<div className='p-1' onClick={() => toggleSidebar("")}>
|
||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col px-4'>
|
||||
{(actionMetaData?.name == 'readSecrets'
|
||||
|| actionMetaData?.name == 'addSecrets'
|
||||
|| actionMetaData?.name == 'deleteSecrets') && actionData?.map((item, id) =>
|
||||
<div key={id}>
|
||||
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
|
||||
<DashboardInputField
|
||||
key={id}
|
||||
onChangeHandler={() => {}}
|
||||
type="value"
|
||||
position={1}
|
||||
value={item.newSecretVersion.value}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{actionMetaData?.name == 'updateSecrets' && actionData?.map((item, id) =>
|
||||
<>
|
||||
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
|
||||
<div className='text-bunker-100 font-mono rounded-md overflow-hidden'>
|
||||
<div className='bg-red/30 px-2'>- {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.bIndex != -1 && <span key={id} className={`${character.aIndex == -1 && "bg-red-700/80"}`}>{character.line}</span>)}</div>
|
||||
<div className='bg-green-500/30 px-2'>+ {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.aIndex != -1 && <span key={id} className={`${character.bIndex == -1 && "bg-green-700/80"}`}>{character.line}</span>)}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
};
|
||||
|
||||
export default ActivitySideBar;
|
||||
129
frontend/ee/components/ActivityTable.tsx
Normal file
129
frontend/ee/components/ActivityTable.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import {
|
||||
faAngleDown,
|
||||
faAngleRight,
|
||||
faUpRightFromSquare
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import timeSince from 'ee/utilities/timeSince';
|
||||
|
||||
import guidGenerator from '../../components/utilities/randomId';
|
||||
|
||||
|
||||
interface PayloadProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
secretVersions: string[];
|
||||
}
|
||||
|
||||
interface logData {
|
||||
_id: string;
|
||||
channel: string;
|
||||
createdAt: string;
|
||||
ipAddress: string;
|
||||
user: string;
|
||||
payload: PayloadProps[];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is a single row of the activity table
|
||||
* @param obj
|
||||
* @param {logData} obj.row - data for a certain event
|
||||
* @param {function} obj.toggleSidebar - open and close sidebar that displays data for a specific event
|
||||
* @returns
|
||||
*/
|
||||
const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar: (value: string) => void; }) => {
|
||||
const [payloadOpened, setPayloadOpened] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full text-sm">
|
||||
<td
|
||||
onClick={() => setPayloadOpened(!payloadOpened)}
|
||||
className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={payloadOpened ? faAngleDown : faAngleRight}
|
||||
className={`mt-2.5 ml-6 text-bunker-100 hover:bg-mineshaft-700 ${
|
||||
payloadOpened && 'bg-mineshaft-500'
|
||||
} p-1 duration-100 h-4 w-4 rounded-md`}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.payload?.map(action => String(action.secretVersions.length) + " " + t("activity:event." + action.name)).join(" and ")}
|
||||
</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.user}
|
||||
</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.channel}
|
||||
</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{timeSince(new Date(row.createdAt))}
|
||||
</td>
|
||||
</tr>
|
||||
{payloadOpened &&
|
||||
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
|
||||
<td></td>
|
||||
<td>Timestamp</td>
|
||||
<td>{row.createdAt}</td>
|
||||
</tr>}
|
||||
{payloadOpened &&
|
||||
row.payload?.map((action, index) =>
|
||||
<tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
|
||||
<td></td>
|
||||
<td className="">{t("activity:event." + action.name)}</td>
|
||||
<td className="text-primary-300 cursor-pointer hover:text-primary duration-200" onClick={() => toggleSidebar(action._id)}>
|
||||
{action.secretVersions.length + (action.secretVersions.length != 1 ? " secrets" : " secret")}
|
||||
<FontAwesomeIcon icon={faUpRightFromSquare} className="ml-2 mb-0.5 font-light w-3 h-3"/>
|
||||
</td>
|
||||
</tr>)}
|
||||
{payloadOpened &&
|
||||
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
|
||||
<td></td>
|
||||
<td>IP Address</td>
|
||||
<td>{row.ipAddress}</td>
|
||||
</tr>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the table for activity logs (one of the tabs)
|
||||
* @param {object} obj
|
||||
* @param {logData} obj.data - data for user activity logs
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @returns
|
||||
*/
|
||||
const ActivityTable = ({ data, toggleSidebar }: { data: logData[], toggleSidebar: (value: string) => void; }) => {
|
||||
return (
|
||||
<div className="w-full px-6 mt-8">
|
||||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative">
|
||||
<div className="absolute rounded-t-md w-full h-[3rem] bg-white/5"></div>
|
||||
<table className="w-full my-1">
|
||||
<thead className="text-bunker-300">
|
||||
<tr className='text-sm'>
|
||||
<th className="text-left pl-6 pt-2.5 pb-3"></th>
|
||||
<th className="text-left font-semibold pt-2.5 pb-3">EVENT</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">USER</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">SOURCE</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">TIME</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.map((row, index) => {
|
||||
return <ActivityLogsRow key={index} row={row} toggleSidebar={toggleSidebar} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityTable;
|
||||
158
frontend/ee/components/PITRecoverySidebar.tsx
Normal file
158
frontend/ee/components/PITRecoverySidebar.tsx
Normal file
@@ -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 Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from "next-i18next";
|
||||
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
|
||||
*/
|
||||
const SecretVersionList = ({ secretId }: { secretId: string; }) => {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [secretVersions, setSecretVersions] = useState<DecryptedSecretVersionListProps[]>([{createdAt: "123", value: "124"}]);
|
||||
const [secretVersions, setSecretVersions] = useState<DecryptedSecretVersionListProps[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const getSecretVersionHistory = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10});
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
||||
@@ -61,43 +65,54 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => {
|
||||
})
|
||||
|
||||
setSecretVersions(decryptedSecretVersions);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
};
|
||||
getSecretVersionHistory();
|
||||
}, []);
|
||||
}, [secretId]);
|
||||
|
||||
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>
|
||||
<div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none'>
|
||||
<div className='h-48 overflow-y-auto overflow-x-none'>
|
||||
{secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.map((version: DecryptedSecretVersionListProps, index: number) =>
|
||||
<div key={index} className='flex flex-row'>
|
||||
<div className='pr-1 flex flex-col items-center'>
|
||||
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
|
||||
<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'
|
||||
})}
|
||||
<p className=''>{t("dashboard:sidebar.version-history")}</p>
|
||||
<div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full'>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
></Image>
|
||||
</div>
|
||||
) : (
|
||||
<div className='h-48 overflow-y-auto overflow-x-none'>
|
||||
{secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.map((version: DecryptedSecretVersionListProps, index: number) =>
|
||||
<div key={index} className='flex flex-row'>
|
||||
<div className='pr-1 flex flex-col items-center'>
|
||||
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
|
||||
<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 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>
|
||||
};
|
||||
|
||||
export default SecretVersionList;
|
||||
|
||||
346
frontend/ee/utilities/findTextDifferences.ts
Normal file
346
frontend/ee/utilities/findTextDifferences.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
*
|
||||
* @param textOld - old secret
|
||||
* @param textNew - new (updated) secret
|
||||
* @param diffPlusFlag - a flag for whether we want to detect moving segments
|
||||
* - doesn't work in some examples (e.g., when we have a full reverse ordering of the text)
|
||||
* @returns
|
||||
*/
|
||||
function patienceDiff(textOld: string[], textNew: string[], diffPlusFlag?: boolean) {
|
||||
|
||||
/**
|
||||
* findUnique finds all unique values in arr[lo..hi], inclusive. This
|
||||
* function is used in preparation for determining the longest common
|
||||
* subsequence. Specifically, it first reduces the array range in question
|
||||
* to unique values.
|
||||
* @param chars - an array of characters
|
||||
* @param lo
|
||||
* @param hi
|
||||
* @returns - an ordered Map, with the arr[i] value as the Map key and the
|
||||
* array index i as the Map value.
|
||||
*/
|
||||
function findUnique(chars: string[], lo: number, hi: number) {
|
||||
const characterMap = new Map();
|
||||
|
||||
for (let i=lo; i<=hi; i++) {
|
||||
const character = chars[i];
|
||||
|
||||
if (characterMap.has(character)) {
|
||||
characterMap.get(character).count++;
|
||||
characterMap.get(character).index = i;
|
||||
} else {
|
||||
characterMap.set(character, { count: 1, index: i });
|
||||
}
|
||||
}
|
||||
|
||||
characterMap.forEach((val, key, map) => {
|
||||
if (val.count !== 1) {
|
||||
map.delete(key);
|
||||
} else {
|
||||
map.set(key, val.index);
|
||||
}
|
||||
});
|
||||
|
||||
return characterMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param aArray
|
||||
* @param aLo
|
||||
* @param aHi
|
||||
* @param bArray
|
||||
* @param bLo
|
||||
* @param bHi
|
||||
* @returns an ordered Map, with the Map key as the common line between aArray
|
||||
* and bArray, with the Map value as an object containing the array indexes of
|
||||
* the matching unique lines.
|
||||
*
|
||||
*/
|
||||
function uniqueCommon(aArray: string[], aLo: number, aHi: number, bArray: string[], bLo: number, bHi: number) {
|
||||
const ma = findUnique(aArray, aLo, aHi);
|
||||
const mb = findUnique(bArray, bLo, bHi);
|
||||
|
||||
ma.forEach((val, key, map) => {
|
||||
if (mb.has(key)) {
|
||||
map.set(key, {
|
||||
indexA: val,
|
||||
indexB: mb.get(key)
|
||||
});
|
||||
} else {
|
||||
map.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
return ma;
|
||||
}
|
||||
|
||||
/**
|
||||
* longestCommonSubsequence takes an ordered Map from the function uniqueCommon
|
||||
* and determines the Longest Common Subsequence (LCS).
|
||||
* @param abMap
|
||||
* @returns an ordered array of objects containing the array indexes of the
|
||||
* matching lines for a LCS.
|
||||
*/
|
||||
function longestCommonSubsequence(abMap: Map<number, { indexA: number, indexB: number, prev?: number }>) {
|
||||
const ja: any = [];
|
||||
|
||||
// First, walk the list creating the jagged array.
|
||||
abMap.forEach((val, key, map) => {
|
||||
let i = 0;
|
||||
|
||||
while (ja[i] && ja[i][ja[i].length - 1].indexB < val.indexB) {
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!ja[i]) {
|
||||
ja[i] = [];
|
||||
}
|
||||
|
||||
if (0 < i) {
|
||||
val.prev = ja[i-1][ja[i - 1].length - 1];
|
||||
}
|
||||
ja[i].push(val);
|
||||
});
|
||||
|
||||
// Now, pull out the longest common subsequence.
|
||||
let lcs: any[] = [];
|
||||
|
||||
if (0 < ja.length) {
|
||||
const n = ja.length - 1;
|
||||
lcs = [ja[n][ja[n].length - 1]];
|
||||
|
||||
while (lcs[lcs.length - 1].prev) {
|
||||
lcs.push(lcs[lcs.length - 1].prev);
|
||||
}
|
||||
}
|
||||
|
||||
return lcs.reverse();
|
||||
}
|
||||
|
||||
// "result" is the array used to accumulate the textOld that are deleted, the
|
||||
// lines that are shared between textOld and textNew, and the textNew that were
|
||||
// inserted.
|
||||
|
||||
const result: any[] = [];
|
||||
let deleted = 0;
|
||||
let inserted = 0;
|
||||
|
||||
// aMove and bMove will contain the lines that don't match, and will be returned
|
||||
// for possible searching of lines that moved.
|
||||
|
||||
const aMove: any[] = [];
|
||||
const aMoveIndex: any[] = [];
|
||||
const bMove: any[] = [];
|
||||
const bMoveIndex: any[] = [];
|
||||
|
||||
/**
|
||||
* addToResult simply pushes the latest value onto the "result" array. This
|
||||
* array captures the diff of the line, aIndex, and bIndex from the textOld
|
||||
* and textNew array.
|
||||
* @param aIndex
|
||||
* @param bIndex
|
||||
*/
|
||||
function addToResult(aIndex: number, bIndex: number) {
|
||||
if (bIndex < 0) {
|
||||
aMove.push(textOld[aIndex]);
|
||||
aMoveIndex.push(result.length);
|
||||
deleted++;
|
||||
} else if (aIndex < 0) {
|
||||
bMove.push(textNew[bIndex]);
|
||||
bMoveIndex.push(result.length);
|
||||
inserted++;
|
||||
}
|
||||
|
||||
result.push({
|
||||
line: 0 <= aIndex ? textOld[aIndex] : textNew[bIndex],
|
||||
aIndex: aIndex,
|
||||
bIndex: bIndex,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* addSubMatch handles the lines between a pair of entries in the LCS. Thus,
|
||||
* this function might recursively call recurseLCS to further match the lines
|
||||
* between textOld and textNew.
|
||||
* @param aLo
|
||||
* @param aHi
|
||||
* @param bLo
|
||||
* @param bHi
|
||||
*/
|
||||
function addSubMatch(aLo: number, aHi: number, bLo: number, bHi: number) {
|
||||
// Match any lines at the beginning of textOld and textNew.
|
||||
while (aLo <= aHi && bLo <= bHi && textOld[aLo] === textNew[bLo]) {
|
||||
addToResult(aLo++, bLo++);
|
||||
}
|
||||
|
||||
// Match any lines at the end of textOld and textNew, but don't place them
|
||||
// in the "result" array just yet, as the lines between these matches at
|
||||
// the beginning and the end need to be analyzed first.
|
||||
|
||||
const aHiTemp = aHi;
|
||||
while (aLo <= aHi && bLo <= bHi && textOld[aHi] === textNew[bHi]) {
|
||||
aHi--;
|
||||
bHi--;
|
||||
}
|
||||
|
||||
// Now, check to determine with the remaining lines in the subsequence
|
||||
// whether there are any unique common lines between textOld and textNew.
|
||||
//
|
||||
// If not, add the subsequence to the result (all textOld having been
|
||||
// deleted, and all textNew having been inserted).
|
||||
//
|
||||
// If there are unique common lines between textOld and textNew, then let's
|
||||
// recursively perform the patience diff on the subsequence.
|
||||
|
||||
const uniqueCommonMap = uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi);
|
||||
|
||||
if (uniqueCommonMap.size === 0) {
|
||||
while (aLo <= aHi) {
|
||||
addToResult(aLo++, -1);
|
||||
}
|
||||
|
||||
while (bLo <= bHi) {
|
||||
addToResult(-1, bLo++);
|
||||
}
|
||||
} else {
|
||||
recurseLCS(aLo, aHi, bLo, bHi, uniqueCommonMap);
|
||||
}
|
||||
|
||||
// Finally, let's add the matches at the end to the result.
|
||||
while (aHi < aHiTemp) {
|
||||
addToResult(++aHi, ++bHi);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* recurseLCS finds the longest common subsequence (LCS) between the arrays
|
||||
* textOld[aLo..aHi] and textNew[bLo..bHi] inclusive. Then for each subsequence
|
||||
* recursively performs another LCS search (via addSubMatch), until there are
|
||||
* none found, at which point the subsequence is dumped to the result.
|
||||
* @param aLo
|
||||
* @param aHi
|
||||
* @param bLo
|
||||
* @param bHi
|
||||
* @param uniqueCommonMap
|
||||
*/
|
||||
function recurseLCS(aLo: number, aHi: number, bLo: number, bHi: number, uniqueCommonMap?: any) {
|
||||
const x = longestCommonSubsequence(uniqueCommonMap || uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi));
|
||||
|
||||
if (x.length === 0) {
|
||||
addSubMatch(aLo, aHi, bLo, bHi);
|
||||
} else {
|
||||
if (aLo < x[0].indexA || bLo < x[0].indexB) {
|
||||
addSubMatch(aLo, x[0].indexA - 1, bLo, x[0].indexB - 1);
|
||||
}
|
||||
|
||||
let i;
|
||||
for (i = 0; i < x.length - 1; i++) {
|
||||
addSubMatch(x[i].indexA, x[i+1].indexA - 1, x[i].indexB, x[i+1].indexB - 1);
|
||||
}
|
||||
|
||||
if (x[i].indexA <= aHi || x[i].indexB <= bHi) {
|
||||
addSubMatch(x[i].indexA, aHi, x[i].indexB, bHi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recurseLCS(0, textOld.length - 1, 0, textNew.length - 1);
|
||||
|
||||
if (diffPlusFlag) {
|
||||
return {
|
||||
lines: result,
|
||||
lineCountDeleted: deleted,
|
||||
lineCountInserted: inserted,
|
||||
lineCountMoved: 0,
|
||||
aMove: aMove,
|
||||
aMoveIndex: aMoveIndex,
|
||||
bMove: bMove,
|
||||
bMoveIndex: bMoveIndex,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
lines: result,
|
||||
lineCountDeleted: deleted,
|
||||
lineCountInserted: inserted,
|
||||
lineCountMoved: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* use: patienceDiffPlus( textOld[], textNew[] )
|
||||
*
|
||||
* where:
|
||||
* textOld[] contains the original text lines.
|
||||
* textNew[] contains the new text lines.
|
||||
*
|
||||
* returns an object with the following properties:
|
||||
* lines[] with properties of:
|
||||
* line containing the line of text from textOld or textNew.
|
||||
* aIndex referencing the index in aLine[].
|
||||
* bIndex referencing the index in textNew[].
|
||||
* (Note: The line is text from either textOld or textNew, with aIndex and bIndex
|
||||
* referencing the original index. If aIndex === -1 then the line is new from textNew,
|
||||
* and if bIndex === -1 then the line is old from textOld.)
|
||||
* moved is true if the line was moved from elsewhere in textOld[] or textNew[].
|
||||
* lineCountDeleted is the number of lines from textOld[] not appearing in textNew[].
|
||||
* lineCountInserted is the number of lines from textNew[] not appearing in textOld[].
|
||||
* lineCountMoved is the number of lines that moved.
|
||||
*/
|
||||
|
||||
function patienceDiffPlus(textOld: string[], textNew: string[]) {
|
||||
|
||||
const difference = patienceDiff(textOld, textNew, true);
|
||||
|
||||
let aMoveNext = difference.aMove;
|
||||
let aMoveIndexNext = difference.aMoveIndex;
|
||||
let bMoveNext = difference.bMove;
|
||||
let bMoveIndexNext = difference.bMoveIndex;
|
||||
|
||||
delete difference.aMove;
|
||||
delete difference.aMoveIndex;
|
||||
delete difference.bMove;
|
||||
delete difference.bMoveIndex;
|
||||
|
||||
let lastLineCountMoved;
|
||||
|
||||
do {
|
||||
const aMove = aMoveNext;
|
||||
const aMoveIndex = aMoveIndexNext;
|
||||
const bMove = bMoveNext;
|
||||
const bMoveIndex = bMoveIndexNext;
|
||||
|
||||
aMoveNext = [];
|
||||
aMoveIndexNext = [];
|
||||
bMoveNext = [];
|
||||
bMoveIndexNext = [];
|
||||
|
||||
const subDiff = patienceDiff(aMove!, bMove!);
|
||||
|
||||
lastLineCountMoved = difference.lineCountMoved;
|
||||
|
||||
subDiff.lines.forEach((v, i) => {
|
||||
|
||||
if (0 <= v.aIndex && 0 <= v.bIndex) {
|
||||
|
||||
difference.lines[aMoveIndex![v.aIndex]].moved = true;
|
||||
difference.lines[bMoveIndex![v.bIndex]].aIndex = aMoveIndex![v.aIndex];
|
||||
difference.lines[bMoveIndex![v.bIndex]].moved = true;
|
||||
difference.lineCountInserted--;
|
||||
difference.lineCountDeleted--;
|
||||
difference.lineCountMoved++;
|
||||
} else if (v.bIndex < 0) {
|
||||
aMoveNext!.push(aMove![v.aIndex]);
|
||||
aMoveIndexNext!.push(aMoveIndex![v.aIndex]);
|
||||
} else {
|
||||
bMoveNext!.push(bMove![v.bIndex]);
|
||||
bMoveIndexNext!.push(bMoveIndex![v.bIndex]);
|
||||
}
|
||||
});
|
||||
} while (0 < difference.lineCountMoved - lastLineCountMoved);
|
||||
|
||||
return difference;
|
||||
|
||||
}
|
||||
|
||||
export default patienceDiff;
|
||||
35
frontend/ee/utilities/timeSince.ts
Normal file
35
frontend/ee/utilities/timeSince.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Time since a certain date
|
||||
* @param {Date} date - the timestamp got which we want to understand how long ago it happened
|
||||
* @returns {String} text - how much time has passed since a certain timestamp
|
||||
*/
|
||||
function timeSince(date: Date) {
|
||||
const seconds = Math.floor(
|
||||
((new Date() as any) - (date as any)) / 1000
|
||||
) as number;
|
||||
|
||||
let interval = seconds / 31536000;
|
||||
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' years ago';
|
||||
}
|
||||
interval = seconds / 2592000;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' months ago';
|
||||
}
|
||||
interval = seconds / 86400;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' days ago';
|
||||
}
|
||||
interval = seconds / 3600;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' hours ago';
|
||||
}
|
||||
interval = seconds / 60;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' minutes ago';
|
||||
}
|
||||
return Math.floor(seconds) + ' seconds ago';
|
||||
}
|
||||
|
||||
export default timeSince;
|
||||
4096
frontend/package-lock.json
generated
4096
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
145
frontend/pages/activity/[id].tsx
Normal file
145
frontend/pages/activity/[id].tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import ActivitySideBar from 'ee/components/ActivitySideBar';
|
||||
|
||||
import Button from '~/components/basic/buttons/Button';
|
||||
import EventFilter from '~/components/basic/EventFilter';
|
||||
import NavHeader from '~/components/navigation/NavHeader';
|
||||
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
|
||||
|
||||
import getProjectLogs from '../../ee/api/secrets/GetProjectLogs';
|
||||
import ActivityTable from '../../ee/components/ActivityTable';
|
||||
|
||||
|
||||
interface logData {
|
||||
_id: string;
|
||||
channel: string;
|
||||
createdAt: string;
|
||||
ipAddress: string;
|
||||
user: {
|
||||
email: string;
|
||||
};
|
||||
actions: {
|
||||
_id: string;
|
||||
name: string;
|
||||
payload: {
|
||||
secretVersions: string[];
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
||||
interface PayloadProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
secretVersions: string[];
|
||||
}
|
||||
|
||||
interface logDataPoint {
|
||||
_id: string;
|
||||
channel: string;
|
||||
createdAt: string;
|
||||
ipAddress: string;
|
||||
user: string;
|
||||
payload: PayloadProps[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the tab that includes all of the user activity logs
|
||||
*/
|
||||
export default function Activity() {
|
||||
const router = useRouter();
|
||||
const [eventChosen, setEventChosen] = useState('');
|
||||
const [logsData, setLogsData] = useState<logDataPoint[]>([]);
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
const currentLimit = 10;
|
||||
const [currentSidebarAction, toggleSidebar] = useState<string>()
|
||||
const { t } = useTranslation();
|
||||
|
||||
// this use effect updates the data in case of a new filter being added
|
||||
useEffect(() => {
|
||||
setCurrentOffset(0);
|
||||
const getLogData = async () => {
|
||||
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: 0, limit: currentLimit, userId: "", actionNames: eventChosen })
|
||||
setLogsData(tempLogsData.map((log: logData) => {
|
||||
return {
|
||||
_id: log._id,
|
||||
channel: log.channel,
|
||||
createdAt: log.createdAt,
|
||||
ipAddress: log.ipAddress,
|
||||
user: log.user.email,
|
||||
payload: log.actions.map(action => {
|
||||
return {
|
||||
_id: action._id,
|
||||
name: action.name,
|
||||
secretVersions: action.payload.secretVersions
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
}
|
||||
getLogData();
|
||||
}, [eventChosen]);
|
||||
|
||||
// this use effect adds more data in case 'View More' button is clicked
|
||||
useEffect(() => {
|
||||
const getLogData = async () => {
|
||||
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: currentOffset, limit: currentLimit, userId: "", actionNames: eventChosen })
|
||||
setLogsData(logsData.concat(tempLogsData.map((log: logData) => {
|
||||
return {
|
||||
_id: log._id,
|
||||
channel: log.channel,
|
||||
createdAt: log.createdAt,
|
||||
ipAddress: log.ipAddress,
|
||||
user: log.user.email,
|
||||
payload: log.actions.map(action => {
|
||||
return {
|
||||
_id: action._id,
|
||||
name: action.name,
|
||||
secretVersions: action.payload.secretVersions
|
||||
}
|
||||
})
|
||||
}
|
||||
})))
|
||||
}
|
||||
getLogData();
|
||||
}, [currentLimit, currentOffset]);
|
||||
|
||||
const loadMoreLogs = () => {
|
||||
setCurrentOffset(currentOffset + currentLimit);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll h-screen">
|
||||
<NavHeader pageName="Project Activity" isProjectRelated={true} />
|
||||
{currentSidebarAction && <ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />}
|
||||
<div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2">
|
||||
<div className="flex flex-row justify-start items-center text-3xl">
|
||||
<p className="font-semibold mr-4 text-bunker-100">Activity Logs</p>
|
||||
</div>
|
||||
<p className="mr-4 text-base text-gray-400">
|
||||
Event history for this Infisical project.
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-6 h-8 mt-2">
|
||||
<EventFilter
|
||||
selected={eventChosen}
|
||||
select={setEventChosen}
|
||||
/>
|
||||
</div>
|
||||
<ActivityTable
|
||||
data={logsData}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
<div className='flex justify-center w-full mb-6'>
|
||||
<div className='items-center w-60'>
|
||||
<Button text="View More" textDisabled="End of History" active={logsData.length % 10 == 0 ? true : false} onButtonPressed={loadMoreLogs} size="md" color="mineshaft"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Activity.requireAuth = true;
|
||||
|
||||
export const getServerSideProps = getTranslatedServerSideProps(["activity"]);
|
||||
@@ -5,14 +5,21 @@ interface Props {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
expiresIn: number;
|
||||
publicKey: string;
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
const addServiceToken = ({
|
||||
@@ -20,11 +27,11 @@ const addServiceToken = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
expiresIn,
|
||||
publicKey,
|
||||
encryptedKey,
|
||||
nonce
|
||||
iv,
|
||||
tag
|
||||
}: Props) => {
|
||||
return SecurityClient.fetchCall('/api/v1/service-token/', {
|
||||
return SecurityClient.fetchCall('/api/v2/service-token/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -34,13 +41,13 @@ const addServiceToken = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
expiresIn,
|
||||
publicKey,
|
||||
encryptedKey,
|
||||
nonce
|
||||
iv,
|
||||
tag
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).token;
|
||||
return (await res.json());
|
||||
} else {
|
||||
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