Update org settings > general tab ui

This commit is contained in:
Tuan Dang
2024-03-12 16:18:17 -07:00
parent a6d7d32156
commit 2ca03abec2
17 changed files with 129 additions and 196 deletions
+11 -9
View File
@@ -164,15 +164,17 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
method: "PATCH", method: "PATCH",
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
schema: { schema: {
body: z.object({ body: z
organizationId: z.string().trim(), .object({
isActive: z.boolean().optional(), isActive: z.boolean(),
url: z.string().trim().optional(), url: z.string().trim(),
bindDN: z.string().trim().optional(), bindDN: z.string().trim(),
bindPass: z.string().trim().optional(), bindPass: z.string().trim(),
searchBase: z.string().trim().optional(), searchBase: z.string().trim(),
caCert: z.string().trim().optional() caCert: z.string().trim()
}), })
.partial()
.merge(z.object({ organizationId: z.string() })),
response: { response: {
200: LdapConfigsSchema 200: LdapConfigsSchema
} }
@@ -180,21 +180,21 @@ export const ldapConfigServiceFactory = ({
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
}); });
if (bindDN) { if (bindDN !== undefined) {
const { ciphertext: encryptedBindDN, iv: bindDNIV, tag: bindDNTag } = encryptSymmetric(bindDN, key); const { ciphertext: encryptedBindDN, iv: bindDNIV, tag: bindDNTag } = encryptSymmetric(bindDN, key);
updateQuery.encryptedBindDN = encryptedBindDN; updateQuery.encryptedBindDN = encryptedBindDN;
updateQuery.bindDNIV = bindDNIV; updateQuery.bindDNIV = bindDNIV;
updateQuery.bindDNTag = bindDNTag; updateQuery.bindDNTag = bindDNTag;
} }
if (bindPass) { if (bindPass !== undefined) {
const { ciphertext: encryptedBindPass, iv: bindPassIV, tag: bindPassTag } = encryptSymmetric(bindPass, key); const { ciphertext: encryptedBindPass, iv: bindPassIV, tag: bindPassTag } = encryptSymmetric(bindPass, key);
updateQuery.encryptedBindPass = encryptedBindPass; updateQuery.encryptedBindPass = encryptedBindPass;
updateQuery.bindPassIV = bindPassIV; updateQuery.bindPassIV = bindPassIV;
updateQuery.bindPassTag = bindPassTag; updateQuery.bindPassTag = bindPassTag;
} }
if (caCert) { if (caCert !== undefined) {
const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
updateQuery.encryptedCACert = encryptedCACert; updateQuery.encryptedCACert = encryptedCACert;
updateQuery.caCertIV = caCertIV; updateQuery.caCertIV = caCertIV;
@@ -17,9 +17,9 @@ export const getDefaultOnPremFeatures = () => {
customAlerts: false, customAlerts: false,
auditLogs: false, auditLogs: false,
auditLogsRetentionDays: 0, auditLogsRetentionDays: 0,
samlSSO: true, samlSSO: false,
scim: true, scim: false,
ldap: true, ldap: false,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true, has_used_trial: true,
@@ -23,9 +23,9 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
customAlerts: false, customAlerts: false,
auditLogs: false, auditLogs: false,
auditLogsRetentionDays: 0, auditLogsRetentionDays: 0,
samlSSO: true, samlSSO: false,
scim: true, scim: false,
ldap: true, ldap: false,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true, has_used_trial: true,
@@ -24,9 +24,9 @@ export type TFeatureSet = {
customAlerts: false; customAlerts: false;
auditLogs: false; auditLogs: false;
auditLogsRetentionDays: 0; auditLogsRetentionDays: 0;
samlSSO: true; samlSSO: false;
scim: true; scim: false;
ldap: true; ldap: false;
status: null; status: null;
trial_end: null; trial_end: null;
has_used_trial: true; has_used_trial: true;
@@ -172,7 +172,7 @@ export const samlConfigServiceFactory = ({
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
}); });
if (entryPoint) { if (entryPoint !== undefined) {
const { const {
ciphertext: encryptedEntryPoint, ciphertext: encryptedEntryPoint,
iv: entryPointIV, iv: entryPointIV,
@@ -182,18 +182,19 @@ export const samlConfigServiceFactory = ({
updateQuery.entryPointIV = entryPointIV; updateQuery.entryPointIV = entryPointIV;
updateQuery.entryPointTag = entryPointTag; updateQuery.entryPointTag = entryPointTag;
} }
if (issuer) { if (issuer !== undefined) {
const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key); const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key);
updateQuery.encryptedIssuer = encryptedIssuer; updateQuery.encryptedIssuer = encryptedIssuer;
updateQuery.issuerIV = issuerIV; updateQuery.issuerIV = issuerIV;
updateQuery.issuerTag = issuerTag; updateQuery.issuerTag = issuerTag;
} }
if (cert) { if (cert !== undefined) {
const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key); const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key);
updateQuery.encryptedCert = encryptedCert; updateQuery.encryptedCert = encryptedCert;
updateQuery.certIV = certIV; updateQuery.certIV = certIV;
updateQuery.certTag = certTag; updateQuery.certTag = certTag;
} }
const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery); const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery);
await orgDAL.updateById(orgId, { authEnforced: false, scimEnabled: false }); await orgDAL.updateById(orgId, { authEnforced: false, scimEnabled: false });
-8
View File
@@ -158,14 +158,6 @@
"documentation/platform/ldap/general" "documentation/platform/ldap/general"
] ]
}, },
{
"group": "LDAP",
"pages": [
"documentation/platform/ldap/overview",
"documentation/platform/ldap/jumpcloud",
"documentation/platform/ldap/general"
]
},
{ {
"group": "SCIM", "group": "SCIM",
"pages": [ "pages": [
@@ -21,10 +21,10 @@ import {
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const LDAPFormSchema = z.object({ const LDAPFormSchema = z.object({
url: z.string().min(1, "URL is requiredx"), url: z.string().default(""),
bindDN: z.string().min(1, "Bind DN is requiredx"), bindDN: z.string().default(""),
bindPass: z.string().min(1, "Bind Pass is required"), bindPass: z.string().default(""),
searchBase: z.string().min(1, "Search Base is required"), searchBase: z.string().default(""),
caCert: z.string().optional() caCert: z.string().optional()
}); });
@@ -122,7 +122,7 @@ export const LDAPModal = ({
reset(); reset();
}} }}
> >
<ModalContent title="Add LDAP"> <ModalContent title="Manage LDAP configuration">
<form onSubmit={handleSubmit(onSSOModalSubmit)}> <form onSubmit={handleSubmit(onSSOModalSubmit)}>
<Controller <Controller
control={control} control={control}
@@ -60,7 +60,7 @@ export const OrgGeneralAuthSection = () => {
return ( return (
<> <>
<div className="py-4"> {/* <div className="py-4">
<div className="mb-2 flex justify-between"> <div className="mb-2 flex justify-between">
<h3 className="text-md text-mineshaft-100">Allow users to send invites</h3> <h3 className="text-md text-mineshaft-100">Allow users to send invites</h3>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}> <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
@@ -75,7 +75,7 @@ export const OrgGeneralAuthSection = () => {
</OrgPermissionCan> </OrgPermissionCan>
</div> </div>
<p className="text-sm text-mineshaft-300">Allow members to invite new users to this organization</p> <p className="text-sm text-mineshaft-300">Allow members to invite new users to this organization</p>
</div> </div> */}
<div className="py-4"> <div className="py-4">
<div className="mb-2 flex justify-between"> <div className="mb-2 flex justify-between">
<h3 className="text-md text-mineshaft-100">Enforce SAML SSO</h3> <h3 className="text-md text-mineshaft-100">Enforce SAML SSO</h3>
@@ -104,24 +104,26 @@ export const OrgLDAPSection = (): JSX.Element => {
</div> </div>
<p className="text-sm text-mineshaft-300">Manage LDAP authentication configuration</p> <p className="text-sm text-mineshaft-300">Manage LDAP authentication configuration</p>
</div> </div>
<div className="py-4"> {data && (
<div className="mb-2 flex items-center justify-between"> <div className="py-4">
<h2 className="text-md text-mineshaft-100">Enable LDAP</h2> <div className="mb-2 flex items-center justify-between">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Ldap}> <h2 className="text-md text-mineshaft-100">Enable LDAP</h2>
{(isAllowed) => ( <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Ldap}>
<Switch {(isAllowed) => (
id="enable-saml-sso" <Switch
onCheckedChange={(value) => handleLDAPToggle(value)} id="enable-saml-sso"
isChecked={data ? data.isActive : false} onCheckedChange={(value) => handleLDAPToggle(value)}
isDisabled={!isAllowed} isChecked={data ? data.isActive : false}
> isDisabled={!isAllowed}
Enable >
</Switch> Enable
)} </Switch>
</OrgPermissionCan> )}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">Allow members to authenticate into Infisical with LDAP</p>
</div> </div>
<p className="text-sm text-mineshaft-300">Allow members to authenticate into Infisical with LDAP</p> )}
</div>
<LDAPModal <LDAPModal
popUp={popUp} popUp={popUp}
handlePopUpClose={handlePopUpClose} handlePopUpClose={handlePopUpClose}
@@ -1,7 +1,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { zodResolver } from "@hookform/resolvers/zod";
import * as yup from "yup"; import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import {
@@ -32,16 +32,16 @@ const ssoAuthProviders = [
{ label: "Google SAML", value: AuthProvider.GOOGLE_SAML } { label: "Google SAML", value: AuthProvider.GOOGLE_SAML }
]; ];
const schema = yup const schema = z
.object({ .object({
authProvider: yup.string().required("SSO Type is required"), authProvider: z.string().min(1, "SSO Type is required"),
entryPoint: yup.string().required("IdP entrypoint is required"), entryPoint: z.string().default(""),
issuer: yup.string().required("Issuer string is required"), issuer: z.string().default(""),
cert: yup.string().required("IdP's public signing certificate is required") cert: z.string().default("")
}) })
.required(); .required();
export type AddSSOFormData = yup.InferType<typeof schema>; export type AddSSOFormData = z.infer<typeof schema>;
type Props = { type Props = {
popUp: UsePopUpState<["addSSO"]>; popUp: UsePopUpState<["addSSO"]>;
@@ -60,7 +60,7 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
defaultValues: { defaultValues: {
authProvider: AuthProvider.OKTA_SAML authProvider: AuthProvider.OKTA_SAML
}, },
resolver: yupResolver(schema) resolver: zodResolver(schema)
}); });
useEffect(() => { useEffect(() => {
@@ -173,7 +173,7 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
reset(); reset();
}} }}
> >
<ModalContent title="Add SSO"> <ModalContent title="Manage SAML configuration">
<form onSubmit={handleSubmit(onSSOModalSubmit)}> <form onSubmit={handleSubmit(onSSOModalSubmit)}>
<Controller <Controller
control={control} control={control}
@@ -45,18 +45,21 @@ export const OrgDeleteSection = () => {
}; };
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <>
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Danger Zone</p> <hr className="border-mineshaft-600" />
<Button <div className="py-4">
isLoading={isLoading} <p className="mb-4 text-md text-mineshaft-100">Danger Zone</p>
colorSchema="danger" <Button
variant="outline_bg" isLoading={isLoading}
type="submit" colorSchema="danger"
onClick={() => handlePopUpOpen("deleteOrg")} variant="outline_bg"
isDisabled={Boolean(membership && membership.role !== "admin")} type="submit"
> onClick={() => handlePopUpOpen("deleteOrg")}
{`Delete ${currentOrg?.name}`} isDisabled={Boolean(membership && membership.role !== "admin")}
</Button> >
{`Delete ${currentOrg?.name}`}
</Button>
</div>
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteOrg.isOpen} isOpen={popUp.deleteOrg.isOpen}
title="Are you sure want to delete this organization?" title="Are you sure want to delete this organization?"
@@ -65,6 +68,6 @@ export const OrgDeleteSection = () => {
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={handleDeleteOrgSubmit} onDeleteApproved={handleDeleteOrgSubmit}
/> />
</div> </>
); );
}; };
@@ -3,15 +3,12 @@ import { useOrgPermission } from "@app/context";
import { OrgDeleteSection } from "../OrgDeleteSection"; import { OrgDeleteSection } from "../OrgDeleteSection";
import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection"; import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection";
import { OrgNameChangeSection } from "../OrgNameChangeSection"; import { OrgNameChangeSection } from "../OrgNameChangeSection";
import { OrgSlugChangeSection } from "../OrgSlugChangeSection";
export const OrgGeneralTab = () => { export const OrgGeneralTab = () => {
const { membership } = useOrgPermission(); const { membership } = useOrgPermission();
return ( return (
<div> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<OrgNameChangeSection /> <OrgNameChangeSection />
<OrgSlugChangeSection />
<OrgIncidentContactsSection /> <OrgIncidentContactsSection />
{membership && membership.role === "admin" && <OrgDeleteSection />} {membership && membership.role === "admin" && <OrgDeleteSection />}
</div> </div>
@@ -18,9 +18,10 @@ export const OrgIncidentContactsSection = () => {
const { permission } = useOrgPermission(); const { permission } = useOrgPermission();
return ( return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"> <>
<div className="flex justify-between mb-4"> <hr className="border-mineshaft-600" />
<p className="min-w-max text-xl font-semibold">{t("section.incident.incident-contacts")}</p> <div className="flex items-center justify-between pt-4">
<p className="text-md text-mineshaft-100">{t("section.incident.incident-contacts")}</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.IncidentAccount}> <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.IncidentAccount}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
@@ -35,16 +36,18 @@ export const OrgIncidentContactsSection = () => {
)} )}
</OrgPermissionCan> </OrgPermissionCan>
</div> </div>
{permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount) ? ( <div className="py-4">
<OrgIncidentContactsTable /> {permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount) ? (
) : ( <OrgIncidentContactsTable />
<PermissionDeniedBanner /> ) : (
)} <PermissionDeniedBanner />
)}
</div>
<AddOrgIncidentContactModal <AddOrgIncidentContactModal
popUp={popUp} popUp={popUp}
handlePopUpClose={handlePopUpClose} handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
</div> </>
); );
}; };
@@ -10,7 +10,12 @@ import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@a
import { useUpdateOrg } from "@app/hooks/api"; import { useUpdateOrg } from "@app/hooks/api";
const formSchema = yup.object({ const formSchema = yup.object({
name: yup.string().required().label("Project Name") name: yup.string().required().label("Organization Name"),
slug: yup
.string()
.matches(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
.required()
.label("Organization Slug")
}); });
type FormData = yup.InferType<typeof formSchema>; type FormData = yup.InferType<typeof formSchema>;
@@ -25,40 +30,44 @@ export const OrgNameChangeSection = (): JSX.Element => {
useEffect(() => { useEffect(() => {
if (currentOrg) { if (currentOrg) {
reset({ name: currentOrg.name }); reset({
name: currentOrg.name,
slug: currentOrg.slug
});
} }
}, [currentOrg]); }, [currentOrg]);
const onFormSubmit = async ({ name }: FormData) => { const onFormSubmit = async ({ name, slug }: FormData) => {
try { try {
if (!currentOrg?.id) return; if (!currentOrg?.id) return;
if (name === "") return;
await mutateAsync({ orgId: currentOrg?.id, name }); await mutateAsync({
orgId: currentOrg?.id,
name,
slug
});
createNotification({ createNotification({
text: "Successfully renamed organization", text: "Successfully updated organization details",
type: "success" type: "success"
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
createNotification({ createNotification({
text: "Failed to rename organization", text: "Failed to update organization details",
type: "error" type: "error"
}); });
} }
}; };
return ( return (
<form <form onSubmit={handleSubmit(onFormSubmit)} className="py-4">
onSubmit={handleSubmit(onFormSubmit)} <div className="">
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4" <h2 className="mb-2 text-md text-mineshaft-100">Organization Name</h2>
>
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Organization Name</p>
<div className="mb-2 max-w-md">
<Controller <Controller
defaultValue="" defaultValue=""
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}> <FormControl isError={Boolean(error)} errorText={error?.message} className="max-w-md">
<Input placeholder="Acme Corp" {...field} /> <Input placeholder="Acme Corp" {...field} />
</FormControl> </FormControl>
)} )}
@@ -66,6 +75,19 @@ export const OrgNameChangeSection = (): JSX.Element => {
name="name" name="name"
/> />
</div> </div>
<div className="py-4">
<h2 className="mb-2 text-md text-mineshaft-100">Organization Slug</h2>
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} className="max-w-md">
<Input placeholder="acme" {...field} />
</FormControl>
)}
control={control}
name="slug"
/>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}> <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
@@ -1,88 +0,0 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useUpdateOrg } from "@app/hooks/api";
const formSchema = yup.object({
slug: yup
.string()
.matches(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
.required()
.label("Project Slug")
});
type FormData = yup.InferType<typeof formSchema>;
export const OrgSlugChangeSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const { handleSubmit, control, reset } = useForm<FormData>({
resolver: yupResolver(formSchema)
});
const { mutateAsync, isLoading } = useUpdateOrg();
useEffect(() => {
if (currentOrg) {
reset({ slug: currentOrg.slug });
}
}, [currentOrg]);
const onFormSubmit = async ({ slug }: FormData) => {
try {
if (!currentOrg?.id) return;
if (slug === "") return;
await mutateAsync({ orgId: currentOrg?.id, slug });
createNotification({
text: "Successfully updated organization slug",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to update organization slug",
type: "error"
});
}
};
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Organization Slug</p>
<div className="mb-2 max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input placeholder="acme" {...field} />
</FormControl>
)}
control={control}
name="slug"
/>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Button
isLoading={isLoading}
isDisabled={!isAllowed}
colorSchema="primary"
variant="outline_bg"
type="submit"
>
Save
</Button>
)}
</OrgPermissionCan>
</form>
);
}
@@ -1 +0,0 @@
export { OrgSlugChangeSection } from "./OrgSlugChangeSection";