This commit is contained in:
Martin McKeaveney 2020-11-17 11:26:19 +00:00
commit a8efa3968d
64 changed files with 911 additions and 814 deletions

View File

@ -81,7 +81,8 @@
"shortid": "^2.2.15", "shortid": "^2.2.15",
"svelte-loading-spinners": "^0.1.1", "svelte-loading-spinners": "^0.1.1",
"svelte-portal": "^0.1.0", "svelte-portal": "^0.1.0",
"yup": "^0.29.2" "yup": "^0.29.2",
"uuid": "^8.3.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.5.5", "@babel/core": "^7.5.5",

View File

@ -50,6 +50,7 @@ export const getFrontendStore = () => {
return state return state
}) })
const screens = await api.get("/api/screens").then(r => r.json()) const screens = await api.get("/api/screens").then(r => r.json())
const routing = await api.get("/api/routing").then(r => r.json())
const mainScreens = screens.filter(screen => const mainScreens = screens.filter(screen =>
screen._id.includes(pkg.pages.main._id) screen._id.includes(pkg.pages.main._id)

View File

@ -1,22 +1,13 @@
import { Screen } from "./utils/Screen"
export default { export default {
name: `Create from scratch`, name: `Create from scratch`,
create: () => createScreen(), create: () => createScreen(),
} }
const createScreen = () => ({ const createScreen = () => {
props: { return new Screen()
_id: "", .mainType("div")
_component: "@budibase/standard-components/container", .component("@budibase/standard-components/container")
_styles: { .json()
normal: {}, }
hover: {},
active: {},
selected: {},
},
type: "div",
_children: [],
_instanceName: "",
},
route: "",
name: "screen-id",
})

View File

@ -1,22 +1,13 @@
import { Screen } from "./utils/Screen"
export default { export default {
name: `New Row (Empty)`, name: `New Row (Empty)`,
create: () => createScreen(), create: () => createScreen(),
} }
const createScreen = () => ({ const createScreen = () => {
props: { return new Screen()
_id: "", .component("@budibase/standard-components/newrow")
_component: "@budibase/standard-components/newrow", .table("")
_styles: { .json()
normal: {}, }
hover: {},
active: {},
selected: {},
},
_children: [],
_instanceName: "",
table: "",
},
route: "",
name: "screen-id",
})

View File

@ -1,22 +1,13 @@
import { Screen } from "./utils/Screen"
export default { export default {
name: `Row Detail (Empty)`, name: `Row Detail (Empty)`,
create: () => createScreen(), create: () => createScreen(),
} }
const createScreen = () => ({ const createScreen = () => {
props: { return new Screen()
_id: "", .component("@budibase/standard-components/rowdetail")
_component: "@budibase/standard-components/rowdetail", .table("")
_styles: { .json()
normal: {}, }
hover: {},
active: {},
selected: {},
},
_children: [],
_instanceName: "",
table: "",
},
route: "",
name: "screen-id",
})

View File

@ -24,7 +24,7 @@ const createTemplateOverride = (frontendState, create) => () => {
} }
screen.props._id = uuid() screen.props._id = uuid()
screen.name = screen.props._id screen.name = screen.props._id
screen.route = screen.route.toLowerCase() screen.routing.route = screen.routing.route.toLowerCase()
return screen return screen
} }

View File

@ -1,5 +1,8 @@
import sanitizeUrl from "./sanitizeUrl" import sanitizeUrl from "./utils/sanitizeUrl"
import { rowListUrl } from "./rowListScreen" import { rowListUrl } from "./rowListScreen"
import { Component } from "./utils/Component"
import { Screen } from "./utils/Screen"
import { linkComponent } from "./utils/commonComponents"
export default function(tables) { export default function(tables) {
return tables.map(table => { return tables.map(table => {
@ -14,242 +17,133 @@ export default function(tables) {
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new`) export const newRowUrl = table => sanitizeUrl(`/${table.name}/new`)
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE" export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
const createScreen = table => ({ function breadcrumbContainer(table) {
props: { const link = linkComponent(table.name).instanceName("Back Link")
_id: "c683c4ca8ffc849c6bdd3b7d637fbbf3c",
_component: "@budibase/standard-components/newrow", const arrowText = new Component("@budibase/standard-components/text")
_styles: { .type("none")
normal: {}, .normalStyle({
hover: {}, "margin-right": "4px",
active: {}, "margin-left": "4px",
selected: {}, })
}, .text(">")
table: table._id, .instanceName("Arrow")
_children: [
{ const newText = new Component("@budibase/standard-components/text")
_id: "ccad6cc135c7947a7ba9c631f655d6e0f", .type("none")
_component: "@budibase/standard-components/container", .normalStyle({
_styles: { color: "#000000",
normal: { })
width: "700px", .text("New")
padding: "0px", .instanceName("Identifier")
background: "white",
"border-radius": "0.5rem", return new Component("@budibase/standard-components/container")
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)", .type("div")
margin: "auto", .normalStyle({
"margin-top": "20px", "font-size": "14px",
"padding-top": "48px", color: "#757575",
"padding-bottom": "48px", })
"padding-right": "48px", .instanceName("Breadcrumbs")
"padding-left": "48px", .addChild(link)
"margin-bottom": "20px", .addChild(arrowText)
.addChild(newText)
}
function titleContainer(table) {
const heading = new Component("@budibase/standard-components/heading")
.normalStyle({
margin: "0px",
"margin-bottom": "0px",
"margin-right": "0px",
"margin-top": "0px",
"margin-left": "0px",
flex: "1 1 auto",
})
.type("h3")
.instanceName("Title")
.text("New Row")
const button = new Component("@budibase/standard-components/button")
.normalStyle({
background: "#000000",
"border-width": "0",
"border-style": "None",
color: "#fff",
"font-family": "Inter",
"font-weight": "500",
"font-size": "14px",
"margin-left": "16px",
})
.hoverStyle({
background: "#4285f4",
})
.text("Save")
.customProps({
className: "",
disabled: false,
onClick: [
{
parameters: {
contextPath: "data",
tableId: table._id,
}, },
hover: {}, "##eventHandlerType": "Save Row",
active: {},
selected: {},
}, },
_code: "", {
className: "", parameters: {
onLoad: [], url: rowListUrl(table),
type: "div",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Container",
_children: [
{
_id: "c6e91622ba7984f468f70bf4bf5120246",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
"font-size": "14px",
color: "#757575",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "div",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Breadcrumbs",
_children: [
{
_id: "caa33353c252c4931b2a51b48a559a7fc",
_component: "@budibase/standard-components/link",
_styles: {
normal: {
color: "#757575",
"text-transform": "capitalize",
},
hover: {
color: "#4285f4",
},
active: {},
selected: {},
},
_code: "",
url: `/${table.name.toLowerCase()}`,
openInNewTab: false,
text: table.name,
color: "",
hoverColor: "",
underline: false,
fontSize: "",
fontFamily: "initial",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Back Link",
_children: [],
},
{
_id: "c6e218170201040e7a74e2c8304fe1860",
_component: "@budibase/standard-components/text",
_styles: {
normal: {
"margin-right": "4px",
"margin-left": "4px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
text: ">",
type: "none",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Arrow",
_children: [],
},
{
_id: "c799da1fa3a84442e947cc9199518f64c",
_component: "@budibase/standard-components/text",
_styles: {
normal: {
color: "#000000",
},
hover: {},
active: {},
selected: {},
},
_code: "",
text: "New",
type: "none",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Identifier",
_children: [],
},
],
}, },
{ "##eventHandlerType": "Navigate To",
_id: "cbd1637cd1e274287a3c28ef0bf235d08", },
_component: "@budibase/standard-components/container", ],
_styles: { })
normal: { .instanceName("Save Button")
display: "flex",
"flex-direction": "row", return new Component("@budibase/standard-components/container")
"justify-content": "space-between", .type("div")
"align-items": "center", .normalStyle({
"margin-top": "32px", display: "flex",
"margin-bottom": "32px", "flex-direction": "row",
}, "justify-content": "space-between",
hover: {}, "align-items": "center",
active: {}, "margin-top": "32px",
selected: {}, "margin-bottom": "32px",
}, })
_code: "", .instanceName("Title Container")
className: "", .addChild(heading)
onLoad: [], .addChild(button)
type: "div", }
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Title Container", const createScreen = table => {
_children: [ const dataform = new Component("@budibase/standard-components/dataformwide")
{ .instanceName("Form")
_id: "c98d3675d04114558bbf28661c5ccfb8e",
_component: "@budibase/standard-components/heading", const mainContainer = new Component("@budibase/standard-components/container")
_styles: { .type("div")
normal: { .normalStyle({
margin: "0px", width: "700px",
"margin-bottom": "0px", padding: "0px",
"margin-right": "0px", background: "white",
"margin-top": "0px", "border-radius": "0.5rem",
"margin-left": "0px", "box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
flex: "1 1 auto", margin: "auto",
}, "margin-top": "20px",
hover: {}, "padding-top": "48px",
active: {}, "padding-bottom": "48px",
selected: {}, "padding-right": "48px",
}, "padding-left": "48px",
_code: "", "margin-bottom": "20px",
className: "", })
text: "New Row", .instanceName("Container")
type: "h3", .addChild(breadcrumbContainer(table))
_instanceName: "Title", .addChild(titleContainer(table))
_children: [], .addChild(dataform)
},
{ return new Screen().component("@budibase/standard-components/newrow")
_id: "cae402bd3c6a44618a8341bf7ab9ab086", .addChild(mainContainer)
_component: "@budibase/standard-components/button", .table(table._id)
_styles: { .route(newRowUrl(table))
normal: { .instanceName(`${table.name} - New`)
background: "#000000", .name("")
"border-width": "0", .json()
"border-style": "None", }
color: "#fff",
"font-family": "Inter",
"font-weight": "500",
"font-size": "14px",
"margin-left": "16px",
},
hover: {
background: "#4285f4",
},
active: {},
selected: {},
},
_code: "",
text: "Save",
className: "",
disabled: false,
onClick: [
{
parameters: {
contextPath: "data",
tableId: table._id,
},
"##eventHandlerType": "Save Row",
},
{
parameters: {
url: rowListUrl(table),
},
"##eventHandlerType": "Navigate To",
},
],
_instanceName: "Save Button",
_children: [],
},
],
},
{
_id: "c5e6c98d7363640f9ad3a7d19c8c10f67",
_component: "@budibase/standard-components/dataformwide",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_code: "",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Form",
_children: [],
},
],
},
],
_instanceName: `${table.name} - New`,
_code: "",
},
route: newRowUrl(table),
name: "",
})

View File

@ -1,4 +1,4 @@
import sanitizeUrl from "./sanitizeUrl" import sanitizeUrl from "./utils/sanitizeUrl"
import { rowListUrl } from "./rowListScreen" import { rowListUrl } from "./rowListScreen"
export default function(tables) { export default function(tables) {
@ -299,6 +299,9 @@ const createScreen = (table, heading) => ({
_instanceName: `${table.name} - Detail`, _instanceName: `${table.name} - Detail`,
_code: "", _code: "",
}, },
route: rowDetailUrl(table), routing: {
route: rowDetailUrl(table),
accessLevelId: "",
},
name: "", name: "",
}) })

View File

@ -1,4 +1,4 @@
import sanitizeUrl from "./sanitizeUrl" import sanitizeUrl from "./utils/sanitizeUrl"
import { newRowUrl } from "./newRowScreen" import { newRowUrl } from "./newRowScreen"
export default function(tables) { export default function(tables) {
@ -167,6 +167,9 @@ const createScreen = table => ({
className: "", className: "",
onLoad: [], onLoad: [],
}, },
route: rowListUrl(table), routing: {
route: rowListUrl(table),
accessLevelId: "",
},
name: "", name: "",
}) })

View File

@ -0,0 +1,36 @@
import { cloneDeep } from "lodash/fp"
export class BaseStructure {
constructor(isScreen) {
this._isScreen = isScreen
this._children = []
this._json = {
}
}
addChild(child) {
this._children.push(child)
return this
}
customProps(props) {
for (let key of Object.keys(props)) {
this._json[key] = props[key]
}
return this
}
json() {
const structure = cloneDeep(this._json)
if (this._children.length !== 0) {
for (let child of this._children) {
if (this._isScreen) {
structure.props._children.push(child.json())
} else {
structure._children.push(child.json())
}
}
}
return structure
}
}

View File

@ -0,0 +1,52 @@
import { cloneDeep } from "lodash/fp"
import { v4 } from "uuid"
import { BaseStructure } from "./BaseStructure"
export class Component extends BaseStructure {
constructor(name) {
super(false)
this._children = []
this._json = {
_id: v4(),
_component: name,
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "",
_instanceName: "",
_children: [],
}
}
type(type) {
this._json.type = type
return this
}
normalStyle(styling) {
this._json._styles.normal = styling
return this
}
hoverStyle(styling) {
this._json._styles.hover = styling
return this
}
text(text) {
this._json.text = text
return this
}
// TODO: do we need this
instanceName(name) {
this._json._instanceName = name
return this
}
}

View File

@ -0,0 +1,56 @@
import { BaseStructure } from "./BaseStructure"
export class Screen extends BaseStructure {
constructor() {
super(true)
this._json = {
props: {
_id: "",
_component: "",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_children: [],
_instanceName: "",
},
routing: {
route: "",
accessLevelId: "",
},
name: "screen-id",
}
}
component(name) {
this._json.props._component = name
return this
}
table(tableName) {
this._json.props.table = tableName
return this
}
mainType(type) {
this._json.type = type
return this
}
route(route) {
this._json.routing.route = route
return this
}
name(name) {
this._json.name = name
return this
}
instanceName(name) {
this._json.props._instanceName = name
return this
}
}

View File

@ -0,0 +1,22 @@
import { Component } from "./Component"
export function linkComponent(tableName) {
return new Component("@budibase/standard-components/link")
.normalStyle({
color: "#757575",
"text-transform": "capitalize",
})
.hoverStyle({
color: "#4285f4",
})
.text(tableName)
.customProps({
url: `/${tableName.toLowerCase()}`,
openInNewTab: false,
color: "",
hoverColor: "",
underline: false,
fontSize: "",
fontFamily: "initial",
})
}

View File

@ -59,10 +59,10 @@
} }
// Create autolink to newly created list page // Create autolink to newly created list page
const listPage = screens.find(screen => const listScreen = screens.find(screen =>
screen.props._instanceName.endsWith("List") screen.props._instanceName.endsWith("List")
) )
await store.actions.components.links.save(listPage.route, table.name) await store.actions.components.links.save(listScreen.routing.route, table.name)
// Navigate to new table // Navigate to new table
$goto(`./table/${table._id}`) $goto(`./table/${table._id}`)

View File

@ -17,11 +17,11 @@
.filter( .filter(
screen => screen =>
screen.props._component.endsWith("/rowdetail") || screen.props._component.endsWith("/rowdetail") ||
screen.route.endsWith(":id") screen.routing.route.endsWith(":id")
) )
.map(screen => ({ .map(screen => ({
name: screen.props._instanceName, name: screen.props._instanceName,
url: screen.route, url: screen.routing.route,
sort: screen.props._component, sort: screen.props._component,
})), })),
] ]

View File

@ -25,7 +25,7 @@
<DataList on:change bind:value={parameter.value}> <DataList on:change bind:value={parameter.value}>
<option value="" /> <option value="" />
{#each $allScreens as screen} {#each $allScreens as screen}
<option value={screen.route}>{screen.props._instanceName}</option> <option value={screen.routing.route}>{screen.props._instanceName}</option>
{/each} {/each}
</DataList> </DataList>
{:else} {:else}

View File

@ -10,7 +10,7 @@
<DataList secondary bind:value={parameters.url}> <DataList secondary bind:value={parameters.url}>
<option value="" /> <option value="" />
{#each $allScreens as screen} {#each $allScreens as screen}
<option value={screen.route}>{screen.props._instanceName}</option> <option value={screen.routing.route}>{screen.props._instanceName}</option>
{/each} {/each}
</DataList> </DataList>
</div> </div>

View File

@ -49,8 +49,8 @@
baseComponent = draftScreen.props._component baseComponent = draftScreen.props._component
} }
if (draftScreen.route) { if (draftScreen.routing) {
route = draftScreen.route route = draftScreen.routing.route
} }
} }
@ -69,7 +69,8 @@
draftScreen.props._instanceName = name draftScreen.props._instanceName = name
draftScreen.props._component = baseComponent draftScreen.props._component = baseComponent
draftScreen.route = route // TODO: need to fix this up correctly
draftScreen.routing = { route, accessLevelId: "ADMIN" }
await store.actions.screens.create(draftScreen) await store.actions.screens.create(draftScreen)
if (createLink) { if (createLink) {
@ -88,7 +89,7 @@
const routeNameExists = route => { const routeNameExists = route => {
return $allScreens.some( return $allScreens.some(
screen => screen.route.toLowerCase() === route.toLowerCase() screen => screen.routing.route.toLowerCase() === route.toLowerCase()
) )
} }

View File

@ -21,7 +21,7 @@
.filter(screen => !screen.props._component.endsWith("/rowdetail")) .filter(screen => !screen.props._component.endsWith("/rowdetail"))
.map(screen => ({ .map(screen => ({
name: screen.props._instanceName, name: screen.props._instanceName,
url: screen.route, url: screen.routing.route,
sort: screen.props._component, sort: screen.props._component,
})), })),
] ]
@ -54,7 +54,7 @@
if (idBinding) { if (idBinding) {
urls.push({ urls.push({
name: detailScreen.props._instanceName, name: detailScreen.props._instanceName,
url: detailScreen.route.replace( url: detailScreen.routing.route.replace(
":id", ":id",
`{{ ${idBinding.runtimeBinding} }}` `{{ ${idBinding.runtimeBinding} }}`
), ),

View File

@ -719,10 +719,10 @@
svelte-flatpickr "^2.4.0" svelte-flatpickr "^2.4.0"
svelte-portal "^1.0.0" svelte-portal "^1.0.0"
"@budibase/client@^0.3.7": "@budibase/client@^0.3.8":
version "0.3.7" version "0.3.8"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.3.7.tgz#8ed2d40d91ba3788a69ee5db5078f757adb4187f" resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.3.8.tgz#75df7e97e8f0d9b58c00e2bb0d3b4a55f8d04735"
integrity sha512-EgpHfw/WOUYeCG4cILDbaN2WFBDSPS698Z+So7FP5l+4E1fvmqtpXVKJYsviwYEx8AKKYyU3nuDi0l6xzb5Flg== integrity sha512-tnFdmCdXKS+uZGoipr69Wa0oVoFHmyoV0ydihI6q0gKQH0KutypVHAaul2qPB8t5a/mTZopC//2WdmCeX1GKVg==
dependencies: dependencies:
deep-equal "^2.0.1" deep-equal "^2.0.1"
mustache "^4.0.1" mustache "^4.0.1"
@ -6412,6 +6412,11 @@ uuid@^3.3.2:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
uuid@^8.3.1:
version "8.3.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==
validate-npm-package-license@^3.0.1: validate-npm-package-license@^3.0.1:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"

View File

@ -1,10 +1,8 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const { const {
generateAdminPermissions, BUILTIN_LEVELS,
generatePowerUserPermissions, AccessLevel,
POWERUSER_LEVEL_ID, } = require("../../utilities/security/accessLevels")
ADMIN_LEVEL_ID,
} = require("../../utilities/accessLevels")
const { const {
generateAccessLevelID, generateAccessLevelID,
getAccessLevelParams, getAccessLevelParams,
@ -19,19 +17,7 @@ exports.fetch = async function(ctx) {
) )
const customAccessLevels = body.rows.map(row => row.doc) const customAccessLevels = body.rows.map(row => row.doc)
const staticAccessLevels = [ const staticAccessLevels = [BUILTIN_LEVELS.ADMIN, BUILTIN_LEVELS.POWER]
{
_id: ADMIN_LEVEL_ID,
name: "Admin",
permissions: await generateAdminPermissions(ctx.user.appId),
},
{
_id: POWERUSER_LEVEL_ID,
name: "Power User",
permissions: await generatePowerUserPermissions(ctx.user.appId),
},
]
ctx.body = [...staticAccessLevels, ...customAccessLevels] ctx.body = [...staticAccessLevels, ...customAccessLevels]
} }
@ -40,64 +26,18 @@ exports.find = async function(ctx) {
ctx.body = await db.get(ctx.params.levelId) ctx.body = await db.get(ctx.params.levelId)
} }
exports.update = async function(ctx) { exports.save = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const level = await db.get(ctx.params.levelId)
level.name = ctx.body.name
level.permissions = ctx.request.body.permissions
const result = await db.put(level)
level._rev = result.rev
ctx.body = level
ctx.message = `Level ${level.name} updated successfully.`
}
exports.patch = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const level = await db.get(ctx.params.levelId)
const { removedPermissions, addedPermissions, _rev } = ctx.request.body
if (!_rev) throw new Error("Must supply a _rev to update an access level")
level._rev = _rev
if (removedPermissions) {
level.permissions = level.permissions.filter(
p =>
!removedPermissions.some(
rem => rem.name === p.name && rem.itemId === p.itemId
)
)
}
if (addedPermissions) {
level.permissions = [
...level.permissions.filter(
p =>
!addedPermissions.some(
add => add.name === p.name && add.itemId === p.itemId
)
),
...addedPermissions,
]
}
const result = await db.put(level)
level._rev = result.rev
ctx.body = level
ctx.message = `Access Level ${level.name} updated successfully.`
}
exports.create = async function(ctx) {
const db = new CouchDB(ctx.user.appId) const db = new CouchDB(ctx.user.appId)
const level = { let id = ctx.request.body._id || generateAccessLevelID()
name: ctx.request.body.name, const level = new AccessLevel(
_rev: ctx.request.body._rev, id,
permissions: ctx.request.body.permissions || [], ctx.request.body.name,
_id: generateAccessLevelID(), ctx.request.body.inherits
type: "accesslevel", )
if (ctx.request.body._rev) {
level._rev = ctx.request.body._rev
} }
const result = await db.put(level) const result = await db.put(level)
level._rev = result.rev level._rev = result.rev
ctx.body = level ctx.body = level

View File

@ -8,6 +8,7 @@ const fs = require("fs-extra")
const { join, resolve } = require("../../utilities/centralPath") const { join, resolve } = require("../../utilities/centralPath")
const packageJson = require("../../../package.json") const packageJson = require("../../../package.json")
const { createLinkView } = require("../../db/linkedRows") const { createLinkView } = require("../../db/linkedRows")
const { createRoutingView } = require("../../utilities/routing")
const { downloadTemplate } = require("../../utilities/templates") const { downloadTemplate } = require("../../utilities/templates")
const { const {
generateAppID, generateAppID,
@ -38,6 +39,7 @@ async function createInstance(template) {
}) })
// add view for linked rows // add view for linked rows
await createLinkView(appId) await createLinkView(appId)
await createRoutingView(appId)
// replicate the template data to the instance DB // replicate the template data to the instance DB
if (template) { if (template) {

View File

@ -34,6 +34,7 @@ exports.authenticate = async ctx => {
userId: dbUser._id, userId: dbUser._id,
accessLevelId: dbUser.accessLevelId, accessLevelId: dbUser.accessLevelId,
version: app.version, version: app.version,
permissions: dbUser.permissions || [],
} }
// if in cloud add the user api key // if in cloud add the user api key
if (env.CLOUD) { if (env.CLOUD) {

View File

@ -0,0 +1,17 @@
const { getRoutingInfo } = require("../../utilities/routing")
const { AccessController } = require("../../utilities/security/accessLevels")
async function getRoutingStructure(appId) {
let baseRouting = await getRoutingInfo(appId)
return baseRouting
}
exports.fetch = async ctx => {
ctx.body = await getRoutingStructure(ctx.appId)
}
exports.clientFetch = async ctx => {
const routing = getRoutingStructure(ctx.appId)
// use the access controller to pick which access level is applicable to this user
const accessController = new AccessController(ctx.appId)
}

View File

@ -1,20 +1,28 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const { getScreenParams, generateScreenID } = require("../../db/utils") const { getScreenParams, generateScreenID } = require("../../db/utils")
const { AccessController } = require("../../utilities/security/accessLevels")
exports.fetch = async ctx => { exports.fetch = async ctx => {
const db = new CouchDB(ctx.user.appId) const appId = ctx.user.appId
const db = new CouchDB(appId)
const screens = await db.allDocs( const screens = (
getScreenParams(null, { await db.allDocs(
include_docs: true, getScreenParams(null, {
}) include_docs: true,
})
)
).rows.map(element => element.doc)
ctx.body = await new AccessController(appId).checkScreensAccess(
screens,
ctx.user.accessLevel._id
) )
ctx.body = screens.rows.map(element => element.doc)
} }
exports.find = async ctx => { exports.find = async ctx => {
const db = new CouchDB(ctx.user.appId) const appId = ctx.user.appId
const db = new CouchDB(appId)
const screens = await db.allDocs( const screens = await db.allDocs(
getScreenParams(ctx.params.pageId, { getScreenParams(ctx.params.pageId, {
@ -22,7 +30,10 @@ exports.find = async ctx => {
}) })
) )
ctx.body = screens.response.rows ctx.body = await new AccessController(appId).checkScreensAccess(
screens,
ctx.user.accessLevel._id
)
} }
exports.save = async ctx => { exports.save = async ctx => {

View File

@ -2,9 +2,11 @@ const CouchDB = require("../../db")
const bcrypt = require("../../utilities/bcrypt") const bcrypt = require("../../utilities/bcrypt")
const { generateUserID, getUserParams } = require("../../db/utils") const { generateUserID, getUserParams } = require("../../db/utils")
const { const {
POWERUSER_LEVEL_ID, BUILTIN_LEVEL_ID_ARRAY,
ADMIN_LEVEL_ID, } = require("../../utilities/security/accessLevels")
} = require("../../utilities/accessLevels") const {
BUILTIN_PERMISSION_NAMES,
} = require("../../utilities/security/permissions")
exports.fetch = async function(ctx) { exports.fetch = async function(ctx) {
const database = new CouchDB(ctx.user.appId) const database = new CouchDB(ctx.user.appId)
@ -18,7 +20,13 @@ exports.fetch = async function(ctx) {
exports.create = async function(ctx) { exports.create = async function(ctx) {
const db = new CouchDB(ctx.user.appId) const db = new CouchDB(ctx.user.appId)
const { username, password, name, accessLevelId } = ctx.request.body const {
username,
password,
name,
accessLevelId,
permissions,
} = ctx.request.body
if (!username || !password) { if (!username || !password) {
ctx.throw(400, "Username and Password Required.") ctx.throw(400, "Username and Password Required.")
@ -35,6 +43,7 @@ exports.create = async function(ctx) {
name: name || username, name: name || username,
type: "user", type: "user",
accessLevelId, accessLevelId,
permissions: permissions || [BUILTIN_PERMISSION_NAMES.POWER],
} }
try { try {
@ -89,10 +98,7 @@ exports.find = async function(ctx) {
const checkAccessLevel = async (db, accessLevelId) => { const checkAccessLevel = async (db, accessLevelId) => {
if (!accessLevelId) return if (!accessLevelId) return
if ( if (BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevelId) !== -1) {
accessLevelId === POWERUSER_LEVEL_ID ||
accessLevelId === ADMIN_LEVEL_ID
) {
return { return {
_id: accessLevelId, _id: accessLevelId,
name: accessLevelId, name: accessLevelId,

View File

@ -5,6 +5,7 @@ const { join } = require("../../../utilities/centralPath")
const os = require("os") const os = require("os")
const exporters = require("./exporters") const exporters = require("./exporters")
const { fetchView } = require("../row") const { fetchView } = require("../row")
const { ViewNames } = require("../../../db/utils")
const controller = { const controller = {
fetch: async ctx => { fetch: async ctx => {
@ -13,8 +14,8 @@ const controller = {
const response = [] const response = []
for (let name of Object.keys(designDoc.views)) { for (let name of Object.keys(designDoc.views)) {
// Only return custom views // Only return custom views, not built ins
if (name === "by_link") { if (Object.values(ViewNames).indexOf(name) !== -1) {
continue continue
} }
response.push({ response.push({

View File

@ -4,25 +4,7 @@ const compress = require("koa-compress")
const zlib = require("zlib") const zlib = require("zlib")
const { budibaseAppsDir } = require("../utilities/budibaseDir") const { budibaseAppsDir } = require("../utilities/budibaseDir")
const { isDev } = require("../utilities") const { isDev } = require("../utilities")
const { const {mainRoutes, authRoutes, staticRoutes} = require("./routes")
authRoutes,
pageRoutes,
screenRoutes,
userRoutes,
deployRoutes,
applicationRoutes,
rowRoutes,
tableRoutes,
viewRoutes,
staticRoutes,
componentRoutes,
automationRoutes,
accesslevelRoutes,
apiKeysRoutes,
templatesRoutes,
analyticsRoutes,
webhookRoutes,
} = require("./routes")
const router = new Router() const router = new Router()
const env = require("../environment") const env = require("../environment")
@ -72,52 +54,12 @@ router.use(authRoutes.routes())
router.use(authRoutes.allowedMethods()) router.use(authRoutes.allowedMethods())
// authenticated routes // authenticated routes
router.use(viewRoutes.routes()) for (let route of mainRoutes) {
router.use(viewRoutes.allowedMethods()) router.use(route.routes())
router.use(route.allowedMethods())
router.use(tableRoutes.routes()) }
router.use(tableRoutes.allowedMethods())
router.use(rowRoutes.routes())
router.use(rowRoutes.allowedMethods())
router.use(userRoutes.routes())
router.use(userRoutes.allowedMethods())
router.use(automationRoutes.routes())
router.use(automationRoutes.allowedMethods())
router.use(webhookRoutes.routes())
router.use(webhookRoutes.allowedMethods())
router.use(deployRoutes.routes())
router.use(deployRoutes.allowedMethods())
router.use(templatesRoutes.routes())
router.use(templatesRoutes.allowedMethods())
// end auth routes
router.use(pageRoutes.routes())
router.use(pageRoutes.allowedMethods())
router.use(screenRoutes.routes())
router.use(screenRoutes.allowedMethods())
router.use(applicationRoutes.routes())
router.use(applicationRoutes.allowedMethods())
router.use(componentRoutes.routes())
router.use(componentRoutes.allowedMethods())
router.use(accesslevelRoutes.routes())
router.use(accesslevelRoutes.allowedMethods())
router.use(apiKeysRoutes.routes())
router.use(apiKeysRoutes.allowedMethods())
router.use(analyticsRoutes.routes())
router.use(analyticsRoutes.allowedMethods())
// WARNING - static routes will catch everything else after them this must be last
router.use(staticRoutes.routes()) router.use(staticRoutes.routes())
router.use(staticRoutes.allowedMethods()) router.use(staticRoutes.allowedMethods())

View File

@ -1,14 +1,18 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/accesslevel") const controller = require("../controllers/accesslevel")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions")
const router = Router() const router = Router()
router router
.post("/api/accesslevels", controller.create) .post("/api/accesslevels", authorized(BUILDER), controller.save)
.put("/api/accesslevels", controller.update) .get("/api/accesslevels", authorized(BUILDER), controller.fetch)
.get("/api/accesslevels", controller.fetch) .get("/api/accesslevels/:levelId", authorized(BUILDER), controller.find)
.get("/api/accesslevels/:levelId", controller.find) .delete(
.delete("/api/accesslevels/:levelId/:rev", controller.destroy) "/api/accesslevels/:levelId/:rev",
.patch("/api/accesslevels/:levelId", controller.patch) authorized(BUILDER),
controller.destroy
)
module.exports = router module.exports = router

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const controller = require("../controllers/analytics") const controller = require("../controllers/analytics")
const { BUILDER } = require("../../utilities/security/permissions")
const router = Router() const router = Router()

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/apikeys") const controller = require("../controllers/apikeys")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels") const { BUILDER } = require("../../utilities/security/permissions")
const router = Router() const router = Router()

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/application") const controller = require("../controllers/application")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels") const { BUILDER } = require("../../utilities/security/permissions")
const router = Router() const router = Router()

View File

@ -2,7 +2,11 @@ const Router = require("@koa/router")
const controller = require("../controllers/automation") const controller = require("../controllers/automation")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const joiValidator = require("../../middleware/joi-validator") const joiValidator = require("../../middleware/joi-validator")
const { BUILDER, EXECUTE_AUTOMATION } = require("../../utilities/accessLevels") const {
BUILDER,
PermissionLevels,
PermissionTypes,
} = require("../../utilities/security/permissions")
const Joi = require("joi") const Joi = require("joi")
const router = Router() const router = Router()
@ -75,7 +79,7 @@ router
) )
.post( .post(
"/api/automations/:id/trigger", "/api/automations/:id/trigger",
authorized(EXECUTE_AUTOMATION), authorized(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE),
controller.trigger controller.trigger
) )
.delete("/api/automations/:id/:rev", authorized(BUILDER), controller.destroy) .delete("/api/automations/:id/:rev", authorized(BUILDER), controller.destroy)

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/component") const controller = require("../controllers/component")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels") const { BUILDER } = require("../../utilities/security/permissions")
const router = Router() const router = Router()

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/deploy") const controller = require("../controllers/deploy")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels") const { BUILDER } = require("../../utilities/security/permissions")
const router = Router() const router = Router()

View File

@ -15,10 +15,10 @@ const deployRoutes = require("./deploy")
const apiKeysRoutes = require("./apikeys") const apiKeysRoutes = require("./apikeys")
const templatesRoutes = require("./templates") const templatesRoutes = require("./templates")
const analyticsRoutes = require("./analytics") const analyticsRoutes = require("./analytics")
const routingRoutes = require("./routing")
module.exports = { exports.mainRoutes = [
deployRoutes, deployRoutes,
authRoutes,
pageRoutes, pageRoutes,
screenRoutes, screenRoutes,
userRoutes, userRoutes,
@ -26,7 +26,6 @@ module.exports = {
rowRoutes, rowRoutes,
tableRoutes, tableRoutes,
viewRoutes, viewRoutes,
staticRoutes,
componentRoutes, componentRoutes,
automationRoutes, automationRoutes,
accesslevelRoutes, accesslevelRoutes,
@ -34,4 +33,8 @@ module.exports = {
templatesRoutes, templatesRoutes,
analyticsRoutes, analyticsRoutes,
webhookRoutes, webhookRoutes,
} routingRoutes,
]
exports.authRoutes = authRoutes
exports.staticRoutes = staticRoutes

View File

@ -1,6 +1,6 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels") const { BUILDER } = require("../../utilities/security/permissions")
const controller = require("../controllers/page") const controller = require("../controllers/page")
const router = Router() const router = Router()

View File

@ -0,0 +1,13 @@
const Router = require("@koa/router")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions")
const controller = require("../controllers/routing")
const router = Router()
// gets the full structure, not just the correct screen ID for your access level
router
.get("/api/routing", authorized(BUILDER), controller.fetch)
.get("/api/routing/client", controller.clientFetch)
module.exports = router

View File

@ -2,46 +2,49 @@ const Router = require("@koa/router")
const rowController = require("../controllers/row") const rowController = require("../controllers/row")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const usage = require("../../middleware/usageQuota") const usage = require("../../middleware/usageQuota")
const { READ_TABLE, WRITE_TABLE } = require("../../utilities/accessLevels") const {
PermissionLevels,
PermissionTypes,
} = require("../../utilities/security/permissions")
const router = Router() const router = Router()
router router
.get( .get(
"/api/:tableId/:rowId/enrich", "/api/:tableId/:rowId/enrich",
authorized(READ_TABLE, ctx => ctx.params.tableId), authorized(PermissionTypes.TABLE, PermissionLevels.READ),
rowController.fetchEnrichedRow rowController.fetchEnrichedRow
) )
.get( .get(
"/api/:tableId/rows", "/api/:tableId/rows",
authorized(READ_TABLE, ctx => ctx.params.tableId), authorized(PermissionTypes.TABLE, PermissionLevels.READ),
rowController.fetchTableRows rowController.fetchTableRows
) )
.get( .get(
"/api/:tableId/rows/:rowId", "/api/:tableId/rows/:rowId",
authorized(READ_TABLE, ctx => ctx.params.tableId), authorized(PermissionTypes.TABLE, PermissionLevels.READ),
rowController.find rowController.find
) )
.post("/api/rows/search", rowController.search) .post("/api/rows/search", rowController.search)
.post( .post(
"/api/:tableId/rows", "/api/:tableId/rows",
authorized(WRITE_TABLE, ctx => ctx.params.tableId), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
usage, usage,
rowController.save rowController.save
) )
.patch( .patch(
"/api/:tableId/rows/:id", "/api/:tableId/rows/:id",
authorized(WRITE_TABLE, ctx => ctx.params.tableId), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
rowController.patch rowController.patch
) )
.post( .post(
"/api/:tableId/rows/validate", "/api/:tableId/rows/validate",
authorized(WRITE_TABLE, ctx => ctx.params.tableId), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
rowController.validate rowController.validate
) )
.delete( .delete(
"/api/:tableId/rows/:rowId/:revId", "/api/:tableId/rows/:rowId/:revId",
authorized(WRITE_TABLE, ctx => ctx.params.tableId), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
usage, usage,
rowController.destroy rowController.destroy
) )

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/screen") const controller = require("../controllers/screen")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels") const { BUILDER } = require("../../utilities/security/permissions")
const joiValidator = require("../../middleware/joi-validator") const joiValidator = require("../../middleware/joi-validator")
const Joi = require("joi") const Joi = require("joi")
@ -12,17 +12,20 @@ function generateSaveValidation() {
return joiValidator.body(Joi.object({ return joiValidator.body(Joi.object({
_css: Joi.string().allow(""), _css: Joi.string().allow(""),
name: Joi.string().required(), name: Joi.string().required(),
route: Joi.string().required(), routing: Joi.object({
route: Joi.string().required(),
accessLevelId: Joi.string().required().allow(""),
}).required().unknown(true),
props: Joi.object({ props: Joi.object({
_id: Joi.string().required(), _id: Joi.string().required(),
_component: Joi.string().required(), _component: Joi.string().required(),
_children: Joi.array().required(), _children: Joi.array().required(),
_instanceName: Joi.string().required(), _instanceName: Joi.string().required(),
_styles: Joi.object().required(), _styles: Joi.object().required(),
type: Joi.string().optional(), type: Joi.string().optional(),
table: Joi.string().optional(), table: Joi.string().optional(),
}).required().unknown(true), }).required().unknown(true),
}).unknown(true)) }).unknown(true))
} }
router router

View File

@ -3,7 +3,7 @@ const controller = require("../controllers/static")
const { budibaseTempDir } = require("../../utilities/budibaseDir") const { budibaseTempDir } = require("../../utilities/budibaseDir")
const env = require("../../environment") const env = require("../../environment")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels") const { BUILDER } = require("../../utilities/security/permissions")
const usage = require("../../middleware/usageQuota") const usage = require("../../middleware/usageQuota")
const router = Router() const router = Router()

View File

@ -1,7 +1,11 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const tableController = require("../controllers/table") const tableController = require("../controllers/table")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER, READ_TABLE } = require("../../utilities/accessLevels") const {
BUILDER,
PermissionLevels,
PermissionTypes,
} = require("../../utilities/security/permissions")
const router = Router() const router = Router()
@ -9,7 +13,7 @@ router
.get("/api/tables", authorized(BUILDER), tableController.fetch) .get("/api/tables", authorized(BUILDER), tableController.fetch)
.get( .get(
"/api/tables/:id", "/api/tables/:id",
authorized(READ_TABLE, ctx => ctx.params.id), authorized(PermissionTypes.TABLE, PermissionLevels.READ),
tableController.find tableController.find
) )
.post("/api/tables", authorized(BUILDER), tableController.save) .post("/api/tables", authorized(BUILDER), tableController.save)

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/templates") const controller = require("../controllers/templates")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels") const { BUILDER } = require("../../utilities/security/permissions")
const router = Router() const router = Router()

View File

@ -6,13 +6,10 @@ const {
defaultHeaders defaultHeaders
} = require("./couchTestUtils") } = require("./couchTestUtils")
const { const {
generateAdminPermissions, BUILTIN_LEVEL_IDS,
generatePowerUserPermissions, } = require("../../../utilities/security/accessLevels")
POWERUSER_LEVEL_ID,
ADMIN_LEVEL_ID, const accessLevelBody = { name: "user", inherits: BUILTIN_LEVEL_IDS.BASIC }
READ_TABLE,
WRITE_TABLE,
} = require("../../../utilities/accessLevels")
describe("/accesslevels", () => { describe("/accesslevels", () => {
let server let server
@ -41,7 +38,7 @@ describe("/accesslevels", () => {
it("returns a success message when level is successfully created", async () => { it("returns a success message when level is successfully created", async () => {
const res = await request const res = await request
.post(`/api/accesslevels`) .post(`/api/accesslevels`)
.send({ name: "user" }) .send(accessLevelBody)
.set(defaultHeaders(appId)) .set(defaultHeaders(appId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -49,7 +46,6 @@ describe("/accesslevels", () => {
expect(res.res.statusMessage).toEqual("Access Level 'user' created successfully.") expect(res.res.statusMessage).toEqual("Access Level 'user' created successfully.")
expect(res.body._id).toBeDefined() expect(res.body._id).toBeDefined()
expect(res.body._rev).toBeDefined() expect(res.body._rev).toBeDefined()
expect(res.body.permissions).toEqual([])
}) })
}); });
@ -59,7 +55,7 @@ describe("/accesslevels", () => {
it("should list custom levels, plus 2 default levels", async () => { it("should list custom levels, plus 2 default levels", async () => {
const createRes = await request const createRes = await request
.post(`/api/accesslevels`) .post(`/api/accesslevels`)
.send({ name: "user", permissions: [ { itemId: table._id, name: READ_TABLE }] }) .send(accessLevelBody)
.set(defaultHeaders(appId)) .set(defaultHeaders(appId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -74,16 +70,17 @@ describe("/accesslevels", () => {
expect(res.body.length).toBe(3) expect(res.body.length).toBe(3)
const adminLevel = res.body.find(r => r._id === ADMIN_LEVEL_ID) const adminLevel = res.body.find(r => r._id === BUILTIN_LEVEL_IDS.ADMIN)
expect(adminLevel.inherits).toEqual(BUILTIN_LEVEL_IDS.POWER)
expect(adminLevel).toBeDefined() expect(adminLevel).toBeDefined()
expect(adminLevel.permissions).toEqual(await generateAdminPermissions(appId))
const powerUserLevel = res.body.find(r => r._id === POWERUSER_LEVEL_ID) const powerUserLevel = res.body.find(r => r._id === BUILTIN_LEVEL_IDS.POWER)
expect(powerUserLevel.inherits).toEqual(BUILTIN_LEVEL_IDS.BASIC)
expect(powerUserLevel).toBeDefined() expect(powerUserLevel).toBeDefined()
expect(powerUserLevel.permissions).toEqual(await generatePowerUserPermissions(appId))
const customLevelFetched = res.body.find(r => r._id === customLevel._id) const customLevelFetched = res.body.find(r => r._id === customLevel._id)
expect(customLevelFetched.permissions).toEqual(customLevel.permissions) expect(customLevelFetched.inherits).toEqual(BUILTIN_LEVEL_IDS.BASIC)
expect(customLevelFetched).toBeDefined()
}) })
}); });
@ -92,7 +89,7 @@ describe("/accesslevels", () => {
it("should delete custom access level", async () => { it("should delete custom access level", async () => {
const createRes = await request const createRes = await request
.post(`/api/accesslevels`) .post(`/api/accesslevels`)
.send({ name: "user", permissions: [ { itemId: table._id, name: READ_TABLE } ] }) .send({ name: "user" })
.set(defaultHeaders(appId)) .set(defaultHeaders(appId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -110,71 +107,4 @@ describe("/accesslevels", () => {
.expect(404) .expect(404)
}) })
}) })
describe("patch", () => {
it("should add given permissions", async () => {
const createRes = await request
.post(`/api/accesslevels`)
.send({ name: "user", permissions: [ { itemId: table._id, name: READ_TABLE }] })
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
const customLevel = createRes.body
await request
.patch(`/api/accesslevels/${customLevel._id}`)
.send({
_rev: customLevel._rev,
addedPermissions: [ { itemId: table._id, name: WRITE_TABLE } ]
})
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
const finalRes = await request
.get(`/api/accesslevels/${customLevel._id}`)
.set(defaultHeaders(appId))
.expect(200)
expect(finalRes.body.permissions.length).toBe(2)
expect(finalRes.body.permissions.some(p => p.name === WRITE_TABLE)).toBe(true)
expect(finalRes.body.permissions.some(p => p.name === READ_TABLE)).toBe(true)
})
it("should remove given permissions", async () => {
const createRes = await request
.post(`/api/accesslevels`)
.send({
name: "user",
permissions: [
{ itemId: table._id, name: READ_TABLE },
{ itemId: table._id, name: WRITE_TABLE },
]
})
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
const customLevel = createRes.body
await request
.patch(`/api/accesslevels/${customLevel._id}`)
.send({
_rev: customLevel._rev,
removedPermissions: [ { itemId: table._id, name: WRITE_TABLE }]
})
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
const finalRes = await request
.get(`/api/accesslevels/${customLevel._id}`)
.set(defaultHeaders(appId))
.expect(200)
expect(finalRes.body.permissions.length).toBe(1)
expect(finalRes.body.permissions.some(p => p.name === READ_TABLE)).toBe(true)
})
})
}); });

View File

@ -1,11 +1,11 @@
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
const supertest = require("supertest") const supertest = require("supertest")
const { const {
POWERUSER_LEVEL_ID, BUILTIN_LEVEL_IDS,
ANON_LEVEL_ID, } = require("../../../utilities/security/accessLevels")
BUILDER_LEVEL_ID, const {
generateAdminPermissions, BUILTIN_PERMISSION_NAMES,
} = require("../../../utilities/accessLevels") } = require("../../../utilities/security/permissions")
const packageJson = require("../../../../package") const packageJson = require("../../../../package")
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const env = require("../../../environment") const env = require("../../../environment")
@ -26,7 +26,7 @@ exports.supertest = async () => {
exports.defaultHeaders = appId => { exports.defaultHeaders = appId => {
const builderUser = { const builderUser = {
userId: "BUILDER", userId: "BUILDER",
accessLevelId: BUILDER_LEVEL_ID, accessLevelId: BUILTIN_LEVEL_IDS.BUILDER,
} }
const builderToken = jwt.sign(builderUser, env.JWT_SECRET) const builderToken = jwt.sign(builderUser, env.JWT_SECRET)
@ -126,21 +126,13 @@ exports.createUser = async (
name: "Bill", name: "Bill",
username, username,
password, password,
accessLevelId: POWERUSER_LEVEL_ID, accessLevelId: BUILTIN_LEVEL_IDS.POWER,
}) })
return res.body return res.body
} }
const createUserWithOnePermission = async ( const createUserWithOnePermission = async (request, appId, permName) => {
request, let permissions = [permName]
appId,
permName,
itemId
) => {
let permissions = await generateAdminPermissions(appId)
permissions = permissions.filter(
p => p.name === permName && p.itemId === itemId
)
return await createUserWithPermissions( return await createUserWithPermissions(
request, request,
@ -151,7 +143,7 @@ const createUserWithOnePermission = async (
} }
const createUserWithAdminPermissions = async (request, appId) => { const createUserWithAdminPermissions = async (request, appId) => {
let permissions = await generateAdminPermissions(appId) let permissions = [BUILTIN_PERMISSION_NAMES.ADMIN]
return await createUserWithPermissions( return await createUserWithPermissions(
request, request,
@ -164,13 +156,9 @@ const createUserWithAdminPermissions = async (request, appId) => {
const createUserWithAllPermissionExceptOne = async ( const createUserWithAllPermissionExceptOne = async (
request, request,
appId, appId,
permName, permName
itemId
) => { ) => {
let permissions = await generateAdminPermissions(appId) let permissions = [permName]
permissions = permissions.filter(
p => !(p.name === permName && p.itemId === itemId)
)
return await createUserWithPermissions( return await createUserWithPermissions(
request, request,
@ -186,11 +174,6 @@ const createUserWithPermissions = async (
permissions, permissions,
username username
) => { ) => {
const accessRes = await request
.post(`/api/accesslevels`)
.send({ name: "TestLevel", permissions })
.set(exports.defaultHeaders(appId))
const password = `password_${username}` const password = `password_${username}`
await request await request
.post(`/api/users`) .post(`/api/users`)
@ -199,12 +182,13 @@ const createUserWithPermissions = async (
name: username, name: username,
username, username,
password, password,
accessLevelId: accessRes.body._id, accessLevelId: BUILTIN_LEVEL_IDS.POWER,
permissions,
}) })
const anonUser = { const anonUser = {
userId: "ANON", userId: "ANON",
accessLevelId: ANON_LEVEL_ID, accessLevelId: BUILTIN_LEVEL_IDS.ANON,
appId: appId, appId: appId,
version: packageJson.version, version: packageJson.version,
} }
@ -232,15 +216,10 @@ exports.testPermissionsForEndpoint = async ({
url, url,
body, body,
appId, appId,
permissionName, permName1,
itemId, permName2,
}) => { }) => {
const headers = await createUserWithOnePermission( const headers = await createUserWithOnePermission(request, appId, permName1)
request,
appId,
permissionName,
itemId
)
await createRequest(request, method, url, body) await createRequest(request, method, url, body)
.set(headers) .set(headers)
@ -249,8 +228,7 @@ exports.testPermissionsForEndpoint = async ({
const noPermsHeaders = await createUserWithAllPermissionExceptOne( const noPermsHeaders = await createUserWithAllPermissionExceptOne(
request, request,
appId, appId,
permissionName, permName2
itemId
) )
await createRequest(request, method, url, body) await createRequest(request, method, url, body)

View File

@ -5,11 +5,12 @@ const {
createUser, createUser,
testPermissionsForEndpoint, testPermissionsForEndpoint,
} = require("./couchTestUtils") } = require("./couchTestUtils")
const { const {
POWERUSER_LEVEL_ID, BUILTIN_PERMISSION_NAMES,
LIST_USERS, } = require("../../../utilities/security/permissions")
USER_MANAGEMENT const {
} = require("../../../utilities/accessLevels") BUILTIN_LEVEL_IDS,
} = require("../../../utilities/security/accessLevels")
describe("/users", () => { describe("/users", () => {
let request let request
@ -53,7 +54,8 @@ describe("/users", () => {
method: "GET", method: "GET",
url: `/api/users`, url: `/api/users`,
appId: appId, appId: appId,
permissionName: LIST_USERS, permName1: BUILTIN_PERMISSION_NAMES.POWER,
permName2: BUILTIN_PERMISSION_NAMES.WRITE,
}) })
}) })
@ -65,7 +67,7 @@ describe("/users", () => {
const res = await request const res = await request
.post(`/api/users`) .post(`/api/users`)
.set(defaultHeaders(appId)) .set(defaultHeaders(appId))
.send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: POWERUSER_LEVEL_ID }) .send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: BUILTIN_LEVEL_IDS.POWER })
.expect(200) .expect(200)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -77,10 +79,11 @@ describe("/users", () => {
await testPermissionsForEndpoint({ await testPermissionsForEndpoint({
request, request,
method: "POST", method: "POST",
body: { name: "brandNewUser", username: "brandNewUser", password: "yeeooo", accessLevelId: POWERUSER_LEVEL_ID }, body: { name: "brandNewUser", username: "brandNewUser", password: "yeeooo", accessLevelId: BUILTIN_LEVEL_IDS.POWER },
url: `/api/users`, url: `/api/users`,
appId: appId, appId: appId,
permissionName: USER_MANAGEMENT, permName1: BUILTIN_PERMISSION_NAMES.ADMIN,
permName2: BUILTIN_PERMISSION_NAMES.POWER,
}) })
}) })

View File

@ -1,19 +1,39 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/user") const controller = require("../controllers/user")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { USER_MANAGEMENT, LIST_USERS } = require("../../utilities/accessLevels") const {
PermissionLevels,
PermissionTypes,
} = require("../../utilities/security/permissions")
const usage = require("../../middleware/usageQuota") const usage = require("../../middleware/usageQuota")
const router = Router() const router = Router()
router router
.get("/api/users", authorized(LIST_USERS), controller.fetch) .get(
.get("/api/users/:username", authorized(USER_MANAGEMENT), controller.find) "/api/users",
.put("/api/users/", authorized(USER_MANAGEMENT), controller.update) authorized(PermissionTypes.USER, PermissionLevels.READ),
.post("/api/users", authorized(USER_MANAGEMENT), usage, controller.create) controller.fetch
)
.get(
"/api/users/:username",
authorized(PermissionTypes.USER, PermissionLevels.READ),
controller.find
)
.put(
"/api/users/",
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
controller.update
)
.post(
"/api/users",
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
usage,
controller.create
)
.delete( .delete(
"/api/users/:username", "/api/users/:username",
authorized(USER_MANAGEMENT), authorized(PermissionTypes.USER, PermissionLevels.WRITE),
usage, usage,
controller.destroy controller.destroy
) )

View File

@ -2,7 +2,11 @@ const Router = require("@koa/router")
const viewController = require("../controllers/view") const viewController = require("../controllers/view")
const rowController = require("../controllers/row") const rowController = require("../controllers/row")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER, READ_VIEW } = require("../../utilities/accessLevels") const {
BUILDER,
PermissionTypes,
PermissionLevels,
} = require("../../utilities/security/permissions")
const usage = require("../../middleware/usageQuota") const usage = require("../../middleware/usageQuota")
const router = Router() const router = Router()
@ -10,7 +14,7 @@ const router = Router()
router router
.get( .get(
"/api/views/:viewName", "/api/views/:viewName",
authorized(READ_VIEW, ctx => ctx.params.viewName), authorized(PermissionTypes.VIEW, PermissionLevels.READ),
rowController.fetchView rowController.fetchView
) )
.get("/api/views", authorized(BUILDER), viewController.fetch) .get("/api/views", authorized(BUILDER), viewController.fetch)

View File

@ -2,7 +2,11 @@ const Router = require("@koa/router")
const controller = require("../controllers/webhook") const controller = require("../controllers/webhook")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const joiValidator = require("../../middleware/joi-validator") const joiValidator = require("../../middleware/joi-validator")
const { BUILDER, EXECUTE_WEBHOOK } = require("../../utilities/accessLevels") const {
BUILDER,
PermissionTypes,
PermissionLevels,
} = require("../../utilities/security/permissions")
const Joi = require("joi") const Joi = require("joi")
const router = Router() const router = Router()
@ -38,7 +42,7 @@ router
) )
.post( .post(
"/api/webhooks/trigger/:instance/:id", "/api/webhooks/trigger/:instance/:id",
authorized(EXECUTE_WEBHOOK), authorized(PermissionTypes.WEBHOOK, PermissionLevels.EXECUTE),
controller.trigger controller.trigger
) )

View File

@ -1,4 +1,4 @@
const accessLevels = require("../../utilities/accessLevels") const accessLevels = require("../../utilities/security/accessLevels")
const userController = require("../../api/controllers/user") const userController = require("../../api/controllers/user")
const env = require("../../environment") const env = require("../../environment")
const usage = require("../../utilities/usageQuota") const usage = require("../../utilities/usageQuota")
@ -11,7 +11,7 @@ module.exports.definition = {
type: "ACTION", type: "ACTION",
stepId: "CREATE_USER", stepId: "CREATE_USER",
inputs: { inputs: {
accessLevelId: accessLevels.POWERUSER_LEVEL_ID, accessLevelId: accessLevels.BUILTIN_LEVEL_IDS.POWER,
}, },
schema: { schema: {
inputs: { inputs: {
@ -28,8 +28,8 @@ module.exports.definition = {
accessLevelId: { accessLevelId: {
type: "string", type: "string",
title: "Access Level", title: "Access Level",
enum: accessLevels.ACCESS_LEVELS, enum: accessLevels.BUILTIN_LEVEL_IDS,
pretty: Object.values(accessLevels.PRETTY_ACCESS_LEVELS), pretty: accessLevels.BUILTIN_LEVEL_NAME_ARRAY,
}, },
}, },
required: ["username", "password", "accessLevelId"], required: ["username", "password", "accessLevelId"],

View File

@ -1,3 +1,5 @@
const { BUILTIN_LEVEL_IDS } = require("../utilities/security/accessLevels")
exports.HOME_SCREEN = { exports.HOME_SCREEN = {
description: "", description: "",
url: "", url: "",
@ -98,6 +100,9 @@ exports.HOME_SCREEN = {
], ],
_instanceName: "Home", _instanceName: "Home",
}, },
route: "/", routing: {
route: "/",
accessLevelId: BUILTIN_LEVEL_IDS.BASIC,
},
name: "d834fea2-1b3e-4320-ab34-f9009f5ecc59", name: "d834fea2-1b3e-4320-ab34-f9009f5ecc59",
} }

View File

@ -1,5 +1,6 @@
const CouchDB = require("../index") const CouchDB = require("../index")
const Sentry = require("@sentry/node") const Sentry = require("@sentry/node")
const { ViewNames, getQueryIndex } = require("../utils")
/** /**
* Only needed so that boolean parameters are being used for includeDocs * Only needed so that boolean parameters are being used for includeDocs
@ -40,7 +41,7 @@ exports.createLinkView = async appId => {
} }
designDoc.views = { designDoc.views = {
...designDoc.views, ...designDoc.views,
by_link: view, [ViewNames.LINK]: view,
} }
await db.put(designDoc) await db.put(designDoc)
} }
@ -76,7 +77,7 @@ exports.getLinkDocuments = async function({
} }
params.include_docs = !!includeDocs params.include_docs = !!includeDocs
try { try {
const response = await db.query("database/by_link", params) const response = await db.query(getQueryIndex(ViewNames.LINK), params)
if (includeDocs) { if (includeDocs) {
return response.rows.map(row => row.doc) return response.rows.map(row => row.doc)
} else { } else {

View File

@ -17,10 +17,20 @@ const DocumentTypes = {
SCREEN: "screen", SCREEN: "screen",
} }
const ViewNames = {
LINK: "by_link",
ROUTING: "screen_routes",
}
exports.ViewNames = ViewNames
exports.DocumentTypes = DocumentTypes exports.DocumentTypes = DocumentTypes
exports.SEPARATOR = SEPARATOR exports.SEPARATOR = SEPARATOR
exports.UNICODE_MAX = UNICODE_MAX exports.UNICODE_MAX = UNICODE_MAX
exports.getQueryIndex = viewName => {
return `database/${viewName}`
}
/** /**
* If creating DB allDocs/query params with only a single top level ID this can be used, this * If creating DB allDocs/query params with only a single top level ID this can be used, this
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on. * is usually the case as most of our docs are top level e.g. tables, automations, users and so on.

View File

@ -1,12 +1,7 @@
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const STATUS_CODES = require("../utilities/statusCodes") const STATUS_CODES = require("../utilities/statusCodes")
const accessLevelController = require("../api/controllers/accesslevel") const accessLevelController = require("../api/controllers/accesslevel")
const { const { BUILTIN_LEVEL_ID_ARRAY } = require("../utilities/security/accessLevels")
ADMIN_LEVEL_ID,
POWERUSER_LEVEL_ID,
BUILDER_LEVEL_ID,
ANON_LEVEL_ID,
} = require("../utilities/accessLevels")
const env = require("../environment") const env = require("../environment")
const { AuthTypes } = require("../constants") const { AuthTypes } = require("../constants")
const { getAppId, getCookieName, setCookie } = require("../utilities") const { getAppId, getCookieName, setCookie } = require("../utilities")
@ -74,12 +69,7 @@ module.exports = async (ctx, next) => {
* @param {*} accessLevelId - the id of the users access level * @param {*} accessLevelId - the id of the users access level
*/ */
const getAccessLevel = async (appId, accessLevelId) => { const getAccessLevel = async (appId, accessLevelId) => {
if ( if (BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevelId) !== -1) {
accessLevelId === POWERUSER_LEVEL_ID ||
accessLevelId === ADMIN_LEVEL_ID ||
accessLevelId === BUILDER_LEVEL_ID ||
accessLevelId === ANON_LEVEL_ID
) {
return { return {
_id: accessLevelId, _id: accessLevelId,
name: accessLevelId, name: accessLevelId,

View File

@ -1,17 +1,17 @@
const { BUILTIN_LEVEL_IDS } = require("../utilities/security/accessLevels")
const { const {
adminPermissions, PermissionTypes,
ADMIN_LEVEL_ID, doesHavePermission,
POWERUSER_LEVEL_ID, } = require("../utilities/security/permissions")
BUILDER_LEVEL_ID,
BUILDER,
} = require("../utilities/accessLevels")
const env = require("../environment") const env = require("../environment")
const { apiKeyTable } = require("../db/dynamoClient") const { apiKeyTable } = require("../db/dynamoClient")
const { AuthTypes } = require("../constants") const { AuthTypes } = require("../constants")
const ADMIN_ACCESS = [BUILTIN_LEVEL_IDS.ADMIN, BUILTIN_LEVEL_IDS.BUILDER]
const LOCAL_PASS = new RegExp(["webhooks/trigger", "webhooks/schema"].join("|")) const LOCAL_PASS = new RegExp(["webhooks/trigger", "webhooks/schema"].join("|"))
module.exports = (permName, getItemId) => async (ctx, next) => { module.exports = (permType, permLevel = null) => async (ctx, next) => {
// webhooks can pass locally // webhooks can pass locally
if (!env.CLOUD && LOCAL_PASS.test(ctx.request.url)) { if (!env.CLOUD && LOCAL_PASS.test(ctx.request.url)) {
return next() return next()
@ -37,7 +37,7 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
} }
// don't expose builder endpoints in the cloud // don't expose builder endpoints in the cloud
if (env.CLOUD && permName === BUILDER) return if (env.CLOUD && permType === PermissionTypes.BUILDER) return
if (!ctx.auth.authenticated) { if (!ctx.auth.authenticated) {
ctx.throw(403, "Session not authenticated") ctx.throw(403, "Session not authenticated")
@ -47,41 +47,19 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
ctx.throw(403, "User not found") ctx.throw(403, "User not found")
} }
if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) { const accessLevel = ctx.user.accessLevel
const permissions = ctx.user.permissions
if (ADMIN_ACCESS.indexOf(accessLevel._id) !== -1) {
return next() return next()
} }
if (ctx.user.accessLevel._id === BUILDER_LEVEL_ID) { if (permType === PermissionTypes.BUILDER) {
return next()
}
if (permName === BUILDER) {
ctx.throw(403, "Not Authorized") ctx.throw(403, "Not Authorized")
return
} }
const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "") if (!doesHavePermission(permType, permLevel, permissions)) {
ctx.throw(403, "User does not have permission")
const thisPermissionId = permissionId({
name: permName,
itemId: getItemId && getItemId(ctx),
})
// power user has everything, except the admin specific perms
if (
ctx.user.accessLevel._id === POWERUSER_LEVEL_ID &&
!adminPermissions.map(permissionId).includes(thisPermissionId)
) {
return next()
} }
if ( return next()
ctx.user.accessLevel.permissions
.map(permissionId)
.includes(thisPermissionId)
) {
return next()
}
ctx.throw(403, "Not Authorized")
} }

View File

@ -1,36 +0,0 @@
// Permissions
module.exports.READ_TABLE = "read-table"
module.exports.WRITE_TABLE = "write-table"
module.exports.READ_VIEW = "read-view"
module.exports.EXECUTE_AUTOMATION = "execute-automation"
module.exports.EXECUTE_WEBHOOK = "execute-webhook"
module.exports.USER_MANAGEMENT = "user-management"
module.exports.BUILDER = "builder"
module.exports.LIST_USERS = "list-users"
// Access Level IDs
module.exports.ADMIN_LEVEL_ID = "ADMIN"
module.exports.POWERUSER_LEVEL_ID = "POWER_USER"
module.exports.BUILDER_LEVEL_ID = "BUILDER"
module.exports.ANON_LEVEL_ID = "ANON"
module.exports.ACCESS_LEVELS = [
module.exports.ADMIN_LEVEL_ID,
module.exports.POWERUSER_LEVEL_ID,
module.exports.BUILDER_LEVEL_ID,
module.exports.ANON_LEVEL_ID,
]
module.exports.PRETTY_ACCESS_LEVELS = {
[module.exports.ADMIN_LEVEL_ID]: "Admin",
[module.exports.POWERUSER_LEVEL_ID]: "Power user",
[module.exports.BUILDER_LEVEL_ID]: "Builder",
}
module.exports.adminPermissions = [
{
name: module.exports.USER_MANAGEMENT,
},
]
// to avoid circular dependencies this is included later, after exporting all enums
const permissions = require("./permissions")
module.exports.generateAdminPermissions = permissions.generateAdminPermissions
module.exports.generatePowerUserPermissions =
permissions.generatePowerUserPermissions

View File

@ -1,4 +1,5 @@
const { BUILDER_LEVEL_ID } = require("../accessLevels") const { BUILTIN_LEVEL_IDS } = require("../security/accessLevels")
const { BUILTIN_PERMISSION_NAMES } = require("../security/permissions")
const env = require("../../environment") const env = require("../../environment")
const CouchDB = require("../../db") const CouchDB = require("../../db")
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
@ -9,7 +10,8 @@ const APP_PREFIX = DocumentTypes.APP + SEPARATOR
module.exports = async (ctx, appId, version) => { module.exports = async (ctx, appId, version) => {
const builderUser = { const builderUser = {
userId: "BUILDER", userId: "BUILDER",
accessLevelId: BUILDER_LEVEL_ID, accessLevelId: BUILTIN_LEVEL_IDS.BUILDER,
permissions: [BUILTIN_PERMISSION_NAMES.ADMIN],
version, version,
} }
if (env.BUDIBASE_API_KEY) { if (env.BUDIBASE_API_KEY) {

View File

@ -1,66 +0,0 @@
const viewController = require("../api/controllers/view")
const tableController = require("../api/controllers/table")
const automationController = require("../api/controllers/automation")
const accessLevels = require("./accessLevels")
// this has been broken out to reduce risk of circular dependency from utilities, no enums defined here
const generateAdminPermissions = async appId => [
...accessLevels.adminPermissions,
...(await generatePowerUserPermissions(appId)),
]
const generatePowerUserPermissions = async appId => {
const fetchTablesCtx = {
user: {
appId,
},
}
await tableController.fetch(fetchTablesCtx)
const tables = fetchTablesCtx.body
const fetchViewsCtx = {
user: {
appId,
},
}
await viewController.fetch(fetchViewsCtx)
const views = fetchViewsCtx.body
const fetchAutomationsCtx = {
user: {
appId,
},
}
await automationController.fetch(fetchAutomationsCtx)
const automations = fetchAutomationsCtx.body
const readTablePermissions = tables.map(m => ({
itemId: m._id,
name: accessLevels.READ_TABLE,
}))
const writeTablePermissions = tables.map(m => ({
itemId: m._id,
name: accessLevels.WRITE_TABLE,
}))
const viewPermissions = views.map(v => ({
itemId: v.name,
name: accessLevels.READ_VIEW,
}))
const executeAutomationPermissions = automations.map(w => ({
itemId: w._id,
name: accessLevels.EXECUTE_AUTOMATION,
}))
return [
...readTablePermissions,
...writeTablePermissions,
...viewPermissions,
...executeAutomationPermissions,
{ name: accessLevels.LIST_USERS },
]
}
module.exports.generateAdminPermissions = generateAdminPermissions
module.exports.generatePowerUserPermissions = generatePowerUserPermissions

View File

@ -0,0 +1,24 @@
const CouchDB = require("../../db")
const { createRoutingView } = require("./routingUtils")
const { ViewNames, getQueryIndex, UNICODE_MAX } = require("../../db/utils")
exports.getRoutingInfo = async appId => {
const db = new CouchDB(appId)
try {
const allRouting = await db.query(getQueryIndex(ViewNames.ROUTING), {
startKey: "",
endKey: UNICODE_MAX,
})
return allRouting.rows.map(row => row.value)
} catch (err) {
// check if the view doesn't exist, it should for all new instances
if (err != null && err.name === "not_found") {
await createRoutingView(appId)
return exports.getRoutingInfo(appId)
} else {
throw err
}
}
}
exports.createRoutingView = createRoutingView

View File

@ -0,0 +1,24 @@
const CouchDB = require("../../db")
const { DocumentTypes, SEPARATOR, ViewNames } = require("../../db/utils")
const SCREEN_PREFIX = DocumentTypes.SCREEN + SEPARATOR
exports.createRoutingView = async appId => {
const db = new CouchDB(appId)
const designDoc = await db.get("_design/database")
const view = {
// if using variables in a map function need to inject them before use
map: `function(doc) {
if (doc._id.startsWith("${SCREEN_PREFIX}")) {
emit(doc._id, {
id: doc._id,
routing: doc.routing,
})
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewNames.ROUTING]: view,
}
await db.put(designDoc)
}

View File

@ -0,0 +1,112 @@
const CouchDB = require("../../db")
const BUILTIN_IDS = {
ADMIN: "ADMIN",
POWER: "POWER_USER",
BASIC: "BASIC",
ANON: "ANON",
BUILDER: "BUILDER",
}
function AccessLevel(id, name, inherits = null) {
this._id = id
this.name = name
if (inherits) {
this.inherits = inherits
}
}
exports.BUILTIN_LEVELS = {
ADMIN: new AccessLevel(BUILTIN_IDS.ADMIN, "Admin", BUILTIN_IDS.POWER),
POWER: new AccessLevel(BUILTIN_IDS.POWER, "Admin", BUILTIN_IDS.BASIC),
BASIC: new AccessLevel(BUILTIN_IDS.BASIC, "Basic", BUILTIN_IDS.ANON),
ANON: new AccessLevel(BUILTIN_IDS.ANON, "Anonymous"),
BUILDER: new AccessLevel(BUILTIN_IDS.BUILDER, "Builder"),
}
exports.BUILTIN_LEVEL_ID_ARRAY = Object.values(exports.BUILTIN_LEVELS).map(
level => level._id
)
exports.BUILTIN_LEVEL_NAME_ARRAY = Object.values(exports.BUILTIN_LEVELS).map(
level => level.name
)
function isBuiltin(accessLevel) {
return exports.BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevel) !== -1
}
class AccessController {
constructor(appId) {
this.appId = appId
this.accessLevels = {}
}
async getAccessLevel(accessLevelId) {
if (this.accessLevels[accessLevelId]) {
return this.accessLevels[accessLevelId]
}
let accessLevel
if (isBuiltin(accessLevelId)) {
accessLevel = Object.values(exports.BUILTIN_LEVELS).find(
level => level._id === accessLevelId
)
} else {
const db = new CouchDB(this.appId)
accessLevel = await db.get(accessLevelId)
}
this.accessLevels[accessLevelId] = accessLevel
return accessLevel
}
async hasAccess(tryingAccessLevelId, userAccessLevelId) {
// special cases, the screen has no access level, the access levels are the same or the user
// is currently in the builder
if (
tryingAccessLevelId == null ||
tryingAccessLevelId === "" ||
tryingAccessLevelId === userAccessLevelId ||
userAccessLevelId === BUILTIN_IDS.BUILDER
) {
return true
}
let userAccess = await this.getAccessLevel(userAccessLevelId)
// check if inherited makes it possible
while (userAccess.inherits) {
if (tryingAccessLevelId === userAccess.inherits) {
return true
}
// go to get the inherited incase it inherits anything
userAccess = await this.getAccessLevel(userAccess.inherits)
}
return false
}
async checkScreensAccess(screens, userAccessLevelId) {
let accessibleScreens = []
// don't want to handle this with Promise.all as this would mean all custom access levels would be
// retrieved at same time, it is likely a custom levels will be re-used and therefore want
// to work in sync for performance save
for (let screen of screens) {
const accessible = await this.checkScreenAccess(screen, userAccessLevelId)
if (accessible) {
accessibleScreens.push(accessible)
}
}
return accessibleScreens
}
async checkScreenAccess(screen, userAccessLevelId) {
const accessLevelId =
screen && screen.routing ? screen.routing.accessLevelId : null
if (await this.hasAccess(accessLevelId, userAccessLevelId)) {
return screen
}
return null
}
}
exports.AccessController = AccessController
exports.BUILTIN_LEVEL_IDS = BUILTIN_IDS
exports.isBuiltin = isBuiltin
exports.AccessLevel = AccessLevel

View File

@ -0,0 +1,113 @@
const { flatten } = require("lodash")
const PermissionLevels = {
READ: "read",
WRITE: "write",
EXECUTE: "execute",
ADMIN: "admin",
}
const PermissionTypes = {
TABLE: "table",
USER: "user",
AUTOMATION: "automation",
WEBHOOK: "webhook",
BUILDER: "builder",
VIEW: "view",
}
function Permission(type, level) {
this.level = level
this.type = type
}
/**
* Given the specified permission level for the user return the levels they are allowed to carry out.
* @param {string} userPermLevel The permission level of the user.
* @return {string[]} All the permission levels this user is allowed to carry out.
*/
function getAllowedLevels(userPermLevel) {
switch (userPermLevel) {
case PermissionLevels.READ:
return [PermissionLevels.READ]
case PermissionLevels.WRITE:
return [PermissionLevels.READ, PermissionLevels.WRITE]
case PermissionLevels.EXECUTE:
return [PermissionLevels.EXECUTE]
case PermissionLevels.ADMIN:
return [
PermissionLevels.READ,
PermissionLevels.WRITE,
PermissionLevels.EXECUTE,
]
default:
return []
}
}
exports.BUILTIN_PERMISSION_NAMES = {
READ_ONLY: "read_only",
WRITE: "write",
ADMIN: "admin",
POWER: "power",
}
exports.BUILTIN_PERMISSIONS = {
READ_ONLY: {
name: exports.BUILTIN_PERMISSION_NAMES.READ_ONLY,
permissions: [
new Permission(PermissionTypes.TABLE, PermissionLevels.READ),
new Permission(PermissionTypes.VIEW, PermissionLevels.READ),
],
},
WRITE: {
name: exports.BUILTIN_PERMISSION_NAMES.WRITE,
permissions: [
new Permission(PermissionTypes.TABLE, PermissionLevels.WRITE),
new Permission(PermissionTypes.VIEW, PermissionLevels.READ),
],
},
POWER: {
name: exports.BUILTIN_PERMISSION_NAMES.POWER,
permissions: [
new Permission(PermissionTypes.TABLE, PermissionLevels.WRITE),
new Permission(PermissionTypes.USER, PermissionLevels.READ),
new Permission(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE),
new Permission(PermissionTypes.VIEW, PermissionLevels.READ),
new Permission(PermissionTypes.WEBHOOK, PermissionLevels.READ),
],
},
ADMIN: {
name: exports.BUILTIN_PERMISSION_NAMES.ADMIN,
permissions: [
new Permission(PermissionTypes.TABLE, PermissionLevels.ADMIN),
new Permission(PermissionTypes.USER, PermissionLevels.ADMIN),
new Permission(PermissionTypes.AUTOMATION, PermissionLevels.ADMIN),
new Permission(PermissionTypes.VIEW, PermissionLevels.ADMIN),
new Permission(PermissionTypes.WEBHOOK, PermissionLevels.READ),
],
},
}
exports.doesHavePermission = (permType, permLevel, userPermissionNames) => {
const builtins = Object.values(exports.BUILTIN_PERMISSIONS)
let permissions = flatten(
builtins
.filter(builtin => userPermissionNames.indexOf(builtin.name) !== -1)
.map(builtin => builtin.permissions)
)
for (let permission of permissions) {
if (
permission.type === permType &&
getAllowedLevels(permission.level).indexOf(permLevel) !== -1
) {
return true
}
}
return false
}
// utility as a lot of things need simply the builder permission
exports.BUILDER = PermissionTypes.BUILDER
exports.PermissionTypes = PermissionTypes
exports.PermissionLevels = PermissionLevels

View File

@ -200,10 +200,10 @@
lodash "^4.17.19" lodash "^4.17.19"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@budibase/client@^0.3.7": "@budibase/client@^0.3.8":
version "0.3.7" version "0.3.8"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.3.7.tgz#8ed2d40d91ba3788a69ee5db5078f757adb4187f" resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.3.8.tgz#75df7e97e8f0d9b58c00e2bb0d3b4a55f8d04735"
integrity sha512-EgpHfw/WOUYeCG4cILDbaN2WFBDSPS698Z+So7FP5l+4E1fvmqtpXVKJYsviwYEx8AKKYyU3nuDi0l6xzb5Flg== integrity sha512-tnFdmCdXKS+uZGoipr69Wa0oVoFHmyoV0ydihI6q0gKQH0KutypVHAaul2qPB8t5a/mTZopC//2WdmCeX1GKVg==
dependencies: dependencies:
deep-equal "^2.0.1" deep-equal "^2.0.1"
mustache "^4.0.1" mustache "^4.0.1"