diff --git a/backend/src/db/migrations/20241007202149_default-org-membership-roles.ts b/backend/src/db/migrations/20241007202149_default-org-membership-roles.ts new file mode 100644 index 000000000..d80853a8f --- /dev/null +++ b/backend/src/db/migrations/20241007202149_default-org-membership-roles.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + // org default role + if (await knex.schema.hasTable(TableName.Organization)) { + const hasDefaultRoleCol = await knex.schema.hasColumn(TableName.Organization, "defaultMembershipRole"); + + if (!hasDefaultRoleCol) { + await knex.schema.alterTable(TableName.Organization, (tb) => { + tb.string("defaultMembershipRole").notNullable().defaultTo("member"); + }); + } + } +} + +export async function down(knex: Knex): Promise { + // org default role + if (await knex.schema.hasTable(TableName.Organization)) { + const hasDefaultRoleCol = await knex.schema.hasColumn(TableName.Organization, "defaultMembershipRole"); + + if (hasDefaultRoleCol) { + await knex.schema.alterTable(TableName.Organization, (tb) => { + tb.dropColumn("defaultMembershipRole"); + }); + } + } +} diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index aa63423c9..7bd20d94d 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -19,7 +19,8 @@ export const OrganizationsSchema = z.object({ authEnforced: z.boolean().default(false).nullable().optional(), scimEnabled: z.boolean().default(false).nullable().optional(), kmsDefaultKeyId: z.string().uuid().nullable().optional(), - kmsEncryptedDataKey: zodBuffer.nullable().optional() + kmsEncryptedDataKey: zodBuffer.nullable().optional(), + defaultMembershipRole: z.string().default("member") }); export type TOrganizations = z.infer; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index ad2da3e23..7caaa5596 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -1,14 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; -import { - OrgMembershipRole, - OrgMembershipStatus, - SecretKeyEncoding, - TableName, - TLdapConfigsUpdate, - TUsers -} from "@app/db/schemas"; +import { OrgMembershipStatus, SecretKeyEncoding, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; @@ -28,6 +21,7 @@ import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; @@ -444,11 +438,14 @@ export const ldapConfigServiceFactory = ({ { tx } ); if (!orgMembership) { + const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); + await orgDAL.createMembership( { userId: userAlias.userId, orgId, - role: OrgMembershipRole.Member, + role, + roleId, status: OrgMembershipStatus.Accepted, isActive: true }, @@ -529,12 +526,15 @@ export const ldapConfigServiceFactory = ({ ); if (!orgMembership) { + const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); + await orgMembershipDAL.create( { userId: newUser.id, inviteEmail: email, orgId, - role: OrgMembershipRole.Member, + role, + roleId, status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later isActive: true }, diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index a8589b309..b68d5497f 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client"; -import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; +import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; @@ -23,6 +23,7 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; 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"; @@ -187,12 +188,15 @@ export const oidcConfigServiceFactory = ({ { tx } ); if (!orgMembership) { + const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); + await orgMembershipDAL.create( { userId: userAlias.userId, inviteEmail: email, orgId, - role: OrgMembershipRole.Member, + role, + roleId, status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later isActive: true }, @@ -261,12 +265,15 @@ export const oidcConfigServiceFactory = ({ ); if (!orgMembership) { + const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); + await orgMembershipDAL.create( { userId: newUser.id, inviteEmail: email, orgId, - role: OrgMembershipRole.Member, + role, + roleId, status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later isActive: true }, 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 0c979d023..69d3626f0 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -2,7 +2,6 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; import { - OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TableName, @@ -26,6 +25,7 @@ import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; 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"; @@ -369,12 +369,15 @@ export const samlConfigServiceFactory = ({ { tx } ); if (!orgMembership) { + const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); + await orgMembershipDAL.create( { userId: userAlias.userId, inviteEmail: email, orgId, - role: OrgMembershipRole.Member, + role, + roleId, status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later isActive: true }, @@ -472,12 +475,15 @@ export const samlConfigServiceFactory = ({ ); if (!orgMembership) { + const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); + await orgMembershipDAL.create( { userId: newUser.id, inviteEmail: email, orgId, - role: OrgMembershipRole.Member, + role, + roleId, status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later isActive: true }, diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index 732f37f42..8b94d5b93 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -16,6 +16,7 @@ import { AuthTokenType } from "@app/services/auth/auth-type"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { deleteOrgMembershipFn } from "@app/services/org/org-fns"; +import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; @@ -318,12 +319,15 @@ export const scimServiceFactory = ({ ); if (!orgMembership) { + const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole); + orgMembership = await orgMembershipDAL.create( { userId: userAlias.userId, inviteEmail: email, orgId, - role: OrgMembershipRole.NoAccess, + role, + roleId, status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later isActive: true }, @@ -391,12 +395,15 @@ export const scimServiceFactory = ({ orgMembership = foundOrgMembership; if (!orgMembership) { + const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole); + orgMembership = await orgMembershipDAL.create( { userId: user.id, inviteEmail: email, orgId, - role: OrgMembershipRole.Member, + role, + roleId, status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later isActive: true }, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 22eddad9e..e9af5043b 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -532,7 +532,7 @@ export const registerRoutes = async ( orgService, licenseService }); - const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDAL }); + const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDAL, orgDAL }); const superAdminService = superAdminServiceFactory({ userDAL, authService: loginService, diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index d19317c4b..0a863f742 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -1,3 +1,4 @@ +import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { @@ -217,7 +218,15 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { .regex(/^[a-zA-Z0-9-]+$/, "Slug must only contain alphanumeric characters or hyphens") .optional(), authEnforced: z.boolean().optional(), - scimEnabled: z.boolean().optional() + scimEnabled: z.boolean().optional(), + defaultMembershipRoleSlug: z + .string() + .min(1) + .trim() + .refine((v) => slugify(v) === v, { + message: "Membership role must be a valid slug" + }) + .optional() }), response: { 200: z.object({ diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index e236d527d..56134bd11 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -372,6 +372,7 @@ export const orgDALFactory = (db: TDbClient) => { db.ref("firstName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users), db.ref("scimEnabled").withSchema(TableName.Organization), + db.ref("defaultMembershipRole").withSchema(TableName.Organization), db.ref("externalId").withSchema(TableName.UserAliases) ) .where({ isGhost: false }); diff --git a/backend/src/services/org/org-role-fns.ts b/backend/src/services/org/org-role-fns.ts new file mode 100644 index 000000000..cccf255c6 --- /dev/null +++ b/backend/src/services/org/org-role-fns.ts @@ -0,0 +1,54 @@ +import { OrgMembershipRole } from "@app/db/schemas"; +import { TFeatureSet } from "@app/ee/services/license/license-types"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; + +const RESERVED_ORG_ROLE_SLUGS = Object.values(OrgMembershipRole).filter((role) => role !== "custom"); + +// this is only for updating an org +export const getDefaultOrgMembershipRoleForUpdateOrg = async ({ + membershipRoleSlug, + orgRoleDAL, + plan, + orgId +}: { + orgId: string; + membershipRoleSlug: string; + orgRoleDAL: TOrgRoleDALFactory; + plan: TFeatureSet; +}) => { + const isCustomRole = !RESERVED_ORG_ROLE_SLUGS.includes(membershipRoleSlug as OrgMembershipRole); + + if (isCustomRole) { + if (!plan?.rbac) + throw new BadRequestError({ + message: + "Failed to set custom default role due to plan RBAC restriction. Upgrade plan to set custom default org membership role." + }); + + const customRole = await orgRoleDAL.findOne({ slug: membershipRoleSlug, orgId }); + if (!customRole) throw new NotFoundError({ name: "UpdateOrg", message: "Organization role not found" }); + + // use ID for default role + return customRole.id; + } + + // not custom, use reserved slug + return membershipRoleSlug; +}; + +// this is only for creating an org membership +export const getDefaultOrgMembershipRole = async ( + defaultOrgMembershipRole: string // can either be ID or reserved slug +) => { + const isCustomRole = !RESERVED_ORG_ROLE_SLUGS.includes(defaultOrgMembershipRole as OrgMembershipRole); + + if (isCustomRole) + return { + roleId: defaultOrgMembershipRole, + role: OrgMembershipRole.Custom + }; + + // will be reserved slug + return { roleId: undefined, role: defaultOrgMembershipRole as OrgMembershipRole }; +}; diff --git a/backend/src/services/org/org-role-service.ts b/backend/src/services/org/org-role-service.ts index 023cbeccf..198a9ea85 100644 --- a/backend/src/services/org/org-role-service.ts +++ b/backend/src/services/org/org-role-service.ts @@ -11,6 +11,7 @@ import { } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; import { ActorAuthMethod } from "../auth/auth-type"; import { TOrgRoleDALFactory } from "./org-role-dal"; @@ -18,11 +19,12 @@ import { TOrgRoleDALFactory } from "./org-role-dal"; type TOrgRoleServiceFactoryDep = { orgRoleDAL: TOrgRoleDALFactory; permissionService: TPermissionServiceFactory; + orgDAL: TOrgDALFactory; }; export type TOrgRoleServiceFactory = ReturnType; -export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRoleServiceFactoryDep) => { +export const orgRoleServiceFactory = ({ orgRoleDAL, orgDAL, permissionService }: TOrgRoleServiceFactoryDep) => { const createRole = async ( userId: string, orgId: string, @@ -129,6 +131,19 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol ) => { const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role); + + const org = await orgDAL.findOrgById(orgId); + + if (!org) + throw new NotFoundError({ + message: "Failed to find organization" + }); + + if (org.defaultMembershipRole === roleId) + throw new BadRequestError({ + message: "Cannot delete default org membership role. Please re-assign and try again." + }); + const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId }); if (!deletedRole) throw new NotFoundError({ message: "Organization role not found", name: "Update role" }); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 4d6681669..fd9b8106b 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -32,6 +32,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro import { groupBy } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { isDisposableEmail } from "@app/lib/validator"; +import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; @@ -264,7 +265,7 @@ export const orgServiceFactory = ({ actorOrgId, actorAuthMethod, orgId, - data: { name, slug, authEnforced, scimEnabled } + data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug } }: TUpdateOrgDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); @@ -298,11 +299,22 @@ export const orgServiceFactory = ({ }); } + let defaultMembershipRole: string | undefined; + if (defaultMembershipRoleSlug) { + defaultMembershipRole = await getDefaultOrgMembershipRoleForUpdateOrg({ + membershipRoleSlug: defaultMembershipRoleSlug, + orgId, + orgRoleDAL, + plan + }); + } + const org = await orgDAL.updateById(orgId, { name, slug: slug ? slugify(slug) : undefined, authEnforced, - scimEnabled + scimEnabled, + defaultMembershipRole }); if (!org) throw new NotFoundError({ message: "Organization not found" }); return org; diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index 085226be0..9f1973349 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -63,7 +63,13 @@ export type TFindAllWorkspacesDTO = { }; export type TUpdateOrgDTO = { - data: Partial<{ name: string; slug: string; authEnforced: boolean; scimEnabled: boolean }>; + data: Partial<{ + name: string; + slug: string; + authEnforced: boolean; + scimEnabled: boolean; + defaultMembershipRoleSlug: string; + }>; } & TOrgPermission; export type TGetOrgGroupsDTO = TOrgPermission; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 670f39146..1de64e058 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -82,12 +82,13 @@ export const useCreateOrg = (options: { invalidate: boolean } = { invalidate: tr export const useUpdateOrg = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, UpdateOrgDTO>({ - mutationFn: ({ name, authEnforced, scimEnabled, slug, orgId }) => { + mutationFn: ({ name, authEnforced, scimEnabled, slug, orgId, defaultMembershipRoleSlug }) => { return apiRequest.patch(`/api/v1/organization/${orgId}`, { name, authEnforced, scimEnabled, - slug + slug, + defaultMembershipRoleSlug }); }, onSuccess: () => { diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index 6be780b77..3d8d5474b 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -10,6 +10,7 @@ export type Organization = { orgAuthMethod: string; scimEnabled: boolean; slug: string; + defaultMembershipRole: string; }; export type UpdateOrgDTO = { @@ -18,6 +19,7 @@ export type UpdateOrgDTO = { authEnforced?: boolean; scimEnabled?: boolean; slug?: string; + defaultMembershipRoleSlug?: string; }; export type BillingDetails = { diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx index 7194432c5..ad7098b45 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx @@ -1,3 +1,4 @@ +import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; import { faCheckCircle, @@ -33,6 +34,7 @@ import { import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { ProjectVersion } from "@app/hooks/api/workspace/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; +import { isCustomOrgRole } from "@app/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable"; import { OrgInviteLink } from "./OrgInviteLink"; @@ -78,7 +80,20 @@ export const AddOrgMemberModal = ({ watch, reset, formState: { isSubmitting } - } = useForm({ resolver: zodResolver(addMemberFormSchema) }); + } = useForm({ + resolver: zodResolver(addMemberFormSchema) + }); + + // set initial form role based off org default role + useEffect(() => { + if (organizationRoles) { + reset({ + organizationRoleSlug: isCustomOrgRole(currentOrg?.defaultMembershipRole!) + ? organizationRoles?.find((role) => role.id === currentOrg?.defaultMembershipRole)?.slug! + : currentOrg?.defaultMembershipRole + }); + } + }, [organizationRoles]); const selectedProjectIds = watch("projectIds", []); @@ -207,7 +222,6 @@ export const AddOrgMemberModal = ({
+ {roles?.map((role) => { + return ( + + {role.name} + + ); + })} + + + )} + /> +
+ )} {(isAllowed) => (