PubNub 安全组管理

PubNub security groups management

我们正在构建一个应用程序,我们决定使用 pubnub 作为通知和聊天的传输方式。 这是要求

** 在某些时候我意识到你不能使用两组密钥发布到同一个频道 - 因为一组密钥标识频道 - 所以服务器主密钥集和客户端无级别密钥不是一个选项**

除此之外,通过使用 pam,这里是通知流程

使用密钥的服务器在全局级别撤销了 pub/sub 密钥的任何权限 {读:假,写:假,manage:false} 这样就没有人可以在全球范围内使用密钥

做任何事情

用户发送一个令牌 - 这个令牌成为 authKey - 只有使用那个 auth 密钥,用户才能在通知通道(称为 'notif-{userId}')上只读

现在服务器需要某种可以发布到所有频道的 masterKey - 所以合乎逻辑的做法是发出 .grant() 请求{ 读:真,写:真,管理:真,authKey:MASTER_KEY} 这里我们失败了 - 因为它响应 "Auth-only grants are reserved for future use"

现在对于聊天来说,想法是创建一个频道名称 'chat-{userId1}-{userId2}' 并将此频道添加到组 'chats-{userId1}' 和 'chats-{userId2}' 以及 .grant() 权限基于每个用户频道的令牌 - 授权成功但用户订阅频道组 - 但实际发布到组失败并出现 状态 : { 类别:"PNAccessDeniedCategory", 操作:"PNPublishOperation" }

这里是复制问题的代码示例

    '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)
};

当然,仅授权 PAM 可以解决这个问题 - 但它似乎不可用 另一种方法是管理每个频道的服务器端密钥——但这有点浪费。

PubNub 访问管理器最佳实践

这里有很多,所以我将只解决错误或不太准确或可以用更好的方式完成的事情。

在全局级别撤消权限

启用访问管理器时的默认设置是取消所有人的所有权限。你在这里做什么:pubnubSecret.grant({read: false, write: false, manage: false, ttl: 0},只是撤销可能在子密钥(应用程序)级别授予的任何权限,但不会撤销任何授权密钥或通道级别(它们本质上是分层的,排序像 CSS 覆盖但相反)。但它是无害的,实际上是一个很好的保护措施,以防某些人(内部开发人员)不小心在子密钥级别授权。

服务器需要某种主密钥

您在授予授权密钥时遇到的问题是预期的行为,但我明白您想要的是什么 - 全局根访问授权密钥。目前,您要么为授权密钥授予通道权限,要么没有授权密钥,但不能向授权密钥授予 nothing(无通道或通道组)权限。但是您可以在通道级别(无授权密钥)或子密钥级别(无通道和无授权密钥)授予访问权限,以便任何人都可以在没有授权密钥的情况下进行访问(如上所述)。

授予 root 访问权限以执行所有操作是一项缺失的功能,将来会添加。现在你必须做两个不同的授权,允许服务器有一个授权密钥,允许它读取、写入和管理所有内容,而不需要为每个创建的新通道授权。

通配符频道组管理

首先,使用冒号 (:) 作为通配符(不要问,但必须使用频道组曾经拥有的已弃用的名称space。

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

就是这样,您的服务器现在可以 add/remove 频道到您创建的任何频道组。

通配符通道读写

对于频道,它有点不同,因为没有授予 read/write 所有频道通配符,就像管理频道组一样 - 至少在 root 等级。我的意思是,如果您为所有频道名称使用频道前缀,例如 notif.,那么频道的名称将类似于 notif.user123notif.user326 等,那么您可以授予read/write 对您服务器在频道 notif.* 上的授权密钥的权限。

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

现在这意味着如果您有任何频道没有 notif.* 前缀,那么它们将不属于此通配符授权范围。而且您不能授予 notif.user123.*。仅适用于通配符名称的第二个 level/segment。

聊天频道和频道组

你说你正在为两个用户创建一个独特的频道来相互交谈:chat-{userId1}-{userId2}。根据上述建议,在它前面加上 notif. 以便您的服务器可以 read/write 访问它,如果它需要这种访问。

并且您表示您正在为每个用户创建频道组并将频道添加到每个用户的频道组,这将允许每个用户接收发布到该频道的消息。但是您错误地或错误地表示您正在尝试 publish 到频道组,但频道组仅用于订阅,而不是发布(即您不能 publish 到频道组)。永远不要向用户授予频道组 manage 权限,因为这将允许恶意用户将任何频道添加到频道组并拥有对该频道的 read 访问权限。

正确的做法是:

  1. 仅向服务器的通道组授予 manage - 您已经使用带有冒号通配符的通配符通道组授权完成了此操作 - 所以,完成了。
  2. 授予read每个用户对他们自己的私人频道组的访问权限:chats-{userId1}chats-{userId2}等,每个用户将订阅他们自己的频道组。
  3. 服务器将生成 chat-user1-user2 频道并将该频道添加到每个用户的频道组,他们将立即订阅新的聊天频道。
  4. 服务器将在 chat-user1-user2 频道上授予 write 权限给每个用户的授权密钥:auth-key-user1auth-key-user1。您可以同时将同一组频道的相同权限授予多个授权密钥。

总结

  • 通配符授予 manage 通道组到服务器使用 ':'
  • 通配符授予 read/write 所有使用 notif.* 通道到服务器的通道
  • 在用户的私人频道组
  • 上向每个用户授予read
  • 在共享聊天频道
  • 上向两个用户授予write
  • 将共享聊天频道添加到每个用户的私人频道组

我认为这几乎涵盖了您遇到的所有问题,但如果有任何不清楚的地方、未能解决您问题中的某些问题或您还有其他问题,请告诉我。

回复评论中的问题

是'.'是否允许在频道名称中使用?

是的,不是正式失效,而是保留。所以警告是不要在不考虑通配符含义(授予和订阅)的情况下使用点字符作为分隔符。这就是为什么我建议在您的用例中使用点,以便您可以授予 notifi.* 权限,以便服务器可以 read/write 访问使用该命名约定创建的所有频道。这里有一些关于 reserved/invalid 个字符的额外细节。

频道和频道组名称与 UTF-8 兼容,长度限制为 92 个字符。有些保留字符不应该用作(但在某些情况下可以成功使用)作为通道或通道组名称。

  • 句号: '.' (适用于频道但不适用于频道组;仅应在计划使用通配符授权时在频道中使用 and/or wildcard subscribe
  • 斜杠: '/'
  • 反斜杠: '\'
  • 逗号:','
  • 冒号: ':'
  • space: ' '

是否有生成频道名称的命令?

不,但是生成(可能是一个太强的术语)我的意思是您的服务器是创建频道名称的服务器。它几乎必须这样做,因为它需要知道该名称是什么,以便授予 write 对客户端的 auth-keys 的权限,以便在其上发送 publish 消息。服务器也需要将频道添加到用户的频道组中。所以它也可能是创建频道名称的那个。

如何生成频道名称由您决定。通常,开发人员只是使用 UUID 生成器来创建动态频道名称。我认为您正在寻找一个可预测的频道名称,以便每个客户都可以通过了解他们正在与之聊天的另一方的用户 ID 轻松知道频道名称是什么。请确保您按字典顺序对频道名称的用户 ID 进行排序,这样您就不会将它们倒过来:chat-userabc-userxyz 而不是 chat-userxyz-userabc(您可能已经想到了这一点,但想提及它是为了方便所有读过这篇文章的人)。如果您希望您的服务器具有自动 readwrite 访问权限(基于对 notif.* 建议的通配符授予),请不要忘记在频道名称前加上 notif. 前缀。