Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions database/initialization-scripts/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -593,3 +593,47 @@ CREATE TABLE response_versions (
PRIMARY KEY(response_id, version)
);

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


CREATE TABLE permissions (
id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
entity varchar(512) NOT NULL,
action varchar(512) NOT NULL,

user_id bigint REFERENCES users(id) DEFAULT NULL,
role_id uuid REFERENCES roles(id) DEFAULT NULL,

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,

created_date timestamptz,
updated_date timestamptz
);

CREATE TABLE roles (
id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
name varchar(1024) NOT NULL,
description varchar(1024) NOT NULL,

journal_id bigint REFERENCES journals(id) DEFAULT NULL,
paper_id bigint REFERENCES papers(id) DEFAULT NULL,

created_date timestamptz,
updated_date timestamptz
);
INSERT INTO roles (name, description) VALUES ('public', 'The general public.');

CREATE TABLE user_roles (
role_id uuid REFERENCS roles(id) NOT NULL,
user_id bigint REFERENCES users(id) NOT NULL,

created_date timestamptz
);
54 changes: 54 additions & 0 deletions documentation/permission-system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Entity:Action Permission System

JournalHub uses an Entity:Action permission system where `action` is granted to
`user` on `entity`. This has a number of benefits and some costs.

The benefits are:

* Querying for permissions is fast, cheap, and easy.
* High level of flexibility in terms of what `action` we can define. Eg. `identify`
* Enables user defined permission models.

The costs are:

* Granting permissions is difficult and expensive. We need to make sure we're thorough.

## The Entities

The top level entities are defined by the controller/DAO combinations. Any
entity that has both a controller and DAO is considered a top-level entities.
Permissions can also be granted to sub-entities using `:` as a separator. For
example, to grant a permission on a Paper Author entity, you would use
`Paper:author`. Top level entities are always capitalized, while sub-entities
are always lower case.

Currently, the top level entities are:

* Field
* File
* Journal
* JournalSubmission
* Notification
* PaperComment
* Paper
* PaperEvent
* PaperVersion
* Review
* Token
* User

## Actions

The actions that may be granted correspond to the basic CRUD actions
and enable each of the REST endpoints.

They are:

* `create` enabling `POST`
* `read` enabling `GET`
* `update` enabling `PATCH`
* `delete` enabling `DELETE`

Additional actions that don't correspond to the basic CRUD are:

* `identify` allowing a user to identify an anonymous individual
82 changes: 82 additions & 0 deletions packages/backend/daos/DAO.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/******************************************************************************
*
* 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/>.
*
******************************************************************************/

export class DAO {

constructor(core) {
this.core = core
}


/**
* @return Promise<void>
*/
async insert(entityName, table, fieldMap, entities) {
if ( ! Array.isArray(entities) ) {
entities = [ entities ]
}

let columns = '('
for (const [field, meta] of Object.entries(fieldMap)) {
columns += ( columns == '(' ? '' : ', ') + field
}

if ( columns == '(' ) {
throw new DAOError('missing-fields',
`Empty field map sent to DAO::insert().`)
}

columns += ', created_date, updated_date)'

let rows = ''
let params = []
for(const entity of entities) {
let row = '('
for(const [field, meta] of Object.entries(fieldMap)) {
if ( meta.required && ! ( meta.key in entity ) ) {
throw new DAOError('missing-field',
`Required '${meta.key}' not found in ${entityName}.`)
}

params.push(( entity[meta.key] ? entity[meta.key] : null ))
row += ( row == '(' ? '' : ', ') + `$${params.length}`
}
row += ', now(), now())'

if ( rows !== '' ) {
rows += ', ' + row
} else {
rows += row
}
}


let sql = `
INSERT INTO ${table} ${columns}
VALUES ${rows}`

await this.core.database.query(sql, params)
}

async update(entityName, table, fieldMap, entities) {


}
}
167 changes: 167 additions & 0 deletions packages/backend/daos/PermissionDAO.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/******************************************************************************
*
* 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/>.
*
******************************************************************************/
import { DAO } from './DAO'

export class PermissionDAO extends DAO {

constructor(core) {
super(core)

this.fieldMap = {
'id': {
required: false,
key: 'id'
},
'entity': {
required: true,
key: 'entity'
},
'action': {
required: true,
key: 'action'
},
'user_id': {
required: false,
key: 'userId'
},
'role_id': {
required: false,
key: 'roleId'
},
'paper_id': {
rquired: false,
key: 'paperId'
},
'paper_version_id': {
required: false,
key: 'paperVersionId'
},
'event_id': {
required: false,
key: 'event_id'
},
'review_id': {
required: false,
key: 'review_id'
},
'paper_comment_id': {
required: false,
key: 'paperCommentId'
},
'submission_id': {
required: false,
key: 'submissionId'
},
'journal_id': {
required: false,
key: 'journalId'
}
}

}

/*
* @return {string}
*/
getPermissionsSelectionString() {
return `
permissions.id as "Permission_id",
permissions.entity as "Permission_entity",
permissions.action as "Permission_action",
permissions.user_id as "Permission_userId",
permissions.role_id as "Permission_roleId",
permissions.paper_id as "Permission_paperId",
permissions.paper_version_id as "Permission_paperVersionId",
permissions.event_id as "Permission_eventId",
permissions.review_id as "Permission_reviewId",
permissions.paper_comment_id as "Permission_paperCommentId",
permissions.submission_id as "Permission_submissionId",
permissions.journal_id as "Permission_journalId",
permissions.created_date as "Permission_createdDate",
permissions.updated_date as "Permission_updatedDate"
`
}

/**
* @return {any}
*/
hydratePermission(row) {
return {
id: row.Permissions_id,
entity: row.Permission_entity,
action: row.Permission_action,
userId: row.Permission_userId,
roleId: row.Permission_roleId,
paperId: row.Permission_paperId,
paperVersionId: row.Permission_paperVersionId,
eventId: row.Permission_eventId,
reviewId: row.Permission_reviewId,
paperCommentId: row.Permission_paperCommentId,
submissionId: row.Permission_submissionId,
journalId: row.Permission_journalId,
createdDate: row.Permission_createdDate,
updatedDate: row.Permission_updatedDate
}
}

/**
* @return dictionary: { [id: string]: any, list: any[]}
*/
hydratePermissions(rows) {
const dictionary = {}
const list = []

for(const row of rows) {
dictionary[row.Permission_id] = this.hydratePermission(row)
list.push(row.Permission_id)
}

return { dictionary: dictionary, list: list }
}

/**
* @param {string} where
* @param {any[]} params
*
* @return {Promise<any>}
*/
async selectPermissions(where, params) {
where = where ? `WHERE ${where}` : ''
params = params ? params : []

const sql = `
SELECT
${this.getPermissionsSelectionString()}
FROM permissions
LEFT OUTER JOIN user_roles ON user_roles.role_id = permissions.role_id
${where}
`

const results = await this.core.database.query(sql, params)
return this.hydratePermissions(results.rows)
}

/**
* @return {Promise<void>}
*/
async insertPermissions(permissions) {
await this.insert('Permission', 'permissions', this.fieldMap, permissions)
}
}
Loading