Skip to content

Commit

Permalink
Test endpoints for modifying user roles (#664)
Browse files Browse the repository at this point in the history
  • Loading branch information
goto-bus-stop authored Nov 24, 2024
1 parent aa6e1cc commit b882eb2
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 8 deletions.
1 change: 1 addition & 0 deletions locale/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ uwave:
invalidResetToken: That reset token is invalid. Please double-check your reset token or request a new password reset.
incorrectPassword: The password is incorrect.
userNotFound: User not found.
roleNotFound: Role not found.
playlistNotFound: Playlist not found.
playlistItemNotFound: Playlist item not found.
itemNotInPlaylist: Item not in playlist.
Expand Down
12 changes: 6 additions & 6 deletions src/controllers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ async function addUserRole(req) {
const { id, role } = req.params;
const { acl, users } = req.uwave;

const selfHasRole = moderator.roles.includes('*') || moderator.roles.includes(role);
if (!selfHasRole) {
throw new PermissionError({ requiredRole: role });
const canModifyRoles = moderator.roles.includes('admin');
if (!canModifyRoles) {
throw new PermissionError({ requiredRole: 'admin' });
}

const user = await users.getUser(id);
Expand Down Expand Up @@ -128,9 +128,9 @@ async function removeUserRole(req) {
const { id, role } = req.params;
const { acl, users } = req.uwave;

const selfHasRole = moderator.roles.includes('*') || moderator.roles.includes(role);
if (!selfHasRole) {
throw new PermissionError({ requiredRole: role });
const canModifyRoles = moderator.roles.includes('admin');
if (!canModifyRoles) {
throw new PermissionError({ requiredRole: 'admin' });
}

const user = await users.getUser(id);
Expand Down
7 changes: 7 additions & 0 deletions src/errors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ const UserNotFoundError = createErrorClass('UserNotFoundError', {
base: NotFound,
});

const RoleNotFoundError = createErrorClass('RoleNotFoundError', {
code: 'role-not-found',
string: 'errors.roleNotFound',
base: NotFound,
});

const PlaylistNotFoundError = createErrorClass('PlaylistNotFoundError', {
code: 'playlist-not-found',
string: 'errors.playlistNotFound',
Expand Down Expand Up @@ -288,6 +294,7 @@ export {
ReCaptchaError,
IncorrectPasswordError,
UserNotFoundError,
RoleNotFoundError,
PlaylistNotFoundError,
PlaylistItemNotFoundError,
HistoryEntryNotFoundError,
Expand Down
11 changes: 9 additions & 2 deletions src/plugins/acl.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { sql } from 'kysely';
import defaultRoles from '../config/defaultRoles.js';
import routes from '../routes/acl.js';
import { jsonb, jsonEach } from '../utils/sqlite.js';
import { isForeignKeyError, jsonb, jsonEach } from '../utils/sqlite.js';
import { RoleNotFoundError } from '../errors/index.js';

/**
* @typedef {import('../schema.js').User} User
Expand Down Expand Up @@ -124,7 +125,13 @@ class Acl {
role: roleName,
})))
.returningAll()
.execute();
.execute()
.catch((err) => {
if (isForeignKeyError(err)) {
throw new RoleNotFoundError();
}
throw err;
});

this.#uw.publish('acl:allow', {
userID: user.id,
Expand Down
8 changes: 8 additions & 0 deletions src/utils/sqlite.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,11 @@ export async function connect(path) {
});
return db;
}

/**
* @param {unknown} err
* @returns {err is (Error & { code: 'SQLITE_CONSTRAINT_FOREIGNKEY' })}
*/
export function isForeignKeyError(err) {
return err instanceof Error && 'code' in err && err.code === 'SQLITE_CONSTRAINT_FOREIGNKEY';
}
118 changes: 118 additions & 0 deletions test/acl.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import supertest from 'supertest';
import * as sinon from 'sinon';
import createUwave from './utils/createUwave.mjs';

const FAKE_USER_ID = '4bdfdf14-df8d-4929-a49c-8fbe6cbe3f90';

describe('ACL', () => {
let user;
let uw;
Expand Down Expand Up @@ -218,4 +220,120 @@ describe('ACL', () => {
assert(!await uw.acl.isAllowed(user, 'test.permission2'));
});
});

describe('PUT /users/:id/roles/:role', () => {
it('requires authentication', async () => {
await supertest(uw.server)
.put(`/api/users/${FAKE_USER_ID}/roles/testRole`)
.expect(401);
});

it('returns 404 for nonexistent user', async () => {
await uw.acl.createRole('testRole', ['test.permission']);
await uw.acl.allow(user, ['admin']);

const token = await uw.test.createTestSessionToken(user);
const res = await supertest(uw.server)
.put(`/api/users/${FAKE_USER_ID}/roles/testRole`)
.set('Cookie', `uwsession=${token}`)
.expect(404);
sinon.assert.match(res.body, {
errors: sinon.match.some(sinon.match.has('code', 'user-not-found')),
});
});

it('returns 404 for nonexistent role', async () => {
await uw.acl.allow(user, ['admin']);

const otherUser = await uw.test.createUser();

const token = await uw.test.createTestSessionToken(user);
const res = await supertest(uw.server)
.put(`/api/users/${otherUser.id}/roles/nonexistentrole`)
.set('Cookie', `uwsession=${token}`)
.expect(404);
sinon.assert.match(res.body, {
errors: sinon.match.some(sinon.match.has('code', 'role-not-found')),
});
});

it('adds the role', async () => {
await uw.acl.allow(user, ['admin']);

const otherUser = await uw.test.createUser();

const token = await uw.test.createTestSessionToken(user);
const beforeRes = await supertest(uw.server)
.get(`/api/users/${otherUser.id}`)
.set('Cookie', `uwsession=${token}`)
.expect(200);
sinon.assert.match(beforeRes.body.data, {
roles: [],
});

await supertest(uw.server)
.put(`/api/users/${otherUser.id}/roles/user`)
.set('Cookie', `uwsession=${token}`)
.expect(200);

const afterRes = await supertest(uw.server)
.get(`/api/users/${otherUser.id}`)
.set('Cookie', `uwsession=${token}`)
.expect(200);
sinon.assert.match(afterRes.body.data, {
roles: ['user'],
});
});
});

describe('DELETE /users/:id/roles/:role', () => {
it('requires authentication', async () => {
await supertest(uw.server)
.delete(`/api/users/${FAKE_USER_ID}/roles/testRole`)
.expect(401);
});

it('returns 404 for nonexistent user', async () => {
await uw.acl.createRole('testRole', ['test.permission']);
await uw.acl.allow(user, ['admin']);

const token = await uw.test.createTestSessionToken(user);
const res = await supertest(uw.server)
.delete(`/api/users/${FAKE_USER_ID}/roles/testRole`)
.set('Cookie', `uwsession=${token}`)
.expect(404);
sinon.assert.match(res.body, {
errors: sinon.match.some(sinon.match.has('code', 'user-not-found')),
});
});

it('removes the role', async () => {
await uw.acl.allow(user, ['admin']);

const otherUser = await uw.test.createUser();
await uw.acl.allow(otherUser, ['user', 'moderator']);

const token = await uw.test.createTestSessionToken(user);
const beforeRes = await supertest(uw.server)
.get(`/api/users/${otherUser.id}`)
.set('Cookie', `uwsession=${token}`)
.expect(200);
sinon.assert.match(beforeRes.body.data, {
roles: ['moderator', 'user'],
});

await supertest(uw.server)
.delete(`/api/users/${otherUser.id}/roles/user`)
.set('Cookie', `uwsession=${token}`)
.expect(200);

const afterRes = await supertest(uw.server)
.get(`/api/users/${otherUser.id}`)
.set('Cookie', `uwsession=${token}`)
.expect(200);
sinon.assert.match(afterRes.body.data, {
roles: ['moderator'],
});
});
});
});

0 comments on commit b882eb2

Please sign in to comment.