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