From fc7015de83e533c418f5dc1ad14140bdb5d5d6bb Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 6 Feb 2024 15:51:24 -0800 Subject: [PATCH] Add lockout-preventative step in saml config setup, add update org slug section in org settings, revise navigate to org flow to account for org-level auth enforced orgs --- .../20240204171758_org-based-auth.ts | 29 ++++--- backend/src/db/schemas/organizations.ts | 2 +- backend/src/db/schemas/saml-configs.ts | 3 +- backend/src/ee/routes/v1/saml-router.ts | 11 +-- .../ee/services/permission/permission-dal.ts | 6 +- .../services/permission/permission-service.ts | 21 ++--- .../services/saml-config/saml-config-dal.ts | 23 ++++- .../saml-config/saml-config-service.ts | 43 +++++----- .../server/plugins/auth/inject-permission.ts | 4 +- .../server/routes/v1/organization-router.ts | 22 +++-- .../src/services/auth/auth-login-service.ts | 40 ++++----- .../identity-ua/identity-ua-service.ts | 51 +++++++++-- backend/src/services/org/org-service.ts | 23 +++-- backend/src/services/org/org-types.ts | 6 ++ .../OrganizationContext.tsx | 2 +- frontend/src/hooks/api/organization/index.ts | 2 +- .../src/hooks/api/organization/queries.tsx | 22 +++-- frontend/src/hooks/api/organization/types.ts | 8 +- frontend/src/layouts/AppLayout/AppLayout.tsx | 19 ++++- frontend/src/views/Login/Login.tsx | 1 - frontend/src/views/Login/Login.utils.tsx | 10 ++- frontend/src/views/Org/NonePage/NonePage.tsx | 8 +- .../components/OrgAuthTab/OrgAuthTab.tsx | 2 + .../OrgAuthTab/OrgGeneralAuthSection.tsx | 74 ++++++++++++++++ .../components/OrgAuthTab/OrgSSOSection.tsx | 5 ++ .../OrgGeneralTab/OrgGeneralTab.tsx | 4 +- .../OrgNameChangeSection.tsx | 8 +- .../OrgSlugChangeSection.tsx | 84 +++++++++++++++++++ .../components/OrgSlugChangeSection/index.tsx | 1 + 29 files changed, 409 insertions(+), 125 deletions(-) create mode 100644 frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx create mode 100644 frontend/src/views/Settings/OrgSettingsPage/components/OrgSlugChangeSection/OrgSlugChangeSection.tsx create mode 100644 frontend/src/views/Settings/OrgSettingsPage/components/OrgSlugChangeSection/index.tsx diff --git a/backend/src/db/migrations/20240204171758_org-based-auth.ts b/backend/src/db/migrations/20240204171758_org-based-auth.ts index 63a377879b..27a1486778 100644 --- a/backend/src/db/migrations/20240204171758_org-based-auth.ts +++ b/backend/src/db/migrations/20240204171758_org-based-auth.ts @@ -1,24 +1,23 @@ import { Knex } from "knex"; + import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { - await knex.schema.alterTable(TableName.Organization, (t) => { - t.boolean("authEnabled").defaultTo(false); - }); + await knex.schema.alterTable(TableName.Organization, (t) => { + t.boolean("authEnforced").defaultTo(false); + }); - await knex(TableName.Organization) - .whereIn( - "id", - knex(TableName.SamlConfig) - .select("orgId") - .where("isActive", true) - ) - .update({ authEnabled: true }); + await knex.schema.alterTable(TableName.SamlConfig, (t) => { + t.datetime("lastUsed"); + }); } export async function down(knex: Knex): Promise { - await knex.schema.alterTable(TableName.Organization, (t) => { - t.dropColumn("authEnabled"); - }); -} + await knex.schema.alterTable(TableName.Organization, (t) => { + t.dropColumn("authEnforced"); + }); + await knex.schema.alterTable(TableName.SamlConfig, (t) => { + t.dropColumn("lastUsed"); + }); +} diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index 32f9d356c5..087a1b7e01 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -14,7 +14,7 @@ export const OrganizationsSchema = z.object({ slug: z.string(), createdAt: z.date(), updatedAt: z.date(), - authEnabled: z.boolean().default(false).nullable().optional(), + authEnforced: z.boolean().default(false).nullable().optional() }); export type TOrganizations = z.infer; diff --git a/backend/src/db/schemas/saml-configs.ts b/backend/src/db/schemas/saml-configs.ts index 4633384b9e..6891d8adde 100644 --- a/backend/src/db/schemas/saml-configs.ts +++ b/backend/src/db/schemas/saml-configs.ts @@ -22,7 +22,8 @@ export const SamlConfigsSchema = z.object({ certTag: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), - orgId: z.string().uuid() + orgId: z.string().uuid(), + lastUsed: z.date().nullable().optional() }); export type TSamlConfigs = z.infer; diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index 88c127c424..a946b3f7bf 100644 --- a/backend/src/ee/routes/v1/saml-router.ts +++ b/backend/src/ee/routes/v1/saml-router.ts @@ -45,19 +45,19 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { getSamlOptions: async (req, done) => { try { const { samlConfigId, orgSlug } = req.params; - + let ssoLookupDetails: TGetSamlCfgDTO; - + if (orgSlug) { ssoLookupDetails = { type: "orgSlug", orgSlug - } + }; } else if (samlConfigId) { ssoLookupDetails = { type: "ssoId", id: samlConfigId - } + }; } else { throw new BadRequestError({ message: "Missing sso identitier or org slug" }); } @@ -215,7 +215,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { isActive: z.boolean(), entryPoint: z.string(), issuer: z.string(), - cert: z.string() + cert: z.string(), + lastUsed: z.date().nullable().optional() }) .optional() } diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index d3e9bab655..ea195bc06e 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -13,7 +13,7 @@ export const permissionDALFactory = (db: TDbClient) => { .join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`) .where("userId", userId) .where(`${TableName.OrgMembership}.orgId`, orgId) - .select(db.ref("authEnabled").withSchema(TableName.Organization).as("orgAuthEnabled")) + .select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced")) .select("permissions") .select(selectAllTableCols(TableName.OrgMembership)) .first(); @@ -32,7 +32,7 @@ export const permissionDALFactory = (db: TDbClient) => { .where("identityId", identityId) .where(`${TableName.IdentityOrgMembership}.orgId`, orgId) .select(selectAllTableCols(TableName.IdentityOrgMembership)) - .select(db.ref("authEnabled").withSchema(TableName.Organization).as("orgAuthEnabled")) + .select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced")) .select("permissions") .first(); return membership; @@ -51,7 +51,7 @@ export const permissionDALFactory = (db: TDbClient) => { .where(`${TableName.ProjectMembership}.projectId`, projectId) .select(selectAllTableCols(TableName.ProjectMembership)) .select( - db.ref("authEnabled").withSchema(TableName.Organization).as("orgAuthEnabled"), + db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("orgId").withSchema(TableName.Project) ) .select("permissions") diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index f4627321bc..fa5741cb2c 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -100,21 +100,18 @@ export const permissionServiceFactory = ({ if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { throw new BadRequestError({ name: "Custom permission not found" }); } - if (membership.orgAuthEnabled && membership.orgId !== orgScope) { + if (membership.orgAuthEnforced && membership.orgId !== orgScope) { throw new BadRequestError({ name: "Cannot access org-scoped resource" }); } return { permission: buildOrgPermission(membership.role, membership.permissions), membership }; }; - const getIdentityOrgPermission = async (identityId: string, orgId: string, orgScope?: string) => { + const getIdentityOrgPermission = async (identityId: string, orgId: string) => { const membership = await permissionDAL.getOrgIdentityPermission(identityId, orgId); if (!membership) throw new UnauthorizedError({ name: "Identity not in org" }); if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { throw new BadRequestError({ name: "Custom permission not found" }); } - if (membership.orgAuthEnabled && membership.orgId !== orgScope) { - throw new BadRequestError({ name: "Cannot access org-scoped resource" }); - } return { permission: buildOrgPermission(membership.role, membership.permissions), membership }; }; @@ -123,7 +120,7 @@ export const permissionServiceFactory = ({ case ActorType.USER: return getUserOrgPermission(id, orgId, orgScope); case ActorType.IDENTITY: - return getIdentityOrgPermission(id, orgId, orgScope); + return getIdentityOrgPermission(id, orgId); default: throw new UnauthorizedError({ message: "Permission not defined", @@ -154,9 +151,11 @@ export const permissionServiceFactory = ({ if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) { throw new BadRequestError({ name: "Custom permission not found" }); } - if (membership.orgAuthEnabled && membership.orgId !== orgScope) { + + if (membership.orgAuthEnforced && membership.orgId !== orgScope) { throw new BadRequestError({ name: "Cannot access org-scoped resource" }); } + return { permission: buildProjectPermission(membership.role, membership.permissions), membership @@ -169,6 +168,7 @@ export const permissionServiceFactory = ({ if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) { throw new BadRequestError({ name: "Custom permission not found" }); } + return { permission: buildProjectPermission(membership.role, membership.permissions), membership @@ -193,11 +193,12 @@ export const permissionServiceFactory = ({ : { permission: MongoAbility; membership: (T extends ActorType.USER ? TProjectMemberships : TIdentityProjectMemberships) & { + orgAuthEnforced: boolean; + orgId: string; permissions?: unknown; }; }; - // TODO: add support for org scope here const getProjectPermission = async ( type: T, id: string, @@ -207,9 +208,9 @@ export const permissionServiceFactory = ({ switch (type) { case ActorType.USER: return getUserProjectPermission(id, projectId, orgScope) as Promise>; - case ActorType.SERVICE: // how to handle org-scope case here? + case ActorType.SERVICE: return getServiceTokenProjectPermission(id, projectId) as Promise>; - case ActorType.IDENTITY: // how to handle org-scope case here? + case ActorType.IDENTITY: return getIdentityProjectPermission(id, projectId) as Promise>; default: throw new UnauthorizedError({ diff --git a/backend/src/ee/services/saml-config/saml-config-dal.ts b/backend/src/ee/services/saml-config/saml-config-dal.ts index 95f6828bcc..1e7b9e47e8 100644 --- a/backend/src/ee/services/saml-config/saml-config-dal.ts +++ b/backend/src/ee/services/saml-config/saml-config-dal.ts @@ -1,10 +1,31 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; export type TSamlConfigDALFactory = ReturnType; export const samlConfigDALFactory = (db: TDbClient) => { const samlCfgOrm = ormify(db, TableName.SamlConfig); - return samlCfgOrm; + + const findEnforceableSamlCfg = async (orgId: string) => { + try { + const samlCfg = await db(TableName.SamlConfig) + .where({ + orgId, + isActive: true + }) + .whereNotNull("lastUsed") + .first(); + + return samlCfg; + } catch (error) { + throw new DatabaseError({ error, name: "Find org by id" }); + } + }; + + return { + ...samlCfgOrm, + findEnforceableSamlCfg + }; }; diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index cc58d77000..2e6b9e50f9 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -18,7 +18,7 @@ import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; -import { AuthTokenType } from "@app/services/auth/auth-type"; +import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TUserDALFactory } from "@app/services/user/user-dal"; @@ -27,18 +27,15 @@ import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { TSamlConfigDALFactory } from "./saml-config-dal"; -import { - SamlProviders, - TCreateSamlCfgDTO, - TGetSamlCfgDTO, - TSamlLoginDTO, - TUpdateSamlCfgDTO -} from "./saml-config-types"; +import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } from "./saml-config-types"; type TSamlConfigServiceFactoryDep = { samlConfigDAL: TSamlConfigDALFactory; userDAL: Pick; - orgDAL: Pick; + orgDAL: Pick< + TOrgDALFactory, + "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" + >; orgBotDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -141,7 +138,6 @@ export const samlConfigServiceFactory = ({ certIV, certTag }); - await orgDAL.updateById(orgId, { authEnabled: isActive }); return samlConfig; }; @@ -166,7 +162,7 @@ export const samlConfigServiceFactory = ({ "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." }); - const updateQuery: TSamlConfigsUpdate = { authProvider, isActive }; + const updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null }; const orgBot = await orgBotDAL.findOne({ orgId }); if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); const key = infisicalSymmetricDecrypt({ @@ -199,8 +195,8 @@ export const samlConfigServiceFactory = ({ updateQuery.certTag = certTag; } const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery); - await orgDAL.updateById(orgId, { authEnabled: isActive }); - + await orgDAL.updateById(orgId, { authEnforced: false }); + return ssoConfig; }; @@ -237,7 +233,12 @@ export const samlConfigServiceFactory = ({ // when dto is type id means it's internally used if (dto.type === "org") { - const { permission } = await permissionService.getOrgPermission(dto.actor, dto.actorId, ssoConfig.orgId, dto.actorOrgScope); + const { permission } = await permissionService.getOrgPermission( + dto.actor, + dto.actorId, + ssoConfig.orgId, + dto.actorOrgScope + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); } const { @@ -294,7 +295,8 @@ export const samlConfigServiceFactory = ({ isActive: ssoConfig.isActive, entryPoint, issuer, - cert + cert, + lastUsed: ssoConfig.lastUsed }; }; @@ -316,13 +318,7 @@ export const samlConfigServiceFactory = ({ if (!organization) throw new BadRequestError({ message: "Org not found" }); if (user) { - const hasSamlEnabled = (user.authMethods || []).some((method) => - Object.values(SamlProviders).includes(method as SamlProviders) - ); await userDAL.transaction(async (tx) => { - if (!hasSamlEnabled) { - await userDAL.updateById(user.id, { authMethods: [authProvider] }, tx); - } const [orgMembership] = await orgDAL.findMembership({ userId: user.id, orgId }, { tx }); if (!orgMembership) { await orgDAL.createMembership( @@ -352,7 +348,7 @@ export const samlConfigServiceFactory = ({ email, firstName, lastName, - authMethods: [authProvider] + authMethods: [AuthMethod.EMAIL] }, tx ); @@ -388,6 +384,9 @@ export const samlConfigServiceFactory = ({ expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME } ); + + await samlConfigDAL.update({ orgId }, { lastUsed: new Date() }); + return { isUserCompleted, providerAuthToken }; }; diff --git a/backend/src/server/plugins/auth/inject-permission.ts b/backend/src/server/plugins/auth/inject-permission.ts index a42076ad94..572814d645 100644 --- a/backend/src/server/plugins/auth/inject-permission.ts +++ b/backend/src/server/plugins/auth/inject-permission.ts @@ -11,9 +11,9 @@ export const injectPermission = fp(async (server) => { if (req.auth.actor === ActorType.USER) { req.permission = { type: ActorType.USER, id: req.auth.userId, orgId: req.auth?.orgId }; } else if (req.auth.actor === ActorType.IDENTITY) { - req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId, orgId: undefined }; + req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId }; } else if (req.auth.actor === ActorType.SERVICE) { - req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId, orgId: undefined }; + req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId }; } }); }); diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index e66e6e2c2f..0fac8ba481 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -83,10 +83,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "PATCH", - url: "/:organizationId/name", + url: "/:organizationId", schema: { params: z.object({ organizationId: z.string().trim() }), - body: z.object({ name: z.string().trim() }), + body: z.object({ + name: z.string().trim().optional(), + slug: z.string().trim().optional(), + authEnforced: z.boolean().optional() + }), response: { 200: z.object({ message: z.string(), @@ -96,12 +100,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const organization = await server.services.org.updateOrgName( - req.permission.id, - req.params.organizationId, - req.body.name, - req.permission.orgId - ); + const organization = await server.services.org.updateOrg({ + actor: req.permission.type, + actorId: req.permission.id, + actorOrgScope: req.permission.orgId, + orgId: req.params.organizationId, + data: req.body + }); + return { message: "Successfully changed organization name", organization diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 3ef9865c4a..6e4d60bbab 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -56,13 +56,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: * Private * Send mfa code via email * */ - const sendUserMfaCode = async ({ - userId, - email - }: { - userId: string; - email: string; - }) => { + const sendUserMfaCode = async ({ userId, email }: { userId: string; email: string }) => { const code = await tokenService.createTokenForUser({ type: TokenType.TOKEN_EMAIL_MFA, userId @@ -171,6 +165,10 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) { const { orgId } = validateProviderAuthToken(providerAuthToken as string, email); organizationId = orgId; + } else if (providerAuthToken) { + // SAML SSO + const { orgId } = validateProviderAuthToken(providerAuthToken, email); + organizationId = orgId; } if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?"); @@ -189,22 +187,26 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: }); // send multi factor auth token if they it enabled if (userEnc.isMfaEnabled) { - const mfaToken = jwt.sign({ - authTokenType: AuthTokenType.MFA_TOKEN, - userId: userEnc.userId, - organizationId - }, cfg.AUTH_SECRET, { - expiresIn: cfg.JWT_MFA_LIFETIME - }); - + const mfaToken = jwt.sign( + { + authTokenType: AuthTokenType.MFA_TOKEN, + userId: userEnc.userId, + organizationId + }, + cfg.AUTH_SECRET, + { + expiresIn: cfg.JWT_MFA_LIFETIME + } + ); + await sendUserMfaCode({ - userId: userEnc.userId, + userId: userEnc.userId, email: userEnc.email }); return { isMfaEnabled: true, token: mfaToken } as const; } - + const token = await generateUserTokens({ user: { ...userEnc, @@ -214,7 +216,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: userAgent, organizationId }); - + return { token, isMfaEnabled: false, user: userEnc } as const; }; @@ -227,7 +229,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: if (!user) return; await sendUserMfaCode({ userId: user.id, - email: user.email, + email: user.email }); }; diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 7f58ed302d..4ae5257ba2 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -127,6 +127,7 @@ export const identityUaServiceFactory = ({ expiresIn: identityAccessToken.accessTokenMaxTTL === 0 ? undefined : identityAccessToken.accessTokenMaxTTL } ); + return { accessToken, identityUa, validClientSecretInfo, identityAccessToken }; }; @@ -152,7 +153,12 @@ export const identityUaServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorOrgScope + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); @@ -241,7 +247,12 @@ export const identityUaServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorOrgScope + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); @@ -302,7 +313,12 @@ export const identityUaServiceFactory = ({ const uaIdentityAuth = await identityUaDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorOrgScope + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId }; }; @@ -322,7 +338,12 @@ export const identityUaServiceFactory = ({ throw new BadRequestError({ message: "The identity does not have universal auth" }); - const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorOrgScope + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( @@ -369,7 +390,12 @@ export const identityUaServiceFactory = ({ throw new BadRequestError({ message: "The identity does not have universal auth" }); - const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorOrgScope + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( @@ -395,14 +421,25 @@ export const identityUaServiceFactory = ({ return { clientSecrets, orgId: identityMembershipOrg.orgId }; }; - const revokeUaClientSecret = async ({ identityId, actorId, actor, actorOrgScope, clientSecretId }: TRevokeUaClientSecretDTO) => { + const revokeUaClientSecret = async ({ + identityId, + actorId, + actor, + actorOrgScope, + clientSecretId + }: TRevokeUaClientSecretDTO) => { const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral) throw new BadRequestError({ message: "The identity does not have universal auth" }); - const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorOrgScope + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 0fbfaaea4e..7ca2531e5e 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -29,6 +29,7 @@ import { TDeleteOrgMembershipDTO, TFindAllWorkspacesDTO, TInviteUserToOrgDTO, + TUpdateOrgDTO, TUpdateOrgMembershipDTO, TVerifyUserToOrgDTO } from "./org-types"; @@ -40,7 +41,7 @@ type TOrgServiceFactoryDep = { userDAL: TUserDALFactory; projectDAL: TProjectDALFactory; incidentContactDAL: TIncidentContactsDALFactory; - samlConfigDAL: Pick; + samlConfigDAL: Pick; smtpService: TSmtpService; tokenService: TAuthTokenServiceFactory; permissionService: TPermissionServiceFactory; @@ -118,12 +119,22 @@ export const orgServiceFactory = ({ }; /* - * Update organization settings + * Update organization details * */ - const updateOrgName = async (userId: string, orgId: string, name: string, actorOrgScope?: string) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope); + const updateOrg = async ({ actor, actorId, actorOrgScope, orgId, data }: TUpdateOrgDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); - const org = await orgDAL.updateById(orgId, { name }); + + if (data.authEnforced) { + const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId); + if (!samlCfg) + throw new BadRequestError({ + name: "No enforceable SAML config found", + message: "No enforceable SAML config found" + }); + } + + const org = await orgDAL.updateById(orgId, data); if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" }); return org; }; @@ -443,7 +454,7 @@ export const orgServiceFactory = ({ findAllOrganizationOfUser, inviteUserToOrganization, verifyUserToOrg, - updateOrgName, + updateOrg, createOrganization, deleteOrganizationById, deleteOrgMembership, diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index 4a59069698..195626b7ce 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -1,3 +1,5 @@ +import { TOrgPermission } from "@app/lib/types"; + import { ActorType } from "../auth/auth-type"; export type TUpdateOrgMembershipDTO = { @@ -34,3 +36,7 @@ export type TFindAllWorkspacesDTO = { actorOrgScope?: string; orgId: string; }; + +export type TUpdateOrgDTO = { + data: Partial<{ name: string; slug: string; authEnforced: boolean }>; +} & TOrgPermission; diff --git a/frontend/src/context/OrganizationContext/OrganizationContext.tsx b/frontend/src/context/OrganizationContext/OrganizationContext.tsx index a4562ef820..e73578d135 100644 --- a/frontend/src/context/OrganizationContext/OrganizationContext.tsx +++ b/frontend/src/context/OrganizationContext/OrganizationContext.tsx @@ -25,7 +25,7 @@ export const OrgProvider = ({ children }: Props): JSX.Element => { const value = useMemo( () => ({ orgs: userOrgs, - currentOrg: (userOrgs || []).find(({ id }) => id === currentWsOrgID) || (userOrgs || [])[0], + currentOrg: (userOrgs || []).find(({ id }) => id === currentWsOrgID), isLoading }), [currentWsOrgID, userOrgs, isLoading] diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index c8a284836b..6a8af3b677 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -17,6 +17,6 @@ export { useGetOrgPmtMethods, useGetOrgTaxIds, useGetOrgTrialUrl, - useRenameOrg, + useUpdateOrg, useUpdateOrgBillingDetails } from "./queries"; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 0edaf2c2f8..018fa1edb0 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -12,8 +12,8 @@ import { PlanBillingInfo, PmtMethod, ProductsTable, - RenameOrgDTO, - TaxID + TaxID, + UpdateOrgDTO } from "./types"; export const organizationKeys = { @@ -65,12 +65,20 @@ export const useCreateOrg = () => { }); }; -export const useRenameOrg = () => { +export const useUpdateOrg = () => { const queryClient = useQueryClient(); - - return useMutation<{}, {}, RenameOrgDTO>({ - mutationFn: ({ newOrgName, orgId }) => { - return apiRequest.patch(`/api/v1/organization/${orgId}/name`, { name: newOrgName }); + return useMutation<{}, {}, UpdateOrgDTO>({ + mutationFn: ({ + name, + authEnforced, + slug, + orgId + }) => { + return apiRequest.patch(`/api/v1/organization/${orgId}`, { + name, + authEnforced, + slug + }); }, onSuccess: () => { queryClient.invalidateQueries(organizationKeys.getUserOrganizations); diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index 3bfbd74191..6492e35157 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -3,11 +3,15 @@ export type Organization = { name: string; createAt: string; updatedAt: string; + authEnforced: boolean; + slug: string; }; -export type RenameOrgDTO = { +export type UpdateOrgDTO = { orgId: string; - newOrgName: string; + name?: string; + authEnforced?: boolean; + slug?: string; }; export type BillingDetails = { diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 3009c07264..7df0d9790f 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -310,10 +310,22 @@ export const AppLayout = ({ children }: LayoutProps) => {
{user?.email}
- {orgs?.map((org) => ( + {orgs?.map((org) => { + return ( - ))} + ) + })} {/* + )} + + + ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgSlugChangeSection/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgSlugChangeSection/index.tsx new file mode 100644 index 0000000000..a4218cbd33 --- /dev/null +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgSlugChangeSection/index.tsx @@ -0,0 +1 @@ +export { OrgSlugChangeSection } from "./OrgSlugChangeSection"; \ No newline at end of file