GSOC 2023: Administration System - Bookbrainz

Personal information

Nickname: Shivam
IRC nick: ShivamAwasthi
GitHub: the-good-boy

Proposal

Project Overview
BookBrainz currently has no administration system, or any good way of giving users special privileges. This certainly needs to change!
This project involves devising and implementing a basic admin system allowing for a flexible privilege hierarchy.

Features

• Admins can give special privileges to certain editors, from an Admin Panel which allows to search for other editors.
• Admins can block/unblock certain abusive users from the aforementioned Admin panel.
• These privileged editors can edit or add relationship types and identifier types.
• There will be public log of all these administrative actions.
• Privileged Editors will be able to view all the available identifier types and relationship types, and edit them if necessary.
• Privileged Editors can also trigger a reindex of the search server

Schema Changes
We can add a column named privileges to the table bookbrainz.editor. We’ll also have to create a table for saving the logs.

bookbrainz.admin_log

CREATE TABLE bookbrainz.admin_log (
    id SERIAL PRIMARY KEY,
    admin_id INT FOREIGN KEY REFERENCES bookbrainz.editor (id),
    user_id INT FOREIGN KEY REFERENCES bookbrainz.editor (id),
    action_type int NOT NULL,
    new_bit_value int NOT NULL,
    time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT timezone('UTC'::TEXT, now()),
    note VARCHAR NOT NULL
);

Here action_type will correspond to the bit of the privilege which has been changed by the admin.
The column new_bit_value will tell whether the aforementioned privilege has been set or unset. These columns will help us in creating a statement for the admin-logs.

Privileges and Middleware:

Let us consider the following privilege types defined as constants:

const ACCESS_THE_ENTITY_EDITOR_FLAG = 1;
const ACCESS_THE_IDENTIFIER_TYPE_EDITOR_FLAG = 2;
const ACCESS_THE_RELATIONSHIP_TYPE_EDITOR_FLAG = 4;
const REINDEX_THE_SEARCH_SERVER_FLAG = 8;
const CAN_GIVE_PRIVILEGES_FLAG = 16;

For the sake of simplicity, let us take the following ideal assumptions:

  • An editor will have a default value of privileges = 1 (that is, an editor, by default, does not have any special privileges)
  • An admin will have a default value of privileges = 31 (that is, an editor which has all the privileges, including the privilege to grant privileges)

A brief explanation of how we can use the bits:

Consider the route below:

router.get('/identifier-type/create', auth.isAuthenticated, auth.isAuthorized(constants.ACCESS_THE_IDENTIFIER_TYPE_EDITOR_FLAG), (req, res) => {
  // Relevant code here
})

The isAuthorized middleware can check whether the req.user.privileges has the 2nd bit set or not. Something like this::

export function isAuthorized(flag) {
  return (req, res, next) => {
    if(req.user.privileges & flag){
    return next();
  }
  throw new error.PermissionDeniedError(
  'You do not have permission to access this route', req
  );
}

We can flip/unflip a particular bit using the XOR operation.

Mockups

We can create a dropdown in our Navbar named Privileges which, for an admin, would appear something like this:

The option to reach Admin Panel will only be for someone with the 5th bit set true (someone who can give or take privileges to users, that is).
This Admin Panel will allow us to search for users and modify their privileges.

Next to each user, there will be a button which will open a modal which will then allow the admins to perform a valid action and optionally write reason/note associated with that action.

Admins can also perform these actions from the profile page of a user.

All of these admin actions will go into a public log :

This system will allow us to give some editors the privilege to edit and add relationship types and identifier types. There will be a Relationship Types page('/relationship-types'), which will list all the relationship types in a tabular format. There will be an edit button next to these types, which will take us to the Relationship Type Editor. There will also be an option to add a new Relationship Type which will lead to a form which will look something like this:

Similar workflow will be there in case of Identifier-Types too.

Routes:
At the very least, we’ll have to create the following routes:

Identifier Type Routes:

  • GET '/identifer-types'
  • GET '/identifier-type/create'
  • POST '/identifier-type/create/handler'
  • GET '/identifier-type/:id/edit'
  • POST '/identifier-type/:id/edit/handler
  • POST '/identifier-type/:id/delete/handler'

Relationship Type Routes:

  • GET '/relationship-types'
  • GET '/relationship-type/create'
  • POST '/relationship-type/create/handler'
  • GET '/relationship-type/:id/edit'
  • POST '/relationship-type/:id/edit/handler'
  • POST '/relationship-type/:id/delete/handler'

Others:

  • GET '/admin-panel'
  • GET '/admin-logs'

TIMELINE

Community Bonding Period:
I will spend this time working on some of the tickets which I am already assigned and some other bug-fixes here and there.

Week 1
The database schema changes and their corresponding models will be written in the first week.

Week 2 and 3:
I would like to finish the routes and frontend logic for administrative actions and admin panel. The corresponding tests will be written alongside.

Week 3 and 4:
I would like to write the middleware which will then give role-specific access to certain pages. The admin-logs page will also be created.

Week 5:
Backend for type-editors .

Week 6:
Frontend for type-editors .

Week 7 and 8:
Tests for type-editors .

Week 9 and 10:
The types page which will list all the types, and the edit functionality will also be done in this period.

STRETCH GOAL
I think I will be able to complete the project comfortably within the time-period. I would like to work on adding more flexible attributes to relationships such as Date/Time.

Other Information

  • Tell us about the computer(s) you have available for working on your SoC project!
    I have a Legion Y530 with an i5 processor and 8 gigs of RAM.

  • When did you first start programming?
    I started programming when I was in class 6th.

  • What type of music do you listen to?
    I mostly listen to alt-rock and rock music. (twenty one pilots - MusicBrainz)

  • What aspects of the project you’re applying for (e.g., MusicBrainz, AcousticBrainz, etc.) interest you the most?
    I’ve been doing some contributions to Bookbrainz since December 2020. I like the community and the discussions focussed on improving the project as a whole.

  • Have you ever used MusicBrainz to tag your files?
    No. I still have to give it a try.

  • Have you contributed to other Open Source projects?
    Yes, I have been contributing to Bookbrainz since December 2020. I have made contributions on all fronts, be it frontend, backend, documentation, some design changes(diagrams), tests, etc. My PRs: Check out

  • How much time do you have available, and how would you plan to use it?
    I think I will be able to give 20 hours a week. I will be completely free for most part of this project duration. Even when I’m not completely free, I will have enough time during the weekends to finish everything in a timely manner.

3 Likes

Thanks for the proposal @ShivamAwasthi !

I have a few bits of feedback:

I’m worried that the proposed editor types could prevent us from having the flexibility we might need in the future.
I’ll copy comments I made on other proposals with a similar structure:

We are bound to end up with more complicated use-cases and combinations of roles.
Can I be an admin but not have some data modification rights (relationships types etc.)?
Can I have some admin rights, but not the right to delete users?
Can I be a privileged user that can edit relationship types, but not have the right to reindex?
What if we want to prevent a user from entering edits?

In short, the system as proposed is not very extensible/future-proof.

I know MusicBrainz uses another type of system to define permissions with bit masking I believe. Have a look at the flags they can set: musicbrainz-server/constants.js at master · metabrainz/musicbrainz-server · GitHub
You can ask someone from the MusicBrainz team in the #metabrainz IRC channel who could have more information about how this is used and set.
Consequently, the database tables and the middleware would be different.
For one, we would only need a numeric column on the user table to define multiple privileges, since the numeric flags can be combined.


More details on the admin panel implementation would be useful.

What other routes would you need for the admin panel? You only mention /identifier-type/create

I’m not sure I get the getRank function. Why not use the ROLE const (which should be a typescript enum probably) that you defined above for the route?

1 Like

Thanks for the feedback, @mr_monkey !
Here’s an attempt to improve the proposal.

We can add a column named privileges to the table bookbrainz.editor.

Let us consider the following privilege types defined as constants:

const ACCESS_THE_ENTITY_EDITOR_FLAG = 1;
const ACCESS_THE_IDENTIFIER_TYPE_EDITOR_FLAG = 2;
const ACCESS_THE_RELATIONSHIP_TYPE_EDITOR_FLAG = 4;
const REINDEX_THE_SEARCH_SERVER_FLAG = 8;
const CAN_GIVE_PRIVILEGES_FLAG = 16;

For the sake of simplicity, let us take the following ideal assumptions:

  • An editor will have a default value of privileges = 1 (that is, an editor, by default, does not have any special privileges)
  • An admin will have a default value of privileges = 31 (that is, an editor which has all the privileges, including the privilege to grant privileges)

A brief explanation of how we can use the bits:

Consider the route below:

router.get('/identifier-type/create', auth.isAuthenticated, auth.isAuthorized(constants.ACCESS_THE_IDENTIFIER_TYPE_EDITOR_FLAG), (req, res) => {
  // Relevant code here
})

The isAuthorized middleware can check whether the req.user.privileges has the 2nd bit set or not. Something like this:

export function isAuthorized(flag) {
  return (req, res, next) => {
    if(req.user.privileges & flag){
    return next();
  }
  throw new error.PermissionDeniedError(
  'You do not have permission to access this route', req
  );
}

We can flip/unflip the a particular bit using the XOR operation.

Privileges Dropdown
We can create a dropdown in our Navbar named Privileges which, for an admin, would appear something like this:

Privileges

Let me talk about the Identifier Types page first. ('/identifier_types')

This route will list all the Identifier Types in a table. We can click on an edit button to edit a particular identifier type ('/identifier_type/:id/edit').
There will also be a button to add a new Identifier Type which will take us to the Identifier Type Editor. ('/identifier_type/create').
A very similar workflow will be in the case of Relationship Editors too.

Let us talk about the Admin Panel now.:

The option to reach Admin Panel will only be for someone with the 5th bit set true (someone who can give or take privileges to users, that is).
This Admin Panel will allow us to search for users. Next to each user there will be a button which opens a modal which will allow us to select from a list of valid admin actions we can perform
on this user. So there will be an option to set/unset a privilege for the user. There will also be the option to Block/Unblock the user.

If we block the user, the privileges will be set to 0 for that user.
While choosing the Block action, we can add an optional note to indicate the reason for blocking.

At the very least, we’ll have to create the following routes:

Identifier Type Routes:

  • router.get('/identifer_types')
  • router.get('/identifier_type/create')
  • router.post('/identifier_type/create/handler')
  • router.get('/identifier_type/:id/edit')
  • router.post('/identifier_type/:id/edit/handler)
  • router.post('/identifier_type/:id/delete/handler')

Relationship Type Routes:

  • router.get('/relationship_types')
  • router.get('/relationship_type/create')
  • router.post('/relationship_type/create/handler')
  • router.get('/relationship_type/:id/edit')
  • router.post('/relationship_type/:id/edit/handler')
  • router.post('/relationship_type/:id/delete/handler')

Others:

  • router.get('/admin_panel')
  • router.get('/admin_logs')
1 Like

I have tweaked some of the mockups, and made some changes to the bookbrainz.admin_log table to better fit the bitmask approach for privileges.