From 82605bb697ba18dffac7747fddaa954bbf7eecac Mon Sep 17 00:00:00 2001 From: garrettmills Date: Thu, 4 Mar 2021 11:26:14 -0600 Subject: [PATCH] #4 - add support for sharing pages publicly, without login --- app/controllers/api/v1/Sharing.controller.js | 52 +++++++++++++++++-- app/models/api/Page.model.js | 42 ++++++++++++++- app/models/auth/PublicUserPermission.model.js | 12 ++++- app/models/auth/User.model.js | 8 +++ .../middleware/api/UserRoute.middleware.js | 4 +- .../middleware/auth/ApiRoute.middleware.js | 2 +- app/routing/routers/api/v1/sharing.routes.js | 25 ++++++--- config/api/forms/sharing.config.js | 9 ++++ 8 files changed, 139 insertions(+), 15 deletions(-) diff --git a/app/controllers/api/v1/Sharing.controller.js b/app/controllers/api/v1/Sharing.controller.js index 7d53737..e55576e 100644 --- a/app/controllers/api/v1/Sharing.controller.js +++ b/app/controllers/api/v1/Sharing.controller.js @@ -12,17 +12,30 @@ class Sharing extends Controller { async share_page(req, res) { const level = req.form.level - await req.form.page.share_with(req.form.user, level) + + if ( req.query.public ) { + await req.form.page.share_public(req.user, level) + } else if ( req.form.user ) { + await req.form.page.share_with(req.form.user, level) + } + return res.api({}) } async revoke_page(req, res) { - await req.form.page.unshare_with(req.form.user) + if ( req.query.public ) { + await req.form.page.unshare_public(req.user) + } else if ( req.form.user ) { + await req.form.page.unshare_with(req.form.user) + } + return res.api({}) } async page_info(req, res) { - const data = { + const PublicUserPermission = this.models.get('auth:PublicUserPermission') + + const data = { view: (await req.form.page.view_users).map(x => { return {username: x.uid, id: x.id, level: 'view'} }), @@ -34,10 +47,25 @@ class Sharing extends Controller { }), } + const public_user_can = async perm => PublicUserPermission.can(`page:${req.form.page.UUID}:${perm}`) + + if ( await public_user_can('manage') ) { + data.manage.push({ username: '(Public Users)', public: true, id: '0', level: 'manage' }) + } else if ( await public_user_can('update') ) { + data.update.push({ username: '(Public Users)', public: true, id: '0', level: 'update' }) + } else if ( await public_user_can('view') ) { + data.view.push({ username: '(Public Users)', public: true, id: '0', level: 'view' }) + } + return res.api(data) } async get_link(req, res) { + if ( req.query.public ) { + await req.form.page.share_public(req.user, req.form.level) + return res.api({}) + } + const KeyAction = this.models.get('auth:KeyAction') const in_1_week = new Date in_1_week.setDate(in_1_week.getDate() + 7) @@ -57,6 +85,24 @@ class Sharing extends Controller { return res.api({ link: action.auth_url() }) } + async permission_check(req, res) { + return res.api({ + check: await req.user.can(req.form.permission), + }) + } + + async permission_check_page(req, res) { + const Page = this.models.get('api:Page') + const page = await Page.findOne({ + UUID: req.params.PageId, + Active: true, + }) + + return res.api({ + check: page && (await page.is_accessible_by(req.user, req.params.level)), + }) + } + async accept_link(req, res) { if ( !req.user ) return req.security.kickout() const Page = this.models.get('api:Page') diff --git a/app/models/api/Page.model.js b/app/models/api/Page.model.js index 2d270b4..fa28a00 100644 --- a/app/models/api/Page.model.js +++ b/app/models/api/Page.model.js @@ -106,7 +106,7 @@ class Page extends VersionedModel { return visible } - is_shared() { + is_shared() { // TODO: public user sharing... return this.shared_users_view.length > 0 || this.shared_users_update.length > 0 || this.shared_users_manage.length > 0 } @@ -215,6 +215,46 @@ class Page extends VersionedModel { else return false } + async share_public(current_user, level = 'view') { + const PublicUserPermission = this.models.get('auth:PublicUserPermission') + + if ( !['view', 'update', 'manage'].includes(level) ) { + throw new Error(`Invalid share level: ${level}`) + } + + const possible_grants = [':view', ':manage', ':update', ''].map(x => `page:${this.UUID}${x}`) + + // Remove existing sharing info + await PublicUserPermission.deleteMany({ + permission: { + $in: possible_grants, + }, + }) + + // Create the new sharing level + const share = new PublicUserPermission({ + associated_user_id: this.OrgUserId, + permission: `page:${this.UUID}:${level}`, + }) + + await this.version_save(`Shared publicly (${level} access)`, current_user.id) + await share.save() + } + + async unshare_public(current_user) { + const PublicUserPermission = this.models.get('auth:PublicUserPermission') + const possible_grants = [':view', ':manage', ':update', ''].map(x => `page:${this.UUID}${x}`) + + // Remove existing sharing info + await PublicUserPermission.deleteMany({ + permission: { + $in: possible_grants, + }, + }) + + await this.version_save(`Un-shared public access)`, current_user.id) + } + async share_with(user, level = 'view') { if ( !['view', 'update', 'manage'].includes(level) ) { throw new Error(`Invalid share level: ${level}`) diff --git a/app/models/auth/PublicUserPermission.model.js b/app/models/auth/PublicUserPermission.model.js index 751c682..1491d07 100644 --- a/app/models/auth/PublicUserPermission.model.js +++ b/app/models/auth/PublicUserPermission.model.js @@ -12,10 +12,18 @@ class PublicUserPermissionModel extends Model { } static async can(permission) { - const permission_parts = permission.split(':'); + const permission_parts = permission.split(':') + const permission_checks = [] + const current_check = [] + + for ( const part of permission_parts ) { + current_check.push(part) + permission_checks.push(current_check.join(':')) + } + const match = await this.findOne({ permission: { - $in: permission_parts + $in: permission_checks } }) diff --git a/app/models/auth/User.model.js b/app/models/auth/User.model.js index ca15672..e9ed982 100644 --- a/app/models/auth/User.model.js +++ b/app/models/auth/User.model.js @@ -76,6 +76,14 @@ class User extends AuthUser { is_public_user() { return false } + + async can(permission) { + if ( super.can(permission) ) return true + + const PublicUserPermission = this.models.get('auth:PublicUserPermission') + return await PublicUserPermission.can(permission) + } } + module.exports = exports = User diff --git a/app/routing/middleware/api/UserRoute.middleware.js b/app/routing/middleware/api/UserRoute.middleware.js index cc38f24..a7c76cc 100644 --- a/app/routing/middleware/api/UserRoute.middleware.js +++ b/app/routing/middleware/api/UserRoute.middleware.js @@ -16,14 +16,14 @@ class UserRoute extends Middleware { * It should either call the next function in the stack, * or it should handle the response accordingly. */ - async test(req, res, next, args = {}){ + async test(req, res, next, {allow_public_user = false}){ const User = this.models.get('auth:User') const user_id = req.form.user_id ? req.form.user_id : req.params.user_id if ( !user_id ) return res.status(400).message('Midding user_id.').api({}) const user = await User.findById(user_id) - if ( !user ) return res.status(404).message('Unable to find user with that ID.').api({}) + if ( !user && !allow_public_user ) return res.status(404).message('Unable to find user with that ID.').api({}) if ( !req.form ) req.form = {} req.form.user = user diff --git a/app/routing/middleware/auth/ApiRoute.middleware.js b/app/routing/middleware/auth/ApiRoute.middleware.js index bba667a..7b8775c 100644 --- a/app/routing/middleware/auth/ApiRoute.middleware.js +++ b/app/routing/middleware/auth/ApiRoute.middleware.js @@ -5,7 +5,7 @@ class ApiRoute extends Middleware { return [...super.services, 'models'] } - async test(req, res, next, { allow_public = false }) { + async test(req, res, next, { allow_public = true }) { // If we have an authenticated session, just continue if ( req.is_auth ) { return next() diff --git a/app/routing/routers/api/v1/sharing.routes.js b/app/routing/routers/api/v1/sharing.routes.js index 186de5d..e5e9d0d 100644 --- a/app/routing/routers/api/v1/sharing.routes.js +++ b/app/routing/routers/api/v1/sharing.routes.js @@ -7,17 +7,15 @@ const index = { prefix: '/api/v1/share', - middleware: [ - 'auth:UserOnly', - ], - get: { '/page/:PageId/info': [ + 'middleware::auth:UserOnly', ['middleware::api:RequiredFields', { form: 'sharing.page' }], ['middleware::api:PageRoute', {level: 'manage'}], 'controller::api:v1:Sharing.page_info', ], '/page/:PageId/link/:level': [ + 'middleware::auth:UserOnly', ['middleware::api:RequiredFields', { form: 'sharing.page_link'}], ['middleware::api:PageRoute', {level: 'manage'}], 'controller::api:v1:Sharing.get_link', @@ -27,19 +25,34 @@ const index = { post: { // Share a page with the specified user. '/page/:PageId/share': [ + 'middleware::auth:UserOnly', ['middleware::api:RequiredFields', { form: 'sharing.page_level' }], ['middleware::api:PageRoute', {level: 'manage'}], - 'middleware::api:UserRoute', + ['middleware::api:UserRoute', { allow_public_user: true }], 'controller::api:v1:Sharing.share_page', ], // Unshare a page with the specified user. '/page/:PageId/revoke': [ + 'middleware::auth:UserOnly', ['middleware::api:RequiredFields', { form: 'sharing.page_user' }], ['middleware::api:PageRoute', {level: 'manage'}], - 'middleware::api:UserRoute', + ['middleware::api:UserRoute', { allow_public_user: true }], 'controller::api:v1:Sharing.revoke_page', ], + + // Check the public user's access to a given resource + '/check': [ + ['middleware::api:RequiredFields', { form: 'sharing.permission_check'}], + ['middleware::auth:ApiRoute', { allow_public: true }], + 'controller::api:v1:Sharing.permission_check', + ], + + // Check the public user's access to a given page + '/check-page/:PageId/:level': [ + ['middleware::auth:ApiRoute', { allow_public: true }], + 'controller::api:v1:Sharing.permission_check_page', + ], }, } diff --git a/config/api/forms/sharing.config.js b/config/api/forms/sharing.config.js index 8d7f5f5..1f6bdfa 100644 --- a/config/api/forms/sharing.config.js +++ b/config/api/forms/sharing.config.js @@ -52,4 +52,13 @@ module.exports = exports = { }, }, }, + + permission_check: { + fields: { + permission: { + required: true, + coerce: String, + }, + }, + }, }