Merge remote-tracking branch 'origin' into feature-github-signin

This commit is contained in:
Tuan Dang
2023-08-03 15:39:45 +07:00
23 changed files with 1176 additions and 815 deletions

View File

@@ -137,6 +137,12 @@ export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
});
}
/**
* Delete payment method with id [pmtMethodId] for organization
* @param req
* @param res
* @returns
*/
export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => {
const { pmtMethodId } = req.params;
@@ -206,4 +212,18 @@ export const getOrganizationInvoices = async (req: Request, res: Response) => {
);
return res.status(200).send(invoices);
}
/**
* Return organization's licenses on file
* @param req
* @param res
* @returns
*/
export const getOrganizationLicenses = async (req: Request, res: Response) => {
const { data: { licenses } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/licenses`
);
return res.status(200).send(licenses);
}

View File

@@ -220,4 +220,18 @@ router.get(
organizationsController.getOrganizationInvoices
);
router.get(
"/:organizationId/licenses",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationLicenses
);
export default router;

View File

@@ -4,18 +4,20 @@ import {
decryptAsymmetric,
decryptSymmetric128BitHexKeyUTF8,
encryptSymmetric128BitHexKeyUTF8,
generateKeyPair,
generateKeyPair
} from "../utils/crypto";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8,
SECRET_SHARED,
SECRET_SHARED
} from "../variables";
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { InternalServerError } from "../utils/errors";
import Folder from "../models/folder";
import { getFolderByPath } from "../services/FolderService";
import { getAllImportedSecrets } from "../services/SecretImportService";
import { expandSecrets } from "./secrets";
/**
* Create an inactive bot with name [name] for workspace with id [workspaceId]
@@ -25,7 +27,7 @@ import { getFolderByPath } from "../services/FolderService";
*/
export const createBot = async ({
name,
workspaceId,
workspaceId
}: {
name: string;
workspaceId: Types.ObjectId;
@@ -36,10 +38,7 @@ export const createBot = async ({
const { publicKey, privateKey } = generateKeyPair();
if (rootEncryptionKey) {
const { ciphertext, iv, tag } = client.encryptSymmetric(
privateKey,
rootEncryptionKey
);
const { ciphertext, iv, tag } = client.encryptSymmetric(privateKey, rootEncryptionKey);
return await new Bot({
name,
@@ -50,12 +49,12 @@ export const createBot = async ({
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64,
keyEncoding: ENCODING_SCHEME_BASE64
}).save();
} else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey,
key: await getEncryptionKey(),
key: await getEncryptionKey()
});
return await new Bot({
@@ -67,12 +66,12 @@ export const createBot = async ({
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
keyEncoding: ENCODING_SCHEME_UTF8
}).save();
}
throw InternalServerError({
message: "Failed to create new bot due to missing encryption key",
message: "Failed to create new bot due to missing encryption key"
});
};
@@ -82,7 +81,7 @@ export const createBot = async ({
*/
export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
const botKey = await BotKey.exists({
workspace: workspaceId,
workspace: workspaceId
});
return botKey ? false : true;
@@ -98,19 +97,19 @@ export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
export const getSecretsBotHelper = async ({
workspaceId,
environment,
secretPath,
secretPath
}: {
workspaceId: Types.ObjectId;
environment: string;
secretPath: string;
}) => {
const content = {} as any;
const content: Record<string, { value: string; comment?: string }> = {};
const key = await getKey({ workspaceId: workspaceId });
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
environment
});
if (!folders && secretPath !== "/") {
@@ -129,7 +128,43 @@ export const getSecretsBotHelper = async ({
workspace: workspaceId,
environment,
type: SECRET_SHARED,
folder: folderId,
folder: folderId
});
const importedSecrets = await getAllImportedSecrets(
workspaceId.toString(),
environment,
folderId
);
importedSecrets.forEach(({ secrets }) => {
secrets.forEach((secret) => {
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
});
content[secretKey] = { value: secretValue };
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
const commentValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key
});
content[secretKey].comment = commentValue;
}
});
});
secrets.forEach((secret: ISecret) => {
@@ -137,19 +172,31 @@ export const getSecretsBotHelper = async ({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key,
key
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key,
key
});
content[secretKey] = secretValue;
content[secretKey] = { value: secretValue };
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
const commentValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key
});
content[secretKey].comment = commentValue;
}
});
await expandSecrets(workspaceId.toString(), key, content);
return content;
};
@@ -160,22 +207,18 @@ export const getSecretsBotHelper = async ({
* @param {String} obj.workspaceId - id of workspace
* @returns {String} key - decrypted workspace key
*/
export const getKey = async ({
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) => {
export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const botKey = await BotKey.findOne({
workspace: workspaceId,
workspace: workspaceId
}).populate<{ sender: IUser }>("sender", "publicKey");
if (!botKey) throw new Error("Failed to find bot key");
const bot = await Bot.findOne({
workspace: workspaceId,
workspace: workspaceId
}).select("+encryptedPrivateKey +iv +tag +algorithm +keyEncoding");
if (!bot) throw new Error("Failed to find bot");
@@ -194,7 +237,7 @@ export const getKey = async ({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
privateKey: privateKeyBot
});
} else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
// case: encoding scheme is utf8
@@ -202,20 +245,19 @@ export const getKey = async ({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: encryptionKey,
key: encryptionKey
});
return decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
privateKey: privateKeyBot
});
}
throw InternalServerError({
message:
"Failed to obtain bot's copy of workspace key needed for bot operations",
message: "Failed to obtain bot's copy of workspace key needed for bot operations"
});
};
@@ -228,7 +270,7 @@ export const getKey = async ({
*/
export const encryptSymmetricHelper = async ({
workspaceId,
plaintext,
plaintext
}: {
workspaceId: Types.ObjectId;
plaintext: string;
@@ -236,13 +278,13 @@ export const encryptSymmetricHelper = async ({
const key = await getKey({ workspaceId: workspaceId });
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext,
key,
key
});
return {
ciphertext,
iv,
tag,
tag
};
};
/**
@@ -258,7 +300,7 @@ export const decryptSymmetricHelper = async ({
workspaceId,
ciphertext,
iv,
tag,
tag
}: {
workspaceId: Types.ObjectId;
ciphertext: string;
@@ -270,7 +312,7 @@ export const decryptSymmetricHelper = async ({
ciphertext,
iv,
tag,
key,
key
});
return plaintext;
@@ -281,24 +323,24 @@ export const decryptSymmetricHelper = async ({
* and [envionment] using bot
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.environment - environment
* @param {String} obj.environment - environment
*/
export const getSecretsCommentBotHelper = async ({
workspaceId,
environment,
secretPath
} : {
}: {
workspaceId: Types.ObjectId;
environment: string;
secretPath: string;
}) => {
const content = {} as any;
const key = await getKey({ workspaceId: workspaceId });
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
environment
});
if (!folders && secretPath !== "/") {
@@ -317,23 +359,23 @@ export const getSecretsCommentBotHelper = async ({
workspace: workspaceId,
environment,
type: SECRET_SHARED,
folder: folderId,
folder: folderId
});
secrets.forEach((secret: ISecret) => {
if(secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key,
key
});
const commentValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key,
key
});
content[secretKey] = commentValue;
@@ -341,4 +383,4 @@ export const getSecretsCommentBotHelper = async ({
});
return content;
}
};

View File

@@ -6,7 +6,7 @@ import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
INTEGRATION_NETLIFY,
INTEGRATION_VERCEL,
INTEGRATION_VERCEL
} from "../variables";
import { UnauthorizedRequestError } from "../utils/errors";
import * as Sentry from "@sentry/node";
@@ -34,7 +34,7 @@ export const handleOAuthExchangeHelper = async ({
workspaceId,
integration,
code,
environment,
environment
}: {
workspaceId: string;
integration: string;
@@ -43,21 +43,20 @@ export const handleOAuthExchangeHelper = async ({
}) => {
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true,
isActive: true
});
if (!bot)
throw new Error("Bot must be enabled for OAuth2 code-token exchange");
if (!bot) throw new Error("Bot must be enabled for OAuth2 code-token exchange");
// exchange code for access and refresh tokens
const res = await exchangeCode({
integration,
code,
code
});
const update: Update = {
workspace: workspaceId,
integration,
integration
};
switch (integration) {
@@ -72,12 +71,12 @@ export const handleOAuthExchangeHelper = async ({
const integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
workspace: workspaceId,
integration,
integration
},
update,
{
new: true,
upsert: true,
upsert: true
}
);
@@ -86,7 +85,7 @@ export const handleOAuthExchangeHelper = async ({
// set integration auth refresh token
await setIntegrationAuthRefreshHelper({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: res.refreshToken,
refreshToken: res.refreshToken
});
}
@@ -97,7 +96,7 @@ export const handleOAuthExchangeHelper = async ({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt,
accessExpiresAt: res.accessExpiresAt
});
}
@@ -111,7 +110,7 @@ export const handleOAuthExchangeHelper = async ({
*/
export const syncIntegrationsHelper = async ({
workspaceId,
environment,
environment
}: {
workspaceId: Types.ObjectId;
environment?: string;
@@ -121,11 +120,11 @@ export const syncIntegrationsHelper = async ({
workspace: workspaceId,
...(environment
? {
environment,
}
: {}),
environment
}
: {}),
isActive: true,
app: { $ne: null },
app: { $ne: null }
});
// for each workspace integration, sync/push secrets
@@ -135,25 +134,16 @@ export const syncIntegrationsHelper = async ({
const secrets = await BotService.getSecrets({
workspaceId: integration.workspace,
environment: integration.environment,
secretPath: integration.secretPath,
secretPath: integration.secretPath
});
// get workspace, environment (shared) secrets comments
const secretComments = await BotService.getSecretComments({
workspaceId: integration.workspace,
environment: integration.environment,
secretPath: integration.secretPath,
})
const integrationAuth = await IntegrationAuth.findById(
integration.integrationAuth
);
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
if (!integrationAuth) throw new Error("Failed to find integration auth");
// get integration auth access token
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth,
integrationAuthId: integration.integrationAuth
});
// sync secrets to integration
@@ -162,14 +152,17 @@ export const syncIntegrationsHelper = async ({
integrationAuth,
secrets,
accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken,
secretComments
accessToken: access.accessToken
});
}
} catch (err) {
Sentry.captureException(err);
console.log(`syncIntegrationsHelper: failed with [workspaceId=${workspaceId}] [environment=${environment}]`, err) // eslint-disable-line no-use-before-define
throw err
// eslint-disable-next-line
console.log(
`syncIntegrationsHelper: failed with [workspaceId=${workspaceId}] [environment=${environment}]`,
err
); // eslint-disable-line no-use-before-define
throw err;
}
};
@@ -182,24 +175,24 @@ export const syncIntegrationsHelper = async ({
* @param {String} refreshToken - decrypted refresh token
*/
export const getIntegrationAuthRefreshHelper = async ({
integrationAuthId,
integrationAuthId
}: {
integrationAuthId: Types.ObjectId;
}) => {
const integrationAuth = await IntegrationAuth.findById(
integrationAuthId
).select("+refreshCiphertext +refreshIV +refreshTag");
const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select(
"+refreshCiphertext +refreshIV +refreshTag"
);
if (!integrationAuth)
throw UnauthorizedRequestError({
message: "Failed to locate Integration Authentication credentials",
message: "Failed to locate Integration Authentication credentials"
});
const refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string,
tag: integrationAuth.refreshTag as string
});
return refreshToken;
@@ -214,28 +207,26 @@ export const getIntegrationAuthRefreshHelper = async ({
* @returns {String} accessToken - decrypted access token
*/
export const getIntegrationAuthAccessHelper = async ({
integrationAuthId,
integrationAuthId
}: {
integrationAuthId: Types.ObjectId;
}) => {
let accessId;
let accessToken;
const integrationAuth = await IntegrationAuth.findById(
integrationAuthId
).select(
const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select(
"workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag"
);
if (!integrationAuth)
throw UnauthorizedRequestError({
message: "Failed to locate Integration Authentication credentials",
message: "Failed to locate Integration Authentication credentials"
});
accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string,
tag: integrationAuth.accessTag as string
});
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
@@ -245,11 +236,11 @@ export const getIntegrationAuthAccessHelper = async ({
if (integrationAuth.accessExpiresAt < new Date()) {
// access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({
integrationAuthId,
integrationAuthId
});
accessToken = await exchangeRefresh({
integrationAuth,
refreshToken,
refreshToken
});
}
}
@@ -263,13 +254,13 @@ export const getIntegrationAuthAccessHelper = async ({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessIdCiphertext as string,
iv: integrationAuth.accessIdIV as string,
tag: integrationAuth.accessIdTag as string,
tag: integrationAuth.accessIdTag as string
});
}
return {
accessId,
accessToken,
accessToken
};
};
@@ -283,7 +274,7 @@ export const getIntegrationAuthAccessHelper = async ({
*/
export const setIntegrationAuthRefreshHelper = async ({
integrationAuthId,
refreshToken,
refreshToken
}: {
integrationAuthId: string;
refreshToken: string;
@@ -294,22 +285,22 @@ export const setIntegrationAuthRefreshHelper = async ({
const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: refreshToken,
plaintext: refreshToken
});
integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
_id: integrationAuthId,
_id: integrationAuthId
},
{
refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv,
refreshTag: obj.tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
keyEncoding: ENCODING_SCHEME_UTF8
},
{
new: true,
new: true
}
);
@@ -329,7 +320,7 @@ export const setIntegrationAuthAccessHelper = async ({
integrationAuthId,
accessId,
accessToken,
accessExpiresAt,
accessExpiresAt
}: {
integrationAuthId: string;
accessId: string | null;
@@ -342,20 +333,20 @@ export const setIntegrationAuthAccessHelper = async ({
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessToken,
plaintext: accessToken
});
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessId,
plaintext: accessId
});
}
integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
_id: integrationAuthId,
_id: integrationAuthId
},
{
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
@@ -366,10 +357,10 @@ export const setIntegrationAuthAccessHelper = async ({
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
keyEncoding: ENCODING_SCHEME_UTF8
},
{
new: true,
new: true
}
);

View File

@@ -42,9 +42,10 @@ import { TelemetryService } from "../services";
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { EELogService, EESecretService } from "../ee/services";
import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth";
import { getFolderIdFromServiceToken } from "../services/FolderService";
import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService";
import picomatch from "picomatch";
import path from "path";
import Folder, { TFolderRootSchema } from "../models/folder";
export const isValidScope = (
authPayload: IServiceTokenData,
@@ -64,10 +65,9 @@ export const isValidScope = (
export function containsGlobPatterns(secretPath: string) {
const globChars = ["*", "?", "[", "]", "{", "}", "**"];
const normalizedPath = path.normalize(secretPath);
return globChars.some(char => normalizedPath.includes(char));
return globChars.some((char) => normalizedPath.includes(char));
}
/**
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
*
@@ -929,3 +929,164 @@ export const deleteSecretHelper = async ({
secret
};
};
const fetchSecretsCrossEnv = (workspaceId: string, folders: TFolderRootSchema[], key: string) => {
const fetchCache: Record<string, Record<string, string>> = {};
return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => {
const secRefPathUrl = path.join("/", ...secRefPath);
const uniqKey = `${secRefEnv}-${secRefPathUrl}`;
if (fetchCache?.[uniqKey]) {
return fetchCache[uniqKey][secRefKey];
}
let folderId = "root";
const folder = folders.find(({ environment }) => environment === secRefEnv);
if (!folder && secRefPathUrl !== "/") {
throw BadRequestError({ message: "Folder not found" });
}
if (folder) {
const selectedFolder = getFolderByPath(folder.nodes, secRefPathUrl);
if (!selectedFolder) {
throw BadRequestError({ message: "Folder not found" });
}
folderId = selectedFolder.id;
}
const secrets = await Secret.find({
workspace: workspaceId,
environment: secRefEnv,
type: SECRET_SHARED,
folder: folderId
});
const decryptedSec = secrets.reduce<Record<string, string>>((prev, secret) => {
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
});
prev[secretKey] = secretValue;
return prev;
}, {});
fetchCache[uniqKey] = decryptedSec;
return fetchCache[uniqKey][secRefKey];
};
};
const INTERPOLATION_SYNTAX_REG = new RegExp(/\${([^}]+)}/g);
const recursivelyExpandSecret = async (
expandedSec: Record<string, string>,
interpolatedSec: Record<string, string>,
fetchCrossEnv: (env: string, secPath: string[], secKey: string) => Promise<string>,
recursionChainBreaker: Record<string, boolean>,
key: string
) => {
if (expandedSec?.[key]) {
return expandedSec[key];
}
if (recursionChainBreaker?.[key]) {
return "";
}
recursionChainBreaker[key] = true;
let interpolatedValue = interpolatedSec[key];
if (!interpolatedValue) {
throw new Error(`Couldn't find referenced value - ${key}`);
}
const refs = interpolatedValue.match(INTERPOLATION_SYNTAX_REG);
if (refs) {
for (const interpolationSyntax of refs) {
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
const entities = interpolationKey.trim().split(".");
if (entities.length === 1) {
const val = await recursivelyExpandSecret(
expandedSec,
interpolatedSec,
fetchCrossEnv,
recursionChainBreaker,
interpolationKey
);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
}
continue;
}
if (entities.length > 1) {
const secRefEnv = entities[0];
const secRefPath = entities.slice(1, entities.length - 1);
const secRefKey = entities[entities.length - 1];
const val = await fetchCrossEnv(secRefEnv, secRefPath, secRefKey);
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
}
}
}
expandedSec[key] = interpolatedValue;
return interpolatedValue;
};
// used to convert multi line ones to quotes ones with \n
const formatMultiValueEnv = (val?: string) => {
if (!val) return "";
if (!val.match("\n")) return val;
return `"${val.replace(/\n/g, "\\n")}"`;
};
export const expandSecrets = async (
workspaceId: string,
rootEncKey: string,
secrets: Record<string, { value: string; comment?: string }>
) => {
const expandedSec: Record<string, string> = {};
const interpolatedSec: Record<string, string> = {};
const folders = await Folder.find({ workspace: workspaceId });
const crossSecEnvFetch = fetchSecretsCrossEnv(workspaceId, folders, rootEncKey);
Object.keys(secrets).forEach((key) => {
if (secrets[key].value.match(INTERPOLATION_SYNTAX_REG)) {
interpolatedSec[key] = secrets[key].value;
} else {
expandedSec[key] = secrets[key].value;
}
});
for (const key of Object.keys(secrets)) {
if (expandedSec?.[key]) {
secrets[key].value = formatMultiValueEnv(expandedSec[key]);
continue;
}
// this is to avoid recursion loop. So the graph should be direct graph rather than cyclic
// so for any recursion building if there is an entity two times same key meaning it will be looped
const recursionChainBreaker: Record<string, boolean> = {};
const expandedVal = await recursivelyExpandSecret(
expandedSec,
interpolatedSec,
crossSecEnvFetch,
recursionChainBreaker,
key
);
secrets[key].value = formatMultiValueEnv(expandedVal);
}
return secrets;
};

File diff suppressed because it is too large Load Diff

View File

@@ -57,7 +57,7 @@ router.get(
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
checkIPAllowlist: false
checkIPAllowlist: true
}),
secretsController.getSecretByNameRaw
);
@@ -86,7 +86,7 @@ router.post(
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
checkIPAllowlist: false
checkIPAllowlist: true
}),
secretsController.createSecretRaw
);
@@ -115,7 +115,7 @@ router.patch(
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
checkIPAllowlist: false
checkIPAllowlist: true
}),
secretsController.updateSecretByNameRaw
);
@@ -143,7 +143,7 @@ router.delete(
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
checkIPAllowlist: false
checkIPAllowlist: true
}),
secretsController.deleteSecretByNameRaw
);
@@ -169,7 +169,7 @@ router.get(
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
checkIPAllowlist: false
checkIPAllowlist: true
}),
secretsController.getSecrets
);
@@ -205,7 +205,7 @@ router.post(
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
checkIPAllowlist: false
checkIPAllowlist: true
}),
secretsController.createSecret
);
@@ -232,7 +232,7 @@ router.get(
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
checkIPAllowlist: false
checkIPAllowlist: true
}),
secretsController.getSecretByName
);
@@ -263,7 +263,7 @@ router.patch(
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
checkIPAllowlist: false
checkIPAllowlist: true
}),
secretsController.updateSecretByName
);
@@ -291,7 +291,7 @@ router.delete(
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
checkIPAllowlist: false
checkIPAllowlist: true
}),
secretsController.deleteSecretByName
);

View File

@@ -1,9 +1,7 @@
{
"compilerOptions": {
"target": "es2016",
"lib": [
"es6"
],
"lib": ["es6", "es2021"],
"module": "commonjs",
"rootDir": "src",
"resolveJsonModule": true,
@@ -15,15 +13,8 @@
"strict": true,
"noImplicitAny": true,
"skipLibCheck": true,
"typeRoots": [
"./src/types",
"./node_modules/@types"
]
"typeRoots": ["./src/types", "./node_modules/@types"]
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
}
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

View File

@@ -196,7 +196,7 @@ func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessib
}
if response.IsError() {
return GetAccessibleEnvironmentsResponse{}, fmt.Errorf("CallGetAccessibleEnvironments: Unsuccessful response: [response=%v]", response)
return GetAccessibleEnvironmentsResponse{}, fmt.Errorf("CallGetAccessibleEnvironments: Unsuccessful response: [response=%v] [response-code=%v] [url=%s]", response, response.StatusCode(), response.Request.URL)
}
return accessibleEnvironmentsResponse, nil

View File

@@ -23,7 +23,8 @@
},
"feedback": {
"suggestEdit": true,
"raiseIssue": true
"raiseIssue": true,
"thumbsRating": true
},
"api": {
"baseUrl": ["https://app.infisical.com", "http://localhost:8080"],

View File

@@ -7,9 +7,6 @@ description: "Use our Helm chart to Install Infisical on your Kubernetes cluster
- Installed [Helm package manager](https://helm.sh/) version v3.11.3 or greater
- You have [kubectl](https://kubernetes.io/docs/reference/kubectl/kubectl/) installed and connected to your kubernetes cluster
<iframe width="100%" height="375" src="https://www.youtube.com/embed/ugJZSCcZaV8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
By deploying Infisical on Kubernetes, you can take advantage of its features to ensure that the application is fault-tolerant, highly available, and scalable.
To make the installation process easier and more streamlined, we have created a Helm chart that you can use to install Infisical on Kubernetes.
@@ -34,10 +31,11 @@ By default, the application will use the latest tag to retrieve the required Doc
However, it's important to specify a particular version of Infisical during installation to prevent any significant updates from disrupting your deployment.
View [properties for frontend and backend](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical#parameters).
To determine the appropriate versions to use for the docker images, follow the links bellow
- [frontend Docker image](https://hub.docker.com/r/infisical/frontend/tags)
- [backend Docker image](https://hub.docker.com/r/infisical/backend/tags)
<Tip>
To find the latest version number of Infisical, follow the links bellow
- [frontend Docker image](https://hub.docker.com/r/infisical/frontend/tags)
- [backend Docker image](https://hub.docker.com/r/infisical/backend/tags)
</Tip>
```yaml simple-values-example.yaml
frontend:
@@ -45,14 +43,14 @@ frontend:
replicaCount: 2
image:
repository: infisical/frontend
tag: "v0.1.3"
tag: "v0.26.0" # <--- frontend version
pullPolicy: Always
backend:
replicaCount: 2
image:
repository: infisical/backend
tag: "v0.1.3"
tag: "v0.26.0" # <--- backend version
pullPolicy: Always
```

View File

@@ -1,4 +1,4 @@
import { forwardRef, InputHTMLAttributes, ReactNode } from "react";
import { ChangeEvent, forwardRef, InputHTMLAttributes, ReactNode } from "react";
import { cva, VariantProps } from "cva";
import { twMerge } from "tailwind-merge";
@@ -10,6 +10,7 @@ type Props = {
rightIcon?: ReactNode;
isDisabled?: boolean;
isReadOnly?: boolean;
autoCapitalization?: boolean;
};
const inputVariants = cva(
@@ -80,10 +81,19 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
variant = "filled",
size = "md",
isReadOnly,
autoCapitalization,
...props
},
ref
): JSX.Element => {
const handleInput = (event: ChangeEvent<HTMLInputElement>) => {
console.log(123, props, autoCapitalization)
if (autoCapitalization) {
// eslint-disable-next-line no-param-reassign
event.target.value = event.target.value.toUpperCase();
}
};
return (
<div className={inputParentContainerVariants({ isRounded, isError, isFullWidth, variant })}>
{leftIcon && <span className="absolute left-0 ml-3 text-sm">{leftIcon}</span>}
@@ -93,6 +103,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
ref={ref}
readOnly={isReadOnly}
disabled={isDisabled}
onInput={handleInput}
className={twMerge(
leftIcon ? "pl-10" : "pl-2.5",
rightIcon ? "pr-10" : "pr-2.5",

View File

@@ -47,9 +47,10 @@ const ActivityLogsRow = ({
const { t } = useTranslation();
const renderUser = () => {
if (row?.user) return `${row.user}`;
if (row?.serviceAccount) return `Service Account: ${row.serviceAccount.name}`;
if (row?.serviceTokenData.name) return `Service Token: ${row.serviceTokenData.name}`;
if (row?.serviceTokenData?.name) return `Service Token: ${row.serviceTokenData.name}`;
return "";
};

View File

@@ -7,6 +7,7 @@ export {
useGetOrganization,
useGetOrgBillingDetails,
useGetOrgInvoices,
useGetOrgLicenses,
useGetOrgPlanBillingInfo,
useGetOrgPlansTable,
useGetOrgPlanTable,
@@ -14,5 +15,4 @@ export {
useGetOrgTaxIds,
useGetOrgTrialUrl,
useRenameOrg,
useUpdateOrgBillingDetails
} from "./queries";
useUpdateOrgBillingDetails} from "./queries";

View File

@@ -5,14 +5,14 @@ import { apiRequest } from "@app/config/request";
import {
BillingDetails,
Invoice,
License,
Organization,
OrgPlanTable,
PlanBillingInfo,
PmtMethod,
ProductsTable,
RenameOrgDTO,
TaxID
} from "./types";
TaxID} from "./types";
const organizationKeys = {
getUserOrganization: ["organization"] as const,
@@ -23,6 +23,7 @@ const organizationKeys = {
getOrgPmtMethods: (orgId: string) => [{ orgId }, "organization-pmt-methods"] as const,
getOrgTaxIds: (orgId: string) => [{ orgId }, "organization-tax-ids"] as const,
getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const,
getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const
};
export const useGetOrganization = () => {
@@ -311,4 +312,20 @@ export const useCreateCustomerPortalSession = () => {
return data;
}
});
};
};
export const useGetOrgLicenses = (organizationId: string) => {
return useQuery({
queryKey: organizationKeys.getOrgLicenses(organizationId),
queryFn: async () => {
if (organizationId === "") return undefined;
const { data } = await apiRequest.get<License[]>(
`/api/v1/organizations/${organizationId}/licenses`
);
return data;
},
enabled: true
});
}

View File

@@ -49,6 +49,17 @@ export type TaxID = {
value: string;
}
export type License = {
_id: string;
customerId: string;
prefix: string;
licenseKey: string;
isActivated: boolean;
expiresAt: string;
createdAt: string;
updatedAt: string;
}
export type OrgPlanTableHead = {
name: string;
}

View File

@@ -971,6 +971,7 @@ export const DashboardPage = () => {
register={register}
control={control}
setValue={setValue}
autoCapitalization={currentWorkspace?.autoCapitalization}
/>
))}
{!isReadOnly && !isRollbackMode && (

View File

@@ -79,6 +79,7 @@ type Props = {
setValue: UseFormSetValue<FormData>;
isKeyError?: boolean;
keyError?: string;
autoCapitalization?: boolean;
};
export const SecretInputRow = memo(
@@ -98,7 +99,8 @@ export const SecretInputRow = memo(
setValue,
isKeyError,
keyError,
secUniqId
secUniqId,
autoCapitalization
}: Props): JSX.Element => {
const isKeySubDisabled = useRef<boolean>(false);
// comment management in a row
@@ -243,6 +245,7 @@ export const SecretInputRow = memo(
isKeySubDisabled.current = false;
field.onBlur();
}}
autoCapitalization={autoCapitalization}
/>
</div>
</td>

View File

@@ -33,7 +33,7 @@ export const CurrentPlanSection = () => {
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<h2 className="mb-8 flex-1 text-xl font-semibold text-white">Current Usage</h2>
<h2 className="mb-8 flex-1 text-xl font-semibold text-white">Current usage</h2>
<TableContainer className="mt-4">
<Table>
<THead>

View File

@@ -0,0 +1,9 @@
import { LicensesSection } from "./LicensesSection";
export const BillingSelfHostedTab = () => {
return (
<div>
<LicensesSection />
</div>
);
}

View File

@@ -0,0 +1,68 @@
import { faFileContract } from "@fortawesome/free-solid-svg-icons";
import {
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetOrgLicenses
} from "@app/hooks/api";
export const LicensesSection = () => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgLicenses(currentOrg?._id ?? "");
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<h2 className="mb-8 flex-1 text-xl font-semibold text-white">Enterprise licenses</h2>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>License Key</Th>
<Th>Status</Th>
<Th>Issued Date</Th>
<Th>Expiry Date</Th>
</Tr>
</THead>
<TBody>
{!isLoading && data && data?.length > 0 && data.map(({
_id,
licenseKey,
isActivated,
createdAt,
expiresAt
}) => {
const formattedCreatedAt = new Date(createdAt).toISOString().split("T")[0];
const formattedExpiresAt = new Date(expiresAt).toISOString().split("T")[0];
return (
<Tr key={`license-${_id}`} className="h-10">
<Td>{licenseKey}</Td>
<Td>{isActivated ? "Active" : "Inactive"}</Td>
<Td>{formattedCreatedAt}</Td>
<Td>{formattedExpiresAt}</Td>
</Tr>
);
})}
{isLoading && <TableSkeleton columns={4} innerKey="licenses" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={4}>
<EmptyState title="No enterprise licenses on file" icon={faFileContract} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from "./BillingSelfHostedTab";

View File

@@ -4,9 +4,11 @@ import { Tab } from "@headlessui/react"
import { BillingCloudTab } from "../BillingCloudTab";
import { BillingDetailsTab } from "../BillingDetailsTab";
import { BillingReceiptsTab } from "../BillingReceiptsTab";
import { BillingSelfHostedTab } from "../BillingSelfHostedTab";
const tabs = [
{ name: "Infisical Cloud", key: "tab-infisical-cloud" },
{ name: "Infisical Self-Hosted", key: "tab-infisical-self-hosted" },
{ name: "Receipts", key: "tab-receipts" },
{ name: "Billing details", key: "tab-billing-details" }
];
@@ -32,6 +34,9 @@ export const BillingTabGroup = () => {
<Tab.Panel>
<BillingCloudTab />
</Tab.Panel>
<Tab.Panel>
<BillingSelfHostedTab />
</Tab.Panel>
<Tab.Panel>
<BillingReceiptsTab />
</Tab.Panel>