Fix: Dont allow non domain policy matching members to be invited to workspace (#2838)
This commit is contained in:
@@ -39,6 +39,7 @@
|
||||
:user="user"
|
||||
:disabled="disabled"
|
||||
:is-owner-role="isOwnerRole"
|
||||
:target-role="role"
|
||||
@invite-user="() => onInviteUser(user)"
|
||||
/>
|
||||
</template>
|
||||
@@ -48,6 +49,7 @@
|
||||
:disabled="disabled"
|
||||
:is-owner-role="isOwnerRole"
|
||||
:is-guest-mode="isGuestMode"
|
||||
:unmatching-domain-policy="unmatchingDomainPolicy"
|
||||
class="p-2"
|
||||
@invite-emails="({ serverRole }) => onInviteUser(emails, serverRole)"
|
||||
/>
|
||||
@@ -82,6 +84,11 @@ import { mapMainRoleToGqlWorkspaceRole } from '~/lib/workspaces/helpers/roles'
|
||||
|
||||
graphql(`
|
||||
fragment WorkspaceInviteDialog_Workspace on Workspace {
|
||||
domainBasedMembershipProtectionEnabled
|
||||
domains {
|
||||
domain
|
||||
id
|
||||
}
|
||||
id
|
||||
team {
|
||||
items {
|
||||
@@ -117,7 +124,8 @@ const { users, emails, hasTargets } = useResolveInviteTargets({
|
||||
...(props.workspace?.invitedTeam?.map((c) => c.user?.id).filter(isNonNullable) ||
|
||||
[])
|
||||
]),
|
||||
excludeEmails: computed(() => props.workspace?.invitedTeam?.map((c) => c.title))
|
||||
excludeEmails: computed(() => props.workspace?.invitedTeam?.map((c) => c.title)),
|
||||
workspaceId: props.workspaceId
|
||||
})
|
||||
const { isGuestMode } = useServerInfo()
|
||||
|
||||
@@ -135,7 +143,18 @@ const buttons = computed((): LayoutDialogButton[] => [
|
||||
])
|
||||
|
||||
const isOwnerRole = computed(() => role.value === Roles.Workspace.Admin)
|
||||
const allowedDomains = computed(() => props.workspace?.domains?.map((c) => c.domain))
|
||||
const unmatchingDomainPolicy = computed(() => {
|
||||
if (props.workspace?.domainBasedMembershipProtectionEnabled) {
|
||||
return role.value === Roles.Workspace.Guest
|
||||
? false
|
||||
: !emails.value?.every((email) =>
|
||||
allowedDomains.value?.includes(email.split('@')[1])
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
const onInviteUser = async (
|
||||
user: UserSearchItemOrEmail | UserSearchItemOrEmail[],
|
||||
serverRole: ServerRoles = Roles.Server.User
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
<template>
|
||||
<div class="flex px-4 py-3 items-center space-x-2">
|
||||
<UserAvatar />
|
||||
<span class="grow truncate text-body-sm">{{ selectedEmails.join(', ') }}</span>
|
||||
<div class="flex px-4 py-3 items-center space-x-2 justify-between">
|
||||
<div class="flex items-center space-x-2 flex-1 truncate">
|
||||
<div
|
||||
v-if="unmatchingDomainPolicy"
|
||||
v-tippy="
|
||||
'Users that do not comply with the domain policy can only be invited as guests'
|
||||
"
|
||||
>
|
||||
<ExclamationCircleIcon class="text-danger w-5 w-4" />
|
||||
</div>
|
||||
<span class="truncate text-body-sm flex-1">
|
||||
{{ selectedEmails.join(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<FormSelectServerRoles
|
||||
v-if="showServerRoleSelect"
|
||||
@@ -27,9 +38,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Roles } from '@speckle/shared'
|
||||
import type { ServerRoles } from '@speckle/shared'
|
||||
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
|
||||
import { ExclamationCircleIcon } from '@heroicons/vue/24/outline'
|
||||
import { Roles, type ServerRoles } from '@speckle/shared'
|
||||
|
||||
defineEmits<{
|
||||
(e: 'invite-emails', payload: { serverRole: ServerRoles }): void
|
||||
@@ -40,6 +51,7 @@ const props = defineProps<{
|
||||
disabled?: boolean
|
||||
isGuestMode: boolean
|
||||
isOwnerRole: boolean
|
||||
unmatchingDomainPolicy?: boolean
|
||||
}>()
|
||||
|
||||
const { isAdmin } = useActiveUser()
|
||||
@@ -58,6 +70,7 @@ const isButtonDisabled = computed(() => {
|
||||
if (props.disabled) return true
|
||||
if (isTryingToSetGuestOwner.value) return true
|
||||
if (!props.selectedEmails.length) return true
|
||||
if (props.unmatchingDomainPolicy) return true
|
||||
return false
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex px-4 py-3 items-center space-x-2 border-b last:border-0 border-outline-3"
|
||||
class="flex px-4 py-3 items-center space-x-2 justify-between border-b last:border-0 border-outline-3"
|
||||
>
|
||||
<UserAvatar :user="user" />
|
||||
<span class="grow truncate text-body-sm">{{ user.name }}</span>
|
||||
<div class="flex items-center space-x-2 flex-1 truncate">
|
||||
<UserAvatar :user="user" />
|
||||
<div
|
||||
v-if="
|
||||
user.workspaceDomainPolicyCompliant === false &&
|
||||
targetRole !== Roles.Workspace.Guest
|
||||
"
|
||||
v-tippy="
|
||||
'Users that do not comply with the domain policy can only be invited as guests'
|
||||
"
|
||||
>
|
||||
<ExclamationCircleIcon class="text-danger w-5 w-4" />
|
||||
</div>
|
||||
<span class="grow truncate text-body-sm">{{ user.name }}</span>
|
||||
</div>
|
||||
<span v-tippy="isTryingToSetGuestOwner ? settingGuestOwnerErrorMessage : undefined">
|
||||
<FormButton
|
||||
:disabled="isButtonDisabled"
|
||||
size="sm"
|
||||
color="outline"
|
||||
:disabled="
|
||||
(user.workspaceDomainPolicyCompliant === false &&
|
||||
targetRole !== Roles.Workspace.Guest) ||
|
||||
isButtonDisabled
|
||||
"
|
||||
@click="() => $emit('invite-user')"
|
||||
>
|
||||
Invite
|
||||
@@ -17,8 +34,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { Roles, type WorkspaceRoles } from '@speckle/shared'
|
||||
import type { UserSearchItem } from '~~/lib/common/composables/users'
|
||||
import { ExclamationCircleIcon } from '@heroicons/vue/24/outline'
|
||||
|
||||
defineEmits<{
|
||||
(e: 'invite-user'): void
|
||||
@@ -30,6 +48,7 @@ const props = withDefaults(
|
||||
user: UserSearchItem
|
||||
disabled?: boolean
|
||||
settingGuestOwnerErrorMessage?: string
|
||||
targetRole: WorkspaceRoles
|
||||
}>(),
|
||||
{
|
||||
settingGuestOwnerErrorMessage: "Server guests can't be workspace owners"
|
||||
|
||||
@@ -123,7 +123,7 @@ const documents = {
|
||||
"\n fragment ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\n }\n }\n": types.ViewerCommentsListItemFragmentDoc,
|
||||
"\n fragment ViewerModelVersionCardItem on Version {\n id\n message\n referencedObject\n sourceApplication\n createdAt\n previewUrl\n authorUser {\n ...LimitedUserAvatar\n }\n }\n": types.ViewerModelVersionCardItemFragmentDoc,
|
||||
"\n fragment WorkspaceAvatar_Workspace on Workspace {\n id\n logo\n defaultLogoIndex\n }\n": types.WorkspaceAvatar_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceInviteDialog_Workspace on Workspace {\n id\n team {\n items {\n id\n user {\n id\n }\n }\n }\n invitedTeam(filter: $invitesFilter) {\n title\n user {\n id\n }\n }\n }\n": types.WorkspaceInviteDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceInviteDialog_Workspace on Workspace {\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n id\n team {\n items {\n id\n user {\n id\n }\n }\n }\n invitedTeam(filter: $invitesFilter) {\n title\n user {\n id\n }\n }\n }\n": types.WorkspaceInviteDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceProjectList_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...ProjectDashboardItem\n }\n cursor\n }\n": types.WorkspaceProjectList_ProjectCollectionFragmentDoc,
|
||||
"\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n role\n name\n logo\n description\n totalProjects: projects {\n totalCount\n }\n billing {\n versionsCount {\n ...WorkspacePageVersionCount_WorkspaceVersionsCount\n }\n }\n team {\n items {\n id\n user {\n id\n name\n ...LimitedUserAvatar\n }\n }\n }\n ...WorkspaceInviteDialog_Workspace\n }\n": types.WorkspaceHeader_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceInviteBanner_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.WorkspaceInviteBanner_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
@@ -150,7 +150,7 @@ const documents = {
|
||||
"\n query ProjectAutomationCreationPublicKeys(\n $projectId: String!\n $automationId: String!\n ) {\n project(id: $projectId) {\n id\n automation(id: $automationId) {\n id\n creationPublicKeys\n }\n }\n }\n": types.ProjectAutomationCreationPublicKeysDocument,
|
||||
"\n query AutomateFunctionsPagePagination($search: String, $cursor: String) {\n ...AutomateFunctionsPageItems_Query\n }\n": types.AutomateFunctionsPagePaginationDocument,
|
||||
"\n query MentionsUserSearch($query: String!, $emailOnly: Boolean = false) {\n userSearch(\n query: $query\n limit: 5\n cursor: null\n archived: false\n emailOnly: $emailOnly\n ) {\n items {\n id\n name\n company\n }\n }\n }\n": types.MentionsUserSearchDocument,
|
||||
"\n query UserSearch($query: String!, $limit: Int, $cursor: String, $archived: Boolean) {\n userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived) {\n cursor\n items {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n }\n": types.UserSearchDocument,
|
||||
"\n query UserSearch(\n $query: String!\n $limit: Int\n $cursor: String\n $archived: Boolean\n $workspaceId: String\n ) {\n userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived) {\n cursor\n items {\n id\n name\n bio\n company\n avatar\n verified\n role\n workspaceDomainPolicyCompliant(workspaceId: $workspaceId)\n }\n }\n }\n": types.UserSearchDocument,
|
||||
"\n query ServerInfoBlobSizeLimit {\n serverInfo {\n blobSizeLimitBytes\n }\n }\n": types.ServerInfoBlobSizeLimitDocument,
|
||||
"\n query ServerInfoAllScopes {\n serverInfo {\n scopes {\n name\n description\n }\n }\n }\n": types.ServerInfoAllScopesDocument,
|
||||
"\n query ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": types.ProjectModelsSelectorValuesDocument,
|
||||
@@ -775,7 +775,7 @@ export function graphql(source: "\n fragment WorkspaceAvatar_Workspace on Works
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment WorkspaceInviteDialog_Workspace on Workspace {\n id\n team {\n items {\n id\n user {\n id\n }\n }\n }\n invitedTeam(filter: $invitesFilter) {\n title\n user {\n id\n }\n }\n }\n"): (typeof documents)["\n fragment WorkspaceInviteDialog_Workspace on Workspace {\n id\n team {\n items {\n id\n user {\n id\n }\n }\n }\n invitedTeam(filter: $invitesFilter) {\n title\n user {\n id\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment WorkspaceInviteDialog_Workspace on Workspace {\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n id\n team {\n items {\n id\n user {\n id\n }\n }\n }\n invitedTeam(filter: $invitesFilter) {\n title\n user {\n id\n }\n }\n }\n"): (typeof documents)["\n fragment WorkspaceInviteDialog_Workspace on Workspace {\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n id\n team {\n items {\n id\n user {\n id\n }\n }\n }\n invitedTeam(filter: $invitesFilter) {\n title\n user {\n id\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -883,7 +883,7 @@ export function graphql(source: "\n query MentionsUserSearch($query: String!, $
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query UserSearch($query: String!, $limit: Int, $cursor: String, $archived: Boolean) {\n userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived) {\n cursor\n items {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n }\n"): (typeof documents)["\n query UserSearch($query: String!, $limit: Int, $cursor: String, $archived: Boolean) {\n userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived) {\n cursor\n items {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n query UserSearch(\n $query: String!\n $limit: Int\n $cursor: String\n $archived: Boolean\n $workspaceId: String\n ) {\n userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived) {\n cursor\n items {\n id\n name\n bio\n company\n avatar\n verified\n role\n workspaceDomainPolicyCompliant(workspaceId: $workspaceId)\n }\n }\n }\n"): (typeof documents)["\n query UserSearch(\n $query: String!\n $limit: Int\n $cursor: String\n $archived: Boolean\n $workspaceId: String\n ) {\n userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived) {\n cursor\n items {\n id\n name\n bio\n company\n avatar\n verified\n role\n workspaceDomainPolicyCompliant(workspaceId: $workspaceId)\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -19,7 +19,13 @@ export const mentionsUserSearchQuery = graphql(`
|
||||
`)
|
||||
|
||||
export const userSearchQuery = graphql(`
|
||||
query UserSearch($query: String!, $limit: Int, $cursor: String, $archived: Boolean) {
|
||||
query UserSearch(
|
||||
$query: String!
|
||||
$limit: Int
|
||||
$cursor: String
|
||||
$archived: Boolean
|
||||
$workspaceId: String
|
||||
) {
|
||||
userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived) {
|
||||
cursor
|
||||
items {
|
||||
@@ -30,6 +36,7 @@ export const userSearchQuery = graphql(`
|
||||
avatar
|
||||
verified
|
||||
role
|
||||
workspaceDomainPolicyCompliant(workspaceId: $workspaceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,13 +79,18 @@ export const useResolveInviteTargets = (params: {
|
||||
*/
|
||||
excludeUserIds?: Ref<MaybeNullOrUndefined<string[]>>
|
||||
excludeEmails?: Ref<MaybeNullOrUndefined<string[]>>
|
||||
/**
|
||||
* Used for searching of users within a workspace context
|
||||
*/
|
||||
workspaceId?: string
|
||||
}) => {
|
||||
const { search, excludeUserIds, excludeEmails } = params
|
||||
const { search, excludeUserIds, excludeEmails, workspaceId } = params
|
||||
|
||||
const { userSearch, searchVariables, loading } = useUserSearch({
|
||||
variables: computed(() => ({
|
||||
query: search.value || '',
|
||||
limit: 5
|
||||
limit: 5,
|
||||
workspaceId
|
||||
}))
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user