From 0721831a00403e7af3b20ff186127648d48667ab Mon Sep 17 00:00:00 2001 From: Iain Sproat <68657+iainsproat@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:25:34 +0100 Subject: [PATCH] feat(server): FF_USERS_INVITE_SCOPE_IS_PUBLIC flag to make users:invite scope public (#5244) --- .../modules/core/graph/generated/graphql.ts | 6 +++ .../server/modules/core/tests/graph.spec.ts | 44 ++++++++++++++++++- .../server/modules/serverinvites/index.ts | 5 ++- packages/server/test/graphql/core.ts | 12 +++++ packages/shared/src/environment/index.ts | 7 +++ .../speckle-server/templates/_helpers.tpl | 4 ++ utils/helm/speckle-server/values.schema.json | 5 +++ utils/helm/speckle-server/values.yaml | 2 + 8 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 packages/server/test/graphql/core.ts diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 42fff872c..60d4de587 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -9598,6 +9598,11 @@ export type DeleteCommitsMutationVariables = Exact<{ export type DeleteCommitsMutation = { __typename?: 'Mutation', commitsDelete: boolean }; +export type GetAllAvailableScopesQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetAllAvailableScopesQuery = { __typename?: 'Query', serverInfo: { __typename?: 'ServerInfo', scopes: Array<{ __typename?: 'Scope', name: string, description: string }> } }; + export type CreateEmbedTokenMutationVariables = Exact<{ token: EmbedTokenCreateInput; }>; @@ -10382,6 +10387,7 @@ export const ReadOtherUsersCommitsDocument = {"kind":"Document","definitions":[{ export const ReadStreamBranchCommitsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ReadStreamBranchCommits"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"branchName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"10"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"branch"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"branchName"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BaseCommitFields"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BaseCommitFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Commit"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"authorName"}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}},{"kind":"Field","name":{"kind":"Name","value":"authorAvatar"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"streamName"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApplication"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"commentCount"}}]}}]} as unknown as DocumentNode; export const MoveCommitsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MoveCommits"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CommitsMoveInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commitsMove"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode; export const DeleteCommitsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteCommits"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CommitsDeleteInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commitsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode; +export const GetAllAvailableScopesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAllAvailableScopes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scopes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateEmbedTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateEmbedToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EmbedTokenCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createEmbedToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetWorkspacePlanPricesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspacePlanPrices"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"planPrices"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"usd"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"monthly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"yearly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"teamUnlimited"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"monthly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"yearly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pro"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"monthly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"yearly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"proUnlimited"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"monthly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"yearly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"gbp"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"monthly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"yearly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"teamUnlimited"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"monthly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"yearly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pro"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"monthly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"yearly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"proUnlimited"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"monthly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"yearly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateProjectModelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateProjectModel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateModelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"modelMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/server/modules/core/tests/graph.spec.ts b/packages/server/modules/core/tests/graph.spec.ts index d7a7dd90f..4b0d9d816 100644 --- a/packages/server/modules/core/tests/graph.spec.ts +++ b/packages/server/modules/core/tests/graph.spec.ts @@ -65,8 +65,16 @@ import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' import { ProjectRecordVisibility } from '@/modules/core/helpers/types' import type { BasicTestStream } from '@/test/speckle-helpers/streamHelper' import { createTestStream } from '@/test/speckle-helpers/streamHelper' +import { GetAllAvailableScopesDocument } from '@/modules/core/graph/generated/graphql' +import { + createTestContext, + testApolloServer, + type TestApolloServer +} from '@/test/graphqlHelper' +import { AllScopes } from '@/modules/core/helpers/mainConstants' -const { FF_PERSONAL_PROJECTS_LIMITS_ENABLED } = getFeatureFlags() +const { FF_PERSONAL_PROJECTS_LIMITS_ENABLED, FF_USERS_INVITE_SCOPE_IS_PUBLIC } = + getFeatureFlags() const getUser = getUserFactory({ db }) const getStream = getStreamFactory({ db }) @@ -132,6 +140,7 @@ const createPersonalAccessToken = createPersonalAccessTokenFactory({ let app: Express let server: Server let sendRequest: Awaited>['sendRequest'] +let apollo: TestApolloServer const changeUserRole = changeUserRoleFactory({ getServerInfo, @@ -218,6 +227,16 @@ const changeUserRole = changeUserRoleFactory({ ] )}` + apollo = await testApolloServer({ + context: await createTestContext({ + auth: true, + userId: userA.id, + role: Roles.Server.Admin, + token: 'asd', + scopes: AllScopes + }) + }) + // Prepare API tokens for use in tests const res1 = await sendRequest(userA.token, { query: @@ -1950,6 +1969,29 @@ const changeUserRole = changeUserRoleFactory({ expect(si.configuration.objectMultipartUploadSizeLimitBytes).to.be.a('number') }) + describe(`FF_USERS_INVITE_SCOPE_IS_PUBLIC is ${ + FF_USERS_INVITE_SCOPE_IS_PUBLIC ? 'enabled' : 'disabled' + }`, () => { + it(`serverInfo scopes ${ + FF_USERS_INVITE_SCOPE_IS_PUBLIC ? 'include' : 'do not include' + } users:invite scope`, async () => { + const { data, errors } = await apollo.execute( + GetAllAvailableScopesDocument, + {} + ) + + if (FF_USERS_INVITE_SCOPE_IS_PUBLIC) { + expect(data?.serverInfo.scopes).to.be.ok + expect(data?.serverInfo.scopes).to.include(Scopes.Users.Invite) + expect(errors).not.to.be.ok + } else { + expect(data?.serverInfo.scopes).to.be.ok + expect(data?.serverInfo.scopes).to.not.include(Scopes.Users.Invite) + expect(errors).not.to.be.ok + } + }) + }) + it('Should update the server info object', async () => { const query = 'mutation updateSInfo($info: ServerInfoUpdateInput!) { serverInfoUpdate( info: $info ) } ' diff --git a/packages/server/modules/serverinvites/index.ts b/packages/server/modules/serverinvites/index.ts index da4c0c12e..129a27c78 100644 --- a/packages/server/modules/serverinvites/index.ts +++ b/packages/server/modules/serverinvites/index.ts @@ -8,12 +8,15 @@ import { publish } from '@/modules/shared/utils/subscriptions' import { getStreamFactory } from '@/modules/core/repositories/streams' import { getProjectInviteProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' import { reportSubscriptionEventsFactory } from '@/modules/serverinvites/events/subscriptionListeners' +import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' + +const { FF_USERS_INVITE_SCOPE_IS_PUBLIC } = getFeatureFlags() const scopes = [ { name: Scopes.Users.Invite, description: 'Invite others to join this server.', - public: false + public: FF_USERS_INVITE_SCOPE_IS_PUBLIC } ] diff --git a/packages/server/test/graphql/core.ts b/packages/server/test/graphql/core.ts new file mode 100644 index 000000000..046b044c9 --- /dev/null +++ b/packages/server/test/graphql/core.ts @@ -0,0 +1,12 @@ +import gql from 'graphql-tag' + +export const getAllAvailableScopesQuery = gql` + query GetAllAvailableScopes { + serverInfo { + scopes { + name + description + } + } + } +` diff --git a/packages/shared/src/environment/index.ts b/packages/shared/src/environment/index.ts index 52038a321..9ba3c8bc8 100644 --- a/packages/shared/src/environment/index.ts +++ b/packages/shared/src/environment/index.ts @@ -155,6 +155,12 @@ export const parseFeatureFlags = ( schema: z.boolean(), description: 'Enables the saved views feature for project models', defaults: { _: false } + }, + FF_USERS_INVITE_SCOPE_IS_PUBLIC: { + schema: z.boolean(), + description: + 'Enables Personal Access Tokens (PAT) to be created with users:invite scope. **WARNING** This can be used to spam invitations to any email address. It is not advised to enable this on servers which are open to public account registration or to which untrusted users have been, or can be, invited.', + defaults: { _: false } } }) @@ -195,6 +201,7 @@ export type FeatureFlags = { FF_EXPERIMENTAL_IFC_IMPORTER_ENABLED: boolean FF_ACC_INTEGRATION_ENABLED: boolean FF_SAVED_VIEWS_ENABLED: boolean + FF_USERS_INVITE_SCOPE_IS_PUBLIC: boolean } export function getFeatureFlags(): FeatureFlags { diff --git a/utils/helm/speckle-server/templates/_helpers.tpl b/utils/helm/speckle-server/templates/_helpers.tpl index 4bed2e88e..642b1032d 100644 --- a/utils/helm/speckle-server/templates/_helpers.tpl +++ b/utils/helm/speckle-server/templates/_helpers.tpl @@ -1154,6 +1154,10 @@ Generate the environment variables for Speckle server and Speckle objects deploy value: {{ .Values.openTelemetry.tracing.value | quote }} {{- end }} +{{- if .Values.featureFlags.usersInviteScopeIsPublic }} +- name: FF_USERS_INVITE_SCOPE_IS_PUBLIC + value: {{ .Values.featureFlags.usersInviteScopeIsPublic | quote }} +{{- end }} {{- if .Values.featureFlags.workspacesMultiRegionEnabled }} # Multi-region diff --git a/utils/helm/speckle-server/values.schema.json b/utils/helm/speckle-server/values.schema.json index ffcc38789..37b285540 100644 --- a/utils/helm/speckle-server/values.schema.json +++ b/utils/helm/speckle-server/values.schema.json @@ -144,6 +144,11 @@ "type": "boolean", "description": "Enables the ability to create and manage saved views", "default": false + }, + "usersInviteScopeIsPublic": { + "type": "boolean", + "description": "Enables the ability to create Personal Access Tokens (PAT) with users:invite privileges. WARNING: This can be used by untrusted users to send spam; do not enable on servers which allow public registration or to which untrusted users have been, or will be, invited.", + "default": false } } }, diff --git a/utils/helm/speckle-server/values.yaml b/utils/helm/speckle-server/values.yaml index 61798871e..2780b5f65 100644 --- a/utils/helm/speckle-server/values.yaml +++ b/utils/helm/speckle-server/values.yaml @@ -79,6 +79,8 @@ featureFlags: rhinoFileImporterEnabled: false ## @param featureFlags.savedViewsEnabled Enables the ability to create and manage saved views savedViewsEnabled: false + ## @param featureFlags.usersInviteScopeIsPublic Enables the ability to create Personal Access Tokens (PAT) with users:invite privileges. WARNING: This can be used by untrusted users to send spam; do not enable on servers which allow public registration or to which untrusted users have been, or will be, invited. + usersInviteScopeIsPublic: false analytics: ## @param analytics.enabled Enable or disable analytics