Merge branch 'heads/main' into overview-page-enchanced

This commit is contained in:
Rhythm Bhiwani
2024-03-12 11:11:20 +05:30
111 changed files with 2177 additions and 747 deletions
@@ -1,401 +0,0 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from "@fortawesome/free-solid-svg-icons";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Select, SelectItem } from "@app/components/v2";
import { useSubscription, useWorkspace } from "@app/context";
import updateUserProjectPermission from "@app/ee/api/memberships/UpdateUserProjectPermission";
import {
useDeleteUserFromWorkspace,
useGetUserWsKey,
useUpdateUserWorkspaceRole,
useUploadWsKey
} from "@app/hooks/api";
import { decryptAssymmetric, encryptAssymmetric } from "../../utilities/cryptography/crypto";
import guidGenerator from "../../utilities/randomId";
import Button from "../buttons/Button";
import UpgradePlanModal from "../dialog/UpgradePlan";
// const roles = ['admin', 'user'];
// TODO: Set type for this
type Props = {
userData: any[];
changeData: (users: any[]) => void;
myUser: string;
filter: string;
isUserListLoading: boolean;
};
type EnvironmentProps = {
name: string;
slug: string;
};
/**
* This is the component that shows the users of a certin project
* #TODO: add the possibility of choosing and doing operations on multiple users.
* @param {*} props
* @returns
*/
const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoading }: Props) => {
const { currentWorkspace } = useWorkspace();
const { subscription } = useSubscription();
const { data: wsKey } = useGetUserWsKey(currentWorkspace?.id ?? "");
const { mutateAsync: deleteUserFromWorkspaceMutateAsync } = useDeleteUserFromWorkspace();
const { mutateAsync: uploadWsKeyMutateAsync } = useUploadWsKey();
const { mutateAsync: updateUserWorkspaceRoleMutateAsync } = useUpdateUserWorkspaceRole();
// const [roleSelected, setRoleSelected] = useState(
// Array(userData?.length).fill(userData.map((user) => user.role))
// );
const router = useRouter();
const [myRole, setMyRole] = useState("member");
const [workspaceEnvs, setWorkspaceEnvs] = useState<EnvironmentProps[]>([]);
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
const { createNotification } = useNotificationContext();
const workspaceId = router.query.id as string;
// Delete the row in the table (e.g. a user)
// #TODO: Add a pop-up that warns you that the user is going to be deleted.
const handleDelete = async (email: string) => {
await deleteUserFromWorkspaceMutateAsync({ emails: [email], workspaceId });
};
const handleRoleUpdate = async (index: number, e: string) => {
await updateUserWorkspaceRoleMutateAsync({
workspaceId,
membershipId: userData[index].membershipId,
role: e.toLowerCase()
});
createNotification({
text: "Successfully changed user role.",
type: "success"
});
};
const handlePermissionUpdate = (
index: number,
val: string,
membershipId: string,
slug: string
) => {
let denials: { ability: string; environmentSlug: string }[];
if (val === "Read Only") {
denials = [
{
ability: "write",
environmentSlug: slug
}
];
} else if (val === "No Access") {
denials = [
{
ability: "write",
environmentSlug: slug
},
{
ability: "read",
environmentSlug: slug
}
];
} else if (val === "Add Only") {
denials = [
{
ability: "read",
environmentSlug: slug
}
];
} else {
denials = [];
}
if (subscription?.rbac === false) {
setIsUpgradeModalOpen(true);
} else {
const allDenials = userData[index].deniedPermissions
.filter(
(perm: { ability: string; environmentSlug: string }) => perm.environmentSlug !== slug
)
.concat(denials);
updateUserProjectPermission({ membershipId, denials: allDenials });
changeData([
...userData.slice(0, index),
...[
{
key: userData[index].key,
firstName: userData[index].firstName,
lastName: userData[index].lastName,
email: userData[index].email,
role: userData[index].role,
status: userData[index].status,
userId: userData[index].userId,
membershipId: userData[index].membershipId,
publicKey: userData[index].publicKey,
deniedPermissions: allDenials
}
],
...userData.slice(index + 1, userData?.length)
]);
createNotification({
text: "Successfully changed user permissions.",
type: "success"
});
}
};
useEffect(() => {
setMyRole(userData.filter((user) => user.email === myUser)[0]?.role);
(async () => {
if (currentWorkspace) {
setWorkspaceEnvs(currentWorkspace.environments);
}
})();
}, [userData, myUser, currentWorkspace]);
const grantAccess = async (id: string, publicKey: string) => {
if (wsKey) {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: wsKey.encryptedKey,
nonce: wsKey.nonce,
publicKey: wsKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key,
publicKey,
privateKey: PRIVATE_KEY
});
await uploadWsKeyMutateAsync({
workspaceId,
userId: id,
encryptedKey: ciphertext,
nonce
});
router.reload();
}
};
const closeUpgradeModal = () => {
setIsUpgradeModalOpen(false);
};
return (
<div className="table-container relative mb-6 mt-1 min-w-max rounded-md border border-mineshaft-600 bg-bunker">
<div className="absolute h-[3.1rem] w-full rounded-t-md bg-white/5" />
{subscription && (
<UpgradePlanModal
isOpen={isUpgradeModalOpen}
onClose={closeUpgradeModal}
text={
subscription.slug === null
? "You can use RBAC under an Enterprise license"
: "You can use RBAC if you switch to Infisical's Team Plan."
}
/>
)}
<table className="my-0.5 w-full">
<thead className="bg-mineshaft-800 text-xs font-light text-gray-400">
<tr>
<th className="py-3.5 pl-4 text-left">NAME</th>
<th className="py-3.5 pl-4 text-left">EMAIL</th>
<th className="py-3.5 pl-6 pr-10 text-left">ROLE</th>
{workspaceEnvs.map((env) => (
<th key={guidGenerator()} className="max-w-min break-normal py-1 pl-2 text-left">
<span>
{env.slug.toUpperCase()}
<br />
</span>
{/* <span>PERMISSION</span> */}
</th>
))}
<th aria-label="buttons" />
</tr>
</thead>
<tbody>
{!isUserListLoading &&
userData?.filter(
(user) =>
user.firstName?.toLowerCase().includes(filter) ||
user.lastName?.toLowerCase().includes(filter) ||
user.email?.toLowerCase().includes(filter)
).length > 0 &&
userData
?.filter(
(user) =>
user.firstName?.toLowerCase().includes(filter) ||
user.lastName?.toLowerCase().includes(filter) ||
user.email?.toLowerCase().includes(filter)
)
.map((row, index) => (
<tr key={guidGenerator()} className="bg-mineshaft-800 text-sm">
<td className="border-t border-mineshaft-600 py-2 pl-4 text-gray-300">
{row.firstName} {row.lastName}
</td>
<td className="border-t border-mineshaft-600 py-2 pl-4 text-gray-300">
{row.email}
</td>
<td className="border-t border-mineshaft-600 py-2 pl-6 pr-10 text-gray-300">
<div className="flex h-full flex-row items-center justify-start">
<Select
className="w-36 bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-700"
// open={isOpen}
onValueChange={(e) => handleRoleUpdate(index, e)}
value={row.role}
isDisabled={myRole !== "admin" || myUser === row.email}
// onOpenChange={(open) => setIsOpen(open)}
>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="member">Member</SelectItem>
</Select>
{row.status === "completed" && myUser !== row.email && (
<div className="rounded-md border border-mineshaft-700 bg-white/5 text-white duration-200 hover:bg-primary hover:text-black">
<Button
onButtonPressed={() => grantAccess(row.userId, row.publicKey)}
color="mineshaft"
text="Grant Access"
size="md"
/>
</div>
)}
</div>
</td>
{workspaceEnvs.map((env) => (
<td
key={guidGenerator()}
className="border-t border-mineshaft-700 py-2 pl-2 text-gray-300"
>
<Select
className="w-16 bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-800 border border-mineshaft-600 text-bunker-200"
position="item-aligned"
// open={isOpen}
onValueChange={(val) =>
handlePermissionUpdate(index, val, row.membershipId, env.slug)
}
value={
// eslint-disable-next-line no-nested-ternary
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("write") &&
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("read")
? "No Access"
: // eslint-disable-next-line no-nested-ternary
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("write") &&
!row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("read")
? "Read Only"
: !row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("write") &&
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("read")
? "Add Only"
: "Read & Write"
}
icon={
// eslint-disable-next-line no-nested-ternary
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("write") &&
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("read")
? faEyeSlash
: // eslint-disable-next-line no-nested-ternary
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("write") &&
!row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("read")
? faEye
: !row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("write") &&
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes("read")
? faPlus
: faPenToSquare
}
isDisabled={myRole !== "admin"}
// onOpenChange={(open) => setIsOpen(open)}
>
<SelectItem value="No Access" customIcon={faEyeSlash}>
No Access
</SelectItem>
<SelectItem value="Read Only" customIcon={faEye}>
Read Only
</SelectItem>
<SelectItem value="Add Only" customIcon={faPlus}>
Add Only
</SelectItem>
<SelectItem value="Read & Write" customIcon={faPenToSquare}>
Read & Write
</SelectItem>
</Select>
</td>
))}
<td className="border-0.5 flex flex-row justify-end border-t border-mineshaft-700 py-2 pl-8 pr-8">
{myUser !== row.email &&
// row.role !== "admin" &&
myRole !== "member" ? (
<div className="mt-0.5 flex items-center opacity-50 hover:opacity-100">
<Button
onButtonPressed={() => handleDelete(row.email)}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
) : (
<div className="h-9 w-9" />
)}
</td>
</tr>
))}
{isUserListLoading && (
<>
<tr
key={guidGenerator()}
className="h-14 w-full animate-pulse bg-mineshaft-800 text-sm"
/>
<tr
key={guidGenerator()}
className="h-14 w-full animate-pulse bg-mineshaft-800 text-sm"
/>
</>
)}
</tbody>
</table>
</div>
);
};
export default ProjectUsersTable;
@@ -15,6 +15,7 @@ export enum OrgPermissionSubjects {
IncidentAccount = "incident-contact",
Scim = "scim",
Sso = "sso",
Ldap = "ldap",
Billing = "billing",
SecretScanning = "secret-scanning",
Identity = "identity"
@@ -29,6 +30,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
+7
View File
@@ -14,6 +14,8 @@ import {
Login1Res,
Login2DTO,
Login2Res,
LoginLDAPDTO,
LoginLDAPRes,
ResetPasswordDTO,
SendMfaTokenDTO,
SRP1DTO,
@@ -37,6 +39,11 @@ export const login2 = async (loginDetails: Login2DTO) => {
return data;
};
export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
const { data } = await apiRequest.post<LoginLDAPRes>("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token
return data;
}
export const useLogin1 = () => {
return useMutation({
mutationFn: async (details: {
+10
View File
@@ -53,6 +53,16 @@ export type Login2Res = {
tag?: string;
}
export type LoginLDAPDTO = {
organizationSlug: string;
username: string;
password: string;
}
export type LoginLDAPRes = {
nextUrl: string;
}
export type SRP1DTO = {
clientPublicKey: string;
}
+1
View File
@@ -8,6 +8,7 @@ export * from "./incidentContacts";
export * from "./integrationAuth";
export * from "./integrations";
export * from "./keys";
export * from "./ldapConfig";
export * from "./organization";
export * from "./roles";
export * from "./scim";
@@ -0,0 +1,5 @@
export {
useCreateLDAPConfig,
useGetLDAPConfig,
useUpdateLDAPConfig
} from "./queries";
@@ -0,0 +1,103 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
const ldapConfigKeys = {
getLDAPConfig: (orgId: string) => [{ orgId }, "organization-ldap"] as const,
}
export const useGetLDAPConfig = (organizationId: string) => {
return useQuery({
queryKey: ldapConfigKeys.getLDAPConfig(organizationId),
queryFn: async () => {
const { data } = await apiRequest.get(
`/api/v1/ldap/config?organizationId=${organizationId}`
);
return data;
},
enabled: true
});
}
export const useCreateLDAPConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}: {
organizationId: string;
isActive: boolean;
url: string;
bindDN: string;
bindPass: string;
searchBase: string;
caCert?: string;
}) => {
const { data } = await apiRequest.post(
"/api/v1/ldap/config",
{
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(ldapConfigKeys.getLDAPConfig(dto.organizationId));
}
});
};
export const useUpdateLDAPConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}: {
organizationId: string;
isActive?: boolean;
url?: string;
bindDN?: string;
bindPass?: string;
searchBase?: string;
caCert?: string;
}) => {
const { data } = await apiRequest.patch(
"/api/v1/ldap/config",
{
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(ldapConfigKeys.getLDAPConfig(dto.organizationId));
}
});
};
@@ -19,6 +19,7 @@ export type SubscriptionPlan = {
environmentLimit: number;
samlSSO: boolean;
scim: boolean;
ldap: boolean;
status:
| "incomplete"
| "incomplete_expired"
+2 -2
View File
@@ -50,9 +50,9 @@ export const useAddUserToWsNonE2EE = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, AddUserToWsDTONonE2EE>({
mutationFn: async ({ projectId, emails }) => {
mutationFn: async ({ projectId, usernames }) => {
const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, {
emails
usernames
});
return data;
},
+7 -4
View File
@@ -7,13 +7,15 @@ export enum AuthMethod {
GITLAB = "gitlab",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
JUMPCLOUD_SAML = "jumpcloud-saml",
LDAP = "ldap"
}
export type User = {
createdAt: Date;
updatedAt: Date;
email: string;
username: string;
email?: string;
superAdmin: boolean;
firstName?: string;
lastName?: string;
@@ -38,7 +40,8 @@ export type UserEnc = {
export type OrgUser = {
id: string;
user: {
email: string;
username: string;
email?: string;
firstName: string;
lastName: string;
id: string;
@@ -75,7 +78,7 @@ export type AddUserToWsDTOE2EE = {
export type AddUserToWsDTONonE2EE = {
projectId: string;
emails: string[];
usernames: string[];
};
export type UpdateOrgUserRoleDTO = {
+2 -2
View File
@@ -323,11 +323,11 @@ export const useDeleteUserFromWorkspace = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ emails, workspaceId }: { workspaceId: string; emails: string[] }) => {
mutationFn: async ({ usernames, workspaceId }: { workspaceId: string; usernames: string[] }) => {
const {
data: { deletedMembership }
} = await apiRequest.delete(`/api/v2/workspace/${workspaceId}/memberships`, {
data: { emails }
data: { usernames }
});
return deletedMembership;
},
@@ -91,7 +91,7 @@ export const AdminLayout = ({ children }: LayoutProps) => {
console.error(error);
}
};
return (
<>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
@@ -121,7 +121,7 @@ export const AdminLayout = ({ children }: LayoutProps) => {
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div>
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
<Link href="/personal-settings">
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
</Link>
+5 -6
View File
@@ -231,11 +231,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
if (addMembers) {
const orgUsers = await fetchOrgUsers(currentOrg.id);
await addUsersToProject.mutateAsync({
emails: orgUsers
.map((member) => member.user.email)
.filter((email) => email !== user.email),
usernames: orgUsers
.map((member) => member.user.username)
.filter((username) => username !== user.username),
projectId: newProjectId
});
}
@@ -292,7 +291,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div>
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
{orgs?.map((org) => {
return (
<DropdownMenuItem key={org.id}>
@@ -367,7 +366,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div>
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
<Link href="/personal-settings">
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
</Link>
@@ -513,9 +513,9 @@ const OrganizationPage = withPermission(
const orgUsers = await fetchOrgUsers(currentOrg);
await addUsersToProject.mutateAsync({
emails: orgUsers
.map((member) => member.user.email)
.filter((email) => email !== user.email),
usernames: orgUsers
.map((member) => member.user.username)
.filter((username) => username !== user.username),
projectId: newProjectId
});
}
+6 -3
View File
@@ -7,9 +7,9 @@ import { getAuthToken, isLoggedIn } from "@app/reactQuery";
import {
InitialStep,
LDAPStep,
MFAStep,
SAMLSSOStep
} from "./components";
SAMLSSOStep} from "./components";
import { navigateUserToOrg } from "./Login.utils";
export const Login = () => {
@@ -72,7 +72,10 @@ export const Login = () => {
return (
<SAMLSSOStep setStep={setStep} />
);
case 3:
return (
<LDAPStep setStep={setStep} />
);
default:
return <div />;
}
+3 -3
View File
@@ -15,7 +15,7 @@ export const LoginSSO = ({ providerAuthToken }: Props) => {
const [password, setPassword] = useState("");
const {
email,
username,
isUserCompleted
} = jwt_decode(providerAuthToken) as any;
@@ -35,7 +35,7 @@ export const LoginSSO = ({ providerAuthToken }: Props) => {
return (
<PasswordStep
providerAuthToken={providerAuthToken}
email={email}
email={username}
password={password}
setPassword={setPassword}
setStep={setStep}
@@ -45,7 +45,7 @@ export const LoginSSO = ({ providerAuthToken }: Props) => {
return (
<MFAStep
providerAuthToken={providerAuthToken}
email={email}
email={username}
password={password}
/>
);
@@ -180,7 +180,20 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with SAML SSO
Continue with SAML
</Button>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setStep(3);
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with LDAP
</Button>
</div>
<div className="my-4 flex w-1/4 min-w-[20rem] flex-row items-center py-2 lg:w-1/6">
@@ -0,0 +1,137 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button, Input } from "@app/components/v2";
import { loginLDAPRedirect } from "@app/hooks/api/auth/queries";
type Props = {
setStep: (step: number) => void;
}
export const LDAPStep = ({
setStep
}: Props) => {
const { createNotification } = useNotificationContext();
const [organizationSlug, setOrganizationSlug] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const { t } = useTranslation();
// const queryParams = new URLSearchParams(window.location.search);
const handleSubmission = async (e:React.FormEvent) => {
e.preventDefault()
try {
const { nextUrl } = await loginLDAPRedirect({
organizationSlug,
username,
password
});
if (!nextUrl) {
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
type: "error"
});
return;
}
createNotification({
text: "Successfully logged in",
type: "success"
});
// redirects either to /login/sso or /signup/sso
window.open(nextUrl);
window.close();
} catch (err) {
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
type: "error"
});
}
// TODO: add callback port support
// const callbackPort = queryParams.get("callback_port");
// window.open(`/api/v1/ldap/redirect/saml2/${ssoIdentifier}${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
// window.close();
}
return (
<div className="mx-auto w-full max-w-md md:px-6">
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
What&apos;s your LDAP Login?
</p>
<form onSubmit={handleSubmission}>
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={organizationSlug}
onChange={(e) => setOrganizationSlug(e.target.value)}
type="text"
placeholder="Enter your organization slug..."
isRequired
autoComplete="email"
id="email"
className="h-12"
/>
</div>
</div>
<div className="mt-2 relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={username}
onChange={(e) => setUsername(e.target.value)}
type="text"
placeholder="Enter your LDAP username..."
isRequired
autoComplete="email"
id="email"
className="h-12"
/>
</div>
</div>
<div className="mt-2 relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your LDAP password..."
isRequired
autoComplete="current-password"
id="current-password"
className="select:-webkit-autofill:focus h-10"
/>
</div>
</div>
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[20rem] md:min-w-[22rem] text-center rounded-md mt-4'>
<Button
type="submit"
colorSchema="primary"
variant="outline_bg"
isFullWidth
className="h-14"
>
{t("login.login")}
</Button>
</div>
</form>
<div className="flex flex-row items-center justify-center mt-4">
<button
onClick={() => {
setStep(0);
}}
type="button"
className="text-bunker-300 text-sm hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
>
{t("login.other-option")}
</button>
</div>
</div>
);
};
@@ -0,0 +1 @@
export { LDAPStep } from "./LDAPStep";
@@ -1,4 +1,5 @@
export { InitialStep } from "./InitialStep";
export { LDAPStep } from "./LDAPStep";
export { MFAStep } from "./MFAStep";
export { SAMLSSOStep } from "./SAMLSSOStep";
@@ -113,8 +113,8 @@ export const OrgMembersSection = () => {
/>
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
title={`Are you sure want to remove member with email ${
(popUp?.removeMember?.data as { email: string })?.email || ""
title={`Are you sure want to remove member with username ${
(popUp?.removeMember?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
deleteKey="confirm"
@@ -41,7 +41,7 @@ type Props = {
popUpName: keyof UsePopUpState<["removeMember", "upgradePlan"]>,
data?: {
orgMembershipId?: string;
email?: string;
username?: string;
description?: string;
}
) => void;
@@ -143,6 +143,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
({ user: u, inviteEmail }) =>
u?.firstName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.lastName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.username?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.email?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
inviteEmail?.includes(searchMemberFilter.toLowerCase())
),
@@ -162,7 +163,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
<THead>
<Tr>
<Th>Name</Th>
<Th>Email</Th>
<Th>Username</Th>
<Th>Role</Th>
<Th className="w-5" />
</Tr>
@@ -174,10 +175,11 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
({ user: u, inviteEmail, role, roleId, id: orgMembershipId, status }) => {
const name = u && u.firstName ? `${u.firstName} ${u.lastName}` : "-";
const email = u?.email || inviteEmail;
const username = u?.username ?? inviteEmail ?? "-";
return (
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
<Td>{name}</Td>
<Td>{email}</Td>
<Td>{username}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
@@ -206,7 +208,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
))}
</Select>
)}
{(status === "invited" || status === "verified") &&
{(status === "invited" || status === "verified") && email &&
serverDetails?.emailConfigured && (
<Button
isDisabled={!isAllowed}
@@ -239,7 +241,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
return;
}
handlePopUpOpen("removeMember", { orgMembershipId, email });
handlePopUpOpen("removeMember", { orgMembershipId, username });
}}
size="lg"
colorSchema="danger"
@@ -82,6 +82,12 @@ const SIMPLE_PERMISSION_OPTIONS = [
icon: faSignIn,
formName: "sso"
},
{
title: "LDAP",
subtitle: "Define organization level LDAP requirements",
icon: faSignIn,
formName: "ldap"
},
{
title: "SCIM",
subtitle: "Define organization level SCIM requirements",
@@ -35,6 +35,7 @@ export const formSchema = z.object({
"secret-scanning": generalPermissionSchema,
sso: generalPermissionSchema,
scim: generalPermissionSchema,
ldap: generalPermissionSchema,
billing: generalPermissionSchema,
identity: generalPermissionSchema
})
@@ -9,10 +9,6 @@ import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import {
Button,
DeleteActionModal,
@@ -51,8 +47,7 @@ import {
useGetProjectRoles,
useGetUserWsKey,
useGetWorkspaceUsers,
useUpdateUserWorkspaceRole,
useUploadWsKey
useUpdateUserWorkspaceRole
} from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
@@ -81,7 +76,7 @@ export const MemberListTab = () => {
const { data: wsKey } = useGetUserWsKey(workspaceId);
const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId);
const { data: orgUsers } = useGetOrgUsers(orgId);
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -99,7 +94,6 @@ export const MemberListTab = () => {
const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE();
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
const { mutateAsync: uploadWsKey } = useUploadWsKey();
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole();
@@ -117,7 +111,7 @@ export const MemberListTab = () => {
const orgUser = (orgUsers || []).find(({ id }) => id === orgMembershipId);
if (!orgUser) return;
try {
try { // TODO: update
if (currentWorkspace.version === ProjectVersion.V1) {
await addUserToWorkspace({
workspaceId,
@@ -128,7 +122,7 @@ export const MemberListTab = () => {
} else if (currentWorkspace.version === ProjectVersion.V2) {
await addUserToWorkspaceNonE2EE({
projectId: workspaceId,
emails: [orgUser.user.email]
usernames: [orgUser.user.username]
});
} else {
createNotification({
@@ -154,11 +148,11 @@ export const MemberListTab = () => {
};
const handleRemoveUser = async () => {
const email = (popUp?.removeMember?.data as { email: string })?.email;
const username = (popUp?.removeMember?.data as { username: string })?.username;
if (!currentOrg?.id) return;
try {
await removeUserFromWorkspace({ workspaceId, emails: [email] });
await removeUserFromWorkspace({ workspaceId, usernames: [username] });
createNotification({
text: "Successfully removed user from project",
type: "success"
@@ -220,6 +214,7 @@ export const MemberListTab = () => {
({ user: u, inviteEmail }) =>
u?.firstName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.lastName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.username?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.email?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
inviteEmail?.includes(searchMemberFilter.toLowerCase())
),
@@ -227,49 +222,15 @@ export const MemberListTab = () => {
);
const filteredOrgUsers = useMemo(() => {
const wsUserEmails = new Map();
const wsUserUsernames = new Map();
members?.forEach((member) => {
wsUserEmails.set(member.user.email, true);
wsUserUsernames.set(member.user.username, true);
});
return (orgUsers || []).filter(
({ status, user: u }) => status === "accepted" && !wsUserEmails.has(u.email)
({ status, user: u }) => status === "accepted" && !wsUserUsernames.has(u.username)
);
}, [orgUsers, members]);
const onGrantAccess = async (grantedUserId: string, publicKey: string) => {
try {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
if (!PRIVATE_KEY || !wsKey) return;
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: wsKey.encryptedKey,
nonce: wsKey.nonce,
publicKey: wsKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key,
publicKey,
privateKey: PRIVATE_KEY
});
await uploadWsKey({
userId: grantedUserId,
nonce,
encryptedKey: ciphertext,
workspaceId: currentWorkspace?.id || ""
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to grant access to user",
type: "error"
});
}
};
const isLoading = isMembersLoading || isRolesLoading;
return (
@@ -302,7 +263,7 @@ export const MemberListTab = () => {
<THead>
<Tr>
<Th>Name</Th>
<Th>Email</Th>
<Th>Username</Th>
<Th>Role</Th>
<Th className="w-5" />
</Tr>
@@ -311,22 +272,20 @@ export const MemberListTab = () => {
{isLoading && <TableSkeleton columns={4} innerKey="project-members" />}
{!isLoading &&
filterdUsers?.map(
({ user: u, inviteEmail, id: membershipId, status, roleId, role }) => {
({ user: u, id: membershipId, roleId, role }) => {
const name = u ? `${u.firstName} ${u.lastName}` : "-";
const email = u?.email || inviteEmail;
const username = u?.username ?? "-";
return (
<Tr key={`membership-${membershipId}`} className="w-full">
<Td>{name}</Td>
<Td>{email}</Td>
<Td>{username}</Td>
<Td>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Member}
>
{(isAllowed) => (
<>
<Select
<Select
value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
isDisabled={userId === u?.id || !isAllowed}
className="w-40 bg-mineshaft-600"
@@ -345,18 +304,6 @@ export const MemberListTab = () => {
</SelectItem>
))}
</Select>
{status === "completed" && user.email !== email && (
<div className="rounded-md border border-mineshaft-700 bg-white/5 text-white duration-200 hover:bg-primary hover:text-black">
<Button
colorSchema="secondary"
isDisabled={!isAllowed}
onClick={() => onGrantAccess(u?.id, u?.publicKey)}
>
Grant Access
</Button>
</div>
)}
</>
)}
</ProjectPermissionCan>
</Td>
@@ -375,7 +322,7 @@ export const MemberListTab = () => {
className="ml-4"
isDisabled={userId === u?.id || !isAllowed}
onClick={() =>
handlePopUpOpen("removeMember", { email: u.email })
handlePopUpOpen("removeMember", { username: u.username })
}
>
<FontAwesomeIcon icon={faXmark} />
@@ -407,20 +354,20 @@ export const MemberListTab = () => {
<form onSubmit={handleSubmit(onAddMember)}>
<Controller
control={control}
defaultValue={filteredOrgUsers?.[0]?.user?.email}
defaultValue={filteredOrgUsers?.[0]?.user?.username}
name="orgMembershipId"
render={({ field, fieldState: { error } }) => (
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
<FormControl label="Username" isError={Boolean(error)} errorText={error?.message}>
<Select
position="popper"
className="w-full"
defaultValue={filteredOrgUsers?.[0]?.user?.email}
defaultValue={filteredOrgUsers?.[0]?.user?.username}
value={field.value}
onValueChange={field.onChange}
>
{filteredOrgUsers.map(({ id: orgUserId, user: u }) => (
<SelectItem value={orgUserId} key={`org-membership-join-${orgUserId}`}>
{u?.email}
{u?.username}
</SelectItem>
))}
</Select>
@@ -1,10 +1,11 @@
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { subject } from "@casl/ability";
import { faArrowDown, faArrowUp } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import NavHeader from "@app/components/navigation/NavHeader";
import { PermissionDeniedBanner } from "@app/components/permissions";
import { ContentLoader } from "@app/components/v2";
@@ -47,9 +48,10 @@ const LOADER_TEXT = [
export const SecretMainPage = () => {
const { t } = useTranslation();
const { currentWorkspace } = useWorkspace();
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
const router = useRouter();
const { permission } = useProjectPermission();
const { createNotification } = useNotificationContext();
const [isVisible, setIsVisible] = useState(false);
const [sortDir, setSortDir] = useState<SortDir>(SortDir.ASC);
@@ -75,6 +77,20 @@ export const SecretMainPage = () => {
ProjectPermissionSub.SecretRollback
);
useEffect(() => {
if (
!isWorkspaceLoading &&
!currentWorkspace?.environments.find((env) => env.slug === environment) &&
router.isReady
) {
router.push(`/project/${workspaceId}/secrets/overview`);
createNotification({
text: "No envronment found with given slug",
type: "error"
});
}
}, [isWorkspaceLoading, currentWorkspace, environment, router.isReady]);
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
// fetch secrets
@@ -0,0 +1,229 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
TextArea
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useCreateLDAPConfig,
useGetLDAPConfig,
useUpdateLDAPConfig
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const LDAPFormSchema = z.object({
url: z.string().min(1, "URL is requiredx"),
bindDN: z.string().min(1, "Bind DN is requiredx"),
bindPass: z.string().min(1, "Bind Pass is required"),
searchBase: z.string().min(1, "Search Base is required"),
caCert: z.string().optional()
});
export type TLDAPFormData = z.infer<typeof LDAPFormSchema>;
type Props = {
popUp: UsePopUpState<["addLDAP"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["addLDAP"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addLDAP"]>, state?: boolean) => void;
};
export const LDAPModal = ({
popUp,
handlePopUpClose,
handlePopUpToggle
}: Props) => {
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateLDAPConfig();
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateLDAPConfig();
const { data } = useGetLDAPConfig(currentOrg?.id?? "");
const {
control,
handleSubmit,
reset,
} = useForm<TLDAPFormData>({
resolver: zodResolver(LDAPFormSchema)
})
useEffect(() => {
if (data) {
reset({
url: data?.url ?? "",
bindDN: data?.bindDN ?? "",
bindPass: data?.bindPass ?? "",
searchBase: data?.searchBase ?? "",
caCert: data?.caCert ?? ""
});
}
}, [data]);
const onSSOModalSubmit = async ({
url,
bindDN,
bindPass,
searchBase,
caCert
}: TLDAPFormData) => {
try {
if (!currentOrg) return;
if (!data) {
await createMutateAsync({
organizationId: currentOrg.id,
isActive: false,
url,
bindDN,
bindPass,
searchBase,
caCert
});
} else {
await updateMutateAsync({
organizationId: currentOrg.id,
isActive: false,
url,
bindDN,
bindPass,
searchBase,
caCert
});
}
handlePopUpClose("addLDAP");
createNotification({
text: `Successfully ${!data ? "added" : "updated"} LDAP configuration`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${!data ? "add" : "update"} LDAP configuration`,
type: "error"
});
}
}
return (
<Modal
isOpen={popUp?.addLDAP?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addLDAP", isOpen);
reset();
}}
>
<ModalContent title="Add LDAP">
<form onSubmit={handleSubmit(onSSOModalSubmit)}>
<Controller
control={control}
name="url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="URL"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="ldaps://ldap.myorg.com:636"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="bindDN"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Bind DN"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="cn=infisical,ou=Users,dc=example,dc=com"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="bindPass"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Bind Pass"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
type="password"
placeholder="********"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="searchBase"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Search Base / User DN"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="ou=people,dc=acme,dc=com"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea
{...field}
placeholder="-----BEGIN CERTIFICATE----- ..."
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={createIsLoading || updateIsLoading}
>
{!data ? "Add" : "Update"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addLDAP")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}
@@ -2,6 +2,7 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { OrgGeneralAuthSection } from "./OrgGeneralAuthSection";
import { OrgLDAPSection } from "./OrgLDAPSection";
import { OrgScimSection } from "./OrgSCIMSection";
import { OrgSSOSection } from "./OrgSSOSection";
@@ -11,6 +12,7 @@ export const OrgAuthTab = withPermission(
<div>
<OrgGeneralAuthSection />
<OrgSSOSection />
<OrgLDAPSection />
<OrgScimSection />
</div>
);
@@ -0,0 +1,137 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
Switch,
UpgradePlanModal
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import {
useCreateLDAPConfig,
useGetLDAPConfig,
useUpdateLDAPConfig
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { LDAPModal } from "./LDAPModal";
export const OrgLDAPSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const { createNotification } = useNotificationContext();
const { data, isLoading } = useGetLDAPConfig(currentOrg?.id ?? "");
const { mutateAsync } = useUpdateLDAPConfig();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addLDAP",
"upgradePlan"
] as const);
const { mutateAsync: createMutateAsync } = useCreateLDAPConfig();
const handleLDAPToggle = async (value: boolean) => {
try {
if (!currentOrg?.id) return;
if (!subscription?.ldap) {
handlePopUpOpen("upgradePlan");
return;
}
await mutateAsync({
organizationId: currentOrg?.id,
isActive: value
});
createNotification({
text: `Successfully ${value ? "enabled" : "disabled"} LDAP`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${value ? "enable" : "disable"} LDAP`,
type: "error"
});
}
};
const addLDAPBtnClick = async () => {
try {
if (subscription?.ldap && currentOrg) {
if (!data) {
// case: LDAP is not configured
// -> initialize empty LDAP configuration
await createMutateAsync({
organizationId: currentOrg.id,
isActive: false,
url: "",
bindDN: "",
bindPass: "",
searchBase: "",
});
}
handlePopUpOpen("addLDAP");
} else {
handlePopUpOpen("upgradePlan");
}
} catch (err) {
console.error(err);
}
};
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">LDAP</h2>
{!isLoading && (
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}>
{(isAllowed) => (
<Button
onClick={addLDAPBtnClick}
colorSchema="secondary"
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Configure
</Button>
)}
</OrgPermissionCan>
)}
</div>
{data && (
<div className="mb-4">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Ldap}>
{(isAllowed) => (
<Switch
id="enable-saml-sso"
onCheckedChange={(value) => handleLDAPToggle(value)}
isChecked={data ? data.isActive : false}
isDisabled={!isAllowed}
>
Enable
</Switch>
)}
</OrgPermissionCan>
</div>
)}
<LDAPModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use LDAP authentication if you switch to Infisical's Enterprise plan."
/>
</div>
);
};
@@ -66,7 +66,7 @@ export const OrgScimSection = () => {
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-8 flex items-center">
<h2 className="flex-1 text-xl font-semibold text-white">SCIM Configuration</h2>
<h2 className="flex-1 text-xl font-semibold text-white">SCIM</h2>
<OrgPermissionCan I={OrgPermissionActions.Read} a={OrgPermissionSubjects.Scim}>
{(isAllowed) => (
<Button
@@ -75,7 +75,7 @@ export const OrgScimSection = () => {
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Manage SCIM Tokens
Configure
</Button>
)}
</OrgPermissionCan>
@@ -94,7 +94,7 @@ export const OrgScimSection = () => {
isChecked={currentOrg?.scimEnabled ?? false}
isDisabled={!isAllowed}
>
Enable SCIM Provisioning
Enable
</Switch>
)}
</OrgPermissionCan>
@@ -1,6 +1,5 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
@@ -16,13 +15,6 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { SSOModal } from "./SSOModal";
const ssoAuthProviderMap: { [key: string]: string } = {
"okta-saml": "Okta SAML",
"azure-saml": "Azure SAML",
"jumpcloud-saml": "JumpCloud SAML",
"google-saml": "Google SAML"
};
export const OrgSSOSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
@@ -39,6 +31,11 @@ export const OrgSSOSection = (): JSX.Element => {
const handleSamlSSOToggle = async (value: boolean) => {
try {
if (!currentOrg?.id) return;
if (!subscription?.samlSSO) {
handlePopUpOpen("upgradePlan");
return;
}
await mutateAsync({
organizationId: currentOrg?.id,
@@ -86,7 +83,7 @@ export const OrgSSOSection = (): JSX.Element => {
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-8 flex items-center">
<h2 className="flex-1 text-xl font-semibold text-white">SAML SSO Configuration</h2>
<h2 className="flex-1 text-xl font-semibold text-white">SAML</h2>
{!isLoading && (
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
@@ -96,13 +93,13 @@ export const OrgSSOSection = (): JSX.Element => {
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
{data ? "Update SAML SSO" : "Set up SAML SSO"}
Configure
</Button>
)}
</OrgPermissionCan>
)}
</div>
{data && (
{/* {data && ( */}
<div className="mb-4">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
@@ -112,36 +109,11 @@ export const OrgSSOSection = (): JSX.Element => {
isChecked={data ? data.isActive : false}
isDisabled={!isAllowed}
>
Enable SAML SSO
Enable
</Switch>
)}
</OrgPermissionCan>
</div>
)}
<div className="mb-4">
<h3 className="text-sm text-mineshaft-400">SSO identifier</h3>
<p className="text-md text-gray-400">{data && data.id !== "" ? data.id : "-"}</p>
</div>
<div className="mb-4">
<h3 className="text-sm text-mineshaft-400">Type</h3>
<p className="text-md text-gray-400">
{data && data.authProvider !== "" ? ssoAuthProviderMap[data.authProvider] : "-"}
</p>
</div>
<div className="mb-4">
<h3 className="text-sm text-mineshaft-400">Entrypoint</h3>
<p className="text-md text-gray-400">
{data && data.entryPoint !== "" ? data.entryPoint : "-"}
</p>
</div>
<div className="mb-4">
<h3 className="text-sm text-mineshaft-400">Issuer</h3>
<p className="text-md text-gray-400">{data && data.issuer !== "" ? data.issuer : "-"}</p>
</div>
<div className="mb-4">
<h3 className="text-sm text-mineshaft-400">Last Logged In</h3>
<p className="text-md text-gray-400">{data?.lastUsed ? format(new Date(data?.lastUsed), "yyyy-MM-dd HH:mm:ss") : "-"}</p>
</div>
<SSOModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
@@ -48,8 +48,6 @@ export const ChangePasswordSection = () => {
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
try {
if (!user?.email) return;
const errorCheck = await checkPassword({
password: newPassword,
setErrors
@@ -59,7 +57,7 @@ export const ChangePasswordSection = () => {
setIsLoading(true);
await attemptChangePassword({
email: user.email,
email: user.username,
currentPassword: oldPassword,
newPassword
});
@@ -1,12 +1,22 @@
import {
useGetUser
} from "@app/hooks/api";
import { AuthMethod } from "@app/hooks/api/users/types";
import { AuthMethodSection } from "../AuthMethodSection";
import { ChangePasswordSection } from "../ChangePasswordSection";
import { MFASection } from "../SecuritySection";
export const PersonalAuthTab = () => {
const { data: user } = useGetUser();
return (
<div>
<MFASection />
<AuthMethodSection />
{user && !user.authMethods.includes(AuthMethod.LDAP) && (
<>
<MFASection />
<AuthMethodSection />
</>
)}
<ChangePasswordSection />
</div>
);
@@ -4,6 +4,7 @@ import {
useGetUser,
useUpdateMfaEnabled} from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { AuthMethod } from "@app/hooks/api/users/types";
import { usePopUp } from "@app/hooks/usePopUp";
export const MFASection = () => {
@@ -18,6 +19,15 @@ export const MFASection = () => {
const toggleMfa = async (state: boolean) => {
try {
if (!user) return;
if (user.authMethods.includes(AuthMethod.LDAP)) {
createNotification({
text: "Two-factor authentication is not available for LDAP users.",
type: "error"
});
return;
}
const newUser = await mutateAsync({
isMfaEnabled: state
});
+3 -3
View File
@@ -16,7 +16,7 @@ export const SignupSSO = ({
const [password, setPassword] = useState("");
const {
email,
username,
organizationName,
firstName,
lastName,
@@ -27,7 +27,7 @@ export const SignupSSO = ({
case 0:
return (
<UserInfoSSOStep
email={email}
username={username}
name={`${firstName} ${lastName}`}
providerOrganizationName={organizationName}
password={password}
@@ -39,7 +39,7 @@ export const SignupSSO = ({
case 1:
return (
<BackupPDFStep
email={email}
email={username}
password={password}
name={`${firstName} ${lastName}`}
/>
@@ -24,7 +24,7 @@ const client = new jsrp.client();
type Props = {
setStep: (step: number) => void;
email: string;
username: string;
password: string;
setPassword: (value: string) => void;
name: string;
@@ -57,7 +57,7 @@ type Errors = {
* @param {string} obj.setLastName - function managing the state of user's last name
*/
export const UserInfoSSOStep = ({
email,
username,
name,
providerOrganizationName,
password,
@@ -113,7 +113,7 @@ export const UserInfoSSOStep = ({
client.init(
{
username: email,
username,
password
},
async () => {
@@ -156,7 +156,7 @@ export const UserInfoSSOStep = ({
});
const response = await completeAccountSignup({
email,
email: username,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(" "),
protectedKey,