diff --git a/packages/builder/package.json b/packages/builder/package.json index 68ff7f79d0..4254e3c7f6 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -81,7 +81,8 @@ "shortid": "^2.2.15", "svelte-loading-spinners": "^0.1.1", "svelte-portal": "^0.1.0", - "yup": "^0.29.2" + "yup": "^0.29.2", + "uuid": "^8.3.1" }, "devDependencies": { "@babel/core": "^7.5.5", diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 31b8dcff22..2c1def6efb 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -50,6 +50,7 @@ export const getFrontendStore = () => { return state }) 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 => screen._id.includes(pkg.pages.main._id) diff --git a/packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js b/packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js index a8ab27df3d..b25562758e 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js @@ -1,22 +1,13 @@ +import { Screen } from "./utils/Screen" + export default { name: `Create from scratch`, create: () => createScreen(), } -const createScreen = () => ({ - props: { - _id: "", - _component: "@budibase/standard-components/container", - _styles: { - normal: {}, - hover: {}, - active: {}, - selected: {}, - }, - type: "div", - _children: [], - _instanceName: "", - }, - route: "", - name: "screen-id", -}) +const createScreen = () => { + return new Screen() + .mainType("div") + .component("@budibase/standard-components/container") + .json() +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/emptyNewRowScreen.js b/packages/builder/src/builderStore/store/screenTemplates/emptyNewRowScreen.js index e58319688b..a2f2f6df67 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/emptyNewRowScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/emptyNewRowScreen.js @@ -1,22 +1,13 @@ +import { Screen } from "./utils/Screen" + export default { name: `New Row (Empty)`, create: () => createScreen(), } -const createScreen = () => ({ - props: { - _id: "", - _component: "@budibase/standard-components/newrow", - _styles: { - normal: {}, - hover: {}, - active: {}, - selected: {}, - }, - _children: [], - _instanceName: "", - table: "", - }, - route: "", - name: "screen-id", -}) +const createScreen = () => { + return new Screen() + .component("@budibase/standard-components/newrow") + .table("") + .json() +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/emptyRowDetailScreen.js b/packages/builder/src/builderStore/store/screenTemplates/emptyRowDetailScreen.js index a75de583cb..5dbdcf4e69 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/emptyRowDetailScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/emptyRowDetailScreen.js @@ -1,22 +1,13 @@ +import { Screen } from "./utils/Screen" + export default { name: `Row Detail (Empty)`, create: () => createScreen(), } -const createScreen = () => ({ - props: { - _id: "", - _component: "@budibase/standard-components/rowdetail", - _styles: { - normal: {}, - hover: {}, - active: {}, - selected: {}, - }, - _children: [], - _instanceName: "", - table: "", - }, - route: "", - name: "screen-id", -}) +const createScreen = () => { + return new Screen() + .component("@budibase/standard-components/rowdetail") + .table("") + .json() +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/index.js b/packages/builder/src/builderStore/store/screenTemplates/index.js index 5abe428966..ddf48cbe44 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/index.js +++ b/packages/builder/src/builderStore/store/screenTemplates/index.js @@ -24,7 +24,7 @@ const createTemplateOverride = (frontendState, create) => () => { } screen.props._id = uuid() screen.name = screen.props._id - screen.route = screen.route.toLowerCase() + screen.routing.route = screen.routing.route.toLowerCase() return screen } diff --git a/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js b/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js index 50e90cddcf..58fb4445a2 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js @@ -1,5 +1,8 @@ -import sanitizeUrl from "./sanitizeUrl" +import sanitizeUrl from "./utils/sanitizeUrl" import { rowListUrl } from "./rowListScreen" +import { Component } from "./utils/Component" +import { Screen } from "./utils/Screen" +import { linkComponent } from "./utils/commonComponents" export default function(tables) { return tables.map(table => { @@ -14,242 +17,133 @@ export default function(tables) { export const newRowUrl = table => sanitizeUrl(`/${table.name}/new`) export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE" -const createScreen = table => ({ - props: { - _id: "c683c4ca8ffc849c6bdd3b7d637fbbf3c", - _component: "@budibase/standard-components/newrow", - _styles: { - normal: {}, - hover: {}, - active: {}, - selected: {}, - }, - table: table._id, - _children: [ - { - _id: "ccad6cc135c7947a7ba9c631f655d6e0f", - _component: "@budibase/standard-components/container", - _styles: { - normal: { - width: "700px", - padding: "0px", - background: "white", - "border-radius": "0.5rem", - "box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)", - margin: "auto", - "margin-top": "20px", - "padding-top": "48px", - "padding-bottom": "48px", - "padding-right": "48px", - "padding-left": "48px", - "margin-bottom": "20px", +function breadcrumbContainer(table) { + const link = linkComponent(table.name).instanceName("Back Link") + + const arrowText = new Component("@budibase/standard-components/text") + .type("none") + .normalStyle({ + "margin-right": "4px", + "margin-left": "4px", + }) + .text(">") + .instanceName("Arrow") + + const newText = new Component("@budibase/standard-components/text") + .type("none") + .normalStyle({ + color: "#000000", + }) + .text("New") + .instanceName("Identifier") + + return new Component("@budibase/standard-components/container") + .type("div") + .normalStyle({ + "font-size": "14px", + color: "#757575", + }) + .instanceName("Breadcrumbs") + .addChild(link) + .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: {}, - active: {}, - selected: {}, + "##eventHandlerType": "Save Row", }, - _code: "", - className: "", - onLoad: [], - 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: [], - }, - ], + { + parameters: { + url: rowListUrl(table), }, - { - _id: "cbd1637cd1e274287a3c28ef0bf235d08", - _component: "@budibase/standard-components/container", - _styles: { - normal: { - display: "flex", - "flex-direction": "row", - "justify-content": "space-between", - "align-items": "center", - "margin-top": "32px", - "margin-bottom": "32px", - }, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - className: "", - onLoad: [], - type: "div", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Title Container", - _children: [ - { - _id: "c98d3675d04114558bbf28661c5ccfb8e", - _component: "@budibase/standard-components/heading", - _styles: { - normal: { - margin: "0px", - "margin-bottom": "0px", - "margin-right": "0px", - "margin-top": "0px", - "margin-left": "0px", - flex: "1 1 auto", - }, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - className: "", - text: "New Row", - type: "h3", - _instanceName: "Title", - _children: [], - }, - { - _id: "cae402bd3c6a44618a8341bf7ab9ab086", - _component: "@budibase/standard-components/button", - _styles: { - normal: { - background: "#000000", - "border-width": "0", - "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: "", -}) + "##eventHandlerType": "Navigate To", + }, + ], + }) + .instanceName("Save Button") + + return new Component("@budibase/standard-components/container") + .type("div") + .normalStyle({ + display: "flex", + "flex-direction": "row", + "justify-content": "space-between", + "align-items": "center", + "margin-top": "32px", + "margin-bottom": "32px", + }) + .instanceName("Title Container") + .addChild(heading) + .addChild(button) +} + +const createScreen = table => { + const dataform = new Component("@budibase/standard-components/dataformwide") + .instanceName("Form") + + const mainContainer = new Component("@budibase/standard-components/container") + .type("div") + .normalStyle({ + width: "700px", + padding: "0px", + background: "white", + "border-radius": "0.5rem", + "box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)", + margin: "auto", + "margin-top": "20px", + "padding-top": "48px", + "padding-bottom": "48px", + "padding-right": "48px", + "padding-left": "48px", + "margin-bottom": "20px", + }) + .instanceName("Container") + .addChild(breadcrumbContainer(table)) + .addChild(titleContainer(table)) + .addChild(dataform) + + return new Screen().component("@budibase/standard-components/newrow") + .addChild(mainContainer) + .table(table._id) + .route(newRowUrl(table)) + .instanceName(`${table.name} - New`) + .name("") + .json() +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js index a4f55f2fd1..2ec985b90a 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js @@ -1,4 +1,4 @@ -import sanitizeUrl from "./sanitizeUrl" +import sanitizeUrl from "./utils/sanitizeUrl" import { rowListUrl } from "./rowListScreen" export default function(tables) { @@ -299,6 +299,9 @@ const createScreen = (table, heading) => ({ _instanceName: `${table.name} - Detail`, _code: "", }, - route: rowDetailUrl(table), + routing: { + route: rowDetailUrl(table), + accessLevelId: "", + }, name: "", }) diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js index 5c71a45f1f..d9243b9b2f 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js @@ -1,4 +1,4 @@ -import sanitizeUrl from "./sanitizeUrl" +import sanitizeUrl from "./utils/sanitizeUrl" import { newRowUrl } from "./newRowScreen" export default function(tables) { @@ -167,6 +167,9 @@ const createScreen = table => ({ className: "", onLoad: [], }, - route: rowListUrl(table), + routing: { + route: rowListUrl(table), + accessLevelId: "", + }, name: "", }) diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/BaseStructure.js b/packages/builder/src/builderStore/store/screenTemplates/utils/BaseStructure.js new file mode 100644 index 0000000000..7b2bb8e927 --- /dev/null +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/BaseStructure.js @@ -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 + } +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js b/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js new file mode 100644 index 0000000000..27b4af2d5b --- /dev/null +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js @@ -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 + } +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js b/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js new file mode 100644 index 0000000000..951e26aeb6 --- /dev/null +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js @@ -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 + } +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js new file mode 100644 index 0000000000..eae2d7f526 --- /dev/null +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js @@ -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", + }) +} \ No newline at end of file diff --git a/packages/builder/src/builderStore/store/screenTemplates/sanitizeUrl.js b/packages/builder/src/builderStore/store/screenTemplates/utils/sanitizeUrl.js similarity index 100% rename from packages/builder/src/builderStore/store/screenTemplates/sanitizeUrl.js rename to packages/builder/src/builderStore/store/screenTemplates/utils/sanitizeUrl.js diff --git a/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte b/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte index 39d5f92a92..f73810c340 100644 --- a/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte +++ b/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte @@ -59,10 +59,10 @@ } // Create autolink to newly created list page - const listPage = screens.find(screen => + const listScreen = screens.find(screen => 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 $goto(`./table/${table._id}`) diff --git a/packages/builder/src/components/userInterface/DetailScreenSelect.svelte b/packages/builder/src/components/userInterface/DetailScreenSelect.svelte index e0b2813c0d..c119985196 100644 --- a/packages/builder/src/components/userInterface/DetailScreenSelect.svelte +++ b/packages/builder/src/components/userInterface/DetailScreenSelect.svelte @@ -17,11 +17,11 @@ .filter( screen => screen.props._component.endsWith("/rowdetail") || - screen.route.endsWith(":id") + screen.routing.route.endsWith(":id") ) .map(screen => ({ name: screen.props._instanceName, - url: screen.route, + url: screen.routing.route, sort: screen.props._component, })), ] diff --git a/packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte b/packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte index 948ab37efd..3de945adc5 100644 --- a/packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte +++ b/packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte @@ -25,7 +25,7 @@ + {/each} {:else} diff --git a/packages/builder/src/components/userInterface/EventsEditor/actions/NavigateTo.svelte b/packages/builder/src/components/userInterface/EventsEditor/actions/NavigateTo.svelte index 916f85dd0b..041237266e 100644 --- a/packages/builder/src/components/userInterface/EventsEditor/actions/NavigateTo.svelte +++ b/packages/builder/src/components/userInterface/EventsEditor/actions/NavigateTo.svelte @@ -10,7 +10,7 @@ + {/each} diff --git a/packages/builder/src/components/userInterface/NewScreenModal.svelte b/packages/builder/src/components/userInterface/NewScreenModal.svelte index b194ab9839..ebee601838 100644 --- a/packages/builder/src/components/userInterface/NewScreenModal.svelte +++ b/packages/builder/src/components/userInterface/NewScreenModal.svelte @@ -49,8 +49,8 @@ baseComponent = draftScreen.props._component } - if (draftScreen.route) { - route = draftScreen.route + if (draftScreen.routing) { + route = draftScreen.routing.route } } @@ -69,7 +69,8 @@ draftScreen.props._instanceName = name 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) if (createLink) { @@ -88,7 +89,7 @@ const routeNameExists = route => { return $allScreens.some( - screen => screen.route.toLowerCase() === route.toLowerCase() + screen => screen.routing.route.toLowerCase() === route.toLowerCase() ) } diff --git a/packages/builder/src/components/userInterface/ScreenSelect.svelte b/packages/builder/src/components/userInterface/ScreenSelect.svelte index b38116e4a1..1c7827e2dd 100644 --- a/packages/builder/src/components/userInterface/ScreenSelect.svelte +++ b/packages/builder/src/components/userInterface/ScreenSelect.svelte @@ -21,7 +21,7 @@ .filter(screen => !screen.props._component.endsWith("/rowdetail")) .map(screen => ({ name: screen.props._instanceName, - url: screen.route, + url: screen.routing.route, sort: screen.props._component, })), ] @@ -54,7 +54,7 @@ if (idBinding) { urls.push({ name: detailScreen.props._instanceName, - url: detailScreen.route.replace( + url: detailScreen.routing.route.replace( ":id", `{{ ${idBinding.runtimeBinding} }}` ), diff --git a/packages/builder/yarn.lock b/packages/builder/yarn.lock index fecd51a497..7c00685aa6 100644 --- a/packages/builder/yarn.lock +++ b/packages/builder/yarn.lock @@ -719,10 +719,10 @@ svelte-flatpickr "^2.4.0" svelte-portal "^1.0.0" -"@budibase/client@^0.3.7": - version "0.3.7" - resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.3.7.tgz#8ed2d40d91ba3788a69ee5db5078f757adb4187f" - integrity sha512-EgpHfw/WOUYeCG4cILDbaN2WFBDSPS698Z+So7FP5l+4E1fvmqtpXVKJYsviwYEx8AKKYyU3nuDi0l6xzb5Flg== +"@budibase/client@^0.3.8": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.3.8.tgz#75df7e97e8f0d9b58c00e2bb0d3b4a55f8d04735" + integrity sha512-tnFdmCdXKS+uZGoipr69Wa0oVoFHmyoV0ydihI6q0gKQH0KutypVHAaul2qPB8t5a/mTZopC//2WdmCeX1GKVg== dependencies: deep-equal "^2.0.1" mustache "^4.0.1" @@ -6412,6 +6412,11 @@ uuid@^3.3.2: version "3.4.0" 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: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" diff --git a/packages/server/src/api/controllers/accesslevel.js b/packages/server/src/api/controllers/accesslevel.js index 143c633e42..22bd1467fb 100644 --- a/packages/server/src/api/controllers/accesslevel.js +++ b/packages/server/src/api/controllers/accesslevel.js @@ -1,10 +1,8 @@ const CouchDB = require("../../db") const { - generateAdminPermissions, - generatePowerUserPermissions, - POWERUSER_LEVEL_ID, - ADMIN_LEVEL_ID, -} = require("../../utilities/accessLevels") + BUILTIN_LEVELS, + AccessLevel, +} = require("../../utilities/security/accessLevels") const { generateAccessLevelID, getAccessLevelParams, @@ -19,19 +17,7 @@ exports.fetch = async function(ctx) { ) const customAccessLevels = body.rows.map(row => row.doc) - const staticAccessLevels = [ - { - _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), - }, - ] - + const staticAccessLevels = [BUILTIN_LEVELS.ADMIN, BUILTIN_LEVELS.POWER] ctx.body = [...staticAccessLevels, ...customAccessLevels] } @@ -40,64 +26,18 @@ exports.find = async function(ctx) { ctx.body = await db.get(ctx.params.levelId) } -exports.update = 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) { +exports.save = async function(ctx) { const db = new CouchDB(ctx.user.appId) - const level = { - name: ctx.request.body.name, - _rev: ctx.request.body._rev, - permissions: ctx.request.body.permissions || [], - _id: generateAccessLevelID(), - type: "accesslevel", + let id = ctx.request.body._id || generateAccessLevelID() + const level = new AccessLevel( + id, + ctx.request.body.name, + ctx.request.body.inherits + ) + if (ctx.request.body._rev) { + level._rev = ctx.request.body._rev } - const result = await db.put(level) level._rev = result.rev ctx.body = level diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index 2185293352..dd44bda69a 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -8,6 +8,7 @@ const fs = require("fs-extra") const { join, resolve } = require("../../utilities/centralPath") const packageJson = require("../../../package.json") const { createLinkView } = require("../../db/linkedRows") +const { createRoutingView } = require("../../utilities/routing") const { downloadTemplate } = require("../../utilities/templates") const { generateAppID, @@ -38,6 +39,7 @@ async function createInstance(template) { }) // add view for linked rows await createLinkView(appId) + await createRoutingView(appId) // replicate the template data to the instance DB if (template) { diff --git a/packages/server/src/api/controllers/auth.js b/packages/server/src/api/controllers/auth.js index 2c162587b3..21136b0214 100644 --- a/packages/server/src/api/controllers/auth.js +++ b/packages/server/src/api/controllers/auth.js @@ -34,6 +34,7 @@ exports.authenticate = async ctx => { userId: dbUser._id, accessLevelId: dbUser.accessLevelId, version: app.version, + permissions: dbUser.permissions || [], } // if in cloud add the user api key if (env.CLOUD) { diff --git a/packages/server/src/api/controllers/routing.js b/packages/server/src/api/controllers/routing.js new file mode 100644 index 0000000000..3be2e3fd41 --- /dev/null +++ b/packages/server/src/api/controllers/routing.js @@ -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) +} diff --git a/packages/server/src/api/controllers/screen.js b/packages/server/src/api/controllers/screen.js index 88166bf0b2..694d171fff 100644 --- a/packages/server/src/api/controllers/screen.js +++ b/packages/server/src/api/controllers/screen.js @@ -1,20 +1,28 @@ const CouchDB = require("../../db") const { getScreenParams, generateScreenID } = require("../../db/utils") +const { AccessController } = require("../../utilities/security/accessLevels") 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( - getScreenParams(null, { - include_docs: true, - }) + const screens = ( + await db.allDocs( + 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 => { - const db = new CouchDB(ctx.user.appId) + const appId = ctx.user.appId + const db = new CouchDB(appId) const screens = await db.allDocs( 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 => { diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index 5e4f963f5b..c51e1fd2b8 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -2,9 +2,11 @@ const CouchDB = require("../../db") const bcrypt = require("../../utilities/bcrypt") const { generateUserID, getUserParams } = require("../../db/utils") const { - POWERUSER_LEVEL_ID, - ADMIN_LEVEL_ID, -} = require("../../utilities/accessLevels") + BUILTIN_LEVEL_ID_ARRAY, +} = require("../../utilities/security/accessLevels") +const { + BUILTIN_PERMISSION_NAMES, +} = require("../../utilities/security/permissions") exports.fetch = async function(ctx) { const database = new CouchDB(ctx.user.appId) @@ -18,7 +20,13 @@ exports.fetch = async function(ctx) { exports.create = async function(ctx) { 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) { ctx.throw(400, "Username and Password Required.") @@ -35,6 +43,7 @@ exports.create = async function(ctx) { name: name || username, type: "user", accessLevelId, + permissions: permissions || [BUILTIN_PERMISSION_NAMES.POWER], } try { @@ -89,10 +98,7 @@ exports.find = async function(ctx) { const checkAccessLevel = async (db, accessLevelId) => { if (!accessLevelId) return - if ( - accessLevelId === POWERUSER_LEVEL_ID || - accessLevelId === ADMIN_LEVEL_ID - ) { + if (BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevelId) !== -1) { return { _id: accessLevelId, name: accessLevelId, diff --git a/packages/server/src/api/controllers/view/index.js b/packages/server/src/api/controllers/view/index.js index 57d4862b7e..0b5b18a93c 100644 --- a/packages/server/src/api/controllers/view/index.js +++ b/packages/server/src/api/controllers/view/index.js @@ -5,6 +5,7 @@ const { join } = require("../../../utilities/centralPath") const os = require("os") const exporters = require("./exporters") const { fetchView } = require("../row") +const { ViewNames } = require("../../../db/utils") const controller = { fetch: async ctx => { @@ -13,8 +14,8 @@ const controller = { const response = [] for (let name of Object.keys(designDoc.views)) { - // Only return custom views - if (name === "by_link") { + // Only return custom views, not built ins + if (Object.values(ViewNames).indexOf(name) !== -1) { continue } response.push({ diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js index 4a4e80d1ed..500ca50ff2 100644 --- a/packages/server/src/api/index.js +++ b/packages/server/src/api/index.js @@ -4,25 +4,7 @@ const compress = require("koa-compress") const zlib = require("zlib") const { budibaseAppsDir } = require("../utilities/budibaseDir") const { isDev } = require("../utilities") -const { - authRoutes, - pageRoutes, - screenRoutes, - userRoutes, - deployRoutes, - applicationRoutes, - rowRoutes, - tableRoutes, - viewRoutes, - staticRoutes, - componentRoutes, - automationRoutes, - accesslevelRoutes, - apiKeysRoutes, - templatesRoutes, - analyticsRoutes, - webhookRoutes, -} = require("./routes") +const {mainRoutes, authRoutes, staticRoutes} = require("./routes") const router = new Router() const env = require("../environment") @@ -72,52 +54,12 @@ router.use(authRoutes.routes()) router.use(authRoutes.allowedMethods()) // authenticated routes -router.use(viewRoutes.routes()) -router.use(viewRoutes.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()) +for (let route of mainRoutes) { + router.use(route.routes()) + router.use(route.allowedMethods()) +} +// WARNING - static routes will catch everything else after them this must be last router.use(staticRoutes.routes()) router.use(staticRoutes.allowedMethods()) diff --git a/packages/server/src/api/routes/accesslevel.js b/packages/server/src/api/routes/accesslevel.js index af1ff80ec6..1f21a1ea29 100644 --- a/packages/server/src/api/routes/accesslevel.js +++ b/packages/server/src/api/routes/accesslevel.js @@ -1,14 +1,18 @@ const Router = require("@koa/router") const controller = require("../controllers/accesslevel") +const authorized = require("../../middleware/authorized") +const { BUILDER } = require("../../utilities/security/permissions") const router = Router() router - .post("/api/accesslevels", controller.create) - .put("/api/accesslevels", controller.update) - .get("/api/accesslevels", controller.fetch) - .get("/api/accesslevels/:levelId", controller.find) - .delete("/api/accesslevels/:levelId/:rev", controller.destroy) - .patch("/api/accesslevels/:levelId", controller.patch) + .post("/api/accesslevels", authorized(BUILDER), controller.save) + .get("/api/accesslevels", authorized(BUILDER), controller.fetch) + .get("/api/accesslevels/:levelId", authorized(BUILDER), controller.find) + .delete( + "/api/accesslevels/:levelId/:rev", + authorized(BUILDER), + controller.destroy + ) module.exports = router diff --git a/packages/server/src/api/routes/analytics.js b/packages/server/src/api/routes/analytics.js index 626e3c2994..0d5e38c34d 100644 --- a/packages/server/src/api/routes/analytics.js +++ b/packages/server/src/api/routes/analytics.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") const controller = require("../controllers/analytics") +const { BUILDER } = require("../../utilities/security/permissions") const router = Router() diff --git a/packages/server/src/api/routes/apikeys.js b/packages/server/src/api/routes/apikeys.js index bec9ab677c..d6d0edeac0 100644 --- a/packages/server/src/api/routes/apikeys.js +++ b/packages/server/src/api/routes/apikeys.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const controller = require("../controllers/apikeys") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const router = Router() diff --git a/packages/server/src/api/routes/application.js b/packages/server/src/api/routes/application.js index aeb815d38c..e3b4ddf6cf 100644 --- a/packages/server/src/api/routes/application.js +++ b/packages/server/src/api/routes/application.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const controller = require("../controllers/application") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const router = Router() diff --git a/packages/server/src/api/routes/automation.js b/packages/server/src/api/routes/automation.js index 3ac7937da2..8644c75787 100644 --- a/packages/server/src/api/routes/automation.js +++ b/packages/server/src/api/routes/automation.js @@ -2,7 +2,11 @@ const Router = require("@koa/router") const controller = require("../controllers/automation") const authorized = require("../../middleware/authorized") 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 router = Router() @@ -75,7 +79,7 @@ router ) .post( "/api/automations/:id/trigger", - authorized(EXECUTE_AUTOMATION), + authorized(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE), controller.trigger ) .delete("/api/automations/:id/:rev", authorized(BUILDER), controller.destroy) diff --git a/packages/server/src/api/routes/component.js b/packages/server/src/api/routes/component.js index 8fbe7ac41a..e9db3bee76 100644 --- a/packages/server/src/api/routes/component.js +++ b/packages/server/src/api/routes/component.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const controller = require("../controllers/component") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const router = Router() diff --git a/packages/server/src/api/routes/deploy.js b/packages/server/src/api/routes/deploy.js index 4f7aa9b33b..d8667c6fc1 100644 --- a/packages/server/src/api/routes/deploy.js +++ b/packages/server/src/api/routes/deploy.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const controller = require("../controllers/deploy") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const router = Router() diff --git a/packages/server/src/api/routes/index.js b/packages/server/src/api/routes/index.js index a19742097c..44f2d08509 100644 --- a/packages/server/src/api/routes/index.js +++ b/packages/server/src/api/routes/index.js @@ -15,10 +15,10 @@ const deployRoutes = require("./deploy") const apiKeysRoutes = require("./apikeys") const templatesRoutes = require("./templates") const analyticsRoutes = require("./analytics") +const routingRoutes = require("./routing") -module.exports = { +exports.mainRoutes = [ deployRoutes, - authRoutes, pageRoutes, screenRoutes, userRoutes, @@ -26,7 +26,6 @@ module.exports = { rowRoutes, tableRoutes, viewRoutes, - staticRoutes, componentRoutes, automationRoutes, accesslevelRoutes, @@ -34,4 +33,8 @@ module.exports = { templatesRoutes, analyticsRoutes, webhookRoutes, -} + routingRoutes, +] + +exports.authRoutes = authRoutes +exports.staticRoutes = staticRoutes diff --git a/packages/server/src/api/routes/pages.js b/packages/server/src/api/routes/pages.js index 1ec01dc780..43fb0e764c 100644 --- a/packages/server/src/api/routes/pages.js +++ b/packages/server/src/api/routes/pages.js @@ -1,6 +1,6 @@ const Router = require("@koa/router") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const controller = require("../controllers/page") const router = Router() diff --git a/packages/server/src/api/routes/routing.js b/packages/server/src/api/routes/routing.js new file mode 100644 index 0000000000..60f84de781 --- /dev/null +++ b/packages/server/src/api/routes/routing.js @@ -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 diff --git a/packages/server/src/api/routes/row.js b/packages/server/src/api/routes/row.js index ae5ae772a6..4409f7f27b 100644 --- a/packages/server/src/api/routes/row.js +++ b/packages/server/src/api/routes/row.js @@ -2,46 +2,49 @@ const Router = require("@koa/router") const rowController = require("../controllers/row") const authorized = require("../../middleware/authorized") const usage = require("../../middleware/usageQuota") -const { READ_TABLE, WRITE_TABLE } = require("../../utilities/accessLevels") +const { + PermissionLevels, + PermissionTypes, +} = require("../../utilities/security/permissions") const router = Router() router .get( "/api/:tableId/:rowId/enrich", - authorized(READ_TABLE, ctx => ctx.params.tableId), + authorized(PermissionTypes.TABLE, PermissionLevels.READ), rowController.fetchEnrichedRow ) .get( "/api/:tableId/rows", - authorized(READ_TABLE, ctx => ctx.params.tableId), + authorized(PermissionTypes.TABLE, PermissionLevels.READ), rowController.fetchTableRows ) .get( "/api/:tableId/rows/:rowId", - authorized(READ_TABLE, ctx => ctx.params.tableId), + authorized(PermissionTypes.TABLE, PermissionLevels.READ), rowController.find ) .post("/api/rows/search", rowController.search) .post( "/api/:tableId/rows", - authorized(WRITE_TABLE, ctx => ctx.params.tableId), + authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), usage, rowController.save ) .patch( "/api/:tableId/rows/:id", - authorized(WRITE_TABLE, ctx => ctx.params.tableId), + authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), rowController.patch ) .post( "/api/:tableId/rows/validate", - authorized(WRITE_TABLE, ctx => ctx.params.tableId), + authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), rowController.validate ) .delete( "/api/:tableId/rows/:rowId/:revId", - authorized(WRITE_TABLE, ctx => ctx.params.tableId), + authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), usage, rowController.destroy ) diff --git a/packages/server/src/api/routes/screen.js b/packages/server/src/api/routes/screen.js index 407bbd1a94..ce49f66043 100644 --- a/packages/server/src/api/routes/screen.js +++ b/packages/server/src/api/routes/screen.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const controller = require("../controllers/screen") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const joiValidator = require("../../middleware/joi-validator") const Joi = require("joi") @@ -12,17 +12,20 @@ function generateSaveValidation() { return joiValidator.body(Joi.object({ _css: Joi.string().allow(""), 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({ - _id: Joi.string().required(), - _component: Joi.string().required(), - _children: Joi.array().required(), - _instanceName: Joi.string().required(), - _styles: Joi.object().required(), - type: Joi.string().optional(), - table: Joi.string().optional(), - }).required().unknown(true), - }).unknown(true)) + _id: Joi.string().required(), + _component: Joi.string().required(), + _children: Joi.array().required(), + _instanceName: Joi.string().required(), + _styles: Joi.object().required(), + type: Joi.string().optional(), + table: Joi.string().optional(), + }).required().unknown(true), + }).unknown(true)) } router diff --git a/packages/server/src/api/routes/static.js b/packages/server/src/api/routes/static.js index 5c33900eca..a519a63781 100644 --- a/packages/server/src/api/routes/static.js +++ b/packages/server/src/api/routes/static.js @@ -3,7 +3,7 @@ const controller = require("../controllers/static") const { budibaseTempDir } = require("../../utilities/budibaseDir") const env = require("../../environment") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const usage = require("../../middleware/usageQuota") const router = Router() diff --git a/packages/server/src/api/routes/table.js b/packages/server/src/api/routes/table.js index 40bfa9326f..ef0eb7caec 100644 --- a/packages/server/src/api/routes/table.js +++ b/packages/server/src/api/routes/table.js @@ -1,7 +1,11 @@ const Router = require("@koa/router") const tableController = require("../controllers/table") const authorized = require("../../middleware/authorized") -const { BUILDER, READ_TABLE } = require("../../utilities/accessLevels") +const { + BUILDER, + PermissionLevels, + PermissionTypes, +} = require("../../utilities/security/permissions") const router = Router() @@ -9,7 +13,7 @@ router .get("/api/tables", authorized(BUILDER), tableController.fetch) .get( "/api/tables/:id", - authorized(READ_TABLE, ctx => ctx.params.id), + authorized(PermissionTypes.TABLE, PermissionLevels.READ), tableController.find ) .post("/api/tables", authorized(BUILDER), tableController.save) diff --git a/packages/server/src/api/routes/templates.js b/packages/server/src/api/routes/templates.js index 3e481610ce..05882a22ea 100644 --- a/packages/server/src/api/routes/templates.js +++ b/packages/server/src/api/routes/templates.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const controller = require("../controllers/templates") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const router = Router() diff --git a/packages/server/src/api/routes/tests/accesslevel.spec.js b/packages/server/src/api/routes/tests/accesslevel.spec.js index 3362cbd713..eeb786f7d3 100644 --- a/packages/server/src/api/routes/tests/accesslevel.spec.js +++ b/packages/server/src/api/routes/tests/accesslevel.spec.js @@ -6,13 +6,10 @@ const { defaultHeaders } = require("./couchTestUtils") const { - generateAdminPermissions, - generatePowerUserPermissions, - POWERUSER_LEVEL_ID, - ADMIN_LEVEL_ID, - READ_TABLE, - WRITE_TABLE, -} = require("../../../utilities/accessLevels") + BUILTIN_LEVEL_IDS, +} = require("../../../utilities/security/accessLevels") + +const accessLevelBody = { name: "user", inherits: BUILTIN_LEVEL_IDS.BASIC } describe("/accesslevels", () => { let server @@ -41,7 +38,7 @@ describe("/accesslevels", () => { it("returns a success message when level is successfully created", async () => { const res = await request .post(`/api/accesslevels`) - .send({ name: "user" }) + .send(accessLevelBody) .set(defaultHeaders(appId)) .expect('Content-Type', /json/) .expect(200) @@ -49,7 +46,6 @@ describe("/accesslevels", () => { expect(res.res.statusMessage).toEqual("Access Level 'user' created successfully.") expect(res.body._id).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 () => { const createRes = await request .post(`/api/accesslevels`) - .send({ name: "user", permissions: [ { itemId: table._id, name: READ_TABLE }] }) + .send(accessLevelBody) .set(defaultHeaders(appId)) .expect('Content-Type', /json/) .expect(200) @@ -74,16 +70,17 @@ describe("/accesslevels", () => { 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.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.permissions).toEqual(await generatePowerUserPermissions(appId)) 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 () => { const createRes = await request .post(`/api/accesslevels`) - .send({ name: "user", permissions: [ { itemId: table._id, name: READ_TABLE } ] }) + .send({ name: "user" }) .set(defaultHeaders(appId)) .expect('Content-Type', /json/) .expect(200) @@ -110,71 +107,4 @@ describe("/accesslevels", () => { .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) - }) - }) }); diff --git a/packages/server/src/api/routes/tests/couchTestUtils.js b/packages/server/src/api/routes/tests/couchTestUtils.js index 02a75d77fd..0e137f1a44 100644 --- a/packages/server/src/api/routes/tests/couchTestUtils.js +++ b/packages/server/src/api/routes/tests/couchTestUtils.js @@ -1,11 +1,11 @@ const CouchDB = require("../../../db") const supertest = require("supertest") const { - POWERUSER_LEVEL_ID, - ANON_LEVEL_ID, - BUILDER_LEVEL_ID, - generateAdminPermissions, -} = require("../../../utilities/accessLevels") + BUILTIN_LEVEL_IDS, +} = require("../../../utilities/security/accessLevels") +const { + BUILTIN_PERMISSION_NAMES, +} = require("../../../utilities/security/permissions") const packageJson = require("../../../../package") const jwt = require("jsonwebtoken") const env = require("../../../environment") @@ -26,7 +26,7 @@ exports.supertest = async () => { exports.defaultHeaders = appId => { const builderUser = { userId: "BUILDER", - accessLevelId: BUILDER_LEVEL_ID, + accessLevelId: BUILTIN_LEVEL_IDS.BUILDER, } const builderToken = jwt.sign(builderUser, env.JWT_SECRET) @@ -126,21 +126,13 @@ exports.createUser = async ( name: "Bill", username, password, - accessLevelId: POWERUSER_LEVEL_ID, + accessLevelId: BUILTIN_LEVEL_IDS.POWER, }) return res.body } -const createUserWithOnePermission = async ( - request, - appId, - permName, - itemId -) => { - let permissions = await generateAdminPermissions(appId) - permissions = permissions.filter( - p => p.name === permName && p.itemId === itemId - ) +const createUserWithOnePermission = async (request, appId, permName) => { + let permissions = [permName] return await createUserWithPermissions( request, @@ -151,7 +143,7 @@ const createUserWithOnePermission = async ( } const createUserWithAdminPermissions = async (request, appId) => { - let permissions = await generateAdminPermissions(appId) + let permissions = [BUILTIN_PERMISSION_NAMES.ADMIN] return await createUserWithPermissions( request, @@ -164,13 +156,9 @@ const createUserWithAdminPermissions = async (request, appId) => { const createUserWithAllPermissionExceptOne = async ( request, appId, - permName, - itemId + permName ) => { - let permissions = await generateAdminPermissions(appId) - permissions = permissions.filter( - p => !(p.name === permName && p.itemId === itemId) - ) + let permissions = [permName] return await createUserWithPermissions( request, @@ -186,11 +174,6 @@ const createUserWithPermissions = async ( permissions, username ) => { - const accessRes = await request - .post(`/api/accesslevels`) - .send({ name: "TestLevel", permissions }) - .set(exports.defaultHeaders(appId)) - const password = `password_${username}` await request .post(`/api/users`) @@ -199,12 +182,13 @@ const createUserWithPermissions = async ( name: username, username, password, - accessLevelId: accessRes.body._id, + accessLevelId: BUILTIN_LEVEL_IDS.POWER, + permissions, }) const anonUser = { userId: "ANON", - accessLevelId: ANON_LEVEL_ID, + accessLevelId: BUILTIN_LEVEL_IDS.ANON, appId: appId, version: packageJson.version, } @@ -232,15 +216,10 @@ exports.testPermissionsForEndpoint = async ({ url, body, appId, - permissionName, - itemId, + permName1, + permName2, }) => { - const headers = await createUserWithOnePermission( - request, - appId, - permissionName, - itemId - ) + const headers = await createUserWithOnePermission(request, appId, permName1) await createRequest(request, method, url, body) .set(headers) @@ -249,8 +228,7 @@ exports.testPermissionsForEndpoint = async ({ const noPermsHeaders = await createUserWithAllPermissionExceptOne( request, appId, - permissionName, - itemId + permName2 ) await createRequest(request, method, url, body) diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js index a569902bae..f9277039ea 100644 --- a/packages/server/src/api/routes/tests/user.spec.js +++ b/packages/server/src/api/routes/tests/user.spec.js @@ -5,11 +5,12 @@ const { createUser, testPermissionsForEndpoint, } = require("./couchTestUtils") -const { - POWERUSER_LEVEL_ID, - LIST_USERS, - USER_MANAGEMENT -} = require("../../../utilities/accessLevels") +const { + BUILTIN_PERMISSION_NAMES, +} = require("../../../utilities/security/permissions") +const { + BUILTIN_LEVEL_IDS, +} = require("../../../utilities/security/accessLevels") describe("/users", () => { let request @@ -53,7 +54,8 @@ describe("/users", () => { method: "GET", url: `/api/users`, 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 .post(`/api/users`) .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('Content-Type', /json/) @@ -77,10 +79,11 @@ describe("/users", () => { await testPermissionsForEndpoint({ request, 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`, appId: appId, - permissionName: USER_MANAGEMENT, + permName1: BUILTIN_PERMISSION_NAMES.ADMIN, + permName2: BUILTIN_PERMISSION_NAMES.POWER, }) }) diff --git a/packages/server/src/api/routes/user.js b/packages/server/src/api/routes/user.js index 5289439e41..9394d842bd 100644 --- a/packages/server/src/api/routes/user.js +++ b/packages/server/src/api/routes/user.js @@ -1,19 +1,39 @@ const Router = require("@koa/router") const controller = require("../controllers/user") 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 router = Router() router - .get("/api/users", authorized(LIST_USERS), controller.fetch) - .get("/api/users/:username", authorized(USER_MANAGEMENT), controller.find) - .put("/api/users/", authorized(USER_MANAGEMENT), controller.update) - .post("/api/users", authorized(USER_MANAGEMENT), usage, controller.create) + .get( + "/api/users", + authorized(PermissionTypes.USER, PermissionLevels.READ), + 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( "/api/users/:username", - authorized(USER_MANAGEMENT), + authorized(PermissionTypes.USER, PermissionLevels.WRITE), usage, controller.destroy ) diff --git a/packages/server/src/api/routes/view.js b/packages/server/src/api/routes/view.js index 3657a9e829..17277b346c 100644 --- a/packages/server/src/api/routes/view.js +++ b/packages/server/src/api/routes/view.js @@ -2,7 +2,11 @@ const Router = require("@koa/router") const viewController = require("../controllers/view") const rowController = require("../controllers/row") 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 router = Router() @@ -10,7 +14,7 @@ const router = Router() router .get( "/api/views/:viewName", - authorized(READ_VIEW, ctx => ctx.params.viewName), + authorized(PermissionTypes.VIEW, PermissionLevels.READ), rowController.fetchView ) .get("/api/views", authorized(BUILDER), viewController.fetch) diff --git a/packages/server/src/api/routes/webhook.js b/packages/server/src/api/routes/webhook.js index a7072904ed..fdcf14e490 100644 --- a/packages/server/src/api/routes/webhook.js +++ b/packages/server/src/api/routes/webhook.js @@ -2,7 +2,11 @@ const Router = require("@koa/router") const controller = require("../controllers/webhook") const authorized = require("../../middleware/authorized") 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 router = Router() @@ -38,7 +42,7 @@ router ) .post( "/api/webhooks/trigger/:instance/:id", - authorized(EXECUTE_WEBHOOK), + authorized(PermissionTypes.WEBHOOK, PermissionLevels.EXECUTE), controller.trigger ) diff --git a/packages/server/src/automations/steps/createUser.js b/packages/server/src/automations/steps/createUser.js index 07d9f05316..4b6250ce36 100644 --- a/packages/server/src/automations/steps/createUser.js +++ b/packages/server/src/automations/steps/createUser.js @@ -1,4 +1,4 @@ -const accessLevels = require("../../utilities/accessLevels") +const accessLevels = require("../../utilities/security/accessLevels") const userController = require("../../api/controllers/user") const env = require("../../environment") const usage = require("../../utilities/usageQuota") @@ -11,7 +11,7 @@ module.exports.definition = { type: "ACTION", stepId: "CREATE_USER", inputs: { - accessLevelId: accessLevels.POWERUSER_LEVEL_ID, + accessLevelId: accessLevels.BUILTIN_LEVEL_IDS.POWER, }, schema: { inputs: { @@ -28,8 +28,8 @@ module.exports.definition = { accessLevelId: { type: "string", title: "Access Level", - enum: accessLevels.ACCESS_LEVELS, - pretty: Object.values(accessLevels.PRETTY_ACCESS_LEVELS), + enum: accessLevels.BUILTIN_LEVEL_IDS, + pretty: accessLevels.BUILTIN_LEVEL_NAME_ARRAY, }, }, required: ["username", "password", "accessLevelId"], diff --git a/packages/server/src/constants/screens.js b/packages/server/src/constants/screens.js index f9a0fb68dc..5c5a9dfd26 100644 --- a/packages/server/src/constants/screens.js +++ b/packages/server/src/constants/screens.js @@ -1,3 +1,5 @@ +const { BUILTIN_LEVEL_IDS } = require("../utilities/security/accessLevels") + exports.HOME_SCREEN = { description: "", url: "", @@ -98,6 +100,9 @@ exports.HOME_SCREEN = { ], _instanceName: "Home", }, - route: "/", + routing: { + route: "/", + accessLevelId: BUILTIN_LEVEL_IDS.BASIC, + }, name: "d834fea2-1b3e-4320-ab34-f9009f5ecc59", } diff --git a/packages/server/src/db/linkedRows/linkUtils.js b/packages/server/src/db/linkedRows/linkUtils.js index dc9d8d3f1e..3a9aff6c33 100644 --- a/packages/server/src/db/linkedRows/linkUtils.js +++ b/packages/server/src/db/linkedRows/linkUtils.js @@ -1,5 +1,6 @@ const CouchDB = require("../index") const Sentry = require("@sentry/node") +const { ViewNames, getQueryIndex } = require("../utils") /** * Only needed so that boolean parameters are being used for includeDocs @@ -40,7 +41,7 @@ exports.createLinkView = async appId => { } designDoc.views = { ...designDoc.views, - by_link: view, + [ViewNames.LINK]: view, } await db.put(designDoc) } @@ -76,7 +77,7 @@ exports.getLinkDocuments = async function({ } params.include_docs = !!includeDocs try { - const response = await db.query("database/by_link", params) + const response = await db.query(getQueryIndex(ViewNames.LINK), params) if (includeDocs) { return response.rows.map(row => row.doc) } else { diff --git a/packages/server/src/db/utils.js b/packages/server/src/db/utils.js index a213dc9066..4edb27a416 100644 --- a/packages/server/src/db/utils.js +++ b/packages/server/src/db/utils.js @@ -17,10 +17,20 @@ const DocumentTypes = { SCREEN: "screen", } +const ViewNames = { + LINK: "by_link", + ROUTING: "screen_routes", +} + +exports.ViewNames = ViewNames exports.DocumentTypes = DocumentTypes exports.SEPARATOR = SEPARATOR 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 * is usually the case as most of our docs are top level e.g. tables, automations, users and so on. diff --git a/packages/server/src/middleware/authenticated.js b/packages/server/src/middleware/authenticated.js index 529581f362..c0fbbdb86c 100644 --- a/packages/server/src/middleware/authenticated.js +++ b/packages/server/src/middleware/authenticated.js @@ -1,12 +1,7 @@ const jwt = require("jsonwebtoken") const STATUS_CODES = require("../utilities/statusCodes") const accessLevelController = require("../api/controllers/accesslevel") -const { - ADMIN_LEVEL_ID, - POWERUSER_LEVEL_ID, - BUILDER_LEVEL_ID, - ANON_LEVEL_ID, -} = require("../utilities/accessLevels") +const { BUILTIN_LEVEL_ID_ARRAY } = require("../utilities/security/accessLevels") const env = require("../environment") const { AuthTypes } = require("../constants") const { getAppId, getCookieName, setCookie } = require("../utilities") @@ -74,12 +69,7 @@ module.exports = async (ctx, next) => { * @param {*} accessLevelId - the id of the users access level */ const getAccessLevel = async (appId, accessLevelId) => { - if ( - accessLevelId === POWERUSER_LEVEL_ID || - accessLevelId === ADMIN_LEVEL_ID || - accessLevelId === BUILDER_LEVEL_ID || - accessLevelId === ANON_LEVEL_ID - ) { + if (BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevelId) !== -1) { return { _id: accessLevelId, name: accessLevelId, diff --git a/packages/server/src/middleware/authorized.js b/packages/server/src/middleware/authorized.js index 34758903f3..5f4b78b97e 100644 --- a/packages/server/src/middleware/authorized.js +++ b/packages/server/src/middleware/authorized.js @@ -1,17 +1,17 @@ +const { BUILTIN_LEVEL_IDS } = require("../utilities/security/accessLevels") const { - adminPermissions, - ADMIN_LEVEL_ID, - POWERUSER_LEVEL_ID, - BUILDER_LEVEL_ID, - BUILDER, -} = require("../utilities/accessLevels") + PermissionTypes, + doesHavePermission, +} = require("../utilities/security/permissions") const env = require("../environment") const { apiKeyTable } = require("../db/dynamoClient") 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("|")) -module.exports = (permName, getItemId) => async (ctx, next) => { +module.exports = (permType, permLevel = null) => async (ctx, next) => { // webhooks can pass locally if (!env.CLOUD && LOCAL_PASS.test(ctx.request.url)) { return next() @@ -37,7 +37,7 @@ module.exports = (permName, getItemId) => async (ctx, next) => { } // 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) { ctx.throw(403, "Session not authenticated") @@ -47,41 +47,19 @@ module.exports = (permName, getItemId) => async (ctx, next) => { 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() } - if (ctx.user.accessLevel._id === BUILDER_LEVEL_ID) { - return next() - } - - if (permName === BUILDER) { + if (permType === PermissionTypes.BUILDER) { ctx.throw(403, "Not Authorized") - return } - const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "") - - 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 (!doesHavePermission(permType, permLevel, permissions)) { + ctx.throw(403, "User does not have permission") } - if ( - ctx.user.accessLevel.permissions - .map(permissionId) - .includes(thisPermissionId) - ) { - return next() - } - - ctx.throw(403, "Not Authorized") + return next() } diff --git a/packages/server/src/utilities/accessLevels.js b/packages/server/src/utilities/accessLevels.js deleted file mode 100644 index e38a7cf23f..0000000000 --- a/packages/server/src/utilities/accessLevels.js +++ /dev/null @@ -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 diff --git a/packages/server/src/utilities/builder/setBuilderToken.js b/packages/server/src/utilities/builder/setBuilderToken.js index 8cf6c44379..f3adf079ad 100644 --- a/packages/server/src/utilities/builder/setBuilderToken.js +++ b/packages/server/src/utilities/builder/setBuilderToken.js @@ -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 CouchDB = require("../../db") const jwt = require("jsonwebtoken") @@ -9,7 +10,8 @@ const APP_PREFIX = DocumentTypes.APP + SEPARATOR module.exports = async (ctx, appId, version) => { const builderUser = { userId: "BUILDER", - accessLevelId: BUILDER_LEVEL_ID, + accessLevelId: BUILTIN_LEVEL_IDS.BUILDER, + permissions: [BUILTIN_PERMISSION_NAMES.ADMIN], version, } if (env.BUDIBASE_API_KEY) { diff --git a/packages/server/src/utilities/permissions.js b/packages/server/src/utilities/permissions.js deleted file mode 100644 index e1513fd0fa..0000000000 --- a/packages/server/src/utilities/permissions.js +++ /dev/null @@ -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 diff --git a/packages/server/src/utilities/routing/index.js b/packages/server/src/utilities/routing/index.js new file mode 100644 index 0000000000..bb0fe5bb62 --- /dev/null +++ b/packages/server/src/utilities/routing/index.js @@ -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 diff --git a/packages/server/src/utilities/routing/routingUtils.js b/packages/server/src/utilities/routing/routingUtils.js new file mode 100644 index 0000000000..5f6a6b5312 --- /dev/null +++ b/packages/server/src/utilities/routing/routingUtils.js @@ -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) +} diff --git a/packages/server/src/utilities/security/accessLevels.js b/packages/server/src/utilities/security/accessLevels.js new file mode 100644 index 0000000000..e99b635f39 --- /dev/null +++ b/packages/server/src/utilities/security/accessLevels.js @@ -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 diff --git a/packages/server/src/utilities/security/permissions.js b/packages/server/src/utilities/security/permissions.js new file mode 100644 index 0000000000..d19f31e393 --- /dev/null +++ b/packages/server/src/utilities/security/permissions.js @@ -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 diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 4348049d51..ee09ecd3b5 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -200,10 +200,10 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@budibase/client@^0.3.7": - version "0.3.7" - resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.3.7.tgz#8ed2d40d91ba3788a69ee5db5078f757adb4187f" - integrity sha512-EgpHfw/WOUYeCG4cILDbaN2WFBDSPS698Z+So7FP5l+4E1fvmqtpXVKJYsviwYEx8AKKYyU3nuDi0l6xzb5Flg== +"@budibase/client@^0.3.8": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.3.8.tgz#75df7e97e8f0d9b58c00e2bb0d3b4a55f8d04735" + integrity sha512-tnFdmCdXKS+uZGoipr69Wa0oVoFHmyoV0ydihI6q0gKQH0KutypVHAaul2qPB8t5a/mTZopC//2WdmCeX1GKVg== dependencies: deep-equal "^2.0.1" mustache "^4.0.1"