Skip to content
Merged
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
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