Merge pull request #774 from Infisical/saml-sso-edge-cases

Block inviting members to organization if SAML SSO is configured
This commit is contained in:
BlackMagiq
2023-07-23 13:27:30 +07:00
committed by GitHub
5 changed files with 77 additions and 20 deletions

View File

@@ -1,6 +1,7 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { MembershipOrg, Organization, User } from "../../models"; import { MembershipOrg, Organization, User } from "../../models";
import { SSOConfig } from "../../ee/models";
import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg"; import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg";
import { createToken } from "../../helpers/auth"; import { createToken } from "../../helpers/auth";
import { updateSubscriptionOrgQuantity } from "../../helpers/organization"; import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
@@ -110,6 +111,18 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
} }
const plan = await EELicenseService.getPlan(organizationId); const plan = await EELicenseService.getPlan(organizationId);
const ssoConfig = await SSOConfig.findOne({
organization: new Types.ObjectId(organizationId)
});
if (ssoConfig && ssoConfig.isActive) {
// case: SAML SSO is enabled for the organization
return res.status(400).send({
message:
"Failed to invite member due to SAML SSO configured for organization"
});
}
if (plan.memberLimit !== null) { if (plan.memberLimit !== null) {
// case: limit imposed on number of members allowed // case: limit imposed on number of members allowed

View File

@@ -10,6 +10,7 @@ import { getSSOConfigHelper } from "../../helpers/organizations";
import { client } from "../../../config"; import { client } from "../../../config";
import { ResourceNotFoundError } from "../../../utils/errors"; import { ResourceNotFoundError } from "../../../utils/errors";
import { getSiteURL } from "../../../config"; import { getSiteURL } from "../../../config";
import { EELicenseService } from "../../services";
/** /**
* Redirect user to appropriate SSO endpoint after successful authentication * Redirect user to appropriate SSO endpoint after successful authentication
@@ -58,6 +59,12 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
cert, cert,
audience audience
} = req.body; } = req.body;
const plan = await EELicenseService.getPlan(organizationId);
if (!plan.samlSSO) return res.status(400).send({
message: "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
});
interface PatchUpdate { interface PatchUpdate {
authProvider?: string; authProvider?: string;
@@ -203,6 +210,12 @@ export const createSSOConfig = async (req: Request, res: Response) => {
cert, cert,
audience audience
} = req.body; } = req.body;
const plan = await EELicenseService.getPlan(organizationId);
if (!plan.samlSSO) return res.status(400).send({
message: "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration."
});
const key = await BotOrgService.getSymmetricKey( const key = await BotOrgService.getSymmetricKey(
new Types.ObjectId(organizationId) new Types.ObjectId(organizationId)

View File

@@ -183,7 +183,9 @@ export default function Users() {
<div className="ml-2 flex min-w-max flex-row items-start justify-start"> <div className="ml-2 flex min-w-max flex-row items-start justify-start">
<Button <Button
text={String(t("section.members.add-member"))} text={String(t("section.members.add-member"))}
onButtonPressed={openAddModal} onButtonPressed={() => {
openAddModal();
}}
color="mineshaft" color="mineshaft"
size="md" size="md"
icon={faPlus} icon={faPlus}

View File

@@ -6,6 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
@@ -26,9 +27,11 @@ import {
Th, Th,
THead, THead,
Tr, Tr,
UpgradePlanModal} from "@app/components/v2"; UpgradePlanModal
import { useWorkspace } from "@app/context"; } from "@app/components/v2";
import { useOrganization , useWorkspace } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { useGetSSOConfig } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { OrgUser, Workspace } from "@app/hooks/api/types"; import { OrgUser, Workspace } from "@app/hooks/api/types";
@@ -69,6 +72,9 @@ export const OrgMembersTable = ({
setCompleteInviteLink setCompleteInviteLink
}: Props) => { }: Props) => {
const router = useRouter(); const router = useRouter();
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(currentOrg?._id ?? "");
const [searchMemberFilter, setSearchMemberFilter] = useState(""); const [searchMemberFilter, setSearchMemberFilter] = useState("");
const {data: serverDetails } = useFetchServerStatus() const {data: serverDetails } = useFetchServerStatus()
const { workspaces } = useWorkspace(); const { workspaces } = useWorkspace();
@@ -79,7 +85,7 @@ export const OrgMembersTable = ({
"upgradePlan", "upgradePlan",
"setUpEmail" "setUpEmail"
] as const); ] as const);
useEffect(() => { useEffect(() => {
if (router.query.action === "invite") { if (router.query.action === "invite") {
handlePopUpOpen("addMember"); handlePopUpOpen("addMember");
@@ -152,6 +158,15 @@ export const OrgMembersTable = ({
<Button <Button
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => { onClick={() => {
if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
createNotification({
text: "You cannot invite users when SAML SSO is configured for your organization",
type: "error"
});
return;
}
if (isMoreUserNotAllowed) { if (isMoreUserNotAllowed) {
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
} else { } else {

View File

@@ -69,13 +69,13 @@ export const UserInfoSSOStep = ({
const [nameError, setNameError] = useState(false); const [nameError, setNameError] = useState(false);
const [organizationName, setOrganizationName] = useState(""); const [organizationName, setOrganizationName] = useState("");
const [organizationNameError, setOrganizationNameError] = useState(false); const [organizationNameError, setOrganizationNameError] = useState(false);
const [attributionSource, setAttributionSource] = useState("");
const [errors, setErrors] = useState<Errors>({}); const [errors, setErrors] = useState<Errors>({});
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
console.log("providerOrganizationName: ", providerOrganizationName); if (providerOrganizationName !== undefined) {
if (providerOrganizationName) {
setOrganizationName(providerOrganizationName); setOrganizationName(providerOrganizationName);
} }
}, []); }, []);
@@ -171,7 +171,8 @@ export const UserInfoSSOStep = ({
providerAuthToken, providerAuthToken,
salt: result.salt, salt: result.salt,
verifier: result.verifier, verifier: result.verifier,
organizationName organizationName,
attributionSource
}); });
// unset signup JWT token and set JWT token // unset signup JWT token and set JWT token
@@ -209,7 +210,7 @@ export const UserInfoSSOStep = ({
setIsLoading(false); setIsLoading(false);
} }
}; };
return ( return (
<div className="h-full mx-auto mb-36 w-max rounded-xl md:px-8 md:mb-16"> <div className="h-full mx-auto mb-36 w-max rounded-xl md:px-8 md:mb-16">
<p className="mx-8 mb-6 flex justify-center text-xl font-bold text-medium md:mx-16 text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200"> <p className="mx-8 mb-6 flex justify-center text-xl font-bold text-medium md:mx-16 text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
@@ -228,18 +229,31 @@ export const UserInfoSSOStep = ({
/> />
{nameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your name</p>} {nameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your name</p>}
</div> </div>
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg"> {providerOrganizationName === undefined && (
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Organization Name</p> <div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
<Input <p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Organization Name</p>
placeholder="Infisical" <Input
value={organizationName} placeholder="Infisical"
onChange={(e) => setOrganizationName(e.target.value)} value={organizationName}
isRequired onChange={(e) => setOrganizationName(e.target.value)}
className="h-12" isRequired
disabled className="h-12"
/> disabled
{organizationNameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your organization name</p>} />
</div> {organizationNameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your organization name</p>}
</div>
)}
{providerOrganizationName === undefined && (
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Where did you hear about us? <span className="font-light">(optional)</span></p>
<Input
placeholder=""
onChange={(e) => setAttributionSource(e.target.value)}
value={attributionSource}
className="h-12"
/>
</div>
)}
<div className="mt-2 flex lg:w-1/6 w-1/4 min-w-[20rem] max-h-60 w-full flex-col items-center justify-center rounded-lg py-2"> <div className="mt-2 flex lg:w-1/6 w-1/4 min-w-[20rem] max-h-60 w-full flex-col items-center justify-center rounded-lg py-2">
<InputField <InputField
label={t("section.password.password")} label={t("section.password.password")}