mirror of https://github.com/nocodb/nocodb
77 changed files with 461 additions and 534 deletions
@ -1,61 +0,0 @@
|
||||
import { isString } from '@vue/shared' |
||||
import { createSharedComposable, rolePermissions, useGlobal, useRoles } from '#imports' |
||||
import type { Permission, ProjectRole, Role } from '#imports' |
||||
|
||||
const hasPermission = (role: Role | ProjectRole, hasRole: boolean, permission: Permission | string) => { |
||||
const rolePermission = rolePermissions[role] |
||||
|
||||
if (!hasRole || !rolePermission) return false |
||||
|
||||
if (isString(rolePermission) && rolePermission === '*') return true |
||||
|
||||
if ('include' in rolePermission && rolePermission.include) { |
||||
return !!rolePermission.include[permission as keyof typeof rolePermission.include] |
||||
} |
||||
|
||||
if ('exclude' in rolePermission && rolePermission.exclude) { |
||||
return !rolePermission.exclude[permission as keyof typeof rolePermission.exclude] |
||||
} |
||||
|
||||
return rolePermission[permission as keyof typeof rolePermission] |
||||
} |
||||
|
||||
export const useUIPermission = createSharedComposable(() => { |
||||
const { previewAs } = useGlobal() |
||||
const { allRoles } = useRoles() |
||||
|
||||
const isUIAllowed = ( |
||||
permission: Permission | string, |
||||
skipPreviewAs = false, |
||||
userRoles: string | Record<string, boolean> | string[] | null = null, |
||||
combineWithStateRoles = false, |
||||
) => { |
||||
if (previewAs.value && !skipPreviewAs) { |
||||
return hasPermission(previewAs.value, true, permission) |
||||
} |
||||
|
||||
let roles: Record<string, boolean> = {} |
||||
|
||||
if (!userRoles) { |
||||
roles = allRoles.value |
||||
} else if (Array.isArray(userRoles) || typeof userRoles === 'string') { |
||||
roles = (Array.isArray(userRoles) ? userRoles : userRoles.split(',')) |
||||
// filter out any empty-string/null/undefined values
|
||||
.filter(Boolean) |
||||
.reduce<Record<string, boolean>>((acc, role) => { |
||||
acc[role] = true |
||||
return acc |
||||
}, {}) |
||||
} else if (typeof userRoles === 'object') { |
||||
roles = userRoles |
||||
} |
||||
|
||||
if (userRoles && combineWithStateRoles) { |
||||
roles = { ...roles, ...allRoles.value } |
||||
} |
||||
|
||||
return Object.entries(roles).some(([role, hasRole]) => hasPermission(role as Role | ProjectRole, hasRole, permission)) |
||||
} |
||||
|
||||
return { isUIAllowed } |
||||
}) |
@ -0,0 +1,148 @@
|
||||
import { OrgUserRoles, ProjectRoles } from 'nocodb-sdk' |
||||
|
||||
const roleScopes = { |
||||
org: [OrgUserRoles.VIEWER, OrgUserRoles.CREATOR], |
||||
project: [ProjectRoles.VIEWER, ProjectRoles.COMMENTER, ProjectRoles.EDITOR, ProjectRoles.CREATOR, ProjectRoles.OWNER], |
||||
} |
||||
|
||||
interface Perm { |
||||
include?: Record<string, boolean> |
||||
} |
||||
|
||||
/** |
||||
* Each permission value means the following |
||||
* `*` - which is wildcard, means all permissions are allowed |
||||
* `include` - which is an object, means only the permissions listed in the object are allowed |
||||
* `undefined` or `{}` - which is the default value, means no permissions are allowed |
||||
* */ |
||||
const rolePermissions = { |
||||
// org level role permissions
|
||||
[OrgUserRoles.SUPER_ADMIN]: '*', |
||||
[OrgUserRoles.CREATOR]: { |
||||
include: { |
||||
projectCreate: true, |
||||
projectMove: true, |
||||
projectDelete: true, |
||||
projectDuplicate: true, |
||||
newUser: true, |
||||
}, |
||||
}, |
||||
[OrgUserRoles.VIEWER]: { |
||||
include: { |
||||
importRequest: true, |
||||
}, |
||||
}, |
||||
|
||||
// Project role permissions
|
||||
[ProjectRoles.OWNER]: { |
||||
include: { |
||||
projectDelete: true, |
||||
}, |
||||
}, |
||||
[ProjectRoles.CREATOR]: { |
||||
include: { |
||||
baseCreate: true, |
||||
fieldUpdate: true, |
||||
hookList: true, |
||||
tableCreate: true, |
||||
tableRename: true, |
||||
tableDelete: true, |
||||
tableDuplicate: true, |
||||
tableSort: true, |
||||
layoutRename: true, |
||||
layoutDelete: true, |
||||
airtableImport: true, |
||||
jsonImport: true, |
||||
excelImport: true, |
||||
settingsPage: true, |
||||
newUser: true, |
||||
webhook: true, |
||||
fieldEdit: true, |
||||
fieldAdd: true, |
||||
tableIconEdit: true, |
||||
viewCreateOrEdit: true, |
||||
viewShare: true, |
||||
projectShare: true, |
||||
}, |
||||
}, |
||||
[ProjectRoles.EDITOR]: { |
||||
include: { |
||||
dataInsert: true, |
||||
dataEdit: true, |
||||
sortSync: true, |
||||
filterSync: true, |
||||
filterChildrenRead: true, |
||||
viewFieldEdit: true, |
||||
csvImport: true, |
||||
apiDocs: true, |
||||
}, |
||||
}, |
||||
[ProjectRoles.COMMENTER]: { |
||||
include: { |
||||
commentEdit: true, |
||||
commentList: true, |
||||
commentCount: true, |
||||
}, |
||||
}, |
||||
[ProjectRoles.VIEWER]: { |
||||
include: { |
||||
projectSettings: true, |
||||
expandedForm: true, |
||||
}, |
||||
}, |
||||
[ProjectRoles.NO_ACCESS]: { |
||||
include: {}, |
||||
}, |
||||
} as Record<OrgUserRoles | ProjectRoles, Perm | '*'> |
||||
|
||||
/* |
||||
We inherit include permissions from previous roles in the same scope (role order) |
||||
To determine role order, we use `roleScopes` object |
||||
|
||||
So for example ProjectRoles.COMMENTER has `commentEdit` permission, |
||||
which means ProjectRoles.EDITOR, ProjectRoles.CREATOR, ProjectRoles.OWNER will also have `commentEdit` permission |
||||
where as ProjectRoles.VIEWER, ProjectRoles.NO_ACCESS will not have `commentEdit` permission. |
||||
|
||||
This is why we are validating that there are no duplicate permissions within the same scope |
||||
even though it is not required for the code to work. It is to keep the code clean and easy to understand. |
||||
*/ |
||||
|
||||
// validate no duplicate permissions within same scope
|
||||
Object.values(roleScopes).forEach((roles) => { |
||||
const scopePermissions: Record<string, boolean> = {} |
||||
const duplicates: string[] = [] |
||||
roles.forEach((role) => { |
||||
const perms = (rolePermissions[role] as Perm).include || {} |
||||
Object.keys(perms).forEach((perm) => { |
||||
if (scopePermissions[perm]) { |
||||
duplicates.push(perm) |
||||
} |
||||
scopePermissions[perm] = true |
||||
}) |
||||
}) |
||||
if (duplicates.length) { |
||||
throw new Error( |
||||
`Duplicate permissions found in roles ${roles.join(', ')}. Please remove duplicate permissions: ${duplicates.join(', ')}`, |
||||
) |
||||
} |
||||
}) |
||||
|
||||
// inherit include permissions within scope (role order)
|
||||
Object.values(roleScopes).forEach((roles) => { |
||||
let roleIndex = 0 |
||||
for (const role of roles) { |
||||
if (roleIndex === 0) { |
||||
roleIndex++ |
||||
continue |
||||
} |
||||
|
||||
if (rolePermissions[role] === '*') continue |
||||
if ((rolePermissions[role] as Perm).include && (rolePermissions[roles[roleIndex - 1]] as Perm).include) { |
||||
Object.assign((rolePermissions[role] as Perm).include!, (rolePermissions[roles[roleIndex - 1]] as Perm).include) |
||||
} |
||||
|
||||
roleIndex++ |
||||
} |
||||
}) |
||||
|
||||
export { rolePermissions } |
@ -1,3 +1,4 @@
|
||||
export * from './constants' |
||||
export * from './enums' |
||||
export * from './types' |
||||
export * from './acl' |
||||
|
@ -1,15 +0,0 @@
|
||||
import { OrgUserRoles } from 'nocodb-sdk' |
||||
import { ProjectRole } from '../lib/enums' |
||||
|
||||
export const projectRoleTagColors = { |
||||
[ProjectRole.Owner]: '#cfdffe', |
||||
[ProjectRole.Creator]: '#d0f1fd', |
||||
[ProjectRole.Editor]: '#c2f5e8', |
||||
[ProjectRole.Commenter]: '#ffdaf6', |
||||
[ProjectRole.Viewer]: '#ffdce5', |
||||
[OrgUserRoles.SUPER_ADMIN]: '#f5d7cb', |
||||
} |
||||
|
||||
export const projectRoles = [ProjectRole.Creator, ProjectRole.Editor, ProjectRole.Commenter, ProjectRole.Viewer] |
||||
|
||||
export const docsProjectRoles = [ProjectRole.Editor, ProjectRole.Viewer] |
Loading…
Reference in new issue