diff options
author | Jonathan Lui <jonathanlui@google.com> | 2020-01-09 13:01:17 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-01-09 13:01:17 -0800 |
commit | fd47fda5e3f5eca63522c8d81cffa22bc2a29ab6 (patch) | |
tree | d6a03622b348290cd12e643c382cb03575e9b78d /google | |
parent | 5e5559202891f7e5b6c22c2cbc549e1ec26eb857 (diff) | |
download | python-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.py | 262 |
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 |