mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-29 22:02:57 +00:00
Compare commits
144 Commits
role-conce
...
secret-sha
Author | SHA1 | Date | |
---|---|---|---|
7e9389cb26 | |||
eda57881ec | |||
553d51e5b3 | |||
16e0a441ae | |||
d6c0941fa9 | |||
7cbd254f06 | |||
4b83b92725 | |||
fe72f034c1 | |||
6803553b21 | |||
1c8299054a | |||
98b6373d6a | |||
1d97921c7c | |||
0d4164ea81 | |||
79bd8613d3 | |||
8deea21a83 | |||
3b3c2be933 | |||
c041e44399 | |||
c1aeb04174 | |||
3f3c0aab0f | |||
b740e8c900 | |||
4416b11094 | |||
d8169a866d | |||
7239158e7f | |||
fefe2d1de1 | |||
3f3e41282d | |||
c14f94177a | |||
ceb741955d | |||
f5bc4e1b5f | |||
06900b9c99 | |||
d71cb96adf | |||
61ebec25b3 | |||
57320c51fb | |||
4aa9cd0f72 | |||
ea39ef9269 | |||
15749a1f52 | |||
9e9aff129e | |||
4ac487c974 | |||
2e50072caa | |||
2bd170df7d | |||
938a7b7e72 | |||
af864b456b | |||
a30e3874cd | |||
de886f8dd0 | |||
b3db29ac37 | |||
ce1db38afd | |||
0fa6b7a08a | |||
29c5bf5491 | |||
4d711ae149 | |||
9dd675ff98 | |||
8fd3e50d04 | |||
391ed0723e | |||
84af8e708e | |||
b39b5bd1a1 | |||
b3d9d91b52 | |||
5ad4061881 | |||
f29862eaf2 | |||
7cb174b644 | |||
bf00d16c80 | |||
e30a0fe8be | |||
6e6f0252ae | |||
2348df7a4d | |||
962cf67dfb | |||
32627c20c4 | |||
c50f8fd78c | |||
1cb4dc9e84 | |||
977ce09245 | |||
08d7dead8c | |||
a30e06e392 | |||
23f3f09cb6 | |||
5cd0f665fa | |||
443e76c1df | |||
4ea22b6761 | |||
ae7e0d0963 | |||
ed6c6d54c0 | |||
428ff5186f | |||
d07b0d20d6 | |||
8e373fe9bf | |||
28087cdcc4 | |||
dcef49950d | |||
1e5d567ef7 | |||
d09c320150 | |||
229599b8de | |||
02eea4d886 | |||
d12144a7e7 | |||
5fa69235d1 | |||
7dd9337b1c | |||
f9eaee4dbc | |||
cd3a64f3e7 | |||
121254f98d | |||
1591c1dbac | |||
3c59d288c4 | |||
632b775d7f | |||
d66da3d770 | |||
da43f405c4 | |||
7a642e7634 | |||
de686acc23 | |||
4abb3ef348 | |||
73e764474d | |||
7eb5689b4c | |||
5d945f432d | |||
1066710c4f | |||
37137b8c68 | |||
8b10cf863d | |||
eb45bed7d9 | |||
1ee65205a0 | |||
f41272d4df | |||
8bf4df9f27 | |||
037a8f2ebb | |||
14bc436283 | |||
a108c7dde1 | |||
54ccd73d2a | |||
729ca7b6d6 | |||
754db67f11 | |||
f97756a07b | |||
22df51ab8e | |||
bff8f55ea2 | |||
2f17f5e7df | |||
72d2247bf2 | |||
4ecd4c0337 | |||
538613dd40 | |||
4c5c24f689 | |||
dead16a98a | |||
224368b172 | |||
2f2c9d4508 | |||
774017adbe | |||
f9d1d9c89f | |||
eb82fc0d9a | |||
e45585a909 | |||
6f0484f074 | |||
4ba529f22d | |||
5360fb033a | |||
7622cac07e | |||
a101602e0a | |||
ca63a7baa7 | |||
ff4f15c437 | |||
d6c2715852 | |||
fc386c0cbc | |||
263a88379f | |||
1f6c33bdb8 | |||
1d6d424c91 | |||
c39ea130b1 | |||
5514508482 | |||
5921dcaa51 | |||
b2c62c4193 |
.github/workflows
backend/src
db
migrations
20240718170955_add-access-secret-sharing.ts20240719182539_add-bypass-reason-secret-approval-requets.ts20240728010334_secret-sharing-name.ts
schemas
ee
routes/v1
services
audit-log
group
secret-approval-request
lib
server/routes
services
certificate
project-bot
project-env
project-role
project
secret-import
secret-sharing
smtp
cli
docs
api-reference/endpoints
changelog
documentation
guides
platform
images
integrations/cloudflare
platform
access-controls
pr-workflows
secret-sharing
self-hosting/deployment-options/native
integrations/cloud
mint.jsonsdks
self-hosting
frontend/src
components/v2/EmptyState
const.tshooks/api
pages
cli-redirect.tsx
integrations
aws-parameter-store
aws-secret-manager
circleci
flyio
github
gitlab
hashicorp-vault
heroku
qovery
render
vercel
login
project/[id]/roles/[roleSlug]
share-secret
shared/secret/[id]
views
Login/components
Org
IdentityPage
MembersPage
MembersPage.tsx
components
OrgIdentityTab/components/IdentitySection
OrgMembersTab/components/OrgMembersSection
OrgRoleTabSection
RolePage
RolePage.tsx
components
Types
UserPage
Project
MembersPage
RolePage
RolePage.tsx
components
RoleDetailsSection.tsxRoleModal.tsx
index.tsxRolePermissionsSection
ProjectRoleModifySection.utils.tsRolePermissionRow.tsxRolePermissionSecretsRow.tsxRolePermissionsSection.tsxindex.tsx
index.tsxTypes
SecretApprovalPage/components/SecretApprovalRequest/components
SecretMainPage/components/SecretListView
SecretOverviewPage/components
ShareSecretPage/components
AddShareSecretForm.tsxAddShareSecretModal.tsxShareSecretSection.tsxShareSecretsRow.tsxShareSecretsTable.tsxViewAndCopySharedSecret.tsx
ShareSecretPublicPage
ViewSecretPublicPage
helm-charts/secrets-operator
k8-operator
13
.github/workflows/build-binaries.yml
vendored
13
.github/workflows/build-binaries.yml
vendored
@ -14,7 +14,6 @@ defaults:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [x64, arm64]
|
||||
@ -24,6 +23,7 @@ jobs:
|
||||
target: node20-linux
|
||||
- os: win
|
||||
target: node20-win
|
||||
runs-on: ${{ (matrix.arch == 'arm64' && matrix.os == 'linux') && 'ubuntu24-arm64' || 'ubuntu-latest' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@ -49,9 +49,9 @@ jobs:
|
||||
- name: Package into node binary
|
||||
run: |
|
||||
if [ "${{ matrix.os }}" != "linux" ]; then
|
||||
pkg --no-bytecode --public-packages "*" --public --compress Brotli --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} .
|
||||
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} .
|
||||
else
|
||||
pkg --no-bytecode --public-packages "*" --public --compress Brotli --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core .
|
||||
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core .
|
||||
fi
|
||||
|
||||
# Set up .deb package structure (Debian/Ubuntu only)
|
||||
@ -84,7 +84,12 @@ jobs:
|
||||
mv infisical-core.deb ./binary/infisical-core-${{matrix.arch}}.deb
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- run: pip install --upgrade cloudsmith-cli
|
||||
with:
|
||||
python-version: "3.x" # Specify the Python version you need
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade cloudsmith-cli
|
||||
|
||||
# Publish .deb file to Cloudsmith (Debian/Ubuntu only)
|
||||
- name: Publish to Cloudsmith (Debian/Ubuntu)
|
||||
|
@ -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");
|
||||
});
|
||||
}
|
||||
}
|
21
backend/src/db/migrations/20240719182539_add-bypass-reason-secret-approval-requets.ts
Normal file
21
backend/src/db/migrations/20240719182539_add-bypass-reason-secret-approval-requets.ts
Normal 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");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
const doesNameExist = await knex.schema.hasColumn(TableName.SecretSharing, "name");
|
||||
if (!doesNameExist) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.string("name").nullable();
|
||||
});
|
||||
}
|
||||
|
||||
const doesLastViewedAtExist = await knex.schema.hasColumn(TableName.SecretSharing, "lastViewedAt");
|
||||
if (!doesLastViewedAtExist) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.timestamp("lastViewedAt").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
const doesNameExist = await knex.schema.hasColumn(TableName.SecretSharing, "name");
|
||||
if (doesNameExist) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.dropColumn("name");
|
||||
});
|
||||
}
|
||||
|
||||
const doesLastViewedAtExist = await knex.schema.hasColumn(TableName.SecretSharing, "lastViewedAt");
|
||||
if (doesLastViewedAtExist) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.dropColumn("lastViewedAt");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -5,8 +5,6 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AccessApprovalPoliciesSchema = z.object({
|
||||
@ -17,7 +15,7 @@ export const AccessApprovalPoliciesSchema = z.object({
|
||||
envId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
enforcementLevel: z.string().default("hard")
|
||||
});
|
||||
|
||||
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
|
||||
|
@ -13,9 +13,9 @@ export const KmsKeysSchema = z.object({
|
||||
isDisabled: z.boolean().default(false).nullable().optional(),
|
||||
isReserved: z.boolean().default(true).nullable().optional(),
|
||||
orgId: z.string().uuid(),
|
||||
slug: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
slug: z.string()
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
||||
|
@ -18,7 +18,7 @@ export const OrgMembershipsSchema = z.object({
|
||||
orgId: z.string().uuid(),
|
||||
roleId: z.string().uuid().nullable().optional(),
|
||||
projectFavorites: z.string().array().nullable().optional(),
|
||||
isActive: z.boolean()
|
||||
isActive: z.boolean().default(true)
|
||||
});
|
||||
|
||||
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;
|
||||
|
@ -19,7 +19,8 @@ export const SecretApprovalRequestsSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
isReplicated: z.boolean().nullable().optional(),
|
||||
committerUserId: z.string().uuid(),
|
||||
statusChangedByUserId: z.string().uuid().nullable().optional()
|
||||
statusChangedByUserId: z.string().uuid().nullable().optional(),
|
||||
bypassReason: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretApprovalRequests = z.infer<typeof SecretApprovalRequestsSchema>;
|
||||
|
@ -18,7 +18,10 @@ export const SecretSharingSchema = z.object({
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
expiresAfterViews: z.number().nullable().optional()
|
||||
expiresAfterViews: z.number().nullable().optional(),
|
||||
accessType: z.string().default("anyone"),
|
||||
name: z.string().nullable().optional(),
|
||||
lastViewedAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||
|
@ -107,7 +107,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
name: z.string().trim().optional(),
|
||||
description: z.string().trim().optional(),
|
||||
permissions: z.any().array()
|
||||
permissions: z.any().array().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@ -101,7 +101,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
message: "Slug must be a valid"
|
||||
}),
|
||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions)
|
||||
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -120,7 +120,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
roleId: req.params.roleId,
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
|
||||
}
|
||||
});
|
||||
return { role };
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -106,6 +106,7 @@ export enum EventType {
|
||||
CREATE_ENVIRONMENT = "create-environment",
|
||||
UPDATE_ENVIRONMENT = "update-environment",
|
||||
DELETE_ENVIRONMENT = "delete-environment",
|
||||
GET_ENVIRONMENT = "get-environment",
|
||||
ADD_WORKSPACE_MEMBER = "add-workspace-member",
|
||||
ADD_BATCH_WORKSPACE_MEMBER = "add-workspace-members",
|
||||
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
|
||||
@ -831,6 +832,13 @@ interface CreateEnvironmentEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetEnvironmentEvent {
|
||||
type: EventType.GET_ENVIRONMENT;
|
||||
metadata: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateEnvironmentEvent {
|
||||
type: EventType.UPDATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
@ -1230,6 +1238,7 @@ export type Event =
|
||||
| UpdateIdentityOidcAuthEvent
|
||||
| GetIdentityOidcAuthEvent
|
||||
| CreateEnvironmentEvent
|
||||
| GetEnvironmentEvent
|
||||
| UpdateEnvironmentEvent
|
||||
| DeleteEnvironmentEvent
|
||||
| AddWorkspaceMemberEvent
|
||||
|
@ -336,31 +336,36 @@ export const removeUsersFromGroupByUserIds = async ({
|
||||
)
|
||||
);
|
||||
|
||||
// TODO: this part can be optimized
|
||||
for await (const userId of userIds) {
|
||||
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(userId, group.id, projectIds, tx);
|
||||
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
|
||||
const promises: Array<Promise<void>> = [];
|
||||
for (const userId of userIds) {
|
||||
promises.push(
|
||||
(async () => {
|
||||
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(userId, group.id, projectIds, tx);
|
||||
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
|
||||
|
||||
if (projectsToDeleteKeyFor.length) {
|
||||
await projectKeyDAL.delete(
|
||||
{
|
||||
receiverId: userId,
|
||||
$in: {
|
||||
projectId: projectsToDeleteKeyFor
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
if (projectsToDeleteKeyFor.length) {
|
||||
await projectKeyDAL.delete(
|
||||
{
|
||||
receiverId: userId,
|
||||
$in: {
|
||||
projectId: projectsToDeleteKeyFor
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
await userGroupMembershipDAL.delete(
|
||||
{
|
||||
groupId: group.id,
|
||||
userId
|
||||
},
|
||||
tx
|
||||
await userGroupMembershipDAL.delete(
|
||||
{
|
||||
groupId: group.id,
|
||||
userId
|
||||
},
|
||||
tx
|
||||
);
|
||||
})()
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
if (membersToRemoveFromGroupPending.length) {
|
||||
|
@ -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: [
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -39,6 +39,7 @@ export type TGenerateSecretApprovalRequestDTO = {
|
||||
|
||||
export type TMergeSecretApprovalRequestDTO = {
|
||||
approvalId: string;
|
||||
bypassReason?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TStatusChangeDTO = {
|
||||
|
@ -425,6 +425,21 @@ export const PROJECTS = {
|
||||
},
|
||||
LIST_INTEGRATION_AUTHORIZATION: {
|
||||
workspaceId: "The ID of the project to list integration auths for."
|
||||
},
|
||||
LIST_CAS: {
|
||||
slug: "The slug of the project to list CAs for.",
|
||||
status: "The status of the CA to filter by.",
|
||||
friendlyName: "The friendly name of the CA to filter by.",
|
||||
commonName: "The common name of the CA to filter by.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th CA.",
|
||||
limit: "The number of CAs to return."
|
||||
},
|
||||
LIST_CERTIFICATES: {
|
||||
slug: "The slug of the project to list certificates for.",
|
||||
friendlyName: "The friendly name of the certificate to filter by.",
|
||||
commonName: "The common name of the certificate to filter by.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th certificate.",
|
||||
limit: "The number of certificates to return."
|
||||
}
|
||||
} as const;
|
||||
|
||||
@ -510,6 +525,10 @@ export const ENVIRONMENTS = {
|
||||
DELETE: {
|
||||
workspaceId: "The ID of the project to delete the environment from.",
|
||||
id: "The ID of the environment to delete."
|
||||
},
|
||||
GET: {
|
||||
workspaceId: "The ID of the project the environment belongs to.",
|
||||
id: "The ID of the environment to fetch."
|
||||
}
|
||||
} as const;
|
||||
|
||||
|
@ -47,3 +47,8 @@ export enum EnforcementLevel {
|
||||
Hard = "hard",
|
||||
Soft = "soft"
|
||||
}
|
||||
|
||||
export enum SecretSharingAccessType {
|
||||
Anyone = "anyone",
|
||||
Organization = "organization"
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -9,6 +9,55 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/environments/:envId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get Environment",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim().describe(ENVIRONMENTS.GET.workspaceId),
|
||||
envId: z.string().trim().describe(ENVIRONMENTS.GET.id)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
environment: ProjectEnvironmentsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const environment = await server.services.projectEnv.getEnvironmentById({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId: req.params.workspaceId,
|
||||
id: req.params.envId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: environment.projectId,
|
||||
event: {
|
||||
type: EventType.GET_ENVIRONMENT,
|
||||
metadata: {
|
||||
id: environment.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { environment };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:workspaceId/environments",
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSharingSchema } from "@app/db/schemas";
|
||||
import { SecretSharingAccessType } from "@app/lib/types";
|
||||
import {
|
||||
publicEndpointLimit,
|
||||
publicSecretShareCreationLimit,
|
||||
@ -18,21 +19,31 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
offset: z.coerce.number().min(0).max(100).default(0),
|
||||
limit: z.coerce.number().min(1).max(100).default(25)
|
||||
}),
|
||||
response: {
|
||||
200: z.array(SecretSharingSchema)
|
||||
200: z.object({
|
||||
secrets: z.array(SecretSharingSchema),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const sharedSecrets = await req.server.services.secretSharing.getSharedSecrets({
|
||||
const { secrets, totalCount } = await req.server.services.secretSharing.getSharedSecrets({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
|
||||
return sharedSecrets;
|
||||
return {
|
||||
secrets,
|
||||
totalCount
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@ -47,7 +58,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
hashedHex: z.string()
|
||||
hashedHex: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: SecretSharingSchema.pick({
|
||||
@ -55,22 +66,28 @@ 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
|
||||
);
|
||||
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById({
|
||||
sharedSecretId: req.params.id,
|
||||
hashedHex: req.query.hashedHex,
|
||||
orgId: req.permission?.orgId
|
||||
});
|
||||
if (!sharedSecret) return undefined;
|
||||
return {
|
||||
encryptedValue: sharedSecret.encryptedValue,
|
||||
iv: sharedSecret.iv,
|
||||
tag: sharedSecret.tag,
|
||||
expiresAt: sharedSecret.expiresAt,
|
||||
expiresAfterViews: sharedSecret.expiresAfterViews
|
||||
expiresAfterViews: sharedSecret.expiresAfterViews,
|
||||
accessType: sharedSecret.accessType,
|
||||
orgName: sharedSecret.orgName
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -84,11 +101,11 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
schema: {
|
||||
body: z.object({
|
||||
encryptedValue: z.string(),
|
||||
hashedHex: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
hashedHex: z.string(),
|
||||
expiresAt: z.string(),
|
||||
expiresAfterViews: z.number()
|
||||
expiresAfterViews: z.number().min(1).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -97,14 +114,9 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
|
||||
const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({
|
||||
encryptedValue,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt: new Date(expiresAt),
|
||||
expiresAfterViews
|
||||
...req.body,
|
||||
accessType: SecretSharingAccessType.Anyone
|
||||
});
|
||||
return { id: sharedSecret.id };
|
||||
}
|
||||
@ -118,12 +130,14 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
name: z.string().max(50).optional(),
|
||||
encryptedValue: z.string(),
|
||||
hashedHex: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
hashedHex: z.string(),
|
||||
expiresAt: z.string(),
|
||||
expiresAfterViews: z.number()
|
||||
expiresAfterViews: z.number().min(1).optional(),
|
||||
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -133,19 +147,13 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
|
||||
const sharedSecret = await req.server.services.secretSharing.createSharedSecret({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
encryptedValue,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt: new Date(expiresAt),
|
||||
expiresAfterViews
|
||||
...req.body
|
||||
});
|
||||
return { id: sharedSecret.id };
|
||||
}
|
||||
|
@ -317,10 +317,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: slugSchema.describe("The slug of the project to list CAs.")
|
||||
slug: slugSchema.describe(PROJECTS.LIST_CAS.slug)
|
||||
}),
|
||||
querystring: z.object({
|
||||
status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional()
|
||||
status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional().describe(PROJECTS.LIST_CAS.status),
|
||||
friendlyName: z.string().optional().describe(PROJECTS.LIST_CAS.friendlyName),
|
||||
commonName: z.string().optional().describe(PROJECTS.LIST_CAS.commonName),
|
||||
offset: z.coerce.number().min(0).max(100).default(0).describe(PROJECTS.LIST_CAS.offset),
|
||||
limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CAS.limit)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -336,11 +340,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
orgId: req.permission.orgId,
|
||||
type: ProjectFilterType.SLUG
|
||||
},
|
||||
status: req.query.status,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type
|
||||
actor: req.permission.type,
|
||||
...req.query
|
||||
});
|
||||
return { cas };
|
||||
}
|
||||
@ -354,11 +358,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: slugSchema.describe("The slug of the project to list certificates.")
|
||||
slug: slugSchema.describe(PROJECTS.LIST_CERTIFICATES.slug)
|
||||
}),
|
||||
querystring: z.object({
|
||||
offset: z.coerce.number().min(0).max(100).default(0),
|
||||
limit: z.coerce.number().min(1).max(100).default(25)
|
||||
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
|
||||
commonName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.commonName),
|
||||
offset: z.coerce.number().min(0).max(100).default(0).describe(PROJECTS.LIST_CERTIFICATES.offset),
|
||||
limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CERTIFICATES.limit)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@ -8,19 +8,35 @@ export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>;
|
||||
export const certificateDALFactory = (db: TDbClient) => {
|
||||
const certificateOrm = ormify(db, TableName.Certificate);
|
||||
|
||||
const countCertificatesInProject = async (projectId: string) => {
|
||||
const countCertificatesInProject = async ({
|
||||
projectId,
|
||||
friendlyName,
|
||||
commonName
|
||||
}: {
|
||||
projectId: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
}) => {
|
||||
try {
|
||||
interface CountResult {
|
||||
count: string;
|
||||
}
|
||||
|
||||
const count = await db
|
||||
let query = db
|
||||
.replicaNode()(TableName.Certificate)
|
||||
.join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`)
|
||||
.join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`)
|
||||
.where(`${TableName.Project}.id`, projectId)
|
||||
.count("*")
|
||||
.first();
|
||||
.where(`${TableName.Project}.id`, projectId);
|
||||
|
||||
if (friendlyName) {
|
||||
query = query.andWhere(`${TableName.Certificate}.friendlyName`, friendlyName);
|
||||
}
|
||||
|
||||
if (commonName) {
|
||||
query = query.andWhere(`${TableName.Certificate}.commonName`, commonName);
|
||||
}
|
||||
|
||||
const count = await query.count("*").first();
|
||||
|
||||
return parseInt((count as unknown as CountResult).count || "0", 10);
|
||||
} catch (error) {
|
||||
|
@ -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 });
|
||||
|
||||
|
@ -24,10 +24,15 @@ export const projectEnvDALFactory = (db: TDbClient) => {
|
||||
// we are using postion based sorting as its a small list
|
||||
// this will return the last value of the position in a folder with secret imports
|
||||
const findLastEnvPosition = async (projectId: string, tx?: Knex) => {
|
||||
// acquire update lock on project environments.
|
||||
// this ensures that concurrent invocations will wait and execute sequentially
|
||||
await (tx || db)(TableName.Environment).where({ projectId }).forUpdate();
|
||||
|
||||
const lastPos = await (tx || db)(TableName.Environment)
|
||||
.where({ projectId })
|
||||
.max("position", { as: "position" })
|
||||
.first();
|
||||
|
||||
return lastPos?.position || 0;
|
||||
};
|
||||
|
||||
|
@ -3,12 +3,12 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TProjectEnvDALFactory } from "./project-env-dal";
|
||||
import { TCreateEnvDTO, TDeleteEnvDTO, TUpdateEnvDTO } from "./project-env-types";
|
||||
import { TCreateEnvDTO, TDeleteEnvDTO, TGetEnvDTO, TUpdateEnvDTO } from "./project-env-types";
|
||||
|
||||
type TProjectEnvServiceFactoryDep = {
|
||||
projectEnvDAL: TProjectEnvDALFactory;
|
||||
@ -139,9 +139,35 @@ export const projectEnvServiceFactory = ({
|
||||
return env;
|
||||
};
|
||||
|
||||
const getEnvironmentById = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TGetEnvDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
|
||||
|
||||
const [env] = await projectEnvDAL.find({
|
||||
id,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!env) {
|
||||
throw new NotFoundError({
|
||||
message: "Environment does not exist"
|
||||
});
|
||||
}
|
||||
|
||||
return env;
|
||||
};
|
||||
|
||||
return {
|
||||
createEnvironment,
|
||||
updateEnvironment,
|
||||
deleteEnvironment
|
||||
deleteEnvironment,
|
||||
getEnvironmentById
|
||||
};
|
||||
};
|
||||
|
@ -20,3 +20,7 @@ export type TReorderEnvDTO = {
|
||||
id: string;
|
||||
pos: number;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetEnvDTO = {
|
||||
id: string;
|
||||
} & TProjectPermission;
|
||||
|
@ -162,12 +162,19 @@ export const projectRoleServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
|
||||
|
||||
if (data?.slug) {
|
||||
const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId });
|
||||
if (existingRole && existingRole.id !== roleId)
|
||||
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
|
||||
}
|
||||
const [updatedRole] = await projectRoleDAL.update({ id: roleId, projectId }, data);
|
||||
const [updatedRole] = await projectRoleDAL.update(
|
||||
{ id: roleId, projectId },
|
||||
{
|
||||
...data,
|
||||
permissions: data.permissions ? data.permissions : undefined
|
||||
}
|
||||
);
|
||||
if (!updatedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
|
||||
return { ...updatedRole, permissions: unpackPermissions(updatedRole.permissions) };
|
||||
};
|
||||
|
@ -575,6 +575,10 @@ export const projectServiceFactory = ({
|
||||
*/
|
||||
const listProjectCas = async ({
|
||||
status,
|
||||
friendlyName,
|
||||
commonName,
|
||||
limit = 25,
|
||||
offset = 0,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
@ -596,10 +600,15 @@ export const projectServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const cas = await certificateAuthorityDAL.find({
|
||||
projectId: project.id,
|
||||
...(status && { status })
|
||||
});
|
||||
const cas = await certificateAuthorityDAL.find(
|
||||
{
|
||||
projectId: project.id,
|
||||
...(status && { status }),
|
||||
...(friendlyName && { friendlyName }),
|
||||
...(commonName && { commonName })
|
||||
},
|
||||
{ offset, limit, sort: [["updatedAt", "desc"]] }
|
||||
);
|
||||
|
||||
return cas;
|
||||
};
|
||||
@ -608,8 +617,10 @@ export const projectServiceFactory = ({
|
||||
* Return list of certificates for project
|
||||
*/
|
||||
const listProjectCertificates = async ({
|
||||
offset,
|
||||
limit,
|
||||
limit = 25,
|
||||
offset = 0,
|
||||
friendlyName,
|
||||
commonName,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
@ -634,12 +645,18 @@ export const projectServiceFactory = ({
|
||||
{
|
||||
$in: {
|
||||
caId: cas.map((ca) => ca.id)
|
||||
}
|
||||
},
|
||||
...(friendlyName && { friendlyName }),
|
||||
...(commonName && { commonName })
|
||||
},
|
||||
{ offset, limit, sort: [["updatedAt", "desc"]] }
|
||||
);
|
||||
|
||||
const count = await certificateDAL.countCertificatesInProject(project.id);
|
||||
const count = await certificateDAL.countCertificatesInProject({
|
||||
projectId: project.id,
|
||||
friendlyName,
|
||||
commonName
|
||||
});
|
||||
|
||||
return {
|
||||
certificates,
|
||||
|
@ -89,6 +89,10 @@ export type AddUserToWsDTO = {
|
||||
|
||||
export type TListProjectCasDTO = {
|
||||
status?: CaStatus;
|
||||
friendlyName?: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
commonName?: string;
|
||||
filter: Filter;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
@ -96,4 +100,6 @@ export type TListProjectCertsDTO = {
|
||||
filter: Filter;
|
||||
offset: number;
|
||||
limit: number;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@ -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 || [];
|
||||
|
||||
|
@ -10,6 +10,25 @@ export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory
|
||||
export const secretSharingDALFactory = (db: TDbClient) => {
|
||||
const sharedSecretOrm = ormify(db, TableName.SecretSharing);
|
||||
|
||||
const countAllUserOrgSharedSecrets = async ({ orgId, userId }: { orgId: string; userId: string }) => {
|
||||
try {
|
||||
interface CountResult {
|
||||
count: string;
|
||||
}
|
||||
|
||||
const count = await db
|
||||
.replicaNode()(TableName.SecretSharing)
|
||||
.where(`${TableName.SecretSharing}.orgId`, orgId)
|
||||
.where(`${TableName.SecretSharing}.userId`, userId)
|
||||
.count("*")
|
||||
.first();
|
||||
|
||||
return parseInt((count as unknown as CountResult).count || "0", 10);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Count all user-org shared secrets" });
|
||||
}
|
||||
};
|
||||
|
||||
const pruneExpiredSharedSecrets = async (tx?: Knex) => {
|
||||
try {
|
||||
const today = new Date();
|
||||
@ -19,8 +38,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
||||
.update({
|
||||
encryptedValue: "",
|
||||
tag: "",
|
||||
iv: "",
|
||||
hashedHex: ""
|
||||
iv: ""
|
||||
});
|
||||
return docs;
|
||||
} catch (error) {
|
||||
@ -50,8 +68,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
||||
await sharedSecretOrm.updateById(id, {
|
||||
encryptedValue: "",
|
||||
iv: "",
|
||||
tag: "",
|
||||
hashedHex: ""
|
||||
tag: ""
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({
|
||||
@ -63,6 +80,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
||||
|
||||
return {
|
||||
...sharedSecretOrm,
|
||||
countAllUserOrgSharedSecrets,
|
||||
pruneExpiredSharedSecrets,
|
||||
softDeleteById,
|
||||
findActiveSharedSecrets
|
||||
|
@ -1,39 +1,45 @@
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, 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,
|
||||
TCreateSharedSecretDTO,
|
||||
TDeleteSharedSecretDTO,
|
||||
TSharedSecretPermission
|
||||
TGetActiveSharedSecretByIdDTO,
|
||||
TGetSharedSecretsDTO
|
||||
} from "./secret-sharing-types";
|
||||
|
||||
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 {
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
encryptedValue,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt,
|
||||
expiresAfterViews
|
||||
} = createSharedSecretInput;
|
||||
const createSharedSecret = async ({
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
encryptedValue,
|
||||
hashedHex,
|
||||
iv,
|
||||
tag,
|
||||
name,
|
||||
accessType,
|
||||
expiresAt,
|
||||
expiresAfterViews
|
||||
}: TCreateSharedSecretDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
||||
|
||||
@ -55,20 +61,30 @@ export const secretSharingServiceFactory = ({
|
||||
}
|
||||
|
||||
const newSharedSecret = await secretSharingDAL.create({
|
||||
name,
|
||||
encryptedValue,
|
||||
hashedHex,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt,
|
||||
expiresAt: new Date(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 createPublicSharedSecret = async ({
|
||||
encryptedValue,
|
||||
hashedHex,
|
||||
iv,
|
||||
tag,
|
||||
expiresAt,
|
||||
expiresAfterViews,
|
||||
accessType
|
||||
}: TCreatePublicSharedSecretDTO) => {
|
||||
if (new Date(expiresAt) < new Date()) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||
}
|
||||
@ -88,37 +104,103 @@ export const secretSharingServiceFactory = ({
|
||||
|
||||
const newSharedSecret = await secretSharingDAL.create({
|
||||
encryptedValue,
|
||||
hashedHex,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt,
|
||||
expiresAfterViews
|
||||
expiresAt: new Date(expiresAt),
|
||||
expiresAfterViews,
|
||||
accessType
|
||||
});
|
||||
return { id: newSharedSecret.id };
|
||||
};
|
||||
|
||||
const getSharedSecrets = async (getSharedSecretsInput: TSharedSecretPermission) => {
|
||||
const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const getSharedSecrets = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
offset,
|
||||
limit
|
||||
}: TGetSharedSecretsDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
||||
const userSharedSecrets = await secretSharingDAL.findActiveSharedSecrets({ userId: actorId, orgId });
|
||||
return userSharedSecrets;
|
||||
|
||||
const secrets = await secretSharingDAL.find(
|
||||
{
|
||||
userId: actorId,
|
||||
orgId: actorOrgId
|
||||
},
|
||||
{ offset, limit, sort: [["createdAt", "desc"]] }
|
||||
);
|
||||
|
||||
const count = await secretSharingDAL.countAllUserOrgSharedSecrets({
|
||||
orgId: actorOrgId,
|
||||
userId: actorId
|
||||
});
|
||||
|
||||
return {
|
||||
secrets,
|
||||
totalCount: count
|
||||
};
|
||||
};
|
||||
|
||||
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => {
|
||||
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
|
||||
if (!sharedSecret) return;
|
||||
if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) {
|
||||
return;
|
||||
const getActiveSharedSecretById = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => {
|
||||
const sharedSecret = await secretSharingDAL.findOne({
|
||||
id: sharedSecretId,
|
||||
hashedHex
|
||||
});
|
||||
if (!sharedSecret)
|
||||
throw new NotFoundError({
|
||||
message: "Shared secret not found"
|
||||
});
|
||||
|
||||
const { accessType, expiresAt, expiresAfterViews } = sharedSecret;
|
||||
|
||||
const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : "";
|
||||
|
||||
if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId)
|
||||
throw new UnauthorizedError();
|
||||
|
||||
if (expiresAt !== null && expiresAt < new Date()) {
|
||||
// check lifetime expiry
|
||||
await secretSharingDAL.softDeleteById(sharedSecretId);
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: Secret has expired by lifetime"
|
||||
});
|
||||
}
|
||||
if (sharedSecret.expiresAfterViews != null && sharedSecret.expiresAfterViews >= 0) {
|
||||
if (sharedSecret.expiresAfterViews === 0) {
|
||||
await secretSharingDAL.softDeleteById(sharedSecretId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (expiresAfterViews !== null && expiresAfterViews === 0) {
|
||||
// check view count expiry
|
||||
await secretSharingDAL.softDeleteById(sharedSecretId);
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: Secret has expired by view count"
|
||||
});
|
||||
}
|
||||
|
||||
if (expiresAfterViews) {
|
||||
// decrement view count if view count expiry set
|
||||
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
|
||||
}
|
||||
return sharedSecret;
|
||||
|
||||
await secretSharingDAL.updateById(sharedSecretId, {
|
||||
lastViewedAt: new Date()
|
||||
});
|
||||
|
||||
return {
|
||||
...sharedSecret,
|
||||
orgName:
|
||||
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
|
||||
? orgName
|
||||
: undefined
|
||||
};
|
||||
};
|
||||
|
||||
const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => {
|
||||
@ -134,6 +216,6 @@ export const secretSharingServiceFactory = ({
|
||||
createPublicSharedSecret,
|
||||
getSharedSecrets,
|
||||
deleteSharedSecretById,
|
||||
getActiveSharedSecretByIdAndHashedHex
|
||||
getActiveSharedSecretById
|
||||
};
|
||||
};
|
||||
|
@ -1,20 +1,36 @@
|
||||
import { SecretSharingAccessType, TGenericPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
|
||||
export type TGetSharedSecretsDTO = {
|
||||
offset: number;
|
||||
limit: number;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TSharedSecretPermission = {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
orgId: string;
|
||||
accessType?: SecretSharingAccessType;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type TCreatePublicSharedSecretDTO = {
|
||||
encryptedValue: string;
|
||||
hashedHex: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
expiresAt: string;
|
||||
expiresAfterViews?: number;
|
||||
accessType: SecretSharingAccessType;
|
||||
};
|
||||
|
||||
export type TGetActiveSharedSecretByIdDTO = {
|
||||
sharedSecretId: string;
|
||||
hashedHex: string;
|
||||
expiresAt: Date;
|
||||
expiresAfterViews: number;
|
||||
orgId?: string;
|
||||
};
|
||||
|
||||
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
||||
|
@ -5,6 +5,7 @@ import handlebars from "handlebars";
|
||||
import { createTransport } from "nodemailer";
|
||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
export type TSmtpConfig = SMTPTransport.Options;
|
||||
@ -12,7 +13,7 @@ export type TSmtpSendMail = {
|
||||
template: SmtpTemplates;
|
||||
subjectLine: string;
|
||||
recipients: string[];
|
||||
substitutions: unknown;
|
||||
substitutions: object;
|
||||
};
|
||||
export type TSmtpService = ReturnType<typeof smtpServiceFactory>;
|
||||
|
||||
@ -23,6 +24,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",
|
||||
@ -46,9 +48,11 @@ export const smtpServiceFactory = (cfg: TSmtpConfig) => {
|
||||
const isSmtpOn = Boolean(cfg.host);
|
||||
|
||||
const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => {
|
||||
const appCfg = getConfig();
|
||||
const html = await fs.readFile(path.resolve(__dirname, "./templates/", template), "utf8");
|
||||
const temp = handlebars.compile(html);
|
||||
const htmlToSend = temp(substitutions);
|
||||
const htmlToSend = temp({ isCloud: appCfg.isCloud, siteUrl: appCfg.SITE_URL, ...substitutions });
|
||||
|
||||
if (isSmtpOn) {
|
||||
await smtp.sendMail({
|
||||
from: cfg.from,
|
||||
|
@ -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>
|
@ -1,19 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>MFA Code</title>
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<h2>Sign in attempt requires further verification</h2>
|
||||
<p>Your MFA code is below — enter it where you started signing in to Infisical.</p>
|
||||
<h2>{{code}}</h2>
|
||||
<p>The MFA code will be valid for 2 minutes.</p>
|
||||
<p>Not you? Contact Infisical or your administrator immediately.</p>
|
||||
</body>
|
||||
<p>Not you? Contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} immediately.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -9,12 +9,12 @@
|
||||
|
||||
<body>
|
||||
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo</h3>
|
||||
<p><a href="https://app.infisical.com/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||
|
||||
<p>If these are production secrets, please rotate them immediately.</p>
|
||||
|
||||
<p>Once you have taken action, be sure to update the status of the risk in your <a
|
||||
href="https://app.infisical.com/">Infisical
|
||||
href="{{siteUrl}}">Infisical
|
||||
dashboard</a>.</p>
|
||||
</body>
|
||||
|
||||
|
@ -1,19 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Successful login for {{email}} from new device</title>
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<p>We're verifying a recent login for {{email}}:</p>
|
||||
<p><strong>Timestamp</strong>: {{timestamp}}</p>
|
||||
<p><strong>IP address</strong>: {{ip}}</p>
|
||||
<p><strong>User agent</strong>: {{userAgent}}</p>
|
||||
<p>If you believe that this login is suspicious, please contact Infisical or reset your password immediately.</p>
|
||||
</body>
|
||||
<p>If you believe that this login is suspicious, please contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} or reset your password immediately.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -9,6 +9,6 @@
|
||||
<h2>Reset your password</h2>
|
||||
<p>Someone requested a password reset.</p>
|
||||
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>
|
||||
<p>If you didn't initiate this request, please contact us immediately at team@infisical.com</p>
|
||||
<p>If you didn't initiate this request, please contact {{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}</p>
|
||||
</body>
|
||||
</html>
|
@ -9,7 +9,7 @@
|
||||
|
||||
<body>
|
||||
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
|
||||
<p><a href="https://app.infisical.com/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||
<p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed
|
||||
by {{pusher_name}} ({{pusher_email}}). If
|
||||
these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as comment
|
||||
@ -18,7 +18,7 @@
|
||||
<p>If these are production secrets, please rotate them immediately.</p>
|
||||
|
||||
<p>Once you have taken action, be sure to update the status of the risk in your <a
|
||||
href="https://app.infisical.com/">Infisical
|
||||
href="{{siteUrl}}">Infisical
|
||||
dashboard</a>.</p>
|
||||
</body>
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
<h2>Confirm your email address</h2>
|
||||
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
|
||||
<h1>{{code}}</h1>
|
||||
<p>Questions about setting up Infisical? Email us at support@infisical.com</p>
|
||||
<p>Questions about setting up Infisical? {{#if isCloud}}Email us at support@infisical.com{{else}}Contact your administrator{{/if}}.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
42
cli/go.mod
42
cli/go.mod
@ -4,36 +4,36 @@ 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/infisical/go-sdk v0.3.3
|
||||
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
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/term v0.20.0
|
||||
golang.org/x/crypto v0.25.0
|
||||
golang.org/x/term v0.22.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.5.1 // indirect
|
||||
cloud.google.com/go/auth v0.7.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.4.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.11 // indirect
|
||||
github.com/alessio/shellescape v1.4.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.27.2 // indirect
|
||||
@ -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
|
||||
@ -64,17 +64,17 @@ require (
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
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
|
||||
@ -91,17 +91,17 @@ require (
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/net v0.27.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/text v0.15.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/api v0.183.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
|
||||
google.golang.org/grpc v1.64.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
google.golang.org/api v0.188.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect
|
||||
google.golang.org/grpc v1.64.1 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
79
cli/go.sum
79
cli/go.sum
@ -18,8 +18,8 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW
|
||||
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
|
||||
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
|
||||
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
|
||||
cloud.google.com/go/auth v0.5.1 h1:0QNO7VThG54LUzKiQxv8C6x1YX7lUrzlAa1nVLF8CIw=
|
||||
cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s=
|
||||
cloud.google.com/go/auth v0.7.0 h1:kf/x9B3WTbBUHkC+1VS8wwwli9TzhSt0vSTVBmMR8Ts=
|
||||
cloud.google.com/go/auth v0.7.0/go.mod h1:D+WqdrpcjmiCgWrXmLLxOVq1GACoE36chW6KXoEvuIw=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
@ -28,13 +28,13 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/compute/metadata v0.4.0 h1:vHzJCWaM4g8XIcm8kopr3XmDA4Gy/lblD3EhhSux05c=
|
||||
cloud.google.com/go/compute/metadata v0.4.0/go.mod h1:SIQh1Kkb4ZJ8zJ874fqVkslA29PRXuleyj6vOzlbK7M=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
|
||||
cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
|
||||
cloud.google.com/go/iam v1.1.11 h1:0mQ8UKSfdHLut6pH9FM3bI55KWR46ketn0PuXleDyxw=
|
||||
cloud.google.com/go/iam v1.1.11/go.mod h1:biXoiLWYIKntto2joP+62sd9uW5EpkZmKIvfNcTWlnQ=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
@ -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=
|
||||
@ -231,8 +233,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
|
||||
github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
@ -263,8 +265,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/infisical/go-sdk v0.3.0 h1:Ls71t227F4CWVQWdStcwv8WDyfHe8eRlyAuMRNHsmlQ=
|
||||
github.com/infisical/go-sdk v0.3.0/go.mod h1:vHTDVw3k+wfStXab513TGk1n53kaKF2xgLqpw/xvtl4=
|
||||
github.com/infisical/go-sdk v0.3.3 h1:TE2WNMmiDej+TCkPKgHk3h8zVlEQUtM5rz8ouVnXTcU=
|
||||
github.com/infisical/go-sdk v0.3.3/go.mod h1:6fWzAwTPIoKU49mQ2Oxu+aFnJu9n7k2JcNrZjzhHM2M=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@ -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=
|
||||
@ -455,8 +453,9 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -536,8 +535,9 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -612,24 +612,26 @@ 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=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -642,8 +644,9 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@ -729,8 +732,8 @@ google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjR
|
||||
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
|
||||
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
|
||||
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
|
||||
google.golang.org/api v0.183.0 h1:PNMeRDwo1pJdgNcFQ9GstuLe/noWKIc89pRWRLMvLwE=
|
||||
google.golang.org/api v0.183.0/go.mod h1:q43adC5/pHoSZTx5h2mSmdF7NcyfW9JuDyIOJAgS9ZQ=
|
||||
google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw=
|
||||
google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@ -779,10 +782,10 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e h1:SkdGTrROJl2jRGT/Fxv5QUf9jtdKCQh4KQJXbXVLAi0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e/go.mod h1:LweJcLbyVij6rCex8YunD8DYR5VDonap/jYl3ZRxcIU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b h1:04+jVzTs2XBnOZcPsLnmrTGqltqJbZQ1Ey26hjYdQQ0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@ -803,8 +806,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
||||
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
|
||||
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@ -817,8 +820,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
}
|
||||
@ -550,7 +550,7 @@ func (tm *AgentManager) FetchAzureAuthAccessToken() (credential infisicalSdk.Mac
|
||||
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get identity id: %v", err)
|
||||
}
|
||||
|
||||
return tm.infisicalClient.Auth().AzureAuthLogin(identityId)
|
||||
return tm.infisicalClient.Auth().AzureAuthLogin(identityId, "")
|
||||
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,6 @@ import (
|
||||
"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"
|
||||
@ -84,7 +83,7 @@ func handleAzureAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.Infis
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return infisicalClient.Auth().AzureAuthLogin(identityId)
|
||||
return infisicalClient.Auth().AzureAuthLogin(identityId, "")
|
||||
}
|
||||
|
||||
func handleGcpIdTokenAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
@ -205,6 +204,7 @@ var loginCmd = &cobra.Command{
|
||||
if !overrideDomain {
|
||||
domainQuery = false
|
||||
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL_MANUAL_OVERRIDE)
|
||||
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", strings.TrimSuffix(config.INFISICAL_URL, "/api"))
|
||||
}
|
||||
|
||||
}
|
||||
@ -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(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()))
|
||||
@ -761,23 +808,22 @@ func browserCliLogin() (models.UserCredentials, error) {
|
||||
log.Debug().Msgf("Callback server listening on port %d", callbackPort)
|
||||
|
||||
go http.Serve(listener, corsHandler)
|
||||
go askToPasteJwtToken(success, failure)
|
||||
|
||||
for {
|
||||
select {
|
||||
case loginResponse := <-success:
|
||||
_ = closeListener(&listener)
|
||||
fmt.Println("Browser login successful")
|
||||
return loginResponse, nil
|
||||
|
||||
case <-failure:
|
||||
err = closeListener(&listener)
|
||||
return models.UserCredentials{}, err
|
||||
case err := <-failure:
|
||||
serverErr := closeListener(&listener)
|
||||
return models.UserCredentials{}, errors.Join(err, serverErr)
|
||||
|
||||
case <-timeout:
|
||||
_ = closeListener(&listener)
|
||||
return models.UserCredentials{}, errors.New("server timeout")
|
||||
|
||||
case <-quit:
|
||||
return models.UserCredentials{}, errors.New("quitting browser login, defaulting to cli...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -33,6 +33,8 @@ const (
|
||||
|
||||
SERVICE_TOKEN_IDENTIFIER = "service-token"
|
||||
UNIVERSAL_AUTH_TOKEN_IDENTIFIER = "universal-auth-token"
|
||||
|
||||
INFISICAL_BACKUP_SECRET = "infisical-backup-secrets"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v2/workspace/{slug}/cas"
|
||||
---
|
4
docs/api-reference/endpoints/certificates/list.mdx
Normal file
4
docs/api-reference/endpoints/certificates/list.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v2/workspace/{slug}/certificates"
|
||||
---
|
@ -4,6 +4,30 @@ title: "Changelog"
|
||||
|
||||
The changelog below reflects new product developments and updates on a monthly basis.
|
||||
|
||||
## July 2024
|
||||
- Released the official [Ruby SDK](https://infisical.com/docs/sdks/languages/ruby).
|
||||
- Increased the speed and efficiency of secret operations.
|
||||
- Released AWS KMS wrapping (bring your own key).
|
||||
- Users can now log in to CLI via SSO in non-browser environments.
|
||||
- Released [Slack Webhooks](https://infisical.com/docs/documentation/platform/webhooks).
|
||||
- Added [Dynamic Secrets with MS SQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/mssql).
|
||||
- Redesigned and simplified the Machine Identities page.
|
||||
- Added the ability to move secrets/folders to another location.
|
||||
- Added [OIDC](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general) support to CLI, Go SDK, and more.
|
||||
- Released [Linux installer for Infisical](https://infisical.com/docs/self-hosting/deployment-options/native/standalone-binary).
|
||||
|
||||
## June 2024
|
||||
- Released [Infisical PKI](https://infisical.com/docs/documentation/platform/pki/overview).
|
||||
- Released the official [Go SDK](https://infisical.com/docs/sdks/languages/go).
|
||||
- Released [OIDC Authentication method](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general).
|
||||
- Allowed users to configure log retention periods on self-hosted instances.
|
||||
- Added [tags](https://registry.terraform.io/providers/Infisical/infisical/latest/docs/resources/secret_tag) to terraform provider.
|
||||
- Released [public secret sharing](https://share.infisical.com).
|
||||
- Built a [native integration with Rundeck](https://infisical.com/docs/integrations/cicd/rundeck).
|
||||
- Added list view for projects in the dashboard.
|
||||
- Fixed offline coding mode in CLI.
|
||||
- Users are now able to leave a particular project themselves.
|
||||
|
||||
## May 2024
|
||||
- Released [AWS](https://infisical.com/docs/documentation/platform/identities/aws-auth), [GCP](https://infisical.com/docs/documentation/platform/identities/gcp-auth), [Azure](https://infisical.com/docs/documentation/platform/identities/azure-auth), and [Kubernetes](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth) Native Auth Methods.
|
||||
- Added [Secret Sharing](https://infisical.com/docs/documentation/platform/secret-sharing) functionality for sharing sensitive data through encrypted links – within and outside of an organization.
|
||||
|
23
docs/documentation/guides/migrating-from-envkey.mdx
Normal file
23
docs/documentation/guides/migrating-from-envkey.mdx
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
title: "Migrating from EnvKey to Infisical"
|
||||
sidebarTitle: "Migration"
|
||||
description: "Learn how to migrate from EnvKey to Infisical in the easiest way possible."
|
||||
---
|
||||
|
||||
## What is Infisical?
|
||||
|
||||
[Infisical](https://infisical.com) is an open-source all-in-one secret management platform that helps developers manage secrets (e.g., API-keys, DB access tokens, [certificates](https://infisical.com/docs/documentation/platform/pki/overview)) across their infrastructure. In addition, Infisical provides [secret sharing](https://infisical.com/docs/documentation/platform/secret-sharing) functionality, ability to [prevent secret leaks](https://infisical.com/docs/cli/scanning-overview), and more.
|
||||
|
||||
Infisical is used by 10,000+ organizations across all indsutries including First American Financial Corporation, Deivery Hero, and [Hugging Face](https://infisical.com/customers/hugging-face).
|
||||
|
||||
## Migrating from EnvKey
|
||||
|
||||
To facilitate customer transition from EnvKey to Infisical, we have been working closely with the EnvKey team to provide a simple migration path for all EnvKey customers.
|
||||
|
||||
## Automated migration
|
||||
|
||||
Our team is currently working on creating an automated migration process that would include secrets, policies, and other important resources. If you are interested in that, please [reach out to our team](mailto:support@infisical.com) with any questions.
|
||||
|
||||
## Talk to our team
|
||||
|
||||
To make the migration process even more seamless, you can [schedule a meeting with our team](https://infisical.cal.com/vlad/migration-from-envkey-to-infisical) to learn more about how Infisical compares to EnvKey and discuss unique needs of your organization. You are also welcome to email us at [support@infisical.com](mailto:support@infisical.com) to ask any questions or get any technical help.
|
@ -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.
|
||||

|
||||

|
||||
|
||||
@ -14,9 +14,14 @@ This functionality works in the following way:
|
||||

|
||||

|
||||
|
||||
3. An eligible approver can approve or reject the access request.
|
||||

|
||||
4. An eligible approver can approve or reject the access request.
|
||||
{/*  */}
|
||||

|
||||
|
||||
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.
|
||||

|
||||
|
||||
|
@ -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).
|
||||
|
||||

|
||||
|
||||
### 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)**.
|
||||
|
||||

|
||||
|
||||
### 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).
|
||||
|
||||

|
||||
|
||||
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)).
|
||||
|
||||

|
@ -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.
|
||||
|
||||

|
||||
|
||||
|
Binary file not shown.
After ![]() (image error) Size: 297 KiB |
BIN
docs/images/platform/access-controls/access-request-bypass.png
Normal file
BIN
docs/images/platform/access-controls/access-request-bypass.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 47 KiB |
Binary file not shown.
Before ![]() (image error) Size: 79 KiB After ![]() (image error) Size: 56 KiB ![]() ![]() |
Binary file not shown.
Before ![]() (image error) Size: 114 KiB After ![]() (image error) Size: 43 KiB ![]() ![]() |
BIN
docs/images/platform/pr-workflows/create-change-policy.png
Normal file
BIN
docs/images/platform/pr-workflows/create-change-policy.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 43 KiB |
Binary file not shown.
Before ![]() (image error) Size: 130 KiB After ![]() (image error) Size: 55 KiB ![]() ![]() |
Binary file not shown.
Before ![]() (image error) Size: 106 KiB After ![]() (image error) Size: 39 KiB ![]() ![]() |
BIN
docs/images/self-hosting/deployment-options/native/ha-stack.png
Normal file
BIN
docs/images/self-hosting/deployment-options/native/ha-stack.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 67 KiB |
Binary file not shown.
After ![]() (image error) Size: 330 KiB |
@ -15,7 +15,7 @@ Prerequisites:
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Copy your [Account ID](https://developers.cloudflare.com/fundamentals/get-started/basic-tasks/find-account-and-zone-ids/) from Account > Workers & Pages > Overview
|
||||
|
||||
@ -35,10 +35,12 @@ Prerequisites:
|
||||
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||
the cloud platform.
|
||||
</Info>
|
||||
|
||||
</Step>
|
||||
<Step title="Start integration">
|
||||
Select which Infisical environment secrets you want to sync to Cloudflare Workers and press create integration to start syncing secrets.
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
</Steps>
|
||||
|
@ -217,7 +217,7 @@
|
||||
"pages": [
|
||||
"self-hosting/overview",
|
||||
{
|
||||
"group": "Installation methods",
|
||||
"group": "Containerized installation methods",
|
||||
"pages": [
|
||||
"self-hosting/deployment-options/standalone-infisical",
|
||||
"self-hosting/deployment-options/docker-swarm",
|
||||
@ -406,6 +406,7 @@
|
||||
"sdks/languages/node",
|
||||
"sdks/languages/python",
|
||||
"sdks/languages/go",
|
||||
"sdks/languages/ruby",
|
||||
"sdks/languages/java",
|
||||
"sdks/languages/csharp"
|
||||
]
|
||||
@ -653,6 +654,7 @@
|
||||
{
|
||||
"group": "Certificate Authorities",
|
||||
"pages": [
|
||||
"api-reference/endpoints/certificate-authorities/list",
|
||||
"api-reference/endpoints/certificate-authorities/create",
|
||||
"api-reference/endpoints/certificate-authorities/read",
|
||||
"api-reference/endpoints/certificate-authorities/update",
|
||||
@ -668,6 +670,7 @@
|
||||
{
|
||||
"group": "Certificates",
|
||||
"pages": [
|
||||
"api-reference/endpoints/certificates/list",
|
||||
"api-reference/endpoints/certificates/read",
|
||||
"api-reference/endpoints/certificates/revoke",
|
||||
"api-reference/endpoints/certificates/delete",
|
||||
|
436
docs/sdks/languages/ruby.mdx
Normal file
436
docs/sdks/languages/ruby.mdx
Normal file
@ -0,0 +1,436 @@
|
||||
---
|
||||
title: "Infisical Ruby SDK"
|
||||
sidebarTitle: "Ruby"
|
||||
icon: "diamond"
|
||||
---
|
||||
|
||||
|
||||
|
||||
If you're working with Ruby , the official [Infisical Ruby SDK](https://github.com/infisical/sdk) package is the easiest way to fetch and work with secrets for your application.
|
||||
|
||||
- [Ruby Package](https://rubygems.org/gems/infisical-sdk)
|
||||
- [Github Repository](https://github.com/infisical/sdk)
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```ruby
|
||||
require 'infisical-sdk'
|
||||
|
||||
# 1. Create the Infisical client
|
||||
infisical = InfisicalSDK::InfisicalClient.new('https://app.infisical.com')
|
||||
|
||||
infisical.auth.universal_auth(client_id: 'YOUR_CLIENT_ID', client_secret: 'YOUR_CLIENT_SECRET')
|
||||
|
||||
test_secret = infisical.secrets.get(
|
||||
secret_name: 'API_KEY',
|
||||
project_id: 'project-id',
|
||||
environment: 'dev'
|
||||
)
|
||||
puts "Secret: #{single_test_secret}"
|
||||
```
|
||||
|
||||
This example demonstrates how to use the Infisical Ruby SDK in a simple Ruby application. The application retrieves a secret named `API_KEY` from the `dev` environment of the `YOUR_PROJECT_ID` project.
|
||||
|
||||
<Warning>
|
||||
We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best.
|
||||
</Warning>
|
||||
|
||||
# Installation
|
||||
|
||||
```console
|
||||
$ gem install infisical-sdk
|
||||
```
|
||||
# Configuration
|
||||
|
||||
Import the SDK and create a client instance.
|
||||
|
||||
```ruby
|
||||
infisical = InfisicalSDK::InfisicalClient.new('https://app.infisical.com') # Optional parameter, default is https://api.infisical.com
|
||||
```
|
||||
|
||||
### Client parameters
|
||||
|
||||
<ParamField query="options" type="object">
|
||||
<Expandable title="properties">
|
||||
<ParamField query="Site URL" type="string" optional>
|
||||
The URL of the Infisical API. Default is `https://api.infisical.com`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="Cache TTL" type="string" required>
|
||||
How long the client should cache secrets for. Default is 5 minutes. Disable by setting to 0.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
### Authentication
|
||||
|
||||
The SDK supports a variety of authentication methods. The most common authentication method is Universal Auth, which uses a client ID and client secret to authenticate.
|
||||
|
||||
#### Universal Auth
|
||||
|
||||
**Using environment variables**
|
||||
|
||||
Call `auth.universal_auth()` with empty arguments to use the following environment variables:
|
||||
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` - Your machine identity client ID.
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret.
|
||||
|
||||
**Using the SDK directly**
|
||||
```ruby
|
||||
infisical.auth.universal_auth(client_id: 'your-client-id', client_secret: 'your-client-secret')
|
||||
```
|
||||
|
||||
#### GCP ID Token Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Google Cloud Platform.
|
||||
Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
|
||||
Call `.auth.gcp_id_token_auth()` with empty arguments to use the following environment variables:
|
||||
|
||||
- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```ruby
|
||||
infisical.auth.gcp_id_token_auth(identity_id: 'MACHINE_IDENTITY_ID')
|
||||
```
|
||||
|
||||
#### GCP IAM Auth
|
||||
|
||||
**Using environment variables**
|
||||
|
||||
Call `.auth.gcp_iam_auth()` with empty arguments to use the following environment variables:
|
||||
|
||||
- `INFISICAL_GCP_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file.
|
||||
|
||||
**Using the SDK directly**
|
||||
```ruby
|
||||
infisical.auth.gcp_iam_auth(identity_id: 'MACHINE_IDENTITY_ID', service_account_key_file_path: 'SERVICE_ACCOUNT_KEY_FILE_PATH')
|
||||
```
|
||||
|
||||
#### AWS IAM Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on AWS.
|
||||
Please [read more](/documentation/platform/identities/aws-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
|
||||
Call `.auth.aws_iam_auth()` with empty arguments to use the following environment variables:
|
||||
|
||||
- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```ruby
|
||||
infisical.auth.aws_iam_auth(identity_id: 'MACHINE_IDENTITY_ID')
|
||||
```
|
||||
|
||||
|
||||
#### Azure Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Azure.
|
||||
Please [read more](/documentation/platform/identities/azure-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
|
||||
Call `.auth.azure_auth()` with empty arguments to use the following environment variables:
|
||||
|
||||
- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```ruby
|
||||
infisical.auth.azure_auth(identity_id: 'MACHINE_IDENTITY_ID')
|
||||
```
|
||||
|
||||
#### Kubernetes Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Kubernetes.
|
||||
Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
|
||||
Call `.auth.kubernetes_auth()` with empty arguments to use the following environment variables:
|
||||
|
||||
- `INFISICAL_KUBERNETES_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`.
|
||||
|
||||
**Using the SDK directly**
|
||||
```ruby
|
||||
# Service account token path will default to /var/run/secrets/kubernetes.io/serviceaccount/token if empty value is passed
|
||||
infisical.auth.kubernetes_auth(identity_id: 'MACHINE_IDENTITY_ID', service_account_token_path: nil)
|
||||
```
|
||||
|
||||
## Working with Secrets
|
||||
|
||||
### client.secrets.list(options)
|
||||
|
||||
```ruby
|
||||
secrets = infisical.secrets.list(
|
||||
project_id: 'PROJECT_ID',
|
||||
environment: 'dev',
|
||||
path: '/foo/bar',
|
||||
)
|
||||
```
|
||||
|
||||
Retrieve all secrets within the Infisical project and environment that client is connected to
|
||||
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object">
|
||||
<Expandable title="properties">
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="project_id" type="string">
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="attach_to_process_env" type="boolean" default="false" optional>
|
||||
Whether or not to set the fetched secrets to the process environment. If true, you can access the secrets like so `System.getenv("SECRET_NAME")`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="include_imports" type="boolean" default="false" optional>
|
||||
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="recursive" type="boolean" default="false" optional>
|
||||
Whether or not to fetch secrets recursively from the specified path. Please note that there's a 20-depth limit for recursive fetching.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="expand_secret_references" type="boolean" default="true" optional>
|
||||
Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
|
||||
### client.secrets.get(options)
|
||||
|
||||
```ruby
|
||||
secret = infisical.secrets.get(
|
||||
secret_name: 'API_KEY',
|
||||
project_id: project_id,
|
||||
environment: env_slug
|
||||
)
|
||||
```
|
||||
|
||||
Retrieve a secret from Infisical.
|
||||
|
||||
By default, `Secrets().Retrieve()` fetches and returns a shared secret.
|
||||
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to retrieve.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
### client.secrets.create(options)
|
||||
|
||||
```ruby
|
||||
new_secret = infisical.secrets.create(
|
||||
secret_name: 'NEW_SECRET',
|
||||
secret_value: 'SECRET_VALUE',
|
||||
project_id: 'PROJECT_ID',
|
||||
environment: 'dev',
|
||||
)
|
||||
```
|
||||
|
||||
Create a new secret in Infisical.
|
||||
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to create.
|
||||
</ParamField>
|
||||
<ParamField query="secret_value" type="string" required>
|
||||
The value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="secret_comment" type="string" optional>
|
||||
A comment for the secret.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be created.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
### client.secrets.update(options)
|
||||
|
||||
```ruby
|
||||
updated_secret = infisical.secrets.update(
|
||||
secret_name: 'SECRET_KEY_TO_UPDATE',
|
||||
secret_value: 'NEW_SECRET_VALUE',
|
||||
project_id: 'PROJECT_ID',
|
||||
environment: 'dev',
|
||||
)
|
||||
```
|
||||
|
||||
Update an existing secret in Infisical.
|
||||
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="secret_value" type="string" required>
|
||||
The new value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="skip_multiline_encoding" type="boolean" default="false" optional>
|
||||
Whether or not to skip multiline encoding for the new secret value.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be updated.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
### client.secrets.delete(options)
|
||||
|
||||
```ruby
|
||||
deleted_secret = infisical.secrets.delete(
|
||||
secret_name: 'SECRET_TO_DELETE',
|
||||
project_id: 'PROJECT_ID',
|
||||
environment: 'dev',
|
||||
)
|
||||
```
|
||||
|
||||
Delete a secret in Infisical.
|
||||
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string">
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be deleted.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
|
||||
## Cryptography
|
||||
|
||||
### Create a symmetric key
|
||||
|
||||
Create a base64-encoded, 256-bit symmetric key to be used for encryption/decryption.
|
||||
|
||||
```ruby
|
||||
key = infisical.cryptography.create_symmetric_key
|
||||
```
|
||||
|
||||
#### Returns (string)
|
||||
`key` (string): A base64-encoded, 256-bit symmetric key, that can be used for encryption/decryption purposes.
|
||||
|
||||
### Encrypt symmetric
|
||||
```ruby
|
||||
encrypted_data = infisical.cryptography.encrypt_symmetric(data: "Hello World!", key: key)
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" required>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="data" type="string">
|
||||
The plaintext you want to encrypt.
|
||||
</ParamField>
|
||||
<ParamField query="key" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
#### Returns (object)
|
||||
`tag` (string): A base64-encoded, 128-bit authentication tag.
|
||||
`iv` (string): A base64-encoded, 96-bit initialization vector.
|
||||
`ciphertext` (string): A base64-encoded, encrypted ciphertext.
|
||||
|
||||
|
||||
### Decrypt symmetric
|
||||
```ruby
|
||||
decrypted_data = infisical.cryptography.decrypt_symmetric(
|
||||
ciphertext: encrypted_data['ciphertext'],
|
||||
iv: encrypted_data['iv'],
|
||||
tag: encrypted_data['tag'],
|
||||
key: key
|
||||
)
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
<ParamField query="Parameters" type="object" required>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="ciphertext" type="string">
|
||||
The ciphertext you want to decrypt.
|
||||
</ParamField>
|
||||
<ParamField query="key" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
<ParamField query="iv" type="string" required>
|
||||
The initialization vector to use for decryption.
|
||||
</ParamField>
|
||||
<ParamField query="tag" type="string" required>
|
||||
The authentication tag to use for decryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
#### Returns (string)
|
||||
`Plaintext` (string): The decrypted plaintext.
|
@ -25,6 +25,10 @@ From local development to production, Infisical SDKs provide the easiest way for
|
||||
<Card href="/sdks/languages/csharp" title="C#" icon="bars" color="#368833">
|
||||
Manage secrets for your C#/.NET application on demand
|
||||
</Card>
|
||||
|
||||
<Card href="/sdks/languages/ruby" title="Ruby" icon="diamond" color="#367B99">
|
||||
Manage secrets for your Ruby application on demand
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## FAQ
|
||||
|
@ -25,6 +25,10 @@ Used to configure platform-specific security and operational settings
|
||||
https://app.infisical.com).
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="PORT" type="int" default="8080" optional>
|
||||
Specifies the internal port on which the application listens.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional>
|
||||
Telemetry helps us improve Infisical but if you want to dsiable it you may set this to `false`.
|
||||
</ParamField>
|
||||
|
@ -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>
|
||||
|
||||

|
||||
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
|
||||

|
||||
|
||||
|
||||
## 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>
|
@ -0,0 +1,202 @@
|
||||
---
|
||||
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 end to end deployment process, and will take care of everything like databases, redis deployment, web serving, and availability.
|
||||
- [Automated Deployment with high availability (HA)](/self-hosting/deployment-options/native/high-availability)
|
||||
|
||||
|
||||
## 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>
|
@ -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> */}
|
||||
|
@ -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
|
||||
)}
|
||||
>
|
||||
|
@ -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,
|
||||
|
@ -29,6 +29,7 @@ export * from "./secretFolders";
|
||||
export * from "./secretImports";
|
||||
export * from "./secretRotation";
|
||||
export * from "./secrets";
|
||||
export * from "./secretSharing";
|
||||
export * from "./secretSnapshots";
|
||||
export * from "./serverDetails";
|
||||
export * from "./serviceTokens";
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
TDeleteOrgRoleDTO,
|
||||
TDeleteProjectRoleDTO,
|
||||
TOrgRole,
|
||||
TProjectRole,
|
||||
TUpdateOrgRoleDTO,
|
||||
TUpdateProjectRoleDTO
|
||||
} from "./types";
|
||||
@ -17,9 +18,13 @@ import {
|
||||
export const useCreateProjectRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ projectSlug, ...dto }: TCreateProjectRoleDTO) =>
|
||||
apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto),
|
||||
return useMutation<TProjectRole, {}, TCreateProjectRoleDTO>({
|
||||
mutationFn: async ({ projectSlug, ...dto }: TCreateProjectRoleDTO) => {
|
||||
const {
|
||||
data: { role }
|
||||
} = await apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto);
|
||||
return role;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
|
||||
}
|
||||
@ -29,9 +34,13 @@ export const useCreateProjectRole = () => {
|
||||
export const useUpdateProjectRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) =>
|
||||
apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto),
|
||||
return useMutation<TProjectRole, {}, TUpdateProjectRoleDTO>({
|
||||
mutationFn: async ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) => {
|
||||
const {
|
||||
data: { role }
|
||||
} = await apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto);
|
||||
return role;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
|
||||
}
|
||||
@ -40,10 +49,13 @@ export const useUpdateProjectRole = () => {
|
||||
|
||||
export const useDeleteProjectRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ projectSlug, id }: TDeleteProjectRoleDTO) =>
|
||||
apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`),
|
||||
return useMutation<TProjectRole, {}, TDeleteProjectRoleDTO>({
|
||||
mutationFn: async ({ projectSlug, id }: TDeleteProjectRoleDTO) => {
|
||||
const {
|
||||
data: { role }
|
||||
} = await apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`);
|
||||
return role;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
|
||||
}
|
||||
@ -79,7 +91,7 @@ export const useUpdateOrgRole = () => {
|
||||
data: { role }
|
||||
} = await apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, {
|
||||
...dto,
|
||||
permissions: permissions?.length ? packRules(permissions) : []
|
||||
permissions: permissions?.length ? packRules(permissions) : undefined
|
||||
});
|
||||
|
||||
return role;
|
||||
|
@ -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 }) => {
|
||||
|
@ -133,4 +133,5 @@ export type TUpdateSecretApprovalRequestStatusDTO = {
|
||||
export type TPerformSecretApprovalRequestMerge = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
bypassReason?: string;
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { secretSharingKeys } from "./queries";
|
||||
import { TCreateSharedSecretRequest, TDeleteSharedSecretRequest, TSharedSecret } from "./types";
|
||||
|
||||
export const useCreateSharedSecret = () => {
|
||||
@ -11,7 +12,7 @@ export const useCreateSharedSecret = () => {
|
||||
const { data } = await apiRequest.post<TSharedSecret>("/api/v1/secret-sharing", inputData);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries(["sharedSecrets"])
|
||||
onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets())
|
||||
});
|
||||
};
|
||||
|
||||
@ -25,7 +26,7 @@ export const useCreatePublicSharedSecret = () => {
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries(["sharedSecrets"])
|
||||
onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets())
|
||||
});
|
||||
};
|
||||
|
||||
@ -38,8 +39,6 @@ export const useDeleteSharedSecret = () => {
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["sharedSecrets"]);
|
||||
}
|
||||
onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets())
|
||||
});
|
||||
};
|
||||
|
@ -4,27 +4,64 @@ import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TSharedSecret, TViewSharedSecretResponse } from "./types";
|
||||
|
||||
export const useGetSharedSecrets = () => {
|
||||
export const secretSharingKeys = {
|
||||
allSharedSecrets: () => ["sharedSecrets"] as const,
|
||||
specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) =>
|
||||
[...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const
|
||||
};
|
||||
|
||||
export const useGetSharedSecrets = ({
|
||||
offset = 0,
|
||||
limit = 25
|
||||
}: {
|
||||
offset: number;
|
||||
limit: number;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: ["sharedSecrets"],
|
||||
queryKey: secretSharingKeys.specificSharedSecrets({ offset, limit }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TSharedSecret[]>("/api/v1/secret-sharing/");
|
||||
const params = new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(limit)
|
||||
});
|
||||
|
||||
const { data } = await apiRequest.get<{ secrets: TSharedSecret[]; totalCount: number }>(
|
||||
"/api/v1/secret-sharing/",
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => {
|
||||
export const useGetActiveSharedSecretById = ({
|
||||
sharedSecretId,
|
||||
hashedHex
|
||||
}: {
|
||||
sharedSecretId: string;
|
||||
hashedHex: string;
|
||||
}) => {
|
||||
return useQuery<TViewSharedSecretResponse, [string]>({
|
||||
enabled: Boolean(sharedSecretId) && Boolean(hashedHex),
|
||||
queryFn: async () => {
|
||||
if(!id || !hashedHex) return Promise.resolve({ encryptedValue: "", iv: "", tag: "" });
|
||||
const params = new URLSearchParams({
|
||||
hashedHex
|
||||
});
|
||||
|
||||
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
|
||||
`/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}`
|
||||
`/api/v1/secret-sharing/public/${sharedSecretId}`,
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
return {
|
||||
encryptedValue: data.encryptedValue,
|
||||
iv: data.iv,
|
||||
tag: data.tag,
|
||||
accessType: data.accessType,
|
||||
orgName: data.orgName
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -4,23 +4,39 @@ export type TSharedSecret = {
|
||||
orgId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
} & TCreateSharedSecretRequest;
|
||||
|
||||
export type TCreateSharedSecretRequest = {
|
||||
name: string | null;
|
||||
lastViewedAt?: Date;
|
||||
expiresAt: Date;
|
||||
expiresAfterViews: number | null;
|
||||
encryptedValue: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
};
|
||||
|
||||
export type TCreateSharedSecretRequest = {
|
||||
name?: string;
|
||||
encryptedValue: string;
|
||||
hashedHex: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
expiresAt: Date;
|
||||
expiresAfterViews: number;
|
||||
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"
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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'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'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've successfully logged in to the Infisical CLI
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user