Dan Kuida Dan Kuida - 4 months ago 22
Node.js Question

PubNub security groups management

We are building an application that we decided to use pubnub as the transport for the notifications and the chat.
here are the demands


  • the users should be able to communicate directly between them,

  • only the two users discussing should be able to see the conversation

  • I assume that both the publish and the subscribe key are compromised

  • the server should be able to send notifications for each user

  • user should be able to see only his notifications



** at some point i realised that you cannot use two set of keys to publish to same channel - as a set of keys identifies the channel - so a server master key set and client no level key is not an option **

So on top of that and by using pam, here is the flow for notifications

Server using the secret key revokes any permissions from the pub/sub keys on the global level
{read: false, write: false, manage:false}
this way nobody can do anything on the global level with the keys

user sends a token - and this token becomes the authKey - only with that auth key a user can read only on the notification channel ( called 'notif-{userId}')

now the server needs to have some kind of masterKey that he can publish to all the channels - so the logical thing to do is to issue a .grant() request with { read: true, write: true, manage: true, authKey: MASTER_KEY}
here we fail - because it responds that "Auth-only grants are reserved for future use"

now for the chats the idea is to create a channel names 'chat-{userId1}-{userId2}' and to add this channel to group 'chats-{userId1}' and to 'chats-{userId2}' and than to .grant() the permission based on the token to each of the users channels - the grant succeedes BUT the user subscribe to the channel group - but the actual publish to the group fails with an
status : {
category : "PNAccessDeniedCategory",
operation: "PNPublishOperation"
}

here is the code sample to replicate the issue

'use strict';

const pubnubConf = require('../config/pubnub-master');

const PubNub = require('pubnub');
const masterKeys = pubnubConf.getMasterKeys();
const pubnub = new PubNub(masterKeys);
const pubnubSecret = new PubNub(pubnubConf.getSecretKeys());

const token = 'lklkdjwdq';

masterKeys.authKey = token;
const pubnubClient = new PubNub(masterKeys);
const userAGroup = 'chats-aaa';
const userBGroup = 'chats-bbb';
const chat = 'chat-aaa-bbb';


function _invalidateServerKeys () {
return new Promise((resolve, reject) => {
pubnubSecret.grant({
read: false,
write: false,
manage: false,
ttl: 0
}, (status, response) => {
if (status.error) {
reject(status);
}
resolve(status);
});
});
}


function _setMasterKeyForChannelGroup () {
return new Promise((resolve, reject) => {
pubnubSecret.grant({
read: true,
write: true,
manage: true,
channels: [chat],
channelGroups: [userAGroup, userBGroup],
authKeys: [pubnubConf.getMasterKey()],
ttl: 0
}, (status, response) => {
if (status.error) {
this._logger.fatal({
status: status,
response: response
});
reject(status);
}
resolve(status);
});
});
}

function _addChanelsToGroup () {
return new Promise((resolve, reject) => {
pubnub.channelGroups.addChannels({
channels: [chat],
channelGroup: [userBGroup]
}, status => {
if (status.error) {
this._logger.fatal(status);
return reject();
}
resolve(status);
});
});
}

function _addTokenPermission () {
return new Promise((resolve, reject) => {
pubnubSecret.grant({
channelGroups: [userBGroup],
authKeys: [token],
ttl: 65,
read: true,
write: true,
manage: false
}, (status, response) => {
if (status.error) {
return reject({
status: status,
response: response
});
}
resolve();
});
});
};

function _subscribeToGroup () {
return new Promise((resolve, reject) => {
pubnubClient.addListener({
status: statusEvent => {
if (statusEvent.error) {
reject(statusEvent);
return;
}
resolve(statusEvent);
},
message: message => {
}
});
pubnubClient.subscribe(
{channelGroups: [userBGroup]});
});
}

function _publishToGroup () {
pubnubClient.publish(
{
message: {
such: 'object'
},
channel: chat,
storeInHistory: false
},
function (status, response) {
if (status.error) {
// handle error
console.log(status)
} else {
console.log("message Published w/ timetoken", response.timetoken)
}
}
);
}

_invalidateServerKeys()
.then(_setMasterKeyForChannelGroup)
.then(_addChanelsToGroup)
.then(_addTokenPermission)
.then(_subscribeToGroup)
.then(_publishToGroup);





const masterAuthKey = 'qdwqqdwdqwqdwqdw';


module.exports = {
getSecretKeys: () => ({
ssl: true,
logVerbosity: true,
publishKey: 'pub-c-',
subscribeKey: 'sub-c-',
secretKey: 'sec-c-'
}),
getMasterKeys: () => ({
ssl: true,
logVerbosity: true,
publishKey: 'pub-c-',
subscribeKey: 'sub-c-',
authKey: masterAuthKey
}),
getMasterKey: () => (masterAuthKey)
};


Of course the auth only PAM would solve that - but it does not seem as usable
the other way would be manage a server side key to each channel - but that would be kind of a waste.

Answer

PubNub Access Manager Best Practices

There is quite a bit here so I will just address the things that are in error or not quite accurate or could be done in a better way.

Revoking permissions at the global level

The default when you enable Access Manager is that all permissions are revoked for everyone. What you are doing here: pubnubSecret.grant({read: false, write: false, manage: false, ttl: 0}, is just revoking any permissions that might have been granted at the sub-key (app) level but would not revoke any auth-key or channel level (they are hierarchical in nature, sort of like CSS overrides but in reverse). But it is harmless and actually a good safe guard in case some one (internal developer) accidentally grants at the sub-key level.

Server needs to have some kind of masterKey

The issue you had with granting to just an auth-key is intended behavior but I see what you want from that - a global root access auth-key. Currently, you either grant permissions to channels for an auth-key or to no auth-key, but you cannot grant permissions to nothing (no channels or channel grups) to an auth-key. But you can grant access at the channel level (no auth-key) or to the sub-key level (no channel and no auth-key) so that anyone can have access with no auth-key (as explained above).

Granting root access to do everything is a missing feature that will be added in the future. For now you have to do two different grants allow the server to have an auth-key that will allow it to read, write and manage everything without granting for each and every new channel that is created.

Wildcard Channel Group Manage

First, grant wildcard permission to your server's auth-key to manage all channel groups using the colon (:) as the wildcard (don't ask, but it has to do with the deprecated namespaces that channel groups used to have).

pubnubSecret.grant({authKey: serverAuthKey, channelGroups:[':'], manage: true, ttl: 0}

That's it and your server can now add/remove channels to any channel group you ever create.

Wildcard Channel Read & Write

With channels, it is a bit different as there is no grant read/write to all channels wildcard like there is for managing channel groups - at least not at the root level. What I mean by that is if you use a channel prefix for all channel names, like notif., so that channels would have names like notif.user123, notif.user326, and such, then you can grant read/write permissions to your server's auth-key on channel notif.*.

pubnubSecret.grant({channels:['notif.*'], read: true, write: true, ttl: 0}

Now this means that if you have any channels that do not have the notif.* prefix, then they will not fall under this wildcard grant. And you cannot grant to notif.user123.*. It is to the second level/segment of the wildcard name only.

Chat Channels and Channel Groups

You say you are creating a unique channel for two users to talk to each other: chat-{userId1}-{userId2}. Based on the above advice, prefix it with notif. so that your server has read/write access to it, if it needs this sort of access.

And you indicate that you are creating channel groups for each user and adding the channel to each user's channel group, which will allow each user to receive messages published to that channel. But you either erroneously or mistakenly said that you are attempting to publish to the channel group, but channel groups are for subscribe only, not publish (i.e. you can't publish to a channel group). And never grant manage on a channel group to a user because this will allow a malicious user to add any channel to the channel group and have read access to that channel.

The right thing to do is:

  1. grant manage to the channel groups to the server only - you have already accomplished this with the wildcard channel group grant with the colon wildcard - so, done.
  2. grant read access to each user on their own private channel group: chats-{userId1}, chats-{userId2}, etc., and each user will subscribe to their own channel group.
  3. the server will generate the chat-user1-user2 channel and add that channel to each user's channel group and they will instantly be subscribed to the new chat channel.
  4. the server will grant write permission on the chat-user1-user2 channel to each of the users' auth-keys: auth-key-user1 and auth-key-user1. You can grant the same permissions to the same set of channels to multiple auth-keys at the same time.

Summary

  • wildcard grant manage for channel groups to the server using the ':'
  • wildcard grant read/write to all channels using notif.* channel to the server
  • grant read to each user on the user's private channel group
  • grant write to the two users on their shared chat channel
  • add the shared chat channel to each users' private channel group

I think that just about wraps up all the issues you were experiencing but let me know if anything is unclear, failed to address something in your question or you have additional questions.

Response to Questions in Comments

Is '.' allowed in channel names?

Yes, it is not officially invalid, but rather, reserved. So the caution is do not use the dot char as a delimiter without thinking about wildcard implications (both grant and subscribe). This is why I recommended the dot in your use case so that you can grant to notifi.* for the server to have read/write access to all channels created with that naming convention. Here's some additional details about reserved/invalid characters.

Channel and Channel Group names are UTF-8 compatible and are limited to 92 characters in length. There are reserved characters that should not be used (but can successfully be used in some scenarios) as channel or channel group names.

  • period: '.' (ok for channels but not channel groups; should only be used in channels when planning to use wildcard grants and/or wildcard subscribe)
  • slash: '/'
  • backslash: '\'
  • comma: ','
  • colon: ':'
  • space: ' '

Is there a command to generate channel name?

No, but generate (maybe too strong a term) I mean that your server is the one that creates the channel name. It pretty much has to since it needs to know what that name is in order to grant write permissions to the clients' auth-keys in order to publish messages on it. And the server needs to add the channel to the users' channel groups, too. So it might as well be the one that creates the channel name.

How you generate the channel name is up to you. Typically, developers just using a UUID generator to create a dynamic channel name. I think you are going for a predictable channel name so that each client can easily know what the channel name is by knowing the userid of the other party that they are chatting with. Just be sure that you lexicographically order the user ids for the channel name so you don't get them backwards: chat-userabc-userxyz instead of chat-userxyz-userabc (you probably already figured that out but wanted to mention it for the benefit of all that read this). Don't forget to prefix the channel name with notif. if you want your server to have automatic read & write access (based on the wildcard grant to notif.* recommendation).