Compare commits
51 Commits
feat/gcp-s
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
7090eea716 | ||
|
01d3443139 | ||
|
c4b23a8d4f | ||
|
90a2a11fff | ||
|
95d7c2082c | ||
|
ab5eb4c696 | ||
|
65aeb81934 | ||
|
a406511405 | ||
|
61da0db49e | ||
|
59666740ca | ||
|
9cc7edc869 | ||
|
e1b016f76d | ||
|
1175b9b5af | ||
|
09521144ec | ||
|
8759944077 | ||
|
aac3c355e9 | ||
|
2a28a462a5 | ||
|
3328e0850f | ||
|
216cae9b33 | ||
|
89d4d4bc92 | ||
|
cffcb28bc9 | ||
|
61388753cf | ||
|
a6145120e6 | ||
|
dacffbef08 | ||
|
4db3e5d208 | ||
|
2a84d61862 | ||
|
a5945204ad | ||
|
e99eb47cf4 | ||
|
cf107c0c0d | ||
|
9fcb1c2161 | ||
|
70515a1ca2 | ||
|
955cf9303a | ||
|
a24ef46d7d | ||
|
ee49f714b9 | ||
|
657aca516f | ||
|
b5d60398d6 | ||
|
c3d515bb95 | ||
|
d74b819f57 | ||
|
2c75e23acf | ||
|
907dd4880a | ||
|
6af7c5c371 | ||
|
72468d5428 | ||
|
939ee892e0 | ||
|
27af943ee1 | ||
|
9b772ad55a | ||
|
94a1fc2809 | ||
|
10c10642a1 | ||
|
92f697e195 | ||
|
8062f0238b | ||
|
27efc908e2 | ||
|
645dfafba0 |
3
Makefile
@@ -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
|
||||
|
@@ -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");
|
||||
}
|
||||
});
|
||||
}
|
@@ -27,7 +27,8 @@ export const OidcConfigsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
orgId: z.string().uuid(),
|
||||
lastUsed: z.date().nullable().optional()
|
||||
lastUsed: z.date().nullable().optional(),
|
||||
manageGroupMemberships: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
||||
|
@@ -153,7 +153,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
discoveryURL: true,
|
||||
isActive: true,
|
||||
orgId: true,
|
||||
allowedEmailDomains: true
|
||||
allowedEmailDomains: true,
|
||||
manageGroupMemberships: true
|
||||
}).extend({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string()
|
||||
@@ -207,7 +208,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
userinfoEndpoint: z.string().trim(),
|
||||
clientId: z.string().trim(),
|
||||
clientSecret: z.string().trim(),
|
||||
isActive: z.boolean()
|
||||
isActive: z.boolean(),
|
||||
manageGroupMemberships: z.boolean().optional()
|
||||
})
|
||||
.partial()
|
||||
.merge(z.object({ orgSlug: z.string() })),
|
||||
@@ -223,7 +225,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
userinfoEndpoint: true,
|
||||
orgId: true,
|
||||
allowedEmailDomains: true,
|
||||
isActive: true
|
||||
isActive: true,
|
||||
manageGroupMemberships: true
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -272,7 +275,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
clientId: z.string().trim(),
|
||||
clientSecret: z.string().trim(),
|
||||
isActive: z.boolean(),
|
||||
orgSlug: z.string().trim()
|
||||
orgSlug: z.string().trim(),
|
||||
manageGroupMemberships: z.boolean().optional().default(false)
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.configurationType === OIDCConfigurationType.CUSTOM) {
|
||||
@@ -334,7 +338,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
userinfoEndpoint: true,
|
||||
orgId: true,
|
||||
isActive: true,
|
||||
allowedEmailDomains: true
|
||||
allowedEmailDomains: true,
|
||||
manageGroupMemberships: true
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -350,4 +355,25 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
return oidc;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/manage-group-memberships",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
orgId: z.string().trim().min(1, "Org ID is required")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
isEnabled: z.boolean()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const isEnabled = await server.services.oidc.isOidcManageGroupMembershipsEnabled(req.query.orgId, req.permission);
|
||||
|
||||
return { isEnabled };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -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);
|
||||
|
@@ -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 })
|
||||
});
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
@@ -45,6 +46,7 @@ type TGroupServiceFactoryDep = {
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">;
|
||||
};
|
||||
|
||||
export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
|
||||
@@ -59,7 +61,8 @@ export const groupServiceFactory = ({
|
||||
projectBotDAL,
|
||||
projectKeyDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
licenseService,
|
||||
oidcConfigDAL
|
||||
}: TGroupServiceFactoryDep) => {
|
||||
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
@@ -311,6 +314,18 @@ export const groupServiceFactory = ({
|
||||
message: `Failed to find group with ID ${id}`
|
||||
});
|
||||
|
||||
const oidcConfig = await oidcConfigDAL.findOne({
|
||||
orgId: group.orgId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (oidcConfig?.manageGroupMemberships) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Cannot add user to group: OIDC group membership mapping is enabled - user must be assigned to this group in your OIDC provider."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
|
||||
// check if user has broader or equal to privileges than group
|
||||
@@ -366,6 +381,18 @@ export const groupServiceFactory = ({
|
||||
message: `Failed to find group with ID ${id}`
|
||||
});
|
||||
|
||||
const oidcConfig = await oidcConfigDAL.findOne({
|
||||
orgId: group.orgId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (oidcConfig?.manageGroupMemberships) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Cannot remove user from group: OIDC group membership mapping is enabled - user must be removed from this group in your OIDC provider."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
|
||||
// check if user has broader or equal to privileges than group
|
||||
|
@@ -5,6 +5,11 @@ import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet }
|
||||
|
||||
import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
||||
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
|
||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
@@ -18,13 +23,18 @@ import {
|
||||
infisicalSymmetricEncypt
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
|
||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
|
||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
|
||||
@@ -45,7 +55,14 @@ import {
|
||||
type TOidcConfigServiceFactoryDep = {
|
||||
userDAL: Pick<
|
||||
TUserDALFactory,
|
||||
"create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId"
|
||||
| "create"
|
||||
| "findOne"
|
||||
| "updateById"
|
||||
| "findById"
|
||||
| "findUserEncKeyByUserId"
|
||||
| "findUserEncKeyByUserIdsBatch"
|
||||
| "find"
|
||||
| "transaction"
|
||||
>;
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||
orgDAL: Pick<
|
||||
@@ -57,8 +74,23 @@ type TOidcConfigServiceFactoryDep = {
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
||||
smtpService: Pick<TSmtpService, "sendMail" | "verify">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getUserOrgPermission">;
|
||||
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">;
|
||||
groupDAL: Pick<TGroupDALFactory, "findByOrgId">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
TUserGroupMembershipDALFactory,
|
||||
| "find"
|
||||
| "transaction"
|
||||
| "insertMany"
|
||||
| "findGroupMembershipsByUserIdInOrg"
|
||||
| "delete"
|
||||
| "filterProjectsByUserMembership"
|
||||
>;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
};
|
||||
|
||||
export type TOidcConfigServiceFactory = ReturnType<typeof oidcConfigServiceFactory>;
|
||||
@@ -73,7 +105,14 @@ export const oidcConfigServiceFactory = ({
|
||||
tokenService,
|
||||
orgBotDAL,
|
||||
smtpService,
|
||||
oidcConfigDAL
|
||||
oidcConfigDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
auditLogService
|
||||
}: TOidcConfigServiceFactoryDep) => {
|
||||
const getOidc = async (dto: TGetOidcCfgDTO) => {
|
||||
const org = await orgDAL.findOne({ slug: dto.orgSlug });
|
||||
@@ -156,11 +195,21 @@ export const oidcConfigServiceFactory = ({
|
||||
isActive: oidcCfg.isActive,
|
||||
allowedEmailDomains: oidcCfg.allowedEmailDomains,
|
||||
clientId,
|
||||
clientSecret
|
||||
clientSecret,
|
||||
manageGroupMemberships: oidcCfg.manageGroupMemberships
|
||||
};
|
||||
};
|
||||
|
||||
const oidcLogin = async ({ externalId, email, firstName, lastName, orgId, callbackPort }: TOidcLoginDTO) => {
|
||||
const oidcLogin = async ({
|
||||
externalId,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
orgId,
|
||||
callbackPort,
|
||||
groups = [],
|
||||
manageGroupMemberships
|
||||
}: TOidcLoginDTO) => {
|
||||
const serverCfg = await getServerCfg();
|
||||
|
||||
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) {
|
||||
@@ -315,6 +364,83 @@ export const oidcConfigServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (manageGroupMemberships) {
|
||||
const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(user.id, orgId);
|
||||
const orgGroups = await groupDAL.findByOrgId(orgId);
|
||||
|
||||
const userGroupsNames = userGroups.map((membership) => membership.groupName);
|
||||
const missingGroupsMemberships = groups.filter((groupName) => !userGroupsNames.includes(groupName));
|
||||
const groupsToAddUserTo = orgGroups.filter((group) => missingGroupsMemberships.includes(group.name));
|
||||
|
||||
for await (const group of groupsToAddUserTo) {
|
||||
await addUsersToGroupByUserIds({
|
||||
userIds: [user.id],
|
||||
group,
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL
|
||||
});
|
||||
}
|
||||
|
||||
if (groupsToAddUserTo.length) {
|
||||
await auditLogService.createAuditLog({
|
||||
actor: {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
},
|
||||
orgId,
|
||||
event: {
|
||||
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
userEmail: user.email ?? user.username,
|
||||
assignedToGroups: groupsToAddUserTo.map(({ id, name }) => ({ id, name })),
|
||||
userGroupsClaim: groups
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const membershipsToRemove = userGroups
|
||||
.filter((membership) => !groups.includes(membership.groupName))
|
||||
.map((membership) => membership.groupId);
|
||||
const groupsToRemoveUserFrom = orgGroups.filter((group) => membershipsToRemove.includes(group.id));
|
||||
|
||||
for await (const group of groupsToRemoveUserFrom) {
|
||||
await removeUsersFromGroupByUserIds({
|
||||
userIds: [user.id],
|
||||
group,
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL
|
||||
});
|
||||
}
|
||||
|
||||
if (groupsToRemoveUserFrom.length) {
|
||||
await auditLogService.createAuditLog({
|
||||
actor: {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
},
|
||||
orgId,
|
||||
event: {
|
||||
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
userEmail: user.email ?? user.username,
|
||||
removedFromGroups: groupsToRemoveUserFrom.map(({ id, name }) => ({ id, name })),
|
||||
userGroupsClaim: groups
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||
|
||||
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||
@@ -385,7 +511,8 @@ export const oidcConfigServiceFactory = ({
|
||||
tokenEndpoint,
|
||||
userinfoEndpoint,
|
||||
clientId,
|
||||
clientSecret
|
||||
clientSecret,
|
||||
manageGroupMemberships
|
||||
}: TUpdateOidcCfgDTO) => {
|
||||
const org = await orgDAL.findOne({
|
||||
slug: orgSlug
|
||||
@@ -448,7 +575,8 @@ export const oidcConfigServiceFactory = ({
|
||||
userinfoEndpoint,
|
||||
jwksUri,
|
||||
isActive,
|
||||
lastUsed: null
|
||||
lastUsed: null,
|
||||
manageGroupMemberships
|
||||
};
|
||||
|
||||
if (clientId !== undefined) {
|
||||
@@ -491,7 +619,8 @@ export const oidcConfigServiceFactory = ({
|
||||
tokenEndpoint,
|
||||
userinfoEndpoint,
|
||||
clientId,
|
||||
clientSecret
|
||||
clientSecret,
|
||||
manageGroupMemberships
|
||||
}: TCreateOidcCfgDTO) => {
|
||||
const org = await orgDAL.findOne({
|
||||
slug: orgSlug
|
||||
@@ -589,7 +718,8 @@ export const oidcConfigServiceFactory = ({
|
||||
clientIdTag,
|
||||
encryptedClientSecret,
|
||||
clientSecretIV,
|
||||
clientSecretTag
|
||||
clientSecretTag,
|
||||
manageGroupMemberships
|
||||
});
|
||||
|
||||
return oidcCfg;
|
||||
@@ -683,7 +813,9 @@ export const oidcConfigServiceFactory = ({
|
||||
firstName: claims.given_name ?? "",
|
||||
lastName: claims.family_name ?? "",
|
||||
orgId: org.id,
|
||||
callbackPort
|
||||
groups: claims.groups as string[] | undefined,
|
||||
callbackPort,
|
||||
manageGroupMemberships: oidcCfg.manageGroupMemberships
|
||||
})
|
||||
.then(({ isUserCompleted, providerAuthToken }) => {
|
||||
cb(null, { isUserCompleted, providerAuthToken });
|
||||
@@ -697,5 +829,16 @@ export const oidcConfigServiceFactory = ({
|
||||
return strategy;
|
||||
};
|
||||
|
||||
return { oidcLogin, getOrgAuthStrategy, getOidc, updateOidcCfg, createOidcCfg };
|
||||
const isOidcManageGroupMembershipsEnabled = async (orgId: string, actor: OrgServiceActor) => {
|
||||
await permissionService.getUserOrgPermission(actor.id, orgId, actor.authMethod, actor.orgId);
|
||||
|
||||
const oidcConfig = await oidcConfigDAL.findOne({
|
||||
orgId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
return Boolean(oidcConfig?.manageGroupMemberships);
|
||||
};
|
||||
|
||||
return { oidcLogin, getOrgAuthStrategy, getOidc, updateOidcCfg, createOidcCfg, isOidcManageGroupMembershipsEnabled };
|
||||
};
|
||||
|
@@ -12,6 +12,8 @@ export type TOidcLoginDTO = {
|
||||
lastName?: string;
|
||||
orgId: string;
|
||||
callbackPort?: string;
|
||||
groups?: string[];
|
||||
manageGroupMemberships?: boolean | null;
|
||||
};
|
||||
|
||||
export type TGetOidcCfgDTO =
|
||||
@@ -37,6 +39,7 @@ export type TCreateOidcCfgDTO = {
|
||||
clientSecret: string;
|
||||
isActive: boolean;
|
||||
orgSlug: string;
|
||||
manageGroupMemberships: boolean;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TUpdateOidcCfgDTO = Partial<{
|
||||
@@ -52,5 +55,6 @@ export type TUpdateOidcCfgDTO = Partial<{
|
||||
clientSecret: string;
|
||||
isActive: boolean;
|
||||
orgSlug: string;
|
||||
manageGroupMemberships: boolean;
|
||||
}> &
|
||||
TGenericPermission;
|
||||
|
@@ -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
|
||||
|
@@ -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.",
|
||||
|
@@ -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({
|
||||
|
@@ -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",
|
||||
|
@@ -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()
|
||||
|
@@ -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" };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -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"`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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"
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
};
|
||||
};
|
||||
|
@@ -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 = {
|
||||
|
@@ -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
|
||||
})) ?? []
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -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
|
||||
};
|
||||
};
|
||||
|
@@ -284,3 +284,8 @@ export type TOctopusDeployVariableSet = {
|
||||
Self: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type GetVercelCustomEnvironmentsDTO = {
|
||||
teamId: string;
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@@ -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
|
||||
|
@@ -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",
|
||||
|
17
backend/src/services/smtp/templates/passwordSetup.handlebars
Normal 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>
|
@@ -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)
|
||||
|
@@ -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 you’re 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 problem’s background, why it’s 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 you’ve got a general direction, you might need to test a some theories. This is where quick proof of concepts (POCs) come in handy, but don’t 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.
|
@@ -66,7 +66,8 @@
|
||||
{
|
||||
"group": "Engineering",
|
||||
"pages": [
|
||||
"documentation/engineering/oncall"
|
||||
"documentation/engineering/oncall",
|
||||
"documentation/engineering/how-to-write-design-doc"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@@ -192,6 +192,17 @@ services:
|
||||
depends_on:
|
||||
- openldap
|
||||
profiles: [ldap]
|
||||
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:26.1.0
|
||||
restart: always
|
||||
environment:
|
||||
- KC_BOOTSTRAP_ADMIN_PASSWORD=admin
|
||||
- KC_BOOTSTRAP_ADMIN_USERNAME=admin
|
||||
command: start-dev
|
||||
ports:
|
||||
- 8088:8080
|
||||
profiles: [ sso ]
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
|
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 'Install'
|
||||
description: "Infisical's CLI is one of the best way to manage environments and secrets. Install it here"
|
||||
description: "Infisical's CLI is one of the best ways to manage environments and secrets. Install it here"
|
||||
---
|
||||
|
||||
The Infisical CLI is a powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables.
|
||||
@@ -127,4 +127,4 @@ You can use it across various environments, whether it's local development, CI/C
|
||||
## Quick Usage Guide
|
||||
<Card color="#00A300" href="./usage">
|
||||
Now that you have the CLI installed on your system, follow this guide to make the best use of it
|
||||
</Card>
|
||||
</Card>
|
||||
|
@@ -1,112 +0,0 @@
|
||||
---
|
||||
title: "Keycloak OIDC"
|
||||
description: "Learn how to configure Keycloak OIDC for Infisical SSO."
|
||||
---
|
||||
|
||||
<Info>
|
||||
Keycloak OIDC SSO is a paid feature. If you're using Infisical Cloud, then it
|
||||
is available under the **Pro Tier**. If you're self-hosting Infisical, then
|
||||
you should contact sales@infisical.com to purchase an enterprise license to
|
||||
use it.
|
||||
</Info>
|
||||
|
||||
<Steps>
|
||||
<Step title="Create an OIDC client application in Keycloak">
|
||||
1.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application.
|
||||
|
||||

|
||||
|
||||
<Info>
|
||||
You don’t 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**.
|
||||
|
||||

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

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

|
||||
<Info>
|
||||
If you’re 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.
|
||||
|
||||

|
||||
|
||||
1.6. Next, click **Add predefined mapper**.
|
||||
|
||||

|
||||
|
||||
1.7. Select the **email**, **given name**, **family name** attributes and click **Add**.
|
||||
|
||||

|
||||

|
||||
|
||||
Once you've completed the above steps, the list of mappers should look like the following:
|
||||

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

|
||||
|
||||
2.2. From the Clients page, navigate to the Credential tab and copy the **Client Secret** to be used in the next steps.
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Finish configuring OIDC in Infisical">
|
||||
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect.
|
||||

|
||||
|
||||
3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**.
|
||||

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

|
||||
|
||||
</Step>
|
||||
<Step title="Enforce OIDC SSO in Infisical">
|
||||
Enforcing OIDC SSO ensures that members in your organization can only access Infisical
|
||||
by logging into the organization via Keycloak.
|
||||
|
||||
To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one Keycloak user with Infisical.
|
||||
Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO.
|
||||
|
||||
<Warning>
|
||||
We recommend ensuring that your account is provisioned using the application in Keycloak
|
||||
prior to enforcing OIDC SSO to prevent any unintended issues.
|
||||
</Warning>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Tip>
|
||||
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite OIDC login.
|
||||
</Tip>
|
||||
|
||||
<Note>
|
||||
If you're configuring OIDC SSO on a self-hosted instance of Infisical, make
|
||||
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
|
||||
work:
|
||||
<div class="height:1px;"/>
|
||||
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
|
||||
can be a random 32-byte base64 string generated with `openssl rand -base64
|
||||
32`.
|
||||
<div class="height:1px;"/>
|
||||
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
|
||||
</Note>
|
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: "Keycloak OIDC Group Membership Mapping"
|
||||
sidebarTitle: "Group Membership Mapping"
|
||||
description: "Learn how to sync Keycloak group members to matching groups in Infisical."
|
||||
---
|
||||
|
||||
You can have Infisical automatically sync group
|
||||
memberships between Keycloak and Infisical by configuring a group membership mapper in Keycloak.
|
||||
When a user logs in via OIDC, they will be added to Infisical groups that match their Keycloak groups names, and removed from any
|
||||
Infisical groups not present in their groups claim.
|
||||
|
||||
<Info>
|
||||
When enabled, manual
|
||||
management of Infisical group memberships will be disabled.
|
||||
</Info>
|
||||
|
||||
<Warning>
|
||||
Group membership changes in the Keycloak only sync with Infisical when a
|
||||
user logs in via OIDC. For example, if you remove a user from a group in Keycloak, this change will not be reflected in Infisical until their next OIDC login. To ensure this behavior, Infisical recommends enabling Enforce OIDC
|
||||
SSO in the OIDC settings.
|
||||
</Warning>
|
||||
|
||||
|
||||
<Steps>
|
||||
<Step title="Configure a group membership mapper in Keycloak">
|
||||
1.1. In your realm, navigate to the **Clients** tab and select your Infisical client.
|
||||
|
||||

|
||||
|
||||
1.2. Select the **Client Scopes** tab.
|
||||
|
||||

|
||||
|
||||
1.3. Next, select the dedicated scope for your Infisical client.
|
||||
|
||||

|
||||
|
||||
1.4. Click on the **Add mapper** button, and select the **By configuration** option.
|
||||
|
||||

|
||||
|
||||
1.5. Select the **Group Membership** option.
|
||||
|
||||

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

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

|
||||
|
||||
2.2. Next, enable **OIDC Group Membership Mapping** in Organization Settings > Security.
|
||||

|
||||
|
||||
2.3. The next time a user logs in they will be synced to their matching Keycloak groups.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
113
docs/documentation/platform/sso/keycloak-oidc/overview.mdx
Normal 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.
|
||||
|
||||

|
||||
|
||||
<Info>
|
||||
You don’t 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**.
|
||||
|
||||

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

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

|
||||
<Info>
|
||||
If you’re 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.
|
||||
|
||||

|
||||
|
||||
1.6. Next, click **Add predefined mapper**.
|
||||
|
||||

|
||||
|
||||
1.7. Select the **email**, **given name**, **family name** attributes and click **Add**.
|
||||
|
||||

|
||||

|
||||
|
||||
Once you've completed the above steps, the list of mappers should look like the following:
|
||||

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

|
||||
|
||||
2.2. From the Clients page, navigate to the Credential tab and copy the **Client Secret** to be used in the next steps.
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Finish configuring OIDC in Infisical">
|
||||
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect.
|
||||

|
||||
|
||||
3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**.
|
||||

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

|
||||
|
||||
</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>
|
Before Width: | Height: | Size: 632 KiB After Width: | Height: | Size: 645 KiB |
After Width: | Height: | Size: 306 KiB |
After Width: | Height: | Size: 341 KiB |
After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 318 KiB |
After Width: | Height: | Size: 1.3 MiB |
After Width: | Height: | Size: 438 KiB |
After Width: | Height: | Size: 396 KiB |
After Width: | Height: | Size: 468 KiB |
After Width: | Height: | Size: 578 KiB |
After Width: | Height: | Size: 1.2 MiB |
@@ -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">
|
||||

|
||||
</Step>
|
||||
<Step title="Navigate to IAM & Admin > Service Accounts in Google Cloud Console">
|
||||

|
||||

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

|
||||

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

|
||||

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

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

|
||||
</Step>
|
||||
|
||||
|
@@ -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**.
|
||||
|
@@ -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**.
|
||||
|
@@ -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:
|
||||
|
||||
|
@@ -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": [
|
||||
|
@@ -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}");
|
||||
|
@@ -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">
|
||||
|
@@ -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(
|
||||
|
@@ -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.`
|
||||
})
|
||||
};
|
||||
|
@@ -114,7 +114,11 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.DELETE_SECRET_SYNC]: "Delete Secret Sync",
|
||||
[EventType.SECRET_SYNC_SYNC_SECRETS]: "Secret Sync synced secrets",
|
||||
[EventType.SECRET_SYNC_IMPORT_SECRETS]: "Secret Sync imported secrets",
|
||||
[EventType.SECRET_SYNC_REMOVE_SECRETS]: "Secret Sync removed secrets"
|
||||
[EventType.SECRET_SYNC_REMOVE_SECRETS]: "Secret Sync removed secrets",
|
||||
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER]:
|
||||
"OIDC group membership mapping assigned user to groups",
|
||||
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]:
|
||||
"OIDC group membership mapping removed user from groups"
|
||||
};
|
||||
|
||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||
|
@@ -127,5 +127,7 @@ export enum EventType {
|
||||
DELETE_SECRET_SYNC = "delete-secret-sync",
|
||||
SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets",
|
||||
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
|
||||
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets"
|
||||
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
||||
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
|
||||
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user"
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ export type TGetAuditLogsFilter = {
|
||||
actorType?: ActorType;
|
||||
projectId?: string;
|
||||
actor?: string; // user ID format
|
||||
secretPath?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
limit: number;
|
||||
|
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -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 = {
|
||||
|
@@ -16,5 +16,6 @@ export {
|
||||
useGetIntegrationAuthTeamCityBuildConfigs,
|
||||
useGetIntegrationAuthTeams,
|
||||
useGetIntegrationAuthVercelBranches,
|
||||
useGetIntegrationAuthVercelCustomEnvironments,
|
||||
useSaveIntegrationAccessToken
|
||||
} from "./queries";
|
||||
|
@@ -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
|
||||
}: {
|
||||
|
@@ -43,6 +43,11 @@ export type Environment = {
|
||||
environmentId: string;
|
||||
};
|
||||
|
||||
export type VercelEnvironment = {
|
||||
id: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type ChecklyGroup = {
|
||||
name: string;
|
||||
groupId: number;
|
||||
|
@@ -20,7 +20,8 @@ export const useUpdateOIDCConfig = () => {
|
||||
clientId,
|
||||
clientSecret,
|
||||
isActive,
|
||||
orgSlug
|
||||
orgSlug,
|
||||
manageGroupMemberships
|
||||
}: {
|
||||
allowedEmailDomains?: string;
|
||||
issuer?: string;
|
||||
@@ -34,6 +35,7 @@ export const useUpdateOIDCConfig = () => {
|
||||
isActive?: boolean;
|
||||
configurationType?: string;
|
||||
orgSlug: string;
|
||||
manageGroupMemberships?: boolean;
|
||||
}) => {
|
||||
const { data } = await apiRequest.patch("/api/v1/sso/oidc/config", {
|
||||
issuer,
|
||||
@@ -47,7 +49,8 @@ export const useUpdateOIDCConfig = () => {
|
||||
clientId,
|
||||
orgSlug,
|
||||
clientSecret,
|
||||
isActive
|
||||
isActive,
|
||||
manageGroupMemberships
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -74,7 +77,8 @@ export const useCreateOIDCConfig = () => {
|
||||
clientId,
|
||||
clientSecret,
|
||||
isActive,
|
||||
orgSlug
|
||||
orgSlug,
|
||||
manageGroupMemberships
|
||||
}: {
|
||||
issuer?: string;
|
||||
configurationType: string;
|
||||
@@ -88,6 +92,7 @@ export const useCreateOIDCConfig = () => {
|
||||
isActive: boolean;
|
||||
orgSlug: string;
|
||||
allowedEmailDomains?: string;
|
||||
manageGroupMemberships?: boolean;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post("/api/v1/sso/oidc/config", {
|
||||
issuer,
|
||||
@@ -101,7 +106,8 @@ export const useCreateOIDCConfig = () => {
|
||||
clientId,
|
||||
clientSecret,
|
||||
isActive,
|
||||
orgSlug
|
||||
orgSlug,
|
||||
manageGroupMemberships
|
||||
});
|
||||
|
||||
return data;
|
||||
|
@@ -5,7 +5,9 @@ import { apiRequest } from "@app/config/request";
|
||||
import { OIDCConfigData } from "./types";
|
||||
|
||||
export const oidcConfigKeys = {
|
||||
getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const
|
||||
getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const,
|
||||
getOIDCManageGroupMembershipsEnabled: (orgId: string) =>
|
||||
["oidc-manage-group-memberships", orgId] as const
|
||||
};
|
||||
|
||||
export const useGetOIDCConfig = (orgSlug: string) => {
|
||||
@@ -25,3 +27,16 @@ export const useGetOIDCConfig = (orgSlug: string) => {
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useOidcManageGroupMembershipsEnabled = (orgId: string) => {
|
||||
return useQuery({
|
||||
queryKey: oidcConfigKeys.getOIDCManageGroupMembershipsEnabled(orgId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ isEnabled: boolean }>(
|
||||
`/api/v1/sso/oidc/manage-group-memberships?orgId=${orgId}`
|
||||
);
|
||||
|
||||
return data.isEnabled;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -12,4 +12,5 @@ export type OIDCConfigData = {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
allowedEmailDomains?: string;
|
||||
manageGroupMemberships: boolean;
|
||||
};
|
||||
|
@@ -136,7 +136,8 @@ export const PasswordResetPage = () => {
|
||||
encryptedPrivateKeyTag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
verificationToken
|
||||
verificationToken,
|
||||
password: newPassword
|
||||
});
|
||||
|
||||
navigate({ to: "/login" });
|
||||
|
349
frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx
Normal 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>
|
||||
);
|
||||
};
|
15
frontend/src/pages/auth/PasswordSetupPage/route.tsx
Normal 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)
|
||||
});
|
@@ -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" });
|
||||
}
|
||||
|
||||
|
@@ -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}
|
||||
|
@@ -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,
|
||||
|
@@ -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>
|
||||
|
@@ -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(),
|
||||
|
@@ -2,8 +2,10 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { DeleteActionModal, IconButton } from "@app/components/v2";
|
||||
import { useRemoveUserFromGroup } from "@app/hooks/api";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { DeleteActionModal, IconButton, Tooltip } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useOidcManageGroupMembershipsEnabled, useRemoveUserFromGroup } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddGroupMembersModal } from "../AddGroupMemberModal";
|
||||
@@ -20,6 +22,11 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {
|
||||
"removeMemberFromGroup"
|
||||
] as const);
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const { data: isOidcManageGroupMembershipsEnabled = false } =
|
||||
useOidcManageGroupMembershipsEnabled(currentOrg.id);
|
||||
|
||||
const { mutateAsync: removeUserFromGroupMutateAsync } = useRemoveUserFromGroup();
|
||||
const handleRemoveUserFromGroup = async (username: string) => {
|
||||
try {
|
||||
@@ -47,19 +54,35 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Group Members</h3>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addGroupMembers", {
|
||||
groupId,
|
||||
slug: groupSlug
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||
{(isAllowed) => (
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={
|
||||
isOidcManageGroupMembershipsEnabled
|
||||
? "OIDC Group Membership Mapping Enabled. Assign users to this group in your OIDC provider."
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-center">
|
||||
<IconButton
|
||||
isDisabled={isOidcManageGroupMembershipsEnabled || !isAllowed}
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addGroupMembers", {
|
||||
groupId,
|
||||
slug: groupSlug
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<GroupMembersTable
|
||||
|
@@ -21,11 +21,12 @@ import {
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { useListGroupUsers } from "@app/hooks/api";
|
||||
import { useListGroupUsers, useOidcManageGroupMembershipsEnabled } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { EFilterReturnedUsers } from "@app/hooks/api/groups/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
@@ -58,6 +59,11 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
|
||||
toggleOrderDirection
|
||||
} = usePagination(GroupMembersOrderBy.Name, { initPerPage: 10 });
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const { data: isOidcManageGroupMembershipsEnabled = false } =
|
||||
useOidcManageGroupMembershipsEnabled(currentOrg.id);
|
||||
|
||||
const { data: groupMemberships, isPending } = useListGroupUsers({
|
||||
id: groupId,
|
||||
groupSlug,
|
||||
@@ -173,19 +179,30 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
|
||||
{!groupMemberships?.users.length && (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||
{(isAllowed) => (
|
||||
<div className="mb-4 flex items-center justify-center">
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addGroupMembers", {
|
||||
groupId,
|
||||
slug: groupSlug
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add members
|
||||
</Button>
|
||||
</div>
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={
|
||||
isOidcManageGroupMembershipsEnabled
|
||||
? "OIDC Group Membership Mapping Enabled. Assign users to this group in your OIDC provider."
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-center">
|
||||
<Button
|
||||
variant="solid"
|
||||
colorSchema="secondary"
|
||||
isDisabled={isOidcManageGroupMembershipsEnabled || !isAllowed}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addGroupMembers", {
|
||||
groupId,
|
||||
slug: groupSlug
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add members
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
|
@@ -3,7 +3,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useOidcManageGroupMembershipsEnabled } from "@app/hooks/api";
|
||||
import { TGroupUser } from "@app/hooks/api/groups/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -19,6 +20,11 @@ export const GroupMembershipRow = ({
|
||||
user: { firstName, lastName, username, joinedGroupAt, email, id },
|
||||
handlePopUpOpen
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const { data: isOidcManageGroupMembershipsEnabled = false } =
|
||||
useOidcManageGroupMembershipsEnabled(currentOrg.id);
|
||||
|
||||
return (
|
||||
<Tr className="items-center" key={`group-user-${id}`}>
|
||||
<Td>
|
||||
@@ -36,15 +42,21 @@ export const GroupMembershipRow = ({
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<Tooltip content="Remove user from group">
|
||||
<Tooltip
|
||||
content={
|
||||
isOidcManageGroupMembershipsEnabled
|
||||
? "OIDC Group Membership Mapping Enabled. Remove user from this group in your OIDC provider."
|
||||
: "Remove user from group"
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
isDisabled={!isAllowed}
|
||||
isDisabled={!isAllowed || isOidcManageGroupMembershipsEnabled}
|
||||
ariaLabel="Remove user from group"
|
||||
onClick={() => handlePopUpOpen("removeMemberFromGroup", { username })}
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
>
|
||||
<FontAwesomeIcon icon={faUserMinus} className="cursor-pointer" />
|
||||
<FontAwesomeIcon icon={faUserMinus} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
@@ -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>
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
@@ -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}
|
||||
|
@@ -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({
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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}`}
|
||||
|
@@ -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}`}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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
|
||||
|
@@ -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}`}
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
@@ -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"
|
||||
|
@@ -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")])
|
||||
]),
|
||||
|
@@ -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"
|
||||
|
@@ -1,3 +1,4 @@
|
||||
{{- if .Values.installCRDs }}
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
@@ -465,4 +466,5 @@ status:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
storedVersions: []
|
||||
{{- end }}
|
@@ -32,7 +32,7 @@ controllerManager:
|
||||
- ALL
|
||||
image:
|
||||
repository: infisical/kubernetes-operator
|
||||
tag: v0.8.7
|
||||
tag: v0.8.8
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
|
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -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()
|
||||
|
@@ -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
|
||||
}
|
||||
|