Compare commits

...

23 Commits

Author SHA1 Message Date
9a5329300c Merge pull request #817 from akhilmhdh/feat/import-sec-dashboard
feat: added copy secret feature in dashboard
2023-08-03 18:09:56 -04:00
b03c346985 nit: text update 2023-08-03 18:03:35 -04:00
84efc3de46 pushed out soem changes 2023-08-03 13:30:22 -04:00
2ff3818ecb feat: made changes as discussed with team on dropzone 2023-08-03 22:23:14 +05:30
6fbcbc4807 Add signup with GitHub option 2023-08-03 16:05:14 +07:00
9048988e2f Merge pull request #818 from JunedKhan101/feature-github-signin
initial-setup for github signin
2023-08-03 15:44:15 +07:00
98cfd72928 Merge remote-tracking branch 'origin' into feature-github-signin 2023-08-03 15:39:45 +07:00
2293abfc80 Revise and finish login with GitHub 2023-08-03 15:34:02 +07:00
817a783ec2 feat: updated text and added select all in copy secrets for dashboard 2023-08-03 13:15:28 +05:30
9006212ab5 Merge pull request #819 from Infisical/view-licenses
Add tab to view enterprise license keys in usage and billing section
2023-08-03 11:59:44 +07:00
1627674c2a Merge remote-tracking branch 'origin' into view-licenses 2023-08-03 11:51:12 +07:00
bc65bf1238 Add section for users to view purchased enterprise license keys in organization usage and billing section 2023-08-03 11:48:10 +07:00
3990b6dc49 fixed the autocapitalization ability 2023-08-02 20:01:06 -07:00
a3b8de2e84 Update mint.json 2023-08-02 18:37:59 -07:00
b5bffdbcac Merge pull request #813 from akhilmhdh/feat/sec-exp-ingtegration
Secret expansion and import in integrations
2023-08-02 19:16:03 -04:00
23e40e523a highlight infisical version in k8 docs 2023-08-02 17:41:45 -04:00
d1749deff0 enable checkIPAllowlist 2023-08-02 12:41:04 -04:00
960aceed29 initial-setup for github signin 2023-08-02 21:28:35 +05:30
466dadc611 feat: added pull secret feature in dashboard with env json parsing and multiline parsing 2023-08-02 16:15:04 +05:30
cc5ca30057 feat: updated multi line format on integrations sync secrets 2023-08-02 16:12:58 +05:30
b1981df8f0 chore: resolved merge conflict 2023-08-01 15:29:34 +05:30
086652a89f fix: resolved infinite recursion cases 2023-08-01 15:24:18 +05:30
6574b6489f fix: added support for secret import and expansion in integrations 2023-08-01 15:24:18 +05:30
43 changed files with 1888 additions and 1025 deletions

View File

@ -45,6 +45,7 @@
"node-cache": "^5.1.2",
"nodemailer": "^6.8.0",
"passport": "^0.6.0",
"passport-github": "^1.1.0",
"passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0",
"probot": "^12.3.1",
@ -11385,6 +11386,17 @@
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-github": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz",
"integrity": "sha512-XARXJycE6fFh/dxF+Uut8OjlwbFEXgbPVj/+V+K7cvriRK7VcAOm+NgBmbiLM9Qv3SSxEAV+V6fIk89nYHXa8A==",
"dependencies": {
"passport-oauth2": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/passport-google-oauth20": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
@ -22858,6 +22870,14 @@
"utils-merge": "^1.0.1"
}
},
"passport-github": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz",
"integrity": "sha512-XARXJycE6fFh/dxF+Uut8OjlwbFEXgbPVj/+V+K7cvriRK7VcAOm+NgBmbiLM9Qv3SSxEAV+V6fIk89nYHXa8A==",
"requires": {
"passport-oauth2": "1.x.x"
}
},
"passport-google-oauth20": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",

View File

@ -36,6 +36,7 @@
"node-cache": "^5.1.2",
"nodemailer": "^6.8.0",
"passport": "^0.6.0",
"passport-github": "^1.1.0",
"passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0",
"probot": "^12.3.1",

View File

@ -36,7 +36,6 @@ export const getClientIdVercel = async () => (await client.getSecret("CLIENT_ID_
export const getClientIdNetlify = async () => (await client.getSecret("CLIENT_ID_NETLIFY")).secretValue;
export const getClientIdGitHub = async () => (await client.getSecret("CLIENT_ID_GITHUB")).secretValue;
export const getClientIdGitLab = async () => (await client.getSecret("CLIENT_ID_GITLAB")).secretValue;
export const getClientIdGoogle = async () => (await client.getSecret("CLIENT_ID_GOOGLE")).secretValue;
export const getClientIdBitBucket = async () => (await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
export const getClientSecretAzure = async () => (await client.getSecret("CLIENT_SECRET_AZURE")).secretValue;
export const getClientSecretHeroku = async () => (await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue;
@ -44,9 +43,14 @@ export const getClientSecretVercel = async () => (await client.getSecret("CLIENT
export const getClientSecretNetlify = async () => (await client.getSecret("CLIENT_SECRET_NETLIFY")).secretValue;
export const getClientSecretGitHub = async () => (await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue;
export const getClientSecretGitLab = async () => (await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue;
export const getClientSecretGoogle = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE")).secretValue;
export const getClientSecretBitBucket = async () => (await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
export const getClientSlugVercel = async () => (await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
export const getClientIdGoogleLogin = async () => (await client.getSecret("CLIENT_ID_GOOGLE_LOGIN")).secretValue;
export const getClientSecretGoogleLogin = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
export const getClientIdGitHubLogin = async () => (await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
export const getClientSecretGitHubLogin = async () => (await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue;
export const getPostHogHost = async () => (await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com";
export const getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";
export const getSentryDSN = async () => (await client.getSecret("SENTRY_DSN")).secretValue;

View File

@ -123,9 +123,15 @@ export const updateAuthProvider = async (req: Request, res: Response) => {
authProvider
} = req.body;
if (req.user?.authProvider === AuthProvider.OKTA_SAML) return res.status(400).send({
message: "Failed to update user authentication method because SAML SSO is enforced"
});
if (
req.user?.authProvider === AuthProvider.OKTA_SAML
|| req.user?.authProvider === AuthProvider.AZURE_SAML
|| req.user?.authProvider === AuthProvider.JUMPCLOUD_SAML
) {
return res.status(400).send({
message: "Failed to update user authentication method because SAML SSO is enforced"
});
}
const user = await User.findByIdAndUpdate(
req.user._id.toString(),

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

@ -41,6 +41,29 @@ router.get(
ssoController.redirectSSO
);
router.get(
"/redirect/github",
authLimiter,
(req, res, next) => {
passport.authenticate("github", {
session: false,
...(req.query.callback_port ? {
state: req.query.callback_port as string
} : {})
})(req, res, next);
}
);
router.get(
"/github",
authLimiter,
passport.authenticate("github", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get(
"/redirect/saml2/:ssoIdentifier",
authLimiter,

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

@ -3,6 +3,7 @@ import { Document, Schema, Types, model } from "mongoose";
export enum AuthProvider {
EMAIL = "email",
GOOGLE = "google",
GITHUB = "github",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml",

View File

@ -50,7 +50,8 @@ router.patch(
}),
body("authProvider").exists().isString().isIn([
AuthProvider.EMAIL,
AuthProvider.GOOGLE
AuthProvider.GOOGLE,
AuthProvider.GITHUB
]),
validateRequest,
usersController.updateAuthProvider

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

@ -12,8 +12,10 @@ import {
} from "../models";
import { createToken } from "../helpers/auth";
import {
getClientIdGoogle,
getClientSecretGoogle,
getClientIdGitHubLogin,
getClientIdGoogleLogin,
getClientSecretGitHubLogin,
getClientSecretGoogleLogin,
getJwtProviderAuthLifetime,
getJwtProviderAuthSecret,
} from "../config";
@ -25,6 +27,8 @@ import { getSiteURL } from "../config";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const GoogleStrategy = require("passport-google-oauth20").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const GitHubStrategy = require("passport-github").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { MultiSamlStrategy } = require("@node-saml/passport-saml");
/**
@ -67,42 +71,97 @@ const getAuthDataPayloadUserObj = (authData: AuthData) => {
}
const initializePassport = async () => {
const googleClientSecret = await getClientSecretGoogle();
const googleClientId = await getClientIdGoogle();
const clientIdGoogleLogin = await getClientIdGoogleLogin();
const clientSecretGoogleLogin = await getClientSecretGoogleLogin();
const clientIdGitHubLogin = await getClientIdGitHubLogin();
const clientSecretGitHubLogin = await getClientSecretGitHubLogin();
passport.use(new GoogleStrategy({
passReqToCallback: true,
clientID: googleClientId,
clientSecret: googleClientSecret,
callbackURL: "/api/v1/sso/google",
scope: ["profile", " email"],
}, async (
req: express.Request,
accessToken: string,
refreshToken: string,
profile: any,
done: any
) => {
try {
if (clientIdGoogleLogin && clientSecretGoogleLogin) {
passport.use(new GoogleStrategy({
passReqToCallback: true,
clientID: clientIdGoogleLogin,
clientSecret: clientSecretGoogleLogin,
callbackURL: "/api/v1/sso/google",
scope: ["profile", " email"],
}, async (
req: express.Request,
accessToken: string,
refreshToken: string,
profile: any,
done: any
) => {
try {
const email = profile.emails[0].value;
let user = await User.findOne({
email
}).select("+publicKey");
if (user && user.authProvider !== AuthProvider.GOOGLE) {
done(InternalServerError());
}
if (!user) {
user = await new User({
email,
authProvider: AuthProvider.GOOGLE,
authId: profile.id,
firstName: profile.name.givenName,
lastName: profile.name.familyName
}).save();
}
const isUserCompleted = !!user.publicKey;
const providerAuthToken = createToken({
payload: {
userId: user._id.toString(),
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
authProvider: user.authProvider,
isUserCompleted,
...(req.query.state ? {
callbackPort: req.query.state as string
} : {})
},
expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(),
});
req.isUserCompleted = isUserCompleted;
req.providerAuthToken = providerAuthToken;
done(null, profile);
} catch (err) {
done(null, false);
}
}));
}
if (clientIdGitHubLogin && clientSecretGitHubLogin) {
passport.use(new GitHubStrategy({
passReqToCallback: true,
clientID: clientIdGitHubLogin,
clientSecret: clientSecretGitHubLogin,
callbackURL: "/api/v1/sso/github"
},
async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => {
const email = profile.emails[0].value;
const firstName = profile.name.givenName;
const lastName = profile.name.familyName;
let user = await User.findOne({
email
}).select("+publicKey");
if (user && user.authProvider !== AuthProvider.GOOGLE) {
if (user && user.authProvider !== AuthProvider.GITHUB) {
done(InternalServerError());
}
if (!user) {
user = await new User({
email,
authProvider: AuthProvider.GOOGLE,
email: email,
authProvider: AuthProvider.GITHUB,
authId: profile.id,
firstName,
lastName
firstName: profile.displayName,
lastName: ""
}).save();
}
@ -111,8 +170,8 @@ const initializePassport = async () => {
payload: {
userId: user._id.toString(),
email: user.email,
firstName,
lastName,
firstName: user.firstName,
lastName: user.lastName,
authProvider: user.authProvider,
isUserCompleted,
...(req.query.state ? {
@ -125,11 +184,10 @@ const initializePassport = async () => {
req.isUserCompleted = isUserCompleted;
req.providerAuthToken = providerAuthToken;
done(null, profile);
} catch (err) {
done(null, false);
return done(null, profile);
}
}));
));
}
passport.use("saml", new MultiSamlStrategy(
{

View File

@ -24,8 +24,6 @@ import {
reencryptSecretBlindIndexDataSalts
} from "./reencryptData";
import {
getClientIdGoogle,
getClientSecretGoogle,
getMongoURL,
getNodeEnv,
getSentryDSN
@ -55,12 +53,7 @@ export const setup = async () => {
// initializing the database connection
await DatabaseService.initDatabase(await getMongoURL());
const googleClientSecret: string = await getClientSecretGoogle();
const googleClientId: string = await getClientIdGoogle();
if (googleClientId && googleClientSecret) {
await initializePassport();
}
await initializePassport();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY

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

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

View File

@ -5,7 +5,7 @@ description: "Configure your environment variables when self-hosting Infisical."
## Backend environment variables
Depending on your choosen self hosted deployment method, you may need to configured at least the required environment variable listed below.
Depending on your choosen self hosted deployment method, you may need to configured at least the required environment variable listed below.
Other environment variables are listed below to increase the functionality of your self hosted instance based on your use case.
<Tabs>
@ -14,25 +14,40 @@ Other environment variables are listed below to increase the functionality of yo
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
</ParamField>
<ParamField query="JWT_SIGNUP_SECRET" type="string" default="none" required>
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
</ParamField>
{" "}
<ParamField query="JWT_REFRESH_SECRET" type="string" default="none" required>
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
</ParamField>
<ParamField query="JWT_SIGNUP_SECRET" type="string" default="none" required>
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex
16`
</ParamField>
<ParamField query="JWT_AUTH_SECRET" type="string" default="none" required>
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
</ParamField>
{" "}
<ParamField query="JWT_MFA_SECRET" type="string" default="none" required>
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
</ParamField>
<ParamField query="JWT_REFRESH_SECRET" type="string" default="none" required>
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex
16`
</ParamField>
<ParamField query="JWT_SERVICE_SECRET" type="string" default="none" required>
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
</ParamField>
{" "}
<ParamField query="JWT_AUTH_SECRET" type="string" default="none" required>
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex
16`
</ParamField>
{" "}
<ParamField query="JWT_MFA_SECRET" type="string" default="none" required>
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex
16`
</ParamField>
{" "}
<ParamField query="JWT_SERVICE_SECRET" type="string" default="none" required>
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex
16`
</ParamField>
<ParamField query="MONGO_URL" type="string" default="none" required>
*TLS based connection string is not yet supported
@ -58,7 +73,7 @@ Other environment variables are listed below to increase the functionality of yo
</ParamField>
<ParamField query="SMTP_SECURE" type="string" default="none" optional>
If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported
If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported
</ParamField>
<ParamField query="SMTP_FROM_ADDRESS" type="string" default="none" optional>
@ -68,9 +83,10 @@ Other environment variables are listed below to increase the functionality of yo
<ParamField query="SMTP_FROM_NAME" type="string" default="none" optional>
Name label to be used in From field (e.g. Team)
</ParamField>
</Tab>
<Tab title="Secret Integrations">
To sync secret to third party services, provide value for the related services
To sync secret to third party services, provide value for the related services
<ParamField query="CLIENT_ID_HEROKU" type="string" default="none" optional>
OAuth2 client ID for Heroku integration
@ -81,7 +97,7 @@ Other environment variables are listed below to increase the functionality of yo
</ParamField>
<ParamField query="CLIENT_ID_VERCEL" type="string" default="none" optional>
OAuth2 client ID for Vercel integration
OAuth2 client ID for Vercel integration
</ParamField>
<ParamField query="CLIENT_SECRET_VERCEL" type="string" default="none" optional>
@ -89,7 +105,7 @@ Other environment variables are listed below to increase the functionality of yo
</ParamField>
<ParamField query="CLIENT_ID_NETLIFY" type="string" default="none" optional>
OAuth2 client ID for Netlify integration
OAuth2 client ID for Netlify integration
</ParamField>
<ParamField query="CLIENT_SECRET_NETLIFY" type="string" default="none" optional>
@ -97,7 +113,7 @@ Other environment variables are listed below to increase the functionality of yo
</ParamField>
<ParamField query="CLIENT_ID_GITHUB" type="string" default="none" optional>
OAuth2 client ID for GitHub integration
OAuth2 client ID for GitHub integration
</ParamField>
<ParamField query="CLIENT_SECRET_GITHUB" type="string" default="none" optional>
@ -109,23 +125,30 @@ Other environment variables are listed below to increase the functionality of yo
</ParamField>
<ParamField query="CLIENT_ID_BITBUCKET" type="string" default="none" optional>
OAuth2 client ID for BitBucket integration
OAuth2 client ID for BitBucket integration
</ParamField>
<ParamField query="CLIENT_SECRET_BITBUCKET" type="string" default="none" optional>
OAuth2 client secret for BitBucket integration
</ParamField>
</Tab>
<Tab title="Auth Integrations">
To integrate with external auth providers, provide value for the related keys
<ParamField query="JWT_PROVIDER_AUTH_SECRET" type="string" required>
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
</ParamField>
<ParamField query="CLIENT_ID_GOOGLE" type="string" default="none" optional>
OAuth2 client ID for Google auth integration
<ParamField query="CLIENT_ID_GOOGLE_LOGIN" type="string" default="none" optional>
OAuth2 client ID for Google login
</ParamField>
<ParamField query="CLIENT_SECRET_GOOGLE" type="string" default="none" optional>
OAuth2 client secret for Google auth integration
<ParamField query="CLIENT_SECRET_GOOGLE_LOGIN" type="string" default="none" optional>
OAuth2 client secret for Google login
</ParamField>
<ParamField query="CLIENT_ID_GITHUB_LOGIN" type="string" default="none" optional>
OAuth2 client ID for GitHub login
</ParamField>
<ParamField query="CLIENT_SECRET_GITHUB_LOGIN" type="string" default="none" optional>
OAuth2 client secret for GitHub login
</ParamField>
</Tab>
<Tab title="Others">
@ -150,18 +173,44 @@ Other environment variables are listed below to increase the functionality of yo
JWT token lifetime expressed in seconds or a string describing a time span
</ParamField>
<ParamField query="MONGO_USERNAME" type="string" default="none" optional></ParamField>
{" "}
<ParamField query="MONGO_PASSWORD" type="string" default="none" optional></ParamField>
<ParamField
query="MONGO_USERNAME"
type="string"
default="none"
optional
></ParamField>
#### Error logging
Infisical uses Sentry to report error logs
<ParamField query="SENTRY_DSN" type="string" default="none" optional></ParamField>
{" "}
#### Settings
<ParamField query="INVITE_ONLY_SIGNUP" type="string" default="false" optional>
Only allow users who are invited to sign up
</ParamField>
<ParamField
query="MONGO_PASSWORD"
type="string"
default="none"
optional
></ParamField>
#### Error logging
Infisical uses Sentry to report error logs
{" "}
<ParamField
query="SENTRY_DSN"
type="string"
default="none"
optional
></ParamField>
#### Settings
{" "}
<ParamField query="INVITE_ONLY_SIGNUP" type="string" default="false" optional>
Only allow users who are invited to sign up
</ParamField>
<ParamField query="SITE_URL" type="string" default="none" optional>
Site URL - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
@ -170,6 +219,11 @@ Other environment variables are listed below to increase the functionality of yo
</Tab>
</Tabs>
## Frontend environment variables
<ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional></ParamField>
<ParamField
query="TELEMETRY_ENABLED"
type="string"
default="true"
optional
></ParamField>

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,7 +1,9 @@
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
import { faGithub,faGoogle } from "@fortawesome/free-brands-svg-icons";
import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "../v2";
@ -24,12 +26,26 @@ export default function InitialSignupStep({
window.open("/api/v1/sso/redirect/google");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
className="h-14 w-full mx-0"
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="h-12 w-full mx-0"
>
{t("signup.continue-with-google")}
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
window.open("/api/v1/sso/redirect/github");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="h-12 w-full mx-0"
>
Continue with GitHub
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
@ -37,10 +53,10 @@ export default function InitialSignupStep({
onClick={() => {
setIsSignupWithEmail(true);
}}
isFullWidth
className="h-14 w-full mx-0"
leftIcon={<FontAwesomeIcon icon={faEnvelope} className="mr-2" />}
className="h-12 w-full mx-0"
>
Sign Up with email
Continue with Email
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
@ -48,10 +64,10 @@ export default function InitialSignupStep({
colorSchema="primary"
variant="outline_bg"
onClick={() => router.push("/saml-sso")}
isFullWidth
className="h-14 w-full mx-0"
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="h-12 w-full mx-0"
>
Continue with SAML SSO
Continue with SSO
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] px-8 text-center mt-6 text-xs text-bunker-400'>

View File

@ -51,10 +51,10 @@ const buttonVariants = cva(
false: ""
},
size: {
xs: ["text-xs", "py-1", "px-1"],
sm: ["text-sm", "py-2", "px-2"],
md: ["text-md", "py-2", "px-4"],
lg: ["text-lg", "py-2", "px-8"]
xs: ["text-xs", "py-1", "px-2"],
sm: ["text-sm", "py-2", "px-4"],
md: ["text-md", "py-2", "px-5"],
lg: ["text-lg", "py-2", "px-6"]
}
},
compoundVariants: [
@ -186,16 +186,17 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
className="absolute rounded-xl opacity-80"
/>
)}
<div
className={twMerge(
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
loadingToggleClass,
leftIcon && "ml-2",
size === "xs" ? "mr-1" : "mr-2"
)}
>
{leftIcon}
</div>
{leftIcon && (
<div
className={twMerge(
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
loadingToggleClass,
size === "xs" ? "mr-1" : "mr-2"
)}
>
{leftIcon}
</div>
)}
<span
className={twMerge(
"transition-all",
@ -205,15 +206,16 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
>
{children}
</span>
<div
className={twMerge(
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
loadingToggleClass,
size === "xs" ? "ml-1" : "ml-2"
)}
>
{rightIcon}
</div>
{rightIcon && (
<div
className={twMerge(
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
loadingToggleClass
)}
>
{rightIcon}
</div>
)}
</button>
);
}

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,18 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
variant = "filled",
size = "md",
isReadOnly,
autoCapitalization,
...props
},
ref
): JSX.Element => {
const handleInput = (event: ChangeEvent<HTMLInputElement>) => {
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 +102,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

@ -54,13 +54,14 @@ export const useGetProjectSecrets = ({
env,
decryptFileKey,
isPaused,
folderId
folderId,
secretPath
}: GetProjectSecretsDTO) =>
useQuery({
// wait for all values to be available
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId || secretPath),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
select: useCallback(
(data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;

View File

@ -293,12 +293,10 @@ export const useRevokeMySessions = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
console.log("useRevokeAllSessions 1");
const { data } = await apiRequest.delete(
"/api/v2/users/me/sessions"
);
console.log("useRevokeAllSessions 2: ", data);
return data;
},
onSuccess() {

View File

@ -1,3 +1,4 @@
export { useDebounce } from "./useDebounce";
export { useLeaveConfirm } from "./useLeaveConfirm";
export { usePersistentState } from "./usePersistentState";
export { usePopUp } from "./usePopUp";

View File

@ -0,0 +1,26 @@
import { useEffect, useState } from "react";
// Ref: https://usehooks.com/useDebounce/
export const useDebounce = <T extends unknown>(value: T, delay = 500): T => {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
};

View File

@ -740,7 +740,7 @@ export const DashboardPage = () => {
return (
<div className="container mx-auto h-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<form autoComplete="off" className="h-full">
<form autoComplete="off" className="h-full flex flex-col">
{/* breadcrumb row */}
<div className="relative right-6 -top-2 mb-2 ml-6">
<NavHeader
@ -924,8 +924,8 @@ export const DashboardPage = () => {
</div>
<div
className={`${
isEmptyPage ? "flex flex-col items-center justify-center" : ""
} no-scrollbar::-webkit-scrollbar mt-3 h-3/4 overflow-x-hidden overflow-y-scroll no-scrollbar`}
isEmptyPage ? "flex flex-col flex-grow items-center justify-center" : ""
} no-scrollbar::-webkit-scrollbar mt-3 flex flex-col overflow-x-hidden overflow-y-scroll no-scrollbar`}
ref={secretContainer}
>
{!isEmptyPage && (
@ -935,7 +935,7 @@ export const DashboardPage = () => {
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
>
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar flex-grow">
<table className="secret-table relative">
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
<tbody className="max-h-96 overflow-y-auto">
@ -971,6 +971,7 @@ export const DashboardPage = () => {
register={register}
control={control}
setValue={setValue}
autoCapitalization={currentWorkspace?.autoCapitalization}
/>
))}
{!isReadOnly && !isRollbackMode && (
@ -1016,9 +1017,12 @@ export const DashboardPage = () => {
</FormProvider>
<SecretDropzone
workspaceId={workspaceId}
isSmaller={!isEmptyPage}
onParsedEnv={handleUploadedEnv}
onAddNewSecret={onAppendSecret}
environments={userAvailableEnvs}
decryptFileKey={latestFileKey!}
/>
</div>
{/* secrets table and drawers, modals */}

View File

@ -75,6 +75,13 @@ export type FormData = yup.InferType<typeof schema>;
export type TSecretDetailsOpen = { index: number; id: string };
export type TSecOverwriteOpt = { secrets: Record<string, { comments: string[]; value: string }> };
// to convert multi line into single line ones by quoting them and changing to string \n
const formatMultiValueEnv = (val?: string) => {
if (!val) return "";
if (!val.match("\n")) return val;
return `"${val.replace(/\n/g, "\\n")}"`;
};
export const downloadSecret = (
secrets: FormData["secrets"] = [],
importedSecrets: { key: string; value?: string; comment?: string }[] = [],
@ -86,9 +93,11 @@ export const downloadSecret = (
});
const finalSecret = [...importedSecrets];
secrets.forEach(({ key, value, valueOverride, overrideAction, comment }) => {
const finalVal =
overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value;
const newValue = {
key,
value: overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value,
value: formatMultiValueEnv(finalVal),
comment
};
// can also be zero thus failing

View File

@ -1,26 +1,128 @@
import { ChangeEvent, DragEvent } from "react";
import { ChangeEvent, DragEvent, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { faUpload } from "@fortawesome/free-solid-svg-icons";
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
import {
faClone,
faKey,
faSearch,
faSquareXmark,
faUpload
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import { twMerge } from "tailwind-merge";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionalityj
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality
import { parseDotEnv } from "@app/components/utilities/parseDotEnv";
import { Button } from "@app/components/v2";
import { useToggle } from "@app/hooks/useToggle";
import {
Button,
Checkbox,
EmptyState,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
ModalTrigger,
Select,
SelectItem,
Skeleton,
Tooltip
} from "@app/components/v2";
import { useDebounce, usePopUp, useToggle } from "@app/hooks";
import { useGetProjectSecrets } from "@app/hooks/api";
import { UserWsKeyPair } from "@app/hooks/api/types";
const formSchema = yup.object({
environment: yup.string().required().label("Environment").trim(),
secretPath: yup
.string()
.required()
.label("Secret Path")
.trim()
.transform((val) =>
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
),
secrets: yup.lazy((val) => {
const valSchema: Record<string, yup.StringSchema> = {};
Object.keys(val).forEach((key) => {
valSchema[key] = yup.string().trim();
});
return yup.object(valSchema);
})
});
type TFormSchema = yup.InferType<typeof formSchema>;
const parseJson = (src: ArrayBuffer) => {
const file = src.toString();
const formatedData: Record<string, string> = JSON.parse(file);
const env: Record<string, { value: string; comments: string[] }> = {};
Object.keys(formatedData).forEach((key) => {
if (typeof formatedData[key] === "string") {
env[key] = { value: formatedData[key], comments: [] };
}
});
return env;
};
type Props = {
isSmaller: boolean;
onParsedEnv: (env: Record<string, { value: string; comments: string[] }>) => void;
onAddNewSecret?: () => void;
environments?: { name: string; slug: string }[];
workspaceId: string;
decryptFileKey: UserWsKeyPair;
};
export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props): JSX.Element => {
export const SecretDropzone = ({
isSmaller,
onParsedEnv,
onAddNewSecret,
environments = [],
workspaceId,
decryptFileKey
}: Props): JSX.Element => {
const { t } = useTranslation();
const [isDragActive, setDragActive] = useToggle();
const [isLoading, setIsLoading] = useToggle();
const { createNotification } = useNotificationContext();
const { popUp, handlePopUpClose, handlePopUpToggle } = usePopUp(["importSecEnv"] as const);
const [searchFilter, setSearchFilter] = useState("");
const [shouldIncludeValues, setShouldIncludeValues] = useState(true);
const {
handleSubmit,
control,
watch,
register,
reset,
setValue,
formState: { isDirty }
} = useForm<TFormSchema>({
resolver: yupResolver(formSchema),
defaultValues: { secretPath: "/", environment: environments?.[0]?.slug }
});
const secretPath = watch("secretPath");
const selectedEnvSlug = watch("environment");
const debouncedSecretPath = useDebounce(secretPath);
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId,
env: selectedEnvSlug,
secretPath: debouncedSecretPath,
isPaused: !(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedSecretPath)),
decryptFileKey
});
useEffect(() => {
setValue("secrets", {});
setSearchFilter("");
}, [debouncedSecretPath]);
const handleDrag = (e: DragEvent) => {
e.preventDefault();
@ -32,7 +134,7 @@ export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props
}
};
const parseFile = (file?: File) => {
const parseFile = (file?: File, isJson?: boolean) => {
const reader = new FileReader();
if (!file) {
createNotification({
@ -47,7 +149,9 @@ export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props
reader.onload = (event) => {
if (!event?.target?.result) return;
// parse function's argument looks like to be ArrayBuffer
const env = parseDotEnv(event.target.result as ArrayBuffer);
const env = isJson
? parseJson(event.target.result as ArrayBuffer)
: parseDotEnv(event.target.result as ArrayBuffer);
setIsLoading.off();
onParsedEnv(env);
};
@ -74,7 +178,31 @@ export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
parseFile(e.target?.files?.[0]);
parseFile(e.target?.files?.[0], e.target?.files?.[0]?.type === "application/json");
};
const handleFormSubmit = (data: TFormSchema) => {
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
Object.keys(data.secrets || {}).forEach((key) => {
if (data.secrets[key]) {
secretsToBePulled[key] = {
value: (shouldIncludeValues && data.secrets[key]) || "",
comments: [""]
};
}
});
onParsedEnv(secretsToBePulled);
handlePopUpClose("importSecEnv");
reset();
};
const handleSecSelectAll = () => {
if (secrets?.secrets) {
setValue(
"secrets",
secrets?.secrets?.reduce((prev, curr) => ({ ...prev, [curr.key]: curr.value }), {})
);
}
};
return (
@ -84,9 +212,9 @@ export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props
onDragOver={handleDrag}
onDrop={handleDrop}
className={twMerge(
"relative mx-0.5 mb-4 mt-4 flex w-full max-w-[calc(100vw-292px)] cursor-pointer items-center justify-center space-x-2 rounded-md bg-mineshaft-900 py-8 px-2 text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
"relative mx-0.5 mb-4 mt-4 flex cursor-pointer items-center justify-center rounded-md bg-mineshaft-900 py-4 text-sm px-2 text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
isDragActive && "opacity-100",
!isSmaller && "max-w-3xl flex-col space-y-4 py-20",
!isSmaller && "w-full max-w-3xl flex-col space-y-4 py-20",
isLoading && "bg-bunker-800"
)}
>
@ -95,35 +223,184 @@ export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
</div>
) : (
<>
<div>
<FontAwesomeIcon icon={faUpload} size={isSmaller ? "2x" : "5x"} />
</div>
<div>
<p className="">{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}</p>
</div>
<input
id="fileSelect"
type="file"
className="absolute h-full w-full cursor-pointer opacity-0"
accept=".txt,.env,.yml,.yaml"
onChange={handleFileUpload}
/>
{!isSmaller && (
<>
<div className="flex w-full flex-row items-center justify-center py-4">
<div className="w-1/5 border-t border-mineshaft-700" />
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
<div className="w-1/5 border-t border-mineshaft-700" />
</div>
<div>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="flex items-center justify-cente flex-col space-y-2">
<div>
<FontAwesomeIcon icon={faUpload} size={isSmaller ? "2x" : "5x"} />
</div>
<div>
<p className="">{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}</p>
</div>
<input
id="fileSelect"
type="file"
className="absolute h-full w-full cursor-pointer opacity-0"
accept=".txt,.env,.yml,.yaml,.json"
onChange={handleFileUpload}
/>
<div
className={twMerge(
"flex w-full flex-row items-center justify-center py-4",
isSmaller && "py-1"
)}
>
<div className="w-1/5 border-t border-mineshaft-700" />
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
<div className="w-1/5 border-t border-mineshaft-700" />
</div>
<div className="flex items-center justify-center space-x-8">
<Modal
isOpen={popUp.importSecEnv.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("importSecEnv", isOpen);
reset();
setSearchFilter("");
}}
>
<ModalTrigger asChild>
<Button variant="star" size={isSmaller ? "xs" : "sm"}>
Copy Secrets From An Environment
</Button>
</ModalTrigger>
<ModalContent
className="max-w-2xl"
title="Copy Secret From An Environment"
subTitle="Copy/paste secrets from other environments into this context"
>
<form>
<div className="flex items-center space-x-2">
<Controller
control={control}
name="environment"
render={({ field: { value, onChange } }) => (
<FormControl label="Environment" isRequired className="w-1/3">
<Select
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
defaultValue={environments?.[0]?.slug}
position="popper"
>
{environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<FormControl label="Secret Path" className="flex-grow" isRequired>
<Input
{...register("secretPath")}
placeholder="Provide a path, default is /"
/>
</FormControl>
</div>
<div className="border-t border-mineshaft-600 pt-4">
<div className="mb-4 flex items-center justify-between">
<div>Secrets</div>
<div className="w-1/2 flex items-center space-x-2">
<Input
placeholder="Search for secret"
value={searchFilter}
size="xs"
leftIcon={<FontAwesomeIcon icon={faSearch} />}
onChange={(evt) => setSearchFilter(evt.target.value)}
/>
<Tooltip content="Select All">
<IconButton
ariaLabel="Select all"
variant="outline_bg"
size="xs"
onClick={handleSecSelectAll}
>
<FontAwesomeIcon icon={faSquareCheck} size="lg" />
</IconButton>
</Tooltip>
<Tooltip content="Unselect All">
<IconButton
ariaLabel="UnSelect all"
variant="outline_bg"
size="xs"
onClick={() => reset()}
>
<FontAwesomeIcon icon={faSquareXmark} size="lg" />
</IconButton>
</Tooltip>
</div>
</div>
{!isSecretsLoading && !secrets?.secrets?.length && (
<EmptyState title="No secrets found" icon={faKey} />
)}
<div className="grid grid-cols-2 gap-4 max-h-64 overflow-auto thin-scrollbar ">
{isSecretsLoading &&
Array.apply(0, Array(2)).map((_x, i) => (
<Skeleton
key={`secret-pull-loading-${i + 1}`}
className="bg-mineshaft-700"
/>
))}
{secrets?.secrets
?.filter(({ key }) =>
key.toLowerCase().includes(searchFilter.toLowerCase())
)
?.map(({ _id, key, value: secVal }) => (
<Controller
key={`pull-secret--${_id}`}
control={control}
name={`secrets.${key}`}
render={({ field: { value, onChange } }) => (
<Checkbox
id={`pull-secret-${_id}`}
isChecked={Boolean(value)}
onCheckedChange={(isChecked) => onChange(isChecked ? secVal : "")}
>
{key}
</Checkbox>
)}
/>
))}
</div>
<div className="mt-6 mb-4">
<Checkbox
id="populate-include-value"
isChecked={shouldIncludeValues}
onCheckedChange={(isChecked) =>
setShouldIncludeValues(isChecked as boolean)
}
>
Include secret values
</Checkbox>
</div>
<div className="flex items-center space-x-2">
<Button
leftIcon={<FontAwesomeIcon icon={faClone} />}
type="submit"
isDisabled={!isDirty}
>
Paste Secrets
</Button>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</div>
</div>
</form>
</ModalContent>
</Modal>
{!isSmaller && (
<Button variant="star" onClick={onAddNewSecret}>
Add a new secret
</Button>
</div>
</>
)}{" "}
</>
)}
</div>
</div>
</form>
)}
</div>
);

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>
@ -278,7 +281,6 @@ export const SecretInputRow = memo(
value={editorRef}
isVisible={!isSecretValueHidden}
onChange={(val, html) => {
console.log(val);
onChange(val);
setEditorRef(html);
}}

View File

@ -2,7 +2,8 @@ import { FormEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
import { faGithub,faGoogle } from "@fortawesome/free-brands-svg-icons";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import axios from "axios"
@ -34,7 +35,6 @@ export const InitialStep = ({
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const [loginEmailChosen, setLoginEmailChosen] = useState(false);
const { data: serverDetails } = useFetchServerStatus();
const queryParams = new URLSearchParams(window.location.search);
@ -68,8 +68,7 @@ export const InitialStep = ({
// send request to server endpoint
const instance = axios.create()
const cliResp = await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse })
console.log(cliResp)
await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse })
// cli page
router.push("/cli-redirect");
@ -121,22 +120,53 @@ export const InitialStep = ({
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant={loginEmailChosen ? "outline_bg" : "solid"}
variant="outline_bg"
onClick={() => {
const callbackPort = queryParams.get("callback_port");
window.open(`/api/v1/sso/redirect/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
className="h-12 w-full mx-0"
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="h-11 w-full mx-0"
>
{t("login.continue-with-google")}
</Button>
</div>
{loginEmailChosen && <>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
const callbackPort = queryParams.get("callback_port");
window.open(`/api/v1/sso/redirect/github${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="h-11 w-full mx-0"
>
Continue with GitHub
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setStep(2);
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="h-11 w-full mx-0"
>
Continue with SSO
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center my-4 py-2'>
<div className='w-full border-t border-mineshaft-500' />
<div className='w-full border-t border-mineshaft-400/60' />
<span className="mx-2 text-mineshaft-200 text-xs">or</span>
<div className='w-full border-t border-mineshaft-400/60' />
</div>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md'>
<Input
@ -146,7 +176,7 @@ export const InitialStep = ({
placeholder="Enter your email..."
isRequired
autoComplete="username"
className="h-12"
className="h-11"
/>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
@ -158,49 +188,21 @@ export const InitialStep = ({
isRequired
autoComplete="current-password"
id="current-password"
className="h-12 select:-webkit-autofill:focus"
className="h-11 select:-webkit-autofill:focus"
/>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-5'>
<Button
type="submit"
size="sm"
isFullWidth
className='h-12'
className='h-11'
colorSchema="primary"
variant="solid"
isLoading={isLoading}
> Continue with Email </Button>
</div>
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center mt-4 py-2'>
<div className='w-full border-t border-mineshaft-500' />
</div></>}
{!loginEmailChosen && <div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<Button
onClick={() => {
setLoginEmailChosen(true);
}}
size="sm"
isFullWidth
className='h-12'
colorSchema="primary"
variant="outline_bg"
> Continue with Email </Button>
</div>}
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setStep(2);
}}
isFullWidth
className="h-12 w-full mx-0"
>
Continue with SAML SSO
</Button>
</div>
{
!serverDetails?.inviteOnlySignup ?
<div className="mt-6 text-bunker-400 text-sm flex flex-row">

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>

View File

@ -17,7 +17,10 @@ import {
const authMethods = [
{ label: "Email", value: "email" },
{ label: "Google SSO", value: "google" },
{ label: "Okta SAML 2.0", value: "okta-saml" },
{ label: "GitHub SSO", value: "github" },
{ label: "Okta SAML", value: "okta-saml" },
{ label: "Azure SAML", value: "azure-saml" },
{ label: "JumpCloud SAML", value: "jumpcloud-saml" }
];
const schema = yup.object({
@ -54,9 +57,13 @@ export const AuthMethodSection = () => {
authMethod
}: FormData) => {
try {
if (authMethod === "okta-saml") {
if (
authMethod === "okta-saml"
|| authMethod === "azure-saml"
|| authMethod === "jumpcloud-saml"
) {
createNotification({
text: "Okta SAML 2.0 can only be configured in your organization settings",
text: "SAML authentication can only be configured in your organization settings",
type: "error"
});

View File

@ -3,7 +3,7 @@ import crypto from "crypto";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { faInfoCircle, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import jsrp from "jsrp";
import nacl from "tweetnacl";
@ -198,7 +198,7 @@ export const UserInfoSSOStep = ({
localStorage.setItem("orgData.id", orgId);
localStorage.setItem("projectData.id", project._id);
setStep(1);
setStep(1);
} catch (error) {
setIsLoading(false);
console.error(error);
@ -256,7 +256,7 @@ export const UserInfoSSOStep = ({
)}
<div className="mt-2 flex lg:w-1/6 w-1/4 min-w-[20rem] max-h-60 w-full flex-col items-center justify-center rounded-lg py-2">
<InputField
label={t("section.password.password")}
label="Infisical Password"
onChangeHandler={(pass: string) => {
setPassword(pass);
checkPassword({
@ -272,6 +272,7 @@ export const UserInfoSSOStep = ({
autoComplete="new-password"
id="new-password"
/>
<div className="mt-2 w-min min-w-[20rem] max-h-60 flex-col items-center justify-center rounded-md px-1.5 bg-mineshaft-500 text-mineshaft-300 text-xs p-1.5"><FontAwesomeIcon icon={faInfoCircle} className="mr-1.5" />Infisical Password is used as part of the encryption mechanism so that even the authentication provider is not able to access your secrets.</div>
{Object.keys(errors).length > 0 && (
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-2 text-sm text-gray-400">{t("section.password.validate-base")}</div>
@ -307,7 +308,7 @@ export const UserInfoSSOStep = ({
onClick={signupErrorCheck}
size="sm"
isFullWidth
className='h-14'
className='h-12'
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}