Compare commits

..

92 Commits

Author SHA1 Message Date
40e5c6ef66 Merge pull request #1945 from Infisical/daniel/scim-fix
Fix: SCIM Groups find by filter
2024-06-11 10:42:24 -07:00
6c95e75d0d Merge pull request #1952 from akhilmhdh/feat/fix-signup
fix: resolved signup failing in cloud
2024-06-11 11:36:53 -04:00
=
d6c9e6db75 fix: resolved signup failing in cloud 2024-06-11 21:03:06 +05:30
76f87a7708 Merge pull request #1951 from akhilmhdh/fix/password-state-stuck
fix: stuck on password step resolved
2024-06-11 20:09:51 +05:30
366f03080d Merge pull request #1757 from Infisical/fix/resolved-cli-offline-mode-get
fix: resolved cli offline mode get
2024-06-11 22:32:59 +08:00
dfdd8e95f9 misc: renamed connection check method 2024-06-11 22:14:19 +08:00
=
c4797ea060 fix: stuck on password step resolved 2024-06-11 19:09:12 +05:30
6e011a0b52 Merge pull request #1950 from Infisical/fix/resolved-import-override-display-1
feat: handled import override in the API layer
2024-06-11 17:41:43 +08:00
05ed00834a misc: used set 2024-06-11 17:14:24 +08:00
38b0edf510 fix: addressed lint issue 2024-06-11 17:07:24 +08:00
56b9506b39 fix: type fix 2024-06-11 16:48:16 +08:00
ae34e015db fix: added missing required property 2024-06-11 16:43:51 +08:00
7c42768cd8 feat: handled import overwrite in the API layer 2024-06-11 16:27:12 +08:00
b4a9e0e62d Merge pull request #1948 from Infisical/fix/resolved-import-override-display
fix: resolved import override behavior
2024-06-11 15:55:19 +08:00
30606093f4 Merge pull request #1949 from Infisical/streamline-smtp
Update SMTP configuration
2024-06-11 00:32:50 -07:00
16862a3b33 Fix lint issue 2024-06-11 00:01:58 -07:00
e800a455c4 Update SMTP config 2024-06-10 23:45:40 -07:00
ba0de6afcf fix: resolved import override behavior 2024-06-11 14:04:59 +08:00
bfc82105bd Merge pull request #1947 from Infisical/patch-multi-line-encoding
Patch multi line encoding when expandSecretRef is enabled
2024-06-11 00:55:31 -04:00
00fd44b33a fix type issues 2024-06-11 00:34:54 -04:00
e2550d70b5 make skipMultilineEncoding null|undefined|bool 2024-06-11 00:28:55 -04:00
163d33509b Patch multi line encoding when expandSecretRef is enabled
By default when you create a secret, it will have multi line encoding off but we actually treat this as true in the backend and UI. User’s aren’t expecting their multi line secrets to be double quoted by and made into a single line with/n, however we are doing it by default at the moment. This PR makes multi line encoding opt in and not opt out
2024-06-10 23:42:07 -04:00
c8a3252c1a Merge pull request #1929 from Infisical/feat/add-option-to-mask-and-protect-gitlab-secrets
feat: add integration option to mask and protect gitlab secrets
2024-06-11 11:38:18 +08:00
0bba1801b9 Merge pull request #1936 from Infisical/daniel/go-sdk-docs
Docs: Go SDK
2024-06-11 04:27:17 +02:00
a61e92c49c Update go.mdx 2024-06-11 04:26:35 +02:00
9945d249d6 Merge pull request #1937 from Infisical/shubham/eng-487-investigate-the-stripedb-inconsistency
fix: update org seats whenever membership status is accepted
2024-06-11 07:05:27 +05:30
8bc9a5efcd Fix: SCIM Groups find by filter 2024-06-11 00:00:16 +02:00
b31d2be3f3 Merge pull request #1931 from minuchi/fix/preserve-annotations-when-updating
fix: preserve existing annotations when updating managed secret
2024-06-10 17:14:29 -04:00
8329cbf299 update toggle lable 2024-06-10 17:09:20 -04:00
9138ab8ed7 update flag describe 2024-06-10 17:01:16 -04:00
ea517bc199 Merge pull request #1941 from belikedeep/docker-compose-command-update
Fix: Updated docker compose command (from compose V1 to compose v2)
2024-06-10 16:46:10 -04:00
a82b813553 describe isReplication flag 2024-06-10 16:20:29 -04:00
cf9169ad6f test: resolved test issues 2024-06-11 03:37:51 +08:00
af03f706ba Merge pull request #1943 from Infisical/create-pull-request/patch-1718043322
GH Action: rename new migration file timestamp
2024-06-10 14:15:55 -04:00
9cf5bbc5d5 chore: renamed new migration files to latest timestamp (gh-action) 2024-06-10 18:15:21 +00:00
9161dd5e13 Merge pull request #1925 from Infisical/feat/add-captcha
feat: added captcha to password login
2024-06-10 14:14:57 -04:00
69b76aea64 misc: added secrets folder path to backup scoping 2024-06-11 01:45:34 +08:00
c9a95023be Revert "adjustment: moved backup logic to cmd layer"
This reverts commit 8fc4fd64f8.
2024-06-11 01:03:26 +08:00
9db5be1c91 Revert "adjustment: moved secret backup logic to cmd layer"
This reverts commit 920b9a7dfa.
2024-06-11 00:58:36 +08:00
a1b41ca454 Revert "feature: added offline support for infisical export"
This reverts commit 88a4fb84e6.
2024-06-11 00:52:59 +08:00
6c252b4bfb misc: revert backup flow modification for run.go 2024-06-11 00:49:57 +08:00
aafddaa856 misc: finalized option label 2024-06-10 23:08:39 +08:00
776f464bee misc: used metadata schema parsing 2024-06-10 22:45:17 +08:00
104b0d6c60 Merge remote-tracking branch 'origin/main' into feat/add-option-to-mask-and-protect-gitlab-secrets 2024-06-10 22:41:44 +08:00
9303124f5f Updated docker compose command (from compose V1 to compose v2) 2024-06-10 20:09:08 +05:30
03c9a5606b Merge pull request #1928 from Infisical/feat/add-option-for-delete-disabling-github-integ
feat: added option for disabling github secret deletion
2024-06-10 22:35:04 +08:00
120e482c6f Merge remote-tracking branch 'origin/main' into fix/resolved-cli-offline-mode-get 2024-06-10 14:15:18 +08:00
1abdb531d9 misc: removed comments 2024-06-10 13:36:53 +08:00
59b3123eb3 adjustment: removed unintended yaml updates 2024-06-10 13:33:42 +08:00
c1954a6386 Merge branch 'feat/add-captcha' of https://github.com/Infisical/infisical into feat/add-captcha 2024-06-10 13:27:31 +08:00
0bbb86ee2a misc: simplified captcha flag and finalized build process 2024-06-10 13:24:44 +08:00
7c9c65312b fix: pass correct id 2024-06-10 09:04:34 +05:30
8a46cbd08f fix: update org seats whenever membership status is accepted 2024-06-10 09:02:11 +05:30
fa05639592 Docs: Go SDK 2024-06-10 05:18:39 +02:00
429b2a284d Merge branch 'main' into feat/add-captcha 2024-06-09 17:44:55 -04:00
6c596092b0 Merge pull request #1927 from Infisical/shubham/eng-983-optimise-secretinput-usage-to-mask-secret-when-not-in-focus
fix: share secret input now masks value onBlur
2024-06-09 17:43:30 -04:00
fcd13eac8a update saml org slug environment 2024-06-09 14:41:23 -04:00
1fb653754c update saml slug env 2024-06-09 14:37:13 -04:00
bb1d73b0f5 Merge pull request #1935 from Infisical/fix-saml-auto-redirect
patch saml auto redirect
2024-06-09 23:09:29 +05:30
59e9226d85 patch saml auto redirect 2024-06-09 13:30:44 -04:00
e6f42e1231 Merge pull request #1933 from Infisical/add-folder-sorting
added sorting for folders in overview
2024-06-08 22:31:07 -04:00
06e7a90a44 added sorting for folders in overview 2024-06-08 22:26:49 -04:00
f075ff23a9 patch encoding type for kms 2024-06-08 18:38:25 -04:00
6d40d951c6 fix: preserve existing annotations when updating managed secret 2024-06-09 01:43:41 +09:00
e5b7ebbabf revert: change in core component 2024-06-08 05:47:47 +05:30
610dd07a57 misc: updated failed password attempt limit for captcha 2024-06-08 00:39:14 +08:00
9d6d7540dc misc: removed unnecessary project property 2024-06-08 00:34:33 +08:00
faa1572faf update docs grammar 2024-06-07 12:04:21 -04:00
d288bcbd74 explain how auto reload works in docs 2024-06-07 12:01:59 -04:00
af1d30a49a Merge pull request #1926 from Infisical/misc/added-auto-redeploy-cf-pages-option
misc: added auto-redeploy option for cf pages integration
2024-06-07 09:58:06 -04:00
2bd9ad0137 feat: add option to maks and protect gitlab secrets 2024-06-07 21:46:34 +08:00
9d46c269d4 fix: secret input on tab moves to next field and masks value 2024-06-07 13:05:04 +05:30
cd92ce627c misc: added autoredeploy option for cf pages integration 2024-06-07 13:23:46 +08:00
15c05b4910 misc: finalized captcha error message 2024-06-06 21:54:35 +08:00
65d88ef08e misc: improved ux by requiring captcha entry before submission 2024-06-06 21:24:25 +08:00
81e4129e51 feat: added base captcha implementation 2024-06-06 20:42:54 +08:00
88a4fb84e6 feature: added offline support for infisical export 2024-05-02 03:21:20 +08:00
a1e8f45a86 misc: added new cli secrets to release build gh action 2024-05-02 01:35:19 +08:00
04dca9432d misc: updated test comment 2024-05-02 01:09:12 +08:00
920b9a7dfa adjustment: moved secret backup logic to cmd layer 2024-05-02 00:59:17 +08:00
8fc4fd64f8 adjustment: moved backup logic to cmd layer 2024-05-02 00:49:29 +08:00
24f7ecc548 misc: removed infisical init logs 2024-05-01 21:41:07 +08:00
a5ca96f2df test: restructed setup and added scripting for infisical init 2024-05-01 21:39:20 +08:00
505ccdf8ea misc: added script for cli-tests env setup 2024-05-01 21:37:18 +08:00
3897bd70fa adjustment: removed cli display for pty 2024-05-01 11:08:58 +08:00
4479e626c7 adjustment: renamed cli vault file phrase env 2024-05-01 01:56:10 +08:00
6640b55504 misc: added envs required for cli test of infisical login 2024-05-01 01:49:06 +08:00
85f024c814 test: added scripting for user login 2024-05-01 01:45:24 +08:00
531fa634a2 feature: add logs for cli execution error 2024-04-30 22:02:22 +08:00
772dd464f5 test: added integration test for secrets get all and secrets get all without connection 2024-04-30 21:11:29 +08:00
877b9a409e adjustment: modified isConnected check to query linked infisical URL 2024-04-30 21:00:34 +08:00
104a91647c fix: resolved cli offline mode get 2024-04-29 21:18:13 +08:00
71 changed files with 1252 additions and 273 deletions

View File

@ -63,3 +63,7 @@ CLIENT_SECRET_GITHUB_LOGIN=
CLIENT_ID_GITLAB_LOGIN=
CLIENT_SECRET_GITLAB_LOGIN=
CAPTCHA_SECRET=
NEXT_PUBLIC_CAPTCHA_SITE_KEY=

View File

@ -22,6 +22,9 @@ jobs:
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
goreleaser:
runs-on: ubuntu-20.04

View File

@ -20,7 +20,12 @@ on:
required: true
CLI_TESTS_ENV_SLUG:
required: true
CLI_TESTS_USER_EMAIL:
required: true
CLI_TESTS_USER_PASSWORD:
required: true
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE:
required: true
jobs:
test:
defaults:
@ -43,5 +48,8 @@ jobs:
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
run: go test -v -count=1 ./test

View File

@ -1,7 +1,7 @@
ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
ARG INTERCOM_ID=intercom-id
ARG SAML_ORG_SLUG=saml-org-slug-default
ARG CAPTCHA_SITE_KEY=captcha-site-key
FROM node:20-alpine AS base
@ -36,8 +36,8 @@ ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ARG SAML_ORG_SLUG
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
# Build
RUN npm run build
@ -113,9 +113,9 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
ARG INTERCOM_ID=intercom-id
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
ARG SAML_ORG_SLUG
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \
BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
WORKDIR /

View File

@ -85,13 +85,13 @@ To set up and run Infisical locally, make sure you have Git and Docker installed
Linux/macOS:
```console
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker-compose -f docker-compose.prod.yml up
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker compose -f docker-compose.prod.yml up
```
Windows Command Prompt:
```console
git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker-compose -f docker-compose.prod.yml up
git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker compose -f docker-compose.prod.yml up
```
Create an account at `http://localhost:80`

View File

@ -0,0 +1,29 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasConsecutiveFailedPasswordAttempts = await knex.schema.hasColumn(
TableName.Users,
"consecutiveFailedPasswordAttempts"
);
await knex.schema.alterTable(TableName.Users, (tb) => {
if (!hasConsecutiveFailedPasswordAttempts) {
tb.integer("consecutiveFailedPasswordAttempts").defaultTo(0);
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasConsecutiveFailedPasswordAttempts = await knex.schema.hasColumn(
TableName.Users,
"consecutiveFailedPasswordAttempts"
);
await knex.schema.alterTable(TableName.Users, (tb) => {
if (hasConsecutiveFailedPasswordAttempts) {
tb.dropColumn("consecutiveFailedPasswordAttempts");
}
});
}

View File

@ -25,7 +25,8 @@ export const UsersSchema = z.object({
isEmailVerified: z.boolean().default(false).nullable().optional(),
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
isLocked: z.boolean().default(false).nullable().optional(),
temporaryLockDateEnd: z.date().nullable().optional()
temporaryLockDateEnd: z.date().nullable().optional(),
consecutiveFailedPasswordAttempts: z.number().default(0).nullable().optional()
});
export type TUsers = z.infer<typeof UsersSchema>;

View File

@ -362,6 +362,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
const groups = await req.server.services.scim.listScimGroups({
orgId: req.permission.orgId,
startIndex: req.query.startIndex,
filter: req.query.filter,
limit: req.query.count
});

View File

@ -77,7 +77,7 @@ type TLdapConfigServiceFactoryDep = {
>;
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
};
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
@ -510,6 +510,7 @@ export const ldapConfigServiceFactory = ({
return newUserAlias;
});
}
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const user = await userDAL.transaction(async (tx) => {
const newUser = await userDAL.findOne({ id: userAlias.userId }, tx);

View File

@ -50,7 +50,7 @@ type TSamlConfigServiceFactoryDep = {
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail">;
};
@ -449,6 +449,7 @@ export const samlConfigServiceFactory = ({
return newUser;
});
}
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const isUserCompleted = Boolean(user.isAccepted);
const providerAuthToken = jwt.sign(

View File

@ -18,6 +18,20 @@ export const buildScimUserList = ({
};
};
export const parseScimFilter = (filterToParse: string | undefined) => {
if (!filterToParse) return {};
const [parsedName, parsedValue] = filterToParse.split("eq").map((s) => s.trim());
let attributeName = parsedName;
if (parsedName === "userName") {
attributeName = "email";
} else if (parsedName === "displayName") {
attributeName = "name";
}
return { [attributeName]: parsedValue.replace(/"/g, "") };
};
export const buildScimUser = ({
orgMembershipId,
username,

View File

@ -30,7 +30,7 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList } from "./scim-fns";
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns";
import {
TCreateScimGroupDTO,
TCreateScimTokenDTO,
@ -184,18 +184,6 @@ export const scimServiceFactory = ({
status: 403
});
const parseFilter = (filterToParse: string | undefined) => {
if (!filterToParse) return {};
const [parsedName, parsedValue] = filterToParse.split("eq").map((s) => s.trim());
let attributeName = parsedName;
if (parsedName === "userName") {
attributeName = "email";
}
return { [attributeName]: parsedValue.replace(/"/g, "") };
};
const findOpts = {
...(startIndex && { offset: startIndex - 1 }),
...(limit && { limit })
@ -204,7 +192,7 @@ export const scimServiceFactory = ({
const users = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.orgId` as "id"]: orgId,
...parseFilter(filter)
...parseScimFilter(filter)
},
findOpts
);
@ -391,7 +379,7 @@ export const scimServiceFactory = ({
);
}
}
await licenseService.updateSubscriptionOrgMemberCount(org.id);
return { user, orgMembership };
});
@ -557,7 +545,7 @@ export const scimServiceFactory = ({
return {}; // intentionally return empty object upon success
};
const listScimGroups = async ({ orgId, startIndex, limit }: TListScimGroupsDTO) => {
const listScimGroups = async ({ orgId, startIndex, limit, filter }: TListScimGroupsDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
@ -580,7 +568,8 @@ export const scimServiceFactory = ({
const groups = await groupDAL.findGroups(
{
orgId
orgId,
...(filter && parseScimFilter(filter))
},
{
offset: startIndex - 1,

View File

@ -66,6 +66,7 @@ export type TDeleteScimUserDTO = {
export type TListScimGroupsDTO = {
startIndex: number;
filter?: string;
limit: number;
orgId: string;
};

View File

@ -386,6 +386,8 @@ export const SECRET_IMPORTS = {
environment: "The slug of the environment to import into.",
path: "The path to import into.",
workspaceId: "The ID of the project you are working in.",
isReplication:
"When true, secrets from the source will be automatically sent to the destination. If approval policies exist at the destination, the secrets will be sent as approval requests instead of being applied immediately.",
import: {
environment: "The slug of the environment to import from.",
path: "The path to import from."
@ -675,6 +677,8 @@ export const INTEGRATION = {
secretAWSTag: "The tags for AWS secrets.",
kmsKeyId: "The ID of the encryption key from AWS KMS.",
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.",
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.",
shouldEnableDelete: "The flag to enable deletion of secrets"
}
},

View File

@ -39,7 +39,9 @@ const envSchema = z
HTTPS_ENABLED: zodStrBool,
// smtp options
SMTP_HOST: zpStr(z.string().optional()),
SMTP_SECURE: zodStrBool,
SMTP_IGNORE_TLS: zodStrBool.default("false"),
SMTP_REQUIRE_TLS: zodStrBool.default("true"),
SMTP_TLS_REJECT_UNAUTHORIZED: zodStrBool.default("true"),
SMTP_PORT: z.coerce.number().default(587),
SMTP_USERNAME: zpStr(z.string().optional()),
SMTP_PASSWORD: zpStr(z.string().optional()),
@ -75,6 +77,7 @@ const envSchema = z
.optional()
.default(process.env.URL_GITLAB_LOGIN ?? GITLAB_URL)
), // fallback since URL_GITLAB_LOGIN has been renamed
DEFAULT_SAML_ORG_SLUG: zpStr(z.string().optional()).default(process.env.NEXT_PUBLIC_SAML_ORG_SLUG),
// integration client secrets
// heroku
CLIENT_ID_HEROKU: zpStr(z.string().optional()),
@ -119,7 +122,8 @@ const envSchema = z
.transform((val) => val === "true")
.optional(),
INFISICAL_CLOUD: zodStrBool.default("false"),
MAINTENANCE_MODE: zodStrBool.default("false")
MAINTENANCE_MODE: zodStrBool.default("false"),
CAPTCHA_SECRET: zpStr(z.string().optional())
})
.transform((data) => ({
...data,
@ -131,7 +135,8 @@ const envSchema = z
isSecretScanningConfigured:
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET)
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG
}));
let envCfg: Readonly<z.infer<typeof envSchema>>;
@ -150,13 +155,20 @@ export const initEnvConfig = (logger: Logger) => {
return envCfg;
};
export const formatSmtpConfig = () => ({
host: envCfg.SMTP_HOST,
port: envCfg.SMTP_PORT,
auth:
envCfg.SMTP_USERNAME && envCfg.SMTP_PASSWORD
? { user: envCfg.SMTP_USERNAME, pass: envCfg.SMTP_PASSWORD }
: undefined,
secure: envCfg.SMTP_SECURE,
from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>`
});
export const formatSmtpConfig = () => {
return {
host: envCfg.SMTP_HOST,
port: envCfg.SMTP_PORT,
auth:
envCfg.SMTP_USERNAME && envCfg.SMTP_PASSWORD
? { user: envCfg.SMTP_USERNAME, pass: envCfg.SMTP_PASSWORD }
: undefined,
secure: envCfg.SMTP_PORT === 465,
from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>`,
ignoreTLS: envCfg.SMTP_IGNORE_TLS,
requireTLS: envCfg.SMTP_REQUIRE_TLS,
tls: {
rejectUnauthorized: envCfg.SMTP_TLS_REJECT_UNAUTHORIZED
}
};
};

View File

@ -5,7 +5,6 @@ import { createTransport } from "nodemailer";
import { formatSmtpConfig, getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { getTlsOption } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
type BootstrapOpt = {
@ -44,7 +43,7 @@ export const bootstrapCheck = async ({ db }: BootstrapOpt) => {
console.info("Testing smtp connection");
const smtpCfg = formatSmtpConfig();
await createTransport({ ...smtpCfg, ...getTlsOption(smtpCfg.host, smtpCfg.secure) })
await createTransport(smtpCfg)
.verify()
.then(async () => {
console.info("SMTP successfully connected");

View File

@ -919,7 +919,8 @@ export const registerRoutes = async (
emailConfigured: z.boolean().optional(),
inviteOnlySignup: z.boolean().optional(),
redisConfigured: z.boolean().optional(),
secretScanningConfigured: z.boolean().optional()
secretScanningConfigured: z.boolean().optional(),
samlDefaultOrgSlug: z.string().optional()
})
}
},
@ -932,7 +933,8 @@ export const registerRoutes = async (
emailConfigured: cfg.isSmtpConfigured,
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
redisConfigured: cfg.isRedisConfigured,
secretScanningConfigured: cfg.isSecretScanningConfigured
secretScanningConfigured: cfg.isSecretScanningConfigured,
samlDefaultOrgSlug: cfg.samlDefaultOrgSlug
};
}
});

View File

@ -30,7 +30,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.import.environment),
path: z.string().trim().transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.import.path)
}),
isReplication: z.boolean().default(false)
isReplication: z.boolean().default(false).describe(SECRET_IMPORTS.CREATE.isReplication)
}),
response: {
200: z.object({

View File

@ -80,7 +80,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
body: z.object({
email: z.string().trim(),
providerAuthToken: z.string().trim().optional(),
clientProof: z.string().trim()
clientProof: z.string().trim(),
captchaToken: z.string().trim().optional()
}),
response: {
200: z.discriminatedUnion("mfaEnabled", [
@ -106,6 +107,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
const data = await server.services.login.loginExchangeClientProof({
captchaToken: req.body.captchaToken,
email: req.body.email,
ip: req.realIp,
userAgent,

View File

@ -3,6 +3,7 @@ import jwt from "jsonwebtoken";
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@ -176,12 +177,16 @@ export const authLoginServiceFactory = ({
clientProof,
ip,
userAgent,
providerAuthToken
providerAuthToken,
captchaToken
}: TLoginClientProofDTO) => {
const appCfg = getConfig();
const userEnc = await userDAL.findUserEncKeyByUsername({
username: email
});
if (!userEnc) throw new Error("Failed to find user");
const user = await userDAL.findById(userEnc.userId);
const cfg = getConfig();
let authMethod = AuthMethod.EMAIL;
@ -196,6 +201,31 @@ export const authLoginServiceFactory = ({
}
}
if (
user.consecutiveFailedPasswordAttempts &&
user.consecutiveFailedPasswordAttempts >= 10 &&
Boolean(appCfg.CAPTCHA_SECRET)
) {
if (!captchaToken) {
throw new BadRequestError({
name: "Captcha Required",
message: "Accomplish the required captcha by logging in via Web"
});
}
// validate captcha token
const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", {
response: captchaToken,
secret: appCfg.CAPTCHA_SECRET
});
if (!response.data.success) {
throw new BadRequestError({
name: "Invalid Captcha"
});
}
}
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
const isValidClientProof = await srpCheckClientProof(
userEnc.salt,
@ -204,15 +234,31 @@ export const authLoginServiceFactory = ({
userEnc.clientPublicKey,
clientProof
);
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
if (!isValidClientProof) {
await userDAL.update(
{ id: userEnc.userId },
{
$incr: {
consecutiveFailedPasswordAttempts: 1
}
}
);
throw new Error("Failed to authenticate. Try again?");
}
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null,
clientPublicKey: null
});
await userDAL.updateById(userEnc.userId, {
consecutiveFailedPasswordAttempts: 0
});
// send multi factor auth token if they it enabled
if (userEnc.isMfaEnabled && userEnc.email) {
const user = await userDAL.findById(userEnc.userId);
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
const mfaToken = jwt.sign(

View File

@ -12,6 +12,7 @@ export type TLoginClientProofDTO = {
providerAuthToken?: string;
ip: string;
userAgent: string;
captchaToken?: string;
};
export type TVerifyMfaTokenDTO = {

View File

@ -1921,13 +1921,13 @@ const syncSecretsGitLab = async ({
return allEnvVariables;
};
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
const allEnvVariables = await getAllEnvVariables(integration?.appId as string, accessToken);
const getSecretsRes: GitLabSecret[] = allEnvVariables
.filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment)
.filter((gitLabSecret) => {
let isValid = true;
const metadata = z.record(z.any()).parse(integration.metadata);
if (metadata.secretPrefix && !gitLabSecret.key.startsWith(metadata.secretPrefix)) {
isValid = false;
}
@ -1947,8 +1947,8 @@ const syncSecretsGitLab = async ({
{
key,
value: secrets[key].value,
protected: false,
masked: false,
protected: Boolean(metadata.shouldProtectSecrets),
masked: Boolean(metadata.shouldMaskSecrets),
raw: false,
environment_scope: integration.targetEnvironment
},
@ -1965,7 +1965,9 @@ const syncSecretsGitLab = async ({
`${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`,
{
...existingSecret,
value: secrets[existingSecret.key].value
value: secrets[existingSecret.key].value,
protected: Boolean(metadata.shouldProtectSecrets),
masked: Boolean(metadata.shouldMaskSecrets)
},
{
headers: {
@ -2754,6 +2756,20 @@ const syncSecretsCloudflarePages = async ({
}
}
);
const metadata = z.record(z.any()).parse(integration.metadata);
if (metadata.shouldAutoRedeploy) {
await request.post(
`${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}/deployments`,
{},
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
};
/**

View File

@ -31,5 +31,7 @@ export const IntegrationMetadataSchema = z.object({
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete),
shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete)
shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete),
shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets),
shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets)
});

View File

@ -29,6 +29,8 @@ export type TCreateIntegrationDTO = {
}[];
kmsKeyId?: string;
shouldDisableDelete?: boolean;
shouldMaskSecrets?: boolean;
shouldProtectSecrets?: boolean;
shouldEnableDelete?: boolean;
};
} & Omit<TProjectPermission, "projectId">;

View File

@ -77,7 +77,7 @@ export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsSe
// This will switch to a seal process and HMS flow in future
const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY;
// if root key its base64 encoded
const isBase64 = Boolean(appCfg.ROOT_ENCRYPTION_KEY);
const isBase64 = !appCfg.ENCRYPTION_KEY;
if (!encryptionKey) throw new Error("Root encryption key not found for KMS service.");
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");

View File

@ -336,6 +336,7 @@ export const orgServiceFactory = ({
return org;
});
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
return organization;
};

View File

@ -309,7 +309,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
};
const expandSecrets = async (
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => {
const expandedSec: Record<string, string> = {};
const interpolatedSec: Record<string, string> = {};
@ -329,8 +329,8 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
// should not do multi line encoding if user has set it to skip
// eslint-disable-next-line
secrets[key].value = secrets[key].skipMultilineEncoding
? expandedSec[key]
: formatMultiValueEnv(expandedSec[key]);
? formatMultiValueEnv(expandedSec[key])
: expandedSec[key];
// eslint-disable-next-line
continue;
}
@ -347,7 +347,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
);
// eslint-disable-next-line
secrets[key].value = secrets[key].skipMultilineEncoding ? expandedVal : formatMultiValueEnv(expandedVal);
secrets[key].value = secrets[key].skipMultilineEncoding ? formatMultiValueEnv(expandedVal) : expandedVal;
}
return secrets;
@ -395,7 +395,8 @@ export const decryptSecretRaw = (
type: secret.type,
_id: secret.id,
id: secret.id,
user: secret.userId
user: secret.userId,
skipMultilineEncoding: secret.skipMultilineEncoding
};
};

View File

@ -1,4 +1,6 @@
/* eslint-disable no-await-in-loop */
import { AxiosError } from "axios";
import { getConfig } from "@app/lib/config/env";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
@ -67,7 +69,10 @@ const MAX_SYNC_SECRET_DEPTH = 5;
export const uniqueSecretQueueKey = (environment: string, secretPath: string) =>
`secret-queue-dedupe-${environment}-${secretPath}`;
type TIntegrationSecret = Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>;
type TIntegrationSecret = Record<
string,
{ value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined }
>;
export const secretQueueFactory = ({
queueService,
integrationDAL,
@ -567,11 +572,14 @@ export const secretQueueFactory = ({
isSynced: true
});
} catch (err: unknown) {
logger.info("Secret integration sync error:", err);
logger.info("Secret integration sync error: %o", err);
const message =
err instanceof AxiosError ? JSON.stringify((err as AxiosError)?.response?.data) : (err as Error)?.message;
await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id,
lastUsed: new Date(),
syncMessage: (err as Error)?.message,
syncMessage: message,
isSynced: false
});
}

View File

@ -952,15 +952,49 @@ export const secretServiceFactory = ({
});
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey));
const decryptedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({
...el,
secrets: importedSecrets.map((sec) =>
const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
const decryptedImportSecrets = importedSecrets.map((sec) =>
decryptSecretRaw(
{ ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath },
botKey
)
)
}));
);
// secret-override to handle duplicate keys from different import levels
// this prioritizes secret values from direct imports
const importedKeys = new Set<string>();
const importedEntries = decryptedImportSecrets.reduce(
(
accum: {
secretKey: string;
secretPath: string;
workspace: string;
environment: string;
secretValue: string;
secretComment: string;
version: number;
type: string;
_id: string;
id: string;
user: string | null | undefined;
skipMultilineEncoding: boolean | null | undefined;
}[],
sec
) => {
if (!importedKeys.has(sec.secretKey)) {
importedKeys.add(sec.secretKey);
return [...accum, sec];
}
return accum;
},
[]
);
return {
...el,
secrets: importedEntries
};
});
if (expandSecretReferences) {
const expandSecrets = interpolateSecrets({
@ -971,10 +1005,24 @@ export const secretServiceFactory = ({
});
const batchSecretsExpand = async (
secretBatch: { secretKey: string; secretValue: string; secretComment?: string; secretPath: string }[]
secretBatch: {
secretKey: string;
secretValue: string;
secretComment?: string;
secretPath: string;
skipMultilineEncoding: boolean | null | undefined;
}[]
) => {
// Group secrets by secretPath
const secretsByPath: Record<string, { secretKey: string; secretValue: string; secretComment?: string }[]> = {};
const secretsByPath: Record<
string,
{
secretKey: string;
secretValue: string;
secretComment?: string;
skipMultilineEncoding: boolean | null | undefined;
}[]
> = {};
secretBatch.forEach((secret) => {
if (!secretsByPath[secret.secretPath]) {
@ -990,11 +1038,15 @@ export const secretServiceFactory = ({
continue;
}
const secretRecord: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }> = {};
const secretRecord: Record<
string,
{ value: string; comment?: string; skipMultilineEncoding: boolean | null | undefined }
> = {};
secretsByPath[secPath].forEach((decryptedSecret) => {
secretRecord[decryptedSecret.secretKey] = {
value: decryptedSecret.secretValue,
comment: decryptedSecret.secretComment
comment: decryptedSecret.secretComment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
};
});
@ -1011,12 +1063,12 @@ export const secretServiceFactory = ({
await batchSecretsExpand(decryptedSecrets);
// expand imports by batch
await Promise.all(decryptedImports.map((decryptedImport) => batchSecretsExpand(decryptedImport.secrets)));
await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets)));
}
return {
secrets: decryptedSecrets,
imports: decryptedImports
imports: processedImports
};
};

View File

@ -41,21 +41,8 @@ export enum SmtpHost {
Office365 = "smtp.office365.com"
}
export const getTlsOption = (host?: SmtpHost | string, secure?: boolean) => {
if (!secure) return { secure: false };
if (!host) return { secure: true };
if ((host as SmtpHost) === SmtpHost.Sendgrid) {
return { secure: true, port: 465 }; // more details here https://nodemailer.com/smtp/
}
if (host.includes("amazonaws.com")) {
return { tls: { ciphers: "TLSv1.2" } };
}
return { requireTLS: true, tls: { ciphers: "TLSv1.2" } };
};
export const smtpServiceFactory = (cfg: TSmtpConfig) => {
const smtp = createTransport({ ...cfg, ...getTlsOption(cfg.host, cfg.secure) });
const smtp = createTransport(cfg);
const isSmtpOn = Boolean(cfg.host);
const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => {

1
cli/.gitignore vendored
View File

@ -1,3 +1,4 @@
.infisical.json
dist/
agent-config.test.yaml
.test.env

View File

@ -3,7 +3,9 @@ module github.com/Infisical/infisical-merge
go 1.21
require (
github.com/bradleyjkemp/cupaloy/v2 v2.8.0
github.com/charmbracelet/lipgloss v0.5.0
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
@ -29,7 +31,6 @@ require (
require (
github.com/alessio/shellescape v1.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/danieljoos/wincred v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect

View File

@ -74,6 +74,8 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE=
github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View File

@ -4,6 +4,8 @@ import (
"fmt"
"net/http"
"os"
"github.com/Infisical/infisical-merge/packages/config"
)
func GetHomeDir() (string, error) {
@ -21,7 +23,7 @@ func WriteToFile(fileName string, dataToWrite []byte, filePerm os.FileMode) erro
return nil
}
func CheckIsConnectedToInternet() (ok bool) {
_, err := http.Get("http://clients3.google.com/generate_204")
func ValidateInfisicalAPIConnection() (ok bool) {
_, err := http.Get(fmt.Sprintf("%v/status", config.INFISICAL_URL))
return err == nil
}

View File

@ -307,32 +307,33 @@ func FilterSecretsByTag(plainTextSecrets []models.SingleEnvironmentVariable, tag
}
func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectConfigFilePath string) ([]models.SingleEnvironmentVariable, error) {
isConnected := CheckIsConnectedToInternet()
var secretsToReturn []models.SingleEnvironmentVariable
// var serviceTokenDetails api.GetServiceTokenDetailsResponse
var errorToReturn error
if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" {
if isConnected {
log.Debug().Msg("GetAllEnvironmentVariables: Connected to internet, checking logged in creds")
if projectConfigFilePath == "" {
RequireLocalWorkspaceFile()
} else {
ValidateWorkspaceFile(projectConfigFilePath)
}
RequireLogin()
if projectConfigFilePath == "" {
RequireLocalWorkspaceFile()
} else {
ValidateWorkspaceFile(projectConfigFilePath)
}
RequireLogin()
log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details")
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
isConnected := ValidateInfisicalAPIConnection()
if isConnected {
log.Debug().Msg("GetAllEnvironmentVariables: Connected to Infisical instance, checking logged in creds")
}
if err != nil {
return nil, err
}
if loggedInUserDetails.LoginExpired {
if isConnected && loggedInUserDetails.LoginExpired {
PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
}
@ -364,12 +365,12 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
if errorToReturn == nil {
WriteBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, backupSecretsEncryptionKey, secretsToReturn)
WriteBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupSecretsEncryptionKey, secretsToReturn)
}
// 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, backupSecretsEncryptionKey)
backedSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupSecretsEncryptionKey)
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
@ -634,8 +635,9 @@ func GetPlainTextSecrets(key []byte, encryptedSecrets []api.EncryptedSecretV3) (
return plainTextSecrets, nil
}
func WriteBackupSecrets(workspace string, environment string, encryptionKey []byte, secrets []models.SingleEnvironmentVariable) error {
fileName := fmt.Sprintf("secrets_%s_%s", workspace, environment)
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()
@ -672,8 +674,9 @@ func WriteBackupSecrets(workspace string, environment string, encryptionKey []by
return nil
}
func ReadBackupSecrets(workspace string, environment string, encryptionKey []byte) ([]models.SingleEnvironmentVariable, error) {
fileName := fmt.Sprintf("secrets_%s_%s", workspace, environment)
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()

View File

@ -0,0 +1,23 @@
#!/bin/bash
TEST_ENV_FILE=".test.env"
# Check if the .env file exists
if [ ! -f "$TEST_ENV_FILE" ]; then
echo "$TEST_ENV_FILE does not exist."
exit 1
fi
# Export the variables
while IFS= read -r line
do
# Skip empty lines and lines starting with #
if [[ -z "$line" || "$line" =~ ^\# ]]; then
continue
fi
# Read the key-value pair
IFS='=' read -r key value <<< "$line"
eval export $key=\$value
done < "$TEST_ENV_FILE"
echo "Test environment variables set."

View File

@ -0,0 +1,7 @@
┌───────────────┬──────────────┬─────────────┐
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
├───────────────┼──────────────┼─────────────┤
│ TEST-SECRET-1 │ test-value-1 │ shared │
│ TEST-SECRET-2 │ test-value-2 │ shared │
│ TEST-SECRET-3 │ test-value-3 │ shared │
└───────────────┴──────────────┴─────────────┘

View File

@ -0,0 +1,8 @@
Warning: Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug
┌───────────────┬──────────────┬─────────────┐
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
├───────────────┼──────────────┼─────────────┤
│ TEST-SECRET-1 │ test-value-1 │ shared │
│ TEST-SECRET-2 │ test-value-2 │ shared │
│ TEST-SECRET-3 │ test-value-3 │ shared │
└───────────────┴──────────────┴─────────────┘

View File

@ -8,7 +8,6 @@ import (
func TestUniversalAuth_ExportSecretsWithImports(t *testing.T) {
MachineIdentityLoginCmd(t)
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "export", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent")
@ -24,8 +23,6 @@ func TestUniversalAuth_ExportSecretsWithImports(t *testing.T) {
}
func TestServiceToken_ExportSecretsWithImports(t *testing.T) {
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "export", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent")
if err != nil {
@ -41,8 +38,6 @@ func TestServiceToken_ExportSecretsWithImports(t *testing.T) {
func TestUniversalAuth_ExportSecretsWithoutImports(t *testing.T) {
MachineIdentityLoginCmd(t)
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "export", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--include-imports=false")
if err != nil {
@ -57,8 +52,6 @@ func TestUniversalAuth_ExportSecretsWithoutImports(t *testing.T) {
}
func TestServiceToken_ExportSecretsWithoutImports(t *testing.T) {
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "export", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--include-imports=false")
if err != nil {

View File

@ -2,10 +2,10 @@ package tests
import (
"fmt"
"log"
"os"
"os/exec"
"strings"
"testing"
)
const (
@ -23,6 +23,8 @@ type Credentials struct {
ServiceToken string
ProjectID string
EnvSlug string
UserEmail string
UserPassword string
}
var creds = Credentials{
@ -32,18 +34,21 @@ var creds = Credentials{
ServiceToken: os.Getenv("CLI_TESTS_SERVICE_TOKEN"),
ProjectID: os.Getenv("CLI_TESTS_PROJECT_ID"),
EnvSlug: os.Getenv("CLI_TESTS_ENV_SLUG"),
UserEmail: os.Getenv("CLI_TESTS_USER_EMAIL"),
UserPassword: os.Getenv("CLI_TESTS_USER_PASSWORD"),
}
func ExecuteCliCommand(command string, args ...string) (string, error) {
cmd := exec.Command(command, args...)
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Println(fmt.Sprint(err) + ": " + string(output))
return strings.TrimSpace(string(output)), err
}
return strings.TrimSpace(string(output)), nil
}
func SetupCli(t *testing.T) {
func SetupCli() {
if creds.ClientID == "" || creds.ClientSecret == "" || creds.ServiceToken == "" || creds.ProjectID == "" || creds.EnvSlug == "" {
panic("Missing required environment variables")
@ -57,7 +62,7 @@ func SetupCli(t *testing.T) {
if !alreadyBuilt {
if err := exec.Command("go", "build", "../.").Run(); err != nil {
t.Fatal(err)
log.Fatal(err)
}
}

View File

@ -1,14 +1,124 @@
package tests
import (
"log"
"os/exec"
"strings"
"testing"
"github.com/creack/pty"
"github.com/stretchr/testify/assert"
)
func MachineIdentityLoginCmd(t *testing.T) {
SetupCli(t)
func UserInitCmd() {
c := exec.Command(FORMATTED_CLI_NAME, "init")
ptmx, err := pty.Start(c)
if err != nil {
log.Fatalf("error running CLI command: %v", err)
}
defer func() { _ = ptmx.Close() }()
stepChan := make(chan int, 10)
go func() {
buf := make([]byte, 1024)
step := -1
for {
n, err := ptmx.Read(buf)
if n > 0 {
terminalOut := string(buf)
if strings.Contains(terminalOut, "Which Infisical organization would you like to select a project from?") && step < 0 {
step += 1
stepChan <- step
} else if strings.Contains(terminalOut, "Which of your Infisical projects would you like to connect this project to?") && step < 1 {
step += 1;
stepChan <- step
}
}
if err != nil {
close(stepChan)
return
}
}
}()
for i := range stepChan {
switch i {
case 0:
ptmx.Write([]byte("\n"))
case 1:
ptmx.Write([]byte("\n"))
}
}
}
func UserLoginCmd() {
// set vault to file because CI has no keyring
vaultCmd := exec.Command(FORMATTED_CLI_NAME, "vault", "set", "file")
_, err := vaultCmd.Output()
if err != nil {
log.Fatalf("error setting vault: %v", err)
}
// Start programmatic interaction with CLI
c := exec.Command(FORMATTED_CLI_NAME, "login", "--interactive")
ptmx, err := pty.Start(c)
if err != nil {
log.Fatalf("error running CLI command: %v", err)
}
defer func() { _ = ptmx.Close() }()
stepChan := make(chan int, 10)
go func() {
buf := make([]byte, 1024)
step := -1
for {
n, err := ptmx.Read(buf)
if n > 0 {
terminalOut := string(buf)
if strings.Contains(terminalOut, "Infisical Cloud") && step < 0 {
step += 1;
stepChan <- step
} else if strings.Contains(terminalOut, "Email") && step < 1 {
step += 1;
stepChan <- step
} else if strings.Contains(terminalOut, "Password") && step < 2 {
step += 1;
stepChan <- step
} else if strings.Contains(terminalOut, "Infisical organization") && step < 3 {
step += 1;
stepChan <- step
} else if strings.Contains(terminalOut, "Enter passphrase") && step < 4 {
step += 1;
stepChan <- step
}
}
if err != nil {
close(stepChan)
return
}
}
}()
for i := range stepChan {
switch i {
case 0:
ptmx.Write([]byte("\n"))
case 1:
ptmx.Write([]byte(creds.UserEmail))
ptmx.Write([]byte("\n"))
case 2:
ptmx.Write([]byte(creds.UserPassword))
ptmx.Write([]byte("\n"))
case 3:
ptmx.Write([]byte("\n"))
}
}
}
func MachineIdentityLoginCmd(t *testing.T) {
if creds.UAAccessToken != "" {
return
}

23
cli/test/main_test.go Normal file
View File

@ -0,0 +1,23 @@
package tests
import (
"fmt"
"os"
"testing"
)
func TestMain(m *testing.M) {
// Setup
fmt.Println("Setting up CLI...")
SetupCli()
fmt.Println("Performing user login...")
UserLoginCmd()
fmt.Println("Performing infisical init...")
UserInitCmd()
// Run the tests
code := m.Run()
// Exit
os.Exit(code)
}

View File

@ -8,8 +8,6 @@ import (
)
func TestServiceToken_RunCmdRecursiveAndImports(t *testing.T) {
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent", "--", "echo", "hello world")
if err != nil {
@ -25,8 +23,6 @@ func TestServiceToken_RunCmdRecursiveAndImports(t *testing.T) {
}
}
func TestServiceToken_RunCmdWithImports(t *testing.T) {
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--", "echo", "hello world")
if err != nil {
@ -44,8 +40,6 @@ func TestServiceToken_RunCmdWithImports(t *testing.T) {
func TestUniversalAuth_RunCmdRecursiveAndImports(t *testing.T) {
MachineIdentityLoginCmd(t)
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent", "--", "echo", "hello world")
if err != nil {
@ -63,8 +57,6 @@ func TestUniversalAuth_RunCmdRecursiveAndImports(t *testing.T) {
func TestUniversalAuth_RunCmdWithImports(t *testing.T) {
MachineIdentityLoginCmd(t)
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--", "echo", "hello world")
if err != nil {
@ -83,8 +75,6 @@ func TestUniversalAuth_RunCmdWithImports(t *testing.T) {
func TestUniversalAuth_RunCmdWithoutImports(t *testing.T) {
MachineIdentityLoginCmd(t)
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--include-imports=false", "--", "echo", "hello world")
if err != nil {
@ -101,8 +91,6 @@ func TestUniversalAuth_RunCmdWithoutImports(t *testing.T) {
}
func TestServiceToken_RunCmdWithoutImports(t *testing.T) {
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--include-imports=false", "--", "echo", "hello world")
if err != nil {

View File

@ -7,8 +7,6 @@ import (
)
func TestServiceToken_GetSecretsByNameRecursive(t *testing.T) {
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "TEST-SECRET-2", "FOLDER-SECRET-1", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
if err != nil {
@ -23,8 +21,6 @@ func TestServiceToken_GetSecretsByNameRecursive(t *testing.T) {
}
func TestServiceToken_GetSecretsByNameWithNotFoundSecret(t *testing.T) {
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "TEST-SECRET-2", "FOLDER-SECRET-1", "DOES-NOT-EXIST", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
if err != nil {
@ -39,8 +35,6 @@ func TestServiceToken_GetSecretsByNameWithNotFoundSecret(t *testing.T) {
}
func TestServiceToken_GetSecretsByNameWithImports(t *testing.T) {
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "STAGING-SECRET-2", "FOLDER-SECRET-1", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
if err != nil {
@ -56,8 +50,6 @@ func TestServiceToken_GetSecretsByNameWithImports(t *testing.T) {
func TestUniversalAuth_GetSecretsByNameRecursive(t *testing.T) {
MachineIdentityLoginCmd(t)
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "TEST-SECRET-2", "FOLDER-SECRET-1", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
if err != nil {
@ -73,8 +65,6 @@ func TestUniversalAuth_GetSecretsByNameRecursive(t *testing.T) {
func TestUniversalAuth_GetSecretsByNameWithNotFoundSecret(t *testing.T) {
MachineIdentityLoginCmd(t)
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "TEST-SECRET-2", "FOLDER-SECRET-1", "DOES-NOT-EXIST", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
if err != nil {
@ -90,8 +80,6 @@ func TestUniversalAuth_GetSecretsByNameWithNotFoundSecret(t *testing.T) {
func TestUniversalAuth_GetSecretsByNameWithImports(t *testing.T) {
MachineIdentityLoginCmd(t)
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "STAGING-SECRET-2", "FOLDER-SECRET-1", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
if err != nil {

View File

@ -3,12 +3,12 @@ package tests
import (
"testing"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/bradleyjkemp/cupaloy/v2"
)
func TestServiceToken_SecretsGetWithImportsAndRecursiveCmd(t *testing.T) {
SetupCli(t)
func TestServiceToken_SecretsGetWithImportsAndRecursiveCmd(t *testing.T) {
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
if err != nil {
@ -23,8 +23,6 @@ func TestServiceToken_SecretsGetWithImportsAndRecursiveCmd(t *testing.T) {
}
func TestServiceToken_SecretsGetWithoutImportsAndWithoutRecursiveCmd(t *testing.T) {
SetupCli(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent")
if err != nil {
@ -39,7 +37,6 @@ func TestServiceToken_SecretsGetWithoutImportsAndWithoutRecursiveCmd(t *testing.
}
func TestUniversalAuth_SecretsGetWithImportsAndRecursiveCmd(t *testing.T) {
SetupCli(t)
MachineIdentityLoginCmd(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
@ -56,7 +53,6 @@ func TestUniversalAuth_SecretsGetWithImportsAndRecursiveCmd(t *testing.T) {
}
func TestUniversalAuth_SecretsGetWithoutImportsAndWithoutRecursiveCmd(t *testing.T) {
SetupCli(t)
MachineIdentityLoginCmd(t)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent")
@ -73,7 +69,6 @@ func TestUniversalAuth_SecretsGetWithoutImportsAndWithoutRecursiveCmd(t *testing
}
func TestUniversalAuth_SecretsGetWrongEnvironment(t *testing.T) {
SetupCli(t)
MachineIdentityLoginCmd(t)
output, _ := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", "invalid-env", "--recursive", "--silent")
@ -85,3 +80,45 @@ func TestUniversalAuth_SecretsGetWrongEnvironment(t *testing.T) {
}
}
func TestUserAuth_SecretsGetAll(t *testing.T) {
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent")
if err != nil {
t.Fatalf("error running CLI command: %v", err)
}
// Use cupaloy to snapshot test the output
err = cupaloy.Snapshot(output)
if err != nil {
t.Fatalf("snapshot failed: %v", err)
}
// explicitly called here because it should happen directly after successful secretsGetAll
testUserAuth_SecretsGetAllWithoutConnection(t)
}
func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) {
originalConfigFile, err := util.GetConfigFile()
if err != nil {
t.Fatalf("error getting config file")
}
newConfigFile := originalConfigFile
// set it to a URL that will always be unreachable
newConfigFile.LoggedInUserDomain = "http://localhost:4999"
util.WriteConfigFile(&newConfigFile)
// restore config file
defer util.WriteConfigFile(&originalConfigFile)
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent")
if err != nil {
t.Fatalf("error running CLI command: %v", err)
}
// Use cupaloy to snapshot test the output
err = cupaloy.Snapshot(output)
if err != nil {
t.Fatalf("snapshot failed: %v", err)
}
}

View File

@ -496,7 +496,6 @@ To enable auto redeployment you simply have to add the following annotation to t
```yaml
secrets.infisical.com/auto-reload: "true"
```
<Accordion title="Deployment example with auto redeploy enabled">
```yaml
apiVersion: apps/v1
@ -527,7 +526,11 @@ spec:
- containerPort: 80
```
</Accordion>
<Info>
#### How it works
When a secret change occurs, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update.
Then, for each deployment that has this annotation present, a rolling update will be triggered.
</Info>
## Global configuration
To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap.

View File

@ -384,6 +384,7 @@
"pages": [
"sdks/languages/node",
"sdks/languages/python",
"sdks/languages/go",
"sdks/languages/java",
"sdks/languages/csharp"
]

438
docs/sdks/languages/go.mdx Normal file
View File

@ -0,0 +1,438 @@
---
title: "Infisical Go SDK"
sidebarTitle: "Go"
icon: "golang"
---
If you're working with Go Lang, the official [Infisical Go SDK](https://github.com/infisical/go-sdk) package is the easiest way to fetch and work with secrets for your application.
- [Package](https://pkg.go.dev/github.com/infisical/go-sdk)
- [Github Repository](https://github.com/infiscial/go-sdk)
## Basic Usage
```go
package main
import (
"fmt"
"os"
infisical "github.com/infisical/go-sdk"
)
func main() {
client, err := infisical.NewInfisicalClient(infisical.Config{
SiteUrl: "https://app.infisical.com", // Optional, default is https://app.infisical.com
})
if err != nil {
fmt.Printf("Error: %v", err)
os.Exit(1)
}
_, err = client.Auth().UniversalAuthLogin("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET")
if err != nil {
fmt.Printf("Authentication failed: %v", err)
os.Exit(1)
}
apiKeySecret, err := client.Secrets().Retrieve(infisical.RetrieveSecretOptions{
SecretKey: "API_KEY",
Environment: "dev",
ProjectID: "YOUR_PROJECT_ID",
SecretPath: "/",
})
if err != nil {
fmt.Printf("Error: %v", err)
os.Exit(1)
}
fmt.Printf("API Key Secret: %v", apiKeySecret)
}
```
This example demonstrates how to use the Infisical Go SDK in a simple Go 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
$ go get github.com/infisical/go-sdk
```
# Configuration
Import the SDK and create a client instance.
```go
client, err := infisical.NewInfisicalClient(infisical.Config{
SiteUrl: "https://app.infisical.com", // Optional, default is https://api.infisical.com
})
if err != nil {
fmt.Printf("Error: %v", err)
os.Exit(1)
}
```
### ClientSettings methods
<ParamField query="options" type="object">
<Expandable title="properties">
<ParamField query="SiteUrl" type="string" optional>
The URL of the Infisical API. Default is `https://api.infisical.com`.
</ParamField>
<ParamField query="UserAgent" type="string" required>
Optionally set the user agent that will be used for HTTP requests. _(Not recommended)_
</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().UniversalAuthLogin()` 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**
```go
_, err := client.Auth().UniversalAuthLogin("CLIENT_ID", "CLIENT_SECRET")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
```
#### 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().GcpIdTokenAuthLogin()` with empty arguments to use the following environment variables:
- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
**Using the SDK directly**
```go
_, err := client.Auth().GcpIdTokenAuthLogin("YOUR_MACHINE_IDENTITY_ID")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
```
#### GCP IAM Auth
**Using environment variables**
Call `.Auth().GcpIamAuthLogin()` 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**
```go
_, err = client.Auth().GcpIamAuthLogin("MACHINE_IDENTITY_ID", "SERVICE_ACCOUNT_KEY_FILE_PATH")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
```
#### 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().AwsIamAuthLogin()` with empty arguments to use the following environment variables:
- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
**Using the SDK directly**
```go
_, err = client.Auth().AwsIamAuthLogin("MACHINE_IDENTITY_ID")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
```
#### 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().AzureAuthLogin()` with empty arguments to use the following environment variables:
- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
**Using the SDK directly**
```go
_, err = client.Auth().AzureAuthLogin("MACHINE_IDENTITY_ID")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
```
#### 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().KubernetesAuthLogin()` 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**
```go
// Service account token path will default to /var/run/secrets/kubernetes.io/serviceaccount/token if empty value is passed
_, err = client.Auth().KubernetesAuthLogin("MACHINE_IDENTITY_ID", "SERVICE_ACCOUNT_TOKEN_PATH")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
```
## Working with Secrets
### client.Secrets().List(options)
```go
secrets, err := client.Secrets().List(infisical.ListSecretsOptions{
ProjectID: "PROJECT_ID",
Environment: "dev",
SecretPath: "/foo/bar",
AttachToProcessEnv: false,
})
```
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="ProjectID" type="string">
The project ID where the secret lives in.
</ParamField>
<ParamField query="SecretPath" type="string" optional>
The path from where secrets should be fetched from.
</ParamField>
<ParamField query="AttachToProcessEnv" 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="IncludeImports" 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="ExpandSecretReferences" 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)
```go
secret, err := client.Secrets().Retrieve(infisical.RetrieveSecretOptions{
SecretKey: "API_KEY",
ProjectID: "PROJECT_ID",
Environment: "dev",
})
```
Retrieve a secret from Infisical.
By default, `Secrets().Get()` fetches and returns a shared secret.
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
<ParamField query="SecretKey" type="string" required>
The key of the secret to retrieve.
</ParamField>
<ParamField query="ProjectID" 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="SecretPath" 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)
```go
secret, err := client.Secrets().Create(infisical.CreateSecretOptions{
ProjectID: "PROJECT_ID",
Environment: "dev",
SecretKey: "NEW_SECRET_KEY",
SecretValue: "NEW_SECRET_VALUE",
SecretComment: "This is a new secret",
})
```
Create a new secret in Infisical.
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
<ParamField query="SecretKey" type="string" required>
The key of the secret to create.
</ParamField>
<ParamField query="SecretValue" type="string" required>
The value of the secret.
</ParamField>
<ParamField query="SecretComment" type="string" optional>
A comment for the secret.
</ParamField>
<ParamField query="ProjectID" 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="SecretPath" 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)
```go
secret, err := client.Secrets().Update(infisical.UpdateSecretOptions{
ProjectID: "PROJECT_ID",
Environment: "dev",
SecretKey: "NEW_SECRET_KEY",
NewSecretValue: "NEW_SECRET_VALUE",
NewSkipMultilineEncoding: false,
})
```
Update an existing secret in Infisical.
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
<ParamField query="SecretKey" type="string" required>
The key of the secret to update.
</ParamField>
<ParamField query="NewSecretValue" type="string" required>
The new value of the secret.
</ParamField>
<ParamField query="NewSkipMultilineEncoding" type="boolean" default="false" optional>
Whether or not to skip multiline encoding for the new secret value.
</ParamField>
<ParamField query="ProjectID" 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="SecretPath" 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)
```go
secret, err := client.Secrets().Delete(infisical.DeleteSecretOptions{
ProjectID: "PROJECT_ID",
Environment: "dev",
SecretKey: "SECRET_KEY",
})
```
Delete a secret in Infisical.
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
<ParamField query="SecretKey" type="string">
The key of the secret to update.
</ParamField>
<ParamField query="ProjectID" 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="SecretPath" 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>

View File

@ -48,44 +48,44 @@ The platform utilizes Postgres to persist all of its data and Redis for caching
Without email configuration, Infisical's core functions like sign-up/login and secret operations work, but this disables multi-factor authentication, email invites for projects, alerts for suspicious logins, and all other email-dependent features.
<Accordion title="Generic Configuration">
<ParamField query="SMTP_HOST" type="string" default="none" optional>
Hostname to connect to for establishing SMTP connections
</ParamField>
{" "}
<ParamField query="SMTP_USERNAME" type="string" default="none" optional>
Credential to connect to host (e.g. team@infisical.com)
<ParamField query="SMTP_HOST" type="string" default="none" optional>
Hostname to connect to for establishing SMTP connections
</ParamField>
{" "}
<ParamField query="SMTP_PASSWORD" type="string" default="none" optional>
Credential to connect to host
</ParamField>
{" "}
<ParamField query="SMTP_PORT" type="string" default="587" optional>
Port to connect to for establishing SMTP connections
</ParamField>
{" "}
<ParamField query="SMTP_SECURE" type="string" default="none" optional>
If true, use TLS when connecting to host. If false, TLS will be used if
STARTTLS is supported
<ParamField query="SMTP_USERNAME" type="string" default="none" optional>
Credential to connect to host (e.g. team@infisical.com)
</ParamField>
{" "}
<ParamField query="SMTP_PASSWORD" type="string" default="none" optional>
Credential to connect to host
</ParamField>
<ParamField query="SMTP_FROM_ADDRESS" type="string" default="none" optional>
Email address to be used for sending emails
</ParamField>
<ParamField query="SMTP_FROM_NAME" type="string" default="none" optional>
Name label to be used in From field (e.g. Team)
</ParamField>
<ParamField query="SMTP_FROM_NAME" type="string" default="none" optional>
Name label to be used in From field (e.g. Team)
</ParamField>
<ParamField query="SMTP_IGNORE_TLS" type="bool" default="false" optional>
If this is `true` and `SMTP_PORT` is not 465 then TLS is not used even if the
server supports STARTTLS extension.
</ParamField>
<ParamField query="SMTP_REQUIRE_TLS" type="bool" default="true" optional>
If this is `true` and `SMTP_PORT` is not 465 then Infisical tries to use
STARTTLS even if the server does not advertise support for it. If the
connection can not be encrypted then message is not sent.
</ParamField>
<ParamField query="SMTP_TLS_REJECT_UNAUTHORIZED" type="bool" default="true" optional>
If this is `true`, Infisical will validate the server's SSL/TLS certificate and reject the connection if the certificate is invalid or not trusted. If set to `false`, the client will accept the server's certificate regardless of its validity, which can be useful in development or testing environments but is not recommended for production use.
</ParamField>
</Accordion>
<Accordion title="Twilio SendGrid">
@ -105,7 +105,6 @@ SMTP_HOST=smtp.sendgrid.net
SMTP_USERNAME=apikey
SMTP_PASSWORD=SG.rqFsfjxYPiqE1lqZTgD_lz7x8IVLx # your SendGrid API Key from step above
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```
@ -128,7 +127,6 @@ SMTP_HOST=smtp.mailgun.org # obtained from credentials page
SMTP_USERNAME=postmaster@example.mailgun.org # obtained from credentials page
SMTP_PASSWORD=password # obtained from credentials page
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```
@ -159,7 +157,6 @@ SMTP_FROM_NAME=Infisical
SMTP_USERNAME=xxx # your SMTP username
SMTP_PASSWORD=xxx # your SMTP password
SMTP_PORT=465
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```
@ -187,7 +184,6 @@ SMTP_HOST=smtp.socketlabs.com
SMTP_USERNAME=username # obtained from your credentials
SMTP_PASSWORD=password # obtained from your credentials
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```
@ -229,7 +225,6 @@ SMTP_HOST=smtp.resend.com
SMTP_USERNAME=resend
SMTP_PASSWORD=YOUR_API_KEY
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```
@ -253,7 +248,6 @@ SMTP_HOST=smtp.gmail.com
SMTP_USERNAME=hey@gmail.com # your email
SMTP_PASSWORD=password # your password
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@gmail.com
SMTP_FROM_NAME=Infisical
```
@ -277,7 +271,6 @@ SMTP_HOST=smtp.office365.com
SMTP_USERNAME=username@yourdomain.com # your username
SMTP_PASSWORD=password # your password
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=username@yourdomain.com
SMTP_FROM_NAME=Infisical
```
@ -294,7 +287,6 @@ SMTP_HOST=smtp.zoho.com
SMTP_USERNAME=username # your email
SMTP_PASSWORD=password # your password
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your personal Zoho email or domain-based email linked to Zoho Mail
SMTP_FROM_NAME=Infisical
```
@ -318,6 +310,12 @@ SMTP_FROM_NAME=Infisical
By default, users can only login via email/password based login method.
To login into Infisical with OAuth providers such as Google, configure the associated variables.
<ParamField query="DEFAULT_SAML_ORG_SLUG" type="string">
When set, all visits to the Infisical login page will automatically redirect users of your Infisical instance to the SAML identity provider associated with the specified organization slug.
</ParamField>
<Accordion title="Google">
Follow detailed guide to configure [Google SSO](/documentation/platform/sso/google)
@ -369,11 +367,6 @@ To login into Infisical with OAuth providers such as Google, configure the assoc
information.
</Accordion>
<ParamField query="NEXT_PUBLIC_SAML_ORG_SLUG" type="string">
Configure SAML organization slug to automatically redirect all users of your
Infisical instance to the identity provider.
</ParamField>
## Native secret integrations
To help you sync secrets from Infisical to services such as Github and Gitlab, Infisical provides native integrations out of the box.

View File

@ -2,6 +2,7 @@ ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
ARG INTERCOM_ID=intercom-id
ARG NEXT_INFISICAL_PLATFORM_VERSION=next-infisical-platform-version
ARG CAPTCHA_SITE_KEY=captcha-site-key
FROM node:16-alpine AS deps
# Install dependencies only when needed. Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
@ -31,6 +32,8 @@ ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
# Build
RUN npm run build
@ -57,7 +60,9 @@ ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \
BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
ARG NEXT_INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION=$NEXT_INFISICAL_PLATFORM_VERSION
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
COPY --chown=nextjs:nodejs --chmod=555 scripts ./scripts
COPY --from=builder /app/public ./public
RUN chown nextjs:nodejs ./public/data

View File

@ -1,13 +1,12 @@
const path = require("path");
const ContentSecurityPolicy = `
default-src 'self';
script-src 'self' https://app.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com 'unsafe-inline' 'unsafe-eval';
style-src 'self' https://rsms.me 'unsafe-inline';
script-src 'self' https://app.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval';
style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com;
child-src https://api.stripe.com;
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/;
connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://127.0.0.1:*;
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/ https://hcaptcha.com https://*.hcaptcha.com;
connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://127.0.0.1:* https://hcaptcha.com https://*.hcaptcha.com;
img-src 'self' https://static.intercomassets.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://*.stripe.com https://i.ytimg.com/ data:;
media-src https://js.intercomcdn.com;
font-src 'self' https://fonts.intercomcdn.com/ https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com;

View File

@ -4,7 +4,6 @@
"requires": true,
"packages": {
"": {
"name": "frontend",
"dependencies": {
"@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0",
@ -19,6 +18,7 @@
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@hcaptcha/react-hcaptcha": "^1.10.1",
"@headlessui/react": "^1.7.7",
"@hookform/resolvers": "^2.9.10",
"@octokit/rest": "^19.0.7",
@ -3200,6 +3200,24 @@
"react": ">=16.3"
}
},
"node_modules/@hcaptcha/loader": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@hcaptcha/loader/-/loader-1.2.4.tgz",
"integrity": "sha512-3MNrIy/nWBfyVVvMPBKdKrX7BeadgiimW0AL/a/8TohNtJqxoySKgTJEXOQvYwlHemQpUzFrIsK74ody7JiMYw=="
},
"node_modules/@hcaptcha/react-hcaptcha": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.10.1.tgz",
"integrity": "sha512-P0en4gEZAecah7Pt3WIaJO2gFlaLZKkI0+Tfdg8fNqsDxqT9VytZWSkH4WAkiPRULK1QcGgUZK+J56MXYmPifw==",
"dependencies": {
"@babel/runtime": "^7.17.9",
"@hcaptcha/loader": "^1.2.1"
},
"peerDependencies": {
"react": ">= 16.3.0",
"react-dom": ">= 16.3.0"
}
},
"node_modules/@headlessui/react": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz",

View File

@ -26,6 +26,7 @@
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@hcaptcha/react-hcaptcha": "^1.10.1",
"@headlessui/react": "^1.7.7",
"@hookform/resolvers": "^2.9.10",
"@octokit/rest": "^19.0.7",

View File

@ -4,7 +4,7 @@ scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY
scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID"
scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG"
scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY" "$NEXT_PUBLIC_CAPTCHA_SITE_KEY"
if [ "$TELEMETRY_ENABLED" != "false" ]; then
echo "Telemetry is enabled"

View File

@ -6,6 +6,8 @@ scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTER
scripts/replace-variable.sh "$BAKED_NEXT_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG"
scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY" "$NEXT_PUBLIC_CAPTCHA_SITE_KEY"
if [ "$TELEMETRY_ENABLED" != "false" ]; then
echo "Telemetry is enabled"
scripts/set-telemetry.sh true

View File

@ -30,11 +30,13 @@ export interface IsCliLoginSuccessful {
const attemptLogin = async ({
email,
password,
providerAuthToken
providerAuthToken,
captchaToken
}: {
email: string;
password: string;
providerAuthToken?: string;
captchaToken?: string;
}): Promise<IsCliLoginSuccessful> => {
const telemetry = new Telemetry().getInstance();
return new Promise((resolve, reject) => {
@ -70,7 +72,8 @@ const attemptLogin = async ({
} = await login2({
email,
clientProof,
providerAuthToken
providerAuthToken,
captchaToken
});
if (mfaEnabled) {
// case: MFA is enabled

View File

@ -22,11 +22,13 @@ interface IsLoginSuccessful {
const attemptLogin = async ({
email,
password,
providerAuthToken
providerAuthToken,
captchaToken
}: {
email: string;
password: string;
providerAuthToken?: string;
captchaToken?: string;
}): Promise<IsLoginSuccessful> => {
const telemetry = new Telemetry().getInstance();
// eslint-disable-next-line new-cap
@ -58,6 +60,7 @@ const attemptLogin = async ({
iv,
tag
} = await login2({
captchaToken,
email,
clientProof,
providerAuthToken

View File

@ -2,5 +2,6 @@ const ENV = process.env.NEXT_PUBLIC_ENV! || "development"; // investigate
const POSTHOG_API_KEY = process.env.NEXT_PUBLIC_POSTHOG_API_KEY!;
const POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
const INTERCOMid = process.env.NEXT_PUBLIC_INTERCOMid!;
const CAPTCHA_SITE_KEY = process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY!;
export { ENV, INTERCOMid, POSTHOG_API_KEY, POSTHOG_HOST };
export { CAPTCHA_SITE_KEY, ENV, INTERCOMid, POSTHOG_API_KEY, POSTHOG_HOST };

View File

@ -93,27 +93,29 @@ const initProjectHelper = async ({ projectName }: { projectName: string }) => {
});
try {
secrets?.forEach((secret) => {
createSecret({
workspaceId: project.id,
environment: secret.environment,
type: secret.type,
secretKey: secret.secretName,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
secretCommentCiphertext: secret.secretCommentCiphertext,
secretCommentIV: secret.secretCommentIV,
secretCommentTag: secret.secretCommentTag,
secretPath: "/",
metadata: {
source: "signup"
}
});
});
await Promise.allSettled(
(secrets || []).map((secret) =>
createSecret({
workspaceId: project.id,
environment: secret.environment,
type: secret.type,
secretKey: secret.secretName,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
secretCommentCiphertext: secret.secretCommentCiphertext,
secretCommentIV: secret.secretCommentIV,
secretCommentTag: secret.secretCommentTag,
secretPath: "/",
metadata: {
source: "signup"
}
})
)
);
} catch (err) {
console.error("Failed to upload secrets", err);
}

View File

@ -30,6 +30,7 @@ export type Login1DTO = {
};
export type Login2DTO = {
captchaToken?: string;
email: string;
clientProof: string;
providerAuthToken?: string;

View File

@ -73,6 +73,8 @@ export const useCreateIntegration = () => {
}[];
kmsKeyId?: string;
shouldDisableDelete?: boolean;
shouldMaskSecrets?: boolean;
shouldProtectSecrets?: boolean;
shouldEnableDelete?: boolean;
};
}) => {

View File

@ -4,4 +4,5 @@ export type ServerStatus = {
emailConfigured: boolean;
secretScanningConfigured: boolean;
redisConfigured: boolean;
samlDefaultOrgSlug: boolean
};

View File

@ -7,7 +7,15 @@ import { createNotification } from "@app/components/notifications";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api";
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "../../../components/v2";
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem,
Switch
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
@ -34,6 +42,7 @@ export default function CloudflarePagesIntegrationPage() {
const [targetApp, setTargetApp] = useState("");
const [targetAppId, setTargetAppId] = useState("");
const [targetEnvironment, setTargetEnvironment] = useState("");
const [shouldAutoRedeploy, setShouldAutoRedeploy] = useState(false);
const [isLoading, setIsLoading] = useState(false);
@ -69,7 +78,10 @@ export default function CloudflarePagesIntegrationPage() {
appId: targetAppId,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
secretPath
secretPath,
metadata: {
shouldAutoRedeploy
}
});
setIsLoading(false);
@ -169,6 +181,15 @@ export default function CloudflarePagesIntegrationPage() {
))}
</Select>
</FormControl>
<div className="mb-[2.36rem] ml-1 px-6">
<Switch
id="redeploy-cloudflare-pages"
onCheckedChange={(isChecked: boolean) => setShouldAutoRedeploy(isChecked)}
isChecked={shouldAutoRedeploy}
>
Auto-redeploy service upon secret change
</Switch>
</div>
<Button
onClick={handleButtonClick}
color="mineshaft"

View File

@ -25,6 +25,7 @@ import {
ModalContent,
Select,
SelectItem,
Switch,
Tab,
TabList,
TabPanel,
@ -58,7 +59,9 @@ const schema = yup.object({
targetAppId: yup.string().required("GitLab project is required"),
targetEnvironment: yup.string(),
secretPrefix: yup.string(),
secretSuffix: yup.string()
secretSuffix: yup.string(),
shouldMaskSecrets: yup.boolean(),
shouldProtectSecrets: yup.boolean()
});
type FormData = yup.InferType<typeof schema>;
@ -138,7 +141,9 @@ export default function GitLabCreateIntegrationPage() {
targetAppId,
targetEnvironment,
secretPrefix,
secretSuffix
secretSuffix,
shouldMaskSecrets,
shouldProtectSecrets
}: FormData) => {
try {
setIsLoading(true);
@ -156,7 +161,9 @@ export default function GitLabCreateIntegrationPage() {
secretPath,
metadata: {
secretPrefix,
secretSuffix
secretSuffix,
shouldMaskSecrets,
shouldProtectSecrets
}
});
@ -390,6 +397,36 @@ export default function GitLabCreateIntegrationPage() {
exit={{ opacity: 0, translateX: 30 }}
className="pb-[14.25rem]"
>
<div className="ml-1">
<Controller
control={control}
name="shouldMaskSecrets"
render={({ field: { onChange, value } }) => (
<Switch
id="should-mask-secrets"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
>
<div className="max-w-md">Mark Infisical secrets in Gitlab as &apos;Masked&apos; secrets</div>
</Switch>
)}
/>
</div>
<div className="ml-1 mt-4 mb-5">
<Controller
control={control}
name="shouldProtectSecrets"
render={({ field: { onChange, value } }) => (
<Switch
id="should-protect-secrets"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
>
Mark Infisical secrets in Gitlab as &apos;Protected&apos; secrets
</Switch>
)}
/>
</div>
<Controller
control={control}
name="secretPrefix"

View File

@ -1,17 +1,20 @@
import { FormEvent, useEffect, useState } from "react";
import { FormEvent, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import HCaptcha from "@hcaptcha/react-hcaptcha";
import Error from "@app/components/basic/Error";
import { createNotification } from "@app/components/notifications";
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
import { Button, Input } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { useFetchServerStatus } from "@app/hooks/api";
import { navigateUserToSelectOrg } from "../../Login.utils";
@ -31,21 +34,18 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
const [loginError, setLoginError] = useState(false);
const { config } = useServerConfig();
const queryParams = new URLSearchParams(window.location.search);
const [captchaToken, setCaptchaToken] = useState("");
const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false);
const captchaRef = useRef<HCaptcha>(null);
const { data: serverDetails } = useFetchServerStatus();
useEffect(() => {
if (
process.env.NEXT_PUBLIC_SAML_ORG_SLUG &&
process.env.NEXT_PUBLIC_SAML_ORG_SLUG !== "saml-org-slug-default"
) {
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/saml2/organizations/${process.env.NEXT_PUBLIC_SAML_ORG_SLUG}${
callbackPort ? `?callback_port=${callbackPort}` : ""
}`
);
window.close();
}
}, []);
if (serverDetails?.samlDefaultOrgSlug){
const callbackPort = queryParams.get("callback_port");
const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${serverDetails?.samlDefaultOrgSlug}${callbackPort ? `?callback_port=${callbackPort}` : ""}`
router.push(redirectUrl);
}
}, [serverDetails?.samlDefaultOrgSlug]);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
@ -61,7 +61,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
// attemptCliLogin
const isCliLoginSuccessful = await attemptCliLogin({
email: email.toLowerCase(),
password
password,
captchaToken
});
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
@ -83,7 +84,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
} else {
const isLoginSuccessful = await attemptLogin({
email: email.toLowerCase(),
password
password,
captchaToken
});
if (isLoginSuccessful && isLoginSuccessful.success) {
@ -117,6 +119,12 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
return;
}
if (err.response.data.error === "Captcha Required") {
setShouldShowCaptcha(true);
setIsLoading(false);
return;
}
setLoginError(true);
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
@ -124,6 +132,11 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
});
}
if (captchaRef.current) {
captchaRef.current.resetCaptcha();
}
setCaptchaToken("");
setIsLoading(false);
};
@ -245,8 +258,19 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
className="select:-webkit-autofill:focus h-10"
/>
</div>
{shouldShowCaptcha && (
<div className="mt-4">
<HCaptcha
theme="dark"
sitekey={CAPTCHA_SITE_KEY}
onVerify={(token) => setCaptchaToken(token)}
ref={captchaRef}
/>
</div>
)}
<div className="mt-3 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
disabled={shouldShowCaptcha && captchaToken === ""}
type="submit"
size="sm"
isFullWidth

View File

@ -1,13 +1,15 @@
import { useState } from "react";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import HCaptcha from "@hcaptcha/react-hcaptcha";
import axios from "axios";
import jwt_decode from "jwt-decode";
import { createNotification } from "@app/components/notifications";
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
import { Button, Input } from "@app/components/v2";
import { useUpdateUserAuthMethods } from "@app/hooks/api";
import { useSelectOrganization } from "@app/hooks/api/auth/queries";
@ -41,6 +43,10 @@ export const PasswordStep = ({
providerAuthToken
) as any;
const [captchaToken, setCaptchaToken] = useState("");
const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false);
const captchaRef = useRef<HCaptcha>(null);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
try {
@ -51,7 +57,8 @@ export const PasswordStep = ({
const isCliLoginSuccessful = await attemptCliLogin({
email,
password,
providerAuthToken
providerAuthToken,
captchaToken
});
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
@ -99,7 +106,8 @@ export const PasswordStep = ({
const loginAttempt = await attemptLogin({
email,
password,
providerAuthToken
providerAuthToken,
captchaToken
});
if (loginAttempt && loginAttempt.success) {
@ -158,11 +166,21 @@ export const PasswordStep = ({
return;
}
if (err.response.data.error === "Captcha Required") {
setShouldShowCaptcha(true);
return;
}
createNotification({
text: "Login unsuccessful. Double-check your master password and try again.",
type: "error"
});
}
if (captchaRef.current) {
captchaRef.current.resetCaptcha();
}
setCaptchaToken("");
};
return (
@ -194,8 +212,19 @@ export const PasswordStep = ({
/>
</div>
</div>
{shouldShowCaptcha && (
<div className="mx-auto mt-4 flex w-full min-w-[22rem] items-center justify-center lg:w-1/6">
<HCaptcha
theme="dark"
sitekey={CAPTCHA_SITE_KEY}
onVerify={(token) => setCaptchaToken(token)}
ref={captchaRef}
/>
</div>
)}
<div className="mx-auto mt-4 flex w-1/4 w-full min-w-[22rem] items-center justify-center rounded-md text-center lg:w-1/6">
<Button
disabled={shouldShowCaptcha && captchaToken === ""}
type="submit"
colorSchema="primary"
variant="outline_bg"

View File

@ -45,7 +45,6 @@ export const computeImportedSecretRows = (
if (importedSecIndex === -1) return [];
const importedSec = importSecrets[importedSecIndex];
const overridenSec: Record<string, { env: string; secretPath: string }> = {};
for (let i = importedSecIndex + 1; i < importSecrets.length; i += 1) {
@ -61,11 +60,28 @@ export const computeImportedSecretRows = (
overridenSec[el.key] = { env: SECRET_IN_DASHBOARD, secretPath: "" };
});
return importedSec.secrets.map(({ key, value }) => ({
key,
value,
overriden: overridenSec?.[key]
}));
const importedEntry: Record<string, boolean> = {};
const importedSecretEntries: {
key: string;
value: string;
overriden: {
env: string;
secretPath: string;
};
}[] = [];
importedSec.secrets.forEach(({ key, value }) => {
if (!importedEntry[key]) {
importedSecretEntries.push({
key,
value,
overriden: overridenSec?.[key]
});
importedEntry[key] = true;
}
});
return importedSecretEntries;
};
type Props = {
@ -159,8 +175,9 @@ export const SecretImportListView = ({
importEnv.slug === environment &&
isReserved &&
importPath ===
`${secretPath === "/" ? "" : secretPath}/${ReservedFolders.SecretReplication
}${replicationImportId}`
`${secretPath === "/" ? "" : secretPath}/${
ReservedFolders.SecretReplication
}${replicationImportId}`
);
if (reservedImport) {
setReplicationSecrets((state) => ({
@ -206,8 +223,9 @@ export const SecretImportListView = ({
isOpen={popUp.deleteSecretImport.isOpen}
deleteKey="unlink"
title="Do you want to remove this secret import?"
subTitle={`This will unlink secrets from environment ${(popUp.deleteSecretImport?.data as TSecretImport)?.importEnv
} of path ${(popUp.deleteSecretImport?.data as TSecretImport)?.importPath}?`}
subTitle={`This will unlink secrets from environment ${
(popUp.deleteSecretImport?.data as TSecretImport)?.importEnv
} of path ${(popUp.deleteSecretImport?.data as TSecretImport)?.importPath}?`}
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
onDeleteApproved={handleSecretImportDelete}
/>

View File

@ -393,15 +393,15 @@ export const SecretDetailSidebar = ({
{(isAllowed) => (
<Switch
id="skipmultiencoding-option"
onCheckedChange={(isChecked) => onChange(!isChecked)}
isChecked={!value}
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
onBlur={onBlur}
isDisabled={!isAllowed}
className="items-center"
>
Enable multi line encoding
Multi line encoding
<Tooltip
content="Infisical encodes multiline secrets by escaping newlines and wrapping in quotes. To disable, enable this option"
content="When enabled, multiline secrets will be handled by escaping newlines and enclosing the entire value in double quotes."
className="z-[100]"
>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />

View File

@ -454,12 +454,12 @@ export const SecretOverviewPage = () => {
const filteredSecretNames = secKeys
?.filter((name) => name.toUpperCase().includes(searchFilter.toUpperCase()))
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
const filteredFolderNames = folderNames?.filter((name) =>
name.toLowerCase().includes(searchFilter.toLowerCase())
);
const filteredDynamicSecrets = dynamicSecretNames?.filter((name) =>
name.toLowerCase().includes(searchFilter.toLowerCase())
);
const filteredFolderNames = folderNames
?.filter((name) => name.toLowerCase().includes(searchFilter.toLowerCase()))
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
const filteredDynamicSecrets = dynamicSecretNames
?.filter((name) => name.toLowerCase().includes(searchFilter.toLowerCase()))
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
const isTableEmpty =
!(

View File

@ -178,7 +178,7 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
errorText={error?.message}
>
<SecretInput
isVisible
isVisible={false}
{...field}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[100px]"
/>

View File

@ -232,7 +232,6 @@ func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context
}
managedKubeSecret.Data = plainProcessedSecrets
managedKubeSecret.ObjectMeta.Annotations = map[string]string{}
managedKubeSecret.ObjectMeta.Annotations[SECRET_VERSION_ANNOTATION] = ETag
err := r.Client.Update(ctx, &managedKubeSecret)