From d7dcdee04dd35d0e0d5da95db22fe03a734f9fb0 Mon Sep 17 00:00:00 2001
From: Tres Seaver <tseaver@palladion.com>
Date: Sun, 20 Mar 2016 20:21:57 -0400
Subject: [PATCH 1/3] Add 'Topic.set_iam_policy' API wrapper.

---
 docs/pubsub-usage.rst       |  4 +-
 gcloud/pubsub/test_topic.py | 80 +++++++++++++++++++++++++++++++++++++
 gcloud/pubsub/topic.py      | 25 ++++++++++++
 3 files changed, 108 insertions(+), 1 deletion(-)

diff --git a/docs/pubsub-usage.rst b/docs/pubsub-usage.rst
index 2e3678fb846e..ff76e5ee0994 100644
--- a/docs/pubsub-usage.rst
+++ b/docs/pubsub-usage.rst
@@ -66,7 +66,7 @@ Delete a topic:
    >>> topic = client.topic('topic_name')
    >>> topic.delete()  # API request
 
-Fetch the IAM policy for a topic:
+Update the IAM policy for a topic:
 
 .. doctest::
 
@@ -82,6 +82,8 @@ Fetch the IAM policy for a topic:
    ['systemAccount:abc-1234@systemaccounts.example.com']
    >>> policy.readers
    ['domain:example.com']
+   >>> policy.writers.add(policy.group('editors-list@example.com'))
+   >>> topic.set_iam_policy(policy)  # API request
 
 
 Publish messages to a topic
diff --git a/gcloud/pubsub/test_topic.py b/gcloud/pubsub/test_topic.py
index 595ab33284cf..51410f115f98 100644
--- a/gcloud/pubsub/test_topic.py
+++ b/gcloud/pubsub/test_topic.py
@@ -519,6 +519,86 @@ def test_get_iam_policy_w_alternate_client(self):
         self.assertEqual(req['method'], 'GET')
         self.assertEqual(req['path'], '/%s' % PATH)
 
+    def test_set_iam_policy_w_bound_client(self):
+        from gcloud.pubsub.iam import Policy
+        OWNER1 = 'group:cloud-logs@google.com'
+        OWNER2 = 'user:phred@example.com'
+        WRITER1 = 'domain:google.com'
+        WRITER2 = 'user:phred@example.com'
+        READER1 = 'serviceAccount:1234-abcdef@service.example.com'
+        READER2 = 'user:phred@example.com'
+        POLICY = {
+            'etag': 'DEADBEEF',
+            'version': 17,
+            'bindings': [
+                {'role': 'roles/owner', 'members': [OWNER1, OWNER2]},
+                {'role': 'roles/writer', 'members': [WRITER1, WRITER2]},
+                {'role': 'roles/reader', 'members': [READER1, READER2]},
+            ],
+        }
+        RESPONSE = POLICY.copy()
+        RESPONSE['etag'] = 'ABACABAF'
+        RESPONSE['version'] = 18
+        TOPIC_NAME = 'topic_name'
+        PROJECT = 'PROJECT'
+        TOPIC_NAME = 'topic_name'
+        PATH = 'projects/%s/topics/%s:setIamPolicy' % (PROJECT, TOPIC_NAME)
+
+        conn = _Connection(RESPONSE)
+        CLIENT = _Client(project=PROJECT, connection=conn)
+        topic = self._makeOne(TOPIC_NAME, client=CLIENT)
+        policy = Policy('DEADBEEF', 17)
+        policy.owners.add(OWNER1)
+        policy.owners.add(OWNER2)
+        policy.writers.add(WRITER1)
+        policy.writers.add(WRITER2)
+        policy.readers.add(READER1)
+        policy.readers.add(READER2)
+
+        new_policy = topic.set_iam_policy(policy)
+
+        self.assertEqual(new_policy.etag, 'ABACABAF')
+        self.assertEqual(new_policy.version, 18)
+        self.assertEqual(sorted(new_policy.owners), [OWNER1, OWNER2])
+        self.assertEqual(sorted(new_policy.writers), [WRITER1, WRITER2])
+        self.assertEqual(sorted(new_policy.readers), [READER1, READER2])
+
+        self.assertEqual(len(conn._requested), 1)
+        req = conn._requested[0]
+        self.assertEqual(req['method'], 'POST')
+        self.assertEqual(req['path'], '/%s' % PATH)
+        self.assertEqual(req['data'], POLICY)
+
+    def test_set_iam_policy_w_alternate_client(self):
+        from gcloud.pubsub.iam import Policy
+        RESPONSE = {'etag': 'ACAB'}
+        TOPIC_NAME = 'topic_name'
+        PROJECT = 'PROJECT'
+        TOPIC_NAME = 'topic_name'
+        PATH = 'projects/%s/topics/%s:setIamPolicy' % (PROJECT, TOPIC_NAME)
+
+        conn1 = _Connection()
+        conn2 = _Connection(RESPONSE)
+        CLIENT1 = _Client(project=PROJECT, connection=conn1)
+        CLIENT2 = _Client(project=PROJECT, connection=conn2)
+        topic = self._makeOne(TOPIC_NAME, client=CLIENT1)
+
+        policy = Policy()
+        new_policy = topic.set_iam_policy(policy, client=CLIENT2)
+
+        self.assertEqual(new_policy.etag, 'ACAB')
+        self.assertEqual(new_policy.version, None)
+        self.assertEqual(sorted(new_policy.owners), [])
+        self.assertEqual(sorted(new_policy.writers), [])
+        self.assertEqual(sorted(new_policy.readers), [])
+
+        self.assertEqual(len(conn1._requested), 0)
+        self.assertEqual(len(conn2._requested), 1)
+        req = conn2._requested[0]
+        self.assertEqual(req['method'], 'POST')
+        self.assertEqual(req['path'], '/%s' % PATH)
+        self.assertEqual(req['data'], {})
+
 
 class TestBatch(unittest2.TestCase):
 
diff --git a/gcloud/pubsub/topic.py b/gcloud/pubsub/topic.py
index 2269993dc302..7b6387b1203b 100644
--- a/gcloud/pubsub/topic.py
+++ b/gcloud/pubsub/topic.py
@@ -278,6 +278,31 @@ def get_iam_policy(self, client=None):
         resp = client.connection.api_request(method='GET', path=path)
         return Policy.from_api_repr(resp)
 
+    def set_iam_policy(self, policy, client=None):
+        """Update the IAM policy for the topic.
+
+        See:
+        https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/setIamPolicy
+
+        :type policy: :class:`gcloud.pubsub.iam.Policy`
+        :param policy: the new policy, typically fetched via
+                       :meth:`getIamPolicy` and updated in place.
+
+        :type client: :class:`gcloud.pubsub.client.Client` or ``NoneType``
+        :param client: the client to use.  If not passed, falls back to the
+                       ``client`` stored on the current batch.
+
+        :rtype: :class:`gcloud.pubsub.iam.Policy`
+        :returns: updated policy created from the resource returned by the
+                  ``setIamPolicy`` API request.
+        """
+        client = self._require_client(client)
+        path = '%s:setIamPolicy' % (self.path,)
+        resource = policy.to_api_repr()
+        resp = client.connection.api_request(
+            method='POST', path=path, data=resource)
+        return Policy.from_api_repr(resp)
+
 
 class Batch(object):
     """Context manager:  collect messages to publish via a single API call.

From 0dd8f5d05828242e6a327f4fcf7eda1682a68ead Mon Sep 17 00:00:00 2001
From: Tres Seaver <tseaver@palladion.com>
Date: Tue, 22 Mar 2016 12:04:10 -0400
Subject: [PATCH 2/3] Use role constants introduced in
 ebf5051e432af9e06bd8a41ce42a64b449755b34.

---
 gcloud/pubsub/test_topic.py | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/gcloud/pubsub/test_topic.py b/gcloud/pubsub/test_topic.py
index 51410f115f98..5d58e627fffd 100644
--- a/gcloud/pubsub/test_topic.py
+++ b/gcloud/pubsub/test_topic.py
@@ -453,6 +453,7 @@ def test_list_subscriptions_missing_key(self):
         self.assertEqual(req['query_params'], {})
 
     def test_get_iam_policy_w_bound_client(self):
+        from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE
         OWNER1 = 'user:phred@example.com'
         OWNER2 = 'group:cloud-logs@google.com'
         WRITER1 = 'domain:google.com'
@@ -463,9 +464,9 @@ def test_get_iam_policy_w_bound_client(self):
             'etag': 'DEADBEEF',
             'version': 17,
             'bindings': [
-                {'role': 'roles/owner', 'members': [OWNER1, OWNER2]},
-                {'role': 'roles/writer', 'members': [WRITER1, WRITER2]},
-                {'role': 'roles/reader', 'members': [READER1, READER2]},
+                {'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]},
+                {'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]},
+                {'role': _READER_ROLE, 'members': [READER1, READER2]},
             ],
         }
         TOPIC_NAME = 'topic_name'
@@ -521,6 +522,7 @@ def test_get_iam_policy_w_alternate_client(self):
 
     def test_set_iam_policy_w_bound_client(self):
         from gcloud.pubsub.iam import Policy
+        from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE
         OWNER1 = 'group:cloud-logs@google.com'
         OWNER2 = 'user:phred@example.com'
         WRITER1 = 'domain:google.com'
@@ -531,9 +533,9 @@ def test_set_iam_policy_w_bound_client(self):
             'etag': 'DEADBEEF',
             'version': 17,
             'bindings': [
-                {'role': 'roles/owner', 'members': [OWNER1, OWNER2]},
-                {'role': 'roles/writer', 'members': [WRITER1, WRITER2]},
-                {'role': 'roles/reader', 'members': [READER1, READER2]},
+                {'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]},
+                {'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]},
+                {'role': _READER_ROLE, 'members': [READER1, READER2]},
             ],
         }
         RESPONSE = POLICY.copy()

From ba7f87c636b8177e8d84c25e0278e38dd14ae6a8 Mon Sep 17 00:00:00 2001
From: Tres Seaver <tseaver@palladion.com>
Date: Tue, 22 Mar 2016 12:07:39 -0400
Subject: [PATCH 3/3] Split out 'Fetch' vs' Update' examples for topic IAM
 policy.

Addresses:
https://github.com/GoogleCloudPlatform/gcloud-python/pull/1641#discussion_r57014382
---
 docs/pubsub-usage.rst | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/docs/pubsub-usage.rst b/docs/pubsub-usage.rst
index ff76e5ee0994..d6b07127727d 100644
--- a/docs/pubsub-usage.rst
+++ b/docs/pubsub-usage.rst
@@ -66,7 +66,7 @@ Delete a topic:
    >>> topic = client.topic('topic_name')
    >>> topic.delete()  # API request
 
-Update the IAM policy for a topic:
+Fetch the IAM policy for a topic:
 
 .. doctest::
 
@@ -82,6 +82,15 @@ Update the IAM policy for a topic:
    ['systemAccount:abc-1234@systemaccounts.example.com']
    >>> policy.readers
    ['domain:example.com']
+
+Update the IAM policy for a topic:
+
+.. doctest::
+
+   >>> from gcloud import pubsub
+   >>> client = pubsub.Client()
+   >>> topic = client.topic('topic_name')
+   >>> policy = topic.get_iam_policy()  # API request
    >>> policy.writers.add(policy.group('editors-list@example.com'))
    >>> topic.set_iam_policy(policy)  # API request