Compare commits

...

16 Commits

Author SHA1 Message Date
1aa9be203e improvement: move sso/provision settings back to org settings tabs 2025-07-11 10:58:35 -07:00
e35ac599f8 Merge pull request #3997 from Infisical/fix-approval-requests-blocking-deletion
fix(approval-workflows): allow null committer on secret approval request and cascade delete on access request
2025-07-11 10:05:19 -07:00
6d91297ca9 Merge pull request #4005 from Infisical/fix/billingPageIdentityLimit
fix(billing): fix feature flags to only use identityLimit
2025-07-11 12:14:58 -03:00
db369b8f51 fix(billing): fix feature flags to only use identityLimit and minor fix invalidate plan query result 2025-07-11 11:36:25 -03:00
a50a95ad6e Merge pull request #3923 from Infisical/daniel/approval-policy-improvements
fix(approval-policies): improve policies handling
2025-07-11 11:44:09 +04:00
4ec0031c42 Merge pull request #4003 from Infisical/offline-docs-dockerfile-update
Allow docs to run fully offline
2025-07-10 21:22:40 -04:00
a6edb67f58 Allow docs to run fully offline 2025-07-10 20:34:56 -04:00
aae5831f35 Merge pull request #4001 from Infisical/server-admin-sidebar-improvements
improvement(frontend): Server admin sidebar improvements
2025-07-10 15:44:25 -07:00
6f78a6b4c1 Merge pull request #4000 from Infisical/fix-remove-jim-as-sole-author-of-secret-leaks
fix(secret-scanning-v2): Remove Jim as sole author of all secret leaks
2025-07-10 15:41:24 -07:00
c2e326b95a fix: remove jim as sole author of all secret leaks 2025-07-10 15:02:38 -07:00
97c96acea5 Update secret-approval-policy-service.ts 2025-07-11 00:59:28 +04:00
5e24015f2a requested changes 2025-07-11 00:54:28 +04:00
f17e1f6699 fix: update approval request user delettion behavior 2025-07-10 10:37:37 -07:00
e71b136859 requested changes 2025-07-10 16:14:40 +04:00
7d2d69fc7d requested changes 2025-07-05 01:56:35 +04:00
0569c7e692 fix(approval-policies): improve policies handling 2025-07-04 03:14:43 +04:00
65 changed files with 375 additions and 286 deletions

View File

@ -0,0 +1,55 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const existingSecretApprovalPolicies = await knex(TableName.SecretApprovalPolicy)
.whereNull("secretPath")
.orWhere("secretPath", "");
const existingAccessApprovalPolicies = await knex(TableName.AccessApprovalPolicy)
.whereNull("secretPath")
.orWhere("secretPath", "");
// update all the secret approval policies secretPath to be "/**"
if (existingSecretApprovalPolicies.length) {
await knex(TableName.SecretApprovalPolicy)
.whereIn(
"id",
existingSecretApprovalPolicies.map((el) => el.id)
)
.update({
secretPath: "/**"
});
}
// update all the access approval policies secretPath to be "/**"
if (existingAccessApprovalPolicies.length) {
await knex(TableName.AccessApprovalPolicy)
.whereIn(
"id",
existingAccessApprovalPolicies.map((el) => el.id)
)
.update({
secretPath: "/**"
});
}
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (table) => {
table.string("secretPath").notNullable().alter();
});
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (table) => {
table.string("secretPath").notNullable().alter();
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (table) => {
table.string("secretPath").nullable().alter();
});
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (table) => {
table.string("secretPath").nullable().alter();
});
}

View File

@ -0,0 +1,35 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
export async function up(knex: Knex): Promise<void> {
const hasCommitterCol = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerUserId");
if (hasCommitterCol) {
await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => {
tb.uuid("committerUserId").nullable().alter();
});
}
const hasRequesterCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "requestedByUserId");
if (hasRequesterCol) {
await knex.schema.alterTable(TableName.AccessApprovalRequest, (tb) => {
tb.dropForeign("requestedByUserId");
tb.foreign("requestedByUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
// can't undo committer nullable
const hasRequesterCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "requestedByUserId");
if (hasRequesterCol) {
await knex.schema.alterTable(TableName.AccessApprovalRequest, (tb) => {
tb.dropForeign("requestedByUserId");
tb.foreign("requestedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
});
}
}

View File

@ -14,8 +14,8 @@ export const AccessApprovalPoliciesApproversSchema = z.object({
updatedAt: z.date(), updatedAt: z.date(),
approverUserId: z.string().uuid().nullable().optional(), approverUserId: z.string().uuid().nullable().optional(),
approverGroupId: z.string().uuid().nullable().optional(), approverGroupId: z.string().uuid().nullable().optional(),
sequence: z.number().default(0).nullable().optional(), sequence: z.number().default(1).nullable().optional(),
approvalsRequired: z.number().default(1).nullable().optional() approvalsRequired: z.number().nullable().optional()
}); });
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>; export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;

View File

@ -11,7 +11,7 @@ export const AccessApprovalPoliciesSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
name: z.string(), name: z.string(),
approvals: z.number().default(1), approvals: z.number().default(1),
secretPath: z.string().nullable().optional(), secretPath: z.string(),
envId: z.string().uuid(), envId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),

View File

@ -12,8 +12,8 @@ export const CertificateAuthoritiesSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
projectId: z.string(), projectId: z.string(),
enableDirectIssuance: z.boolean().default(true),
status: z.string(), status: z.string(),
enableDirectIssuance: z.boolean().default(true),
name: z.string() name: z.string()
}); });

View File

@ -25,8 +25,8 @@ export const CertificatesSchema = z.object({
certificateTemplateId: z.string().uuid().nullable().optional(), certificateTemplateId: z.string().uuid().nullable().optional(),
keyUsages: z.string().array().nullable().optional(), keyUsages: z.string().array().nullable().optional(),
extendedKeyUsages: z.string().array().nullable().optional(), extendedKeyUsages: z.string().array().nullable().optional(),
pkiSubscriberId: z.string().uuid().nullable().optional(), projectId: z.string(),
projectId: z.string() pkiSubscriberId: z.string().uuid().nullable().optional()
}); });
export type TCertificates = z.infer<typeof CertificatesSchema>; export type TCertificates = z.infer<typeof CertificatesSchema>;

View File

@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
export const SecretApprovalPoliciesSchema = z.object({ export const SecretApprovalPoliciesSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
name: z.string(), name: z.string(),
secretPath: z.string().nullable().optional(), secretPath: z.string(),
approvals: z.number().default(1), approvals: z.number().default(1),
envId: z.string().uuid(), envId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),

View File

@ -18,7 +18,7 @@ export const SecretApprovalRequestsSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
isReplicated: z.boolean().nullable().optional(), isReplicated: z.boolean().nullable().optional(),
committerUserId: z.string().uuid(), committerUserId: z.string().uuid().nullable().optional(),
statusChangedByUserId: z.string().uuid().nullable().optional(), statusChangedByUserId: z.string().uuid().nullable().optional(),
bypassReason: z.string().nullable().optional() bypassReason: z.string().nullable().optional()
}); });

View File

@ -2,6 +2,7 @@ import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { ApproverType, BypasserType } from "@app/ee/services/access-approval-policy/access-approval-policy-types"; import { ApproverType, BypasserType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
import { removeTrailingSlash } from "@app/lib/fn";
import { EnforcementLevel } from "@app/lib/types"; import { EnforcementLevel } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -19,7 +20,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
body: z.object({ body: z.object({
projectSlug: z.string().trim(), projectSlug: z.string().trim(),
name: z.string().optional(), name: z.string().optional(),
secretPath: z.string().trim().default("/"), secretPath: z.string().trim().min(1, { message: "Secret path cannot be empty" }).transform(removeTrailingSlash),
environment: z.string(), environment: z.string(),
approvers: z approvers: z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
@ -174,8 +175,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
secretPath: z secretPath: z
.string() .string()
.trim() .trim()
.min(1, { message: "Secret path cannot be empty" })
.optional() .optional()
.transform((val) => (val === "" ? "/" : val)), .transform((val) => (val ? removeTrailingSlash(val) : val)),
approvers: z approvers: z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
z.object({ z.object({

View File

@ -23,10 +23,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
environment: z.string(), environment: z.string(),
secretPath: z secretPath: z
.string() .string()
.optional() .min(1, { message: "Secret path cannot be empty" })
.nullable() .transform((val) => removeTrailingSlash(val)),
.default("/")
.transform((val) => (val ? removeTrailingSlash(val) : val)),
approvers: z approvers: z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
z.object({ type: z.literal(ApproverType.Group), id: z.string() }), z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
@ -100,10 +98,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
approvals: z.number().min(1).default(1), approvals: z.number().min(1).default(1),
secretPath: z secretPath: z
.string() .string()
.trim()
.min(1, { message: "Secret path cannot be empty" })
.optional() .optional()
.nullable() .transform((val) => (val ? removeTrailingSlash(val) : undefined)),
.transform((val) => (val ? removeTrailingSlash(val) : val))
.transform((val) => (val === "" ? "/" : val)),
enforcementLevel: z.nativeEnum(EnforcementLevel).optional(), enforcementLevel: z.nativeEnum(EnforcementLevel).optional(),
allowedSelfApprovals: z.boolean().default(true) allowedSelfApprovals: z.boolean().default(true)
}), }),

View File

@ -58,7 +58,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
deletedAt: z.date().nullish(), deletedAt: z.date().nullish(),
allowedSelfApprovals: z.boolean() allowedSelfApprovals: z.boolean()
}), }),
committerUser: approvalRequestUser, committerUser: approvalRequestUser.nullish(),
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(), commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
environment: z.string(), environment: z.string(),
reviewers: z.object({ userId: z.string(), status: z.string() }).array(), reviewers: z.object({ userId: z.string(), status: z.string() }).array(),
@ -308,7 +308,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
}), }),
environment: z.string(), environment: z.string(),
statusChangedByUser: approvalRequestUser.optional(), statusChangedByUser: approvalRequestUser.optional(),
committerUser: approvalRequestUser, committerUser: approvalRequestUser.nullish(),
reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(), reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
secretPath: z.string(), secretPath: z.string(),
commits: secretRawSchema commits: secretRawSchema

View File

@ -53,7 +53,7 @@ export interface TAccessApprovalPolicyDALFactory
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;
@ -93,7 +93,7 @@ export interface TAccessApprovalPolicyDALFactory
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;
@ -116,7 +116,7 @@ export interface TAccessApprovalPolicyDALFactory
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
}>; }>;
findLastValidPolicy: ( findLastValidPolicy: (
@ -138,7 +138,7 @@ export interface TAccessApprovalPolicyDALFactory
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
} }
| undefined | undefined
@ -190,7 +190,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
}>; }>;
deleteAccessApprovalPolicy: ({ deleteAccessApprovalPolicy: ({
@ -214,7 +214,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;
@ -252,7 +252,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
}>; }>;
getAccessApprovalPolicyByProjectSlug: ({ getAccessApprovalPolicyByProjectSlug: ({
@ -286,7 +286,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;
@ -337,7 +337,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;

View File

@ -60,6 +60,26 @@ export const accessApprovalPolicyServiceFactory = ({
accessApprovalRequestReviewerDAL, accessApprovalRequestReviewerDAL,
orgMembershipDAL orgMembershipDAL
}: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => { }: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => {
const $policyExists = async ({
envId,
secretPath,
policyId
}: {
envId: string;
secretPath: string;
policyId?: string;
}) => {
const policy = await accessApprovalPolicyDAL
.findOne({
envId,
secretPath,
deletedAt: null
})
.catch(() => null);
return policyId ? policy && policy.id !== policyId : Boolean(policy);
};
const createAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["createAccessApprovalPolicy"] = async ({ const createAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["createAccessApprovalPolicy"] = async ({
name, name,
actor, actor,
@ -106,6 +126,12 @@ export const accessApprovalPolicyServiceFactory = ({
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id }); const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
if (!env) throw new NotFoundError({ message: `Environment with slug '${environment}' not found` }); if (!env) throw new NotFoundError({ message: `Environment with slug '${environment}' not found` });
if (await $policyExists({ envId: env.id, secretPath })) {
throw new BadRequestError({
message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'`
});
}
let approverUserIds = userApprovers; let approverUserIds = userApprovers;
if (userApproverNames.length) { if (userApproverNames.length) {
const approverUsersInDB = await userDAL.find({ const approverUsersInDB = await userDAL.find({
@ -279,7 +305,11 @@ export const accessApprovalPolicyServiceFactory = ({
) as { username: string; sequence?: number }[]; ) as { username: string; sequence?: number }[];
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId); const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Approval policy not found" }); if (!accessApprovalPolicy) {
throw new NotFoundError({
message: `Access approval policy with ID '${policyId}' not found`
});
}
const currentApprovals = approvals || accessApprovalPolicy.approvals; const currentApprovals = approvals || accessApprovalPolicy.approvals;
if ( if (
@ -290,9 +320,18 @@ export const accessApprovalPolicyServiceFactory = ({
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
} }
if (!accessApprovalPolicy) { if (
throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` }); await $policyExists({
envId: accessApprovalPolicy.envId,
secretPath: secretPath || accessApprovalPolicy.secretPath,
policyId: accessApprovalPolicy.id
})
) {
throw new BadRequestError({
message: `A policy for secret path '${secretPath}' already exists in environment '${accessApprovalPolicy.environment.slug}'`
});
} }
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,

View File

@ -122,7 +122,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
}>; }>;
deleteAccessApprovalPolicy: ({ deleteAccessApprovalPolicy: ({
@ -146,7 +146,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;
@ -218,7 +218,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;
@ -269,7 +269,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;

View File

@ -1711,7 +1711,7 @@ interface SecretApprovalReopened {
interface SecretApprovalRequest { interface SecretApprovalRequest {
type: EventType.SECRET_APPROVAL_REQUEST; type: EventType.SECRET_APPROVAL_REQUEST;
metadata: { metadata: {
committedBy: string; committedBy?: string | null;
secretApprovalRequestSlug: string; secretApprovalRequestSlug: string;
secretApprovalRequestId: string; secretApprovalRequestId: string;
eventType: SecretApprovalEvent; eventType: SecretApprovalEvent;

View File

@ -361,13 +361,6 @@ export const ldapConfigServiceFactory = ({
}); });
} else { } else {
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({
message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members."
});
}
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({ throw new BadRequestError({

View File

@ -1,5 +1,4 @@
export const BillingPlanRows = { export const BillingPlanRows = {
MemberLimit: { name: "Organization member limit", field: "memberLimit" },
IdentityLimit: { name: "Organization identity limit", field: "identityLimit" }, IdentityLimit: { name: "Organization identity limit", field: "identityLimit" },
WorkspaceLimit: { name: "Project limit", field: "workspaceLimit" }, WorkspaceLimit: { name: "Project limit", field: "workspaceLimit" },
EnvironmentLimit: { name: "Environment limit", field: "environmentLimit" }, EnvironmentLimit: { name: "Environment limit", field: "environmentLimit" },

View File

@ -442,9 +442,7 @@ export const licenseServiceFactory = ({
rows: data.rows.map((el) => { rows: data.rows.map((el) => {
let used = "-"; let used = "-";
if (el.name === BillingPlanRows.MemberLimit.name) { if (el.name === BillingPlanRows.WorkspaceLimit.name) {
used = orgMembersUsed.toString();
} else if (el.name === BillingPlanRows.WorkspaceLimit.name) {
used = projectCount.toString(); used = projectCount.toString();
} else if (el.name === BillingPlanRows.IdentityLimit.name) { } else if (el.name === BillingPlanRows.IdentityLimit.name) {
used = (identityUsed + orgMembersUsed).toString(); used = (identityUsed + orgMembersUsed).toString();
@ -464,12 +462,10 @@ export const licenseServiceFactory = ({
const allowed = onPremFeatures[field as keyof TFeatureSet]; const allowed = onPremFeatures[field as keyof TFeatureSet];
let used = "-"; let used = "-";
if (field === BillingPlanRows.MemberLimit.field) { if (field === BillingPlanRows.WorkspaceLimit.field) {
used = orgMembersUsed.toString();
} else if (field === BillingPlanRows.WorkspaceLimit.field) {
used = projectCount.toString(); used = projectCount.toString();
} else if (field === BillingPlanRows.IdentityLimit.field) { } else if (field === BillingPlanRows.IdentityLimit.field) {
used = identityUsed.toString(); used = (identityUsed + orgMembersUsed).toString();
} }
return { return {

View File

@ -311,13 +311,6 @@ export const samlConfigServiceFactory = ({
}); });
} else { } else {
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({
message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members."
});
}
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({ throw new BadRequestError({

View File

@ -55,6 +55,26 @@ export const secretApprovalPolicyServiceFactory = ({
licenseService, licenseService,
secretApprovalRequestDAL secretApprovalRequestDAL
}: TSecretApprovalPolicyServiceFactoryDep) => { }: TSecretApprovalPolicyServiceFactoryDep) => {
const $policyExists = async ({
envId,
secretPath,
policyId
}: {
envId: string;
secretPath: string;
policyId?: string;
}) => {
const policy = await secretApprovalPolicyDAL
.findOne({
envId,
secretPath,
deletedAt: null
})
.catch(() => null);
return policyId ? policy && policy.id !== policyId : Boolean(policy);
};
const createSecretApprovalPolicy = async ({ const createSecretApprovalPolicy = async ({
name, name,
actor, actor,
@ -106,10 +126,17 @@ export const secretApprovalPolicyServiceFactory = ({
} }
const env = await projectEnvDAL.findOne({ slug: environment, projectId }); const env = await projectEnvDAL.findOne({ slug: environment, projectId });
if (!env) if (!env) {
throw new NotFoundError({ throw new NotFoundError({
message: `Environment with slug '${environment}' not found in project with ID ${projectId}` message: `Environment with slug '${environment}' not found in project with ID ${projectId}`
}); });
}
if (await $policyExists({ envId: env.id, secretPath })) {
throw new BadRequestError({
message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'`
});
}
let groupBypassers: string[] = []; let groupBypassers: string[] = [];
let bypasserUserIds: string[] = []; let bypasserUserIds: string[] = [];
@ -260,6 +287,18 @@ export const secretApprovalPolicyServiceFactory = ({
}); });
} }
if (
await $policyExists({
envId: secretApprovalPolicy.envId,
secretPath: secretPath || secretApprovalPolicy.secretPath,
policyId: secretApprovalPolicy.id
})
) {
throw new BadRequestError({
message: `A policy for secret path '${secretPath}' already exists in environment '${secretApprovalPolicy.environment.slug}'`
});
}
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,

View File

@ -4,7 +4,7 @@ import { ApproverType, BypasserType } from "../access-approval-policy/access-app
export type TCreateSapDTO = { export type TCreateSapDTO = {
approvals: number; approvals: number;
secretPath?: string | null; secretPath: string;
environment: string; environment: string;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[]; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[];
bypassers?: ( bypassers?: (
@ -20,7 +20,7 @@ export type TCreateSapDTO = {
export type TUpdateSapDTO = { export type TUpdateSapDTO = {
secretPolicyId: string; secretPolicyId: string;
approvals?: number; approvals?: number;
secretPath?: string | null; secretPath?: string;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[]; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[];
bypassers?: ( bypassers?: (
| { type: BypasserType.Group; id: string } | { type: BypasserType.Group; id: string }

View File

@ -45,7 +45,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.statusChangedByUserId`, `${TableName.SecretApprovalRequest}.statusChangedByUserId`,
`statusChangedByUser.id` `statusChangedByUser.id`
) )
.join<TUsers>( .leftJoin<TUsers>(
db(TableName.Users).as("committerUser"), db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
`committerUser.id` `committerUser.id`
@ -173,13 +173,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
username: el.statusChangedByUserUsername username: el.statusChangedByUserUsername
} }
: undefined, : undefined,
committerUser: { committerUser: el.committerUserId
? {
userId: el.committerUserId, userId: el.committerUserId,
email: el.committerUserEmail, email: el.committerUserEmail,
firstName: el.committerUserFirstName, firstName: el.committerUserFirstName,
lastName: el.committerUserLastName, lastName: el.committerUserLastName,
username: el.committerUserUsername username: el.committerUserUsername
}, }
: null,
policy: { policy: {
id: el.policyId, id: el.policyId,
name: el.policyName, name: el.policyName,
@ -377,7 +379,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalPolicyBypasser}.bypasserGroupId`, `${TableName.SecretApprovalPolicyBypasser}.bypasserGroupId`,
`bypasserUserGroupMembership.groupId` `bypasserUserGroupMembership.groupId`
) )
.join<TUsers>( .leftJoin<TUsers>(
db(TableName.Users).as("committerUser"), db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
`committerUser.id` `committerUser.id`
@ -488,13 +490,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
enforcementLevel: el.policyEnforcementLevel, enforcementLevel: el.policyEnforcementLevel,
allowedSelfApprovals: el.policyAllowedSelfApprovals allowedSelfApprovals: el.policyAllowedSelfApprovals
}, },
committerUser: { committerUser: el.committerUserId
? {
userId: el.committerUserId, userId: el.committerUserId,
email: el.committerUserEmail, email: el.committerUserEmail,
firstName: el.committerUserFirstName, firstName: el.committerUserFirstName,
lastName: el.committerUserLastName, lastName: el.committerUserLastName,
username: el.committerUserUsername username: el.committerUserUsername
} }
: null
}), }),
childrenMapper: [ childrenMapper: [
{ {
@ -581,7 +585,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalPolicyBypasser}.bypasserGroupId`, `${TableName.SecretApprovalPolicyBypasser}.bypasserGroupId`,
`bypasserUserGroupMembership.groupId` `bypasserUserGroupMembership.groupId`
) )
.join<TUsers>( .leftJoin<TUsers>(
db(TableName.Users).as("committerUser"), db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
`committerUser.id` `committerUser.id`
@ -693,13 +697,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
enforcementLevel: el.policyEnforcementLevel, enforcementLevel: el.policyEnforcementLevel,
allowedSelfApprovals: el.policyAllowedSelfApprovals allowedSelfApprovals: el.policyAllowedSelfApprovals
}, },
committerUser: { committerUser: el.committerUserId
? {
userId: el.committerUserId, userId: el.committerUserId,
email: el.committerUserEmail, email: el.committerUserEmail,
firstName: el.committerUserFirstName, firstName: el.committerUserFirstName,
lastName: el.committerUserLastName, lastName: el.committerUserLastName,
username: el.committerUserUsername username: el.committerUserUsername
} }
: null
}), }),
childrenMapper: [ childrenMapper: [
{ {

View File

@ -1320,7 +1320,7 @@ export const secretApprovalRequestServiceFactory = ({
}); });
const env = await projectEnvDAL.findOne({ id: policy.envId }); const env = await projectEnvDAL.findOne({ id: policy.envId });
const user = await userDAL.findById(secretApprovalRequest.committerUserId); const user = await userDAL.findById(actorId);
await triggerWorkflowIntegrationNotification({ await triggerWorkflowIntegrationNotification({
input: { input: {
@ -1657,7 +1657,7 @@ export const secretApprovalRequestServiceFactory = ({
return { ...doc, commits: approvalCommits }; return { ...doc, commits: approvalCommits };
}); });
const user = await userDAL.findById(secretApprovalRequest.committerUserId); const user = await userDAL.findById(actorId);
const env = await projectEnvDAL.findOne({ id: policy.envId }); const env = await projectEnvDAL.findOne({ id: policy.envId });
await triggerWorkflowIntegrationNotification({ await triggerWorkflowIntegrationNotification({

View File

@ -37,7 +37,8 @@ import {
TQueueSecretScanningDataSourceFullScan, TQueueSecretScanningDataSourceFullScan,
TQueueSecretScanningResourceDiffScan, TQueueSecretScanningResourceDiffScan,
TQueueSecretScanningSendNotification, TQueueSecretScanningSendNotification,
TSecretScanningDataSourceWithConnection TSecretScanningDataSourceWithConnection,
TSecretScanningFinding
} from "./secret-scanning-v2-types"; } from "./secret-scanning-v2-types";
type TSecretRotationV2QueueServiceFactoryDep = { type TSecretRotationV2QueueServiceFactoryDep = {
@ -459,13 +460,16 @@ export const secretScanningV2QueueServiceFactory = async ({
const newFindings = allFindings.filter((finding) => finding.scanId === scanId); const newFindings = allFindings.filter((finding) => finding.scanId === scanId);
if (newFindings.length) { if (newFindings.length) {
const finding = newFindings[0] as TSecretScanningFinding;
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, { await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
status: SecretScanningScanStatus.Completed, status: SecretScanningScanStatus.Completed,
resourceName: resource.name, resourceName: resource.name,
isDiffScan: true, isDiffScan: true,
dataSource, dataSource,
numberOfSecrets: newFindings.length, numberOfSecrets: newFindings.length,
scanId scanId,
authorName: finding?.details?.author,
authorEmail: finding?.details?.email
}); });
} }
@ -582,8 +586,8 @@ export const secretScanningV2QueueServiceFactory = async ({
substitutions: substitutions:
payload.status === SecretScanningScanStatus.Completed payload.status === SecretScanningScanStatus.Completed
? { ? {
authorName: "Jim", authorName: payload.authorName,
authorEmail: "jim@infisical.com", authorEmail: payload.authorEmail,
resourceName, resourceName,
numberOfSecrets: payload.numberOfSecrets, numberOfSecrets: payload.numberOfSecrets,
isDiffScan: payload.isDiffScan, isDiffScan: payload.isDiffScan,

View File

@ -119,7 +119,14 @@ export type TQueueSecretScanningSendNotification = {
resourceName: string; resourceName: string;
} & ( } & (
| { status: SecretScanningScanStatus.Failed; errorMessage: string } | { status: SecretScanningScanStatus.Failed; errorMessage: string }
| { status: SecretScanningScanStatus.Completed; numberOfSecrets: number; scanId: string; isDiffScan: boolean } | {
status: SecretScanningScanStatus.Completed;
numberOfSecrets: number;
scanId: string;
isDiffScan: boolean;
authorName?: string;
authorEmail?: string;
}
); );
export type TCloneRepository = { export type TCloneRepository = {

View File

@ -912,14 +912,6 @@ export const orgServiceFactory = ({
// if there exist no org membership we set is as given by the request // if there exist no org membership we set is as given by the request
if (!inviteeOrgMembership) { if (!inviteeOrgMembership) {
if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({
name: "InviteUser",
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
});
}
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({ throw new BadRequestError({

View File

@ -1,6 +1,32 @@
FROM node:20-alpine FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
RUN npm install -g mint
RUN npm install -g mint@4.2.13
COPY . . COPY . .
# Install a local version of our OpenAPI spec
RUN apk add --no-cache wget jq && \
wget -O spec.json https://app.infisical.com/api/docs/json && \
jq '.api.openapi = "./spec.json"' docs.json > temp.json && \
mv temp.json docs.json
# Run mint dev briefly to download the web client
RUN timeout 30 mint dev || true
FROM node:20-alpine
WORKDIR /app
RUN npm install -g mint@4.2.13
COPY . .
COPY --from=builder /root/.mintlify /root/.mintlify
COPY --from=builder /app/docs.json /app/docs.json
COPY --from=builder /app/spec.json /app/spec.json
EXPOSE 3000 EXPOSE 3000
CMD ["mint", "dev"] CMD ["mint", "dev"]

View File

@ -78,7 +78,10 @@
}, },
{ {
"group": "Infisical SSH", "group": "Infisical SSH",
"pages": ["documentation/platform/ssh/overview", "documentation/platform/ssh/host-groups"] "pages": [
"documentation/platform/ssh/overview",
"documentation/platform/ssh/host-groups"
]
}, },
{ {
"group": "Key Management (KMS)", "group": "Key Management (KMS)",
@ -375,7 +378,10 @@
}, },
{ {
"group": "Architecture", "group": "Architecture",
"pages": ["internals/architecture/components", "internals/architecture/cloud"] "pages": [
"internals/architecture/components",
"internals/architecture/cloud"
]
}, },
"internals/security", "internals/security",
"internals/service-tokens" "internals/service-tokens"
@ -546,7 +552,10 @@
"integrations/cloud/gcp-secret-manager", "integrations/cloud/gcp-secret-manager",
{ {
"group": "Cloudflare", "group": "Cloudflare",
"pages": ["integrations/cloud/cloudflare-pages", "integrations/cloud/cloudflare-workers"] "pages": [
"integrations/cloud/cloudflare-pages",
"integrations/cloud/cloudflare-workers"
]
}, },
"integrations/cloud/terraform-cloud", "integrations/cloud/terraform-cloud",
"integrations/cloud/databricks", "integrations/cloud/databricks",
@ -658,7 +667,11 @@
"cli/commands/reset", "cli/commands/reset",
{ {
"group": "infisical scan", "group": "infisical scan",
"pages": ["cli/commands/scan", "cli/commands/scan-git-changes", "cli/commands/scan-install"] "pages": [
"cli/commands/scan",
"cli/commands/scan-git-changes",
"cli/commands/scan-install"
]
} }
] ]
}, },
@ -982,7 +995,9 @@
"pages": [ "pages": [
{ {
"group": "Kubernetes", "group": "Kubernetes",
"pages": ["api-reference/endpoints/dynamic-secrets/kubernetes/create-lease"] "pages": [
"api-reference/endpoints/dynamic-secrets/kubernetes/create-lease"
]
}, },
"api-reference/endpoints/dynamic-secrets/create", "api-reference/endpoints/dynamic-secrets/create",
"api-reference/endpoints/dynamic-secrets/update", "api-reference/endpoints/dynamic-secrets/update",

View File

@ -29,10 +29,6 @@ export const ROUTE_PATHS = Object.freeze({
"/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback" "/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback"
) )
}, },
SsoPage: setRoute(
"/organization/sso",
"/_authenticate/_inject-org-details/_org-layout/organization/sso"
),
SecretSharing: setRoute( SecretSharing: setRoute(
"/organization/secret-sharing", "/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/" "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/"

View File

@ -170,7 +170,7 @@ export type TCreateAccessPolicyDTO = {
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[]; bypassers?: Bypasser[];
approvals?: number; approvals?: number;
secretPath?: string; secretPath: string;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[]; approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];

View File

@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { organizationKeys } from "../organization/queries"; import { organizationKeys } from "../organization/queries";
import { subscriptionQueryKeys } from "../subscriptions/queries";
import { identitiesKeys } from "./queries"; import { identitiesKeys } from "./queries";
import { import {
AddIdentityAliCloudAuthDTO, AddIdentityAliCloudAuthDTO,
@ -82,6 +83,9 @@ export const useCreateIdentity = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
}); });
queryClient.invalidateQueries({
queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId)
});
} }
}); });
}; };
@ -123,6 +127,9 @@ export const useDeleteIdentity = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
}); });
queryClient.invalidateQueries({
queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId)
});
} }
}); });
}; };

View File

@ -49,7 +49,7 @@ export type TCreateSecretPolicyDTO = {
workspaceId: string; workspaceId: string;
name?: string; name?: string;
environment: string; environment: string;
secretPath?: string | null; secretPath: string;
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[]; bypassers?: Bypasser[];
approvals?: number; approvals?: number;
@ -62,7 +62,7 @@ export type TUpdateSecretPolicyDTO = {
name?: string; name?: string;
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[]; bypassers?: Bypasser[];
secretPath?: string | null; secretPath?: string;
approvals?: number; approvals?: number;
allowedSelfApprovals?: boolean; allowedSelfApprovals?: boolean;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;

View File

@ -9,6 +9,7 @@ import { APIKeyDataV2 } from "../apiKeys/types";
import { MfaMethod } from "../auth/types"; import { MfaMethod } from "../auth/types";
import { TGroupWithProjectMemberships } from "../groups/types"; import { TGroupWithProjectMemberships } from "../groups/types";
import { setAuthToken } from "../reactQuery"; import { setAuthToken } from "../reactQuery";
import { subscriptionQueryKeys } from "../subscriptions/queries";
import { workspaceKeys } from "../workspace"; import { workspaceKeys } from "../workspace";
import { userKeys } from "./query-keys"; import { userKeys } from "./query-keys";
import { import {
@ -188,6 +189,9 @@ export const useAddUsersToOrg = () => {
}, },
onSuccess: (_, { organizationId, projects }) => { onSuccess: (_, { organizationId, projects }) => {
queryClient.invalidateQueries({ queryKey: userKeys.getOrgUsers(organizationId) }); queryClient.invalidateQueries({ queryKey: userKeys.getOrgUsers(organizationId) });
queryClient.invalidateQueries({
queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId)
});
projects?.forEach((project) => { projects?.forEach((project) => {
if (project.slug) { if (project.slug) {

View File

@ -1,6 +1,5 @@
import { import {
faBook, faBook,
faCheckCircle,
faCog, faCog,
faCubes, faCubes,
faDoorClosed, faDoorClosed,
@ -100,18 +99,6 @@ export const OrgSidebar = ({ isHidden }: Props) => {
</MenuItem> </MenuItem>
)} )}
</Link> </Link>
<Link to="/organization/sso">
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCheckCircle} className="mr-4" />
</div>
SSO Settings
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/settings"> <Link to="/organization/settings">
{({ isActive }) => ( {({ isActive }) => (
<MenuItem isSelected={isActive}> <MenuItem isSelected={isActive}>

View File

@ -39,10 +39,6 @@ export const OrgMembersSection = () => {
const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership(); const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership(); const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
const isMoreUsersAllowed = subscription?.memberLimit
? subscription.membersUsed < subscription.memberLimit
: true;
const isMoreIdentitiesAllowed = subscription?.identityLimit const isMoreIdentitiesAllowed = subscription?.identityLimit
? subscription.identitiesUsed < subscription.identityLimit ? subscription.identitiesUsed < subscription.identityLimit
: true; : true;
@ -58,7 +54,7 @@ export const OrgMembersSection = () => {
return; return;
} }
if ((!isMoreUsersAllowed || !isMoreIdentitiesAllowed) && !isEnterprise) { if (!isMoreIdentitiesAllowed && !isEnterprise) {
handlePopUpOpen("upgradePlan", { handlePopUpOpen("upgradePlan", {
description: "You can add more members if you upgrade your Infisical plan." description: "You can add more members if you upgrade your Infisical plan."
}); });

View File

@ -1,6 +1,3 @@
import { Link } from "@tanstack/react-router";
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc"; import { withPermission } from "@app/hoc";
@ -11,21 +8,6 @@ export const OrgSecurityTab = withPermission(
() => { () => {
return ( return (
<> <>
<NoticeBannerV2
className="mx-auto mb-4"
titleClassName="text-base"
title="Single Sign-On (SSO) Settings"
>
<p className="mt-1 text-mineshaft-300">
SSO Settings have been relocated:{" "}
<Link
className="text-mineshaft-200 underline underline-offset-2"
to="/organization/sso"
>
Click here to view SSO Settings
</Link>
</p>
</NoticeBannerV2>
<OrgGenericAuthSection /> <OrgGenericAuthSection />
<OrgUserAccessTokenLimitSection /> <OrgUserAccessTokenLimitSection />
</> </>

View File

@ -9,8 +9,10 @@ import { ImportTab } from "../ImportTab";
import { KmipTab } from "../KmipTab/OrgKmipTab"; import { KmipTab } from "../KmipTab/OrgKmipTab";
import { OrgEncryptionTab } from "../OrgEncryptionTab"; import { OrgEncryptionTab } from "../OrgEncryptionTab";
import { OrgGeneralTab } from "../OrgGeneralTab"; import { OrgGeneralTab } from "../OrgGeneralTab";
import { OrgProvisioningTab } from "../OrgProvisioningTab";
import { OrgSecurityTab } from "../OrgSecurityTab"; import { OrgSecurityTab } from "../OrgSecurityTab";
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab"; import { OrgSsoTab } from "../OrgSsoTab";
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab";
import { ProjectTemplatesTab } from "../ProjectTemplatesTab"; import { ProjectTemplatesTab } from "../ProjectTemplatesTab";
export const OrgTabGroup = () => { export const OrgTabGroup = () => {
@ -19,6 +21,16 @@ export const OrgTabGroup = () => {
}); });
const tabs = [ const tabs = [
{ name: "General", key: "tab-org-general", component: OrgGeneralTab }, { name: "General", key: "tab-org-general", component: OrgGeneralTab },
{
name: "SSO",
key: "sso-settings",
component: OrgSsoTab
},
{
name: "Provisioning",
key: "provisioning-settings",
component: OrgProvisioningTab
},
{ name: "Security", key: "tab-org-security", component: OrgSecurityTab }, { name: "Security", key: "tab-org-security", component: OrgSecurityTab },
{ name: "Encryption", key: "tab-org-encryption", component: OrgEncryptionTab }, { name: "Encryption", key: "tab-org-encryption", component: OrgEncryptionTab },
{ {

View File

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

View File

@ -1,21 +0,0 @@
import { Helmet } from "react-helmet";
import { PageHeader } from "@app/components/v2";
import { SsoTabGroup } from "./components/SsoTabGroup";
export const SsoPage = () => {
return (
<>
<Helmet>
<title>Single Sign-On (SSO)</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader title="Single Sign-On (SSO)" />
<SsoTabGroup />
</div>
</div>
</>
);
};

View File

@ -1,37 +0,0 @@
import { useState } from "react";
import { useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { OrgProvisioningTab } from "../OrgProvisioningTab";
import { OrgSsoTab } from "../OrgSsoTab";
export const SsoTabGroup = () => {
const search = useSearch({
from: ROUTE_PATHS.Organization.SsoPage.id
});
const tabs = [
{ name: "General", key: "tab-sso-auth", component: OrgSsoTab },
{ name: "Provisioning", key: "tab-sso-identity", component: OrgProvisioningTab }
];
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
return (
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
<TabList>
{tabs.map((tab) => (
<Tab value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
</TabList>
{tabs.map(({ key, component: Component }) => (
<TabPanel value={key} key={`tab-panel-${key}`}>
<Component />
</TabPanel>
))}
</Tabs>
);
};

View File

@ -1 +0,0 @@
export { SsoTabGroup } from "./SsoTabGroup";

View File

@ -1,26 +0,0 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { SsoPage } from "./SsoPage";
const SettingsPageQueryParams = z.object({
selectedTab: z.string().catch("")
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/sso"
)({
component: SsoPage,
validateSearch: zodValidator(SettingsPageQueryParams),
search: {
middlewares: [stripSearchParams({ selectedTab: "" })]
},
context: () => ({
breadcrumbs: [
{
label: "Single Sign-On (SSO)"
}
]
})
});

View File

@ -55,7 +55,7 @@ const formSchema = z
.object({ .object({
environment: z.object({ slug: z.string(), name: z.string() }), environment: z.object({ slug: z.string(), name: z.string() }),
name: z.string().optional(), name: z.string().optional(),
secretPath: z.string().optional(), secretPath: z.string().trim().min(1),
approvals: z.number().min(1).default(1), approvals: z.number().min(1).default(1),
userApprovers: z userApprovers: z
.object({ type: z.literal(ApproverType.User), id: z.string() }) .object({ type: z.literal(ApproverType.User), id: z.string() })
@ -93,10 +93,8 @@ const formSchema = z
.optional() .optional()
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if ( if (data.policyType === PolicyType.ChangePolicy) {
data.policyType === PolicyType.ChangePolicy && if (!(data.groupApprovers.length || data.userApprovers.length)) {
!(data.groupApprovers.length || data.userApprovers.length)
) {
ctx.addIssue({ ctx.addIssue({
path: ["userApprovers"], path: ["userApprovers"],
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
@ -108,6 +106,7 @@ const formSchema = z
message: "At least one approver should be provided" message: "At least one approver should be provided"
}); });
} }
}
}); });
type TFormSchema = z.infer<typeof formSchema>; type TFormSchema = z.infer<typeof formSchema>;
@ -127,6 +126,7 @@ const Form = ({
control, control,
handleSubmit, handleSubmit,
watch, watch,
resetField,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<TFormSchema>({ } = useForm<TFormSchema>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
@ -177,6 +177,7 @@ const Form = ({
: undefined, : undefined,
defaultValues: !editValues defaultValues: !editValues
? { ? {
secretPath: "/",
sequenceApprovers: [{ approvals: 1 }] sequenceApprovers: [{ approvals: 1 }]
} }
: undefined : undefined
@ -405,7 +406,10 @@ const Form = ({
<Select <Select
isDisabled={isEditMode} isDisabled={isEditMode}
value={value} value={value}
onValueChange={(val) => onChange(val as PolicyType)} onValueChange={(val) => {
onChange(val as PolicyType);
resetField("secretPath");
}}
className="w-full border border-mineshaft-500" className="w-full border border-mineshaft-500"
> >
{Object.values(PolicyType).map((policyType) => { {Object.values(PolicyType).map((policyType) => {
@ -465,6 +469,7 @@ const Form = ({
<FormControl <FormControl
tooltipText="Secret paths support glob patterns. For example, '/**' will match all paths." tooltipText="Secret paths support glob patterns. For example, '/**' will match all paths."
label="Secret Path" label="Secret Path"
isRequired
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
className="flex-1" className="flex-1"

View File

@ -338,8 +338,14 @@ export const SecretApprovalRequest = () => {
</div> </div>
<span className="text-xs leading-3 text-gray-500"> <span className="text-xs leading-3 text-gray-500">
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "} Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
{committerUser ? (
<>
{committerUser?.firstName || ""} {committerUser?.lastName || ""} ( {committerUser?.firstName || ""} {committerUser?.lastName || ""} (
{committerUser?.email}) {committerUser?.email})
</>
) : (
<span className="text-gray-600">Deleted User</span>
)}
{!isReviewed && status === "open" && " - Review required"} {!isReviewed && status === "open" && " - Review required"}
</span> </span>
</div> </div>

View File

@ -250,10 +250,17 @@ export const SecretApprovalRequestChanges = ({
secretApprovalRequestDetails.isReplicated secretApprovalRequestDetails.isReplicated
)} )}
</div> </div>
<span className="-mt-1 flex items-center space-x-2 text-xs text-gray-400"> <p className="-mt-1 text-xs text-gray-400">
By {secretApprovalRequestDetails?.committerUser?.firstName} ( By{" "}
{secretApprovalRequestDetails?.committerUser ? (
<>
{secretApprovalRequestDetails?.committerUser?.firstName} (
{secretApprovalRequestDetails?.committerUser?.email}) {secretApprovalRequestDetails?.committerUser?.email})
</span> </>
) : (
<span className="text-gray-500">Deleted User</span>
)}
</p>
</div> </div>
{!hasMerged && {!hasMerged &&
secretApprovalRequestDetails.status === "open" && secretApprovalRequestDetails.status === "open" &&

View File

@ -47,7 +47,6 @@ import { Route as adminEnvironmentPageRouteImport } from './pages/admin/Environm
import { Route as adminEncryptionPageRouteImport } from './pages/admin/EncryptionPage/route' import { Route as adminEncryptionPageRouteImport } from './pages/admin/EncryptionPage/route'
import { Route as adminCachingPageRouteImport } from './pages/admin/CachingPage/route' import { Route as adminCachingPageRouteImport } from './pages/admin/CachingPage/route'
import { Route as adminAuthenticationPageRouteImport } from './pages/admin/AuthenticationPage/route' import { Route as adminAuthenticationPageRouteImport } from './pages/admin/AuthenticationPage/route'
import { Route as organizationSsoPageRouteImport } from './pages/organization/SsoPage/route'
import { Route as organizationProjectsPageRouteImport } from './pages/organization/ProjectsPage/route' import { Route as organizationProjectsPageRouteImport } from './pages/organization/ProjectsPage/route'
import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route' import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route'
import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route' import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route'
@ -591,12 +590,6 @@ const adminAuthenticationPageRouteRoute =
getParentRoute: () => adminLayoutRoute, getParentRoute: () => adminLayoutRoute,
} as any) } as any)
const organizationSsoPageRouteRoute = organizationSsoPageRouteImport.update({
id: '/sso',
path: '/sso',
getParentRoute: () => AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const organizationProjectsPageRouteRoute = const organizationProjectsPageRouteRoute =
organizationProjectsPageRouteImport.update({ organizationProjectsPageRouteImport.update({
id: '/projects', id: '/projects',
@ -2159,13 +2152,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationProjectsPageRouteImport preLoaderRoute: typeof organizationProjectsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
} }
'/_authenticate/_inject-org-details/_org-layout/organization/sso': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/sso'
path: '/sso'
fullPath: '/organization/sso'
preLoaderRoute: typeof organizationSsoPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/admin/_admin-layout/authentication': { '/_authenticate/_inject-org-details/admin/_admin-layout/authentication': {
id: '/_authenticate/_inject-org-details/admin/_admin-layout/authentication' id: '/_authenticate/_inject-org-details/admin/_admin-layout/authentication'
path: '/authentication' path: '/authentication'
@ -3428,7 +3414,6 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
organizationAuditLogsPageRouteRoute: typeof organizationAuditLogsPageRouteRoute organizationAuditLogsPageRouteRoute: typeof organizationAuditLogsPageRouteRoute
organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute
organizationProjectsPageRouteRoute: typeof organizationProjectsPageRouteRoute organizationProjectsPageRouteRoute: typeof organizationProjectsPageRouteRoute
organizationSsoPageRouteRoute: typeof organizationSsoPageRouteRoute
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
@ -3447,7 +3432,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
organizationAuditLogsPageRouteRoute: organizationAuditLogsPageRouteRoute, organizationAuditLogsPageRouteRoute: organizationAuditLogsPageRouteRoute,
organizationBillingPageRouteRoute: organizationBillingPageRouteRoute, organizationBillingPageRouteRoute: organizationBillingPageRouteRoute,
organizationProjectsPageRouteRoute: organizationProjectsPageRouteRoute, organizationProjectsPageRouteRoute: organizationProjectsPageRouteRoute,
organizationSsoPageRouteRoute: organizationSsoPageRouteRoute,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute:
@ -4345,7 +4329,6 @@ export interface FileRoutesByFullPath {
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/organization/billing': typeof organizationBillingPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute
'/organization/projects': typeof organizationProjectsPageRouteRoute '/organization/projects': typeof organizationProjectsPageRouteRoute
'/organization/sso': typeof organizationSsoPageRouteRoute
'/admin/authentication': typeof adminAuthenticationPageRouteRoute '/admin/authentication': typeof adminAuthenticationPageRouteRoute
'/admin/caching': typeof adminCachingPageRouteRoute '/admin/caching': typeof adminCachingPageRouteRoute
'/admin/encryption': typeof adminEncryptionPageRouteRoute '/admin/encryption': typeof adminEncryptionPageRouteRoute
@ -4542,7 +4525,6 @@ export interface FileRoutesByTo {
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/organization/billing': typeof organizationBillingPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute
'/organization/projects': typeof organizationProjectsPageRouteRoute '/organization/projects': typeof organizationProjectsPageRouteRoute
'/organization/sso': typeof organizationSsoPageRouteRoute
'/admin/authentication': typeof adminAuthenticationPageRouteRoute '/admin/authentication': typeof adminAuthenticationPageRouteRoute
'/admin/caching': typeof adminCachingPageRouteRoute '/admin/caching': typeof adminCachingPageRouteRoute
'/admin/encryption': typeof adminEncryptionPageRouteRoute '/admin/encryption': typeof adminEncryptionPageRouteRoute
@ -4739,7 +4721,6 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/projects': typeof organizationProjectsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/projects': typeof organizationProjectsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/sso': typeof organizationSsoPageRouteRoute
'/_authenticate/_inject-org-details/admin/_admin-layout/authentication': typeof adminAuthenticationPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/authentication': typeof adminAuthenticationPageRouteRoute
'/_authenticate/_inject-org-details/admin/_admin-layout/caching': typeof adminCachingPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/caching': typeof adminCachingPageRouteRoute
'/_authenticate/_inject-org-details/admin/_admin-layout/encryption': typeof adminEncryptionPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/encryption': typeof adminEncryptionPageRouteRoute
@ -4949,7 +4930,6 @@ export interface FileRouteTypes {
| '/organization/audit-logs' | '/organization/audit-logs'
| '/organization/billing' | '/organization/billing'
| '/organization/projects' | '/organization/projects'
| '/organization/sso'
| '/admin/authentication' | '/admin/authentication'
| '/admin/caching' | '/admin/caching'
| '/admin/encryption' | '/admin/encryption'
@ -5145,7 +5125,6 @@ export interface FileRouteTypes {
| '/organization/audit-logs' | '/organization/audit-logs'
| '/organization/billing' | '/organization/billing'
| '/organization/projects' | '/organization/projects'
| '/organization/sso'
| '/admin/authentication' | '/admin/authentication'
| '/admin/caching' | '/admin/caching'
| '/admin/encryption' | '/admin/encryption'
@ -5340,7 +5319,6 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/organization/billing' | '/_authenticate/_inject-org-details/_org-layout/organization/billing'
| '/_authenticate/_inject-org-details/_org-layout/organization/projects' | '/_authenticate/_inject-org-details/_org-layout/organization/projects'
| '/_authenticate/_inject-org-details/_org-layout/organization/sso'
| '/_authenticate/_inject-org-details/admin/_admin-layout/authentication' | '/_authenticate/_inject-org-details/admin/_admin-layout/authentication'
| '/_authenticate/_inject-org-details/admin/_admin-layout/caching' | '/_authenticate/_inject-org-details/admin/_admin-layout/caching'
| '/_authenticate/_inject-org-details/admin/_admin-layout/encryption' | '/_authenticate/_inject-org-details/admin/_admin-layout/encryption'
@ -5732,7 +5710,6 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/audit-logs", "/_authenticate/_inject-org-details/_org-layout/organization/audit-logs",
"/_authenticate/_inject-org-details/_org-layout/organization/billing", "/_authenticate/_inject-org-details/_org-layout/organization/billing",
"/_authenticate/_inject-org-details/_org-layout/organization/projects", "/_authenticate/_inject-org-details/_org-layout/organization/projects",
"/_authenticate/_inject-org-details/_org-layout/organization/sso",
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections", "/_authenticate/_inject-org-details/_org-layout/organization/app-connections",
"/_authenticate/_inject-org-details/_org-layout/organization/gateways", "/_authenticate/_inject-org-details/_org-layout/organization/gateways",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing", "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing",
@ -5782,10 +5759,6 @@ export const routeTree = rootRoute
"filePath": "organization/ProjectsPage/route.tsx", "filePath": "organization/ProjectsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization" "parent": "/_authenticate/_inject-org-details/_org-layout/organization"
}, },
"/_authenticate/_inject-org-details/_org-layout/organization/sso": {
"filePath": "organization/SsoPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
},
"/_authenticate/_inject-org-details/admin/_admin-layout/authentication": { "/_authenticate/_inject-org-details/admin/_admin-layout/authentication": {
"filePath": "admin/AuthenticationPage/route.tsx", "filePath": "admin/AuthenticationPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/admin/_admin-layout" "parent": "/_authenticate/_inject-org-details/admin/_admin-layout"

View File

@ -31,7 +31,6 @@ const organizationRoutes = route("/organization", [
index("organization/SettingsPage/route.tsx"), index("organization/SettingsPage/route.tsx"),
route("/oauth/callback", "organization/SettingsPage/OauthCallbackPage/route.tsx") route("/oauth/callback", "organization/SettingsPage/OauthCallbackPage/route.tsx")
]), ]),
route("/sso", "organization/SsoPage/route.tsx"),
route("/groups/$groupId", "organization/GroupDetailsByIDPage/route.tsx"), route("/groups/$groupId", "organization/GroupDetailsByIDPage/route.tsx"),
route("/members/$membershipId", "organization/UserDetailsByIDPage/route.tsx"), route("/members/$membershipId", "organization/UserDetailsByIDPage/route.tsx"),
route("/roles/$roleId", "organization/RoleByIDPage/route.tsx"), route("/roles/$roleId", "organization/RoleByIDPage/route.tsx"),