Fix merge conflicts

This commit is contained in:
Tuan Dang
2023-06-10 11:07:31 +01:00
42 changed files with 1270 additions and 955 deletions
@@ -1,6 +1,6 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { ISecret, Secret } from "../../models"; import { ISecret, Secret, ServiceTokenData } from "../../models";
import { IAction, SecretVersion } from "../../ee/models"; import { IAction, SecretVersion } from "../../ee/models";
import { import {
SECRET_PERSONAL, SECRET_PERSONAL,
@@ -29,6 +29,7 @@ import { BatchSecretRequest, BatchSecret } from "../../types/secret";
import Folder from "../../models/folder"; import Folder from "../../models/folder";
import { import {
getFolderByPath, getFolderByPath,
getFolderIdFromServiceToken,
searchByFolderId, searchByFolderId,
} from "../../services/FolderService"; } from "../../services/FolderService";
@@ -45,14 +46,15 @@ export const batchSecrets = async (req: Request, res: Response) => {
const { const {
workspaceId, workspaceId,
environment, environment,
folderId,
requests, requests,
secretPath,
}: { }: {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
folderId: string;
requests: BatchSecretRequest[]; requests: BatchSecretRequest[];
secretPath: string;
} = req.body; } = req.body;
let folderId = req.body.folderId as string;
const createSecrets: BatchSecret[] = []; const createSecrets: BatchSecret[] = [];
const updateSecrets: BatchSecret[] = []; const updateSecrets: BatchSecret[] = [];
@@ -70,6 +72,25 @@ export const batchSecrets = async (req: Request, res: Response) => {
if (!folder) throw BadRequestError({ message: "Folder not found" }); if (!folder) throw BadRequestError({ message: "Folder not found" });
} }
if (req.authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
// in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used
if (
(!secretPath && folderId !== "root") ||
(secretPath && secretPath !== serviceTkScopedSecretPath)
) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
if (secretPath) {
folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
}
for await (const request of requests) { for await (const request of requests) {
// do a validation // do a validation
@@ -152,6 +173,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
numberOfSecrets: createdSecrets.length, numberOfSecrets: createdSecrets.length,
environment, environment,
workspaceId, workspaceId,
folderId,
channel, channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"],
}, },
@@ -218,7 +240,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8,
tags: u.tags, tags: u.tags,
folder: u.folder folder: u.folder,
}) })
); );
@@ -248,6 +270,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
numberOfSecrets: updateSecrets.length, numberOfSecrets: updateSecrets.length,
environment, environment,
workspaceId, workspaceId,
folderId,
channel, channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"],
}, },
@@ -395,8 +418,13 @@ export const createSecrets = async (req: Request, res: Response) => {
const { const {
workspaceId, workspaceId,
environment, environment,
folderId, secretPath,
}: { workspaceId: string; environment: string; folderId: string } = req.body; }: {
workspaceId: string;
environment: string;
secretPath?: string;
} = req.body;
let folderId = req.body.folderId;
if (req.user) { if (req.user) {
const hasAccess = await userHasWorkspaceAccess( const hasAccess = await userHasWorkspaceAccess(
@@ -421,6 +449,24 @@ export const createSecrets = async (req: Request, res: Response) => {
// case: create 1 secret // case: create 1 secret
listOfSecretsToCreate = [req.body.secrets]; listOfSecretsToCreate = [req.body.secrets];
} }
if (req.authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
// in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used
if (
(!secretPath && folderId !== "root") ||
(secretPath && secretPath !== serviceTkScopedSecretPath)
) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
if (secretPath) {
folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
}
// get secret blind index salt // get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({ const salt = await SecretService.getSecretBlindIndexSalt({
@@ -585,6 +631,7 @@ export const createSecrets = async (req: Request, res: Response) => {
environment, environment,
workspaceId, workspaceId,
channel: channel, channel: channel,
folderId,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"],
}, },
}); });
@@ -660,6 +707,18 @@ export const getSecrets = async (req: Request, res: Response) => {
if (!folder) throw BadRequestError({ message: "Folder not found" }); if (!folder) throw BadRequestError({ message: "Folder not found" });
} }
if (req.authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
// in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used
if (
(!secretPath && folderId !== "root") ||
(secretPath && secretPath !== serviceTkScopedSecretPath)
) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
if (folders && secretPath) { if (folders && secretPath) {
if (!folders) throw BadRequestError({ message: "Folder not found" }); if (!folders) throw BadRequestError({ message: "Folder not found" });
const folder = getFolderByPath(folders.nodes, secretPath as string); const folder = getFolderByPath(folders.nodes, secretPath as string);
@@ -800,6 +859,7 @@ export const getSecrets = async (req: Request, res: Response) => {
environment, environment,
workspaceId, workspaceId,
channel, channel,
folderId,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"],
}, },
}); });
@@ -1,20 +1,18 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import crypto from 'crypto'; import crypto from "crypto";
import bcrypt from 'bcrypt'; import bcrypt from "bcrypt";
import { import { User, ServiceAccount, ServiceTokenData } from "../../models";
User, import { userHasWorkspaceAccess } from "../../ee/helpers/checkMembershipPermissions";
ServiceAccount,
ServiceTokenData
} from '../../models';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
import { import {
PERMISSION_READ_SECRETS, PERMISSION_READ_SECRETS,
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN AUTH_MODE_SERVICE_TOKEN,
} from '../../variables'; } from "../../variables";
import { getSaltRounds } from '../../config'; import { getSaltRounds } from "../../config";
import { BadRequestError } from '../../utils/errors'; import { BadRequestError } from "../../utils/errors";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
/** /**
* Return service token data associated with service token on request * Return service token data associated with service token on request
@@ -49,17 +47,19 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
} }
*/ */
if (!(req.authData.authPayload instanceof ServiceTokenData)) throw BadRequestError({ if (!(req.authData.authPayload instanceof ServiceTokenData))
message: 'Failed accepted client validation for service token data' throw BadRequestError({
message: "Failed accepted client validation for service token data",
}); });
const serviceTokenData = await ServiceTokenData const serviceTokenData = await ServiceTokenData.findById(
.findById(req.authData.authPayload._id) req.authData.authPayload._id
.select('+encryptedKey +iv +tag') )
.populate('user'); .select("+encryptedKey +iv +tag")
.populate("user");
return res.status(200).json(serviceTokenData); return res.status(200).json(serviceTokenData);
} };
/** /**
* Create new service token data for workspace with id [workspaceId] and * Create new service token data for workspace with id [workspaceId] and
@@ -79,25 +79,44 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
iv, iv,
tag, tag,
expiresIn, expiresIn,
permissions secretPath,
permissions,
} = req.body; } = req.body;
const secret = crypto.randomBytes(16).toString('hex'); const folders = await Folder.findOne({
workspace: workspaceId,
environment,
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (folder == undefined) {
throw BadRequestError({ message: "Path for service token does not exist" })
}
}
const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds()); const secretHash = await bcrypt.hash(secret, await getSaltRounds());
let expiresAt; let expiresAt;
if (expiresIn) { if (expiresIn) {
expiresAt = new Date() expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
} }
let user, serviceAccount; let user, serviceAccount;
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) { if (
req.authData.authMode === AUTH_MODE_JWT &&
req.authData.authPayload instanceof User
) {
user = req.authData.authPayload._id; user = req.authData.authPayload._id;
} }
if (req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && req.authData.authPayload instanceof ServiceAccount) { if (
req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
req.authData.authPayload instanceof ServiceAccount
) {
serviceAccount = req.authData.authPayload._id; serviceAccount = req.authData.authPayload._id;
} }
@@ -113,21 +132,22 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
encryptedKey, encryptedKey,
iv, iv,
tag, tag,
permissions secretPath,
permissions,
}).save(); }).save();
// return service token data without sensitive data // return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (!serviceTokenData) throw new Error('Failed to find service token data'); if (!serviceTokenData) throw new Error("Failed to find service token data");
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
return res.status(200).send({ return res.status(200).send({
serviceToken, serviceToken,
serviceTokenData serviceTokenData,
}); });
} };
/** /**
* Delete service token data with id [serviceTokenDataId]. * Delete service token data with id [serviceTokenDataId].
@@ -138,9 +158,11 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
export const deleteServiceTokenData = async (req: Request, res: Response) => { export const deleteServiceTokenData = async (req: Request, res: Response) => {
const { serviceTokenDataId } = req.params; const { serviceTokenDataId } = req.params;
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); const serviceTokenData = await ServiceTokenData.findByIdAndDelete(
serviceTokenDataId
);
return res.status(200).send({ return res.status(200).send({
serviceTokenData serviceTokenData,
}); });
} };
+87 -32
View File
@@ -1,11 +1,7 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import { SecretService, EventService } from "../../services";
SecretService, import { eventPushSecrets } from "../../events";
TelemetryService,
EventService
} from '../../services';
import { eventPushSecrets } from '../../events';
/** /**
* Get secrets for workspace with id [workspaceId] and environment * Get secrets for workspace with id [workspaceId] and environment
@@ -16,17 +12,19 @@ import { eventPushSecrets } from '../../events';
export const getSecrets = async (req: Request, res: Response) => { export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string; const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string; const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
authData: req.authData secretPath,
authData: req.authData,
}); });
return res.status(200).send({ return res.status(200).send({
secrets secrets,
}); });
} };
/** /**
* Get secret with name [secretName] * Get secret with name [secretName]
@@ -37,20 +35,22 @@ export const getSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const workspaceId = req.query.workspaceId as string; const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string; const environment = req.query.environment as string;
const type = req.query.type as 'shared' | 'personal' | undefined; const secretPath = req.query.secretPath as string;
const type = req.query.type as "shared" | "personal" | undefined;
const secret = await SecretService.getSecret({ const secret = await SecretService.getSecret({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
authData: req.authData secretPath,
authData: req.authData,
}); });
return res.status(200).send({ return res.status(200).send({
secret secret,
}); });
} };
/** /**
* Create secret with name [secretName] * Create secret with name [secretName]
@@ -58,6 +58,7 @@ export const getSecretByName = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const createSecret = async (req: Request, res: Response) => { export const createSecret = async (req: Request, res: Response) => {
<<<<<<< HEAD
const { secretName } = req.params; const { secretName } = req.params;
const { const {
workspaceId, workspaceId,
@@ -109,6 +110,61 @@ export const createSecret = async (req: Request, res: Response) => {
secret: secretWithoutBlindIndex secret: secretWithoutBlindIndex
}); });
} }
=======
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretPath = "/",
} = req.body;
const secret = await SecretService.createSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath,
...(secretCommentCiphertext && secretCommentIV && secretCommentTag
? {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
}
: {}),
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({
secret: secretWithoutBlindIndex,
});
};
>>>>>>> origin
/** /**
* Update secret with name [secretName] * Update secret with name [secretName]
@@ -123,7 +179,8 @@ export const updateSecretByName = async (req: Request, res: Response) => {
type, type,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag secretValueTag,
secretPath = "/",
} = req.body; } = req.body;
const secret = await SecretService.updateSecret({ const secret = await SecretService.updateSecret({
@@ -134,20 +191,21 @@ export const updateSecretByName = async (req: Request, res: Response) => {
authData: req.authData, authData: req.authData,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag secretValueTag,
secretPath,
}); });
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment environment,
}) }),
}); });
return res.status(200).send({ return res.status(200).send({
secret secret,
}); });
} };
/** /**
* Delete secret with name [secretName] * Delete secret with name [secretName]
@@ -156,28 +214,25 @@ export const updateSecretByName = async (req: Request, res: Response) => {
*/ */
export const deleteSecretByName = async (req: Request, res: Response) => { export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const { workspaceId, environment, type, secretPath = "/" } = req.body;
workspaceId,
environment,
type
} = req.body;
const { secret, secrets } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({
secretName, secretName,
workspaceId, workspaceId,
environment, environment,
type, type,
authData: req.authData authData: req.authData,
secretPath,
}); });
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment environment,
}) }),
}); });
return res.status(200).send({ return res.status(200).send({
secret secret,
}); });
} };
+3 -1
View File
@@ -28,6 +28,7 @@ interface FeatureSet {
customRateLimits: boolean; customRateLimits: boolean;
customAlerts: boolean; customAlerts: boolean;
auditLogs: boolean; auditLogs: boolean;
envLimit?: number | null;
} }
/** /**
@@ -55,7 +56,8 @@ class EELicenseService {
rbac: true, rbac: true,
customRateLimits: true, customRateLimits: true,
customAlerts: true, customAlerts: true,
auditLogs: false auditLogs: false,
envLimit: null
} }
public localFeatureSet: NodeCache; public localFeatureSet: NodeCache;
+119 -32
View File
@@ -1,4 +1,4 @@
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import {
CreateSecretParams, CreateSecretParams,
GetSecretsParams, GetSecretsParams,
@@ -7,18 +7,19 @@ import {
DeleteSecretParams, DeleteSecretParams,
} from '../interfaces/services/SecretService'; } from '../interfaces/services/SecretService';
import { import {
ISecret,
Secret, Secret,
ISecret,
SecretBlindIndexData, SecretBlindIndexData,
BotKey ServiceTokenData,
} from '../models'; } from "../models";
import { SecretVersion } from '../ee/models'; import { SecretVersion } from "../ee/models";
import { import {
BadRequestError, BadRequestError,
SecretNotFoundError, SecretNotFoundError,
SecretBlindIndexDataNotFoundError, SecretBlindIndexDataNotFoundError,
InternalServerError, InternalServerError,
} from '../utils/errors'; UnauthorizedRequestError,
} from "../utils/errors";
import { import {
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED,
@@ -29,20 +30,21 @@ import {
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64, ENCODING_SCHEME_BASE64,
} from '../variables'; } from "../variables";
import crypto from 'crypto'; import crypto from "crypto";
import * as argon2 from 'argon2'; import * as argon2 from "argon2";
import { import {
encryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric128BitHexKeyUTF8, decryptSymmetric128BitHexKeyUTF8,
} from '../utils/crypto'; } from '../utils/crypto';
import { getEncryptionKey, client, getRootEncryptionKey } from '../config';
import { BotService, TelemetryService } from '../services'; import { BotService, TelemetryService } from '../services';
import { EESecretService, EELogService } from '../ee/services'; import { getEncryptionKey, client, getRootEncryptionKey } from "../config";
import { EESecretService, EELogService } from "../ee/services";
import { import {
getAuthDataPayloadIdObj, getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj, getAuthDataPayloadUserObj,
} from '../utils/auth'; } from "../utils/auth";
import { getFolderIdFromServiceToken } from "../services/FolderService";
/** /**
* Create secret blind index data containing encrypted blind index [salt] * Create secret blind index data containing encrypted blind index [salt]
@@ -51,12 +53,12 @@ import {
* @param {Types.ObjectId} obj.workspaceId * @param {Types.ObjectId} obj.workspaceId
*/ */
export const createSecretBlindIndexDataHelper = async ({ export const createSecretBlindIndexDataHelper = async ({
workspaceId workspaceId,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) => { }) => {
// initialize random blind index salt for workspace // initialize random blind index salt for workspace
const salt = crypto.randomBytes(16).toString('base64'); const salt = crypto.randomBytes(16).toString("base64");
const encryptionKey = await getEncryptionKey(); const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey();
@@ -104,7 +106,7 @@ export const createSecretBlindIndexDataHelper = async ({
* @returns * @returns
*/ */
export const getSecretBlindIndexSaltHelper = async ({ export const getSecretBlindIndexSaltHelper = async ({
workspaceId workspaceId,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) => { }) => {
@@ -113,7 +115,7 @@ export const getSecretBlindIndexSaltHelper = async ({
const secretBlindIndexData = await SecretBlindIndexData.findOne({ const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId, workspace: workspaceId,
}).select('+algorithm +keyEncoding'); }).select("+algorithm +keyEncoding");
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
@@ -141,7 +143,7 @@ export const getSecretBlindIndexSaltHelper = async ({
} }
throw InternalServerError({ throw InternalServerError({
message: 'Failed to obtain workspace salt needed for secret blind indexing', message: "Failed to obtain workspace salt needed for secret blind indexing",
}); });
}; };
@@ -154,7 +156,7 @@ export const getSecretBlindIndexSaltHelper = async ({
*/ */
export const generateSecretBlindIndexWithSaltHelper = async ({ export const generateSecretBlindIndexWithSaltHelper = async ({
secretName, secretName,
salt salt,
}: { }: {
secretName: string; secretName: string;
salt: string; salt: string;
@@ -163,14 +165,14 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
const secretBlindIndex = ( const secretBlindIndex = (
await argon2.hash(secretName, { await argon2.hash(secretName, {
type: argon2.argon2id, type: argon2.argon2id,
salt: Buffer.from(salt, 'base64'), salt: Buffer.from(salt, "base64"),
saltLength: 16, // default 16 bytes saltLength: 16, // default 16 bytes
memoryCost: 65536, // default pool of 64 MiB per thread. memoryCost: 65536, // default pool of 64 MiB per thread.
hashLength: 32, hashLength: 32,
parallelism: 1, parallelism: 1,
raw: true, raw: true,
}) })
).toString('base64'); ).toString("base64");
return secretBlindIndex; return secretBlindIndex;
}; };
@@ -184,7 +186,7 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
*/ */
export const generateSecretBlindIndexHelper = async ({ export const generateSecretBlindIndexHelper = async ({
secretName, secretName,
workspaceId workspaceId,
}: { }: {
secretName: string; secretName: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
@@ -195,7 +197,7 @@ export const generateSecretBlindIndexHelper = async ({
const secretBlindIndexData = await SecretBlindIndexData.findOne({ const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId, workspace: workspaceId,
}).select('+algorithm +keyEncoding'); }).select("+algorithm +keyEncoding");
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
@@ -238,7 +240,7 @@ export const generateSecretBlindIndexHelper = async ({
} }
throw InternalServerError({ throw InternalServerError({
message: 'Failed to generate secret blind index' message: "Failed to generate secret blind index",
}); });
}; };
@@ -286,7 +288,7 @@ export const createSecretHelper = async ({
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
folderId, secretPath = "/",
}: CreateSecretParams) => { }: CreateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
@@ -294,16 +296,30 @@ export const createSecretHelper = async ({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
}); });
// if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
const exists = await Secret.exists({ const exists = await Secret.exists({
secretBlindIndex, secretBlindIndex,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
folder: folderId,
type, type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
}); });
if (exists) if (exists)
throw BadRequestError({ throw BadRequestError({
message: 'Failed to create secret that already exists', message: "Failed to create secret that already exists",
}); });
if (type === SECRET_PERSONAL) { if (type === SECRET_PERSONAL) {
@@ -312,6 +328,7 @@ export const createSecretHelper = async ({
const exists = await Secret.exists({ const exists = await Secret.exists({
secretBlindIndex, secretBlindIndex,
folder: folderId,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
type: SECRET_SHARED, type: SECRET_SHARED,
}); });
@@ -319,7 +336,7 @@ export const createSecretHelper = async ({
if (!exists) if (!exists)
throw BadRequestError({ throw BadRequestError({
message: message:
'Failed to create personal secret override for no corresponding shared secret', "Failed to create personal secret override for no corresponding shared secret",
}); });
} }
@@ -396,6 +413,7 @@ export const createSecretHelper = async ({
version: secret.version, version: secret.version,
workspace: secret.workspace, workspace: secret.workspace,
type, type,
folder: folderId,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment, environment: secret.environment,
isDeleted: false, isDeleted: false,
@@ -443,7 +461,7 @@ export const createSecretHelper = async ({
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets added', event: "secrets added",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData,
}), }),
@@ -451,6 +469,7 @@ export const createSecretHelper = async ({
numberOfSecrets: 1, numberOfSecrets: 1,
environment, environment,
workspaceId, workspaceId,
folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent,
}, },
@@ -471,14 +490,28 @@ export const createSecretHelper = async ({
export const getSecretsHelper = async ({ export const getSecretsHelper = async ({
workspaceId, workspaceId,
environment, environment,
authData authData,
secretPath = "/",
}: GetSecretsParams) => { }: GetSecretsParams) => {
let secrets: ISecret[] = []; let secrets: ISecret[] = [];
// if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
// get personal secrets first // get personal secrets first
secrets = await Secret.find({ secrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId,
type: SECRET_PERSONAL, type: SECRET_PERSONAL,
...getAuthDataPayloadUserObj(authData), ...getAuthDataPayloadUserObj(authData),
}).lean(); }).lean();
@@ -488,6 +521,7 @@ export const getSecretsHelper = async ({
await Secret.find({ await Secret.find({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId,
type: SECRET_SHARED, type: SECRET_SHARED,
secretBlindIndex: { secretBlindIndex: {
$nin: secrets.map((secret) => secret.secretBlindIndex), $nin: secrets.map((secret) => secret.secretBlindIndex),
@@ -516,7 +550,7 @@ export const getSecretsHelper = async ({
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets pulled', event: "secrets pulled",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData,
}), }),
@@ -524,6 +558,7 @@ export const getSecretsHelper = async ({
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent,
}, },
@@ -586,18 +621,32 @@ export const getSecretHelper = async ({
environment, environment,
type, type,
authData, authData,
secretPath = "/",
}: GetSecretParams) => { }: GetSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
}); });
let secret: ISecret | null = null; let secret: ISecret | null = null;
// if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
// try getting personal secret first (if exists) // try getting personal secret first (if exists)
secret = await Secret.findOne({ secret = await Secret.findOne({
secretBlindIndex, secretBlindIndex,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId,
type: type ?? SECRET_PERSONAL, type: type ?? SECRET_PERSONAL,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
}).lean(); }).lean();
@@ -609,6 +658,7 @@ export const getSecretHelper = async ({
secretBlindIndex, secretBlindIndex,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId,
type: SECRET_SHARED, type: SECRET_SHARED,
}).lean(); }).lean();
} }
@@ -636,7 +686,7 @@ export const getSecretHelper = async ({
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets pull', event: "secrets pull",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData,
}), }),
@@ -644,6 +694,7 @@ export const getSecretHelper = async ({
numberOfSecrets: 1, numberOfSecrets: 1,
environment, environment,
workspaceId, workspaceId,
folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent,
}, },
@@ -704,6 +755,7 @@ export const updateSecretHelper = async ({
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretPath,
}: UpdateSecretParams) => { }: UpdateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
@@ -711,6 +763,18 @@ export const updateSecretHelper = async ({
}); });
let secret: ISecret | null = null; let secret: ISecret | null = null;
// if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
if (type === SECRET_SHARED) { if (type === SECRET_SHARED) {
// case: update shared secret // case: update shared secret
@@ -719,6 +783,7 @@ export const updateSecretHelper = async ({
secretBlindIndex, secretBlindIndex,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId,
type, type,
}, },
{ {
@@ -740,6 +805,7 @@ export const updateSecretHelper = async ({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
folder: folderId,
...getAuthDataPayloadUserObj(authData), ...getAuthDataPayloadUserObj(authData),
}, },
{ {
@@ -760,6 +826,7 @@ export const updateSecretHelper = async ({
secret: secret._id, secret: secret._id,
version: secret.version, version: secret.version,
workspace: secret.workspace, workspace: secret.workspace,
folder: folderId,
type, type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment, environment: secret.environment,
@@ -808,7 +875,7 @@ export const updateSecretHelper = async ({
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets modified', event: "secrets modified",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData,
}), }),
@@ -816,6 +883,7 @@ export const updateSecretHelper = async ({
numberOfSecrets: 1, numberOfSecrets: 1,
environment, environment,
workspaceId, workspaceId,
folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent,
}, },
@@ -841,12 +909,26 @@ export const deleteSecretHelper = async ({
environment, environment,
type, type,
authData, authData,
secretPath = "/",
}: DeleteSecretParams) => { }: DeleteSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
}); });
// if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
let secrets: ISecret[] = []; let secrets: ISecret[] = [];
let secret: ISecret | null = null; let secret: ISecret | null = null;
@@ -855,6 +937,7 @@ export const deleteSecretHelper = async ({
secretBlindIndex, secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId,
}); });
secret = await Secret.findOneAndDelete({ secret = await Secret.findOneAndDelete({
@@ -862,16 +945,19 @@ export const deleteSecretHelper = async ({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
folder: folderId,
}); });
await Secret.deleteMany({ await Secret.deleteMany({
secretBlindIndex, secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId,
}); });
} else { } else {
secret = await Secret.findOneAndDelete({ secret = await Secret.findOneAndDelete({
secretBlindIndex, secretBlindIndex,
folder: folderId,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
@@ -918,7 +1004,7 @@ export const deleteSecretHelper = async ({
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets deleted', event: "secrets deleted",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData,
}), }),
@@ -926,6 +1012,7 @@ export const deleteSecretHelper = async ({
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent,
}, },
@@ -5,7 +5,6 @@ export interface CreateSecretParams {
secretName: string; secretName: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment: string;
folderId?: string;
type: "shared" | "personal"; type: "shared" | "personal";
authData: AuthData; authData: AuthData;
secretKeyCiphertext?: string; secretKeyCiphertext?: string;
@@ -19,17 +18,20 @@ export interface CreateSecretParams {
secretCommentCiphertext?: string; secretCommentCiphertext?: string;
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
secretPath: string;
} }
export interface GetSecretsParams { export interface GetSecretsParams {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment: string;
secretPath: string;
authData: AuthData; authData: AuthData;
} }
export interface GetSecretParams { export interface GetSecretParams {
secretName: string; secretName: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
secretPath: string;
environment: string; environment: string;
type?: "shared" | "personal"; type?: "shared" | "personal";
authData: AuthData; authData: AuthData;
@@ -44,7 +46,7 @@ export interface UpdateSecretParams {
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
folderId?: string; secretPath: string;
} }
export interface DeleteSecretParams { export interface DeleteSecretParams {
@@ -53,4 +55,5 @@ export interface DeleteSecretParams {
environment: string; environment: string;
type: "shared" | "personal"; type: "shared" | "personal";
authData: AuthData; authData: AuthData;
secretPath: string;
} }
+28 -19
View File
@@ -1,4 +1,4 @@
import { Schema, model, Types, Document } from 'mongoose'; import { Schema, model, Types, Document } from "mongoose";
export interface IServiceTokenData extends Document { export interface IServiceTokenData extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
@@ -13,6 +13,7 @@ export interface IServiceTokenData extends Document {
encryptedKey: string; encryptedKey: string;
iv: string; iv: string;
tag: string; tag: string;
secretPath: string;
permissions: string[]; permissions: string[];
} }
@@ -20,60 +21,68 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
{ {
name: { name: {
type: String, type: String,
required: true required: true,
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Workspace', ref: "Workspace",
required: true required: true,
}, },
environment: { environment: {
type: String, type: String,
required: true required: true,
}, },
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'User', ref: "User",
required: true required: true,
}, },
serviceAccount: { serviceAccount: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'ServiceAccount' ref: "ServiceAccount",
}, },
lastUsed: { lastUsed: {
type: Date type: Date,
}, },
expiresAt: { expiresAt: {
type: Date type: Date,
}, },
secretHash: { secretHash: {
type: String, type: String,
required: true, required: true,
select: false select: false,
}, },
encryptedKey: { encryptedKey: {
type: String, type: String,
select: false select: false,
}, },
iv: { iv: {
type: String, type: String,
select: false select: false,
}, },
tag: { tag: {
type: String, type: String,
select: false select: false,
}, },
permissions: { permissions: {
type: [String], type: [String],
enum: ['read', 'write'], enum: ["read", "write"],
default: ['read'] default: ["read"],
} },
secretPath: {
type: String,
default: "/",
required: true,
},
}, },
{ {
timestamps: true timestamps: true,
} }
); );
const ServiceTokenData = model<IServiceTokenData>('ServiceTokenData', serviceTokenDataSchema); const ServiceTokenData = model<IServiceTokenData>(
"ServiceTokenData",
serviceTokenDataSchema
);
export default ServiceTokenData; export default ServiceTokenData;
-4
View File
@@ -37,10 +37,6 @@ const workspaceSchema = new Schema<IWorkspace>({
name: "Development", name: "Development",
slug: "dev" slug: "dev"
}, },
{
name: "Test",
slug: "test"
},
{ {
name: "Staging", name: "Staging",
slug: "staging" slug: "staging"
+50 -47
View File
@@ -1,15 +1,15 @@
import express from 'express'; import express from "express";
const router = express.Router(); const router = express.Router();
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
requireSecretsAuth, requireSecretsAuth,
validateRequest, validateRequest,
} from '../../middleware'; } from "../../middleware";
import { validateClientForSecrets } from '../../validation'; import { validateClientForSecrets } from "../../validation";
import { query, body } from 'express-validator'; import { query, body } from "express-validator";
import { secretsController } from '../../controllers/v2'; import { secretsController } from "../../controllers/v2";
import { import {
ADMIN, ADMIN,
MEMBER, MEMBER,
@@ -21,11 +21,11 @@ import {
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
} from '../../variables'; } from "../../variables";
import { BatchSecretRequest } from '../../types/secret'; import { BatchSecretRequest } from "../../types/secret";
router.post( router.post(
'/batch', "/batch",
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
AUTH_MODE_JWT, AUTH_MODE_JWT,
@@ -35,12 +35,13 @@ router.post(
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body', locationWorkspaceId: "body",
}), }),
body('workspaceId').exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body('folderId').default('root').isString().trim(), body("folderId").default("root").isString().trim(),
body('environment').exists().isString().trim(), body("environment").exists().isString().trim(),
body('requests') body("secretPath").optional().isString().trim(),
body("requests")
.exists() .exists()
.custom(async (requests: BatchSecretRequest[], { req }) => { .custom(async (requests: BatchSecretRequest[], { req }) => {
if (Array.isArray(requests)) { if (Array.isArray(requests)) {
@@ -65,17 +66,18 @@ router.post(
); );
router.post( router.post(
'/', "/",
body('workspaceId').exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body('environment').exists().isString().trim(), body("environment").exists().isString().trim(),
body('folderId').default('root').isString().trim(), body("folderId").default("root").isString().trim(),
body('secrets') body("secretPath").optional().isString().trim(),
body("secrets")
.exists() .exists()
.custom((value) => { .custom((value) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: create multiple secrets // case: create multiple secrets
if (value.length === 0) if (value.length === 0)
throw new Error('secrets cannot be an empty array'); throw new Error("secrets cannot be an empty array");
for (const secret of value) { for (const secret of value) {
if ( if (
!secret.type || !secret.type ||
@@ -85,16 +87,16 @@ router.post(
!secret.secretKeyCiphertext || !secret.secretKeyCiphertext ||
!secret.secretKeyIV || !secret.secretKeyIV ||
!secret.secretKeyTag || !secret.secretKeyTag ||
typeof secret.secretValueCiphertext !== 'string' || typeof secret.secretValueCiphertext !== "string" ||
!secret.secretValueIV || !secret.secretValueIV ||
!secret.secretValueTag !secret.secretValueTag
) { ) {
throw new Error( throw new Error(
'secrets array must contain objects that have required secret properties' "secrets array must contain objects that have required secret properties"
); );
} }
} }
} else if (typeof value === 'object') { } else if (typeof value === "object") {
// case: update 1 secret // case: update 1 secret
if ( if (
!value.type || !value.type ||
@@ -107,11 +109,11 @@ router.post(
!value.secretValueTag !value.secretValueTag
) { ) {
throw new Error( throw new Error(
'secrets object is missing required secret properties' "secrets object is missing required secret properties"
); );
} }
} else { } else {
throw new Error('secrets must be an object or an array of objects'); throw new Error("secrets must be an object or an array of objects");
} }
return true; return true;
@@ -126,19 +128,20 @@ router.post(
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body', locationWorkspaceId: "body",
locationEnvironment: 'body', locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
}), }),
secretsController.createSecrets secretsController.createSecrets
); );
router.get( router.get(
'/', "/",
query('workspaceId').exists().trim(), query("workspaceId").exists().trim(),
query('environment').exists().trim(), query("environment").exists().trim(),
query('tagSlugs'), query("tagSlugs"),
query('folderId').default('root').isString().trim(), query("folderId").default("root").isString().trim(),
query("secretPath").optional().isString().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
@@ -150,34 +153,34 @@ router.get(
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'query', locationWorkspaceId: "query",
locationEnvironment: 'query', locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
}), }),
secretsController.getSecrets secretsController.getSecrets
); );
router.patch( router.patch(
'/', "/",
body('secrets') body("secrets")
.exists() .exists()
.custom((value) => { .custom((value) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: update multiple secrets // case: update multiple secrets
if (value.length === 0) if (value.length === 0)
throw new Error('secrets cannot be an empty array'); throw new Error("secrets cannot be an empty array");
for (const secret of value) { for (const secret of value) {
if (!secret.id) { if (!secret.id) {
throw new Error('Each secret must contain a ID property'); throw new Error("Each secret must contain a ID property");
} }
} }
} else if (typeof value === 'object') { } else if (typeof value === "object") {
// case: update 1 secret // case: update 1 secret
if (!value.id) { if (!value.id) {
throw new Error('secret must contain a ID property'); throw new Error("secret must contain a ID property");
} }
} else { } else {
throw new Error('secrets must be an object or an array of objects'); throw new Error("secrets must be an object or an array of objects");
} }
return true; return true;
@@ -198,21 +201,21 @@ router.patch(
); );
router.delete( router.delete(
'/', "/",
body('secretIds') body("secretIds")
.exists() .exists()
.custom((value) => { .custom((value) => {
// case: delete 1 secret // case: delete 1 secret
if (typeof value === 'string') return true; if (typeof value === "string") return true;
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: delete multiple secrets // case: delete multiple secrets
if (value.length === 0) if (value.length === 0)
throw new Error('secrets cannot be an empty array'); throw new Error("secrets cannot be an empty array");
return value.every((id: string) => typeof id === 'string'); return value.every((id: string) => typeof id === "string");
} }
throw new Error('secretIds must be a string or an array of strings'); throw new Error("secretIds must be a string or an array of strings");
}) })
.not() .not()
.isEmpty(), .isEmpty(),
+37 -30
View File
@@ -1,70 +1,77 @@
import express from 'express'; import express from "express";
const router = express.Router(); const router = express.Router();
import { import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
requireServiceTokenDataAuth, requireServiceTokenDataAuth,
validateRequest validateRequest,
} from '../../middleware'; } from "../../middleware";
import { param, body } from 'express-validator'; import { param, body } from "express-validator";
import { import {
ADMIN, ADMIN,
MEMBER, MEMBER,
PERMISSION_WRITE_SECRETS, PERMISSION_WRITE_SECRETS,
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN AUTH_MODE_SERVICE_TOKEN,
} from '../../variables'; } from "../../variables";
import { serviceTokenDataController } from '../../controllers/v2'; import { serviceTokenDataController } from "../../controllers/v2";
router.get( router.get(
'/', "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN] acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN],
}), }),
serviceTokenDataController.getServiceTokenData serviceTokenDataController.getServiceTokenData
); );
router.post( router.post(
'/', "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT] acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body', locationWorkspaceId: "body",
locationEnvironment: 'body', locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS] requiredPermissions: [PERMISSION_WRITE_SECRETS],
}), }),
body('name').exists().isString().trim(), body("name").exists().isString().trim(),
body('workspaceId').exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body('environment').exists().isString().trim(), body("environment").exists().isString().trim(),
body('encryptedKey').exists().isString().trim(), body("encryptedKey").exists().isString().trim(),
body('iv').exists().isString().trim(), body("iv").exists().isString().trim(),
body('tag').exists().isString().trim(), body("secretPath").isString().default("/").trim(),
body('expiresIn').exists().isNumeric(), // measured in ms body("tag").exists().isString().trim(),
body('permissions').isArray({ min: 1 }).custom((value: string[]) => { body("expiresIn").exists().isNumeric(), // measured in ms
const allowedPermissions = ['read', 'write']; body("permissions")
const invalidValues = value.filter((v) => !allowedPermissions.includes(v)); .isArray({ min: 1 })
.custom((value: string[]) => {
const allowedPermissions = ["read", "write"];
const invalidValues = value.filter(
(v) => !allowedPermissions.includes(v)
);
if (invalidValues.length > 0) { if (invalidValues.length > 0) {
throw new Error(`permissions contains invalid values: ${invalidValues.join(', ')}`); throw new Error(
`permissions contains invalid values: ${invalidValues.join(", ")}`
);
} }
return true return true;
}), }),
validateRequest, validateRequest,
serviceTokenDataController.createServiceTokenData serviceTokenDataController.createServiceTokenData
); );
router.delete( router.delete(
'/:serviceTokenDataId', "/:serviceTokenDataId",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT] acceptedAuthModes: [AUTH_MODE_JWT],
}), }),
requireServiceTokenDataAuth({ requireServiceTokenDataAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
}), }),
param('serviceTokenDataId').exists().trim(), param("serviceTokenDataId").exists().trim(),
validateRequest, validateRequest,
serviceTokenDataController.deleteServiceTokenData serviceTokenDataController.deleteServiceTokenData
); );
+66 -63
View File
@@ -1,12 +1,12 @@
import express from 'express'; import express from "express";
const router = express.Router(); const router = express.Router();
import { import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest validateRequest,
} from '../../middleware'; } from "../../middleware";
import { body, param, query } from 'express-validator'; import { body, param, query } from "express-validator";
import { secretsController } from '../../controllers/v3'; import { secretsController } from "../../controllers/v3";
import { import {
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
@@ -17,26 +17,27 @@ import {
PERMISSION_WRITE_SECRETS, PERMISSION_WRITE_SECRETS,
SECRET_SHARED, SECRET_SHARED,
SECRET_PERSONAL, SECRET_PERSONAL,
PERMISSION_READ_SECRETS PERMISSION_READ_SECRETS,
} from '../../variables'; } from "../../variables";
router.get( router.get(
'/', "/",
query('workspaceId').exists().isString().trim(), query("workspaceId").exists().isString().trim(),
query('environment').exists().isString().trim(), query("environment").exists().isString().trim(),
query("secretPath").default("/").isString().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT AUTH_MODE_SERVICE_ACCOUNT,
] ],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'query', locationWorkspaceId: "query",
locationEnvironment: 'query', locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
}), }),
@@ -44,34 +45,33 @@ router.get(
); );
router.post( router.post(
'/:secretName', "/:secretName",
body('workspaceId').exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body('environment').exists().isString().trim(), body("environment").exists().isString().trim(),
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
body('secretKeyCiphertext').optional().isString().trim(), body("secretKeyCiphertext").exists().isString().trim(),
body('secretKeyIV').optional().isString().trim(), body("secretKeyIV").exists().isString().trim(),
body('secretKeyTag').optional().isString().trim(), body("secretKeyTag").exists().isString().trim(),
body('secretValue').optional().isString().trim(), body("secretValueCiphertext").exists().isString().trim(),
body('secretValueCiphertext').optional().isString().trim(), body("secretValueIV").exists().isString().trim(),
body('secretValueIV').optional().isString().trim(), body("secretValueTag").exists().isString().trim(),
body('secretValueTag').optional().isString().trim(), body("secretCommentCiphertext").optional().isString().trim(),
body('secretComment').optional().isString().trim(), body("secretCommentIV").optional().isString().trim(),
body('secretCommentCiphertext').optional().isString().trim(), body("secretCommentTag").optional().isString().trim(),
body('secretCommentIV').optional().isString().trim(), body("secretPath").default("/").isString().trim(),
body('secretCommentTag').optional().isString().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT AUTH_MODE_SERVICE_ACCOUNT,
] ],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body', locationWorkspaceId: "body",
locationEnvironment: 'body', locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
}), }),
@@ -79,24 +79,25 @@ router.post(
); );
router.get( router.get(
'/:secretName', "/:secretName",
param('secretName').exists().isString().trim(), param("secretName").exists().isString().trim(),
query('workspaceId').exists().isString().trim(), query("workspaceId").exists().isString().trim(),
query('environment').exists().isString().trim(), query("environment").exists().isString().trim(),
query('type').optional().isIn([SECRET_SHARED, SECRET_PERSONAL]), query("secretPath").default("/").isString().trim(),
query("type").optional().isIn([SECRET_SHARED, SECRET_PERSONAL]),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT AUTH_MODE_SERVICE_ACCOUNT,
] ],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'query', locationWorkspaceId: "query",
locationEnvironment: 'query', locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
}), }),
@@ -104,27 +105,28 @@ router.get(
); );
router.patch( router.patch(
'/:secretName', "/:secretName",
param('secretName').exists().isString().trim(), param("secretName").exists().isString().trim(),
body('workspaceId').exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body('environment').exists().isString().trim(), body("environment").exists().isString().trim(),
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
body('secretValueCiphertext').exists().isString().trim(), body("secretValueCiphertext").exists().isString().trim(),
body('secretValueIV').exists().isString().trim(), body("secretValueIV").exists().isString().trim(),
body('secretValueTag').exists().isString().trim(), body("secretValueTag").exists().isString().trim(),
body("secretPath").default("/").isString().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT AUTH_MODE_SERVICE_ACCOUNT,
] ],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body', locationWorkspaceId: "body",
locationEnvironment: 'body', locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
}), }),
@@ -132,24 +134,25 @@ router.patch(
); );
router.delete( router.delete(
'/:secretName', "/:secretName",
param('secretName').exists().isString().trim(), param("secretName").exists().isString().trim(),
body('workspaceId').exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body('environment').exists().isString().trim(), body("environment").exists().isString().trim(),
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), body("secretPath").default("/").isString().trim(),
body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT AUTH_MODE_SERVICE_ACCOUNT,
] ],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body', locationWorkspaceId: "body",
locationEnvironment: 'body', locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
}), }),
+29 -1
View File
@@ -1,5 +1,6 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { TFolderSchema } from "../models/folder"; import { Types } from "mongoose";
import Folder, { TFolderSchema } from "../models/folder";
type TAppendFolderDTO = { type TAppendFolderDTO = {
folderName: string; folderName: string;
@@ -174,6 +175,11 @@ export const searchByFolderIdWithDir = (
// to get folder of a path given // to get folder of a path given
// Like /frontend/folder#1 // Like /frontend/folder#1
export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => { export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => {
// corner case when its just / return root
if (searchPath === "/") {
return folders.id === "root" ? folders : undefined;
}
const path = searchPath.split("/").filter(Boolean); const path = searchPath.split("/").filter(Boolean);
const queue = [folders]; const queue = [folders];
let segment: TFolderSchema | undefined; let segment: TFolderSchema | undefined;
@@ -187,3 +193,25 @@ export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => {
} }
return segment; return segment;
}; };
export const getFolderIdFromServiceToken = async (
workspaceId: Types.ObjectId | string,
environment: string,
secretPath: string
) => {
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
});
if (!folders) {
if (secretPath !== "/") throw new Error("Invalid path. Folders not found");
} else {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw new Error("Folder not found");
}
return folder.id;
}
return "root";
};
+8 -8
View File
@@ -1,4 +1,4 @@
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import {
CreateSecretParams, CreateSecretParams,
GetSecretsParams, GetSecretsParams,
@@ -19,7 +19,6 @@ import {
} from '../helpers/secrets'; } from '../helpers/secrets';
class SecretService { class SecretService {
/** /**
* Create secret blind index data containing encrypted blind index salt * Create secret blind index data containing encrypted blind index salt
* for workspace with id [workspaceId] * for workspace with id [workspaceId]
@@ -33,7 +32,7 @@ class SecretService {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) { }) {
return await createSecretBlindIndexDataHelper({ return await createSecretBlindIndexDataHelper({
workspaceId workspaceId,
}); });
} }
@@ -44,12 +43,12 @@ class SecretService {
* @returns * @returns
*/ */
static async getSecretBlindIndexSalt({ static async getSecretBlindIndexSalt({
workspaceId workspaceId,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) { }) {
return await getSecretBlindIndexSaltHelper({ return await getSecretBlindIndexSaltHelper({
workspaceId workspaceId,
}); });
} }
@@ -62,14 +61,14 @@ class SecretService {
*/ */
static async generateSecretBlindIndexWithSalt({ static async generateSecretBlindIndexWithSalt({
secretName, secretName,
salt salt,
}: { }: {
secretName: string; secretName: string;
salt: string; salt: string;
}) { }) {
return await generateSecretBlindIndexWithSaltHelper({ return await generateSecretBlindIndexWithSaltHelper({
secretName, secretName,
salt salt,
}); });
} }
@@ -89,7 +88,7 @@ class SecretService {
}) { }) {
return await generateSecretBlindIndexHelper({ return await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId workspaceId,
}); });
} }
@@ -130,6 +129,7 @@ class SecretService {
* @returns * @returns
*/ */
static async getSecret(getSecretParams: GetSecretParams) { static async getSecret(getSecretParams: GetSecretParams) {
// TODO(akhilmhdh) The one above is diff. Change this to some other name
return await getSecretHelper(getSecretParams); return await getSecretHelper(getSecretParams);
} }
+24 -4
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import crypto from "crypto"; import crypto from "crypto";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto"; import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto";
@@ -11,6 +12,7 @@ import {
Bot, Bot,
BackupPrivateKey, BackupPrivateKey,
IntegrationAuth, IntegrationAuth,
ServiceTokenData,
} from "../../models"; } from "../../models";
import { generateKeyPair } from "../../utils/crypto"; import { generateKeyPair } from "../../utils/crypto";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
@@ -64,7 +66,7 @@ export const backfillSecretVersions = async () => {
), ),
}); });
} }
console.log("Migration: Secret version migration v1 complete") console.log("Migration: Secret version migration v1 complete");
}; };
/** /**
@@ -380,13 +382,15 @@ export const backfillSecretFolders = async () => {
}); });
const newSnapshots = Object.keys(groupSnapByEnv).map((snapEnv) => { const newSnapshots = Object.keys(groupSnapByEnv).map((snapEnv) => {
const secretIdsOfEnvGroup = groupSnapByEnv[snapEnv] ? groupSnapByEnv[snapEnv].map(secretVersion => secretVersion._id) : [] const secretIdsOfEnvGroup = groupSnapByEnv[snapEnv]
? groupSnapByEnv[snapEnv].map((secretVersion) => secretVersion._id)
: [];
return { return {
...secSnapshot.toObject({ virtuals: false }), ...secSnapshot.toObject({ virtuals: false }),
_id: new Types.ObjectId(), _id: new Types.ObjectId(),
environment: snapEnv, environment: snapEnv,
secretVersions: secretIdsOfEnvGroup, secretVersions: secretIdsOfEnvGroup,
} };
}); });
await SecretSnapshot.insertMany(newSnapshots); await SecretSnapshot.insertMany(newSnapshots);
@@ -402,5 +406,21 @@ export const backfillSecretFolders = async () => {
.limit(50); .limit(50);
} }
console.log("Migration: Folder migration v1 complete") console.log("Migration: Folder migration v1 complete");
};
export const backfillServiceToken = async () => {
await ServiceTokenData.updateMany(
{
secretPath: {
$exists: false,
},
},
{
$set: {
secretPath: "/",
},
}
);
console.log("Migration: Service token migration v1 complete");
}; };
+14 -12
View File
@@ -1,30 +1,31 @@
import * as Sentry from '@sentry/node'; import * as Sentry from "@sentry/node";
import { DatabaseService, TelemetryService } from '../../services'; import { DatabaseService, TelemetryService } from "../../services";
import { setTransporter } from '../../helpers/nodemailer'; import { setTransporter } from "../../helpers/nodemailer";
import { EELicenseService } from '../../ee/services'; import { EELicenseService } from "../../ee/services";
import { initSmtp } from '../../services/smtp'; import { initSmtp } from "../../services/smtp";
import { createTestUserForDevelopment } from '../addDevelopmentUser'; import { createTestUserForDevelopment } from "../addDevelopmentUser";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
import { validateEncryptionKeysConfig } from './validateConfig'; import { validateEncryptionKeysConfig } from "./validateConfig";
import { import {
backfillSecretVersions, backfillSecretVersions,
backfillBots, backfillBots,
backfillSecretBlindIndexData, backfillSecretBlindIndexData,
backfillEncryptionMetadata, backfillEncryptionMetadata,
backfillSecretFolders, backfillSecretFolders,
} from './backfillData'; backfillServiceToken,
} from "./backfillData";
import { import {
reencryptBotPrivateKeys, reencryptBotPrivateKeys,
reencryptSecretBlindIndexDataSalts, reencryptSecretBlindIndexDataSalts,
} from './reencryptData'; } from "./reencryptData";
import { import {
getNodeEnv, getNodeEnv,
getMongoURL, getMongoURL,
getSentryDSN, getSentryDSN,
getClientSecretGoogle, getClientSecretGoogle,
getClientIdGoogle, getClientIdGoogle,
} from '../../config'; } from "../../config";
import { initializePassport } from '../auth'; import { initializePassport } from "../auth";
/** /**
* Prepare Infisical upon startup. This includes tasks like: * Prepare Infisical upon startup. This includes tasks like:
@@ -75,6 +76,7 @@ export const setup = async () => {
await backfillSecretBlindIndexData(); await backfillSecretBlindIndexData();
await backfillEncryptionMetadata(); await backfillEncryptionMetadata();
await backfillSecretFolders(); await backfillSecretFolders();
await backfillServiceToken();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY
@@ -85,7 +87,7 @@ export const setup = async () => {
Sentry.init({ Sentry.init({
dsn: await getSentryDSN(), dsn: await getSentryDSN(),
tracesSampleRate: 1.0, tracesSampleRate: 1.0,
debug: (await getNodeEnv()) === 'production' ? false : true, debug: (await getNodeEnv()) === "production" ? false : true,
environment: await getNodeEnv(), environment: await getNodeEnv(),
}); });
@@ -53,7 +53,7 @@ const AddProjectMemberDialog = ({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="w-full max-w-md transform rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all"> <Dialog.Panel className="w-full max-w-md transform rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-left align-middle shadow-xl transition-all">
{data?.length > 0 ? ( {data?.length > 0 ? (
<Dialog.Title <Dialog.Title
as="h3" as="h3"
@@ -64,7 +64,7 @@ const AddProjectMemberDialog = ({
) : ( ) : (
<Dialog.Title <Dialog.Title
as="h3" as="h3"
className="z-50 text-lg font-medium leading-6 text-gray-400" className="z-50 text-lg font-medium text-mineshaft-300 mb-4"
> >
{t('section.members.add-dialog.already-all-invited')} {t('section.members.add-dialog.already-all-invited')}
</Dialog.Title> </Dialog.Title>
@@ -25,6 +25,7 @@ type Props = {
changeData: (users: any[]) => void; changeData: (users: any[]) => void;
myUser: string; myUser: string;
filter: string; filter: string;
isUserListLoading: boolean;
}; };
type EnvironmentProps = { type EnvironmentProps = {
@@ -38,7 +39,7 @@ type EnvironmentProps = {
* @param {*} props * @param {*} props
* @returns * @returns
*/ */
const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => { const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoading }: Props) => {
const [roleSelected, setRoleSelected] = useState( const [roleSelected, setRoleSelected] = useState(
Array(userData?.length).fill(userData.map((user) => user.role)) Array(userData?.length).fill(userData.map((user) => user.role))
); );
@@ -205,7 +206,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
}; };
return ( return (
<div className="table-container relative mb-6 mt-1 min-w-max rounded-md border border-mineshaft-700 bg-bunker"> <div className="table-container relative mb-6 mt-1 min-w-max rounded-md border border-mineshaft-600 bg-bunker">
<div className="absolute h-[3.1rem] w-full rounded-t-md bg-white/5" /> <div className="absolute h-[3.1rem] w-full rounded-t-md bg-white/5" />
<UpgradePlanModal <UpgradePlanModal
isOpen={isUpgradeModalOpen} isOpen={isUpgradeModalOpen}
@@ -213,7 +214,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
text="You can change user permissions if you switch to Infisical's Professional plan." text="You can change user permissions if you switch to Infisical's Professional plan."
/> />
<table className="my-0.5 w-full"> <table className="my-0.5 w-full">
<thead className="text-xs font-light text-gray-400"> <thead className="text-xs font-light text-gray-400 bg-mineshaft-800">
<tr> <tr>
<th className="py-3.5 pl-4 text-left">NAME</th> <th className="py-3.5 pl-4 text-left">NAME</th>
<th className="py-3.5 pl-4 text-left">EMAIL</th> <th className="py-3.5 pl-4 text-left">EMAIL</th>
@@ -231,7 +232,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{userData?.filter( {!isUserListLoading && userData?.filter(
(user) => (user) =>
user.firstName?.toLowerCase().includes(filter) || user.firstName?.toLowerCase().includes(filter) ||
user.lastName?.toLowerCase().includes(filter) || user.lastName?.toLowerCase().includes(filter) ||
@@ -245,14 +246,14 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
user.email?.toLowerCase().includes(filter) user.email?.toLowerCase().includes(filter)
) )
.map((row, index) => ( .map((row, index) => (
<tr key={guidGenerator()} className="bg-bunker-600 text-sm hover:bg-bunker-500"> <tr key={guidGenerator()} className="bg-mineshaft-800 text-sm">
<td className="border-t border-mineshaft-700 py-2 pl-4 text-gray-300"> <td className="border-t border-mineshaft-600 py-2 pl-4 text-gray-300">
{row.firstName} {row.lastName} {row.firstName} {row.lastName}
</td> </td>
<td className="border-t border-mineshaft-700 py-2 pl-4 text-gray-300"> <td className="border-t border-mineshaft-600 py-2 pl-4 text-gray-300">
{row.email} {row.email}
</td> </td>
<td className="border-t border-mineshaft-700 py-2 pl-6 pr-10 text-gray-300"> <td className="border-t border-mineshaft-600 py-2 pl-6 pr-10 text-gray-300">
<div className="flex h-full flex-row items-center justify-start"> <div className="flex h-full flex-row items-center justify-start">
<Select <Select
className="w-36 bg-mineshaft-700" className="w-36 bg-mineshaft-700"
@@ -391,6 +392,10 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
</td> </td>
</tr> </tr>
))} ))}
{isUserListLoading && <>
<tr key={guidGenerator()} className="bg-mineshaft-800 text-sm animate-pulse h-14 w-full"/>
<tr key={guidGenerator()} className="bg-mineshaft-800 text-sm animate-pulse h-14 w-full"/>
</>}
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -121,7 +121,7 @@ export default function NavHeader({
<span className="text-sm font-semibold text-bunker-300">{name}</span> <span className="text-sm font-semibold text-bunker-300">{name}</span>
) : ( ) : (
<Link passHref legacyBehavior href={{ pathname: '/dashboard/[id]', query }}> <Link passHref legacyBehavior href={{ pathname: '/dashboard/[id]', query }}>
<a className="text-sm font-semibold capitalize text-primary/80 hover:text-primary"> <a className="text-sm font-semibold text-primary/80 hover:text-primary">
{name === 'root' ? selectedEnv?.name : name} {name === 'root' ? selectedEnv?.name : name}
</a> </a>
</Link> </Link>
@@ -21,7 +21,7 @@ export const EmptyState = ({
}: Props) => ( }: Props) => (
<div <div
className={twMerge( className={twMerge(
'flex w-full flex-col items-center bg-bunker-700 px-2 pt-6 text-bunker-300', 'flex w-full flex-col items-center bg-mineshaft-800 px-2 pt-6 text-bunker-300',
className className
)} )}
> >
+6 -6
View File
@@ -16,8 +16,8 @@ export const TableContainer = ({
}: TableContainerProps): JSX.Element => ( }: TableContainerProps): JSX.Element => (
<div <div
className={twMerge( className={twMerge(
'relative w-full overflow-x-auto border border-solid border-mineshaft-700 bg-mineshaft-800 font-inter shadow-md', 'relative w-full overflow-x-auto border border-solid border-mineshaft-700 bg-mineshaft-800 font-inter',
isRounded && 'rounded-md', isRounded && 'rounded-lg',
className className
)} )}
> >
@@ -34,7 +34,7 @@ export type TableProps = {
export const Table = ({ children, className }: TableProps): JSX.Element => ( export const Table = ({ children, className }: TableProps): JSX.Element => (
<table <table
className={twMerge( className={twMerge(
'w-full rounded-md bg-bunker-800 p-2 text-left text-sm text-gray-300', 'w-full bg-mineshaft-800 p-2 text-left text-sm text-gray-300',
className className
)} )}
> >
@@ -49,7 +49,7 @@ export type THeadProps = {
}; };
export const THead = ({ children, className }: THeadProps): JSX.Element => ( export const THead = ({ children, className }: THeadProps): JSX.Element => (
<thead className={twMerge('bg-bunker text-xs uppercase text-bunker-300', className)}> <thead className={twMerge('bg-mineshaft-800 text-xs uppercase text-bunker-300', className)}>
{children} {children}
</thead> </thead>
); );
@@ -62,7 +62,7 @@ export type TrProps = {
export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => ( export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => (
<tr <tr
className={twMerge('border border-solid border-mineshaft-700 hover:bg-bunker-700', className)} className={twMerge('border border-solid border-mineshaft-700 cursor-default', className)}
{...props} {...props}
> >
{children} {children}
@@ -76,7 +76,7 @@ export type ThProps = {
}; };
export const Th = ({ children, className }: ThProps): JSX.Element => ( export const Th = ({ children, className }: ThProps): JSX.Element => (
<th className={twMerge('bg-bunker-500 px-5 pt-4 pb-3.5 font-semibold', className)}>{children}</th> <th className={twMerge('bg-mineshaft-800 px-5 pt-4 pb-3.5 font-semibold border-b-2 border-mineshaft-600', className)}>{children}</th>
); );
// table body // table body
@@ -18,7 +18,7 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
href={`/settings/billing/${localStorage.getItem('projectData.id') as string}`} href={`/settings/billing/${localStorage.getItem('projectData.id') as string}`}
key="upgrade-plan" key="upgrade-plan"
> >
<Button className="mr-4 ml-2">Upgrade Plan</Button> <Button className="mr-4 ml-2 mb-2">Upgrade Plan</Button>
</Link>, </Link>,
<ModalClose asChild key="upgrade-plan-cancel"> <ModalClose asChild key="upgrade-plan-cancel">
<Button colorSchema="secondary" variant="plain"> <Button colorSchema="secondary" variant="plain">
@@ -1,14 +1,13 @@
import { createContext, ReactNode, useContext, useMemo } from 'react'; import { createContext, ReactNode, useContext, useMemo } from 'react';
import { useGetOrgSubscription } from '@app/hooks/api'; import { useGetOrgSubscription } from '@app/hooks/api';
import { GetSubscriptionPlan } from '@app/hooks/api/types'; import { SubscriptionPlan } from '@app/hooks/api/types';
import { useWorkspace } from '../WorkspaceContext'; import { useWorkspace } from '../WorkspaceContext';
// import { Subscription } from '@app/hooks/api/workspace/types'; // import { Subscription } from '@app/hooks/api/workspace/types';
type TSubscriptionContext = { type TSubscriptionContext = {
subscription?: GetSubscriptionPlan; subscription?: SubscriptionPlan;
subscriptionPlan: string;
isLoading: boolean; isLoading: boolean;
}; };
@@ -28,7 +27,6 @@ export const SubscriptionProvider = ({ children }: Props): JSX.Element => {
const value = useMemo<TSubscriptionContext>( const value = useMemo<TSubscriptionContext>(
() => ({ () => ({
subscription: data, subscription: data,
subscriptionPlan: data?.data?.[0]?.plan?.product || '',
isLoading isLoading
}), }),
[data, isLoading] [data, isLoading]
+70 -64
View File
@@ -1,7 +1,6 @@
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Image from 'next/image';
import { faAngleDown, faAngleRight, faUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'; import { faAngleDown, faAngleRight, faUpRightFromSquare } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -48,7 +47,7 @@ const ActivityLogsRow = ({
const { t } = useTranslation(); const { t } = useTranslation();
const renderUser = () => { const renderUser = () => {
if (row?.user) return `User: ${row.user}`; if (row?.user) return `${row.user}`;
if (row?.serviceAccount) return `Service Account: ${row.serviceAccount.name}`; if (row?.serviceAccount) return `Service Account: ${row.serviceAccount.name}`;
if (row?.serviceTokenData.name) return `Service Token: ${row.serviceTokenData.name}`; if (row?.serviceTokenData.name) return `Service Token: ${row.serviceTokenData.name}`;
@@ -56,54 +55,57 @@ const ActivityLogsRow = ({
}; };
return ( return (
<> <>
<tr key={guidGenerator()} className="w-full bg-bunker-800 text-sm duration-100"> <div key={guidGenerator()} className="w-full bg-mineshaft-800 text-sm text-mineshaft-200 duration-100 flex flex-row items-center">
<td <button
onKeyDown={() => null} type="button"
onClick={() => setPayloadOpened(!payloadOpened)} onClick={() => setPayloadOpened(!payloadOpened)}
className="flex cursor-pointer items-center border-t border-mineshaft-700 text-gray-300" className="border-t border-mineshaft-700 pt-[0.58rem]"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={payloadOpened ? faAngleDown : faAngleRight} icon={payloadOpened ? faAngleDown : faAngleRight}
className={`mt-2.5 ml-6 text-bunker-100 hover:bg-mineshaft-700 ${ className={`ml-6 mb-2 text-mineshaft-300 cursor-pointer ${
payloadOpened && 'bg-mineshaft-500' payloadOpened ? 'bg-mineshaft-500 hover:bg-mineshaft-500' : 'hover:bg-mineshaft-700'
} h-4 w-4 rounded-md p-1 duration-100`} } h-4 w-4 rounded-md p-1 duration-100`}
/> />
</td> </button>
<td className="border-t border-mineshaft-700 py-3 text-gray-300"> <div className="border-t border-mineshaft-700 py-3 w-1/4 pl-6">
{row.payload {row.payload
?.map( ?.map(
(action) => (action) =>
`${String(action.secretVersions.length)} ${t(`activity.event.${action.name}`)}` `${String(action.secretVersions.length)} ${t(`activity.event.${action.name}`)}`
) )
.join(' and ')} .join(' and ')}
</td> </div>
<td className="border-t border-mineshaft-700 py-3 pl-6 text-gray-300">{renderUser()}</td> <div className="border-t border-mineshaft-700 py-3 pl-6 w-1/4">{renderUser()}</div>
<td className="border-t border-mineshaft-700 py-3 pl-6 text-gray-300">{row.channel}</td> <div className="border-t border-mineshaft-700 py-3 pl-6 w-1/4">{row.channel}</div>
<td className="border-t border-mineshaft-700 py-3 pl-6 text-gray-300"> <div className="border-t border-mineshaft-700 py-3 pl-6 w-1/4">
{timeSince(new Date(row.createdAt))} {timeSince(new Date(row.createdAt))}
</td> </div>
</tr> </div>
{payloadOpened && ( {payloadOpened && (
<tr className="h-9 border-t border-mineshaft-700 text-sm text-bunker-200"> <div className="h-9 border-t border-mineshaft-700 text-sm text-bunker-200 bg-mineshaft-900/50 w-full flex flex-row items-center">
<td /> <div className='max-w-xl w-full flex flex-row items-center'>
<td>{String(t('common.timestamp'))}</td> <div className='w-24' />
<td>{row.createdAt}</td> <div className='w-1/2'>{String(t('common.timestamp'))}</div>
</tr> <div className='w-1/2'>{row.createdAt}</div>
</div>
</div>
)} )}
{payloadOpened && {payloadOpened &&
row.payload?.map( row.payload?.map(
(action) => (action) =>
action.secretVersions.length > 0 && ( action.secretVersions.length > 0 && (
<tr <div
key={action._id} key={action.name}
className="h-9 border-t border-mineshaft-700 text-sm text-bunker-200" className="h-9 border-t border-mineshaft-700 text-sm text-bunker-200 bg-mineshaft-900/50 w-full flex flex-row items-center"
> >
<td /> <div className='max-w-xl w-full flex flex-row items-center'>
<td className="">{t(`activity.event.${action.name}`)}</td> <div className='w-24' />
<td <div className='w-1/2'>{t(`activity.event.${action.name}`)}</div>
onKeyDown={() => null} <button
className="cursor-pointer text-primary-300 duration-200 hover:text-primary" type="button"
onClick={() => toggleSidebar(action._id)} onClick={() => toggleSidebar(action._id)}
className='w-1/2 text-primary-300 hover:text-primary-500 flex flex-row justify-left items-center duration-100'
> >
{action.secretVersions.length + {action.secretVersions.length +
(action.secretVersions.length !== 1 ? ' secrets' : ' secret')} (action.secretVersions.length !== 1 ? ' secrets' : ' secret')}
@@ -111,16 +113,19 @@ const ActivityLogsRow = ({
icon={faUpRightFromSquare} icon={faUpRightFromSquare}
className="ml-2 mb-0.5 h-3 w-3 font-light" className="ml-2 mb-0.5 h-3 w-3 font-light"
/> />
</td> </button>
</tr> </div>
</div>
) )
)} )}
{payloadOpened && ( {payloadOpened && (
<tr className="h-9 border-t border-mineshaft-700 text-sm text-bunker-200"> <div className="h-9 border-t border-mineshaft-700 text-sm text-bunker-200 bg-mineshaft-900/50 w-full flex flex-row items-center">
<td /> <div className='max-w-xl w-full flex flex-row items-center'>
<td>{String(t('activity.ip-address'))}</td> <div className='w-24' />
<td>{row.ipAddress}</td> <div className='w-1/2'>{String(t('activity.ip-address'))}</div>
</tr> <div className='w-1/2'>{row.ipAddress}</div>
</div>
</div>
)} )}
</> </>
); );
@@ -147,28 +152,37 @@ const ActivityTable = ({
return ( return (
<div className="mt-8 w-full px-6"> <div className="mt-8 w-full px-6">
<div className="table-container relative mb-6 w-full rounded-md border border-mineshaft-700 bg-bunker"> <div className="table-container relative mb-6 w-full rounded-md border border-mineshaft-700 bg-mineshaft-800">
<div className="absolute h-[3rem] w-full rounded-t-md bg-white/5" /> {/* <div className="absolute h-[3rem] w-full rounded-t-md bg-white/5" /> */}
<table className="my-1 w-full"> <div className="my-1 w-full">
<thead className="text-bunker-300"> <div className="text-bunker-300 border-b border-mineshaft-600">
<tr className="text-sm"> <div className="text-sm flex flex-row w-full">
<th aria-label="actions" className="pl-6 pt-2.5 pb-3 text-left" /> <button
<th className="pt-2.5 pb-3 text-left font-semibold"> type="button"
onClick={() => {}}
className="opacity-0"
>
<FontAwesomeIcon
icon={faAngleRight}
className="ml-6 mb-2 text-bunker-100 hover:bg-mineshaft-700 cursor-pointer h-4 w-4 rounded-md p-1 duration-100"
/>
</button>
<div className="flex flex-row justify-between w-full">
<div className="pt-2.5 pb-3 text-left font-semibold w-1/4 pl-6">
{String(t('common.event')).toUpperCase()} {String(t('common.event')).toUpperCase()}
</th> </div>
<th className="pl-6 pt-2.5 pb-3 text-left font-semibold"> <div className="pl-6 pt-2.5 pb-3 text-left font-semibold w-1/4 pl-6">
{String(t('common.user')).toUpperCase()} {String(t('common.user')).toUpperCase()}
</th> </div>
<th className="pl-6 pt-2.5 pb-3 text-left font-semibold"> <div className="pl-6 pt-2.5 pb-3 text-left font-semibold w-1/4 pl-6">
{String(t('common.source')).toUpperCase()} {String(t('common.source')).toUpperCase()}
</th> </div>
<th className="pl-6 pt-2.5 pb-3 text-left font-semibold"> <div className="pl-6 pt-2.5 pb-3 text-left font-semibold w-1/4 pl-6">
{String(t('common.time')).toUpperCase()} {String(t('common.time')).toUpperCase()}
</th> </div>
<th aria-label="action" /> </div>
</tr> </div>
</thead> </div>
<tbody>
{data?.map((row, index) => ( {data?.map((row, index) => (
<ActivityLogsRow <ActivityLogsRow
key={`activity.${index + 1}.${row._id}`} key={`activity.${index + 1}.${row._id}`}
@@ -176,18 +190,10 @@ const ActivityTable = ({
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
/> />
))} ))}
</tbody> </div>
</table>
</div> </div>
{isLoading && ( {isLoading && (
<div className="mb-8 mt-4 flex w-full justify-center"> <div className="mb-8 mt-4 bg-mineshaft-800 rounded-md h-60 flex w-full justify-center animate-pulse" />
<Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="loading animation"
/>
</div>
)} )}
</div> </div>
); );
@@ -3,6 +3,7 @@ export type ServiceToken = {
name: string; name: string;
workspace: string; workspace: string;
environment: string; environment: string;
secretPath: string;
user: string; user: string;
expiresAt: string; expiresAt: string;
createdAt: string; createdAt: string;
@@ -15,6 +16,7 @@ export type CreateServiceTokenDTO = {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
expiresIn: number; expiresIn: number;
secretPath: string;
encryptedKey: string; encryptedKey: string;
iv: string; iv: string;
tag: string; tag: string;
@@ -2,20 +2,20 @@ import { useQuery } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request'; import { apiRequest } from '@app/config/request';
import { GetSubscriptionPlan } from './types'; import { SubscriptionPlan } from './types';
// import { Workspace } from './types'; // import { Workspace } from './types';
const subscriptionKeys = { const subscriptionKeys = {
getOrgSubsription: (orgID: string) => ['subscription', { orgID }] as const getOrgSubsription: (orgID: string) => ['plan', { orgID }] as const
}; };
const fetchOrgSubscription = async (orgID: string) => { const fetchOrgSubscription = async (orgID: string) => {
const { data } = await apiRequest.get<{ subscriptions: GetSubscriptionPlan }>( const { data } = await apiRequest.get<{ plan: SubscriptionPlan }>(
`/api/v1/organization/${orgID}/subscriptions` `/api/v1/organizations/${orgID}/plan`
); );
return data.subscriptions; return data.plan;
}; };
type UseGetOrgSubscriptionProps = { type UseGetOrgSubscriptionProps = {
+14 -23
View File
@@ -1,25 +1,16 @@
export type GetSubscriptionPlan = {
data: { plan: SubscriptionPlan }[];
};
export type SubscriptionPlan = { export type SubscriptionPlan = {
id: string; _id: string;
object: string; membersUsed: number;
active: boolean; membersLimit: number;
aggregate_usage: unknown; auditLogs: boolean;
amount: 1400; customAlerts: boolean;
amount_decimal: 1400; customRateLimits: boolean;
billing_scheme: string; pitRecovery: boolean;
created: 1674833546; rbac: boolean;
currency: string; secretVersioning: boolean;
interval: string; slug: string;
interval_count: 1; tier: number;
livemode: false; workspaceLimit: number;
metadata: {}; workspacesUsed: number;
nickname: null; envLimit: number;
product: string;
tiers_mode: unknown;
transform_usage: unknown;
trial_period_days: unknown;
usage_type: string;
}; };
+1 -1
View File
@@ -3,7 +3,7 @@ export type { IncidentContact } from './incidentContacts/types';
export type { UserWsKeyPair } from './keys/types'; export type { UserWsKeyPair } from './keys/types';
export type { Organization } from './organization/types'; export type { Organization } from './organization/types';
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types'; export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types'; export type { SubscriptionPlan } from './subscriptions/types';
export type { WsTag } from './tags/types'; export type { WsTag } from './tags/types';
export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './users/types'; export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './users/types';
export type { export type {
+4 -5
View File
@@ -31,7 +31,6 @@ import {
SelectItem, SelectItem,
UpgradePlanModal UpgradePlanModal
} from '@app/components/v2'; } from '@app/components/v2';
import { plans } from '@app/const';
import { useOrganization, useSubscription, useUser, useWorkspace } from '@app/context'; import { useOrganization, useSubscription, useUser, useWorkspace } from '@app/context';
import { usePopUp } from '@app/hooks'; import { usePopUp } from '@app/hooks';
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from '@app/hooks/api'; import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from '@app/hooks/api';
@@ -59,10 +58,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { workspaces, currentWorkspace } = useWorkspace(); const { workspaces, currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { user } = useUser(); const { user } = useUser();
const { subscriptionPlan } = useSubscription(); const { subscription } = useSubscription();
const host = window.location.origin; const host = window.location.origin;
const isAddingProjectsAllowed = const isAddingProjectsAllowed = ((subscription?.workspacesUsed || 1) < (subscription?.workspaceLimit || 3)) || host !== 'https://app.infisical.com';
subscriptionPlan !== plans.starter || (subscriptionPlan === plans.starter && workspaces.length < 3) || host !== 'https://app.infisical.com';
const createWs = useCreateWorkspace(); const createWs = useCreateWorkspace();
const uploadWsKey = useUploadWsKey(); const uploadWsKey = useUploadWsKey();
@@ -104,7 +103,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
}); });
const userWorkspaces = orgUserProjects; const userWorkspaces = orgUserProjects;
if ( if (
(userWorkspaces.length === 0 && (userWorkspaces?.length === 0 &&
router.asPath !== '/noprojects' && router.asPath !== '/noprojects' &&
!router.asPath.includes('home') && !router.asPath.includes('home') &&
!router.asPath.includes('settings')) || !router.asPath.includes('settings')) ||
+17 -1
View File
@@ -6,7 +6,10 @@ import { useRouter } from 'next/router';
import Button from '@app/components/basic/buttons/Button'; import Button from '@app/components/basic/buttons/Button';
import EventFilter from '@app/components/basic/EventFilter'; import EventFilter from '@app/components/basic/EventFilter';
import NavHeader from '@app/components/navigation/NavHeader'; import NavHeader from '@app/components/navigation/NavHeader';
import { UpgradePlanModal } from '@app/components/v2';
import { useSubscription } from '@app/context';
import ActivitySideBar from '@app/ee/components/ActivitySideBar'; import ActivitySideBar from '@app/ee/components/ActivitySideBar';
import { usePopUp } from '@app/hooks/usePopUp';
import getProjectLogs from '../../ee/api/secrets/GetProjectLogs'; import getProjectLogs from '../../ee/api/secrets/GetProjectLogs';
import ActivityTable from '../../ee/components/ActivityTable'; import ActivityTable from '../../ee/components/ActivityTable';
@@ -67,6 +70,10 @@ export default function Activity() {
const currentLimit = 10; const currentLimit = 10;
const [currentSidebarAction, toggleSidebar] = useState<string>(); const [currentSidebarAction, toggleSidebar] = useState<string>();
const { t } = useTranslation(); const { t } = useTranslation();
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
'upgradePlan'
] as const);
// this use effect updates the data in case of a new filter being added // this use effect updates the data in case of a new filter being added
useEffect(() => { useEffect(() => {
@@ -137,11 +144,15 @@ export default function Activity() {
}, [currentLimit, currentOffset]); }, [currentLimit, currentOffset]);
const loadMoreLogs = () => { const loadMoreLogs = () => {
if (subscription?.auditLogs === false) {
handlePopUpOpen('upgradePlan');
} else {
setCurrentOffset(currentOffset + currentLimit); setCurrentOffset(currentOffset + currentLimit);
}
}; };
return ( return (
<div className="mx-6 lg:mx-0 w-full h-screen"> <div className="mx-6 lg:mx-0 w-full h-full">
<Head> <Head>
<title>Audit Logs</title> <title>Audit Logs</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
@@ -173,6 +184,11 @@ export default function Activity() {
/> />
</div> </div>
</div> </div>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={() => handlePopUpClose('upgradePlan')}
text="You can see more logs if you switch to Infisical's Business/Professional Plan."
/>
</div> </div>
); );
} }
+13 -12
View File
@@ -11,6 +11,7 @@ import AddProjectMemberDialog from '@app/components/basic/dialog/AddProjectMembe
import ProjectUsersTable from '@app/components/basic/table/ProjectUsersTable'; import ProjectUsersTable from '@app/components/basic/table/ProjectUsersTable';
import NavHeader from '@app/components/navigation/NavHeader'; import NavHeader from '@app/components/navigation/NavHeader';
import guidGenerator from '@app/components/utilities/randomId'; import guidGenerator from '@app/components/utilities/randomId';
import { Input } from '@app/components/v2';
import { import {
decryptAssymmetric, decryptAssymmetric,
@@ -56,6 +57,7 @@ export default function Users() {
const workspaceId = router.query.id as string; const workspaceId = router.query.id as string;
const [userList, setUserList] = useState<any[]>([]); const [userList, setUserList] = useState<any[]>([]);
const [isUserListLoading, setIsUserListLoading] = useState(true);
const [orgUserList, setOrgUserList] = useState<any[]>([]); const [orgUserList, setOrgUserList] = useState<any[]>([]);
useEffect(() => { useEffect(() => {
@@ -81,6 +83,8 @@ export default function Users() {
})); }));
setUserList(tempUserList); setUserList(tempUserList);
setIsUserListLoading(false);
// This is needed to know wha users from an org (if any), we are able to add to a certain project // This is needed to know wha users from an org (if any), we are able to add to a certain project
const orgUsers = await getOrganizationUsers({ const orgUsers = await getOrganizationUsers({
orgId: String(localStorage.getItem('orgData.id')) orgId: String(localStorage.getItem('orgData.id'))
@@ -151,9 +155,8 @@ export default function Users() {
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Head> </Head>
<NavHeader pageName={t('settings.members.title')} isProjectRelated /> <NavHeader pageName={t('settings.members.title')} isProjectRelated />
<div className="flex flex-col items-start justify-start px-6 py-6 pb-4 text-3xl"> <div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
<p className="mr-4 font-semibold text-white">{t('settings.members.title')}</p> <p className="mr-4 font-semibold text-white">{t('settings.members.title')}</p>
<p className="mr-4 text-base text-gray-400">{t('settings.members.description')}</p>
</div> </div>
<AddProjectMemberDialog <AddProjectMemberDialog
isOpen={isAddOpen} isOpen={isAddOpen}
@@ -169,20 +172,17 @@ export default function Users() {
setEmail={setEmail} setEmail={setEmail}
/> />
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */} {/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
<div className="flex w-full flex-row items-start px-6 pb-1"> <div className="absolute right-4 top-36 flex w-full flex-row items-start px-6 pb-1">
<div className="mt-2 flex h-10 w-full flex-row items-center rounded-md bg-white/5"> <div className="flex w-full max-w-sm flex flex-row ml-auto">
<FontAwesomeIcon <Input
className="rounded-l-md bg-white/5 py-3 pl-4 pr-2 text-gray-400" className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
icon={faMagnifyingGlass} placeholder="Search by users..."
/>
<input
className="h-full w-full rounded-r-md bg-white/5 pl-2 text-gray-400 outline-none"
value={searchUsers} value={searchUsers}
onChange={(e) => setSearchUsers(e.target.value)} onChange={(e) => setSearchUsers(e.target.value)}
placeholder={String(t('section.members.search-members'))} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
/> />
</div> </div>
<div className="mt-2 ml-2 flex min-w-max flex-row items-start justify-start"> <div className="ml-2 flex min-w-max flex-row items-start justify-start">
<Button <Button
text={String(t('section.members.add-member'))} text={String(t('section.members.add-member'))}
onButtonPressed={openAddModal} onButtonPressed={openAddModal}
@@ -198,6 +198,7 @@ export default function Users() {
changeData={setUserList} changeData={setUserList}
myUser={personalEmail} myUser={personalEmail}
filter={searchUsers} filter={searchUsers}
isUserListLoading={isUserListLoading}
// onClick={openDeleteModal} // onClick={openDeleteModal}
// deleteUser={deleteMembership} // deleteUser={deleteMembership}
// setUserIdToBeDeleted={setUserIdToBeDeleted} // setUserIdToBeDeleted={setUserIdToBeDeleted}
@@ -2,10 +2,12 @@ import { useEffect, useMemo, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { faKey, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import NavHeader from '@app/components/navigation/NavHeader'; import NavHeader from '@app/components/navigation/NavHeader';
import { Button, TableContainer, Tooltip } from '@app/components/v2'; import { Button, Input, TableContainer, Tooltip } from '@app/components/v2';
import { useWorkspace } from '@app/context'; import { useWorkspace } from '@app/context';
import { import {
useGetProjectSecretsByKey, useGetProjectSecretsByKey,
@@ -27,6 +29,8 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
const workspaceId = currentWorkspace?._id as string; const workspaceId = currentWorkspace?._id as string;
const { data: latestFileKey } = useGetUserWsKey(workspaceId); const { data: latestFileKey } = useGetUserWsKey(workspaceId);
const [searchFilter, setSearchFilter] = useState('');
useEffect(() => { useEffect(() => {
if (!isLoading && !workspaceId && router.isReady) { if (!isLoading && !workspaceId && router.isReady) {
router.push('/noprojects'); router.push('/noprojects');
@@ -88,7 +92,7 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
} }
// when secrets is not loading and secrets list is empty // when secrets is not loading and secrets list is empty
const isDashboardSecretEmpty = !isSecretsLoading && !Object.keys(secrets?.secrets || {})?.length; const isDashboardSecretEmpty = !isSecretsLoading && !Object.keys(secrets?.secrets || {})?.filter((secret: any) => secret.toUpperCase().includes(searchFilter.toUpperCase()))?.length;
return ( return (
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]"> <div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
@@ -121,6 +125,15 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
</a> </a>
</p> </p>
</div> </div>
<div className="absolute top-[11.1rem] right-6 flex w-full max-w-sm flex-grow space-x-2">
<Input
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
placeholder="Search by secret name..."
value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
/>
</div>
<div className="overflow-y-auto"> <div className="overflow-y-auto">
<div className="sticky top-0 mt-8 flex h-10 min-w-[60.3rem] flex-row rounded-md border border-mineshaft-600 bg-mineshaft-800"> <div className="sticky top-0 mt-8 flex h-10 min-w-[60.3rem] flex-row rounded-md border border-mineshaft-600 bg-mineshaft-800">
<div className="sticky top-0 flex w-10 items-center justify-center border-none px-4"> <div className="sticky top-0 flex w-10 items-center justify-center border-none px-4">
@@ -167,7 +180,7 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
<TableContainer className="border-none"> <TableContainer className="border-none">
<table className="secret-table relative w-full bg-mineshaft-900"> <table className="secret-table relative w-full bg-mineshaft-900">
<tbody className="max-h-screen overflow-y-auto"> <tbody className="max-h-screen overflow-y-auto">
{Object.keys(secrets?.secrets || {}).map((key, index) => ( {Object.keys(secrets?.secrets || {})?.filter((secret: any) => secret.toUpperCase().includes(searchFilter.toUpperCase())).map((key, index) => (
<EnvComparisonRow <EnvComparisonRow
key={`row-${key}`} key={`row-${key}`}
secrets={secrets?.secrets?.[key]} secrets={secrets?.secrets?.[key]}
@@ -184,8 +197,9 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
{isDashboardSecretEmpty && ( {isDashboardSecretEmpty && (
<div className="flex h-40 w-full flex-row rounded-md"> <div className="flex h-40 w-full flex-row rounded-md">
<div className="flex w-full min-w-[11rem] flex-col items-center justify-center rounded-md border-none bg-mineshaft-800 text-bunker-300"> <div className="flex w-full min-w-[11rem] flex-col items-center justify-center rounded-md border-none bg-mineshaft-800 text-bunker-300">
<span className="mb-1">No secrets are available in this project yet.</span> <FontAwesomeIcon icon={faKey} className="text-4xl mb-4" />
<span>You can go into any environment to add secrets there.</span> <span className="mb-1">No secrets found.</span>
<span>To add more secrets you can explore any environment.</span>
</div> </div>
</div> </div>
)} )}
@@ -1,6 +1,6 @@
/* eslint-disable react/jsx-no-useless-fragment */ /* eslint-disable react/jsx-no-useless-fragment */
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; import { faCircle, faEye, faEyeSlash, faMinus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
@@ -29,7 +29,9 @@ const DashboardInput = ({
const syntaxHighlight = useCallback((val: string) => { const syntaxHighlight = useCallback((val: string) => {
if (val === undefined) if (val === undefined)
return ( return (
<span className="cursor-default font-sans text-xs italic text-red-500/80">missing</span> <span className="cursor-default font-sans text-xs italic text-red-500/80">
<FontAwesomeIcon icon={faMinus} className="mt-1" />
</span>
); );
if (val?.length === 0) if (val?.length === 0)
return <span className="w-full font-sans text-bunker-400/80">EMPTY</span>; return <span className="w-full font-sans text-bunker-400/80">EMPTY</span>;
@@ -23,17 +23,20 @@ export const FolderSection = ({
{folders {folders
.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase())) .filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()))
.map(({ id, name }) => ( .map(({ id, name }) => (
<tr key={id} className="group flex flex-row items-center hover:bg-mineshaft-700 cursor-default"> <tr
<td className="flex h-10 w-10 items-center justify-center border-none px-4 ml-0.5"> key={id}
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
>
<td className="ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4">
<FontAwesomeIcon icon={faFolder} className="text-primary-700" /> <FontAwesomeIcon icon={faFolder} className="text-primary-700" />
</td> </td>
<td <td
colSpan={2} colSpan={2}
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis uppercase lg:min-w-[240px] xl:min-w-[280px]" className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis lg:min-w-[240px] xl:min-w-[280px]"
style={{ paddingTop: '0', paddingBottom: '0' }} style={{ paddingTop: '0', paddingBottom: '0' }}
> >
<div <div
className="flex-grow p-2 cursor-default" className="flex-grow cursor-default p-2"
onKeyDown={() => null} onKeyDown={() => null}
tabIndex={0} tabIndex={0}
role="button" role="button"
@@ -38,7 +38,7 @@ export const OrgSettingsPage = () => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { user } = useUser(); const { user } = useUser();
const { subscriptionPlan } = useSubscription(); const { subscription } = useSubscription();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const orgId = currentOrg?._id || ''; const orgId = currentOrg?._id || '';
@@ -60,14 +60,7 @@ export const OrgSettingsPage = () => {
const [completeInviteLink, setcompleteInviteLink] = useState<string | undefined>(''); const [completeInviteLink, setcompleteInviteLink] = useState<string | undefined>('');
const isMoreUsersNotAllowed = const isMoreUsersNotAllowed = ((subscription?.membersUsed || 0) >= (subscription?.membersLimit || 1)) && host === 'https://app.infisical.com';
(orgUsers || []).length >= 5 &&
subscriptionPlan === plans.starter &&
host === 'https://app.infisical.com' &&
currentWorkspace?._id !== '63ea8121b6e2b0543ba79616' &&
currentWorkspace?._id !== '634870246fd2e26f28e76996' &&
currentWorkspace?._id !== '63d823cef9e728a0a961255a' &&
currentWorkspace?._id !== '6412ec319db25595ac00b8c6';
const onRenameOrg = async (name: string) => { const onRenameOrg = async (name: string) => {
if (!currentOrg?._id) return; if (!currentOrg?._id) return;
@@ -13,7 +13,6 @@ import {
encryptSymmetric encryptSymmetric
} from '@app/components/utilities/cryptography/crypto'; } from '@app/components/utilities/cryptography/crypto';
import { Button, FormControl, Input } from '@app/components/v2'; import { Button, FormControl, Input } from '@app/components/v2';
import { plans } from '@app/const';
import { useSubscription, useWorkspace } from '@app/context'; import { useSubscription, useWorkspace } from '@app/context';
import { useToggle } from '@app/hooks'; import { useToggle } from '@app/hooks';
import { import {
@@ -89,10 +88,9 @@ export const ProjectSettingsPage = () => {
const deleteWsTag = useDeleteWsTag(); const deleteWsTag = useDeleteWsTag();
// get user subscription // get user subscription
const { subscriptionPlan } = useSubscription(); const { subscription } = useSubscription();
const host = window.location.origin; const host = window.location.origin;
const isEnvServiceAllowed = const isEnvServiceAllowed = ((currentWorkspace?.environments || []).length < (subscription?.envLimit || 3) && host === 'https://app.infisical.com');
subscriptionPlan !== plans.starter || host !== 'https://app.infisical.com';
const onRenameWorkspace = async (name: string) => { const onRenameWorkspace = async (name: string) => {
try { try {
@@ -219,7 +217,8 @@ export const ProjectSettingsPage = () => {
environment, environment,
expiresIn, expiresIn,
name, name,
permissions permissions,
secretPath
}: CreateServiceToken) => { }: CreateServiceToken) => {
// type guard // type guard
if (!latestFileKey) return ''; if (!latestFileKey) return '';
@@ -243,6 +242,7 @@ export const ProjectSettingsPage = () => {
iv, iv,
tag, tag,
environment, environment,
secretPath,
expiresIn: Number(expiresIn), expiresIn: Number(expiresIn),
name, name,
workspaceId: workspaceID, workspaceId: workspaceID,
@@ -402,7 +402,7 @@ export const ProjectSettingsPage = () => {
{!isBlindIndexedLoading && !isBlindIndexed && ( {!isBlindIndexedLoading && !isBlindIndexed && (
<ProjectIndexSecretsSection onEnableBlindIndices={onEnableBlindIndices} /> <ProjectIndexSecretsSection onEnableBlindIndices={onEnableBlindIndices} />
)} )}
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md border-l border-red bg-white/5 px-6 pl-6 pb-4 pt-4"> <div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md border-l border-red bg-mineshaft-900 px-6 pl-6 pb-4 pt-4">
<p className="text-xl font-bold text-red">{t('settings.project.danger-zone')}</p> <p className="text-xl font-bold text-red">{t('settings.project.danger-zone')}</p>
<p className="text-md mt-2 text-gray-400">{t('settings.project.danger-zone-note')}</p> <p className="text-md mt-2 text-gray-400">{t('settings.project.danger-zone-note')}</p>
<div className="mr-auto mt-4 max-h-28 w-full max-w-md"> <div className="mr-auto mt-4 max-h-28 w-full max-w-md">
@@ -418,6 +418,7 @@ export const ProjectSettingsPage = () => {
onChange={(e) => setDeleteProjectInput(e.target.value)} onChange={(e) => setDeleteProjectInput(e.target.value)}
value={deleteProjectInput} value={deleteProjectInput}
placeholder="Type the project name to delete" placeholder="Type the project name to delete"
className="bg-mineshaft-800"
/> />
</FormControl> </FormControl>
</div> </div>
@@ -13,7 +13,7 @@ export const AutoCapitalizationSection = ({
}: Props) => { }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2"> <div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-mineshaft-900 px-6 pb-6 pt-2">
<p className="mb-4 mt-2 text-xl font-semibold">{t('settings.project.auto-capitalization')}</p> <p className="mb-4 mt-2 text-xl font-semibold">{t('settings.project.auto-capitalization')}</p>
<Checkbox <Checkbox
className="data-[state=checked]:bg-primary" className="data-[state=checked]:bg-primary"
@@ -28,24 +28,9 @@ export const CopyProjectIDSection = ({ workspaceID }: Props): JSX.Element => {
}; };
return ( return (
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-4 pb-2"> <div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-mineshaft-900 px-6 pt-4 pb-2">
<p className="self-start text-xl font-semibold">{t('common.project-id')}</p> <p className="self-start text-xl font-semibold">{t('common.project-id')}</p>
<p className="mt-4 self-start text-base font-normal text-gray-400"> <p className="mt-4 text-sm text-bunker-300 mb-2">{t('settings.project.auto-generated')}</p>
{t('settings.project.project-id-description')}
</p>
<p className="mt-2 self-start text-base font-normal text-gray-400">
{t('settings.project.project-id-description2')}
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener"
className="text-primary duration-200 hover:opacity-80"
>
{t('settings.project.docs')}
</a>
</p>
<p className="mt-4 text-xs text-bunker-300">{t('settings.project.auto-generated')}</p>
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] text-base text-gray-400"> <div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] text-base text-gray-400">
<p className="mr-2 pl-4 font-bold">{`${t('common.project-id')}:`}</p> <p className="mr-2 pl-4 font-bold">{`${t('common.project-id')}:`}</p>
<p className="mr-4">{workspaceID}</p> <p className="mr-4">{workspaceID}</p>
@@ -1,5 +1,5 @@
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { faPencil, faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons'; import { faPencil, faPlus, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup'; import * as yup from 'yup';
@@ -82,7 +82,7 @@ export const EnvironmentSection = ({
}; };
return ( return (
<div className="mt-4 mb-4 flex w-full flex-col items-start rounded-md bg-white/5 p-6"> <div className="mt-4 mb-4 flex w-full flex-col items-start rounded-md bg-mineshaft-900 p-6">
<div className="mb-2 flex w-full flex-row justify-between"> <div className="mb-2 flex w-full flex-row justify-between">
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<p className="mb-3 text-xl font-semibold">Project Environments</p> <p className="mb-3 text-xl font-semibold">Project Environments</p>
@@ -128,32 +128,27 @@ export const EnvironmentSection = ({
<Td>{slug}</Td> <Td>{slug}</Td>
<Td className="flex items-center justify-end"> <Td className="flex items-center justify-end">
<IconButton <IconButton
className="mr-3" className="mr-3 py-2"
onClick={() => { onClick={() => {
if (isEnvServiceAllowed) {
handlePopUpOpen('createUpdateEnv', { name, slug }); handlePopUpOpen('createUpdateEnv', { name, slug });
reset({ environmentName: name, environmentSlug: slug }); reset({ environmentName: name, environmentSlug: slug });
} else {
handlePopUpOpen('upgradePlan');
}
}} }}
colorSchema="secondary" colorSchema="primary"
variant="plain"
ariaLabel="update" ariaLabel="update"
> >
<FontAwesomeIcon icon={faPencil} /> <FontAwesomeIcon icon={faPencil} />
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => { onClick={() => {
if (isEnvServiceAllowed) {
handlePopUpOpen('deleteEnv', { name, slug }); handlePopUpOpen('deleteEnv', { name, slug });
} else {
handlePopUpOpen('upgradePlan');
}
}} }}
size="lg"
colorSchema="danger" colorSchema="danger"
variant="plain"
ariaLabel="update" ariaLabel="update"
> >
<FontAwesomeIcon icon={faTrashCan} /> <FontAwesomeIcon icon={faXmark} />
</IconButton> </IconButton>
</Td> </Td>
</Tr> </Tr>
@@ -11,7 +11,7 @@ export const ProjectIndexSecretsSection = ({
onEnableBlindIndices onEnableBlindIndices
}: Props) => { }: Props) => {
return ( return (
<div className="rounded-md bg-white/5 p-6 my-2"> <div className="rounded-md bg-mineshaft-900 p-6 my-2">
<p className="mb-4 text-xl font-semibold">Blind Indices</p> <p className="mb-4 text-xl font-semibold">Blind Indices</p>
<p className="mb-4 text-sm text-gray-400"> <p className="mb-4 text-sm text-gray-400">
Your project, created before the introduction of blind indexing, contains unindexed secrets. To access individual secrets by name through the SDK and public API, please enable blind indexing. Your project, created before the introduction of blind indexing, contains unindexed secrets. To access individual secrets by name through the SDK and public API, please enable blind indexing.
@@ -41,14 +41,14 @@ export const ProjectNameChangeSection = ({
return ( return (
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-3"> <div className="mb-6 flex w-full flex-col items-start rounded-md bg-mineshaft-900 px-6 pb-6 pt-3">
<p className="mb-4 mt-2 text-xl font-semibold">{t('common.display-name')}</p> <p className="mb-4 mt-2 text-xl font-semibold">{t('common.display-name')}</p>
<div className="mb-2 w-full max-w-lg"> <div className="mb-2 w-full max-w-lg">
<Controller <Controller
defaultValue="" defaultValue=""
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}> <FormControl isError={Boolean(error)} errorText={error?.message}>
<Input placeholder="Type your project name" {...field} /> <Input placeholder="Type your project name" {...field} className="bg-mineshaft-800" />
</FormControl> </FormControl>
)} )}
control={control} control={control}
@@ -74,7 +74,7 @@ export const SecretTagsSection = ({
}; };
return ( return (
<div className="mt-4 mb-4 flex w-full flex-col items-start rounded-md bg-white/5 p-6"> <div className="mt-4 mb-4 flex w-full flex-col items-start rounded-md bg-mineshaft-900 p-6">
<div className="flex w-full flex-row justify-between"> <div className="flex w-full flex-row justify-between">
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<p className="mb-3 text-xl font-semibold">Secret Tags</p> <p className="mb-3 text-xl font-semibold">Secret Tags</p>
@@ -42,8 +42,9 @@ const apiTokenExpiry = [
]; ];
const createServiceTokenSchema = yup.object({ const createServiceTokenSchema = yup.object({
name: yup.string().required().label('Service Token Name'), name: yup.string().max(100).required().label('Service Token Name'),
environment: yup.string().required().label('Environment'), environment: yup.string().max(50).required().label('Environment'),
secretPath: yup.string().required().default('/').label('Secret Path'),
expiresIn: yup.string().optional().label('Service Token Expiration'), expiresIn: yup.string().optional().label('Service Token Expiration'),
permissions: yup permissions: yup
.object() .object()
@@ -120,23 +121,11 @@ export const ServiceTokenSection = ({
}; };
return ( return (
<div className="mt-4 mb-4 flex w-full flex-col items-start rounded-md bg-white/5 p-6"> <div className="mt-4 mb-4 flex w-full flex-col items-start rounded-md bg-mineshaft-900 p-6">
<div className="flex w-full flex-row justify-between"> <div className="flex w-full flex-row justify-between">
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<p className="mb-3 text-xl font-semibold">{t('section.token.service-tokens')}</p> <p className="mb-3 text-xl font-semibold">{t('section.token.service-tokens')}</p>
<p className="text-sm text-gray-400">{t('section.token.service-tokens-description')}</p> <p className="text-sm text-gray-400 mb-4">{t('section.token.service-tokens-description')}</p>
<p className="mb-4 text-sm text-gray-400">
Please, make sure you are on the
<a
className="ml-1 text-primary underline underline-offset-2"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noreferrer"
>
latest version of CLI
</a>
.
</p>
</div> </div>
<div> <div>
<Modal <Modal
@@ -201,6 +190,22 @@ export const ServiceTokenSection = ({
</FormControl> </FormControl>
)} )}
/> />
<Controller
control={control}
name="secretPath"
defaultValue="/"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secrets Path"
isError={Boolean(error)}
helperText="Tokens can be scoped to a folder path. Default path is /"
errorText={error?.message}
>
<Input {...field} placeholder="Provide a path, default is /" />
</FormControl>
)}
/>
<Controller <Controller
control={control} control={control}
name="expiresIn" name="expiresIn"
@@ -329,6 +334,7 @@ export const ServiceTokenSection = ({
<Tr> <Tr>
<Th>Token Name</Th> <Th>Token Name</Th>
<Th>Environment</Th> <Th>Environment</Th>
<Th>Secret Path</Th>
<Th>Valid Until</Th> <Th>Valid Until</Th>
<Th aria-label="button" /> <Th aria-label="button" />
</Tr> </Tr>
@@ -340,6 +346,7 @@ export const ServiceTokenSection = ({
<Tr key={row._id}> <Tr key={row._id}>
<Td>{row.name}</Td> <Td>{row.name}</Td>
<Td>{row.environment}</Td> <Td>{row.environment}</Td>
<Td>{row.secretPath}</Td>
<Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td> <Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td>
<Td className="flex items-center justify-end"> <Td className="flex items-center justify-end">
<IconButton <IconButton
@@ -359,7 +366,7 @@ export const ServiceTokenSection = ({
))} ))}
{!isLoading && tokens?.length === 0 && ( {!isLoading && tokens?.length === 0 && (
<Tr> <Tr>
<Td colSpan={4} className="py-6 text-center text-bunker-400"> <Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
<EmptyState title="No service tokens found" icon={faKey} /> <EmptyState title="No service tokens found" icon={faKey} />
</Td> </Td>
</Tr> </Tr>