+
diff --git a/frontend/src/views/Login/components/SSOStep/index.tsx b/frontend/src/views/Login/components/SSOStep/index.tsx
new file mode 100644
index 000000000..e7d80b2c0
--- /dev/null
+++ b/frontend/src/views/Login/components/SSOStep/index.tsx
@@ -0,0 +1 @@
+export { SSOStep } from "./SSOStep";
diff --git a/frontend/src/views/Login/components/index.tsx b/frontend/src/views/Login/components/index.tsx
index 7c55c4acf..296b0503e 100644
--- a/frontend/src/views/Login/components/index.tsx
+++ b/frontend/src/views/Login/components/index.tsx
@@ -1,6 +1,6 @@
export { InitialStep } from "./InitialStep";
export { MFAStep } from "./MFAStep";
-export { SAMLSSOStep } from "./SAMLSSOStep";
+export { SSOStep } from "./SSOStep";
// SSO-specific step
export { PasswordStep } from "./PasswordStep";
From 96ad3b02643ece9313038c346d649c639ccf206c Mon Sep 17 00:00:00 2001
From: Sheen Capadngan
Date: Tue, 18 Jun 2024 14:20:04 +0800
Subject: [PATCH 012/302] misc: used redis for oic session managemen
---
backend/package-lock.json | 91 +++++++++++++++++++++++++
backend/package.json | 1 +
backend/src/ee/routes/v1/oidc-router.ts | 13 +++-
3 files changed, 104 insertions(+), 1 deletion(-)
diff --git a/backend/package-lock.json b/backend/package-lock.json
index b0ff466c2..4e22ed5a1 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -36,6 +36,7 @@
"bcrypt": "^5.1.1",
"bullmq": "^5.4.2",
"cassandra-driver": "^4.7.2",
+ "connect-redis": "^7.1.1",
"cron": "^3.1.7",
"dotenv": "^16.4.1",
"fastify": "^4.26.0",
@@ -6633,6 +6634,17 @@
"integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==",
"dev": true
},
+ "node_modules/connect-redis": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-7.1.1.tgz",
+ "integrity": "sha512-M+z7alnCJiuzKa8/1qAYdGUXHYfDnLolOGAUjOioB07pP39qxjG+X9ibsud7qUBc4jMV5Mcy3ugGv8eFcgamJQ==",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "express-session": ">=1"
+ }
+ },
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@@ -7739,6 +7751,55 @@
"node": ">= 0.10.0"
}
},
+ "node_modules/express-session": {
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz",
+ "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==",
+ "peer": true,
+ "dependencies": {
+ "cookie": "0.6.0",
+ "cookie-signature": "1.0.7",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-headers": "~1.0.2",
+ "parseurl": "~1.3.3",
+ "safe-buffer": "5.2.1",
+ "uid-safe": "~2.1.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/express-session/node_modules/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express-session/node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "peer": true
+ },
+ "node_modules/express-session/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "peer": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express-session/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "peer": true
+ },
"node_modules/express/node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
@@ -10737,6 +10798,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -11813,6 +11883,15 @@
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
},
+ "node_modules/random-bytes": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+ "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -13871,6 +13950,18 @@
"node": ">=0.8.0"
}
},
+ "node_modules/uid-safe": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+ "peer": true,
+ "dependencies": {
+ "random-bytes": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/uid2": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
diff --git a/backend/package.json b/backend/package.json
index 7aecc01bb..dbd55705e 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -97,6 +97,7 @@
"bcrypt": "^5.1.1",
"bullmq": "^5.4.2",
"cassandra-driver": "^4.7.2",
+ "connect-redis": "^7.1.1",
"cron": "^3.1.7",
"dotenv": "^16.4.1",
"fastify": "^4.26.0",
diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts
index 00844309d..c5433c008 100644
--- a/backend/src/ee/routes/v1/oidc-router.ts
+++ b/backend/src/ee/routes/v1/oidc-router.ts
@@ -8,6 +8,8 @@
import { Authenticator, Strategy } from "@fastify/passport";
import fastifySession from "@fastify/session";
+import RedisStore from "connect-redis";
+import { Redis } from "ioredis";
import { z } from "zod";
import { OidcConfigsSchema } from "@app/db/schemas/oidc-configs";
@@ -18,13 +20,22 @@ import { AuthMode } from "@app/services/auth/auth-type";
export const registerOidcRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
+ const redis = new Redis(appCfg.REDIS_URL);
const passport = new Authenticator({ key: "oidc", userProperty: "passportUser" });
+ const redisStore = new RedisStore({
+ client: redis,
+ prefix: "oidc-session:",
+ ttl: 600 // 10 minutes
+ });
+
await server.register(fastifySession, {
secret: appCfg.COOKIE_SECRET_SIGN_KEY,
+ store: redisStore,
cookie: {
- secure: false // has to be set to false if testing locally
+ secure: false // set to true in production
}
});
+
await server.register(passport.initialize());
await server.register(passport.secureSession());
From 8de4443be172111eaadeee70a7f6a978c49fc4b6 Mon Sep 17 00:00:00 2001
From: Sheen Capadngan
Date: Tue, 18 Jun 2024 15:26:08 +0800
Subject: [PATCH 013/302] feat: added support for login via cli
---
backend/src/ee/routes/v1/oidc-router.ts | 22 ++++++++++++++-----
.../ee/services/oidc/oidc-config-service.ts | 10 +++++----
.../src/ee/services/oidc/oidc-config-types.ts | 1 +
.../Login/components/SSOStep/SSOStep.tsx | 7 ++++--
4 files changed, 29 insertions(+), 11 deletions(-)
diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts
index c5433c008..55d3be19d 100644
--- a/backend/src/ee/routes/v1/oidc-router.ts
+++ b/backend/src/ee/routes/v1/oidc-router.ts
@@ -45,14 +45,20 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
method: "GET",
schema: {
querystring: z.object({
- orgSlug: z.string().trim()
+ orgSlug: z.string().trim(),
+ callbackPort: z.string().trim().optional()
})
},
handler: async (req, res) => {
// get params, save to session
- const { orgSlug } = req.query;
+ const { orgSlug, callbackPort } = req.query;
req.session.set("oidcOrgSlug", orgSlug);
- const oidcStrategy = await server.services.oidc.getOrgAuthStrategy(orgSlug);
+
+ if (callbackPort) {
+ req.session.set("callbackPort", callbackPort);
+ }
+
+ const oidcStrategy = await server.services.oidc.getOrgAuthStrategy(orgSlug, callbackPort);
(
passport.authenticate(oidcStrategy as Strategy, {
scope: "profile email openid"
@@ -67,7 +73,9 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
method: "GET",
handler: async (req, res) => {
const oidcOrgSlug = req.session.get("oidcOrgSlug");
- const oidcStrategy = await server.services.oidc.getOrgAuthStrategy(oidcOrgSlug);
+ const callbackPort = req.session.get("callbackPort");
+ const oidcStrategy = await server.services.oidc.getOrgAuthStrategy(oidcOrgSlug, callbackPort);
+
await (
passport.authenticate(oidcStrategy as Strategy, {
failureRedirect: "/api/v1/sso/oidc/login/error",
@@ -76,6 +84,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
}) as any
)(req, res);
+ await req.session.destroy();
+
if (req.passportUser.isUserCompleted) {
return res.redirect(
`http://localhost:8080/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
@@ -92,7 +102,9 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/login/error",
method: "GET",
- handler: (req, res) => {
+ handler: async (req, res) => {
+ await req.session.destroy();
+
return res.status(500).send({
error: "Authentication error",
details: req.query
diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts
index 03d74ea16..b880c8664 100644
--- a/backend/src/ee/services/oidc/oidc-config-service.ts
+++ b/backend/src/ee/services/oidc/oidc-config-service.ts
@@ -63,7 +63,7 @@ export const oidcConfigServiceFactory = ({
smtpService,
oidcConfigDAL
}: TOidcConfigServiceFactoryDep) => {
- const oidcLogin = async ({ externalId, email, firstName, lastName, orgId }: TOidcLoginDTO) => {
+ const oidcLogin = async ({ externalId, email, firstName, lastName, orgId, callbackPort }: TOidcLoginDTO) => {
const appCfg = getConfig();
const userAlias = await userAliasDAL.findOne({
externalId,
@@ -188,7 +188,8 @@ export const oidcConfigServiceFactory = ({
organizationSlug: organization.slug,
authMethod: AuthMethod.OIDC,
authType: UserAliasType.OIDC,
- isUserCompleted
+ isUserCompleted,
+ ...(callbackPort && { callbackPort })
},
appCfg.AUTH_SECRET,
{
@@ -491,7 +492,7 @@ export const oidcConfigServiceFactory = ({
return oidcCfg;
};
- const getOrgAuthStrategy = async (orgSlug: string) => {
+ const getOrgAuthStrategy = async (orgSlug: string, callbackPort?: string) => {
const appCfg = getConfig();
const org = await orgDAL.findOne({
@@ -542,7 +543,8 @@ export const oidcConfigServiceFactory = ({
externalId: claims.sub,
firstName: claims.given_name ?? "",
lastName: claims.family_name ?? "",
- orgId: org.id
+ orgId: org.id,
+ callbackPort
})
.then(({ isUserCompleted, providerAuthToken }) => {
cb(null, { isUserCompleted, providerAuthToken });
diff --git a/backend/src/ee/services/oidc/oidc-config-types.ts b/backend/src/ee/services/oidc/oidc-config-types.ts
index a160b272a..4c729ceea 100644
--- a/backend/src/ee/services/oidc/oidc-config-types.ts
+++ b/backend/src/ee/services/oidc/oidc-config-types.ts
@@ -6,6 +6,7 @@ export type TOidcLoginDTO = {
firstName: string;
lastName?: string;
orgId: string;
+ callbackPort?: string;
};
export type TGetOidcCfgDTO =
diff --git a/frontend/src/views/Login/components/SSOStep/SSOStep.tsx b/frontend/src/views/Login/components/SSOStep/SSOStep.tsx
index 5e70cf1a7..9663c04a9 100644
--- a/frontend/src/views/Login/components/SSOStep/SSOStep.tsx
+++ b/frontend/src/views/Login/components/SSOStep/SSOStep.tsx
@@ -24,8 +24,11 @@ export const SSOStep = ({ setStep, type }: Props) => {
}`
);
} else {
- // TODO: Sheen - Add callback support for CLI login
- window.open(`/api/v1/sso/oidc/login?orgSlug=${ssoIdentifier}`);
+ window.open(
+ `/api/v1/sso/oidc/login?orgSlug=${ssoIdentifier}${
+ callbackPort ? `&callbackPort=${callbackPort}` : ""
+ }`
+ );
}
window.close();
From c5c00b520c7378887fa0111b5cbb8da57d6fe226 Mon Sep 17 00:00:00 2001
From: Sheen Capadngan
Date: Tue, 18 Jun 2024 15:35:18 +0800
Subject: [PATCH 014/302] misc: added session regenerate for fresh state
---
backend/src/ee/routes/v1/oidc-router.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts
index 55d3be19d..36ad8a085 100644
--- a/backend/src/ee/routes/v1/oidc-router.ts
+++ b/backend/src/ee/routes/v1/oidc-router.ts
@@ -50,8 +50,11 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
})
},
handler: async (req, res) => {
- // get params, save to session
const { orgSlug, callbackPort } = req.query;
+
+ // ensure fresh session state per login attempt
+ await req.session.regenerate();
+
req.session.set("oidcOrgSlug", orgSlug);
if (callbackPort) {
From 371b96a13af8683bb22994876d494e22c87a3ced Mon Sep 17 00:00:00 2001
From: Sheen Capadngan
Date: Tue, 18 Jun 2024 15:36:49 +0800
Subject: [PATCH 015/302] misc: removed cookie path proxy for dev envs
---
nginx/default.dev.conf | 2 --
1 file changed, 2 deletions(-)
diff --git a/nginx/default.dev.conf b/nginx/default.dev.conf
index 94c5d7b67..0bdb335ac 100644
--- a/nginx/default.dev.conf
+++ b/nginx/default.dev.conf
@@ -10,8 +10,6 @@ server {
proxy_pass http://backend:4000;
proxy_redirect off;
-
- proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
}
location / {
From bcd65333c0a032a05f7200767b8f2cd02f322caa Mon Sep 17 00:00:00 2001
From: Sheen Capadngan
Date: Tue, 18 Jun 2024 19:17:54 +0800
Subject: [PATCH 016/302] misc: added handling of inactive and undefined oidc
config
---
backend/src/ee/services/oidc/oidc-config-service.ts | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts
index b880c8664..7c4b7e87e 100644
--- a/backend/src/ee/services/oidc/oidc-config-service.ts
+++ b/backend/src/ee/services/oidc/oidc-config-service.ts
@@ -510,6 +510,12 @@ export const oidcConfigServiceFactory = ({
orgSlug
});
+ if (!oidcCfg || !oidcCfg.isActive) {
+ throw new BadRequestError({
+ message: "Failed to authenticate with OIDC SSO"
+ });
+ }
+
const openIdIssuer = new OpenIdIssuer({
issuer: oidcCfg.issuer,
authorization_endpoint: oidcCfg.authorizationEndpoint,
From bdc7c018eb5e38b4085689943cd46ccdc7e916df Mon Sep 17 00:00:00 2001
From: Sheen Capadngan
Date: Tue, 18 Jun 2024 20:36:09 +0800
Subject: [PATCH 017/302] misc: added comment regarding session and redis usage
---
backend/src/ee/routes/v1/oidc-router.ts | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts
index 36ad8a085..8b68a8aef 100644
--- a/backend/src/ee/routes/v1/oidc-router.ts
+++ b/backend/src/ee/routes/v1/oidc-router.ts
@@ -22,6 +22,12 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
const redis = new Redis(appCfg.REDIS_URL);
const passport = new Authenticator({ key: "oidc", userProperty: "passportUser" });
+
+ /*
+ - OIDC protocol cannot work without sessions: https://github.com/panva/node-openid-client/issues/190
+ - Current redis usage is not ideal and will eventually have to be refactored to use a better structure
+ - Fastify session <> Redis structure is based on the ff: https://github.com/fastify/session/blob/master/examples/redis.js
+ */
const redisStore = new RedisStore({
client: redis,
prefix: "oidc-session:",
From 18e69578f03240fd83043d850d9d862270b2caa3 Mon Sep 17 00:00:00 2001
From: Sheen Capadngan
Date: Wed, 19 Jun 2024 01:29:26 +0800
Subject: [PATCH 018/302] feat: added support for limiting email domains
---
.../20240617041053_add-oidc-auth.ts | 1 +
backend/src/db/schemas/oidc-configs.ts | 1 +
backend/src/ee/routes/v1/oidc-router.ts | 33 +++-
.../ee/services/oidc/oidc-config-service.ts | 173 ++++++++++--------
.../src/ee/services/oidc/oidc-config-types.ts | 2 +
.../src/hooks/api/oidcConfig/mutations.tsx | 6 +
frontend/src/hooks/api/oidcConfig/types.ts | 1 +
.../components/OrgAuthTab/OIDCModal.tsx | 20 +-
8 files changed, 154 insertions(+), 83 deletions(-)
diff --git a/backend/src/db/migrations/20240617041053_add-oidc-auth.ts b/backend/src/db/migrations/20240617041053_add-oidc-auth.ts
index b11e6bbab..8ef34854b 100644
--- a/backend/src/db/migrations/20240617041053_add-oidc-auth.ts
+++ b/backend/src/db/migrations/20240617041053_add-oidc-auth.ts
@@ -17,6 +17,7 @@ export async function up(knex: Knex): Promise {
tb.text("encryptedClientSecret").notNullable();
tb.string("clientSecretIV").notNullable();
tb.string("clientSecretTag").notNullable();
+ tb.string("allowedEmailDomains").nullable();
tb.boolean("isActive").notNullable();
tb.timestamps(true, true, true);
tb.uuid("orgId").notNullable().unique();
diff --git a/backend/src/db/schemas/oidc-configs.ts b/backend/src/db/schemas/oidc-configs.ts
index b06114df6..3de22d1d0 100644
--- a/backend/src/db/schemas/oidc-configs.ts
+++ b/backend/src/db/schemas/oidc-configs.ts
@@ -20,6 +20,7 @@ export const OidcConfigsSchema = z.object({
encryptedClientSecret: z.string(),
clientSecretIV: z.string(),
clientSecretTag: z.string(),
+ allowedEmailDomains: z.string().nullable().optional(),
isActive: z.boolean(),
createdAt: z.date(),
updatedAt: z.date(),
diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts
index 8b68a8aef..d64704dce 100644
--- a/backend/src/ee/routes/v1/oidc-router.ts
+++ b/backend/src/ee/routes/v1/oidc-router.ts
@@ -137,7 +137,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
tokenEndpoint: true,
userinfoEndpoint: true,
isActive: true,
- orgId: true
+ orgId: true,
+ allowedEmailDomains: true
}).extend({
clientId: z.string(),
clientSecret: z.string()
@@ -169,6 +170,19 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
schema: {
body: z
.object({
+ allowedEmailDomains: z
+ .string()
+ .trim()
+ .optional()
+ .default("")
+ .transform((data) => {
+ if (data === "") return "";
+ // Trim each ID and join with ', ' to ensure formatting
+ return data
+ .split(",")
+ .map((id) => id.trim())
+ .join(", ");
+ }),
issuer: z.string().trim(),
authorizationEndpoint: z.string().trim(),
jwksUri: z.string().trim(),
@@ -189,6 +203,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
tokenEndpoint: true,
userinfoEndpoint: true,
orgId: true,
+ allowedEmailDomains: true,
isActive: true
})
}
@@ -215,6 +230,19 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
schema: {
body: z.object({
issuer: z.string().trim(),
+ allowedEmailDomains: z
+ .string()
+ .trim()
+ .optional()
+ .default("")
+ .transform((data) => {
+ if (data === "") return "";
+ // Trim each ID and join with ', ' to ensure formatting
+ return data
+ .split(",")
+ .map((id) => id.trim())
+ .join(", ");
+ }),
authorizationEndpoint: z.string().trim(),
jwksUri: z.string().trim(),
tokenEndpoint: z.string().trim(),
@@ -233,7 +261,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
tokenEndpoint: true,
userinfoEndpoint: true,
orgId: true,
- isActive: true
+ isActive: true,
+ allowedEmailDomains: true
})
}
},
diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts
index 7c4b7e87e..3f58036b6 100644
--- a/backend/src/ee/services/oidc/oidc-config-service.ts
+++ b/backend/src/ee/services/oidc/oidc-config-service.ts
@@ -63,6 +63,86 @@ export const oidcConfigServiceFactory = ({
smtpService,
oidcConfigDAL
}: TOidcConfigServiceFactoryDep) => {
+ const getOidc = async (dto: TGetOidcCfgDTO) => {
+ const org = await orgDAL.findOne({ slug: dto.orgSlug });
+ if (!org) {
+ throw new BadRequestError({
+ message: "Organization not found",
+ name: "OrgNotFound"
+ });
+ }
+ if (dto.type === "external") {
+ const { permission } = await permissionService.getOrgPermission(
+ dto.actor,
+ dto.actorId,
+ org.id,
+ dto.actorAuthMethod,
+ dto.actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
+ }
+
+ const oidcCfg = await oidcConfigDAL.findOne({
+ orgId: org.id
+ });
+
+ if (!oidcCfg) {
+ throw new BadRequestError({
+ message: "Failed to find organization OIDC configuration"
+ });
+ }
+
+ // decrypt and return cfg
+ const orgBot = await orgBotDAL.findOne({ orgId: oidcCfg.orgId });
+ if (!orgBot) {
+ throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
+ }
+
+ const key = infisicalSymmetricDecrypt({
+ ciphertext: orgBot.encryptedSymmetricKey,
+ iv: orgBot.symmetricKeyIV,
+ tag: orgBot.symmetricKeyTag,
+ keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
+ });
+
+ const { encryptedClientId, clientIdIV, clientIdTag, encryptedClientSecret, clientSecretIV, clientSecretTag } =
+ oidcCfg;
+
+ let clientId = "";
+ if (encryptedClientId && clientIdIV && clientIdTag) {
+ clientId = decryptSymmetric({
+ ciphertext: encryptedClientId,
+ key,
+ tag: clientIdTag,
+ iv: clientIdIV
+ });
+ }
+
+ let clientSecret = "";
+ if (encryptedClientSecret && clientSecretIV && clientSecretTag) {
+ clientSecret = decryptSymmetric({
+ key,
+ tag: clientSecretTag,
+ iv: clientSecretIV,
+ ciphertext: encryptedClientSecret
+ });
+ }
+
+ return {
+ id: oidcCfg.id,
+ issuer: oidcCfg.issuer,
+ authorizationEndpoint: oidcCfg.authorizationEndpoint,
+ jwksUri: oidcCfg.jwksUri,
+ tokenEndpoint: oidcCfg.tokenEndpoint,
+ userinfoEndpoint: oidcCfg.userinfoEndpoint,
+ orgId: oidcCfg.orgId,
+ isActive: oidcCfg.isActive,
+ allowedEmailDomains: oidcCfg.allowedEmailDomains,
+ clientId,
+ clientSecret
+ };
+ };
+
const oidcLogin = async ({ externalId, email, firstName, lastName, orgId, callbackPort }: TOidcLoginDTO) => {
const appCfg = getConfig();
const userAlias = await userAliasDAL.findOne({
@@ -216,87 +296,9 @@ export const oidcConfigServiceFactory = ({
return { isUserCompleted, providerAuthToken };
};
- const getOidc = async (dto: TGetOidcCfgDTO) => {
- const org = await orgDAL.findOne({ slug: dto.orgSlug });
- if (!org) {
- throw new BadRequestError({
- message: "Organization not found",
- name: "OrgNotFound"
- });
- }
- if (dto.type === "external") {
- const { permission } = await permissionService.getOrgPermission(
- dto.actor,
- dto.actorId,
- org.id,
- dto.actorAuthMethod,
- dto.actorOrgId
- );
- ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
- }
-
- const oidcCfg = await oidcConfigDAL.findOne({
- orgId: org.id
- });
-
- if (!oidcCfg) {
- throw new BadRequestError({
- message: "Failed to find organization OIDC configuration"
- });
- }
-
- // decrypt and return cfg
- const orgBot = await orgBotDAL.findOne({ orgId: oidcCfg.orgId });
- if (!orgBot) {
- throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
- }
-
- const key = infisicalSymmetricDecrypt({
- ciphertext: orgBot.encryptedSymmetricKey,
- iv: orgBot.symmetricKeyIV,
- tag: orgBot.symmetricKeyTag,
- keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
- });
-
- const { encryptedClientId, clientIdIV, clientIdTag, encryptedClientSecret, clientSecretIV, clientSecretTag } =
- oidcCfg;
-
- let clientId = "";
- if (encryptedClientId && clientIdIV && clientIdTag) {
- clientId = decryptSymmetric({
- ciphertext: encryptedClientId,
- key,
- tag: clientIdTag,
- iv: clientIdIV
- });
- }
-
- let clientSecret = "";
- if (encryptedClientSecret && clientSecretIV && clientSecretTag) {
- clientSecret = decryptSymmetric({
- key,
- tag: clientSecretTag,
- iv: clientSecretIV,
- ciphertext: encryptedClientSecret
- });
- }
-
- return {
- id: oidcCfg.id,
- issuer: oidcCfg.issuer,
- authorizationEndpoint: oidcCfg.authorizationEndpoint,
- jwksUri: oidcCfg.jwksUri,
- tokenEndpoint: oidcCfg.tokenEndpoint,
- userinfoEndpoint: oidcCfg.userinfoEndpoint,
- orgId: oidcCfg.orgId,
- isActive: oidcCfg.isActive,
- clientId,
- clientSecret
- };
- };
-
const updateOidcCfg = async ({
orgSlug,
+ allowedEmailDomains,
actor,
actorOrgId,
actorAuthMethod,
@@ -346,6 +348,7 @@ export const oidcConfigServiceFactory = ({
});
const updateQuery: TOidcConfigsUpdate = {
+ allowedEmailDomains,
issuer,
authorizationEndpoint,
tokenEndpoint,
@@ -374,12 +377,12 @@ export const oidcConfigServiceFactory = ({
}
const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery);
-
return ssoConfig;
};
const createOidcCfg = async ({
orgSlug,
+ allowedEmailDomains,
actor,
actorOrgId,
actorAuthMethod,
@@ -477,6 +480,7 @@ export const oidcConfigServiceFactory = ({
issuer,
isActive,
authorizationEndpoint,
+ allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
@@ -544,6 +548,15 @@ export const oidcConfigServiceFactory = ({
});
}
+ if (oidcCfg.allowedEmailDomains) {
+ const allowedDomains = oidcCfg.allowedEmailDomains.split(", ");
+ if (!allowedDomains.includes(claims.email.split("@")[1])) {
+ throw new BadRequestError({
+ message: "Email not allowed."
+ });
+ }
+ }
+
oidcLogin({
email: claims.email,
externalId: claims.sub,
diff --git a/backend/src/ee/services/oidc/oidc-config-types.ts b/backend/src/ee/services/oidc/oidc-config-types.ts
index 4c729ceea..f773bbe92 100644
--- a/backend/src/ee/services/oidc/oidc-config-types.ts
+++ b/backend/src/ee/services/oidc/oidc-config-types.ts
@@ -22,6 +22,7 @@ export type TGetOidcCfgDTO =
export type TCreateOidcCfgDTO = {
issuer: string;
authorizationEndpoint: string;
+ allowedEmailDomains: string;
jwksUri: string;
tokenEndpoint: string;
userinfoEndpoint: string;
@@ -34,6 +35,7 @@ export type TCreateOidcCfgDTO = {
export type TUpdateOidcCfgDTO = Partial<{
issuer: string;
authorizationEndpoint: string;
+ allowedEmailDomains: string;
jwksUri: string;
tokenEndpoint: string;
userinfoEndpoint: string;
diff --git a/frontend/src/hooks/api/oidcConfig/mutations.tsx b/frontend/src/hooks/api/oidcConfig/mutations.tsx
index bfcf92d1f..39467e84c 100644
--- a/frontend/src/hooks/api/oidcConfig/mutations.tsx
+++ b/frontend/src/hooks/api/oidcConfig/mutations.tsx
@@ -13,11 +13,13 @@ export const useUpdateOIDCConfig = () => {
jwksUri,
tokenEndpoint,
userinfoEndpoint,
+ allowedEmailDomains,
clientId,
clientSecret,
isActive,
orgSlug
}: {
+ allowedEmailDomains?: string;
issuer?: string;
authorizationEndpoint?: string;
jwksUri?: string;
@@ -30,6 +32,7 @@ export const useUpdateOIDCConfig = () => {
}) => {
const { data } = await apiRequest.patch("/api/v1/sso/oidc/config", {
issuer,
+ allowedEmailDomains,
authorizationEndpoint,
jwksUri,
tokenEndpoint,
@@ -54,6 +57,7 @@ export const useCreateOIDCConfig = () => {
mutationFn: async ({
issuer,
authorizationEndpoint,
+ allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
@@ -71,10 +75,12 @@ export const useCreateOIDCConfig = () => {
clientSecret: string;
isActive: boolean;
orgSlug: string;
+ allowedEmailDomains?: string;
}) => {
const { data } = await apiRequest.post("/api/v1/sso/oidc/config", {
issuer,
authorizationEndpoint,
+ allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
diff --git a/frontend/src/hooks/api/oidcConfig/types.ts b/frontend/src/hooks/api/oidcConfig/types.ts
index 579590e09..457a86cb7 100644
--- a/frontend/src/hooks/api/oidcConfig/types.ts
+++ b/frontend/src/hooks/api/oidcConfig/types.ts
@@ -9,4 +9,5 @@ export type OIDCConfigData = {
orgId: string;
clientId: string;
clientSecret: string;
+ allowedEmailDomains?: string;
};
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OIDCModal.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OIDCModal.tsx
index 55b7924a0..229f437f9 100644
--- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OIDCModal.tsx
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OIDCModal.tsx
@@ -23,7 +23,8 @@ const schema = z.object({
tokenEndpoint: z.string().min(1),
userinfoEndpoint: z.string().min(1),
clientId: z.string().min(1),
- clientSecret: z.string().min(1)
+ clientSecret: z.string().min(1),
+ allowedEmailDomains: z.string().optional()
});
export type OIDCFormData = z.infer;
@@ -48,12 +49,14 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
setValue("userinfoEndpoint", data.userinfoEndpoint);
setValue("clientId", data.clientId);
setValue("clientSecret", data.clientSecret);
+ setValue("allowedEmailDomains", data.allowedEmailDomains);
}
}, [data]);
const onOIDCModalSubmit = async ({
issuer,
authorizationEndpoint,
+ allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
@@ -67,6 +70,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
await createMutateAsync({
issuer,
authorizationEndpoint,
+ allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
@@ -79,6 +83,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
await updateMutateAsync({
issuer,
authorizationEndpoint,
+ allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
@@ -187,6 +192,19 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
)}
/>
+ (
+
+
+
+ )}
+ />
Date: Wed, 19 Jun 2024 13:46:17 +0800
Subject: [PATCH 019/302] misc: added option for trusting OIDC emails by
default
---
.../20240617041053_add-oidc-auth.ts | 16 +++++++++
backend/src/db/schemas/super-admin.ts | 3 +-
.../ee/services/oidc/oidc-config-service.ts | 18 ++++++++--
backend/src/server/routes/v1/admin-router.ts | 3 +-
frontend/src/hooks/api/admin/types.ts | 1 +
.../admin/DashboardPage/DashboardPage.tsx | 35 +++++++++++++++----
6 files changed, 65 insertions(+), 11 deletions(-)
diff --git a/backend/src/db/migrations/20240617041053_add-oidc-auth.ts b/backend/src/db/migrations/20240617041053_add-oidc-auth.ts
index 8ef34854b..b74f0040a 100644
--- a/backend/src/db/migrations/20240617041053_add-oidc-auth.ts
+++ b/backend/src/db/migrations/20240617041053_add-oidc-auth.ts
@@ -24,8 +24,24 @@ export async function up(knex: Knex): Promise {
tb.foreign("orgId").references("id").inTable(TableName.Organization);
});
}
+
+ if (await knex.schema.hasTable(TableName.SuperAdmin)) {
+ if (!(await knex.schema.hasColumn(TableName.SuperAdmin, "trustOidcEmails"))) {
+ await knex.schema.alterTable(TableName.SuperAdmin, (tb) => {
+ tb.boolean("trustOidcEmails").defaultTo(false);
+ });
+ }
+ }
}
export async function down(knex: Knex): Promise {
await knex.schema.dropTableIfExists(TableName.OidcConfig);
+
+ if (await knex.schema.hasTable(TableName.SuperAdmin)) {
+ if (await knex.schema.hasColumn(TableName.SuperAdmin, "trustOidcEmails")) {
+ await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
+ t.dropColumn("trustOidcEmails");
+ });
+ }
+ }
}
diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts
index 417d4e05e..87ba35c83 100644
--- a/backend/src/db/schemas/super-admin.ts
+++ b/backend/src/db/schemas/super-admin.ts
@@ -16,7 +16,8 @@ export const SuperAdminSchema = z.object({
allowedSignUpDomain: z.string().nullable().optional(),
instanceId: z.string().uuid().default("00000000-0000-0000-0000-000000000000"),
trustSamlEmails: z.boolean().default(false).nullable().optional(),
- trustLdapEmails: z.boolean().default(false).nullable().optional()
+ trustLdapEmails: z.boolean().default(false).nullable().optional(),
+ trustOidcEmails: z.boolean().default(false).nullable().optional()
});
export type TSuperAdmin = z.infer;
diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts
index 3f58036b6..546de7d9d 100644
--- a/backend/src/ee/services/oidc/oidc-config-service.ts
+++ b/backend/src/ee/services/oidc/oidc-config-service.ts
@@ -25,6 +25,7 @@ import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
+import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
@@ -144,6 +145,7 @@ export const oidcConfigServiceFactory = ({
};
const oidcLogin = async ({ externalId, email, firstName, lastName, orgId, callbackPort }: TOidcLoginDTO) => {
+ const serverCfg = await getServerCfg();
const appCfg = getConfig();
const userAlias = await userAliasDAL.findOne({
externalId,
@@ -192,14 +194,25 @@ export const oidcConfigServiceFactory = ({
} else {
user = await userDAL.transaction(async (tx) => {
let newUser: TUsers | undefined;
+
+ if (serverCfg.trustOidcEmails) {
+ newUser = await userDAL.findOne(
+ {
+ email,
+ isEmailVerified: true
+ },
+ tx
+ );
+ }
+
if (!newUser) {
const uniqueUsername = await normalizeUsername(externalId, userDAL);
newUser = await userDAL.create(
{
email,
firstName,
- isEmailVerified: false,
- username: uniqueUsername,
+ isEmailVerified: serverCfg.trustOidcEmails,
+ username: serverCfg.trustOidcEmails ? email : uniqueUsername,
lastName,
authMethods: [],
isGhost: false
@@ -252,6 +265,7 @@ export const oidcConfigServiceFactory = ({
return newUser;
});
}
+
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const isUserCompleted = Boolean(user.isAccepted);
diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts
index 572409d9b..d8f64b86f 100644
--- a/backend/src/server/routes/v1/admin-router.ts
+++ b/backend/src/server/routes/v1/admin-router.ts
@@ -51,7 +51,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
allowSignUp: z.boolean().optional(),
allowedSignUpDomain: z.string().optional().nullable(),
trustSamlEmails: z.boolean().optional(),
- trustLdapEmails: z.boolean().optional()
+ trustLdapEmails: z.boolean().optional(),
+ trustOidcEmails: z.boolean().optional()
}),
response: {
200: z.object({
diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts
index 6a42e6ed0..80025ab8f 100644
--- a/frontend/src/hooks/api/admin/types.ts
+++ b/frontend/src/hooks/api/admin/types.ts
@@ -5,6 +5,7 @@ export type TServerConfig = {
isMigrationModeOn?: boolean;
trustSamlEmails: boolean;
trustLdapEmails: boolean;
+ trustOidcEmails: boolean;
isSecretScanningDisabled: boolean;
};
diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx
index 52fddb22c..c5f7db906 100644
--- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx
+++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx
@@ -39,7 +39,8 @@ const formSchema = z.object({
signUpMode: z.nativeEnum(SignUpModes),
allowedSignUpDomain: z.string().optional().nullable(),
trustSamlEmails: z.boolean(),
- trustLdapEmails: z.boolean()
+ trustLdapEmails: z.boolean(),
+ trustOidcEmails: z.boolean()
});
type TDashboardForm = z.infer;
@@ -60,7 +61,8 @@ export const AdminDashboardPage = () => {
signUpMode: config.allowSignUp ? SignUpModes.Anyone : SignUpModes.Disabled,
allowedSignUpDomain: config.allowedSignUpDomain,
trustSamlEmails: config.trustSamlEmails,
- trustLdapEmails: config.trustLdapEmails
+ trustLdapEmails: config.trustLdapEmails,
+ trustOidcEmails: config.trustOidcEmails
}
});
@@ -84,13 +86,15 @@ export const AdminDashboardPage = () => {
const onFormSubmit = async (formData: TDashboardForm) => {
try {
- const { signUpMode, allowedSignUpDomain, trustSamlEmails, trustLdapEmails } = formData;
+ const { signUpMode, allowedSignUpDomain, trustSamlEmails, trustLdapEmails, trustOidcEmails } =
+ formData;
await updateServerConfig({
allowSignUp: signUpMode !== SignUpModes.Disabled,
allowedSignUpDomain: signUpMode === SignUpModes.Anyone ? allowedSignUpDomain : null,
trustSamlEmails,
- trustLdapEmails
+ trustLdapEmails,
+ trustOidcEmails
});
createNotification({
text: "Successfully changed sign up setting.",
@@ -190,9 +194,9 @@ export const AdminDashboardPage = () => {
Trust emails
- Select if you want Infisical to trust external emails from SAML/LDAP identity
- providers. If set to false, then Infisical will prompt SAML/LDAP provisioned
- users to verify their email upon their first login.
+ Select if you want Infisical to trust external emails from SAML/LDAP/OIDC
+ identity providers. If set to false, then Infisical will prompt SAML/LDAP
+ provisioned users to verify their email upon their first login.
{
);
}}
/>
+ {
+ return (
+
+ field.onChange(value)}
+ isChecked={field.value}
+ >
+ Trust OIDC emails
+
+
+ );
+ }}
+ />
);
},
diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx
index 17f32369c..8943f2940 100644
--- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx
+++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx
@@ -23,7 +23,6 @@ import { AddOrgMemberModal } from "./AddOrgMemberModal";
import { OrgMembersTable } from "./OrgMembersTable";
export const OrgMembersSection = () => {
-
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id ?? "";
@@ -39,9 +38,13 @@ export const OrgMembersSection = () => {
const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
- const isMoreUsersNotAllowed = subscription?.memberLimit
- ? subscription.membersUsed >= subscription.memberLimit
- : false;
+ const isMoreUsersAllowed = subscription?.memberLimit
+ ? subscription.membersUsed < subscription.memberLimit
+ : true;
+
+ const isMoreIdentitiesAllowed = subscription?.identityLimit
+ ? subscription.identitiesUsed < subscription.identityLimit
+ : true;
const handleAddMemberModal = () => {
if (currentOrg?.authEnforced) {
@@ -52,13 +55,14 @@ export const OrgMembersSection = () => {
return;
}
- if (isMoreUsersNotAllowed) {
+ if (!isMoreUsersAllowed || !isMoreIdentitiesAllowed) {
handlePopUpOpen("upgradePlan", {
description: "You can add more members if you upgrade your Infisical plan."
});
- } else {
- handlePopUpOpen("addMember");
+ return;
}
+
+ handlePopUpOpen("addMember");
};
const onRemoveMemberSubmit = async (orgMembershipId: string) => {
From d64e2fa2432868bc0868873a07283fe1595fd0af Mon Sep 17 00:00:00 2001
From: Sheen Capadngan