aboutsummaryrefslogtreecommitdiff
path: root/google
diff options
context:
space:
mode:
authorJonathan Lui <jonathanlui@google.com>2020-01-09 13:01:17 -0800
committerGitHub <noreply@github.com>2020-01-09 13:01:17 -0800
commitfd47fda5e3f5eca63522c8d81cffa22bc2a29ab6 (patch)
treed6a03622b348290cd12e643c382cb03575e9b78d /google
parent5e5559202891f7e5b6c22c2cbc549e1ec26eb857 (diff)
downloadpython-api-core-fd47fda5e3f5eca63522c8d81cffa22bc2a29ab6.tar.gz
feat(api_core): support version 3 policy bindings (#9869)
* feat(api_core): support version 3 policy bindings * fix(doc): fix documenting bindings structure * try fixing docs * fix pytype error * fill test coverage * indent docs * fix docs * improve test coverage * linty * remove unused variable
Diffstat (limited to 'google')
-rw-r--r--google/api_core/iam.py262
1 files changed, 224 insertions, 38 deletions
diff --git a/google/api_core/iam.py b/google/api_core/iam.py
index 04680eb..a7a1c00 100644
--- a/google/api_core/iam.py
+++ b/google/api_core/iam.py
@@ -21,19 +21,38 @@ Example usage:
.. code-block:: python
# ``get_iam_policy`` returns a :class:'~google.api_core.iam.Policy`.
- policy = resource.get_iam_policy()
-
- phred = policy.user("phred@example.com")
- admin_group = policy.group("admins@groups.example.com")
- account = policy.service_account("account-1234@accounts.example.com")
- policy["roles/owner"] = [phred, admin_group, account]
- policy["roles/editor"] = policy.authenticated_users()
- policy["roles/viewer"] = policy.all_users()
+ policy = resource.get_iam_policy(requested_policy_version=3)
+
+ phred = "user:phred@example.com"
+ admin_group = "group:admins@groups.example.com"
+ account = "serviceAccount:account-1234@accounts.example.com"
+
+ policy.version = 3
+ policy.bindings = [
+ {
+ "role": "roles/owner",
+ "members": {phred, admin_group, account}
+ },
+ {
+ "role": "roles/editor",
+ "members": {"allAuthenticatedUsers"}
+ },
+ {
+ "role": "roles/viewer",
+ "members": {"allUsers"}
+ "condition": {
+ "title": "request_time",
+ "description": "Requests made before 2021-01-01T00:00:00Z",
+ "expression": "request.time < timestamp(\"2021-01-01T00:00:00Z\")"
+ }
+ }
+ ]
resource.set_iam_policy(policy)
"""
import collections
+import operator
import warnings
try:
@@ -53,18 +72,41 @@ VIEWER_ROLE = "roles/viewer"
"""Generic role implying rights to access an object."""
_ASSIGNMENT_DEPRECATED_MSG = """\
-Assigning to '{}' is deprecated. Replace with 'policy[{}] = members."""
+Assigning to '{}' is deprecated. Use the `policy.bindings` property to modify bindings instead."""
+
+_FACTORY_DEPRECATED_MSG = """\
+Factory method {0} is deprecated. Replace with '{0}'."""
+
+_DICT_ACCESS_MSG = """\
+Dict access is not supported on policies with version > 1 or with conditional bindings."""
+
+
+class InvalidOperationException(Exception):
+ """Raised when trying to use Policy class as a dict."""
+
+ pass
class Policy(collections_abc.MutableMapping):
"""IAM Policy
- See
- https://cloud.google.com/iam/reference/rest/v1/Policy
-
Args:
etag (Optional[str]): ETag used to identify a unique of the policy
- version (Optional[int]): unique version of the policy
+ version (Optional[int]): The syntax schema version of the policy.
+
+ Note:
+ Using conditions in bindings requires the policy's version to be set
+ to `3` or greater, depending on the versions that are currently supported.
+
+ Accessing the policy using dict operations will raise InvalidOperationException
+ when the policy's version is set to 3.
+
+ Use the policy.bindings getter/setter to retrieve and modify the policy's bindings.
+
+ See:
+ IAM Policy https://cloud.google.com/iam/reference/rest/v1/Policy
+ Policy versions https://cloud.google.com/iam/docs/policies#versions
+ Conditions overview https://cloud.google.com/iam/docs/conditions-overview.
"""
_OWNER_ROLES = (OWNER_ROLE,)
@@ -79,31 +121,120 @@ class Policy(collections_abc.MutableMapping):
def __init__(self, etag=None, version=None):
self.etag = etag
self.version = version
- self._bindings = collections.defaultdict(set)
+ self._bindings = []
def __iter__(self):
- return iter(self._bindings)
+ self.__check_version__()
+ return (binding["role"] for binding in self._bindings)
def __len__(self):
+ self.__check_version__()
return len(self._bindings)
def __getitem__(self, key):
- return self._bindings[key]
+ self.__check_version__()
+ for b in self._bindings:
+ if b["role"] == key:
+ return b["members"]
+ return set()
def __setitem__(self, key, value):
- self._bindings[key] = set(value)
+ self.__check_version__()
+ value = set(value)
+ for binding in self._bindings:
+ if binding["role"] == key:
+ binding["members"] = value
+ return
+ self._bindings.append({"role": key, "members": value})
def __delitem__(self, key):
- del self._bindings[key]
+ self.__check_version__()
+ for b in self._bindings:
+ if b["role"] == key:
+ self._bindings.remove(b)
+ return
+ raise KeyError(key)
+
+ def __check_version__(self):
+ """Raise InvalidOperationException if version is greater than 1 or policy contains conditions."""
+ raise_version = self.version is not None and self.version > 1
+
+ if raise_version or self._contains_conditions():
+ raise InvalidOperationException(_DICT_ACCESS_MSG)
+
+ def _contains_conditions(self):
+ for b in self._bindings:
+ if b.get("condition") is not None:
+ return True
+ return False
+
+ @property
+ def bindings(self):
+ """The policy's list of bindings.
+
+ A binding is specified by a dictionary with keys:
+
+ * role (str): Role that is assigned to `members`.
+
+ * members (:obj:`set` of str): Specifies the identities associated to this binding.
+
+ * condition (:obj:`dict` of str:str): Specifies a condition under which this binding will apply.
+
+ * title (str): Title for the condition.
+
+ * description (:obj:str, optional): Description of the condition.
+
+ * expression: A CEL expression.
+
+ Type:
+ :obj:`list` of :obj:`dict`
+
+ See:
+ Policy versions https://cloud.google.com/iam/docs/policies#versions
+ Conditions overview https://cloud.google.com/iam/docs/conditions-overview.
+
+ Example:
+
+ .. code-block:: python
+
+ USER = "user:phred@example.com"
+ ADMIN_GROUP = "group:admins@groups.example.com"
+ SERVICE_ACCOUNT = "serviceAccount:account-1234@accounts.example.com"
+ CONDITION = {
+ "title": "request_time",
+ "description": "Requests made before 2021-01-01T00:00:00Z", # Optional
+ "expression": "request.time < timestamp(\"2021-01-01T00:00:00Z\")"
+ }
+
+ # Set policy's version to 3 before setting bindings containing conditions.
+ policy.version = 3
+
+ policy.bindings = [
+ {
+ "role": "roles/viewer",
+ "members": {USER, ADMIN_GROUP, SERVICE_ACCOUNT},
+ "condition": CONDITION
+ },
+ ...
+ ]
+ """
+ return self._bindings
+
+ @bindings.setter
+ def bindings(self, bindings):
+ self._bindings = bindings
@property
def owners(self):
"""Legacy access to owner role.
- DEPRECATED: use ``policy["roles/owners"]`` instead."""
+ Raise InvalidOperationException if version is greater than 1 or policy contains conditions.
+
+ DEPRECATED: use `policy.bindings` to access bindings instead.
+ """
result = set()
for role in self._OWNER_ROLES:
- for member in self._bindings.get(role, ()):
+ for member in self.get(role, ()):
result.add(member)
return frozenset(result)
@@ -111,7 +242,10 @@ class Policy(collections_abc.MutableMapping):
def owners(self, value):
"""Update owners.
- DEPRECATED: use ``policy["roles/owners"] = value`` instead."""
+ Raise InvalidOperationException if version is greater than 1 or policy contains conditions.
+
+ DEPRECATED: use `policy.bindings` to access bindings instead.
+ """
warnings.warn(
_ASSIGNMENT_DEPRECATED_MSG.format("owners", OWNER_ROLE), DeprecationWarning
)
@@ -121,10 +255,13 @@ class Policy(collections_abc.MutableMapping):
def editors(self):
"""Legacy access to editor role.
- DEPRECATED: use ``policy["roles/editors"]`` instead."""
+ Raise InvalidOperationException if version is greater than 1 or policy contains conditions.
+
+ DEPRECATED: use `policy.bindings` to access bindings instead.
+ """
result = set()
for role in self._EDITOR_ROLES:
- for member in self._bindings.get(role, ()):
+ for member in self.get(role, ()):
result.add(member)
return frozenset(result)
@@ -132,7 +269,10 @@ class Policy(collections_abc.MutableMapping):
def editors(self, value):
"""Update editors.
- DEPRECATED: use ``policy["roles/editors"] = value`` instead."""
+ Raise InvalidOperationException if version is greater than 1 or policy contains conditions.
+
+ DEPRECATED: use `policy.bindings` to modify bindings instead.
+ """
warnings.warn(
_ASSIGNMENT_DEPRECATED_MSG.format("editors", EDITOR_ROLE),
DeprecationWarning,
@@ -143,11 +283,13 @@ class Policy(collections_abc.MutableMapping):
def viewers(self):
"""Legacy access to viewer role.
- DEPRECATED: use ``policy["roles/viewers"]`` instead
+ Raise InvalidOperationException if version is greater than 1 or policy contains conditions.
+
+ DEPRECATED: use `policy.bindings` to modify bindings instead.
"""
result = set()
for role in self._VIEWER_ROLES:
- for member in self._bindings.get(role, ()):
+ for member in self.get(role, ()):
result.add(member)
return frozenset(result)
@@ -155,7 +297,9 @@ class Policy(collections_abc.MutableMapping):
def viewers(self, value):
"""Update viewers.
- DEPRECATED: use ``policy["roles/viewers"] = value`` instead.
+ Raise InvalidOperationException if version is greater than 1 or policy contains conditions.
+
+ DEPRECATED: use `policy.bindings` to modify bindings instead.
"""
warnings.warn(
_ASSIGNMENT_DEPRECATED_MSG.format("viewers", VIEWER_ROLE),
@@ -172,7 +316,12 @@ class Policy(collections_abc.MutableMapping):
Returns:
str: A member string corresponding to the given user.
+
+ DEPRECATED: set the role `user:{email}` in the binding instead.
"""
+ warnings.warn(
+ _FACTORY_DEPRECATED_MSG.format("user:{email}"), DeprecationWarning,
+ )
return "user:%s" % (email,)
@staticmethod
@@ -184,7 +333,13 @@ class Policy(collections_abc.MutableMapping):
Returns:
str: A member string corresponding to the given service account.
+
+ DEPRECATED: set the role `serviceAccount:{email}` in the binding instead.
"""
+ warnings.warn(
+ _FACTORY_DEPRECATED_MSG.format("serviceAccount:{email}"),
+ DeprecationWarning,
+ )
return "serviceAccount:%s" % (email,)
@staticmethod
@@ -196,7 +351,12 @@ class Policy(collections_abc.MutableMapping):
Returns:
str: A member string corresponding to the given group.
+
+ DEPRECATED: set the role `group:{email}` in the binding instead.
"""
+ warnings.warn(
+ _FACTORY_DEPRECATED_MSG.format("group:{email}"), DeprecationWarning,
+ )
return "group:%s" % (email,)
@staticmethod
@@ -208,7 +368,12 @@ class Policy(collections_abc.MutableMapping):
Returns:
str: A member string corresponding to the given domain.
+
+ DEPRECATED: set the role `domain:{email}` in the binding instead.
"""
+ warnings.warn(
+ _FACTORY_DEPRECATED_MSG.format("domain:{email}"), DeprecationWarning,
+ )
return "domain:%s" % (domain,)
@staticmethod
@@ -217,7 +382,12 @@ class Policy(collections_abc.MutableMapping):
Returns:
str: A member string representing all users.
+
+ DEPRECATED: set the role `allUsers` in the binding instead.
"""
+ warnings.warn(
+ _FACTORY_DEPRECATED_MSG.format("allUsers"), DeprecationWarning,
+ )
return "allUsers"
@staticmethod
@@ -226,7 +396,12 @@ class Policy(collections_abc.MutableMapping):
Returns:
str: A member string representing all authenticated users.
+
+ DEPRECATED: set the role `allAuthenticatedUsers` in the binding instead.
"""
+ warnings.warn(
+ _FACTORY_DEPRECATED_MSG.format("allAuthenticatedUsers"), DeprecationWarning,
+ )
return "allAuthenticatedUsers"
@classmethod
@@ -242,10 +417,11 @@ class Policy(collections_abc.MutableMapping):
version = resource.get("version")
etag = resource.get("etag")
policy = cls(etag, version)
- for binding in resource.get("bindings", ()):
- role = binding["role"]
- members = sorted(binding["members"])
- policy[role] = members
+ policy.bindings = resource.get("bindings", [])
+
+ for binding in policy.bindings:
+ binding["members"] = set(binding.get("members", ()))
+
return policy
def to_api_repr(self):
@@ -262,13 +438,23 @@ class Policy(collections_abc.MutableMapping):
if self.version is not None:
resource["version"] = self.version
- if self._bindings:
- bindings = resource["bindings"] = []
- for role, members in sorted(self._bindings.items()):
+ if self._bindings and len(self._bindings) > 0:
+ bindings = []
+ for binding in self._bindings:
+ members = binding.get("members")
if members:
- bindings.append({"role": role, "members": sorted(set(members))})
-
- if not bindings:
- del resource["bindings"]
+ new_binding = {
+ "role": binding["role"],
+ "members": sorted(members)
+ }
+ condition = binding.get("condition")
+ if condition:
+ new_binding["condition"] = condition
+ bindings.append(new_binding)
+
+ if bindings:
+ # Sort bindings by role
+ key = operator.itemgetter("role")
+ resource["bindings"] = sorted(bindings, key=key)
return resource