Merge branch 'develop' into account-portal-auth-api-testing-2
This commit is contained in:
commit
720516ed9f
37 changed files with 616 additions and 310 deletions
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.9.33-alpha.11",
|
||||
"version": "2.9.33-alpha.16",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -11,7 +11,11 @@ export function getDB(dbName?: string, opts?: any): Database {
|
|||
// we have to use a callback for this so that we can close
|
||||
// the DB when we're done, without this manual requests would
|
||||
// need to close the database when done with it to avoid memory leaks
|
||||
export async function doWithDB(dbName: string, cb: any, opts = {}) {
|
||||
export async function doWithDB<T>(
|
||||
dbName: string,
|
||||
cb: (db: Database) => Promise<T>,
|
||||
opts = {}
|
||||
) {
|
||||
const db = getDB(dbName, opts)
|
||||
// need this to be async so that we can correctly close DB after all
|
||||
// async operations have been completed
|
||||
|
|
|
@ -87,6 +87,7 @@ export const BUILTIN_PERMISSIONS = {
|
|||
new Permission(PermissionType.QUERY, PermissionLevel.WRITE),
|
||||
new Permission(PermissionType.TABLE, PermissionLevel.WRITE),
|
||||
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
|
||||
new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ),
|
||||
],
|
||||
},
|
||||
POWER: {
|
||||
|
@ -97,6 +98,7 @@ export const BUILTIN_PERMISSIONS = {
|
|||
new Permission(PermissionType.USER, PermissionLevel.READ),
|
||||
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
|
||||
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
|
||||
new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ),
|
||||
],
|
||||
},
|
||||
ADMIN: {
|
||||
|
@ -108,6 +110,7 @@ export const BUILTIN_PERMISSIONS = {
|
|||
new Permission(PermissionType.AUTOMATION, PermissionLevel.ADMIN),
|
||||
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
|
||||
new Permission(PermissionType.QUERY, PermissionLevel.ADMIN),
|
||||
new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ),
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
|
@ -253,7 +253,7 @@ export function checkForRoleResourceArray(
|
|||
* Given an app ID this will retrieve all of the roles that are currently within that app.
|
||||
* @return {Promise<object[]>} An array of the role objects that were found.
|
||||
*/
|
||||
export async function getAllRoles(appId?: string) {
|
||||
export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
|
||||
if (appId) {
|
||||
return doWithDB(appId, internal)
|
||||
} else {
|
||||
|
@ -312,37 +312,6 @@ export async function getAllRoles(appId?: string) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This retrieves the required role for a resource
|
||||
* @param permLevel The level of request
|
||||
* @param resourceId The resource being requested
|
||||
* @param subResourceId The sub resource being requested
|
||||
* @return {Promise<{permissions}|Object>} returns the permissions required to access.
|
||||
*/
|
||||
export async function getRequiredResourceRole(
|
||||
permLevel: string,
|
||||
{ resourceId, subResourceId }: { resourceId?: string; subResourceId?: string }
|
||||
) {
|
||||
const roles = await getAllRoles()
|
||||
let main = [],
|
||||
sub = []
|
||||
for (let role of roles) {
|
||||
// no permissions, ignore it
|
||||
if (!role.permissions) {
|
||||
continue
|
||||
}
|
||||
const mainRes = resourceId ? role.permissions[resourceId] : undefined
|
||||
const subRes = subResourceId ? role.permissions[subResourceId] : undefined
|
||||
if (mainRes && mainRes.indexOf(permLevel) !== -1) {
|
||||
main.push(role._id)
|
||||
} else if (subRes && subRes.indexOf(permLevel) !== -1) {
|
||||
sub.push(role._id)
|
||||
}
|
||||
}
|
||||
// for now just return the IDs
|
||||
return main.concat(sub)
|
||||
}
|
||||
|
||||
export class AccessController {
|
||||
userHierarchies: { [key: string]: string[] }
|
||||
constructor() {
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
if (!perms["execute"]) {
|
||||
role = "BASIC"
|
||||
} else {
|
||||
role = perms["execute"]
|
||||
role = perms["execute"].role
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,25 +5,19 @@
|
|||
|
||||
export let resourceId
|
||||
export let disabled = false
|
||||
export let requiresLicence
|
||||
|
||||
let modal
|
||||
let resourcePermissions
|
||||
|
||||
async function openDropdown() {
|
||||
resourcePermissions = await permissions.forResource(resourceId)
|
||||
async function openModal() {
|
||||
resourcePermissions = await permissions.forResourceDetailed(resourceId)
|
||||
modal.show()
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionButton icon="LockClosed" quiet on:click={openDropdown} {disabled}>
|
||||
<ActionButton icon="LockClosed" quiet on:click={openModal} {disabled}>
|
||||
Access
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<ManageAccessModal
|
||||
{resourceId}
|
||||
{requiresLicence}
|
||||
levels={$permissions}
|
||||
permissions={resourcePermissions}
|
||||
/>
|
||||
<ManageAccessModal {resourceId} permissions={resourcePermissions} />
|
||||
</Modal>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script>
|
||||
import { licensing, admin } from "stores/portal"
|
||||
import ManageAccessButton from "../ManageAccessButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
|
@ -13,17 +12,6 @@
|
|||
}
|
||||
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>
|
||||
|
||||
<ManageAccessButton {resourceId} {requiresLicence} />
|
||||
<ManageAccessButton {resourceId} />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { PermissionSource } from "@budibase/types"
|
||||
import { roles, permissions as permissionsStore } from "stores/backend"
|
||||
import {
|
||||
Label,
|
||||
|
@ -9,58 +10,126 @@
|
|||
ModalContent,
|
||||
Tags,
|
||||
Tag,
|
||||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import { capitalise } from "helpers"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export let resourceId
|
||||
export let permissions
|
||||
export let requiresLicence
|
||||
|
||||
const inheritedRoleId = "inherited"
|
||||
|
||||
async function changePermission(level, role) {
|
||||
try {
|
||||
await permissionsStore.save({
|
||||
level,
|
||||
role,
|
||||
resource: resourceId,
|
||||
})
|
||||
if (role === inheritedRoleId) {
|
||||
await permissionsStore.remove({
|
||||
level,
|
||||
role,
|
||||
resource: resourceId,
|
||||
})
|
||||
} else {
|
||||
await permissionsStore.save({
|
||||
level,
|
||||
role,
|
||||
resource: resourceId,
|
||||
})
|
||||
}
|
||||
|
||||
// Show updated permissions in UI: REMOVE
|
||||
permissions = await permissionsStore.forResource(resourceId)
|
||||
permissions = await permissionsStore.forResourceDetailed(resourceId)
|
||||
notifications.success("Updated permissions")
|
||||
} catch (error) {
|
||||
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>
|
||||
|
||||
<ModalContent showCancelButton={false} confirmText="Done">
|
||||
<span slot="header">
|
||||
Manage Access
|
||||
{#if requiresLicence}
|
||||
{#if requiresPlanToModify}
|
||||
<span class="lock-tag">
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">{requiresLicence.tier}</Tag>
|
||||
<Tag icon="LockClosed">{capitalise(requiresPlanToModify)}</Tag>
|
||||
</Tags>
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
{#if requiresLicence}
|
||||
<Body size="S">{requiresLicence.message}</Body>
|
||||
{:else}
|
||||
<Body size="S">Specify the minimum access level role for this data.</Body>
|
||||
<div class="row">
|
||||
<Label extraSmall grey>Level</Label>
|
||||
<Label extraSmall grey>Role</Label>
|
||||
{#each Object.keys(permissions) as level}
|
||||
<Input value={capitalise(level)} disabled />
|
||||
<Select
|
||||
value={permissions[level]}
|
||||
on:change={e => changePermission(level, e.detail)}
|
||||
options={$roles}
|
||||
getOptionLabel={x => x.name}
|
||||
getOptionValue={x => x._id}
|
||||
/>
|
||||
{/each}
|
||||
<Body size="S">Specify the minimum access level role for this data.</Body>
|
||||
<div class="row">
|
||||
<Label extraSmall grey>Level</Label>
|
||||
<Label extraSmall grey>Role</Label>
|
||||
{#each Object.keys(computedPermissions) as level}
|
||||
<Input value={capitalise(level)} disabled />
|
||||
<Select
|
||||
disabled={requiresPlanToModify}
|
||||
placeholder={false}
|
||||
value={computedPermissions[level].selectedValue}
|
||||
on:change={e => changePermission(level, e.detail)}
|
||||
options={computedPermissions[level].options}
|
||||
getOptionLabel={x => x.name}
|
||||
getOptionValue={x => x._id}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if dependantsInfoMessage}
|
||||
<div class="inheriting-resources">
|
||||
<Icon name="Alert" />
|
||||
<Body size="S">
|
||||
<i>
|
||||
{dependantsInfoMessage}
|
||||
</i>
|
||||
</Body>
|
||||
</div>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
|
@ -75,4 +144,9 @@
|
|||
.lock-tag {
|
||||
padding-left: var(--spacing-s);
|
||||
}
|
||||
|
||||
.inheriting-resources {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
return
|
||||
}
|
||||
try {
|
||||
roleId = (await permissions.forResource(queryToFetch._id))["read"]
|
||||
roleId = (await permissions.forResource(queryToFetch._id))["read"].role
|
||||
} catch (err) {
|
||||
roleId = Constants.Roles.BASIC
|
||||
}
|
||||
|
|
|
@ -120,7 +120,7 @@
|
|||
await usersFetch.refresh()
|
||||
|
||||
filteredUsers = $usersFetch.rows
|
||||
.filter(user => !user?.admin?.global) // filter out global admins
|
||||
.filter(user => user.email !== $auth.user.email)
|
||||
.map(user => {
|
||||
const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder(
|
||||
user,
|
||||
|
@ -150,13 +150,10 @@
|
|||
}
|
||||
|
||||
const sortInviteRoles = (a, b) => {
|
||||
const aEmpty =
|
||||
!a.info?.appBuilders?.length && Object.keys(a.info.apps).length === 0
|
||||
const bEmpty =
|
||||
!b.info?.appBuilders?.length && Object.keys(b.info.apps).length === 0
|
||||
const aAppsEmpty = !a.info?.apps?.length && !a.info?.builder?.apps?.length
|
||||
const bAppsEmpty = !b.info?.apps?.length && !b.info?.builder?.apps?.length
|
||||
|
||||
if (aEmpty && !bEmpty) return 1
|
||||
if (!aEmpty && bEmpty) return -1
|
||||
return aAppsEmpty && !bAppsEmpty ? 1 : !aAppsEmpty && bAppsEmpty ? -1 : 0
|
||||
}
|
||||
|
||||
const sortRoles = (a, b) => {
|
||||
|
@ -366,18 +363,19 @@
|
|||
const payload = [
|
||||
{
|
||||
email: newUserEmail,
|
||||
builder: !!creationRoleType === Constants.BudibaseRoles.Admin,
|
||||
admin: !!creationRoleType === Constants.BudibaseRoles.Admin,
|
||||
builder: { global: creationRoleType === Constants.BudibaseRoles.Admin },
|
||||
admin: { global: creationRoleType === Constants.BudibaseRoles.Admin },
|
||||
},
|
||||
]
|
||||
|
||||
if (creationAccessType === Constants.Roles.CREATOR) {
|
||||
payload[0].appBuilders = [prodAppId]
|
||||
} else {
|
||||
payload[0].apps = {
|
||||
[prodAppId]: creationAccessType,
|
||||
}
|
||||
const notCreatingAdmin = creationRoleType !== Constants.BudibaseRoles.Admin
|
||||
const isCreator = creationAccessType === Constants.Roles.CREATOR
|
||||
if (notCreatingAdmin && isCreator) {
|
||||
payload[0].builder.apps = [prodAppId]
|
||||
} else if (notCreatingAdmin && !isCreator) {
|
||||
payload[0].apps = { [prodAppId]: creationAccessType }
|
||||
}
|
||||
|
||||
let userInviteResponse
|
||||
try {
|
||||
userInviteResponse = await users.onboard(payload)
|
||||
|
@ -438,10 +436,11 @@
|
|||
}
|
||||
|
||||
if (role === Constants.Roles.CREATOR) {
|
||||
updateBody.appBuilders = [...(updateBody.appBuilders ?? []), prodAppId]
|
||||
updateBody.builder = updateBody.builder || {}
|
||||
updateBody.builder.apps = [...(updateBody.builder.apps ?? []), prodAppId]
|
||||
delete updateBody?.apps?.[prodAppId]
|
||||
} else if (role !== Constants.Roles.CREATOR && invite?.appBuilders) {
|
||||
invite.appBuilders = []
|
||||
} else if (role !== Constants.Roles.CREATOR && invite?.builder?.apps) {
|
||||
invite.builder.apps = []
|
||||
}
|
||||
await users.updateInvite(updateBody)
|
||||
await filterInvites(query)
|
||||
|
@ -494,6 +493,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
const getInviteRoleValue = invite => {
|
||||
if (invite.info?.admin?.global && invite.info?.builder?.global) {
|
||||
return Constants.Roles.ADMIN
|
||||
}
|
||||
|
||||
if (invite.info?.builder?.apps?.includes(prodAppId)) {
|
||||
return Constants.Roles.CREATOR
|
||||
}
|
||||
|
||||
return invite.info.apps?.[prodAppId]
|
||||
}
|
||||
|
||||
const getRoleFooter = user => {
|
||||
if (user.group) {
|
||||
const role = $roles.find(role => role._id === user.role)
|
||||
|
@ -531,7 +542,9 @@
|
|||
<Heading size="S">{invitingFlow ? "Invite new user" : "Users"}</Heading>
|
||||
</div>
|
||||
<div class="header">
|
||||
<Button on:click={openInviteFlow} size="S" cta>Invite user</Button>
|
||||
{#if !invitingFlow}
|
||||
<Button on:click={openInviteFlow} size="S" cta>Invite user</Button>
|
||||
{/if}
|
||||
<Icon
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
name="RailRightClose"
|
||||
|
@ -600,6 +613,11 @@
|
|||
<div class="auth-entity-access-title">Access</div>
|
||||
</div>
|
||||
{#each filteredInvites as invite}
|
||||
{@const user = {
|
||||
isAdminOrGlobalBuilder:
|
||||
invite.info?.admin?.global && invite.info?.builder?.global,
|
||||
}}
|
||||
|
||||
<div class="auth-entity">
|
||||
<div class="details">
|
||||
<div class="user-email" title={invite.email}>
|
||||
|
@ -608,10 +626,9 @@
|
|||
</div>
|
||||
<div class="auth-entity-access">
|
||||
<RoleSelect
|
||||
footer={getRoleFooter(user)}
|
||||
placeholder={false}
|
||||
value={invite.info?.appBuilders?.includes(prodAppId)
|
||||
? Constants.Roles.CREATOR
|
||||
: invite.info.apps?.[prodAppId]}
|
||||
value={getInviteRoleValue(invite)}
|
||||
allowRemove={invite.info.apps?.[prodAppId]}
|
||||
allowPublic={false}
|
||||
allowCreator={true}
|
||||
|
@ -624,6 +641,9 @@
|
|||
}}
|
||||
autoWidth
|
||||
align="right"
|
||||
allowedRoles={user.isAdminOrGlobalBuilder
|
||||
? [Constants.Roles.ADMIN]
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
|
||||
let loaded = false
|
||||
let editModal, deleteModal
|
||||
$: console.log(group)
|
||||
|
||||
$: scimEnabled = $features.isScimEnabled
|
||||
$: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled
|
||||
$: group = $groups.find(x => x._id === groupId)
|
||||
|
@ -62,7 +62,7 @@
|
|||
? Constants.Roles.CREATOR
|
||||
: group?.roles?.[apps.getProdAppID(app.devId)],
|
||||
}))
|
||||
$: console.log(groupApps)
|
||||
|
||||
$: {
|
||||
if (loaded && !group?._id) {
|
||||
$goto("./")
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
export let value
|
||||
export let row
|
||||
$: console.log(row)
|
||||
$: priviliged = sdk.users.isAdminOrBuilder(row)
|
||||
$: count = getCount(row)
|
||||
|
||||
|
@ -14,10 +13,10 @@
|
|||
return $apps.length
|
||||
} else {
|
||||
return sdk.users.hasAppBuilderPermissions(row)
|
||||
? row.builder.apps.length +
|
||||
Object.keys(row.roles || {}).filter(appId =>
|
||||
row.builder.apps.includes(appId)
|
||||
).length
|
||||
? row?.builder?.apps?.length +
|
||||
Object.keys(row.roles || {}).filter(appId => {
|
||||
row?.builder?.apps?.includes(appId)
|
||||
}).length
|
||||
: value?.length || 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
admin: "Full access",
|
||||
}
|
||||
|
||||
$: role = Constants.BudibaseRoleOptions.find(
|
||||
$: role = Constants.BudibaseRoleOptionsOld.find(
|
||||
x => x.value === users.getUserRole(row)
|
||||
)
|
||||
$: value = role?.label || "Not available"
|
||||
|
|
|
@ -13,9 +13,22 @@ export function createPermissionStore() {
|
|||
level,
|
||||
})
|
||||
},
|
||||
remove: async ({ level, role, resource }) => {
|
||||
return await API.removePermissionFromResource({
|
||||
resourceId: resource,
|
||||
roleId: role,
|
||||
level,
|
||||
})
|
||||
},
|
||||
forResource: async resourceId => {
|
||||
return (await API.getPermissionForResource(resourceId)).permissions
|
||||
},
|
||||
forResourceDetailed: async resourceId => {
|
||||
return await API.getPermissionForResource(resourceId)
|
||||
},
|
||||
getDependantsInfo: async resourceId => {
|
||||
return await API.getDependants(resourceId)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -121,8 +121,11 @@ export function createUsersStore() {
|
|||
}
|
||||
|
||||
const getUserRole = user =>
|
||||
sdk.users.isAdminOrGlobalBuilder(user) ? "admin" : "appUser"
|
||||
|
||||
sdk.users.isAdmin(user)
|
||||
? "admin"
|
||||
: sdk.users.isBuilder(user)
|
||||
? "developer"
|
||||
: "appUser"
|
||||
const refreshUsage =
|
||||
fn =>
|
||||
async (...args) => {
|
||||
|
|
|
@ -21,4 +21,27 @@ export const buildPermissionsEndpoints = API => ({
|
|||
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`,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
@ -144,8 +144,8 @@ export const buildUserEndpoints = API => ({
|
|||
body: {
|
||||
email,
|
||||
userInfo: {
|
||||
admin: admin ? { global: true } : undefined,
|
||||
builder: builder ? { global: true } : undefined,
|
||||
admin: admin?.global ? { global: true } : undefined,
|
||||
builder: builder?.global ? { global: true } : undefined,
|
||||
apps: apps ? apps : undefined,
|
||||
},
|
||||
},
|
||||
|
@ -156,14 +156,13 @@ export const buildUserEndpoints = API => ({
|
|||
return await API.post({
|
||||
url: "/api/global/users/onboard",
|
||||
body: payload.map(invite => {
|
||||
const { email, admin, builder, apps, appBuilders } = invite
|
||||
const { email, admin, builder, apps } = invite
|
||||
return {
|
||||
email,
|
||||
userInfo: {
|
||||
admin: admin ? { global: true } : undefined,
|
||||
builder: builder ? { global: true } : undefined,
|
||||
admin,
|
||||
builder,
|
||||
apps: apps ? apps : undefined,
|
||||
appBuilders,
|
||||
},
|
||||
}
|
||||
}),
|
||||
|
@ -176,12 +175,11 @@ export const buildUserEndpoints = API => ({
|
|||
* @param invite the invite code sent in the email
|
||||
*/
|
||||
updateUserInvite: async invite => {
|
||||
console.log(invite)
|
||||
await API.post({
|
||||
url: `/api/global/users/invite/update/${invite.code}`,
|
||||
body: {
|
||||
apps: invite.apps,
|
||||
appBuilders: invite.appBuilders,
|
||||
builder: invite.builder,
|
||||
},
|
||||
})
|
||||
},
|
||||
|
|
|
@ -23,6 +23,11 @@ export const BudibaseRoles = {
|
|||
Admin: "admin",
|
||||
}
|
||||
|
||||
export const BudibaseRoleOptionsOld = [
|
||||
{ label: "Developer", value: BudibaseRoles.Developer },
|
||||
{ label: "Member", value: BudibaseRoles.AppUser },
|
||||
{ label: "Admin", value: BudibaseRoles.Admin },
|
||||
]
|
||||
export const BudibaseRoleOptions = [
|
||||
{ label: "Member", value: BudibaseRoles.AppUser },
|
||||
{ label: "Admin", value: BudibaseRoles.Admin },
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
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 {
|
||||
CURRENTLY_SUPPORTED_LEVELS,
|
||||
|
@ -145,33 +153,40 @@ export async function fetch(ctx: UserCtx) {
|
|||
ctx.body = finalPermissions
|
||||
}
|
||||
|
||||
export async function getResourcePerms(ctx: UserCtx) {
|
||||
export async function getResourcePerms(
|
||||
ctx: UserCtx<void, GetResourcePermsResponse>
|
||||
) {
|
||||
const resourceId = ctx.params.resourceId
|
||||
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 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) {
|
||||
|
|
|
@ -23,6 +23,11 @@ router
|
|||
authorized(permissions.BUILDER),
|
||||
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
|
||||
.post(
|
||||
"/api/permission/:roleId/:resourceId/:level",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const mockedSdk = sdk.permissions as jest.Mocked<typeof sdk.permissions>
|
||||
jest.mock("../../../sdk/app/permissions", () => ({
|
||||
...jest.requireActual("../../../sdk/app/permissions"),
|
||||
resourceActionAllowed: jest.fn(),
|
||||
}))
|
||||
|
||||
|
@ -78,8 +79,12 @@ describe("/permission", () => {
|
|||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body["read"]).toEqual(STD_ROLE_ID)
|
||||
expect(res.body["write"]).toEqual(HIGHER_ROLE_ID)
|
||||
expect(res.body).toEqual({
|
||||
permissions: {
|
||||
read: { permissionType: "EXPLICIT", role: STD_ROLE_ID },
|
||||
write: { permissionType: "BASE", role: HIGHER_ROLE_ID },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should get resource permissions with multiple roles", async () => {
|
||||
|
@ -89,15 +94,20 @@ describe("/permission", () => {
|
|||
level: PermissionLevel.WRITE,
|
||||
})
|
||||
const res = await config.api.permission.get(table._id)
|
||||
expect(res.body["read"]).toEqual(STD_ROLE_ID)
|
||||
expect(res.body["write"]).toEqual(HIGHER_ROLE_ID)
|
||||
expect(res.body).toEqual({
|
||||
permissions: {
|
||||
read: { permissionType: "EXPLICIT", role: STD_ROLE_ID },
|
||||
write: { permissionType: "EXPLICIT", role: HIGHER_ROLE_ID },
|
||||
},
|
||||
})
|
||||
|
||||
const allRes = await request
|
||||
.get(`/api/permission`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.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]["write"]).toEqual(HIGHER_ROLE_ID)
|
||||
})
|
||||
|
||||
it("throw forbidden if the action is not allowed for the resource", async () => {
|
||||
|
@ -260,4 +270,21 @@ describe("/permission", () => {
|
|||
expect(publicPerm.name).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("default permissions", () => {
|
||||
it("legacy views", async () => {
|
||||
const legacyView = await config.createLegacyView()
|
||||
|
||||
const res = await config.api.permission.get(legacyView.name)
|
||||
|
||||
expect(res.body).toEqual({
|
||||
permissions: {
|
||||
read: {
|
||||
permissionType: "BASE",
|
||||
role: "BASIC",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import tk from "timekeeper"
|
||||
import { outputProcessing } from "../../../utilities/rowProcessor"
|
||||
import * as setup from "./utilities"
|
||||
import { context, tenancy } from "@budibase/backend-core"
|
||||
import { context, roles, tenancy } from "@budibase/backend-core"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import {
|
||||
FieldType,
|
||||
MonthlyQuotaName,
|
||||
PermissionLevel,
|
||||
QuotaUsageType,
|
||||
Row,
|
||||
SortOrder,
|
||||
|
@ -16,6 +17,7 @@ import {
|
|||
import {
|
||||
expectAnyInternalColsAttributes,
|
||||
generator,
|
||||
mocks,
|
||||
structures,
|
||||
} from "@budibase/backend-core/tests"
|
||||
|
||||
|
@ -37,6 +39,7 @@ describe("/rows", () => {
|
|||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
mocks.licenses.useCloudFree()
|
||||
table = await config.createTable()
|
||||
row = basicRow(table._id!)
|
||||
})
|
||||
|
@ -670,7 +673,7 @@ describe("/rows", () => {
|
|||
})
|
||||
|
||||
it("should be able to run on a view", async () => {
|
||||
const view = await config.createView()
|
||||
const view = await config.createLegacyView()
|
||||
const row = await config.createRow()
|
||||
const rowUsage = await getRowUsage()
|
||||
const queryUsage = await getQueryUsage()
|
||||
|
@ -1314,6 +1317,85 @@ describe("/rows", () => {
|
|||
bookmark: expect.any(String),
|
||||
})
|
||||
})
|
||||
|
||||
describe("permissions", () => {
|
||||
let viewId: string
|
||||
let tableId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
const table = await config.createTable(userTable())
|
||||
const rows = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
rows.push(await config.createRow({ tableId: table._id }))
|
||||
}
|
||||
|
||||
const createViewResponse = await config.api.viewV2.create()
|
||||
|
||||
tableId = table._id!
|
||||
viewId = createViewResponse.id
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.licenses.useViewPermissions()
|
||||
})
|
||||
|
||||
it("does not allow public users to fetch by default", async () => {
|
||||
await config.publish()
|
||||
await config.api.viewV2.search(viewId, undefined, {
|
||||
expectStatus: 403,
|
||||
usePublicUser: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("allow public users to fetch when permissions are explicit", async () => {
|
||||
await config.api.permission.set({
|
||||
roleId: roles.BUILTIN_ROLE_IDS.PUBLIC,
|
||||
level: PermissionLevel.READ,
|
||||
resourceId: viewId,
|
||||
})
|
||||
await config.publish()
|
||||
|
||||
const response = await config.api.viewV2.search(viewId, undefined, {
|
||||
usePublicUser: true,
|
||||
})
|
||||
|
||||
expect(response.body.rows).toHaveLength(10)
|
||||
})
|
||||
|
||||
it("allow public users to fetch when permissions are inherited", async () => {
|
||||
await config.api.permission.set({
|
||||
roleId: roles.BUILTIN_ROLE_IDS.PUBLIC,
|
||||
level: PermissionLevel.READ,
|
||||
resourceId: tableId,
|
||||
})
|
||||
await config.publish()
|
||||
|
||||
const response = await config.api.viewV2.search(viewId, undefined, {
|
||||
usePublicUser: true,
|
||||
})
|
||||
|
||||
expect(response.body.rows).toHaveLength(10)
|
||||
})
|
||||
|
||||
it("respects inherited permissions, not allowing not public views from public tables", async () => {
|
||||
await config.api.permission.set({
|
||||
roleId: roles.BUILTIN_ROLE_IDS.PUBLIC,
|
||||
level: PermissionLevel.READ,
|
||||
resourceId: tableId,
|
||||
})
|
||||
await config.api.permission.set({
|
||||
roleId: roles.BUILTIN_ROLE_IDS.POWER,
|
||||
level: PermissionLevel.READ,
|
||||
resourceId: viewId,
|
||||
})
|
||||
await config.publish()
|
||||
|
||||
await config.api.viewV2.search(viewId, undefined, {
|
||||
usePublicUser: true,
|
||||
expectStatus: 403,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -87,7 +87,7 @@ describe("/tables", () => {
|
|||
|
||||
it("updates all the row fields for a table when a schema key is renamed", async () => {
|
||||
const testTable = await config.createTable()
|
||||
await config.createView({
|
||||
await config.createLegacyView({
|
||||
name: "TestView",
|
||||
field: "Price",
|
||||
calculation: "stats",
|
||||
|
@ -254,7 +254,7 @@ describe("/tables", () => {
|
|||
}))
|
||||
|
||||
await config.api.viewV2.create({ tableId })
|
||||
await config.createView({ tableId, name: generator.guid() })
|
||||
await config.createLegacyView({ tableId, name: generator.guid() })
|
||||
|
||||
const res = await config.api.table.fetch()
|
||||
|
||||
|
|
|
@ -249,7 +249,7 @@ describe("/views", () => {
|
|||
})
|
||||
|
||||
it("returns only custom views", async () => {
|
||||
await config.createView({
|
||||
await config.createLegacyView({
|
||||
name: "TestView",
|
||||
field: "Price",
|
||||
calculation: "stats",
|
||||
|
@ -267,7 +267,7 @@ describe("/views", () => {
|
|||
|
||||
describe("query", () => {
|
||||
it("returns data for the created view", async () => {
|
||||
await config.createView({
|
||||
await config.createLegacyView({
|
||||
name: "TestView",
|
||||
field: "Price",
|
||||
calculation: "stats",
|
||||
|
@ -295,7 +295,7 @@ describe("/views", () => {
|
|||
})
|
||||
|
||||
it("returns data for the created view using a group by", async () => {
|
||||
await config.createView({
|
||||
await config.createLegacyView({
|
||||
calculation: "stats",
|
||||
name: "TestView",
|
||||
field: "Price",
|
||||
|
@ -331,7 +331,7 @@ describe("/views", () => {
|
|||
describe("destroy", () => {
|
||||
it("should be able to delete a view", async () => {
|
||||
const table = await config.createTable(priceTable())
|
||||
const view = await config.createView()
|
||||
const view = await config.createLegacyView()
|
||||
const res = await request
|
||||
.delete(`/api/views/${view.name}`)
|
||||
.set(config.defaultHeaders())
|
||||
|
@ -395,7 +395,7 @@ describe("/views", () => {
|
|||
|
||||
it("should be able to export a view as JSON", async () => {
|
||||
let table = await setupExport()
|
||||
const view = await config.createView()
|
||||
const view = await config.createLegacyView()
|
||||
table = await config.getTable(table._id)
|
||||
|
||||
let res = await exportView(view.name, "json")
|
||||
|
@ -407,7 +407,7 @@ describe("/views", () => {
|
|||
|
||||
it("should be able to export a view as CSV", async () => {
|
||||
let table = await setupExport()
|
||||
const view = await config.createView()
|
||||
const view = await config.createLegacyView()
|
||||
table = await config.getTable(table._id)
|
||||
|
||||
let res = await exportView(view.name, "csv")
|
||||
|
|
|
@ -296,7 +296,7 @@ describe.each([
|
|||
})
|
||||
|
||||
it("cannot update views v1", async () => {
|
||||
const viewV1 = await config.createView()
|
||||
const viewV1 = await config.createLegacyView()
|
||||
await config.api.viewV2.update(
|
||||
{
|
||||
...viewV1,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Router from "@koa/router"
|
||||
import * as viewController from "../controllers/view"
|
||||
import * as rowController from "../controllers/row"
|
||||
import authorized from "../../middleware/authorized"
|
||||
import authorized, { authorizedResource } from "../../middleware/authorized"
|
||||
import { paramResource } from "../../middleware/resourceId"
|
||||
import { permissions } from "@budibase/backend-core"
|
||||
|
||||
|
@ -10,10 +10,10 @@ const router: Router = new Router()
|
|||
router
|
||||
.get(
|
||||
"/api/v2/views/:viewId",
|
||||
paramResource("viewId"),
|
||||
authorized(
|
||||
permissions.PermissionType.TABLE,
|
||||
permissions.PermissionLevel.READ
|
||||
authorizedResource(
|
||||
permissions.PermissionType.VIEW,
|
||||
permissions.PermissionLevel.READ,
|
||||
"viewId"
|
||||
),
|
||||
viewController.v2.get
|
||||
)
|
||||
|
|
|
@ -6,11 +6,10 @@ import {
|
|||
users,
|
||||
} from "@budibase/backend-core"
|
||||
import { PermissionLevel, PermissionType, Role, UserCtx } from "@budibase/types"
|
||||
import { features } from "@budibase/pro"
|
||||
import builderMiddleware from "./builder"
|
||||
import { isWebhookEndpoint } from "./utils"
|
||||
import { paramResource } from "./resourceId"
|
||||
import { extractViewInfoFromID, isViewID } from "../db/utils"
|
||||
import sdk from "../sdk"
|
||||
|
||||
function hasResource(ctx: any) {
|
||||
return ctx.resourceId != null
|
||||
|
@ -77,31 +76,6 @@ const checkAuthorizedResource = async (
|
|||
}
|
||||
}
|
||||
|
||||
const resourceIdTranformers: Partial<
|
||||
Record<PermissionType, (ctx: UserCtx) => Promise<void>>
|
||||
> = {
|
||||
[PermissionType.VIEW]: async ctx => {
|
||||
const { resourceId } = ctx
|
||||
if (!resourceId) {
|
||||
ctx.throw(400, `Cannot obtain the view id`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isViewID(resourceId)) {
|
||||
ctx.throw(400, `"${resourceId}" is not a valid view id`)
|
||||
return
|
||||
}
|
||||
|
||||
if (await features.isViewPermissionEnabled()) {
|
||||
ctx.subResourceId = ctx.resourceId
|
||||
ctx.resourceId = extractViewInfoFromID(resourceId).tableId
|
||||
} else {
|
||||
ctx.resourceId = extractViewInfoFromID(resourceId).tableId
|
||||
delete ctx.subResourceId
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const authorized =
|
||||
(
|
||||
permType: PermissionType,
|
||||
|
@ -121,8 +95,8 @@ const authorized =
|
|||
}
|
||||
|
||||
// get the resource roles
|
||||
let resourceRoles: any = []
|
||||
let otherLevelRoles: any = []
|
||||
let resourceRoles: string[] = []
|
||||
let otherLevelRoles: string[] = []
|
||||
const otherLevel =
|
||||
permLevel === PermissionLevel.READ
|
||||
? PermissionLevel.WRITE
|
||||
|
@ -133,21 +107,28 @@ const authorized =
|
|||
paramResource(resourcePath)(ctx, () => {})
|
||||
}
|
||||
|
||||
if (resourceIdTranformers[permType]) {
|
||||
await resourceIdTranformers[permType]!(ctx)
|
||||
}
|
||||
|
||||
if (hasResource(ctx)) {
|
||||
const { resourceId, subResourceId } = ctx
|
||||
resourceRoles = await roles.getRequiredResourceRole(permLevel!, {
|
||||
resourceId,
|
||||
subResourceId,
|
||||
})
|
||||
|
||||
const permissions = await sdk.permissions.getResourcePerms(resourceId)
|
||||
const subPermissions =
|
||||
!!subResourceId &&
|
||||
(await sdk.permissions.getResourcePerms(subResourceId))
|
||||
|
||||
function getPermLevel(permLevel: string) {
|
||||
let result: string[] = []
|
||||
if (permissions[permLevel]) {
|
||||
result.push(permissions[permLevel].role)
|
||||
}
|
||||
if (subPermissions && subPermissions[permLevel]) {
|
||||
result.push(subPermissions[permLevel].role)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
resourceRoles = getPermLevel(permLevel!)
|
||||
if (opts && opts.schema) {
|
||||
otherLevelRoles = await roles.getRequiredResourceRole(otherLevel, {
|
||||
resourceId,
|
||||
subResourceId,
|
||||
})
|
||||
otherLevelRoles = getPermLevel(otherLevel!)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,28 +1,20 @@
|
|||
jest.mock("@budibase/backend-core", () => ({
|
||||
...jest.requireActual("@budibase/backend-core"),
|
||||
roles: {
|
||||
...jest.requireActual("@budibase/backend-core").roles,
|
||||
getRequiredResourceRole: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
}))
|
||||
jest.mock("../../environment", () => ({
|
||||
prod: false,
|
||||
isTest: () => true,
|
||||
// @ts-ignore
|
||||
isProd: () => this.prod,
|
||||
_set: function (_key: string, value: string) {
|
||||
this.prod = value === "production"
|
||||
},
|
||||
jest.mock("../../sdk/app/permissions", () => ({
|
||||
...jest.requireActual("../../sdk/app/permissions"),
|
||||
getResourcePerms: jest.fn().mockResolvedValue([]),
|
||||
}))
|
||||
|
||||
import { PermissionType, PermissionLevel } from "@budibase/types"
|
||||
import {
|
||||
PermissionType,
|
||||
PermissionLevel,
|
||||
PermissionSource,
|
||||
} from "@budibase/types"
|
||||
|
||||
import authorizedMiddleware from "../authorized"
|
||||
import env from "../../environment"
|
||||
import { generateTableID, generateViewID } from "../../db/utils"
|
||||
import { roles } from "@budibase/backend-core"
|
||||
import { mocks } from "@budibase/backend-core/tests"
|
||||
import { generator, mocks } from "@budibase/backend-core/tests"
|
||||
import { initProMocks } from "../../tests/utilities/mocks/pro"
|
||||
import { getResourcePerms } from "../../sdk/app/permissions"
|
||||
|
||||
const APP_ID = ""
|
||||
|
||||
|
@ -189,23 +181,26 @@ describe("Authorization middleware", () => {
|
|||
)
|
||||
})
|
||||
|
||||
describe("view type", () => {
|
||||
const tableId = generateTableID()
|
||||
const viewId = generateViewID(tableId)
|
||||
|
||||
const mockedGetRequiredResourceRole =
|
||||
roles.getRequiredResourceRole as jest.MockedFunction<
|
||||
typeof roles.getRequiredResourceRole
|
||||
>
|
||||
describe("with resource", () => {
|
||||
let resourceId: string
|
||||
const mockedGetResourcePerms = getResourcePerms as jest.MockedFunction<
|
||||
typeof getResourcePerms
|
||||
>
|
||||
|
||||
beforeEach(() => {
|
||||
config.setMiddlewareRequiredPermission(
|
||||
PermissionType.VIEW,
|
||||
PermissionLevel.READ
|
||||
)
|
||||
config.setResourceId(viewId)
|
||||
resourceId = generator.guid()
|
||||
config.setResourceId(resourceId)
|
||||
|
||||
mockedGetRequiredResourceRole.mockResolvedValue(["PUBLIC"])
|
||||
mockedGetResourcePerms.mockResolvedValue({
|
||||
[PermissionLevel.READ]: {
|
||||
role: "PUBLIC",
|
||||
type: PermissionSource.BASE,
|
||||
},
|
||||
})
|
||||
|
||||
config.setUser({
|
||||
_id: "user",
|
||||
|
@ -215,57 +210,14 @@ describe("Authorization middleware", () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("will ignore view permissions if flag is off", async () => {
|
||||
it("will fetch resource permissions when resource is set", async () => {
|
||||
await config.executeMiddleware()
|
||||
|
||||
expect(config.throw).not.toBeCalled()
|
||||
expect(config.next).toHaveBeenCalled()
|
||||
|
||||
expect(mockedGetRequiredResourceRole).toBeCalledTimes(1)
|
||||
expect(mockedGetRequiredResourceRole).toBeCalledWith(
|
||||
PermissionLevel.READ,
|
||||
expect.objectContaining({
|
||||
resourceId: tableId,
|
||||
subResourceId: undefined,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("will use view permissions if flag is on", async () => {
|
||||
mocks.licenses.useViewPermissions()
|
||||
await config.executeMiddleware()
|
||||
|
||||
expect(config.throw).not.toBeCalled()
|
||||
expect(config.next).toHaveBeenCalled()
|
||||
|
||||
expect(mockedGetRequiredResourceRole).toBeCalledTimes(1)
|
||||
expect(mockedGetRequiredResourceRole).toBeCalledWith(
|
||||
PermissionLevel.READ,
|
||||
expect.objectContaining({
|
||||
resourceId: tableId,
|
||||
subResourceId: viewId,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("throw an exception if the resource id is not provided", async () => {
|
||||
config.setResourceId(undefined)
|
||||
await config.executeMiddleware()
|
||||
expect(config.throw).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
400,
|
||||
"Cannot obtain the view id"
|
||||
)
|
||||
})
|
||||
|
||||
it("throw an exception if the resource id is not a valid view id", async () => {
|
||||
config.setResourceId(tableId)
|
||||
await config.executeMiddleware()
|
||||
expect(config.throw).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
400,
|
||||
`"${tableId}" is not a valid view id`
|
||||
)
|
||||
expect(mockedGetResourcePerms).toBeCalledTimes(1)
|
||||
expect(mockedGetResourcePerms).toBeCalledWith(resourceId)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -50,9 +50,9 @@ describe("migrations", () => {
|
|||
await config.createRole()
|
||||
await config.createRole()
|
||||
await config.createTable()
|
||||
await config.createView()
|
||||
await config.createLegacyView()
|
||||
await config.createTable()
|
||||
await config.createView(structures.view(config.table!._id!))
|
||||
await config.createLegacyView(structures.view(config.table!._id!))
|
||||
await config.createScreen()
|
||||
await config.createScreen()
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ function tarFilesToTmp(tmpDir: string, files: string[]) {
|
|||
export async function exportDB(
|
||||
dbName: string,
|
||||
opts: DBDumpOpts = {}
|
||||
): Promise<DBDumpOpts> {
|
||||
): Promise<string> {
|
||||
const exportOpts = {
|
||||
filter: opts?.filter,
|
||||
batch_size: 1000,
|
||||
|
|
|
@ -1,10 +1,24 @@
|
|||
import { context, db, env, roles } from "@budibase/backend-core"
|
||||
import { features } from "@budibase/pro"
|
||||
import {
|
||||
DocumentType,
|
||||
PermissionLevel,
|
||||
PermissionSource,
|
||||
PlanType,
|
||||
Role,
|
||||
VirtualDocumentType,
|
||||
} from "@budibase/types"
|
||||
import { isViewID } from "../../../db/utils"
|
||||
import { features } from "@budibase/pro"
|
||||
import {
|
||||
extractViewInfoFromID,
|
||||
getRoleParams,
|
||||
isViewID,
|
||||
} from "../../../db/utils"
|
||||
import {
|
||||
CURRENTLY_SUPPORTED_LEVELS,
|
||||
getBasePermissions,
|
||||
} from "../../../utilities/security"
|
||||
import sdk from "../../../sdk"
|
||||
import { isV2 } from "../views"
|
||||
|
||||
type ResourceActionAllowedResult =
|
||||
| { allowed: true }
|
||||
|
@ -35,3 +49,117 @@ export async function resourceActionAllowed({
|
|||
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
|
||||
}
|
||||
|
|
|
@ -622,7 +622,7 @@ class TestConfiguration {
|
|||
|
||||
// VIEW
|
||||
|
||||
async createView(config?: any) {
|
||||
async createLegacyView(config?: any) {
|
||||
if (!this.table) {
|
||||
throw "Test requires table to be configured."
|
||||
}
|
||||
|
|
|
@ -23,6 +23,9 @@ export function getPermissionType(resourceId: string) {
|
|||
case DocumentType.QUERY:
|
||||
case DocumentType.DATASOURCE:
|
||||
return permissions.PermissionType.QUERY
|
||||
default:
|
||||
// legacy views don't have an ID, will end up here
|
||||
return permissions.PermissionType.LEGACY_VIEW
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,3 +4,4 @@ export * from "./row"
|
|||
export * from "./view"
|
||||
export * from "./rows"
|
||||
export * from "./table"
|
||||
export * from "./permission"
|
||||
|
|
16
packages/types/src/api/web/app/permission.ts
Normal file
16
packages/types/src/api/web/app/permission.ts
Normal file
|
@ -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>
|
||||
}
|
|
@ -16,4 +16,11 @@ export enum PermissionType {
|
|||
GLOBAL_BUILDER = "globalBuilder",
|
||||
QUERY = "query",
|
||||
VIEW = "view",
|
||||
LEGACY_VIEW = "legacy_view",
|
||||
}
|
||||
|
||||
export enum PermissionSource {
|
||||
EXPLICIT = "EXPLICIT",
|
||||
INHERITED = "INHERITED",
|
||||
BASE = "BASE",
|
||||
}
|
||||
|
|
|
@ -266,17 +266,14 @@ export const onboardUsers = async (ctx: Ctx<InviteUsersRequest>) => {
|
|||
|
||||
// Temp password to be passed to the user.
|
||||
createdPasswords[invite.email] = password
|
||||
let builder: { global: boolean; apps?: string[] } = { global: false }
|
||||
if (invite.userInfo.appBuilders) {
|
||||
builder.apps = invite.userInfo.appBuilders
|
||||
}
|
||||
|
||||
return {
|
||||
email: invite.email,
|
||||
password,
|
||||
forceResetPassword: true,
|
||||
roles: invite.userInfo.apps,
|
||||
admin: { global: false },
|
||||
builder,
|
||||
admin: invite.userInfo.admin,
|
||||
builder: invite.userInfo.builder,
|
||||
tenantId: tenancy.getTenantId(),
|
||||
}
|
||||
})
|
||||
|
@ -371,13 +368,10 @@ export const updateInvite = async (ctx: any) => {
|
|||
...invite,
|
||||
}
|
||||
|
||||
if (!updateBody?.appBuilders || !updateBody.appBuilders?.length) {
|
||||
updated.info.appBuilders = []
|
||||
} else {
|
||||
updated.info.appBuilders = [
|
||||
...(invite.info.appBuilders ?? []),
|
||||
...updateBody.appBuilders,
|
||||
]
|
||||
if (!updateBody?.builder?.apps && updated.info?.builder?.apps) {
|
||||
updated.info.builder.apps = []
|
||||
} else if (updateBody?.builder) {
|
||||
updated.info.builder = updateBody.builder
|
||||
}
|
||||
|
||||
if (!updateBody?.apps || !Object.keys(updateBody?.apps).length) {
|
||||
|
@ -409,15 +403,17 @@ export const inviteAccept = async (
|
|||
lastName,
|
||||
password,
|
||||
email,
|
||||
admin: { global: info?.admin?.global || false },
|
||||
roles: info.apps,
|
||||
tenantId: info.tenantId,
|
||||
}
|
||||
let builder: { global: boolean; apps?: string[] } = { global: false }
|
||||
let builder: { global: boolean; apps?: string[] } = {
|
||||
global: info?.builder?.global || false,
|
||||
}
|
||||
|
||||
if (info.appBuilders) {
|
||||
builder.apps = info.appBuilders
|
||||
if (info?.builder?.apps) {
|
||||
builder.apps = info.builder.apps
|
||||
request.builder = builder
|
||||
delete info.appBuilders
|
||||
}
|
||||
delete info.apps
|
||||
request = {
|
||||
|
|
Loading…
Reference in a new issue