Skip to content

Commit fd1b3fd

Browse files
authored
Storage: ProjectID Regex (#4543)
* Storage: ProjectID Regex from https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects#Project.FIELDS.project_id * Re-factor as part of review process. Made the regex check in `BucketNotification.from_api_repr` into a standalone helper. This way all the tests can be in "one place".
1 parent 07627e0 commit fd1b3fd

File tree

2 files changed

+107
-15
lines changed

2 files changed

+107
-15
lines changed

storage/google/cloud/storage/notification.py

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@
2828
NONE_PAYLOAD_FORMAT = 'NONE'
2929

3030
_TOPIC_REF_FMT = '//pubsub.googleapis.com/projects/{}/topics/{}'
31-
_PROJECT_PATTERN = r'(?P<project>[a-z]+-[a-z]+-\d+)'
31+
_PROJECT_PATTERN = r'(?P<project>[a-z][a-z0-9-]{4,28}[a-z0-9])'
3232
_TOPIC_NAME_PATTERN = r'(?P<name>[A-Za-z](\w|[-_.~+%])+)'
3333
_TOPIC_REF_PATTERN = _TOPIC_REF_FMT.format(
3434
_PROJECT_PATTERN, _TOPIC_NAME_PATTERN)
3535
_TOPIC_REF_RE = re.compile(_TOPIC_REF_PATTERN)
36+
_BAD_TOPIC = (
37+
'Resource has invalid topic: {}; see '
38+
'https://cloud.google.com/storage/docs/json_api/v1/'
39+
'notifications/insert#topic')
3640

3741

3842
class BucketNotification(object):
@@ -110,25 +114,12 @@ def from_api_repr(cls, resource, bucket):
110114
111115
:rtype: :class:`BucketNotification`
112116
:returns: the new notification instance
113-
:raises ValueError:
114-
if resource is missing 'topic' key, or if it is not formatted
115-
per the spec documented in
116-
https://cloud.google.com/storage/docs/json_api/v1/notifications/insert#topic
117117
"""
118118
topic_path = resource.get('topic')
119119
if topic_path is None:
120120
raise ValueError('Resource has no topic')
121121

122-
match = _TOPIC_REF_RE.match(topic_path)
123-
if match is None:
124-
raise ValueError(
125-
'Resource has invalid topic: {}; see {}'.format(
126-
topic_path,
127-
'https://cloud.google.com/storage/docs/json_api/v1/'
128-
'notifications/insert#topic'))
129-
130-
name = match.group('name')
131-
project = match.group('project')
122+
name, project = _parse_topic_path(topic_path)
132123
instance = cls(bucket, name, topic_project=project)
133124
instance._properties = resource
134125

@@ -360,3 +351,40 @@ def delete(self, client=None):
360351
path=self.path,
361352
query_params=query_params,
362353
)
354+
355+
356+
def _parse_topic_path(topic_path):
357+
"""Verify that a topic path is in the correct format.
358+
359+
.. _resource manager docs: https://cloud.google.com/resource-manager/\
360+
reference/rest/v1beta1/projects#\
361+
Project.FIELDS.project_id
362+
.. _topic spec: https://cloud.google.com/storage/docs/json_api/v1/\
363+
notifications/insert#topic
364+
365+
Expected to be of the form:
366+
367+
//pubsub.googleapis.com/projects/{project}/topics/{topic}
368+
369+
where the ``project`` value must be "6 to 30 lowercase letters, digits,
370+
or hyphens. It must start with a letter. Trailing hyphens are prohibited."
371+
(see `resource manager docs`_) and ``topic`` must have length at least two,
372+
must start with a letter and may only contain alphanumeric characters or
373+
``-``, ``_``, ``.``, ``~``, ``+`` or ``%`` (i.e characters used for URL
374+
encoding, see `topic spec`_).
375+
376+
Args:
377+
topic_path (str): The topic path to be verified.
378+
379+
Returns:
380+
Tuple[str, str]: The ``project`` and ``topic`` parsed from the
381+
``topic_path``.
382+
383+
Raises:
384+
ValueError: If the topic path is invalid.
385+
"""
386+
match = _TOPIC_REF_RE.match(topic_path)
387+
if match is None:
388+
raise ValueError(_BAD_TOPIC.format(topic_path))
389+
390+
return match.group('name'), match.group('project')

storage/tests/unit/test_notification.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,3 +487,67 @@ def test_delete_hit(self):
487487
path=self.NOTIFICATION_PATH,
488488
query_params={'userProject': USER_PROJECT},
489489
)
490+
491+
492+
class Test__parse_topic_path(unittest.TestCase):
493+
494+
@staticmethod
495+
def _call_fut(*args, **kwargs):
496+
from google.cloud.storage import notification
497+
498+
return notification._parse_topic_path(*args, **kwargs)
499+
500+
@staticmethod
501+
def _make_topic_path(project, topic_name):
502+
from google.cloud.storage import notification
503+
504+
return notification._TOPIC_REF_FMT.format(project, topic_name)
505+
506+
def test_project_name_too_long(self):
507+
project = 'a' * 31
508+
topic_path = self._make_topic_path(project, 'topic-name')
509+
with self.assertRaises(ValueError):
510+
self._call_fut(topic_path)
511+
512+
def test_project_name_uppercase(self):
513+
project = 'aaaAaa'
514+
topic_path = self._make_topic_path(project, 'topic-name')
515+
with self.assertRaises(ValueError):
516+
self._call_fut(topic_path)
517+
518+
def test_leading_digit(self):
519+
project = '1aaaaa'
520+
topic_path = self._make_topic_path(project, 'topic-name')
521+
with self.assertRaises(ValueError):
522+
self._call_fut(topic_path)
523+
524+
def test_leading_hyphen(self):
525+
project = '-aaaaa'
526+
topic_path = self._make_topic_path(project, 'topic-name')
527+
with self.assertRaises(ValueError):
528+
self._call_fut(topic_path)
529+
530+
def test_trailing_hyphen(self):
531+
project = 'aaaaa-'
532+
topic_path = self._make_topic_path(project, 'topic-name')
533+
with self.assertRaises(ValueError):
534+
self._call_fut(topic_path)
535+
536+
def test_invalid_format(self):
537+
topic_path = '@#$%'
538+
with self.assertRaises(ValueError):
539+
self._call_fut(topic_path)
540+
541+
def test_success(self):
542+
topic_name = 'tah-pic-nehm'
543+
project_choices = (
544+
'a' * 30, # Max length.
545+
'a-b--c---d', # Valid hyphen usage.
546+
'abcdefghijklmnopqrstuvwxyz', # Valid letters.
547+
'z0123456789', # Valid digits (non-leading).
548+
'a-bcdefghijklmn12opqrstuv0wxyz',
549+
)
550+
for project in project_choices:
551+
topic_path = self._make_topic_path(project, topic_name)
552+
result = self._call_fut(topic_path)
553+
self.assertEqual(result, (topic_name, project))

0 commit comments

Comments
 (0)