mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
misc: add instance banner and consent support
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasAuthConsentContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "authConsentContent");
|
||||
const hasPageFrameContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "pageFrameContent");
|
||||
if (await knex.schema.hasTable(TableName.SuperAdmin)) {
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
if (!hasAuthConsentContentCol) {
|
||||
t.text("authConsentContent");
|
||||
}
|
||||
if (!hasPageFrameContentCol) {
|
||||
t.text("pageFrameContent");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasAuthConsentContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "authConsentContent");
|
||||
const hasPageFrameContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "pageFrameContent");
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
if (hasAuthConsentContentCol) {
|
||||
t.dropColumn("authConsentContent");
|
||||
}
|
||||
if (hasPageFrameContentCol) {
|
||||
t.dropColumn("pageFrameContent");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -23,7 +23,9 @@ export const SuperAdminSchema = z.object({
|
||||
defaultAuthOrgId: z.string().uuid().nullable().optional(),
|
||||
enabledLoginMethods: z.string().array().nullable().optional(),
|
||||
encryptedSlackClientId: zodBuffer.nullable().optional(),
|
||||
encryptedSlackClientSecret: zodBuffer.nullable().optional()
|
||||
encryptedSlackClientSecret: zodBuffer.nullable().optional(),
|
||||
authConsentContent: z.string().nullable().optional(),
|
||||
pageFrameContent: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||
|
||||
@@ -72,7 +72,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
message: "At least one login method should be enabled."
|
||||
}),
|
||||
slackClientId: z.string().optional(),
|
||||
slackClientSecret: z.string().optional()
|
||||
slackClientSecret: z.string().optional(),
|
||||
authConsentContent: z.string().optional(),
|
||||
pageFrameContent: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
||||
24
frontend/package-lock.json
generated
24
frontend/package-lock.json
generated
@@ -54,6 +54,7 @@
|
||||
"classnames": "^2.5.1",
|
||||
"cva": "npm:class-variance-authority@^0.7.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^11.14.1",
|
||||
"i18next": "^24.1.0",
|
||||
@@ -4078,6 +4079,12 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz",
|
||||
@@ -5945,11 +5952,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz",
|
||||
"integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optional": true
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
|
||||
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.0",
|
||||
@@ -8358,6 +8366,12 @@
|
||||
"html2canvas": "^1.0.0-rc.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jspdf/node_modules/dompurify": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz",
|
||||
"integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/jsrp": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/jsrp/-/jsrp-0.2.4.tgz",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"classnames": "^2.5.1",
|
||||
"cva": "npm:class-variance-authority@^0.7.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^11.14.1",
|
||||
"i18next": "^24.1.0",
|
||||
|
||||
17
frontend/src/components/page-frames/Banner.tsx
Normal file
17
frontend/src/components/page-frames/Banner.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable react/no-danger */
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
import { useServerConfig } from "@app/context";
|
||||
|
||||
export const Banner = () => {
|
||||
const { config } = useServerConfig();
|
||||
|
||||
// eslint-disable-next-line react/no-danger-with-children
|
||||
return config.pageFrameContent ? (
|
||||
<div className="h-[3vh] w-full text-center">
|
||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(config.pageFrameContent) }} />
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
||||
@@ -59,7 +59,8 @@ export const leaveConfirmDefaultMessage =
|
||||
|
||||
export enum SessionStorageKeys {
|
||||
CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN",
|
||||
ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL"
|
||||
ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL",
|
||||
AUTH_CONSENT = "AUTH_CONSENT"
|
||||
}
|
||||
|
||||
export const secretTagsColors = [
|
||||
|
||||
@@ -22,6 +22,8 @@ export type TServerConfig = {
|
||||
defaultAuthOrgAuthMethod?: string | null;
|
||||
defaultAuthOrgAuthEnforced?: boolean | null;
|
||||
enabledLoginMethods: LoginMethod[];
|
||||
authConsentContent?: string;
|
||||
pageFrameContent?: string;
|
||||
};
|
||||
|
||||
export type TCreateAdminUserDTO = {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
|
||||
import { WishForm } from "@app/components/features/WishForm";
|
||||
import { Banner } from "@app/components/page-frames/Banner";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/v2";
|
||||
import { envConfig } from "@app/config/env";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
|
||||
@@ -18,10 +20,14 @@ import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/Mini
|
||||
|
||||
export const AdminLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
const { config } = useServerConfig();
|
||||
|
||||
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||
<Banner />
|
||||
<div className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden md:flex`}>
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||
@@ -69,7 +75,6 @@ export const AdminLayout = () => {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
</nav>
|
||||
</aside>
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
|
||||
@@ -83,6 +88,7 @@ export const AdminLayout = () => {
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
<Banner />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
|
||||
import { Banner } from "@app/components/page-frames/Banner";
|
||||
import {
|
||||
BreadcrumbContainer,
|
||||
Menu,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
MenuItem,
|
||||
TBreadcrumbFormat
|
||||
} from "@app/components/v2";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
|
||||
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
|
||||
@@ -22,6 +24,7 @@ import { SidebarHeader } from "./components/SidebarHeader";
|
||||
export const OrganizationLayout = () => {
|
||||
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
|
||||
const location = useLocation();
|
||||
const { config } = useServerConfig();
|
||||
const isOrganizationSpecificPage = location.pathname.startsWith("/organization");
|
||||
const breadcrumbs =
|
||||
isOrganizationSpecificPage && matches && "breadcrumbs" in matches
|
||||
@@ -45,9 +48,14 @@ export const OrganizationLayout = () => {
|
||||
] as string[]
|
||||
).includes(location.pathname);
|
||||
|
||||
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex">
|
||||
<Banner />
|
||||
<div
|
||||
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
|
||||
>
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<MinimizedOrgSidebar />
|
||||
@@ -137,6 +145,7 @@ export const OrganizationLayout = () => {
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
<Banner />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,7 +19,8 @@ import {
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
Tabs
|
||||
Tabs,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useServerConfig, useUser } from "@app/context";
|
||||
import {
|
||||
@@ -55,7 +56,9 @@ const formSchema = z.object({
|
||||
trustSamlEmails: z.boolean(),
|
||||
trustLdapEmails: z.boolean(),
|
||||
trustOidcEmails: z.boolean(),
|
||||
defaultAuthOrgId: z.string()
|
||||
defaultAuthOrgId: z.string(),
|
||||
authConsentContent: z.string().optional(),
|
||||
pageFrameContent: z.string().optional()
|
||||
});
|
||||
|
||||
type TDashboardForm = z.infer<typeof formSchema>;
|
||||
@@ -80,7 +83,9 @@ export const OverviewPage = () => {
|
||||
trustSamlEmails: config.trustSamlEmails,
|
||||
trustLdapEmails: config.trustLdapEmails,
|
||||
trustOidcEmails: config.trustOidcEmails,
|
||||
defaultAuthOrgId: config.defaultAuthOrgId ?? ""
|
||||
defaultAuthOrgId: config.defaultAuthOrgId ?? "",
|
||||
authConsentContent: config.authConsentContent,
|
||||
pageFrameContent: config.pageFrameContent
|
||||
}
|
||||
});
|
||||
|
||||
@@ -95,7 +100,14 @@ export const OverviewPage = () => {
|
||||
const isNotAllowed = !user?.superAdmin;
|
||||
const onFormSubmit = async (formData: TDashboardForm) => {
|
||||
try {
|
||||
const { allowedSignUpDomain, trustSamlEmails, trustLdapEmails, trustOidcEmails } = formData;
|
||||
const {
|
||||
allowedSignUpDomain,
|
||||
trustSamlEmails,
|
||||
trustLdapEmails,
|
||||
trustOidcEmails,
|
||||
authConsentContent,
|
||||
pageFrameContent
|
||||
} = formData;
|
||||
|
||||
await updateServerConfig({
|
||||
defaultAuthOrgId: defaultAuthOrgId || null,
|
||||
@@ -103,7 +115,9 @@ export const OverviewPage = () => {
|
||||
allowedSignUpDomain: signUpMode === SignUpModes.Anyone ? allowedSignUpDomain : null,
|
||||
trustSamlEmails,
|
||||
trustLdapEmails,
|
||||
trustOidcEmails
|
||||
trustOidcEmails,
|
||||
authConsentContent,
|
||||
pageFrameContent
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully changed sign up setting.",
|
||||
@@ -324,6 +338,51 @@ export const OverviewPage = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-start">
|
||||
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Notices</div>
|
||||
<div className="mb-4 max-w-lg text-sm text-mineshaft-400">
|
||||
Configure system-wide notification banners and security messages. These
|
||||
settings control the text displayed to users during authentication and
|
||||
throughout their session
|
||||
</div>
|
||||
<Controller
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Auth Consent Content"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="Auth Consent Message"
|
||||
{...field}
|
||||
rows={3}
|
||||
className="thin-scrollbar h-48 max-w-lg !resize-none bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="authConsentContent"
|
||||
/>
|
||||
<Controller
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Page Frame Content"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="Page Frame Content"
|
||||
{...field}
|
||||
rows={3}
|
||||
className="thin-scrollbar h-48 max-w-lg !resize-none bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="pageFrameContent"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { createFileRoute, redirect, stripSearchParams } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import { createFileRoute, Outlet, redirect, stripSearchParams } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { addSeconds, formatISO } from "date-fns";
|
||||
import DOMPurify from "dompurify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button } from "@app/components/v2";
|
||||
import { SessionStorageKeys } from "@app/const";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
|
||||
import { setAuthToken } from "@app/hooks/api/reactQuery";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
@@ -10,6 +16,59 @@ const QueryParamsSchema = z.object({
|
||||
callback_port: z.coerce.number().optional().catch(undefined)
|
||||
});
|
||||
|
||||
export const AuthConsentWrapper = () => {
|
||||
const { config } = useServerConfig();
|
||||
const [hasConsented, setHasConsented] = useState(() => {
|
||||
const consentInfo = sessionStorage.getItem(SessionStorageKeys.AUTH_CONSENT);
|
||||
if (!consentInfo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { expiry, data } = JSON.parse(consentInfo);
|
||||
if (new Date() > new Date(expiry)) {
|
||||
sessionStorage.removeItem(SessionStorageKeys.AUTH_CONSENT);
|
||||
return false;
|
||||
}
|
||||
|
||||
return data === "true";
|
||||
});
|
||||
|
||||
const handleConsent = () => {
|
||||
sessionStorage.setItem(
|
||||
SessionStorageKeys.AUTH_CONSENT,
|
||||
JSON.stringify({
|
||||
expiry: formatISO(addSeconds(new Date(), 60)),
|
||||
data: "true"
|
||||
})
|
||||
);
|
||||
|
||||
setHasConsented(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{config.authConsentContent && !hasConsented && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-mineshaft-700/80 bg-opacity-90">
|
||||
<div className="max-h-[80vh] w-4/12 overflow-y-auto rounded-lg bg-bunker-800 p-6 text-white">
|
||||
<div
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(config.authConsentContent)
|
||||
}}
|
||||
/>
|
||||
<div className="mt-6 flex justify-end">
|
||||
<Button onClick={handleConsent} colorSchema="secondary">
|
||||
OK
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Outlet />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_restrict-login-signup")({
|
||||
validateSearch: zodValidator(QueryParamsSchema),
|
||||
search: {
|
||||
@@ -45,5 +104,6 @@ export const Route = createFileRoute("/_restrict-login-signup")({
|
||||
throw redirect({
|
||||
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
||||
});
|
||||
}
|
||||
},
|
||||
component: AuthConsentWrapper
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user