Merge branch 'develop' into fix/daysjs-export-err

This commit is contained in:
José Vte. Calderón 2023-09-06 08:01:43 +02:00 committed by GitHub
commit b3e2c6334c
65 changed files with 799 additions and 518 deletions

View File

@ -1,4 +1,5 @@
{{- if .Values.globals.createSecrets -}} {{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (include "budibase.fullname" .) }}
{{- if .Values.globals.createSecrets }}
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
@ -10,8 +11,15 @@ metadata:
heritage: "{{ .Release.Service }}" heritage: "{{ .Release.Service }}"
type: Opaque type: Opaque
data: data:
{{- if $existingSecret }}
internalApiKey: {{ index $existingSecret.data "internalApiKey" }}
jwtSecret: {{ index $existingSecret.data "jwtSecret" }}
objectStoreAccess: {{ index $existingSecret.data "objectStoreAccess" }}
objectStoreSecret: {{ index $existingSecret.data "objectStoreSecret" }}
{{- else }}
internalApiKey: {{ template "budibase.defaultsecret" .Values.globals.internalApiKey }} internalApiKey: {{ template "budibase.defaultsecret" .Values.globals.internalApiKey }}
jwtSecret: {{ template "budibase.defaultsecret" .Values.globals.jwtSecret }} jwtSecret: {{ template "budibase.defaultsecret" .Values.globals.jwtSecret }}
objectStoreAccess: {{ template "budibase.defaultsecret" .Values.services.objectStore.accessKey }} objectStoreAccess: {{ template "budibase.defaultsecret" .Values.services.objectStore.accessKey }}
objectStoreSecret: {{ template "budibase.defaultsecret" .Values.services.objectStore.secretKey }} objectStoreSecret: {{ template "budibase.defaultsecret" .Values.services.objectStore.secretKey }}
{{- end -}} {{- end }}
{{- end }}

View File

@ -1,5 +1,5 @@
{ {
"version": "2.9.33-alpha.8", "version": "2.9.39-alpha.1",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -6,7 +6,7 @@
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
"exports": { "exports": {
".": "./dist/index.js", ".": "./dist/index.js",
"./tests": "./dist/tests.js", "./tests": "./dist/tests/index.js",
"./*": "./dist/*.js" "./*": "./dist/*.js"
}, },
"author": "Budibase", "author": "Budibase",

View File

@ -1,6 +1,21 @@
#!/usr/bin/node #!/usr/bin/node
const coreBuild = require("../../../scripts/build") const coreBuild = require("../../../scripts/build")
coreBuild("./src/plugin/index.ts", "./dist/plugins.js") coreBuild("./src/plugin/index.ts", "./dist/plugins.js")
coreBuild("./src/index.ts", "./dist/index.js") coreBuild("./src/index.ts", "./dist/index.js")
coreBuild("./tests/index.ts", "./dist/tests.js")
const glob = require("glob")
const inputFiles = [
...glob.sync("./src/**/*.[tj]s", { nodir: true }),
...glob.sync("./tests/**/*.[tj]s", { nodir: true }),
]
const path = require("path")
for (const file of inputFiles) {
coreBuild(file, `./${path.join("dist", file.replace(/\.ts$/, ".js"))}`, {
skipMeta: true,
bundle: false,
forcedFormat: "cjs",
})
}

View File

@ -8,7 +8,6 @@ import {
DatabasePutOpts, DatabasePutOpts,
DatabaseCreateIndexOpts, DatabaseCreateIndexOpts,
DatabaseDeleteIndexOpts, DatabaseDeleteIndexOpts,
DocExistsResponse,
Document, Document,
isDocument, isDocument,
} from "@budibase/types" } from "@budibase/types"
@ -121,19 +120,6 @@ export class DatabaseImpl implements Database {
return this.updateOutput(() => db.get(id)) return this.updateOutput(() => db.get(id))
} }
async docExists(docId: string): Promise<DocExistsResponse> {
const db = await this.checkSetup()
let _rev, exists
try {
const { etag } = await db.head(docId)
_rev = etag
exists = true
} catch (err) {
exists = false
}
return { _rev, exists }
}
async remove(idOrDoc: string | Document, rev?: string) { async remove(idOrDoc: string | Document, rev?: string) {
const db = await this.checkSetup() const db = await this.checkSetup()
let _id: string let _id: string

View File

@ -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 // we have to use a callback for this so that we can close
// the DB when we're done, without this manual requests would // the DB when we're done, without this manual requests would
// need to close the database when done with it to avoid memory leaks // 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) const db = getDB(dbName, opts)
// need this to be async so that we can correctly close DB after all // need this to be async so that we can correctly close DB after all
// async operations have been completed // async operations have been completed

View File

@ -87,6 +87,7 @@ export const BUILTIN_PERMISSIONS = {
new Permission(PermissionType.QUERY, PermissionLevel.WRITE), new Permission(PermissionType.QUERY, PermissionLevel.WRITE),
new Permission(PermissionType.TABLE, PermissionLevel.WRITE), new Permission(PermissionType.TABLE, PermissionLevel.WRITE),
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ),
], ],
}, },
POWER: { POWER: {
@ -97,6 +98,7 @@ export const BUILTIN_PERMISSIONS = {
new Permission(PermissionType.USER, PermissionLevel.READ), new Permission(PermissionType.USER, PermissionLevel.READ),
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ),
], ],
}, },
ADMIN: { ADMIN: {
@ -108,6 +110,7 @@ export const BUILTIN_PERMISSIONS = {
new Permission(PermissionType.AUTOMATION, PermissionLevel.ADMIN), new Permission(PermissionType.AUTOMATION, PermissionLevel.ADMIN),
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
new Permission(PermissionType.QUERY, PermissionLevel.ADMIN), new Permission(PermissionType.QUERY, PermissionLevel.ADMIN),
new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ),
], ],
}, },
} }

View File

@ -253,7 +253,7 @@ export function checkForRoleResourceArray(
* Given an app ID this will retrieve all of the roles that are currently within that app. * 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. * @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) { if (appId) {
return doWithDB(appId, internal) return doWithDB(appId, internal)
} else { } 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 { export class AccessController {
userHierarchies: { [key: string]: string[] } userHierarchies: { [key: string]: string[] }
constructor() { constructor() {

View File

@ -4,7 +4,7 @@
export let text = null export let text = null
export let condition = true export let condition = true
export let duration = 3000 export let duration = 5000
export let position export let position
export let type export let type

View File

@ -3,6 +3,9 @@ import { Screen } from "./utils/Screen"
import { Component } from "./utils/Component" import { Component } from "./utils/Component"
export default function (datasources) { export default function (datasources) {
if (!Array.isArray(datasources)) {
return []
}
return datasources.map(datasource => { return datasources.map(datasource => {
return { return {
name: `${datasource.name} - List`, name: `${datasource.name} - List`,

View File

@ -73,7 +73,7 @@
if (!perms["execute"]) { if (!perms["execute"]) {
role = "BASIC" role = "BASIC"
} else { } else {
role = perms["execute"] role = perms["execute"].role
} }
} }

View File

@ -10,6 +10,7 @@
import ManageAccessButton from "./buttons/ManageAccessButton.svelte" import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte" import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { ROW_EXPORT_FORMATS } from "constants/backend"
export let view = {} export let view = {}
@ -19,6 +20,14 @@
let type = "internal" let type = "internal"
$: name = view.name $: name = view.name
$: calculation = view.calculation
$: supportedFormats = Object.values(ROW_EXPORT_FORMATS).filter(key => {
if (calculation && key === ROW_EXPORT_FORMATS.JSON_WITH_SCHEMA) {
return false
}
return true
})
// Fetch rows for specified view // Fetch rows for specified view
$: fetchViewData(name, view.field, view.groupBy, view.calculation) $: fetchViewData(name, view.field, view.groupBy, view.calculation)
@ -68,5 +77,5 @@
{/if} {/if}
<ManageAccessButton resourceId={decodeURI(name)} /> <ManageAccessButton resourceId={decodeURI(name)} />
<HideAutocolumnButton bind:hideAutocolumns /> <HideAutocolumnButton bind:hideAutocolumns />
<ExportButton view={view.name} /> <ExportButton view={view.name} formats={supportedFormats} />
</Table> </Table>

View File

@ -7,6 +7,7 @@
export let sorting export let sorting
export let disabled = false export let disabled = false
export let selectedRows export let selectedRows
export let formats
let modal let modal
</script> </script>
@ -15,5 +16,5 @@
Export Export
</ActionButton> </ActionButton>
<Modal bind:this={modal}> <Modal bind:this={modal}>
<ExportModal {view} {filters} {sorting} {selectedRows} /> <ExportModal {view} {filters} {sorting} {selectedRows} {formats} />
</Modal> </Modal>

View File

@ -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>

View File

@ -1,18 +1,30 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import { Modal, ActionButton } from "@budibase/bbui" import { Modal, ActionButton, TooltipType, TempTooltip } from "@budibase/bbui"
import GridCreateViewModal from "../../modals/grid/GridCreateViewModal.svelte" import GridCreateViewModal from "../../modals/grid/GridCreateViewModal.svelte"
const { rows, columns } = getContext("grid") const { rows, columns, filter } = getContext("grid")
let modal let modal
let firstFilterUsage = false
$: disabled = !$columns.length || !$rows.length $: disabled = !$columns.length || !$rows.length
$: {
if ($filter?.length && !firstFilterUsage) {
firstFilterUsage = true
}
}
</script> </script>
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}> <TempTooltip
Add view text="Create a view to save your filters"
</ActionButton> type={TooltipType.Info}
condition={firstFilterUsage}
>
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
Create view
</ActionButton>
</TempTooltip>
<Modal bind:this={modal}> <Modal bind:this={modal}>
<GridCreateViewModal /> <GridCreateViewModal />
</Modal> </Modal>

View File

@ -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} />

View File

@ -9,30 +9,43 @@
import download from "downloadjs" import download from "downloadjs"
import { API } from "api" import { API } from "api"
import { Constants, LuceneUtils } from "@budibase/frontend-core" import { Constants, LuceneUtils } from "@budibase/frontend-core"
import { ROW_EXPORT_FORMATS } from "constants/backend"
const FORMATS = [
{
name: "CSV",
key: "csv",
},
{
name: "JSON",
key: "json",
},
{
name: "JSON with Schema",
key: "jsonWithSchema",
},
]
export let view export let view
export let filters export let filters
export let sorting export let sorting
export let selectedRows = [] export let selectedRows = []
export let formats
let exportFormat = FORMATS[0].key const FORMATS = [
{
name: "CSV",
key: ROW_EXPORT_FORMATS.CSV,
},
{
name: "JSON",
key: ROW_EXPORT_FORMATS.JSON,
},
{
name: "JSON with Schema",
key: ROW_EXPORT_FORMATS.JSON_WITH_SCHEMA,
},
]
$: options = FORMATS.filter(format => {
if (formats && !formats.includes(format.key)) {
return false
}
return true
})
let exportFormat
let filterLookup let filterLookup
$: if (options && !exportFormat) {
exportFormat = Array.isArray(options) ? options[0]?.key : []
}
$: luceneFilter = LuceneUtils.buildLuceneQuery(filters) $: luceneFilter = LuceneUtils.buildLuceneQuery(filters)
$: exportOpDisplay = buildExportOpDisplay(sorting, filterDisplay, filters) $: exportOpDisplay = buildExportOpDisplay(sorting, filterDisplay, filters)
@ -190,7 +203,7 @@
<Select <Select
label="Format" label="Format"
bind:value={exportFormat} bind:value={exportFormat}
options={FORMATS} {options}
placeholder={null} placeholder={null}
getOptionLabel={x => x.name} getOptionLabel={x => x.name}
getOptionValue={x => x.key} getOptionValue={x => x.key}

View File

@ -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,58 +10,126 @@
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 {
await permissionsStore.save({ if (role === inheritedRoleId) {
level, await permissionsStore.remove({
role, level,
resource: resourceId, role,
}) resource: resourceId,
})
} else {
await permissionsStore.save({
level,
role,
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">Specify the minimum access level role for this data.</Body>
<Body size="S">{requiresLicence.message}</Body> <div class="row">
{:else} <Label extraSmall grey>Level</Label>
<Body size="S">Specify the minimum access level role for this data.</Body> <Label extraSmall grey>Role</Label>
<div class="row"> {#each Object.keys(computedPermissions) as level}
<Label extraSmall grey>Level</Label> <Input value={capitalise(level)} disabled />
<Label extraSmall grey>Role</Label> <Select
{#each Object.keys(permissions) as level} disabled={requiresPlanToModify}
<Input value={capitalise(level)} disabled /> placeholder={false}
<Select value={computedPermissions[level].selectedValue}
value={permissions[level]} on:change={e => changePermission(level, e.detail)}
on:change={e => changePermission(level, e.detail)} options={computedPermissions[level].options}
options={$roles} getOptionLabel={x => x.name}
getOptionLabel={x => x.name} getOptionValue={x => x._id}
getOptionValue={x => x._id} />
/> {/each}
{/each} </div>
{#if dependantsInfoMessage}
<div class="inheriting-resources">
<Icon name="Alert" />
<Body size="S">
<i>
{dependantsInfoMessage}
</i>
</Body>
</div> </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>

View File

@ -46,13 +46,13 @@
</script> </script>
<ModalContent <ModalContent
title="Create View" title="Create view"
confirmText="Create View" confirmText="Create view"
onConfirm={saveView} onConfirm={saveView}
disabled={nameExists} disabled={nameExists}
> >
<Input <Input
label="View Name" label="View name"
thin thin
bind:value={name} bind:value={name}
error={nameExists ? "A view already exists with that name" : null} error={nameExists ? "A view already exists with that name" : null}

View File

@ -33,17 +33,19 @@
let anchors = {} let anchors = {}
let draggableItems = [] let draggableItems = []
const buildDragable = items => { const buildDraggable = items => {
return items.map(item => { return items
return { .map(item => {
id: listItemKey ? item[listItemKey] : generate(), return {
item, id: listItemKey ? item[listItemKey] : generate(),
} item,
}) }
})
.filter(item => item.id)
} }
$: if (items) { $: if (items) {
draggableItems = buildDragable(items) draggableItems = buildDraggable(items)
} }
const updateRowOrder = e => { const updateRowOrder = e => {

View File

@ -99,6 +99,9 @@
} }
const type = getComponentForField(instance.field, schema) const type = getComponentForField(instance.field, schema)
if (!type) {
return null
}
instance._component = `@budibase/standard-components/${type}` instance._component = `@budibase/standard-components/${type}`
const pseudoComponentInstance = store.actions.components.createInstance( const pseudoComponentInstance = store.actions.components.createInstance(
@ -116,7 +119,9 @@
} }
$: if (sanitisedFields) { $: if (sanitisedFields) {
fieldList = [...sanitisedFields, ...unconfigured].map(buildSudoInstance) fieldList = [...sanitisedFields, ...unconfigured]
.map(buildSudoInstance)
.filter(x => x != null)
} }
const processItemUpdate = e => { const processItemUpdate = e => {

View File

@ -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
} }

View File

@ -287,3 +287,9 @@ export const DatasourceTypes = {
GRAPH: "Graph", GRAPH: "Graph",
API: "API", API: "API",
} }
export const ROW_EXPORT_FORMATS = {
CSV: "csv",
JSON: "json",
JSON_WITH_SCHEMA: "jsonWithSchema",
}

View File

@ -120,7 +120,7 @@
await usersFetch.refresh() await usersFetch.refresh()
filteredUsers = $usersFetch.rows filteredUsers = $usersFetch.rows
.filter(user => !user?.admin?.global) // filter out global admins .filter(user => user.email !== $auth.user.email)
.map(user => { .map(user => {
const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder( const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder(
user, user,
@ -150,13 +150,10 @@
} }
const sortInviteRoles = (a, b) => { const sortInviteRoles = (a, b) => {
const aEmpty = const aAppsEmpty = !a.info?.apps?.length && !a.info?.builder?.apps?.length
!a.info?.appBuilders?.length && Object.keys(a.info.apps).length === 0 const bAppsEmpty = !b.info?.apps?.length && !b.info?.builder?.apps?.length
const bEmpty =
!b.info?.appBuilders?.length && Object.keys(b.info.apps).length === 0
if (aEmpty && !bEmpty) return 1 return aAppsEmpty && !bAppsEmpty ? 1 : !aAppsEmpty && bAppsEmpty ? -1 : 0
if (!aEmpty && bEmpty) return -1
} }
const sortRoles = (a, b) => { const sortRoles = (a, b) => {
@ -366,18 +363,19 @@
const payload = [ const payload = [
{ {
email: newUserEmail, email: newUserEmail,
builder: !!creationRoleType === Constants.BudibaseRoles.Admin, builder: { global: creationRoleType === Constants.BudibaseRoles.Admin },
admin: !!creationRoleType === Constants.BudibaseRoles.Admin, admin: { global: creationRoleType === Constants.BudibaseRoles.Admin },
}, },
] ]
if (creationAccessType === Constants.Roles.CREATOR) { const notCreatingAdmin = creationRoleType !== Constants.BudibaseRoles.Admin
payload[0].appBuilders = [prodAppId] const isCreator = creationAccessType === Constants.Roles.CREATOR
} else { if (notCreatingAdmin && isCreator) {
payload[0].apps = { payload[0].builder.apps = [prodAppId]
[prodAppId]: creationAccessType, } else if (notCreatingAdmin && !isCreator) {
} payload[0].apps = { [prodAppId]: creationAccessType }
} }
let userInviteResponse let userInviteResponse
try { try {
userInviteResponse = await users.onboard(payload) userInviteResponse = await users.onboard(payload)
@ -438,10 +436,11 @@
} }
if (role === Constants.Roles.CREATOR) { 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] delete updateBody?.apps?.[prodAppId]
} else if (role !== Constants.Roles.CREATOR && invite?.appBuilders) { } else if (role !== Constants.Roles.CREATOR && invite?.builder?.apps) {
invite.appBuilders = [] invite.builder.apps = []
} }
await users.updateInvite(updateBody) await users.updateInvite(updateBody)
await filterInvites(query) 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 => { const getRoleFooter = user => {
if (user.group) { if (user.group) {
const role = $roles.find(role => role._id === user.role) const role = $roles.find(role => role._id === user.role)
@ -531,7 +542,9 @@
<Heading size="S">{invitingFlow ? "Invite new user" : "Users"}</Heading> <Heading size="S">{invitingFlow ? "Invite new user" : "Users"}</Heading>
</div> </div>
<div class="header"> <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 <Icon
color="var(--spectrum-global-color-gray-600)" color="var(--spectrum-global-color-gray-600)"
name="RailRightClose" name="RailRightClose"
@ -600,6 +613,11 @@
<div class="auth-entity-access-title">Access</div> <div class="auth-entity-access-title">Access</div>
</div> </div>
{#each filteredInvites as invite} {#each filteredInvites as invite}
{@const user = {
isAdminOrGlobalBuilder:
invite.info?.admin?.global && invite.info?.builder?.global,
}}
<div class="auth-entity"> <div class="auth-entity">
<div class="details"> <div class="details">
<div class="user-email" title={invite.email}> <div class="user-email" title={invite.email}>
@ -608,10 +626,9 @@
</div> </div>
<div class="auth-entity-access"> <div class="auth-entity-access">
<RoleSelect <RoleSelect
footer={getRoleFooter(user)}
placeholder={false} placeholder={false}
value={invite.info?.appBuilders?.includes(prodAppId) value={getInviteRoleValue(invite)}
? Constants.Roles.CREATOR
: invite.info.apps?.[prodAppId]}
allowRemove={invite.info.apps?.[prodAppId]} allowRemove={invite.info.apps?.[prodAppId]}
allowPublic={false} allowPublic={false}
allowCreator={true} allowCreator={true}
@ -624,6 +641,9 @@
}} }}
autoWidth autoWidth
align="right" align="right"
allowedRoles={user.isAdminOrGlobalBuilder
? [Constants.Roles.ADMIN]
: null}
/> />
</div> </div>
</div> </div>

View File

@ -110,7 +110,7 @@
if (mode === "table") { if (mode === "table") {
datasourceModal.show() datasourceModal.show()
} else if (mode === "blank") { } else if (mode === "blank") {
let templates = getTemplates($store, $tables.list) let templates = getTemplates($tables.list)
const blankScreenTemplate = templates.find( const blankScreenTemplate = templates.find(
t => t.id === "createFromScratch" t => t.id === "createFromScratch"
) )

View File

@ -46,7 +46,7 @@
let loaded = false let loaded = false
let editModal, deleteModal let editModal, deleteModal
$: console.log(group)
$: scimEnabled = $features.isScimEnabled $: scimEnabled = $features.isScimEnabled
$: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled $: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled
$: group = $groups.find(x => x._id === groupId) $: group = $groups.find(x => x._id === groupId)
@ -62,7 +62,7 @@
? Constants.Roles.CREATOR ? Constants.Roles.CREATOR
: group?.roles?.[apps.getProdAppID(app.devId)], : group?.roles?.[apps.getProdAppID(app.devId)],
})) }))
$: console.log(groupApps)
$: { $: {
if (loaded && !group?._id) { if (loaded && !group?._id) {
$goto("./") $goto("./")

View File

@ -5,7 +5,6 @@
export let value export let value
export let row export let row
$: console.log(row)
$: priviliged = sdk.users.isAdminOrBuilder(row) $: priviliged = sdk.users.isAdminOrBuilder(row)
$: count = getCount(row) $: count = getCount(row)
@ -14,10 +13,10 @@
return $apps.length return $apps.length
} else { } else {
return sdk.users.hasAppBuilderPermissions(row) return sdk.users.hasAppBuilderPermissions(row)
? row.builder.apps.length + ? row?.builder?.apps?.length +
Object.keys(row.roles || {}).filter(appId => Object.keys(row.roles || {}).filter(appId => {
row.builder.apps.includes(appId) row?.builder?.apps?.includes(appId)
).length }).length
: value?.length || 0 : value?.length || 0
} }
} }

View File

@ -10,7 +10,7 @@
admin: "Full access", admin: "Full access",
} }
$: role = Constants.BudibaseRoleOptions.find( $: role = Constants.BudibaseRoleOptionsOld.find(
x => x.value === users.getUserRole(row) x => x.value === users.getUserRole(row)
) )
$: value = role?.label || "Not available" $: value = role?.label || "Not available"

View File

@ -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)
},
} }
} }

View File

@ -121,8 +121,11 @@ export function createUsersStore() {
} }
const getUserRole = user => const getUserRole = user =>
sdk.users.isAdminOrGlobalBuilder(user) ? "admin" : "appUser" sdk.users.isAdmin(user)
? "admin"
: sdk.users.isBuilder(user)
? "developer"
: "appUser"
const refreshUsage = const refreshUsage =
fn => fn =>
async (...args) => { async (...args) => {

View File

@ -136,7 +136,7 @@
// Check arrays - remove any values not present in the field schema and // Check arrays - remove any values not present in the field schema and
// convert any values supplied to strings // convert any values supplied to strings
if (Array.isArray(value) && type === "array" && schema) { if (Array.isArray(value) && type === "array" && schema) {
const options = schema?.constraints.inclusion || [] const options = schema?.constraints?.inclusion || []
return value.map(opt => String(opt)).filter(opt => options.includes(opt)) return value.map(opt => String(opt)).filter(opt => options.includes(opt))
} }
return value return value

View File

@ -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`,
})
},
}) })

View File

@ -144,8 +144,8 @@ export const buildUserEndpoints = API => ({
body: { body: {
email, email,
userInfo: { userInfo: {
admin: admin ? { global: true } : undefined, admin: admin?.global ? { global: true } : undefined,
builder: builder ? { global: true } : undefined, builder: builder?.global ? { global: true } : undefined,
apps: apps ? apps : undefined, apps: apps ? apps : undefined,
}, },
}, },
@ -156,14 +156,13 @@ export const buildUserEndpoints = API => ({
return await API.post({ return await API.post({
url: "/api/global/users/onboard", url: "/api/global/users/onboard",
body: payload.map(invite => { body: payload.map(invite => {
const { email, admin, builder, apps, appBuilders } = invite const { email, admin, builder, apps } = invite
return { return {
email, email,
userInfo: { userInfo: {
admin: admin ? { global: true } : undefined, admin,
builder: builder ? { global: true } : undefined, builder,
apps: apps ? apps : undefined, apps: apps ? apps : undefined,
appBuilders,
}, },
} }
}), }),
@ -176,12 +175,11 @@ export const buildUserEndpoints = API => ({
* @param invite the invite code sent in the email * @param invite the invite code sent in the email
*/ */
updateUserInvite: async invite => { updateUserInvite: async invite => {
console.log(invite)
await API.post({ await API.post({
url: `/api/global/users/invite/update/${invite.code}`, url: `/api/global/users/invite/update/${invite.code}`,
body: { body: {
apps: invite.apps, apps: invite.apps,
appBuilders: invite.appBuilders, builder: invite.builder,
}, },
}) })
}, },

View File

@ -2,6 +2,7 @@
* Operator options for lucene queries * Operator options for lucene queries
*/ */
export { OperatorOptions, SqlNumberTypeRangeMap } from "@budibase/shared-core" export { OperatorOptions, SqlNumberTypeRangeMap } from "@budibase/shared-core"
export { Feature as Features } from "@budibase/types"
// Cookie names // Cookie names
export const Cookies = { export const Cookies = {
@ -22,6 +23,11 @@ export const BudibaseRoles = {
Admin: "admin", Admin: "admin",
} }
export const BudibaseRoleOptionsOld = [
{ label: "Developer", value: BudibaseRoles.Developer },
{ label: "Member", value: BudibaseRoles.AppUser },
{ label: "Admin", value: BudibaseRoles.Admin },
]
export const BudibaseRoleOptions = [ export const BudibaseRoleOptions = [
{ label: "Member", value: BudibaseRoles.AppUser }, { label: "Member", value: BudibaseRoles.AppUser },
{ label: "Admin", value: BudibaseRoles.Admin }, { label: "Admin", value: BudibaseRoles.Admin },

View File

@ -39,8 +39,9 @@ import {
} from "../../db/defaultData/datasource_bb_default" } from "../../db/defaultData/datasource_bb_default"
import { removeAppFromUserRoles } from "../../utilities/workerRequests" import { removeAppFromUserRoles } from "../../utilities/workerRequests"
import { stringToReadStream } from "../../utilities" import { stringToReadStream } from "../../utilities"
import { doesUserHaveLock } from "../../utilities/redis" import { doesUserHaveLock, getLocksById } from "../../utilities/redis"
import { cleanupAutomations } from "../../automations/utils" import { cleanupAutomations } from "../../automations/utils"
import { checkAppMetadata } from "../../automations/logging"
import { getUniqueRows } from "../../utilities/usageQuota/rows" import { getUniqueRows } from "../../utilities/usageQuota/rows"
import { groups, licensing, quotas } from "@budibase/pro" import { groups, licensing, quotas } from "@budibase/pro"
import { import {
@ -50,6 +51,7 @@ import {
PlanType, PlanType,
Screen, Screen,
UserCtx, UserCtx,
ContextUser,
} from "@budibase/types" } from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
import sdk from "../../sdk" import sdk from "../../sdk"

View File

@ -20,7 +20,7 @@ import {
Automation, Automation,
AutomationActionStepId, AutomationActionStepId,
AutomationResults, AutomationResults,
Ctx, BBContext,
} from "@budibase/types" } from "@budibase/types"
import { getActionDefinitions as actionDefs } from "../../automations/actions" import { getActionDefinitions as actionDefs } from "../../automations/actions"
import sdk from "../../sdk" import sdk from "../../sdk"
@ -73,7 +73,7 @@ function cleanAutomationInputs(automation: Automation) {
return automation return automation
} }
export async function create(ctx: Ctx) { export async function create(ctx: BBContext) {
const db = context.getAppDB() const db = context.getAppDB()
let automation = ctx.request.body let automation = ctx.request.body
automation.appId = ctx.appId automation.appId = ctx.appId
@ -142,7 +142,7 @@ export async function handleStepEvents(
} }
} }
export async function update(ctx: Ctx) { export async function update(ctx: BBContext) {
const db = context.getAppDB() const db = context.getAppDB()
let automation = ctx.request.body let automation = ctx.request.body
automation.appId = ctx.appId automation.appId = ctx.appId
@ -193,7 +193,7 @@ export async function update(ctx: Ctx) {
builderSocket?.emitAutomationUpdate(ctx, automation) builderSocket?.emitAutomationUpdate(ctx, automation)
} }
export async function fetch(ctx: Ctx) { export async function fetch(ctx: BBContext) {
const db = context.getAppDB() const db = context.getAppDB()
const response = await db.allDocs( const response = await db.allDocs(
getAutomationParams(null, { getAutomationParams(null, {
@ -203,11 +203,12 @@ export async function fetch(ctx: Ctx) {
ctx.body = response.rows.map(row => row.doc) ctx.body = response.rows.map(row => row.doc)
} }
export async function find(ctx: Ctx) { export async function find(ctx: BBContext) {
ctx.body = await sdk.automations.get(ctx.params.id) const db = context.getAppDB()
ctx.body = await db.get(ctx.params.id)
} }
export async function destroy(ctx: Ctx) { export async function destroy(ctx: BBContext) {
const db = context.getAppDB() const db = context.getAppDB()
const automationId = ctx.params.id const automationId = ctx.params.id
const oldAutomation = await db.get<Automation>(automationId) const oldAutomation = await db.get<Automation>(automationId)
@ -221,11 +222,11 @@ export async function destroy(ctx: Ctx) {
builderSocket?.emitAutomationDeletion(ctx, automationId) builderSocket?.emitAutomationDeletion(ctx, automationId)
} }
export async function logSearch(ctx: Ctx) { export async function logSearch(ctx: BBContext) {
ctx.body = await automations.logs.logSearch(ctx.request.body) ctx.body = await automations.logs.logSearch(ctx.request.body)
} }
export async function clearLogError(ctx: Ctx) { export async function clearLogError(ctx: BBContext) {
const { automationId, appId } = ctx.request.body const { automationId, appId } = ctx.request.body
await context.doInAppContext(appId, async () => { await context.doInAppContext(appId, async () => {
const db = context.getProdAppDB() const db = context.getProdAppDB()
@ -244,15 +245,15 @@ export async function clearLogError(ctx: Ctx) {
}) })
} }
export async function getActionList(ctx: Ctx) { export async function getActionList(ctx: BBContext) {
ctx.body = await getActionDefinitions() ctx.body = await getActionDefinitions()
} }
export async function getTriggerList(ctx: Ctx) { export async function getTriggerList(ctx: BBContext) {
ctx.body = getTriggerDefinitions() ctx.body = getTriggerDefinitions()
} }
export async function getDefinitionList(ctx: Ctx) { export async function getDefinitionList(ctx: BBContext) {
ctx.body = { ctx.body = {
trigger: getTriggerDefinitions(), trigger: getTriggerDefinitions(),
action: await getActionDefinitions(), action: await getActionDefinitions(),
@ -265,7 +266,7 @@ export async function getDefinitionList(ctx: Ctx) {
* * * *
*********************/ *********************/
export async function trigger(ctx: Ctx) { export async function trigger(ctx: BBContext) {
const db = context.getAppDB() const db = context.getAppDB()
let automation = await db.get<Automation>(ctx.params.id) let automation = await db.get<Automation>(ctx.params.id)
@ -310,7 +311,7 @@ function prepareTestInput(input: any) {
return input return input
} }
export async function test(ctx: Ctx) { export async function test(ctx: BBContext) {
const db = context.getAppDB() const db = context.getAppDB()
let automation = await db.get<Automation>(ctx.params.id) let automation = await db.get<Automation>(ctx.params.id)
await setTestFlag(automation._id!) await setTestFlag(automation._id!)

View File

@ -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(
ctx: UserCtx<void, GetResourcePermsResponse>
) {
const resourceId = ctx.params.resourceId const resourceId = ctx.params.resourceId
const db = context.getAppDB() const resourcePermissions = await sdk.permissions.getResourcePerms(resourceId)
const body = await db.allDocs( const inheritablePermissions =
getRoleParams(null, { await sdk.permissions.getInheritablePermissions(resourceId)
include_docs: true,
}) ctx.body = {
) permissions: Object.entries(resourcePermissions).reduce(
const rolesList = body.rows.map(row => row.doc) (p, [level, role]) => {
let permissions: Record<string, string> = {} p[level] = {
for (let level of SUPPORTED_LEVELS) { role: role.role,
// update the various roleIds in the resource permissions permissionType: role.type,
for (let role of rolesList) { inheritablePermission:
const rolePerms = roles.checkForRoleResourceArray( inheritablePermissions && inheritablePermissions[level].role,
role.permissions, }
resourceId return p
) },
if ( {} as Record<string, ResourcePermissionInfo>
rolePerms && ),
rolePerms[resourceId] && requiresPlanToModify: (
rolePerms[resourceId].indexOf(level) !== -1 await sdk.permissions.allowsExplicitPermissions(resourceId)
) { ).minPlan,
permissions[level] = roles.getExternalRoleID(role._id, role.version)! }
} }
}
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) {

View File

@ -95,7 +95,7 @@ export async function fetchView(ctx: any) {
() => () =>
sdk.rows.fetchView(tableId, viewName, { sdk.rows.fetchView(tableId, viewName, {
calculation, calculation,
group, group: calculation ? group : null,
field, field,
}), }),
{ {

View File

@ -27,7 +27,7 @@ export function json(rows: Row[]) {
export function jsonWithSchema(schema: TableSchema, rows: Row[]) { export function jsonWithSchema(schema: TableSchema, rows: Row[]) {
const newSchema: TableSchema = {} const newSchema: TableSchema = {}
Object.values(schema).forEach(column => { Object.values(schema).forEach(column => {
if (!column.autocolumn) { if (!column.autocolumn && column.name) {
newSchema[column.name] = column newSchema[column.name] = column
} }
}) })

View File

@ -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",

View File

@ -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 () => {
@ -260,4 +270,21 @@ describe("/permission", () => {
expect(publicPerm.name).toBeDefined() 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",
},
},
})
})
})
}) })

View File

@ -1,11 +1,12 @@
import tk from "timekeeper" import tk from "timekeeper"
import { outputProcessing } from "../../../utilities/rowProcessor" import { outputProcessing } from "../../../utilities/rowProcessor"
import * as setup from "./utilities" 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 { quotas } from "@budibase/pro"
import { import {
FieldType, FieldType,
MonthlyQuotaName, MonthlyQuotaName,
PermissionLevel,
QuotaUsageType, QuotaUsageType,
Row, Row,
SortOrder, SortOrder,
@ -16,6 +17,7 @@ import {
import { import {
expectAnyInternalColsAttributes, expectAnyInternalColsAttributes,
generator, generator,
mocks,
structures, structures,
} from "@budibase/backend-core/tests" } from "@budibase/backend-core/tests"
@ -37,6 +39,7 @@ describe("/rows", () => {
}) })
beforeEach(async () => { beforeEach(async () => {
mocks.licenses.useCloudFree()
table = await config.createTable() table = await config.createTable()
row = basicRow(table._id!) row = basicRow(table._id!)
}) })
@ -670,7 +673,7 @@ describe("/rows", () => {
}) })
it("should be able to run on a view", async () => { 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 row = await config.createRow()
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage() const queryUsage = await getQueryUsage()
@ -1314,6 +1317,85 @@ describe("/rows", () => {
bookmark: expect.any(String), 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,
})
})
})
}) })
}) })
}) })

View File

@ -87,7 +87,7 @@ describe("/tables", () => {
it("updates all the row fields for a table when a schema key is renamed", async () => { it("updates all the row fields for a table when a schema key is renamed", async () => {
const testTable = await config.createTable() const testTable = await config.createTable()
await config.createView({ await config.createLegacyView({
name: "TestView", name: "TestView",
field: "Price", field: "Price",
calculation: "stats", calculation: "stats",
@ -254,7 +254,7 @@ describe("/tables", () => {
})) }))
await config.api.viewV2.create({ tableId }) 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() const res = await config.api.table.fetch()

View File

@ -249,7 +249,7 @@ describe("/views", () => {
}) })
it("returns only custom views", async () => { it("returns only custom views", async () => {
await config.createView({ await config.createLegacyView({
name: "TestView", name: "TestView",
field: "Price", field: "Price",
calculation: "stats", calculation: "stats",
@ -267,7 +267,7 @@ describe("/views", () => {
describe("query", () => { describe("query", () => {
it("returns data for the created view", async () => { it("returns data for the created view", async () => {
await config.createView({ await config.createLegacyView({
name: "TestView", name: "TestView",
field: "Price", field: "Price",
calculation: "stats", calculation: "stats",
@ -295,7 +295,7 @@ describe("/views", () => {
}) })
it("returns data for the created view using a group by", async () => { it("returns data for the created view using a group by", async () => {
await config.createView({ await config.createLegacyView({
calculation: "stats", calculation: "stats",
name: "TestView", name: "TestView",
field: "Price", field: "Price",
@ -331,7 +331,7 @@ describe("/views", () => {
describe("destroy", () => { describe("destroy", () => {
it("should be able to delete a view", async () => { it("should be able to delete a view", async () => {
const table = await config.createTable(priceTable()) const table = await config.createTable(priceTable())
const view = await config.createView() const view = await config.createLegacyView()
const res = await request const res = await request
.delete(`/api/views/${view.name}`) .delete(`/api/views/${view.name}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
@ -395,7 +395,7 @@ describe("/views", () => {
it("should be able to export a view as JSON", async () => { it("should be able to export a view as JSON", async () => {
let table = await setupExport() let table = await setupExport()
const view = await config.createView() const view = await config.createLegacyView()
table = await config.getTable(table._id) table = await config.getTable(table._id)
let res = await exportView(view.name, "json") let res = await exportView(view.name, "json")
@ -407,7 +407,7 @@ describe("/views", () => {
it("should be able to export a view as CSV", async () => { it("should be able to export a view as CSV", async () => {
let table = await setupExport() let table = await setupExport()
const view = await config.createView() const view = await config.createLegacyView()
table = await config.getTable(table._id) table = await config.getTable(table._id)
let res = await exportView(view.name, "csv") let res = await exportView(view.name, "csv")

View File

@ -296,7 +296,7 @@ describe.each([
}) })
it("cannot update views v1", async () => { it("cannot update views v1", async () => {
const viewV1 = await config.createView() const viewV1 = await config.createLegacyView()
await config.api.viewV2.update( await config.api.viewV2.update(
{ {
...viewV1, ...viewV1,

View File

@ -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
) )

View File

@ -6,11 +6,11 @@ import { isDevAppID } from "../db/utils"
// need this to call directly, so we can get a response // need this to call directly, so we can get a response
import { automationQueue } from "./bullboard" import { automationQueue } from "./bullboard"
import { checkTestFlag } from "../utilities/redis" import { checkTestFlag } from "../utilities/redis"
import * as utils from "./utils"
import env from "../environment" import env from "../environment"
import { context, db as dbCore } from "@budibase/backend-core" import { context, db as dbCore } from "@budibase/backend-core"
import { Automation, Row, AutomationData, AutomationJob } from "@budibase/types" import { Automation, Row, AutomationData, AutomationJob } from "@budibase/types"
import { executeSynchronously } from "../threads/automation" import { executeSynchronously } from "../threads/automation"
import sdk from "../sdk"
export const TRIGGER_DEFINITIONS = definitions export const TRIGGER_DEFINITIONS = definitions
const JOB_OPTS = { const JOB_OPTS = {
@ -142,7 +142,7 @@ export async function rebootTrigger() {
let automations = await getAllAutomations() let automations = await getAllAutomations()
let rebootEvents = [] let rebootEvents = []
for (let automation of automations) { for (let automation of automations) {
if (sdk.automations.isReboot(automation)) { if (utils.isRebootTrigger(automation)) {
const job = { const job = {
automation, automation,
event: { event: {

View File

@ -17,17 +17,16 @@ import {
import sdk from "../sdk" import sdk from "../sdk"
import { automationsEnabled } from "../features" import { automationsEnabled } from "../features"
const REBOOT_CRON = "@reboot"
const WH_STEP_ID = definitions.WEBHOOK.stepId const WH_STEP_ID = definitions.WEBHOOK.stepId
const CRON_STEP_ID = definitions.CRON.stepId
let Runner: Thread let Runner: Thread
if (automationsEnabled()) { if (automationsEnabled()) {
Runner = new Thread(ThreadType.AUTOMATION) Runner = new Thread(ThreadType.AUTOMATION)
} }
function loggingArgs( function loggingArgs(job: AutomationJob) {
job: AutomationJob, return [
timing?: { start: number; complete?: boolean }
) {
const logs: any[] = [
{ {
_logKey: "automation", _logKey: "automation",
trigger: job.data.automation.definition.trigger.event, trigger: job.data.automation.definition.trigger.event,
@ -37,53 +36,24 @@ function loggingArgs(
jobId: job.id, jobId: job.id,
}, },
] ]
if (timing?.start) {
logs.push({
_logKey: "startTime",
start: timing.start,
})
}
if (timing?.start && timing?.complete) {
const end = new Date().getTime()
const duration = end - timing.start
logs.push({
_logKey: "endTime",
end,
})
logs.push({
_logKey: "duration",
duration,
})
}
return logs
} }
export async function processEvent(job: AutomationJob) { export async function processEvent(job: AutomationJob) {
const appId = job.data.event.appId! const appId = job.data.event.appId!
const automationId = job.data.automation._id! const automationId = job.data.automation._id!
const start = new Date().getTime()
const task = async () => { const task = async () => {
try { try {
// need to actually await these so that an error can be captured properly // need to actually await these so that an error can be captured properly
console.log("automation running", ...loggingArgs(job, { start })) console.log("automation running", ...loggingArgs(job))
const runFn = () => Runner.run(job) const runFn = () => Runner.run(job)
const result = await quotas.addAutomation(runFn, { const result = await quotas.addAutomation(runFn, {
automationId, automationId,
}) })
const end = new Date().getTime() console.log("automation completed", ...loggingArgs(job))
const duration = end - start
console.log(
"automation completed",
...loggingArgs(job, { start, complete: true })
)
return result return result
} catch (err) { } catch (err) {
console.error( console.error(`automation was unable to run`, err, ...loggingArgs(job))
`automation was unable to run`,
err,
...loggingArgs(job, { start, complete: true })
)
return { err } return { err }
} }
} }
@ -163,6 +133,19 @@ export async function clearMetadata() {
await db.bulkDocs(automationMetadata) await db.bulkDocs(automationMetadata)
} }
export function isCronTrigger(auto: Automation) {
return (
auto &&
auto.definition.trigger &&
auto.definition.trigger.stepId === CRON_STEP_ID
)
}
export function isRebootTrigger(auto: Automation) {
const trigger = auto ? auto.definition.trigger : null
return isCronTrigger(auto) && trigger?.inputs.cron === REBOOT_CRON
}
/** /**
* This function handles checking of any cron jobs that need to be enabled/updated. * This function handles checking of any cron jobs that need to be enabled/updated.
* @param {string} appId The ID of the app in which we are checking for webhooks * @param {string} appId The ID of the app in which we are checking for webhooks
@ -170,14 +153,14 @@ export async function clearMetadata() {
*/ */
export async function enableCronTrigger(appId: any, automation: Automation) { export async function enableCronTrigger(appId: any, automation: Automation) {
const trigger = automation ? automation.definition.trigger : null const trigger = automation ? automation.definition.trigger : null
const validCron = sdk.automations.isCron(automation) && trigger?.inputs.cron
const needsCreated =
!sdk.automations.isReboot(automation) &&
!sdk.automations.disabled(automation)
let enabled = false let enabled = false
// need to create cron job // need to create cron job
if (validCron && needsCreated) { if (
isCronTrigger(automation) &&
!isRebootTrigger(automation) &&
trigger?.inputs.cron
) {
// make a job id rather than letting Bull decide, makes it easier to handle on way out // make a job id rather than letting Bull decide, makes it easier to handle on way out
const jobId = `${appId}_cron_${newid()}` const jobId = `${appId}_cron_${newid()}`
const job: any = await automationQueue.add( const job: any = await automationQueue.add(

View File

@ -6,11 +6,10 @@ import {
users, users,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { PermissionLevel, PermissionType, Role, UserCtx } from "@budibase/types" import { PermissionLevel, PermissionType, Role, UserCtx } from "@budibase/types"
import { features } from "@budibase/pro"
import builderMiddleware from "./builder" import builderMiddleware from "./builder"
import { isWebhookEndpoint } from "./utils" import { isWebhookEndpoint } from "./utils"
import { paramResource } from "./resourceId" import { paramResource } from "./resourceId"
import { extractViewInfoFromID, isViewID } from "../db/utils" import sdk from "../sdk"
function hasResource(ctx: any) { function hasResource(ctx: any) {
return ctx.resourceId != null 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 = const authorized =
( (
permType: PermissionType, permType: PermissionType,
@ -121,8 +95,8 @@ const authorized =
} }
// get the resource roles // get the resource roles
let resourceRoles: any = [] let resourceRoles: string[] = []
let otherLevelRoles: any = [] let otherLevelRoles: string[] = []
const otherLevel = const otherLevel =
permLevel === PermissionLevel.READ permLevel === PermissionLevel.READ
? PermissionLevel.WRITE ? PermissionLevel.WRITE
@ -133,21 +107,28 @@ const authorized =
paramResource(resourcePath)(ctx, () => {}) paramResource(resourcePath)(ctx, () => {})
} }
if (resourceIdTranformers[permType]) {
await resourceIdTranformers[permType]!(ctx)
}
if (hasResource(ctx)) { if (hasResource(ctx)) {
const { resourceId, subResourceId } = ctx const { resourceId, subResourceId } = ctx
resourceRoles = await roles.getRequiredResourceRole(permLevel!, {
resourceId, const permissions = await sdk.permissions.getResourcePerms(resourceId)
subResourceId, 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) { if (opts && opts.schema) {
otherLevelRoles = await roles.getRequiredResourceRole(otherLevel, { otherLevelRoles = getPermLevel(otherLevel!)
resourceId,
subResourceId,
})
} }
} }

View File

@ -1,28 +1,20 @@
jest.mock("@budibase/backend-core", () => ({ jest.mock("../../sdk/app/permissions", () => ({
...jest.requireActual("@budibase/backend-core"), ...jest.requireActual("../../sdk/app/permissions"),
roles: { getResourcePerms: jest.fn().mockResolvedValue([]),
...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"
},
})) }))
import { PermissionType, PermissionLevel } from "@budibase/types" import {
PermissionType,
PermissionLevel,
PermissionSource,
} from "@budibase/types"
import authorizedMiddleware from "../authorized" import authorizedMiddleware from "../authorized"
import env from "../../environment" import env from "../../environment"
import { generateTableID, generateViewID } from "../../db/utils" import { generateTableID, generateViewID } from "../../db/utils"
import { roles } from "@budibase/backend-core" import { generator, mocks } from "@budibase/backend-core/tests"
import { mocks } from "@budibase/backend-core/tests"
import { initProMocks } from "../../tests/utilities/mocks/pro" import { initProMocks } from "../../tests/utilities/mocks/pro"
import { getResourcePerms } from "../../sdk/app/permissions"
const APP_ID = "" const APP_ID = ""
@ -189,23 +181,26 @@ describe("Authorization middleware", () => {
) )
}) })
describe("view type", () => { describe("with resource", () => {
const tableId = generateTableID() let resourceId: string
const viewId = generateViewID(tableId) const mockedGetResourcePerms = getResourcePerms as jest.MockedFunction<
typeof getResourcePerms
const mockedGetRequiredResourceRole = >
roles.getRequiredResourceRole as jest.MockedFunction<
typeof roles.getRequiredResourceRole
>
beforeEach(() => { beforeEach(() => {
config.setMiddlewareRequiredPermission( config.setMiddlewareRequiredPermission(
PermissionType.VIEW, PermissionType.VIEW,
PermissionLevel.READ 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({ config.setUser({
_id: "user", _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() await config.executeMiddleware()
expect(config.throw).not.toBeCalled() expect(config.throw).not.toBeCalled()
expect(config.next).toHaveBeenCalled() expect(config.next).toHaveBeenCalled()
expect(mockedGetRequiredResourceRole).toBeCalledTimes(1) expect(mockedGetResourcePerms).toBeCalledTimes(1)
expect(mockedGetRequiredResourceRole).toBeCalledWith( expect(mockedGetResourcePerms).toBeCalledWith(resourceId)
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`
)
}) })
}) })
}) })

View File

@ -50,9 +50,9 @@ describe("migrations", () => {
await config.createRole() await config.createRole()
await config.createRole() await config.createRole()
await config.createTable() await config.createTable()
await config.createView() await config.createLegacyView()
await config.createTable() 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()
await config.createScreen() await config.createScreen()

View File

@ -1,38 +0,0 @@
import { context } from "@budibase/backend-core"
import { Automation, AutomationState, DocumentType } from "@budibase/types"
import { definitions } from "../../../automations/triggerInfo"
const REBOOT_CRON = "@reboot"
export async function exists(automationId: string) {
if (!automationId?.startsWith(DocumentType.AUTOMATION)) {
throw new Error("Invalid automation ID.")
}
const db = context.getAppDB()
return db.docExists(automationId)
}
export async function get(automationId: string) {
const db = context.getAppDB()
return (await db.get(automationId)) as Automation
}
export function disabled(automation: Automation) {
return automation.state === AutomationState.DISABLED || !hasSteps(automation)
}
export function isCron(automation: Automation) {
return (
automation?.definition.trigger &&
automation?.definition.trigger.stepId === definitions.CRON.stepId
)
}
export function isReboot(automation: Automation) {
const trigger = automation?.definition.trigger
return isCron(automation) && trigger?.inputs.cron === REBOOT_CRON
}
export function hasSteps(automation: Automation) {
return automation?.definition?.steps?.length > 0
}

View File

@ -1,9 +1,7 @@
import * as webhook from "./webhook" import * as webhook from "./webhook"
import * as utils from "./utils" import * as utils from "./utils"
import * as automations from "./automations"
export default { export default {
webhook, webhook,
utils, utils,
...automations,
} }

View File

@ -60,7 +60,7 @@ function tarFilesToTmp(tmpDir: string, files: string[]) {
export async function exportDB( export async function exportDB(
dbName: string, dbName: string,
opts: DBDumpOpts = {} opts: DBDumpOpts = {}
): Promise<DBDumpOpts> { ): Promise<string> {
const exportOpts = { const exportOpts = {
filter: opts?.filter, filter: opts?.filter,
batch_size: 1000, batch_size: 1000,

View File

@ -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
}

View File

@ -622,7 +622,7 @@ class TestConfiguration {
// VIEW // VIEW
async createView(config?: any) { async createLegacyView(config?: any) {
if (!this.table) { if (!this.table) {
throw "Test requires table to be configured." throw "Test requires table to be configured."
} }

View File

@ -1,6 +1,5 @@
import { default as threadUtils } from "./utils" import { default as threadUtils } from "./utils"
import { Job } from "bull" import { Job } from "bull"
threadUtils.threadSetup()
import { import {
disableCronById, disableCronById,
isErrorInOutput, isErrorInOutput,
@ -35,8 +34,8 @@ import { cloneDeep } from "lodash/fp"
import { performance } from "perf_hooks" import { performance } from "perf_hooks"
import * as sdkUtils from "../sdk/utils" import * as sdkUtils from "../sdk/utils"
import env from "../environment" import env from "../environment"
import sdk from "../sdk"
threadUtils.threadSetup()
const FILTER_STEP_ID = actions.BUILTIN_ACTION_DEFINITIONS.FILTER.stepId const FILTER_STEP_ID = actions.BUILTIN_ACTION_DEFINITIONS.FILTER.stepId
const LOOP_STEP_ID = actions.BUILTIN_ACTION_DEFINITIONS.LOOP.stepId const LOOP_STEP_ID = actions.BUILTIN_ACTION_DEFINITIONS.LOOP.stepId
const CRON_STEP_ID = triggerDefs.CRON.stepId const CRON_STEP_ID = triggerDefs.CRON.stepId
@ -520,8 +519,7 @@ class Orchestrator {
export function execute(job: Job<AutomationData>, callback: WorkerCallback) { export function execute(job: Job<AutomationData>, callback: WorkerCallback) {
const appId = job.data.event.appId const appId = job.data.event.appId
const automation = job.data.automation const automationId = job.data.automation._id
const automationId = automation._id
if (!appId) { if (!appId) {
throw new Error("Unable to execute, event doesn't contain app ID.") throw new Error("Unable to execute, event doesn't contain app ID.")
} }
@ -532,30 +530,10 @@ export function execute(job: Job<AutomationData>, callback: WorkerCallback) {
appId, appId,
automationId, automationId,
task: async () => { task: async () => {
let automation = job.data.automation,
isCron = sdk.automations.isCron(job.data.automation),
notFound = false
try {
automation = await sdk.automations.get(automationId)
} catch (err: any) {
// automation no longer exists
notFound = err
}
const disabled = sdk.automations.disabled(automation)
const stopAutomation = disabled || notFound
const envVars = await sdkUtils.getEnvironmentVariables() const envVars = await sdkUtils.getEnvironmentVariables()
// put into automation thread for whole context // put into automation thread for whole context
await context.doInEnvironmentContext(envVars, async () => { await context.doInEnvironmentContext(envVars, async () => {
const automationOrchestrator = new Orchestrator(job) const automationOrchestrator = new Orchestrator(job)
// hard stop on automations
if (isCron && stopAutomation) {
await automationOrchestrator.stopCron(
disabled ? "disabled" : "not_found"
)
}
if (stopAutomation) {
return
}
try { try {
const response = await automationOrchestrator.execute() const response = await automationOrchestrator.execute()
callback(null, response) callback(null, response)

View File

@ -23,6 +23,9 @@ export function getPermissionType(resourceId: string) {
case DocumentType.QUERY: case DocumentType.QUERY:
case DocumentType.DATASOURCE: case DocumentType.DATASOURCE:
return permissions.PermissionType.QUERY return permissions.PermissionType.QUERY
default:
// legacy views don't have an ID, will end up here
return permissions.PermissionType.LEGACY_VIEW
} }
} }

View File

@ -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"

View 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>
}

View File

@ -100,10 +100,6 @@ export const AutomationStepIdArray = [
...Object.values(AutomationTriggerStepId), ...Object.values(AutomationTriggerStepId),
] ]
export enum AutomationState {
DISABLED = "disabled",
}
export interface Automation extends Document { export interface Automation extends Document {
definition: { definition: {
steps: AutomationStep[] steps: AutomationStep[]
@ -116,7 +112,6 @@ export interface Automation extends Document {
name: string name: string
internal?: boolean internal?: boolean
type?: string type?: string
state?: AutomationState
} }
interface BaseIOStructure { interface BaseIOStructure {

View File

@ -40,11 +40,6 @@ export type DatabasePutOpts = {
force?: boolean force?: boolean
} }
export type DocExistsResponse = {
_rev?: string
exists: boolean
}
export type DatabaseCreateIndexOpts = { export type DatabaseCreateIndexOpts = {
index: { index: {
fields: string[] fields: string[]
@ -95,7 +90,6 @@ export interface Database {
exists(): Promise<boolean> exists(): Promise<boolean>
checkSetup(): Promise<Nano.DocumentScope<any>> checkSetup(): Promise<Nano.DocumentScope<any>>
get<T>(id?: string): Promise<T> get<T>(id?: string): Promise<T>
docExists(id: string): Promise<DocExistsResponse>
remove( remove(
id: string | Document, id: string | Document,
rev?: string rev?: string

View File

@ -16,4 +16,11 @@ export enum PermissionType {
GLOBAL_BUILDER = "globalBuilder", GLOBAL_BUILDER = "globalBuilder",
QUERY = "query", QUERY = "query",
VIEW = "view", VIEW = "view",
LEGACY_VIEW = "legacy_view",
}
export enum PermissionSource {
EXPLICIT = "EXPLICIT",
INHERITED = "INHERITED",
BASE = "BASE",
} }

View File

@ -266,17 +266,14 @@ export const onboardUsers = async (ctx: Ctx<InviteUsersRequest>) => {
// Temp password to be passed to the user. // Temp password to be passed to the user.
createdPasswords[invite.email] = password createdPasswords[invite.email] = password
let builder: { global: boolean; apps?: string[] } = { global: false }
if (invite.userInfo.appBuilders) {
builder.apps = invite.userInfo.appBuilders
}
return { return {
email: invite.email, email: invite.email,
password, password,
forceResetPassword: true, forceResetPassword: true,
roles: invite.userInfo.apps, roles: invite.userInfo.apps,
admin: { global: false }, admin: invite.userInfo.admin,
builder, builder: invite.userInfo.builder,
tenantId: tenancy.getTenantId(), tenantId: tenancy.getTenantId(),
} }
}) })
@ -371,13 +368,10 @@ export const updateInvite = async (ctx: any) => {
...invite, ...invite,
} }
if (!updateBody?.appBuilders || !updateBody.appBuilders?.length) { if (!updateBody?.builder?.apps && updated.info?.builder?.apps) {
updated.info.appBuilders = [] updated.info.builder.apps = []
} else { } else if (updateBody?.builder) {
updated.info.appBuilders = [ updated.info.builder = updateBody.builder
...(invite.info.appBuilders ?? []),
...updateBody.appBuilders,
]
} }
if (!updateBody?.apps || !Object.keys(updateBody?.apps).length) { if (!updateBody?.apps || !Object.keys(updateBody?.apps).length) {
@ -409,15 +403,17 @@ export const inviteAccept = async (
lastName, lastName,
password, password,
email, email,
admin: { global: info?.admin?.global || false },
roles: info.apps, roles: info.apps,
tenantId: info.tenantId, 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) { if (info?.builder?.apps) {
builder.apps = info.appBuilders builder.apps = info.builder.apps
request.builder = builder request.builder = builder
delete info.appBuilders
} }
delete info.apps delete info.apps
request = { request = {

View File

@ -15,7 +15,7 @@ const { nodeExternalsPlugin } = require("esbuild-node-externals")
var argv = require("minimist")(process.argv.slice(2)) var argv = require("minimist")(process.argv.slice(2))
function runBuild(entry, outfile) { function runBuild(entry, outfile, opts = { skipMeta: false, bundle: true }) {
const isDev = process.env.NODE_ENV !== "production" const isDev = process.env.NODE_ENV !== "production"
const tsconfig = argv["p"] || `tsconfig.build.json` const tsconfig = argv["p"] || `tsconfig.build.json`
const tsconfigPathPluginContent = JSON.parse( const tsconfigPathPluginContent = JSON.parse(
@ -36,12 +36,16 @@ function runBuild(entry, outfile) {
] ]
} }
const metafile = !opts.skipMeta
const { bundle } = opts
const sharedConfig = { const sharedConfig = {
entryPoints: [entry], entryPoints: [entry],
bundle: true, bundle,
minify: !isDev, minify: !isDev,
sourcemap: isDev, sourcemap: isDev,
tsconfig, tsconfig,
format: opts?.forcedFormat,
plugins: [ plugins: [
TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }), TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }),
nodeExternalsPlugin(), nodeExternalsPlugin(),
@ -50,15 +54,10 @@ function runBuild(entry, outfile) {
loader: { loader: {
".svelte": "copy", ".svelte": "copy",
}, },
metafile: true, metafile,
external: [ external: bundle
"deasync", ? ["deasync", "mock-aws-s3", "nock", "pino", "koa-pino-logger", "bull"]
"mock-aws-s3", : undefined,
"nock",
"pino",
"koa-pino-logger",
"bull",
],
} }
build({ build({
@ -77,10 +76,12 @@ function runBuild(entry, outfile) {
) )
}) })
fs.writeFileSync( if (metafile) {
`dist/${path.basename(outfile)}.meta.json`, fs.writeFileSync(
JSON.stringify(result.metafile) `dist/${path.basename(outfile)}.meta.json`,
) JSON.stringify(result.metafile)
)
}
}) })
} }