This commit is contained in:
Tuan Dang
2023-06-17 10:35:49 +07:00
22 changed files with 795 additions and 473 deletions

View File

@@ -11,6 +11,7 @@ import {
validateFolderName,
generateFolderId,
getParentFromFolderId,
getFolderByPath,
} from "../../services/FolderService";
import { ADMIN, MEMBER } from "../../variables";
import { validateMembership } from "../../helpers/membership";
@@ -177,11 +178,13 @@ export const deleteFolder = async (req: Request, res: Response) => {
// TODO: validate workspace
export const getFolders = async (req: Request, res: Response) => {
const { workspaceId, environment, parentFolderId } = req.query as {
workspaceId: string;
environment: string;
parentFolderId?: string;
};
const { workspaceId, environment, parentFolderId, parentFolderPath } =
req.query as {
workspaceId: string;
environment: string;
parentFolderId?: string;
parentFolderPath?: string;
};
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
@@ -196,6 +199,20 @@ export const getFolders = async (req: Request, res: Response) => {
acceptedRoles: [ADMIN, MEMBER],
});
// if instead of parentFolderId given a path like /folder1/folder2
if (parentFolderPath) {
const folder = getFolderByPath(folders.nodes, parentFolderPath);
if (!folder) {
res.send({ folders: [], dir: [] });
return;
}
// dir is not needed at present as this is only used in overview section of secrets
res.send({
folders: folder.children.map(({ id, name }) => ({ id, name })),
dir: [{ name: folder.name, id: folder.id }],
});
}
if (!parentFolderId) {
const rootFolders = folders.nodes.children.map(({ id, name }) => ({
id,

View File

@@ -700,11 +700,15 @@ export const getSecrets = async (req: Request, res: Response) => {
(!folders && folderId && folderId !== "root") ||
(!folders && secretPath)
) {
throw BadRequestError({ message: "Folder not found" });
res.send({ secrets: [] });
return;
}
if (folders && folderId !== "root") {
const folder = searchByFolderId(folders.nodes, folderId as string);
if (!folder) throw BadRequestError({ message: "Folder not found" });
if (!folder) {
res.send({ secrets: [] });
return;
}
}
if (req.authData.authPayload instanceof ServiceTokenData) {
@@ -720,10 +724,11 @@ export const getSecrets = async (req: Request, res: Response) => {
}
if (folders && secretPath) {
if (!folders) throw BadRequestError({ message: "Folder not found" });
// avoid throwing error and send empty list
const folder = getFolderByPath(folders.nodes, secretPath as string);
if (!folder) {
throw BadRequestError({ message: "Secret path not found" });
res.send({ secrets: [] });
return;
}
folderId = folder.id;
}

View File

@@ -34,7 +34,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
secret,
key
});
return rep;
})
});
@@ -88,7 +88,7 @@ export const createSecretRaw = async (req: Request, res: Response) => {
secretComment,
secretPath = "/"
} = req.body;
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
});
@@ -102,12 +102,12 @@ export const createSecretRaw = async (req: Request, res: Response) => {
plaintext: secretValue,
key
});
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretComment,
key
});
});
const secret = await SecretService.createSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
@@ -135,7 +135,7 @@ export const createSecretRaw = async (req: Request, res: Response) => {
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({
secret: repackageSecretToRaw({
secret: secretWithoutBlindIndex,
@@ -202,11 +202,11 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
*/
export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretPath = "/"
const {
workspaceId,
environment,
type,
secretPath = "/"
} = req.body;
const { secret } = await SecretService.deleteSecret({
@@ -391,11 +391,11 @@ export const updateSecretByName = async (req: Request, res: Response) => {
*/
export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretPath = "/"
const {
workspaceId,
environment,
type,
secretPath = "/"
} = req.body;
const { secret } = await SecretService.deleteSecret({

View File

@@ -1,16 +1,16 @@
import rateLimit from 'express-rate-limit';
const MongoStore = require('rate-limit-mongo');
// const MongoStore = require('rate-limit-mongo');
// 200 per minute
export const apiLimiter = rateLimit({
store: new MongoStore({
uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60,
collectionName: "expressRateRecords-apiLimiter",
errorHandler: console.error.bind(null, 'rate-limit-mongo')
}),
windowMs: 1000 * 60,
max: 200,
// store: new MongoStore({
// uri: process.env.MONGO_URL,
// expireTimeMs: 1000 * 60,
// collectionName: "expressRateRecords-apiLimiter",
// errorHandler: console.error.bind(null, 'rate-limit-mongo')
// }),
windowMs: 60 * 1000,
max: 240,
standardHeaders: true,
legacyHeaders: false,
skip: (request) => {
@@ -23,14 +23,14 @@ export const apiLimiter = rateLimit({
// 50 requests per 1 hours
const authLimit = rateLimit({
store: new MongoStore({
uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60 * 60,
errorHandler: console.error.bind(null, 'rate-limit-mongo'),
collectionName: "expressRateRecords-authLimit",
}),
windowMs: 1000 * 60 * 60,
max: 50,
// store: new MongoStore({
// uri: process.env.MONGO_URL,
// expireTimeMs: 1000 * 60 * 60,
// errorHandler: console.error.bind(null, 'rate-limit-mongo'),
// collectionName: "expressRateRecords-authLimit",
// }),
windowMs: 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => {
@@ -40,14 +40,14 @@ const authLimit = rateLimit({
// 5 requests per 1 hour
export const passwordLimiter = rateLimit({
store: new MongoStore({
uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60 * 60,
errorHandler: console.error.bind(null, 'rate-limit-mongo'),
collectionName: "expressRateRecords-passwordLimiter",
}),
windowMs: 1000 * 60 * 60,
max: 5,
// store: new MongoStore({
// uri: process.env.MONGO_URL,
// expireTimeMs: 1000 * 60 * 60,
// errorHandler: console.error.bind(null, 'rate-limit-mongo'),
// collectionName: "expressRateRecords-passwordLimiter",
// }),
windowMs: 60 * 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => {

View File

@@ -57,7 +57,7 @@ import { getFolderIdFromServiceToken } from "../services/FolderService";
export const repackageSecretToRaw = ({
secret,
key
}:{
}: {
secret: ISecret;
key: string;
}) => {
@@ -76,8 +76,8 @@ export const repackageSecretToRaw = ({
key
});
let secretComment: string = '';
let secretComment: string = '';
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
secretComment = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext,
@@ -86,7 +86,7 @@ export const repackageSecretToRaw = ({
key
});
}
return ({
_id: secret._id,
version: secret.version,
@@ -503,7 +503,7 @@ export const getSecretsHelper = async ({
folder: folderId,
type: SECRET_PERSONAL,
...getAuthDataPayloadUserObj(authData),
}).lean();
}).populate("tags").lean();
// concat with shared secrets
secrets = secrets.concat(
@@ -515,7 +515,7 @@ export const getSecretsHelper = async ({
secretBlindIndex: {
$nin: secrets.map((secret) => secret.secretBlindIndex),
},
}).lean()
}).populate("tags").lean()
);
// (EE) create (audit) log
@@ -553,7 +553,7 @@ export const getSecretsHelper = async ({
},
});
}
return secrets;
};
@@ -652,7 +652,7 @@ export const getSecretHelper = async ({
},
});
}
return secret;
};
@@ -843,7 +843,7 @@ export const deleteSecretHelper = async ({
// 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" });
}
@@ -909,12 +909,12 @@ export const deleteSecretHelper = async ({
});
action && (await EELogService.createLog({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP,
}));
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP,
}));
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
@@ -941,7 +941,7 @@ export const deleteSecretHelper = async ({
},
});
}
return ({
secrets,
secret

View File

@@ -63,6 +63,7 @@ router.get(
query("workspaceId").exists().isString().trim(),
query("environment").exists().isString().trim(),
query("parentFolderId").optional().isString().trim(),
query("parentFolderPath").optional().isString().trim(),
validateRequest,
getFolders
);

View File

@@ -11,63 +11,6 @@ import (
const USER_AGENT = "cli"
func CallBatchModifySecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchModifySecretsByWorkspaceAndEnvRequest) error {
endpoint := fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL)
response, err := httpClient.
R().
SetBody(request).
SetHeader("User-Agent", USER_AGENT).
Patch(endpoint)
if err != nil {
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallBatchCreateSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchCreateSecretsByWorkspaceAndEnvRequest) error {
endpoint := fmt.Sprintf("%v/v2/secrets/", config.INFISICAL_URL)
response, err := httpClient.
R().
SetBody(request).
SetHeader("User-Agent", USER_AGENT).
Post(endpoint)
if err != nil {
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchDeleteSecretsBySecretIdsRequest) error {
endpoint := fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL)
response, err := httpClient.
R().
SetBody(request).
SetHeader("User-Agent", USER_AGENT).
Delete(endpoint)
if err != nil {
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncryptedWorkspaceKeyRequest) (GetEncryptedWorkspaceKeyResponse, error) {
endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", config.INFISICAL_URL, request.WorkspaceId)
var result GetEncryptedWorkspaceKeyResponse
@@ -107,28 +50,6 @@ func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDeta
return tokenDetailsResponse, nil
}
func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Request) (GetEncryptedSecretsV2Response, error) {
var secretsResponse GetEncryptedSecretsV2Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId).
SetQueryParam("tagSlugs", request.TagSlugs).
Get(fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL))
if err != nil {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unsuccessful response: [response=%s]", response)
}
return secretsResponse, nil
}
func CallLogin1V2(httpClient *resty.Client, request GetLoginOneV2Request) (GetLoginOneV2Response, error) {
var loginOneV2Response GetLoginOneV2Response
response, err := httpClient.
@@ -303,3 +224,110 @@ func CallGetNewAccessTokenWithRefreshToken(httpClient *resty.Client, refreshToke
return newAccessToken, nil
}
func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Request) (GetEncryptedSecretsV3Response, error) {
var secretsResponse GetEncryptedSecretsV3Response
httpRequest := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId)
if request.SecretPath != "" {
httpRequest.SetQueryParam("secretPath", request.SecretPath)
}
response, err := httpRequest.Get(fmt.Sprintf("%v/v3/secrets", config.INFISICAL_URL))
if err != nil {
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return secretsResponse, nil
}
func CallCreateSecretsV3(httpClient *resty.Client, request CreateSecretV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
if err != nil {
return fmt.Errorf("CallCreateSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallCreateSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return nil
}
func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Delete(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
if err != nil {
return fmt.Errorf("CallDeleteSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallDeleteSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return nil
}
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
if err != nil {
return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallUpdateSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return nil
}
func CallGetSingleSecretByNameV3(httpClient *resty.Client, request CreateSecretV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
if err != nil {
return fmt.Errorf("CallGetSingleSecretByNameV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallGetSingleSecretByNameV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return nil
}

View File

@@ -143,24 +143,7 @@ type Secret struct {
SecretCommentHash string `json:"secretCommentHash,omitempty"`
Type string `json:"type,omitempty"`
ID string `json:"id,omitempty"`
}
type BatchCreateSecretsByWorkspaceAndEnvRequest struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
Secrets []Secret `json:"secrets"`
}
type BatchModifySecretsByWorkspaceAndEnvRequest struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
Secrets []Secret `json:"secrets"`
}
type BatchDeleteSecretsBySecretIdsRequest struct {
EnvironmentName string `json:"environmentName"`
WorkspaceId string `json:"workspaceId"`
SecretIds []string `json:"secretIds"`
PlainTextKey string `json:"plainTextKey"`
}
type GetEncryptedWorkspaceKeyRequest struct {
@@ -194,41 +177,6 @@ type GetSecretsByWorkspaceIdAndEnvironmentRequest struct {
WorkspaceId string `json:"workspaceId"`
}
type GetEncryptedSecretsV2Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
TagSlugs string `json:"tagSlugs"`
}
type GetEncryptedSecretsV2Response struct {
Secrets []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
User string `json:"user,omitempty"`
Tags []struct {
ID string `json:"_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Workspace string `json:"workspace"`
} `json:"tags"`
} `json:"secrets"`
}
type GetServiceTokenDetailsResponse struct {
ID string `json:"_id"`
Name string `json:"name"`
@@ -320,3 +268,109 @@ type VerifyMfaTokenErrorResponse struct {
type GetNewAccessTokenWithRefreshTokenResponse struct {
Token string `json:"token"`
}
type GetEncryptedSecretsV3Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
}
type GetEncryptedSecretsV3Response struct {
Secrets []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Tags []struct {
ID string `json:"_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Workspace string `json:"workspace"`
} `json:"tags"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
Algorithm string `json:"algorithm"`
KeyEncoding string `json:"keyEncoding"`
Folder string `json:"folder"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"secrets"`
}
type CreateSecretV3Request struct {
SecretName string `json:"secretName"`
WorkspaceID string `json:"workspaceId"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
SecretPath string `json:"secretPath"`
}
type DeleteSecretV3Request struct {
SecretName string `json:"secretName"`
WorkspaceId string `json:"workspaceId"`
Environment string `json:"environment"`
Type string `json:"type"`
SecretPath string `json:"secretPath"`
}
type UpdateSecretByNameV3Request struct {
SecretName string `json:"secretName"`
WorkspaceID string `json:"workspaceId"`
Environment string `json:"environment"`
Type string `json:"type"`
SecretPath string `json:"secretPath"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
}
type GetSingleSecretByNameV3Request struct {
SecretName string `json:"secretName"`
WorkspaceId string `json:"workspaceId"`
Environment string `json:"environment"`
Type string `json:"type"`
SecretPath string `json:"secretPath"`
}
type GetSingleSecretByNameSecretResponse struct {
Secrets []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
Algorithm string `json:"algorithm"`
KeyEncoding string `json:"keyEncoding"`
Folder string `json:"folder"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"secrets"`
}

View File

@@ -82,7 +82,12 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath})
if err != nil {
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
@@ -184,6 +189,7 @@ func init() {
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ")
runCmd.Flags().String("path", "/", "get secrets within a folder path")
}
// Will execute a single command and pass in the given secrets into the process

View File

@@ -44,6 +44,11 @@ var secretsCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
if err != nil {
util.HandleError(err)
@@ -54,7 +59,7 @@ var secretsCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath})
if err != nil {
util.HandleError(err)
}
@@ -103,6 +108,11 @@ var secretsSetCmd = &cobra.Command{
}
}
secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil {
util.HandleError(err, "Unable to get your local config details")
@@ -140,7 +150,7 @@ var secretsSetCmd = &cobra.Command{
plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
// pull current secrets
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName})
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath})
if err != nil {
util.HandleError(err, "unable to retrieve secrets")
}
@@ -191,6 +201,8 @@ var secretsSetCmd = &cobra.Command{
SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce),
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
SecretValueHash: hashedValue,
PlainTextKey: key,
Type: existingSecret.Type,
}
// Only add to modifications if the value is different
@@ -222,6 +234,7 @@ var secretsSetCmd = &cobra.Command{
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
SecretValueHash: hashedValue,
Type: util.SECRET_TYPE_SHARED,
PlainTextKey: key,
}
secretsToCreate = append(secretsToCreate, encryptedSecretDetails)
secretOperations = append(secretOperations, SecretSetOperation{
@@ -232,30 +245,43 @@ var secretsSetCmd = &cobra.Command{
}
}
if len(secretsToCreate) > 0 {
batchCreateRequest := api.BatchCreateSecretsByWorkspaceAndEnvRequest{
WorkspaceId: workspaceFile.WorkspaceId,
Environment: environmentName,
Secrets: secretsToCreate,
for _, secret := range secretsToCreate {
createSecretRequest := api.CreateSecretV3Request{
WorkspaceID: workspaceFile.WorkspaceId,
Environment: environmentName,
SecretName: secret.PlainTextKey,
SecretKeyCiphertext: secret.SecretKeyCiphertext,
SecretKeyIV: secret.SecretKeyIV,
SecretKeyTag: secret.SecretKeyTag,
SecretValueCiphertext: secret.SecretValueCiphertext,
SecretValueIV: secret.SecretValueIV,
SecretValueTag: secret.SecretValueTag,
Type: secret.Type,
SecretPath: secretsPath,
}
err = api.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest)
err = api.CallCreateSecretsV3(httpClient, createSecretRequest)
if err != nil {
util.HandleError(err, "Unable to process new secret creations")
return
}
}
if len(secretsToModify) > 0 {
batchModifyRequest := api.BatchModifySecretsByWorkspaceAndEnvRequest{
WorkspaceId: workspaceFile.WorkspaceId,
Environment: environmentName,
Secrets: secretsToModify,
for _, secret := range secretsToModify {
updateSecretRequest := api.UpdateSecretByNameV3Request{
WorkspaceID: workspaceFile.WorkspaceId,
Environment: environmentName,
SecretName: secret.PlainTextKey,
SecretValueCiphertext: secret.SecretValueCiphertext,
SecretValueIV: secret.SecretValueIV,
SecretValueTag: secret.SecretValueTag,
Type: secret.Type,
SecretPath: secretsPath,
}
err = api.CallBatchModifySecretsByWorkspaceAndEnv(httpClient, batchModifyRequest)
err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest)
if err != nil {
util.HandleError(err, "Unable to process the modifications to your secrets")
util.HandleError(err, "Unable to process secret update request")
return
}
}
@@ -288,6 +314,16 @@ var secretsDeleteCmd = &cobra.Command{
}
}
secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secretType, err := cmd.Flags().GetString("type")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
if err != nil {
util.HandleError(err, "Unable to authenticate")
@@ -298,46 +334,28 @@ var secretsDeleteCmd = &cobra.Command{
util.HandleError(err, "Unable to get local project details")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName})
if err != nil {
util.HandleError(err, "Unable to fetch secrets")
}
secretByKey := getSecretsByKeys(secrets)
validSecretIdsToDelete := []string{}
invalidSecretNamesThatDoNotExist := []string{}
for _, secretKeyFromArg := range args {
if value, ok := secretByKey[strings.ToUpper(secretKeyFromArg)]; ok {
validSecretIdsToDelete = append(validSecretIdsToDelete, value.ID)
} else {
invalidSecretNamesThatDoNotExist = append(invalidSecretNamesThatDoNotExist, secretKeyFromArg)
for _, secretName := range args {
request := api.DeleteSecretV3Request{
WorkspaceId: workspaceFile.WorkspaceId,
Environment: environmentName,
SecretName: secretName,
Type: secretType,
SecretPath: secretsPath,
}
}
if len(invalidSecretNamesThatDoNotExist) != 0 {
message := fmt.Sprintf("secret name(s) [%v] does not exist in your project. To see which secrets exist run [infisical secrets]", strings.Join(invalidSecretNamesThatDoNotExist, ", "))
util.PrintErrorMessageAndExit(message)
}
httpClient := resty.New().
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
SetHeader("Accept", "application/json")
request := api.BatchDeleteSecretsBySecretIdsRequest{
WorkspaceId: workspaceFile.WorkspaceId,
EnvironmentName: environmentName,
SecretIds: validSecretIdsToDelete,
}
httpClient := resty.New().
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
SetHeader("Accept", "application/json")
err = api.CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient, request)
if err != nil {
util.HandleError(err, "Unable to complete your batch delete request")
err = api.CallDeleteSecretsV3(httpClient, request)
if err != nil {
util.HandleError(err, "Unable to complete your delete request")
}
}
fmt.Printf("secret name(s) [%v] have been deleted from your project \n", strings.Join(args, ", "))
Telemetry.CaptureEvent("cli-command:secrets delete", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION))
Telemetry.CaptureEvent("cli-command:secrets delete", posthog.NewProperties().Set("secretCount", len(args)).Set("version", util.CLI_VERSION))
},
}
@@ -611,11 +629,15 @@ func init() {
secretsCmd.AddCommand(secretsGetCmd)
secretsCmd.AddCommand(secretsSetCmd)
secretsSetCmd.Flags().String("path", "/", "get secrets within a folder path")
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
}
secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)")
secretsDeleteCmd.Flags().String("path", "/", "get secrets within a folder path")
secretsCmd.AddCommand(secretsDeleteCmd)
secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin()
@@ -626,5 +648,6 @@ func init() {
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
secretsCmd.Flags().String("path", "/", "get secrets within a folder path")
rootCmd.AddCommand(secretsCmd)
}

View File

@@ -64,4 +64,5 @@ type GetAllSecretsParameters struct {
InfisicalToken string
TagSlugs string
WorkspaceId string
SecretsPath string
}

View File

@@ -34,7 +34,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
}
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace,
Environment: serviceTokenDetails.Environment,
})
@@ -61,7 +61,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
return plainTextSecrets, serviceTokenDetails, nil
}
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string) ([]models.SingleEnvironmentVariable, error) {
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string) ([]models.SingleEnvironmentVariable, error) {
httpClient := resty.New()
httpClient.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json")
@@ -102,11 +102,17 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
getSecretsRequest := api.GetEncryptedSecretsV3Request{
WorkspaceId: workspaceId,
Environment: environmentName,
TagSlugs: tagSlugs,
})
// TagSlugs: tagSlugs,
}
if secretsPath != "" {
getSecretsRequest.SecretPath = secretsPath
}
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest)
if err != nil {
return nil, err
@@ -162,7 +168,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err)
}
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs)
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs, params.SecretsPath)
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
@@ -333,7 +339,7 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
return secretsToReturn
}
func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2Response) ([]models.SingleEnvironmentVariable, error) {
func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV3Response) ([]models.SingleEnvironmentVariable, error) {
plainTextSecrets := []models.SingleEnvironmentVariable{}
for _, secret := range encryptedSecrets.Secrets {
// Decrypt key

View File

@@ -1 +1,7 @@
export { useCreateFolder, useDeleteFolder, useGetProjectFolders, useUpdateFolder } from './queries';
export {
useCreateFolder,
useDeleteFolder,
useGetProjectFolders,
useGetProjectFoldersBatch,
useUpdateFolder
} from './queries';

View File

@@ -1,5 +1,5 @@
import { useCallback } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
@@ -7,6 +7,7 @@ import { secretSnapshotKeys } from '../secretSnapshots/queries';
import {
CreateFolderDTO,
DeleteFolderDTO,
GetProjectFoldersBatchDTO,
GetProjectFoldersDTO,
TSecretFolder,
UpdateFolderDTO
@@ -17,6 +18,26 @@ const queryKeys = {
['secret-folders', { workspaceId, environment, parentFolderId }] as const
};
const fetchProjectFolders = async (
workspaceId: string,
environment: string,
parentFolderId?: string,
parentFolderPath?: string
) => {
const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>(
'/api/v1/folders',
{
params: {
workspaceId,
environment,
parentFolderId,
parentFolderPath
}
}
);
return data;
};
export const useGetProjectFolders = ({
workspaceId,
parentFolderId,
@@ -27,19 +48,7 @@ export const useGetProjectFolders = ({
useQuery({
queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderId),
enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused,
queryFn: async () => {
const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>(
'/api/v1/folders',
{
params: {
workspaceId,
environment,
parentFolderId
}
}
);
return data;
},
queryFn: async () => fetchProjectFolders(workspaceId, environment, parentFolderId),
select: useCallback(
({ folders, dir }: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({
dir,
@@ -53,6 +62,25 @@ export const useGetProjectFolders = ({
)
});
export const useGetProjectFoldersBatch = ({
folders = [],
isPaused,
parentFolderPath
}: GetProjectFoldersBatchDTO) =>
useQueries({
queries: folders.map(({ workspaceId, environment, parentFolderId }) => ({
queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderPath),
queryFn: async () =>
fetchProjectFolders(workspaceId, environment, parentFolderId, parentFolderPath),
enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused,
select: (data: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({
environment,
folders: data.folders,
dir: data.dir
})
}))
});
export const useCreateFolder = () => {
const queryClient = useQueryClient();

View File

@@ -11,6 +11,12 @@ export type GetProjectFoldersDTO = {
sortDir?: 'asc' | 'desc';
};
export type GetProjectFoldersBatchDTO = {
folders: Omit<GetProjectFoldersDTO, 'isPaused' | 'sortDir'>[];
isPaused?: boolean;
parentFolderPath?: string;
};
export type CreateFolderDTO = {
workspaceId: string;
environment: string;

View File

@@ -1,4 +1,5 @@
/* eslint-disable no-param-reassign */
import { useCallback } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
@@ -29,14 +30,16 @@ export const secretKeys = {
const fetchProjectEncryptedSecrets = async (
workspaceId: string,
env: string | string[],
folderId?: string
folderId?: string,
secretPath?: string
) => {
if (typeof env === 'string') {
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
params: {
environment: env,
workspaceId,
folderId: folderId || undefined
folderId: folderId || undefined,
secretPath
}
});
return data.secrets;
@@ -52,7 +55,8 @@ const fetchProjectEncryptedSecrets = async (
params: {
environment: envPoint,
workspaceId,
folderId
folderId,
secretPath
}
});
allEnvData = allEnvData.concat(data.secrets);
@@ -77,7 +81,7 @@ export const useGetProjectSecrets = ({
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
select: (data) => {
select: useCallback((data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
@@ -146,21 +150,24 @@ export const useGetProjectSecrets = ({
}
});
return { secrets: sharedSecrets };
}
}, [decryptFileKey])
});
export const useGetProjectSecretsByKey = ({
workspaceId,
env,
decryptFileKey,
isPaused
isPaused,
folderId,
secretPath
}: GetProjectSecretsDTO) =>
useQuery({
// wait for all values to be available
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
queryKey: secretKeys.getProjectSecret(workspaceId, env),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env),
select: (data) => {
// right now secretpath is passed as folderid as only this is used in overview
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
select: useCallback((data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
@@ -235,7 +242,7 @@ export const useGetProjectSecretsByKey = ({
});
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
}
}, [decryptFileKey])
});
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
@@ -256,7 +263,7 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
enabled: Boolean(dto.secretId && dto.decryptFileKey),
queryKey: secretKeys.getSecretVersion(dto.secretId),
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
select: (data) => {
select: useCallback((data: EncryptedSecretVersion[]) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
const latestKey = dto.decryptFileKey;
const key = decryptAssymmetric({
@@ -278,7 +285,7 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
})
}))
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}
}, [])
});
export const useBatchSecretsOp = () => {

View File

@@ -96,6 +96,7 @@ export type GetProjectSecretsDTO = {
env: string | string[];
decryptFileKey: UserWsKeyPair;
folderId?: string;
secretPath?: string;
isPaused?: boolean;
onSuccess?: (data: DecryptedSecret[]) => void;
};

View File

@@ -12,13 +12,6 @@ const Dashboard = () => {
const queryEnv = router.query.env as string;
const isOverviewMode = !queryEnv;
const onExploreEnv = (slug: string) => {
router.push({
pathname: router.pathname,
query: { ...router.query, env: slug }
});
};
return (
<>
<Head>
@@ -29,11 +22,7 @@ const Dashboard = () => {
<meta name="og:description" content={String(t('dashboard.og-description'))} />
</Head>
<div className="h-full">
{isOverviewMode ? (
<DashboardEnvOverview onEnvChange={onExploreEnv} />
) : (
<DashboardPage envFromTop={queryEnv} />
)}
{isOverviewMode ? <DashboardEnvOverview /> : <DashboardPage envFromTop={queryEnv} />}
</div>
</>
);

View File

@@ -4,20 +4,20 @@
@layer utilities {
.flex-0 {
flex:0;
flex: 0;
}
.flex-2 {
flex-grow: 2;
}
.flex-3 {
flex-grow: 3;
flex-grow: 3;
}
}
@layer components {
.secret-table {
@apply bg-mineshaft-800 text-left text-bunker-300 w-full;
@apply w-full bg-mineshaft-800 text-left text-bunker-300;
}
/* padding except for comment column */
@@ -29,13 +29,45 @@
@apply py-1 px-1 pr-2 text-sm;
}
.secret-table th:not(:last-child),.secret-table td:not(:last-child) {
.secret-table th:not(:last-child),
.secret-table td:not(:last-child) {
@apply border-r border-mineshaft-600;
}
.secret-table tr {
@apply border-b border-mineshaft-600;
}
.breadcrumb::after,
.breadcrumb::before {
content: '';
height: 60%;
width: 100%;
z-index: -1;
display: block;
position: absolute;
@apply bg-mineshaft-800;
}
.breadcrumb:hover::before {
@apply bg-mineshaft-600;
}
.breadcrumb:hover::after {
@apply bg-mineshaft-600;
}
.breadcrumb::after {
left: 5px;
bottom: -3px;
transform: skew(-30deg);
}
.breadcrumb::before {
left: 5px;
top: -3px;
transform: skew(30deg);
}
}
@import '@fontsource/inter/400.css';

View File

@@ -1,35 +1,31 @@
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 { faFolderOpen, 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, Input, TableContainer, Tooltip } from '@app/components/v2';
import { useWorkspace } from '@app/context';
import {
useGetProjectFoldersBatch,
useGetProjectSecretsByKey,
useGetUserWsEnvironments,
useGetUserWsKey
} from '@app/hooks/api';
import { WorkspaceEnv } from '@app/hooks/api/types';
import { EnvComparisonRow } from './components/EnvComparisonRow';
import { FormData, schema } from './DashboardPage.utils';
import { FolderComparisonRow } from './components/EnvComparisonRow/FolderComparisonRow';
export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
export const DashboardEnvOverview = () => {
const { t } = useTranslation();
const router = useRouter();
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
const { currentWorkspace, isLoading } = useWorkspace();
const workspaceId = currentWorkspace?._id as string;
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
const [searchFilter, setSearchFilter] = useState('');
const secretPath = router.query?.secretPath as string;
useEffect(() => {
if (!isLoading && !workspaceId && router.isReady) {
@@ -38,14 +34,7 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
}, [isLoading, workspaceId, router.isReady]);
const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({
workspaceId,
onSuccess: (data) => {
// get an env with one of the access available
const env = data.find(({ isReadDenied }) => !isReadDenied);
if (env) {
setSelectedEnv(env);
}
}
workspaceId
});
const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied);
@@ -54,17 +43,32 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
workspaceId,
env: userAvailableEnvs?.map((env) => env.slug) ?? [],
decryptFileKey: latestFileKey!,
isPaused: false
isPaused: false,
secretPath
});
const method = useForm<FormData>({
// why any: well yup inferred ts expects other keys to defined as undefined
defaultValues: secrets as any,
values: secrets as any,
mode: 'onBlur',
resolver: yupResolver(schema)
const folders = useGetProjectFoldersBatch({
folders:
userAvailableEnvs?.map((env) => ({
environment: env.slug,
workspaceId
})) ?? [],
parentFolderPath: secretPath
});
const foldersGroupedByEnv = useMemo(() => {
const res: Record<string, Record<string, boolean>> = {};
folders.forEach(({ data }) => {
data?.folders
?.filter(({ name }) => name.toLowerCase().includes(searchFilter))
?.forEach((folder) => {
if (!res?.[folder.name]) res[folder.name] = {};
res[folder.name][data.environment] = true;
});
});
return res;
}, [folders, userAvailableEnvs, searchFilter]);
const numSecretsMissingPerEnv = useMemo(() => {
// first get all sec in the env then subtract with total to get missing ones
const secPerEnvMissing: Record<string, number> = Object.fromEntries(
@@ -81,7 +85,43 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
return secPerEnvMissing;
}, [secrets, userAvailableEnvs]);
const isReadOnly = selectedEnv?.isWriteDenied;
const onExploreEnv = (slug: string) => {
const query: Record<string, string> = { ...router.query, env: slug };
delete query.secretPath;
// the dir return will have the present directory folder id
// use that when clicking on explore to redirect user to there
const envFolder = folders.find(({ data }) => slug === data?.environment);
const dir = envFolder?.data?.dir?.pop();
if (dir) {
query.folderId = dir.id;
}
router.push({
pathname: router.pathname,
query
});
};
const onFolderClick = (path: string) => {
router.push({
pathname: router.pathname,
query: {
...router.query,
secretPath: `${router.query?.secretPath || ''}/${path}`
}
});
};
const onFolderCrumbClick = (index: number) => {
const newSecPath = secretPath.split('/').filter(Boolean).slice(0, index).join('/');
const query = { ...router.query, secretPath: `/${newSecPath}` } as Record<string, string>;
// root condition
if (index === 0) delete query.secretPath;
router.push({
pathname: router.pathname,
query
});
};
if (isSecretsLoading || isEnvListLoading) {
return (
@@ -91,165 +131,195 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
);
}
const filteredSecrets = Object.keys(secrets?.secrets || {})?.filter((secret: any) =>
secret.toUpperCase().includes(searchFilter.toUpperCase())
);
// when secrets is not loading and secrets list is empty
const isDashboardSecretEmpty = !isSecretsLoading && !Object.keys(secrets?.secrets || {})?.filter((secret: any) => secret.toUpperCase().includes(searchFilter.toUpperCase()))?.length;
const isDashboardSecretEmpty = !isSecretsLoading && !filteredSecrets?.length;
const isFoldersEmtpy =
!folders.some(({ isLoading: isFolderLoading }) => isFolderLoading) &&
!Object.keys(foldersGroupedByEnv).length;
const isDashboardEmpty = isFoldersEmtpy && isDashboardSecretEmpty;
return (
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<FormProvider {...method}>
<form autoComplete="off">
{/* breadcrumb row */}
<div className="relative right-5">
<NavHeader pageName={t('dashboard.title')} isProjectRelated />
<div className="relative right-5">
<NavHeader pageName={t('dashboard.title')} isProjectRelated />
</div>
<div className="mt-6">
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
<p className="text-md text-bunker-300">
Inject your secrets using
<a
className="mx-1 text-primary/80 hover:text-primary"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical CLI
</a>
or
<a
className="mx-1 text-primary/80 hover:text-primary"
href="https://infisical.com/docs/sdks/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical SDKs
</a>
</p>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="flex items-center space-x-2">
<div
className="breadcrumb relative z-20 border-solid border-mineshaft-600 bg-mineshaft-800 hover:bg-mineshaft-600 py-1 pl-5 pr-2 text-sm"
onClick={() => onFolderCrumbClick(0)}
onKeyDown={() => null}
role="button"
tabIndex={0}
>
<FontAwesomeIcon icon={faFolderOpen} className="text-primary" />
</div>
<div className="mt-6">
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
<p className="text-md text-bunker-300">
Inject your secrets using
<a
className="mx-1 text-primary/80 hover:text-primary"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noopener noreferrer"
{(secretPath || '')
.split('/')
.filter(Boolean)
.map((path, index, arr) => (
<div
key={`secret-path-${index + 1}`}
className={`breadcrumb relative z-20 ${
index + 1 === arr.length ? 'cursor-default' : 'cursor-pointer'
} border-solid border-mineshaft-600 py-1 pl-5 pr-2 text-sm text-mineshaft-200`}
onClick={() => onFolderCrumbClick(index + 1)}
onKeyDown={() => null}
role="button"
tabIndex={0}
>
Infisical CLI
</a>
or
<a
className="mx-1 text-primary/80 hover:text-primary"
href="https://infisical.com/docs/sdks/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical SDKs
</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">
<div className="w-10 text-center text-xs text-transparent">{0}</div>
{path}
</div>
<div className="sticky top-0 border-none">
<div className="relative flex h-full w-full min-w-[200px] items-center justify-start lg:min-w-[220px] xl:min-w-[250px]">
<div className="text-sm font-medium ">Secret</div>
</div>
</div>
{numSecretsMissingPerEnv &&
userAvailableEnvs?.map((env) => {
return (
<div
key={`header-${env.slug}`}
className="flex w-full min-w-[11rem] flex-row items-center rounded-md border-none bg-mineshaft-800"
>
<div className="flex w-full flex-row justify-center text-center text-sm font-medium text-bunker-200/[.99]">
{env.name}
{numSecretsMissingPerEnv[env.slug] > 0 && (
<div className="mt-0.5 ml-2.5 flex h-[1.1rem] w-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red text-xs text-bunker-100">
<Tooltip
content={`${
numSecretsMissingPerEnv[env.slug]
} secrets missing compared to other environments`}
>
<span className="text-bunker-100">
{numSecretsMissingPerEnv[env.slug]}
</span>
</Tooltip>
</div>
)}
))}
</div>
<div className="w-80">
<Input
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
placeholder="Search by secret/folder name..."
value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
/>
</div>
</div>
<div className="overflow-y-auto">
<div className="sticky top-0 mt-3 flex h-10 min-w-[60.3rem] flex-row rounded-md border border-mineshaft-600 bg-mineshaft-800">
<div className="sticky top-0 flex w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-transparent">{0}</div>
</div>
<div className="sticky top-0 border-none">
<div className="relative flex h-full w-full min-w-[200px] items-center justify-start lg:min-w-[220px] xl:min-w-[250px]">
<div className="text-sm font-medium ">Secret</div>
</div>
</div>
{numSecretsMissingPerEnv &&
userAvailableEnvs?.map((env) => {
return (
<div
key={`header-${env.slug}`}
className="flex w-full min-w-[11rem] flex-row items-center rounded-md border-none bg-mineshaft-800"
>
<div className="flex w-full flex-row justify-center text-center text-sm font-medium text-bunker-200/[.99]">
{env.name}
{numSecretsMissingPerEnv[env.slug] > 0 && (
<div className="mt-0.5 ml-2.5 flex h-[1.1rem] w-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red text-xs text-bunker-100">
<Tooltip
content={`${
numSecretsMissingPerEnv[env.slug]
} secrets missing compared to other environments`}
>
<span className="text-bunker-100">
{numSecretsMissingPerEnv[env.slug]}
</span>
</Tooltip>
</div>
</div>
);
})}
</div>
<div
className={`${
isDashboardSecretEmpty ? '' : ''
} no-scrollbar::-webkit-scrollbar mt-3 flex h-full max-h-[calc(100vh-370px)] w-full min-w-[60.3rem] flex-grow flex-row items-start justify-center overflow-x-hidden rounded-md border border-mineshaft-600 no-scrollbar`}
>
{!isDashboardSecretEmpty && (
<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 || {})?.filter((secret: any) => secret.toUpperCase().includes(searchFilter.toUpperCase())).map((key, index) => (
<EnvComparisonRow
key={`row-${key}`}
secrets={secrets?.secrets?.[key]}
isReadOnly={isReadOnly}
index={index}
isSecretValueHidden
userAvailableEnvs={userAvailableEnvs}
/>
))}
</tbody>
</table>
</TableContainer>
)}
{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">
<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>
)}
{/* In future, we should add an option to add environments here
<div className="flex items-start justify-center h-full ml-10">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus}/>}
onClick={() => prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false })}
variant="outline_bg"
colorSchema="primary"
isFullWidth
className="h-10"
>
Add Environment
</Button>
</div> */}
</div>
<div className="group mt-4 flex min-w-[60.3rem] flex-row items-center">
<div className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-transparent">0</div>
);
})}
</div>
<div
className={`${
isDashboardEmpty ? '' : ''
} no-scrollbar::-webkit-scrollbar mt-3 flex h-full max-h-[calc(100vh-370px)] w-full min-w-[60.3rem] flex-grow flex-row items-start justify-center overflow-x-hidden rounded-md border border-mineshaft-600 no-scrollbar`}
>
{!isDashboardEmpty && (
<TableContainer className="border-none">
<table className="secret-table relative w-full bg-mineshaft-900">
<tbody className="max-h-screen overflow-y-auto">
{Object.keys(foldersGroupedByEnv || {}).map((folderName, index) => (
<FolderComparisonRow
key={`${folderName}-${index + 1}`}
folderName={folderName}
userAvailableEnvs={userAvailableEnvs}
folderInEnv={foldersGroupedByEnv[folderName]}
onClick={onFolderClick}
/>
))}
{Object.keys(secrets?.secrets || {})
?.filter((secret: any) =>
secret.toUpperCase().includes(searchFilter.toUpperCase())
)
.map((key) => (
<EnvComparisonRow
key={`row-${key}`}
secrets={secrets?.secrets?.[key]}
isReadOnly
isSecretValueHidden
userAvailableEnvs={userAvailableEnvs}
/>
))}
</tbody>
</table>
</TableContainer>
)}
{isDashboardEmpty && (
<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">
<FontAwesomeIcon icon={faKey} className="mb-4 text-4xl" />
<span className="mb-1">No secrets/folders found.</span>
<span>To add more secrets you can explore any environment.</span>
</div>
<div className="flex min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
<span className="text-transparent">0</span>
<button type="button" className="mr-2 text-transparent">
1
</button>
</div>
{userAvailableEnvs?.map((env) => {
return (
<div
key={`button-${env.slug}`}
className="mx-2 mb-1 flex h-10 w-full min-w-[11rem] flex-row items-center justify-center border-none"
>
<Button
onClick={() => onEnvChange(env.slug)}
// router.push(`${router.asPath }?env=${env.slug}`)
variant="outline_bg"
colorSchema="primary"
isFullWidth
className="h-10"
>
Explore {env.name}
</Button>
</div>
);
})}
</div>
)}
</div>
<div className="group mt-4 flex min-w-[60.3rem] flex-row items-center">
<div className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-transparent">0</div>
</div>
</form>
</FormProvider>
<div className="flex min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
<span className="text-transparent">0</span>
<button type="button" className="mr-2 text-transparent">
1
</button>
</div>
{userAvailableEnvs?.map((env) => {
return (
<div
key={`button-${env.slug}`}
className="mx-2 mb-1 flex h-10 w-full min-w-[11rem] flex-row items-center justify-center border-none"
>
<Button
onClick={() => onExploreEnv(env.slug)}
variant="outline_bg"
colorSchema="primary"
isFullWidth
className="h-10"
>
Explore {env.name}
</Button>
</div>
);
})}
</div>
</div>
</div>
);
};

View File

@@ -1,11 +1,10 @@
/* eslint-disable react/jsx-no-useless-fragment */
import { useCallback, useState } from 'react';
import { faCircle, faEye, faEyeSlash, faMinus } from '@fortawesome/free-solid-svg-icons';
import { faCircle, faEye, faEyeSlash, faKey, faMinus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { twMerge } from 'tailwind-merge';
type Props = {
index: number;
secrets: any[] | undefined;
// permission and external state's that decided to hide or show
isReadOnly?: boolean;
@@ -30,7 +29,7 @@ const DashboardInput = ({
if (val === undefined)
return (
<span className="cursor-default font-sans text-xs italic text-red-500/80">
<FontAwesomeIcon icon={faMinus} className="mt-1" />
<FontAwesomeIcon icon={faMinus} className="mt-1" />
</span>
);
if (val?.length === 0)
@@ -110,7 +109,6 @@ const DashboardInput = ({
};
export const EnvComparisonRow = ({
index,
secrets,
isSecretValueHidden,
isReadOnly,
@@ -126,7 +124,9 @@ export const EnvComparisonRow = ({
return (
<tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800">
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
<div className="w-10 text-center text-xs text-bunker-400">
<FontAwesomeIcon icon={faKey} />
</div>
</td>
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
<div className="flex h-8 cursor-default flex-row items-center truncate">

View File

@@ -0,0 +1,42 @@
import { faCheck, faFolder, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
type Props = {
folderInEnv: Record<string, boolean>;
userAvailableEnvs?: Array<{ slug: string; name: string }>;
folderName: string;
onClick: (folderName: string) => void;
};
export const FolderComparisonRow = ({
folderInEnv = {},
userAvailableEnvs = [],
folderName,
onClick
}: Props) => (
<tr
className="group flex min-w-full cursor-pointer flex-row items-center hover:bg-mineshaft-800"
onClick={() => onClick(folderName)}
>
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-bunker-400">
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
</div>
</td>
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[200px] xl:min-w-[250px]">
<div className="flex h-8 flex-row items-center truncate">{folderName}</div>
</td>
{userAvailableEnvs?.map(({ slug }) => (
<td
className={`flex h-10 w-full cursor-default flex-row items-center justify-center ${
folderInEnv[slug]
? 'bg-mineshaft-900/30 text-green-500/80'
: 'bg-red-800/10 text-red-500/80'
}`}
key={`${folderName}-${slug}`}
>
<FontAwesomeIcon icon={folderInEnv[slug] ? faCheck : faXmark} />
</td>
))}
</tr>
);