From 8490f5d558f37e4755746a27dadf81d0bf02748d Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 13 Apr 2024 07:01:15 -0600 Subject: [PATCH] Add security preference for workspace creation in cloud (#6801) --- app/models/Team.ts | 4 ++++ app/scenes/Settings/Security.tsx | 14 ++++++++++++++ .../20240413042634-member-team-create.js | 15 +++++++++++++++ server/models/Team.ts | 4 ++++ server/policies/team.ts | 7 ++++--- server/presenters/team.ts | 1 + server/routes/api/teams/schema.ts | 2 ++ shared/i18n/locales/en_US/translation.json | 2 ++ 8 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 server/migrations/20240413042634-member-team-create.js diff --git a/app/models/Team.ts b/app/models/Team.ts index 436fcbe2f..529a2ebf0 100644 --- a/app/models/Team.ts +++ b/app/models/Team.ts @@ -44,6 +44,10 @@ class Team extends Model { @observable memberCollectionCreate: boolean; + @Field + @observable + memberTeamCreate: boolean; + @Field @observable guestSignin: boolean; diff --git a/app/scenes/Settings/Security.tsx b/app/scenes/Settings/Security.tsx index d584d4463..60715cc1c 100644 --- a/app/scenes/Settings/Security.tsx +++ b/app/scenes/Settings/Security.tsx @@ -34,6 +34,7 @@ function Security() { guestSignin: team.guestSignin, defaultUserRole: team.defaultUserRole, memberCollectionCreate: team.memberCollectionCreate, + memberTeamCreate: team.memberTeamCreate, inviteRequired: team.inviteRequired, }); @@ -300,6 +301,19 @@ function Security() { onChange={handleChange} /> + {isCloudHosted && ( + + + + )} ); } diff --git a/server/migrations/20240413042634-member-team-create.js b/server/migrations/20240413042634-member-team-create.js new file mode 100644 index 000000000..ae5844d9c --- /dev/null +++ b/server/migrations/20240413042634-member-team-create.js @@ -0,0 +1,15 @@ +"use strict"; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn("teams", "memberTeamCreate", { + type: Sequelize.BOOLEAN, + defaultValue: true, + allowNull: false, + }); + }, + + down: async (queryInterface) => { + await queryInterface.removeColumn("teams", "memberTeamCreate"); + }, +}; diff --git a/server/models/Team.ts b/server/models/Team.ts index c3d37e340..04659f646 100644 --- a/server/models/Team.ts +++ b/server/models/Team.ts @@ -152,6 +152,10 @@ class Team extends ParanoidModel< @Column memberCollectionCreate: boolean; + @Default(true) + @Column + memberTeamCreate: boolean; + @Default(UserRole.Member) @IsIn([[UserRole.Viewer, UserRole.Member]]) @Column(DataType.STRING) diff --git a/server/policies/team.ts b/server/policies/team.ts index 9e50f3bb0..fb9593d60 100644 --- a/server/policies/team.ts +++ b/server/policies/team.ts @@ -1,6 +1,6 @@ import { Team, User } from "@server/models"; import { allow } from "./cancan"; -import { and, isCloudHosted, isTeamAdmin, isTeamModel } from "./utils"; +import { and, isCloudHosted, isTeamAdmin, isTeamModel, or } from "./utils"; allow(User, "read", Team, isTeamModel); @@ -13,12 +13,13 @@ allow(User, "share", Team, (actor, team) => ) ); -allow(User, "createTeam", Team, (actor) => +allow(User, "createTeam", Team, (actor, team) => and( // isCloudHosted(), !actor.isGuest, - !actor.isViewer + !actor.isViewer, + or(actor.isAdmin, !!team?.memberTeamCreate) ) ); diff --git a/server/presenters/team.ts b/server/presenters/team.ts index e9fa4e640..f756968e0 100644 --- a/server/presenters/team.ts +++ b/server/presenters/team.ts @@ -7,6 +7,7 @@ export default function presentTeam(team: Team) { avatarUrl: team.avatarUrl, sharing: team.sharing, memberCollectionCreate: team.memberCollectionCreate, + memberTeamCreate: team.memberTeamCreate, defaultCollectionId: team.defaultCollectionId, documentEmbeds: team.documentEmbeds, guestSignin: team.emailSigninEnabled, diff --git a/server/routes/api/teams/schema.ts b/server/routes/api/teams/schema.ts index 368f3b735..cdc7ebef0 100644 --- a/server/routes/api/teams/schema.ts +++ b/server/routes/api/teams/schema.ts @@ -18,6 +18,8 @@ export const TeamsUpdateSchema = BaseSchema.extend({ documentEmbeds: z.boolean().optional(), /** Whether team members are able to create new collections */ memberCollectionCreate: z.boolean().optional(), + /** Whether team members are able to create new workspaces */ + memberTeamCreate: z.boolean().optional(), /** The default landing collection for the team */ defaultCollectionId: z.string().uuid().nullish(), /** The default user role */ diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index fd1d23a2b..a5a169ccd 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -930,6 +930,8 @@ "Links to supported services are shown as rich embeds within your documents": "Links to supported services are shown as rich embeds within your documents", "Collection creation": "Collection creation", "Allow editors to create new collections within the workspace": "Allow editors to create new collections within the workspace", + "Workspace creation": "Workspace creation", + "Allow editors to create new workspaces": "Allow editors to create new workspaces", "Draw.io deployment": "Draw.io deployment", "Add your self-hosted draw.io installation url here to enable automatic embedding of diagrams within documents.": "Add your self-hosted draw.io installation url here to enable automatic embedding of diagrams within documents.", "Grist deployment": "Grist deployment",