1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-25 14:05:03 +00:00

Compare commits

..

45 Commits

Author SHA1 Message Date
7154b19703 update azure app connection docs 2025-02-05 19:26:14 -05:00
9ce465b3e2 Update azure-app-configuration.mdx 2025-02-05 19:22:05 -05:00
598e5c0be5 Update azure-app-configuration.mdx 2025-02-05 19:16:57 -05:00
72f08a6b89 Merge pull request from Infisical/fix-dashboard-search-exclude-replicas
Fix: Exclude Reserved Folders from Deep Search Folder Query
2025-02-05 13:58:05 -08:00
55d8762351 fix: exclude reserved folders from deep search 2025-02-05 13:53:14 -08:00
3c92ec4dc3 Merge pull request from akhilmhdh/fix/increare-gcp-sa-limit
feat: increased identity gcp auth cred limit from 255 to respective limits
2025-02-06 01:53:55 +05:30
f2224262a4 Merge pull request from Infisical/misc/removed-unused-and-outdated-metadata-field
misc: removed outdated metadata field
2025-02-05 12:19:24 -05:00
23eac40740 Merge pull request from Infisical/secrets-overview-page-move-secrets
Feature: Secrets Overview Page Move Secrets
2025-02-05 08:54:06 -08:00
4ae88c0447 misc: removed outdated metadata field 2025-02-05 18:55:16 +08:00
=
7aecaad050 feat: increased identity gcp auth cred limit from 255 to respective limits 2025-02-05 10:38:10 +05:30
cf61390e52 improvements: address feedback 2025-02-04 20:14:47 -08:00
7adc103ed2 Merge pull request from Infisical/app-connections-and-secret-syncs-unique-constraint
Fix: Move App Connection and Secret Sync Unique Name Constraint to DB
2025-02-04 09:42:02 -08:00
5bdbf37171 improvement: add error codes enum for re-use 2025-02-04 08:37:06 -08:00
4f874734ab Update operator version 2025-02-04 10:10:59 -05:00
eb6fd8259b Merge pull request from Infisical/combine-helm-release
Combine image release with helm
2025-02-04 10:07:52 -05:00
1766a44dd0 Combine image release with helm
Combine image release with helm release so that one happens after the other. This will help reduce manual work.
2025-02-04 09:59:32 -05:00
624c9ef8da Merge pull request from akhilmhdh/fix/base64-decode-issue
Resolved base64 decode saving file as ansii
2025-02-04 20:04:02 +05:30
=
dfd4b13574 fix: resolved base64 decode saving file as ansii 2025-02-04 16:14:28 +05:30
22b57b7a74 chore: add migration file 2025-02-03 19:40:00 -08:00
1ba0b9c204 improvement: move unique name constraint to db for secret syncs and app connections 2025-02-03 19:36:37 -08:00
a903537441 fix: clear selection if modal is closed through cancel button and secrets have been moved 2025-02-03 18:44:52 -08:00
92c4d83714 improvement: make results look better 2025-02-03 18:29:38 -08:00
a6414104ad feature: secrets overview page move secrets 2025-02-03 18:18:00 -08:00
110d0e95b0 Merge pull request from carlosvargas9103/carlosvargas9103-fix-typo-readme
fixed typo in README.md
2025-02-03 13:26:32 -05:00
a8c0bbb7ca Merge pull request from Infisical/update-security-docs
Update Security Docs
2025-02-03 10:13:26 -08:00
6af8a4fab8 Update security docs 2025-02-03 10:07:57 -08:00
43ecd31b74 fixed typo in README.md 2025-02-03 16:18:17 +01:00
ccee0f5428 Merge pull request from Infisical/fix-oidc-doc-images
Fix: Remove Relative Paths for ODIC Overview Docs
2025-01-31 15:33:40 -08:00
14586c7cd0 fix: remove relative path for oidc docs 2025-01-31 15:30:38 -08:00
7090eea716 Merge pull request from Infisical/oidc-group-membership-mapping
Feature: OIDC Group Membership Mapping
2025-01-31 11:32:38 -08:00
01d3443139 improvement: update docker dev and makefile for keycloak dev 2025-01-31 11:14:49 -08:00
c4b23a8d4f improvement: improve grammar 2025-01-31 11:05:56 -08:00
90a2a11fff improvement: update tooltips 2025-01-31 11:04:20 -08:00
95d7c2082c improvements: address feedback 2025-01-31 11:01:54 -08:00
ab5eb4c696 Merge pull request from Infisical/misc/readded-operator-installation-flag
misc: readded operator installation flag for secret CRD
2025-01-31 16:53:57 +08:00
65aeb81934 Merge pull request from xinbenlv/patch-1
Fix grammar on overview.mdx
2025-01-31 14:22:03 +05:30
a406511405 Merge pull request from isaiahmartin847/refactor/copy-secret
Improve Visibility and Alignment of Tooltips and Copy Secret Key Icon
2025-01-31 14:20:02 +05:30
61da0db49e misc: readded operator installation flag for CRD 2025-01-31 16:03:42 +08:00
59666740ca chore: revert license and remove unused query key/doc reference 2025-01-30 10:35:23 -08:00
9cc7edc869 feature: oidc group membership mapping 2025-01-30 10:21:30 -08:00
e1b016f76d Merge pull request from nicogiard/patch-1
fix: wrong client variable in c# code example
2025-01-29 22:24:03 +01:00
1175b9b5af fix: wrong client variable
The InfisicalClient variable was wrong
2025-01-29 21:57:57 +01:00
92f697e195 I removed the hover opacity on the 'copy secret name' icon so the icon is always visible instead of appearing only on hover. I believe this will make it more noticeable to users.
As a user myself, I didn't realize it was possible to copy a secret name until I accidentally hovered over it.
2025-01-27 12:26:22 -07:00
8062f0238b I added a wrapper div with a class of relative to make the icon and tooltip align vertically inline. 2025-01-27 12:25:38 -07:00
645dfafba0 Fix grammar on overview.mdx 2025-01-20 09:02:18 -08:00
67 changed files with 1545 additions and 761 deletions
.github/workflows
MakefileREADME.md
backend/src
docker-compose.dev.yml
docs
frontend/src
hooks/api
pages
organization
AuditLogsPage/components
GroupDetailsByIDPage/components/GroupMembersSection
SettingsPage/components/OrgAuthTab
secret-manager/OverviewPage/components
SecretOverviewTableRow
SelectionPanel
helm-charts
k8-operator
config/samples/crd/infisicalsecret
controllers/infisicalsecret

@ -1,4 +1,4 @@
name: Release Helm Charts
name: Release Infisical Core Helm chart
on: [workflow_dispatch]
@ -17,6 +17,6 @@ jobs:
- name: Install Cloudsmith CLI
run: pip install --upgrade cloudsmith-cli
- name: Build and push helm package to Cloudsmith
run: cd helm-charts && sh upload-to-cloudsmith.sh
run: cd helm-charts && sh upload-infisical-core-helm-cloudsmith.sh
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}

@ -1,4 +1,4 @@
name: Release Docker image for K8 operator
name: Release image + Helm chart K8s Operator
on:
push:
tags:
@ -35,3 +35,18 @@ jobs:
tags: |
infisical/kubernetes-operator:latest
infisical/kubernetes-operator:${{ steps.extract_version.outputs.version }}
- name: Checkout
uses: actions/checkout@v2
- name: Install Helm
uses: azure/setup-helm@v3
with:
version: v3.10.0
- name: Install python
uses: actions/setup-python@v4
- name: Install Cloudsmith CLI
run: pip install --upgrade cloudsmith-cli
- name: Build and push helm package to Cloudsmith
run: cd helm-charts && sh upload-k8s-operator-cloudsmith.sh
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}

@ -30,3 +30,6 @@ reviewable-api:
npm run type:check
reviewable: reviewable-ui reviewable-api
up-dev-sso:
docker compose -f docker-compose.dev.yml --profile sso up --build

@ -125,7 +125,7 @@ Install pre commit hook to scan each commit before you push to your repository
infisical scan install --pre-commit-hook
```
Lean about Infisical's code scanning feature [here](https://infisical.com/docs/cli/scanning-overview)
Learn about Infisical's code scanning feature [here](https://infisical.com/docs/cli/scanning-overview)
## Open-source vs. paid

@ -0,0 +1,23 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasManageGroupMembershipsCol = await knex.schema.hasColumn(TableName.OidcConfig, "manageGroupMemberships");
await knex.schema.alterTable(TableName.OidcConfig, (tb) => {
if (!hasManageGroupMembershipsCol) {
tb.boolean("manageGroupMemberships").notNullable().defaultTo(false);
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasManageGroupMembershipsCol = await knex.schema.hasColumn(TableName.OidcConfig, "manageGroupMemberships");
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
if (hasManageGroupMembershipsCol) {
t.dropColumn("manageGroupMemberships");
}
});
}

@ -0,0 +1,23 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.AppConnection, (t) => {
t.unique(["orgId", "name"]);
});
await knex.schema.alterTable(TableName.SecretSync, (t) => {
t.unique(["projectId", "name"]);
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.AppConnection, (t) => {
t.dropUnique(["orgId", "name"]);
});
await knex.schema.alterTable(TableName.SecretSync, (t) => {
t.dropUnique(["projectId", "name"]);
});
}

@ -0,0 +1,36 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasTable = await knex.schema.hasTable(TableName.IdentityGcpAuth);
const hasAllowedProjectsColumn = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedProjects");
const hasAllowedServiceAccountsColumn = await knex.schema.hasColumn(
TableName.IdentityGcpAuth,
"allowedServiceAccounts"
);
const hasAllowedZones = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedZones");
if (hasTable) {
await knex.schema.alterTable(TableName.IdentityGcpAuth, (t) => {
if (hasAllowedProjectsColumn) t.string("allowedProjects", 2500).alter();
if (hasAllowedServiceAccountsColumn) t.string("allowedServiceAccounts", 5000).alter();
if (hasAllowedZones) t.string("allowedZones", 2500).alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasTable = await knex.schema.hasTable(TableName.IdentityGcpAuth);
const hasAllowedProjectsColumn = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedProjects");
const hasAllowedServiceAccountsColumn = await knex.schema.hasColumn(
TableName.IdentityGcpAuth,
"allowedServiceAccounts"
);
const hasAllowedZones = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedZones");
if (hasTable) {
await knex.schema.alterTable(TableName.IdentityGcpAuth, (t) => {
if (hasAllowedProjectsColumn) t.string("allowedProjects").alter();
if (hasAllowedServiceAccountsColumn) t.string("allowedServiceAccounts").alter();
if (hasAllowedZones) t.string("allowedZones").alter();
});
}
}

@ -17,9 +17,9 @@ export const IdentityGcpAuthsSchema = z.object({
updatedAt: z.date(),
identityId: z.string().uuid(),
type: z.string(),
allowedServiceAccounts: z.string(),
allowedProjects: z.string(),
allowedZones: z.string()
allowedServiceAccounts: z.string().nullable().optional(),
allowedProjects: z.string().nullable().optional(),
allowedZones: z.string().nullable().optional()
});
export type TIdentityGcpAuths = z.infer<typeof IdentityGcpAuthsSchema>;

@ -27,7 +27,8 @@ export const OidcConfigsSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
orgId: z.string().uuid(),
lastUsed: z.date().nullable().optional()
lastUsed: z.date().nullable().optional(),
manageGroupMemberships: z.boolean().default(false)
});
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;

@ -153,7 +153,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
discoveryURL: true,
isActive: true,
orgId: true,
allowedEmailDomains: true
allowedEmailDomains: true,
manageGroupMemberships: true
}).extend({
clientId: z.string(),
clientSecret: z.string()
@ -207,7 +208,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
userinfoEndpoint: z.string().trim(),
clientId: z.string().trim(),
clientSecret: z.string().trim(),
isActive: z.boolean()
isActive: z.boolean(),
manageGroupMemberships: z.boolean().optional()
})
.partial()
.merge(z.object({ orgSlug: z.string() })),
@ -223,7 +225,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
userinfoEndpoint: true,
orgId: true,
allowedEmailDomains: true,
isActive: true
isActive: true,
manageGroupMemberships: true
})
}
},
@ -272,7 +275,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
clientId: z.string().trim(),
clientSecret: z.string().trim(),
isActive: z.boolean(),
orgSlug: z.string().trim()
orgSlug: z.string().trim(),
manageGroupMemberships: z.boolean().optional().default(false)
})
.superRefine((data, ctx) => {
if (data.configurationType === OIDCConfigurationType.CUSTOM) {
@ -334,7 +338,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
userinfoEndpoint: true,
orgId: true,
isActive: true,
allowedEmailDomains: true
allowedEmailDomains: true,
manageGroupMemberships: true
})
}
},
@ -350,4 +355,25 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
return oidc;
}
});
server.route({
method: "GET",
url: "/manage-group-memberships",
schema: {
querystring: z.object({
orgId: z.string().trim().min(1, "Org ID is required")
}),
response: {
200: z.object({
isEnabled: z.boolean()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const isEnabled = await server.services.oidc.isOidcManageGroupMembershipsEnabled(req.query.orgId, req.permission);
return { isEnabled };
}
});
};

@ -249,7 +249,9 @@ export enum EventType {
DELETE_SECRET_SYNC = "delete-secret-sync",
SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets",
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets"
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user"
}
interface UserActorMetadata {
@ -760,9 +762,9 @@ interface AddIdentityGcpAuthEvent {
metadata: {
identityId: string;
type: string;
allowedServiceAccounts: string;
allowedProjects: string;
allowedZones: string;
allowedServiceAccounts?: string | null;
allowedProjects?: string | null;
allowedZones?: string | null;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
@ -782,9 +784,9 @@ interface UpdateIdentityGcpAuthEvent {
metadata: {
identityId: string;
type?: string;
allowedServiceAccounts?: string;
allowedProjects?: string;
allowedZones?: string;
allowedServiceAccounts?: string | null;
allowedProjects?: string | null;
allowedZones?: string | null;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
@ -2044,6 +2046,26 @@ interface SecretSyncRemoveSecretsEvent {
};
}
interface OidcGroupMembershipMappingAssignUserEvent {
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER;
metadata: {
assignedToGroups: { id: string; name: string }[];
userId: string;
userEmail: string;
userGroupsClaim: string[];
};
}
interface OidcGroupMembershipMappingRemoveUserEvent {
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER;
metadata: {
removedFromGroups: { id: string; name: string }[];
userId: string;
userEmail: string;
userGroupsClaim: string[];
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@ -2232,4 +2254,6 @@ export type Event =
| DeleteSecretSyncEvent
| SecretSyncSyncSecretsEvent
| SecretSyncImportSecretsEvent
| SecretSyncRemoveSecretsEvent;
| SecretSyncRemoveSecretsEvent
| OidcGroupMembershipMappingAssignUserEvent
| OidcGroupMembershipMappingRemoveUserEvent;

@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
@ -45,6 +46,7 @@ type TGroupServiceFactoryDep = {
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">;
};
export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
@ -59,7 +61,8 @@ export const groupServiceFactory = ({
projectBotDAL,
projectKeyDAL,
permissionService,
licenseService
licenseService,
oidcConfigDAL
}: TGroupServiceFactoryDep) => {
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
@ -311,6 +314,18 @@ export const groupServiceFactory = ({
message: `Failed to find group with ID ${id}`
});
const oidcConfig = await oidcConfigDAL.findOne({
orgId: group.orgId,
isActive: true
});
if (oidcConfig?.manageGroupMemberships) {
throw new BadRequestError({
message:
"Cannot add user to group: OIDC group membership mapping is enabled - user must be assigned to this group in your OIDC provider."
});
}
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group
@ -366,6 +381,18 @@ export const groupServiceFactory = ({
message: `Failed to find group with ID ${id}`
});
const oidcConfig = await oidcConfigDAL.findOne({
orgId: group.orgId,
isActive: true
});
if (oidcConfig?.manageGroupMemberships) {
throw new BadRequestError({
message:
"Cannot remove user from group: OIDC group membership mapping is enabled - user must be removed from this group in your OIDC provider."
});
}
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group

@ -5,6 +5,11 @@ import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet }
import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@ -18,13 +23,18 @@ import {
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { OrgServiceActor } from "@app/lib/types";
import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
@ -45,7 +55,14 @@ import {
type TOidcConfigServiceFactoryDep = {
userDAL: Pick<
TUserDALFactory,
"create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId"
| "create"
| "findOne"
| "updateById"
| "findById"
| "findUserEncKeyByUserId"
| "findUserEncKeyByUserIdsBatch"
| "find"
| "transaction"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
orgDAL: Pick<
@ -57,8 +74,23 @@ type TOidcConfigServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail" | "verify">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getUserOrgPermission">;
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">;
groupDAL: Pick<TGroupDALFactory, "findByOrgId">;
userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory,
| "find"
| "transaction"
| "insertMany"
| "findGroupMembershipsByUserIdInOrg"
| "delete"
| "filterProjectsByUserMembership"
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
};
export type TOidcConfigServiceFactory = ReturnType<typeof oidcConfigServiceFactory>;
@ -73,7 +105,14 @@ export const oidcConfigServiceFactory = ({
tokenService,
orgBotDAL,
smtpService,
oidcConfigDAL
oidcConfigDAL,
userGroupMembershipDAL,
groupDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
auditLogService
}: TOidcConfigServiceFactoryDep) => {
const getOidc = async (dto: TGetOidcCfgDTO) => {
const org = await orgDAL.findOne({ slug: dto.orgSlug });
@ -156,11 +195,21 @@ export const oidcConfigServiceFactory = ({
isActive: oidcCfg.isActive,
allowedEmailDomains: oidcCfg.allowedEmailDomains,
clientId,
clientSecret
clientSecret,
manageGroupMemberships: oidcCfg.manageGroupMemberships
};
};
const oidcLogin = async ({ externalId, email, firstName, lastName, orgId, callbackPort }: TOidcLoginDTO) => {
const oidcLogin = async ({
externalId,
email,
firstName,
lastName,
orgId,
callbackPort,
groups = [],
manageGroupMemberships
}: TOidcLoginDTO) => {
const serverCfg = await getServerCfg();
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) {
@ -315,6 +364,83 @@ export const oidcConfigServiceFactory = ({
});
}
if (manageGroupMemberships) {
const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(user.id, orgId);
const orgGroups = await groupDAL.findByOrgId(orgId);
const userGroupsNames = userGroups.map((membership) => membership.groupName);
const missingGroupsMemberships = groups.filter((groupName) => !userGroupsNames.includes(groupName));
const groupsToAddUserTo = orgGroups.filter((group) => missingGroupsMemberships.includes(group.name));
for await (const group of groupsToAddUserTo) {
await addUsersToGroupByUserIds({
userIds: [user.id],
group,
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL
});
}
if (groupsToAddUserTo.length) {
await auditLogService.createAuditLog({
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
orgId,
event: {
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER,
metadata: {
userId: user.id,
userEmail: user.email ?? user.username,
assignedToGroups: groupsToAddUserTo.map(({ id, name }) => ({ id, name })),
userGroupsClaim: groups
}
}
});
}
const membershipsToRemove = userGroups
.filter((membership) => !groups.includes(membership.groupName))
.map((membership) => membership.groupId);
const groupsToRemoveUserFrom = orgGroups.filter((group) => membershipsToRemove.includes(group.id));
for await (const group of groupsToRemoveUserFrom) {
await removeUsersFromGroupByUserIds({
userIds: [user.id],
group,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL
});
}
if (groupsToRemoveUserFrom.length) {
await auditLogService.createAuditLog({
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
orgId,
event: {
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER,
metadata: {
userId: user.id,
userEmail: user.email ?? user.username,
removedFromGroups: groupsToRemoveUserFrom.map(({ id, name }) => ({ id, name })),
userGroupsClaim: groups
}
}
});
}
}
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
@ -385,7 +511,8 @@ export const oidcConfigServiceFactory = ({
tokenEndpoint,
userinfoEndpoint,
clientId,
clientSecret
clientSecret,
manageGroupMemberships
}: TUpdateOidcCfgDTO) => {
const org = await orgDAL.findOne({
slug: orgSlug
@ -448,7 +575,8 @@ export const oidcConfigServiceFactory = ({
userinfoEndpoint,
jwksUri,
isActive,
lastUsed: null
lastUsed: null,
manageGroupMemberships
};
if (clientId !== undefined) {
@ -491,7 +619,8 @@ export const oidcConfigServiceFactory = ({
tokenEndpoint,
userinfoEndpoint,
clientId,
clientSecret
clientSecret,
manageGroupMemberships
}: TCreateOidcCfgDTO) => {
const org = await orgDAL.findOne({
slug: orgSlug
@ -589,7 +718,8 @@ export const oidcConfigServiceFactory = ({
clientIdTag,
encryptedClientSecret,
clientSecretIV,
clientSecretTag
clientSecretTag,
manageGroupMemberships
});
return oidcCfg;
@ -683,7 +813,9 @@ export const oidcConfigServiceFactory = ({
firstName: claims.given_name ?? "",
lastName: claims.family_name ?? "",
orgId: org.id,
callbackPort
groups: claims.groups as string[] | undefined,
callbackPort,
manageGroupMemberships: oidcCfg.manageGroupMemberships
})
.then(({ isUserCompleted, providerAuthToken }) => {
cb(null, { isUserCompleted, providerAuthToken });
@ -697,5 +829,16 @@ export const oidcConfigServiceFactory = ({
return strategy;
};
return { oidcLogin, getOrgAuthStrategy, getOidc, updateOidcCfg, createOidcCfg };
const isOidcManageGroupMembershipsEnabled = async (orgId: string, actor: OrgServiceActor) => {
await permissionService.getUserOrgPermission(actor.id, orgId, actor.authMethod, actor.orgId);
const oidcConfig = await oidcConfigDAL.findOne({
orgId,
isActive: true
});
return Boolean(oidcConfig?.manageGroupMemberships);
};
return { oidcLogin, getOrgAuthStrategy, getOidc, updateOidcCfg, createOidcCfg, isOidcManageGroupMembershipsEnabled };
};

@ -12,6 +12,8 @@ export type TOidcLoginDTO = {
lastName?: string;
orgId: string;
callbackPort?: string;
groups?: string[];
manageGroupMemberships?: boolean | null;
};
export type TGetOidcCfgDTO =
@ -37,6 +39,7 @@ export type TCreateOidcCfgDTO = {
clientSecret: string;
isActive: boolean;
orgSlug: string;
manageGroupMemberships: boolean;
} & TGenericPermission;
export type TUpdateOidcCfgDTO = Partial<{
@ -52,5 +55,6 @@ export type TUpdateOidcCfgDTO = Partial<{
clientSecret: string;
isActive: boolean;
orgSlug: string;
manageGroupMemberships: boolean;
}> &
TGenericPermission;

@ -0,0 +1,4 @@
export enum DatabaseErrorCode {
ForeignKeyViolation = "23503",
UniqueViolation = "23505"
}

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

@ -467,7 +467,8 @@ export const registerRoutes = async (
projectBotDAL,
projectKeyDAL,
permissionService,
licenseService
licenseService,
oidcConfigDAL
});
const groupProjectService = groupProjectServiceFactory({
groupDAL,
@ -1337,7 +1338,14 @@ export const registerRoutes = async (
smtpService,
orgBotDAL,
permissionService,
oidcConfigDAL
oidcConfigDAL,
projectBotDAL,
projectKeyDAL,
projectDAL,
userGroupMembershipDAL,
groupProjectDAL,
groupDAL,
auditLogService
});
const userEngagementService = userEngagementServiceFactory({

@ -110,7 +110,6 @@ export const secretRawSchema = z.object({
secretReminderNote: z.string().nullable().optional(),
secretReminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
metadata: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});

@ -3,6 +3,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { generateHash } from "@app/lib/crypto/encryption";
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
@ -144,54 +145,40 @@ export const appConnectionServiceFactory = ({
OrgPermissionSubjects.AppConnections
);
const appConnection = await appConnectionDAL.transaction(async (tx) => {
const isConflictingName = Boolean(
await appConnectionDAL.findOne(
{
name: params.name,
orgId: actor.orgId
},
tx
)
);
const validatedCredentials = await validateAppConnectionCredentials({
app,
credentials,
method,
orgId: actor.orgId
} as TAppConnectionConfig);
if (isConflictingName)
throw new BadRequestError({
message: `An App Connection with the name "${params.name}" already exists`
});
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: validatedCredentials,
orgId: actor.orgId,
kmsService
});
const validatedCredentials = await validateAppConnectionCredentials({
app,
credentials,
method,
orgId: actor.orgId
} as TAppConnectionConfig);
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: validatedCredentials,
try {
const connection = await appConnectionDAL.create({
orgId: actor.orgId,
kmsService
encryptedCredentials,
method,
app,
...params
});
const connection = await appConnectionDAL.create(
{
orgId: actor.orgId,
encryptedCredentials,
method,
app,
...params
},
tx
);
return {
...connection,
credentialsHash: generateHash(connection.encryptedCredentials),
credentials: validatedCredentials
};
});
} as TAppConnection;
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
throw new BadRequestError({ message: `An App Connection with the name "${params.name}" already exists` });
}
return appConnection as TAppConnection;
throw err;
}
};
const updateAppConnection = async (
@ -215,72 +202,55 @@ export const appConnectionServiceFactory = ({
OrgPermissionSubjects.AppConnections
);
const updatedAppConnection = await appConnectionDAL.transaction(async (tx) => {
if (params.name && appConnection.name !== params.name) {
const isConflictingName = Boolean(
await appConnectionDAL.findOne(
{
name: params.name,
orgId: appConnection.orgId
},
tx
)
);
let encryptedCredentials: undefined | Buffer;
if (isConflictingName)
throw new BadRequestError({
message: `An App Connection with the name "${params.name}" already exists`
});
}
if (credentials) {
const { app, method } = appConnection as DiscriminativePick<TAppConnectionConfig, "app" | "method">;
let encryptedCredentials: undefined | Buffer;
if (credentials) {
const { app, method } = appConnection as DiscriminativePick<TAppConnectionConfig, "app" | "method">;
if (
!VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[app].safeParse({
method,
credentials
}).success
)
throw new BadRequestError({
message: `Invalid credential format for ${
APP_CONNECTION_NAME_MAP[app]
} Connection with method ${getAppConnectionMethodName(method)}`
});
const validatedCredentials = await validateAppConnectionCredentials({
app,
orgId: actor.orgId,
credentials,
method
} as TAppConnectionConfig);
if (!validatedCredentials)
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
encryptedCredentials = await encryptAppConnectionCredentials({
credentials: validatedCredentials,
orgId: actor.orgId,
kmsService
if (
!VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[app].safeParse({
method,
credentials
}).success
)
throw new BadRequestError({
message: `Invalid credential format for ${
APP_CONNECTION_NAME_MAP[app]
} Connection with method ${getAppConnectionMethodName(method)}`
});
const validatedCredentials = await validateAppConnectionCredentials({
app,
orgId: actor.orgId,
credentials,
method
} as TAppConnectionConfig);
if (!validatedCredentials)
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
encryptedCredentials = await encryptAppConnectionCredentials({
credentials: validatedCredentials,
orgId: actor.orgId,
kmsService
});
}
try {
const updatedConnection = await appConnectionDAL.updateById(connectionId, {
orgId: actor.orgId,
encryptedCredentials,
...params
});
return await decryptAppConnection(updatedConnection, kmsService);
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
throw new BadRequestError({ message: `An App Connection with the name "${params.name}" already exists` });
}
const updatedConnection = await appConnectionDAL.updateById(
connectionId,
{
orgId: actor.orgId,
encryptedCredentials,
...params
},
tx
);
return updatedConnection;
});
return decryptAppConnection(updatedAppConnection, kmsService);
throw err;
}
};
const deleteAppConnection = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
@ -311,7 +281,10 @@ export const appConnectionServiceFactory = ({
return await decryptAppConnection(deletedAppConnection, kmsService);
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === "23503") {
if (
err instanceof DatabaseError &&
(err.error as { code: string })?.code === DatabaseErrorCode.ForeignKeyViolation
) {
throw new BadRequestError({
message:
"Cannot delete App Connection with existing connections. Remove all existing connections and try again."

@ -493,6 +493,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
db.ref("parents.environment")
)
.from(TableName.SecretFolder)
.where(`${TableName.SecretFolder}.isReserved`, false)
.join("parents", `${TableName.SecretFolder}.parentId`, "parents.id");
})
)

@ -123,47 +123,39 @@ export const secretSyncDALFactory = (
};
const create = async (data: Parameters<(typeof secretSyncOrm)["create"]>[0]) => {
try {
const secretSync = (await secretSyncOrm.transaction(async (tx) => {
const sync = await secretSyncOrm.create(data, tx);
const secretSync = (await secretSyncOrm.transaction(async (tx) => {
const sync = await secretSyncOrm.create(data, tx);
return baseSecretSyncQuery({
filter: { id: sync.id },
db,
tx
}).first();
}))!;
return baseSecretSyncQuery({
filter: { id: sync.id },
db,
tx
}).first();
}))!;
// TODO (scott): replace with cached folder path once implemented
const [folderWithPath] = secretSync.folderId
? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId])
: [];
return expandSecretSync(secretSync, folderWithPath);
} catch (error) {
throw new DatabaseError({ error, name: "Create - Secret Sync" });
}
// TODO (scott): replace with cached folder path once implemented
const [folderWithPath] = secretSync.folderId
? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId])
: [];
return expandSecretSync(secretSync, folderWithPath);
};
const updateById = async (syncId: string, data: Parameters<(typeof secretSyncOrm)["updateById"]>[1]) => {
try {
const secretSync = (await secretSyncOrm.transaction(async (tx) => {
const sync = await secretSyncOrm.updateById(syncId, data, tx);
const secretSync = (await secretSyncOrm.transaction(async (tx) => {
const sync = await secretSyncOrm.updateById(syncId, data, tx);
return baseSecretSyncQuery({
filter: { id: sync.id },
db,
tx
}).first();
}))!;
return baseSecretSyncQuery({
filter: { id: sync.id },
db,
tx
}).first();
}))!;
// TODO (scott): replace with cached folder path once implemented
const [folderWithPath] = secretSync.folderId
? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId])
: [];
return expandSecretSync(secretSync, folderWithPath);
} catch (error) {
throw new DatabaseError({ error, name: "Update by ID - Secret Sync" });
}
// TODO (scott): replace with cached folder path once implemented
const [folderWithPath] = secretSync.folderId
? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId])
: [];
return expandSecretSync(secretSync, folderWithPath);
};
const findOne = async (filter: Parameters<(typeof secretSyncOrm)["findOne"]>[0], tx?: Knex) => {

@ -8,7 +8,8 @@ import {
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
@ -197,37 +198,26 @@ export const secretSyncServiceFactory = ({
// validates permission to connect and app is valid for sync destination
await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor);
const secretSync = await secretSyncDAL.transaction(async (tx) => {
const isConflictingName = Boolean(
(
await secretSyncDAL.find(
{
name: params.name,
projectId
},
tx
)
).length
);
if (isConflictingName)
throw new BadRequestError({
message: `A Secret Sync with the name "${params.name}" already exists for the project with ID "${folder.projectId}"`
});
const sync = await secretSyncDAL.create({
try {
const secretSync = await secretSyncDAL.create({
folderId: folder.id,
...params,
...(params.isAutoSyncEnabled && { syncStatus: SecretSyncStatus.Pending }),
projectId
});
return sync;
});
if (secretSync.isAutoSyncEnabled) await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId: secretSync.id });
if (secretSync.isAutoSyncEnabled) await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId: secretSync.id });
return secretSync as TSecretSync;
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
throw new BadRequestError({
message: `A Secret Sync with the name "${params.name}" already exists for the project with ID "${folder.projectId}"`
});
}
return secretSync as TSecretSync;
throw err;
}
};
const updateSecretSync = async (
@ -260,78 +250,65 @@ export const secretSyncServiceFactory = ({
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
});
const updatedSecretSync = await secretSyncDAL.transaction(async (tx) => {
let { folderId } = secretSync;
let { folderId } = secretSync;
if (params.connectionId) {
const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync];
if (params.connectionId) {
const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync];
// validates permission to connect and app is valid for sync destination
await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor);
}
// validates permission to connect and app is valid for sync destination
await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor);
}
if (
(secretPath && secretPath !== secretSync.folder?.path) ||
(environment && environment !== secretSync.environment?.slug)
) {
const updatedEnvironment = environment ?? secretSync.environment?.slug;
const updatedSecretPath = secretPath ?? secretSync.folder?.path;
if (
(secretPath && secretPath !== secretSync.folder?.path) ||
(environment && environment !== secretSync.environment?.slug)
) {
const updatedEnvironment = environment ?? secretSync.environment?.slug;
const updatedSecretPath = secretPath ?? secretSync.folder?.path;
if (!updatedEnvironment || !updatedSecretPath)
throw new BadRequestError({ message: "Must specify both source environment and secret path" });
if (!updatedEnvironment || !updatedSecretPath)
throw new BadRequestError({ message: "Must specify both source environment and secret path" });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: updatedEnvironment,
secretPath: updatedSecretPath
})
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: updatedEnvironment,
secretPath: updatedSecretPath
})
);
const newFolder = await folderDAL.findBySecretPath(secretSync.projectId, updatedEnvironment, updatedSecretPath);
const newFolder = await folderDAL.findBySecretPath(secretSync.projectId, updatedEnvironment, updatedSecretPath);
if (!newFolder)
throw new BadRequestError({
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${secretSync.projectId}"`
});
if (!newFolder)
throw new BadRequestError({
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${secretSync.projectId}"`
});
folderId = newFolder.id;
}
folderId = newFolder.id;
}
if (params.name && secretSync.name !== params.name) {
const isConflictingName = Boolean(
(
await secretSyncDAL.find(
{
name: params.name,
projectId: secretSync.projectId
},
tx
)
).length
);
const isAutoSyncEnabled = params.isAutoSyncEnabled ?? secretSync.isAutoSyncEnabled;
if (isConflictingName)
throw new BadRequestError({
message: `A Secret Sync with the name "${params.name}" already exists for project with ID "${secretSync.projectId}"`
});
}
const isAutoSyncEnabled = params.isAutoSyncEnabled ?? secretSync.isAutoSyncEnabled;
const updatedSync = await secretSyncDAL.updateById(syncId, {
try {
const updatedSecretSync = await secretSyncDAL.updateById(syncId, {
...params,
...(isAutoSyncEnabled && folderId && { syncStatus: SecretSyncStatus.Pending }),
folderId
});
return updatedSync;
});
if (updatedSecretSync.isAutoSyncEnabled)
await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId: secretSync.id });
if (updatedSecretSync.isAutoSyncEnabled)
await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId: secretSync.id });
return updatedSecretSync as TSecretSync;
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
throw new BadRequestError({
message: `A Secret Sync with the name "${params.name}" already exists for the project with ID "${secretSync.projectId}"`
});
}
return updatedSecretSync as TSecretSync;
throw err;
}
};
const deleteSecretSync = async (

@ -192,6 +192,17 @@ services:
depends_on:
- openldap
profiles: [ldap]
keycloak:
image: quay.io/keycloak/keycloak:26.1.0
restart: always
environment:
- KC_BOOTSTRAP_ADMIN_PASSWORD=admin
- KC_BOOTSTRAP_ADMIN_USERNAME=admin
command: start-dev
ports:
- 8088:8080
profiles: [ sso ]
volumes:
postgres-data:

@ -1,6 +1,6 @@
---
title: 'Install'
description: "Infisical's CLI is one of the best way to manage environments and secrets. Install it here"
description: "Infisical's CLI is one of the best ways to manage environments and secrets. Install it here"
---
The Infisical CLI is a powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables.
@ -127,4 +127,4 @@ You can use it across various environments, whether it's local development, CI/C
## Quick Usage Guide
<Card color="#00A300" href="./usage">
Now that you have the CLI installed on your system, follow this guide to make the best use of it
</Card>
</Card>

@ -1,112 +0,0 @@
---
title: "Keycloak OIDC"
description: "Learn how to configure Keycloak OIDC for Infisical SSO."
---
<Info>
Keycloak OIDC SSO is a paid feature. If you're using Infisical Cloud, then it
is available under the **Pro Tier**. If you're self-hosting Infisical, then
you should contact sales@infisical.com to purchase an enterprise license to
use it.
</Info>
<Steps>
<Step title="Create an OIDC client application in Keycloak">
1.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application.
![OIDC keycloak list of clients](../../../images/sso/keycloak-oidc/clients-list.png)
<Info>
You dont typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm.
</Info>
1.2. In the General Settings step, set **Client type** to **OpenID Connect**, the **Client ID** field to an appropriate identifier, and the **Name** field to a friendly name like **Infisical**.
![OIDC keycloak create client general settings](../../../images/sso/keycloak-oidc/create-client-general-settings.png)
1.3. Next, in the Capability Config step, ensure that **Client Authentication** is set to On and that **Standard flow** is enabled in the Authentication flow section.
![OIDC keycloak create client capability config settings](../../../images/sso/keycloak-oidc/create-client-capability.png)
1.4. In the Login Settings step, set the following values:
- Root URL: `https://app.infisical.com`.
- Home URL: `https://app.infisical.com`.
- Valid Redirect URIs: `https://app.infisical.com/api/v1/sso/oidc/callback`.
- Web origins: `https://app.infisical.com`.
![OIDC keycloak create client login settings](../../../images/sso/keycloak-oidc/create-client-login-settings.png)
<Info>
If youre self-hosting Infisical, then you will want to replace https://app.infisical.com (base URL) with your own domain.
</Info>
1.5. Next, navigate to the **Client scopes** tab and select the client's dedicated scope.
![OIDC keycloak client scopes list](../../../images/sso/keycloak-oidc/client-scope-list.png)
1.6. Next, click **Add predefined mapper**.
![OIDC keycloak client mappers empty](../../../images/sso/keycloak-oidc/client-scope-mapper-menu.png)
1.7. Select the **email**, **given name**, **family name** attributes and click **Add**.
![OIDC keycloak client mappers predefined 1](../../../images/sso/keycloak-oidc/scope-predefined-mapper-1.png)
![OIDC keycloak client mappers predefined 2](../../../images/sso/keycloak-oidc/scope-predefined-mapper-2.png)
Once you've completed the above steps, the list of mappers should look like the following:
![OIDC keycloak client mappers completed](../../../images/sso/keycloak-oidc/client-scope-complete-overview.png)
</Step>
<Step title="Retrieve Identity Provider (IdP) Information from Keycloak">
2.1. Back in Keycloak, navigate to Configure > Realm settings > General tab > Endpoints > OpenID Endpoint Configuration and copy the opened URL. This is what is to referred to as the Discovery Document URL and it takes the form: `https://keycloak-mysite.com/realms/myrealm/.well-known/openid-configuration`.
![OIDC keycloak realm OIDC metadata](../../../images/sso/keycloak-oidc/realm-setting-oidc-config.png)
2.2. From the Clients page, navigate to the Credential tab and copy the **Client Secret** to be used in the next steps.
![OIDC keycloak realm OIDC secret](../../../images/sso/keycloak-oidc/client-secret.png)
</Step>
<Step title="Finish configuring OIDC in Infisical">
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect.
![OIDC keycloak manage org Infisical](../../../images/sso/keycloak-oidc/manage-org-oidc.png)
3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**.
![OIDC keycloak paste values into Infisical](../../../images/sso/keycloak-oidc/create-oidc.png)
Once you've done that, press **Update** to complete the required configuration.
</Step>
<Step title="Enable OIDC SSO in Infisical">
Enabling OIDC SSO allows members in your organization to log into Infisical via Keycloak.
![OIDC keycloak enable OIDC](../../../images/sso/keycloak-oidc/enable-oidc.png)
</Step>
<Step title="Enforce OIDC SSO in Infisical">
Enforcing OIDC SSO ensures that members in your organization can only access Infisical
by logging into the organization via Keycloak.
To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one Keycloak user with Infisical.
Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO.
<Warning>
We recommend ensuring that your account is provisioned using the application in Keycloak
prior to enforcing OIDC SSO to prevent any unintended issues.
</Warning>
</Step>
</Steps>
<Tip>
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite OIDC login.
</Tip>
<Note>
If you're configuring OIDC SSO on a self-hosted instance of Infisical, make
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work:
<div class="height:1px;"/>
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
can be a random 32-byte base64 string generated with `openssl rand -base64
32`.
<div class="height:1px;"/>
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
</Note>

@ -0,0 +1,62 @@
---
title: "Keycloak OIDC Group Membership Mapping"
sidebarTitle: "Group Membership Mapping"
description: "Learn how to sync Keycloak group members to matching groups in Infisical."
---
You can have Infisical automatically sync group
memberships between Keycloak and Infisical by configuring a group membership mapper in Keycloak.
When a user logs in via OIDC, they will be added to Infisical groups that match their Keycloak groups names, and removed from any
Infisical groups not present in their groups claim.
<Info>
When enabled, manual
management of Infisical group memberships will be disabled.
</Info>
<Warning>
Group membership changes in the Keycloak only sync with Infisical when a
user logs in via OIDC. For example, if you remove a user from a group in Keycloak, this change will not be reflected in Infisical until their next OIDC login. To ensure this behavior, Infisical recommends enabling Enforce OIDC
SSO in the OIDC settings.
</Warning>
<Steps>
<Step title="Configure a group membership mapper in Keycloak">
1.1. In your realm, navigate to the **Clients** tab and select your Infisical client.
![OIDC keycloak client](/images/sso/keycloak-oidc/group-membership-mapping/select-client.png)
1.2. Select the **Client Scopes** tab.
![OIDC keycloak client scopes](/images/sso/keycloak-oidc/group-membership-mapping/select-client-scopes.png)
1.3. Next, select the dedicated scope for your Infisical client.
![OIDC keycloak dedicated scope](/images/sso/keycloak-oidc/group-membership-mapping/select-dedicated-scope.png)
1.4. Click on the **Add mapper** button, and select the **By configuration** option.
![OIDC keycloak add mapper by configuration](/images/sso/keycloak-oidc/group-membership-mapping/create-mapper-by-configuration.png)
1.5. Select the **Group Membership** option.
![OIDC keycloak group membership option](/images/sso/keycloak-oidc/group-membership-mapping/select-group-membership-mapper.png)
1.6. Give your mapper a name and ensure the following properties are set to the following before saving:
- **Token Claim Name** is set to `groups`
- **Full group path** is disabled
![OIDC keycloak group membership mapper](/images/sso/keycloak-oidc/group-membership-mapping/create-group-membership-mapper.png)
</Step>
<Step title="Setup groups in Infisical and enable OIDC Group Membership Mapping">
2.1. In Infisical, create any groups you would like to sync users to. Make sure the name of the Infisical group is an exact match of the Keycloak group name.
![OIDC keycloak infisical group](/images/sso/keycloak-oidc/group-membership-mapping/create-infisical-group.png)
2.2. Next, enable **OIDC Group Membership Mapping** in Organization Settings > Security.
![OIDC keycloak enable group membership mapping](/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png)
2.3. The next time a user logs in they will be synced to their matching Keycloak groups.
![OIDC keycloak synced users](/images/sso/keycloak-oidc/group-membership-mapping/synced-users.png)
</Step>
</Steps>

@ -0,0 +1,113 @@
---
title: "Keycloak OIDC Overview"
sidebarTitle: "Overview"
description: "Learn how to configure Keycloak OIDC for Infisical SSO."
---
<Info>
Keycloak OIDC SSO is a paid feature. If you're using Infisical Cloud, then it
is available under the **Pro Tier**. If you're self-hosting Infisical, then
you should contact sales@infisical.com to purchase an enterprise license to
use it.
</Info>
<Steps>
<Step title="Create an OIDC client application in Keycloak">
1.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application.
![OIDC keycloak list of clients](/images/sso/keycloak-oidc/clients-list.png)
<Info>
You dont typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm.
</Info>
1.2. In the General Settings step, set **Client type** to **OpenID Connect**, the **Client ID** field to an appropriate identifier, and the **Name** field to a friendly name like **Infisical**.
![OIDC keycloak create client general settings](/images/sso/keycloak-oidc/create-client-general-settings.png)
1.3. Next, in the Capability Config step, ensure that **Client Authentication** is set to On and that **Standard flow** is enabled in the Authentication flow section.
![OIDC keycloak create client capability config settings](/images/sso/keycloak-oidc/create-client-capability.png)
1.4. In the Login Settings step, set the following values:
- Root URL: `https://app.infisical.com`.
- Home URL: `https://app.infisical.com`.
- Valid Redirect URIs: `https://app.infisical.com/api/v1/sso/oidc/callback`.
- Web origins: `https://app.infisical.com`.
![OIDC keycloak create client login settings](/images/sso/keycloak-oidc/create-client-login-settings.png)
<Info>
If youre self-hosting Infisical, then you will want to replace https://app.infisical.com (base URL) with your own domain.
</Info>
1.5. Next, navigate to the **Client scopes** tab and select the client's dedicated scope.
![OIDC keycloak client scopes list](/images/sso/keycloak-oidc/client-scope-list.png)
1.6. Next, click **Add predefined mapper**.
![OIDC keycloak client mappers empty](/images/sso/keycloak-oidc/client-scope-mapper-menu.png)
1.7. Select the **email**, **given name**, **family name** attributes and click **Add**.
![OIDC keycloak client mappers predefined 1](/images/sso/keycloak-oidc/scope-predefined-mapper-1.png)
![OIDC keycloak client mappers predefined 2](/images/sso/keycloak-oidc/scope-predefined-mapper-2.png)
Once you've completed the above steps, the list of mappers should look like the following:
![OIDC keycloak client mappers completed](/images/sso/keycloak-oidc/client-scope-complete-overview.png)
</Step>
<Step title="Retrieve Identity Provider (IdP) Information from Keycloak">
2.1. Back in Keycloak, navigate to Configure > Realm settings > General tab > Endpoints > OpenID Endpoint Configuration and copy the opened URL. This is what is to referred to as the Discovery Document URL and it takes the form: `https://keycloak-mysite.com/realms/myrealm/.well-known/openid-configuration`.
![OIDC keycloak realm OIDC metadata](/images/sso/keycloak-oidc/realm-setting-oidc-config.png)
2.2. From the Clients page, navigate to the Credential tab and copy the **Client Secret** to be used in the next steps.
![OIDC keycloak realm OIDC secret](/images/sso/keycloak-oidc/client-secret.png)
</Step>
<Step title="Finish configuring OIDC in Infisical">
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect.
![OIDC keycloak manage org Infisical](/images/sso/keycloak-oidc/manage-org-oidc.png)
3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**.
![OIDC keycloak paste values into Infisical](/images/sso/keycloak-oidc/create-oidc.png)
Once you've done that, press **Update** to complete the required configuration.
</Step>
<Step title="Enable OIDC SSO in Infisical">
Enabling OIDC SSO allows members in your organization to log into Infisical via Keycloak.
![OIDC keycloak enable OIDC](/images/sso/keycloak-oidc/enable-oidc.png)
</Step>
<Step title="Enforce OIDC SSO in Infisical">
Enforcing OIDC SSO ensures that members in your organization can only access Infisical
by logging into the organization via Keycloak.
To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one Keycloak user with Infisical.
Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO.
<Warning>
We recommend ensuring that your account is provisioned using the application in Keycloak
prior to enforcing OIDC SSO to prevent any unintended issues.
</Warning>
</Step>
</Steps>
<Tip>
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite OIDC login.
</Tip>
<Note>
If you're configuring OIDC SSO on a self-hosted instance of Infisical, make
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work:
<div class="height:1px;"/>
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
can be a random 32-byte base64 string generated with `openssl rand -base64
32`.
<div class="height:1px;"/>
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
</Note>

@ -1,36 +0,0 @@
---
title: "Networking"
sidebarTitle: "Networking"
description: "Network configuration details for Infisical Cloud"
---
## Overview
When integrating your infrastructure with Infisical Cloud, you may need to configure network access controls. This page provides the IP addresses that Infisical uses to communicate with your services.
## Egress IP Addresses
Infisical Cloud operates from two regions: US and EU. If your infrastructure has strict network policies, you may need to allow traffic from Infisical by adding the following IP addresses to your ingress rules. These are the egress IPs Infisical uses when making outbound requests to your services.
### US Region
To allow connections from Infisical US, add these IP addresses to your ingress rules:
- `3.213.63.16`
- `54.164.68.7`
### EU Region
To allow connections from Infisical EU, add these IP addresses to your ingress rules:
- `3.77.89.19`
- `3.125.209.189`
## Common Use Cases
You may need to allow Infisicals egress IPs if your services require inbound connections for:
- Secret rotation - When Infisical needs to send requests to your systems to automatically rotate credentials
- Dynamic secrets - When Infisical generates and manages temporary credentials for your cloud services
- Secret integrations - When syncing secrets with third-party services like Azure Key Vault
- Native authentication with machine identities - When using methods like Kubernetes authentication

Binary file not shown.

After

(image error) Size: 341 KiB

Binary file not shown.

After

(image error) Size: 1.2 MiB

Binary file not shown.

After

(image error) Size: 318 KiB

Binary file not shown.

After

(image error) Size: 1.3 MiB

Binary file not shown.

After

(image error) Size: 438 KiB

Binary file not shown.

After

(image error) Size: 396 KiB

Binary file not shown.

After

(image error) Size: 468 KiB

Binary file not shown.

After

(image error) Size: 578 KiB

Binary file not shown.

After

(image error) Size: 1.2 MiB

@ -30,13 +30,13 @@ description: "How to sync secrets from Infisical to Azure App Configuration"
Press create integration to start syncing secrets to Azure App Configuration.
<Note>
<Warning>
The Azure App Configuration integration requires the following permissions to be set on the user / service principal
for Infisical to sync secrets to Azure App Configuration: `Read Key-Value`, `Write Key-Value`, `Delete Key-Value`.
Any role with these permissions would work such as the **App Configuration Data Owner** role. Alternatively, you can use the
**App Configuration Data Reader** role for read-only access or **App Configuration Data Contributor** role for read/write access.
</Note>
</Warning>
</Step>
<Step title="Additional Configuration">

@ -1,97 +0,0 @@
---
title: "Flows"
description: "Infisical's core flows have strong cryptographic underpinnings."
---
## Signup
When a user signs up for an account using email/password, they verify their email by correctly entering the 6-digit OTP code sent to it.
After this procedure, the user creates a password that is checked against strict requirements to ensure that it has sufficient entropy; this is critical because passwords have both authentication-related and cryptographic implications in Infisical. In accordance to the [secure remote password protocol (SRP)](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol), the password is used to generate a salt and X; this is kept handy on the client side.
Next, a few user-associated symmetric keys are generated for subsequent use:
- The password is transformed into a 256-bit symmetric key, called the generated key, using the [Argon2id](https://en.wikipedia.org/wiki/Argon2) key derivation function.
- A 256-bit symmetric key, called the protected key, is generated.
- A public-private key pair is generated.
The symmetric keys are used in sequence to encrypt the users private key:
- The protected key is used to encrypt the private key.
- The generated key is used to encrypt the protected key.
Finally, the encrypted private key, the protected key, salt, and X are sent to the Infisical API to be stored in the storage backend. Note that the top-level secret used to secure the users account and private key is their password. Therefore, it must be unknown to the Infisical API and strong by nature.
## Login
When a user logs in, they enter their password to authenticate with Infisical via SRP. If successful, the encrypted protected key and encrypted private key are returned to the client side.
The password is then used in reverse sequence to decrypt the private key:
- The password is transformed back into the generated key.
- The generated key is used to decrypt the encrypted protected key.
- The protected key is used to decrypt the encrypted private key.
The private key is stored on the client side and kept handy.
## Single sign-on
When a SSO authentication method like Google, GitHub, or SAML SSO is used to login or signup to Infisical, the process is identical to logging in with email/password except that it is contingent on first successfully logging in via the authentication provider. This means, for example, a user with Google SSO enabled must first log in with Google and then enter their password for Infisical to complete logging into the platform.
This approach implies that the users password assumes only the role of a master decryption key or secret. It also ensures that the authentication provider does not know this top-level secret, keeping the platform zero-knowledge as intended.
## Account recovery
When a user signs up for Infisical, they are issued a backup PDF containing a symmetric key that can be used to recover their account by decrypting a copy of that users private key; using the backup PDF is the only way to recover a users account in the event of a lockout - this is intentional by design of Infisicals zero-knowledge architecture.
We strongly encourage all users to download, print, and keep their backup PDFs in a secure location.
## Secrets
In Infisical, secrets belong to environments in projects, and projects belong to organizations. Each project can be thought of as a vault and has its own symmetric key, called the project key. The project key is used to encrypt the secrets contained in that project.
Similar to each users private key, the project key is sensitive and must remain unknown to the server to preserve the zero-knowledge aspect of Infisical; knowledge of the project key would allow the server to decrypt the secrets of that project which would be undesirable if the server is compromised.
In order to preserve the zero-knowledge aspect of Infisical, each project key is encrypted on the client side before being sent to the server. More specifically, for each project, we make copies of its project key for each member of that project; each copy is encrypted under that members public key and only then sent off to the server for storage. A few relevant sequences:
- The initial member of a project generates its project key, encrypts it under their public key, and uploads it to the server for storage.
- When a new member is added to the project, an existing member of the project (e.g. the initial member) fetches their copy of the project key, decrypts that copy, encrypts it under the public key of the new member, and uploads it to the server for storage.
- When a member is removed from a project, their copy of the project key is hard deleted from the storage backend.
When dealing with secrets, this implies a specific sequence of decryption/encryption steps to fetch and create/update them. Assuming that were dealing with the Infisical Web UI, lets start with fetching secrets which happens after the user logs in and selects a project:
- The user fetches encrypted secrets back to the client side.
- The user also fetches the encrypted project key, encrypted under their public key, for these secrets.
- The encrypted project key is decrypted by the users private key which is kept handy on the client side.
- The project key is finally used to decrypt the secrets belonging to the project.
- The secrets are displayed to the user in the Infisical Web UI.
Similarly, when a user creates/updates a secret, the reverse sequence is performed:
- The user fetches the encrypted project key, encrypted under the users public key.
- The project key is decrypted by the users private key which is kept handy on the client side.
- The user encrypts the new/updated secret under the project key.
- The user sends the new/updated secret to the server for storage.
These sequences are performed across various Infisical clients including the web UI, CLI, SDKs, and K8s operators when dealing with the Infisical API. They are also relevant in the implementations of Infisicals versioning features like secret versions and snapshots.
## Native integrations
Previously, we mentioned that Infisical is zero-knowledge; this is partly true because Infisical can be used this way. Under certain circumstances, however, a user can explicitly share their copy of the project key with the server to enable more advanced features like native integrations.
The way a project key is shared with Infisical is via an abstraction that we call a bot. Each project has a bot with a public-private key pair generated on the server; the private key of each bot is symmetrically encrypted by the root encryption key of the server. This implies a few things:
- The server may partake in the sharing of project keys via its own public-private keys bound to each project bot.
- The server root encryption key must be kept secure.
With that, lets discuss native integrations. A native integrations is a connection between Infisical and a target platform like GitHub, GitLab, or Vercel that allows secrets to be synced from Infisical to the target platform using its API. Since native integrations require secrets to be sent over in plaintext, they require the server to have access to the secrets. The sequence for how integrations are implemented is fairly simple:
- A user explicitly shares copy of the project key with the server via the Infisical Web UI. In this step, the user fetches the public key of the bot assigned to that project, encrypts the project key under that public key, and sends it back to the server.
- The user selects a target platform to integrate with their project and enters details such as the source environment within the project to send secrets from as well as the project and environment in the target platform to sync secrets to.
- The user creates the integration, triggering the first sync wherein Infisical decrypts the projects key, uses it to decrypt the secrets of that project, and sends the secrets to the target platform.
- Finally, on any subsequent mutations applied to the source environment of an active integration, Infisical automatically triggers a re-sync to the target platform. This keeps Infisical as a ground source-of-truth for a teams secrets.
## Resources
- For in depth details, consult the code.
- To get started with Infisical, try out the [Getting Started](https://infisical.com/docs/documentation/getting-started/introduction) overview.

@ -6,24 +6,23 @@ description: "Read how Infisical works under the hood."
This section covers the internals of Infisical including its technical underpinnings, architecture, and security properties.
<Note>
Knowledge of this section is recommended but not required to use Infisical. However, if you're operating Infisical, we recommend understanding the internals.
Knowledge of this section is recommended but not required to use Infisical.
However, if you're operating Infisical, we recommend understanding the
internals.
</Note>
## Learn More
<CardGroup cols={2}>
<Card href="./components" title="Components" icon="boxes-stacked" color="#000000">
Learn about the fundamental parts of Infisical.
</Card>
<Card href="./flows" title="Flows" icon="bars-staggered" color="#000000">
Find out more about the structure of core user flows in Infisical.
</Card>
<Card
href="./security"
title="Security"
icon="shield"
href="./components"
title="Components"
icon="boxes-stacked"
color="#000000"
>
Learn about the fundamental parts of Infisical.
</Card>
<Card href="./security" title="Security" icon="shield" color="#000000">
Read about most common security-related topics and questions.
</Card>
<Card
@ -32,6 +31,8 @@ This section covers the internals of Infisical including its technical underpinn
icon="ticket"
color="#000000"
>
Learn best practices for utilizing Infisical service tokens. Please note that service tokens are now deprecated and will be removed entirely in the future.
Learn best practices for utilizing Infisical service tokens. Please note
that service tokens are now deprecated and will be removed entirely in the
future.
</Card>
</CardGroup>

@ -52,6 +52,7 @@ Refer to the table below for a list of subjects and the actions they support.
| `pki-collections` | `read`, `create`, `edit`, `delete` |
| `kms` | `edit` |
| `cmek` | `read`, `create`, `edit`, `delete`, `encrypt`, `decrypt` |
| `secret-syncs` | `read`, `create`, `edit`, `delete`, `sync-secrets`, `import-secrets`, `remove-secrets` |
</Tab>
@ -63,21 +64,23 @@ Refer to the table below for a list of subjects and the actions they support.
`read`, `create`, `edit`, `delete`.
</Note>
| Subject | Actions |
| ------------------ | ---------------------------------- |
| `workspace` | `read`, `create` |
| `role` | `read`, `create`, `edit`, `delete` |
| `member` | `read`, `create`, `edit`, `delete` |
| `secret-scanning` | `read`, `create`, `edit`, `delete` |
| `settings` | `read`, `create`, `edit`, `delete` |
| `incident-account` | `read`, `create`, `edit`, `delete` |
| `sso` | `read`, `create`, `edit`, `delete` |
| `scim` | `read`, `create`, `edit`, `delete` |
| `ldap` | `read`, `create`, `edit`, `delete` |
| `groups` | `read`, `create`, `edit`, `delete` |
| `billing` | `read`, `create`, `edit`, `delete` |
| `identity` | `read`, `create`, `edit`, `delete` |
| `kms` | `read` |
| Subject | Actions |
| --------------------- | ------------------------------------------------ |
| `workspace` | `read`, `create` |
| `role` | `read`, `create`, `edit`, `delete` |
| `member` | `read`, `create`, `edit`, `delete` |
| `secret-scanning` | `read`, `create`, `edit`, `delete` |
| `settings` | `read`, `create`, `edit`, `delete` |
| `incident-account` | `read`, `create`, `edit`, `delete` |
| `sso` | `read`, `create`, `edit`, `delete` |
| `scim` | `read`, `create`, `edit`, `delete` |
| `ldap` | `read`, `create`, `edit`, `delete` |
| `groups` | `read`, `create`, `edit`, `delete` |
| `billing` | `read`, `create`, `edit`, `delete` |
| `identity` | `read`, `create`, `edit`, `delete` |
| `project-templates` | `read`, `create`, `edit`, `delete` |
| `app-connections` | `read`, `create`, `edit`, `delete`, `connect` |
| `kms` | `read` |
</Tab>
</Tabs>
@ -90,7 +93,6 @@ Permission inversion allows you to explicitly deny actions instead of allowing t
- secret-folders
- secret-imports
- dynamic-secrets
- cmek
When a permission is inverted, it changes from an "allow" rule to a "deny" rule. For example:

@ -3,25 +3,25 @@ title: "Security"
description: "Infisical's security model includes many considerations and initiatives."
---
Given that Infisical is a secret management platform that manages sensitive data, the Infisical security model is very important.
The goal of Infisical's security model is to ensure the security and integrity of all of its managed data as well as all associated operations.
As a security infrastructure platform dealing with highly-sensitive data, Infisical follows a robust security model with the goal of ensuring the security and integrity of all its managed data and associated components.
This means that data at rest and in transit must be secure from eavesdropping or tampering. All clients must be authenticated and authorized to access data. Additionally, all interactions must be auditable and traced uniquely back to their source.
As part of the security model, data at rest and in transit must be secure from eavesdropping or tampering, clients must be authenticated and authorized to access data, and all operations in the platform are audited and can be traced back to their source.
This page documents security measures used by [Infisical](https://github.com/Infisical/infisical), the software, and [Infisical Cloud](https://infisical.com/), a separate managed service offering for the software.
## Threat model
Infisicals threat model spans communication, storage, response mechanisms, failover strategies, and more.
Infisicals (the software) threat model spans communication, storage, response mechanisms, and more.
- Eavesdropping on communications: Infisical ensures end-to-end encryption for all client interactions with the Infisical API.
- Eavesdropping on communications: Infisical secures client communication with the server and from the server to the storage backend.
- Tampering with data (at rest or in transit): Infisical implements data integrity checks to detect tampering. If inconsistencies are found, Infisical aborts transactions and raises alerts.
- Unauthorized access (lacking authentication/authorization): Infisical mandates rigorous authentication and authorization checks for all inbound requests; it also offers multi-factor authentication and role-based access controls.
- Actions without accountability: Infisical logs all project-level events, including policy updates, queries/mutations applied to secrets, and more. Every event is timestamped and information about actor, source (i.e. IP address, user-agent, etc.), and relevant metadata is included.
- Breach of data storage confidentiality: Infisical encrypts all stored secrets using proven cryptographic techniques such as AES-256-GCM for symmetric encryption.
- Loss of service availability or secret data due to failures: Infisical leverages the robust container orchestration capabilities of Kubernetes and the inherent high availability features of Bitnami MongoDB to ensure resilience and fault tolerance. By deploying multiple replicas of Infisical application on Kubernetes, operations can continue even if a single instance fails.
- Unauthorized access (lacking authentication/authorization): Infisical mandates rigorous authentication and authorization checks for all inbound requests; it also offers multi-factor authentication and role/attribute-based access controls.
- Actions without accountability: Infisical logs events, including policy updates, queries/mutations applied to secrets, certificates, and more. Every event is timestamped and information about actor, source (i.e. IP address, user-agent, etc.), and relevant metadata is included.
- Breach of data storage confidentiality: Infisical encrypts all stored secrets using proven cryptographic techniques for symmetric encryption.
- Unrecognized suspicious activities: Infisical monitors for any anomalous activities such as authentication attempts from previously unseen sources.
- Unidentified system vulnerabilities: Infisical undergoes penetration tests and vulnerability assessments twice a year; we act on findings to bolster the system's defense mechanisms.
- Unidentified system vulnerabilities: Infisical undergoes penetration tests and vulnerability assessments twice a year; we act on findings to bolster the systems defense mechanisms.
That said, Infisical does not consider the following as part of its threat model:
Infisical (the software) does not consider the following as part of its threat model:
- Uncontrolled access to the storage mechanism: An attacker with unfettered access to the storage system can manipulate data in unpredictable ways, including erasing or tampering with stored secrets. Furthermore, the attacker could potentially implement state rollbacks to favor their objectives.
- Disclosure of secret presence: If an adversary gains read access to the storage backend, they might discern the existence of certain secrets, even if the actual contents remain encrypted and concealed.
@ -30,137 +30,74 @@ That said, Infisical does not consider the following as part of its threat model
- Breaches via compromised clients: If a system or application accessing Infisical is compromised, and its credentials to the platform are exposed, an attacker might gain access at the privilege level of that compromised entity.
- Configuration tampering by administrators: Any configuration data, whether supplied through admin interfaces or configuration files, needs scrutiny. If an attacker can manipulate these configurations, it poses risks to data confidentiality and integrity.
- Physical access to deployment infrastructure: An attacker with physical access to the servers or infrastructure where Infisical is deployed can potentially compromise the system in ways that are challenging to guard against, such as direct hardware tampering or booting from malicious media.
- Social engineering attacks on personnel: Attacks that target personnel, tricking them into divulging sensitive information or performing compromising actions, fall outside the platform's direct defensive purview.
- Social engineering attacks on personnel: Attacks that target personnel, tricking them into divulging sensitive information or performing compromising actions, fall outside the platforms direct defensive purview.
It's essential to note that while these points fall outside the platform's direct threat model, they still form crucial considerations for an overarching security strategy.
Note that while these points fall outside the Infisicals threat model, they remain considerations in the broader platform architecture.
## External threat overview
Infisical's architecture consists of various systems:
Infisicals architecture consists of various systems which together we refer to as the Infisical platform:
- Infisical API
- Storage backend
- Redis
- Infisical Web UI
- Infisical clients
- Server: The Infisical API that serves requests.
- Clients: The Web UI and other applications that send requests to the server.
- Storage backend: PostgreSQL used by the server to persist data.
- Redis: Used by Infisical for caching, queueing and cron job scheduling.
The Infisical API requires that the Infisical Web UI and all Infisical clients are authenticated and authorized for every inbound request. If using [Infisical Cloud](https://app.infisical.com), all traffic is routed through [Cloudflare](https://www.cloudflare.com) which enforces TLS and requires a minimum of TLS 1.2.
The server requires clients to be authenticated and authorized for every inbound request. If using [Infisical Cloud](https://infisical.com/), all traffic is routed through [Cloudflare](https://www.cloudflare.com/) which enforces TLS and requires a minimum of TLS 1.2.
The Infisical API is untrusted by design when dealing with secrets. All secrets are encrypted/decrypted on the client-side before reaching the Infisical API by default; granting Infisical access to secrets afterward is optional and up to your organization.
The server mandates that each request includes a valid token (issued for a user or machine identity) used to identify the client before performing any actions on the platform. Clients without a valid token can only access login endpoints with the exception of a few intentionally unauthenticated endpoints. For tokens issued for machine identities, Infisical provides significant configuration, including support for native authentication methods (e.g. [AWS](https://infisical.com/docs/documentation/platform/identities/aws-auth), [Azure](https://infisical.com/docs/documentation/platform/identities/azure-auth), [Kubernetes](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth), etc.); custom TTLs to restrict token lifespan; IP restrictions to enforce network-based access controls; and usage caps to limit the maximum number of times that a token can be used.
The storage backend used by Infisical is also untrusted by design. All sensitive data is encrypted either symmetrically with AES-256-GCM or asymmetrically with x25519-xsalsa20-poly1305 prior to entering the storage backend, depending on the context either on the client-side or server-side. Moreover, Infisical communicates with the storage backend over TLS to provide an added layer of security.
When accessing Infisical via web browser, JWT tokens are stored in browser memory and appended to outbound requests requiring authentication; refresh tokens are stored in HttpOnly cookies and included in requests as part of token renewal. Note also that Infisical utilizes the latest HTTP security headers and employs a strict Content-Security-Policy to mitigate XSS.
To mitigate abuse and enhance system stability, the server enforces configurable rate limiting on read, write, and secrets operations. This prevents excessive API requests from impacting system performance while ensuring fair usage across clients.
Once traffic enters the server, any sensitive data (e.g. secrets, certificates entering the server), where applicable, is encrypted using a 256-bit [Advanced Encryption Standard (AES)](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) cipher in the [Galois Counter Mode (GCM)](https://en.wikipedia.org/wiki/Galois/Counter_Mode) with 96-bit nonces prior to being persisted in the storage backend. Encryption is an integral part of Infisicals platform-wide cryptographic architecture, which also supports seal-wrapping with external KMS and HSMs. Before responding to a client request, the server securely retrieves and decrypts requested data from the storage backend. Each decryption operation includes integrity verification to ensure data has not been altered or tampered with.
## Internal threat overview
Within Infisical, a critical security concern is an attacker gaining access to sensitive data that they are not permitted to, especially if they already has some degree of access to the system. There are currently two authentication methods categories used by clients for where we apply robust authentication and authorization logic.
Within Infisical, an internal threat and critical security concern is an attacker gaining access to sensitive data that they are not permitted to, especially if they are able to authenticate with some degree of access to the system.
### JWT / API Key
Before a client can perform any actions on the platform, it must authenticate with the server using a supported authentication method such as username-password, SAML, SSO, LDAP, AWS/GCP/Azure, OIDC, or Kubernetes authentication. A successful authentication results in the issuance of a client (JWT) token containing a reference to the user or machine identity bound to it.
This token category is used by users and included in requests made from the Infisical Web UI or elsewhere to the Infisical API.
When a client uses the token to make authenticated requests against the server, Infisical validates the token and maps the bound-identity to access control policies that exist at the organization and project level, both types of namespaces within the platform. The access control policies are configured by operators of Infisical ahead of time and may involve role-based, attribute-based, and one-off “additional privilege” resource constraints. Given the robustness of the access control system, we recommend reading the full documentation for it.
Each token is authenticated against the API and mapped to an existing user in Infisical. If no existing user is found for the token, the request is rejected by the API. Each token assumes the permission set of the user that it is mapped to. For example, if a user corresponding to a token is not allowed access to a certain organization or project, then the token is also not be valid for any requests concerning those specific resources.
For example, an operator of Infisical may define the following constraints to restrict client access to particular resources:
In the event of compromise, an attacker could use the token to impersonate the associated user and perform actions within the permission set of that user. While they could retrieve secrets for a project that the user is part of, they could not, however, decrypt secrets if the project follows Infisical's default zero-knowledge architecture. In any case, it would be critical for the user to invalidate this token and change their password immediately to prevent further unintended actions and consequences.
### Service token
This token category is provisioned by users for applications and infrastructure to perform secret operations against the Infisical API.
Each token is scoped to a project in Infisical and configurable with an expiration date and permission set (also known as **scopes**) for specific environment(s) and path(s) within them. For example, you may provision an application a service token to authenticate against the Infisical API and retrieve secrets from some `/environment-variables` path in the production environment of a project. If the token is tried for another project, environment, or path outside of its permission set, then it is rejected by the API.
It should also be noted that projects in Infisical can be configured to restrict service token access to specific IP addresses or CIDR ranges; this can be useful for limiting access to traffic coming from corporate networks.
In the event of compromise, an attacker could use a service token to access the secrets that it is provisioned for. It would be critical here for project administrator(s) to revoke the token immediately to prevent further unintended access to resources; it would also be advisable currently to transfer secrets to a new project where a new project key is created on the client-side.
- Read and write access to a secret resource via an additional privilege attached to the bound-identity.
- Read-only access to a secret resource via one or multiple roles attached to the bound-identity.
- Read-only access to a secret resource via a group membership for which the associated bound-identity is part of; the group itself is assigned one or multiple roles with access to the secret resource.
## Cryptography
Infisical uses AES-256-GCM for symmetric encryption and x25519-xsalsa20-poly1305 for asymmetric encryption operations; asymmetric algorithms are implemented with the [TweetNaCl.js](https://tweetnacl.js.org/#/) library which has been well-audited and recommended for use by cybersecurity firm Cure53. Lastly, the secure remote password (SRP) implementation uses [jsrp](https://github.com/alax/jsrp) package for user authentication.
All symmetric encryption operations, with the exception of those proxied through external KMS and HSM systems, in Infisical use a software-backed, 256-bit Advanced Encryption Standard (AES) cipher in the Galois Counter Mode (GCM) with 96-bit nonces — AES-256-GCM.
By default, Infisical employs a zero-knowledge-first approach to securely storing and sharing secrets.
Infisical employs a multilayer approach to its encryption architecture with components that can be optionally linked to external KMS or HSM systems. At a high-level, a master key, backed by an operator-provided key, is used to encrypt (internal) “KMS” keys that are used to then encrypt data keys; the data keys are used to protect sensitive data stored in Infisical. The keys in the architecture are stored encrypted in the storage backend, retrieved, decrypted, and only then used as part of server operations when needed. Since server configuration is needed to decrypt any keys as part of the encryption architecture, accessing any sensitive data in Infisical requires access to both server configuration and data in the storage backend. Note that the platforms encryption architecture has components that can be linked to external KMS and HSM systems; opting for these make the use of the software more FIPS aligned.
- Each secret belongs to a project and is symmetrically encrypted by that project's unique key. Each member of a project is shared a copy of the project key, encrypted under their public key, when they are first invited to join the project.
Since these encryption operations occur on the client-side, the Infisical API is not able to view the value of any secret and the default zero-knowledge property of Infisical is retained; as you'd expect, it follows that decryption operations also occur on the client-side.
- An exception to the zero-knowledge property occurs when a member of a project explicitly shares that project's unique key with Infisical. It is often necessary to share the project key with Infisical in order to use features like native integrations and secret rotation that wouldn't be possible to offer otherwise.
To be specific:
## Infrastructure
- The architecture starts with a 256-bit master key that can be secured by a root key which can either be a 128-bit key, passed into the server by an operator of Infisical as an environment variable, or an external key from an HSM module such as [Thales Luna HSM](https://cpl.thalesgroup.com/encryption/data-protection-on-demand/services/luna-cloud-hsm) or [AWS Cloud HSM](https://aws.amazon.com/cloudhsm/) linked via specified configuration parameters.
- The master key secures 256-bit keys in Infisical henceforth referred to as KMS keys.
- Each organization in Infisical has its own KMS key and a separate data key; the KMS key is used to secure the data key which encrypts organization-level data.
- Each project in Infisical has a designated KMS key and a separate data key; the KMS key is used to secure the data key which encrypts project-level data. Note that a project KMS key can be substituted for an external key from a KMS such as [AWS KMS](https://infisical.com/docs/documentation/platform/kms-configuration/aws-kms), [AWS Cloud HSM](https://infisical.com/docs/documentation/platform/kms-configuration/aws-hsm), and [GCP KMS](https://infisical.com/docs/documentation/platform/kms-configuration/gcp-kms). We recommend reading the fuller [documentation](https://infisical.com/docs/documentation/platform/kms-configuration/overview) or integrating with an external KMS
### High availability
## Infrastructure & High availability (Infisical Cloud)
Infisical Cloud uses a number of strategies to keep services running smoothly and ensure data stays available, even during failures; we document these strategies below:
- Multi-AZ AWS RDS: Infisical Cloud runs AWS Relational Database Service (RDS) with Multi-AZ deployments to improve availability and durability. This setup keeps a standby replica in a different Availability Zone (AZ) and automatically fails over if the primary instance goes down. Continuous backups and replication help protect data and minimize interruptions.
- Multi-AZ ElastiCache (Redis): For caching, Infisical Cloud runs Amazon ElastiCache (Redis) in a Multi-AZ setup. This means data is replicated across different AZs, so if one goes down, the system can automatically fail over to a healthy node. This helps keep response times low and ensures caching stays reliable.
- Multi-AZ ECS for Container Orchestration: Infisical Cloud runs on Amazon Elastic Container Service (ECS) across multiple availability zones, making sure containers stay available even if an AZ fails. If one zone has an issue, traffic automatically shifts to healthy instances in other zones, keeping downtime to a minimum.
Infisical Cloud utilizes several strategies to ensure high availability, leveraging AWS services to maintain continuous operation and data integrity.
#### Multi-AZ AWS RDS
## Cross-Region Replication for Disaster Recovery (Infisical Cloud)
Infisical Cloud uses AWS Relational Database Service (RDS) with Multi-AZ deployments.
This configuration ensures that the database service is highly available and durable.
AWS RDS automatically provisions and maintains a synchronous standby replica of the database in a different Availability Zone (AZ).
This setup facilitates immediate failover to the standby in the event of an AZ failure, thereby ensuring that database operations can continue with minimal interruption.
The continuous backup and replication to the standby instance safeguard data against loss and ensure its availability even during system failures.
To handle regional failures, Infisical Cloud keeps standby regions updated and ready to take over when needed.
#### Multi-AZ ECS for Container Orchestration
- ElastiCache (Redis): Data is replicated across regions using AWS Global Datastore, keeping cached data consistent and available even if a primary region goes down.
- RDS (PostgreSQL): Cross-region read replicas ensure database data is available in multiple locations, allowing for failover in case of a regional outage.
Infisical Cloud leverages Amazon Elastic Container Service (ECS) in a Multi-AZ configuration for container orchestration.
This arrangement enables the management and operation of containers across multiple availability zones, increasing the application's fault tolerance.
Should there be an AZ failure, load is seamlessly sent to an operational AZ, thus minimizing downtime and preserving service availability.
#### Standby Regions for Regional Failover
To fight regional outages, secondary regions are always in standby mode and maintained with up-to-date configurations and data, ready to take over in case the primary region fails.
The standby regions enable a rapid transition and service continuity with minimal disruption in the event of a complete regional failure, ensuring that Infisical Cloud services remain accessible.
### Snapshots
A snapshot is a complete copy of data in the storage backend at a point in time.
If using [Infisical Cloud](https://app.infisical.com), snapshots of MongoDB databases are taken regularly; this can be enabled on your own storage backend as well.
### Offline usage
Many teams and organizations use the [Infisical CLI](https://infisical.com/docs/cli/overview) to fetch and inject secrets back from Infisical into their applications and infrastructure locally; the CLI has offline fallback capabilities.
If you have previously retrieved secrets for a specific project and environment, the `run/secret` command will utilize the saved secrets, even when offline, on subsequent fetch attempts to ensure that you always have access to secrets.
## Platform
### Web application
Infisical utilizes the latest HTTP security headers and employs a strict Content-Security-Policy to mitigate XSS.
JWT tokens are stored in browser memory and appended to outbound requests requiring authentication; refresh tokens are stored in `HttpOnly` cookies and included in future requests to `/api/token` for JWT token renewal.
### User authentication
Infisical supports several authentication methods including email/password, Google SSO, GitHub SSO, SAML 2.0 (Okta, Azure, JumpCloud), and OpenID Connect; Infisical also currently offers email-based 2FA with authenticator app methods coming in Q1 2024.
Infisical uses the [secure remote password protocol](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol#:~:text=The%20SRP%20protocol%20has%20a,the%20user%20to%20the%20server), commonly found in other zero-knowledge platform architectures, for authentication.
Put simply, the protocol enables Infisical to validate a user's knowledge of their password without ever seeing it by constructing a mutual secret; we use this protocol because each user's password is used to seed the generation of a master encryption/decryption key via KDF for that user which the platform
should not see.
Lastly, Infisical enforces strong password requirements according to the guidance set forth in [NIST Special Publication 80063B](https://pages.nist.gov/800-63-3/sp800-63b.html#appA). Since passwords in Infisical also has cryptographic implications, Infisical validates each password on client-side to meet minimum length and entropy requirements; Infisical also considers each password against the [Have I Been Pwned (HIBP) API](https://haveibeenpwned.com), which checks the password against around 700M breached passwords, in a privacy-preserving way.
<Note>
Since Infisical's unique zero-knowledge architecture requires a master decryption key for every user account, users with Google SSO, GitHub SSO, or SAML 2.0 enabled must still enter a secret after the
authentication step to access their secrets in Infisical. In practice, this implies stronger security since users must successfully authenticate with a single sign-on provider and provide a master decryption key
to access the platform.
We strongly encourage users to generate and store their passwords / master decryption key in a password manager, such as 1Password, Bitwarden, or Dashlane.
</Note>
## Role-based access control (RBAC)
[Infisical's RBAC](https://infisical.com/docs/documentation/platform/role-based-access-controls) feature enables organization owners and administrators to manage fine-grained access policies for members of their organization in Infisical; with RBAC, administrators can define custom roles with permission sets to be conveniently assigned to other members.
For example, you can define a role provisioning access to secrets in a specific project and environment in it with read-only permissions; the role can be assigned to members of an organization in Infisical.
### Audit logging
Infisical's audit logging feature spans 25+ events, tracking everything from permission changes to queries and mutations applied to secrets, for security and compliance teams at enterprises to monitor information access in the event of any suspicious activity or incident review. Every event is timestamped and information about actor, source (i.e. IP address, user-agent, etc.), and relevant metadata is included.
### IP allowlisting
Infisical's IP allowlisting feature can be configured to restrict client access to specific IP addresses or CIDR ranges. This applies to any client using service tokens and can be useful, for example, for limiting access to traffic coming from corporate networks.
By default, each project is initialized with the `0.0.0.0/0` entry, representing all possible IPv4 addresses. For enhanced security, we strongly recommend replacing the default entry with your client IPs to tighten access to your secrets.
With standby regions and automated failovers in place, Infisical Cloud faces minimal service disruptions even during large-scale outages.
## Penetration testing
@ -179,7 +116,7 @@ Whether or not Infisical or your employees can access data in the Infisical inst
It should be noted that, even on Infisical Cloud, it is physically impossible for employees of Infisical to view the values of secrets if users have not explicitly granted Infisical access to their project (i.e. opted out of zero-knowledge).
Please email security@infisical.com if you have any specific inquiries about employee data access policies.
Please email security@infisical.com if you have any specific inquiries about employee data and security policies.
## Get in touch

@ -85,10 +85,6 @@
"documentation/guides/microsoft-power-apps",
"documentation/guides/organization-structure"
]
},
{
"group": "Setup",
"pages": ["documentation/setup/networking"]
}
]
},
@ -253,7 +249,13 @@
"documentation/platform/sso/keycloak-saml",
"documentation/platform/sso/google-saml",
"documentation/platform/sso/auth0-saml",
"documentation/platform/sso/keycloak-oidc",
{
"group": "Keycloak OIDC",
"pages": [
"documentation/platform/sso/keycloak-oidc/overview",
"documentation/platform/sso/keycloak-oidc/group-membership-mapping"
]
},
"documentation/platform/sso/auth0-oidc",
"documentation/platform/sso/general-oidc"
]
@ -1037,7 +1039,6 @@
"internals/overview",
"internals/permissions",
"internals/components",
"internals/flows",
"internals/security",
"internals/service-tokens"
]

@ -42,7 +42,7 @@ namespace Example
ProjectId = "PROJECT_ID",
Environment = "dev",
};
var secret = infisical.GetSecret(getSecretOptions);
var secret = infisicalClient.GetSecret(getSecretOptions);
Console.WriteLine($"The value of secret '{secret.SecretKey}', is: {secret.SecretValue}");

@ -114,7 +114,11 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.DELETE_SECRET_SYNC]: "Delete Secret Sync",
[EventType.SECRET_SYNC_SYNC_SECRETS]: "Secret Sync synced secrets",
[EventType.SECRET_SYNC_IMPORT_SECRETS]: "Secret Sync imported secrets",
[EventType.SECRET_SYNC_REMOVE_SECRETS]: "Secret Sync removed secrets"
[EventType.SECRET_SYNC_REMOVE_SECRETS]: "Secret Sync removed secrets",
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER]:
"OIDC group membership mapping assigned user to groups",
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]:
"OIDC group membership mapping removed user from groups"
};
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {

@ -127,5 +127,7 @@ export enum EventType {
DELETE_SECRET_SYNC = "delete-secret-sync",
SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets",
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets"
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user"
}

@ -75,7 +75,7 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit<
};
export type TDashboardProjectSecretsQuickSearchResponse = {
folders: (TSecretFolder & { environment: string; path: string })[];
folders: (TSecretFolder & { envId: string; path: string })[];
dynamicSecrets: (TDynamicSecret & { environment: string; path: string })[];
secrets: SecretV3Raw[];
};
@ -83,7 +83,7 @@ export type TDashboardProjectSecretsQuickSearchResponse = {
export type TDashboardProjectSecretsQuickSearch = {
folders: Record<string, TDashboardProjectSecretsQuickSearchResponse["folders"]>;
secrets: Record<string, SecretV3RawSanitized[]>;
dynamicSecrets: Record<string, TDashboardProjectSecretsQuickSearchResponse["folders"]>;
dynamicSecrets: Record<string, TDashboardProjectSecretsQuickSearchResponse["dynamicSecrets"]>;
};
export type TGetDashboardProjectSecretsQuickSearchDTO = {

@ -20,7 +20,8 @@ export const useUpdateOIDCConfig = () => {
clientId,
clientSecret,
isActive,
orgSlug
orgSlug,
manageGroupMemberships
}: {
allowedEmailDomains?: string;
issuer?: string;
@ -34,6 +35,7 @@ export const useUpdateOIDCConfig = () => {
isActive?: boolean;
configurationType?: string;
orgSlug: string;
manageGroupMemberships?: boolean;
}) => {
const { data } = await apiRequest.patch("/api/v1/sso/oidc/config", {
issuer,
@ -47,7 +49,8 @@ export const useUpdateOIDCConfig = () => {
clientId,
orgSlug,
clientSecret,
isActive
isActive,
manageGroupMemberships
});
return data;
@ -74,7 +77,8 @@ export const useCreateOIDCConfig = () => {
clientId,
clientSecret,
isActive,
orgSlug
orgSlug,
manageGroupMemberships
}: {
issuer?: string;
configurationType: string;
@ -88,6 +92,7 @@ export const useCreateOIDCConfig = () => {
isActive: boolean;
orgSlug: string;
allowedEmailDomains?: string;
manageGroupMemberships?: boolean;
}) => {
const { data } = await apiRequest.post("/api/v1/sso/oidc/config", {
issuer,
@ -101,7 +106,8 @@ export const useCreateOIDCConfig = () => {
clientId,
clientSecret,
isActive,
orgSlug
orgSlug,
manageGroupMemberships
});
return data;

@ -5,7 +5,9 @@ import { apiRequest } from "@app/config/request";
import { OIDCConfigData } from "./types";
export const oidcConfigKeys = {
getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const
getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const,
getOIDCManageGroupMembershipsEnabled: (orgId: string) =>
["oidc-manage-group-memberships", orgId] as const
};
export const useGetOIDCConfig = (orgSlug: string) => {
@ -25,3 +27,16 @@ export const useGetOIDCConfig = (orgSlug: string) => {
enabled: true
});
};
export const useOidcManageGroupMembershipsEnabled = (orgId: string) => {
return useQuery({
queryKey: oidcConfigKeys.getOIDCManageGroupMembershipsEnabled(orgId),
queryFn: async () => {
const { data } = await apiRequest.get<{ isEnabled: boolean }>(
`/api/v1/sso/oidc/manage-group-memberships?orgId=${orgId}`
);
return data.isEnabled;
}
});
};

@ -12,4 +12,5 @@ export type OIDCConfigData = {
clientId: string;
clientSecret: string;
allowedEmailDomains?: string;
manageGroupMemberships: boolean;
};

@ -40,6 +40,12 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
<p>Machine Identity</p>
</Td>
);
case ActorType.PLATFORM:
return (
<Td>
<p>Platform</p>
</Td>
);
case ActorType.UNKNOWN_USER:
return (
<Td>

@ -2,8 +2,10 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal, IconButton } from "@app/components/v2";
import { useRemoveUserFromGroup } from "@app/hooks/api";
import { OrgPermissionCan } from "@app/components/permissions";
import { DeleteActionModal, IconButton, Tooltip } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useOidcManageGroupMembershipsEnabled, useRemoveUserFromGroup } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { AddGroupMembersModal } from "../AddGroupMemberModal";
@ -20,6 +22,11 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {
"removeMemberFromGroup"
] as const);
const { currentOrg } = useOrganization();
const { data: isOidcManageGroupMembershipsEnabled = false } =
useOidcManageGroupMembershipsEnabled(currentOrg.id);
const { mutateAsync: removeUserFromGroupMutateAsync } = useRemoveUserFromGroup();
const handleRemoveUserFromGroup = async (username: string) => {
try {
@ -47,19 +54,35 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Group Members</h3>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {
handlePopUpOpen("addGroupMembers", {
groupId,
slug: groupSlug
});
}}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<Tooltip
className="text-center"
content={
isOidcManageGroupMembershipsEnabled
? "OIDC Group Membership Mapping Enabled. Assign users to this group in your OIDC provider."
: undefined
}
>
<div className="mb-4 flex items-center justify-center">
<IconButton
isDisabled={isOidcManageGroupMembershipsEnabled || !isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {
handlePopUpOpen("addGroupMembers", {
groupId,
slug: groupSlug
});
}}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</div>
</Tooltip>
)}
</OrgPermissionCan>
</div>
<div className="py-4">
<GroupMembersTable

@ -21,11 +21,12 @@ import {
TBody,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useListGroupUsers } from "@app/hooks/api";
import { useListGroupUsers, useOidcManageGroupMembershipsEnabled } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { EFilterReturnedUsers } from "@app/hooks/api/groups/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
@ -58,6 +59,11 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
toggleOrderDirection
} = usePagination(GroupMembersOrderBy.Name, { initPerPage: 10 });
const { currentOrg } = useOrganization();
const { data: isOidcManageGroupMembershipsEnabled = false } =
useOidcManageGroupMembershipsEnabled(currentOrg.id);
const { data: groupMemberships, isPending } = useListGroupUsers({
id: groupId,
groupSlug,
@ -173,19 +179,30 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
{!groupMemberships?.users.length && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<div className="mb-4 flex items-center justify-center">
<Button
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("addGroupMembers", {
groupId,
slug: groupSlug
});
}}
>
Add members
</Button>
</div>
<Tooltip
className="text-center"
content={
isOidcManageGroupMembershipsEnabled
? "OIDC Group Membership Mapping Enabled. Assign users to this group in your OIDC provider."
: undefined
}
>
<div className="mb-4 flex items-center justify-center">
<Button
variant="solid"
colorSchema="secondary"
isDisabled={isOidcManageGroupMembershipsEnabled || !isAllowed}
onClick={() => {
handlePopUpOpen("addGroupMembers", {
groupId,
slug: groupSlug
});
}}
>
Add members
</Button>
</div>
</Tooltip>
)}
</OrgPermissionCan>
)}

@ -3,7 +3,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useOidcManageGroupMembershipsEnabled } from "@app/hooks/api";
import { TGroupUser } from "@app/hooks/api/groups/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
@ -19,6 +20,11 @@ export const GroupMembershipRow = ({
user: { firstName, lastName, username, joinedGroupAt, email, id },
handlePopUpOpen
}: Props) => {
const { currentOrg } = useOrganization();
const { data: isOidcManageGroupMembershipsEnabled = false } =
useOidcManageGroupMembershipsEnabled(currentOrg.id);
return (
<Tr className="items-center" key={`group-user-${id}`}>
<Td>
@ -36,15 +42,21 @@ export const GroupMembershipRow = ({
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => {
return (
<Tooltip content="Remove user from group">
<Tooltip
content={
isOidcManageGroupMembershipsEnabled
? "OIDC Group Membership Mapping Enabled. Remove user from this group in your OIDC provider."
: "Remove user from group"
}
>
<IconButton
isDisabled={!isAllowed}
isDisabled={!isAllowed || isOidcManageGroupMembershipsEnabled}
ariaLabel="Remove user from group"
onClick={() => handlePopUpOpen("removeMemberFromGroup", { username })}
variant="plain"
colorSchema="danger"
>
<FontAwesomeIcon icon={faUserMinus} className="cursor-pointer" />
<FontAwesomeIcon icon={faUserMinus} />
</IconButton>
</Tooltip>
);

@ -1,7 +1,10 @@
import { faInfoCircle, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, Switch } from "@app/components/v2";
import { Button, Switch, Tooltip } from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
@ -79,6 +82,29 @@ export const OrgOIDCSection = (): JSX.Element => {
}
};
const handleOIDCGroupManagement = async (value: boolean) => {
try {
if (!currentOrg?.id) return;
if (!subscription?.oidcSSO) {
handlePopUpOpen("upgradePlan");
return;
}
await mutateAsync({
orgSlug: currentOrg?.slug,
manageGroupMemberships: value
});
createNotification({
text: `Successfully ${value ? "enabled" : "disabled"} OIDC group membership mapping`,
type: "success"
});
} catch (err) {
console.error(err);
}
};
const addOidcButtonClick = async () => {
if (subscription?.oidcSSO && currentOrg) {
handlePopUpOpen("addOIDC");
@ -148,6 +174,65 @@ export const OrgOIDCSection = (): JSX.Element => {
Enforce members to authenticate via OIDC to access this organization
</p>
</div>
<div className="py-4">
<div className="mb-2 flex justify-between">
<div className="text-md flex items-center text-mineshaft-100">
<span>OIDC Group Membership Mapping</span>
<Tooltip
className="max-w-lg"
content={
<>
<p>
When this feature is enabled, Infisical will automatically sync group
memberships between the OIDC provider and Infisical. Users will be added to
Infisical groups that match their OIDC group names, and removed from any
Infisical groups not present in their groups claim. When enabled, manual
management of Infisical group memberships will be disabled.
</p>
<p className="mt-4">
To use this feature you must include group claims in the OIDC token.
</p>
<a
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 hover:text-mineshaft-300"
href="https://infisical.com/docs/documentation/platform/sso/overview"
>
See your OIDC provider docs for details.
</a>
<p className="mt-4 text-yellow">
<FontAwesomeIcon className="mr-1" icon={faWarning} />
Group membership changes in the OIDC provider only sync with Infisical when a
user logs in via OIDC. For example, if you remove a user from a group in the
OIDC provider, this change will not be reflected in Infisical until their next
OIDC login. To ensure this behavior, Infisical recommends enabling Enforce OIDC
SSO.
</p>
</>
}
>
<FontAwesomeIcon
icon={faInfoCircle}
size="sm"
className="ml-1 mt-0.5 inline-block text-mineshaft-400"
/>
</Tooltip>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enforce-org-auth"
isChecked={data?.manageGroupMemberships ?? false}
onCheckedChange={(value) => handleOIDCGroupManagement(value)}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Infisical will manage user group memberships based on the OIDC provider
</p>
</div>
<OIDCModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}

@ -189,17 +189,19 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
animate={{ x: 0, opacity: 1 }}
exit={{ x: 10, opacity: 0 }}
>
<Tooltip content="Copy secret name">
<IconButton
ariaLabel="copy-value"
variant="plain"
size="sm"
className="p-0 opacity-0 group-hover:opacity-100"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isSecNameCopied ? faCheck : faCopy} />
</IconButton>
</Tooltip>
<div className="relative">
<Tooltip content="Copy secret name">
<IconButton
ariaLabel="copy-value"
variant="plain"
size="sm"
className="p-0 opacity-100"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isSecNameCopied ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</motion.div>
) : (
<motion.div
@ -209,44 +211,48 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
animate={{ x: 0, opacity: 1 }}
exit={{ x: -10, opacity: 0 }}
>
<Tooltip content={errors.key ? errors.key.message : "Save"}>
<IconButton
ariaLabel="more"
variant="plain"
type="submit"
size="md"
className={twMerge(
"p-0 text-primary opacity-0 group-hover:opacity-100",
isDirty && "opacity-100"
)}
isDisabled={isSubmitting || Boolean(errors.key)}
>
{isSubmitting ? (
<Spinner className="m-0 h-4 w-4 p-0" />
) : (
<FontAwesomeIcon
icon={faCheck}
size="lg"
className={twMerge("text-primary", errors.key && "text-mineshaft-400")}
/>
)}
</IconButton>
</Tooltip>
<Tooltip content="Cancel">
<IconButton
ariaLabel="more"
variant="plain"
size="md"
className={twMerge(
"p-0 opacity-0 group-hover:opacity-100",
isDirty && "opacity-100"
)}
onClick={() => reset()}
isDisabled={isSubmitting}
>
<FontAwesomeIcon icon={faClose} size="lg" />
</IconButton>
</Tooltip>
<div className="relative">
<Tooltip content={errors.key ? errors.key.message : "Save"}>
<IconButton
ariaLabel="more"
variant="plain"
type="submit"
size="md"
className={twMerge(
"p-0 text-primary opacity-0 group-hover:opacity-100",
isDirty && "opacity-100"
)}
isDisabled={isSubmitting || Boolean(errors.key)}
>
{isSubmitting ? (
<Spinner className="m-0 h-4 w-4 p-0" />
) : (
<FontAwesomeIcon
icon={faCheck}
size="lg"
className={twMerge("text-primary", errors.key && "text-mineshaft-400")}
/>
)}
</IconButton>
</Tooltip>
</div>
<div className="relative">
<Tooltip content="Cancel">
<IconButton
ariaLabel="more"
variant="plain"
size="md"
className={twMerge(
"p-0 opacity-0 group-hover:opacity-100",
isDirty && "opacity-100"
)}
onClick={() => reset()}
isDisabled={isSubmitting}
>
<FontAwesomeIcon icon={faClose} size="lg" />
</IconButton>
</Tooltip>
</div>
</motion.div>
)}
</AnimatePresence>

@ -1,5 +1,5 @@
import { subject } from "@casl/ability";
import { faMinusSquare, faTrash } from "@fortawesome/free-solid-svg-icons";
import { faAnglesRight, faMinusSquare, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
@ -19,6 +19,7 @@ import {
TDeleteSecretBatchDTO,
TSecretFolder
} from "@app/hooks/api/types";
import { MoveSecretsModal } from "@app/pages/secret-manager/OverviewPage/components/SelectionPanel/components";
export enum EntryType {
FOLDER = "folder",
@ -38,7 +39,8 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
const { permission } = useProjectPermission();
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"bulkDeleteEntries"
"bulkDeleteEntries",
"bulkMoveSecrets"
] as const);
const selectedFolderCount = Object.keys(selectedEntries.folder).length;
@ -165,6 +167,8 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
}
};
const areFoldersSelected = Boolean(Object.keys(selectedEntries[EntryType.FOLDER]).length);
return (
<>
<div
@ -181,19 +185,46 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
</Tooltip>
<div className="ml-1 flex-grow px-2 text-sm">{selectedCount} Selected</div>
{shouldShowDelete && (
<Button
variant="outline_bg"
colorSchema="danger"
leftIcon={<FontAwesomeIcon icon={faTrash} />}
className="ml-4"
onClick={() => handlePopUpOpen("bulkDeleteEntries")}
size="xs"
>
Delete
</Button>
<>
<Tooltip content={areFoldersSelected ? "Moving folders is not supported" : undefined}>
<div>
<Button
isDisabled={areFoldersSelected}
variant="outline_bg"
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faAnglesRight} />}
className="ml-4"
onClick={() => handlePopUpOpen("bulkMoveSecrets")}
size="xs"
>
Move
</Button>
</div>
</Tooltip>
<Button
variant="outline_bg"
colorSchema="danger"
leftIcon={<FontAwesomeIcon icon={faTrash} />}
className="ml-4"
onClick={() => handlePopUpOpen("bulkDeleteEntries")}
size="xs"
>
Delete
</Button>
</>
)}
</div>
</div>
<MoveSecretsModal
isOpen={popUp.bulkMoveSecrets.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("bulkMoveSecrets", isOpen)}
environments={userAvailableEnvs}
projectId={workspaceId}
projectSlug={currentWorkspace.slug}
sourceSecretPath={secretPath}
secrets={selectedEntries[EntryType.SECRET]}
onComplete={resetSelectedEntries}
/>
<DeleteActionModal
isOpen={popUp.bulkDeleteEntries.isOpen}
deleteKey="delete"

@ -0,0 +1,395 @@
import { useEffect, useMemo, useState } from "react";
import { SingleValue } from "react-select";
import { subject } from "@casl/ability";
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
import {
faBan,
faCheckCircle,
faExclamationCircle,
faEyeSlash,
faInfoCircle,
faWarning
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import axios from "axios";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import {
Button,
FilterableSelect,
FormControl,
Modal,
ModalClose,
ModalContent,
Spinner,
Switch
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { useDebounce } from "@app/hooks";
import { useMoveSecrets } from "@app/hooks/api";
import { useGetProjectSecretsQuickSearch } from "@app/hooks/api/dashboard";
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
environments: WorkspaceEnv[];
projectId: string;
projectSlug: string;
sourceSecretPath: string;
secrets: Record<string, Record<string, SecretV3RawSanitized>>;
onComplete: () => void;
};
type ContentProps = Omit<Props, "isOpen" | "onOpenChange">;
type OptionValue = { secretPath: string };
enum MoveResult {
Success = "success",
Info = "info",
Error = "error"
}
type MoveResults = {
status: MoveResult;
name: string;
id: string;
message: string;
}[];
const Content = ({
onComplete,
secrets,
projectSlug,
environments,
projectId,
sourceSecretPath
}: ContentProps) => {
const [search, setSearch] = useState(sourceSecretPath);
const [debouncedSearch] = useDebounce(search);
const [value, setValue] = useState<OptionValue | null>({ secretPath: sourceSecretPath });
const [previousValue, setPreviousValue] = useState<OptionValue | null>(value);
const moveSecrets = useMoveSecrets();
const [shouldOverwrite, setShouldOverwrite] = useState(false);
const { permission } = useProjectPermission();
const [moveResults, setMoveResults] = useState<MoveResults | null>(null);
const { data, isPending, isLoading, isFetching } = useGetProjectSecretsQuickSearch({
secretPath: "/",
environments: environments.map((env) => env.slug),
projectId,
search: debouncedSearch,
tags: {}
});
const { folders = {} } = data ?? {};
const folderEnvironments = value && folders[value.secretPath]?.map((folder) => folder.envId);
const moveSecretsEligibility = useMemo(() => {
return Object.fromEntries(
environments.map((env) => [
env.slug,
{
missingPermissions: permission.cannot(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment: env.slug,
secretPath: sourceSecretPath,
secretName: "*",
secretTags: ["*"]
})
),
missingPath: folderEnvironments && !folderEnvironments?.includes(env.id)
}
])
);
}, [permission, folderEnvironments]);
const destinationSelected = Boolean(value?.secretPath) && sourceSecretPath !== value?.secretPath;
const environmentsToBeSkipped = useMemo(() => {
if (!destinationSelected) return [];
const environmentWarnings: { type: "permission" | "missing"; message: string; id: string }[] =
[];
environments.forEach((env) => {
if (moveSecretsEligibility[env.slug].missingPermissions) {
environmentWarnings.push({
id: env.id,
type: "permission",
message: `${env.name}: You do not have permission to remove secrets from this environment`
});
return;
}
if (moveSecretsEligibility[env.slug].missingPath) {
environmentWarnings.push({
id: env.id,
type: "missing",
message: `${env.name}: Secret path does not exist in environment`
});
}
});
return environmentWarnings;
}, [moveSecretsEligibility]);
const handleMoveSecrets = async () => {
if (!value) {
createNotification({
text: "error",
title: "You must specify a secret path to move the selected secrets to"
});
return;
}
const results: MoveResults = [];
const secretsByEnv: Record<string, SecretV3RawSanitized[]> = Object.fromEntries(
environments.map((env) => [env.slug, []])
);
Object.values(secrets).forEach((secretRecord) =>
Object.entries(secretRecord).map(([env, secret]) => secretsByEnv[env].push(secret))
);
// eslint-disable-next-line no-restricted-syntax
for await (const environment of environments) {
const envSlug = environment.slug;
const secretsToMove = secretsByEnv[envSlug];
if (
moveSecretsEligibility[envSlug].missingPermissions ||
moveSecretsEligibility[envSlug].missingPath
) {
// eslint-disable-next-line no-continue
continue;
}
if (!secretsToMove.length) {
results.push({
name: environment.name,
message: "No secrets selected in environment",
status: MoveResult.Info,
id: environment.id
});
// eslint-disable-next-line no-continue
continue;
}
try {
const { isDestinationUpdated, isSourceUpdated } = await moveSecrets.mutateAsync({
projectSlug,
shouldOverwrite,
sourceEnvironment: environment.slug,
sourceSecretPath,
destinationEnvironment: environment.slug,
destinationSecretPath: value.secretPath,
projectId,
secretIds: secretsToMove.map((sec) => sec.id)
});
let message = "";
let status: MoveResult = MoveResult.Info;
if (isDestinationUpdated && isSourceUpdated) {
message = "Successfully moved selected secrets";
status = MoveResult.Success;
} else if (isDestinationUpdated) {
message =
"Successfully created secrets in destination. A secret approval request has been generated for the source.";
} else if (isSourceUpdated) {
message = "A secret approval request has been generated in the destination";
} else {
message =
"A secret approval request has been generated in both the source and the destination.";
}
results.push({
name: environment.name,
message,
status,
id: environment.id
});
} catch (error) {
let errorMessage = (error as Error)?.message ?? "Failed to move secrets";
if (axios.isAxiosError(error)) {
const { message } = error?.response?.data as { message: string };
if (message) errorMessage = message;
}
results.push({
name: environment.name,
message: errorMessage,
status: MoveResult.Error,
id: environment.id
});
}
}
setMoveResults(results);
};
useEffect(() => {
return () => {
if (moveResults) onComplete();
};
}, [moveResults]);
if (moveResults) {
return (
<div className="w-full">
<div className="mb-2">Results</div>
<div className="mb-4 flex flex-col divide-y divide-mineshaft-600 rounded bg-mineshaft-900 px-3 py-2">
{moveResults.map(({ id, name, status, message }) => {
let className: string;
let icon: IconDefinition;
switch (status) {
case MoveResult.Success:
icon = faCheckCircle;
className = "text-green";
break;
case MoveResult.Info:
icon = faInfoCircle;
className = "text-blue-500";
break;
case MoveResult.Error:
default:
icon = faExclamationCircle;
className = "text-red";
}
return (
<div key={id} className="p-2 text-sm">
<FontAwesomeIcon className={twMerge(className, "mr-1")} icon={icon} /> {name}:{" "}
{message}
</div>
);
})}
</div>
<ModalClose asChild>
<Button size="sm" colorSchema="secondary" onClick={() => onComplete()}>
Dismiss
</Button>
</ModalClose>
</div>
);
}
if (moveSecrets.isPending) {
return (
<div className="flex h-full flex-col items-center justify-center py-2.5">
<Spinner size="lg" className="text-mineshaft-500" />
<p className="mt-4 text-sm text-mineshaft-400">Moving secrets...</p>
</div>
);
}
return (
<>
<FormControl
label="Select New Location"
helperText="Nested folders will be displayed as secret path is typed"
>
<FilterableSelect
isLoading={isPending || isLoading || isFetching || search !== debouncedSearch}
options={Object.keys(folders).map((secretPath) => ({
secretPath
}))}
onMenuOpen={() => {
setPreviousValue(value);
setSearch(value?.secretPath ?? "/");
setValue(null);
}}
onMenuClose={() => {
if (!value) setValue(previousValue);
}}
inputValue={search}
onInputChange={setSearch}
value={value}
onChange={(newValue) => {
setPreviousValue(value);
setValue(newValue as SingleValue<OptionValue>);
}}
getOptionLabel={(option) => option.secretPath}
getOptionValue={(option) => option.secretPath}
/>
</FormControl>
{Boolean(environmentsToBeSkipped.length) && (
<div className="rounded bg-mineshaft-900 px-3 py-2">
<span className="text-sm text-yellow">
<FontAwesomeIcon icon={faWarning} className="mr-0.5" /> The following environments will
not be affected
</span>
{environmentsToBeSkipped.map((env) => (
<div
key={env.id}
className={`${env.type === "permission" ? "text-red" : "text-mineshaft-300"} mb-0.5 flex items-start gap-2 text-sm`}
>
<FontAwesomeIcon
className="mt-1"
icon={env.type === "permission" ? faBan : faEyeSlash}
/>
<span>{env.message}</span>
</div>
))}
</div>
)}
<FormControl
className="my-4"
helperText={
shouldOverwrite
? "Secrets with conflicting keys at the destination will be overwritten"
: "Secrets with conflicting keys at the destination will not be overwritten"
}
>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-yellow/80"
id="overwrite-existing-secrets"
thumbClassName="bg-mineshaft-800"
onCheckedChange={setShouldOverwrite}
isChecked={shouldOverwrite}
>
<p className="w-[11rem]">Overwrite Existing Secrets</p>
</Switch>
</FormControl>
<div className="mt-6 flex items-center">
<Button
isDisabled={!destinationSelected}
className="mr-4"
size="sm"
colorSchema="secondary"
onClick={handleMoveSecrets}
>
Move Secrets
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</>
);
};
export const MoveSecretsModal = ({ isOpen, onOpenChange, ...props }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
bodyClassName="overflow-visible"
title="Move Secrets Folder Location"
subTitle="Move the selected secrets across all environments to a new folder location"
>
<Content {...props} />
</ModalContent>
</Modal>
);
};

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

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

@ -1,3 +1,4 @@
{{- if .Values.installCRDs }}
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
@ -465,4 +466,5 @@ status:
kind: ""
plural: ""
conditions: []
storedVersions: []
storedVersions: []
{{- end }}

@ -32,7 +32,7 @@ controllerManager:
- ALL
image:
repository: infisical/kubernetes-operator
tag: v0.8.8
tag: v0.8.11
resources:
limits:
cpu: 500m

@ -0,0 +1,8 @@
cd "infisical-standalone-postgres"
helm dependency update
helm package .
for i in *.tgz; do
[ -f "$i" ] || break
cloudsmith push helm --republish infisical/helm-charts "$i"
done
cd ..

@ -0,0 +1,8 @@
cd secrets-operator
helm dependency update
helm package .
for i in *.tgz; do
[ -f "$i" ] || break
cloudsmith push helm --republish infisical/helm-charts "$i"
done
cd ..

@ -104,7 +104,7 @@ spec:
includeAllSecrets: true
data:
SSH_KEY: "{{ .KEY.SecretPath }} {{ .KEY.Value }}"
BINARY_KEY: "{{ toBase64DecodedString .BINARY_KEY_BASE64.Value }}"
BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}"
creationPolicy: "Orphan" ## Owner | Orphan
# secretType: kubernetes.io/dockerconfigjson

@ -156,12 +156,12 @@ func (r *InfisicalSecretReconciler) getInfisicalServiceAccountCredentialsFromKub
}
var infisicalSecretTemplateFunctions = template.FuncMap{
"decodeBase64ToBytes": func(encodedString string) []byte {
"decodeBase64ToBytes": func(encodedString string) string {
decoded, err := base64.StdEncoding.DecodeString(encodedString)
if err != nil {
panic(fmt.Sprintf("Error: %v", err))
}
return decoded
return string(decoded)
},
}
@ -222,7 +222,6 @@ func (r *InfisicalSecretReconciler) createInfisicalManagedKubeSecret(ctx context
}
annotations[constants.SECRET_VERSION_ANNOTATION] = ETag
// create a new secret as specified by the managed secret spec of CRD
newKubeSecretInstance := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{