mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-17 15:08:32 +00:00
Compare commits
28 Commits
infisical/
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
41dd2fda8a | |||
22ca4f2e92 | |||
5882eb6f8a | |||
c13d5e29f4 | |||
d99c54ca50 | |||
9dd0dac2f9 | |||
98efffafaa | |||
342ee50063 | |||
553cf11ad2 | |||
4616cffecd | |||
39feb9a6ae | |||
82c1f8607d | |||
d4c3cbb53a | |||
1dea6749ba | |||
631eac803e | |||
facabc683b | |||
4b99a9ea93 | |||
445afb397c | |||
7d554f46d5 | |||
bbef7d415c | |||
bb7b398fa7 | |||
570457c7c9 | |||
1b77b1d70b | |||
0f697a91ab | |||
df6d23d1d3 | |||
0187d3012b | |||
22beebc5d0 | |||
6cb0a20675 |
@ -25,6 +25,8 @@ ARG POSTHOG_HOST
|
||||
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
|
||||
ARG POSTHOG_API_KEY
|
||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
|
||||
ARG INTERCOM_ID
|
||||
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
||||
|
||||
# Build
|
||||
RUN npm run build
|
||||
@ -42,6 +44,9 @@ VOLUME /app/.next/cache/images
|
||||
ARG POSTHOG_API_KEY
|
||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
||||
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
|
||||
ARG INTERCOM_ID
|
||||
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
||||
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
||||
|
||||
COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts
|
||||
COPY --from=frontend-builder /app/public ./public
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Types } from "mongoose";
|
||||
import { Request, Response } from "express";
|
||||
import { ISecret, Secret } from "../../models";
|
||||
import { ISecret, Secret, ServiceTokenData } from "../../models";
|
||||
import { IAction, SecretVersion } from "../../ee/models";
|
||||
import {
|
||||
SECRET_PERSONAL,
|
||||
@ -29,6 +29,7 @@ import { BatchSecretRequest, BatchSecret } from "../../types/secret";
|
||||
import Folder from "../../models/folder";
|
||||
import {
|
||||
getFolderByPath,
|
||||
getFolderIdFromServiceToken,
|
||||
searchByFolderId,
|
||||
} from "../../services/FolderService";
|
||||
|
||||
@ -45,14 +46,15 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId,
|
||||
requests,
|
||||
secretPath,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId: string;
|
||||
requests: BatchSecretRequest[];
|
||||
secretPath: string;
|
||||
} = req.body;
|
||||
let folderId = req.body.folderId as string;
|
||||
|
||||
const createSecrets: BatchSecret[] = [];
|
||||
const updateSecrets: BatchSecret[] = [];
|
||||
@ -70,6 +72,25 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
|
||||
// in service token when not giving secretpath folderid must be root
|
||||
// this is to avoid giving folderid when service tokens are used
|
||||
if (
|
||||
(!secretPath && folderId !== "root") ||
|
||||
(secretPath && secretPath !== serviceTkScopedSecretPath)
|
||||
) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
if (secretPath) {
|
||||
folderId = await getFolderIdFromServiceToken(
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
);
|
||||
}
|
||||
|
||||
for await (const request of requests) {
|
||||
// do a validation
|
||||
|
||||
@ -152,6 +173,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
numberOfSecrets: createdSecrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel,
|
||||
userAgent: req.headers?.["user-agent"],
|
||||
},
|
||||
@ -218,7 +240,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
tags: u.tags,
|
||||
folder: u.folder
|
||||
folder: u.folder,
|
||||
})
|
||||
);
|
||||
|
||||
@ -248,6 +270,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
numberOfSecrets: updateSecrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel,
|
||||
userAgent: req.headers?.["user-agent"],
|
||||
},
|
||||
@ -395,8 +418,13 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId,
|
||||
}: { workspaceId: string; environment: string; folderId: string } = req.body;
|
||||
secretPath,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
} = req.body;
|
||||
let folderId = req.body.folderId;
|
||||
|
||||
if (req.user) {
|
||||
const hasAccess = await userHasWorkspaceAccess(
|
||||
@ -421,6 +449,24 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
// case: create 1 secret
|
||||
listOfSecretsToCreate = [req.body.secrets];
|
||||
}
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
|
||||
// in service token when not giving secretpath folderid must be root
|
||||
// this is to avoid giving folderid when service tokens are used
|
||||
if (
|
||||
(!secretPath && folderId !== "root") ||
|
||||
(secretPath && secretPath !== serviceTkScopedSecretPath)
|
||||
) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
if (secretPath) {
|
||||
folderId = await getFolderIdFromServiceToken(
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
);
|
||||
}
|
||||
|
||||
// get secret blind index salt
|
||||
const salt = await SecretService.getSecretBlindIndexSalt({
|
||||
@ -585,6 +631,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel,
|
||||
folderId,
|
||||
userAgent: req.headers?.["user-agent"],
|
||||
},
|
||||
});
|
||||
@ -660,6 +707,18 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
|
||||
// in service token when not giving secretpath folderid must be root
|
||||
// this is to avoid giving folderid when service tokens are used
|
||||
if (
|
||||
(!secretPath && folderId !== "root") ||
|
||||
(secretPath && secretPath !== serviceTkScopedSecretPath)
|
||||
) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
|
||||
if (folders && secretPath) {
|
||||
if (!folders) throw BadRequestError({ message: "Folder not found" });
|
||||
const folder = getFolderByPath(folders.nodes, secretPath as string);
|
||||
@ -800,6 +859,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
environment,
|
||||
workspaceId,
|
||||
channel,
|
||||
folderId,
|
||||
userAgent: req.headers?.["user-agent"],
|
||||
},
|
||||
});
|
||||
@ -910,13 +970,13 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
tags,
|
||||
...(secretCommentCiphertext !== undefined &&
|
||||
secretCommentIV &&
|
||||
secretCommentTag
|
||||
secretCommentIV &&
|
||||
secretCommentTag
|
||||
? {
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
}
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
|
@ -1,29 +1,27 @@
|
||||
import { Request, Response } from 'express';
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { Request, Response } from "express";
|
||||
import crypto from "crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
import { User, ServiceAccount, ServiceTokenData } from "../../models";
|
||||
import { userHasWorkspaceAccess } from "../../ee/helpers/checkMembershipPermissions";
|
||||
import {
|
||||
User,
|
||||
ServiceAccount,
|
||||
ServiceTokenData
|
||||
} from '../../models';
|
||||
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
|
||||
import {
|
||||
PERMISSION_READ_SECRETS,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN
|
||||
} from '../../variables';
|
||||
import { getSaltRounds } from '../../config';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
PERMISSION_READ_SECRETS,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
} from "../../variables";
|
||||
import { getSaltRounds } from "../../config";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import Folder from "../../models/folder";
|
||||
import { getFolderByPath } from "../../services/FolderService";
|
||||
|
||||
/**
|
||||
* Return service token data associated with service token on request
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getServiceTokenData = async (req: Request, res: Response) => {
|
||||
/*
|
||||
/*
|
||||
#swagger.summary = 'Return Infisical Token data'
|
||||
#swagger.description = 'Return Infisical Token data'
|
||||
|
||||
@ -36,111 +34,135 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"serviceTokenData": {
|
||||
"type": "object",
|
||||
$ref: "#/components/schemas/ServiceTokenData",
|
||||
"description": "Details of service token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
if (!(req.authData.authPayload instanceof ServiceTokenData)) throw BadRequestError({
|
||||
message: 'Failed accepted client validation for service token data'
|
||||
if (!(req.authData.authPayload instanceof ServiceTokenData))
|
||||
throw BadRequestError({
|
||||
message: "Failed accepted client validation for service token data",
|
||||
});
|
||||
|
||||
const serviceTokenData = await ServiceTokenData
|
||||
.findById(req.authData.authPayload._id)
|
||||
.select('+encryptedKey +iv +tag')
|
||||
.populate('user');
|
||||
const serviceTokenData = await ServiceTokenData.findById(
|
||||
req.authData.authPayload._id
|
||||
)
|
||||
.select("+encryptedKey +iv +tag")
|
||||
.populate("user");
|
||||
|
||||
return res.status(200).json(serviceTokenData);
|
||||
}
|
||||
return res.status(200).json(serviceTokenData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new service token data for workspace with id [workspaceId] and
|
||||
* environment [environment].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
let serviceTokenData;
|
||||
let serviceTokenData;
|
||||
|
||||
const {
|
||||
name,
|
||||
workspaceId,
|
||||
environment,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
expiresIn,
|
||||
permissions
|
||||
} = req.body;
|
||||
const {
|
||||
name,
|
||||
workspaceId,
|
||||
environment,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
expiresIn,
|
||||
secretPath,
|
||||
permissions,
|
||||
} = req.body;
|
||||
|
||||
const secret = crypto.randomBytes(16).toString('hex');
|
||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
});
|
||||
|
||||
let expiresAt;
|
||||
if (expiresIn) {
|
||||
expiresAt = new Date()
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (folder == undefined) {
|
||||
throw BadRequestError({ message: "Path for service token does not exist" })
|
||||
}
|
||||
}
|
||||
|
||||
let user, serviceAccount;
|
||||
|
||||
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) {
|
||||
user = req.authData.authPayload._id;
|
||||
}
|
||||
const secret = crypto.randomBytes(16).toString("hex");
|
||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||
|
||||
if (req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && req.authData.authPayload instanceof ServiceAccount) {
|
||||
serviceAccount = req.authData.authPayload._id;
|
||||
}
|
||||
|
||||
serviceTokenData = await new ServiceTokenData({
|
||||
name,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
user,
|
||||
serviceAccount,
|
||||
lastUsed: new Date(),
|
||||
expiresAt,
|
||||
secretHash,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
permissions
|
||||
}).save();
|
||||
let expiresAt;
|
||||
if (expiresIn) {
|
||||
expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
}
|
||||
|
||||
// return service token data without sensitive data
|
||||
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
|
||||
let user, serviceAccount;
|
||||
|
||||
if (!serviceTokenData) throw new Error('Failed to find service token data');
|
||||
if (
|
||||
req.authData.authMode === AUTH_MODE_JWT &&
|
||||
req.authData.authPayload instanceof User
|
||||
) {
|
||||
user = req.authData.authPayload._id;
|
||||
}
|
||||
|
||||
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
|
||||
if (
|
||||
req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
|
||||
req.authData.authPayload instanceof ServiceAccount
|
||||
) {
|
||||
serviceAccount = req.authData.authPayload._id;
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceToken,
|
||||
serviceTokenData
|
||||
});
|
||||
}
|
||||
serviceTokenData = await new ServiceTokenData({
|
||||
name,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
user,
|
||||
serviceAccount,
|
||||
lastUsed: new Date(),
|
||||
expiresAt,
|
||||
secretHash,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
secretPath,
|
||||
permissions,
|
||||
}).save();
|
||||
|
||||
// return service token data without sensitive data
|
||||
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
|
||||
|
||||
if (!serviceTokenData) throw new Error("Failed to find service token data");
|
||||
|
||||
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
|
||||
|
||||
return res.status(200).send({
|
||||
serviceToken,
|
||||
serviceTokenData,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete service token data with id [serviceTokenDataId].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||
const { serviceTokenDataId } = req.params;
|
||||
const { serviceTokenDataId } = req.params;
|
||||
|
||||
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
|
||||
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(
|
||||
serviceTokenDataId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
}
|
||||
return res.status(200).send({
|
||||
serviceTokenData,
|
||||
});
|
||||
};
|
||||
|
@ -1,183 +1,420 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
SecretService,
|
||||
TelemetryService,
|
||||
EventService
|
||||
} from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
import { getAuthDataPayloadIdObj } from '../../utils/auth';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { SecretService, EventService } from "../../services";
|
||||
import { eventPushSecrets } from "../../events";
|
||||
import { BotService } from "../../services";
|
||||
import { repackageSecretToRaw } from "../../helpers/secrets";
|
||||
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
|
||||
|
||||
/**
|
||||
* Get secrets for workspace with id [workspaceId] and environment
|
||||
* [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* Return secrets for workspace with id [workspaceId] and environment
|
||||
* [environment] in plaintext
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getSecrets = async (req: Request, res: Response) => {
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
export const getSecretsRaw = async (req: Request, res: Response) => {
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
const secretPath = req.query.secretPath as string;
|
||||
|
||||
const secrets = await SecretService.getSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
authData: req.authData
|
||||
});
|
||||
const secrets = await SecretService.getSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
authData: req.authData,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets
|
||||
});
|
||||
}
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: secrets.map((secret) => {
|
||||
const rep = repackageSecretToRaw({
|
||||
secret,
|
||||
key
|
||||
});
|
||||
|
||||
return rep;
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get secret with name [secretName]
|
||||
* @param req
|
||||
* @param res
|
||||
* Return secret with name [secretName] in plaintext
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getSecretByName = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
const type = req.query.type as 'shared' | 'personal' | undefined;
|
||||
export const getSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
const secretPath = req.query.secretPath as string;
|
||||
const type = req.query.type as "shared" | "personal" | undefined;
|
||||
|
||||
const secret = await SecretService.getSecret({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret
|
||||
});
|
||||
}
|
||||
const secret = await SecretService.getSecret({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
secretPath,
|
||||
authData: req.authData,
|
||||
});
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret: repackageSecretToRaw({
|
||||
secret,
|
||||
key
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create secret with name [secretName]
|
||||
* @param req
|
||||
* Create secret with name [secretName] in plaintext
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const createSecret = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag
|
||||
} = req.body;
|
||||
|
||||
const secret = await SecretService.createSecret({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
...((secretCommentCiphertext && secretCommentIV && secretCommentTag) ? {
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag
|
||||
} : {})
|
||||
});
|
||||
export const createSecretRaw = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretValue,
|
||||
secretComment,
|
||||
secretPath = "/"
|
||||
} = req.body;
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment
|
||||
})
|
||||
});
|
||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: secretName,
|
||||
key
|
||||
});
|
||||
|
||||
const secretWithoutBlindIndex = secret.toObject();
|
||||
delete secretWithoutBlindIndex.secretBlindIndex;
|
||||
|
||||
return res.status(200).send({
|
||||
secret: secretWithoutBlindIndex
|
||||
});
|
||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: secretValue,
|
||||
key
|
||||
});
|
||||
|
||||
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: secretComment,
|
||||
key
|
||||
});
|
||||
|
||||
const secret = await SecretService.createSecret({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
||||
secretKeyIV: secretKeyEncrypted.iv,
|
||||
secretKeyTag: secretKeyEncrypted.tag,
|
||||
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||
secretValueIV: secretValueEncrypted.iv,
|
||||
secretValueTag: secretValueEncrypted.tag,
|
||||
secretPath,
|
||||
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||
secretCommentIV: secretCommentEncrypted.iv,
|
||||
secretCommentTag: secretCommentEncrypted.tag
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
});
|
||||
|
||||
const secretWithoutBlindIndex = secret.toObject();
|
||||
delete secretWithoutBlindIndex.secretBlindIndex;
|
||||
|
||||
return res.status(200).send({
|
||||
secret: repackageSecretToRaw({
|
||||
secret: secretWithoutBlindIndex,
|
||||
key
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update secret with name [secretName]
|
||||
* @param req
|
||||
* @param res
|
||||
* @param res
|
||||
*/
|
||||
export const updateSecretByName = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag
|
||||
} = req.body;
|
||||
export const updateSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretValue,
|
||||
secretPath = "/",
|
||||
} = req.body;
|
||||
|
||||
const secret = await SecretService.updateSecret({
|
||||
secretName,
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment
|
||||
})
|
||||
});
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret
|
||||
});
|
||||
}
|
||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: secretValue,
|
||||
key
|
||||
});
|
||||
|
||||
const secret = await SecretService.updateSecret({
|
||||
secretName,
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||
secretValueIV: secretValueEncrypted.iv,
|
||||
secretValueTag: secretValueEncrypted.tag,
|
||||
secretPath,
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret: repackageSecretToRaw({
|
||||
secret,
|
||||
key
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete secret with name [secretName]
|
||||
* @param req
|
||||
* @param res
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretPath = "/"
|
||||
} = req.body;
|
||||
|
||||
const { secret } = await SecretService.deleteSecret({
|
||||
secretName,
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretPath,
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
});
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret: repackageSecretToRaw({
|
||||
secret,
|
||||
key
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get secrets for workspace with id [workspaceId] and environment
|
||||
* [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getSecrets = async (req: Request, res: Response) => {
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
const secretPath = req.query.secretPath as string;
|
||||
|
||||
const secrets = await SecretService.getSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
authData: req.authData,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return secret with name [secretName]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getSecretByName = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
const secretPath = req.query.secretPath as string;
|
||||
const type = req.query.type as "shared" | "personal" | undefined;
|
||||
|
||||
const secret = await SecretService.getSecret({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
secretPath,
|
||||
authData: req.authData,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create secret with name [secretName]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const createSecret = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretPath = "/",
|
||||
} = req.body;
|
||||
|
||||
const secret = await SecretService.createSecret({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretPath,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
});
|
||||
|
||||
const secretWithoutBlindIndex = secret.toObject();
|
||||
delete secretWithoutBlindIndex.secretBlindIndex;
|
||||
|
||||
return res.status(200).send({
|
||||
secret: secretWithoutBlindIndex,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Update secret with name [secretName]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const updateSecretByName = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretPath = "/",
|
||||
} = req.body;
|
||||
|
||||
const secret = await SecretService.updateSecret({
|
||||
secretName,
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretPath,
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete secret with name [secretName]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecretByName = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type
|
||||
} = req.body;
|
||||
|
||||
const { secret, secrets } = await SecretService.deleteSecret({
|
||||
secretName,
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData
|
||||
});
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretPath = "/"
|
||||
} = req.body;
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment
|
||||
})
|
||||
});
|
||||
const { secret } = await SecretService.deleteSecret({
|
||||
secretName,
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretPath,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret
|
||||
});
|
||||
}
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret,
|
||||
});
|
||||
};
|
||||
|
@ -95,6 +95,13 @@ class EELicenseService {
|
||||
|
||||
return this.globalFeatureSet;
|
||||
}
|
||||
|
||||
public async refreshOrganizationPlan(organizationId: string) {
|
||||
if (this.instanceType === 'cloud') {
|
||||
this.localFeatureSet.del(organizationId);
|
||||
await this.getOrganizationPlan(organizationId);
|
||||
}
|
||||
}
|
||||
|
||||
public async initGlobalFeatureSet() {
|
||||
const licenseServerKey = await getLicenseServerKey();
|
||||
|
@ -86,6 +86,18 @@ export const createBot = async ({
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return whether or not workspace with id [workspaceId] is end-to-end encrypted
|
||||
* @param {Types.ObjectId} workspaceId - id of workspace to check
|
||||
*/
|
||||
export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
|
||||
const botKey = await BotKey.exists({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
return botKey ? false : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return decrypted secrets for workspace with id [workspaceId]
|
||||
* and [environment] using bot
|
||||
@ -101,7 +113,7 @@ export const getSecretsBotHelper = async ({
|
||||
environment: string;
|
||||
}) => {
|
||||
const content = {} as any;
|
||||
const key = await getKey({ workspaceId: workspaceId.toString() });
|
||||
const key = await getKey({ workspaceId: workspaceId });
|
||||
const secrets = await Secret.find({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
@ -136,7 +148,7 @@ export const getSecretsBotHelper = async ({
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @returns {String} key - decrypted workspace key
|
||||
*/
|
||||
export const getKey = async ({ workspaceId }: { workspaceId: string }) => {
|
||||
export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => {
|
||||
const encryptionKey = await getEncryptionKey();
|
||||
const rootEncryptionKey = await getRootEncryptionKey();
|
||||
|
||||
@ -201,7 +213,7 @@ export const encryptSymmetricHelper = async ({
|
||||
workspaceId: Types.ObjectId;
|
||||
plaintext: string;
|
||||
}) => {
|
||||
const key = await getKey({ workspaceId: workspaceId.toString() });
|
||||
const key = await getKey({ workspaceId: workspaceId });
|
||||
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext,
|
||||
key,
|
||||
@ -233,7 +245,7 @@ export const decryptSymmetricHelper = async ({
|
||||
iv: string;
|
||||
tag: string;
|
||||
}) => {
|
||||
const key = await getKey({ workspaceId: workspaceId.toString() });
|
||||
const key = await getKey({ workspaceId: workspaceId });
|
||||
const plaintext = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext,
|
||||
iv,
|
||||
|
@ -153,22 +153,24 @@ export const updateSubscriptionOrgQuantity = async ({
|
||||
|
||||
EELicenseService.localFeatureSet.del(organizationId);
|
||||
}
|
||||
|
||||
if (EELicenseService.instanceType === 'enterprise-self-hosted') {
|
||||
// instance of Infisical is an enterprise self-hosted instance
|
||||
|
||||
const usedSeats = await MembershipOrg.countDocuments({
|
||||
status: ACCEPTED
|
||||
});
|
||||
|
||||
await licenseKeyRequest.patch(
|
||||
`${await getLicenseServerUrl()}/api/license/v1/license`,
|
||||
{
|
||||
usedSeats
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (EELicenseService.instanceType === 'enterprise-self-hosted') {
|
||||
// instance of Infisical is an enterprise self-hosted instance
|
||||
|
||||
const usedSeats = await MembershipOrg.countDocuments({
|
||||
status: ACCEPTED
|
||||
});
|
||||
|
||||
await licenseKeyRequest.patch(
|
||||
`${await getLicenseServerUrl()}/api/license/v1/license`,
|
||||
{
|
||||
usedSeats
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await EELicenseService.refreshOrganizationPlan(organizationId);
|
||||
|
||||
return stripeSubscription;
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { Types } from 'mongoose';
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
CreateSecretParams,
|
||||
GetSecretsParams,
|
||||
@ -6,14 +6,20 @@ import {
|
||||
UpdateSecretParams,
|
||||
DeleteSecretParams,
|
||||
} from '../interfaces/services/SecretService';
|
||||
import { Secret, ISecret, SecretBlindIndexData } from '../models';
|
||||
import { SecretVersion } from '../ee/models';
|
||||
import {
|
||||
Secret,
|
||||
ISecret,
|
||||
SecretBlindIndexData,
|
||||
ServiceTokenData,
|
||||
} from "../models";
|
||||
import { SecretVersion } from "../ee/models";
|
||||
import {
|
||||
BadRequestError,
|
||||
SecretNotFoundError,
|
||||
SecretBlindIndexDataNotFoundError,
|
||||
InternalServerError,
|
||||
} from '../utils/errors';
|
||||
UnauthorizedRequestError,
|
||||
} from "../utils/errors";
|
||||
import {
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED,
|
||||
@ -24,20 +30,75 @@ import {
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
ENCODING_SCHEME_BASE64,
|
||||
} from '../variables';
|
||||
import crypto from 'crypto';
|
||||
import * as argon2 from 'argon2';
|
||||
} from "../variables";
|
||||
import crypto from "crypto";
|
||||
import * as argon2 from "argon2";
|
||||
import {
|
||||
encryptSymmetric128BitHexKeyUTF8,
|
||||
decryptSymmetric128BitHexKeyUTF8,
|
||||
} from '../utils/crypto';
|
||||
import { getEncryptionKey, client, getRootEncryptionKey } from '../config';
|
||||
import { TelemetryService } from '../services';
|
||||
import { EESecretService, EELogService } from '../ee/services';
|
||||
import { getEncryptionKey, client, getRootEncryptionKey } from "../config";
|
||||
import { EESecretService, EELogService } from "../ee/services";
|
||||
import {
|
||||
getAuthDataPayloadIdObj,
|
||||
getAuthDataPayloadUserObj,
|
||||
} from '../utils/auth';
|
||||
} from "../utils/auth";
|
||||
import { getFolderIdFromServiceToken } from "../services/FolderService";
|
||||
|
||||
/**
|
||||
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
|
||||
*
|
||||
* Precondition: the workspace for secret [secret] must have E2EE disabled
|
||||
* @param {ISecret} secret - secret to repackage to raw
|
||||
* @param {String} key - symmetric key to use to decrypt secret
|
||||
* @returns
|
||||
*/
|
||||
export const repackageSecretToRaw = ({
|
||||
secret,
|
||||
key
|
||||
}:{
|
||||
secret: ISecret;
|
||||
key: string;
|
||||
}) => {
|
||||
|
||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
let secretComment: string = '';
|
||||
|
||||
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
|
||||
secretComment = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretCommentCiphertext,
|
||||
iv: secret.secretCommentIV,
|
||||
tag: secret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
}
|
||||
|
||||
return ({
|
||||
_id: secret._id,
|
||||
version: secret.version,
|
||||
workspace: secret.workspace,
|
||||
type: secret.type,
|
||||
environment: secret.environment,
|
||||
user: secret.user,
|
||||
secretKey,
|
||||
secretValue,
|
||||
secretComment
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create secret blind index data containing encrypted blind index [salt]
|
||||
@ -46,12 +107,12 @@ import {
|
||||
* @param {Types.ObjectId} obj.workspaceId
|
||||
*/
|
||||
export const createSecretBlindIndexDataHelper = async ({
|
||||
workspaceId
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) => {
|
||||
// initialize random blind index salt for workspace
|
||||
const salt = crypto.randomBytes(16).toString('base64');
|
||||
const salt = crypto.randomBytes(16).toString("base64");
|
||||
|
||||
const encryptionKey = await getEncryptionKey();
|
||||
const rootEncryptionKey = await getRootEncryptionKey();
|
||||
@ -99,7 +160,7 @@ export const createSecretBlindIndexDataHelper = async ({
|
||||
* @returns
|
||||
*/
|
||||
export const getSecretBlindIndexSaltHelper = async ({
|
||||
workspaceId
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) => {
|
||||
@ -108,7 +169,7 @@ export const getSecretBlindIndexSaltHelper = async ({
|
||||
|
||||
const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
||||
workspace: workspaceId,
|
||||
}).select('+algorithm +keyEncoding');
|
||||
}).select("+algorithm +keyEncoding");
|
||||
|
||||
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
||||
|
||||
@ -136,7 +197,7 @@ export const getSecretBlindIndexSaltHelper = async ({
|
||||
}
|
||||
|
||||
throw InternalServerError({
|
||||
message: 'Failed to obtain workspace salt needed for secret blind indexing',
|
||||
message: "Failed to obtain workspace salt needed for secret blind indexing",
|
||||
});
|
||||
};
|
||||
|
||||
@ -148,8 +209,8 @@ export const getSecretBlindIndexSaltHelper = async ({
|
||||
* @param {String} obj.salt - base64-salt
|
||||
*/
|
||||
export const generateSecretBlindIndexWithSaltHelper = async ({
|
||||
secretName,
|
||||
salt
|
||||
secretName,
|
||||
salt,
|
||||
}: {
|
||||
secretName: string;
|
||||
salt: string;
|
||||
@ -158,14 +219,14 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
|
||||
const secretBlindIndex = (
|
||||
await argon2.hash(secretName, {
|
||||
type: argon2.argon2id,
|
||||
salt: Buffer.from(salt, 'base64'),
|
||||
salt: Buffer.from(salt, "base64"),
|
||||
saltLength: 16, // default 16 bytes
|
||||
memoryCost: 65536, // default pool of 64 MiB per thread.
|
||||
hashLength: 32,
|
||||
parallelism: 1,
|
||||
raw: true,
|
||||
})
|
||||
).toString('base64');
|
||||
).toString("base64");
|
||||
|
||||
return secretBlindIndex;
|
||||
};
|
||||
@ -178,8 +239,8 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
|
||||
*/
|
||||
export const generateSecretBlindIndexHelper = async ({
|
||||
secretName,
|
||||
workspaceId
|
||||
secretName,
|
||||
workspaceId,
|
||||
}: {
|
||||
secretName: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
@ -190,7 +251,7 @@ export const generateSecretBlindIndexHelper = async ({
|
||||
|
||||
const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
||||
workspace: workspaceId,
|
||||
}).select('+algorithm +keyEncoding');
|
||||
}).select("+algorithm +keyEncoding");
|
||||
|
||||
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
||||
|
||||
@ -231,9 +292,9 @@ export const generateSecretBlindIndexHelper = async ({
|
||||
|
||||
return secretBlindIndex;
|
||||
}
|
||||
|
||||
|
||||
throw InternalServerError({
|
||||
message: 'Failed to generate secret blind index'
|
||||
message: "Failed to generate secret blind index",
|
||||
});
|
||||
};
|
||||
|
||||
@ -262,23 +323,38 @@ export const createSecretHelper = async ({
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
folderId,
|
||||
secretPath = "/",
|
||||
}: CreateSecretParams) => {
|
||||
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
});
|
||||
|
||||
// if using service token filter towards the folderId by secretpath
|
||||
if (authData.authPayload instanceof ServiceTokenData) {
|
||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
||||
if (secretPath !== serviceTkScopedSecretPath) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
const folderId = await getFolderIdFromServiceToken(
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
);
|
||||
|
||||
const exists = await Secret.exists({
|
||||
secretBlindIndex,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
folder: folderId,
|
||||
type,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
||||
});
|
||||
|
||||
if (exists)
|
||||
throw BadRequestError({
|
||||
message: 'Failed to create secret that already exists',
|
||||
message: "Failed to create secret that already exists",
|
||||
});
|
||||
|
||||
if (type === SECRET_PERSONAL) {
|
||||
@ -287,6 +363,7 @@ export const createSecretHelper = async ({
|
||||
|
||||
const exists = await Secret.exists({
|
||||
secretBlindIndex,
|
||||
folder: folderId,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
type: SECRET_SHARED,
|
||||
});
|
||||
@ -294,7 +371,7 @@ export const createSecretHelper = async ({
|
||||
if (!exists)
|
||||
throw BadRequestError({
|
||||
message:
|
||||
'Failed to create personal secret override for no corresponding shared secret',
|
||||
"Failed to create personal secret override for no corresponding shared secret",
|
||||
});
|
||||
}
|
||||
|
||||
@ -325,6 +402,7 @@ export const createSecretHelper = async ({
|
||||
version: secret.version,
|
||||
workspace: secret.workspace,
|
||||
type,
|
||||
folder: folderId,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
||||
environment: secret.environment,
|
||||
isDeleted: false,
|
||||
@ -372,7 +450,7 @@ export const createSecretHelper = async ({
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets added',
|
||||
event: "secrets added",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData,
|
||||
}),
|
||||
@ -380,6 +458,7 @@ export const createSecretHelper = async ({
|
||||
numberOfSecrets: 1,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.authChannel,
|
||||
userAgent: authData.authUserAgent,
|
||||
},
|
||||
@ -398,30 +477,45 @@ export const createSecretHelper = async ({
|
||||
* @returns
|
||||
*/
|
||||
export const getSecretsHelper = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
authData
|
||||
workspaceId,
|
||||
environment,
|
||||
authData,
|
||||
secretPath = "/",
|
||||
}: GetSecretsParams) => {
|
||||
let secrets: ISecret[] = [];
|
||||
// if using service token filter towards the folderId by secretpath
|
||||
if (authData.authPayload instanceof ServiceTokenData) {
|
||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
||||
if (secretPath !== serviceTkScopedSecretPath) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
const folderId = await getFolderIdFromServiceToken(
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
);
|
||||
|
||||
// get personal secrets first
|
||||
secrets = await Secret.find({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
type: SECRET_PERSONAL,
|
||||
...getAuthDataPayloadUserObj(authData),
|
||||
});
|
||||
}).lean();
|
||||
|
||||
// concat with shared secrets
|
||||
secrets = secrets.concat(
|
||||
await Secret.find({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
type: SECRET_SHARED,
|
||||
secretBlindIndex: {
|
||||
$nin: secrets.map((secret) => secret.secretBlindIndex),
|
||||
},
|
||||
})
|
||||
}).lean()
|
||||
);
|
||||
|
||||
// (EE) create (audit) log
|
||||
@ -445,7 +539,7 @@ export const getSecretsHelper = async ({
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets pulled',
|
||||
event: "secrets pulled",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData,
|
||||
}),
|
||||
@ -453,12 +547,13 @@ export const getSecretsHelper = async ({
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.authChannel,
|
||||
userAgent: authData.authUserAgent,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return secrets;
|
||||
};
|
||||
|
||||
@ -478,21 +573,35 @@ export const getSecretHelper = async ({
|
||||
environment,
|
||||
type,
|
||||
authData,
|
||||
secretPath = "/",
|
||||
}: GetSecretParams) => {
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
});
|
||||
let secret: ISecret | null = null;
|
||||
// if using service token filter towards the folderId by secretpath
|
||||
if (authData.authPayload instanceof ServiceTokenData) {
|
||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
||||
if (secretPath !== serviceTkScopedSecretPath) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
const folderId = await getFolderIdFromServiceToken(
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
);
|
||||
|
||||
// try getting personal secret first (if exists)
|
||||
secret = await Secret.findOne({
|
||||
secretBlindIndex,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
type: type ?? SECRET_PERSONAL,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
||||
});
|
||||
}).lean();
|
||||
|
||||
if (!secret) {
|
||||
// case: failed to find personal secret matching criteria
|
||||
@ -501,8 +610,9 @@ export const getSecretHelper = async ({
|
||||
secretBlindIndex,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
type: SECRET_SHARED,
|
||||
});
|
||||
}).lean();
|
||||
}
|
||||
|
||||
if (!secret) throw SecretNotFoundError();
|
||||
@ -528,7 +638,7 @@ export const getSecretHelper = async ({
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets pull',
|
||||
event: "secrets pull",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData,
|
||||
}),
|
||||
@ -536,12 +646,13 @@ export const getSecretHelper = async ({
|
||||
numberOfSecrets: 1,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.authChannel,
|
||||
userAgent: authData.authUserAgent,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return secret;
|
||||
};
|
||||
|
||||
@ -568,6 +679,7 @@ export const updateSecretHelper = async ({
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretPath,
|
||||
}: UpdateSecretParams) => {
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
@ -575,6 +687,18 @@ export const updateSecretHelper = async ({
|
||||
});
|
||||
|
||||
let secret: ISecret | null = null;
|
||||
// if using service token filter towards the folderId by secretpath
|
||||
if (authData.authPayload instanceof ServiceTokenData) {
|
||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
||||
if (secretPath !== serviceTkScopedSecretPath) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
const folderId = await getFolderIdFromServiceToken(
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
);
|
||||
|
||||
if (type === SECRET_SHARED) {
|
||||
// case: update shared secret
|
||||
@ -583,6 +707,7 @@ export const updateSecretHelper = async ({
|
||||
secretBlindIndex,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
type,
|
||||
},
|
||||
{
|
||||
@ -604,6 +729,7 @@ export const updateSecretHelper = async ({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
folder: folderId,
|
||||
...getAuthDataPayloadUserObj(authData),
|
||||
},
|
||||
{
|
||||
@ -624,6 +750,7 @@ export const updateSecretHelper = async ({
|
||||
secret: secret._id,
|
||||
version: secret.version,
|
||||
workspace: secret.workspace,
|
||||
folder: folderId,
|
||||
type,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
||||
environment: secret.environment,
|
||||
@ -672,7 +799,7 @@ export const updateSecretHelper = async ({
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets modified',
|
||||
event: "secrets modified",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData,
|
||||
}),
|
||||
@ -680,6 +807,7 @@ export const updateSecretHelper = async ({
|
||||
numberOfSecrets: 1,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.authChannel,
|
||||
userAgent: authData.authUserAgent,
|
||||
},
|
||||
@ -705,12 +833,27 @@ export const deleteSecretHelper = async ({
|
||||
environment,
|
||||
type,
|
||||
authData,
|
||||
secretPath = "/",
|
||||
}: DeleteSecretParams) => {
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
});
|
||||
|
||||
// if using service token filter towards the folderId by secretpath
|
||||
if (authData.authPayload instanceof ServiceTokenData) {
|
||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
||||
|
||||
if (secretPath !== serviceTkScopedSecretPath) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
const folderId = await getFolderIdFromServiceToken(
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
);
|
||||
|
||||
let secrets: ISecret[] = [];
|
||||
let secret: ISecret | null = null;
|
||||
|
||||
@ -719,28 +862,32 @@ export const deleteSecretHelper = async ({
|
||||
secretBlindIndex,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
});
|
||||
folder: folderId,
|
||||
}).lean();
|
||||
|
||||
secret = await Secret.findOneAndDelete({
|
||||
secretBlindIndex,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
});
|
||||
folder: folderId,
|
||||
}).lean();
|
||||
|
||||
await Secret.deleteMany({
|
||||
secretBlindIndex,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
});
|
||||
} else {
|
||||
secret = await Secret.findOneAndDelete({
|
||||
secretBlindIndex,
|
||||
folder: folderId,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
...getAuthDataPayloadUserObj(authData),
|
||||
});
|
||||
}).lean();
|
||||
|
||||
if (secret) {
|
||||
secrets = [secret];
|
||||
@ -761,9 +908,7 @@ export const deleteSecretHelper = async ({
|
||||
secretIds: secrets.map((secret) => secret._id),
|
||||
});
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
action &&
|
||||
(await EELogService.createLog({
|
||||
action && (await EELogService.createLog({
|
||||
...getAuthDataPayloadIdObj(authData),
|
||||
workspaceId,
|
||||
actions: [action],
|
||||
@ -782,7 +927,7 @@ export const deleteSecretHelper = async ({
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets deleted',
|
||||
event: "secrets deleted",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData,
|
||||
}),
|
||||
@ -790,14 +935,15 @@ export const deleteSecretHelper = async ({
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.authChannel,
|
||||
userAgent: authData.authUserAgent,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
return ({
|
||||
secrets,
|
||||
secret,
|
||||
};
|
||||
};
|
||||
secret
|
||||
});
|
||||
};
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
Secret
|
||||
} from '../models';
|
||||
import { createBot } from '../helpers/bot';
|
||||
import { EELicenseService } from '../ee/services';
|
||||
import { SecretService } from '../services';
|
||||
|
||||
/**
|
||||
@ -22,24 +23,25 @@ export const createWorkspace = async ({
|
||||
name: string;
|
||||
organizationId: string;
|
||||
}) => {
|
||||
// create workspace
|
||||
const workspace = await new Workspace({
|
||||
name,
|
||||
organization: organizationId,
|
||||
autoCapitalization: true
|
||||
}).save();
|
||||
// create workspace
|
||||
const workspace = await new Workspace({
|
||||
name,
|
||||
organization: organizationId,
|
||||
autoCapitalization: true
|
||||
}).save();
|
||||
|
||||
// initialize bot for workspace
|
||||
await createBot({
|
||||
name: 'Infisical Bot',
|
||||
workspaceId: workspace._id
|
||||
});
|
||||
// initialize bot for workspace
|
||||
await createBot({
|
||||
name: 'Infisical Bot',
|
||||
workspaceId: workspace._id
|
||||
});
|
||||
|
||||
// initialize blind index salt for workspace
|
||||
await SecretService.createSecretBlindIndexData({
|
||||
workspaceId: workspace._id
|
||||
});
|
||||
// initialize blind index salt for workspace
|
||||
await SecretService.createSecretBlindIndexData({
|
||||
workspaceId: workspace._id
|
||||
});
|
||||
|
||||
await EELicenseService.refreshOrganizationPlan(organizationId);
|
||||
|
||||
return workspace;
|
||||
};
|
||||
|
0
backend/src/interfaces/services/BotService/index.ts
Normal file
0
backend/src/interfaces/services/BotService/index.ts
Normal file
@ -5,7 +5,6 @@ export interface CreateSecretParams {
|
||||
secretName: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
type: "shared" | "personal";
|
||||
authData: AuthData;
|
||||
secretKeyCiphertext: string;
|
||||
@ -17,17 +16,20 @@ export interface CreateSecretParams {
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretPath: string;
|
||||
}
|
||||
|
||||
export interface GetSecretsParams {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
authData: AuthData;
|
||||
}
|
||||
|
||||
export interface GetSecretParams {
|
||||
secretName: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
type?: "shared" | "personal";
|
||||
authData: AuthData;
|
||||
@ -42,7 +44,7 @@ export interface UpdateSecretParams {
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
folderId?: string;
|
||||
secretPath: string;
|
||||
}
|
||||
|
||||
export interface DeleteSecretParams {
|
||||
@ -51,4 +53,5 @@ export interface DeleteSecretParams {
|
||||
environment: string;
|
||||
type: "shared" | "personal";
|
||||
authData: AuthData;
|
||||
secretPath: string;
|
||||
}
|
||||
|
@ -16,13 +16,15 @@ const requireWorkspaceAuth = ({
|
||||
locationWorkspaceId,
|
||||
locationEnvironment = undefined,
|
||||
requiredPermissions = [],
|
||||
requireBlindIndicesEnabled = false
|
||||
requireBlindIndicesEnabled = false,
|
||||
requireE2EEOff = false
|
||||
}: {
|
||||
acceptedRoles: Array<'admin' | 'member'>;
|
||||
locationWorkspaceId: req;
|
||||
locationEnvironment?: req | undefined;
|
||||
requiredPermissions?: string[];
|
||||
requireBlindIndicesEnabled?: boolean;
|
||||
requireE2EEOff?: boolean;
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const workspaceId = req[locationWorkspaceId]?.workspaceId;
|
||||
@ -35,7 +37,8 @@ const requireWorkspaceAuth = ({
|
||||
environment,
|
||||
acceptedRoles,
|
||||
requiredPermissions,
|
||||
requireBlindIndicesEnabled
|
||||
requireBlindIndicesEnabled,
|
||||
requireE2EEOff
|
||||
});
|
||||
|
||||
if (membership) {
|
||||
|
@ -1,79 +1,88 @@
|
||||
import { Schema, model, Types, Document } from 'mongoose';
|
||||
import { Schema, model, Types, Document } from "mongoose";
|
||||
|
||||
export interface IServiceTokenData extends Document {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
workspace: Types.ObjectId;
|
||||
environment: string;
|
||||
user: Types.ObjectId;
|
||||
serviceAccount: Types.ObjectId;
|
||||
lastUsed: Date;
|
||||
expiresAt: Date;
|
||||
secretHash: string;
|
||||
encryptedKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
permissions: string[];
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
workspace: Types.ObjectId;
|
||||
environment: string;
|
||||
user: Types.ObjectId;
|
||||
serviceAccount: Types.ObjectId;
|
||||
lastUsed: Date;
|
||||
expiresAt: Date;
|
||||
secretHash: string;
|
||||
encryptedKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
secretPath: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
const serviceTokenDataSchema = new Schema<IServiceTokenData>(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
serviceAccount: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceAccount'
|
||||
},
|
||||
lastUsed: {
|
||||
type: Date
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date
|
||||
},
|
||||
secretHash: {
|
||||
type: String,
|
||||
required: true,
|
||||
select: false
|
||||
},
|
||||
encryptedKey: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
iv: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
permissions: {
|
||||
type: [String],
|
||||
enum: ['read', 'write'],
|
||||
default: ['read']
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true,
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
required: true,
|
||||
},
|
||||
serviceAccount: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "ServiceAccount",
|
||||
},
|
||||
lastUsed: {
|
||||
type: Date,
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date,
|
||||
},
|
||||
secretHash: {
|
||||
type: String,
|
||||
required: true,
|
||||
select: false,
|
||||
},
|
||||
encryptedKey: {
|
||||
type: String,
|
||||
select: false,
|
||||
},
|
||||
iv: {
|
||||
type: String,
|
||||
select: false,
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
select: false,
|
||||
},
|
||||
permissions: {
|
||||
type: [String],
|
||||
enum: ["read", "write"],
|
||||
default: ["read"],
|
||||
},
|
||||
secretPath: {
|
||||
type: String,
|
||||
default: "/",
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
const ServiceTokenData = model<IServiceTokenData>(
|
||||
"ServiceTokenData",
|
||||
serviceTokenDataSchema
|
||||
);
|
||||
|
||||
const ServiceTokenData = model<IServiceTokenData>('ServiceTokenData', serviceTokenDataSchema);
|
||||
|
||||
export default ServiceTokenData;
|
||||
|
@ -1,15 +1,15 @@
|
||||
import express from 'express';
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import { Types } from 'mongoose';
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
requireSecretsAuth,
|
||||
validateRequest,
|
||||
} from '../../middleware';
|
||||
import { validateClientForSecrets } from '../../validation';
|
||||
import { query, body } from 'express-validator';
|
||||
import { secretsController } from '../../controllers/v2';
|
||||
} from "../../middleware";
|
||||
import { validateClientForSecrets } from "../../validation";
|
||||
import { query, body } from "express-validator";
|
||||
import { secretsController } from "../../controllers/v2";
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
@ -21,11 +21,11 @@ import {
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY,
|
||||
} from '../../variables';
|
||||
import { BatchSecretRequest } from '../../types/secret';
|
||||
} from "../../variables";
|
||||
import { BatchSecretRequest } from "../../types/secret";
|
||||
|
||||
router.post(
|
||||
'/batch',
|
||||
"/batch",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
@ -35,12 +35,13 @@ router.post(
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'body',
|
||||
locationWorkspaceId: "body",
|
||||
}),
|
||||
body('workspaceId').exists().isString().trim(),
|
||||
body('folderId').default('root').isString().trim(),
|
||||
body('environment').exists().isString().trim(),
|
||||
body('requests')
|
||||
body("workspaceId").exists().isString().trim(),
|
||||
body("folderId").default("root").isString().trim(),
|
||||
body("environment").exists().isString().trim(),
|
||||
body("secretPath").optional().isString().trim(),
|
||||
body("requests")
|
||||
.exists()
|
||||
.custom(async (requests: BatchSecretRequest[], { req }) => {
|
||||
if (Array.isArray(requests)) {
|
||||
@ -65,17 +66,18 @@ router.post(
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
body('workspaceId').exists().isString().trim(),
|
||||
body('environment').exists().isString().trim(),
|
||||
body('folderId').default('root').isString().trim(),
|
||||
body('secrets')
|
||||
"/",
|
||||
body("workspaceId").exists().isString().trim(),
|
||||
body("environment").exists().isString().trim(),
|
||||
body("folderId").default("root").isString().trim(),
|
||||
body("secretPath").optional().isString().trim(),
|
||||
body("secrets")
|
||||
.exists()
|
||||
.custom((value) => {
|
||||
if (Array.isArray(value)) {
|
||||
// case: create multiple secrets
|
||||
if (value.length === 0)
|
||||
throw new Error('secrets cannot be an empty array');
|
||||
throw new Error("secrets cannot be an empty array");
|
||||
for (const secret of value) {
|
||||
if (
|
||||
!secret.type ||
|
||||
@ -85,16 +87,16 @@ router.post(
|
||||
!secret.secretKeyCiphertext ||
|
||||
!secret.secretKeyIV ||
|
||||
!secret.secretKeyTag ||
|
||||
typeof secret.secretValueCiphertext !== 'string' ||
|
||||
typeof secret.secretValueCiphertext !== "string" ||
|
||||
!secret.secretValueIV ||
|
||||
!secret.secretValueTag
|
||||
) {
|
||||
throw new Error(
|
||||
'secrets array must contain objects that have required secret properties'
|
||||
"secrets array must contain objects that have required secret properties"
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
} else if (typeof value === "object") {
|
||||
// case: update 1 secret
|
||||
if (
|
||||
!value.type ||
|
||||
@ -107,11 +109,11 @@ router.post(
|
||||
!value.secretValueTag
|
||||
) {
|
||||
throw new Error(
|
||||
'secrets object is missing required secret properties'
|
||||
"secrets object is missing required secret properties"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error('secrets must be an object or an array of objects');
|
||||
throw new Error("secrets must be an object or an array of objects");
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -126,19 +128,20 @@ router.post(
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'body',
|
||||
locationEnvironment: 'body',
|
||||
locationWorkspaceId: "body",
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
}),
|
||||
secretsController.createSecrets
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
query('workspaceId').exists().trim(),
|
||||
query('environment').exists().trim(),
|
||||
query('tagSlugs'),
|
||||
query('folderId').default('root').isString().trim(),
|
||||
"/",
|
||||
query("workspaceId").exists().trim(),
|
||||
query("environment").exists().trim(),
|
||||
query("tagSlugs"),
|
||||
query("folderId").default("root").isString().trim(),
|
||||
query("secretPath").optional().isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
@ -150,34 +153,34 @@ router.get(
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'query',
|
||||
locationEnvironment: 'query',
|
||||
locationWorkspaceId: "query",
|
||||
locationEnvironment: "query",
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
}),
|
||||
secretsController.getSecrets
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/',
|
||||
body('secrets')
|
||||
"/",
|
||||
body("secrets")
|
||||
.exists()
|
||||
.custom((value) => {
|
||||
if (Array.isArray(value)) {
|
||||
// case: update multiple secrets
|
||||
if (value.length === 0)
|
||||
throw new Error('secrets cannot be an empty array');
|
||||
throw new Error("secrets cannot be an empty array");
|
||||
for (const secret of value) {
|
||||
if (!secret.id) {
|
||||
throw new Error('Each secret must contain a ID property');
|
||||
throw new Error("Each secret must contain a ID property");
|
||||
}
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
} else if (typeof value === "object") {
|
||||
// case: update 1 secret
|
||||
if (!value.id) {
|
||||
throw new Error('secret must contain a ID property');
|
||||
throw new Error("secret must contain a ID property");
|
||||
}
|
||||
} else {
|
||||
throw new Error('secrets must be an object or an array of objects');
|
||||
throw new Error("secrets must be an object or an array of objects");
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -198,21 +201,21 @@ router.patch(
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/',
|
||||
body('secretIds')
|
||||
"/",
|
||||
body("secretIds")
|
||||
.exists()
|
||||
.custom((value) => {
|
||||
// case: delete 1 secret
|
||||
if (typeof value === 'string') return true;
|
||||
if (typeof value === "string") return true;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// case: delete multiple secrets
|
||||
if (value.length === 0)
|
||||
throw new Error('secrets cannot be an empty array');
|
||||
return value.every((id: string) => typeof id === 'string');
|
||||
throw new Error("secrets cannot be an empty array");
|
||||
return value.every((id: string) => typeof id === "string");
|
||||
}
|
||||
|
||||
throw new Error('secretIds must be a string or an array of strings');
|
||||
throw new Error("secretIds must be a string or an array of strings");
|
||||
})
|
||||
.not()
|
||||
.isEmpty(),
|
||||
|
@ -1,72 +1,79 @@
|
||||
import express from 'express';
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
requireServiceTokenDataAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { param, body } from 'express-validator';
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
requireServiceTokenDataAuth,
|
||||
validateRequest,
|
||||
} from "../../middleware";
|
||||
import { param, body } from "express-validator";
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
PERMISSION_WRITE_SECRETS,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN
|
||||
} from '../../variables';
|
||||
import { serviceTokenDataController } from '../../controllers/v2';
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
PERMISSION_WRITE_SECRETS,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
} from "../../variables";
|
||||
import { serviceTokenDataController } from "../../controllers/v2";
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN]
|
||||
}),
|
||||
serviceTokenDataController.getServiceTokenData
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN],
|
||||
}),
|
||||
serviceTokenDataController.getServiceTokenData
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'body',
|
||||
locationEnvironment: 'body',
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
}),
|
||||
body('name').exists().isString().trim(),
|
||||
body('workspaceId').exists().isString().trim(),
|
||||
body('environment').exists().isString().trim(),
|
||||
body('encryptedKey').exists().isString().trim(),
|
||||
body('iv').exists().isString().trim(),
|
||||
body('tag').exists().isString().trim(),
|
||||
body('expiresIn').exists().isNumeric(), // measured in ms
|
||||
body('permissions').isArray({ min: 1 }).custom((value: string[]) => {
|
||||
const allowedPermissions = ['read', 'write'];
|
||||
const invalidValues = value.filter((v) => !allowedPermissions.includes(v));
|
||||
if (invalidValues.length > 0) {
|
||||
throw new Error(`permissions contains invalid values: ${invalidValues.join(', ')}`);
|
||||
}
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "body",
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
}),
|
||||
body("name").exists().isString().trim(),
|
||||
body("workspaceId").exists().isString().trim(),
|
||||
body("environment").exists().isString().trim(),
|
||||
body("encryptedKey").exists().isString().trim(),
|
||||
body("iv").exists().isString().trim(),
|
||||
body("secretPath").isString().default("/").trim(),
|
||||
body("tag").exists().isString().trim(),
|
||||
body("expiresIn").exists().isNumeric(), // measured in ms
|
||||
body("permissions")
|
||||
.isArray({ min: 1 })
|
||||
.custom((value: string[]) => {
|
||||
const allowedPermissions = ["read", "write"];
|
||||
const invalidValues = value.filter(
|
||||
(v) => !allowedPermissions.includes(v)
|
||||
);
|
||||
if (invalidValues.length > 0) {
|
||||
throw new Error(
|
||||
`permissions contains invalid values: ${invalidValues.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
return true
|
||||
return true;
|
||||
}),
|
||||
validateRequest,
|
||||
serviceTokenDataController.createServiceTokenData
|
||||
validateRequest,
|
||||
serviceTokenDataController.createServiceTokenData
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:serviceTokenDataId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireServiceTokenDataAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('serviceTokenDataId').exists().trim(),
|
||||
validateRequest,
|
||||
serviceTokenDataController.deleteServiceTokenData
|
||||
"/:serviceTokenDataId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
requireServiceTokenDataAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
}),
|
||||
param("serviceTokenDataId").exists().trim(),
|
||||
validateRequest,
|
||||
serviceTokenDataController.deleteServiceTokenData
|
||||
);
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
@ -1,157 +1,302 @@
|
||||
import express from 'express';
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { body, param, query } from 'express-validator';
|
||||
import { secretsController } from '../../controllers/v3';
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest,
|
||||
} from "../../middleware";
|
||||
import { body, param, query } from "express-validator";
|
||||
import { secretsController } from "../../controllers/v3";
|
||||
import {
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
PERMISSION_WRITE_SECRETS,
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
PERMISSION_READ_SECRETS
|
||||
} from '../../variables';
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
PERMISSION_WRITE_SECRETS,
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
PERMISSION_READ_SECRETS,
|
||||
} from "../../variables";
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
query('workspaceId').exists().isString().trim(),
|
||||
query('environment').exists().isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'query',
|
||||
locationEnvironment: 'query',
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
}),
|
||||
secretsController.getSecrets
|
||||
"/raw",
|
||||
query("workspaceId").exists().isString().trim(),
|
||||
query("workspaceId").exists().isString().trim(),
|
||||
query("environment").exists().isString().trim(),
|
||||
query("secretPath").default("/").isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "query",
|
||||
locationEnvironment: "query",
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true
|
||||
}),
|
||||
secretsController.getSecretsRaw
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/raw/:secretName",
|
||||
param("secretName").exists().isString().trim(),
|
||||
query("workspaceId").exists().isString().trim(),
|
||||
query("environment").exists().isString().trim(),
|
||||
query("secretPath").default("/").isString().trim(),
|
||||
query("type").optional().isIn([SECRET_SHARED, SECRET_PERSONAL]),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "query",
|
||||
locationEnvironment: "query",
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true
|
||||
}),
|
||||
secretsController.getSecretByNameRaw
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:secretName',
|
||||
body('workspaceId').exists().isString().trim(),
|
||||
body('environment').exists().isString().trim(),
|
||||
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
|
||||
body('secretKeyCiphertext').exists().isString().trim(),
|
||||
body('secretKeyIV').exists().isString().trim(),
|
||||
body('secretKeyTag').exists().isString().trim(),
|
||||
body('secretValueCiphertext').exists().isString().trim(),
|
||||
body('secretValueIV').exists().isString().trim(),
|
||||
body('secretValueTag').exists().isString().trim(),
|
||||
body('secretCommentCiphertext').optional().isString().trim(),
|
||||
body('secretCommentIV').optional().isString().trim(),
|
||||
body('secretCommentTag').optional().isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'body',
|
||||
locationEnvironment: 'body',
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
}),
|
||||
secretsController.createSecret
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:secretName',
|
||||
param('secretName').exists().isString().trim(),
|
||||
query('workspaceId').exists().isString().trim(),
|
||||
query('environment').exists().isString().trim(),
|
||||
query('type').optional().isIn([SECRET_SHARED, SECRET_PERSONAL]),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'query',
|
||||
locationEnvironment: 'query',
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
}),
|
||||
secretsController.getSecretByName
|
||||
"/raw/:secretName",
|
||||
body("workspaceId").exists().isString().trim(),
|
||||
body("environment").exists().isString().trim(),
|
||||
body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
|
||||
body("secretValue").exists().isString().trim(),
|
||||
body("secretComment").default("").isString().trim(),
|
||||
body("secretPath").default("/").isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "body",
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true
|
||||
}),
|
||||
secretsController.createSecretRaw
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:secretName',
|
||||
param('secretName').exists().isString().trim(),
|
||||
body('workspaceId').exists().isString().trim(),
|
||||
body('environment').exists().isString().trim(),
|
||||
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
|
||||
body('secretValueCiphertext').exists().isString().trim(),
|
||||
body('secretValueIV').exists().isString().trim(),
|
||||
body('secretValueTag').exists().isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'body',
|
||||
locationEnvironment: 'body',
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
}),
|
||||
secretsController.updateSecretByName
|
||||
"/raw/:secretName",
|
||||
param("secretName").exists().isString().trim(),
|
||||
body("workspaceId").exists().isString().trim(),
|
||||
body("environment").exists().isString().trim(),
|
||||
body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
|
||||
body("secretValue").exists().isString().trim(),
|
||||
body("secretPath").default("/").isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "body",
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true
|
||||
}),
|
||||
secretsController.updateSecretByNameRaw
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:secretName',
|
||||
param('secretName').exists().isString().trim(),
|
||||
body('workspaceId').exists().isString().trim(),
|
||||
body('environment').exists().isString().trim(),
|
||||
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'body',
|
||||
locationEnvironment: 'body',
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
}),
|
||||
secretsController.deleteSecretByName
|
||||
"/raw/:secretName",
|
||||
param("secretName").exists().isString().trim(),
|
||||
body("workspaceId").exists().isString().trim(),
|
||||
body("environment").exists().isString().trim(),
|
||||
body("secretPath").default("/").isString().trim(),
|
||||
body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "body",
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true
|
||||
}),
|
||||
secretsController.deleteSecretByNameRaw
|
||||
);
|
||||
|
||||
export default router;
|
||||
router.get(
|
||||
"/",
|
||||
query("workspaceId").exists().isString().trim(),
|
||||
query("environment").exists().isString().trim(),
|
||||
query("secretPath").default("/").isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "query",
|
||||
locationEnvironment: "query",
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false
|
||||
}),
|
||||
secretsController.getSecrets
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/:secretName",
|
||||
body("workspaceId").exists().isString().trim(),
|
||||
body("environment").exists().isString().trim(),
|
||||
body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
|
||||
body("secretKeyCiphertext").exists().isString().trim(),
|
||||
body("secretKeyIV").exists().isString().trim(),
|
||||
body("secretKeyTag").exists().isString().trim(),
|
||||
body("secretValueCiphertext").exists().isString().trim(),
|
||||
body("secretValueIV").exists().isString().trim(),
|
||||
body("secretValueTag").exists().isString().trim(),
|
||||
body("secretCommentCiphertext").optional().isString().trim(),
|
||||
body("secretCommentIV").optional().isString().trim(),
|
||||
body("secretCommentTag").optional().isString().trim(),
|
||||
body("secretPath").default("/").isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "body",
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false
|
||||
}),
|
||||
secretsController.createSecret
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/:secretName",
|
||||
param("secretName").exists().isString().trim(),
|
||||
query("workspaceId").exists().isString().trim(),
|
||||
query("environment").exists().isString().trim(),
|
||||
query("secretPath").default("/").isString().trim(),
|
||||
query("type").optional().isIn([SECRET_SHARED, SECRET_PERSONAL]),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "query",
|
||||
locationEnvironment: "query",
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
}),
|
||||
secretsController.getSecretByName
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:secretName",
|
||||
param("secretName").exists().isString().trim(),
|
||||
body("workspaceId").exists().isString().trim(),
|
||||
body("environment").exists().isString().trim(),
|
||||
body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
|
||||
body("secretValueCiphertext").exists().isString().trim(),
|
||||
body("secretValueIV").exists().isString().trim(),
|
||||
body("secretValueTag").exists().isString().trim(),
|
||||
body("secretPath").default("/").isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "body",
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false
|
||||
}),
|
||||
secretsController.updateSecretByName
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:secretName",
|
||||
param("secretName").exists().isString().trim(),
|
||||
body("workspaceId").exists().isString().trim(),
|
||||
body("environment").exists().isString().trim(),
|
||||
body("secretPath").default("/").isString().trim(),
|
||||
body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "body",
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false
|
||||
}),
|
||||
secretsController.deleteSecretByName
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
@ -8,10 +8,9 @@ import {
|
||||
import { workspacesController } from '../../controllers/v3';
|
||||
import {
|
||||
AUTH_MODE_JWT,
|
||||
ADMIN,
|
||||
PERMISSION_READ_SECRETS
|
||||
ADMIN
|
||||
} from '../../variables';
|
||||
import { param, body, validationResult } from 'express-validator';
|
||||
import { param, body } from 'express-validator';
|
||||
|
||||
// -- migration to blind indices endpoints
|
||||
|
||||
|
@ -2,7 +2,9 @@ import { Types } from 'mongoose';
|
||||
import {
|
||||
getSecretsBotHelper,
|
||||
encryptSymmetricHelper,
|
||||
decryptSymmetricHelper
|
||||
decryptSymmetricHelper,
|
||||
getKey,
|
||||
getIsWorkspaceE2EEHelper
|
||||
} from '../helpers/bot';
|
||||
|
||||
/**
|
||||
@ -10,6 +12,31 @@ import {
|
||||
*/
|
||||
class BotService {
|
||||
|
||||
/**
|
||||
* Return whether or not workspace with id [workspaceId] is end-to-end encrypted
|
||||
* @param workspaceId - id of workspace
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) {
|
||||
return await getIsWorkspaceE2EEHelper(workspaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workspace key for workspace with id [workspaceId] shared to bot.
|
||||
* @param {Object} obj
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get workspace key for
|
||||
* @returns
|
||||
*/
|
||||
static async getWorkspaceKeyWithBot({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
return await getKey({
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return decrypted secrets for workspace with id [workspaceId] and
|
||||
* environment [environmen] shared to bot.
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { TFolderSchema } from "../models/folder";
|
||||
import { Types } from "mongoose";
|
||||
import Folder, { TFolderSchema } from "../models/folder";
|
||||
|
||||
type TAppendFolderDTO = {
|
||||
folderName: string;
|
||||
@ -174,6 +175,11 @@ export const searchByFolderIdWithDir = (
|
||||
// to get folder of a path given
|
||||
// Like /frontend/folder#1
|
||||
export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => {
|
||||
// corner case when its just / return root
|
||||
if (searchPath === "/") {
|
||||
return folders.id === "root" ? folders : undefined;
|
||||
}
|
||||
|
||||
const path = searchPath.split("/").filter(Boolean);
|
||||
const queue = [folders];
|
||||
let segment: TFolderSchema | undefined;
|
||||
@ -187,3 +193,25 @@ export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => {
|
||||
}
|
||||
return segment;
|
||||
};
|
||||
|
||||
export const getFolderIdFromServiceToken = async (
|
||||
workspaceId: Types.ObjectId | string,
|
||||
environment: string,
|
||||
secretPath: string
|
||||
) => {
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
});
|
||||
|
||||
if (!folders) {
|
||||
if (secretPath !== "/") throw new Error("Invalid path. Folders not found");
|
||||
} else {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (!folder) {
|
||||
throw new Error("Folder not found");
|
||||
}
|
||||
return folder.id;
|
||||
}
|
||||
return "root";
|
||||
};
|
||||
|
@ -1,7 +1,4 @@
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
ISecret
|
||||
} from '../models';
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
CreateSecretParams,
|
||||
GetSecretsParams,
|
||||
@ -22,150 +19,150 @@ import {
|
||||
} from '../helpers/secrets';
|
||||
|
||||
class SecretService {
|
||||
/**
|
||||
* Create secret blind index data containing encrypted blind index salt
|
||||
* for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {Buffer} obj.salt - 16-byte random salt
|
||||
* @param {Types.ObjectId} obj.workspaceId
|
||||
*/
|
||||
static async createSecretBlindIndexData({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
return await createSecretBlindIndexDataHelper({
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create secret blind index data containing encrypted blind index salt
|
||||
* for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {Buffer} obj.salt - 16-byte random salt
|
||||
* @param {Types.ObjectId} obj.workspaceId
|
||||
*/
|
||||
static async createSecretBlindIndexData({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
return await createSecretBlindIndexDataHelper({
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get secret blind index salt for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
|
||||
* @returns
|
||||
*/
|
||||
static async getSecretBlindIndexSalt({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
return await getSecretBlindIndexSaltHelper({
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get secret blind index salt for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
|
||||
* @returns
|
||||
*/
|
||||
static async getSecretBlindIndexSalt({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
return await getSecretBlindIndexSaltHelper({
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Generate blind index for secret with name [secretName]
|
||||
* and salt [salt]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.secretName - name of secret to generate blind index for
|
||||
* @param {String} obj.salt - base64-salt
|
||||
*/
|
||||
static async generateSecretBlindIndexWithSalt({
|
||||
secretName,
|
||||
salt,
|
||||
}: {
|
||||
secretName: string;
|
||||
salt: string;
|
||||
}) {
|
||||
return await generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate blind index for secret with name [secretName]
|
||||
* and salt [salt]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.secretName - name of secret to generate blind index for
|
||||
* @param {String} obj.salt - base64-salt
|
||||
*/
|
||||
static async generateSecretBlindIndexWithSalt({
|
||||
secretName,
|
||||
salt
|
||||
}: {
|
||||
secretName: string;
|
||||
salt: string;
|
||||
}) {
|
||||
return await generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create and return blind index for secret with
|
||||
* name [secretName] part of workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.secretName - name of secret to generate blind index for
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
|
||||
*/
|
||||
static async generateSecretBlindIndex({
|
||||
secretName,
|
||||
workspaceId,
|
||||
}: {
|
||||
secretName: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
return await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return blind index for secret with
|
||||
* name [secretName] part of workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.secretName - name of secret to generate blind index for
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
|
||||
*/
|
||||
static async generateSecretBlindIndex({
|
||||
secretName,
|
||||
workspaceId,
|
||||
}: {
|
||||
secretName: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
return await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create secret with name [secretName]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.secretName - name of secret to create
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to create secret for
|
||||
* @param {String} obj.environment - environment in workspace to create secret for
|
||||
* @param {'shared' | 'personal'} obj.type - type of secret
|
||||
* @param {AuthData} obj.authData - authentication data on request
|
||||
* @returns
|
||||
*/
|
||||
static async createSecret(createSecretParams: CreateSecretParams) {
|
||||
return await createSecretHelper(createSecretParams);
|
||||
}
|
||||
/**
|
||||
* Create secret with name [secretName]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.secretName - name of secret to create
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to create secret for
|
||||
* @param {String} obj.environment - environment in workspace to create secret for
|
||||
* @param {'shared' | 'personal'} obj.type - type of secret
|
||||
* @param {AuthData} obj.authData - authentication data on request
|
||||
* @returns
|
||||
*/
|
||||
static async createSecret(createSecretParams: CreateSecretParams) {
|
||||
return await createSecretHelper(createSecretParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get secrets for workspace with id [workspaceId] and environment [environment]
|
||||
* @param {Object} obj
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.environment - environment in workspace
|
||||
* @param {AuthData} obj.authData - authentication data on request
|
||||
* @returns
|
||||
*/
|
||||
static async getSecrets(getSecretsParams: GetSecretsParams) {
|
||||
return await getSecretsHelper(getSecretsParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get secret with name [secretName]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.secretName - name of secret to get
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
|
||||
* @param {String} obj.environment - environment in workspace that secret belongs to
|
||||
* @param {'shared' | 'personal'} obj.type - type of secret
|
||||
* @param {AuthData} obj.authData - authentication data on request
|
||||
* @returns
|
||||
*/
|
||||
static async getSecret(getSecretParams: GetSecretParams) {
|
||||
return await getSecretHelper(getSecretParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update secret with name [secretName]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.secretName - name of secret to update
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
|
||||
* @param {String} obj.environment - environment in workspace that secret belongs to
|
||||
* @param {'shared' | 'personal'} obj.type - type of secret
|
||||
* @param {String} obj.secretValueCiphertext - ciphertext of secret value
|
||||
* @param {String} obj.secretValueIV - IV of secret value
|
||||
* @param {String} obj.secretValueTag - tag of secret value
|
||||
* @param {AuthData} obj.authData - authentication data on request
|
||||
* @returns
|
||||
*/
|
||||
static async updateSecret(updateSecretParams: UpdateSecretParams) {
|
||||
return await updateSecretHelper(updateSecretParams);
|
||||
}
|
||||
/**
|
||||
* Get secrets for workspace with id [workspaceId] and environment [environment]
|
||||
* @param {Object} obj
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.environment - environment in workspace
|
||||
* @param {AuthData} obj.authData - authentication data on request
|
||||
* @returns
|
||||
*/
|
||||
static async getSecrets(getSecretsParams: GetSecretsParams) {
|
||||
return await getSecretsHelper(getSecretsParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete secret with name [secretName]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.secretName - name of secret to delete
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
|
||||
* @param {String} obj.environment - environment in workspace that secret belongs to
|
||||
* @param {'shared' | 'personal'} obj.type - type of secret
|
||||
* @param {AuthData} obj.authData - authentication data on request
|
||||
* @returns
|
||||
*/
|
||||
static async deleteSecret(deleteSecretParams: DeleteSecretParams) {
|
||||
return await deleteSecretHelper(deleteSecretParams);
|
||||
}
|
||||
/**
|
||||
* Get secret with name [secretName]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.secretName - name of secret to get
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
|
||||
* @param {String} obj.environment - environment in workspace that secret belongs to
|
||||
* @param {'shared' | 'personal'} obj.type - type of secret
|
||||
* @param {AuthData} obj.authData - authentication data on request
|
||||
* @returns
|
||||
*/
|
||||
static async getSecret(getSecretParams: GetSecretParams) {
|
||||
// TODO(akhilmhdh) The one above is diff. Change this to some other name
|
||||
return await getSecretHelper(getSecretParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update secret with name [secretName]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.secretName - name of secret to update
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
|
||||
* @param {String} obj.environment - environment in workspace that secret belongs to
|
||||
* @param {'shared' | 'personal'} obj.type - type of secret
|
||||
* @param {String} obj.secretValueCiphertext - ciphertext of secret value
|
||||
* @param {String} obj.secretValueIV - IV of secret value
|
||||
* @param {String} obj.secretValueTag - tag of secret value
|
||||
* @param {AuthData} obj.authData - authentication data on request
|
||||
* @returns
|
||||
*/
|
||||
static async updateSecret(updateSecretParams: UpdateSecretParams) {
|
||||
return await updateSecretHelper(updateSecretParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete secret with name [secretName]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.secretName - name of secret to delete
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
|
||||
* @param {String} obj.environment - environment in workspace that secret belongs to
|
||||
* @param {'shared' | 'personal'} obj.type - type of secret
|
||||
* @param {AuthData} obj.authData - authentication data on request
|
||||
* @returns
|
||||
*/
|
||||
static async deleteSecret(deleteSecretParams: DeleteSecretParams) {
|
||||
return await deleteSecretHelper(deleteSecretParams);
|
||||
}
|
||||
}
|
||||
|
||||
export default SecretService;
|
||||
export default SecretService;
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
import crypto from "crypto";
|
||||
import { Types } from "mongoose";
|
||||
import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto";
|
||||
@ -11,6 +12,7 @@ import {
|
||||
Bot,
|
||||
BackupPrivateKey,
|
||||
IntegrationAuth,
|
||||
ServiceTokenData,
|
||||
} from "../../models";
|
||||
import { generateKeyPair } from "../../utils/crypto";
|
||||
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
|
||||
@ -64,7 +66,7 @@ export const backfillSecretVersions = async () => {
|
||||
),
|
||||
});
|
||||
}
|
||||
console.log("Migration: Secret version migration v1 complete")
|
||||
console.log("Migration: Secret version migration v1 complete");
|
||||
};
|
||||
|
||||
/**
|
||||
@ -380,13 +382,15 @@ export const backfillSecretFolders = async () => {
|
||||
});
|
||||
|
||||
const newSnapshots = Object.keys(groupSnapByEnv).map((snapEnv) => {
|
||||
const secretIdsOfEnvGroup = groupSnapByEnv[snapEnv] ? groupSnapByEnv[snapEnv].map(secretVersion => secretVersion._id) : []
|
||||
const secretIdsOfEnvGroup = groupSnapByEnv[snapEnv]
|
||||
? groupSnapByEnv[snapEnv].map((secretVersion) => secretVersion._id)
|
||||
: [];
|
||||
return {
|
||||
...secSnapshot.toObject({ virtuals: false }),
|
||||
_id: new Types.ObjectId(),
|
||||
environment: snapEnv,
|
||||
secretVersions: secretIdsOfEnvGroup,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
await SecretSnapshot.insertMany(newSnapshots);
|
||||
@ -402,5 +406,21 @@ export const backfillSecretFolders = async () => {
|
||||
.limit(50);
|
||||
}
|
||||
|
||||
console.log("Migration: Folder migration v1 complete")
|
||||
console.log("Migration: Folder migration v1 complete");
|
||||
};
|
||||
|
||||
export const backfillServiceToken = async () => {
|
||||
await ServiceTokenData.updateMany(
|
||||
{
|
||||
secretPath: {
|
||||
$exists: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
secretPath: "/",
|
||||
},
|
||||
}
|
||||
);
|
||||
console.log("Migration: Service token migration v1 complete");
|
||||
};
|
||||
|
@ -1,30 +1,31 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { DatabaseService, TelemetryService } from '../../services';
|
||||
import { setTransporter } from '../../helpers/nodemailer';
|
||||
import { EELicenseService } from '../../ee/services';
|
||||
import { initSmtp } from '../../services/smtp';
|
||||
import { createTestUserForDevelopment } from '../addDevelopmentUser';
|
||||
import * as Sentry from "@sentry/node";
|
||||
import { DatabaseService, TelemetryService } from "../../services";
|
||||
import { setTransporter } from "../../helpers/nodemailer";
|
||||
import { EELicenseService } from "../../ee/services";
|
||||
import { initSmtp } from "../../services/smtp";
|
||||
import { createTestUserForDevelopment } from "../addDevelopmentUser";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
import { validateEncryptionKeysConfig } from './validateConfig';
|
||||
import { validateEncryptionKeysConfig } from "./validateConfig";
|
||||
import {
|
||||
backfillSecretVersions,
|
||||
backfillBots,
|
||||
backfillSecretBlindIndexData,
|
||||
backfillEncryptionMetadata,
|
||||
backfillSecretFolders,
|
||||
} from './backfillData';
|
||||
backfillServiceToken,
|
||||
} from "./backfillData";
|
||||
import {
|
||||
reencryptBotPrivateKeys,
|
||||
reencryptSecretBlindIndexDataSalts,
|
||||
} from './reencryptData';
|
||||
} from "./reencryptData";
|
||||
import {
|
||||
getNodeEnv,
|
||||
getMongoURL,
|
||||
getSentryDSN,
|
||||
getClientSecretGoogle,
|
||||
getClientIdGoogle,
|
||||
} from '../../config';
|
||||
import { initializePassport } from '../auth';
|
||||
} from "../../config";
|
||||
import { initializePassport } from "../auth";
|
||||
|
||||
/**
|
||||
* Prepare Infisical upon startup. This includes tasks like:
|
||||
@ -75,6 +76,7 @@ export const setup = async () => {
|
||||
await backfillSecretBlindIndexData();
|
||||
await backfillEncryptionMetadata();
|
||||
await backfillSecretFolders();
|
||||
await backfillServiceToken();
|
||||
|
||||
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
||||
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||
@ -85,7 +87,7 @@ export const setup = async () => {
|
||||
Sentry.init({
|
||||
dsn: await getSentryDSN(),
|
||||
tracesSampleRate: 1.0,
|
||||
debug: (await getNodeEnv()) === 'production' ? false : true,
|
||||
debug: (await getNodeEnv()) === "production" ? false : true,
|
||||
environment: await getNodeEnv(),
|
||||
});
|
||||
|
||||
|
@ -13,6 +13,7 @@ import { validateServiceAccountClientForWorkspace } from './serviceAccount';
|
||||
import { validateUserClientForWorkspace } from './user';
|
||||
import { validateServiceTokenDataClientForWorkspace } from './serviceTokenData';
|
||||
import {
|
||||
BadRequestError,
|
||||
UnauthorizedRequestError,
|
||||
WorkspaceNotFoundError
|
||||
} from '../utils/errors';
|
||||
@ -22,6 +23,7 @@ import {
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../variables';
|
||||
import { BotService } from '../services';
|
||||
|
||||
/**
|
||||
* Validate authenticated clients for workspace with id [workspaceId] based
|
||||
@ -39,7 +41,8 @@ export const validateClientForWorkspace = async ({
|
||||
environment,
|
||||
acceptedRoles,
|
||||
requiredPermissions,
|
||||
requireBlindIndicesEnabled
|
||||
requireBlindIndicesEnabled,
|
||||
requireE2EEOff
|
||||
}: {
|
||||
authData: {
|
||||
authMode: string;
|
||||
@ -50,6 +53,7 @@ export const validateClientForWorkspace = async ({
|
||||
acceptedRoles: Array<'admin' | 'member'>;
|
||||
requiredPermissions?: string[];
|
||||
requireBlindIndicesEnabled: boolean;
|
||||
requireE2EEOff: boolean;
|
||||
}) => {
|
||||
const workspace = await Workspace.findById(workspaceId);
|
||||
|
||||
@ -70,6 +74,14 @@ export const validateClientForWorkspace = async ({
|
||||
message: 'Failed workspace authorization due to blind indices not being enabled'
|
||||
});
|
||||
}
|
||||
|
||||
if (requireE2EEOff) {
|
||||
const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId);
|
||||
|
||||
if (isWorkspaceE2EE) throw BadRequestError({
|
||||
message: 'Failed workspace authorization due to end-to-end encryption not being disabled'
|
||||
});
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||
const membership = await validateUserClientForWorkspace({
|
||||
|
@ -5,10 +5,9 @@ description: "How to authenticate with the Infisical Public API"
|
||||
|
||||
## Essentials
|
||||
|
||||
The Public API accepts multiple modes of authentication being via API Key, Service Account credentials, or [Infisical Token](/documentation/platform/token).
|
||||
The Public API accepts multiple modes of authentication being via API Key or [Infisical Token](/documentation/platform/token).
|
||||
|
||||
- API Key: Provides full access to all endpoints representing the user.
|
||||
- Service Account: Provides scoped access to an organization and select projects representing a machine such as a VM or application client.
|
||||
- API Key: Provides full access to all endpoints representing the user without ability to encrypt/decrypt secrets in **E2EE** mode.
|
||||
- [Infisical Token](/documentation/platform/token): Provides short-lived, scoped CRUD access to the secrets of a specific project and environment.
|
||||
|
||||
<AccordionGroup>
|
||||
@ -21,14 +20,6 @@ You can obtain an API key in User Settings > API Keys
|
||||
|
||||

|
||||

|
||||
</Accordion>
|
||||
<Accordion title="Service Account">
|
||||
The Service Account mode uses an Access Key to authenticate with the API and a Public Key and Private Key to perform any cryptographic operations.
|
||||
|
||||
To authenticate requests with Infisical using the Access Key, you must include it in the `Authorization` header of HTTP requests made to the platform with the value `Bearer <access_key>`.
|
||||
|
||||
You can create a Service Account in Organization Settings > Service Accounts
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Infisical Token">
|
||||
|
||||
@ -40,12 +31,4 @@ You can obtain an Infisical Token in Project Settings > Service Tokens.
|
||||
|
||||

|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Use Cases
|
||||
|
||||
Depending on your use case, it may make sense to use one or another authentication mode:
|
||||
|
||||
- API Key (not recommended): Use if you need full access to the Public API without needing to access any secrets endpoints (because API keys can't encrypt/decrypt secrets).
|
||||
- Service Account (recommeded): Use if you need access to multiple projects and environments in an organization; service accounts can generate short-lived access tokens, making them useful for some complex setups.
|
||||
- Service Token (recommeded): Use if you need short-lived, scoped CRUD access to the secrets of a specific project and environment.
|
||||
</AccordionGroup>
|
861
docs/api-reference/overview/encryption-modes/e2ee-mode.mdx
Normal file
861
docs/api-reference/overview/encryption-modes/e2ee-mode.mdx
Normal file
@ -0,0 +1,861 @@
|
||||
---
|
||||
title: "E2EE Mode"
|
||||
---
|
||||
|
||||
End-to-End Encrypted (E2EE) mode is the default way to use Infisical's API. With it, you must perform client-side encryption/decryption
|
||||
when reading/writing secrets via HTTP call to Infisical.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com).
|
||||
- Create an [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled.
|
||||
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
|
||||
- [Ensure that your project is blind-indexed](../blind-indices).
|
||||
|
||||
Below, we showcase how to execute common CRUD operations to manage secrets in **E2EE** mode:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Retrieve secrets">
|
||||
<Tabs>
|
||||
<Tab title="Javascript">
|
||||
```js
|
||||
const crypto = require('crypto');
|
||||
const axios = require('axios');
|
||||
|
||||
const BASE_URL = 'https://app.infisical.com';
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
|
||||
const decrypt = ({ ciphertext, iv, tag, secret}) => {
|
||||
const decipher = crypto.createDecipheriv(
|
||||
ALGORITHM,
|
||||
secret,
|
||||
Buffer.from(iv, 'base64')
|
||||
);
|
||||
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
||||
|
||||
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
||||
cleartext += decipher.final('utf8');
|
||||
|
||||
return cleartext;
|
||||
}
|
||||
|
||||
const getSecrets = async () => {
|
||||
const serviceToken = 'your_service_token';
|
||||
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
|
||||
|
||||
// 1. Get your Infisical Token data
|
||||
const { data: serviceTokenData } = await axios.get(
|
||||
`${BASE_URL}/api/v2/service-token`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 2. Get secrets for your project and environment
|
||||
const { data } = await axios.get(
|
||||
`${BASE_URL}/api/v3/secrets?${new URLSearchParams({
|
||||
environment: serviceTokenData.environment,
|
||||
workspaceId: serviceTokenData.workspace
|
||||
})}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const encryptedSecrets = data.secrets;
|
||||
|
||||
// 3. Decrypt the (encrypted) project key with the key from your Infisical Token
|
||||
const projectKey = decrypt({
|
||||
ciphertext: serviceTokenData.encryptedKey,
|
||||
iv: serviceTokenData.iv,
|
||||
tag: serviceTokenData.tag,
|
||||
secret: serviceTokenSecret
|
||||
});
|
||||
|
||||
// 4. Decrypt the (encrypted) secrets
|
||||
const secrets = encryptedSecrets.map((secret) => {
|
||||
const secretKey = decrypt({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
const secretValue = decrypt({
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
return ({
|
||||
secretKey,
|
||||
secretValue
|
||||
});
|
||||
});
|
||||
|
||||
console.log('secrets: ', secrets);
|
||||
}
|
||||
|
||||
getSecrets();
|
||||
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Python">
|
||||
```Python
|
||||
import requests
|
||||
import base64
|
||||
from Cryptodome.Cipher import AES
|
||||
|
||||
|
||||
BASE_URL = "http://app.infisical.com"
|
||||
|
||||
|
||||
def decrypt(ciphertext, iv, tag, secret):
|
||||
secret = bytes(secret, "utf-8")
|
||||
iv = base64.standard_b64decode(iv)
|
||||
tag = base64.standard_b64decode(tag)
|
||||
ciphertext = base64.standard_b64decode(ciphertext)
|
||||
|
||||
cipher = AES.new(secret, AES.MODE_GCM, iv)
|
||||
cipher.update(tag)
|
||||
cleartext = cipher.decrypt(ciphertext).decode("utf-8")
|
||||
return cleartext
|
||||
|
||||
|
||||
def get_secrets():
|
||||
service_token = "your_service_token"
|
||||
service_token_secret = service_token[service_token.rindex(".") + 1 :]
|
||||
|
||||
# 1. Get your Infisical Token data
|
||||
service_token_data = requests.get(
|
||||
f"{BASE_URL}/api/v2/service-token",
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
).json()
|
||||
|
||||
# 2. Get secrets for your project and environment
|
||||
data = requests.get(
|
||||
f"{BASE_URL}/api/v3/secrets",
|
||||
params={
|
||||
"environment": service_token_data["environment"],
|
||||
"workspaceId": service_token_data["workspace"],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
).json()
|
||||
|
||||
encrypted_secrets = data["secrets"]
|
||||
|
||||
# 3. Decrypt the (encrypted) project key with the key from your Infisical Token
|
||||
project_key = decrypt(
|
||||
ciphertext=service_token_data["encryptedKey"],
|
||||
iv=service_token_data["iv"],
|
||||
tag=service_token_data["tag"],
|
||||
secret=service_token_secret,
|
||||
)
|
||||
|
||||
# 4. Decrypt the (encrypted) secrets
|
||||
secrets = []
|
||||
for secret in encrypted_secrets:
|
||||
secret_key = decrypt(
|
||||
ciphertext=secret["secretKeyCiphertext"],
|
||||
iv=secret["secretKeyIV"],
|
||||
tag=secret["secretKeyTag"],
|
||||
secret=project_key,
|
||||
)
|
||||
|
||||
secret_value = decrypt(
|
||||
ciphertext=secret["secretValueCiphertext"],
|
||||
iv=secret["secretValueIV"],
|
||||
tag=secret["secretValueTag"],
|
||||
secret=project_key,
|
||||
)
|
||||
|
||||
secrets.append(
|
||||
{
|
||||
"secret_key": secret_key,
|
||||
"secret_value": secret_value,
|
||||
}
|
||||
)
|
||||
|
||||
print("secrets:", secrets)
|
||||
|
||||
|
||||
get_secrets()
|
||||
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Accordion>
|
||||
<Accordion title="Create secret">
|
||||
<Tabs>
|
||||
<Tab title="Javascript">
|
||||
```js
|
||||
const crypto = require('crypto');
|
||||
const axios = require('axios');
|
||||
const nacl = require('tweetnacl');
|
||||
|
||||
const BASE_URL = 'https://app.infisical.com';
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const BLOCK_SIZE_BYTES = 16;
|
||||
|
||||
const encrypt = ({ text, secret }) => {
|
||||
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
|
||||
|
||||
let ciphertext = cipher.update(text, 'utf8', 'base64');
|
||||
ciphertext += cipher.final('base64');
|
||||
return {
|
||||
ciphertext,
|
||||
iv: iv.toString('base64'),
|
||||
tag: cipher.getAuthTag().toString('base64')
|
||||
};
|
||||
}
|
||||
|
||||
const decrypt = ({ ciphertext, iv, tag, secret}) => {
|
||||
const decipher = crypto.createDecipheriv(
|
||||
ALGORITHM,
|
||||
secret,
|
||||
Buffer.from(iv, 'base64')
|
||||
);
|
||||
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
||||
|
||||
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
||||
cleartext += decipher.final('utf8');
|
||||
|
||||
return cleartext;
|
||||
}
|
||||
|
||||
const createSecrets = async () => {
|
||||
const serviceToken = '';
|
||||
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
|
||||
|
||||
const secretType = 'shared'; // 'shared' or 'personal'
|
||||
const secretKey = 'some_key';
|
||||
const secretValue = 'some_value';
|
||||
const secretComment = 'some_comment';
|
||||
|
||||
// 1. Get your Infisical Token data
|
||||
const { data: serviceTokenData } = await axios.get(
|
||||
`${BASE_URL}/api/v2/service-token`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 2. Decrypt the (encrypted) project key with the key from your Infisical Token
|
||||
const projectKey = decrypt({
|
||||
ciphertext: serviceTokenData.encryptedKey,
|
||||
iv: serviceTokenData.iv,
|
||||
tag: serviceTokenData.tag,
|
||||
secret: serviceTokenSecret
|
||||
});
|
||||
|
||||
// 3. Encrypt your secret with the project key
|
||||
const {
|
||||
ciphertext: secretKeyCiphertext,
|
||||
iv: secretKeyIV,
|
||||
tag: secretKeyTag
|
||||
} = encrypt({
|
||||
text: secretKey,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
const {
|
||||
ciphertext: secretValueCiphertext,
|
||||
iv: secretValueIV,
|
||||
tag: secretValueTag
|
||||
} = encrypt({
|
||||
text: secretValue,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
const {
|
||||
ciphertext: secretCommentCiphertext,
|
||||
iv: secretCommentIV,
|
||||
tag: secretCommentTag
|
||||
} = encrypt({
|
||||
text: secretComment,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
// 4. Send (encrypted) secret to Infisical
|
||||
await axios.post(
|
||||
`${BASE_URL}/api/v3/secrets/${secretKey}`,
|
||||
{
|
||||
workspaceId: serviceTokenData.workspace,
|
||||
environment: serviceTokenData.environment,
|
||||
type: secretType,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createSecrets();
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Python">
|
||||
```Python
|
||||
import base64
|
||||
import requests
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Random import get_random_bytes
|
||||
|
||||
|
||||
BASE_URL = "https://app.infisical.com"
|
||||
BLOCK_SIZE_BYTES = 16
|
||||
|
||||
|
||||
def encrypt(text, secret):
|
||||
iv = get_random_bytes(BLOCK_SIZE_BYTES)
|
||||
secret = bytes(secret, "utf-8")
|
||||
cipher = AES.new(secret, AES.MODE_GCM, iv)
|
||||
ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8"))
|
||||
return {
|
||||
"ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"),
|
||||
"tag": base64.standard_b64encode(tag).decode("utf-8"),
|
||||
"iv": base64.standard_b64encode(iv).decode("utf-8"),
|
||||
}
|
||||
|
||||
|
||||
def decrypt(ciphertext, iv, tag, secret):
|
||||
secret = bytes(secret, "utf-8")
|
||||
iv = base64.standard_b64decode(iv)
|
||||
tag = base64.standard_b64decode(tag)
|
||||
ciphertext = base64.standard_b64decode(ciphertext)
|
||||
|
||||
cipher = AES.new(secret, AES.MODE_GCM, iv)
|
||||
cipher.update(tag)
|
||||
cleartext = cipher.decrypt(ciphertext).decode("utf-8")
|
||||
return cleartext
|
||||
|
||||
|
||||
def create_secrets():
|
||||
service_token = "your_service_token"
|
||||
service_token_secret = service_token[service_token.rindex(".") + 1 :]
|
||||
|
||||
secret_type = "shared" # "shared or "personal"
|
||||
secret_key = "some_key"
|
||||
secret_value = "some_value"
|
||||
secret_comment = "some_comment"
|
||||
|
||||
# 1. Get your Infisical Token data
|
||||
service_token_data = requests.get(
|
||||
f"{BASE_URL}/api/v2/service-token",
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
).json()
|
||||
|
||||
# 2. Decrypt the (encrypted) project key with the key from your Infisical Token
|
||||
project_key = decrypt(
|
||||
ciphertext=service_token_data["encryptedKey"],
|
||||
iv=service_token_data["iv"],
|
||||
tag=service_token_data["tag"],
|
||||
secret=service_token_secret,
|
||||
)
|
||||
|
||||
# 3. Encrypt your secret with the project key
|
||||
encrypted_key_data = encrypt(text=secret_key, secret=project_key)
|
||||
encrypted_value_data = encrypt(text=secret_value, secret=project_key)
|
||||
encrypted_comment_data = encrypt(text=secret_comment, secret=project_key)
|
||||
|
||||
# 4. Send (encrypted) secret to Infisical
|
||||
requests.post(
|
||||
f"{BASE_URL}/api/v3/secrets/{secret_key}",
|
||||
json={
|
||||
"workspaceId": service_token_data["workspace"],
|
||||
"environment": service_token_data["environment"],
|
||||
"type": secret_type,
|
||||
"secretKeyCiphertext": encrypted_key_data["ciphertext"],
|
||||
"secretKeyIV": encrypted_key_data["iv"],
|
||||
"secretKeyTag": encrypted_key_data["tag"],
|
||||
"secretValueCiphertext": encrypted_value_data["ciphertext"],
|
||||
"secretValueIV": encrypted_value_data["iv"],
|
||||
"secretValueTag": encrypted_value_data["tag"],
|
||||
"secretCommentCiphertext": encrypted_comment_data["ciphertext"],
|
||||
"secretCommentIV": encrypted_comment_data["iv"],
|
||||
"secretCommentTag": encrypted_comment_data["tag"]
|
||||
},
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
)
|
||||
|
||||
|
||||
create_secrets()
|
||||
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Accordion>
|
||||
<Accordion title="Retrieve secret">
|
||||
<Tabs>
|
||||
<Tab title="Javascript">
|
||||
```js
|
||||
const crypto = require('crypto');
|
||||
const axios = require('axios');
|
||||
|
||||
const BASE_URL = 'https://app.infisical.com';
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
|
||||
const decrypt = ({ ciphertext, iv, tag, secret}) => {
|
||||
const decipher = crypto.createDecipheriv(
|
||||
ALGORITHM,
|
||||
secret,
|
||||
Buffer.from(iv, 'base64')
|
||||
);
|
||||
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
||||
|
||||
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
||||
cleartext += decipher.final('utf8');
|
||||
|
||||
return cleartext;
|
||||
}
|
||||
|
||||
const getSecret = async () => {
|
||||
const serviceToken = 'your_service_token';
|
||||
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
|
||||
|
||||
const secretType = 'shared' // 'shared' or 'personal'
|
||||
const secretKey = 'some_key';
|
||||
|
||||
// 1. Get your Infisical Token data
|
||||
const { data: serviceTokenData } = await axios.get(
|
||||
`${BASE_URL}/api/v2/service-token`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 2. Get the secret from your project and environment
|
||||
const { data } = await axios.get(
|
||||
`${BASE_URL}/api/v3/secrets/${secretKey}?${new URLSearchParams({
|
||||
environment: serviceTokenData.environment,
|
||||
workspaceId: serviceTokenData.workspace,
|
||||
type: secretType // optional, defaults to 'shared'
|
||||
})}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const encryptedSecret = data.secret;
|
||||
|
||||
// 3. Decrypt the (encrypted) project key with the key from your Infisical Token
|
||||
const projectKey = decrypt({
|
||||
ciphertext: serviceTokenData.encryptedKey,
|
||||
iv: serviceTokenData.iv,
|
||||
tag: serviceTokenData.tag,
|
||||
secret: serviceTokenSecret
|
||||
});
|
||||
|
||||
// 4. Decrypt the (encrypted) secret value
|
||||
|
||||
const secretValue = decrypt({
|
||||
ciphertext: encryptedSecret.secretValueCiphertext,
|
||||
iv: encryptedSecret.secretValueIV,
|
||||
tag: encryptedSecret.secretValueTag,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
console.log('secret: ', ({
|
||||
secretKey,
|
||||
secretValue
|
||||
}));
|
||||
}
|
||||
|
||||
getSecret();
|
||||
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Python">
|
||||
```Python
|
||||
import requests
|
||||
import base64
|
||||
from Cryptodome.Cipher import AES
|
||||
|
||||
|
||||
BASE_URL = "http://app.infisical.com"
|
||||
|
||||
|
||||
def decrypt(ciphertext, iv, tag, secret):
|
||||
secret = bytes(secret, "utf-8")
|
||||
iv = base64.standard_b64decode(iv)
|
||||
tag = base64.standard_b64decode(tag)
|
||||
ciphertext = base64.standard_b64decode(ciphertext)
|
||||
|
||||
cipher = AES.new(secret, AES.MODE_GCM, iv)
|
||||
cipher.update(tag)
|
||||
cleartext = cipher.decrypt(ciphertext).decode("utf-8")
|
||||
return cleartext
|
||||
|
||||
|
||||
def get_secret():
|
||||
service_token = "your_service_token"
|
||||
service_token_secret = service_token[service_token.rindex(".") + 1 :]
|
||||
|
||||
secret_type = "shared" # "shared" or "personal"
|
||||
secret_key = "some_key"
|
||||
|
||||
# 1. Get your Infisical Token data
|
||||
service_token_data = requests.get(
|
||||
f"{BASE_URL}/api/v2/service-token",
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
).json()
|
||||
|
||||
# 2. Get secret from your project and environment
|
||||
data = requests.get(
|
||||
f"{BASE_URL}/api/v3/secrets/{secret_key}",
|
||||
params={
|
||||
"environment": service_token_data["environment"],
|
||||
"workspaceId": service_token_data["workspace"],
|
||||
"type": secret_type # optional, defaults to "shared"
|
||||
},
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
).json()
|
||||
|
||||
encrypted_secret = data["secret"]
|
||||
|
||||
# 3. Decrypt the (encrypted) project key with the key from your Infisical Token
|
||||
project_key = decrypt(
|
||||
ciphertext=service_token_data["encryptedKey"],
|
||||
iv=service_token_data["iv"],
|
||||
tag=service_token_data["tag"],
|
||||
secret=service_token_secret,
|
||||
)
|
||||
|
||||
# 4. Decrypt the (encrypted) secret value
|
||||
secret_value = decrypt(
|
||||
ciphertext=encrypted_secret["secretValueCiphertext"],
|
||||
iv=encrypted_secret["secretValueIV"],
|
||||
tag=encrypted_secret["secretValueTag"],
|
||||
secret=project_key,
|
||||
)
|
||||
|
||||
print("secret: ", {
|
||||
"secret_key": secret_key,
|
||||
"secret_value": secret_value
|
||||
})
|
||||
|
||||
|
||||
get_secret()
|
||||
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Accordion>
|
||||
<Accordion title="Update secret">
|
||||
<Tabs>
|
||||
<Tab title="Javascript">
|
||||
```js
|
||||
const crypto = require('crypto');
|
||||
const axios = require('axios');
|
||||
|
||||
const BASE_URL = 'https://app.infisical.com';
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const BLOCK_SIZE_BYTES = 16;
|
||||
|
||||
const encrypt = ({ text, secret }) => {
|
||||
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
|
||||
|
||||
let ciphertext = cipher.update(text, 'utf8', 'base64');
|
||||
ciphertext += cipher.final('base64');
|
||||
return {
|
||||
ciphertext,
|
||||
iv: iv.toString('base64'),
|
||||
tag: cipher.getAuthTag().toString('base64')
|
||||
};
|
||||
}
|
||||
|
||||
const decrypt = ({ ciphertext, iv, tag, secret}) => {
|
||||
const decipher = crypto.createDecipheriv(
|
||||
ALGORITHM,
|
||||
secret,
|
||||
Buffer.from(iv, 'base64')
|
||||
);
|
||||
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
||||
|
||||
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
||||
cleartext += decipher.final('utf8');
|
||||
|
||||
return cleartext;
|
||||
}
|
||||
|
||||
const updateSecrets = async () => {
|
||||
const serviceToken = 'your_service_token';
|
||||
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
|
||||
|
||||
const secretType = 'shared' // 'shared' or 'personal'
|
||||
const secretKey = 'some_key';
|
||||
const secretValue = 'updated_value';
|
||||
const secretComment = 'updated_comment';
|
||||
|
||||
// 1. Get your Infisical Token data
|
||||
const { data: serviceTokenData } = await axios.get(
|
||||
`${BASE_URL}/api/v2/service-token`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 2. Decrypt the (encrypted) project key with the key from your Infisical Token
|
||||
const projectKey = decrypt({
|
||||
ciphertext: serviceTokenData.encryptedKey,
|
||||
iv: serviceTokenData.iv,
|
||||
tag: serviceTokenData.tag,
|
||||
secret: serviceTokenSecret
|
||||
});
|
||||
|
||||
// 3. Encrypt your updated secret with the project key
|
||||
const {
|
||||
ciphertext: secretKeyCiphertext,
|
||||
iv: secretKeyIV,
|
||||
tag: secretKeyTag
|
||||
} = encrypt({
|
||||
text: secretKey,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
const {
|
||||
ciphertext: secretValueCiphertext,
|
||||
iv: secretValueIV,
|
||||
tag: secretValueTag
|
||||
} = encrypt({
|
||||
text: secretValue,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
const {
|
||||
ciphertext: secretCommentCiphertext,
|
||||
iv: secretCommentIV,
|
||||
tag: secretCommentTag
|
||||
} = encrypt({
|
||||
text: secretComment,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
// 4. Send (encrypted) updated secret to Infisical
|
||||
await axios.patch(
|
||||
`${BASE_URL}/api/v3/secrets/${secretKey}`,
|
||||
{
|
||||
workspaceId: serviceTokenData.workspace,
|
||||
environment: serviceTokenData.environment,
|
||||
type: secretType,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updateSecrets();
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Python">
|
||||
```Python
|
||||
import base64
|
||||
import requests
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Random import get_random_bytes
|
||||
|
||||
|
||||
BASE_URL = "https://app.infisical.com"
|
||||
BLOCK_SIZE_BYTES = 16
|
||||
|
||||
|
||||
def encrypt(text, secret):
|
||||
iv = get_random_bytes(BLOCK_SIZE_BYTES)
|
||||
secret = bytes(secret, "utf-8")
|
||||
cipher = AES.new(secret, AES.MODE_GCM, iv)
|
||||
ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8"))
|
||||
return {
|
||||
"ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"),
|
||||
"tag": base64.standard_b64encode(tag).decode("utf-8"),
|
||||
"iv": base64.standard_b64encode(iv).decode("utf-8"),
|
||||
}
|
||||
|
||||
|
||||
def decrypt(ciphertext, iv, tag, secret):
|
||||
secret = bytes(secret, "utf-8")
|
||||
iv = base64.standard_b64decode(iv)
|
||||
tag = base64.standard_b64decode(tag)
|
||||
ciphertext = base64.standard_b64decode(ciphertext)
|
||||
|
||||
cipher = AES.new(secret, AES.MODE_GCM, iv)
|
||||
cipher.update(tag)
|
||||
cleartext = cipher.decrypt(ciphertext).decode("utf-8")
|
||||
return cleartext
|
||||
|
||||
|
||||
def update_secret():
|
||||
service_token = "your_service_token"
|
||||
service_token_secret = service_token[service_token.rindex(".") + 1 :]
|
||||
|
||||
secret_type = "shared" # "shared" or "personal"
|
||||
secret_key = "some_key"
|
||||
secret_value = "updated_value"
|
||||
secret_comment = "updated_comment"
|
||||
|
||||
# 1. Get your Infisical Token data
|
||||
service_token_data = requests.get(
|
||||
f"{BASE_URL}/api/v2/service-token",
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
).json()
|
||||
|
||||
# 2. Decrypt the (encrypted) project key with the key from your Infisical Token
|
||||
project_key = decrypt(
|
||||
ciphertext=service_token_data["encryptedKey"],
|
||||
iv=service_token_data["iv"],
|
||||
tag=service_token_data["tag"],
|
||||
secret=service_token_secret,
|
||||
)
|
||||
|
||||
# 3. Encrypt your updated secret with the project key
|
||||
encrypted_key_data = encrypt(text=secret_key, secret=project_key)
|
||||
encrypted_value_data = encrypt(text=secret_value, secret=project_key)
|
||||
encrypted_comment_data = encrypt(text=secret_comment, secret=project_key)
|
||||
|
||||
# 4. Send (encrypted) updated secret to Infisical
|
||||
requests.patch(
|
||||
f"{BASE_URL}/api/v3/secrets/{secret_key}",
|
||||
json={
|
||||
"workspaceId": service_token_data["workspace"],
|
||||
"environment": service_token_data["environment"],
|
||||
"type": secret_type,
|
||||
"secretKeyCiphertext": encrypted_key_data["ciphertext"],
|
||||
"secretKeyIV": encrypted_key_data["iv"],
|
||||
"secretKeyTag": encrypted_key_data["tag"],
|
||||
"secretValueCiphertext": encrypted_value_data["ciphertext"],
|
||||
"secretValueIV": encrypted_value_data["iv"],
|
||||
"secretValueTag": encrypted_value_data["tag"],
|
||||
"secretCommentCiphertext": encrypted_comment_data["ciphertext"],
|
||||
"secretCommentIV": encrypted_comment_data["iv"],
|
||||
"secretCommentTag": encrypted_comment_data["tag"]
|
||||
},
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
)
|
||||
|
||||
|
||||
update_secret()
|
||||
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Accordion>
|
||||
<Accordion title="Delete secret">
|
||||
<Tabs>
|
||||
<Tab title="Javascript">
|
||||
```js
|
||||
const axios = require('axios');
|
||||
const BASE_URL = 'https://app.infisical.com';
|
||||
|
||||
const deleteSecrets = async () => {
|
||||
const serviceToken = 'your_service_token';
|
||||
const secretType = 'shared' // 'shared' or 'personal'
|
||||
const secretKey = 'some_key'
|
||||
|
||||
// 1. Get your Infisical Token data
|
||||
const { data: serviceTokenData } = await axios.get(
|
||||
`${BASE_URL}/api/v2/service-token`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 2. Delete secret from Infisical
|
||||
await axios.delete(
|
||||
`${BASE_URL}/api/v3/secrets/${secretKey}`,
|
||||
{
|
||||
workspaceId: serviceTokenData.workspace,
|
||||
environment: serviceTokenData.environment,
|
||||
type: secretType
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
deleteSecrets();
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Python">
|
||||
```Python
|
||||
import requests
|
||||
|
||||
BASE_URL = "https://app.infisical.com"
|
||||
|
||||
|
||||
def delete_secrets():
|
||||
service_token = "<your_service_token>"
|
||||
secret_type = "shared" # "shared" or "personal"
|
||||
secret_key = "some_key"
|
||||
|
||||
# 1. Get your Infisical Token data
|
||||
service_token_data = requests.get(
|
||||
f"{BASE_URL}/api/v2/service-token",
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
).json()
|
||||
|
||||
# 2. Delete secret from Infisical
|
||||
requests.delete(
|
||||
f"{BASE_URL}/api/v2/secrets/{secret_key}",
|
||||
json={
|
||||
"workspaceId": service_token_data["workspace"],
|
||||
"environment": service_token_data["environment"],
|
||||
"type": secret_type
|
||||
},
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
)
|
||||
|
||||
|
||||
delete_secrets()
|
||||
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<Info>
|
||||
If using an `API_KEY` to authenticate with the Infisical API, then you should include it in the `X_API_KEY` header.
|
||||
</Info>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
92
docs/api-reference/overview/encryption-modes/es-mode.mdx
Normal file
92
docs/api-reference/overview/encryption-modes/es-mode.mdx
Normal file
@ -0,0 +1,92 @@
|
||||
---
|
||||
title: "ES Mode"
|
||||
---
|
||||
|
||||
Encrypted Standard (ES) mode is the easiest way to use Infisical's API. With it, you can make HTTP calls to Infisical
|
||||
to read/write secrets in plaintext.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com).
|
||||
- Create an [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled.
|
||||
- [Ensure that your project is blind-indexed](../blind-indices).
|
||||
|
||||
Below, we showcase how to execute common CRUD operations to manage secrets in **ES** mode:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Retrieve secrets">
|
||||
<Tabs>
|
||||
<Tab title="cURL">
|
||||
```bash
|
||||
curl --location --request GET 'http://localhost:8080/api/v3/secrets/raw?environment=dev&workspaceId=xxx' \
|
||||
--header 'Authorization: Bearer st.xxx'
|
||||
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Accordion>
|
||||
<Accordion title="Create secret">
|
||||
<Tabs>
|
||||
<Tab title="cURL">
|
||||
```bash
|
||||
curl --location --request POST 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME' \
|
||||
--header 'Authorization: Bearer st.xxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"workspaceId": "xxx",
|
||||
"environment": "dev",
|
||||
"type": "shared",
|
||||
"secretValue": "SECRET_VALUE",
|
||||
"secretPath": "/"
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Accordion>
|
||||
<Accordion title="Retrieve secret">
|
||||
<Tabs>
|
||||
<Tab title="cURL">
|
||||
```bash
|
||||
curl --location --request GET 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME?workspaceId=xxx&environment=dev&secretPath=/' \
|
||||
--header 'Authorization: Bearer st.xxx'
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Accordion>
|
||||
<Accordion title="Update secret">
|
||||
<Tabs>
|
||||
<Tab title="cURL">
|
||||
```bash
|
||||
curl --location --request PATCH 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME' \
|
||||
--header 'Authorization: Bearer st.xxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"workspaceId": "xxx",
|
||||
"environment": "dev",
|
||||
"type": "shared",
|
||||
"secretValue": "SECRET_VALUE",
|
||||
"secretPath": "/"
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Accordion>
|
||||
<Accordion title="Delete secret">
|
||||
<Tabs>
|
||||
<Tab title="cURL">
|
||||
```bash
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v3/secrets/raw/SECRET_NAME' \
|
||||
--header 'Authorization: Bearer st.xxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"workspaceId": "xxx",
|
||||
"environment": "dev",
|
||||
"type": "shared",
|
||||
"secretValue": "SECRET_VALUE",
|
||||
"secretPath": "/"
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
57
docs/api-reference/overview/encryption-modes/overview.mdx
Normal file
57
docs/api-reference/overview/encryption-modes/overview.mdx
Normal file
@ -0,0 +1,57 @@
|
||||
---
|
||||
title: "Preface"
|
||||
---
|
||||
|
||||
Each project in Infisical can be used either in **End-to-End Encrypted (E2EE)** mode or **Encrypted Standard (ES)** mode which dictates how it can be interacted with via the Infisical API.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="Encrypted Standard (ES)"
|
||||
href="/api-reference/overview/encryption-modes/es-mode"
|
||||
icon="shield-halved"
|
||||
color="#3c8639"
|
||||
>
|
||||
Secret operations without client-side encryption/decryption
|
||||
</Card>
|
||||
<Card href="/api-reference/overview/encryption-modes/e2ee-mode" title="End-to-End Encrypted (E2EE)" icon="shield" color="#3775a9">
|
||||
Secret operations with client-side encryption/decryption
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
By default, all projects are initialized in **E2EE** mode which means the server is not able to decrypt any values because all secret encryption/decryption operations occur on the client-side. However, this has limitations around functionality and ease-of-use:
|
||||
|
||||
- You cannot make HTTP calls to Infisical to read/write secrets in plaintext.
|
||||
- You cannot leverage non-E2EE features like native integrations and in-platform automations like dynamic secrets and secret rotation.
|
||||
|
||||
For this reason, Infisical also provides the **ES** mode of operation to unlock the above limitations by enabling the server to decrypt your values. You can optionally switch a project to using **ES** mode
|
||||
in your Project Settings.
|
||||
|
||||
<Note>
|
||||
Make no mistake, the limitations of **E2EE** mode do not prevent you from syncing secrets from Infisical to platforms like GitLab. They just imply
|
||||
that you have to do things the "E2EE-way" such as by embedding the Infisical CLI into your GitLab CI/CD pipelines to fetch and decrypt
|
||||
secrets on the client-side.
|
||||
</Note>
|
||||
|
||||
## FAQ
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Is E2EE mode or ES mode right for me?">
|
||||
We recommend starting with **E2EE** mode and switching to **ES** mode when:
|
||||
|
||||
- Your team needs more power out of non-E2EE features available in **ES** mode such as secret rotation, dynamic secrets, etc.
|
||||
- Your team wants an easier way to read/write secrets with Infisical.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="How can I switch from E2EE mode to ES mode?">
|
||||
By default, all projects in Infisical are initialized to **E2EE** mode and can be switched to **ES** mode in the Project Settings by disabling end-to-end encryption.
|
||||
</Accordion>
|
||||
<Accordion title="Is ES mode secure if it's not E2EE?">
|
||||
**ES** mode is secure and in fact what most vendors in the secret management industry are doing at the moment. In this mode, secrets are encrypted at rest by
|
||||
a series of keys, secured ultimately by a top-level `ROOT_ENCRYPTION_KEY` located on the server.
|
||||
|
||||
If you're concerned about Infisical Cloud's ability to read your secrets if using **ES** mode in Infisical Cloud, then you may wish to
|
||||
use Infisical Cloud in **E2EE** mode or self-host Infisical on your own infrastructure and then use **ES** mode; this of course which means setting up firewalls and securing the instance yourself.
|
||||
|
||||
As an organization, we prohibit reading any customer secrets without explicit permission; access to the `ROOT_ENCRYPTION_KEY` is restricted to one individual in the organization.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
@ -8,31 +8,6 @@ rotating credentials, or for integrating secret management into a larger system.
|
||||
|
||||
With the Public API, users can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more.
|
||||
|
||||
<Warning>
|
||||
We highly recommend using one of the available SDKs when working with the Infisical API.
|
||||
|
||||
If you decide to make your own requests using the API reference instead, be prepared for a steeper learning curve and more manual work.
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
In April 2023, we added the capability for users to query for secrets by name to improve the user experience of Infisical. If your project was created prior to April 2023, please read and follow the section on [blind indices](./blind-indices) and how to enable them for better usage of Infisical.
|
||||
</Warning>
|
||||
|
||||
## Concepts
|
||||
|
||||
Using Infisical's API to manage secrets requires a basic understanding of the system and its underlying cryptography detailed [here](/security/overview). A few key points:
|
||||
|
||||
- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by protected keys that are encrypted by keys derived from Argon2id applied to the user's password before being sent off to the server during the account signup process.
|
||||
- Each (encrypted) secret belongs to a project and environment.
|
||||
- Each project has an (encrypted) project key used to encrypt the secrets within that project; Infisical stores copies of the project key, for each member of that project, encrypted under each member's public key.
|
||||
- Secrets are encrypted symmetrically by your copy of the project key belonging to the project containing.
|
||||
- Infisical Tokens contain a symmetric key that can be used to decrypt a copy of a project key from the [call to get the Infisical Token data](/api-reference/endpoints/service-tokens/get).
|
||||
- Infisical uses AES256-GCM and [TweetNaCl.js](https://tweetnacl.js.org/#/) for symmetric and asymmetric encryption/decryption operations.
|
||||
|
||||
<Info>
|
||||
Infisical's system requires that secrets be encrypted/decrypted on the
|
||||
client-side to maintain E2EE. We strongly recommend you read up on the system
|
||||
prior to using the Infisical API. The (opt-in) ability to retrieve secrets
|
||||
back in decrypted format if you choose to share secrets with Infisical is on
|
||||
our roadmap.
|
||||
</Info>
|
||||
</Warning>
|
@ -247,17 +247,15 @@
|
||||
"pages": [
|
||||
"api-reference/overview/introduction",
|
||||
"api-reference/overview/authentication",
|
||||
"api-reference/overview/blind-indices",
|
||||
{
|
||||
"group": "Examples",
|
||||
"pages": [
|
||||
"api-reference/overview/examples/retrieve-secrets",
|
||||
"api-reference/overview/examples/create-secret",
|
||||
"api-reference/overview/examples/retrieve-secret",
|
||||
"api-reference/overview/examples/update-secret",
|
||||
"api-reference/overview/examples/delete-secret"
|
||||
"api-reference/overview/encryption-modes/overview",
|
||||
"api-reference/overview/encryption-modes/es-mode",
|
||||
"api-reference/overview/encryption-modes/e2ee-mode"
|
||||
]
|
||||
}
|
||||
},
|
||||
"api-reference/overview/blind-indices"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -28,6 +28,8 @@ ARG POSTHOG_HOST
|
||||
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
|
||||
ARG POSTHOG_API_KEY
|
||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
|
||||
ARG INTERCOM_ID
|
||||
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
||||
|
||||
# Build
|
||||
RUN npm run build
|
||||
@ -46,6 +48,9 @@ VOLUME /app/.next/cache/images
|
||||
ARG POSTHOG_API_KEY
|
||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
||||
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
|
||||
ARG INTERCOM_ID
|
||||
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
||||
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
||||
|
||||
COPY --chown=nextjs:nodejs --chmod=555 scripts ./scripts
|
||||
COPY --from=builder /app/public ./public
|
||||
|
@ -7,14 +7,14 @@ const path = require('path');
|
||||
|
||||
const ContentSecurityPolicy = `
|
||||
default-src 'self';
|
||||
script-src 'self' https://app.posthog.com https://js.stripe.com https://api.stripe.com 'unsafe-inline' 'unsafe-eval';
|
||||
script-src 'self' https://app.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com 'unsafe-inline' 'unsafe-eval';
|
||||
style-src 'self' https://rsms.me 'unsafe-inline';
|
||||
child-src https://api.stripe.com;
|
||||
frame-src https://js.stripe.com/ https://api.stripe.com;
|
||||
connect-src 'self' https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com;
|
||||
img-src 'self' https://*.stripe.com https://i.ytimg.com/ data:;
|
||||
media-src;
|
||||
font-src 'self' https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com;
|
||||
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/;
|
||||
connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com;
|
||||
img-src 'self' https://static.intercomassets.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://*.stripe.com https://i.ytimg.com/ data:;
|
||||
media-src https://js.intercomcdn.com;
|
||||
font-src 'self' https://fonts.intercomcdn.com/ https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com;
|
||||
`;
|
||||
|
||||
// You can choose which headers to add to the list
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY" "$NEXT_PUBLIC_POSTHOG_API_KEY"
|
||||
|
||||
scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID"
|
||||
|
||||
if [ "$TELEMETRY_ENABLED" != "false" ]; then
|
||||
echo "Telemetry is enabled"
|
||||
scripts/set-telemetry.sh true
|
||||
|
@ -121,7 +121,7 @@ export default function NavHeader({
|
||||
<span className="text-sm font-semibold text-bunker-300">{name}</span>
|
||||
) : (
|
||||
<Link passHref legacyBehavior href={{ pathname: '/dashboard/[id]', query }}>
|
||||
<a className="text-sm font-semibold capitalize text-primary/80 hover:text-primary">
|
||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">
|
||||
{name === 'root' ? selectedEnv?.name : name}
|
||||
</a>
|
||||
</Link>
|
||||
|
@ -23,7 +23,7 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
const redirectToHome = async () => {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
const userWorkspace = userWorkspaces[0]._id;
|
||||
router.push(`/home/${userWorkspace}`);
|
||||
router.push(`/dashboard/${userWorkspace}`);
|
||||
};
|
||||
|
||||
const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
|
||||
|
@ -4,11 +4,12 @@ const POSTHOG_HOST =
|
||||
process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
|
||||
const STRIPE_PRODUCT_PRO = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_PRO!;
|
||||
const STRIPE_PRODUCT_STARTER = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_STARTER!;
|
||||
const INTERCOM_ID = process.env.NEXT_PUBLIC_INTERCOM_ID!;
|
||||
|
||||
export {
|
||||
ENV,
|
||||
INTERCOM_ID,
|
||||
POSTHOG_API_KEY,
|
||||
POSTHOG_HOST,
|
||||
STRIPE_PRODUCT_PRO,
|
||||
STRIPE_PRODUCT_STARTER
|
||||
};
|
||||
STRIPE_PRODUCT_STARTER};
|
64
frontend/src/components/utilities/intercom/intercom.ts
Normal file
64
frontend/src/components/utilities/intercom/intercom.ts
Normal file
@ -0,0 +1,64 @@
|
||||
/* eslint-disable prefer-template */
|
||||
/* eslint-disable prefer-rest-params */
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable no-unexpected-multiline */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable vars-on-top */
|
||||
/* eslint-disable no-var */
|
||||
/* eslint-disable func-names */
|
||||
// @ts-nocheck
|
||||
|
||||
import { INTERCOM_ID as APP_ID } from '@app/components/utilities/config';
|
||||
|
||||
// Loads Intercom with the snippet
|
||||
// This must be run before boot, it initializes window.Intercom
|
||||
|
||||
// prettier-ignore
|
||||
export const load = () => {
|
||||
(function(){
|
||||
var w=window;
|
||||
var ic=w.Intercom;
|
||||
|
||||
if(typeof ic==="function"){
|
||||
ic('reattach_activator');
|
||||
ic('update',w.intercomSettings);
|
||||
} else {
|
||||
var d=document;
|
||||
var i=function() {
|
||||
i.c(arguments);
|
||||
};
|
||||
i.q=[];
|
||||
i.c=function(args) {
|
||||
i.q.push(args);
|
||||
};
|
||||
w.Intercom=i;
|
||||
var l=function() {
|
||||
var s=d.createElement('script');
|
||||
s.type='text/javascript';
|
||||
s.async=true;
|
||||
s.src='https://widget.intercom.io/widget/' + APP_ID;
|
||||
var x=d.getElementsByTagName('script')[0];
|
||||
x.parentNode.insertBefore(s, x);
|
||||
};
|
||||
if (document.readyState==='complete') {
|
||||
l();
|
||||
} else if (w.attachEvent) {
|
||||
w.attachEvent('onload',l);
|
||||
} else {
|
||||
w.addEventListener('load',l,false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// Initializes Intercom
|
||||
export const boot = (options = {}) => {
|
||||
window &&
|
||||
window.Intercom &&
|
||||
window.Intercom("boot", { app_id: APP_ID, ...options });
|
||||
};
|
||||
|
||||
export const update = () => {
|
||||
window && window.Intercom && window.Intercom("update");
|
||||
};
|
@ -0,0 +1,37 @@
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import {
|
||||
boot as bootIntercom,
|
||||
load as loadIntercom,
|
||||
update as updateIntercom,
|
||||
} from "./intercom";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const IntercomProvider = ({ children }: { children: any }) => {
|
||||
const router = useRouter();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
loadIntercom();
|
||||
bootIntercom();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const handleRouteChange = (url: string) => {
|
||||
if (typeof window !== "undefined") {
|
||||
updateIntercom();
|
||||
}
|
||||
};
|
||||
|
||||
router.events.on("routeChangeStart", handleRouteChange);
|
||||
|
||||
// If the component is unmounted, unsubscribe
|
||||
// from the event with the `off` method:
|
||||
return () => {
|
||||
router.events.off("routeChangeStart", handleRouteChange);
|
||||
};
|
||||
}, [router.events]);
|
||||
|
||||
return children;
|
||||
};
|
@ -3,6 +3,7 @@ export type ServiceToken = {
|
||||
name: string;
|
||||
workspace: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
user: string;
|
||||
expiresAt: string;
|
||||
createdAt: string;
|
||||
@ -15,6 +16,7 @@ export type CreateServiceTokenDTO = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
expiresIn: number;
|
||||
secretPath: string;
|
||||
encryptedKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
|
@ -1,7 +1,7 @@
|
||||
export type SubscriptionPlan = {
|
||||
_id: string;
|
||||
membersUsed: number;
|
||||
membersLimit: number;
|
||||
memberLimit: number;
|
||||
auditLogs: boolean;
|
||||
customAlerts: boolean;
|
||||
customRateLimits: boolean;
|
||||
|
@ -3,7 +3,7 @@ export type { IncidentContact } from './incidentContacts/types';
|
||||
export type { UserWsKeyPair } from './keys/types';
|
||||
export type { Organization } from './organization/types';
|
||||
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
|
||||
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
|
||||
export type { SubscriptionPlan } from './subscriptions/types';
|
||||
export type { WsTag } from './tags/types';
|
||||
export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './users/types';
|
||||
export type {
|
||||
|
@ -12,4 +12,5 @@ export {
|
||||
useNameWorkspaceSecrets,
|
||||
useRenameWorkspace,
|
||||
useToggleAutoCapitalization,
|
||||
useUpdateWsEnvironment} from './queries';
|
||||
useUpdateWsEnvironment
|
||||
} from './queries';
|
||||
|
@ -1,6 +1,10 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable no-unexpected-multiline */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable vars-on-top */
|
||||
/* eslint-disable no-var */
|
||||
/* eslint-disable func-names */
|
||||
// @ts-nocheck
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
@ -60,8 +64,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
const { user } = useUser();
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const host = window.location.origin;
|
||||
const isAddingProjectsAllowed = ((subscription?.workspacesUsed || 0) < (subscription?.workspaceLimit || 1)) || host !== 'https://app.infisical.com';
|
||||
const isAddingProjectsAllowed = subscription?.workspaceLimit ? (subscription.workspacesUsed < subscription.workspaceLimit) : true;
|
||||
|
||||
const createWs = useCreateWorkspace();
|
||||
const uploadWsKey = useUploadWsKey();
|
||||
@ -86,6 +89,18 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const handleRouteChange = () => {
|
||||
(window).Intercom('update');
|
||||
};
|
||||
|
||||
router.events.on('routeChangeComplete', handleRouteChange);
|
||||
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', handleRouteChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid
|
||||
// Placing the localstorage as much as possible
|
||||
// Wait till tony integrates the azure and its launched
|
||||
|
@ -1,4 +1,9 @@
|
||||
/* eslint-disable vars-on-top */
|
||||
/* eslint-disable no-var */
|
||||
/* eslint-disable func-names */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
// @ts-nocheck
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { AppProps } from 'next/app';
|
||||
import { useRouter } from 'next/router';
|
||||
@ -6,6 +11,7 @@ import { config } from '@fortawesome/fontawesome-svg-core';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
|
||||
import { IntercomProvider } from '@app/components/utilities/intercom/intercomProvider';
|
||||
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
|
||||
import { TooltipProvider } from '@app/components/v2';
|
||||
import { publicPaths } from '@app/const';
|
||||
@ -38,6 +44,7 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
||||
const telemetry = new Telemetry().getInstance();
|
||||
|
||||
const handleRouteChange = () => {
|
||||
// (window).Intercom('update');
|
||||
if (typeof window !== 'undefined') {
|
||||
telemetry.capture('$pageview');
|
||||
}
|
||||
@ -75,9 +82,11 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
||||
<SubscriptionProvider>
|
||||
<UserProvider>
|
||||
<NotificationProvider>
|
||||
<AppLayout>
|
||||
<Component {...pageProps} />
|
||||
</AppLayout>
|
||||
<IntercomProvider>
|
||||
<AppLayout>
|
||||
<Component {...pageProps} />
|
||||
</AppLayout>
|
||||
</IntercomProvider>
|
||||
</NotificationProvider>
|
||||
</UserProvider>
|
||||
</SubscriptionProvider>
|
||||
|
@ -176,7 +176,7 @@ export default function Users() {
|
||||
<div className="flex w-full max-w-sm flex flex-row ml-auto">
|
||||
<Input
|
||||
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||
placeholder="Search by secret name..."
|
||||
placeholder="Search by users..."
|
||||
value={searchUsers}
|
||||
onChange={(e) => setSearchUsers(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
|
@ -23,17 +23,20 @@ export const FolderSection = ({
|
||||
{folders
|
||||
.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()))
|
||||
.map(({ id, name }) => (
|
||||
<tr key={id} className="group flex flex-row items-center hover:bg-mineshaft-700 cursor-default">
|
||||
<td className="flex h-10 w-10 items-center justify-center border-none px-4 ml-0.5">
|
||||
<tr
|
||||
key={id}
|
||||
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
|
||||
>
|
||||
<td className="ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4">
|
||||
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
|
||||
</td>
|
||||
<td
|
||||
colSpan={2}
|
||||
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis uppercase lg:min-w-[240px] xl:min-w-[280px]"
|
||||
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis lg:min-w-[240px] xl:min-w-[280px]"
|
||||
style={{ paddingTop: '0', paddingBottom: '0' }}
|
||||
>
|
||||
<div
|
||||
className="flex-grow p-2 cursor-default"
|
||||
className="flex-grow cursor-default p-2"
|
||||
onKeyDown={() => null}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
|
@ -60,7 +60,7 @@ export const OrgSettingsPage = () => {
|
||||
|
||||
const [completeInviteLink, setcompleteInviteLink] = useState<string | undefined>('');
|
||||
|
||||
const isMoreUsersNotAllowed = ((subscription?.membersUsed || 0) >= (subscription?.membersLimit || 1)) && host === 'https://app.infisical.com';
|
||||
const isMoreUsersNotAllowed = subscription?.memberLimit ? (subscription.membersUsed >= subscription.memberLimit) : false;
|
||||
|
||||
const onRenameOrg = async (name: string) => {
|
||||
if (!currentOrg?._id) return;
|
||||
|
@ -44,7 +44,8 @@ import {
|
||||
EnvironmentSection,
|
||||
ProjectIndexSecretsSection,
|
||||
ProjectNameChangeSection,
|
||||
ServiceTokenSection
|
||||
ServiceTokenSection,
|
||||
E2EESection
|
||||
} from './components';
|
||||
|
||||
export const ProjectSettingsPage = () => {
|
||||
@ -90,7 +91,7 @@ export const ProjectSettingsPage = () => {
|
||||
// get user subscription
|
||||
const { subscription } = useSubscription();
|
||||
const host = window.location.origin;
|
||||
const isEnvServiceAllowed = ((currentWorkspace?.environments || []).length < (subscription?.envLimit || 3) && host === 'https://app.infisical.com');
|
||||
const isEnvServiceAllowed = ((currentWorkspace?.environments || []).length < (subscription?.envLimit || 3) || host !== 'https://app.infisical.com');
|
||||
|
||||
const onRenameWorkspace = async (name: string) => {
|
||||
try {
|
||||
@ -217,7 +218,8 @@ export const ProjectSettingsPage = () => {
|
||||
environment,
|
||||
expiresIn,
|
||||
name,
|
||||
permissions
|
||||
permissions,
|
||||
secretPath
|
||||
}: CreateServiceToken) => {
|
||||
// type guard
|
||||
if (!latestFileKey) return '';
|
||||
@ -241,6 +243,7 @@ export const ProjectSettingsPage = () => {
|
||||
iv,
|
||||
tag,
|
||||
environment,
|
||||
secretPath,
|
||||
expiresIn: Number(expiresIn),
|
||||
name,
|
||||
workspaceId: workspaceID,
|
||||
@ -398,8 +401,13 @@ export const ProjectSettingsPage = () => {
|
||||
onAutoCapitalizationChange={onAutoCapitalizationToggle}
|
||||
/>
|
||||
{!isBlindIndexedLoading && !isBlindIndexed && (
|
||||
<ProjectIndexSecretsSection onEnableBlindIndices={onEnableBlindIndices} />
|
||||
<ProjectIndexSecretsSection
|
||||
onEnableBlindIndices={onEnableBlindIndices}
|
||||
/>
|
||||
)}
|
||||
<E2EESection
|
||||
workspaceId={currentWorkspace?._id || ''}
|
||||
/>
|
||||
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md border-l border-red bg-mineshaft-900 px-6 pl-6 pb-4 pt-4">
|
||||
<p className="text-xl font-bold text-red">{t('settings.project.danger-zone')}</p>
|
||||
<p className="text-md mt-2 text-gray-400">{t('settings.project.danger-zone-note')}</p>
|
||||
|
@ -0,0 +1,115 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import {
|
||||
Checkbox
|
||||
} from "@app/components/v2";
|
||||
|
||||
import getBot from '../../../../../pages/api/bot/getBot';
|
||||
import setBotActiveStatus from '../../../../../pages/api/bot/setBotActiveStatus';
|
||||
import getLatestFileKey from '../../../../../pages/api/workspace/getLatestFileKey';
|
||||
|
||||
type Props = {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export const E2EESection = ({
|
||||
workspaceId
|
||||
}: Props) => {
|
||||
const [bot, setBot] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
// get project bot
|
||||
setBot(await getBot({ workspaceId }));
|
||||
})();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Activate bot for project by performing the following steps:
|
||||
* 1. Get the (encrypted) project key
|
||||
* 2. Decrypt project key with user's private key
|
||||
* 3. Encrypt project key with bot's public key
|
||||
* 4. Send encrypted project key to backend and set bot status to active
|
||||
*/
|
||||
const toggleBotActivate = async () => {
|
||||
let botKey;
|
||||
try {
|
||||
if (bot) {
|
||||
// case: there is a bot
|
||||
|
||||
if (!bot.isActive) {
|
||||
// bot is not active -> activate bot
|
||||
const key = await getLatestFileKey({ workspaceId });
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
if (!PRIVATE_KEY) {
|
||||
throw new Error('Private Key missing');
|
||||
}
|
||||
|
||||
const WORKSPACE_KEY = decryptAssymmetric({
|
||||
ciphertext: key.latestKey.encryptedKey,
|
||||
nonce: key.latestKey.nonce,
|
||||
publicKey: key.latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: WORKSPACE_KEY,
|
||||
publicKey: bot.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
botKey = {
|
||||
encryptedKey: ciphertext,
|
||||
nonce
|
||||
};
|
||||
|
||||
const botx = await setBotActiveStatus({
|
||||
botId: bot._id,
|
||||
isActive: true,
|
||||
botKey
|
||||
});
|
||||
|
||||
setBot(botx.bot);
|
||||
} else {
|
||||
// bot is active -> deactivate bot
|
||||
const botx = await setBotActiveStatus({
|
||||
botId: bot._id,
|
||||
isActive: false
|
||||
});
|
||||
|
||||
setBot(botx.bot);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return bot ? (
|
||||
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-mineshaft-900 px-6 pb-6 pt-2">
|
||||
<p className="mb-4 mt-2 text-xl font-semibold">End-to-End Encryption</p>
|
||||
<p className="text-md my-2 text-gray-400">
|
||||
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to cloud providers as well as HTTP calls to get secrets back raw but enables the server to read/decrypt your secret values.
|
||||
</p>
|
||||
<p className="text-md my-2 mb-4 text-gray-400">
|
||||
Note that, even with E2EE disabled, your secrets are always encrypted at rest.
|
||||
</p>
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="autoCapitalization"
|
||||
isChecked={!bot.isActive}
|
||||
onCheckedChange={async () => {
|
||||
await toggleBotActivate();
|
||||
}}
|
||||
>
|
||||
End-to-end encryption enabled
|
||||
</Checkbox>
|
||||
</div>
|
||||
) : <div />;
|
||||
};
|
||||
|
@ -0,0 +1,3 @@
|
||||
export {
|
||||
E2EESection
|
||||
} from './E2EESection';
|
@ -42,8 +42,9 @@ const apiTokenExpiry = [
|
||||
];
|
||||
|
||||
const createServiceTokenSchema = yup.object({
|
||||
name: yup.string().required().label('Service Token Name'),
|
||||
environment: yup.string().required().label('Environment'),
|
||||
name: yup.string().max(100).required().label('Service Token Name'),
|
||||
environment: yup.string().max(50).required().label('Environment'),
|
||||
secretPath: yup.string().required().default('/').label('Secret Path'),
|
||||
expiresIn: yup.string().optional().label('Service Token Expiration'),
|
||||
permissions: yup
|
||||
.object()
|
||||
@ -189,6 +190,22 @@ export const ServiceTokenSection = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
defaultValue="/"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secrets Path"
|
||||
isError={Boolean(error)}
|
||||
helperText="Tokens can be scoped to a folder path. Default path is /"
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Provide a path, default is /" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresIn"
|
||||
@ -317,6 +334,7 @@ export const ServiceTokenSection = ({
|
||||
<Tr>
|
||||
<Th>Token Name</Th>
|
||||
<Th>Environment</Th>
|
||||
<Th>Secret Path</Th>
|
||||
<Th>Valid Until</Th>
|
||||
<Th aria-label="button" />
|
||||
</Tr>
|
||||
@ -328,6 +346,7 @@ export const ServiceTokenSection = ({
|
||||
<Tr key={row._id}>
|
||||
<Td>{row.name}</Td>
|
||||
<Td>{row.environment}</Td>
|
||||
<Td>{row.secretPath}</Td>
|
||||
<Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td>
|
||||
<Td className="flex items-center justify-end">
|
||||
<IconButton
|
||||
|
@ -6,3 +6,4 @@ export { ProjectNameChangeSection } from './ProjectNameChangeSection';
|
||||
export type { CreateWsTag } from './SecretTagsSection/SecretTagsSection';
|
||||
export { ServiceTokenSection } from './ServiceTokenSection';
|
||||
export type { CreateServiceToken } from './ServiceTokenSection/ServiceTokenSection';
|
||||
export { E2EESection } from './E2EESection';
|
||||
|
Reference in New Issue
Block a user