Skip to content

Commit

Permalink
Add room power levels to verify user in room response
Browse files Browse the repository at this point in the history
If Synapse has the related admin api (matrix-org/synapse#9168), fetch power levels and add them to the result of the verify user in room.

The response structure is the m.room.power_levels state event content with the users stripped out. The user level is found separately on the higher level to avoid confusion related to all the power levels for users in the room.
  • Loading branch information
jaywink committed Jan 21, 2021
1 parent a1b833f commit e710a47
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 8 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@

* Better documentation in readme.

* The `/verify/user_in_room` now also returns power levels of the room. In addition to
the user power level in the room returned are the levels required for various actions
in the room and default levels.

## v1.1.0

### Added
Expand Down
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Main features:
* Verifies a C2S [Open ID token](https://matrix.org/docs/spec/client_server/r0.6.1#id154)
using the S2S [UserInfo endpoint](https://matrix.org/docs/spec/server_server/r0.1.4#openid).
* Can verify user is a member in a given room (Synapse only currently, requires admin level token).
In addition to returning membership status, returned will be user power level, the room power
defaults and required power for events.

## How to use

Expand Down Expand Up @@ -147,7 +149,26 @@ Successful validation response:
"room_membership": true,
"user": true
},
"user_id": "@user:domain.tld"
"user_id": "@user:domain.tld",
"power_levels": {
"room": {
"ban": 50,
"events": {
"m.room.avatar": 50,
"m.room.canonical_alias": 50,
"m.room.history_visibility": 100,
"m.room.name": 50,
"m.room.power_levels": 100
},
"events_default": 0,
"invite": 0,
"kick": 50,
"redact": 50,
"state_default": 50,
"users_default": 0
},
"user": 50
}
}
```

Expand All @@ -159,7 +180,8 @@ Failed validation, in case token is not valid:
"room_membership": false,
"user": false
},
"user_id": null
"user_id": null,
"power_levels": null
}
```

Expand All @@ -171,7 +193,8 @@ In the token was validated but user is not in room, the failed response is:
"room_membership": false,
"user": true
},
"user_id": "@user:domain.tld"
"user_id": "@user:domain.tld",
"power_levels": null
}
```

Expand Down
19 changes: 16 additions & 3 deletions src/routes.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const {
getRoomPowerLevels,
sanityCheckRequest,
verifyOpenIDToken,
verifyRoomMembership,
Expand Down Expand Up @@ -59,28 +60,40 @@ const routes = {
logger.log('info', 'Request sanity check failed.', {requestId: req.requestId});
return;
}
// First verify token is ok
const tokenResult = await verifyOpenIDToken(req);
if (!tokenResult) {
res.send({
results: { user: false, room_membership: null },
user_id: null,
user_id: null, power_levels: null,
});
logger.log('info', 'User token check failed.', {requestId: req.requestId});
return false;
}
// Then verify room membership
// noinspection JSUnresolvedVariable
const membershipResult = await verifyRoomMembership(tokenResult, req);
if (!membershipResult) {
res.send({
results: { user: true, room_membership: false },
user_id: tokenResult,
user_id: tokenResult, power_levels: null,
});
logger.log('info', 'User verified but room membership check failed.', {requestId: req.requestId});
return;
}
// Then get power level if available
const powerLevelResult = await getRoomPowerLevels(tokenResult, req);
if (!powerLevelResult) {
logger.log('info', 'User and room membership verified, but failed to fetch power levels',
{requestId: req.requestId});
res.send({
results: { user: true, room_membership: true },
user_id: tokenResult, power_levels: null,
});
}
res.send({
results: { user: true, room_membership: true },
user_id: tokenResult,
user_id: tokenResult, power_levels: powerLevelResult,
});
logger.log('info', 'Token and room membership check out, user verified.', {requestId: req.requestId});
},
Expand Down
48 changes: 48 additions & 0 deletions src/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,53 @@ const {errorLogger, tryStringify} = require('./utils');

require('dotenv').config();

/**
* Fetch power levels for a room.
*
* Uses Synapse admin API. Returns an object of;
*
* `room` - the content of the state event `m.room.power_levels` but with `users` removed
* `user` - the power level of the user
*
* @param {string} userId Matrix user ID
* @param req Request object
* @returns {Promise<object|null>}
*/
async function getRoomPowerLevels(userId, req) {
let response;
const homeserverUrl = process.env.UVS_HOMESERVER_URL;
try {
const url = `${homeserverUrl}/_synapse/admin/v1/rooms/${req.body.room_id}/state`;
logger.log('debug', `Making request to: ${url}`, {requestId: req.requestId});
response = await axios.get(
url,
{
headers: {
Authorization: `Bearer ${process.env.UVS_ACCESS_TOKEN}`,
},
},
);
} catch (error) {
errorLogger(error, req);
return;
}
if (response && response.data && response.data.state) {
try {
const content = response.data.state.filter(o => o.type === 'm.room.power_levels')[0].content;
const userLevel = content.users[userId];
delete content.users;
return {
room: content,
user: userLevel,
};
} catch (error) {
logger.log('warn', `Failed to find power levels in state ${req.body.room_id}`, {requestId: req.requestId});
return;
}
}
logger.log('debug', `Failed to fetch power levels for room ${req.body.room_id}`, {requestId: req.requestId});
}

function sanityCheckRequest(req, res, fields=[]) {
if (!req.body) {
res.status(400);
Expand Down Expand Up @@ -119,6 +166,7 @@ async function verifyRoomMembership(userId, req) {
}

module.exports = {
getRoomPowerLevels,
sanityCheckRequest,
verifyOpenIDToken,
verifyRoomMembership,
Expand Down
56 changes: 54 additions & 2 deletions tests/routes.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,46 @@ describe('routes', function() {
await routes.postVerifyUserInRoom(req, res);

expect(res.send.firstCall.args[0]).to.deep.equal({
results: {user: true, room_membership: false}, user_id: '@user:synapse.local',
results: {user: true, room_membership: false},
user_id: '@user:synapse.local',
power_levels: null,
});
expect(res.send.calledOnce).to.be.true;
});

it('returns true and user ID on valid token', async function() {
axiosStub = sinon.stub(axios, 'get').onFirstCall().returns({data: {sub: '@user:synapse.local'}});
axiosStub.onSecondCall().returns({data: {members: ['@user:synapse.local']}});
axiosStub.onThirdCall().returns({data: { state: [
{
type: 'random.state_event',
},
{
type: 'm.room.power_levels', content: {
ban: 50,
events: {
'm.room.avatar': 50,
'm.room.canonical_alias': 50,
'm.room.history_visibility': 100,
'm.room.name': 50,
'm.room.power_levels': 100,
},
events_default: 0,
invite: 0,
kick: 50,
redact: 50,
state_default: 50,
users_default: 0,
users: {
'@user:synapse.local': 100,
'@user2:synapse.local': 50,
},
},
},
{
type: 'some.other.state_event',
},
]}});
let req = {
body: {
room_id: '!barfoo:synapse.local',
Expand All @@ -256,7 +288,27 @@ describe('routes', function() {
await routes.postVerifyUserInRoom(req, res);

expect(res.send.firstCall.args[0]).to.deep.equal({
results: {user: true, room_membership: true}, user_id: '@user:synapse.local',
results: {user: true, room_membership: true},
user_id: '@user:synapse.local',
power_levels: {
room: {
ban: 50,
events: {
'm.room.avatar': 50,
'm.room.canonical_alias': 50,
'm.room.history_visibility': 100,
'm.room.name': 50,
'm.room.power_levels': 100,
},
events_default: 0,
invite: 0,
kick: 50,
redact: 50,
state_default: 50,
users_default: 0,
},
user: 100,
},
});
expect(res.send.calledOnce).to.be.true;
});
Expand Down

0 comments on commit e710a47

Please sign in to comment.