Compare commits

...

5 Commits

Author SHA1 Message Date
Daniel Hougaard
d587e779f5 requested changes 2025-08-16 00:26:06 +04:00
Daniel Hougaard
09db98db50 fix: typescript complaining 2025-08-14 06:58:45 +04:00
Daniel Hougaard
a37f1eb1f8 requested changes & frontend lint 2025-08-14 06:53:57 +04:00
Daniel Hougaard
2113abcfdc Update license-fns.ts 2025-08-14 06:15:25 +04:00
Daniel Hougaard
ea2707651c feat(sso): enforce google SSO on org-level 2025-08-14 06:13:24 +04:00
25 changed files with 439 additions and 284 deletions

View File

@@ -148,6 +148,7 @@ declare module "fastify" {
interface Session {
callbackPort: string;
isAdminLogin: boolean;
orgSlug?: string;
}
interface FastifyRequest {

View File

@@ -0,0 +1,39 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
const GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME = "googleSsoAuthEnforced";
const GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME = "googleSsoAuthLastUsed";
export async function up(knex: Knex): Promise<void> {
const hasGoogleSsoAuthEnforcedColumn = await knex.schema.hasColumn(
TableName.Organization,
GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME
);
const hasGoogleSsoAuthLastUsedColumn = await knex.schema.hasColumn(
TableName.Organization,
GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME
);
await knex.schema.alterTable(TableName.Organization, (table) => {
if (!hasGoogleSsoAuthEnforcedColumn)
table.boolean(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME).defaultTo(false).notNullable();
if (!hasGoogleSsoAuthLastUsedColumn) table.timestamp(GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME).nullable();
});
}
export async function down(knex: Knex): Promise<void> {
const hasGoogleSsoAuthEnforcedColumn = await knex.schema.hasColumn(
TableName.Organization,
GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME
);
const hasGoogleSsoAuthLastUsedColumn = await knex.schema.hasColumn(
TableName.Organization,
GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME
);
await knex.schema.alterTable(TableName.Organization, (table) => {
if (hasGoogleSsoAuthEnforcedColumn) table.dropColumn(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME);
if (hasGoogleSsoAuthLastUsedColumn) table.dropColumn(GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME);
});
}

View File

@@ -36,7 +36,9 @@ export const OrganizationsSchema = z.object({
scannerProductEnabled: z.boolean().default(true).nullable().optional(),
shareSecretsProductEnabled: z.boolean().default(true).nullable().optional(),
maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
maxSharedSecretViewLimit: z.number().nullable().optional()
maxSharedSecretViewLimit: z.number().nullable().optional(),
googleSsoAuthEnforced: z.boolean().default(false),
googleSsoAuthLastUsed: z.date().nullable().optional()
});
export type TOrganizations = z.infer<typeof OrganizationsSchema>;

View File

@@ -32,6 +32,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
auditLogStreams: false,
auditLogStreamLimit: 3,
samlSSO: false,
enforceGoogleSSO: false,
hsm: false,
oidcSSO: false,
scim: false,

View File

@@ -47,6 +47,7 @@ export type TFeatureSet = {
auditLogStreamLimit: 3;
githubOrgSync: false;
samlSSO: false;
enforceGoogleSSO: false;
hsm: false;
oidcSSO: false;
secretAccessInsights: false;

View File

@@ -35,6 +35,7 @@ export interface TPermissionDALFactory {
projectFavorites?: string[] | null | undefined;
customRoleSlug?: string | null | undefined;
orgAuthEnforced?: boolean | null | undefined;
orgGoogleSsoAuthEnforced: boolean;
} & {
groups: {
id: string;
@@ -87,6 +88,7 @@ export interface TPermissionDALFactory {
}[];
orgId: string;
orgAuthEnforced: boolean | null | undefined;
orgGoogleSsoAuthEnforced: boolean;
orgRole: OrgMembershipRole;
userId: string;
projectId: string;
@@ -350,6 +352,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
db.ref("permissions").withSchema(TableName.OrgRoles),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"),
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
db.ref("groupId").withSchema("userGroups"),
db.ref("groupOrgId").withSchema("userGroups"),
@@ -369,6 +372,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
OrgMembershipsSchema.extend({
permissions: z.unknown(),
orgAuthEnforced: z.boolean().optional().nullable(),
orgGoogleSsoAuthEnforced: z.boolean(),
bypassOrgAuthEnabled: z.boolean(),
customRoleSlug: z.string().optional().nullable(),
shouldUseNewPrivilegeSystem: z.boolean()
@@ -988,6 +992,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"),
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"),
db.ref("orgId").withSchema(TableName.Project),
@@ -1003,6 +1008,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
orgId,
username,
orgAuthEnforced,
orgGoogleSsoAuthEnforced,
orgRole,
membershipId,
groupMembershipId,
@@ -1016,6 +1022,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
}) => ({
orgId,
orgAuthEnforced,
orgGoogleSsoAuthEnforced,
orgRole: orgRole as OrgMembershipRole,
userId,
projectId,

View File

@@ -121,6 +121,7 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
function validateOrgSSO(
actorAuthMethod: ActorAuthMethod,
isOrgSsoEnforced: TOrganizations["authEnforced"],
isOrgGoogleSsoEnforced: TOrganizations["googleSsoAuthEnforced"],
isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"],
orgRole: OrgMembershipRole
) {
@@ -128,10 +129,16 @@ function validateOrgSSO(
throw new UnauthorizedError({ name: "No auth method defined" });
}
if (isOrgSsoEnforced && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) {
if ((isOrgSsoEnforced || isOrgGoogleSsoEnforced) && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) {
return;
}
// case: google sso is enforced, but the actor is not using google sso
if (isOrgGoogleSsoEnforced && actorAuthMethod !== null && actorAuthMethod !== AuthMethod.GOOGLE) {
throw new ForbiddenRequestError({ name: "Org auth enforced. Cannot access org-scoped resource" });
}
// case: SAML SSO is enforced, but the actor is not using SAML SSO
if (
isOrgSsoEnforced &&
actorAuthMethod !== null &&

View File

@@ -146,6 +146,7 @@ export const permissionServiceFactory = ({
validateOrgSSO(
authMethod,
membership.orgAuthEnforced,
membership.orgGoogleSsoAuthEnforced,
membership.bypassOrgAuthEnabled,
membership.role as OrgMembershipRole
);
@@ -238,6 +239,7 @@ export const permissionServiceFactory = ({
validateOrgSSO(
authMethod,
userProjectPermission.orgAuthEnforced,
userProjectPermission.orgGoogleSsoAuthEnforced,
userProjectPermission.bypassOrgAuthEnabled,
userProjectPermission.orgRole
);

View File

@@ -279,6 +279,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
name: GenericResourceNameSchema.optional(),
slug: slugSchema({ max: 64 }).optional(),
authEnforced: z.boolean().optional(),
googleSsoAuthEnforced: z.boolean().optional(),
scimEnabled: z.boolean().optional(),
defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
enforceMfa: z.boolean().optional(),

View File

@@ -54,6 +54,8 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
try {
// @ts-expect-error this is because this is express type and not fastify
const callbackPort = req.session.get("callbackPort");
// @ts-expect-error this is because this is express type and not fastify
const orgSlug = req.session.get("orgSlug");
const email = profile?.emails?.[0]?.value;
if (!email)
@@ -67,7 +69,8 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
firstName: profile?.name?.givenName || "",
lastName: profile?.name?.familyName || "",
authMethod: AuthMethod.GOOGLE,
callbackPort
callbackPort,
orgSlug
});
cb(null, { isUserCompleted, providerAuthToken });
} catch (error) {
@@ -215,6 +218,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
schema: {
querystring: z.object({
callback_port: z.string().optional(),
org_slug: z.string().optional(),
is_admin_login: z
.string()
.optional()
@@ -223,12 +227,15 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
},
preValidation: [
async (req, res) => {
const { callback_port: callbackPort, is_admin_login: isAdminLogin } = req.query;
const { callback_port: callbackPort, is_admin_login: isAdminLogin, org_slug: orgSlug } = req.query;
// ensure fresh session state per login attempt
await req.session.regenerate();
if (callbackPort) {
req.session.set("callbackPort", callbackPort);
}
if (orgSlug) {
req.session.set("orgSlug", orgSlug);
}
if (isAdminLogin) {
req.session.set("isAdminLogin", isAdminLogin);
}

View File

@@ -448,15 +448,34 @@ export const authLoginServiceFactory = ({
// Check if the user actually has access to the specified organization.
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId && org.userStatus !== "invited");
const selectedOrgMembership = userOrgs.find((org) => org.id === organizationId && org.userStatus !== "invited");
const selectedOrg = await orgDAL.findById(organizationId);
if (!hasOrganizationMembership) {
if (!selectedOrgMembership) {
throw new ForbiddenRequestError({
message: `User does not have access to the organization named ${selectedOrg?.name}`
});
}
if (selectedOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) {
const canBypass = selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === OrgMembershipRole.Admin;
if (!canBypass) {
throw new ForbiddenRequestError({
message: "Google SSO is enforced for this organization. Please use Google SSO to login.",
error: "GoogleSsoEnforced"
});
}
}
if (decodedToken.authMethod === AuthMethod.GOOGLE) {
await orgDAL.updateById(selectedOrg.id, {
googleSsoAuthLastUsed: new Date()
});
}
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
@@ -502,7 +521,8 @@ export const authLoginServiceFactory = ({
selectedOrg.authEnforced &&
selectedOrg.bypassOrgAuthEnabled &&
!isAuthMethodSaml(decodedToken.authMethod) &&
decodedToken.authMethod !== AuthMethod.OIDC
decodedToken.authMethod !== AuthMethod.OIDC &&
decodedToken.authMethod !== AuthMethod.GOOGLE
) {
await auditLogService.createAuditLog({
orgId: organizationId,
@@ -705,7 +725,7 @@ export const authLoginServiceFactory = ({
/*
* OAuth2 login for google,github, and other oauth2 provider
* */
const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort }: TOauthLoginDTO) => {
const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort, orgSlug }: TOauthLoginDTO) => {
// akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserByUsername(email);
let user = usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
@@ -759,6 +779,8 @@ export const authLoginServiceFactory = ({
const appCfg = getConfig();
let orgId = "";
let orgName: undefined | string;
if (!user) {
// Create a new user based on oAuth
if (!serverCfg?.allowSignUp) throw new BadRequestError({ message: "Sign up disabled", name: "Oauth 2 login" });
@@ -784,7 +806,6 @@ export const authLoginServiceFactory = ({
});
if (authMethod === AuthMethod.GITHUB && serverCfg.defaultAuthOrgId && !appCfg.isCloud) {
let orgId = "";
const defaultOrg = await orgDAL.findOrgById(serverCfg.defaultAuthOrgId);
if (!defaultOrg) {
throw new BadRequestError({
@@ -824,11 +845,39 @@ export const authLoginServiceFactory = ({
}
}
if (!orgId && orgSlug) {
const org = await orgDAL.findOrgBySlug(orgSlug);
if (org) {
// checks for the membership and only sets the orgId / orgName if the user is a member of the specified org
const orgMembership = await orgDAL.findMembership({
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
[`${TableName.OrgMembership}.orgId` as "orgId"]: org.id,
[`${TableName.OrgMembership}.isActive` as "isActive"]: true,
[`${TableName.OrgMembership}.status` as "status"]: OrgMembershipStatus.Accepted
});
if (orgMembership) {
orgId = org.id;
orgName = org.name;
}
}
}
const isUserCompleted = user.isAccepted;
const providerAuthToken = crypto.jwt().sign(
{
authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id,
...(orgId && orgSlug && orgName !== undefined
? {
organizationId: orgId,
organizationName: orgName,
organizationSlug: orgSlug
}
: {}),
username: user.username,
email: user.email,
isEmailVerified: user.isEmailVerified,

View File

@@ -32,6 +32,7 @@ export type TOauthLoginDTO = {
lastName?: string;
authMethod: AuthMethod;
callbackPort?: string;
orgSlug?: string;
};
export type TOauthTokenExchangeDTO = {

View File

@@ -8,6 +8,7 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
createdAt: true,
updatedAt: true,
authEnforced: true,
googleSsoAuthEnforced: true,
scimEnabled: true,
kmsDefaultKeyId: true,
defaultMembershipRole: true,

View File

@@ -355,6 +355,7 @@ export const orgServiceFactory = ({
name,
slug,
authEnforced,
googleSsoAuthEnforced,
scimEnabled,
defaultMembershipRoleSlug,
enforceMfa,
@@ -421,6 +422,21 @@ export const orgServiceFactory = ({
}
}
if (googleSsoAuthEnforced !== undefined) {
if (!plan.enforceGoogleSSO) {
throw new BadRequestError({
message: "Failed to enforce Google SSO due to plan restriction. Upgrade plan to enforce Google SSO."
});
}
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
}
if (authEnforced && googleSsoAuthEnforced) {
throw new BadRequestError({
message: "SAML/OIDC auth enforcement and Google SSO auth enforcement cannot be enabled at the same time."
});
}
if (authEnforced) {
const samlCfg = await samlConfigDAL.findOne({
orgId,
@@ -451,6 +467,21 @@ export const orgServiceFactory = ({
}
}
if (googleSsoAuthEnforced) {
if (googleSsoAuthEnforced && currentOrg.authEnforced) {
throw new BadRequestError({
message: "Google SSO auth enforcement cannot be enabled when SAML/OIDC auth enforcement is enabled."
});
}
if (!currentOrg.googleSsoAuthLastUsed) {
throw new BadRequestError({
message:
"Google SSO auth enforcement cannot be enabled because Google SSO has not been used yet. Please log in via Google SSO at least once before enforcing it for your organization."
});
}
}
let defaultMembershipRole: string | undefined;
if (defaultMembershipRoleSlug) {
defaultMembershipRole = await getDefaultOrgMembershipRoleForUpdateOrg({
@@ -465,6 +496,7 @@ export const orgServiceFactory = ({
name,
slug: slug ? slugify(slug) : undefined,
authEnforced,
googleSsoAuthEnforced,
scimEnabled,
defaultMembershipRole,
enforceMfa,

View File

@@ -74,6 +74,7 @@ export type TUpdateOrgDTO = {
name: string;
slug: string;
authEnforced: boolean;
googleSsoAuthEnforced: boolean;
scimEnabled: boolean;
defaultMembershipRoleSlug: string;
enforceMfa: boolean;

View File

@@ -104,6 +104,7 @@ export const useUpdateOrg = () => {
mutationFn: ({
name,
authEnforced,
googleSsoAuthEnforced,
scimEnabled,
slug,
orgId,
@@ -125,6 +126,7 @@ export const useUpdateOrg = () => {
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
name,
authEnforced,
googleSsoAuthEnforced,
scimEnabled,
slug,
defaultMembershipRoleSlug,

View File

@@ -9,6 +9,7 @@ export type Organization = {
createAt: string;
updatedAt: string;
authEnforced: boolean;
googleSsoAuthEnforced: boolean;
bypassOrgAuthEnabled: boolean;
orgAuthMethod: string;
scimEnabled: boolean;
@@ -34,6 +35,7 @@ export type UpdateOrgDTO = {
orgId: string;
name?: string;
authEnforced?: boolean;
googleSsoAuthEnforced?: boolean;
scimEnabled?: boolean;
slug?: string;
defaultMembershipRoleSlug?: string;

View File

@@ -48,6 +48,7 @@ export type SubscriptionPlan = {
externalKms: boolean;
pkiEst: boolean;
enforceMfa: boolean;
enforceGoogleSSO: boolean;
projectTemplates: boolean;
kmip: boolean;
secretScanning: boolean;

View File

@@ -240,6 +240,13 @@ export const Navbar = () => {
return;
}
if (org.googleSsoAuthEnforced) {
await logout.mutateAsync();
window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`);
window.close();
return;
}
handleOrgChange(org?.id);
}}
variant="plain"

View File

@@ -82,25 +82,40 @@ export const SelectOrganizationSection = () => {
}
}
if (organization.authEnforced && !canBypassOrgAuth) {
if ((organization.authEnforced || organization.googleSsoAuthEnforced) && !canBypassOrgAuth) {
const authToken = jwtDecode(getAuthToken()) as { authMethod: AuthMethod };
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
let url = "";
if (organization.orgAuthMethod === AuthMethod.OIDC) {
url = `/api/v1/sso/oidc/login?orgSlug=${organization.slug}${
callbackPort ? `&callbackPort=${callbackPort}` : ""
}`;
} else {
} else if (organization.orgAuthMethod === AuthMethod.SAML) {
url = `/api/v1/sso/redirect/saml2/organizations/${organization.slug}`;
if (callbackPort) {
url += `?callback_port=${callbackPort}`;
}
} else if (
organization.googleSsoAuthEnforced &&
authToken.authMethod !== AuthMethod.GOOGLE
) {
url = `/api/v1/sso/redirect/google?org_slug=${organization.slug}`;
if (callbackPort) {
url += `&callback_port=${callbackPort}`;
}
}
window.location.href = url;
return;
// we are conditionally checking if the url is set because it may not be set if google SSO is enforced, but the user is already logged in with google SSO
// see line 103-106
if (url) {
await logout.mutateAsync();
window.location.href = url;
return;
}
}
const { token, isMfaEnabled, mfaMethod } = await selectOrg

View File

@@ -13,8 +13,23 @@ import {
} from "@app/context";
import { useLogoutUser, useUpdateOrg } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { twMerge } from "tailwind-merge";
export const OrgGeneralAuthSection = () => {
enum EnforceAuthType {
SAML = "saml",
GOOGLE = "google",
OIDC = "oidc"
}
export const OrgGeneralAuthSection = ({
isSamlConfigured,
isOidcConfigured,
isGoogleConfigured
}: {
isSamlConfigured: boolean;
isOidcConfigured: boolean;
isGoogleConfigured: boolean;
}) => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
@@ -23,27 +38,61 @@ export const OrgGeneralAuthSection = () => {
const logout = useLogoutUser();
const handleEnforceOrgAuthToggle = async (value: boolean) => {
const handleEnforceOrgAuthToggle = async (value: boolean, type: EnforceAuthType) => {
try {
if (!currentOrg?.id) return;
if (!subscription?.samlSSO) {
handlePopUpOpen("upgradePlan");
return;
if (type === EnforceAuthType.SAML) {
if (!subscription?.samlSSO) {
handlePopUpOpen("upgradePlan");
return;
}
await mutateAsync({
orgId: currentOrg?.id,
authEnforced: value
});
} else if (type === EnforceAuthType.GOOGLE) {
if (!subscription?.enforceGoogleSSO) {
handlePopUpOpen("upgradePlan");
return;
}
await mutateAsync({
orgId: currentOrg?.id,
googleSsoAuthEnforced: value
});
} else if (type === EnforceAuthType.OIDC) {
if (!subscription?.oidcSSO) {
handlePopUpOpen("upgradePlan");
return;
}
await mutateAsync({
orgId: currentOrg?.id,
authEnforced: value
});
} else {
createNotification({
text: `Invalid auth enforcement type ${type}`,
type: "error"
});
}
await mutateAsync({
orgId: currentOrg?.id,
authEnforced: value
});
createNotification({
text: `Successfully ${value ? "enforced" : "un-enforced"} org-level auth`,
text: `Successfully ${value ? "enabled" : "disabled"} org-level auth`,
type: "success"
});
if (value) {
await logout.mutateAsync();
window.open(`/api/v1/sso/redirect/saml2/organizations/${currentOrg.slug}`);
if (type === EnforceAuthType.SAML) {
window.open(`/api/v1/sso/redirect/saml2/organizations/${currentOrg.slug}`);
} else if (type === EnforceAuthType.GOOGLE) {
window.open(`/api/v1/sso/redirect/google?org_slug=${currentOrg.slug}`);
}
window.close();
}
} catch (err) {
@@ -78,45 +127,91 @@ export const OrgGeneralAuthSection = () => {
};
return (
<>
{/* <div className="py-4">
<div className="mb-2 flex justify-between">
<h3 className="text-md text-mineshaft-100">Allow users to send invites</h3>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="allow-org-invites"
onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)}
isChecked={currentOrg?.authEnforced ?? false}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">Allow members to invite new users to this organization</p>
</div> */}
<div className="py-4">
<div className="mb-2 flex justify-between">
<div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enforce SAML SSO</span>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enforce-org-auth"
onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)}
isChecked={currentOrg?.authEnforced ?? false}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Enforce users to authenticate via SAML to access this organization
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div>
<p className="text-xl font-semibold text-gray-200">SSO Enforcement</p>
<p className="mb-2 mt-1 text-gray-400">
Manage strict enforcement of specific authentication methods for your organization.
</p>
</div>
{currentOrg?.authEnforced && (
<div className="py-4">
<div className="flex flex-col gap-2 py-4">
<div className={twMerge("mt-4", !isSamlConfigured && "hidden")}>
<div className="mb-2 flex justify-between">
<div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enforce SAML SSO</span>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enforce-saml-auth"
onCheckedChange={(value) =>
handleEnforceOrgAuthToggle(value, EnforceAuthType.SAML)
}
isChecked={currentOrg?.authEnforced ?? false}
isDisabled={!isAllowed || currentOrg?.googleSsoAuthEnforced}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Enforce users to authenticate via SAML to access this organization.
<br />
When this is enabled your organization members will only be able to login with SAML.
</p>
</div>
<div className={twMerge("mt-4", !isOidcConfigured && "hidden")}>
<div className="mb-2 flex justify-between">
<div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enforce OIDC SSO</span>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enforce-oidc-auth"
isChecked={currentOrg?.authEnforced ?? false}
onCheckedChange={(value) =>
handleEnforceOrgAuthToggle(value, EnforceAuthType.OIDC)
}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Enforce users to authenticate via OIDC to access this organization.
<br />
When this is enabled your organization members will only be able to login with OIDC.
</p>
</div>
<div className={twMerge("mt-2", !isGoogleConfigured && "hidden")}>
<div className="mb-2 flex justify-between">
<div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enforce Google SSO</span>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enforce-google-sso"
onCheckedChange={(value) =>
handleEnforceOrgAuthToggle(value, EnforceAuthType.GOOGLE)
}
isChecked={currentOrg?.googleSsoAuthEnforced ?? false}
isDisabled={!isAllowed || currentOrg?.authEnforced}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Enforce users to authenticate via Google to access this organization.
<br />
When this is enabled your organization members will only be able to login with Google.
</p>
</div>
</div>
{(currentOrg?.authEnforced || currentOrg?.googleSsoAuthEnforced) && (
<div className="mt-4 py-4">
<div className="mb-2 flex justify-between">
<div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enable Admin SSO Bypass</span>
@@ -125,8 +220,8 @@ export const OrgGeneralAuthSection = () => {
content={
<div>
<span>
When this is enabled, we strongly recommend enforcing MFA at the organization
level.
When enabling admin SSO bypass, we highly recommend enabling MFA enforcement
at the organization-level for security reasons.
</span>
<p className="mt-4">
In case of a lockout, admins can use the{" "}
@@ -182,6 +277,6 @@ export const OrgGeneralAuthSection = () => {
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can enforce SAML SSO if you switch to Infisical's Pro plan."
/>
</>
</div>
);
};

View File

@@ -95,43 +95,25 @@ export const OrgLDAPSection = (): JSX.Element => {
};
return (
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div className="mb-4">
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">LDAP</h2>
<div className="flex">
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}>
{(isAllowed) => (
<Button onClick={addLDAPBtnClick} colorSchema="secondary" isDisabled={!isAllowed}>
Manage
</Button>
)}
</OrgPermissionCan>
<div className="mb-4 flex items-center justify-between">
<div>
<p className="text-xl font-semibold text-gray-200">LDAP</p>
<p className="mb-2 text-gray-400">Manage LDAP authentication configuration</p>
</div>
</div>
<p className="text-sm text-mineshaft-300">Manage LDAP authentication configuration</p>
</div>
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">LDAP Group Mappings</h2>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}>
{(isAllowed) => (
<Button
onClick={openLDAPGroupMapModal}
colorSchema="secondary"
isDisabled={!isAllowed}
>
<Button onClick={addLDAPBtnClick} colorSchema="secondary" isDisabled={!isAllowed}>
Manage
</Button>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Manage how LDAP groups are mapped to internal groups in Infisical
</p>
</div>
{data && (
<div className="py-4">
<div className="pt-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">Enable LDAP</h2>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Ldap}>
@@ -152,6 +134,27 @@ export const OrgLDAPSection = (): JSX.Element => {
</p>
</div>
)}
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">LDAP Group Mappings</h2>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}>
{(isAllowed) => (
<Button
onClick={openLDAPGroupMapModal}
colorSchema="secondary"
isDisabled={!isAllowed}
>
Configure
</Button>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Manage how LDAP groups are mapped to internal groups in Infisical
</p>
</div>
<LDAPModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}

View File

@@ -11,7 +11,7 @@ import {
useOrganization,
useSubscription
} from "@app/context";
import { useGetOIDCConfig, useLogoutUser, useUpdateOrg } from "@app/hooks/api";
import { useGetOIDCConfig } from "@app/hooks/api";
import { useUpdateOIDCConfig } from "@app/hooks/api/oidcConfig/mutations";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -23,9 +23,7 @@ export const OrgOIDCSection = (): JSX.Element => {
const { data, isPending } = useGetOIDCConfig(currentOrg?.id ?? "");
const { mutateAsync } = useUpdateOIDCConfig();
const { mutateAsync: updateOrg } = useUpdateOrg();
const logout = useLogoutUser();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addOIDC",
"upgradePlan"
@@ -54,56 +52,6 @@ export const OrgOIDCSection = (): JSX.Element => {
}
};
const handleEnforceOrgAuthToggle = async (value: boolean) => {
try {
if (!currentOrg?.id) return;
if (!subscription?.oidcSSO) {
handlePopUpOpen("upgradePlan");
return;
}
await updateOrg({
orgId: currentOrg?.id,
authEnforced: value
});
createNotification({
text: `Successfully ${value ? "enforced" : "un-enforced"} org-level auth`,
type: "success"
});
if (value) {
await logout.mutateAsync();
window.open(`/api/v1/sso/oidc/login?orgSlug=${currentOrg.slug}`);
window.close();
}
} catch (err) {
console.error(err);
}
};
const handleEnableBypassOrgAuthToggle = async (value: boolean) => {
try {
if (!currentOrg?.id) return;
if (!subscription?.oidcSSO) {
handlePopUpOpen("upgradePlan");
return;
}
await updateOrg({
orgId: currentOrg?.id,
bypassOrgAuthEnabled: value
});
createNotification({
text: `Successfully ${value ? "enabled" : "disabled"} admin bypassing of org-level auth`,
type: "success"
});
} catch (err) {
console.error(err);
}
};
const handleOIDCGroupManagement = async (value: boolean) => {
try {
if (!currentOrg?.id) return;
@@ -136,25 +84,22 @@ export const OrgOIDCSection = (): JSX.Element => {
};
return (
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">OIDC</h2>
{!isPending && (
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Button
onClick={addOidcButtonClick}
colorSchema="secondary"
isDisabled={!isAllowed}
>
Manage
</Button>
)}
</OrgPermissionCan>
)}
<div className="mb-4 rounded-lg border-mineshaft-600 bg-mineshaft-900">
<div className="mb-4 flex items-center justify-between">
<div>
<p className="text-xl font-semibold text-gray-200">OIDC</p>
<p className="mb-2 text-gray-400">Manage OIDC authentication configuration</p>
</div>
<p className="text-sm text-mineshaft-300">Manage OIDC authentication configuration</p>
{!isPending && (
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Button onClick={addOidcButtonClick} colorSchema="secondary" isDisabled={!isAllowed}>
Manage
</Button>
)}
</OrgPermissionCan>
)}
</div>
{data && (
<div className="py-4">
@@ -178,88 +123,6 @@ export const OrgOIDCSection = (): JSX.Element => {
</p>
</div>
)}
<div className="py-4">
<div className="mb-2 flex justify-between">
<div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enforce OIDC SSO</span>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enforce-org-auth"
isChecked={currentOrg?.authEnforced ?? false}
onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
<span>Enforce users to authenticate via OIDC to access this organization.</span>
</p>
</div>
{currentOrg?.authEnforced && (
<div className="py-4">
<div className="mb-2 flex justify-between">
<div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enable Admin SSO Bypass</span>
<Tooltip
className="max-w-lg"
content={
<div>
<span>
When this is enabled, we strongly recommend enforcing MFA at the organization
level.
</span>
<p className="mt-4">
In case of a lockout, admins can use the{" "}
<a
target="_blank"
className="underline underline-offset-2 hover:text-mineshaft-300"
href="https://infisical.com/docs/documentation/platform/sso/overview#admin-login-portal"
rel="noreferrer"
>
Admin Login Portal
</a>{" "}
at{" "}
<a
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 hover:text-mineshaft-300"
href={`${window.location.origin}/login/admin`}
>
{window.location.origin}/login/admin
</a>
</p>
</div>
}
>
<FontAwesomeIcon
icon={faInfoCircle}
size="sm"
className="mt-0.5 inline-block text-mineshaft-400"
/>
</Tooltip>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="allow-admin-bypass"
isChecked={currentOrg?.bypassOrgAuthEnabled ?? false}
onCheckedChange={(value) => handleEnableBypassOrgAuthToggle(value)}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
<span>
Allow organization admins to bypass OIDC enforcement when SSO is unavailable,
misconfigured, or inaccessible.
</span>
</p>
</div>
)}
<div className="py-4">
<div className="mb-2 flex justify-between">
<div className="text-md flex items-center text-mineshaft-100">

View File

@@ -79,25 +79,24 @@ export const OrgSSOSection = (): JSX.Element => {
};
return (
<>
<hr className="border-mineshaft-600" />
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">SAML</h2>
{!isPending && (
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Button onClick={addSSOBtnClick} colorSchema="secondary" isDisabled={!isAllowed}>
Manage
</Button>
)}
</OrgPermissionCan>
)}
<div className="space-y-4">
<div className="mb-4 flex items-center justify-between">
<div>
<p className="text-xl font-semibold text-gray-200">SAML</p>
<p className="mb-2 text-gray-400">Manage SAML authentication configuration</p>
</div>
<p className="text-sm text-mineshaft-300">Manage SAML authentication configuration</p>
{!isPending && (
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Button onClick={addSSOBtnClick} colorSchema="secondary" isDisabled={!isAllowed}>
Manage
</Button>
)}
</OrgPermissionCan>
)}
</div>
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<div>
<div className="mb-2 flex items-center justify-between pt-4">
<h2 className="text-md text-mineshaft-100">Enable SAML</h2>
{!isPending && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
@@ -126,6 +125,6 @@ export const OrgSSOSection = (): JSX.Element => {
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use SAML SSO if you switch to Infisical's Pro plan."
/>
</>
</div>
);
};

View File

@@ -49,13 +49,19 @@ export const OrgSsoTab = withPermission(
);
const areConfigsLoading = isLoadingOidcConfig || isLoadingSamlConfig || isLoadingLdapConfig;
const shouldDisplaySection = (method: LoginMethod) =>
!enabledLoginMethods || enabledLoginMethods.includes(method);
const shouldDisplaySection = (method: LoginMethod[] | LoginMethod) => {
if (Array.isArray(method)) {
return method.some((m) => !enabledLoginMethods || enabledLoginMethods.includes(m));
}
const isOidcConfigured = oidcConfig && (oidcConfig.discoveryURL || oidcConfig.issuer);
return !enabledLoginMethods || enabledLoginMethods.includes(method);
};
const isOidcConfigured = Boolean(oidcConfig && (oidcConfig.discoveryURL || oidcConfig.issuer));
const isSamlConfigured =
samlConfig && (samlConfig.entryPoint || samlConfig.issuer || samlConfig.cert);
const isLdapConfigured = ldapConfig && ldapConfig.url;
const isGoogleConfigured = shouldDisplaySection(LoginMethod.GOOGLE);
const shouldShowCreateIdentityProviderView =
!isOidcConfigured && !isSamlConfigured && !isLdapConfigured;
@@ -65,11 +71,14 @@ export const OrgSsoTab = withPermission(
shouldDisplaySection(LoginMethod.OIDC) ||
shouldDisplaySection(LoginMethod.LDAP) ? (
<>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<p className="text-xl font-semibold text-gray-200">Connect an Identity Provider</p>
<p className="mb-2 mt-1 text-gray-400">
Connect your identity provider to simplify user management
</p>
<div className="mb-4 space-y-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div>
<p className="text-xl font-semibold text-gray-200">Connect an Identity Provider</p>
<p className="mb-2 mt-1 text-gray-400">
Connect your identity provider to simplify user management with options like SAML,
OIDC, and LDAP.
</p>
</div>
{shouldDisplaySection(LoginMethod.SAML) && (
<div
className={twMerge(
@@ -169,20 +178,27 @@ export const OrgSsoTab = withPermission(
return (
<>
{shouldShowCreateIdentityProviderView ? (
createIdentityProviderView
) : (
<>
{isSamlConfigured && shouldDisplaySection(LoginMethod.SAML) && (
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<OrgGeneralAuthSection />
<OrgSSOSection />
<div className="space-y-4">
{shouldDisplaySection([LoginMethod.SAML, LoginMethod.GOOGLE]) && (
<OrgGeneralAuthSection
isSamlConfigured={isSamlConfigured}
isOidcConfigured={isOidcConfigured}
isGoogleConfigured={isGoogleConfigured}
/>
)}
{shouldShowCreateIdentityProviderView ? (
createIdentityProviderView
) : (
<div className="mb-4 space-y-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div>
{isSamlConfigured && shouldDisplaySection(LoginMethod.SAML) && <OrgSSOSection />}
{isOidcConfigured && shouldDisplaySection(LoginMethod.OIDC) && <OrgOIDCSection />}
{isLdapConfigured && shouldDisplaySection(LoginMethod.LDAP) && <OrgLDAPSection />}
</div>
)}
{isOidcConfigured && shouldDisplaySection(LoginMethod.OIDC) && <OrgOIDCSection />}
{isLdapConfigured && shouldDisplaySection(LoginMethod.LDAP) && <OrgLDAPSection />}
</>
)}
</div>
)}
</div>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}