mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'main' of https://github.com/Infisical/infisical
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
|||||||
validateFolderName,
|
validateFolderName,
|
||||||
generateFolderId,
|
generateFolderId,
|
||||||
getParentFromFolderId,
|
getParentFromFolderId,
|
||||||
|
getFolderByPath,
|
||||||
} from "../../services/FolderService";
|
} from "../../services/FolderService";
|
||||||
import { ADMIN, MEMBER } from "../../variables";
|
import { ADMIN, MEMBER } from "../../variables";
|
||||||
import { validateMembership } from "../../helpers/membership";
|
import { validateMembership } from "../../helpers/membership";
|
||||||
@@ -177,11 +178,13 @@ export const deleteFolder = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
// TODO: validate workspace
|
// TODO: validate workspace
|
||||||
export const getFolders = async (req: Request, res: Response) => {
|
export const getFolders = async (req: Request, res: Response) => {
|
||||||
const { workspaceId, environment, parentFolderId } = req.query as {
|
const { workspaceId, environment, parentFolderId, parentFolderPath } =
|
||||||
workspaceId: string;
|
req.query as {
|
||||||
environment: string;
|
workspaceId: string;
|
||||||
parentFolderId?: string;
|
environment: string;
|
||||||
};
|
parentFolderId?: string;
|
||||||
|
parentFolderPath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||||
if (!folders) {
|
if (!folders) {
|
||||||
@@ -196,6 +199,20 @@ export const getFolders = async (req: Request, res: Response) => {
|
|||||||
acceptedRoles: [ADMIN, MEMBER],
|
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) {
|
if (!parentFolderId) {
|
||||||
const rootFolders = folders.nodes.children.map(({ id, name }) => ({
|
const rootFolders = folders.nodes.children.map(({ id, name }) => ({
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -700,11 +700,15 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
(!folders && folderId && folderId !== "root") ||
|
(!folders && folderId && folderId !== "root") ||
|
||||||
(!folders && secretPath)
|
(!folders && secretPath)
|
||||||
) {
|
) {
|
||||||
throw BadRequestError({ message: "Folder not found" });
|
res.send({ secrets: [] });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (folders && folderId !== "root") {
|
if (folders && folderId !== "root") {
|
||||||
const folder = searchByFolderId(folders.nodes, folderId as string);
|
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) {
|
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||||
@@ -720,10 +724,11 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (folders && secretPath) {
|
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);
|
const folder = getFolderByPath(folders.nodes, secretPath as string);
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
throw BadRequestError({ message: "Secret path not found" });
|
res.send({ secrets: [] });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
folderId = folder.id;
|
folderId = folder.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
|
|||||||
secret,
|
secret,
|
||||||
key
|
key
|
||||||
});
|
});
|
||||||
|
|
||||||
return rep;
|
return rep;
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -88,7 +88,7 @@ export const createSecretRaw = async (req: Request, res: Response) => {
|
|||||||
secretComment,
|
secretComment,
|
||||||
secretPath = "/"
|
secretPath = "/"
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const key = await BotService.getWorkspaceKeyWithBot({
|
const key = await BotService.getWorkspaceKeyWithBot({
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
});
|
});
|
||||||
@@ -102,12 +102,12 @@ export const createSecretRaw = async (req: Request, res: Response) => {
|
|||||||
plaintext: secretValue,
|
plaintext: secretValue,
|
||||||
key
|
key
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||||
plaintext: secretComment,
|
plaintext: secretComment,
|
||||||
key
|
key
|
||||||
});
|
});
|
||||||
|
|
||||||
const secret = await SecretService.createSecret({
|
const secret = await SecretService.createSecret({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
@@ -135,7 +135,7 @@ export const createSecretRaw = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const secretWithoutBlindIndex = secret.toObject();
|
const secretWithoutBlindIndex = secret.toObject();
|
||||||
delete secretWithoutBlindIndex.secretBlindIndex;
|
delete secretWithoutBlindIndex.secretBlindIndex;
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secret: repackageSecretToRaw({
|
secret: repackageSecretToRaw({
|
||||||
secret: secretWithoutBlindIndex,
|
secret: secretWithoutBlindIndex,
|
||||||
@@ -202,11 +202,11 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
|
export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
|
||||||
const { secretName } = req.params;
|
const { secretName } = req.params;
|
||||||
const {
|
const {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
type,
|
type,
|
||||||
secretPath = "/"
|
secretPath = "/"
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const { secret } = await SecretService.deleteSecret({
|
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) => {
|
export const deleteSecretByName = async (req: Request, res: Response) => {
|
||||||
const { secretName } = req.params;
|
const { secretName } = req.params;
|
||||||
const {
|
const {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
type,
|
type,
|
||||||
secretPath = "/"
|
secretPath = "/"
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const { secret } = await SecretService.deleteSecret({
|
const { secret } = await SecretService.deleteSecret({
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
const MongoStore = require('rate-limit-mongo');
|
// const MongoStore = require('rate-limit-mongo');
|
||||||
|
|
||||||
// 200 per minute
|
// 200 per minute
|
||||||
export const apiLimiter = rateLimit({
|
export const apiLimiter = rateLimit({
|
||||||
store: new MongoStore({
|
// store: new MongoStore({
|
||||||
uri: process.env.MONGO_URL,
|
// uri: process.env.MONGO_URL,
|
||||||
expireTimeMs: 1000 * 60,
|
// expireTimeMs: 1000 * 60,
|
||||||
collectionName: "expressRateRecords-apiLimiter",
|
// collectionName: "expressRateRecords-apiLimiter",
|
||||||
errorHandler: console.error.bind(null, 'rate-limit-mongo')
|
// errorHandler: console.error.bind(null, 'rate-limit-mongo')
|
||||||
}),
|
// }),
|
||||||
windowMs: 1000 * 60,
|
windowMs: 60 * 1000,
|
||||||
max: 200,
|
max: 240,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
skip: (request) => {
|
skip: (request) => {
|
||||||
@@ -23,14 +23,14 @@ export const apiLimiter = rateLimit({
|
|||||||
|
|
||||||
// 50 requests per 1 hours
|
// 50 requests per 1 hours
|
||||||
const authLimit = rateLimit({
|
const authLimit = rateLimit({
|
||||||
store: new MongoStore({
|
// store: new MongoStore({
|
||||||
uri: process.env.MONGO_URL,
|
// uri: process.env.MONGO_URL,
|
||||||
expireTimeMs: 1000 * 60 * 60,
|
// expireTimeMs: 1000 * 60 * 60,
|
||||||
errorHandler: console.error.bind(null, 'rate-limit-mongo'),
|
// errorHandler: console.error.bind(null, 'rate-limit-mongo'),
|
||||||
collectionName: "expressRateRecords-authLimit",
|
// collectionName: "expressRateRecords-authLimit",
|
||||||
}),
|
// }),
|
||||||
windowMs: 1000 * 60 * 60,
|
windowMs: 60 * 1000,
|
||||||
max: 50,
|
max: 10,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
keyGenerator: (req, res) => {
|
keyGenerator: (req, res) => {
|
||||||
@@ -40,14 +40,14 @@ const authLimit = rateLimit({
|
|||||||
|
|
||||||
// 5 requests per 1 hour
|
// 5 requests per 1 hour
|
||||||
export const passwordLimiter = rateLimit({
|
export const passwordLimiter = rateLimit({
|
||||||
store: new MongoStore({
|
// store: new MongoStore({
|
||||||
uri: process.env.MONGO_URL,
|
// uri: process.env.MONGO_URL,
|
||||||
expireTimeMs: 1000 * 60 * 60,
|
// expireTimeMs: 1000 * 60 * 60,
|
||||||
errorHandler: console.error.bind(null, 'rate-limit-mongo'),
|
// errorHandler: console.error.bind(null, 'rate-limit-mongo'),
|
||||||
collectionName: "expressRateRecords-passwordLimiter",
|
// collectionName: "expressRateRecords-passwordLimiter",
|
||||||
}),
|
// }),
|
||||||
windowMs: 1000 * 60 * 60,
|
windowMs: 60 * 60 * 1000,
|
||||||
max: 5,
|
max: 10,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
keyGenerator: (req, res) => {
|
keyGenerator: (req, res) => {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ import { getFolderIdFromServiceToken } from "../services/FolderService";
|
|||||||
export const repackageSecretToRaw = ({
|
export const repackageSecretToRaw = ({
|
||||||
secret,
|
secret,
|
||||||
key
|
key
|
||||||
}:{
|
}: {
|
||||||
secret: ISecret;
|
secret: ISecret;
|
||||||
key: string;
|
key: string;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -76,8 +76,8 @@ export const repackageSecretToRaw = ({
|
|||||||
key
|
key
|
||||||
});
|
});
|
||||||
|
|
||||||
let secretComment: string = '';
|
let secretComment: string = '';
|
||||||
|
|
||||||
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
|
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
|
||||||
secretComment = decryptSymmetric128BitHexKeyUTF8({
|
secretComment = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: secret.secretCommentCiphertext,
|
ciphertext: secret.secretCommentCiphertext,
|
||||||
@@ -86,7 +86,7 @@ export const repackageSecretToRaw = ({
|
|||||||
key
|
key
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return ({
|
return ({
|
||||||
_id: secret._id,
|
_id: secret._id,
|
||||||
version: secret.version,
|
version: secret.version,
|
||||||
@@ -503,7 +503,7 @@ export const getSecretsHelper = async ({
|
|||||||
folder: folderId,
|
folder: folderId,
|
||||||
type: SECRET_PERSONAL,
|
type: SECRET_PERSONAL,
|
||||||
...getAuthDataPayloadUserObj(authData),
|
...getAuthDataPayloadUserObj(authData),
|
||||||
}).lean();
|
}).populate("tags").lean();
|
||||||
|
|
||||||
// concat with shared secrets
|
// concat with shared secrets
|
||||||
secrets = secrets.concat(
|
secrets = secrets.concat(
|
||||||
@@ -515,7 +515,7 @@ export const getSecretsHelper = async ({
|
|||||||
secretBlindIndex: {
|
secretBlindIndex: {
|
||||||
$nin: secrets.map((secret) => secret.secretBlindIndex),
|
$nin: secrets.map((secret) => secret.secretBlindIndex),
|
||||||
},
|
},
|
||||||
}).lean()
|
}).populate("tags").lean()
|
||||||
);
|
);
|
||||||
|
|
||||||
// (EE) create (audit) log
|
// (EE) create (audit) log
|
||||||
@@ -553,7 +553,7 @@ export const getSecretsHelper = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return secrets;
|
return secrets;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -652,7 +652,7 @@ export const getSecretHelper = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return secret;
|
return secret;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -843,7 +843,7 @@ export const deleteSecretHelper = async ({
|
|||||||
// if using service token filter towards the folderId by secretpath
|
// if using service token filter towards the folderId by secretpath
|
||||||
if (authData.authPayload instanceof ServiceTokenData) {
|
if (authData.authPayload instanceof ServiceTokenData) {
|
||||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
||||||
|
|
||||||
if (secretPath !== serviceTkScopedSecretPath) {
|
if (secretPath !== serviceTkScopedSecretPath) {
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||||
}
|
}
|
||||||
@@ -909,12 +909,12 @@ export const deleteSecretHelper = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
action && (await EELogService.createLog({
|
action && (await EELogService.createLog({
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actions: [action],
|
actions: [action],
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
ipAddress: authData.authIP,
|
ipAddress: authData.authIP,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
@@ -941,7 +941,7 @@ export const deleteSecretHelper = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return ({
|
return ({
|
||||||
secrets,
|
secrets,
|
||||||
secret
|
secret
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ router.get(
|
|||||||
query("workspaceId").exists().isString().trim(),
|
query("workspaceId").exists().isString().trim(),
|
||||||
query("environment").exists().isString().trim(),
|
query("environment").exists().isString().trim(),
|
||||||
query("parentFolderId").optional().isString().trim(),
|
query("parentFolderId").optional().isString().trim(),
|
||||||
|
query("parentFolderPath").optional().isString().trim(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
getFolders
|
getFolders
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,63 +11,6 @@ import (
|
|||||||
|
|
||||||
const USER_AGENT = "cli"
|
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) {
|
func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncryptedWorkspaceKeyRequest) (GetEncryptedWorkspaceKeyResponse, error) {
|
||||||
endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", config.INFISICAL_URL, request.WorkspaceId)
|
endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", config.INFISICAL_URL, request.WorkspaceId)
|
||||||
var result GetEncryptedWorkspaceKeyResponse
|
var result GetEncryptedWorkspaceKeyResponse
|
||||||
@@ -107,28 +50,6 @@ func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDeta
|
|||||||
return tokenDetailsResponse, nil
|
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) {
|
func CallLogin1V2(httpClient *resty.Client, request GetLoginOneV2Request) (GetLoginOneV2Response, error) {
|
||||||
var loginOneV2Response GetLoginOneV2Response
|
var loginOneV2Response GetLoginOneV2Response
|
||||||
response, err := httpClient.
|
response, err := httpClient.
|
||||||
@@ -303,3 +224,110 @@ func CallGetNewAccessTokenWithRefreshToken(httpClient *resty.Client, refreshToke
|
|||||||
|
|
||||||
return newAccessToken, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -143,24 +143,7 @@ type Secret struct {
|
|||||||
SecretCommentHash string `json:"secretCommentHash,omitempty"`
|
SecretCommentHash string `json:"secretCommentHash,omitempty"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
}
|
PlainTextKey string `json:"plainTextKey"`
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetEncryptedWorkspaceKeyRequest struct {
|
type GetEncryptedWorkspaceKeyRequest struct {
|
||||||
@@ -194,41 +177,6 @@ type GetSecretsByWorkspaceIdAndEnvironmentRequest struct {
|
|||||||
WorkspaceId string `json:"workspaceId"`
|
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 {
|
type GetServiceTokenDetailsResponse struct {
|
||||||
ID string `json:"_id"`
|
ID string `json:"_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -320,3 +268,109 @@ type VerifyMfaTokenErrorResponse struct {
|
|||||||
type GetNewAccessTokenWithRefreshTokenResponse struct {
|
type GetNewAccessTokenWithRefreshTokenResponse struct {
|
||||||
Token string `json:"token"`
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -82,7 +82,12 @@ var runCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
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 {
|
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")
|
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().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||||
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
|
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
|
||||||
runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ")
|
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
|
// Will execute a single command and pass in the given secrets into the process
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ var secretsCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
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")
|
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
@@ -54,7 +59,7 @@ var secretsCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
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 {
|
if err != nil {
|
||||||
util.HandleError(err)
|
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()
|
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to get your local config details")
|
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)
|
plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
|
||||||
|
|
||||||
// pull current secrets
|
// pull current secrets
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName})
|
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "unable to retrieve secrets")
|
util.HandleError(err, "unable to retrieve secrets")
|
||||||
}
|
}
|
||||||
@@ -191,6 +201,8 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce),
|
SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce),
|
||||||
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
|
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
|
||||||
SecretValueHash: hashedValue,
|
SecretValueHash: hashedValue,
|
||||||
|
PlainTextKey: key,
|
||||||
|
Type: existingSecret.Type,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add to modifications if the value is different
|
// Only add to modifications if the value is different
|
||||||
@@ -222,6 +234,7 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
|
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
|
||||||
SecretValueHash: hashedValue,
|
SecretValueHash: hashedValue,
|
||||||
Type: util.SECRET_TYPE_SHARED,
|
Type: util.SECRET_TYPE_SHARED,
|
||||||
|
PlainTextKey: key,
|
||||||
}
|
}
|
||||||
secretsToCreate = append(secretsToCreate, encryptedSecretDetails)
|
secretsToCreate = append(secretsToCreate, encryptedSecretDetails)
|
||||||
secretOperations = append(secretOperations, SecretSetOperation{
|
secretOperations = append(secretOperations, SecretSetOperation{
|
||||||
@@ -232,30 +245,43 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(secretsToCreate) > 0 {
|
for _, secret := range secretsToCreate {
|
||||||
batchCreateRequest := api.BatchCreateSecretsByWorkspaceAndEnvRequest{
|
createSecretRequest := api.CreateSecretV3Request{
|
||||||
WorkspaceId: workspaceFile.WorkspaceId,
|
WorkspaceID: workspaceFile.WorkspaceId,
|
||||||
Environment: environmentName,
|
Environment: environmentName,
|
||||||
Secrets: secretsToCreate,
|
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 {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to process new secret creations")
|
util.HandleError(err, "Unable to process new secret creations")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(secretsToModify) > 0 {
|
for _, secret := range secretsToModify {
|
||||||
batchModifyRequest := api.BatchModifySecretsByWorkspaceAndEnvRequest{
|
updateSecretRequest := api.UpdateSecretByNameV3Request{
|
||||||
WorkspaceId: workspaceFile.WorkspaceId,
|
WorkspaceID: workspaceFile.WorkspaceId,
|
||||||
Environment: environmentName,
|
Environment: environmentName,
|
||||||
Secrets: secretsToModify,
|
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 {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to process the modifications to your secrets")
|
util.HandleError(err, "Unable to process secret update request")
|
||||||
return
|
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()
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to authenticate")
|
util.HandleError(err, "Unable to authenticate")
|
||||||
@@ -298,46 +334,28 @@ var secretsDeleteCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to get local project details")
|
util.HandleError(err, "Unable to get local project details")
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName})
|
for _, secretName := range args {
|
||||||
if err != nil {
|
request := api.DeleteSecretV3Request{
|
||||||
util.HandleError(err, "Unable to fetch secrets")
|
WorkspaceId: workspaceFile.WorkspaceId,
|
||||||
}
|
Environment: environmentName,
|
||||||
|
SecretName: secretName,
|
||||||
secretByKey := getSecretsByKeys(secrets)
|
Type: secretType,
|
||||||
validSecretIdsToDelete := []string{}
|
SecretPath: secretsPath,
|
||||||
invalidSecretNamesThatDoNotExist := []string{}
|
|
||||||
|
|
||||||
for _, secretKeyFromArg := range args {
|
|
||||||
if value, ok := secretByKey[strings.ToUpper(secretKeyFromArg)]; ok {
|
|
||||||
validSecretIdsToDelete = append(validSecretIdsToDelete, value.ID)
|
|
||||||
} else {
|
|
||||||
invalidSecretNamesThatDoNotExist = append(invalidSecretNamesThatDoNotExist, secretKeyFromArg)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if len(invalidSecretNamesThatDoNotExist) != 0 {
|
httpClient := resty.New().
|
||||||
message := fmt.Sprintf("secret name(s) [%v] does not exist in your project. To see which secrets exist run [infisical secrets]", strings.Join(invalidSecretNamesThatDoNotExist, ", "))
|
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
|
||||||
util.PrintErrorMessageAndExit(message)
|
SetHeader("Accept", "application/json")
|
||||||
}
|
|
||||||
|
|
||||||
request := api.BatchDeleteSecretsBySecretIdsRequest{
|
err = api.CallDeleteSecretsV3(httpClient, request)
|
||||||
WorkspaceId: workspaceFile.WorkspaceId,
|
if err != nil {
|
||||||
EnvironmentName: environmentName,
|
util.HandleError(err, "Unable to complete your delete request")
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("secret name(s) [%v] have been deleted from your project \n", strings.Join(args, ", "))
|
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(secretsGetCmd)
|
||||||
|
|
||||||
secretsCmd.AddCommand(secretsSetCmd)
|
secretsCmd.AddCommand(secretsSetCmd)
|
||||||
|
secretsSetCmd.Flags().String("path", "/", "get secrets within a folder path")
|
||||||
|
|
||||||
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||||
util.RequireLogin()
|
util.RequireLogin()
|
||||||
util.RequireLocalWorkspaceFile()
|
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)
|
secretsCmd.AddCommand(secretsDeleteCmd)
|
||||||
secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||||
util.RequireLogin()
|
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.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.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||||
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
|
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
|
||||||
|
secretsCmd.Flags().String("path", "/", "get secrets within a folder path")
|
||||||
rootCmd.AddCommand(secretsCmd)
|
rootCmd.AddCommand(secretsCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,4 +64,5 @@ type GetAllSecretsParameters struct {
|
|||||||
InfisicalToken string
|
InfisicalToken string
|
||||||
TagSlugs string
|
TagSlugs string
|
||||||
WorkspaceId string
|
WorkspaceId string
|
||||||
|
SecretsPath string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
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,
|
WorkspaceId: serviceTokenDetails.Workspace,
|
||||||
Environment: serviceTokenDetails.Environment,
|
Environment: serviceTokenDetails.Environment,
|
||||||
})
|
})
|
||||||
@@ -61,7 +61,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
|
|||||||
return plainTextSecrets, serviceTokenDetails, nil
|
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 := resty.New()
|
||||||
httpClient.SetAuthToken(JTWToken).
|
httpClient.SetAuthToken(JTWToken).
|
||||||
SetHeader("Accept", "application/json")
|
SetHeader("Accept", "application/json")
|
||||||
@@ -102,11 +102,17 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
|
|||||||
|
|
||||||
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
|
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
|
||||||
|
|
||||||
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
|
getSecretsRequest := api.GetEncryptedSecretsV3Request{
|
||||||
WorkspaceId: workspaceId,
|
WorkspaceId: workspaceId,
|
||||||
Environment: environmentName,
|
Environment: environmentName,
|
||||||
TagSlugs: tagSlugs,
|
// TagSlugs: tagSlugs,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if secretsPath != "" {
|
||||||
|
getSecretsRequest.SecretPath = secretsPath
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
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)
|
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
|
||||||
|
|
||||||
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
|
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
|
||||||
@@ -333,7 +339,7 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
|
|||||||
return secretsToReturn
|
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{}
|
plainTextSecrets := []models.SingleEnvironmentVariable{}
|
||||||
for _, secret := range encryptedSecrets.Secrets {
|
for _, secret := range encryptedSecrets.Secrets {
|
||||||
// Decrypt key
|
// Decrypt key
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
export { useCreateFolder, useDeleteFolder, useGetProjectFolders, useUpdateFolder } from './queries';
|
export {
|
||||||
|
useCreateFolder,
|
||||||
|
useDeleteFolder,
|
||||||
|
useGetProjectFolders,
|
||||||
|
useGetProjectFoldersBatch,
|
||||||
|
useUpdateFolder
|
||||||
|
} from './queries';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback } from 'react';
|
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';
|
import { apiRequest } from '@app/config/request';
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ import { secretSnapshotKeys } from '../secretSnapshots/queries';
|
|||||||
import {
|
import {
|
||||||
CreateFolderDTO,
|
CreateFolderDTO,
|
||||||
DeleteFolderDTO,
|
DeleteFolderDTO,
|
||||||
|
GetProjectFoldersBatchDTO,
|
||||||
GetProjectFoldersDTO,
|
GetProjectFoldersDTO,
|
||||||
TSecretFolder,
|
TSecretFolder,
|
||||||
UpdateFolderDTO
|
UpdateFolderDTO
|
||||||
@@ -17,6 +18,26 @@ const queryKeys = {
|
|||||||
['secret-folders', { workspaceId, environment, parentFolderId }] as const
|
['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 = ({
|
export const useGetProjectFolders = ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
parentFolderId,
|
parentFolderId,
|
||||||
@@ -27,19 +48,7 @@ export const useGetProjectFolders = ({
|
|||||||
useQuery({
|
useQuery({
|
||||||
queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderId),
|
queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderId),
|
||||||
enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused,
|
enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused,
|
||||||
queryFn: async () => {
|
queryFn: async () => fetchProjectFolders(workspaceId, environment, parentFolderId),
|
||||||
const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>(
|
|
||||||
'/api/v1/folders',
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
parentFolderId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
select: useCallback(
|
select: useCallback(
|
||||||
({ folders, dir }: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({
|
({ folders, dir }: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({
|
||||||
dir,
|
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 = () => {
|
export const useCreateFolder = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ export type GetProjectFoldersDTO = {
|
|||||||
sortDir?: 'asc' | 'desc';
|
sortDir?: 'asc' | 'desc';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetProjectFoldersBatchDTO = {
|
||||||
|
folders: Omit<GetProjectFoldersDTO, 'isPaused' | 'sortDir'>[];
|
||||||
|
isPaused?: boolean;
|
||||||
|
parentFolderPath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateFolderDTO = {
|
export type CreateFolderDTO = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
|
import { useCallback } from 'react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -29,14 +30,16 @@ export const secretKeys = {
|
|||||||
const fetchProjectEncryptedSecrets = async (
|
const fetchProjectEncryptedSecrets = async (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
env: string | string[],
|
env: string | string[],
|
||||||
folderId?: string
|
folderId?: string,
|
||||||
|
secretPath?: string
|
||||||
) => {
|
) => {
|
||||||
if (typeof env === 'string') {
|
if (typeof env === 'string') {
|
||||||
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
|
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
|
||||||
params: {
|
params: {
|
||||||
environment: env,
|
environment: env,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
folderId: folderId || undefined
|
folderId: folderId || undefined,
|
||||||
|
secretPath
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return data.secrets;
|
return data.secrets;
|
||||||
@@ -52,7 +55,8 @@ const fetchProjectEncryptedSecrets = async (
|
|||||||
params: {
|
params: {
|
||||||
environment: envPoint,
|
environment: envPoint,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
folderId
|
folderId,
|
||||||
|
secretPath
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
allEnvData = allEnvData.concat(data.secrets);
|
allEnvData = allEnvData.concat(data.secrets);
|
||||||
@@ -77,7 +81,7 @@ export const useGetProjectSecrets = ({
|
|||||||
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
||||||
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
|
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
|
||||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
|
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
|
||||||
select: (data) => {
|
select: useCallback((data: EncryptedSecret[]) => {
|
||||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||||
const latestKey = decryptFileKey;
|
const latestKey = decryptFileKey;
|
||||||
const key = decryptAssymmetric({
|
const key = decryptAssymmetric({
|
||||||
@@ -146,21 +150,24 @@ export const useGetProjectSecrets = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { secrets: sharedSecrets };
|
return { secrets: sharedSecrets };
|
||||||
}
|
}, [decryptFileKey])
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useGetProjectSecretsByKey = ({
|
export const useGetProjectSecretsByKey = ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
env,
|
env,
|
||||||
decryptFileKey,
|
decryptFileKey,
|
||||||
isPaused
|
isPaused,
|
||||||
|
folderId,
|
||||||
|
secretPath
|
||||||
}: GetProjectSecretsDTO) =>
|
}: GetProjectSecretsDTO) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
// wait for all values to be available
|
// wait for all values to be available
|
||||||
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
||||||
queryKey: secretKeys.getProjectSecret(workspaceId, env),
|
// right now secretpath is passed as folderid as only this is used in overview
|
||||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env),
|
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath),
|
||||||
select: (data) => {
|
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
|
||||||
|
select: useCallback((data: EncryptedSecret[]) => {
|
||||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||||
const latestKey = decryptFileKey;
|
const latestKey = decryptFileKey;
|
||||||
const key = decryptAssymmetric({
|
const key = decryptAssymmetric({
|
||||||
@@ -235,7 +242,7 @@ export const useGetProjectSecretsByKey = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
|
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
|
||||||
}
|
}, [decryptFileKey])
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
|
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
|
||||||
@@ -256,7 +263,7 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
|
|||||||
enabled: Boolean(dto.secretId && dto.decryptFileKey),
|
enabled: Boolean(dto.secretId && dto.decryptFileKey),
|
||||||
queryKey: secretKeys.getSecretVersion(dto.secretId),
|
queryKey: secretKeys.getSecretVersion(dto.secretId),
|
||||||
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
|
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
|
||||||
select: (data) => {
|
select: useCallback((data: EncryptedSecretVersion[]) => {
|
||||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||||
const latestKey = dto.decryptFileKey;
|
const latestKey = dto.decryptFileKey;
|
||||||
const key = decryptAssymmetric({
|
const key = decryptAssymmetric({
|
||||||
@@ -278,7 +285,7 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
|
|||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||||
}
|
}, [])
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useBatchSecretsOp = () => {
|
export const useBatchSecretsOp = () => {
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export type GetProjectSecretsDTO = {
|
|||||||
env: string | string[];
|
env: string | string[];
|
||||||
decryptFileKey: UserWsKeyPair;
|
decryptFileKey: UserWsKeyPair;
|
||||||
folderId?: string;
|
folderId?: string;
|
||||||
|
secretPath?: string;
|
||||||
isPaused?: boolean;
|
isPaused?: boolean;
|
||||||
onSuccess?: (data: DecryptedSecret[]) => void;
|
onSuccess?: (data: DecryptedSecret[]) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,13 +12,6 @@ const Dashboard = () => {
|
|||||||
const queryEnv = router.query.env as string;
|
const queryEnv = router.query.env as string;
|
||||||
const isOverviewMode = !queryEnv;
|
const isOverviewMode = !queryEnv;
|
||||||
|
|
||||||
const onExploreEnv = (slug: string) => {
|
|
||||||
router.push({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query: { ...router.query, env: slug }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -29,11 +22,7 @@ const Dashboard = () => {
|
|||||||
<meta name="og:description" content={String(t('dashboard.og-description'))} />
|
<meta name="og:description" content={String(t('dashboard.og-description'))} />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
{isOverviewMode ? (
|
{isOverviewMode ? <DashboardEnvOverview /> : <DashboardPage envFromTop={queryEnv} />}
|
||||||
<DashboardEnvOverview onEnvChange={onExploreEnv} />
|
|
||||||
) : (
|
|
||||||
<DashboardPage envFromTop={queryEnv} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,20 +4,20 @@
|
|||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.flex-0 {
|
.flex-0 {
|
||||||
flex:0;
|
flex: 0;
|
||||||
}
|
}
|
||||||
.flex-2 {
|
.flex-2 {
|
||||||
flex-grow: 2;
|
flex-grow: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-3 {
|
.flex-3 {
|
||||||
flex-grow: 3;
|
flex-grow: 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.secret-table {
|
.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 */
|
/* padding except for comment column */
|
||||||
@@ -29,13 +29,45 @@
|
|||||||
@apply py-1 px-1 pr-2 text-sm;
|
@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;
|
@apply border-r border-mineshaft-600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.secret-table tr {
|
.secret-table tr {
|
||||||
@apply border-b border-mineshaft-600;
|
@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';
|
@import '@fontsource/inter/400.css';
|
||||||
|
|||||||
@@ -1,35 +1,31 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { faKey, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons';
|
import { faFolderOpen, faKey, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
|
||||||
|
|
||||||
import NavHeader from '@app/components/navigation/NavHeader';
|
import NavHeader from '@app/components/navigation/NavHeader';
|
||||||
import { Button, Input, TableContainer, Tooltip } from '@app/components/v2';
|
import { Button, Input, TableContainer, Tooltip } from '@app/components/v2';
|
||||||
import { useWorkspace } from '@app/context';
|
import { useWorkspace } from '@app/context';
|
||||||
import {
|
import {
|
||||||
|
useGetProjectFoldersBatch,
|
||||||
useGetProjectSecretsByKey,
|
useGetProjectSecretsByKey,
|
||||||
useGetUserWsEnvironments,
|
useGetUserWsEnvironments,
|
||||||
useGetUserWsKey
|
useGetUserWsKey
|
||||||
} from '@app/hooks/api';
|
} from '@app/hooks/api';
|
||||||
import { WorkspaceEnv } from '@app/hooks/api/types';
|
|
||||||
|
|
||||||
import { EnvComparisonRow } from './components/EnvComparisonRow';
|
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 { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
|
|
||||||
|
|
||||||
const { currentWorkspace, isLoading } = useWorkspace();
|
const { currentWorkspace, isLoading } = useWorkspace();
|
||||||
const workspaceId = currentWorkspace?._id as string;
|
const workspaceId = currentWorkspace?._id as string;
|
||||||
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
||||||
|
|
||||||
const [searchFilter, setSearchFilter] = useState('');
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
|
const secretPath = router.query?.secretPath as string;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading && !workspaceId && router.isReady) {
|
if (!isLoading && !workspaceId && router.isReady) {
|
||||||
@@ -38,14 +34,7 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
|
|||||||
}, [isLoading, workspaceId, router.isReady]);
|
}, [isLoading, workspaceId, router.isReady]);
|
||||||
|
|
||||||
const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({
|
const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({
|
||||||
workspaceId,
|
workspaceId
|
||||||
onSuccess: (data) => {
|
|
||||||
// get an env with one of the access available
|
|
||||||
const env = data.find(({ isReadDenied }) => !isReadDenied);
|
|
||||||
if (env) {
|
|
||||||
setSelectedEnv(env);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied);
|
const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied);
|
||||||
@@ -54,17 +43,32 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
env: userAvailableEnvs?.map((env) => env.slug) ?? [],
|
env: userAvailableEnvs?.map((env) => env.slug) ?? [],
|
||||||
decryptFileKey: latestFileKey!,
|
decryptFileKey: latestFileKey!,
|
||||||
isPaused: false
|
isPaused: false,
|
||||||
|
secretPath
|
||||||
});
|
});
|
||||||
|
|
||||||
const method = useForm<FormData>({
|
const folders = useGetProjectFoldersBatch({
|
||||||
// why any: well yup inferred ts expects other keys to defined as undefined
|
folders:
|
||||||
defaultValues: secrets as any,
|
userAvailableEnvs?.map((env) => ({
|
||||||
values: secrets as any,
|
environment: env.slug,
|
||||||
mode: 'onBlur',
|
workspaceId
|
||||||
resolver: yupResolver(schema)
|
})) ?? [],
|
||||||
|
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(() => {
|
const numSecretsMissingPerEnv = useMemo(() => {
|
||||||
// first get all sec in the env then subtract with total to get missing ones
|
// first get all sec in the env then subtract with total to get missing ones
|
||||||
const secPerEnvMissing: Record<string, number> = Object.fromEntries(
|
const secPerEnvMissing: Record<string, number> = Object.fromEntries(
|
||||||
@@ -81,7 +85,43 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
|
|||||||
return secPerEnvMissing;
|
return secPerEnvMissing;
|
||||||
}, [secrets, userAvailableEnvs]);
|
}, [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) {
|
if (isSecretsLoading || isEnvListLoading) {
|
||||||
return (
|
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
|
// 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 (
|
return (
|
||||||
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
<FormProvider {...method}>
|
<div className="relative right-5">
|
||||||
<form autoComplete="off">
|
<NavHeader pageName={t('dashboard.title')} isProjectRelated />
|
||||||
{/* breadcrumb row */}
|
</div>
|
||||||
<div className="relative right-5">
|
<div className="mt-6">
|
||||||
<NavHeader pageName={t('dashboard.title')} isProjectRelated />
|
<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>
|
||||||
<div className="mt-6">
|
{(secretPath || '')
|
||||||
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
|
.split('/')
|
||||||
<p className="text-md text-bunker-300">
|
.filter(Boolean)
|
||||||
Inject your secrets using
|
.map((path, index, arr) => (
|
||||||
<a
|
<div
|
||||||
className="mx-1 text-primary/80 hover:text-primary"
|
key={`secret-path-${index + 1}`}
|
||||||
href="https://infisical.com/docs/cli/overview"
|
className={`breadcrumb relative z-20 ${
|
||||||
target="_blank"
|
index + 1 === arr.length ? 'cursor-default' : 'cursor-pointer'
|
||||||
rel="noopener noreferrer"
|
} 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
|
{path}
|
||||||
</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>
|
|
||||||
</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>
|
||||||
<div className="text-sm font-medium ">Secret</div>
|
<div className="w-80">
|
||||||
</div>
|
<Input
|
||||||
</div>
|
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||||
{numSecretsMissingPerEnv &&
|
placeholder="Search by secret/folder name..."
|
||||||
userAvailableEnvs?.map((env) => {
|
value={searchFilter}
|
||||||
return (
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
<div
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
key={`header-${env.slug}`}
|
/>
|
||||||
className="flex w-full min-w-[11rem] flex-row items-center rounded-md border-none bg-mineshaft-800"
|
</div>
|
||||||
>
|
</div>
|
||||||
<div className="flex w-full flex-row justify-center text-center text-sm font-medium text-bunker-200/[.99]">
|
<div className="overflow-y-auto">
|
||||||
{env.name}
|
<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">
|
||||||
{numSecretsMissingPerEnv[env.slug] > 0 && (
|
<div className="sticky top-0 flex w-10 items-center justify-center border-none px-4">
|
||||||
<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">
|
<div className="w-10 text-center text-xs text-transparent">{0}</div>
|
||||||
<Tooltip
|
</div>
|
||||||
content={`${
|
<div className="sticky top-0 border-none">
|
||||||
numSecretsMissingPerEnv[env.slug]
|
<div className="relative flex h-full w-full min-w-[200px] items-center justify-start lg:min-w-[220px] xl:min-w-[250px]">
|
||||||
} secrets missing compared to other environments`}
|
<div className="text-sm font-medium ">Secret</div>
|
||||||
>
|
</div>
|
||||||
<span className="text-bunker-100">
|
</div>
|
||||||
{numSecretsMissingPerEnv[env.slug]}
|
{numSecretsMissingPerEnv &&
|
||||||
</span>
|
userAvailableEnvs?.map((env) => {
|
||||||
</Tooltip>
|
return (
|
||||||
</div>
|
<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>
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
{/* In future, we should add an option to add environments here
|
})}
|
||||||
<div className="flex items-start justify-center h-full ml-10">
|
</div>
|
||||||
<Button
|
<div
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus}/>}
|
className={`${
|
||||||
onClick={() => prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false })}
|
isDashboardEmpty ? '' : ''
|
||||||
variant="outline_bg"
|
} 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`}
|
||||||
colorSchema="primary"
|
>
|
||||||
isFullWidth
|
{!isDashboardEmpty && (
|
||||||
className="h-10"
|
<TableContainer className="border-none">
|
||||||
>
|
<table className="secret-table relative w-full bg-mineshaft-900">
|
||||||
Add Environment
|
<tbody className="max-h-screen overflow-y-auto">
|
||||||
</Button>
|
{Object.keys(foldersGroupedByEnv || {}).map((folderName, index) => (
|
||||||
</div> */}
|
<FolderComparisonRow
|
||||||
</div>
|
key={`${folderName}-${index + 1}`}
|
||||||
<div className="group mt-4 flex min-w-[60.3rem] flex-row items-center">
|
folderName={folderName}
|
||||||
<div className="flex h-10 w-10 items-center justify-center border-none px-4">
|
userAvailableEnvs={userAvailableEnvs}
|
||||||
<div className="w-10 text-center text-xs text-transparent">0</div>
|
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>
|
||||||
<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>
|
||||||
|
<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>
|
||||||
</form>
|
<div className="flex min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
||||||
</FormProvider>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
/* eslint-disable react/jsx-no-useless-fragment */
|
/* eslint-disable react/jsx-no-useless-fragment */
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { faCircle, faEye, faEyeSlash, 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
index: number;
|
|
||||||
secrets: any[] | undefined;
|
secrets: any[] | undefined;
|
||||||
// permission and external state's that decided to hide or show
|
// permission and external state's that decided to hide or show
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
@@ -30,7 +29,7 @@ const DashboardInput = ({
|
|||||||
if (val === undefined)
|
if (val === undefined)
|
||||||
return (
|
return (
|
||||||
<span className="cursor-default font-sans text-xs italic text-red-500/80">
|
<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>
|
</span>
|
||||||
);
|
);
|
||||||
if (val?.length === 0)
|
if (val?.length === 0)
|
||||||
@@ -110,7 +109,6 @@ const DashboardInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EnvComparisonRow = ({
|
export const EnvComparisonRow = ({
|
||||||
index,
|
|
||||||
secrets,
|
secrets,
|
||||||
isSecretValueHidden,
|
isSecretValueHidden,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
@@ -126,7 +124,9 @@ export const EnvComparisonRow = ({
|
|||||||
return (
|
return (
|
||||||
<tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800">
|
<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">
|
<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>
|
||||||
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
<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">
|
<div className="flex h-8 cursor-default flex-row items-center truncate">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user