feat(server): workspace roles taken into account in project queries (#4319)

* Workspace.projects fixed

* Query.project tested & fixed

* personalOnly flag added

* withProjectRoleOnly flag

* authorizeResolver implicit workspace roles

* minor cleanup

* reorg + support for throwing auth errors

* global error mapping

* undo special borkage

* CR fixes

* more CR fixes

* shared tests fix

* minor adjustment

* tests fix

* see if removing cached roles fixes it?

* more fixes

* clean up debugging garbage
This commit is contained in:
Kristaps Fabians Geikins
2025-04-07 12:52:07 +03:00
committed by GitHub
parent e3d3c1446b
commit 820a1e2ebf
68 changed files with 1813 additions and 986 deletions
@@ -7,7 +7,7 @@ export const workspaceEventNamespace = 'workspace' as const
const eventPrefix = `${workspaceEventNamespace}.` as const
export const WorkspaceEvents = {
Authorized: `${eventPrefix}authorized`,
Authorizing: `${eventPrefix}authorizing`,
Created: `${eventPrefix}created`,
Updated: `${eventPrefix}updated`,
Deleted: `${eventPrefix}deleted`,
@@ -47,7 +47,7 @@ type WorkspaceJoinedFromDiscoveryPayload = {
}
export type WorkspaceEventsPayloads = {
[WorkspaceEvents.Authorized]: WorkspaceAuthorizedPayload
[WorkspaceEvents.Authorizing]: WorkspaceAuthorizedPayload
[WorkspaceEvents.Created]: WorkspaceCreatedPayload
[WorkspaceEvents.Updated]: WorkspaceUpdatedPayload
[WorkspaceEvents.Deleted]: { workspaceId: string }
@@ -0,0 +1,25 @@
import { WorkspaceAcl, WorkspaceSeat } from '@/modules/workspacesCore/domain/types'
import { Nullable } from '@speckle/shared'
export type GetWorkspaceRolesAndSeats = (params: {
workspaceId: string
userIds?: string[]
}) => Promise<{
[userId: string]: {
role: WorkspaceAcl
seat: Nullable<WorkspaceSeat>
userId: string
}
}>
export type GetWorkspaceRoleAndSeat = (params: {
workspaceId: string
userId: string
}) => Promise<
| {
role: WorkspaceAcl
seat: Nullable<WorkspaceSeat>
userId: string
}
| undefined
>
@@ -72,3 +72,18 @@ export type WorkspaceJoinRequest = {
createdAt: Date
updatedAt: Date
}
export const WorkspaceSeatType = <const>{
Viewer: 'viewer',
Editor: 'editor'
}
export type WorkspaceSeatType =
(typeof WorkspaceSeatType)[keyof typeof WorkspaceSeatType]
export type WorkspaceSeat = {
workspaceId: string
userId: string
type: WorkspaceSeatType
createdAt: Date
updatedAt: Date
}
@@ -36,3 +36,11 @@ export const WorkspaceJoinRequests = buildTableHelper('workspace_join_requests',
'createdAt',
'updatedAt'
])
export const WorkspaceSeats = buildTableHelper('workspace_seats', [
'workspaceId',
'userId',
'type',
'createdAt',
'updatedAt'
])
@@ -0,0 +1,65 @@
import { formatJsonArrayRecords } from '@/modules/shared/helpers/dbHelper'
import {
GetWorkspaceRoleAndSeat,
GetWorkspaceRolesAndSeats
} from '@/modules/workspacesCore/domain/operations'
import {
WorkspaceSeat,
WorkspaceAcl as WorkspaceAclRecord
} from '@/modules/workspacesCore/domain/types'
import { WorkspaceAcl, WorkspaceSeats } from '@/modules/workspacesCore/helpers/db'
import { Knex } from 'knex'
const tables = {
workspaceSeats: (db: Knex) => db<WorkspaceSeat>(WorkspaceSeats.name),
workspaceAcl: (db: Knex) => db<WorkspaceAclRecord>(WorkspaceAcl.name)
}
export const getWorkspaceRolesAndSeatsFactory =
(deps: { db: Knex }): GetWorkspaceRolesAndSeats =>
async ({ workspaceId, userIds }) => {
const q = tables
.workspaceAcl(deps.db)
.select<Array<{ seats: WorkspaceSeat[]; roles: WorkspaceAclRecord[] }>>([
// There's only ever gonna be 1 role and seat per user, but this way we can avoid having to group
// by many columns and we can get everything in 1 query
WorkspaceAcl.groupArray('roles'),
WorkspaceSeats.groupArray('seats')
])
.leftJoin(WorkspaceSeats.name, (j1) => {
j1.on(WorkspaceSeats.col.userId, WorkspaceAcl.col.userId).andOnVal(
WorkspaceSeats.col.workspaceId,
workspaceId
)
})
.where(WorkspaceAcl.col.workspaceId, workspaceId)
.groupBy(WorkspaceAcl.col.userId)
if (userIds?.length) {
q.whereIn(WorkspaceAcl.col.userId, userIds)
}
const res = await q
return res.reduce((acc, row) => {
const role = formatJsonArrayRecords(row.roles)[0]
if (!role) return acc
acc[role.userId] = {
role,
seat: formatJsonArrayRecords(row.seats || [])[0] || null,
userId: role.userId
}
return acc
}, {} as Awaited<ReturnType<GetWorkspaceRolesAndSeats>>)
}
export const getWorkspaceRoleAndSeatFactory =
(deps: { db: Knex }): GetWorkspaceRoleAndSeat =>
async ({ workspaceId, userId }) => {
const getWorkspaceRolesAndSeats = getWorkspaceRolesAndSeatsFactory(deps)
const rolesAndSeats = await getWorkspaceRolesAndSeats({
workspaceId,
userIds: [userId]
})
return rolesAndSeats[userId]
}