Skip to content

Commit

Permalink
Issue #257 -- Entity:Action permissions for PaperController.
Browse files Browse the repository at this point in the history
  • Loading branch information
danielBingham committed Oct 6, 2024
1 parent a7d4b17 commit a291ac5
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 55 deletions.
52 changes: 52 additions & 0 deletions database/initialization-scripts/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -593,3 +593,55 @@ CREATE TABLE response_versions (
PRIMARY KEY(response_id, version)
);

/******************************************************************************
* Permissions
******************************************************************************/


CREATE TABLE permissions (
user_id bigint REFERENCES users(id),
entity varchar(512),
action varchar(512),

paper_id bigint REFERENCES papers(id) DEFAULT NULL,
paper_version_id uuid REFERENCES paper_versions(id) DEFAULT NULL,
event_id bigint REFERENCES paper_events(id) DEFAULT NULL,
review_id bigint REFERENCES reviews(id) DEFAULT NULL,
paper_comment_id bigint REFERENCES paper_comments(id) DEFAULT NULL,
submission_id bigint REFERENCES journal_submissions(id) DEFAULT NULL,
journal_id bigint REFERENCES journals(id) DEFAULT NULL
);

CREATE TYPE role_type AS ENUM('public', 'author', 'editor', 'reviewer');
CREATE TABLE roles (
id bigserial PRIMARY KEY,
name varchar(1024),
short_description varchar(1024),
type role_type,
is_owner boolean,

description text,
journal_id bigint REFERENCES journals(id) DEFAULT NULL,
paper_id bigint REFERENCES papers(id) DEFAULT NULL
);
INSERT INTO roles (name, type, description) VALUES ('public', 'public', 'The general public.');

CREATE TABLE role_permissions (
role_id bigint REFERENCES roles(id) DEFAULT NULL,
permission permission_type,

paper_id bigint REFERENCES papers(id) DEFAULT null,
version int DEFAULT null,
event_id bigint REFERENCES paper_events(id) DEFAULT NULL,
review_id bigint REFERENCES reviews(id) DEFAULT null,
paper_comment_id bigint REFERENCES paper_comments(id) DEFAULT NULL,
submission_id bigint REFERENCES journal_submissions(id) DEFAULT NULL,
journal_id bigint REFERENCES journals(id) DEFAULT NULL
);
INSERT INTO role_permissions (role_id, permission)
SELECT roles.id, 'Papers:create' FROM roles WHERE roles.name = 'public';

CREATE TABLE user_roles (
role_id bigint REFERENCS roles(id) DEFAULT NULL,
user_id bigint REFERENCES users(id) DEFAULT NULL
);
1 change: 1 addition & 0 deletions packages/backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ exports.OpenAlexService = require('./services/OpenAlexService')
exports.PageMetadataService = require('./services/PageMetadataService')
exports.PaperEventService = require('./services/PaperEventService')
exports.PaperService = require('./services/PaperService')
exports.PermissionService = require('./services/PermissionsService')
exports.ReputationGenerationService = require('./services/ReputationGenerationService')
exports.ReputationPermissionService = require('./services/ReputationPermissionService')
exports.S3FileService = require('./services/S3FileService')
Expand Down
43 changes: 43 additions & 0 deletions packages/backend/services/PermissionService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/******************************************************************************
*
* JournalHub -- Universal Scholarly Publishing
* Copyright (C) 2022 - 2024 Daniel Bingham
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
******************************************************************************/
module.exports = class PermissionService {
constructor(core) {
this.core = core
}

/**
* Can `user` perform `action` on `entity` identified by `context.
*
* @returns {boolean} True if the `user` can perform `action` on `entity`
* identified by `context`, false otherwise.
*/
async can(user, action, entity, context) {

}

async let(user, action, entity, context) {

}

async has(user, role, context) {

}

}
117 changes: 62 additions & 55 deletions web-application/server/controllers/PaperController.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ module.exports = class PaperController {
this.journalSubmissionDAO = new backend.JournalSubmissionDAO(core)

this.submissionService = new backend.SubmissionService(core)
this.PaperService = new backend.PaperService(core)
this.paperService = new backend.PaperService(core)
this.paperEventService = new backend.PaperEventService(core)
this.notificationService = new backend.NotificationService(core)
this.permissionService = new backend.PermissionService(core)

}

Expand Down Expand Up @@ -178,17 +179,17 @@ module.exports = class PaperController {

// Preprints the session user can review.
if ( query.type == 'preprint') {
visibleIds = await this.PaperService.getPreprints()
visibleIds = await this.paperService.getPreprints()

// Retrieves all of a
} else if (session.user && query.type == 'drafts' ) {
visibleIds = await this.PaperService.getDrafts(session.user.id)
visibleIds = await this.paperService.getDrafts(session.user.id)
} else if ( session.user && query.type == 'private-drafts' ) {
visibleIds = await this.PaperService.getPrivateDrafts(session.user.id)
visibleIds = await this.paperService.getPrivateDrafts(session.user.id)
} else if ( session.user && query.type == 'user-submissions' ) {
visibleIds = await this.PaperService.getUserSubmissions(session.user.id)
visibleIds = await this.paperService.getUserSubmissions(session.user.id)
} else if (session.user && query.type == 'review-submissions' ) {
visibleIds = await this.PaperService.getVisibleDraftSubmissions(session.user.id)
visibleIds = await this.paperService.getVisibleDraftSubmissions(session.user.id)
} else if ( session.user && query.type == 'assigned-review' ) {
const assignedResults = await this.database.query(`
SELECT journal_submissions.paper_id
Expand Down Expand Up @@ -491,7 +492,8 @@ module.exports = class PaperController {
* Permissions Checking and Input Validation
*
* 1. User is logged in.
* 2. User is an author and owner of the paper being submitted.
* 2. User must have 'create' permissions on 'paper'.
* 3. User is an author and owner of the paper being submitted.
*
* Data validation:
*
Expand All @@ -510,7 +512,14 @@ module.exports = class PaperController {

const user = request.session.user

// 2. User is an author and owner of the paper being submitted.
// 2. User must have 'create' permissions on 'paper'.
const canCreate = await this.permissionService.can(user, 'create', 'Paper')
if ( ! canCreate ) {
throw new ControllerError(403, 'not-authorized',
`User(${user.id}) attempted to create a paper without permissions.`)
}

// 3. User is an author and owner of the paper being submitted.
if ( ! paper.authors.find((a) => a.userId == user.id && a.owner) ) {
throw new ControllerError(403, 'not-authorized:not-owner',
`User(${user.id}) submitted a paper with out being an owner of that paper!`)
Expand Down Expand Up @@ -658,29 +667,30 @@ module.exports = class PaperController {
* @returns {Promise} Resolves to void.
*/
async getPaper(request, response) {
const results = await this.paperDAO.selectPapers('WHERE papers.id=$1', [request.params.id])

/*************************************************************
* Permissions Checking and Input Validation
*
* 1. If the paper is a draft, user must be logged in and have review
* privileges on that draft.
* 1. User must have 'view' permissions on 'paper'.
*
*
* **********************************************************/
const currentUser = request.session.user

if ( ! results.dictionary[request.params.id] ) {
throw new ControllerError(404, 'not-found', `Paper(${request.params.id}) not found.`)
// 1. User must have 'view' permissions on 'paper'.
const canView = await this.permissionService.can(currentUser, 'view', 'Paper', { paperId: request.params.id })
if ( ! canView ) {
throw new ControllerError(403, 'not-authorized',
`User attempted to access a Paper they were not authorized to view.`)
}

const paper = results.dictionary[request.params.id]
if ( paper.isDraft ) {
if ( ! request.session.user && ! paper.showPreprint ) {
throw new ControllerError(403, 'not-authenticated', `Unauthenticated user attempting to view draft.`)
}

// TODO update visibility permissions
const results = await this.paperDAO.selectPapers('WHERE papers.id=$1', [request.params.id])


if ( ! results.dictionary[request.params.id] ) {
throw new ControllerError(404, 'not-found', `Paper(${request.params.id}) not found.`)
}
const paper = results.dictionary[request.params.id]

/************************************************************
* Permissions Checking Complete
Expand All @@ -694,18 +704,6 @@ module.exports = class PaperController {
})
}

/**
* PUT /paper/:id
*
* Replace an existing paper wholesale with the provided JSON.
*
* NOTE: Intentionally left unimplemented until we have a need for it, or
* have time to decide how to secure it.
*/
async putPaper(request, response) {
throw new ControllerError(501, 'not-implemented', `Attempt to put a paper, when PUT /paper/:id is unimplemented.`)
}

/**
* PATCH /paper/:id
*
Expand All @@ -722,46 +720,47 @@ module.exports = class PaperController {
* @returns {Promise} Resolves to void.
*/
async patchPaper(request, response) {
const paper = request.body
// We want to use the params.id over any id in the body.
paper.id = request.params.id

/*************************************************************
* Permissions Checking and Input Validation
*
* 1. User must be logged in.
* 2. Paper(:paper_id) must exist.
* 3. User must be an owning author on Paper(:paper_id).
* 2. User must have 'edit' on Paper(:paperId)
* 3. Paper(:paper_id) must exist.
* 4. Paper(:paper_id) must be a draft.
* 5. Only title and isDraft may be patched.
*
* **********************************************************/

const paper = request.body
// We want to use the params.id over any id in the body.
paper.id = request.params.id

// 1. User must be logged in.
if ( ! request.session.user ) {
throw new ControllerError(401, 'not-authenticated', `Unauthenticated user attempting to patch paper(${paper.id}).`)
}

const user = request.session.user

// 2. User must have 'edit' on Paper(:paperId)
const canEdit = await this.permissionService.can(user, 'edit', 'Paper', { paperId: paper.id })
if ( ! canEdit ) {
throw new ControllerError(403, 'not-authorized',
`User(${user.id}) attempted to edit Paper(${paper.id}) without permissions.`)
}

const existingResults = await this.paperDAO.selectPapers('WHERE papers.id=$1', [ paper.id ])
const existing = existingResults.dictionary[paper.id]

// 2. Paper(:paper_id) must exist.
// 3. Paper(:paper_id) must exist.
if ( ! existing ) {
throw new ControllerError(404, 'not-found', `Attempt to patch a paper(${paper.id}) that doesn't exist!`)
}

// 3. User must be an owning author on the Paper(:paper_id)
if ( ! existing.authors.find((a) => a.userId == user.id && a.owner) ) {
throw new ControllerError(403, 'not-authorized:not-owner',
`Non-owner user(${user.id}) attempting to PATCH paper(${paper.id}).`)
}

// 4. Paper(:paper_id) must be a draft.
if ( ! existing.isDraft ) {
throw new ControllerError(403, `not-authorized:published`,
`User(${user.id}) attempting to PATCH a published paper.`)
`User(${user.id}) attempting to PATCH published Paper(${paper.id}).`)
}


Expand Down Expand Up @@ -831,34 +830,38 @@ module.exports = class PaperController {
* @returns {Promise} Resolves to void.
*/
async deletePaper(request, response) {
const paperId = request.params.id

/*************************************************************
* Permissions Checking and Input Validation
*
* 1. User must be logged in.
* 2. Paper(:paper_id) must exist.
* 3. User must be an owning author on Paper(:paper_id).
* 2. User must have 'delete' on Paper(:paperId)
* 3. Paper(:paper_id) must exist.
* 4. Paper(:paper_id) must be a draft.
*
* **********************************************************/
const paperId = request.params.id

// 1. User must be logged in.
if ( ! request.session.user ) {
throw new ControllerError(403, 'not-authorized', `Unauthenticated user attempting to delete paper(${request.params.id}).`)
throw new ControllerError(403, 'not-authorized', `Unauthenticated user attempting to delete paper(${paperId}).`)
}

const user = request.session.user

// 2. User must have 'delete' on Paper(:paperId)
const canDelete = await this.permissionService.can(user, 'delete', 'Paper', { paperId: paperId })
if ( ! canDelete ) {
throw new ControllerError(403, 'not-authorized',
`User(${user.id}) attempted to DELETE Paper(${paperId}) without permissions.`)
}

const existingResults = await this.database.query(`
SELECT paper_authors.user_id, paper_authors.owner, papers.is_draft as "isDraft"
SELECT papers.is_draft as "isDraft"
FROM papers
JOIN paper_authors on papers.id = paper_authors.paper_id
WHERE papers.id = $1 AND paper_authors.user_id = $2 AND owner = true
WHERE papers.id = $1
`, [ paperId, user.id])

// 2. Paper(:paper_id) must exist.
// 3. User must be an owning author on Paper(:paper_id)
if ( existingResults.rows.length <= 0 ) {
throw new ControllerError(403, 'not-owner',
`Non-owner user(${user.id}) attempting to delete paper(${request.params.id}).`)
Expand Down Expand Up @@ -908,6 +911,10 @@ module.exports = class PaperController {

const user = request.session.user

const visibleSubmissionsResults = await this.database.query(`
SELECT submission_id FROM permissions WHERE entity='Submission' AND action='view' AND paper_id = $1
`, [ paperId ])

const paperAuthorResults = await this.database.query(`
SELECT paper_authors.user_id
FROM papers
Expand Down

0 comments on commit a291ac5

Please sign in to comment.