mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 14:26:38 +00:00
feat(sso): enforce google SSO on org-level
This commit is contained in:
Vendored
+1
@@ -148,6 +148,7 @@ declare module "fastify" {
|
|||||||
interface Session {
|
interface Session {
|
||||||
callbackPort: string;
|
callbackPort: string;
|
||||||
isAdminLogin: boolean;
|
isAdminLogin: boolean;
|
||||||
|
orgSlug: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FastifyRequest {
|
interface FastifyRequest {
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
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);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -36,7 +36,9 @@ export const OrganizationsSchema = z.object({
|
|||||||
scannerProductEnabled: z.boolean().default(true).nullable().optional(),
|
scannerProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||||
shareSecretsProductEnabled: z.boolean().default(true).nullable().optional(),
|
shareSecretsProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||||
maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
|
maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
|
||||||
maxSharedSecretViewLimit: z.number().nullable().optional()
|
maxSharedSecretViewLimit: z.number().nullable().optional(),
|
||||||
|
googleSsoAuthEnforced: z.boolean().default(false).nullable().optional(),
|
||||||
|
googleSsoAuthLastUsed: z.date().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export type TFeatureSet = {
|
|||||||
auditLogStreamLimit: 3;
|
auditLogStreamLimit: 3;
|
||||||
githubOrgSync: false;
|
githubOrgSync: false;
|
||||||
samlSSO: false;
|
samlSSO: false;
|
||||||
|
enforceGoogleSSO: false;
|
||||||
hsm: false;
|
hsm: false;
|
||||||
oidcSSO: false;
|
oidcSSO: false;
|
||||||
secretAccessInsights: false;
|
secretAccessInsights: false;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export interface TPermissionDALFactory {
|
|||||||
projectFavorites?: string[] | null | undefined;
|
projectFavorites?: string[] | null | undefined;
|
||||||
customRoleSlug?: string | null | undefined;
|
customRoleSlug?: string | null | undefined;
|
||||||
orgAuthEnforced?: boolean | null | undefined;
|
orgAuthEnforced?: boolean | null | undefined;
|
||||||
|
orgGoogleSsoAuthEnforced?: boolean | null | undefined;
|
||||||
} & {
|
} & {
|
||||||
groups: {
|
groups: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -87,6 +88,7 @@ export interface TPermissionDALFactory {
|
|||||||
}[];
|
}[];
|
||||||
orgId: string;
|
orgId: string;
|
||||||
orgAuthEnforced: boolean | null | undefined;
|
orgAuthEnforced: boolean | null | undefined;
|
||||||
|
orgGoogleSsoAuthEnforced: boolean | null | undefined;
|
||||||
orgRole: OrgMembershipRole;
|
orgRole: OrgMembershipRole;
|
||||||
userId: string;
|
userId: string;
|
||||||
projectId: 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("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
|
||||||
db.ref("permissions").withSchema(TableName.OrgRoles),
|
db.ref("permissions").withSchema(TableName.OrgRoles),
|
||||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
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("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
|
||||||
db.ref("groupId").withSchema("userGroups"),
|
db.ref("groupId").withSchema("userGroups"),
|
||||||
db.ref("groupOrgId").withSchema("userGroups"),
|
db.ref("groupOrgId").withSchema("userGroups"),
|
||||||
@@ -369,6 +372,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
|||||||
OrgMembershipsSchema.extend({
|
OrgMembershipsSchema.extend({
|
||||||
permissions: z.unknown(),
|
permissions: z.unknown(),
|
||||||
orgAuthEnforced: z.boolean().optional().nullable(),
|
orgAuthEnforced: z.boolean().optional().nullable(),
|
||||||
|
orgGoogleSsoAuthEnforced: z.boolean().optional().nullable(),
|
||||||
bypassOrgAuthEnabled: z.boolean(),
|
bypassOrgAuthEnabled: z.boolean(),
|
||||||
customRoleSlug: z.string().optional().nullable(),
|
customRoleSlug: z.string().optional().nullable(),
|
||||||
shouldUseNewPrivilegeSystem: z.boolean()
|
shouldUseNewPrivilegeSystem: z.boolean()
|
||||||
@@ -988,6 +992,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
|||||||
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
|
||||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
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("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
|
||||||
db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"),
|
db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"),
|
||||||
db.ref("orgId").withSchema(TableName.Project),
|
db.ref("orgId").withSchema(TableName.Project),
|
||||||
@@ -1003,6 +1008,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
|||||||
orgId,
|
orgId,
|
||||||
username,
|
username,
|
||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
|
orgGoogleSsoAuthEnforced,
|
||||||
orgRole,
|
orgRole,
|
||||||
membershipId,
|
membershipId,
|
||||||
groupMembershipId,
|
groupMembershipId,
|
||||||
@@ -1016,6 +1022,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
|||||||
}) => ({
|
}) => ({
|
||||||
orgId,
|
orgId,
|
||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
|
orgGoogleSsoAuthEnforced,
|
||||||
orgRole: orgRole as OrgMembershipRole,
|
orgRole: orgRole as OrgMembershipRole,
|
||||||
userId,
|
userId,
|
||||||
projectId,
|
projectId,
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
|||||||
function validateOrgSSO(
|
function validateOrgSSO(
|
||||||
actorAuthMethod: ActorAuthMethod,
|
actorAuthMethod: ActorAuthMethod,
|
||||||
isOrgSsoEnforced: TOrganizations["authEnforced"],
|
isOrgSsoEnforced: TOrganizations["authEnforced"],
|
||||||
|
isOrgGoogleSsoEnforced: TOrganizations["googleSsoAuthEnforced"],
|
||||||
isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"],
|
isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"],
|
||||||
orgRole: OrgMembershipRole
|
orgRole: OrgMembershipRole
|
||||||
) {
|
) {
|
||||||
@@ -128,10 +129,16 @@ function validateOrgSSO(
|
|||||||
throw new UnauthorizedError({ name: "No auth method defined" });
|
throw new UnauthorizedError({ name: "No auth method defined" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOrgSsoEnforced && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) {
|
if ((isOrgSsoEnforced || isOrgGoogleSsoEnforced) && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) {
|
||||||
return;
|
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 (
|
if (
|
||||||
isOrgSsoEnforced &&
|
isOrgSsoEnforced &&
|
||||||
actorAuthMethod !== null &&
|
actorAuthMethod !== null &&
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ export const permissionServiceFactory = ({
|
|||||||
validateOrgSSO(
|
validateOrgSSO(
|
||||||
authMethod,
|
authMethod,
|
||||||
membership.orgAuthEnforced,
|
membership.orgAuthEnforced,
|
||||||
|
membership.orgGoogleSsoAuthEnforced,
|
||||||
membership.bypassOrgAuthEnabled,
|
membership.bypassOrgAuthEnabled,
|
||||||
membership.role as OrgMembershipRole
|
membership.role as OrgMembershipRole
|
||||||
);
|
);
|
||||||
@@ -238,6 +239,7 @@ export const permissionServiceFactory = ({
|
|||||||
validateOrgSSO(
|
validateOrgSSO(
|
||||||
authMethod,
|
authMethod,
|
||||||
userProjectPermission.orgAuthEnforced,
|
userProjectPermission.orgAuthEnforced,
|
||||||
|
userProjectPermission.orgGoogleSsoAuthEnforced,
|
||||||
userProjectPermission.bypassOrgAuthEnabled,
|
userProjectPermission.bypassOrgAuthEnabled,
|
||||||
userProjectPermission.orgRole
|
userProjectPermission.orgRole
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -279,6 +279,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
name: GenericResourceNameSchema.optional(),
|
name: GenericResourceNameSchema.optional(),
|
||||||
slug: slugSchema({ max: 64 }).optional(),
|
slug: slugSchema({ max: 64 }).optional(),
|
||||||
authEnforced: z.boolean().optional(),
|
authEnforced: z.boolean().optional(),
|
||||||
|
googleSsoAuthEnforced: z.boolean().optional(),
|
||||||
scimEnabled: z.boolean().optional(),
|
scimEnabled: z.boolean().optional(),
|
||||||
defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
|
defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
|
||||||
enforceMfa: z.boolean().optional(),
|
enforceMfa: z.boolean().optional(),
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
try {
|
try {
|
||||||
// @ts-expect-error this is because this is express type and not fastify
|
// @ts-expect-error this is because this is express type and not fastify
|
||||||
const callbackPort = req.session.get("callbackPort");
|
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;
|
const email = profile?.emails?.[0]?.value;
|
||||||
if (!email)
|
if (!email)
|
||||||
@@ -67,7 +69,8 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
firstName: profile?.name?.givenName || "",
|
firstName: profile?.name?.givenName || "",
|
||||||
lastName: profile?.name?.familyName || "",
|
lastName: profile?.name?.familyName || "",
|
||||||
authMethod: AuthMethod.GOOGLE,
|
authMethod: AuthMethod.GOOGLE,
|
||||||
callbackPort
|
callbackPort,
|
||||||
|
orgSlug
|
||||||
});
|
});
|
||||||
cb(null, { isUserCompleted, providerAuthToken });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -215,6 +218,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
callback_port: z.string().optional(),
|
callback_port: z.string().optional(),
|
||||||
|
org_slug: z.string().optional(),
|
||||||
is_admin_login: z
|
is_admin_login: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -223,12 +227,15 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
preValidation: [
|
preValidation: [
|
||||||
async (req, res) => {
|
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
|
// ensure fresh session state per login attempt
|
||||||
await req.session.regenerate();
|
await req.session.regenerate();
|
||||||
if (callbackPort) {
|
if (callbackPort) {
|
||||||
req.session.set("callbackPort", callbackPort);
|
req.session.set("callbackPort", callbackPort);
|
||||||
}
|
}
|
||||||
|
if (orgSlug) {
|
||||||
|
req.session.set("orgSlug", orgSlug);
|
||||||
|
}
|
||||||
if (isAdminLogin) {
|
if (isAdminLogin) {
|
||||||
req.session.set("isAdminLogin", isAdminLogin);
|
req.session.set("isAdminLogin", isAdminLogin);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -448,15 +448,34 @@ export const authLoginServiceFactory = ({
|
|||||||
|
|
||||||
// Check if the user actually has access to the specified organization.
|
// Check if the user actually has access to the specified organization.
|
||||||
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
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);
|
const selectedOrg = await orgDAL.findById(organizationId);
|
||||||
|
|
||||||
if (!hasOrganizationMembership) {
|
if (!selectedOrgMembership) {
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: `User does not have access to the organization named ${selectedOrg?.name}`
|
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 shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
|
||||||
const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||||
const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||||
@@ -502,7 +521,8 @@ export const authLoginServiceFactory = ({
|
|||||||
selectedOrg.authEnforced &&
|
selectedOrg.authEnforced &&
|
||||||
selectedOrg.bypassOrgAuthEnabled &&
|
selectedOrg.bypassOrgAuthEnabled &&
|
||||||
!isAuthMethodSaml(decodedToken.authMethod) &&
|
!isAuthMethodSaml(decodedToken.authMethod) &&
|
||||||
decodedToken.authMethod !== AuthMethod.OIDC
|
decodedToken.authMethod !== AuthMethod.OIDC &&
|
||||||
|
decodedToken.authMethod !== AuthMethod.GOOGLE
|
||||||
) {
|
) {
|
||||||
await auditLogService.createAuditLog({
|
await auditLogService.createAuditLog({
|
||||||
orgId: organizationId,
|
orgId: organizationId,
|
||||||
@@ -705,7 +725,7 @@ export const authLoginServiceFactory = ({
|
|||||||
/*
|
/*
|
||||||
* OAuth2 login for google,github, and other oauth2 provider
|
* 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
|
// akhilmhdh: case sensitive email resolution
|
||||||
const usersByUsername = await userDAL.findUserByUsername(email);
|
const usersByUsername = await userDAL.findUserByUsername(email);
|
||||||
let user = usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
|
let user = usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
|
||||||
@@ -759,6 +779,8 @@ export const authLoginServiceFactory = ({
|
|||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
let orgId = "";
|
||||||
|
let orgName: undefined | string;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// Create a new user based on oAuth
|
// Create a new user based on oAuth
|
||||||
if (!serverCfg?.allowSignUp) throw new BadRequestError({ message: "Sign up disabled", name: "Oauth 2 login" });
|
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) {
|
if (authMethod === AuthMethod.GITHUB && serverCfg.defaultAuthOrgId && !appCfg.isCloud) {
|
||||||
let orgId = "";
|
|
||||||
const defaultOrg = await orgDAL.findOrgById(serverCfg.defaultAuthOrgId);
|
const defaultOrg = await orgDAL.findOrgById(serverCfg.defaultAuthOrgId);
|
||||||
if (!defaultOrg) {
|
if (!defaultOrg) {
|
||||||
throw new BadRequestError({
|
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 isUserCompleted = user.isAccepted;
|
||||||
const providerAuthToken = crypto.jwt().sign(
|
const providerAuthToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|
||||||
|
...(orgId && orgSlug && orgName !== undefined
|
||||||
|
? {
|
||||||
|
organizationId: orgId,
|
||||||
|
organizationName: orgName,
|
||||||
|
organizationSlug: orgSlug
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
isEmailVerified: user.isEmailVerified,
|
isEmailVerified: user.isEmailVerified,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export type TOauthLoginDTO = {
|
|||||||
lastName?: string;
|
lastName?: string;
|
||||||
authMethod: AuthMethod;
|
authMethod: AuthMethod;
|
||||||
callbackPort?: string;
|
callbackPort?: string;
|
||||||
|
orgSlug?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOauthTokenExchangeDTO = {
|
export type TOauthTokenExchangeDTO = {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
|||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
authEnforced: true,
|
authEnforced: true,
|
||||||
|
googleSsoAuthEnforced: true,
|
||||||
scimEnabled: true,
|
scimEnabled: true,
|
||||||
kmsDefaultKeyId: true,
|
kmsDefaultKeyId: true,
|
||||||
defaultMembershipRole: true,
|
defaultMembershipRole: true,
|
||||||
|
|||||||
@@ -355,6 +355,7 @@ export const orgServiceFactory = ({
|
|||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
authEnforced,
|
authEnforced,
|
||||||
|
googleSsoAuthEnforced,
|
||||||
scimEnabled,
|
scimEnabled,
|
||||||
defaultMembershipRoleSlug,
|
defaultMembershipRoleSlug,
|
||||||
enforceMfa,
|
enforceMfa,
|
||||||
@@ -421,6 +422,15 @@ 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) {
|
if (authEnforced) {
|
||||||
const samlCfg = await samlConfigDAL.findOne({
|
const samlCfg = await samlConfigDAL.findOne({
|
||||||
orgId,
|
orgId,
|
||||||
@@ -451,6 +461,33 @@ export const orgServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (googleSsoAuthEnforced || authEnforced) {
|
||||||
|
if (googleSsoAuthEnforced && authEnforced) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Google SSO and SAML/OIDC auth enforcement cannot be enabled at the same time."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (googleSsoAuthEnforced && currentOrg.authEnforced) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Google SSO auth enforcement cannot be enabled when SAML/OIDC auth enforcement is enabled."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authEnforced && currentOrg.googleSsoAuthEnforced) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "SAML/OIDC auth enforcement cannot be enabled when Google SSO 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;
|
let defaultMembershipRole: string | undefined;
|
||||||
if (defaultMembershipRoleSlug) {
|
if (defaultMembershipRoleSlug) {
|
||||||
defaultMembershipRole = await getDefaultOrgMembershipRoleForUpdateOrg({
|
defaultMembershipRole = await getDefaultOrgMembershipRoleForUpdateOrg({
|
||||||
@@ -465,6 +502,7 @@ export const orgServiceFactory = ({
|
|||||||
name,
|
name,
|
||||||
slug: slug ? slugify(slug) : undefined,
|
slug: slug ? slugify(slug) : undefined,
|
||||||
authEnforced,
|
authEnforced,
|
||||||
|
googleSsoAuthEnforced,
|
||||||
scimEnabled,
|
scimEnabled,
|
||||||
defaultMembershipRole,
|
defaultMembershipRole,
|
||||||
enforceMfa,
|
enforceMfa,
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export type TUpdateOrgDTO = {
|
|||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
authEnforced: boolean;
|
authEnforced: boolean;
|
||||||
|
googleSsoAuthEnforced: boolean;
|
||||||
scimEnabled: boolean;
|
scimEnabled: boolean;
|
||||||
defaultMembershipRoleSlug: string;
|
defaultMembershipRoleSlug: string;
|
||||||
enforceMfa: boolean;
|
enforceMfa: boolean;
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export const useUpdateOrg = () => {
|
|||||||
mutationFn: ({
|
mutationFn: ({
|
||||||
name,
|
name,
|
||||||
authEnforced,
|
authEnforced,
|
||||||
|
googleSsoAuthEnforced,
|
||||||
scimEnabled,
|
scimEnabled,
|
||||||
slug,
|
slug,
|
||||||
orgId,
|
orgId,
|
||||||
@@ -125,6 +126,7 @@ export const useUpdateOrg = () => {
|
|||||||
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
||||||
name,
|
name,
|
||||||
authEnforced,
|
authEnforced,
|
||||||
|
googleSsoAuthEnforced,
|
||||||
scimEnabled,
|
scimEnabled,
|
||||||
slug,
|
slug,
|
||||||
defaultMembershipRoleSlug,
|
defaultMembershipRoleSlug,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type Organization = {
|
|||||||
createAt: string;
|
createAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
authEnforced: boolean;
|
authEnforced: boolean;
|
||||||
|
googleSsoAuthEnforced: boolean;
|
||||||
bypassOrgAuthEnabled: boolean;
|
bypassOrgAuthEnabled: boolean;
|
||||||
orgAuthMethod: string;
|
orgAuthMethod: string;
|
||||||
scimEnabled: boolean;
|
scimEnabled: boolean;
|
||||||
@@ -34,6 +35,7 @@ export type UpdateOrgDTO = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
authEnforced?: boolean;
|
authEnforced?: boolean;
|
||||||
|
googleSsoAuthEnforced?: boolean;
|
||||||
scimEnabled?: boolean;
|
scimEnabled?: boolean;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
defaultMembershipRoleSlug?: string;
|
defaultMembershipRoleSlug?: string;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export type SubscriptionPlan = {
|
|||||||
externalKms: boolean;
|
externalKms: boolean;
|
||||||
pkiEst: boolean;
|
pkiEst: boolean;
|
||||||
enforceMfa: boolean;
|
enforceMfa: boolean;
|
||||||
|
enforceGoogleSSO: boolean;
|
||||||
projectTemplates: boolean;
|
projectTemplates: boolean;
|
||||||
kmip: boolean;
|
kmip: boolean;
|
||||||
secretScanning: boolean;
|
secretScanning: boolean;
|
||||||
|
|||||||
@@ -238,6 +238,11 @@ export const Navbar = () => {
|
|||||||
}
|
}
|
||||||
window.close();
|
window.close();
|
||||||
return;
|
return;
|
||||||
|
} else if (org.googleSsoAuthEnforced) {
|
||||||
|
await logout.mutateAsync();
|
||||||
|
window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`);
|
||||||
|
window.close();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOrgChange(org?.id);
|
handleOrgChange(org?.id);
|
||||||
|
|||||||
@@ -82,25 +82,42 @@ export const SelectOrganizationSection = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (organization.authEnforced && !canBypassOrgAuth) {
|
if ((organization.authEnforced || organization.googleSsoAuthEnforced) && !canBypassOrgAuth) {
|
||||||
|
const authToken = jwtDecode(getAuthToken()) as { authMethod: AuthMethod };
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5_000));
|
||||||
|
|
||||||
// org has an org-level auth method enabled (e.g. SAML)
|
// org has an org-level auth method enabled (e.g. SAML)
|
||||||
// -> logout + redirect to SAML SSO
|
// -> logout + redirect to SAML SSO
|
||||||
await logout.mutateAsync();
|
|
||||||
let url = "";
|
let url = "";
|
||||||
if (organization.orgAuthMethod === AuthMethod.OIDC) {
|
if (organization.orgAuthMethod === AuthMethod.OIDC) {
|
||||||
url = `/api/v1/sso/oidc/login?orgSlug=${organization.slug}${
|
url = `/api/v1/sso/oidc/login?orgSlug=${organization.slug}${
|
||||||
callbackPort ? `&callbackPort=${callbackPort}` : ""
|
callbackPort ? `&callbackPort=${callbackPort}` : ""
|
||||||
}`;
|
}`;
|
||||||
} else {
|
} else if (organization.orgAuthMethod === AuthMethod.SAML) {
|
||||||
url = `/api/v1/sso/redirect/saml2/organizations/${organization.slug}`;
|
url = `/api/v1/sso/redirect/saml2/organizations/${organization.slug}`;
|
||||||
|
|
||||||
if (callbackPort) {
|
if (callbackPort) {
|
||||||
url += `?callback_port=${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;
|
// 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
|
||||||
return;
|
// see line 103-106
|
||||||
|
if (url) {
|
||||||
|
await logout.mutateAsync();
|
||||||
|
window.location.href = url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { token, isMfaEnabled, mfaMethod } = await selectOrg
|
const { token, isMfaEnabled, mfaMethod } = await selectOrg
|
||||||
@@ -198,6 +215,8 @@ export const SelectOrganizationSection = () => {
|
|||||||
handleCliRedirect();
|
handleCliRedirect();
|
||||||
setIsInitialOrgCheckLoading(false);
|
setIsInitialOrgCheckLoading(false);
|
||||||
} else {
|
} else {
|
||||||
|
console.log(organizations.data);
|
||||||
|
console.log("Calling this with single org?!??!?!::::", organizations.data.length);
|
||||||
handleSelectOrganization(organizations.data[0]);
|
handleSelectOrganization(organizations.data[0]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -207,6 +226,7 @@ export const SelectOrganizationSection = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (defaultSelectedOrg) {
|
if (defaultSelectedOrg) {
|
||||||
|
console.log("Calling this with default org?!??!?!::::", defaultSelectedOrg);
|
||||||
handleSelectOrganization(defaultSelectedOrg);
|
handleSelectOrganization(defaultSelectedOrg);
|
||||||
}
|
}
|
||||||
}, [defaultSelectedOrg]);
|
}, [defaultSelectedOrg]);
|
||||||
|
|||||||
+130
-49
@@ -13,8 +13,21 @@ import {
|
|||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { useLogoutUser, useUpdateOrg } from "@app/hooks/api";
|
import { useLogoutUser, useUpdateOrg } from "@app/hooks/api";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
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
|
||||||
|
}: {
|
||||||
|
isSamlConfigured: boolean;
|
||||||
|
isOidcConfigured: boolean;
|
||||||
|
}) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||||
@@ -23,27 +36,61 @@ export const OrgGeneralAuthSection = () => {
|
|||||||
|
|
||||||
const logout = useLogoutUser();
|
const logout = useLogoutUser();
|
||||||
|
|
||||||
const handleEnforceOrgAuthToggle = async (value: boolean) => {
|
const handleEnforceOrgAuthToggle = async (value: boolean, type: EnforceAuthType) => {
|
||||||
try {
|
try {
|
||||||
if (!currentOrg?.id) return;
|
if (!currentOrg?.id) return;
|
||||||
if (!subscription?.samlSSO) {
|
|
||||||
handlePopUpOpen("upgradePlan");
|
if (type === EnforceAuthType.SAML) {
|
||||||
return;
|
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({
|
createNotification({
|
||||||
text: `Successfully ${value ? "enforced" : "un-enforced"} org-level auth`,
|
text: `Successfully ${value ? "enabled" : "disabled"} org-level auth`,
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
await logout.mutateAsync();
|
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();
|
window.close();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -79,44 +126,78 @@ export const OrgGeneralAuthSection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <div className="py-4">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="mb-2 flex justify-between">
|
<div className={twMerge("mt-4", !isSamlConfigured && "hidden")}>
|
||||||
<h3 className="text-md text-mineshaft-100">Allow users to send invites</h3>
|
<div className="mb-2 flex justify-between">
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
<div className="flex items-center gap-1">
|
||||||
{(isAllowed) => (
|
<span className="text-md text-mineshaft-100">Enforce SAML SSO</span>
|
||||||
<Switch
|
</div>
|
||||||
id="allow-org-invites"
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||||
onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)}
|
{(isAllowed) => (
|
||||||
isChecked={currentOrg?.authEnforced ?? false}
|
<Switch
|
||||||
isDisabled={!isAllowed}
|
id="enforce-org-auth"
|
||||||
/>
|
onCheckedChange={(value) =>
|
||||||
)}
|
handleEnforceOrgAuthToggle(value, EnforceAuthType.SAML)
|
||||||
</OrgPermissionCan>
|
}
|
||||||
</div>
|
isChecked={currentOrg?.authEnforced ?? false}
|
||||||
<p className="text-sm text-mineshaft-300">Allow members to invite new users to this organization</p>
|
isDisabled={!isAllowed || currentOrg?.googleSsoAuthEnforced}
|
||||||
</div> */}
|
/>
|
||||||
<div className="py-4">
|
)}
|
||||||
<div className="mb-2 flex justify-between">
|
</OrgPermissionCan>
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span className="text-md text-mineshaft-100">Enforce SAML SSO</span>
|
|
||||||
</div>
|
</div>
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
<p className="text-sm text-mineshaft-300">
|
||||||
{(isAllowed) => (
|
Enforce users to authenticate via SAML to access this organization.
|
||||||
<Switch
|
</p>
|
||||||
id="enforce-org-auth"
|
</div>
|
||||||
onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)}
|
|
||||||
isChecked={currentOrg?.authEnforced ?? false}
|
<div className={twMerge("mt-4", !isOidcConfigured && "hidden")}>
|
||||||
isDisabled={!isAllowed}
|
<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>
|
||||||
</OrgPermissionCan>
|
</div>
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Switch
|
||||||
|
id="enforce-org-auth"
|
||||||
|
isChecked={currentOrg?.authEnforced ?? false}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
handleEnforceOrgAuthToggle(value, EnforceAuthType.OIDC)
|
||||||
|
}
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<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.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-mineshaft-300">
|
|
||||||
Enforce users to authenticate via SAML to access this organization
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{currentOrg?.authEnforced && (
|
{(currentOrg?.authEnforced || currentOrg?.googleSsoAuthEnforced) && (
|
||||||
<div className="py-4">
|
<div className="mt-4 py-4">
|
||||||
<div className="mb-2 flex justify-between">
|
<div className="mb-2 flex justify-between">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="text-md text-mineshaft-100">Enable Admin SSO Bypass</span>
|
<span className="text-md text-mineshaft-100">Enable Admin SSO Bypass</span>
|
||||||
@@ -125,8 +206,8 @@ export const OrgGeneralAuthSection = () => {
|
|||||||
content={
|
content={
|
||||||
<div>
|
<div>
|
||||||
<span>
|
<span>
|
||||||
When this is enabled, we strongly recommend enforcing MFA at the organization
|
When enabling admin SSO bypass, we highly recommend enabling MFA enforcement
|
||||||
level.
|
at the organization-level for security reasons.
|
||||||
</span>
|
</span>
|
||||||
<p className="mt-4">
|
<p className="mt-4">
|
||||||
In case of a lockout, admins can use the{" "}
|
In case of a lockout, admins can use the{" "}
|
||||||
|
|||||||
+1
-135
@@ -11,7 +11,7 @@ import {
|
|||||||
useOrganization,
|
useOrganization,
|
||||||
useSubscription
|
useSubscription
|
||||||
} from "@app/context";
|
} 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 { useUpdateOIDCConfig } from "@app/hooks/api/oidcConfig/mutations";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
@@ -23,9 +23,7 @@ export const OrgOIDCSection = (): JSX.Element => {
|
|||||||
|
|
||||||
const { data, isPending } = useGetOIDCConfig(currentOrg?.id ?? "");
|
const { data, isPending } = useGetOIDCConfig(currentOrg?.id ?? "");
|
||||||
const { mutateAsync } = useUpdateOIDCConfig();
|
const { mutateAsync } = useUpdateOIDCConfig();
|
||||||
const { mutateAsync: updateOrg } = useUpdateOrg();
|
|
||||||
|
|
||||||
const logout = useLogoutUser();
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"addOIDC",
|
"addOIDC",
|
||||||
"upgradePlan"
|
"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) => {
|
const handleOIDCGroupManagement = async (value: boolean) => {
|
||||||
try {
|
try {
|
||||||
if (!currentOrg?.id) return;
|
if (!currentOrg?.id) return;
|
||||||
@@ -178,88 +126,6 @@ export const OrgOIDCSection = (): JSX.Element => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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="py-4">
|
||||||
<div className="mb-2 flex justify-between">
|
<div className="mb-2 flex justify-between">
|
||||||
<div className="text-md flex items-center text-mineshaft-100">
|
<div className="text-md flex items-center text-mineshaft-100">
|
||||||
|
|||||||
@@ -79,10 +79,9 @@ export const OrgSSOSection = (): JSX.Element => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="space-y-4">
|
||||||
<hr className="border-mineshaft-600" />
|
<div>
|
||||||
<div className="py-4">
|
<div className="flex items-center justify-between">
|
||||||
<div className="mb-2 flex items-center justify-between">
|
|
||||||
<h2 className="text-md text-mineshaft-100">SAML</h2>
|
<h2 className="text-md text-mineshaft-100">SAML</h2>
|
||||||
{!isPending && (
|
{!isPending && (
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
|
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
|
||||||
@@ -96,7 +95,7 @@ export const OrgSSOSection = (): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-mineshaft-300">Manage SAML authentication configuration</p>
|
<p className="text-sm text-mineshaft-300">Manage SAML authentication configuration</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-4">
|
<div>
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<h2 className="text-md text-mineshaft-100">Enable SAML</h2>
|
<h2 className="text-md text-mineshaft-100">Enable SAML</h2>
|
||||||
{!isPending && (
|
{!isPending && (
|
||||||
@@ -126,6 +125,6 @@ export const OrgSSOSection = (): JSX.Element => {
|
|||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
text="You can use SAML SSO if you switch to Infisical's Pro plan."
|
text="You can use SAML SSO if you switch to Infisical's Pro plan."
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,10 +49,15 @@ export const OrgSsoTab = withPermission(
|
|||||||
);
|
);
|
||||||
const areConfigsLoading = isLoadingOidcConfig || isLoadingSamlConfig || isLoadingLdapConfig;
|
const areConfigsLoading = isLoadingOidcConfig || isLoadingSamlConfig || isLoadingLdapConfig;
|
||||||
|
|
||||||
const shouldDisplaySection = (method: LoginMethod) =>
|
const shouldDisplaySection = (method: LoginMethod[] | LoginMethod) => {
|
||||||
!enabledLoginMethods || enabledLoginMethods.includes(method);
|
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 =
|
const isSamlConfigured =
|
||||||
samlConfig && (samlConfig.entryPoint || samlConfig.issuer || samlConfig.cert);
|
samlConfig && (samlConfig.entryPoint || samlConfig.issuer || samlConfig.cert);
|
||||||
const isLdapConfigured = ldapConfig && ldapConfig.url;
|
const isLdapConfigured = ldapConfig && ldapConfig.url;
|
||||||
@@ -65,10 +70,11 @@ export const OrgSsoTab = withPermission(
|
|||||||
shouldDisplaySection(LoginMethod.OIDC) ||
|
shouldDisplaySection(LoginMethod.OIDC) ||
|
||||||
shouldDisplaySection(LoginMethod.LDAP) ? (
|
shouldDisplaySection(LoginMethod.LDAP) ? (
|
||||||
<>
|
<>
|
||||||
<div className="mb-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="text-xl font-semibold text-gray-200">Connect an Identity Provider</p>
|
||||||
<p className="mb-2 mt-1 text-gray-400">
|
<p className="mb-2 mt-1 text-gray-400">
|
||||||
Connect your identity provider to simplify user management
|
Connect your identity provider to simplify user management with options like SAML,
|
||||||
|
OIDC, and LDAP.
|
||||||
</p>
|
</p>
|
||||||
{shouldDisplaySection(LoginMethod.SAML) && (
|
{shouldDisplaySection(LoginMethod.SAML) && (
|
||||||
<div
|
<div
|
||||||
@@ -170,18 +176,31 @@ export const OrgSsoTab = withPermission(
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{shouldShowCreateIdentityProviderView ? (
|
{shouldShowCreateIdentityProviderView ? (
|
||||||
createIdentityProviderView
|
<div className="mb-4 space-y-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||||
|
<OrgGeneralAuthSection
|
||||||
|
isSamlConfigured={isSamlConfigured}
|
||||||
|
isOidcConfigured={isOidcConfigured}
|
||||||
|
/>
|
||||||
|
<hr className="border-mineshaft-600" />
|
||||||
|
{createIdentityProviderView}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="mb-4 space-y-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||||
{isSamlConfigured && shouldDisplaySection(LoginMethod.SAML) && (
|
<div>
|
||||||
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
{/* {shouldDisplaySection([LoginMethod.SAML, LoginMethod.GOOGLE]) && ( */}
|
||||||
<OrgGeneralAuthSection />
|
<OrgGeneralAuthSection
|
||||||
<OrgSSOSection />
|
isSamlConfigured={isSamlConfigured}
|
||||||
</div>
|
isOidcConfigured={isOidcConfigured}
|
||||||
)}
|
/>
|
||||||
{isOidcConfigured && shouldDisplaySection(LoginMethod.OIDC) && <OrgOIDCSection />}
|
{/* )} */}
|
||||||
{isLdapConfigured && shouldDisplaySection(LoginMethod.LDAP) && <OrgLDAPSection />}
|
</div>
|
||||||
</>
|
<hr className="border-mineshaft-600" />
|
||||||
|
<div>
|
||||||
|
{isSamlConfigured && shouldDisplaySection(LoginMethod.SAML) && <OrgSSOSection />}
|
||||||
|
{isOidcConfigured && shouldDisplaySection(LoginMethod.OIDC) && <OrgOIDCSection />}
|
||||||
|
{isLdapConfigured && shouldDisplaySection(LoginMethod.LDAP) && <OrgLDAPSection />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user