Compare commits

...

44 Commits

Author SHA1 Message Date
1066710c4f Fix: Rename tips to tip 2024-07-22 13:14:25 +02:00
37137b8c68 Merge pull request #2156 from akhilmhdh/feat/login-cli-token-paste
Feat/login cli token paste
2024-07-21 23:37:41 -04:00
8b10cf863d add verify jwt, update text phrasing and fix double render input field 2024-07-21 23:35:09 -04:00
=
eb45bed7d9 feat: updated text over cli 2024-07-21 22:34:23 +05:30
=
1ee65205a0 feat: ui option to paste token on sending token to cli fails 2024-07-21 19:25:41 +05:30
=
f41272d4df feat: added option for manually adding token in browser login failure 2024-07-21 19:24:54 +05:30
8bf4df9f27 Merge pull request #2123 from akhilmhdh/cli-operation-to-raw
Switched CLI and K8s operator to secret raw endpoint
2024-07-20 13:19:13 -04:00
037a8f2ebb Merge branch 'main' into cli-operation-to-raw 2024-07-20 13:15:56 -04:00
14bc436283 Merge pull request #2155 from Infisical/fix/import-failing 2024-07-20 09:19:10 -04:00
a108c7dde1 fix: resolved get secret failing when import is invalid 2024-07-20 14:42:26 +05:30
54ccd73d2a Merge pull request #2153 from aheruz/feat/secret-request-bypass-reason
feat: add bypass reason on bypassed secret requests
2024-07-19 17:22:18 -04:00
729ca7b6d6 small nits for bypass pr 2024-07-19 17:19:06 -04:00
754db67f11 update chart version 2024-07-19 16:47:21 -04:00
f97756a07b doc: approval workflows revamp 2024-07-19 22:14:40 +02:00
22df51ab8e feat(frontend): send bypass reason on bypassed merges 2024-07-19 21:33:41 +02:00
bff8f55ea2 feat(backend): save bypass reason on secret_approval_requests 2024-07-19 21:31:48 +02:00
2f17f5e7df update bot not found message 2024-07-19 14:50:16 -04:00
72d2247bf2 add support for --projectId when user is logged in on secrets set 2024-07-19 12:55:52 -04:00
4ecd4c0337 Merge pull request #2152 from aheruz/feat/ENG-985-secret-share-organization
feat(ENG-985): secret share within organization
2024-07-19 10:30:32 -04:00
=
538613dd40 feat: added bot error message for old instance 2024-07-19 19:59:16 +05:30
4c5c24f689 feat: remove accessType hardcap in migration + style 2024-07-19 16:19:45 +02:00
dead16a98a Merge pull request #2147 from Infisical/daniel/deployment-documentation
docs(deployment): Standalone and high availability deployment
2024-07-19 08:28:26 -04:00
224368b172 fix: general access accessible only from private creation 2024-07-19 12:20:46 +02:00
2f2c9d4508 style: message structure on SecretTable 2024-07-19 03:36:24 +02:00
774017adbe style: remove logs 2024-07-19 03:32:46 +02:00
f9d1d9c89f doc: Adding accessType on secret sharing 2024-07-19 03:26:26 +02:00
eb82fc0d9a feat(frontend): Adding accessType on secret sharing 2024-07-19 03:17:47 +02:00
e45585a909 feat(backend): Adding accessType on secret sharing 2024-07-19 03:15:38 +02:00
6f0484f074 update bot key message 2024-07-18 18:29:39 -04:00
4ba529f22d print error message when projectId flag is not passed for set secret 2024-07-18 18:05:01 -04:00
5360fb033a fix set secret 2024-07-18 17:53:25 -04:00
7622cac07e Update high-availability.mdx 2024-07-18 09:58:48 +02:00
a101602e0a Update overview.mdx 2024-07-18 08:25:37 +02:00
ca63a7baa7 Standalone binary docs 2024-07-18 08:25:31 +02:00
ff4f15c437 HA deployment docs 2024-07-18 08:25:26 +02:00
d6c2715852 Update mint.json 2024-07-18 08:25:14 +02:00
fc386c0cbc Create haproxy-stats.png 2024-07-18 08:25:11 +02:00
263a88379f Create ha-stack.png 2024-07-18 08:25:07 +02:00
=
1f6c33bdb8 feat: updated bot not found error message 2024-07-18 01:46:28 +05:30
=
1d6d424c91 fix: removed print not used 2024-07-16 14:08:09 +05:30
=
c39ea130b1 feat: changed backup secret to keyring and resolved backup not working in previous versions 2024-07-16 14:05:38 +05:30
=
5514508482 refactor(cli): removed unused secret logic for e2ee used 2024-07-15 01:23:47 +05:30
=
5921dcaa51 feat: switched operator to raw endpoints for secret management 2024-07-15 01:23:03 +05:30
=
b2c62c4193 feat(cli): changed all secret endpoint to raw endpoint 2024-07-14 15:05:53 +05:30
73 changed files with 1513 additions and 1133 deletions

View File

@ -0,0 +1,23 @@
import { Knex } from "knex";
import { SecretSharingAccessType } from "@app/lib/types";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SecretSharing, "accessType");
if (!hasColumn) {
await knex.schema.table(TableName.SecretSharing, (table) => {
table.string("accessType").notNullable().defaultTo(SecretSharingAccessType.Anyone);
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SecretSharing, "accessType");
if (hasColumn) {
await knex.schema.table(TableName.SecretSharing, (table) => {
table.dropColumn("accessType");
});
}
}

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "bypassReason");
if (!hasColumn) {
await knex.schema.table(TableName.SecretApprovalRequest, (table) => {
table.string("bypassReason").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "bypassReason");
if (hasColumn) {
await knex.schema.table(TableName.SecretApprovalRequest, (table) => {
table.dropColumn("bypassReason");
});
}
}

View File

@ -15,6 +15,7 @@ export const SecretApprovalRequestsSchema = z.object({
conflicts: z.unknown().nullable().optional(),
slug: z.string(),
folderId: z.string().uuid(),
bypassReason: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
isReplicated: z.boolean().nullable().optional(),

View File

@ -5,6 +5,8 @@
import { z } from "zod";
import { SecretSharingAccessType } from "@app/lib/types";
import { TImmutableDBKeys } from "./models";
export const SecretSharingSchema = z.object({
@ -16,6 +18,7 @@ export const SecretSharingSchema = z.object({
expiresAt: z.date(),
userId: z.string().uuid().nullable().optional(),
orgId: z.string().uuid().nullable().optional(),
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization),
createdAt: z.date(),
updatedAt: z.date(),
expiresAfterViews: z.number().nullable().optional()

View File

@ -117,6 +117,9 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
params: z.object({
id: z.string()
}),
body: z.object({
bypassReason: z.string().optional()
}),
response: {
200: z.object({
approval: SecretApprovalRequestsSchema
@ -130,7 +133,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
approvalId: req.params.id
approvalId: req.params.id,
bypassReason: req.body.bypassReason
});
return { approval };
}

View File

@ -94,6 +94,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("projectId").withSchema(TableName.Environment),
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"),
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals")
);
@ -130,7 +131,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
name: el.policyName,
approvals: el.policyApprovals,
secretPath: el.policySecretPath,
enforcementLevel: el.policyEnforcementLevel
enforcementLevel: el.policyEnforcementLevel,
envId: el.policyEnvId
}
}),
childrenMapper: [

View File

@ -7,6 +7,7 @@ import {
SecretType,
TSecretApprovalRequestsSecretsInsert
} from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { groupBy, pick, unique } from "@app/lib/fn";
@ -15,6 +16,7 @@ import { EnforcementLevel } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import {
fnSecretBlindIndexCheck,
@ -31,6 +33,8 @@ import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
@ -63,8 +67,11 @@ type TSecretApprovalRequestServiceFactoryDep = {
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectById">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<TUserDALFactory, "find" | "findOne">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
};
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
@ -83,7 +90,10 @@ export const secretApprovalRequestServiceFactory = ({
snapshotService,
secretVersionDAL,
secretQueueService,
projectBotService
projectBotService,
smtpService,
userDAL,
projectEnvDAL
}: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
@ -258,7 +268,8 @@ export const secretApprovalRequestServiceFactory = ({
actor,
actorId,
actorOrgId,
actorAuthMethod
actorAuthMethod,
bypassReason
}: TMergeSecretApprovalRequestDTO) => {
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
@ -470,7 +481,8 @@ export const secretApprovalRequestServiceFactory = ({
conflicts: JSON.stringify(conflicts),
hasMerged: true,
status: RequestState.Closed,
statusChangedByUserId: actorId
statusChangedByUserId: actorId,
bypassReason
},
tx
);
@ -489,6 +501,35 @@ export const secretApprovalRequestServiceFactory = ({
actorId,
actor
});
if (isSoftEnforcement) {
const cfg = getConfig();
const project = await projectDAL.findProjectById(projectId);
const env = await projectEnvDAL.findOne({ id: policy.envId });
const requestedByUser = await userDAL.findOne({ id: actorId });
const approverUsers = await userDAL.find({
$in: {
id: policy.approvers.map((approver: { userId: string }) => approver.userId)
}
});
await smtpService.sendMail({
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
subjectLine: "Infisical Secret Change Policy Bypassed",
substitutions: {
projectName: project.name,
requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`,
requesterEmail: requestedByUser.email,
bypassReason,
secretPath: policy.secretPath,
environment: env.name,
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
},
template: SmtpTemplates.AccessSecretRequestBypassed
});
}
return mergeStatus;
};

View File

@ -39,6 +39,7 @@ export type TGenerateSecretApprovalRequestDTO = {
export type TMergeSecretApprovalRequestDTO = {
approvalId: string;
bypassReason?: string;
} & Omit<TProjectPermission, "projectId">;
export type TStatusChangeDTO = {

View File

@ -47,3 +47,8 @@ export enum EnforcementLevel {
Hard = "hard",
Soft = "soft"
}
export enum SecretSharingAccessType {
Anyone = "anyone",
Organization = "organization"
}

View File

@ -737,7 +737,8 @@ export const registerRoutes = async (
const secretSharingService = secretSharingServiceFactory({
permissionService,
secretSharingDAL
secretSharingDAL,
orgDAL
});
const secretApprovalRequestService = secretApprovalRequestServiceFactory({
@ -754,7 +755,10 @@ export const registerRoutes = async (
secretApprovalRequestDAL,
snapshotService,
secretVersionTagDAL,
secretQueueService
secretQueueService,
smtpService,
userDAL,
projectEnvDAL
});
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({

View File

@ -1,6 +1,7 @@
import { z } from "zod";
import { SecretSharingSchema } from "@app/db/schemas";
import { SecretSharingAccessType } from "@app/lib/types";
import {
publicEndpointLimit,
publicSecretShareCreationLimit,
@ -55,14 +56,18 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
iv: true,
tag: true,
expiresAt: true,
expiresAfterViews: true
expiresAfterViews: true,
accessType: true
}).extend({
orgName: z.string().optional()
})
}
},
handler: async (req) => {
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretByIdAndHashedHex(
req.params.id,
req.query.hashedHex
req.query.hashedHex,
req.permission?.orgId
);
if (!sharedSecret) return undefined;
return {
@ -70,7 +75,9 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
iv: sharedSecret.iv,
tag: sharedSecret.tag,
expiresAt: sharedSecret.expiresAt,
expiresAfterViews: sharedSecret.expiresAfterViews
expiresAfterViews: sharedSecret.expiresAfterViews,
accessType: sharedSecret.accessType,
orgName: sharedSecret.orgName
};
}
});
@ -104,7 +111,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
tag,
hashedHex,
expiresAt: new Date(expiresAt),
expiresAfterViews
expiresAfterViews,
accessType: SecretSharingAccessType.Anyone
});
return { id: sharedSecret.id };
}
@ -123,7 +131,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
tag: z.string(),
hashedHex: z.string(),
expiresAt: z.string(),
expiresAfterViews: z.number()
expiresAfterViews: z.number(),
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
}),
response: {
200: z.object({
@ -145,7 +154,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
tag,
hashedHex,
expiresAt: new Date(expiresAt),
expiresAfterViews
expiresAfterViews,
accessType: req.body.accessType
});
return { id: sharedSecret.id };
}

View File

@ -24,10 +24,10 @@ export const getBotKeyFnFactory = (
const bot = await projectBotDAL.findOne({ projectId: project.id });
if (!bot) throw new BadRequestError({ message: "Failed to find bot key" });
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" });
if (!bot) throw new BadRequestError({ message: "Failed to find bot key", name: "bot_not_found_error" });
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active", name: "bot_not_found_error" });
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
throw new BadRequestError({ message: "Encryption key missing" });
throw new BadRequestError({ message: "Encryption key missing", name: "bot_not_found_error" });
const botPrivateKey = getBotPrivateKey({ bot });

View File

@ -90,7 +90,7 @@ export const fnSecretsFromImports = async ({
const secretsFromdeeperImportGroupedByFolderId = groupBy(secretsFromDeeperImports, (i) => i.importFolderId);
const secrets = allowedImports.map(({ importPath, importEnv, id, folderId }, i) => {
const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`][0];
const sourceImportFolder = importedFolderGroupBySourceImport?.[`${importEnv.id}-${importPath}`]?.[0];
const folderDeeperImportSecrets =
secretsFromdeeperImportGroupedByFolderId?.[sourceImportFolder?.id || ""]?.[0]?.secrets || [];

View File

@ -1,6 +1,8 @@
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { SecretSharingAccessType } from "@app/lib/types";
import { TOrgDALFactory } from "../org/org-dal";
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
import {
TCreatePublicSharedSecretDTO,
@ -12,13 +14,15 @@ import {
type TSecretSharingServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
secretSharingDAL: TSecretSharingDALFactory;
orgDAL: TOrgDALFactory;
};
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
export const secretSharingServiceFactory = ({
permissionService,
secretSharingDAL
secretSharingDAL,
orgDAL
}: TSecretSharingServiceFactoryDep) => {
const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => {
const {
@ -30,6 +34,7 @@ export const secretSharingServiceFactory = ({
encryptedValue,
iv,
tag,
accessType,
hashedHex,
expiresAt,
expiresAfterViews
@ -62,13 +67,14 @@ export const secretSharingServiceFactory = ({
expiresAt,
expiresAfterViews,
userId: actorId,
orgId
orgId,
accessType
});
return { id: newSharedSecret.id };
};
const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => {
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = createSharedSecretInput;
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews, accessType } = createSharedSecretInput;
if (new Date(expiresAt) < new Date()) {
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
}
@ -92,7 +98,8 @@ export const secretSharingServiceFactory = ({
tag,
hashedHex,
expiresAt,
expiresAfterViews
expiresAfterViews,
accessType
});
return { id: newSharedSecret.id };
};
@ -105,9 +112,21 @@ export const secretSharingServiceFactory = ({
return userSharedSecrets;
};
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => {
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string, orgId?: string) => {
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
if (!sharedSecret) return;
const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : "";
// Support organization level access for secret sharing
if (sharedSecret.accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) {
return {
...sharedSecret,
encryptedValue: "",
iv: "",
tag: "",
orgName
};
}
if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) {
return;
}
@ -118,7 +137,10 @@ export const secretSharingServiceFactory = ({
}
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
}
return sharedSecret;
if (sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId) {
return { ...sharedSecret, orgName };
}
return { ...sharedSecret, orgName: undefined };
};
const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => {

View File

@ -1,3 +1,5 @@
import { SecretSharingAccessType } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
export type TSharedSecretPermission = {
@ -6,6 +8,7 @@ export type TSharedSecretPermission = {
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
orgId: string;
accessType?: SecretSharingAccessType;
};
export type TCreatePublicSharedSecretDTO = {
@ -15,6 +18,7 @@ export type TCreatePublicSharedSecretDTO = {
hashedHex: string;
expiresAt: Date;
expiresAfterViews: number;
accessType: SecretSharingAccessType;
};
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;

View File

@ -23,6 +23,7 @@ export enum SmtpTemplates {
EmailMfa = "emailMfa.handlebars",
UnlockAccount = "unlockAccount.handlebars",
AccessApprovalRequest = "accessApprovalRequest.handlebars",
AccessSecretRequestBypassed = "accessSecretRequestBypassed.handlebars",
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
NewDeviceJoin = "newDevice.handlebars",
OrgInvite = "organizationInvitation.handlebars",

View File

@ -0,0 +1,28 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Secret Approval Request Policy Bypassed</title>
</head>
<body>
<h1>Infisical</h1>
<h2>Secret Approval Request Bypassed</h2>
<p>A secret approval request has been bypassed in the project "{{projectName}}".</p>
<p>
{{requesterFullName}} ({{requesterEmail}}) has merged
a secret to environment {{environment}} at secret path {{secretPath}}
without obtaining the required approvals.
</p>
<p>
The following reason was provided for bypassing the policy:
<em>{{bypassReason}}</em>
</p>
<p>
To review this action, please visit the request panel
<a href="{{approvalUrl}}">here</a>.
</p>
</body>
</html>

View File

@ -4,20 +4,20 @@ go 1.21
require (
github.com/bradleyjkemp/cupaloy/v2 v2.8.0
github.com/charmbracelet/lipgloss v0.5.0
github.com/charmbracelet/lipgloss v0.9.1
github.com/chzyer/readline v1.5.1
github.com/creack/pty v1.1.21
github.com/denisbrodbeck/machineid v1.0.1
github.com/fatih/semgroup v1.2.0
github.com/gitleaks/go-gitdiff v0.8.0
github.com/h2non/filetype v1.1.3
github.com/infisical/go-sdk v0.3.0
github.com/mattn/go-isatty v0.0.14
github.com/mattn/go-isatty v0.0.18
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
github.com/muesli/mango-cobra v1.2.0
github.com/muesli/reflow v0.3.0
github.com/muesli/roff v0.1.0
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
github.com/rs/cors v1.11.0
github.com/rs/zerolog v1.26.1
@ -49,7 +49,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 // indirect
github.com/aws/smithy-go v1.20.2 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/danieljoos/wincred v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dvsekhvalnov/jose2go v1.6.0 // indirect
@ -69,12 +69,12 @@ require (
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/muesli/mango v0.1.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@ -94,7 +94,7 @@ require (
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/api v0.183.0 // indirect

View File

@ -83,13 +83,15 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 h1:M/1u4HBpwLuMtjlxuI2y6HoVLzF
github.com/aws/aws-sdk-go-v2/service/sts v1.28.12/go.mod h1:kcfd+eTdEi/40FIbLq4Hif3XMXnl5b/+t/KTfLt9xIk=
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
@ -292,13 +294,12 @@ github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69Y
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@ -324,13 +325,12 @@ github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbY
github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA=
github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg=
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 h1:STjmj0uFfRryL9fzRA/OupNppeAID6QJYPMavTL7jtY=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
@ -340,8 +340,6 @@ github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5d
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 h1:lL+y4Xv20pVlCGyLzNHRC0I0rIHhIL1lTvHizoS/dU8=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
@ -612,17 +610,18 @@ golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=

View File

@ -225,25 +225,6 @@ func CallIsAuthenticated(httpClient *resty.Client) bool {
return true
}
func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessibleEnvironmentsRequest) (GetAccessibleEnvironmentsResponse, error) {
var accessibleEnvironmentsResponse GetAccessibleEnvironmentsResponse
response, err := httpClient.
R().
SetResult(&accessibleEnvironmentsResponse).
SetHeader("User-Agent", USER_AGENT).
Get(fmt.Sprintf("%v/v2/workspace/%s/environments", config.INFISICAL_URL, request.WorkspaceId))
if err != nil {
return GetAccessibleEnvironmentsResponse{}, err
}
if response.IsError() {
return GetAccessibleEnvironmentsResponse{}, fmt.Errorf("CallGetAccessibleEnvironments: Unsuccessful response: [response=%v] [response-code=%v] [url=%s]", response, response.StatusCode(), response.Request.URL)
}
return accessibleEnvironmentsResponse, nil
}
func CallGetNewAccessTokenWithRefreshToken(httpClient *resty.Client, refreshToken string) (GetNewAccessTokenWithRefreshTokenResponse, error) {
var newAccessToken GetNewAccessTokenWithRefreshTokenResponse
response, err := httpClient.
@ -267,45 +248,6 @@ func CallGetNewAccessTokenWithRefreshToken(httpClient *resty.Client, refreshToke
return newAccessToken, nil
}
func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Request) (GetEncryptedSecretsV3Response, error) {
var secretsResponse GetEncryptedSecretsV3Response
httpRequest := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId)
if request.Recursive {
httpRequest.SetQueryParam("recursive", "true")
}
if request.IncludeImport {
httpRequest.SetQueryParam("include_imports", "true")
}
if request.SecretPath != "" {
httpRequest.SetQueryParam("secretPath", request.SecretPath)
}
response, err := httpRequest.Get(fmt.Sprintf("%v/v3/secrets", config.INFISICAL_URL))
if err != nil {
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
if response.StatusCode() == 401 {
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Request to access secrets with [environment=%v] [path=%v] [workspaceId=%v] is denied. Please check if your authentication method has access to requested scope", request.Environment, request.SecretPath, request.WorkspaceId)
} else {
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%v]", response.RawResponse)
}
}
return secretsResponse, nil
}
func CallGetFoldersV1(httpClient *resty.Client, request GetFoldersV1Request) (GetFoldersV1Response, error) {
var foldersResponse GetFoldersV1Response
httpRequest := httpClient.
@ -370,27 +312,7 @@ func CallDeleteFolderV1(httpClient *resty.Client, request DeleteFolderV1Request)
return folderResponse, nil
}
func CallCreateSecretsV3(httpClient *resty.Client, request CreateSecretV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
if err != nil {
return fmt.Errorf("CallCreateSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallCreateSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return nil
}
func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request) error {
func CallDeleteSecretsRawV3(httpClient *resty.Client, request DeleteSecretV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
@ -398,7 +320,7 @@ func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Delete(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
Delete(fmt.Sprintf("%v/v3/secrets/raw/%s", config.INFISICAL_URL, request.SecretName))
if err != nil {
return fmt.Errorf("CallDeleteSecretsV3: Unable to complete api request [err=%s]", err)
@ -411,46 +333,6 @@ func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request
return nil
}
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request, secretName string) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, secretName))
if err != nil {
return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallUpdateSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return nil
}
func CallGetSingleSecretByNameV3(httpClient *resty.Client, request CreateSecretV3Request) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
if err != nil {
return fmt.Errorf("CallGetSingleSecretByNameV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return fmt.Errorf("CallGetSingleSecretByNameV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
return nil
}
func CallCreateServiceToken(httpClient *resty.Client, request CreateServiceTokenRequest) (CreateServiceTokenResponse, error) {
var createServiceTokenResponse CreateServiceTokenResponse
response, err := httpClient.
@ -535,8 +417,12 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques
return GetRawSecretsV3Response{}, fmt.Errorf("CallGetRawSecretsV3: Unable to complete api request [err=%w]", err)
}
if response.IsError() && strings.Contains(response.String(), "bot_not_found_error") {
return GetRawSecretsV3Response{}, fmt.Errorf("project with id %s is a legacy project type, please navigate to project settings and disable end to end encryption then try again", request.WorkspaceId)
if response.IsError() &&
(strings.Contains(response.String(), "bot_not_found_error") ||
strings.Contains(strings.ToLower(response.String()), "failed to find bot key") ||
strings.Contains(strings.ToLower(response.String()), "bot is not active")) {
return GetRawSecretsV3Response{}, fmt.Errorf(`Project with id %s is incompatible with your current CLI version. Upgrade your project by visiting the project settings page. If you're self hosting and project upgrade option isn't yet available, contact your administrator to upgrade your Infisical instance to the latest release.
`, request.WorkspaceId)
}
if response.IsError() {

View File

@ -312,7 +312,7 @@ func ParseAgentConfig(configFile []byte) (*Config, error) {
func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) {
return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) {
res, err := util.GetPlainTextSecretsViaMachineIdentity(accessToken, projectID, envSlug, secretPath, false, false)
res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, false, false)
if err != nil {
return nil, err
}

View File

@ -24,10 +24,10 @@ import (
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/srp"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/chzyer/readline"
"github.com/fatih/color"
"github.com/go-resty/resty/v2"
"github.com/manifoldco/promptui"
"github.com/pkg/browser"
"github.com/posthog/posthog-go"
"github.com/rs/cors"
"github.com/rs/zerolog/log"
@ -226,9 +226,9 @@ var loginCmd = &cobra.Command{
//call browser login function
if !interactiveLogin {
fmt.Println("Logging in via browser... To login via interactive mode run [infisical login -i]")
userCredentialsToBeStored, err = browserCliLogin()
if err != nil {
fmt.Printf("Login via browser failed. %s", err.Error())
//default to cli login on error
cliDefaultLogin(&userCredentialsToBeStored)
}
@ -713,10 +713,62 @@ func askForMFACode() string {
return mfaVerifyCode
}
func askToPasteJwtToken(stdin *readline.CancelableStdin, success chan models.UserCredentials, failure chan error) {
time.Sleep(time.Second * 5)
fmt.Println("\n\nOnce login is completed via browser, the CLI should be authenticated automatically.")
fmt.Println("However, if browser fails to communicate with the CLI, please paste the token from the browser below.")
fmt.Print("\n\nToken: ")
bytePassword, err := term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
failure <- err
fmt.Println("\nError reading input:", err)
os.Exit(1)
}
infisicalPastedToken := strings.TrimSpace(string(bytePassword))
userCredentials, err := decodePastedBase64Token(infisicalPastedToken)
if err != nil {
failure <- err
fmt.Println("Invalid user credentials provided", err)
os.Exit(1)
}
// verify JTW
httpClient := resty.New().
SetAuthToken(userCredentials.JTWToken).
SetHeader("Accept", "application/json")
isAuthenticated := api.CallIsAuthenticated(httpClient)
if !isAuthenticated {
fmt.Println("Invalid user credentials provided", err)
failure <- err
os.Exit(1)
}
success <- *userCredentials
}
func decodePastedBase64Token(token string) (*models.UserCredentials, error) {
data, err := base64.StdEncoding.DecodeString(token)
if err != nil {
return nil, err
}
var loginResponse models.UserCredentials
err = json.Unmarshal(data, &loginResponse)
if err != nil {
return nil, err
}
return &loginResponse, nil
}
// Manages the browser login flow.
// Returns a UserCredentials object on success and an error on failure
func browserCliLogin() (models.UserCredentials, error) {
SERVER_TIMEOUT := 60 * 10
SERVER_TIMEOUT := 10 * 60
//create listener
listener, err := net.Listen("tcp", "127.0.0.1:0")
@ -728,17 +780,12 @@ func browserCliLogin() (models.UserCredentials, error) {
callbackPort := listener.Addr().(*net.TCPAddr).Port
url := fmt.Sprintf("%s?callback_port=%d", config.INFISICAL_LOGIN_URL, callbackPort)
//open browser and login
err = browser.OpenURL(url)
if err != nil {
return models.UserCredentials{}, err
}
fmt.Printf("\n\nTo complete your login, open this address in your browser: %v \n", url)
//flow channels
success := make(chan models.UserCredentials)
failure := make(chan error)
timeout := time.After(time.Second * time.Duration(SERVER_TIMEOUT))
quit := make(chan bool)
//terminal state
oldState, err := term.GetState(int(os.Stdin.Fd()))
@ -760,24 +807,27 @@ func browserCliLogin() (models.UserCredentials, error) {
log.Debug().Msgf("Callback server listening on port %d", callbackPort)
stdin := readline.NewCancelableStdin(os.Stdin)
go http.Serve(listener, corsHandler)
go askToPasteJwtToken(stdin, success, failure)
for {
select {
case loginResponse := <-success:
_ = closeListener(&listener)
_ = stdin.Close()
fmt.Println("Browser login successful")
return loginResponse, nil
case <-failure:
err = closeListener(&listener)
return models.UserCredentials{}, err
case err := <-failure:
serverErr := closeListener(&listener)
stdErr := stdin.Close()
return models.UserCredentials{}, errors.Join(err, serverErr, stdErr)
case <-timeout:
_ = closeListener(&listener)
_ = stdin.Close()
return models.UserCredentials{}, errors.New("server timeout")
case <-quit:
return models.UserCredentials{}, errors.New("quitting browser login, defaulting to cli...")
}
}
}

View File

@ -187,12 +187,34 @@ var secretsSetCmd = &cobra.Command{
var secretOperations []models.SecretSetOperation
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
if projectId == "" {
util.PrintErrorMessageAndExit("When using service tokens or machine identities, you must set the --projectId flag")
}
secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token)
} else {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
if projectId == "" {
workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil {
util.HandleError(err, "unable to get your local config details [err=%v]")
}
secretOperations, err = util.SetEncryptedSecrets(args, secretType, environmentName, secretsPath)
projectId = workspaceFile.WorkspaceId
}
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
if err != nil {
util.HandleError(err, "unable to authenticate [err=%v]")
}
if loggedInUserDetails.LoginExpired {
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
}
secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{
Type: "",
Token: loggedInUserDetails.UserCredentials.JTWToken,
})
}
if err != nil {
@ -285,7 +307,7 @@ var secretsDeleteCmd = &cobra.Command{
SecretPath: secretsPath,
}
err = api.CallDeleteSecretsV3(httpClient, request)
err = api.CallDeleteSecretsRawV3(httpClient, request)
if err != nil {
util.HandleError(err, "Unable to complete your delete request")
}

View File

@ -140,3 +140,10 @@ type SecretSetOperation struct {
SecretValue string
SecretOperation string
}
type BackupSecretKeyRing struct {
ProjectID string `json:"projectId"`
Environment string `json:"environment"`
SecretPath string `json:"secretPath"`
Secrets []SingleEnvironmentVariable
}

View File

@ -244,10 +244,5 @@ func WriteConfigFile(configFile *models.ConfigFile) error {
return fmt.Errorf("writeConfigFile: Unable to write to file [err=%s]", err)
}
if err != nil {
return fmt.Errorf("writeConfigFile: unable to write config file because an error occurred when write the config to file [err=%s]", err)
}
return nil
}

View File

@ -33,6 +33,8 @@ const (
SERVICE_TOKEN_IDENTIFIER = "service-token"
UNIVERSAL_AUTH_TOKEN_IDENTIFIER = "universal-auth-token"
INFISICAL_BACKUP_SECRET = "infisical-backup-secrets"
)
var (

View File

@ -52,10 +52,6 @@ func GetUserCredsFromKeyRing(userEmail string) (credentials models.UserCredentia
return models.UserCredentials{}, fmt.Errorf("getUserCredsFromKeyRing: Something went wrong when unmarshalling user creds [err=%s]", err)
}
if err != nil {
return models.UserCredentials{}, fmt.Errorf("GetUserCredsFromKeyRing: Unable to store user credentials [err=%s]", err)
}
return userCredentials, err
}

View File

@ -1,7 +1,6 @@
package util
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
@ -9,6 +8,7 @@ import (
"os"
"path"
"regexp"
"slices"
"strings"
"unicode"
@ -17,12 +17,13 @@ import (
"github.com/Infisical/infisical-merge/packages/models"
"github.com/go-resty/resty/v2"
"github.com/rs/zerolog/log"
"github.com/zalando/go-keyring"
)
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) {
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
}
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
@ -34,19 +35,19 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
if err != nil {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
return nil, fmt.Errorf("unable to get service token details. [err=%v]", err)
}
// if multiple scopes are there then user needs to specify which environment and secret path
if environment == "" {
if len(serviceTokenDetails.Scopes) != 1 {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("you need to provide the --env for multiple environment scoped token")
return nil, fmt.Errorf("you need to provide the --env for multiple environment scoped token")
} else {
environment = serviceTokenDetails.Scopes[0].Environment
}
}
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
rawSecrets, err := api.CallGetRawSecretsV3(httpClient, api.GetRawSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace,
Environment: environment,
SecretPath: secretPath,
@ -54,109 +55,28 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
Recursive: recursive,
})
if err != nil {
return nil, api.GetServiceTokenDetailsResponse{}, err
}
decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag)
if err != nil {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err)
}
plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV)
if err != nil {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decrypt the required workspace key")
}
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets.Secrets)
if err != nil {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
}
if includeImports {
plainTextSecrets, err = InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecrets.ImportedSecrets)
if err != nil {
return nil, api.GetServiceTokenDetailsResponse{}, err
}
}
return plainTextSecrets, serviceTokenDetails, nil
}
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, error) {
httpClient := resty.New()
httpClient.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json")
request := api.GetEncryptedWorkspaceKeyRequest{
WorkspaceId: workspaceId,
}
workspaceKeyResponse, err := api.CallGetEncryptedWorkspaceKey(httpClient, request)
if err != nil {
return nil, fmt.Errorf("unable to get your encrypted workspace key. [err=%v]", err)
}
encryptedWorkspaceKey, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey)
if err != nil {
HandleError(err, "Unable to get bytes represented by the base64 for encryptedWorkspaceKey")
}
encryptedWorkspaceKeySenderPublicKey, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey)
if err != nil {
HandleError(err, "Unable to get bytes represented by the base64 for encryptedWorkspaceKeySenderPublicKey")
}
encryptedWorkspaceKeyNonce, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce)
if err != nil {
HandleError(err, "Unable to get bytes represented by the base64 for encryptedWorkspaceKeyNonce")
}
currentUsersPrivateKey, err := base64.StdEncoding.DecodeString(receiversPrivateKey)
if err != nil {
HandleError(err, "Unable to get bytes represented by the base64 for currentUsersPrivateKey")
}
if len(currentUsersPrivateKey) == 0 || len(encryptedWorkspaceKeySenderPublicKey) == 0 {
log.Debug().Msgf("Missing credentials for generating plainTextEncryptionKey: [currentUsersPrivateKey=%s] [encryptedWorkspaceKeySenderPublicKey=%s]", currentUsersPrivateKey, encryptedWorkspaceKeySenderPublicKey)
PrintErrorMessageAndExit("Some required user credentials are missing to generate your [plainTextEncryptionKey]. Please run [infisical login] then try again")
}
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
getSecretsRequest := api.GetEncryptedSecretsV3Request{
WorkspaceId: workspaceId,
Environment: environmentName,
IncludeImport: includeImports,
Recursive: recursive,
// TagSlugs: tagSlugs,
}
if secretsPath != "" {
getSecretsRequest.SecretPath = secretsPath
}
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest)
if err != nil {
return nil, err
}
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets.Secrets)
if err != nil {
return nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
plainTextSecrets := []models.SingleEnvironmentVariable{}
for _, secret := range rawSecrets.Secrets {
plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue, Type: secret.Type, WorkspaceId: secret.Workspace})
}
if includeImports {
plainTextSecrets, err = InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecrets.ImportedSecrets)
plainTextSecrets, err = InjectRawImportedSecret(plainTextSecrets, rawSecrets.Imports)
if err != nil {
return nil, err
}
}
return plainTextSecrets, nil
}
func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool) (models.PlaintextSecretResult, error) {
func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool) (models.PlaintextSecretResult, error) {
httpClient := resty.New()
httpClient.SetAuthToken(accessToken).
SetHeader("Accept", "application/json")
@ -180,9 +100,6 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin
}
plainTextSecrets := []models.SingleEnvironmentVariable{}
if err != nil {
return models.PlaintextSecretResult{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
}
for _, secret := range rawSecrets.Secrets {
plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue, Type: secret.Type, WorkspaceId: secret.Workspace})
@ -226,34 +143,6 @@ func CreateDynamicSecretLease(accessToken string, projectSlug string, environmen
}, nil
}
func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]models.SingleEnvironmentVariable, error) {
if importedSecrets == nil {
return secrets, nil
}
hasOverriden := make(map[string]bool)
for _, sec := range secrets {
hasOverriden[sec.Key] = true
}
for i := len(importedSecrets) - 1; i >= 0; i-- {
importSec := importedSecrets[i]
plainTextImportedSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, importSec.Secrets)
if err != nil {
return nil, fmt.Errorf("unable to decrypt your imported secrets [err=%v]", err)
}
for _, sec := range plainTextImportedSecrets {
if _, ok := hasOverriden[sec.Key]; !ok {
secrets = append(secrets, sec)
hasOverriden[sec.Key] = true
}
}
}
return secrets, nil
}
func InjectRawImportedSecret(secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedRawSecretV3) ([]models.SingleEnvironmentVariable, error) {
if importedSecrets == nil {
return secrets, nil
@ -361,18 +250,19 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
infisicalDotJson.WorkspaceId = params.WorkspaceId
}
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, infisicalDotJson.WorkspaceId,
params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport, params.Recursive)
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
res, err := GetPlainTextSecretsV3(loggedInUserDetails.UserCredentials.JTWToken, infisicalDotJson.WorkspaceId,
params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", err)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
if errorToReturn == nil {
WriteBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupSecretsEncryptionKey, secretsToReturn)
if err == nil {
WriteBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, res.Secrets)
}
secretsToReturn = res.Secrets
errorToReturn = err
// only attempt to serve cached secrets if no internet connection and if at least one secret cached
if !isConnected {
backedSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupSecretsEncryptionKey)
backedSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath)
if len(backedSecrets) > 0 {
PrintWarning("Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug")
secretsToReturn = backedSecrets
@ -383,7 +273,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
} else {
if params.InfisicalToken != "" {
log.Debug().Msg("Trying to fetch secrets using service token")
secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
} else if params.UniversalAuthAccessToken != "" {
if params.WorkspaceId == "" {
@ -391,7 +281,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
}
log.Debug().Msg("Trying to fetch secrets using universal auth")
res, err := GetPlainTextSecretsViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
res, err := GetPlainTextSecretsV3(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
errorToReturn = err
secretsToReturn = res.Secrets
@ -556,174 +446,71 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
return secretsToReturn
}
func GetPlainTextSecrets(key []byte, encryptedSecrets []api.EncryptedSecretV3) ([]models.SingleEnvironmentVariable, error) {
plainTextSecrets := []models.SingleEnvironmentVariable{}
for _, secret := range encryptedSecrets {
// Decrypt key
key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV)
if err != nil {
return nil, fmt.Errorf("unable to decode secret IV for secret key")
}
key_tag, err := base64.StdEncoding.DecodeString(secret.SecretKeyTag)
if err != nil {
return nil, fmt.Errorf("unable to decode secret authentication tag for secret key")
}
key_ciphertext, err := base64.StdEncoding.DecodeString(secret.SecretKeyCiphertext)
if err != nil {
return nil, fmt.Errorf("unable to decode secret cipher text for secret key")
}
plainTextKey, err := crypto.DecryptSymmetric(key, key_ciphertext, key_tag, key_iv)
if err != nil {
return nil, fmt.Errorf("unable to symmetrically decrypt secret key")
}
// Decrypt value
value_iv, err := base64.StdEncoding.DecodeString(secret.SecretValueIV)
if err != nil {
return nil, fmt.Errorf("unable to decode secret IV for secret value")
}
value_tag, err := base64.StdEncoding.DecodeString(secret.SecretValueTag)
if err != nil {
return nil, fmt.Errorf("unable to decode secret authentication tag for secret value")
}
value_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretValueCiphertext)
if err != nil {
return nil, fmt.Errorf("unable to decode secret cipher text for secret key")
}
plainTextValue, err := crypto.DecryptSymmetric(key, value_ciphertext, value_tag, value_iv)
if err != nil {
return nil, fmt.Errorf("unable to symmetrically decrypt secret value")
}
// Decrypt comment
comment_iv, err := base64.StdEncoding.DecodeString(secret.SecretCommentIV)
if err != nil {
return nil, fmt.Errorf("unable to decode secret IV for secret value")
}
comment_tag, err := base64.StdEncoding.DecodeString(secret.SecretCommentTag)
if err != nil {
return nil, fmt.Errorf("unable to decode secret authentication tag for secret value")
}
comment_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretCommentCiphertext)
if err != nil {
return nil, fmt.Errorf("unable to decode secret cipher text for secret key")
}
plainTextComment, err := crypto.DecryptSymmetric(key, comment_ciphertext, comment_tag, comment_iv)
if err != nil {
return nil, fmt.Errorf("unable to symmetrically decrypt secret comment")
}
plainTextSecret := models.SingleEnvironmentVariable{
Key: string(plainTextKey),
Value: string(plainTextValue),
Type: string(secret.Type),
ID: secret.ID,
Tags: secret.Tags,
Comment: string(plainTextComment),
}
plainTextSecrets = append(plainTextSecrets, plainTextSecret)
}
return plainTextSecrets, nil
}
func WriteBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte, secrets []models.SingleEnvironmentVariable) error {
formattedPath := strings.ReplaceAll(secretsPath, "/", "-")
fileName := fmt.Sprintf("secrets_%s_%s_%s", workspace, environment, formattedPath)
secrets_backup_folder_name := "secrets-backup"
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
func WriteBackupSecrets(workspace string, environment string, secretsPath string, secrets []models.SingleEnvironmentVariable) error {
var backedUpSecrets []models.BackupSecretKeyRing
secretValueInKeyRing, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET)
if err != nil {
return fmt.Errorf("WriteBackupSecrets: unable to get full config folder path [err=%s]", err)
}
// create secrets backup directory
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
err := os.Mkdir(fullPathToSecretsBackupFolder, os.ModePerm)
if err != nil {
return err
if err == keyring.ErrUnsupportedPlatform {
return errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token")
} else if err != keyring.ErrNotFound {
return fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err)
}
}
_ = json.Unmarshal([]byte(secretValueInKeyRing), &backedUpSecrets)
var encryptedSecrets []models.SymmetricEncryptionResult
for _, secret := range secrets {
marshaledSecrets, _ := json.Marshal(secret)
result, err := crypto.EncryptSymmetric(marshaledSecrets, encryptionKey)
if err != nil {
return err
}
encryptedSecrets = append(encryptedSecrets, result)
backedUpSecrets = slices.DeleteFunc(backedUpSecrets, func(e models.BackupSecretKeyRing) bool {
return e.SecretPath == secretsPath && e.ProjectID == workspace && e.Environment == environment
})
newBackupSecret := models.BackupSecretKeyRing{
ProjectID: workspace,
Environment: environment,
SecretPath: secretsPath,
Secrets: secrets,
}
backedUpSecrets = append(backedUpSecrets, newBackupSecret)
listOfSecretsMarshalled, _ := json.Marshal(encryptedSecrets)
err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, 0600)
listOfSecretsMarshalled, err := json.Marshal(backedUpSecrets)
if err != nil {
return fmt.Errorf("WriteBackupSecrets: Unable to write backup secrets to file [err=%s]", err)
return err
}
err = SetValueInKeyring(INFISICAL_BACKUP_SECRET, string(listOfSecretsMarshalled))
if err != nil {
return fmt.Errorf("StoreUserCredsInKeyRing: unable to store user credentials because [err=%s]", err)
}
return nil
}
func ReadBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte) ([]models.SingleEnvironmentVariable, error) {
formattedPath := strings.ReplaceAll(secretsPath, "/", "-")
fileName := fmt.Sprintf("secrets_%s_%s_%s", workspace, environment, formattedPath)
secrets_backup_folder_name := "secrets-backup"
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
func ReadBackupSecrets(workspace string, environment string, secretsPath string) ([]models.SingleEnvironmentVariable, error) {
secretValueInKeyRing, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET)
if err != nil {
return nil, fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err)
if err == keyring.ErrUnsupportedPlatform {
return nil, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token")
} else if err == keyring.ErrNotFound {
return nil, errors.New("credentials not found in system keyring")
} else {
return nil, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err)
}
}
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
return nil, nil
}
encryptedBackupSecretsFilePath := fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName)
encryptedBackupSecretsAsBytes, err := os.ReadFile(encryptedBackupSecretsFilePath)
var backedUpSecrets []models.BackupSecretKeyRing
err = json.Unmarshal([]byte(secretValueInKeyRing), &backedUpSecrets)
if err != nil {
return nil, err
return nil, fmt.Errorf("getUserCredsFromKeyRing: Something went wrong when unmarshalling user creds [err=%s]", err)
}
var listOfEncryptedBackupSecrets []models.SymmetricEncryptionResult
_ = json.Unmarshal(encryptedBackupSecretsAsBytes, &listOfEncryptedBackupSecrets)
var plainTextSecrets []models.SingleEnvironmentVariable
for _, encryptedSecret := range listOfEncryptedBackupSecrets {
result, err := crypto.DecryptSymmetric(encryptionKey, encryptedSecret.CipherText, encryptedSecret.AuthTag, encryptedSecret.Nonce)
if err != nil {
return nil, err
for _, backupSecret := range backedUpSecrets {
if backupSecret.Environment == environment && backupSecret.ProjectID == workspace && backupSecret.SecretPath == secretsPath {
return backupSecret.Secrets, nil
}
var plainTextSecret models.SingleEnvironmentVariable
err = json.Unmarshal(result, &plainTextSecret)
if err != nil {
return nil, err
}
plainTextSecrets = append(plainTextSecrets, plainTextSecret)
}
return plainTextSecrets, nil
return nil, nil
}
func DeleteBackupSecrets() error {
// keeping this logic for now. Need to remove it later as more users migrate keyring would be used and this folder will be removed completely by then
secrets_backup_folder_name := "secrets-backup"
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
@ -733,6 +520,8 @@ func DeleteBackupSecrets() error {
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
DeleteValueInKeyring(INFISICAL_BACKUP_SECRET)
return os.RemoveAll(fullPathToSecretsBackupFolder)
}
@ -809,200 +598,6 @@ func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey str
return crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey), nil
}
func SetEncryptedSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string) ([]models.SecretSetOperation, error) {
workspaceFile, err := GetWorkSpaceFromFile()
if err != nil {
return nil, fmt.Errorf("unable to get your local config details [err=%v]", err)
}
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
if err != nil {
return nil, fmt.Errorf("unable to authenticate [err=%v]", err)
}
if loggedInUserDetails.LoginExpired {
PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
}
httpClient := resty.New().
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
SetHeader("Accept", "application/json")
request := api.GetEncryptedWorkspaceKeyRequest{
WorkspaceId: workspaceFile.WorkspaceId,
}
workspaceKeyResponse, err := api.CallGetEncryptedWorkspaceKey(httpClient, request)
if err != nil {
return nil, fmt.Errorf("unable to get your encrypted workspace key [err=%v]", err)
}
encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey)
encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey)
encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce)
currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(loggedInUserDetails.UserCredentials.PrivateKey)
if len(currentUsersPrivateKey) == 0 || len(encryptedWorkspaceKeySenderPublicKey) == 0 {
log.Debug().Msgf("Missing credentials for generating plainTextEncryptionKey: [currentUsersPrivateKey=%s] [encryptedWorkspaceKeySenderPublicKey=%s]", currentUsersPrivateKey, encryptedWorkspaceKeySenderPublicKey)
PrintErrorMessageAndExit("Some required user credentials are missing to generate your [plainTextEncryptionKey]. Please run [infisical login] then try again")
}
// decrypt workspace key
plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
infisicalTokenEnv := os.Getenv(INFISICAL_TOKEN_NAME)
// pull current secrets
secrets, err := GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath, InfisicalToken: infisicalTokenEnv}, "")
if err != nil {
return nil, fmt.Errorf("unable to retrieve secrets [err=%v]", err)
}
secretsToCreate := []api.Secret{}
secretsToModify := []api.Secret{}
secretOperations := []models.SecretSetOperation{}
sharedSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets))
personalSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets))
for _, secret := range secrets {
if secret.Type == SECRET_TYPE_PERSONAL {
personalSecretMapByName[secret.Key] = secret
} else {
sharedSecretMapByName[secret.Key] = secret
}
}
for _, arg := range secretArgs {
splitKeyValueFromArg := strings.SplitN(arg, "=", 2)
if splitKeyValueFromArg[0] == "" || splitKeyValueFromArg[1] == "" {
PrintErrorMessageAndExit("ensure that each secret has a none empty key and value. Modify the input and try again")
}
if unicode.IsNumber(rune(splitKeyValueFromArg[0][0])) {
PrintErrorMessageAndExit("keys of secrets cannot start with a number. Modify the key name(s) and try again")
}
// Key and value from argument
key := strings.TrimSpace(splitKeyValueFromArg[0])
value := splitKeyValueFromArg[1]
hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key)))
encryptedKey, err := crypto.EncryptSymmetric([]byte(key), []byte(plainTextEncryptionKey))
if err != nil {
return nil, fmt.Errorf("unable to encrypt your secrets [err=%v]", err)
}
hashedValue := fmt.Sprintf("%x", sha256.Sum256([]byte(value)))
encryptedValue, err := crypto.EncryptSymmetric([]byte(value), []byte(plainTextEncryptionKey))
if err != nil {
return nil, fmt.Errorf("unable to encrypt your secrets [err=%v]", err)
}
var existingSecret models.SingleEnvironmentVariable
var doesSecretExist bool
if secretType == SECRET_TYPE_SHARED {
existingSecret, doesSecretExist = sharedSecretMapByName[key]
} else {
existingSecret, doesSecretExist = personalSecretMapByName[key]
}
if doesSecretExist {
// case: secret exists in project so it needs to be modified
encryptedSecretDetails := api.Secret{
ID: existingSecret.ID,
SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText),
SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce),
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
SecretValueHash: hashedValue,
PlainTextKey: key,
Type: existingSecret.Type,
}
// Only add to modifications if the value is different
if existingSecret.Value != value {
secretsToModify = append(secretsToModify, encryptedSecretDetails)
secretOperations = append(secretOperations, models.SecretSetOperation{
SecretKey: key,
SecretValue: value,
SecretOperation: "SECRET VALUE MODIFIED",
})
} else {
// Current value is same as exisitng so no change
secretOperations = append(secretOperations, models.SecretSetOperation{
SecretKey: key,
SecretValue: value,
SecretOperation: "SECRET VALUE UNCHANGED",
})
}
} else {
// case: secret doesn't exist in project so it needs to be created
encryptedSecretDetails := api.Secret{
SecretKeyCiphertext: base64.StdEncoding.EncodeToString(encryptedKey.CipherText),
SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce),
SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag),
SecretKeyHash: hashedKey,
SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText),
SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce),
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
SecretValueHash: hashedValue,
Type: secretType,
PlainTextKey: key,
}
secretsToCreate = append(secretsToCreate, encryptedSecretDetails)
secretOperations = append(secretOperations, models.SecretSetOperation{
SecretKey: key,
SecretValue: value,
SecretOperation: "SECRET CREATED",
})
}
}
for _, secret := range secretsToCreate {
createSecretRequest := api.CreateSecretV3Request{
WorkspaceID: workspaceFile.WorkspaceId,
Environment: environmentName,
SecretName: secret.PlainTextKey,
SecretKeyCiphertext: secret.SecretKeyCiphertext,
SecretKeyIV: secret.SecretKeyIV,
SecretKeyTag: secret.SecretKeyTag,
SecretValueCiphertext: secret.SecretValueCiphertext,
SecretValueIV: secret.SecretValueIV,
SecretValueTag: secret.SecretValueTag,
Type: secret.Type,
SecretPath: secretsPath,
}
err = api.CallCreateSecretsV3(httpClient, createSecretRequest)
if err != nil {
return nil, fmt.Errorf("unable to process new secret creations [err=%v]", err)
}
}
for _, secret := range secretsToModify {
updateSecretRequest := api.UpdateSecretByNameV3Request{
WorkspaceID: workspaceFile.WorkspaceId,
Environment: environmentName,
SecretValueCiphertext: secret.SecretValueCiphertext,
SecretValueIV: secret.SecretValueIV,
SecretValueTag: secret.SecretValueTag,
Type: secret.Type,
SecretPath: secretsPath,
}
err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest, secret.PlainTextKey)
if err != nil {
return nil, fmt.Errorf("unable to process secret update request [err=%v]", err)
}
}
return secretOperations, nil
}
func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails) ([]models.SecretSetOperation, error) {
if tokenDetails == nil {
@ -1012,7 +607,9 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin
getAllEnvironmentVariablesRequest := models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath, WorkspaceId: projectId}
if tokenDetails.Type == UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
getAllEnvironmentVariablesRequest.UniversalAuthAccessToken = tokenDetails.Token
} else {
}
if tokenDetails.Type == SERVICE_TOKEN_IDENTIFIER {
getAllEnvironmentVariablesRequest.InfisicalToken = tokenDetails.Token
}

View File

@ -6,7 +6,7 @@ description: "Learn how to request access to sensitive resources in Infisical."
In certain situations, developers need to expand their access to a certain new project or a sensitive environment. For those use cases, it is helpful to utilize Infisical's **Access Requests** functionality.
This functionality works in the following way:
1. A project administrator sets up a policy that assigns access managers (also known as eligible approvers) to a certain sensitive folder or environment.
1. A project administrator sets up an access policy that assigns access managers (also known as eligible approvers) to a certain sensitive folder or environment.
![Create Access Request Policy Modal](/images/platform/access-controls/create-access-request-policy.png)
![Access Request Policies](/images/platform/access-controls/access-request-policies.png)
@ -14,9 +14,14 @@ This functionality works in the following way:
![Access Request Create](/images/platform/access-controls/request-access.png)
![Access Request Dashboard](/images/platform/access-controls/access-requests-pending.png)
3. An eligible approver can approve or reject the access request.
![Access Request Review](/images/platform/access-controls/review-access-request.png)
4. An eligible approver can approve or reject the access request.
{/* ![Access Request Review](/images/platform/access-controls/review-access-request.png) */}
![Access Request Bypass](/images/platform/access-controls/access-request-bypass.png)
4. As soon as the request is approved, developer is able to access the sought resources.
<Info>
If the access request matches with a policy that has a **Soft** enforcement level, the requester may bypass the policy and get access to the resource without full approval.
</Info>
5. As soon as the request is approved, developer is able to access the sought resources.
![Access Request Dashboard](/images/platform/access-controls/access-requests-completed.png)

View File

@ -18,16 +18,26 @@ In a similar way, to solve the above-mentioned issues, Infisical provides a feat
### Setting a policy
First, you would need to create a set of policies for a certain environment. In the example below, a generic policy for a production environment is shown. In this case, any user who submits a change to `prod` would first have to get an approval by a predefined approver (or multiple approvers).
First, you would need to create a set of policies for a certain environment. In the example below, a generic change policy for a production environment is shown. In this case, any user who submits a change to `prod` would first have to get an approval by a predefined approver (or multiple approvers).
![create secret update policy](../../images/platform/pr-workflows/secret-update-policy.png)
### Policy enforcement levels
The enforcement level determines how strict the policy is. A **Hard** enforcement level means that any change that matches the policy will need full approval prior merging. A **Soft** enforcement level allows for break glass functionality on the request. If a change request is bypassed, the approvers will be notified via email.
### Example of creating a change policy
When creating a policy, you can choose the type of policy you want to create. In this case, we will be creating a `Change Policy`. Other types of policies include `Access Policy` that creates policies for **[Access Requests](/documentation/platform/access-controls/access-requests)**.
![create panel secret update policy](../../images/platform/pr-workflows/create-change-policy.png)
### Example of updating secrets with Approval workflows
When a user submits a change to an enviropnment that is under a particular policy, a corresponsing change request will go to a predefined approver (or multiple approvers).
![secret update change requests](../../images/platform/pr-workflows/secret-update-request.png)
An approver is notified by email and/or Slack as soon as the request is initiated. In the Infisical Dashboard, they will be able to `approve` and `merge` (or `deny`) a request for a change in a particular environment. After that, depending on the workflows setup, the change will be automatically propagated to the right applications (e.g., using [Infisical Kubernetes Operator](https://infisical.com/docs/integrations/platforms/kubernetes)).
Approvers are notified by email and/or Slack as soon as the request is initiated. In the Infisical Dashboard, they will be able to `approve` and `merge` (or `deny`) a request for a change in a particular environment. After that, depending on the workflows setup, the change will be automatically propagated to the right applications (e.g., using [Infisical Kubernetes Operator](https://infisical.com/docs/integrations/platforms/kubernetes)).
![secrets update pull request](../../images/platform/pr-workflows/secret-update-pr.png)

View File

@ -21,7 +21,8 @@ With its zero-knowledge architecture, secrets shared via Infisical remain unread
zero knowledge architecture.
</Note>
3. Click on the **Share Secret** button. Set the secret, its expiration time as well as the number of views allowed. It expires as soon as any of the conditions are met.
3. Click on the **Share Secret** button. Set the secret, its expiration time and specify if the secret can be viewed only once. It expires as soon as any of the conditions are met.
Also, specify if the secret can be accessed by anyone or only people within your organization.
![Add View-Bound Sharing Secret](../../images/platform/secret-sharing/create-new-secret.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

View File

@ -217,7 +217,14 @@
"pages": [
"self-hosting/overview",
{
"group": "Installation methods",
"group": "Native installation methods",
"pages": [
"self-hosting/deployment-options/native/standalone-binary",
"self-hosting/deployment-options/native/high-availability"
]
},
{
"group": "Containerized installation methods",
"pages": [
"self-hosting/deployment-options/standalone-infisical",
"self-hosting/deployment-options/docker-swarm",

View File

@ -0,0 +1,520 @@
---
title: "Automatically deploy Infisical with High Availability"
sidebarTitle: "High Availability"
---
# Self-Hosting Infisical with a native High Availability (HA) deployment
This page describes the Infisical architecture designed to provide high availability (HA) and how to deploy Infisical with high availability. The high availability deployment is designed to ensure that Infisical services are always available and can handle service failures gracefully, without causing service disruptions.
<Info>
This deployment option is currently only available for Debian-based nodes (e.g., Ubuntu, Debian).
We plan on adding support for other operating systems in the future.
</Info>
## High availability architecture
| Service | Nodes | Configuration | GCP | AWS |
|----------------------------------|----------------|------------------------------|---------------|--------------|
| External load balancer$^1$ | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5n.xlarge |
| Internal load balancer$^2$ | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5n.xlarge |
| Etcd cluster$^3$ | 3 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5n.xlarge |
| PostgreSQL$^4$ | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large |
| Sentinel$^4$ | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large |
| Redis$^4$ | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large |
| Infisical Core | 3 | 8 vCPU, 7.2 GB memory | n1-highcpu-8 | c5.2xlarge |
**Footnotes:**
1. External load balancer: If you wish to have multiple instances of the internal load balancer, you will need to use an external load balancer to distribute incoming traffic across multiple internal load balancers.
Using multiple internal load balancers is recommended for high-traffic environments. In the following guide we will use a single internal load balancer, as external load balancing falls outside the scope of this guide.
2. Internal load balancer: The internal load balancer (a HAProxy instance) is used to distribute incoming traffic across multiple Infisical Core instances, Postgres nodes, and Redis nodes. The internal load balancer exposes a set of ports _(80 for Infiscial, 5000 for Read/Write postgres, 5001 for Read-only postgres, and 6379 for Redis)_. Where these ports route to is determained by the internal load balancer based on the availability and health of the service nodes.
The internal load balancer is only accessible from within the same network, and is not exposed to the public internet.
3. Etcd cluster: Etcd is a distributed key-value store used to store and distribute data between the PostgreSQL nodes. Etcd is dependent on high disk I/O performance, therefore it is highly recommended to use highly performant SSD disks for the Etcd nodes, with _at least_ 80GB of disk space.
4. The Redis and PostgreSQL nodes will automatically be configured for high availability and used in your Infisical Core instances. However, you can optionally choose to bring your own database (BYOD), and skip these nodes. See more on how to [provide your own databases](#provide-your-own-databases).
<Info>
For all services that require multiple nodes, it is recommended to deploy them across multiple availability zones (AZs) to ensure high availability and fault tolerance. This will help prevent service disruptions in the event of an AZ failure.
</Info>
![High availability stack](../../images/self-hosting/deployment-options/native/ha-stack.png)
The image above shows how a high availability deployment of Infisical is structured. In this example, an external load balancer is used to distribute incoming traffic across multiple internal load balancers. The internal load balancers. The external load balancer isn't required, and it will require additional configuration to set up.
### Fault Tolerance
This setup provides N+1 redundancy, meaning it can tolerate the failure of any single node without service interruption.
## Ansible
### What is Ansible
Ansible is an open-source automation tool that simplifies application deployment, configuration management, and task automation.
At Infisical, we use Ansible to automate the deployment of Infisical services. The Ansible roles are designed to make it easy to deploy Infisical services in a high availability environment.
### Installing Ansible
<Steps>
<Step title="Install using the pipx Python package manager">
```bash
pipx install --include-deps ansible
```
</Step>
<Step title="Verify the installation">
```bash
ansible --version
```
</Step>
</Steps>
### Understanding Ansible Concepts
* Inventory _(inventory.ini)_: A file that lists your target hosts.
* Playbook _(playbook.yml)_: YAML file containing a set of tasks to be executed on hosts.
* Roles: Reusable units of organization for playbooks. Roles are used to group tasks together in a structured and reusable manner.
### Basic Ansible Commands
Running a playbook with with an invetory file:
```bash
ansible-playbook -i inventory.ini playbook.yml
```
This is how you would run the playbook containing the roles for setting up Infisical in a high availability environment.
### Installing the Infisical High Availability Deployment Ansible Role
The Infisical Ansible role is available on Ansible Galaxy. You can install the role by running the following command:
```bash
ansible-galaxy collection install infisical.infisical_core_ha_deployment
```
## Set up components
1. External load balancer (optional, and not covered in this guide)
2. [Configure Etcd cluster](#configure-etcd-cluster)
3. [Configure PostgreSQL database](#configure-postgresql-database)
4. [Configure Redis/Sentinel](#configure-redis-and-sentinel)
5. [Configure Infisical Core](#configure-infisical-core)
The servers start on the same 52.1.0.0/24 private network range, and can connect to each other freely on these addresses.
The following list includes descriptions of each server and its assigned IP:
52.1.0.1: External Load Balancer
52.1.0.2: Internal Load Balancer
52.1.0.3: Etcd 1
52.1.0.4: Etcd 2
52.1.0.5: Etcd 3
52.1.0.6: PostgreSQL 1
52.1.0.7: PostgreSQL 2
52.1.0.8: PostgreSQL 3
52.1.0.9: Redis 1
52.1.0.10: Redis 2
52.1.0.11: Redis 3
52.1.0.12: Sentinel 1
52.1.0.13: Sentinel 2
52.1.0.14: Sentinel 3
52.1.0.15: Infisical Core 1
52.1.0.16: Infisical Core 2
52.1.0.17: Infisical Core 3
### Configure Etcd cluster
Configuring the ETCD cluster is the first step in setting up a high availability deployment of Infisical.
The ETCD cluster is used to store and distribute data between the PostgreSQL nodes. The ETCD cluster is a distributed key-value store that is highly available and fault-tolerant.
```yaml example.playbook.yml
- hosts: all
gather_facts: true
- name: Set up etcd cluster
hosts: etcd
become: true
collections:
- infisical.infisical_core_ha_deployment
roles:
- role: etcd
```
```ini example.inventory.ini
[etcd]
etcd1 ansible_host=52.1.0.3
etcd2 ansible_host=52.1.0.4
etcd3 ansible_host=52.1.0.5
[etcd:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=./ssh-key.pem
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
```
### Configure PostgreSQL database
The Postgres role takes a set of parameters that are used to configure your PostgreSQL database.
Make sure to set the following variables in your playbook.yml file:
- `postgres_super_user_password`: The password for the 'postgres' database user.
- `postgres_db_name`: The name of the database that will be created on the leader node and replicated to the secondary nodes.
- `postgres_user`: The name of the user that will be created on the leader node and replicated to the secondary nodes.
- `postgres_user_password`: The password for the user that will be created on the leader node and replicated to the secondary nodes.
- `etcd_hosts`: The list of etcd hosts that the PostgreSQL nodes will use to communicate with etcd. By default you want to keep this value set to `"{{ groups['etcd'] }}"`
```yaml example.playbook.yml
- hosts: all
gather_facts: true
- name: Set up PostgreSQL with Patroni
hosts: postgres
become: true
collections:
- infisical.infisical_core_ha_deployment
roles:
- role: postgres
vars:
postgres_super_user_password: "your-super-user-password"
postgres_user: infisical-user
postgres_user_password: "your-password"
postgres_db_name: infisical-db
etcd_hosts: "{{ groups['etcd'] }}"
```
```ini example.inventory.ini
[postgres]
postgres1 ansible_host=52.1.0.6
postgres2 ansible_host=52.1.0.7
postgres3 ansible_host=52.1.0.8
```
### Configure Redis and Sentinel
The Redis role takes a single variable as input, which is the redis password.
The Sentinel and Redis hosts will run the same role, therefore we are running the task for both the sentinel and redis hosts, `hosts: redis:sentinel`.
- `redis_password`: The password that will be set for the Redis instance.
```yaml example.playbook.yml
- hosts: all
gather_facts: true
- name: Setup Redis and Sentinel
hosts: redis:sentinel
become: true
collections:
- infisical.infisical_core_ha_deployment
roles:
- role: redis
vars:
redis_password: "REDIS_PASSWORD"
```
```ini example.inventory.ini
[redis]
redis1 ansible_host=52.1.0.9
redis2 ansible_host=52.1.0.10
redis3 ansible_host=52.1.0.11
[sentinel]
sentinel1 ansible_host=52.1.0.12
sentinel2 ansible_host=52.1.0.13
sentinel3 ansible_host=52.1.0.14
```
### Configure Internal Load Balancer
The internal load balancer used is HAProxy. HAProxy will expose a set of ports as listed below. Each port will route to a different service based on the availability and health of the service nodes.
- Port 80: Infisical Core
- Port 5000: Read/Write PostgreSQL
- Port 5001: Read-only PostgreSQL
- Port 6379: Redis
- Port 7000: HAProxy monitoring
These ports will need to be exposed on your network to become accessible from the outside world.
The HAProxy configuration file is generated by the Infisical Core role, and is located at `/etc/haproxy/haproxy.cfg` on your internal load balancer node.
The HAProxy setup comes with a monitoring panel. You have to set the username/password combination for the monitoring panel by setting the `stats_user` and `stats_password` variables in the HAProxy role.
Once the HAProxy role has fully executed, you can monitor your HA setup by navigating to `http://52.1.0.2:7000/haproxy?stats` in your browser.
```ini example.inventory.ini
[haproxy]
internal_lb ansible_host=52.1.0.2
```
```yaml example.playbook.yml
- name: Set up HAProxy
hosts: haproxy
become: true
collections:
- infisical.infisical_core_ha_deployment
roles:
- role: haproxy
vars:
stats_user: "stats-username"
stats_password: "stats-password!"
postgres_servers: "{{ groups['postgres'] }}"
infisical_servers: "{{ groups['infisical'] }}"
redis_servers: "{{ groups['redis'] }}"
```
### Configure Infisical Core
The Infisical Core role will set up your actual Infisical instances.
The `env_vars` variable is used to set the environment variables that Infisical will use. The minimum required environment variables are `ENCRYPTION_KEY` and `AUTH_SECRET`. You can find a list of all available environment variables [here](/docs/self-hosting/configuration/envars#general-platform).
The `DB_CONNECTION_URI` and `REDIS_URL` variables will automatically be set if you're running the full playbook. However, you can choose to set them yourself, and skip the Postgres, etcd, redis/sentinel roles entirely.
<Info>
If you later need to add new environment varibles to your Infisical deployments, it's important you add the variables to **all** your Infisical nodes.<br/>
You can find the environment file for Infisical at `/etc/infisical/environment`.<br/>
After editing the environment file, you need to reload the Infisical service by doing `systemctl restart infisical`.
</Info>
```yaml example.playbook.yml
- hosts: all
gather_facts: true
- name: Setup Infisical
hosts: infisical
become: true
collections:
- infisical.infisical_core_ha_deployment
roles:
- role: infisical
env_vars:
ENCRYPTION_KEY: "YOUR_ENCRYPTION_KEY" # openssl rand -hex 16
AUTH_SECRET: "YOUR_AUTH_SECRET" # openssl rand -base64 32
```
```ini example.inventory.ini
[infisical]
infisical1 ansible_host=52.1.0.15
infisical2 ansible_host=52.1.0.16
infisical3 ansible_host=52.1.0.17
```
## Provide your own databases
Bringing your own database is an option using the Infisical Core deployment role.
By bringing your own database, you're able to skip the Etcd, Postgres, and Redis/Sentinel roles entirely.
To bring your own database, you need to set the `DB_CONNECTION_URI` and `REDIS_URL` environment variables in the Infisical Core role.
```yaml example.playbook.yml
- hosts: all
gather_facts: true
- name: Setup Infisical
hosts: infisical
become: true
collections:
- infisical.infisical_core_ha_deployment
roles:
- role: infisical
env_vars:
ENCRYPTION_KEY: "YOUR_ENCRYPTION_KEY" # openssl rand -hex 16
AUTH_SECRET: "YOUR_AUTH_SECRET" # openssl rand -base64 32
DB_CONNECTION_URI: "postgres://user:password@localhost:5432/infisical"
REDIS_URL: "redis://localhost:6379"
```
```ini example.inventory.ini
[infisical]
infisical1 ansible_host=52.1.0.15
infisical2 ansible_host=52.1.0.16
infisical3 ansible_host=52.1.0.17
```
## Full deployment example
To make it easier to get started, we've provided a full deployment example that you can use to deploy Infisical in a high availability environment.
- This deployment does not use an external load balancer.
- You **must** change the environment variables defined in the `playbook.yml` example.
- You have update the IP addresses in the `inventory.ini` file to match your own network configuration.
- You need to set the SSH key and ssh user in the `inventory.ini` file.
<Steps>
<Step title="Install Ansible">
Install Ansible using the pipx Python package manager.
```bash
pipx install --include-deps ansible
```
</Step>
<Step title="Install the Infisical deployment Ansible Role">
Install the Infisical deployment role from Ansible Galaxy.
```bash
ansible-galaxy collection install infisical.infisical_core_ha_deployment
```
</Step>
<Step title="Setup your hosts">
Create an `inventory.ini` file, and define your hosts and their IP addresses. You can use the example below as a template, and update the IP addresses to match your own network configuration.
Make sure to set the SSH key and ssh user in the `inventory.ini` file. Please see the example below.
```ini example.inventory.ini
[etcd]
etcd1 ansible_host=52.1.0.3
etcd2 ansible_host=52.1.0.4
etcd3 ansible_host=52.1.0.5
[postgres]
postgres1 ansible_host=52.1.0.6
postgres2 ansible_host=52.1.0.7
postgres3 ansible_host=52.1.0.8
[infisical]
infisical1 ansible_host=52.1.0.15
infisical2 ansible_host=52.1.0.16
infisical3 ansible_host=52.1.0.17
[redis]
redis1 ansible_host=52.1.0.9
redis2 ansible_host=52.1.0.10
redis3 ansible_host=52.1.0.11
[sentinel]
sentinel1 ansible_host=52.1.0.12
sentinel2 ansible_host=52.1.0.13
sentinel3 ansible_host=52.1.0.14
[haproxy]
internal_lb ansible_host=52.1.0.2
; This can be defined individually for each host, or globally for all hosts.
; In this case the credentials are the same for all hosts, so we define them globally as seen below ([all:vars]).
[all:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=./your-ssh-key.pem
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
```
</Step>
<Step title="Setup your Ansible playbook">
The Ansible playbook is where you define which roles/tasks to execute on which hosts.
```yaml example.playbook.yml
---
# Important, we must gather facts from all hosts prior to running the roles to ensure we have all the information we need.
- hosts: all
gather_facts: true
- name: Set up etcd cluster
hosts: etcd
become: true
collections:
- infisical.infisical_core_ha_deployment
roles:
- role: etcd
- name: Set up PostgreSQL with Patroni
hosts: postgres
become: true
collections:
- infisical.infisical_core_ha_deployment
roles:
- role: postgres
vars:
postgres_super_user_password: "<ENTER_SUPERUSER_PASSWORD>" # Password for the 'postgres' database user
# A database with these credentials will be created on the leader node, and replicated to the secondary nodes.
postgres_db_name: <ENTER_DB_NAME>
postgres_user: <ENTER_DB_USER>
postgres_user_password: <ENTER_DB_USER_PASSWORD>
etcd_hosts: "{{ groups['etcd'] }}"
- name: Setup Redis and Sentinel
hosts: redis:sentinel
become: true
collections:
- infisical.infisical_core_ha_deployment
roles:
- role: redis
vars:
redis_password: "<ENTER_REDIS_PASSWORD>"
- name: Set up HAProxy
hosts: haproxy
become: true
collections:
- infisical.infisical_core_ha_deployment
roles:
- role: haproxy
vars:
stats_user: "<ENTER_HAPROXY_STATS_USERNAME>"
stats_password: "<ENTER_HAPROXY_STATS_PASSWORD>"
postgres_servers: "{{ groups['postgres'] }}"
infisical_servers: "{{ groups['infisical'] }}"
redis_servers: "{{ groups['redis'] }}"
- name: Setup Infisical
hosts: infisical
become: true
collections:
- infisical.infisical_core_ha_deployment
roles:
- role: infisical
env_vars:
ENCRYPTION_KEY: "YOUR_ENCRYPTION_KEY" # openssl rand -hex 16
AUTH_SECRET: "YOUR_AUTH_SECRET" # openssl rand -base64 32
```
</Step>
<Step title="Run the Ansible playbook">
After creating the `playbook.yml` and `inventory.ini` files, you can run the playbook using the following command
```bash
ansible-playbook -i inventory.ini playbook.yml
```
This step may take upwards of 10 minutes to complete, depending on the number of nodes and the network speed.
Once the playbook has completed, you should have a fully deployed high availability Infisical environment.
To access Infisical, you can try navigating to `http://52.1.0.2`, in order to view your newly deployed Infisical instance.
</Step>
</Steps>
## Post-deployment steps
After deploying Infisical in a high availability environment, you should perform the following post-deployment steps:
- Check your deployment to ensure that all services are running as expected. You can use the HAProxy monitoring panel to check the status of your services (http://52.1.0.2:7000/haproxy?stats)
- Attempt to access the Infisical Core instances to ensure that they are accessible from the internal load balancer. (http://52.1.0.2)
A HAProxy stats page indicating success will look like the image below
![HAProxy stats page](../../images/self-hosting/deployment-options/native/haproxy-stats.png)
## Security Considerations
### Network Security
Secure the network that your instances run on. While this falls outside the scope of Infisical deployment, it's crucial for overall security.
AWS-specific recommendations:
Use Virtual Private Cloud (VPC) to isolate your infrastructure.
Configure security groups to restrict inbound and outbound traffic.
Use Network Access Control Lists (NACLs) for additional network-level security.
<Note>
Please take note that the Infisical team cannot provide infrastructure support for **free self-hosted** deployments.<br/>If you need help with infrastructure, we recommend upgrading to a [paid plan](https://infisical.com/pricing) which includes infrastructure support.
You can also join our community [Slack](https://infisical.com/slack) for help and support from the community.
</Note>
### Troubleshooting
<Accordion title="Ansible: Failed to set permissions on the temporary files Ansible needs to create when becoming an unprivileged user">
If you encounter this issue, please update your ansible config (`ansible.cfg`) file with the following configuration:
```ini
[defaults]
allow_world_readable_tmpfiles = true
```
You can read more about the solution [here](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/sh_shell.html#parameter-world_readable_temp)
</Accordion>
<Accordion title="I'm unable to connect to access the Infisical instance on the web">
This issue can be caused by a number of reasons, mostly realted to the network configuration. Here are a few things you can check:
1. Ensure that the firewall is not blocking the connection. You can check this by running `ufw status`. Ensure that port 80 is open.
2. If you're using a cloud provider like AWS or GCP, ensure that the security group allows traffic on port 80.
3. Ensure that the HAProxy service is running. You can check this by running `systemctl status haproxy`.
4. Ensure that the Infisical service is running. You can check this by running `systemctl status infisical`.
</Accordion>

View File

@ -0,0 +1,203 @@
---
title: "Standalone"
description: "Learn how to deploy Infisical in a standalone environment."
---
# Self-Hosting Infisical with Standalone Infisical
Deploying Infisical in a standalone environment is a great way to get started with Infisical without having to use containers. This guide will walk you through the process of deploying Infisical in a standalone environment.
This is one of the easiest ways to deploy Infisical. It is a single executable, currently only supported on Debian-based systems.
The standalone deployment implements the "bring your own database" (BYOD) approach. This means that you will need to provide your own databases (specifically Postgres and Redis) for the Infisical services to use. The standalone deployment does not include any databases.
If you wish to streamline the deployment process, we recommend using the Ansible role for Infisical. The Ansible role automates the deployment process and includes the databases:
- [Automated Deployment](https://google.com)
- [Automated Deployment with high availability (HA)](https://google.com)
## Prerequisites
- A server running a Debian-based operating system (e.g., Ubuntu, Debian)
- A Postgres database
- A Redis database
## Installing Infisical
Installing Infisical is as simple as running a single command. You can install Infisical by running the following command:
```bash
$ curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-core/cfg/setup/bash.deb.sh' | sudo bash && sudo apt-get install -y infisical-core
```
## Running Infisical
Running Infisical and serving it to the web has a few steps. Below are the steps to get you started with running Infisical in a standalone environment.
* Setup environment variables
* Running Postgres migrations
* Create system daemon
* Exposing Infisical to the internet
<Steps>
<Step title="Setup environment variables">
To use Infisical you'll need to configure the environment variables beforehand. You can acheive this by creating an environment file to be used by Infisical.
#### Create environment file
```bash
$ mkdir -p /etc/infisical && touch /etc/infisical/environment
```
After creating the environment file, you'll need to fill it out with your environment variables.
#### Edit environment file
```bash
$ nano /etc/infisical/environment
```
```bash
DB_CONNECTION_URI=postgres://user:password@localhost:5432/infisical # Replace with your Postgres database connection URI
REDIS_URL=redis://localhost:6379 # Replace with your Redis connection URI
ENCRYPTION_KEY=your_encryption_key # Replace with your encryption key (can be generated with: openssl rand -hex 16)
AUTH_SECRET=your_auth_secret # Replace with your auth secret (can be generated with: openssl rand -base64 32)
```
<Info>
The minimum required environment variables are `DB_CONNECTION_URI`, `REDIS_URL`, `ENCRYPTION_KEY`, and `AUTH_SECRET`. We recommend You take a look at our [list of all available environment variables](/docs/self-hosting/configuration/envars#general-platform), and configure the ones you need.
</Info>
</Step>
<Step title="Running Postgres migrations">
Assuming you're starting with a fresh Postgres database, you'll need to run the Postgres migrations to syncronize the database schema.
The migration command will use the environment variables you configured in the previous step.
```bash
$ eval $(cat /etc/infisical/environment) infisical-core migration:latest
```
<Info>
This step will need to be repeated if you update Infisical in the future.
</Info>
</Step>
<Step title="Create service file">
```bash
$ nano /etc/systemd/system/infisical.service
```
</Step>
<Step title="Create Infisical service">
Create a systemd service file for Infisical. Creating a systemd service file will allow Infisical to start automatically when the system boots or in case of a crash.
```bash
$ nano /etc/systemd/system/infisical.service
```
```ini
[Unit]
Description=Infisical Service
After=network.target
[Service]
# The path to the environment file we created in the previous step
EnvironmentFile=/etc/infisical/environment
Type=simple
# Change the user to the user you want to run Infisical as
User=root
ExecStart=/usr/local/bin/infisical-core
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target
```
Now we need to reload the systemd daemon and start the Infisical service.
```bash
$ systemctl daemon-reload
$ systemctl start infisical
$ systemctl enable infisical
```
<Info>
You can check the status of the Infisical service by running `systemctl status infisical`.
It is also a good idea to check the logs for any errors by running `journalctl --no-pager -u infisical`.
</Info>
</Step>
<Step title="Exposing Infisical to the internet">
Exposing Infisical to the internet requires setting up a reverse proxy. You can use any reverse proxy of your choice, but we recommend using HAProxy or Nginx. Below is an example of how to set up a reverse proxy using HAProxy.
#### Install HAProxy
```bash
$ apt-get install -y haproxy
```
#### Edit HAProxy configuration
```bash
$ nano /etc/haproxy/haproxy.cfg
```
```ini
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
frontend http-in
bind *:80
default_backend infisical
backend infisical
server infisicalapp 127.0.0.1:8080 check
```
<Warning>
If you decide to use Nginx, then please be aware that the configuration will be different. **Infisical listens on port 8080**.
</Warning>
#### Restart HAProxy
```bash
$ systemctl restart haproxy
```
</Step>
</Steps>
And that's it! You have successfully deployed Infisical in a standalone environment. You can now access Infisical by visiting `http://your-server-ip`.
<Note>
Please take note that the Infisical team cannot provide infrastructure support for **free self-hosted** deployments.<br/>If you need help with infrastructure, we recommend upgrading to a [paid plan](https://infisical.com/pricing) which includes infrastructure support.
You can also join our community [Slack](https://infisical.com/slack) for help and support from the community.
</Note>
## Troubleshooting
<Accordion title="I'm getting a error related to the HAProxy (Missing LF on last line, file might have been truncated at position X)">
This is a common issue related to the HAProxy configuration file. The error is caused by the missing newline character at the end of the file. You can fix this by adding a newline character at the end of the file.
```bash
$ echo "" >> /etc/haproxy/haproxy.cfg
```
</Accordion>
<Accordion title="I'm unable to connect to access the Infisical instance on the web">
This issue can be caused by a number of reasons, mostly realted to the network configuration. Here are a few things you can check:
1. Ensure that the firewall is not blocking the connection. You can check this by running `ufw status`. Ensure that port 80 is open.
2. If you're using a cloud provider like AWS or GCP, ensure that the security group allows traffic on port 80.
3. Ensure that the HAProxy service is running. You can check this by running `systemctl status haproxy`.
4. Ensure that the Infisical service is running. You can check this by running `systemctl status infisical`.
</Accordion>

View File

@ -33,3 +33,21 @@ Choose from a number of deployment options listed below to get started.
Use our Helm chart to Install Infisical on your Kubernetes cluster.
</Card>
</CardGroup>
<CardGroup cols={2}>
<Card
title="Native Deployment"
color="#000000"
icon="box"
href="deployment-options/native/standalone-binary"
>
Install Infisical on your Debian-based system without containers using our standalone binary.
</Card>
<Card
title="Native Deployment, High Availability"
color="#000000"
icon="boxes-stacked"
href="deployment-options/native/high-availability"
>
Install Infisical on your Debian-based instances without containers using our standalone binary with high availability out of the box.
</Card>
</CardGroup>

View File

@ -21,7 +21,7 @@ export const EmptyState = ({
}: Props) => (
<div
className={twMerge(
"flex w-full flex-col items-center bg-mineshaft-800 px-2 pt-6 text-bunker-300",
"flex w-full flex-col items-center bg-mineshaft-800 px-2 pt-4 text-bunker-300",
className
)}
>

View File

@ -57,6 +57,10 @@ export const plans = plansProd || plansDev;
export const leaveConfirmDefaultMessage =
"Your changes will be lost if you leave the page. Are you sure you want to continue?";
export enum SessionStorageKeys {
CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN"
}
export const secretTagsColors = [
{
id: 1,

View File

@ -46,8 +46,10 @@ export const usePerformSecretApprovalRequestMerge = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TPerformSecretApprovalRequestMerge>({
mutationFn: async ({ id }) => {
const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/merge`);
mutationFn: async ({ id, bypassReason }) => {
const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/merge`, {
bypassReason
});
return data;
},
onSuccess: (_, { id, workspaceId }) => {

View File

@ -133,4 +133,5 @@ export type TUpdateSecretApprovalRequestStatusDTO = {
export type TPerformSecretApprovalRequestMerge = {
id: string;
workspaceId: string;
bypassReason?: string;
};

View File

@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TSharedSecret, TViewSharedSecretResponse } from "./types";
import { SecretSharingAccessType, TSharedSecret, TViewSharedSecretResponse } from "./types";
export const useGetSharedSecrets = () => {
return useQuery({
@ -17,7 +17,7 @@ export const useGetSharedSecrets = () => {
export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => {
return useQuery<TViewSharedSecretResponse, [string]>({
queryFn: async () => {
if(!id || !hashedHex) return Promise.resolve({ encryptedValue: "", iv: "", tag: "" });
if(!id || !hashedHex) return Promise.resolve({ encryptedValue: "", iv: "", tag: "", accessType: SecretSharingAccessType.Organization, orgName: "" });
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
`/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}`
);
@ -25,6 +25,8 @@ export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex:
encryptedValue: data.encryptedValue,
iv: data.iv,
tag: data.tag,
accessType: data.accessType,
orgName: data.orgName
};
}
});

View File

@ -13,14 +13,22 @@ export type TCreateSharedSecretRequest = {
hashedHex: string;
expiresAt: Date;
expiresAfterViews: number;
accessType: SecretSharingAccessType;
};
export type TViewSharedSecretResponse = {
encryptedValue: string;
iv: string;
tag: string;
accessType: SecretSharingAccessType;
orgName?: string;
};
export type TDeleteSharedSecretRequest = {
sharedSecretId: string;
};
export enum SecretSharingAccessType {
Anyone = "anyone",
Organization = "organization"
}

View File

@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { SessionStorageKeys } from "@app/const";
import { setAuthToken } from "@app/reactQuery";
import { APIKeyDataV2 } from "../apiKeys/types";
@ -293,6 +294,7 @@ export const useLogoutUser = (keepQueryClient?: boolean) => {
localStorage.removeItem("PRIVATE_KEY");
localStorage.removeItem("orgData.id");
localStorage.removeItem("projectData.id");
sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN);
if (!keepQueryClient) {
queryClient.clear();

View File

@ -1,23 +1,89 @@
import Head from "next/head";
import Image from "next/image";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconButton, SecretInput } from "@app/components/v2";
import { SessionStorageKeys } from "@app/const";
import { useTimedReset } from "@app/hooks";
const getTerminalCliToken = () => {
const cliTerminalTokenInfo = sessionStorage.getItem(SessionStorageKeys.CLI_TERMINAL_TOKEN);
if (!cliTerminalTokenInfo) return;
const { expiry, data } = JSON.parse(cliTerminalTokenInfo);
if (new Date() > new Date(expiry)) {
sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN);
return;
}
// eslint-disable-next-line
return data as string;
};
export default function CliRedirect() {
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
initialState: false
});
const cliToken = getTerminalCliToken();
const copyUrlToClipboard = () => {
if (cliToken) {
navigator.clipboard.writeText(cliToken);
setIsUrlCopied(true);
sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN);
}
};
return (
<div className="flex flex-col justify-between bg-bunker-800 md:h-screen">
<Head>
<title>Infisical CLI | Login Successful!</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<div className="flex h-screen w-screen flex-col items-center justify-center text-gray-200">
<div className="mb-8 flex justify-center">
<div className="flex h-screen w-screen flex-col items-center justify-center space-y-4 text-gray-200">
<div className="mb-4 flex justify-center">
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
</div>
<p className="bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-3xl font-medium text-transparent">
Head back to your terminal
</p>
<p className="text-light mb-1 text-lg text-mineshaft-400">
You&apos;ve successfully logged in to the Infisical CLI
</p>
{cliToken ? (
<>
<div className="pb-4">
<p className="bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-3xl font-medium text-transparent">
Unable to reach CLI
</p>
<p className="text-light mb-1 text-lg text-mineshaft-400 text-center">
Your login was successful but, Infisical couldn&apos;t automatically push your login token to the CLI.
</p>
<p className="text-light mb-1 text-lg text-mineshaft-400 text-center">
Please copy the token below and manually provide it to your CLI.
</p>
</div>
<div className="dark relative flex max-h-36 max-w-xl flex-col items-center space-y-4 overflow-y-auto rounded-md border border-mineshaft-700 bg-mineshaft-900 p-3">
<SecretInput value={cliToken as string} />
<div className="mx-1 flex">
<IconButton
variant="outline_bg"
colorSchema="primary"
ariaLabel="copy to clipboard"
onClick={copyUrlToClipboard}
className=" flex items-center rounded py-2"
>
<FontAwesomeIcon className="pr-2" icon={isUrlCopied ? faCheck : faCopy} /> Copy to
clipboard
</IconButton>
</div>
</div>
</>
) : (
<>
<p className="bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-3xl font-medium text-transparent">
Head back to your terminal
</p>
<p className="text-light mb-1 text-lg text-mineshaft-400">
You&apos;ve successfully logged in to the Infisical CLI
</p>
</>
)}
</div>
</div>
);

View File

@ -352,7 +352,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex flex-row items-center">
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
<span className="text-md ml-3 text-mineshaft-100">Pro Tips</span>
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
</div>
<span className="mt-4 text-sm text-mineshaft-300">
After creating an integration, your secrets will start syncing immediately. This might

View File

@ -384,7 +384,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex flex-row items-center">
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
<span className="text-md ml-3 text-mineshaft-100">Pro Tips</span>
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
</div>
<span className="mt-4 text-sm text-mineshaft-300">
After creating an integration, your secrets will start syncing immediately. This might

View File

@ -192,7 +192,7 @@ export default function CircleCICreateIntegrationPage() {
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex flex-row items-center">
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
<span className="text-md ml-3 text-mineshaft-100">Pro Tips</span>
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
</div>
<span className="mt-4 text-sm text-mineshaft-300">
After creating an integration, your secrets will start syncing immediately. This might

View File

@ -200,7 +200,7 @@ export default function FlyioCreateIntegrationPage() {
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex flex-row items-center">
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
<span className="text-md ml-3 text-mineshaft-100">Pro Tips</span>
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
</div>
<span className="mt-4 text-sm text-mineshaft-300">
After creating an integration, your secrets will start syncing immediately. This might

View File

@ -602,7 +602,7 @@ export default function GitHubCreateIntegrationPage() {
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex flex-row items-center">
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
<span className="text-md ml-3 text-mineshaft-100">Pro Tips</span>
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
</div>
<span className="mt-4 text-sm text-mineshaft-300">
After creating an integration, your secrets will start syncing immediately. This might

View File

@ -470,7 +470,7 @@ export default function GitLabCreateIntegrationPage() {
</Card>
{/* <div className="border-t border-mineshaft-800 w-full max-w-md mt-6"/>
<div className="flex flex-col bg-mineshaft-800 border border-mineshaft-600 w-full p-4 max-w-lg mt-6 rounded-md">
<div className="flex flex-row items-center"><FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-200 text-xl"/> <span className="ml-3 text-md text-mineshaft-100">Pro Tips</span></div>
<div className="flex flex-row items-center"><FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-200 text-xl"/> <span className="ml-3 text-md text-mineshaft-100">Pro Tip</span></div>
<span className="text-mineshaft-300 text-sm mt-4">After creating an integration, your secrets will start syncing immediately. This might cause an unexpected override of current secrets in GitLab with secrets from Infisical.</span>
</div> */}
<Modal

View File

@ -185,7 +185,7 @@ export default function HashiCorpVaultCreateIntegrationPage() {
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex flex-row items-center">
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
<span className="text-md ml-3 text-mineshaft-100">Pro Tips</span>
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
</div>
<span className="mt-4 text-sm text-mineshaft-300">
After creating an integration, your secrets will start syncing immediately. This might

View File

@ -353,7 +353,7 @@ export default function HerokuCreateIntegrationPage() {
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex flex-row items-center">
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
<span className="text-md ml-3 text-mineshaft-100">Pro Tips</span>
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
</div>
<span className="mt-4 text-sm text-mineshaft-300">
After creating an integration, your secrets will start syncing immediately. This might

View File

@ -395,7 +395,7 @@ export default function QoveryCreateIntegrationPage() {
</Card>
{/* <div className="border-t border-mineshaft-800 w-full max-w-md mt-6"/>
<div className="flex flex-col bg-mineshaft-800 border border-mineshaft-600 w-full p-4 max-w-lg mt-6 rounded-md">
<div className="flex flex-row items-center"><FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-200 text-xl"/> <span className="ml-3 text-md text-mineshaft-100">Pro Tips</span></div>
<div className="flex flex-row items-center"><FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-200 text-xl"/> <span className="ml-3 text-md text-mineshaft-100">Pro Tip</span></div>
<span className="text-mineshaft-300 text-sm mt-4">After creating an integration, your secrets will start syncing immediately. This might cause an unexpected override of current secrets in Qovery with secrets from Infisical.</span>
</div> */}
</div>

View File

@ -262,7 +262,7 @@ export default function RenderCreateIntegrationPage() {
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex flex-row items-center">
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
<span className="text-md ml-3 text-mineshaft-100">Pro Tips</span>
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
</div>
<span className="mt-4 text-sm text-mineshaft-300">
After creating an integration, your secrets will start syncing immediately. This might

View File

@ -246,7 +246,7 @@ export default function VercelCreateIntegrationPage() {
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex flex-row items-center">
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
<span className="text-md ml-3 text-mineshaft-100">Pro Tips</span>
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
</div>
<span className="mt-4 text-sm text-mineshaft-300">
After creating an integration, your secrets will start syncing immediately. This might

View File

@ -7,11 +7,13 @@ import { useRouter } from "next/router";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import axios from "axios";
import { addSeconds, formatISO } from "date-fns";
import jwt_decode from "jwt-decode";
import { createNotification } from "@app/components/notifications";
import { IsCliLoginSuccessful } from "@app/components/utilities/attemptCliLogin";
import { Button, Spinner } from "@app/components/v2";
import { SessionStorageKeys } from "@app/const";
import { useUser } from "@app/context";
import { useGetOrganizations, useLogoutUser, useSelectOrganization } from "@app/hooks/api";
import { Organization } from "@app/hooks/api/types";
@ -94,9 +96,19 @@ export default function LoginPage() {
// send request to server endpoint
const instance = axios.create();
await instance.post(`http://127.0.0.1:${callbackPort}/`, payload);
// cli page
await instance.post(`http://127.0.0.1:${callbackPort}/`, payload).catch(() => {
// if error happens to communicate we set the token with an expiry in sessino storage
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
sessionStorage.setItem(
SessionStorageKeys.CLI_TERMINAL_TOKEN,
JSON.stringify({
expiry: formatISO(addSeconds(new Date(), 30)),
data: window.btoa(JSON.stringify(payload))
})
);
});
router.push("/cli-redirect");
// cli page
} else {
navigateUserToOrg(router, organization.id);
}

View File

@ -3,6 +3,7 @@ import ReactCodeInput from "react-code-input";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import axios from "axios";
import { addSeconds, formatISO } from "date-fns";
import jwt_decode from "jwt-decode";
import Error from "@app/components/basic/Error";
@ -11,6 +12,7 @@ import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa";
import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button } from "@app/components/v2";
import { SessionStorageKeys } from "@app/const";
import { useSendMfaToken } from "@app/hooks/api/auth";
import { useSelectOrganization, verifyMfaToken } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
@ -79,11 +81,24 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
if (callbackPort) {
const cliUrl = `http://127.0.0.1:${callbackPort}/`;
const instance = axios.create();
await instance.post(cliUrl, {
const payload = {
email,
privateKey,
JTWToken: newJwtToken
};
await instance.post(cliUrl, payload).catch(() => {
// if error happens to communicate we set the token with an expiry in sessino storage
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
sessionStorage.setItem(
SessionStorageKeys.CLI_TERMINAL_TOKEN,
JSON.stringify({
expiry: formatISO(addSeconds(new Date(), 30)),
data: window.btoa(JSON.stringify(payload))
})
);
});
router.push("/cli-redirect");
return;
}
await navigateUserToOrg(router, organizationId);
}

View File

@ -6,12 +6,13 @@ import {
faLockOpen,
faSquareCheck,
faSquareXmark,
faTriangleExclamation,
faUserLock} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { Button, Checkbox } from "@app/components/v2";
import { Button, Checkbox, FormControl, Input } from "@app/components/v2";
import {
usePerformSecretApprovalRequestMerge,
useUpdateSecretApprovalRequestStatus
@ -48,12 +49,19 @@ export const SecretApprovalRequestAction = ({
useUpdateSecretApprovalRequestStatus();
const [byPassApproval, setByPassApproval] = useState(false);
const [bypassReason, setBypassReason] = useState("");
const isValidBypassReason = (value: string) => {
const trimmedValue = value.trim();
return trimmedValue.length >= 10;
};
const handleSecretApprovalRequestMerge = async () => {
try {
await performSecretApprovalMerge({
id: approvalRequestId,
workspaceId
workspaceId,
bypassReason: byPassApproval ? bypassReason : undefined
});
createNotification({
type: "success",
@ -92,7 +100,7 @@ export const SecretApprovalRequestAction = ({
if (!hasMerged && status === "open") {
return (
<div className="flex w-full items-center justify-between">
<div className="flex w-full items-start justify-between transition-all">
<div className="flex items-start space-x-4">
<FontAwesomeIcon
icon={isMergable ? faSquareCheck : faSquareXmark}
@ -105,18 +113,33 @@ export const SecretApprovalRequestAction = ({
{Boolean(statusChangeByEmail) && `. Reopened by ${statusChangeByEmail}`}
</span>
{!canApprove && isSoftEnforcement && (
<div className="mt-1">
<div className="mt-2 flex flex-col space-y-2">
<Checkbox
onCheckedChange={(checked) => setByPassApproval(checked === true)}
isChecked={byPassApproval}
id="byPassApproval"
checkIndicatorBg="text-white"
className={byPassApproval ? "bg-red hover:bg-red-600 border-red" : ""}
className={twMerge("mr-2", byPassApproval ? "bg-red hover:bg-red-600 border-red" : "")}
>
<span className="text-red text-sm">
<span className="text-red text-xs">
Merge without waiting for approval (bypass secret change policy)
</span>
</Checkbox>
{byPassApproval && (
<FormControl
label="Reason for bypass"
className="mt-2"
isRequired
tooltipText="Enter a reason for bypassing the secret change policy"
>
<Input
value={bypassReason}
onChange={(e) => setBypassReason(e.target.value)}
placeholder="Enter reason for bypass (min 10 chars)"
leftIcon={<FontAwesomeIcon icon={faTriangleExclamation} />}
/>
</FormControl>
)}
</div>
)}
</span>
@ -137,7 +160,7 @@ export const SecretApprovalRequestAction = ({
leftIcon={<FontAwesomeIcon icon={!canApprove ? faLandMineOn : faCheck} />}
isDisabled={
(!isMergable && canApprove)
|| (!canApprove && isSoftEnforcement && !byPassApproval)
|| (!canApprove && isSoftEnforcement && (!byPassApproval || !isValidBypassReason(bypassReason)))
}
isLoading={isMerging}
onClick={handleSecretApprovalRequestMerge}

View File

@ -1,5 +1,6 @@
import crypto from "crypto";
import { useEffect, useRef } from "react";
import { Controller } from "react-hook-form";
import { AxiosError } from "axios";
import * as yup from "yup";
@ -7,13 +8,14 @@ import * as yup from "yup";
import { createNotification } from "@app/components/notifications";
import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto";
import { Button, Checkbox, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api/secretSharing";
import { SecretSharingAccessType, useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api/secretSharing";
const schema = yup.object({
value: yup.string().max(10000).required().label("Shared Secret Value"),
expiresAfterSingleView: yup.boolean().required().label("Expires After Views"),
expiresInValue: yup.number().min(1).required().label("Expiration Value"),
expiresInUnit: yup.string().required().label("Expiration Unit")
expiresInUnit: yup.string().required().label("Expiration Unit"),
accessType: yup.string().required().label("General Access")
});
export type FormData = yup.InferType<typeof schema>;
@ -35,6 +37,14 @@ export const AddShareSecretForm = ({
setNewSharedSecret: (value: string) => void;
isInputDisabled?: boolean;
}) => {
const isMounted = useRef(true);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
const publicSharedSecretCreator = useCreatePublicSharedSecret();
const privateSharedSecretCreator = useCreateSharedSecret();
const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator;
@ -65,7 +75,8 @@ export const AddShareSecretForm = ({
value,
expiresInValue,
expiresInUnit,
expiresAfterSingleView
expiresAfterSingleView,
accessType
}: FormData) => {
try {
const key = crypto.randomBytes(16).toString("hex");
@ -89,18 +100,21 @@ export const AddShareSecretForm = ({
tag,
hashedHex,
expiresAt,
expiresAfterViews: expiresAfterSingleView ? 1 : 1000
expiresAfterViews: expiresAfterSingleView ? 1 : 1000,
accessType: accessType as SecretSharingAccessType
});
setNewSharedSecret(
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
hashedHex
)}-${encodeURIComponent(key)}`
);
createNotification({
text: "Successfully created a shared secret",
type: "success"
});
if (isMounted.current) {
setNewSharedSecret(
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
hashedHex
)}-${encodeURIComponent(key)}`
);
createNotification({
text: "Successfully created a shared secret",
type: "success"
});
}
} catch (err) {
console.error(err);
const axiosError = err as AxiosError;
@ -143,7 +157,7 @@ export const AddShareSecretForm = ({
)}
/>
</div>
<div className="flex w-full flex-col md:flex-row justify-start">
<div className="flex w-full flex-col md:flex-row justify-stretch">
<div className="flex justify-start">
<div className="flex justify-start">
<div className="flex w-full justify-center pr-2">
@ -188,8 +202,8 @@ export const AddShareSecretForm = ({
</div>
</div>
</div>
<div className="sm:w-1/7 mx-auto items-center justify-center px-6 hidden md:flex">
<p className="px-4 mt-2 text-sm text-gray-400">AND</p>
<div className="sm:w-1/7 mx-auto items-center justify-center hidden md:flex">
<p className="mt-2 text-sm text-gray-400">AND</p>
</div>
<div className="items-center pb-4 md:pb-0 md:pt-2 flex">
<Controller
@ -227,7 +241,25 @@ export const AddShareSecretForm = ({
</div>
</div>
</div>
<div className={`flex items-center ${!inModal && "justify-start pt-2"}`}>
{!isPublic && (
<Controller
control={control}
name="accessType"
defaultValue="organization"
render={({ field: { onChange, ...field } }) => (
<FormControl label="General Access">
<Select
{...field}
onValueChange={(e) => onChange(e)}
>
<SelectItem value="organization">People within your organization</SelectItem>
<SelectItem value="anyone">Anyone</SelectItem>
</Select>
</FormControl>
)}
/>
)}
<div className={`flex items-center space-x-4 pt-2 ${!inModal && ""}`}>
<Button className="mr-0" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
{inModal ? "Create" : "Share Secret"}
</Button>

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo } from "react";
import { useMemo } from "react";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
@ -26,6 +26,8 @@ export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean
id as string,
hashedHex as string
);
const accessType = data?.accessType;
const orgName = data?.orgName;
const decryptedSecret = useMemo(() => {
if (data && data.encryptedValue && publicKey) {
@ -44,12 +46,6 @@ export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean
initialState: false
});
useEffect(() => {
if (isUrlCopied) {
setTimeout(() => setIsUrlCopied(false), 2000);
}
}, [isUrlCopied]);
const copyUrlToClipboard = () => {
navigator.clipboard.writeText(decryptedSecret);
setIsUrlCopied(true);
@ -57,12 +53,12 @@ export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean
const { popUp, handlePopUpToggle } = usePopUp(["createSharedSecret"] as const);
return (
<div className="h-screen dark:[color-scheme:dark] flex flex-col overflow-y-auto bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200">
<div className="flex h-screen flex-col overflow-y-auto bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
<Head>
<title>Secret Shared | Infisical</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<div className="w-full flex flex-grow items-center justify-center dark:[color-scheme:dark]">
<div className="flex w-full flex-grow items-center justify-center dark:[color-scheme:dark]">
<div className="relative">
<div className="mb-4 flex justify-center pt-8">
<Link href="https://infisical.com">
@ -75,9 +71,15 @@ export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean
/>
</Link>
</div>
<div className="w-full flex justify-center">
<h1 className={`${id ? "max-w-sm mb-4": "max-w-md mt-4 mb-6"} bg-gradient-to-b from-white to-bunker-200 bg-clip-text px-4 text-center text-3xl font-medium text-transparent`}>
{id ? "Someone shared a secret via Infisical with you" : "Share a secret via Infisical"}
<div className="flex w-full justify-center">
<h1
className={`${
id ? "mb-4 max-w-sm" : "mt-4 mb-6 max-w-md"
} bg-gradient-to-b from-white to-bunker-200 bg-clip-text px-4 text-center text-3xl font-medium text-transparent`}
>
{id
? "Someone shared a secret via Infisical with you"
: "Share a secret via Infisical"}
</h1>
</div>
<div className="m-auto mt-4 flex w-full max-w-2xl justify-center px-6">
@ -87,6 +89,8 @@ export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean
decryptedSecret={decryptedSecret}
isUrlCopied={isUrlCopied}
copyUrlToClipboard={copyUrlToClipboard}
accessType={accessType}
orgName={orgName}
/>
)}
</div>
@ -110,7 +114,7 @@ export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<Button
className="bg-mineshaft-700 text-bunker-200 w-full py-3"
className="w-full bg-mineshaft-700 py-3 text-bunker-200"
colorSchema="primary"
variant="outline_bg"
size="sm"
@ -122,34 +126,45 @@ export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean
</a>
</div>
)}
<div className="m-auto my-6 flex w-full max-w-xl justify-center px-8 px-4 sm:my-8">
<div className="m-auto my-6 flex w-full max-w-xl justify-center px-4 sm:my-8">
<div className="w-full border-t border-mineshaft-600" />
</div>
<div className="flex flex-col justify-center items-center m-auto max-w-2xl px-4 sm:px-6">
<div className="m-auto mb-12 flex flex max-w-2xl w-full flex-col justify-center rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
<p className="pb-2 font-semibold text-mineshaft-100 md:pb-3 text-lg md:text-xl w-full">
Open source <span className="bg-clip-text text-transparent bg-gradient-to-tr from-yellow-500 to-primary-500">secret management</span> for developers
<div className="m-auto flex max-w-2xl flex-col items-center justify-center px-4 sm:px-6">
<div className="m-auto mb-12 flex w-full max-w-2xl flex-col justify-center rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
Open source{" "}
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
secret management
</span>{" "}
for developers
</p>
<div className="flex flex-col sm:flex-row gap-x-4">
<div className="flex flex-col gap-x-4 sm:flex-row">
<p className="md:text-md text-md">
<a
href="https://github.com/infisical/infisical"
target="_blank"
rel="noopener noreferrer"
className="bg-clip-text text-transparent bg-gradient-to-tr from-yellow-500 to-primary-500 text-bold"
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
>
Infisical
</a>{" "} is the all-in-one secret management platform to securely manage secrets, configs, and certificates across your team and infrastructure.
</a>{" "}
is the all-in-one secret management platform to securely manage secrets, configs,
and certificates across your team and infrastructure.
</p>
<Link href="https://infisical.com">
<span className="mt-4 border border-mineshaft-400/40 w-[17.5rem] h-min py-2 px-3 rounded-md bg-mineshaft-600 cursor-pointer duration-200 hover:text-white hover:border-primary/60 hover:bg-primary/20">
Try Infisical <FontAwesomeIcon icon={faArrowRight} className="pl-1"/>
<span className="mt-4 h-min w-[17.5rem] cursor-pointer rounded-md border border-mineshaft-400/40 bg-mineshaft-600 py-2 px-3 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
Try Infisical <FontAwesomeIcon icon={faArrowRight} className="pl-1" />
</span>
</Link>
</div>
</div>
</div>
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} isPublic inModal />
<AddShareSecretModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
isPublic
inModal
/>
</div>
</div>
<div className="mt-auto flex w-full items-center justify-center bg-mineshaft-600 p-2">

View File

@ -1,14 +1,17 @@
import { faCheck, faCopy, faEye, faEyeSlash, faKey } from "@fortawesome/free-solid-svg-icons";
import { faArrowRight, faCheck, faCopy, faEye, faEyeSlash, faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { EmptyState, IconButton, Td, Tr } from "@app/components/v2";
import { Button, EmptyState, IconButton, Td, Tr } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { SecretSharingAccessType } from "@app/hooks/api/secretSharing/types";
type Props = {
isLoading: boolean;
decryptedSecret: string;
isUrlCopied: boolean;
copyUrlToClipboard: () => void;
accessType?: SecretSharingAccessType;
orgName?: string;
};
const replaceContentWithDot = (str: string) => {
@ -24,20 +27,49 @@ export const SecretTable = ({
isLoading,
decryptedSecret,
isUrlCopied,
copyUrlToClipboard
copyUrlToClipboard,
accessType,
orgName
}: Props) => {
const [isVisible, setIsVisible] = useToggle(false);
const title = orgName
? (<p>Someone from <strong>{orgName}</strong> organization has shared a secret with you</p>)
: (<p>You need to be logged in to view this secret</p>);
return (
<div className="flex w-full items-center justify-center rounded-md border border-solid border-mineshaft-700 bg-mineshaft-800 p-2">
{isLoading && <div className="bg-mineshaft-800 text-center text-bunker-400">Loading...</div>}
{!isLoading && !decryptedSecret && (
{!isLoading && !decryptedSecret && accessType !== SecretSharingAccessType.Organization && (
<Tr>
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
<EmptyState title="Secret has either expired or does not exist!" icon={faKey} />
</Td>
</Tr>
)}
{!isLoading && !decryptedSecret && accessType === SecretSharingAccessType.Organization && (
<Tr>
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-4000">
<EmptyState title={title} icon={faKey}>
<div className="flex flex-1 flex-col items-center justify-center pt-6">
<a
href="/login"
target="_blank"
rel="noopener noreferrer"
>
<Button
colorSchema="primary"
size="sm"
onClick={() => {}}
rightIcon={<FontAwesomeIcon icon={faArrowRight} className="ml-2" />}
>
Login into <strong>{orgName}</strong> to view this secret
</Button>
</a>
</div>
</EmptyState>
</Td>
</Tr>
)}
{!isLoading && decryptedSecret && (
<div className="dark relative flex h-full w-full items-center overflow-y-auto rounded-md border border-mineshaft-700 bg-mineshaft-900 p-2 pr-2 md:p-3">
<div

View File

@ -13,9 +13,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: v0.6.5
version: v0.7.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "v0.6.5"
appVersion: "v0.7.0"

View File

@ -40,6 +40,10 @@ func (r *InfisicalSecretReconciler) HandleAuthentication(ctx context.Context, in
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
}
if infisicalToken != "" {
infisicalClient.Auth().SetAccessToken(infisicalToken)
return AuthenticationDetails{authStrategy: AuthStrategy.SERVICE_TOKEN}, nil
}
// ? Legacy support, service account auth
serviceAccountCreds, err := r.GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
@ -48,9 +52,8 @@ func (r *InfisicalSecretReconciler) HandleAuthentication(ctx context.Context, in
}
if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" {
infisicalClient.Auth().SetAccessToken(serviceAccountCreds.AccessKey)
return AuthenticationDetails{authStrategy: AuthStrategy.SERVICE_ACCOUNT}, nil
} else if infisicalToken != "" {
return AuthenticationDetails{authStrategy: AuthStrategy.SERVICE_TOKEN}, nil
}
authStrategies := map[AuthStrategyType]func(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error){
@ -69,7 +72,7 @@ func (r *InfisicalSecretReconciler) HandleAuthentication(ctx context.Context, in
return authDetails, nil
}
if err != nil && !errors.Is(err, ErrAuthNotApplicable) {
if !errors.Is(err, ErrAuthNotApplicable) {
return AuthenticationDetails{}, fmt.Errorf("authentication failed for strategy [%s] [err=%w]", authStrategy, err)
}
}
@ -360,11 +363,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
return fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
}
if err != nil {
return fmt.Errorf("unable to load Infisical Token from the specified Kubernetes secret with error [%w]", err)
}
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceAccount(serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag)
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceAccount(infisicalClient, serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag)
if err != nil {
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
}
@ -381,7 +380,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
secretsPath := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.SecretsPath
recursive := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.Recursive
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath, recursive)
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceToken(infisicalClient, infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath, recursive)
if err != nil {
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
}

View File

@ -8,26 +8,6 @@ import (
const USER_AGENT_NAME = "k8-operator"
func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncryptedWorkspaceKeyRequest) (GetEncryptedWorkspaceKeyResponse, error) {
endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", API_HOST_URL, request.WorkspaceId)
var result GetEncryptedWorkspaceKeyResponse
response, err := httpClient.
R().
SetResult(&result).
SetHeader("User-Agent", USER_AGENT_NAME).
Get(endpoint)
if err != nil {
return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response: [response=%s]", response)
}
return result, nil
}
func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDetailsResponse, error) {
var tokenDetailsResponse GetServiceTokenDetailsResponse
response, err := httpClient.
@ -51,43 +31,6 @@ func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDeta
return tokenDetailsResponse, nil
}
func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Request) (GetEncryptedSecretsV3Response, error) {
var secretsResponse GetEncryptedSecretsV3Response
httpRequest := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT_NAME).
SetQueryParam("environment", request.Environment).
SetQueryParam("include_imports", "true"). // TODO needs to be set as a option
SetQueryParam("workspaceId", request.WorkspaceId)
if request.SecretPath != "" {
httpRequest.SetQueryParam("secretPath", request.SecretPath)
}
if request.Recursive {
httpRequest.SetQueryParam("recursive", "true")
}
response, err := httpRequest.Get(fmt.Sprintf("%v/v3/secrets", API_HOST_URL))
if err != nil {
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
}
responseETag := response.Header().Get("etag")
secretsResponse.Modified = request.ETag != responseETag
secretsResponse.ETag = responseETag
return secretsResponse, nil
}
func CallGetServiceTokenAccountDetailsV2(httpClient *resty.Client) (ServiceAccountDetailsResponse, error) {
var serviceAccountDetailsResponse ServiceAccountDetailsResponse
response, err := httpClient.
@ -149,42 +92,6 @@ func CallUniversalMachineIdentityRefreshAccessToken(request MachineIdentityUnive
return universalAuthRefreshResponse, nil
}
func CallGetDecryptedSecretsV3(httpClient *resty.Client, request GetDecryptedSecretsV3Request) (GetDecryptedSecretsV3Response, error) {
var decryptedSecretsResponse GetDecryptedSecretsV3Response
req := httpClient.
R().
SetResult(&decryptedSecretsResponse).
SetHeader("User-Agent", USER_AGENT_NAME).
SetQueryParam("include_imports", "true").
SetQueryParam("secretPath", request.SecretPath).
SetQueryParam("workspaceSlug", request.ProjectSlug).
SetQueryParam("environment", request.Environment)
if request.Recursive {
req.SetQueryParam("recursive", "true")
}
if request.ExpandSecretReferences {
req.SetQueryParam("expandSecretReferences", "true")
}
response, err := req.Get(fmt.Sprintf("%v/v3/secrets/raw", API_HOST_URL))
if err != nil {
return GetDecryptedSecretsV3Response{}, fmt.Errorf("CallGetDecryptedSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return GetDecryptedSecretsV3Response{}, fmt.Errorf("CallGetDecryptedSecretsV3: Unsuccessful response: [response=%s]", response)
}
responseETag := response.Header().Get("etag")
decryptedSecretsResponse.Modified = request.ETag != responseETag
decryptedSecretsResponse.ETag = responseETag
return decryptedSecretsResponse, nil
}
func CallGetServiceAccountWorkspacePermissionsV2(httpClient *resty.Client) (ServiceAccountWorkspacePermissions, error) {
var serviceAccountWorkspacePermissionsResponse ServiceAccountWorkspacePermissions
response, err := httpClient.

View File

@ -106,6 +106,7 @@ type GetDecryptedSecretsV3Response struct {
}
type GetDecryptedSecretsV3Request struct {
ProjectID string `json:"workspaceId"`
ProjectSlug string `json:"workspaceSlug"`
Environment string `json:"environment"`
SecretPath string `json:"secretPath"`

View File

@ -1,10 +1,7 @@
package util
import (
"encoding/base64"
"fmt"
"path"
"regexp"
"strings"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
@ -87,7 +84,7 @@ func GetPlainTextSecretsViaMachineIdentity(infisicalClient infisical.InfisicalCl
}, nil
}
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, envSlug string, secretPath string, recursive bool) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) {
func GetPlainTextSecretsViaServiceToken(infisicalClient infisical.InfisicalClientInterface, fullServiceToken string, etag string, envSlug string, secretPath string, recursive bool) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 {
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
@ -105,51 +102,44 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, en
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
}
encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace,
Environment: envSlug,
Recursive: recursive,
ETag: etag,
SecretPath: secretPath,
secrets, err := infisicalClient.Secrets().List(infisical.ListSecretsOptions{
ProjectID: serviceTokenDetails.Workspace,
Environment: envSlug,
Recursive: recursive,
SecretPath: secretPath,
IncludeImports: true,
ExpandSecretReferences: true,
})
if err != nil {
return nil, model.RequestUpdateUpdateDetails{}, err
}
decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag)
if err != nil {
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err)
var environmentVariables []model.SingleEnvironmentVariable
for _, secret := range secrets {
environmentVariables = append(environmentVariables, model.SingleEnvironmentVariable{
Key: secret.SecretKey,
Value: secret.SecretValue,
Type: secret.Type,
ID: secret.ID,
})
}
plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV)
if err != nil {
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to decrypt the required workspace key")
}
newEtag := crypto.ComputeEtag([]byte(fmt.Sprintf("%v", environmentVariables)))
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets)
if err != nil {
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
}
plainTextSecretsMergedWithImports, err := InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecretsResponse.ImportedSecrets)
if err != nil {
return nil, model.RequestUpdateUpdateDetails{}, err
}
// expand secrets that are referenced
expandedSecrets := ExpandSecrets(plainTextSecretsMergedWithImports, fullServiceToken)
return expandedSecrets, model.RequestUpdateUpdateDetails{
Modified: encryptedSecretsResponse.Modified,
ETag: encryptedSecretsResponse.ETag,
return environmentVariables, model.RequestUpdateUpdateDetails{
Modified: etag != newEtag,
ETag: newEtag,
}, nil
}
// Fetches plaintext secrets from an API endpoint using a service account.
// The function fetches the service account details and keys, decrypts the workspace key, fetches the encrypted secrets for the specified project and environment, and decrypts the secrets using the decrypted workspace key.
// Returns the plaintext secrets, encrypted secrets response, and any errors that occurred during the process.
func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string, etag string) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) {
func GetPlainTextSecretsViaServiceAccount(infisicalClient infisical.InfisicalClientInterface, serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string, etag string) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) {
httpClient := resty.New()
httpClient.SetAuthToken(serviceAccountCreds.AccessKey).
SetHeader("Accept", "application/json")
@ -176,226 +166,36 @@ func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccou
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err)
}
cipherText, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.EncryptedKey)
if err != nil {
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode EncryptedKey secrets because [err=%v]", err)
}
nonce, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.Nonce)
if err != nil {
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode nonce secrets because [err=%v]", err)
}
publickey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PublicKey)
if err != nil {
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PublicKey secrets because [err=%v]", err)
}
privateKey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PrivateKey)
if err != nil {
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PrivateKey secrets because [err=%v]", err)
}
plainTextWorkspaceKey := crypto.DecryptAsymmetric(cipherText, nonce, publickey, privateKey)
encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: projectId,
Environment: environmentName,
ETag: etag,
secrets, err := infisicalClient.Secrets().List(infisical.ListSecretsOptions{
ProjectID: projectId,
Environment: environmentName,
Recursive: false,
SecretPath: "/",
IncludeImports: true,
ExpandSecretReferences: true,
})
if err != nil {
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err)
return nil, model.RequestUpdateUpdateDetails{}, err
}
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets)
if err != nil {
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err)
}
var environmentVariables []model.SingleEnvironmentVariable
return plainTextSecrets, model.RequestUpdateUpdateDetails{
Modified: encryptedSecretsResponse.Modified,
ETag: encryptedSecretsResponse.ETag,
}, nil
}
func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) {
cipherx, err := base64.StdEncoding.DecodeString(cipher)
if err != nil {
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode cipher text [err=%v]", err)
}
keyx, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode key [err=%v]", err)
}
IVx, err := base64.StdEncoding.DecodeString(IV)
if err != nil {
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode IV [err=%v]", err)
}
tagx, err := base64.StdEncoding.DecodeString(tag)
if err != nil {
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode tag [err=%v]", err)
}
return DecodedSymmetricEncryptionDetails{
Key: keyx,
Cipher: cipherx,
IV: IVx,
Tag: tagx,
}, nil
}
func GetPlainTextSecrets(key []byte, encryptedSecrets []api.EncryptedSecretV3) ([]model.SingleEnvironmentVariable, error) {
plainTextSecrets := []model.SingleEnvironmentVariable{}
for _, secret := range encryptedSecrets {
// Decrypt key
key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV)
if err != nil {
return nil, fmt.Errorf("unable to decode secret IV for secret key")
}
key_tag, err := base64.StdEncoding.DecodeString(secret.SecretKeyTag)
if err != nil {
return nil, fmt.Errorf("unable to decode secret authentication tag for secret key")
}
key_ciphertext, err := base64.StdEncoding.DecodeString(secret.SecretKeyCiphertext)
if err != nil {
return nil, fmt.Errorf("unable to decode secret cipher text for secret key")
}
plainTextKey, err := crypto.DecryptSymmetric(key, key_ciphertext, key_tag, key_iv)
if err != nil {
return nil, fmt.Errorf("unable to symmetrically decrypt secret key")
}
// Decrypt value
value_iv, err := base64.StdEncoding.DecodeString(secret.SecretValueIV)
if err != nil {
return nil, fmt.Errorf("unable to decode secret IV for secret value")
}
value_tag, err := base64.StdEncoding.DecodeString(secret.SecretValueTag)
if err != nil {
return nil, fmt.Errorf("unable to decode secret authentication tag for secret value")
}
value_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretValueCiphertext)
if err != nil {
return nil, fmt.Errorf("unable to decode secret cipher text for secret key")
}
plainTextValue, err := crypto.DecryptSymmetric(key, value_ciphertext, value_tag, value_iv)
if err != nil {
return nil, fmt.Errorf("unable to symmetrically decrypt secret value")
}
plainTextSecret := model.SingleEnvironmentVariable{
Key: string(plainTextKey),
Value: string(plainTextValue),
Type: string(secret.Type),
for _, secret := range secrets {
environmentVariables = append(environmentVariables, model.SingleEnvironmentVariable{
Key: secret.SecretKey,
Value: secret.SecretValue,
Type: secret.Type,
ID: secret.ID,
}
plainTextSecrets = append(plainTextSecrets, plainTextSecret)
})
}
return plainTextSecrets, nil
}
newEtag := crypto.ComputeEtag([]byte(fmt.Sprintf("%v", environmentVariables)))
var secRefRegex = regexp.MustCompile(`\${([^\}]*)}`)
func recursivelyExpandSecret(expandedSecs map[string]string, interpolatedSecs map[string]string, crossSecRefFetch func(env string, path []string, key string) string, key string) string {
if v, ok := expandedSecs[key]; ok {
return v
}
interpolatedVal, ok := interpolatedSecs[key]
if !ok {
return ""
// panic(fmt.Errorf("Could not find referred secret with key name %s", key), "Please check it refers a")
}
refs := secRefRegex.FindAllStringSubmatch(interpolatedVal, -1)
for _, val := range refs {
// key: "${something}" val: [${something},something]
interpolatedExp, interpolationKey := val[0], val[1]
ref := strings.Split(interpolationKey, ".")
// ${KEY1} => [key1]
if len(ref) == 1 {
val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey)
interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val)
continue
}
// cross board reference ${env.folder.key1} => [env folder key1]
if len(ref) > 1 {
secEnv, tmpSecPath, secKey := ref[0], ref[1:len(ref)-1], ref[len(ref)-1]
interpolatedSecs[interpolationKey] = crossSecRefFetch(secEnv, tmpSecPath, secKey) // get the reference value
val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey)
interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val)
}
}
expandedSecs[key] = interpolatedVal
return interpolatedVal
}
func ExpandSecrets(secrets []model.SingleEnvironmentVariable, infisicalToken string) []model.SingleEnvironmentVariable {
expandedSecs := make(map[string]string)
interpolatedSecs := make(map[string]string)
// map[env.secret-path][keyname]Secret
crossEnvRefSecs := make(map[string]map[string]model.SingleEnvironmentVariable) // a cache to hold all cross board reference secrets
for _, sec := range secrets {
// get all references in a secret
refs := secRefRegex.FindAllStringSubmatch(sec.Value, -1)
// nil means its a secret without reference
if refs == nil {
expandedSecs[sec.Key] = sec.Value // atomic secrets without any interpolation
} else {
interpolatedSecs[sec.Key] = sec.Value
}
}
for i, sec := range secrets {
// already present pick that up
if expandedVal, ok := expandedSecs[sec.Key]; ok {
secrets[i].Value = expandedVal
continue
}
expandedVal := recursivelyExpandSecret(expandedSecs, interpolatedSecs, func(env string, secPaths []string, secKey string) string {
secPaths = append([]string{"/"}, secPaths...)
secPath := path.Join(secPaths...)
secPathDot := strings.Join(secPaths, ".")
uniqKey := fmt.Sprintf("%s.%s", env, secPathDot)
if crossRefSec, ok := crossEnvRefSecs[uniqKey]; !ok {
// if not in cross reference cache, fetch it from server
refSecs, _, err := GetPlainTextSecretsViaServiceToken(infisicalToken, "", env, secPath, false)
if err != nil {
fmt.Printf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath)
// HandleError(err, fmt.Sprintf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath), "If you are using a service token to fetch secrets, please ensure it is valid")
}
refSecsByKey := getSecretsByKeys(refSecs)
// save it to avoid calling api again for same environment and folder path
crossEnvRefSecs[uniqKey] = refSecsByKey
return refSecsByKey[secKey].Value
} else {
return crossRefSec[secKey].Value
}
}, sec.Key)
secrets[i].Value = expandedVal
}
return secrets
return environmentVariables, model.RequestUpdateUpdateDetails{
Modified: etag != newEtag,
ETag: newEtag,
}, nil
}
func getSecretsByKeys(secrets []model.SingleEnvironmentVariable) map[string]model.SingleEnvironmentVariable {
@ -408,35 +208,6 @@ func getSecretsByKeys(secrets []model.SingleEnvironmentVariable) map[string]mode
return secretMapByName
}
func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []model.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]model.SingleEnvironmentVariable, error) {
if importedSecrets == nil {
return secrets, nil
}
hasOverriden := make(map[string]bool)
for _, sec := range secrets {
hasOverriden[sec.Key] = true
}
for i := len(importedSecrets) - 1; i >= 0; i-- {
importSec := importedSecrets[i]
plainTextImportedSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, importSec.Secrets)
if err != nil {
return nil, fmt.Errorf("unable to decrypt your imported secrets [err=%v]", err)
}
for _, sec := range plainTextImportedSecrets {
if _, ok := hasOverriden[sec.Key]; !ok {
secrets = append(secrets, sec)
hasOverriden[sec.Key] = true
}
}
}
return secrets, nil
}
func MergeRawImportedSecrets(secrets []model.SingleEnvironmentVariable, importedSecrets []api.ImportedRawSecretV3) []model.SingleEnvironmentVariable {
if importedSecrets == nil {
return secrets