Skip to content
Open
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
13 changes: 13 additions & 0 deletions pubnub/endpoints/access/grant.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def __init__(self, pubnub):
self._channels = []
self._groups = []
self._uuids = []
self._categories = []
self._read = None
self._write = None
self._manage = None
Expand Down Expand Up @@ -54,6 +55,15 @@ def channel_groups(self, channel_groups):
utils.extend_list(self._groups, channel_groups)
return self

def categories(self, categories):
""" Grants category-level (enumeration) permissions on a whole App Context resource type.

Supported values are `channels` and `uuids`. Requires an auth key and allows
only the `get` permission; the server rejects anything else. Revoke with `get(False)`.
"""
utils.extend_list(self._categories, categories)
return self

def read(self, flag):
self._read = flag
return self
Expand Down Expand Up @@ -118,6 +128,9 @@ def custom_params(self):
if self._uuids:
params['target-uuid'] = utils.join_items(self._uuids)

if self._categories:
params['category'] = utils.join_items(self._categories)

if self._ttl is not None:
params['ttl'] = str(int(self._ttl))

Expand Down
19 changes: 17 additions & 2 deletions pubnub/endpoints/access/grant_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pubnub.enums import HttpMethod, PNOperationType
from pubnub.models.consumer.common import PNStatus
from pubnub.models.consumer.v3.access_manager import PNGrantTokenResult
from pubnub.models.consumer.v3.category import Category
from pubnub.models.consumer.v3.channel import Channel
from pubnub.models.consumer.v3.group import Group
from pubnub.models.consumer.v3.space import Space
Expand All @@ -24,7 +25,8 @@ class GrantToken(Endpoint):

def __init__(self, pubnub, channels: Union[str, List[str]] = None, channel_groups: Union[str, List[str]] = None,
users: Union[str, List[str]] = None, spaces: Union[str, List[str]] = None,
authorized_user_id: str = None, ttl: Optional[int] = None, meta: Optional[any] = None):
authorized_user_id: str = None, ttl: Optional[int] = None, meta: Optional[any] = None,
categories: Optional[List[Category]] = None):
Endpoint.__init__(self, pubnub)
self._ttl = ttl
self._meta = meta
Expand All @@ -42,6 +44,10 @@ def __init__(self, pubnub, channels: Union[str, List[str]] = None, channel_group
if users:
utils.extend_list(self._uuids, users)

self._categories = []
if categories:
utils.extend_list(self._categories, categories)

self._sort_params = True

def ttl(self, ttl: int) -> 'GrantToken':
Expand Down Expand Up @@ -80,6 +86,10 @@ def uuids(self, uuids: List[UUID]) -> 'GrantToken':
self._uuids = uuids
return self

def categories(self, categories: List[Category]) -> 'GrantToken':
self._categories = categories
return self

def custom_params(self):
return {}

Expand All @@ -99,6 +109,11 @@ def build_data(self):
permissions['resources'] = resources
permissions['patterns'] = patterns

if self._categories:
permissions['categories'] = {
category.get_id(): utils.calculate_bitmask(category) for category in self._categories
}

if self._meta:
if isinstance(self._meta, dict):
permissions['meta'] = self._meta
Expand Down Expand Up @@ -148,7 +163,7 @@ def name(self):
return "Grant Token"

def validate_resources(self):
if not any((self._channels, self._groups, self._uuids)):
if not any((self._channels, self._groups, self._uuids, self._categories)):
raise PubNubException(pn_error=PNERR_RESOURCES_MISSING)

def validate_ttl(self):
Expand Down
9 changes: 9 additions & 0 deletions pubnub/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,15 @@ def parse_token(cls, token):
token[resource_type][resource]
)

categories = {}
for wire_key, category_name in (("chan", "channels"), ("uuid", "uuids")):
permissions = token.get("cat", {}).get(wire_key)
if permissions is not None:
categories[category_name] = {"get": utils.has_get_permission(permissions)}

if categories:
parsed_token["categories"] = categories

return parsed_token

@staticmethod
Expand Down
14 changes: 13 additions & 1 deletion pubnub/models/consumer/access_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@


class _PAMResult:
def __init__(self, level, subscribe_key, channels, groups, uuids, ttl=None, r=None, w=None, m=None, d=None):
def __init__(self, level, subscribe_key, channels, groups, uuids, ttl=None, r=None, w=None, m=None, d=None,
categories=None):
self.level = level
self.subscribe_key = subscribe_key
self.channels = channels
Expand All @@ -15,12 +16,14 @@ def __init__(self, level, subscribe_key, channels, groups, uuids, ttl=None, r=No
self.write_enabled = w
self.manage_enabled = m
self.delete_enabled = d
self.categories = categories if categories is not None else {}

@classmethod
def from_json(cls, json_input):
constructed_channels = {}
constructed_groups = {}
constructed_uuids = {}
constructed_categories = {}

# only extract ttl, others are to be fetched on per uuid level
r, w, m, d, g, u, j, ttl = fetch_permissions(json_input)
Expand Down Expand Up @@ -72,6 +75,10 @@ def from_json(cls, json_input):
for uuid, value in json_input['uuids'].items():
constructed_uuids[uuid] = PNAccessManagerUuidsData.from_json(uuid, value)

if 'categories' in json_input:
for category_name, value in json_input['categories'].items():
constructed_categories[category_name] = PNAccessManagerCategoryData.from_json(category_name, value)

return cls(
level=json_input['level'],
subscribe_key=json_input['subscribe_key'],
Expand All @@ -83,6 +90,7 @@ def from_json(cls, json_input):
m=m,
d=d,
ttl=ttl,
categories=constructed_categories,
)


Expand Down Expand Up @@ -136,6 +144,10 @@ class PNAccessManagerUuidsData(_PAMEntityData):
pass


class PNAccessManagerCategoryData(_PAMEntityData):
pass


class PNAccessManagerKeyData(object):
def __init__(self, r, w, m, d, g, u, j, ttl=None):
self.read_enabled = r
Expand Down
23 changes: 23 additions & 0 deletions pubnub/models/consumer/v3/category.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from pubnub.models.consumer.v3.pn_resource import PNResource


class Category(PNResource):
"""Category-level permission for a whole App Context resource type."""

CHANNELS = 'channels'
UUIDS = 'uuids'

def __init__(self, resource_name=None):
super(Category, self).__init__(resource_name)

@staticmethod
def channels():
return Category(resource_name=Category.CHANNELS)

@staticmethod
def uuids():
return Category(resource_name=Category.UUIDS)

def get(self):
self._get = True
return self
7 changes: 5 additions & 2 deletions pubnub/pubnub_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def my_listener(message, event):
from pubnub.models.consumer.objects_v2.channel_members import PNUUID
from pubnub.models.consumer.objects_v2.common import MemberIncludes, MembershipIncludes
from pubnub.models.consumer.objects_v2.page import PNPage
from pubnub.models.consumer.v3.category import Category
from pubnub.models.subscription import PubNubChannel, PubNubChannelGroup, PubNubChannelMetadata, PubNubUserMetadata, \
PNSubscriptionRegistry, PubNubSubscriptionSet

Expand Down Expand Up @@ -571,7 +572,8 @@ def grant_token(
spaces: Union[str, List[str]] = None,
authorized_user_id: str = None,
ttl: Optional[int] = None,
meta: Optional[Any] = None
meta: Optional[Any] = None,
categories: Optional[List[Category]] = None
) -> GrantToken:
return GrantToken(
self,
Expand All @@ -581,7 +583,8 @@ def grant_token(
spaces=spaces,
authorized_user_id=authorized_user_id,
ttl=ttl,
meta=meta
meta=meta,
categories=categories
)

def revoke_token(self, token: str) -> RevokeToken:
Expand Down
53 changes: 52 additions & 1 deletion tests/functional/test_grant.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ def setUp(self):

def test_grant_read_and_write_to_channel(self):
self.grant.channels('ch').read(True).write(True).ttl(7)

self.assertEqual(self.grant.build_path(), Grant.GRANT_PATH % pnconf_pam.subscribe_key)

pam_args = utils.prepare_pam_arguments({
Expand All @@ -54,6 +53,58 @@ def test_grant_read_and_write_to_channel(self):
'signature': "v2." + utils.sign_sha256(pnconf_pam.secret_key, sign_input).rstrip("=")
})

def test_grant_categories_to_auth_key(self):
self.grant.auth_keys('my-auth-key').categories(['channels', 'uuids']).get(True).ttl(1440)

pam_args = utils.prepare_pam_arguments({
'g': '1',
'auth': 'my-auth-key',
'category': 'channels,uuids',
'ttl': '1440',
'timestamp': 123,
'pnsdk': sdk_name,
'uuid': self.pubnub.uuid,
})
sign_input = HttpMethod.string(self.grant.http_method()).upper() + "\n" + \
pnconf_pam.publish_key + "\n" + \
self.grant.build_path() + "\n" + \
pam_args + "\n"

self.assertEqual(self.grant.build_params_callback()({}), {
'g': '1',
'auth': 'my-auth-key',
'category': 'channels,uuids',
'ttl': '1440',
'timestamp': '123',
'pnsdk': sdk_name,
'uuid': self.pubnub.uuid,
'signature': "v2." + utils.sign_sha256(pnconf_pam.secret_key, sign_input).rstrip("=")
})

def test_grant_categories_accepts_string(self):
self.grant.auth_keys('my-auth-key').categories('channels,uuids').get(True)
self.assertEqual(self.grant.build_params_callback()({})['category'], 'channels,uuids')

def test_grant_categories_alongside_channel(self):
self.grant.auth_keys('my-auth-key').channels('ch1').categories(['channels']).get(True).ttl(1440)

params = self.grant.build_params_callback()({})
self.assertEqual(params['channel'], 'ch1')
self.assertEqual(params['category'], 'channels')
self.assertEqual(params['g'], '1')

def test_grant_categories_revoke_with_get_false(self):
self.grant.auth_keys('my-auth-key').categories(['channels']).get(False)

params = self.grant.build_params_callback()({})
self.assertEqual(params['g'], '0')
self.assertEqual(params['category'], 'channels')
self.assertNotIn('ttl', params)

def test_grant_without_categories_omits_param(self):
self.grant.channels('ch').read(True).write(True)
self.assertNotIn('category', self.grant.build_params_callback()({}))

def test_grant_read_and_write_to_channel_group(self):
self.grant.channel_groups(['gr1', 'gr2']).read(True).write(True)

Expand Down
59 changes: 59 additions & 0 deletions tests/functional/test_grant_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import json
import unittest

from pubnub.endpoints.access.grant_token import GrantToken
from pubnub.models.consumer.v3.category import Category
from pubnub.models.consumer.v3.channel import Channel
from pubnub.models.consumer.v3.uuid import UUID

from pubnub.pubnub import PubNub
from tests.helper import pnconf_pam_copy


GET_PERMISSION = 32


class TestGrantTokenCategories(unittest.TestCase):
def setUp(self):
self.pubnub = PubNub(pnconf_pam_copy())

def permissions(self, grant_token):
return json.loads(grant_token.build_data())['permissions']

def test_categories_only_grant(self):
grant_token = GrantToken(
self.pubnub,
ttl=1440,
categories=[Category.channels().get(), Category.uuids().get()]
)

grant_token.validate_resources()
categories_permission = self.permissions(grant_token)['categories']
self.assertEqual(categories_permission, {'channels': GET_PERMISSION, 'uuids': GET_PERMISSION})

def test_categories_alongside_resources(self):
grant_token = GrantToken(self.pubnub, ttl=1440) \
.channels([Channel.id('ch1').read(), Channel.pattern('.*').get()]) \
.uuids([UUID.id('uuid1').get()]) \
.categories([Category.uuids().get()])

permissions = self.permissions(grant_token)
self.assertEqual(permissions['categories'], {'uuids': GET_PERMISSION})
self.assertEqual(permissions['resources']['channels'], {'ch1': 1})
self.assertEqual(permissions['patterns']['channels'], {'.*': GET_PERMISSION})
self.assertEqual(permissions['resources']['uuids'], {'uuid1': GET_PERMISSION})

def test_resource_get_permissions_do_not_create_category_permissions(self):
grant_token = GrantToken(self.pubnub, ttl=1440) \
.channels([Channel.id('ch1').get(), Channel.pattern('.*').get()]) \
.uuids([UUID.id('uuid1').get(), UUID.pattern('.*').get()])

permissions = self.permissions(grant_token)
self.assertNotIn('categories', permissions)
self.assertEqual(permissions['patterns']['channels'], {'.*': GET_PERMISSION})
self.assertEqual(permissions['patterns']['uuids'], {'.*': GET_PERMISSION})

def test_request_without_categories_is_unchanged(self):
grant_token = GrantToken(self.pubnub, ttl=1440).channels([Channel.id('ch1').read()])

self.assertEqual(sorted(self.permissions(grant_token).keys()), ['meta', 'patterns', 'resources'])
Loading
Loading