Compare commits

...

51 Commits

Author SHA1 Message Date
Scott Wilson
7090eea716 Merge pull request #3069 from Infisical/oidc-group-membership-mapping
Feature: OIDC Group Membership Mapping
2025-01-31 11:32:38 -08:00
Scott Wilson
01d3443139 improvement: update docker dev and makefile for keycloak dev 2025-01-31 11:14:49 -08:00
Scott Wilson
c4b23a8d4f improvement: improve grammar 2025-01-31 11:05:56 -08:00
Scott Wilson
90a2a11fff improvement: update tooltips 2025-01-31 11:04:20 -08:00
Scott Wilson
95d7c2082c improvements: address feedback 2025-01-31 11:01:54 -08:00
Sheen
ab5eb4c696 Merge pull request #3070 from Infisical/misc/readded-operator-installation-flag
misc: readded operator installation flag for secret CRD
2025-01-31 16:53:57 +08:00
Akhil Mohan
65aeb81934 Merge pull request #3011 from xinbenlv/patch-1
Fix grammar on overview.mdx
2025-01-31 14:22:03 +05:30
Akhil Mohan
a406511405 Merge pull request #3048 from isaiahmartin847/refactor/copy-secret
Improve Visibility and Alignment of Tooltips and Copy Secret Key Icon
2025-01-31 14:20:02 +05:30
Sheen Capadngan
61da0db49e misc: readded operator installation flag for CRD 2025-01-31 16:03:42 +08:00
Scott Wilson
59666740ca chore: revert license and remove unused query key/doc reference 2025-01-30 10:35:23 -08:00
Scott Wilson
9cc7edc869 feature: oidc group membership mapping 2025-01-30 10:21:30 -08:00
Daniel Hougaard
e1b016f76d Merge pull request #3068 from nicogiard/patch-1
fix: wrong client variable in c# code example
2025-01-29 22:24:03 +01:00
Nicolas Giard
1175b9b5af fix: wrong client variable
The InfisicalClient variable was wrong
2025-01-29 21:57:57 +01:00
Maidul Islam
09521144ec Merge pull request #3066 from akhilmhdh/fix/secret-list-plain
Resolved list secret plain to have key as well
2025-01-29 14:04:49 -05:00
=
8759944077 feat: resolved list secret plain to have key as well 2025-01-30 00:31:47 +05:30
Maidul Islam
aac3c355e9 Merge pull request #3061 from Infisical/secret-sync-ui-doc-improvements
improvements: Import Behavior Doc/UI Clarification and Minor Integration Layout Adjustments
2025-01-29 13:16:21 -05:00
Akhil Mohan
2a28a462a5 Merge pull request #3053 from Infisical/daniel/k8s-insight
k8s: bug fixes and better prints
2025-01-29 23:16:46 +05:30
Scott Wilson
3328e0850f improvements: revise descriptions 2025-01-29 09:44:46 -08:00
Maidul Islam
216cae9b33 Merge pull request #3058 from Infisical/misc/improved-helper-text-for-gcp-sa-field
misc: improved helper text for GCP sa field
2025-01-29 09:54:20 -05:00
Akhil Mohan
89d4d4bc92 Merge pull request #3064 from akhilmhdh/fix/secret-path-validation-permission
feat: added validation for secret path in permission
2025-01-29 18:46:38 +05:30
=
cffcb28bc9 feat: removed secret path check in glob 2025-01-29 17:50:02 +05:30
=
61388753cf feat: updated to support in error in ui 2025-01-29 17:32:13 +05:30
=
a6145120e6 feat: added validation for secret path in permission 2025-01-29 17:01:45 +05:30
Sheen Capadngan
dacffbef08 doc: documentation updates for gcp app connection 2025-01-29 18:12:17 +08:00
Sheen Capadngan
4db3e5d208 Merge remote-tracking branch 'origin/main' into misc/improved-helper-text-for-gcp-sa-field 2025-01-29 17:43:48 +08:00
Maidul Islam
2a84d61862 add guide for how to wrote a design doc 2025-01-28 23:31:12 -05:00
Scott Wilson
a5945204ad improvements: import behavior clarification and minor integration layout adjustments 2025-01-28 19:09:43 -08:00
Vlad Matsiiako
e99eb47cf4 Merge pull request #3059 from Infisical/minor-doc-adjustments
Improvements: Integration Docs Nav Bar Reorder & Azure Integration Logo fix
2025-01-28 14:14:54 -08:00
Scott Wilson
cf107c0c0d improvements: change integration nav bar order and correct azure integrations image references 2025-01-28 12:51:24 -08:00
Sheen Capadngan
9fcb1c2161 misc: added emphasis on suffix 2025-01-29 04:38:16 +08:00
Daniel Hougaard
70515a1ca2 Merge pull request #3045 from Infisical/daniel/auditlogs-secret-path-query
feat(audit-logs): query by secret path
2025-01-28 21:17:42 +01:00
Scott Wilson
955cf9303a Merge pull request #3052 from Infisical/set-password-feature
Feature: Setup Password
2025-01-28 12:08:24 -08:00
Daniel Hougaard
a24ef46d7d requested changes 2025-01-28 20:44:45 +01:00
Sheen Capadngan
ee49f714b9 misc: added valid example to error thrown for sa mismatch 2025-01-29 03:41:24 +08:00
Daniel Hougaard
657aca516f Merge pull request #3049 from Infisical/daniel/vercel-custom-envs
feat(integrations/vercel): custom environments support
2025-01-28 20:38:40 +01:00
Sheen Capadngan
b5d60398d6 misc: improved helper text for GCP sa field 2025-01-29 03:10:37 +08:00
Sheen
c3d515bb95 Merge pull request #3039 from Infisical/feat/gcp-secret-sync
feat: gcp app connections and secret sync
2025-01-29 02:23:22 +08:00
Scott Wilson
d74b819f57 improvements: make logged in status disclaimer in email more prominent and only add email auth method if not already present 2025-01-28 09:53:40 -08:00
Daniel Hougaard
2c75e23acf helm 2025-01-28 04:21:29 +01:00
Daniel Hougaard
907dd4880a fix(k8): reconcile on status update 2025-01-28 04:20:51 +01:00
Scott Wilson
6af7c5c371 improvements: remove removed property reference and remove excess padding/margin on secret sync pages 2025-01-27 19:12:05 -08:00
Scott Wilson
72468d5428 feature: setup password 2025-01-27 18:51:35 -08:00
Daniel Hougaard
939ee892e0 chore: cleanup 2025-01-28 01:02:18 +01:00
Daniel Hougaard
27af943ee1 Update integration-sync-secret.ts 2025-01-27 23:18:46 +01:00
Daniel Hougaard
9b772ad55a Update VercelConfigurePage.tsx 2025-01-27 23:11:57 +01:00
Daniel Hougaard
94a1fc2809 chore: cleanup 2025-01-27 23:11:14 +01:00
Daniel Hougaard
10c10642a1 feat(integrations/vercel): custom environments support 2025-01-27 23:08:47 +01:00
isaiahmartin847
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
isaiahmartin847
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
Daniel Hougaard
27efc908e2 feat(audit-logs): query by secret path 2025-01-27 15:53:07 +01:00
xinbenlv
645dfafba0 Fix grammar on overview.mdx 2025-01-20 09:02:18 -08:00
99 changed files with 2015 additions and 446 deletions

View File

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

View File

@@ -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");
}
});
}

View File

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

View File

@@ -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 };
}
});
};

View File

@@ -39,11 +39,13 @@ export const auditLogDALFactory = (db: TDbClient) => {
offset = 0,
actorId,
actorType,
secretPath,
eventType,
eventMetadata
}: Omit<TFindQuery, "actor" | "eventType"> & {
actorId?: string;
actorType?: ActorType;
secretPath?: string;
eventType?: EventType[];
eventMetadata?: Record<string, string>;
},
@@ -88,6 +90,10 @@ export const auditLogDALFactory = (db: TDbClient) => {
});
}
if (projectId && secretPath) {
void sqlQuery.whereRaw(`"eventMetadata" @> jsonb_build_object('secretPath', ?::text)`, [secretPath]);
}
// Filter by actor type
if (actorType) {
void sqlQuery.where("actor", actorType);

View File

@@ -46,10 +46,6 @@ export const auditLogServiceFactory = ({
actorOrgId
);
/**
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
* to the organization level ✅
*/
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
}
@@ -64,6 +60,7 @@ export const auditLogServiceFactory = ({
actorId: filter.auditLogActorId,
actorType: filter.actorType,
eventMetadata: filter.eventMetadata,
secretPath: filter.secretPath,
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
});

View File

@@ -32,6 +32,7 @@ export type TListProjectAuditLogDTO = {
projectId?: string;
auditLogActorId?: string;
actorType?: ActorType;
secretPath?: string;
eventMetadata?: Record<string, string>;
};
} & Omit<TProjectPermission, "projectId">;
@@ -248,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 {
@@ -2043,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
@@ -2231,4 +2254,6 @@ export type Event =
| DeleteSecretSyncEvent
| SecretSyncSyncSecretsEvent
| SecretSyncImportSecretsEvent
| SecretSyncRemoveSecretsEvent;
| SecretSyncRemoveSecretsEvent
| OidcGroupMembershipMappingAssignUserEvent
| OidcGroupMembershipMappingRemoveUserEvent;

View File

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

View File

@@ -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 };
};

View File

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

View File

@@ -163,6 +163,27 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
z.string().refine((val) => val.startsWith("/"), SECRET_PATH_MISSING_SLASH_ERR_MSG),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ].refine(
(val) => val.startsWith("/"),
SECRET_PATH_MISSING_SLASH_ERR_MSG
),
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ].refine(
(val) => val.startsWith("/"),
SECRET_PATH_MISSING_SLASH_ERR_MSG
),
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN].refine(
(val) => val.every((el) => el.startsWith("/")),
SECRET_PATH_MISSING_SLASH_ERR_MSG
),
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
})
.partial()
]);
// akhilmhdh: don't modify this for v2
// if you want to update create a new schema
const SecretConditionV1Schema = z
@@ -177,17 +198,7 @@ const SecretConditionV1Schema = z
})
.partial()
]),
secretPath: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
})
.partial()
])
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA
})
.partial();
@@ -204,17 +215,7 @@ const SecretConditionV2Schema = z
})
.partial()
]),
secretPath: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
})
.partial()
]),
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA,
secretName: z.union([
z.string(),
z

View File

@@ -828,6 +828,8 @@ export const AUDIT_LOGS = {
projectId:
"Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.",
eventType: "The type of the event to export.",
secretPath:
"The path of the secret to query audit logs for. Note that the projectId parameter must also be provided.",
userAgentType: "Choose which consuming application to export audit logs for.",
eventMetadata:
"Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.",

View File

@@ -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({

View File

@@ -1151,6 +1151,50 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
}
});
server.route({
method: "GET",
url: "/:integrationAuthId/vercel/custom-environments",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
querystring: z.object({
teamId: z.string().trim()
}),
params: z.object({
integrationAuthId: z.string().trim()
}),
response: {
200: z.object({
environments: z
.object({
appId: z.string(),
customEnvironments: z
.object({
id: z.string(),
slug: z.string()
})
.array()
})
.array()
})
}
},
handler: async (req) => {
const environments = await server.services.integrationAuth.getVercelCustomEnvironments({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
teamId: req.query.teamId
});
return { environments };
}
});
server.route({
method: "GET",
url: "/:integrationAuthId/octopus-deploy/spaces",

View File

@@ -11,7 +11,7 @@ import {
} from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getLastMidnightDateISO } from "@app/lib/fn";
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -113,6 +113,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
querystring: z.object({
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
actorType: z.nativeEnum(ActorType).optional(),
secretPath: z
.string()
.optional()
.transform((val) => (!val ? val : removeTrailingSlash(val)))
.describe(AUDIT_LOGS.EXPORT.secretPath),
// eventType is split with , for multiple values, we need to transform it to array
eventType: z
.string()

View File

@@ -203,7 +203,8 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim()
verifier: z.string().trim(),
password: z.string().trim()
}),
response: {
200: z.object({
@@ -218,7 +219,69 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
userId: token.userId
});
return { message: "Successfully updated backup private key" };
return { message: "Successfully reset password" };
}
});
server.route({
method: "POST",
url: "/email/password-setup",
config: {
rateLimit: authRateLimit
},
schema: {
response: {
200: z.object({
message: z.string()
})
}
},
handler: async (req) => {
await server.services.password.sendPasswordSetupEmail(req.permission);
return {
message: "A password setup link has been sent"
};
}
});
server.route({
method: "POST",
url: "/password-setup",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
password: z.string().trim(),
token: z.string().trim()
}),
response: {
200: z.object({
message: z.string()
})
}
},
handler: async (req, res) => {
await server.services.password.setupPassword(req.body, req.permission);
const appCfg = getConfig();
void res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
return { message: "Successfully setup password" };
}
});
};

View File

@@ -153,7 +153,7 @@ export const validateGcpConnectionCredentials = async (appConnection: TGcpConnec
const serviceAccountId = appConnection.credentials.serviceAccountEmail.split("@")[0];
if (!serviceAccountId.endsWith(expectedAccountIdSuffix)) {
throw new BadRequestError({
message: `GCP service account ID (the part of the email before '@') must have a suffix of "${expectedAccountIdSuffix}"`
message: `GCP service account ID must have a suffix of "${expectedAccountIdSuffix}" e.g. service-account-${expectedAccountIdSuffix}@my-project.iam.gserviceaccount.com"`
});
}
}

View File

@@ -57,6 +57,12 @@ export const getTokenConfig = (tokenType: TokenType) => {
const expiresAt = new Date(new Date().getTime() + 86400000);
return { token, expiresAt };
}
case TokenType.TOKEN_EMAIL_PASSWORD_SETUP: {
// generate random hex
const token = crypto.randomBytes(16).toString("hex");
const expiresAt = new Date(new Date().getTime() + 86400000);
return { token, expiresAt };
}
case TokenType.TOKEN_USER_UNLOCK: {
const token = crypto.randomBytes(16).toString("hex");
const expiresAt = new Date(new Date().getTime() + 259200000);

View File

@@ -6,6 +6,7 @@ export enum TokenType {
TOKEN_EMAIL_MFA = "emailMfa",
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset",
TOKEN_EMAIL_PASSWORD_SETUP = "passwordSetup",
TOKEN_USER_UNLOCK = "userUnlock"
}

View File

@@ -4,6 +4,8 @@ import jwt from "jsonwebtoken";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
@@ -11,8 +13,13 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TAuthDALFactory } from "./auth-dal";
import { TChangePasswordDTO, TCreateBackupPrivateKeyDTO, TResetPasswordViaBackupKeyDTO } from "./auth-password-type";
import { AuthTokenType } from "./auth-type";
import {
TChangePasswordDTO,
TCreateBackupPrivateKeyDTO,
TResetPasswordViaBackupKeyDTO,
TSetupPasswordViaBackupKeyDTO
} from "./auth-password-type";
import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
type TAuthPasswordServiceFactoryDep = {
authDAL: TAuthDALFactory;
@@ -169,8 +176,13 @@ export const authPaswordServiceFactory = ({
verifier,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
userId
userId,
password
}: TResetPasswordViaBackupKeyDTO) => {
const cfg = getConfig();
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
await userDAL.updateUserEncryptionByUserId(userId, {
encryptionVersion: 2,
protectedKey,
@@ -180,7 +192,8 @@ export const authPaswordServiceFactory = ({
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
verifier,
hashedPassword
});
await userDAL.updateById(userId, {
@@ -267,6 +280,108 @@ export const authPaswordServiceFactory = ({
return backupKey;
};
const sendPasswordSetupEmail = async (actor: OrgServiceActor) => {
if (actor.type !== ActorType.USER)
throw new BadRequestError({ message: `Actor of type ${actor.type} cannot set password` });
const user = await userDAL.findById(actor.id);
if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` });
if (!user.isAccepted || !user.authMethods)
throw new BadRequestError({ message: `You must complete signup to set a password` });
const cfg = getConfig();
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
userId: user.id
});
const email = user.email ?? user.username;
await smtpService.sendMail({
template: SmtpTemplates.SetupPassword,
recipients: [email],
subjectLine: "Infisical Password Setup",
substitutions: {
email,
token,
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-setup` : ""
}
});
};
const setupPassword = async (
{
encryptedPrivateKey,
protectedKeyTag,
protectedKey,
protectedKeyIV,
salt,
verifier,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
password,
token
}: TSetupPasswordViaBackupKeyDTO,
actor: OrgServiceActor
) => {
try {
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
userId: actor.id,
code: token
});
} catch (e) {
throw new BadRequestError({ message: "Expired or invalid token. Please try again." });
}
await userDAL.transaction(async (tx) => {
const user = await userDAL.findById(actor.id, tx);
if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` });
if (!user.isAccepted || !user.authMethods)
throw new BadRequestError({ message: `You must complete signup to set a password` });
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
await userDAL.updateById(
actor.id,
{
authMethods: [...user.authMethods, AuthMethod.EMAIL]
},
tx
);
}
const cfg = getConfig();
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
await userDAL.updateUserEncryptionByUserId(
actor.id,
{
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
hashedPassword,
serverPrivateKey: null,
clientPublicKey: null
},
tx
);
});
await tokenService.revokeAllMySessions(actor.id);
};
return {
generateServerPubKey,
changePassword,
@@ -274,6 +389,8 @@ export const authPaswordServiceFactory = ({
sendPasswordResetEmail,
verifyPasswordResetEmail,
createBackupPrivateKey,
getBackupPrivateKeyOfUser
getBackupPrivateKeyOfUser,
sendPasswordSetupEmail,
setupPassword
};
};

View File

@@ -23,6 +23,20 @@ export type TResetPasswordViaBackupKeyDTO = {
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
password: string;
};
export type TSetupPasswordViaBackupKeyDTO = {
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
password: string;
token: string;
};
export type TCreateBackupPrivateKeyDTO = {

View File

@@ -132,16 +132,26 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
/**
* Return list of names of apps for Vercel integration
* This is re-used for getting custom environments for Vercel
*/
const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => {
const apps: Array<{ name: string; appId: string }> = [];
export const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => {
const apps: Array<{ name: string; appId: string; customEnvironments: Array<{ slug: string; id: string }> }> = [];
const limit = "20";
let hasMorePages = true;
let next: number | null = null;
interface Response {
projects: { name: string; id: string }[];
projects: {
name: string;
id: string;
customEnvironments?: {
id: string;
type: string;
description: string;
slug: string;
}[];
}[];
pagination: {
count: number;
next: number | null;
@@ -173,7 +183,12 @@ const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null;
data.projects.forEach((a) => {
apps.push({
name: a.name,
appId: a.id
appId: a.id,
customEnvironments:
a.customEnvironments?.map((env) => ({
slug: env.slug,
id: env.id
})) ?? []
});
});

View File

@@ -25,11 +25,12 @@ import { TIntegrationDALFactory } from "../integration/integration-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { getApps } from "./integration-app-list";
import { getApps, getAppsVercel } from "./integration-app-list";
import { TCircleCIContext } from "./integration-app-types";
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
import {
GetVercelCustomEnvironmentsDTO,
OctopusDeployScope,
TBitbucketEnvironment,
TBitbucketWorkspace,
@@ -1825,6 +1826,41 @@ export const integrationAuthServiceFactory = ({
return integrationAuthDAL.create(newIntegrationAuth);
};
const getVercelCustomEnvironments = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
teamId,
id
}: GetVercelCustomEnvironmentsDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: integrationAuth.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const vercelApps = await getAppsVercel({
accessToken,
teamId
});
return vercelApps.map((app) => ({
customEnvironments: app.customEnvironments,
appId: app.appId
}));
};
const getOctopusDeploySpaces = async ({
actorId,
actor,
@@ -1944,6 +1980,7 @@ export const integrationAuthServiceFactory = ({
getIntegrationAccessToken,
duplicateIntegrationAuth,
getOctopusDeploySpaces,
getOctopusDeployScopeValues
getOctopusDeployScopeValues,
getVercelCustomEnvironments
};
};

View File

@@ -284,3 +284,8 @@ export type TOctopusDeployVariableSet = {
Self: string;
};
};
export type GetVercelCustomEnvironmentsDTO = {
teamId: string;
id: string;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -1450,9 +1450,13 @@ const syncSecretsVercel = async ({
secrets: Record<string, { value: string; comment?: string } | null>;
accessToken: string;
}) => {
const isCustomEnvironment = !["development", "preview", "production"].includes(
integration.targetEnvironment as string
);
interface VercelSecret {
id?: string;
type: string;
customEnvironmentIds?: string[];
key: string;
value: string;
target: string[];
@@ -1486,6 +1490,16 @@ const syncSecretsVercel = async ({
}
)
).data.envs.filter((secret) => {
if (isCustomEnvironment) {
if (!secret.customEnvironmentIds?.includes(integration.targetEnvironment as string)) {
// case: secret does not have the same custom environment
return false;
}
// no need to check for preview environment, as custom environments are not available in preview
return true;
}
if (!secret.target.includes(integration.targetEnvironment as string)) {
// case: secret does not have the same target environment
return false;
@@ -1583,7 +1597,13 @@ const syncSecretsVercel = async ({
key,
value: infisicalSecrets[key]?.value,
type: "encrypted",
target: [integration.targetEnvironment as string],
...(isCustomEnvironment
? {
customEnvironmentIds: [integration.targetEnvironment as string]
}
: {
target: [integration.targetEnvironment as string]
}),
...(integration.path
? {
gitBranch: integration.path
@@ -1607,9 +1627,19 @@ const syncSecretsVercel = async ({
key,
value: infisicalSecrets[key]?.value,
type: res[key].type,
target: res[key].target.includes(integration.targetEnvironment as string)
? [...res[key].target]
: [...res[key].target, integration.targetEnvironment as string],
...(!isCustomEnvironment
? {
target: res[key].target.includes(integration.targetEnvironment as string)
? [...res[key].target]
: [...res[key].target, integration.targetEnvironment as string]
}
: {
customEnvironmentIds: res[key].customEnvironmentIds?.includes(integration.targetEnvironment as string)
? [...(res[key].customEnvironmentIds || [])]
: [...(res[key]?.customEnvironmentIds || []), integration.targetEnvironment as string]
}),
...(integration.path
? {
gitBranch: integration.path

View File

@@ -30,6 +30,7 @@ export enum SmtpTemplates {
NewDeviceJoin = "newDevice.handlebars",
OrgInvite = "organizationInvitation.handlebars",
ResetPassword = "passwordReset.handlebars",
SetupPassword = "passwordSetup.handlebars",
SecretLeakIncident = "secretLeakIncident.handlebars",
WorkspaceInvite = "workspaceInvitation.handlebars",
ScimUserProvisioned = "scimUserProvisioned.handlebars",

View File

@@ -0,0 +1,17 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Password Setup</title>
</head>
<body>
<h2>Setup your password</h2>
<p>Someone requested to set up a password for your account.</p>
<p><strong>Make sure you are already logged in to Infisical in the current browser before clicking the link below.</strong></p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Setup password</a>
<p>If you didn't initiate this request, please contact
{{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}</p>
{{emailFooter}}
</body>
</html>

View File

@@ -110,7 +110,7 @@ var secretsCmd = &cobra.Command{
if plainOutput {
for _, secret := range secrets {
fmt.Println(secret.Value)
fmt.Println(fmt.Sprintf("%s=%s", secret.Key, secret.Value))
}
} else {
visualize.PrintAllSecretDetails(secrets)

View File

@@ -0,0 +1,67 @@
---
title: "How to write a design document"
sidebarTitle: "Writing Design Docs"
description: "Learn how to write a design document at Infisical"
---
## **Why write a design document?**
Writing a design document helps you efficiently solve broad, complex engineering problems at Infisical. While planning is important, we are a startup, so speed and urgency should be your top of mind. Keep the process lightweight and time boxed so that we can get the most out of it.
**Writing a design will help you:**
- **Understand the problem space:** Deeply understand the problem youre solving to make sure it is well scoped.
- **Stay on the right path:** Without proper planning, you risk cycling between partial implementation and replanning, encountering roadblocks that force you back to square one. A solid plan minimizes wasted engineering hours.
- **An opportunity to collaborate:** Bring relevant engineers into the discussion to develop well-thought-out solutions and catch potential issues you might have overlooked.
- **Faster implementation:** A well-thought-out plan will help you catch roadblocks early and ship quickly because you know exactly what needs to get implemented.
**When to write a design document:**
- **Write a design doc**: If the feature is not well defined, high-security, or will take more than **1 full engineering week** to build.
- **Skip the design doc**: For small, straightforward features that can be built quickly with informal discussions.
If you are unsure when to create a design doc, chat with @maidul.
## **What to Include in your Design Document**
Every feature/problem is unique, but your design docs should generally include the following sections. If you need to include additional sections, feel free to do so.
1. **Title**
- A descriptive title.
- Name of document owner and name of reviewer(s).
2. **Overview**
- A high-level summary of the problem and proposed solution. Keep it brief (max 3 paragraphs).
3. **Context**
- Explain the problems background, why its important to solve now, and any constraints (e.g., technical, sales, or timeline-related). What do we get out of solving this problem? (needed to close a deal, scale, performance, etc.).
4. **Solution**
- Provide a big-picture explanation of the solution, followed by detailed technical architecture.
- Use diagrams/charts where needed.
- Write clearly so that another engineer could implement the solution in your absence.
5. **Milestones**
- Break the project into phases with clear start and end dates estimates. Use a table or bullet points.
6. **FAQ**
- Common questions or concerns someone might have while reading your document that can be quickly addressed.
## **How to Write a Design Doc**
- **Keep it Simple**: Use clear, simple language. Opt for short sentences, bullet points, and concrete examples over fluff writing.
- **Use Visuals**: Add diagrams and charts for clarity to convey your ideas.
- **Make it Self-Explanatory**: Ensure that anyone reading the document can understand and implement the plan without needing additional context.
Before sharing your design docs with others, review your design doc as if you were a teammate seeing it for the first time. Anticipate questions and address them.
## **Process from start to finish**
1. **Research/Discuss**
- Before you start writing, take some time to research and get a solid understanding of the problem space. Look into how other well-established companies are tackling similar challenges, if they are.
Talk through the problem and your initial solution with other engineers on the team—bounce ideas around and get their feedback. If you have ideas on how the system could if implemented in Infisical, would it effect any downstream features/systems, etc?
Once youve got a general direction, you might need to test a some theories. This is where quick proof of concepts (POCs) come in handy, but dont get too caught up in the details. The goal of a POC is simply to validate a core idea or concept so you can get to the rest of your planning.
2. **Write the Doc**
- Based on your research/discussions, write the design doc and include all relevant sections. Your goal is to come up with a convincing plan on why this is the correct why to solve the problem at hand.
3. **Assign Reviewers**
- Ask a relevant engineer(s) to review your document. Their role is to identify blind spots, challenge assumptions, and ensure everything is clear. Once you and the reviewer are on the same page on the approach, update the document with any missing details they brought up.
4. **Team Review and Feedback**
- Invite the relevant engineers to a design doc review meeting and give them 10-15 minutes to read through the document. After everyone has had a chance to review it, open the floor up for discussion. Address any feedback or concerns raised during this meeting. If significant points were overlooked during your initial planning, you may need to revisit the drawing board. Your goal is to think about the feature holistically and minimize the need for drastic changes to your design doc later on.

View File

@@ -66,7 +66,8 @@
{
"group": "Engineering",
"pages": [
"documentation/engineering/oncall"
"documentation/engineering/oncall",
"documentation/engineering/how-to-write-design-doc"
]
}
]

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 632 KiB

After

Width:  |  Height:  |  Size: 645 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -10,16 +10,21 @@ Infisical supports [service account impersonation](https://cloud.google.com/iam/
configuring your instance to use it.
<Steps>
<Step title="Enable the IAM Service Account Credentials API">
![Service Account API](/images/app-connections/gcp/service-account-credentials-api.png)
</Step>
<Step title="Navigate to IAM & Admin > Service Accounts in Google Cloud Console">
![Service Account Page](/images/app-connections/gcp/service-account-overview.png)
![Service Account IAM Page](/images/app-connections/gcp/service-account-overview.png)
</Step>
<Step title="Create a Service Account">
Create a new service account that will be used to impersonate other GCP service accounts for your app connections.
![Service Account Page](/images/app-connections/gcp/create-instance-service-account.png)
![Create Service Account Page](/images/app-connections/gcp/create-instance-service-account.png)
Press "DONE" after creating the service account.
</Step>
<Step title="Generate Service Account Key">
Download the JSON key file for your service account. This will be used to authenticate your instance with GCP.
![Service Account Page](/images/app-connections/gcp/create-service-account-credential.png)
![Service Account Credential Page](/images/app-connections/gcp/create-service-account-credential.png)
</Step>
<Step title="Configure Your Instance">
1. Copy the entire contents of the downloaded JSON key file.
@@ -55,9 +60,19 @@ Infisical supports [service account impersonation](https://cloud.google.com/iam/
![Assign Service Account Permission](/images/app-connections/gcp/service-account-secret-sync-permission.png)
</Tab>
</Tabs>
After configuring the appropriate roles, press "DONE".
</Step>
<Step title="Enable Service Account Impersonation">
On the new service account, assign the `Service Account Token Creator` role to the Infisical instance's service account. This allows Infisical to impersonate the new service account.
To enable service account impersonation, you'll need to grant the **Service Account Token Creator** role to the Infisical instance's service account. This configuration allows Infisical to securely impersonate the new service account.
- Navigate to the IAM & Admin > Service Accounts section in your Google Cloud Console
- Select the newly created service account
- Click on the "PERMISSIONS" tab
- Click "Grant Access" to add a new principal
If you're using Infisical Cloud US, use the following service account: infisical-us@infisical-us.iam.gserviceaccount.com
If you're using Infisical Cloud EU, use the following service account: infisical-eu@infisical-eu.iam.gserviceaccount.com
![Service Account Page](/images/app-connections/gcp/service-account-grant-access.png)
</Step>

View File

@@ -38,8 +38,8 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Infisical if secrets conflict.
- **Import Secrets (Prioritize Parameter Store)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Parameter Store if secrets conflict.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict.
- **Import Secrets (Prioritize Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict.
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
6. Configure the **Details** of your Parameter Store Sync, then click **Next**.

View File

@@ -40,8 +40,8 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Infisical if secrets conflict.
- **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in GCP secret manager if secrets conflict.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over GCP Secret Manager when keys conflict.
- **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from GCP Secret Manager over Infisical when keys conflict.
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
6. Configure the **Details** of your GCP Secret Manager Sync, then click **Next**.

View File

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

View File

@@ -249,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"
]
@@ -344,34 +350,6 @@
"cli/faq"
]
},
{
"group": "App Connections",
"pages": [
"integrations/app-connections/overview",
{
"group": "Connections",
"pages": [
"integrations/app-connections/aws",
"integrations/app-connections/github",
"integrations/app-connections/gcp"
]
}
]
},
{
"group": "Secret Syncs",
"pages": [
"integrations/secret-syncs/overview",
{
"group": "Syncs",
"pages": [
"integrations/secret-syncs/aws-parameter-store",
"integrations/secret-syncs/github",
"integrations/secret-syncs/gcp-secret-manager"
]
}
]
},
{
"group": "Infrastructure Integrations",
"pages": [
@@ -406,6 +384,34 @@
"integrations/platforms/ansible"
]
},
{
"group": "App Connections",
"pages": [
"integrations/app-connections/overview",
{
"group": "Connections",
"pages": [
"integrations/app-connections/aws",
"integrations/app-connections/github",
"integrations/app-connections/gcp"
]
}
]
},
{
"group": "Secret Syncs",
"pages": [
"integrations/secret-syncs/overview",
{
"group": "Syncs",
"pages": [
"integrations/secret-syncs/aws-parameter-store",
"integrations/secret-syncs/github",
"integrations/secret-syncs/gcp-secret-manager"
]
}
]
},
{
"group": "Native Integrations",
"pages": [

View File

@@ -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}");

View File

@@ -87,7 +87,7 @@ const Content = ({ secretSync, onComplete }: ContentProps) => {
tooltipText={
<div className="flex flex-col gap-3">
<p>
Specify how Infisical should resolve the initial sync to {destinationName}. The
Specify how Infisical should resolve importing secrets from {destinationName}. The
following options are available:
</p>
<ul className="flex list-disc flex-col gap-3 pl-4">

View File

@@ -13,7 +13,8 @@ export const ROUTE_PATHS = Object.freeze({
"/_restrict-login-signup/login/provider/success"
),
SignUpSsoPage: setRoute("/signup/sso", "/_restrict-login-signup/signup/sso"),
PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset")
PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset"),
PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup")
},
Organization: {
SecretScanning: setRoute(

View File

@@ -27,11 +27,11 @@ export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<
}),
[SecretSyncInitialSyncBehavior.ImportPrioritizeSource]: (destinationName: string) => ({
name: "Import Destination Secrets - Prioritize Infisical Values",
description: `Infisical will import any secrets present in the ${destinationName} destination prior to syncing, prioritizing values present in Infisical over ${destinationName}.`
description: `Infisical will import any secrets present in the ${destinationName} destination prior to syncing, prioritizing values from Infisical over ${destinationName} when keys conflict.`
}),
[SecretSyncInitialSyncBehavior.ImportPrioritizeDestination]: (destinationName: string) => ({
name: `Import Destination Secrets - Prioritize ${destinationName} Values`,
description: `Infisical will import any secrets present in the ${destinationName} destination prior to syncing, prioritizing values present in ${destinationName} over Infisical.`
description: `Infisical will import any secrets present in the ${destinationName} destination prior to syncing, prioritizing values from ${destinationName} over Infisical when keys conflict.`
})
};
@@ -41,10 +41,10 @@ export const SECRET_SYNC_IMPORT_BEHAVIOR_MAP: Record<
> = {
[SecretSyncImportBehavior.PrioritizeSource]: (destinationName: string) => ({
name: "Prioritize Infisical Values",
description: `Infisical will import any secrets present in the ${destinationName} destination, prioritizing values present in Infisical over ${destinationName}.`
description: `Infisical will import any secrets present in the ${destinationName} destination, prioritizing values from Infisical over ${destinationName} when keys conflict.`
}),
[SecretSyncImportBehavior.PrioritizeDestination]: (destinationName: string) => ({
name: `Prioritize ${destinationName} Values`,
description: `Infisical will import any secrets present in the ${destinationName} destination, prioritizing values present in ${destinationName} over Infisical.`
description: `Infisical will import any secrets present in the ${destinationName} destination, prioritizing values from ${destinationName} over Infisical when keys conflict.`
})
};

View File

@@ -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 } = {

View File

@@ -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"
}

View File

@@ -10,6 +10,7 @@ export type TGetAuditLogsFilter = {
actorType?: ActorType;
projectId?: string;
actor?: string; // user ID format
secretPath?: string;
startDate?: Date;
endDate?: Date;
limit: number;

View File

@@ -23,6 +23,7 @@ import {
MfaMethod,
ResetPasswordDTO,
SendMfaTokenDTO,
SetupPasswordDTO,
SRP1DTO,
SRPR1Res,
TOauthTokenExchangeDTO,
@@ -286,7 +287,8 @@ export const useResetPassword = () => {
encryptedPrivateKeyIV: details.encryptedPrivateKeyIV,
encryptedPrivateKeyTag: details.encryptedPrivateKeyTag,
salt: details.salt,
verifier: details.verifier
verifier: details.verifier,
password: details.password
},
{
headers: {
@@ -336,3 +338,23 @@ export const checkUserTotpMfa = async () => {
return data.isVerified;
};
export const useSendPasswordSetupEmail = () => {
return useMutation({
mutationFn: async () => {
const { data } = await apiRequest.post("/api/v1/password/email/password-setup");
return data;
}
});
};
export const useSetupPassword = () => {
return useMutation({
mutationFn: async (payload: SetupPasswordDTO) => {
const { data } = await apiRequest.post("/api/v1/password/password-setup", payload);
return data;
}
});
};

View File

@@ -133,6 +133,20 @@ export type ResetPasswordDTO = {
salt: string;
verifier: string;
verificationToken: string;
password: string;
};
export type SetupPasswordDTO = {
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
token: string;
password: string;
};
export type IssueBackupPrivateKeyDTO = {

View File

@@ -16,5 +16,6 @@ export {
useGetIntegrationAuthTeamCityBuildConfigs,
useGetIntegrationAuthTeams,
useGetIntegrationAuthVercelBranches,
useGetIntegrationAuthVercelCustomEnvironments,
useSaveIntegrationAccessToken
} from "./queries";

View File

@@ -21,7 +21,8 @@ import {
Team,
TeamCityBuildConfig,
TGetIntegrationAuthOctopusDeployScopeValuesDTO,
TOctopusDeployVariableSetScopeValues
TOctopusDeployVariableSetScopeValues,
VercelEnvironment
} from "./types";
const integrationAuthKeys = {
@@ -132,7 +133,9 @@ const integrationAuthKeys = {
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const,
getIntegrationAuthCircleCIOrganizations: (integrationAuthId: string) =>
[{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const
[{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const,
getIntegrationAuthVercelCustomEnv: (integrationAuthId: string, teamId: string) =>
[{ integrationAuthId, teamId }, "integrationAuthVercelCustomEnv"] as const
};
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
@@ -362,6 +365,29 @@ const fetchIntegrationAuthQoveryScopes = async ({
return undefined;
};
const fetchIntegrationAuthVercelCustomEnvironments = async ({
integrationAuthId,
teamId
}: {
integrationAuthId: string;
teamId: string;
}) => {
const {
data: { environments }
} = await apiRequest.get<{
environments: {
appId: string;
customEnvironments: VercelEnvironment[];
}[];
}>(`/api/v1/integration-auth/${integrationAuthId}/vercel/custom-environments`, {
params: {
teamId
}
});
return environments;
};
const fetchIntegrationAuthHerokuPipelines = async ({
integrationAuthId
}: {
@@ -730,6 +756,24 @@ export const useGetIntegrationAuthQoveryScopes = ({
});
};
export const useGetIntegrationAuthVercelCustomEnvironments = ({
integrationAuthId,
teamId
}: {
integrationAuthId: string;
teamId: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthVercelCustomEnv(integrationAuthId, teamId),
queryFn: () =>
fetchIntegrationAuthVercelCustomEnvironments({
integrationAuthId,
teamId
}),
enabled: Boolean(teamId && integrationAuthId)
});
};
export const useGetIntegrationAuthHerokuPipelines = ({
integrationAuthId
}: {

View File

@@ -43,6 +43,11 @@ export type Environment = {
environmentId: string;
};
export type VercelEnvironment = {
id: string;
slug: string;
};
export type ChecklyGroup = {
name: string;
groupId: number;

View File

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

View File

@@ -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;
}
});
};

View File

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

View File

@@ -136,7 +136,8 @@ export const PasswordResetPage = () => {
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier,
verificationToken
verificationToken,
password: newPassword
});
navigate({ to: "/login" });

View File

@@ -0,0 +1,349 @@
import crypto from "crypto";
import { FormEvent, useState } from "react";
import { faCheck, faEye, faEyeSlash, faKey, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import jsrp from "jsrp";
import { createNotification } from "@app/components/notifications";
import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useSetupPassword } from "@app/hooks/api/auth/queries";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
export const PasswordSetupPage = () => {
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [passwordsMatch, setPasswordsMatch] = useState(true);
const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(true);
const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false);
const [passwordErrorNoLetterChar, setPasswordErrorNoLetterChar] = useState(true);
const [passwordErrorNoNumOrSpecialChar, setPasswordErrorNoNumOrSpecialChar] = useState(true);
const [passwordErrorRepeatedChar, setPasswordErrorRepeatedChar] = useState(false);
const [passwordErrorEscapeChar, setPasswordErrorEscapeChar] = useState(false);
const [passwordErrorLowEntropy, setPasswordErrorLowEntropy] = useState(false);
const [passwordErrorBreached, setPasswordErrorBreached] = useState(false);
const [isRedirecting, setIsRedirecting] = useState(false);
const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordSetupPage.id });
const navigate = useNavigate();
const setupPassword = useSetupPassword();
const parsedUrl = search;
const token = parsedUrl.token as string;
const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
const handleSetPassword = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const errorCheck = await passwordCheck({
password,
setPasswordErrorTooShort,
setPasswordErrorTooLong,
setPasswordErrorNoLetterChar,
setPasswordErrorNoNumOrSpecialChar,
setPasswordErrorRepeatedChar,
setPasswordErrorEscapeChar,
setPasswordErrorLowEntropy,
setPasswordErrorBreached
});
if (password !== confirmPassword) {
setPasswordsMatch(false);
return;
}
setPasswordsMatch(true);
if (!errorCheck) {
client.init(
{
username: email,
password
},
async () => {
client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
const derivedKey = await deriveArgonKey({
password,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: localStorage.getItem("PRIVATE_KEY") as string,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString("hex"),
secret: Buffer.from(derivedKey.hash)
});
try {
await setupPassword.mutateAsync({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier,
token,
password
});
setIsRedirecting(true);
createNotification({
type: "success",
title: "Password successfully set",
text: "Redirecting to login..."
});
setTimeout(() => {
window.location.href = "/login";
}, 3000);
} catch (error) {
createNotification({
type: "error",
text: (error as Error).message ?? "Error setting password"
});
navigate({ to: "/personal-settings" });
}
});
}
);
}
};
const isInvalidPassword =
passwordErrorTooShort ||
passwordErrorTooLong ||
passwordErrorNoLetterChar ||
passwordErrorNoNumOrSpecialChar ||
passwordErrorRepeatedChar ||
passwordErrorEscapeChar ||
passwordErrorLowEntropy ||
passwordErrorBreached;
return (
<div className="flex h-screen w-full flex-col items-center justify-center bg-bunker-800">
<form onSubmit={handleSetPassword}>
<Card className="flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 px-8 py-4">
<CardTitle
className="p-0 pb-4 pt-2 text-left text-xl"
subTitle="Make sure to store your password somewhere safe."
>
<div className="flex flex-row items-center">
<div className="flex items-center pb-0.5">
<FontAwesomeIcon icon={faKey} />
</div>
<span className="ml-2.5">Set Password</span>
</div>
</CardTitle>
<FormControl label="Password">
<Input
value={password}
type={showPassword ? "text" : "password"}
autoComplete="new-password"
onChange={(e) => {
setPassword(e.target.value);
passwordCheck({
password: e.target.value,
setPasswordErrorTooShort,
setPasswordErrorTooLong,
setPasswordErrorNoLetterChar,
setPasswordErrorNoNumOrSpecialChar,
setPasswordErrorRepeatedChar,
setPasswordErrorEscapeChar,
setPasswordErrorLowEntropy,
setPasswordErrorBreached
});
}}
rightIcon={
<button
type="button"
onClick={() => {
setShowPassword((prev) => !prev);
}}
className="cursor-pointer self-end text-gray-400"
>
{showPassword ? (
<FontAwesomeIcon size="sm" icon={faEyeSlash} />
) : (
<FontAwesomeIcon size="sm" icon={faEye} />
)}
</button>
}
/>
</FormControl>
<FormControl
label="Confirm Password"
errorText="Passwords must match"
isError={!passwordsMatch}
>
<Input
value={confirmPassword}
type={showConfirmPassword ? "text" : "password"}
autoComplete="new-password"
onChange={(e) => setConfirmPassword(e.target.value)}
rightIcon={
<button
type="button"
onClick={() => {
setShowConfirmPassword((prev) => !prev);
}}
className="cursor-pointer self-end text-gray-400"
>
{showConfirmPassword ? (
<FontAwesomeIcon size="sm" icon={faEyeSlash} />
) : (
<FontAwesomeIcon size="sm" icon={faEye} />
)}
</button>
}
/>
</FormControl>
<div className="mb-4 flex w-full max-w-md flex-col items-start rounded-md bg-mineshaft-700 px-2 py-2 transition-opacity duration-100">
<div className="mb-1 text-sm text-gray-400">Password must contain:</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorTooShort ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorTooShort ? "text-gray-400" : "text-gray-600"} text-sm`}
>
at least 14 characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorTooLong ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorTooLong ? "text-gray-400" : "text-gray-600"} text-sm`}
>
at most 100 characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNoLetterChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorNoLetterChar ? "text-gray-400" : "text-gray-600"} text-sm`}
>
at least 1 letter character
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNoNumOrSpecialChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${
passwordErrorNoNumOrSpecialChar ? "text-gray-400" : "text-gray-600"
} text-sm`}
>
at least 1 number or special character
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorRepeatedChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorRepeatedChar ? "text-gray-400" : "text-gray-600"} text-sm`}
>
at most 3 repeated, consecutive characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorEscapeChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorEscapeChar ? "text-gray-400" : "text-gray-600"} text-sm`}
>
no escape characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLowEntropy ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorLowEntropy ? "text-gray-400" : "text-gray-600"} text-sm`}
>
no personal information
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorBreached ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorBreached ? "text-gray-400" : "text-gray-600"} text-sm`}
>
password not found in a data breach.
</div>
</div>
</div>
<Button
isDisabled={isInvalidPassword || setupPassword.isPending || isRedirecting}
colorSchema="secondary"
type="submit"
isLoading={setupPassword.isPending}
>
Submit
</Button>
</Card>
</form>
</div>
);
};

View File

@@ -0,0 +1,15 @@
import { createFileRoute } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { PasswordSetupPage } from "./PasswordSetupPage";
const PasswordSetupPageQueryParamsSchema = z.object({
token: z.string(),
to: z.string()
});
export const Route = createFileRoute("/_authenticate/password-setup")({
component: PasswordSetupPage,
validateSearch: zodValidator(PasswordSetupPageQueryParamsSchema)
});

View File

@@ -1,12 +1,13 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { ROUTE_PATHS } from "@app/const/routes";
import { userKeys } from "@app/hooks/api";
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
import { fetchUserDetails } from "@app/hooks/api/users/queries";
export const Route = createFileRoute("/_authenticate")({
beforeLoad: async ({ context }) => {
beforeLoad: async ({ context, location }) => {
if (!context.serverConfig.initialized) {
throw redirect({ to: "/admin/signup" });
}
@@ -26,7 +27,7 @@ export const Route = createFileRoute("/_authenticate")({
});
});
if (!data.organizationId) {
if (!data.organizationId && location.pathname !== ROUTE_PATHS.Auth.PasswordSetupPage.path) {
throw redirect({ to: "/login/select-organization" });
}

View File

@@ -14,6 +14,7 @@ import {
DropdownMenuTrigger,
FilterableSelect,
FormControl,
Input,
Select,
SelectItem
} from "@app/components/v2";
@@ -22,6 +23,7 @@ import { useGetAuditLogActorFilterOpts, useGetUserWorkspaces } from "@app/hooks/
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
import { Actor } from "@app/hooks/api/auditLogs/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { AuditLogFilterFormData } from "./types";
@@ -50,6 +52,7 @@ export const LogsFilter = ({
className,
control,
reset,
setValue,
watch
}: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
@@ -101,6 +104,7 @@ export const LogsFilter = ({
};
const selectedEventTypes = watch("eventType") as EventType[] | undefined;
const selectedProject = watch("project");
return (
<div
@@ -109,30 +113,48 @@ export const LogsFilter = ({
className
)}
>
{isOrgAuditLogs && workspacesInOrg.length > 0 && (
<Controller
control={control}
name="project"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label="Project"
errorText={error?.message}
isError={Boolean(error)}
className="mr-12 w-64"
>
<FilterableSelect
value={value}
isClearable
onChange={onChange}
placeholder="Select a project..."
options={workspacesInOrg.map(({ name, id }) => ({ name, id }))}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
)}
<div className="flex items-center gap-4">
{isOrgAuditLogs && workspacesInOrg.length > 0 && (
<Controller
control={control}
name="project"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label="Project"
errorText={error?.message}
isError={Boolean(error)}
className="w-64"
>
<FilterableSelect
value={value}
isClearable
onChange={(e) => {
if (e === null) {
setValue("secretPath", "");
}
onChange(e);
}}
placeholder="Select a project..."
options={workspacesInOrg.map(({ name, id, type }) => ({ name, id, type }))}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
)}
{selectedProject?.type === ProjectType.SecretManager && (
<Controller
control={control}
name="secretPath"
render={({ field: { onChange, value, ...field } }) => (
<FormControl label="Secret path" className="w-40">
<Input {...field} value={value} onChange={(e) => onChange(e.target.value)} />
</FormControl>
)}
/>
)}
</div>
<div className="mt-1 flex items-center space-x-2">
<Controller
control={control}

View File

@@ -5,6 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { withPermission } from "@app/hoc";
import { useDebounce } from "@app/hooks";
import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -67,10 +68,13 @@ export const LogsSection = withPermission(
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
const actor = watch("actor");
const projectId = watch("project")?.id;
const secretPath = watch("secretPath");
const startDate = watch("startDate");
const endDate = watch("endDate");
const [debouncedSecretPath] = useDebounce<string>(secretPath!, 500);
return (
<div>
{showFilters && (
@@ -90,6 +94,7 @@ export const LogsSection = withPermission(
isOrgAuditLogs={isOrgAuditLogs}
showActorColumn={!!showActorColumn}
filter={{
secretPath: debouncedSecretPath || undefined,
eventMetadata: presets?.eventMetadata,
projectId,
actorType: presets?.actorType,

View File

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

View File

@@ -1,14 +1,19 @@
import { z } from "zod";
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const auditLogFilterFormSchema = z
.object({
eventMetadata: z.object({}).optional(),
project: z.object({ id: z.string(), name: z.string() }).optional().nullable(),
project: z
.object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) })
.optional()
.nullable(),
eventType: z.nativeEnum(EventType).array(),
actor: z.string().optional(),
userAgentType: z.nativeEnum(UserAgentType),
secretPath: z.string().optional(),
startDate: z.date().optional(),
endDate: z.date().optional(),
page: z.coerce.number().optional(),

View File

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

View File

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

View File

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

View File

@@ -114,36 +114,42 @@ export const GcpConnectionForm = ({ appConnection, onSubmit }: Props) => {
className="group"
helperText={
<>
<span>
{`Service account ID (the part of the email before '@') must be suffixed with "${expectedAccountIdSuffix}"`}
</span>
<Tooltip className="relative right-2" position="bottom" content="Copy">
<IconButton
variant="plain"
ariaLabel="copy"
onClick={() => {
if (isCopied) {
return;
}
<div>
{`Service account ID must be suffixed with "${expectedAccountIdSuffix}"`}
<Tooltip className="relative right-2" position="bottom" content="Copy">
<IconButton
variant="plain"
ariaLabel="copy"
onClick={() => {
if (isCopied) {
return;
}
navigator.clipboard.writeText(expectedAccountIdSuffix);
navigator.clipboard.writeText(expectedAccountIdSuffix);
createNotification({
text: "Copied to clipboard",
type: "info"
});
createNotification({
text: "Copied to clipboard",
type: "info"
});
toggleIsCopied(2000);
}}
className="hover:bg-bunker-100/10"
>
<FontAwesomeIcon
icon={!isCopied ? faCopy : faCheck}
size="sm"
className="cursor-pointer"
/>
</IconButton>
</Tooltip>
toggleIsCopied(2000);
}}
className="hover:bg-bunker-100/10"
>
<FontAwesomeIcon
icon={!isCopied ? faCopy : faCheck}
size="sm"
className="cursor-pointer"
/>
</IconButton>
</Tooltip>
</div>
<div>
Example:
<span className="ml-1">service-account-</span>
<span className="font-semibold">{expectedAccountIdSuffix}</span>
<span>@my-project.iam.gserviceaccount.com</span>
</div>
</>
}
>

View File

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

View File

@@ -80,6 +80,19 @@ const ConditionSchema = z
return true;
},
{ message: "Duplicate operator found for a condition" }
)
.refine(
(val) =>
val
.filter(
(el) => el.lhs === "secretPath" && el.operator !== PermissionConditionOperators.$GLOB
)
.every((el) =>
el.operator === PermissionConditionOperators.$IN
? el.rhs.split(",").every((i) => i.trim().startsWith("/"))
: el.rhs.trim().startsWith("/")
),
{ message: "Invalid Secret Path. Must start with '/'" }
);
export const projectRoleFormSchema = z.object({

View File

@@ -36,6 +36,10 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
name: `permissions.secrets.${position}.conditions`
});
const conditionErrorMessage =
errors?.permissions?.secrets?.[position]?.conditions?.message ||
errors?.permissions?.secrets?.[position]?.conditions?.root?.message;
return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
<p className="mt-2 text-gray-300">Conditions</p>
@@ -147,10 +151,10 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
);
})}
</div>
{errors?.permissions?.secrets?.[position]?.conditions?.message && (
{conditionErrorMessage && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.secrets?.[position]?.conditions?.message}</span>
<span>{conditionErrorMessage}</span>
</div>
)}
<div>

View File

@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Badge, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { Badge, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { IntegrationsListPageTabs } from "@app/types/integrations";
@@ -45,14 +45,12 @@ export const IntegrationsListPage = () => {
<meta name="og:description" content={t("integrations.description") as string} />
</Helmet>
<div className="container relative mx-auto max-w-7xl pb-12 text-white">
<div className="mx-6 mb-8">
<div className="mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">Integrations</h1>
<p className="text-base text-bunker-300">
Manage integrations with third-party services.
</p>
</div>
<div className="mx-2 mb-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
<div className="mb-8">
<PageHeader
title="Integrations"
description="Manage integrations with third-party services."
/>
<div className="mb-4 mt-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
<div className="mb-1 flex items-center text-sm">
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1 text-primary" />
Integrations Update

View File

@@ -12,11 +12,10 @@ export const FrameworkIntegrationTab = () => {
return (
<>
<div className="mx-4 mb-4 flex flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">{t("integrations.framework-integrations")}</h1>
<div className="mb-4 flex flex-col items-start justify-between px-2 text-xl">
<p className="text-base text-gray-400">{t("integrations.click-to-setup")}</p>
</div>
<div className="mx-2 mt-4 grid grid-cols-3 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-7">
<div className="mt-4 grid grid-cols-3 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-7">
{sortedFrameworks.map((framework) => (
<a
key={`framework-integration-${framework.slug}`}

View File

@@ -5,13 +5,12 @@ export const InfrastructureIntegrationTab = () => {
return (
<>
<div className="mx-4 mb-4 flex flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">Infrastructure Integrations</h1>
<div className="mb-4 flex flex-col items-start justify-between px-2 text-xl">
<p className="text-base text-gray-400">
Click on of the integration to read the documentation.
Click on an integration to read the documentation.
</p>
</div>
<div className="mx-6 grid grid-cols-2 gap-4 lg:grid-cols-3 2xl:grid-cols-4">
<div className="grid grid-cols-2 gap-4 lg:grid-cols-3 2xl:grid-cols-4">
{sortedIntegrations.map((integration) => (
<a
key={`framework-integration-${integration.slug}`}

View File

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

View File

@@ -73,7 +73,7 @@ const PageContent = () => {
return (
<>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 font-inter text-white">
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6">
<div className="mx-auto mb-6 w-full max-w-7xl">
<Button
variant="link"
type="submit"
@@ -89,7 +89,6 @@ const PageContent = () => {
}
});
}}
className="mb-4"
>
Secret Syncs
</Button>

View File

@@ -65,7 +65,7 @@ export const AzureDevopsAuthorizePage = () => {
<div className="flex flex-row items-center">
<div className="flex items-center">
<img
src="/images/integrations/Amazon Web Services.png"
src="/images/integrations/Microsoft Azure.png"
height={35}
width={35}
alt="Azure DevOps logo"

View File

@@ -106,7 +106,7 @@ export const AzureDevopsConfigurePage = () => {
<div className="flex flex-row items-center">
<div className="flex items-center">
<img
src="/images/integrations/Amazon Web Services.png"
src="/images/integrations/Microsoft Azure.png"
height={35}
width={35}
alt="Azure DevOps logo"

View File

@@ -47,7 +47,12 @@ export function AzureKeyVaultAuthorizePage() {
>
<div className="flex flex-row items-center">
<div className="flex items-center pb-0.5">
<img src="/images/integrations/GitHub.png" height={30} width={30} alt="Github logo" />
<img
src="/images/integrations/Microsoft Azure.png"
height={30}
width={30}
alt="Azure Key Vault logo"
/>
</div>
<span className="ml-2.5">Azure Key Vault Integration </span>
<a

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Helmet } from "react-helmet";
import {
faArrowUpRightFromSquare,
@@ -25,7 +25,8 @@ import { useCreateIntegration } from "@app/hooks/api";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById,
useGetIntegrationAuthVercelBranches
useGetIntegrationAuthVercelBranches,
useGetIntegrationAuthVercelCustomEnvironments
} from "@app/hooks/api/integrationAuth";
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
@@ -75,6 +76,11 @@ export const VercelConfigurePage = () => {
teamId: integrationAuth?.teamId as string
});
const { data: customEnvironments } = useGetIntegrationAuthVercelCustomEnvironments({
teamId: integrationAuth?.teamId as string,
integrationAuthId: integrationAuthId as string
});
const { data: branches } = useGetIntegrationAuthVercelBranches({
integrationAuthId: integrationAuthId as string,
appId: targetAppId
@@ -135,6 +141,26 @@ export const VercelConfigurePage = () => {
}
};
const selectedVercelEnvironments = useMemo(() => {
let selectedEnvironments = vercelEnvironments;
const environments = customEnvironments?.find(
(e) => e.appId === targetAppId
)?.customEnvironments;
if (environments && environments.length > 0) {
selectedEnvironments = [
...selectedEnvironments,
...environments.map((env) => ({
name: env.slug,
slug: env.id
}))
];
}
return selectedEnvironments;
}, [targetAppId, customEnvironments]);
return integrationAuth &&
selectedSourceEnvironment &&
integrationAuthApps &&
@@ -210,7 +236,13 @@ export const VercelConfigurePage = () => {
>
<Select
value={targetAppId}
onValueChange={(val) => setTargetAppId(val)}
onValueChange={(val) => {
if (vercelEnvironments.every((env) => env.slug !== targetEnvironment)) {
setTargetEnvironment(vercelEnvironments[0].slug);
}
setTargetAppId(val);
}}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
@@ -236,7 +268,7 @@ export const VercelConfigurePage = () => {
onValueChange={(val) => setTargetEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{vercelEnvironments.map((vercelEnvironment) => (
{selectedVercelEnvironments.map((vercelEnvironment) => (
<SelectItem
value={vercelEnvironment.slug}
key={`target-environment-${vercelEnvironment.slug}`}

View File

@@ -11,6 +11,7 @@ import attemptChangePassword from "@app/components/utilities/attemptChangePasswo
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
import { Button, FormControl, Input } from "@app/components/v2";
import { useUser } from "@app/context";
import { useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries";
type Errors = {
tooShort?: string;
@@ -45,6 +46,7 @@ export const ChangePasswordSection = () => {
});
const [errors, setErrors] = useState<Errors>({});
const [isLoading, setIsLoading] = useState(false);
const sendSetupPasswordEmail = useSendPasswordSetupEmail();
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
try {
@@ -80,6 +82,24 @@ export const ChangePasswordSection = () => {
}
};
const onSetupPassword = async () => {
try {
await sendSetupPasswordEmail.mutateAsync();
createNotification({
title: "Password setup verification email sent",
text: "Check your email to confirm password setup",
type: "info"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to send password setup email",
type: "error"
});
}
};
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
@@ -142,6 +162,16 @@ export const ChangePasswordSection = () => {
<Button type="submit" colorSchema="secondary" isLoading={isLoading} isDisabled={isLoading}>
Save
</Button>
<p className="mt-2 font-inter text-sm text-mineshaft-400">
Need to setup a password?{" "}
<button
onClick={onSetupPassword}
type="button"
className="underline underline-offset-2 hover:text-mineshaft-200"
>
Click here
</button>
</p>
</form>
);
};

View File

@@ -24,6 +24,7 @@ import { Route as authSignUpInvitePageRouteImport } from './pages/auth/SignUpInv
import { Route as authRequestNewInvitePageRouteImport } from './pages/auth/RequestNewInvitePage/route'
import { Route as authPasswordResetPageRouteImport } from './pages/auth/PasswordResetPage/route'
import { Route as authEmailNotVerifiedPageRouteImport } from './pages/auth/EmailNotVerifiedPage/route'
import { Route as authPasswordSetupPageRouteImport } from './pages/auth/PasswordSetupPage/route'
import { Route as userLayoutImport } from './pages/user/layout'
import { Route as organizationLayoutImport } from './pages/organization/layout'
import { Route as publicViewSharedSecretByIDPageRouteImport } from './pages/public/ViewSharedSecretByIDPage/route'
@@ -310,6 +311,14 @@ const authEmailNotVerifiedPageRouteRoute =
getParentRoute: () => middlewaresRestrictLoginSignupRoute,
} as any)
const authPasswordSetupPageRouteRoute = authPasswordSetupPageRouteImport.update(
{
id: '/password-setup',
path: '/password-setup',
getParentRoute: () => middlewaresAuthenticateRoute,
} as any,
)
const userLayoutRoute = userLayoutImport.update({
id: '/_layout',
getParentRoute: () => AuthenticatePersonalSettingsRoute,
@@ -1577,6 +1586,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof middlewaresRestrictLoginSignupImport
parentRoute: typeof rootRoute
}
'/_authenticate/password-setup': {
id: '/_authenticate/password-setup'
path: '/password-setup'
fullPath: '/password-setup'
preLoaderRoute: typeof authPasswordSetupPageRouteImport
parentRoute: typeof middlewaresAuthenticateImport
}
'/_restrict-login-signup/email-not-verified': {
id: '/_restrict-login-signup/email-not-verified'
path: '/email-not-verified'
@@ -3397,12 +3413,14 @@ const AuthenticatePersonalSettingsRouteWithChildren =
)
interface middlewaresAuthenticateRouteChildren {
authPasswordSetupPageRouteRoute: typeof authPasswordSetupPageRouteRoute
middlewaresInjectOrgDetailsRoute: typeof middlewaresInjectOrgDetailsRouteWithChildren
AuthenticatePersonalSettingsRoute: typeof AuthenticatePersonalSettingsRouteWithChildren
}
const middlewaresAuthenticateRouteChildren: middlewaresAuthenticateRouteChildren =
{
authPasswordSetupPageRouteRoute: authPasswordSetupPageRouteRoute,
middlewaresInjectOrgDetailsRoute:
middlewaresInjectOrgDetailsRouteWithChildren,
AuthenticatePersonalSettingsRoute:
@@ -3487,6 +3505,7 @@ export interface FileRoutesByFullPath {
'/cli-redirect': typeof authCliRedirectPageRouteRoute
'/share-secret': typeof publicShareSecretPageRouteRoute
'': typeof organizationLayoutRouteWithChildren
'/password-setup': typeof authPasswordSetupPageRouteRoute
'/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
'/password-reset': typeof authPasswordResetPageRouteRoute
'/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
@@ -3657,6 +3676,7 @@ export interface FileRoutesByTo {
'/cli-redirect': typeof authCliRedirectPageRouteRoute
'/share-secret': typeof publicShareSecretPageRouteRoute
'': typeof organizationLayoutRouteWithChildren
'/password-setup': typeof authPasswordSetupPageRouteRoute
'/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
'/password-reset': typeof authPasswordResetPageRouteRoute
'/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
@@ -3824,6 +3844,7 @@ export interface FileRoutesById {
'/share-secret': typeof publicShareSecretPageRouteRoute
'/_authenticate': typeof middlewaresAuthenticateRouteWithChildren
'/_restrict-login-signup': typeof middlewaresRestrictLoginSignupRouteWithChildren
'/_authenticate/password-setup': typeof authPasswordSetupPageRouteRoute
'/_restrict-login-signup/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
'/_restrict-login-signup/password-reset': typeof authPasswordResetPageRouteRoute
'/_restrict-login-signup/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
@@ -4004,6 +4025,7 @@ export interface FileRouteTypes {
| '/cli-redirect'
| '/share-secret'
| ''
| '/password-setup'
| '/email-not-verified'
| '/password-reset'
| '/requestnewinvite'
@@ -4173,6 +4195,7 @@ export interface FileRouteTypes {
| '/cli-redirect'
| '/share-secret'
| ''
| '/password-setup'
| '/email-not-verified'
| '/password-reset'
| '/requestnewinvite'
@@ -4338,6 +4361,7 @@ export interface FileRouteTypes {
| '/share-secret'
| '/_authenticate'
| '/_restrict-login-signup'
| '/_authenticate/password-setup'
| '/_restrict-login-signup/email-not-verified'
| '/_restrict-login-signup/password-reset'
| '/_restrict-login-signup/requestnewinvite'
@@ -4562,6 +4586,7 @@ export const routeTree = rootRoute
"/_authenticate": {
"filePath": "middlewares/authenticate.tsx",
"children": [
"/_authenticate/password-setup",
"/_authenticate/_inject-org-details",
"/_authenticate/personal-settings"
]
@@ -4579,6 +4604,10 @@ export const routeTree = rootRoute
"/_restrict-login-signup/admin/signup"
]
},
"/_authenticate/password-setup": {
"filePath": "auth/PasswordSetupPage/route.tsx",
"parent": "/_authenticate"
},
"/_restrict-login-signup/email-not-verified": {
"filePath": "auth/EmailNotVerifiedPage/route.tsx",
"parent": "/_restrict-login-signup"

View File

@@ -335,6 +335,7 @@ export const routes = rootRoute("root.tsx", [
route("/verify-email", "auth/VerifyEmailPage/route.tsx")
]),
middleware("authenticate.tsx", [
route("/password-setup", "auth/PasswordSetupPage/route.tsx"),
route("/personal-settings", [
layout("user/layout.tsx", [index("user/PersonalSettingsPage/route.tsx")])
]),

View File

@@ -13,9 +13,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: v0.8.7
version: v0.8.9
# 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.7"
appVersion: "v0.8.9"

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, errorToConditionOn error) error {
func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, secretsCount int, errorToConditionOn error) error {
if infisicalSecret.Status.Conditions == nil {
infisicalSecret.Status.Conditions = []metav1.Condition{}
}
@@ -35,7 +35,7 @@ func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.
Type: "secrets.infisical.com/ReadyToSyncSecrets",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: "Infisical controller has started syncing your secrets",
Message: fmt.Sprintf("Infisical controller has started syncing your secrets. Last reconcile synced %d secrets", secretsCount),
})
}

View File

@@ -151,11 +151,10 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
api.API_CA_CERTIFICATE = ""
}
err = r.ReconcileInfisicalSecret(ctx, logger, infisicalSecretCRD, managedKubeSecretReferences)
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCRD, err)
secretsCount, err := r.ReconcileInfisicalSecret(ctx, logger, infisicalSecretCRD, managedKubeSecretReferences)
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCRD, secretsCount, err)
if err != nil {
logger.Error(err, fmt.Sprintf("unable to reconcile InfisicalSecret. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
@@ -172,7 +171,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
}
// Sync again after the specified time
logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime))
logger.Info(fmt.Sprintf("Successfully synced %d secrets. Operator will requeue after [%v]", secretsCount, requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
@@ -182,6 +181,10 @@ func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&secretsv1alpha1.InfisicalSecret{}, builder.WithPredicates(predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
if e.ObjectOld.GetGeneration() == e.ObjectNew.GetGeneration() {
return false // Skip reconciliation for status-only changes
}
if infisicalSecretResourceVariablesMap != nil {
if rv, ok := infisicalSecretResourceVariablesMap[string(e.ObjectNew.GetUID())]; ok {
rv.CancelCtx()

View File

@@ -337,7 +337,7 @@ func (r *InfisicalSecretReconciler) updateResourceVariables(infisicalSecret v1al
infisicalSecretResourceVariablesMap[string(infisicalSecret.UID)] = resourceVariables
}
func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedKubeSecretReferences []v1alpha1.ManagedKubeSecretConfig) error {
func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedKubeSecretReferences []v1alpha1.ManagedKubeSecretConfig) (int, error) {
resourceVariables := r.getResourceVariables(infisicalSecret)
infisicalClient := resourceVariables.InfisicalClient
@@ -351,7 +351,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
r.SetInfisicalTokenLoadCondition(ctx, logger, &infisicalSecret, authDetails.AuthStrategy, err)
if err != nil {
return fmt.Errorf("unable to authenticate [err=%s]", err)
return 0, fmt.Errorf("unable to authenticate [err=%s]", err)
}
r.updateResourceVariables(infisicalSecret, util.ResourceVariables{
@@ -361,6 +361,8 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
})
}
secretsCount := 0
for _, managedSecretReference := range managedKubeSecretReferences {
// Look for managed secret by name and namespace
managedKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
@@ -369,7 +371,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
})
if err != nil && !k8Errors.IsNotFound(err) {
return fmt.Errorf("something went wrong when fetching the managed Kubernetes secret [%w]", err)
return 0, fmt.Errorf("something went wrong when fetching the managed Kubernetes secret [%w]", err)
}
// Get exiting Etag if exists
@@ -384,12 +386,12 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_ACCOUNT { // Service Account // ! Legacy auth method
serviceAccountCreds, err := r.getInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
if err != nil {
return fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
return 0, fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
}
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceAccount(infisicalClient, serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag)
if err != nil {
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
return 0, fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
}
logger.Info("ReconcileInfisicalSecret: Fetched secrets via service account")
@@ -397,7 +399,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
} else if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_TOKEN { // Service Tokens // ! Legacy / Deprecated auth method
infisicalToken, err := r.getInfisicalTokenFromKubeSecret(ctx, infisicalSecret)
if err != nil {
return fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
return 0, fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
}
envSlug := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.EnvSlug
@@ -406,7 +408,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceToken(infisicalClient, infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath, recursive)
if err != nil {
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
return 0, fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
}
logger.Info("ReconcileInfisicalSecret: Fetched secrets via [type=SERVICE_TOKEN]")
@@ -415,30 +417,27 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaMachineIdentity(infisicalClient, secretVersionBasedOnETag, authDetails.MachineIdentityScope)
if err != nil {
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
return 0, fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
}
logger.Info(fmt.Sprintf("ReconcileInfisicalSecret: Fetched secrets via machine identity [type=%v]", authDetails.AuthStrategy))
} else {
return errors.New("no authentication method provided. Please configure a authentication method then try again")
return 0, errors.New("no authentication method provided. Please configure a authentication method then try again")
}
if !updateDetails.Modified {
logger.Info("ReconcileInfisicalSecret: No secrets modified so reconcile not needed")
continue
}
secretsCount = len(plainTextSecretsFromApi)
if managedKubeSecret == nil {
if err := r.createInfisicalManagedKubeSecret(ctx, logger, infisicalSecret, managedSecretReference, plainTextSecretsFromApi, updateDetails.ETag); err != nil {
return fmt.Errorf("failed to create managed secret [err=%s]", err)
return 0, fmt.Errorf("failed to create managed secret [err=%s]", err)
}
} else {
if err := r.updateInfisicalManagedKubeSecret(ctx, logger, managedSecretReference, *managedKubeSecret, plainTextSecretsFromApi, updateDetails.ETag); err != nil {
return fmt.Errorf("failed to update managed secret [err=%s]", err)
return 0, fmt.Errorf("failed to update managed secret [err=%s]", err)
}
}
}
return nil
return secretsCount, nil
}