Merge pull request #11644 from Budibase/BUDI-7393/display_inheritance_permission
Handle inheritance permissions
This commit is contained in:
commit
4610a202bd
|
@ -73,7 +73,7 @@
|
||||||
if (!perms["execute"]) {
|
if (!perms["execute"]) {
|
||||||
role = "BASIC"
|
role = "BASIC"
|
||||||
} else {
|
} else {
|
||||||
role = perms["execute"]
|
role = perms["execute"].role
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,25 +5,19 @@
|
||||||
|
|
||||||
export let resourceId
|
export let resourceId
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let requiresLicence
|
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
let resourcePermissions
|
let resourcePermissions
|
||||||
|
|
||||||
async function openDropdown() {
|
async function openModal() {
|
||||||
resourcePermissions = await permissions.forResource(resourceId)
|
resourcePermissions = await permissions.forResourceDetailed(resourceId)
|
||||||
modal.show()
|
modal.show()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="LockClosed" quiet on:click={openDropdown} {disabled}>
|
<ActionButton icon="LockClosed" quiet on:click={openModal} {disabled}>
|
||||||
Access
|
Access
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<ManageAccessModal
|
<ManageAccessModal {resourceId} permissions={resourcePermissions} />
|
||||||
{resourceId}
|
|
||||||
{requiresLicence}
|
|
||||||
levels={$permissions}
|
|
||||||
permissions={resourcePermissions}
|
|
||||||
/>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { licensing, admin } from "stores/portal"
|
|
||||||
import ManageAccessButton from "../ManageAccessButton.svelte"
|
import ManageAccessButton from "../ManageAccessButton.svelte"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
@ -13,17 +12,6 @@
|
||||||
}
|
}
|
||||||
return datasource.type === "table" ? datasource.tableId : datasource.id
|
return datasource.type === "table" ? datasource.tableId : datasource.id
|
||||||
}
|
}
|
||||||
|
|
||||||
var requiresLicence
|
|
||||||
$: {
|
|
||||||
if ($datasource.type === "viewV2" && !$licensing.isViewPermissionsEnabled) {
|
|
||||||
const requiredLicense = $admin?.cloud ? "Premium" : "Business"
|
|
||||||
requiresLicence = {
|
|
||||||
tier: requiredLicense,
|
|
||||||
message: `A ${requiredLicense} subscription is required to specify access level roles for this view.`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ManageAccessButton {resourceId} {requiresLicence} />
|
<ManageAccessButton {resourceId} />
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { PermissionSource } from "@budibase/types"
|
||||||
import { roles, permissions as permissionsStore } from "stores/backend"
|
import { roles, permissions as permissionsStore } from "stores/backend"
|
||||||
import {
|
import {
|
||||||
Label,
|
Label,
|
||||||
|
@ -9,59 +10,127 @@
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Tags,
|
Tags,
|
||||||
Tag,
|
Tag,
|
||||||
|
Icon,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export let resourceId
|
export let resourceId
|
||||||
export let permissions
|
export let permissions
|
||||||
export let requiresLicence
|
|
||||||
|
const inheritedRoleId = "inherited"
|
||||||
|
|
||||||
async function changePermission(level, role) {
|
async function changePermission(level, role) {
|
||||||
try {
|
try {
|
||||||
|
if (role === inheritedRoleId) {
|
||||||
|
await permissionsStore.remove({
|
||||||
|
level,
|
||||||
|
role,
|
||||||
|
resource: resourceId,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
await permissionsStore.save({
|
await permissionsStore.save({
|
||||||
level,
|
level,
|
||||||
role,
|
role,
|
||||||
resource: resourceId,
|
resource: resourceId,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Show updated permissions in UI: REMOVE
|
// Show updated permissions in UI: REMOVE
|
||||||
permissions = await permissionsStore.forResource(resourceId)
|
permissions = await permissionsStore.forResourceDetailed(resourceId)
|
||||||
notifications.success("Updated permissions")
|
notifications.success("Updated permissions")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error updating permissions")
|
notifications.error("Error updating permissions")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: computedPermissions = Object.entries(permissions.permissions).reduce(
|
||||||
|
(p, [level, roleInfo]) => {
|
||||||
|
p[level] = {
|
||||||
|
selectedValue:
|
||||||
|
roleInfo.permissionType === PermissionSource.INHERITED
|
||||||
|
? inheritedRoleId
|
||||||
|
: roleInfo.role,
|
||||||
|
options: [...get(roles)],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roleInfo.inheritablePermission) {
|
||||||
|
p[level].inheritOption = roleInfo.inheritablePermission
|
||||||
|
p[level].options.unshift({
|
||||||
|
_id: inheritedRoleId,
|
||||||
|
name: `Inherit (${
|
||||||
|
get(roles).find(x => x._id === roleInfo.inheritablePermission).name
|
||||||
|
})`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
$: requiresPlanToModify = permissions.requiresPlanToModify
|
||||||
|
|
||||||
|
let dependantsInfoMessage
|
||||||
|
async function loadDependantInfo() {
|
||||||
|
const dependantsInfo = await permissionsStore.getDependantsInfo(resourceId)
|
||||||
|
|
||||||
|
const resourceByType = dependantsInfo?.resourceByType
|
||||||
|
|
||||||
|
if (resourceByType) {
|
||||||
|
const total = Object.values(resourceByType).reduce((p, c) => p + c, 0)
|
||||||
|
let resourceDisplay =
|
||||||
|
Object.keys(resourceByType).length === 1 && resourceByType.view
|
||||||
|
? "view"
|
||||||
|
: "resource"
|
||||||
|
|
||||||
|
if (total === 1) {
|
||||||
|
dependantsInfoMessage = `1 ${resourceDisplay} is inheriting this access.`
|
||||||
|
} else if (total > 1) {
|
||||||
|
dependantsInfoMessage = `${total} ${resourceDisplay}s are inheriting this access.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadDependantInfo()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent showCancelButton={false} confirmText="Done">
|
<ModalContent showCancelButton={false} confirmText="Done">
|
||||||
<span slot="header">
|
<span slot="header">
|
||||||
Manage Access
|
Manage Access
|
||||||
{#if requiresLicence}
|
{#if requiresPlanToModify}
|
||||||
<span class="lock-tag">
|
<span class="lock-tag">
|
||||||
<Tags>
|
<Tags>
|
||||||
<Tag icon="LockClosed">{requiresLicence.tier}</Tag>
|
<Tag icon="LockClosed">{capitalise(requiresPlanToModify)}</Tag>
|
||||||
</Tags>
|
</Tags>
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{#if requiresLicence}
|
|
||||||
<Body size="S">{requiresLicence.message}</Body>
|
|
||||||
{:else}
|
|
||||||
<Body size="S">Specify the minimum access level role for this data.</Body>
|
<Body size="S">Specify the minimum access level role for this data.</Body>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<Label extraSmall grey>Level</Label>
|
<Label extraSmall grey>Level</Label>
|
||||||
<Label extraSmall grey>Role</Label>
|
<Label extraSmall grey>Role</Label>
|
||||||
{#each Object.keys(permissions) as level}
|
{#each Object.keys(computedPermissions) as level}
|
||||||
<Input value={capitalise(level)} disabled />
|
<Input value={capitalise(level)} disabled />
|
||||||
<Select
|
<Select
|
||||||
value={permissions[level]}
|
disabled={requiresPlanToModify}
|
||||||
|
placeholder={false}
|
||||||
|
value={computedPermissions[level].selectedValue}
|
||||||
on:change={e => changePermission(level, e.detail)}
|
on:change={e => changePermission(level, e.detail)}
|
||||||
options={$roles}
|
options={computedPermissions[level].options}
|
||||||
getOptionLabel={x => x.name}
|
getOptionLabel={x => x.name}
|
||||||
getOptionValue={x => x._id}
|
getOptionValue={x => x._id}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if dependantsInfoMessage}
|
||||||
|
<div class="inheriting-resources">
|
||||||
|
<Icon name="Alert" />
|
||||||
|
<Body size="S">
|
||||||
|
<i>
|
||||||
|
{dependantsInfoMessage}
|
||||||
|
</i>
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
|
@ -75,4 +144,9 @@
|
||||||
.lock-tag {
|
.lock-tag {
|
||||||
padding-left: var(--spacing-s);
|
padding-left: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inheriting-resources {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
roleId = (await permissions.forResource(queryToFetch._id))["read"]
|
roleId = (await permissions.forResource(queryToFetch._id))["read"].role
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
roleId = Constants.Roles.BASIC
|
roleId = Constants.Roles.BASIC
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,22 @@ export function createPermissionStore() {
|
||||||
level,
|
level,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
remove: async ({ level, role, resource }) => {
|
||||||
|
return await API.removePermissionFromResource({
|
||||||
|
resourceId: resource,
|
||||||
|
roleId: role,
|
||||||
|
level,
|
||||||
|
})
|
||||||
|
},
|
||||||
forResource: async resourceId => {
|
forResource: async resourceId => {
|
||||||
|
return (await API.getPermissionForResource(resourceId)).permissions
|
||||||
|
},
|
||||||
|
forResourceDetailed: async resourceId => {
|
||||||
return await API.getPermissionForResource(resourceId)
|
return await API.getPermissionForResource(resourceId)
|
||||||
},
|
},
|
||||||
|
getDependantsInfo: async resourceId => {
|
||||||
|
return await API.getDependants(resourceId)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,4 +21,27 @@ export const buildPermissionsEndpoints = API => ({
|
||||||
url: `/api/permission/${roleId}/${resourceId}/${level}`,
|
url: `/api/permission/${roleId}/${resourceId}/${level}`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the the permissions for a certain resource
|
||||||
|
* @param resourceId the ID of the resource to update
|
||||||
|
* @param roleId the ID of the role to update the permissions of
|
||||||
|
* @param level the level to remove the role for this resource
|
||||||
|
* @return {Promise<*>}
|
||||||
|
*/
|
||||||
|
removePermissionFromResource: async ({ resourceId, roleId, level }) => {
|
||||||
|
return await API.delete({
|
||||||
|
url: `/api/permission/${roleId}/${resourceId}/${level}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets info about the resources that depend on this resource permissions
|
||||||
|
* @param resourceId the resource ID to check
|
||||||
|
*/
|
||||||
|
getDependants: async resourceId => {
|
||||||
|
return await API.get({
|
||||||
|
url: `/api/permission/${resourceId}/dependants`,
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
import { permissions, roles, context, HTTPError } from "@budibase/backend-core"
|
import { permissions, roles, context, HTTPError } from "@budibase/backend-core"
|
||||||
import { UserCtx, Database, Role, PermissionLevel } from "@budibase/types"
|
import {
|
||||||
|
UserCtx,
|
||||||
|
Database,
|
||||||
|
Role,
|
||||||
|
PermissionLevel,
|
||||||
|
GetResourcePermsResponse,
|
||||||
|
ResourcePermissionInfo,
|
||||||
|
GetDependantResourcesResponse,
|
||||||
|
} from "@budibase/types"
|
||||||
import { getRoleParams } from "../../db/utils"
|
import { getRoleParams } from "../../db/utils"
|
||||||
import {
|
import {
|
||||||
CURRENTLY_SUPPORTED_LEVELS,
|
CURRENTLY_SUPPORTED_LEVELS,
|
||||||
|
@ -145,33 +153,40 @@ export async function fetch(ctx: UserCtx) {
|
||||||
ctx.body = finalPermissions
|
ctx.body = finalPermissions
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getResourcePerms(ctx: UserCtx) {
|
export async function getResourcePerms(
|
||||||
const resourceId = ctx.params.resourceId
|
ctx: UserCtx<void, GetResourcePermsResponse>
|
||||||
const db = context.getAppDB()
|
|
||||||
const body = await db.allDocs(
|
|
||||||
getRoleParams(null, {
|
|
||||||
include_docs: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const rolesList = body.rows.map(row => row.doc)
|
|
||||||
let permissions: Record<string, string> = {}
|
|
||||||
for (let level of SUPPORTED_LEVELS) {
|
|
||||||
// update the various roleIds in the resource permissions
|
|
||||||
for (let role of rolesList) {
|
|
||||||
const rolePerms = roles.checkForRoleResourceArray(
|
|
||||||
role.permissions,
|
|
||||||
resourceId
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
rolePerms &&
|
|
||||||
rolePerms[resourceId] &&
|
|
||||||
rolePerms[resourceId].indexOf(level) !== -1
|
|
||||||
) {
|
) {
|
||||||
permissions[level] = roles.getExternalRoleID(role._id, role.version)!
|
const resourceId = ctx.params.resourceId
|
||||||
|
const resourcePermissions = await sdk.permissions.getResourcePerms(resourceId)
|
||||||
|
const inheritablePermissions =
|
||||||
|
await sdk.permissions.getInheritablePermissions(resourceId)
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
permissions: Object.entries(resourcePermissions).reduce(
|
||||||
|
(p, [level, role]) => {
|
||||||
|
p[level] = {
|
||||||
|
role: role.role,
|
||||||
|
permissionType: role.type,
|
||||||
|
inheritablePermission:
|
||||||
|
inheritablePermissions && inheritablePermissions[level].role,
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
},
|
||||||
|
{} as Record<string, ResourcePermissionInfo>
|
||||||
|
),
|
||||||
|
requiresPlanToModify: (
|
||||||
|
await sdk.permissions.allowsExplicitPermissions(resourceId)
|
||||||
|
).minPlan,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getDependantResources(
|
||||||
|
ctx: UserCtx<void, GetDependantResourcesResponse>
|
||||||
|
) {
|
||||||
|
const resourceId = ctx.params.resourceId
|
||||||
|
ctx.body = {
|
||||||
|
resourceByType: await sdk.permissions.getDependantResources(resourceId),
|
||||||
}
|
}
|
||||||
ctx.body = Object.assign(getBasePermissions(resourceId), permissions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addPermission(ctx: UserCtx) {
|
export async function addPermission(ctx: UserCtx) {
|
||||||
|
|
|
@ -23,6 +23,11 @@ router
|
||||||
authorized(permissions.BUILDER),
|
authorized(permissions.BUILDER),
|
||||||
controller.getResourcePerms
|
controller.getResourcePerms
|
||||||
)
|
)
|
||||||
|
.get(
|
||||||
|
"/api/permission/:resourceId/dependants",
|
||||||
|
authorized(permissions.BUILDER),
|
||||||
|
controller.getDependantResources
|
||||||
|
)
|
||||||
// adding a specific role/level for the resource overrides the underlying access control
|
// adding a specific role/level for the resource overrides the underlying access control
|
||||||
.post(
|
.post(
|
||||||
"/api/permission/:roleId/:resourceId/:level",
|
"/api/permission/:roleId/:resourceId/:level",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const mockedSdk = sdk.permissions as jest.Mocked<typeof sdk.permissions>
|
const mockedSdk = sdk.permissions as jest.Mocked<typeof sdk.permissions>
|
||||||
jest.mock("../../../sdk/app/permissions", () => ({
|
jest.mock("../../../sdk/app/permissions", () => ({
|
||||||
|
...jest.requireActual("../../../sdk/app/permissions"),
|
||||||
resourceActionAllowed: jest.fn(),
|
resourceActionAllowed: jest.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -78,8 +79,12 @@ describe("/permission", () => {
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.body["read"]).toEqual(STD_ROLE_ID)
|
expect(res.body).toEqual({
|
||||||
expect(res.body["write"]).toEqual(HIGHER_ROLE_ID)
|
permissions: {
|
||||||
|
read: { permissionType: "EXPLICIT", role: STD_ROLE_ID },
|
||||||
|
write: { permissionType: "BASE", role: HIGHER_ROLE_ID },
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should get resource permissions with multiple roles", async () => {
|
it("should get resource permissions with multiple roles", async () => {
|
||||||
|
@ -89,15 +94,20 @@ describe("/permission", () => {
|
||||||
level: PermissionLevel.WRITE,
|
level: PermissionLevel.WRITE,
|
||||||
})
|
})
|
||||||
const res = await config.api.permission.get(table._id)
|
const res = await config.api.permission.get(table._id)
|
||||||
expect(res.body["read"]).toEqual(STD_ROLE_ID)
|
expect(res.body).toEqual({
|
||||||
expect(res.body["write"]).toEqual(HIGHER_ROLE_ID)
|
permissions: {
|
||||||
|
read: { permissionType: "EXPLICIT", role: STD_ROLE_ID },
|
||||||
|
write: { permissionType: "EXPLICIT", role: HIGHER_ROLE_ID },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const allRes = await request
|
const allRes = await request
|
||||||
.get(`/api/permission`)
|
.get(`/api/permission`)
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(allRes.body[table._id]["write"]).toEqual(HIGHER_ROLE_ID)
|
|
||||||
expect(allRes.body[table._id]["read"]).toEqual(STD_ROLE_ID)
|
expect(allRes.body[table._id]["read"]).toEqual(STD_ROLE_ID)
|
||||||
|
expect(allRes.body[table._id]["write"]).toEqual(HIGHER_ROLE_ID)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("throw forbidden if the action is not allowed for the resource", async () => {
|
it("throw forbidden if the action is not allowed for the resource", async () => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Router from "@koa/router"
|
import Router from "@koa/router"
|
||||||
import * as viewController from "../controllers/view"
|
import * as viewController from "../controllers/view"
|
||||||
import * as rowController from "../controllers/row"
|
import * as rowController from "../controllers/row"
|
||||||
import authorized from "../../middleware/authorized"
|
import authorized, { authorizedResource } from "../../middleware/authorized"
|
||||||
import { paramResource } from "../../middleware/resourceId"
|
import { paramResource } from "../../middleware/resourceId"
|
||||||
import { permissions } from "@budibase/backend-core"
|
import { permissions } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
@ -10,10 +10,10 @@ const router: Router = new Router()
|
||||||
router
|
router
|
||||||
.get(
|
.get(
|
||||||
"/api/v2/views/:viewId",
|
"/api/v2/views/:viewId",
|
||||||
paramResource("viewId"),
|
authorizedResource(
|
||||||
authorized(
|
permissions.PermissionType.VIEW,
|
||||||
permissions.PermissionType.TABLE,
|
permissions.PermissionLevel.READ,
|
||||||
permissions.PermissionLevel.READ
|
"viewId"
|
||||||
),
|
),
|
||||||
viewController.v2.get
|
viewController.v2.get
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,10 +1,24 @@
|
||||||
|
import { context, db, env, roles } from "@budibase/backend-core"
|
||||||
|
import { features } from "@budibase/pro"
|
||||||
import {
|
import {
|
||||||
DocumentType,
|
DocumentType,
|
||||||
PermissionLevel,
|
PermissionLevel,
|
||||||
|
PermissionSource,
|
||||||
|
PlanType,
|
||||||
|
Role,
|
||||||
VirtualDocumentType,
|
VirtualDocumentType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { isViewID } from "../../../db/utils"
|
import {
|
||||||
import { features } from "@budibase/pro"
|
extractViewInfoFromID,
|
||||||
|
getRoleParams,
|
||||||
|
isViewID,
|
||||||
|
} from "../../../db/utils"
|
||||||
|
import {
|
||||||
|
CURRENTLY_SUPPORTED_LEVELS,
|
||||||
|
getBasePermissions,
|
||||||
|
} from "../../../utilities/security"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
import { isV2 } from "../views"
|
||||||
|
|
||||||
type ResourceActionAllowedResult =
|
type ResourceActionAllowedResult =
|
||||||
| { allowed: true }
|
| { allowed: true }
|
||||||
|
@ -35,3 +49,117 @@ export async function resourceActionAllowed({
|
||||||
resourceType: VirtualDocumentType.VIEW,
|
resourceType: VirtualDocumentType.VIEW,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResourcePermissions = Record<
|
||||||
|
string,
|
||||||
|
{ role: string; type: PermissionSource }
|
||||||
|
>
|
||||||
|
|
||||||
|
export async function getInheritablePermissions(
|
||||||
|
resourceId: string
|
||||||
|
): Promise<ResourcePermissions | undefined> {
|
||||||
|
if (isViewID(resourceId)) {
|
||||||
|
return await getResourcePerms(extractViewInfoFromID(resourceId).tableId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function allowsExplicitPermissions(resourceId: string) {
|
||||||
|
if (isViewID(resourceId)) {
|
||||||
|
const allowed = await features.isViewPermissionEnabled()
|
||||||
|
const minPlan = !allowed
|
||||||
|
? env.SELF_HOSTED
|
||||||
|
? PlanType.BUSINESS
|
||||||
|
: PlanType.PREMIUM
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed,
|
||||||
|
minPlan,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getResourcePerms(
|
||||||
|
resourceId: string
|
||||||
|
): Promise<ResourcePermissions> {
|
||||||
|
const db = context.getAppDB()
|
||||||
|
const body = await db.allDocs(
|
||||||
|
getRoleParams(null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const rolesList = body.rows.map<Role>(row => row.doc)
|
||||||
|
let permissions: ResourcePermissions = {}
|
||||||
|
|
||||||
|
const permsToInherit = await getInheritablePermissions(resourceId)
|
||||||
|
|
||||||
|
const allowsExplicitPerm = (await allowsExplicitPermissions(resourceId))
|
||||||
|
.allowed
|
||||||
|
|
||||||
|
for (let level of CURRENTLY_SUPPORTED_LEVELS) {
|
||||||
|
// update the various roleIds in the resource permissions
|
||||||
|
for (let role of rolesList) {
|
||||||
|
const rolePerms = allowsExplicitPerm
|
||||||
|
? roles.checkForRoleResourceArray(role.permissions, resourceId)
|
||||||
|
: {}
|
||||||
|
if (rolePerms[resourceId]?.indexOf(level) > -1) {
|
||||||
|
permissions[level] = {
|
||||||
|
role: roles.getExternalRoleID(role._id!, role.version),
|
||||||
|
type: PermissionSource.EXPLICIT,
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
!permissions[level] &&
|
||||||
|
permsToInherit &&
|
||||||
|
permsToInherit[level]
|
||||||
|
) {
|
||||||
|
permissions[level] = {
|
||||||
|
role: permsToInherit[level].role,
|
||||||
|
type: PermissionSource.INHERITED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const basePermissions = Object.entries(
|
||||||
|
getBasePermissions(resourceId)
|
||||||
|
).reduce<ResourcePermissions>((p, [level, role]) => {
|
||||||
|
p[level] = { role, type: PermissionSource.BASE }
|
||||||
|
return p
|
||||||
|
}, {})
|
||||||
|
const result = Object.assign(basePermissions, permissions)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDependantResources(
|
||||||
|
resourceId: string
|
||||||
|
): Promise<Record<string, number> | undefined> {
|
||||||
|
if (db.isTableId(resourceId)) {
|
||||||
|
const dependants: Record<string, Set<string>> = {}
|
||||||
|
|
||||||
|
const table = await sdk.tables.getTable(resourceId)
|
||||||
|
const views = Object.values(table.views || {})
|
||||||
|
|
||||||
|
for (const view of views) {
|
||||||
|
if (!isV2(view)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = await getResourcePerms(view.id)
|
||||||
|
for (const [level, roleInfo] of Object.entries(permissions)) {
|
||||||
|
if (roleInfo.type === PermissionSource.INHERITED) {
|
||||||
|
dependants[VirtualDocumentType.VIEW] ??= new Set()
|
||||||
|
dependants[VirtualDocumentType.VIEW].add(view.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(dependants).reduce((p, [type, resources]) => {
|
||||||
|
p[type] = resources.size
|
||||||
|
return p
|
||||||
|
}, {} as Record<string, number>)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
@ -4,3 +4,4 @@ export * from "./row"
|
||||||
export * from "./view"
|
export * from "./view"
|
||||||
export * from "./rows"
|
export * from "./rows"
|
||||||
export * from "./table"
|
export * from "./table"
|
||||||
|
export * from "./permission"
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { PlanType } from "../../../sdk"
|
||||||
|
|
||||||
|
export interface ResourcePermissionInfo {
|
||||||
|
role: string
|
||||||
|
permissionType: string
|
||||||
|
inheritablePermission?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetResourcePermsResponse {
|
||||||
|
permissions: Record<string, ResourcePermissionInfo>
|
||||||
|
requiresPlanToModify?: PlanType
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetDependantResourcesResponse {
|
||||||
|
resourceByType?: Record<string, number>
|
||||||
|
}
|
|
@ -17,3 +17,9 @@ export enum PermissionType {
|
||||||
QUERY = "query",
|
QUERY = "query",
|
||||||
VIEW = "view",
|
VIEW = "view",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PermissionSource {
|
||||||
|
EXPLICIT = "EXPLICIT",
|
||||||
|
INHERITED = "INHERITED",
|
||||||
|
BASE = "BASE",
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue