From 1b87edc954a60d8d320e56a2b9f5ec394f621ee2 Mon Sep 17 00:00:00 2001 From: Michael Shanks Date: Thu, 21 May 2020 14:31:23 +0100 Subject: [PATCH] auth, first version, needing tested --- packages/server/.vscode/launch.json | 13 ++ .../server/src/api/controllers/accesslevel.js | 108 ++++++++++ packages/server/src/api/controllers/auth.js | 2 +- packages/server/src/api/controllers/record.js | 30 ++- packages/server/src/api/controllers/user.js | 39 +++- packages/server/src/api/index.js | 8 +- packages/server/src/api/routes/accesslevel.js | 14 ++ packages/server/src/api/routes/application.js | 12 +- packages/server/src/api/routes/client.js | 4 +- packages/server/src/api/routes/component.js | 3 + packages/server/src/api/routes/index.js | 4 +- packages/server/src/api/routes/instance.js | 6 +- packages/server/src/api/routes/model.js | 45 ++++- packages/server/src/api/routes/pages.js | 120 ++++++----- packages/server/src/api/routes/record.js | 12 -- packages/server/src/api/routes/screen.js | 12 +- .../src/api/routes/tests/accesslevel.spec.js | 184 +++++++++++++++++ .../src/api/routes/tests/couchTestUtils.js | 22 +- .../server/src/api/routes/tests/model.spec.js | 9 +- .../src/api/routes/tests/record.spec.js | 23 ++- .../server/src/api/routes/tests/user.spec.js | 3 +- packages/server/src/api/routes/user.js | 22 +- packages/server/src/api/routes/view.js | 14 +- .../server/src/middleware/authenticated.js | 34 +++- packages/server/src/middleware/authorized.js | 58 ++++++ packages/server/src/utilities/accessLevels.js | 64 ++++++ packages/server/yarn.lock | 190 +----------------- 27 files changed, 746 insertions(+), 309 deletions(-) create mode 100644 packages/server/src/api/controllers/accesslevel.js create mode 100644 packages/server/src/api/routes/accesslevel.js delete mode 100644 packages/server/src/api/routes/record.js create mode 100644 packages/server/src/api/routes/tests/accesslevel.spec.js create mode 100644 packages/server/src/middleware/authorized.js create mode 100644 packages/server/src/utilities/accessLevels.js diff --git a/packages/server/.vscode/launch.json b/packages/server/.vscode/launch.json index 964e9297f4..4dc6b653cb 100644 --- a/packages/server/.vscode/launch.json +++ b/packages/server/.vscode/launch.json @@ -49,6 +49,19 @@ "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", } }, + { + "type": "node", + "request": "launch", + "name": "Jest - Access Levels", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["accesslevel.spec", "--runInBand"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", + } + }, { "type": "node", "request": "launch", diff --git a/packages/server/src/api/controllers/accesslevel.js b/packages/server/src/api/controllers/accesslevel.js new file mode 100644 index 0000000000..9c9c7ff727 --- /dev/null +++ b/packages/server/src/api/controllers/accesslevel.js @@ -0,0 +1,108 @@ +const CouchDB = require("../../db") +const newid = require("../../db/newid") +const { + generateAdminPermissions, + generatePowerUserPermissions, + POWERUSER_LEVEL_ID, + ADMIN_LEVEL_ID, +} = require("../../utilities/accessLevels") + +exports.fetch = async function(ctx) { + const db = new CouchDB(ctx.params.instanceId) + const body = await db.query("database/by_type", { + include_docs: true, + key: ["accesslevel"], + }) + const customAccessLevels = body.rows.map(row => row.doc) + + const staticAccessLevels = [ + { + _id: ADMIN_LEVEL_ID, + name: "Admin", + permissions: await generateAdminPermissions(ctx.params.instanceId), + }, + { + _id: POWERUSER_LEVEL_ID, + name: "Power User", + permissions: await generatePowerUserPermissions(ctx.params.instanceId), + }, + ] + + ctx.body = [...staticAccessLevels, ...customAccessLevels] +} + +exports.find = async function(ctx) { + const db = new CouchDB(ctx.params.instanceId) + ctx.body = await db.get(ctx.params.levelId) +} + +exports.update = async function(ctx) { + const db = new CouchDB(ctx.params.instanceId) + 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.params.instanceId) + 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.params.instanceId) + + const level = { + name: ctx.request.body.name, + _rev: ctx.request.body._rev, + permissions: ctx.request.body.permissions || [], + _id: newid(), + type: "accesslevel", + } + + const result = await db.put(level) + level._rev = result.rev + ctx.body = level + ctx.message = `Access Level '${level.name}' created successfully.` +} + +exports.destroy = async function(ctx) { + const db = new CouchDB(ctx.params.instanceId) + await db.remove(ctx.params.levelId, ctx.params.rev) + ctx.message = `Access Level ${ctx.params.id} deleted successfully` + ctx.status = 200 +} diff --git a/packages/server/src/api/controllers/auth.js b/packages/server/src/api/controllers/auth.js index 4cfc2f438a..b9e1150648 100644 --- a/packages/server/src/api/controllers/auth.js +++ b/packages/server/src/api/controllers/auth.js @@ -37,7 +37,7 @@ exports.authenticate = async ctx => { if (await bcrypt.compare(password, dbUser.password)) { const payload = { userId: dbUser._id, - accessLevel: "", + accessLevelId: dbUser.accessLevelId, instanceId: instanceId, } diff --git a/packages/server/src/api/controllers/record.js b/packages/server/src/api/controllers/record.js index 2cd648b6c3..7a5b045136 100644 --- a/packages/server/src/api/controllers/record.js +++ b/packages/server/src/api/controllers/record.js @@ -7,6 +7,7 @@ const ajv = new Ajv() exports.save = async function(ctx) { const db = new CouchDB(ctx.params.instanceId) const record = ctx.request.body + record.modelId = ctx.params.modelId if (!record._rev && !record._id) { record._id = newid() @@ -43,16 +44,12 @@ exports.save = async function(ctx) { record.type = "record" const response = await db.post(record) record._rev = response.rev - // await ctx.publish(events.recordApi.save.onRecordCreated, { - // record: record, - // }) - ctx.body = record ctx.status = 200 ctx.message = `${model.name} created successfully` } -exports.fetch = async function(ctx) { +exports.fetchView = async function(ctx) { const db = new CouchDB(ctx.params.instanceId) const response = await db.query(`database/${ctx.params.viewName}`, { include_docs: true, @@ -60,13 +57,30 @@ exports.fetch = async function(ctx) { ctx.body = response.rows.map(row => row.doc) } +exports.fetchModel = async function(ctx) { + const db = new CouchDB(ctx.params.instanceId) + const response = await db.query(`database/all_${ctx.params.modelId}`, { + include_docs: true, + }) + ctx.body = response.rows.map(row => row.doc) +} + exports.find = async function(ctx) { const db = new CouchDB(ctx.params.instanceId) - ctx.body = await db.get(ctx.params.recordId) + const record = await db.get(ctx.params.recordId) + if (record.modelId !== ctx.params.modelId) { + ctx.throw(400, "Supplied modelId doe not match the record's modelId") + return + } + ctx.body = record } exports.destroy = async function(ctx) { - const databaseId = ctx.params.instanceId - const db = new CouchDB(databaseId) + const db = new CouchDB(ctx.params.instanceId) + const record = await db.get(ctx.params.recordId) + if (record.modelId !== ctx.params.modelId) { + ctx.throw(400, "Supplied modelId doe not match the record's modelId") + return + } ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId) } diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index 2be0132a47..f654caa4c0 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -2,8 +2,11 @@ const CouchDB = require("../../db") const clientDb = require("../../db/clientDb") const bcrypt = require("../../utilities/bcrypt") const env = require("../../environment") - const getUserId = userName => `user_${userName}` +const { + POWERUSER_LEVEL_ID, + ADMIN_LEVEL_ID, +} = require("../../utilities/accessLevels") exports.fetch = async function(ctx) { const database = new CouchDB(ctx.params.instanceId) @@ -18,17 +21,26 @@ exports.fetch = async function(ctx) { exports.create = async function(ctx) { const database = new CouchDB(ctx.params.instanceId) const appId = (await database.get("_design/database")).metadata.applicationId - const { username, password, name } = ctx.request.body + const { username, password, name, accessLevelId } = ctx.request.body - if (!username || !password) ctx.throw(400, "Username and Password Required.") + if (!username || !password) { + ctx.throw(400, "Username and Password Required.") + } - const response = await database.post({ + const accessLevel = await checkAccessLevel(database, accessLevelId) + + if (!accessLevel) ctx.throw(400, "Invalid Access Level") + + const user = { _id: getUserId(username), username, password: await bcrypt.hash(password), name: name || username, type: "user", - }) + accessLevelId, + } + + const response = await database.post(user) // the clientDB needs to store a map of users against the app const db = new CouchDB(clientDb.name(env.CLIENT_ID)) @@ -49,6 +61,8 @@ exports.create = async function(ctx) { } } +exports.update = async function(ctx) {} + exports.destroy = async function(ctx) { const database = new CouchDB(ctx.params.instanceId) await database.destroy(getUserId(ctx.params.username)) @@ -65,3 +79,18 @@ exports.find = async function(ctx) { _rev: user._rev, } } + +const checkAccessLevel = async (db, accessLevelId) => { + if (!accessLevelId) return + if ( + accessLevelId === POWERUSER_LEVEL_ID || + accessLevelId === ADMIN_LEVEL_ID + ) { + return { + _id: accessLevelId, + name: accessLevelId, + permissions: [], + } + } + return await db.get(accessLevelId) +} diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js index 176eaaff96..d8f3bde76b 100644 --- a/packages/server/src/api/index.js +++ b/packages/server/src/api/index.js @@ -7,7 +7,6 @@ const { authRoutes, pageRoutes, userRoutes, - recordRoutes, instanceRoutes, clientRoutes, applicationRoutes, @@ -15,6 +14,7 @@ const { viewRoutes, staticRoutes, componentRoutes, + accesslevelRoutes, } = require("./routes") const router = new Router() @@ -70,9 +70,6 @@ router.use(modelRoutes.allowedMethods()) router.use(userRoutes.routes()) router.use(userRoutes.allowedMethods()) -router.use(recordRoutes.routes()) -router.use(recordRoutes.allowedMethods()) - router.use(instanceRoutes.routes()) router.use(instanceRoutes.allowedMethods()) // end auth routes @@ -89,6 +86,9 @@ router.use(componentRoutes.allowedMethods()) router.use(clientRoutes.routes()) router.use(clientRoutes.allowedMethods()) +router.use(accesslevelRoutes.routes()) +router.use(accesslevelRoutes.allowedMethods()) + 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 new file mode 100644 index 0000000000..d34acab02c --- /dev/null +++ b/packages/server/src/api/routes/accesslevel.js @@ -0,0 +1,14 @@ +const Router = require("@koa/router") +const controller = require("../controllers/accesslevel") + +const router = Router() + +router + .post("/api/:instanceId/accesslevels", controller.create) + .put("/api/:instanceId/accesslevels", controller.update) + .get("/api/:instanceId/accesslevels", controller.fetch) + .get("/api/:instanceId/accesslevels/:levelId", controller.find) + .delete("/api/:instanceId/accesslevels/:levelId/:rev", controller.destroy) + .patch("/api/:instanceId/accesslevels/:levelId", controller.patch) + +module.exports = router diff --git a/packages/server/src/api/routes/application.js b/packages/server/src/api/routes/application.js index dddf64a710..60cc781ac6 100644 --- a/packages/server/src/api/routes/application.js +++ b/packages/server/src/api/routes/application.js @@ -1,11 +1,17 @@ const Router = require("@koa/router") const controller = require("../controllers/application") +const authorized = require("../../middleware/authorized") +const { BUILDER } = require("../../utilities/accessLevels") const router = Router() router - .get("/api/applications", controller.fetch) - .get("/api/:applicationId/appPackage", controller.fetchAppPackage) - .post("/api/applications", controller.create) + .get("/api/applications", authorized(BUILDER), controller.fetch) + .get( + "/api/:applicationId/appPackage", + authorized(BUILDER), + controller.fetchAppPackage + ) + .post("/api/applications", authorized(BUILDER), controller.create) module.exports = router diff --git a/packages/server/src/api/routes/client.js b/packages/server/src/api/routes/client.js index ff87b82e22..16acf1b7a3 100644 --- a/packages/server/src/api/routes/client.js +++ b/packages/server/src/api/routes/client.js @@ -1,8 +1,10 @@ const Router = require("@koa/router") const controller = require("../controllers/client") +const authorized = require("../../middleware/authorized") +const { BUILDER } = require("../../utilities/accessLevels") const router = Router() -router.get("/api/client/id", controller.getClientId) +router.get("/api/client/id", authorized(BUILDER), controller.getClientId) module.exports = router diff --git a/packages/server/src/api/routes/component.js b/packages/server/src/api/routes/component.js index 5df34381fa..8fbe7ac41a 100644 --- a/packages/server/src/api/routes/component.js +++ b/packages/server/src/api/routes/component.js @@ -1,10 +1,13 @@ const Router = require("@koa/router") const controller = require("../controllers/component") +const authorized = require("../../middleware/authorized") +const { BUILDER } = require("../../utilities/accessLevels") const router = Router() router.get( "/:appId/components/definitions", + authorized(BUILDER), controller.fetchAppComponentDefinitions ) diff --git a/packages/server/src/api/routes/index.js b/packages/server/src/api/routes/index.js index a8f57c0e14..6a299f8351 100644 --- a/packages/server/src/api/routes/index.js +++ b/packages/server/src/api/routes/index.js @@ -1,7 +1,6 @@ const authRoutes = require("./auth") const pageRoutes = require("./pages") const userRoutes = require("./user") -const recordRoutes = require("./record") const instanceRoutes = require("./instance") const clientRoutes = require("./client") const applicationRoutes = require("./application") @@ -9,12 +8,12 @@ const modelRoutes = require("./model") const viewRoutes = require("./view") const staticRoutes = require("./static") const componentRoutes = require("./component") +const accesslevelRoutes = require("./accesslevel") module.exports = { authRoutes, pageRoutes, userRoutes, - recordRoutes, instanceRoutes, clientRoutes, applicationRoutes, @@ -22,4 +21,5 @@ module.exports = { viewRoutes, staticRoutes, componentRoutes, + accesslevelRoutes, } diff --git a/packages/server/src/api/routes/instance.js b/packages/server/src/api/routes/instance.js index fd74a98bf1..9b7b3db511 100644 --- a/packages/server/src/api/routes/instance.js +++ b/packages/server/src/api/routes/instance.js @@ -1,10 +1,12 @@ const Router = require("@koa/router") const controller = require("../controllers/instance") +const authorized = require("../../middleware/authorized") +const { BUILDER } = require("../../utilities/accessLevels") const router = Router() router - .post("/api/:applicationId/instances", controller.create) - .delete("/api/instances/:instanceId", controller.destroy) + .post("/api/:applicationId/instances", authorized(BUILDER), controller.create) + .delete("/api/instances/:instanceId", authorized(BUILDER), controller.destroy) module.exports = router diff --git a/packages/server/src/api/routes/model.js b/packages/server/src/api/routes/model.js index d25d0d17cb..d9eb5cf798 100644 --- a/packages/server/src/api/routes/model.js +++ b/packages/server/src/api/routes/model.js @@ -1,12 +1,49 @@ const Router = require("@koa/router") -const controller = require("../controllers/model") +const modelController = require("../controllers/model") +const recordController = require("../controllers/record") +const authorized = require("../../middleware/authorized") +const { + READ_MODEL, + WRITE_MODEL, + BUILDER, +} = require("../../utilities/accessLevels") const router = Router() +// records + router - .get("/api/:instanceId/models", controller.fetch) - .post("/api/:instanceId/models", controller.create) + .get( + "/api/:instanceId/:modelId/records", + authorized(READ_MODEL, ctx => ctx.params.modelId), + recordController.fetchModel + ) + .get( + "/api/:instanceId/:modelId/records/:recordId", + authorized(READ_MODEL, ctx => ctx.params.modelId), + recordController.find + ) + .post( + "/api/:instanceId/:modelId/records", + authorized(WRITE_MODEL, ctx => ctx.params.modelId), + recordController.save + ) + .delete( + "/api/:instanceId/:modelId/records/:recordId/:revId", + authorized(WRITE_MODEL, ctx => ctx.params.modelId), + recordController.destroy + ) + +// models + +router + .get("/api/:instanceId/models", authorized(BUILDER), modelController.fetch) + .post("/api/:instanceId/models", authorized(BUILDER), modelController.create) // .patch("/api/:instanceId/models", controller.update) - .delete("/api/:instanceId/models/:modelId/:revId", controller.destroy) + .delete( + "/api/:instanceId/models/:modelId/:revId", + authorized(BUILDER), + modelController.destroy + ) module.exports = router diff --git a/packages/server/src/api/routes/pages.js b/packages/server/src/api/routes/pages.js index 88fd1239f4..98f0f08b92 100644 --- a/packages/server/src/api/routes/pages.js +++ b/packages/server/src/api/routes/pages.js @@ -7,63 +7,85 @@ const { renameScreen, deleteScreen, } = require("../../utilities/builder") +const authorized = require("../../middleware/authorized") +const { BUILDER } = require("../../utilities/accessLevels") const router = Router() -router.post("/_builder/api/:appId/pages/:pageName", async ctx => { - await buildPage( - ctx.config, - ctx.params.appId, - ctx.params.pageName, - ctx.request.body - ) - ctx.response.status = StatusCodes.OK -}) +router.post( + "/_builder/api/:appId/pages/:pageName", + authorized(BUILDER), + async ctx => { + await buildPage( + ctx.config, + ctx.params.appId, + ctx.params.pageName, + ctx.request.body + ) + ctx.response.status = StatusCodes.OK + } +) -router.get("/_builder/api/:appId/pages/:pagename/screens", async ctx => { - ctx.body = await listScreens( - ctx.config, - ctx.params.appId, - ctx.params.pagename - ) - ctx.response.status = StatusCodes.OK -}) +router.get( + "/_builder/api/:appId/pages/:pagename/screens", + authorized(BUILDER), + async ctx => { + ctx.body = await listScreens( + ctx.config, + ctx.params.appId, + ctx.params.pagename + ) + ctx.response.status = StatusCodes.OK + } +) -router.post("/_builder/api/:appId/pages/:pagename/screen", async ctx => { - ctx.body = await saveScreen( - ctx.config, - ctx.params.appId, - ctx.params.pagename, - ctx.request.body - ) - ctx.response.status = StatusCodes.OK -}) +router.post( + "/_builder/api/:appId/pages/:pagename/screen", + authorized(BUILDER), + async ctx => { + ctx.body = await saveScreen( + ctx.config, + ctx.params.appId, + ctx.params.pagename, + ctx.request.body + ) + ctx.response.status = StatusCodes.OK + } +) -router.patch("/_builder/api/:appname/pages/:pagename/screen", async ctx => { - await renameScreen( - ctx.config, - ctx.params.appname, - ctx.params.pagename, - ctx.request.body.oldname, - ctx.request.body.newname - ) - ctx.response.status = StatusCodes.OK -}) +router.patch( + "/_builder/api/:appname/pages/:pagename/screen", + authorized(BUILDER), + async ctx => { + await renameScreen( + ctx.config, + ctx.params.appname, + ctx.params.pagename, + ctx.request.body.oldname, + ctx.request.body.newname + ) + ctx.response.status = StatusCodes.OK + } +) -router.delete("/_builder/api/:appname/pages/:pagename/screen/*", async ctx => { - const name = ctx.request.path.replace( - `/_builder/api/${ctx.params.appname}/pages/${ctx.params.pagename}/screen/`, - "" - ) +router.delete( + "/_builder/api/:appname/pages/:pagename/screen/*", + authorized(BUILDER), + async ctx => { + const name = ctx.request.path.replace( + `/_builder/api/${ctx.params.appname}/pages/${ctx.params.pagename}/screen/`, + "" + ) - await deleteScreen( - ctx.config, - ctx.params.appname, - ctx.params.pagename, - decodeURI(name) - ) + await deleteScreen( + ctx.config, + ctx.params.appname, + ctx.params.pagename, + decodeURI(name) + ) - ctx.response.status = StatusCodes.OK -}) + ctx.response.status = StatusCodes.OK + } +) module.exports = router diff --git a/packages/server/src/api/routes/record.js b/packages/server/src/api/routes/record.js deleted file mode 100644 index fb0b543dc1..0000000000 --- a/packages/server/src/api/routes/record.js +++ /dev/null @@ -1,12 +0,0 @@ -const Router = require("@koa/router") -const controller = require("../controllers/record") - -const router = Router() - -router - .get("/api/:instanceId/:viewName/records", controller.fetch) - .get("/api/:instanceId/records/:recordId", controller.find) - .post("/api/:instanceId/records", controller.save) - .delete("/api/:instanceId/records/:recordId/:revId", controller.destroy) - -module.exports = router diff --git a/packages/server/src/api/routes/screen.js b/packages/server/src/api/routes/screen.js index 3a167b6ef6..19823aab68 100644 --- a/packages/server/src/api/routes/screen.js +++ b/packages/server/src/api/routes/screen.js @@ -1,11 +1,17 @@ const Router = require("@koa/router") const controller = require("../controllers/screen") +const authorized = require("../../middleware/authorized") +const { BUILDER } = require("../../utilities/accessLevels") const router = Router() router - .get("/api/:instanceId/screens", controller.fetch) - .post("/api/:instanceId/screens", controller.save) - .delete("/api/:instanceId/:screenId/:revId", controller.destroy) + .get("/api/:instanceId/screens", authorized(BUILDER), controller.fetch) + .post("/api/:instanceId/screens", authorized(BUILDER), controller.save) + .delete( + "/api/:instanceId/:screenId/:revId", + authorized(BUILDER), + controller.destroy + ) module.exports = router diff --git a/packages/server/src/api/routes/tests/accesslevel.spec.js b/packages/server/src/api/routes/tests/accesslevel.spec.js new file mode 100644 index 0000000000..ef2d1575cd --- /dev/null +++ b/packages/server/src/api/routes/tests/accesslevel.spec.js @@ -0,0 +1,184 @@ +const { + createInstance, + createClientDatabase, + createApplication, + createModel, + createView, + supertest, + defaultHeaders +} = require("./couchTestUtils") +const { + generateAdminPermissions, + generatePowerUserPermissions, + POWERUSER_LEVEL_ID, + ADMIN_LEVEL_ID, + READ_MODEL, + WRITE_MODEL, +} = require("../../../utilities/accessLevels") + +describe("/accesslevels", () => { + let appId + let server + let request + let instanceId + let model + let view + + beforeAll(async () => { + ({ request, server } = await supertest()) + await createClientDatabase(request); + appId = (await createApplication(request))._id + }); + + afterAll(async () => { + server.close(); + }) + + beforeEach(async () => { + instanceId = (await createInstance(request, appId))._id + model = await createModel(request, instanceId) + view = await createView(request, instanceId) + }) + + describe("create", () => { + + it("returns a success message when level is successfully created", async () => { + const res = await request + .post(`/api/${instanceId}/accesslevels`) + .send({ name: "user" }) + .set(defaultHeaders) + .expect('Content-Type', /json/) + .expect(200) + + 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([]) + }) + + }); + + describe("fetch", () => { + + it("should list custom levels, plus 2 default levels", async () => { + const createRes = await request + .post(`/api/${instanceId}/accesslevels`) + .send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] }) + .set(defaultHeaders) + .expect('Content-Type', /json/) + .expect(200) + + const customLevel = createRes.body + + const res = await request + .get(`/api/${instanceId}/accesslevels`) + .set(defaultHeaders) + .expect('Content-Type', /json/) + .expect(200) + + expect(res.body.length).toBe(3) + + const adminLevel = res.body.find(r => r._id === ADMIN_LEVEL_ID) + expect(adminLevel).toBeDefined() + expect(adminLevel.permissions).toEqual(await generateAdminPermissions(instanceId)) + + const powerUserLevel = res.body.find(r => r._id === POWERUSER_LEVEL_ID) + expect(powerUserLevel).toBeDefined() + expect(powerUserLevel.permissions).toEqual(await generatePowerUserPermissions(instanceId)) + + const customLevelFetched = res.body.find(r => r._id === customLevel._id) + expect(customLevelFetched.permissions).toEqual(customLevel.permissions) + }) + + }); + + describe("destroy", () => { + it("should delete custom access level", async () => { + const createRes = await request + .post(`/api/${instanceId}/accesslevels`) + .send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL } ] }) + .set(defaultHeaders) + .expect('Content-Type', /json/) + .expect(200) + + const customLevel = createRes.body + + await request + .delete(`/api/${instanceId}/accesslevels/${customLevel._id}/${customLevel._rev}`) + .set(defaultHeaders) + .expect(200) + + await request + .get(`/api/${instanceId}/accesslevels/${customLevel._id}`) + .set(defaultHeaders) + .expect(404) + }) + }) + + describe("patch", () => { + it("should add given permissions", async () => { + const createRes = await request + .post(`/api/${instanceId}/accesslevels`) + .send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] }) + .set(defaultHeaders) + .expect('Content-Type', /json/) + .expect(200) + + const customLevel = createRes.body + + await request + .patch(`/api/${instanceId}/accesslevels/${customLevel._id}`) + .send({ + _rev: customLevel._rev, + addedPermissions: [ { itemId: model._id, name: WRITE_MODEL } ] + }) + .set(defaultHeaders) + .expect('Content-Type', /json/) + .expect(200) + + const finalRes = await request + .get(`/api/${instanceId}/accesslevels/${customLevel._id}`) + .set(defaultHeaders) + .expect(200) + + expect(finalRes.body.permissions.length).toBe(2) + expect(finalRes.body.permissions.some(p => p.name === WRITE_MODEL)).toBe(true) + expect(finalRes.body.permissions.some(p => p.name === READ_MODEL)).toBe(true) + }) + + it("should remove given permissions", async () => { + const createRes = await request + .post(`/api/${instanceId}/accesslevels`) + .send({ + name: "user", + permissions: [ + { itemId: model._id, name: READ_MODEL }, + { itemId: model._id, name: WRITE_MODEL }, + ] + }) + .set(defaultHeaders) + .expect('Content-Type', /json/) + .expect(200) + + const customLevel = createRes.body + + await request + .patch(`/api/${instanceId}/accesslevels/${customLevel._id}`) + .send({ + _rev: customLevel._rev, + removedPermissions: [ { itemId: model._id, name: WRITE_MODEL }] + }) + .set(defaultHeaders) + .expect('Content-Type', /json/) + .expect(200) + + const finalRes = await request + .get(`/api/${instanceId}/accesslevels/${customLevel._id}`) + .set(defaultHeaders) + .expect(200) + + expect(finalRes.body.permissions.length).toBe(1) + expect(finalRes.body.permissions.some(p => p.name === READ_MODEL)).toBe(true) + }) + }) +}); diff --git a/packages/server/src/api/routes/tests/couchTestUtils.js b/packages/server/src/api/routes/tests/couchTestUtils.js index 7018ec24b2..841d46ca46 100644 --- a/packages/server/src/api/routes/tests/couchTestUtils.js +++ b/packages/server/src/api/routes/tests/couchTestUtils.js @@ -2,6 +2,7 @@ const CouchDB = require("../../../db") const { create, destroy } = require("../../../db/clientDb") const supertest = require("supertest") const app = require("../../../app") +const { POWERUSER_LEVEL_ID } = require("../../../utilities/accessLevels") const TEST_CLIENT_ID = "test-client-id" @@ -17,7 +18,7 @@ exports.supertest = async () => { exports.defaultHeaders = { Accept: "application/json", - Authorization: "Basic test-admin-secret", + Cookie: ["builder:token=test-admin-secret"], } exports.createModel = async (request, instanceId, model) => { @@ -37,6 +38,18 @@ exports.createModel = async (request, instanceId, model) => { return res.body } +exports.createView = async (request, instanceId, view) => { + view = view || { + map: "function(doc) { emit(doc[doc.key], doc._id); } ", + } + + const res = await request + .post(`/api/${instanceId}/views`) + .set(exports.defaultHeaders) + .send(view) + return res.body +} + exports.createClientDatabase = async () => await create(TEST_CLIENT_ID) exports.createApplication = async (request, name = "test_application") => { @@ -70,7 +83,12 @@ exports.createUser = async ( const res = await request .post(`/api/${instanceId}/users`) .set(exports.defaultHeaders) - .send({ name: "Bill", username, password }) + .send({ + name: "Bill", + username, + password, + accessLevelId: POWERUSER_LEVEL_ID, + }) return res.body } diff --git a/packages/server/src/api/routes/tests/model.spec.js b/packages/server/src/api/routes/tests/model.spec.js index e32c94224f..3e224536ed 100644 --- a/packages/server/src/api/routes/tests/model.spec.js +++ b/packages/server/src/api/routes/tests/model.spec.js @@ -3,7 +3,8 @@ const { createModel, supertest, createClientDatabase, - createApplication + createApplication , + defaultHeaders } = require("./couchTestUtils") describe("/models", () => { @@ -38,7 +39,7 @@ describe("/models", () => { name: { type: "string" } } }) - .set("Accept", "application/json") + .set(defaultHeaders) .expect('Content-Type', /json/) .expect(200) .end(async (err, res) => { @@ -60,7 +61,7 @@ describe("/models", () => { it("returns all the models for that instance in the response body", done => { request .get(`/api/${instance._id}/models`) - .set("Accept", "application/json") + .set(defaultHeaders) .expect('Content-Type', /json/) .expect(200) .end(async (_, res) => { @@ -83,7 +84,7 @@ describe("/models", () => { it("returns a success response when a model is deleted.", done => { request .delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`) - .set("Accept", "application/json") + .set(defaultHeaders) .expect('Content-Type', /json/) .expect(200) .end(async (_, res) => { diff --git a/packages/server/src/api/routes/tests/record.spec.js b/packages/server/src/api/routes/tests/record.spec.js index b75b245135..16af0b6031 100644 --- a/packages/server/src/api/routes/tests/record.spec.js +++ b/packages/server/src/api/routes/tests/record.spec.js @@ -3,7 +3,8 @@ const { createClientDatabase, createInstance, createModel, - supertest + supertest, + defaultHeaders, } = require("./couchTestUtils"); describe("/records", () => { @@ -38,9 +39,9 @@ describe("/records", () => { const createRecord = async r => await request - .post(`/api/${instance._id}/records`) + .post(`/api/${instance._id}/${model._id}/records`) .send(r || record) - .set("Accept", "application/json") + .set(defaultHeaders) .expect('Content-Type', /json/) .expect(200) @@ -56,14 +57,14 @@ describe("/records", () => { const existing = rec.body const res = await request - .post(`/api/${instance._id}/records`) + .post(`/api/${instance._id}/${model._id}/records`) .send({ _id: existing._id, _rev: existing._rev, modelId: model._id, name: "Updated Name", }) - .set("Accept", "application/json") + .set(defaultHeaders) .expect('Content-Type', /json/) .expect(200) @@ -76,8 +77,8 @@ describe("/records", () => { const existing = rec.body const res = await request - .get(`/api/${instance._id}/records/${existing._id}`) - .set("Accept", "application/json") + .get(`/api/${instance._id}/${model._id}/records/${existing._id}`) + .set(defaultHeaders) .expect('Content-Type', /json/) .expect(200) @@ -99,8 +100,8 @@ describe("/records", () => { await createRecord(newRecord) const res = await request - .get(`/api/${instance._id}/all_${newRecord.modelId}/records`) - .set("Accept", "application/json") + .get(`/api/${instance._id}/${model._id}/records`) + .set(defaultHeaders) .expect('Content-Type', /json/) .expect(200) @@ -112,8 +113,8 @@ describe("/records", () => { it("load should return 404 when record does not exist", async () => { await createRecord() await request - .get(`/api/${instance._id}/records/not-a-valid-id`) - .set("Accept", "application/json") + .get(`/api/${instance._id}/${model._id}/records/not-a-valid-id`) + .set(defaultHeaders) .expect('Content-Type', /json/) .expect(404) }) diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js index bba748daff..f994798c2e 100644 --- a/packages/server/src/api/routes/tests/user.spec.js +++ b/packages/server/src/api/routes/tests/user.spec.js @@ -6,6 +6,7 @@ const { defaultHeaders, createUser, } = require("./couchTestUtils") +const { POWERUSER_LEVEL_ID } = require("../../../utilities/accessLevels") describe("/users", () => { let request @@ -51,7 +52,7 @@ describe("/users", () => { const res = await request .post(`/api/${instance._id}/users`) .set(defaultHeaders) - .send({ name: "Bill", username: "bill", password: "bills_password" }) + .send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: POWERUSER_LEVEL_ID }) .expect(200) .expect('Content-Type', /json/) diff --git a/packages/server/src/api/routes/user.js b/packages/server/src/api/routes/user.js index 61f4a096fb..20e17c7473 100644 --- a/packages/server/src/api/routes/user.js +++ b/packages/server/src/api/routes/user.js @@ -1,12 +1,26 @@ const Router = require("@koa/router") const controller = require("../controllers/user") +const authorized = require("../../middleware/authorized") +const { USER_MANAGEMENT, LIST_USERS } = require("../../utilities/accessLevels") const router = Router() router - .get("/api/:instanceId/users", controller.fetch) - .get("/api/:instanceId/users/:username", controller.find) - .post("/api/:instanceId/users", controller.create) - .delete("/api/:instanceId/users/:username", controller.destroy) + .get("/api/:instanceId/users", authorized(LIST_USERS), controller.fetch) + .get( + "/api/:instanceId/users/:username", + authorized(USER_MANAGEMENT), + controller.find + ) + .post( + "/api/:instanceId/users", + authorized(USER_MANAGEMENT), + controller.create + ) + .delete( + "/api/:instanceId/users/:username", + authorized(USER_MANAGEMENT), + controller.destroy + ) module.exports = router diff --git a/packages/server/src/api/routes/view.js b/packages/server/src/api/routes/view.js index f7ce9b41b5..3bae02c4b3 100644 --- a/packages/server/src/api/routes/view.js +++ b/packages/server/src/api/routes/view.js @@ -1,12 +1,20 @@ const Router = require("@koa/router") -const controller = require("../controllers/view") +const viewController = require("../controllers/view") +const recordController = require("../controllers/record") +const authorized = require("../../middleware/authorized") +const { BUILDER, READ_VIEW } = require("../../utilities/accessLevels") const router = Router() router - .get("/api/:instanceId/views", controller.fetch) + .get( + "/api/:instanceId/view/:viewName", + authorized(READ_VIEW, ctx => ctx.params.viewName), + recordController.fetchView + ) + .get("/api/:instanceId/views", authorized(BUILDER), viewController.fetch) // .patch("/api/:databaseId/views", controller.update); // .delete("/api/:instanceId/views/:viewId/:revId", controller.destroy); - .post("/api/:instanceId/views", controller.create) + .post("/api/:instanceId/views", authorized(BUILDER), viewController.create) module.exports = router diff --git a/packages/server/src/middleware/authenticated.js b/packages/server/src/middleware/authenticated.js index 1bcc5575c3..1a28e6a418 100644 --- a/packages/server/src/middleware/authenticated.js +++ b/packages/server/src/middleware/authenticated.js @@ -1,6 +1,11 @@ const jwt = require("jsonwebtoken") const STATUS_CODES = require("../utilities/statusCodes") const env = require("../environment") +const accessLevelController = require("../api/controllers/accesslevel") +const { + ADMIN_LEVEL_ID, + POWERUSER_LEVEL_ID, +} = require("../utilities/accessLevels") module.exports = async (ctx, next) => { if (ctx.path === "/_builder") { @@ -8,8 +13,9 @@ module.exports = async (ctx, next) => { return } - if (ctx.isDev && ctx.cookies.get("builder:token") === env.ADMIN_SECRET) { + if (ctx.cookies.get("builder:token") === env.ADMIN_SECRET) { ctx.isAuthenticated = true + ctx.isBuilder = true await next() return } @@ -23,7 +29,12 @@ module.exports = async (ctx, next) => { } try { - ctx.jwtPayload = jwt.verify(token, ctx.config.jwtSecret) + const jwtPayload = jwt.verify(token, ctx.config.jwtSecret) + + ctx.user = { + ...jwtPayload, + accessLevel: await getAccessLevel(jwtPayload.accessLevelId), + } ctx.isAuthenticated = true } catch (err) { ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text) @@ -31,3 +42,22 @@ module.exports = async (ctx, next) => { await next() } + +const getAccessLevel = async accessLevelId => { + if ( + accessLevelId === POWERUSER_LEVEL_ID || + accessLevelId === ADMIN_LEVEL_ID + ) { + return { + _id: accessLevelId, + name: accessLevelId, + permissions: [], + } + } + + const findAccessContext = { + params: { levelId: accessLevelId }, + } + await accessLevelController.find(findAccessContext) + return findAccessContext.body +} diff --git a/packages/server/src/middleware/authorized.js b/packages/server/src/middleware/authorized.js new file mode 100644 index 0000000000..15a7b25458 --- /dev/null +++ b/packages/server/src/middleware/authorized.js @@ -0,0 +1,58 @@ +const { + adminPermissions, + ADMIN_LEVEL_ID, + POWERUSER_LEVEL_ID, + BUILDER, +} = require("../utilities/accessLevels") + +module.exports = (permName, getItemId) => async (ctx, next) => { + if (!ctx.isAuthenticated) { + ctx.throw(403, "Session not authenticated") + } + + if (ctx.isBuilder) { + await next() + return + } + + if (permName === BUILDER) { + ctx.throw(403, "Not Authorized") + return + } + + if (!ctx.user) { + ctx.throw(403, "User not found") + } + + const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "") + + if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) { + await next() + return + } + + const thisPermissionId = { + 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) + ) { + await next() + return + } + + if ( + ctx.user.accessLevel.permissions + .map(permissionId) + .includes(thisPermissionId) + ) { + await next() + return + } + + ctx.throw(403, "Not Authorized") +} diff --git a/packages/server/src/utilities/accessLevels.js b/packages/server/src/utilities/accessLevels.js new file mode 100644 index 0000000000..25a231ed73 --- /dev/null +++ b/packages/server/src/utilities/accessLevels.js @@ -0,0 +1,64 @@ +const viewController = require("../api/controllers/view") +const modelController = require("../api/controllers/model") + +exports.ADMIN_LEVEL_ID = "ADMIN" +exports.POWERUSER_LEVEL_ID = "POWER_USER" + +exports.READ_MODEL = "read-model" +exports.WRITE_MODEL = "write-model" +exports.READ_VIEW = "read-view" +exports.EXECUTE_WORKFLOW = "execute-workflow" +exports.USER_MANAGEMENT = "user-management" +exports.BUILDER = "builder" +exports.LIST_USERS = "list-users" + +exports.adminPermissions = [ + { + name: exports.USER_MANAGEMENT, + }, +] + +exports.generateAdminPermissions = async instanceId => [ + ...exports.adminPermissions, + ...(await exports.generatePowerUserPermissions(instanceId)), +] + +exports.generatePowerUserPermissions = async instanceId => { + const fetchModelsCtx = { + params: { + instanceId, + }, + } + await modelController.fetch(fetchModelsCtx) + const models = fetchModelsCtx.body + + const fetchViewsCtx = { + params: { + instanceId, + }, + } + await viewController.fetch(fetchViewsCtx) + const views = fetchViewsCtx.body + + const readModelPermissions = models.map(m => ({ + itemId: m._id, + name: exports.READ_MODEL, + })) + + const writeModelPermissions = models.map(m => ({ + itemId: m._id, + name: exports.WRITE_MODEL, + })) + + const viewPermissions = views.map(v => ({ + itemId: v.name, + name: exports.READ_VIEW, + })) + + return [ + ...readModelPermissions, + ...writeModelPermissions, + ...viewPermissions, + { name: exports.LIST_USERS }, + ] +} diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 0959e3e991..8a64a7cb8a 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -194,20 +194,6 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@budibase/client@^0.0.32": - version "0.0.32" - resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.0.32.tgz#76d9f147563a0bf939eae7f32ce75b2a527ba496" - integrity sha512-jmCCLn0CUoQbL6h623S5IqK6+GYLqX3WzUTZInSb1SCBOM3pI0eLP5HwTR6s7r42SfD0v9jTWRdyTnHiElNj8A== - dependencies: - "@nx-js/compiler-util" "^2.0.0" - bcryptjs "^2.4.3" - deep-equal "^2.0.1" - lodash "^4.17.15" - lunr "^2.3.5" - regexparam "^1.3.0" - shortid "^2.2.8" - svelte "^3.9.2" - "@budibase/core@^0.0.32": version "0.0.32" resolved "https://registry.yarnpkg.com/@budibase/core/-/core-0.0.32.tgz#c5d9ab869c5e9596a1ac337aaf041e795b1cc7fa" @@ -839,11 +825,6 @@ array-equal@^1.0.0: resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= -array-filter@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" - integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= - array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" @@ -916,13 +897,6 @@ atomic-sleep@^1.0.0: resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== -available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" - integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ== - dependencies: - array-filter "^1.0.0" - aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -1638,26 +1612,6 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" -deep-equal@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0" - integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA== - dependencies: - es-abstract "^1.17.5" - es-get-iterator "^1.1.0" - is-arguments "^1.0.4" - is-date-object "^1.0.2" - is-regex "^1.0.5" - isarray "^2.0.5" - object-is "^1.1.2" - object-keys "^1.1.1" - object.assign "^4.1.0" - regexp.prototype.flags "^1.3.0" - side-channel "^1.0.2" - which-boxed-primitive "^1.0.1" - which-collection "^1.0.1" - which-typed-array "^1.1.2" - deep-equal@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -2011,7 +1965,7 @@ error-inject@^1.0.0: resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc= -es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.17.5: +es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5: version "1.17.5" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9" integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg== @@ -2028,19 +1982,6 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstrac string.prototype.trimleft "^2.1.1" string.prototype.trimright "^2.1.1" -es-get-iterator@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" - integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== - dependencies: - es-abstract "^1.17.4" - has-symbols "^1.0.1" - is-arguments "^1.0.4" - is-map "^2.0.1" - is-set "^2.0.1" - is-string "^1.0.5" - isarray "^2.0.5" - es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -3026,21 +2967,11 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" -is-arguments@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" - integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= -is-bigint@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" - integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g== - is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -3048,11 +2979,6 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" - integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== - is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -3089,7 +3015,7 @@ is-data-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" -is-date-object@^1.0.1, is-date-object@^1.0.2: +is-date-object@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== @@ -3164,21 +3090,11 @@ is-installed-globally@^0.3.1: global-dirs "^2.0.1" is-path-inside "^3.0.1" -is-map@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" - integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== - is-npm@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== -is-number-object@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" - integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== - is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -3215,21 +3131,11 @@ is-regex@^1.0.5: dependencies: has "^1.0.3" -is-set@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" - integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== - is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= -is-string@^1.0.4, is-string@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" - integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== - is-symbol@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" @@ -3246,31 +3152,11 @@ is-type-of@^1.0.0: is-class-hotfix "~0.0.6" isstream "~0.1.2" -is-typed-array@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d" - integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ== - dependencies: - available-typed-arrays "^1.0.0" - es-abstract "^1.17.4" - foreach "^2.0.5" - has-symbols "^1.0.1" - is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= -is-weakmap@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" - integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== - -is-weakset@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83" - integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw== - is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -3296,11 +3182,6 @@ isarray@1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - isbinaryfile@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b" @@ -4708,14 +4589,6 @@ object-inspect@^1.7.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== -object-is@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" - integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.6, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -5437,19 +5310,6 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp.prototype.flags@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" - integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - -regexparam@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-1.3.0.tgz#2fe42c93e32a40eff6235d635e0ffa344b92965f" - integrity sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g== - regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" @@ -5778,14 +5638,6 @@ shortid@^2.2.8: dependencies: nanoid "^2.1.0" -side-channel@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947" - integrity sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA== - dependencies: - es-abstract "^1.17.0-next.1" - object-inspect "^1.7.0" - signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -6194,11 +6046,6 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -svelte@^3.9.2: - version "3.22.3" - resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.22.3.tgz#6af3bdcfea44c2fadbf17a32c479f49bdf1aba4b" - integrity sha512-DumSy5eWPFPlMUGf3+eHyFSkt5yLqyAmMdCuXOE4qc5GtFyLxwTAGKZmgKmW2jmbpTTeFQ/fSQfDBQbl9Eo7yw== - symbol-tree@^3.2.2: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -6679,44 +6526,11 @@ whatwg-url@^7.0.0: tr46 "^1.0.1" webidl-conversions "^4.0.2" -which-boxed-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1" - integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ== - dependencies: - is-bigint "^1.0.0" - is-boolean-object "^1.0.0" - is-number-object "^1.0.3" - is-string "^1.0.4" - is-symbol "^1.0.2" - -which-collection@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" - integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== - dependencies: - is-map "^2.0.1" - is-set "^2.0.1" - is-weakmap "^2.0.1" - is-weakset "^2.0.1" - which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which-typed-array@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2" - integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ== - dependencies: - available-typed-arrays "^1.0.2" - es-abstract "^1.17.5" - foreach "^2.0.5" - function-bind "^1.1.1" - has-symbols "^1.0.1" - is-typed-array "^1.1.3" - which@^1.2.9, which@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"