mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #3196 from Infisical/org-name-constraint
Improvement: Add Organization Name Constraint
This commit is contained in:
@@ -21,3 +21,10 @@ export const slugSchema = ({ min = 1, max = 32, field = "Slug" }: SlugSchemaInpu
|
||||
message: `${field} field can only contain lowercase letters, numbers, and hyphens`
|
||||
});
|
||||
};
|
||||
|
||||
export const GenericResourceNameSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Name must be at least 1 character" })
|
||||
.max(64, { message: "Name must be 64 or fewer characters" })
|
||||
.regex(/^[a-zA-Z0-9\-_\s]+$/, "Name can only contain alphanumeric characters, dashes, underscores, and spaces");
|
||||
|
||||
@@ -13,7 +13,7 @@ import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-t
|
||||
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
||||
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { GenericResourceNameSchema, slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||
@@ -251,7 +251,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
body: z.object({
|
||||
name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(),
|
||||
name: GenericResourceNameSchema.optional(),
|
||||
slug: slugSchema({ max: 64 }).optional(),
|
||||
authEnforced: z.boolean().optional(),
|
||||
scimEnabled: z.boolean().optional(),
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@@ -330,7 +331,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
name: z.string().trim()
|
||||
name: GenericResourceNameSchema
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { UsersSchema } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
@@ -100,7 +101,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
||||
encryptedPrivateKeyTag: z.string().trim(),
|
||||
salt: z.string().trim(),
|
||||
verifier: z.string().trim(),
|
||||
organizationName: z.string().trim().min(1),
|
||||
organizationName: GenericResourceNameSchema,
|
||||
providerAuthToken: z.string().trim().optional().nullish(),
|
||||
attributionSource: z.string().trim().optional(),
|
||||
password: z.string()
|
||||
|
||||
@@ -11,6 +11,7 @@ import { encodeBase64 } from "tweetnacl-util";
|
||||
import { initProjectHelper } from "@app/helpers/project";
|
||||
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { onRequestError } from "@app/hooks/api/reactQuery";
|
||||
|
||||
import InputField from "../basic/InputField";
|
||||
import checkPassword from "../utilities/checks/password/checkPassword";
|
||||
@@ -206,6 +207,7 @@ export default function UserInfoStep({
|
||||
|
||||
incrementStep();
|
||||
} catch (error) {
|
||||
onRequestError(error);
|
||||
setIsLoading(false);
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { useCreateOrg, useSelectOrganization } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { GenericResourceNameSchema } from "@app/lib/schemas";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
name: z.string().nonempty({ message: "Name is required" })
|
||||
name: GenericResourceNameSchema.nonempty({ message: "Name is required" })
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -78,7 +79,7 @@ export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) =>
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen}>
|
||||
<Modal modal={false} isOpen={isOpen}>
|
||||
<ModalContent
|
||||
title="Create Organization"
|
||||
subTitle="Looks like you're not part of any organizations. Create one to start using Infisical"
|
||||
|
||||
@@ -18,268 +18,44 @@ export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"];
|
||||
export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"];
|
||||
export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"];
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
mutationCache: new MutationCache({
|
||||
onError: (error) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const serverResponse = error.response?.data as TApiErrors;
|
||||
if (serverResponse?.error === ApiErrorTypes.ValidationError) {
|
||||
createNotification(
|
||||
{
|
||||
title: "Validation Error",
|
||||
type: "error",
|
||||
text: "Please check the input and try again.",
|
||||
callToAction: (
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<Button variant="outline_bg" size="xs">
|
||||
Show more
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent title="Validation Error Details">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Field</Th>
|
||||
<Th>Issue</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{serverResponse.message?.map(({ message, path }) => (
|
||||
<Tr key={path.join(".")}>
|
||||
<Td>{path.join(".")}</Td>
|
||||
<Td>{message.toLowerCase()}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
),
|
||||
copyActions: [
|
||||
{
|
||||
value: serverResponse.reqId,
|
||||
name: "Request ID",
|
||||
label: `Request ID: ${serverResponse.reqId}`
|
||||
}
|
||||
]
|
||||
},
|
||||
{ closeOnClick: false }
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (serverResponse?.error === ApiErrorTypes.PermissionBoundaryError) {
|
||||
createNotification(
|
||||
{
|
||||
title: "Forbidden Access",
|
||||
type: "error",
|
||||
text: `${serverResponse.message}.`,
|
||||
callToAction: serverResponse?.details?.missingPermissions?.length ? (
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<Button variant="outline_bg" size="xs">
|
||||
Show more
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent title="Missing Permission">
|
||||
<div className="flex flex-col gap-2">
|
||||
{serverResponse.details?.missingPermissions?.map((el, index) => {
|
||||
const hasConditions = Boolean(Object.keys(el.conditions || {}).length);
|
||||
return (
|
||||
<div
|
||||
key={`Forbidden-error-details-${index + 1}`}
|
||||
className="rounded-md border border-gray-600 p-4"
|
||||
>
|
||||
<div>
|
||||
You are not authorized to perform the <b>{el.action}</b> action on the{" "}
|
||||
<b>{el.subject}</b> resource.{" "}
|
||||
{hasConditions &&
|
||||
"Your permission does not allow access to the following conditions:"}
|
||||
</div>
|
||||
{hasConditions && (
|
||||
<ul className="flex list-disc flex-col gap-1 pl-5 pt-2 text-sm">
|
||||
{Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => {
|
||||
const operators = (
|
||||
el.conditions as Record<
|
||||
string,
|
||||
| string
|
||||
| { [K in PermissionConditionOperators]: string | string[] }
|
||||
>
|
||||
)[field];
|
||||
|
||||
const formattedFieldName = camelCaseToSpaces(field).toLowerCase();
|
||||
if (typeof operators === "string") {
|
||||
return (
|
||||
<li
|
||||
key={`Forbidden-error-details-${index + 1}-${
|
||||
fieldIndex + 1
|
||||
}`}
|
||||
>
|
||||
<span className="font-bold capitalize">
|
||||
{formattedFieldName}
|
||||
</span>{" "}
|
||||
<span className="text-mineshaft-200">equal to</span>{" "}
|
||||
<span className="text-yellow-600">{operators}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return Object.keys(operators).map((operator, operatorIndex) => (
|
||||
<li
|
||||
key={`Forbidden-error-details-${index + 1}-${
|
||||
fieldIndex + 1
|
||||
}-${operatorIndex + 1}`}
|
||||
>
|
||||
<span className="font-bold capitalize">
|
||||
{formattedFieldName}
|
||||
</span>{" "}
|
||||
<span className="text-mineshaft-200">
|
||||
{
|
||||
formatedConditionsOperatorNames[
|
||||
operator as PermissionConditionOperators
|
||||
]
|
||||
}
|
||||
</span>{" "}
|
||||
<span className="text-yellow-600">
|
||||
{operators[
|
||||
operator as PermissionConditionOperators
|
||||
].toString()}
|
||||
</span>
|
||||
</li>
|
||||
));
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
) : undefined,
|
||||
copyActions: [
|
||||
{
|
||||
value: serverResponse.reqId,
|
||||
name: "Request ID",
|
||||
label: `Request ID: ${serverResponse.reqId}`
|
||||
}
|
||||
]
|
||||
},
|
||||
{ closeOnClick: false }
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (serverResponse?.error === ApiErrorTypes.ForbiddenError) {
|
||||
createNotification(
|
||||
{
|
||||
title: "Forbidden Access",
|
||||
type: "error",
|
||||
text: `${serverResponse.message}.`,
|
||||
callToAction: serverResponse?.details?.length ? (
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<Button variant="outline_bg" size="xs">
|
||||
Show more
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
title="Validation Rules"
|
||||
subTitle="Please review the allowed rules below."
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{serverResponse.details?.map((el, index) => {
|
||||
const hasConditions = Boolean(Object.keys(el.conditions || {}).length);
|
||||
return (
|
||||
<div
|
||||
key={`Forbidden-error-details-${index + 1}`}
|
||||
className="rounded-md border border-gray-600 p-4"
|
||||
>
|
||||
<div>
|
||||
{el.inverted ? "Cannot" : "Can"}{" "}
|
||||
<span className="text-yellow-600">
|
||||
{el.action.toString().replaceAll(",", ", ")}
|
||||
</span>{" "}
|
||||
{el.subject.toString()} {hasConditions && "with conditions:"}
|
||||
</div>
|
||||
{hasConditions && (
|
||||
<ul className="flex list-disc flex-col gap-1 pl-5 pt-2 text-sm">
|
||||
{Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => {
|
||||
const operators = (
|
||||
el.conditions as Record<
|
||||
string,
|
||||
| string
|
||||
| { [K in PermissionConditionOperators]: string | string[] }
|
||||
>
|
||||
)[field];
|
||||
|
||||
const formattedFieldName = camelCaseToSpaces(field).toLowerCase();
|
||||
if (typeof operators === "string") {
|
||||
return (
|
||||
<li
|
||||
key={`Forbidden-error-details-${index + 1}-${
|
||||
fieldIndex + 1
|
||||
}`}
|
||||
>
|
||||
<span className="font-bold capitalize">
|
||||
{formattedFieldName}
|
||||
</span>{" "}
|
||||
<span className="text-mineshaft-200">equal to</span>{" "}
|
||||
<span className="text-yellow-600">{operators}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return Object.keys(operators).map((operator, operatorIndex) => (
|
||||
<li
|
||||
key={`Forbidden-error-details-${index + 1}-${
|
||||
fieldIndex + 1
|
||||
}-${operatorIndex + 1}`}
|
||||
>
|
||||
<span className="font-bold capitalize">
|
||||
{formattedFieldName}
|
||||
</span>{" "}
|
||||
<span className="text-mineshaft-200">
|
||||
{
|
||||
formatedConditionsOperatorNames[
|
||||
operator as PermissionConditionOperators
|
||||
]
|
||||
}
|
||||
</span>{" "}
|
||||
<span className="text-yellow-600">
|
||||
{operators[
|
||||
operator as PermissionConditionOperators
|
||||
].toString()}
|
||||
</span>
|
||||
</li>
|
||||
));
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
) : undefined,
|
||||
copyActions: [
|
||||
{
|
||||
value: serverResponse.reqId,
|
||||
name: "Request ID",
|
||||
label: `Request ID: ${serverResponse.reqId}`
|
||||
}
|
||||
]
|
||||
},
|
||||
{ closeOnClick: false }
|
||||
);
|
||||
return;
|
||||
}
|
||||
createNotification({
|
||||
title: "Bad Request",
|
||||
export const onRequestError = (error: unknown) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const serverResponse = error.response?.data as TApiErrors;
|
||||
if (serverResponse?.error === ApiErrorTypes.ValidationError) {
|
||||
createNotification(
|
||||
{
|
||||
title: "Validation Error",
|
||||
type: "error",
|
||||
text: `${serverResponse.message}${serverResponse.message?.endsWith(".") ? "" : "."}`,
|
||||
text: "Please check the input and try again.",
|
||||
callToAction: (
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<Button variant="outline_bg" size="xs">
|
||||
Show more
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent title="Validation Error Details">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Field</Th>
|
||||
<Th>Issue</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{serverResponse.message?.map(({ message, path }) => (
|
||||
<Tr key={path.join(".")}>
|
||||
<Td>{path.join(".")}</Td>
|
||||
<Td>{message.toLowerCase()}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
),
|
||||
copyActions: [
|
||||
{
|
||||
value: serverResponse.reqId,
|
||||
@@ -287,9 +63,128 @@ export const queryClient = new QueryClient({
|
||||
label: `Request ID: ${serverResponse.reqId}`
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
},
|
||||
{ closeOnClick: false }
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (serverResponse?.error === ApiErrorTypes.ForbiddenError) {
|
||||
createNotification(
|
||||
{
|
||||
title: "Forbidden Access",
|
||||
type: "error",
|
||||
text: `${serverResponse.message}.`,
|
||||
callToAction: serverResponse?.details?.length ? (
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<Button variant="outline_bg" size="xs">
|
||||
Show more
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
title="Validation Rules"
|
||||
subTitle="Please review the allowed rules below."
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{serverResponse.details?.map((el, index) => {
|
||||
const hasConditions = Boolean(Object.keys(el.conditions || {}).length);
|
||||
return (
|
||||
<div
|
||||
key={`Forbidden-error-details-${index + 1}`}
|
||||
className="rounded-md border border-gray-600 p-4"
|
||||
>
|
||||
<div>
|
||||
{el.inverted ? "Cannot" : "Can"}{" "}
|
||||
<span className="text-yellow-600">
|
||||
{el.action.toString().replaceAll(",", ", ")}
|
||||
</span>{" "}
|
||||
{el.subject.toString()} {hasConditions && "with conditions:"}
|
||||
</div>
|
||||
{hasConditions && (
|
||||
<ul className="flex list-disc flex-col gap-1 pl-5 pt-2 text-sm">
|
||||
{Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => {
|
||||
const operators = (
|
||||
el.conditions as Record<
|
||||
string,
|
||||
| string
|
||||
| { [K in PermissionConditionOperators]: string | string[] }
|
||||
>
|
||||
)[field];
|
||||
|
||||
const formattedFieldName = camelCaseToSpaces(field).toLowerCase();
|
||||
if (typeof operators === "string") {
|
||||
return (
|
||||
<li
|
||||
key={`Forbidden-error-details-${index + 1}-${fieldIndex + 1}`}
|
||||
>
|
||||
<span className="font-bold capitalize">
|
||||
{formattedFieldName}
|
||||
</span>{" "}
|
||||
<span className="text-mineshaft-200">equal to</span>{" "}
|
||||
<span className="text-yellow-600">{operators}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return Object.keys(operators).map((operator, operatorIndex) => (
|
||||
<li
|
||||
key={`Forbidden-error-details-${index + 1}-${
|
||||
fieldIndex + 1
|
||||
}-${operatorIndex + 1}`}
|
||||
>
|
||||
<span className="font-bold capitalize">{formattedFieldName}</span>{" "}
|
||||
<span className="text-mineshaft-200">
|
||||
{
|
||||
formatedConditionsOperatorNames[
|
||||
operator as PermissionConditionOperators
|
||||
]
|
||||
}
|
||||
</span>{" "}
|
||||
<span className="text-yellow-600">
|
||||
{operators[operator as PermissionConditionOperators].toString()}
|
||||
</span>
|
||||
</li>
|
||||
));
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
) : undefined,
|
||||
copyActions: [
|
||||
{
|
||||
value: serverResponse.reqId,
|
||||
name: "Request ID",
|
||||
label: `Request ID: ${serverResponse.reqId}`
|
||||
}
|
||||
]
|
||||
},
|
||||
{ closeOnClick: false }
|
||||
);
|
||||
return;
|
||||
}
|
||||
createNotification({
|
||||
title: "Bad Request",
|
||||
type: "error",
|
||||
text: `${serverResponse.message}${serverResponse.message?.endsWith(".") ? "" : "."}`,
|
||||
copyActions: [
|
||||
{
|
||||
value: serverResponse.reqId,
|
||||
name: "Request ID",
|
||||
label: `Request ID: ${serverResponse.reqId}`
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
mutationCache: new MutationCache({
|
||||
onError: onRequestError
|
||||
}),
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
|
||||
@@ -1 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export * from "./slugSchema";
|
||||
|
||||
export const GenericResourceNameSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Name must be at least 1 character" })
|
||||
.max(64, { message: "Name must be 64 or fewer characters" })
|
||||
.regex(
|
||||
/^[a-zA-Z0-9\-_\s]+$/,
|
||||
"Name can only contain alphanumeric characters, dashes, underscores, and spaces"
|
||||
);
|
||||
|
||||
@@ -14,9 +14,10 @@ import {
|
||||
} from "@app/context";
|
||||
import { isCustomOrgRole } from "@app/helpers/roles";
|
||||
import { useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
|
||||
import { GenericResourceNameSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().max(64, "Too long, maximum length is 64 characters"),
|
||||
name: GenericResourceNameSchema,
|
||||
slug: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens"),
|
||||
|
||||
Reference in New Issue
Block a user