Skip to content

Commit a3cb86b

Browse files
docs(iam): update comments and terminology in IAM samples (#13118)
* docs(iam): update comments and terminology in IAM samples * docs(iam): fix comment in quickstart.py * docs(iam): rename functions from '_member' to '_principal' * docs(iam): add backoff on InvalidArgument exception - Fix google.api_core.exceptions.InvalidArgument: 400 Service account [EMAIL] does not exist on Python 3.13 * docs(iam): add backoff to deleting service account - Fix "The {email} service account was not deleted." for Python 3.13 CI * docs(iam): add backoff to test_project_policies and test_service_account - Fix 'NotFound: 404 Service account [NAME] does not exist.' - Fix 'Aborted: 409 There were concurrent policy changes. Please retry the whole read-modify-write with exponential backoff.'
1 parent ba64b94 commit a3cb86b

8 files changed

+89
-75
lines changed

iam/cloud-client/snippets/iam_modify_policy_add_role.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414

1515

1616
# [START iam_modify_policy_add_role]
17-
def modify_policy_add_role(policy: dict, role: str, member: str) -> dict:
17+
def modify_policy_add_role(policy: dict, role: str, principal: str) -> dict:
1818
"""Adds a new role binding to a policy."""
1919

20-
binding = {"role": role, "members": [member]}
20+
binding = {"role": role, "members": [principal]}
2121
policy["bindings"].append(binding)
2222
print(policy)
2323
return policy

iam/cloud-client/snippets/modify_policy_add_member.py

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,22 @@
2020
from snippets.set_policy import set_project_policy
2121

2222

23-
def modify_policy_add_member(
24-
project_id: str, role: str, member: str
23+
def modify_policy_add_principal(
24+
project_id: str, role: str, principal: str
2525
) -> policy_pb2.Policy:
26-
"""Add a member to certain role in project policy.
26+
"""Add a principal to certain role in project policy.
2727
2828
project_id: ID or number of the Google Cloud project you want to use.
29-
role: role to which member need to be added.
30-
member: The principals requesting access.
31-
32-
Possible format for member:
33-
* user:{emailid}
34-
* serviceAccount:{emailid}
35-
* group:{emailid}
36-
* deleted:user:{emailid}?uid={uniqueid}
37-
* deleted:serviceAccount:{emailid}?uid={uniqueid}
38-
* deleted:group:{emailid}?uid={uniqueid}
39-
* domain:{domain}
29+
role: role to which principal need to be added.
30+
principal: The principal requesting access.
31+
32+
For principal ID formats, see https://cloud.google.com/iam/docs/principal-identifiers
4033
"""
4134
policy = get_project_policy(project_id)
4235

4336
for bind in policy.bindings:
4437
if bind.role == role:
45-
bind.members.append(member)
38+
bind.members.append(principal)
4639
break
4740

4841
return set_project_policy(project_id, policy)
@@ -57,6 +50,6 @@ def modify_policy_add_member(
5750
PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "your-google-cloud-project-id")
5851

5952
role = "roles/viewer"
60-
member = f"serviceAccount:test-service-account@{PROJECT_ID}.iam.gserviceaccount.com"
53+
principal = f"serviceAccount:test-service-account@{PROJECT_ID}.iam.gserviceaccount.com"
6154

62-
modify_policy_add_member(PROJECT_ID, role, member)
55+
modify_policy_add_principal(PROJECT_ID, role, principal)

iam/cloud-client/snippets/modify_policy_remove_member.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,23 @@
2020
from snippets.set_policy import set_project_policy
2121

2222

23-
def modify_policy_remove_member(
24-
project_id: str, role: str, member: str
23+
def modify_policy_remove_principal(
24+
project_id: str, role: str, principal: str
2525
) -> policy_pb2.Policy:
26-
"""Remove a member from certain role in project policy.
26+
"""Remove a principal from certain role in project policy.
2727
2828
project_id: ID or number of the Google Cloud project you want to use.
29-
role: role to which member need to be added.
30-
member: The principals requesting access.
31-
32-
Possible format for member:
33-
* user:{emailid}
34-
* serviceAccount:{emailid}
35-
* group:{emailid}
36-
* deleted:user:{emailid}?uid={uniqueid}
37-
* deleted:serviceAccount:{emailid}?uid={uniqueid}
38-
* deleted:group:{emailid}?uid={uniqueid}
39-
* domain:{domain}
29+
role: role to revoke.
30+
principal: The principal to revoke access from.
31+
32+
For principal ID formats, see https://cloud.google.com/iam/docs/principal-identifiers
4033
"""
4134
policy = get_project_policy(project_id)
4235

4336
for bind in policy.bindings:
4437
if bind.role == role:
45-
if member in bind.members:
46-
bind.members.remove(member)
38+
if principal in bind.members:
39+
bind.members.remove(principal)
4740
break
4841

4942
return set_project_policy(project_id, policy, False)
@@ -58,6 +51,6 @@ def modify_policy_remove_member(
5851
PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "your-google-cloud-project-id")
5952

6053
role = "roles/viewer"
61-
member = f"serviceAccount:test-service-account@{PROJECT_ID}.iam.gserviceaccount.com"
54+
principal = f"serviceAccount:test-service-account@{PROJECT_ID}.iam.gserviceaccount.com"
6255

63-
modify_policy_remove_member(PROJECT_ID, role, member)
56+
modify_policy_remove_principal(PROJECT_ID, role, principal)

iam/cloud-client/snippets/quickstart.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,35 @@
1818
from google.iam.v1 import iam_policy_pb2, policy_pb2
1919

2020

21-
def quickstart(project_id: str, member: str) -> None:
22-
"""Gets a policy, adds a member, prints their permissions, and removes the member.
21+
def quickstart(project_id: str, principal: str) -> None:
22+
"""Demonstrates basic IAM operations.
2323
24-
project_id: ID or number of the Google Cloud project you want to use.
25-
member: The principals requesting the access.
24+
This quickstart shows how to get a project's IAM policy,
25+
add a principal to a role, list members of a role,
26+
and remove a principal from a role.
27+
28+
Args:
29+
project_id: ID or number of the Google Cloud project you want to use.
30+
principal: The principal ID requesting the access.
2631
"""
2732

2833
# Role to be granted.
2934
role = "roles/logging.logWriter"
3035
crm_service = resourcemanager_v3.ProjectsClient()
3136

32-
# Grants your member the 'Log Writer' role for the project.
33-
modify_policy_add_role(crm_service, project_id, role, member)
37+
# Grants your principal the 'Log Writer' role for the project.
38+
modify_policy_add_role(crm_service, project_id, role, principal)
3439

35-
# Gets the project's policy and prints all members with the 'Log Writer' role.
40+
# Gets the project's policy and prints all principals with the 'Log Writer' role.
3641
policy = get_policy(crm_service, project_id)
3742
binding = next(b for b in policy.bindings if b.role == role)
3843
print(f"Role: {(binding.role)}")
3944
print("Members: ")
4045
for m in binding.members:
4146
print(f"[{m}]")
4247

43-
# Removes the member from the 'Log Writer' role.
44-
modify_policy_remove_member(crm_service, project_id, role, member)
48+
# Removes the principal from the 'Log Writer' role.
49+
modify_policy_remove_principal(crm_service, project_id, role, principal)
4550

4651

4752
def get_policy(
@@ -74,48 +79,49 @@ def modify_policy_add_role(
7479
crm_service: resourcemanager_v3.ProjectsClient,
7580
project_id: str,
7681
role: str,
77-
member: str,
82+
principal: str,
7883
) -> None:
7984
"""Adds a new role binding to a policy."""
8085

8186
policy = get_policy(crm_service, project_id)
8287

8388
for bind in policy.bindings:
8489
if bind.role == role:
85-
bind.members.append(member)
90+
bind.members.append(principal)
8691
break
8792
else:
8893
binding = policy_pb2.Binding()
8994
binding.role = role
90-
binding.members.append(member)
95+
binding.members.append(principal)
9196
policy.bindings.append(binding)
9297

9398
set_policy(crm_service, project_id, policy)
9499

95100

96-
def modify_policy_remove_member(
101+
def modify_policy_remove_principal(
97102
crm_service: resourcemanager_v3.ProjectsClient,
98103
project_id: str,
99104
role: str,
100-
member: str,
105+
principal: str,
101106
) -> None:
102-
"""Removes a member from a role binding."""
107+
"""Removes a principal from a role binding."""
103108

104109
policy = get_policy(crm_service, project_id)
105110

106111
for bind in policy.bindings:
107112
if bind.role == role:
108-
if member in bind.members:
109-
bind.members.remove(member)
113+
if principal in bind.members:
114+
bind.members.remove(principal)
110115
break
111116

112117
set_policy(crm_service, project_id, policy)
113118

114119

115120
if __name__ == "__main__":
116-
# TODO: Replace with your project ID
121+
# TODO: Replace with your project ID.
117122
project_id = "your-project-id"
118-
# TODO: Replace with the ID of your member in the form 'user:member@example.com'.
119-
member = "your-member"
120-
quickstart(project_id, member)
123+
# TODO: Replace with the ID of your principal.
124+
# For examples, see https://cloud.google.com/iam/docs/principal-identifiers
125+
principal = "your-principal"
126+
quickstart(project_id, principal)
121127
# [END iam_quickstart]

iam/cloud-client/snippets/quickstart_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def test_member(capsys: "pytest.CaptureFixture[str]") -> str:
7878

7979
def test_quickstart(test_member: str, capsys: pytest.CaptureFixture) -> None:
8080
@backoff.on_exception(backoff.expo, Aborted, max_tries=6)
81+
@backoff.on_exception(backoff.expo, InvalidArgument, max_tries=6)
8182
def test_call() -> None:
8283
quickstart(PROJECT_ID, test_member)
8384
out, _ = capsys.readouterr()

iam/cloud-client/snippets/test_project_policies.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
from snippets.delete_service_account import delete_service_account
2929
from snippets.get_policy import get_project_policy
3030
from snippets.list_service_accounts import get_service_account
31-
from snippets.modify_policy_add_member import modify_policy_add_member
32-
from snippets.modify_policy_remove_member import modify_policy_remove_member
31+
from snippets.modify_policy_add_member import modify_policy_add_principal
32+
from snippets.modify_policy_remove_member import modify_policy_remove_principal
3333
from snippets.query_testable_permissions import query_testable_permissions
3434
from snippets.set_policy import set_project_policy
3535

@@ -98,6 +98,7 @@ def execute_wrapped(
9898
pytest.skip("Service account wasn't created")
9999

100100

101+
@backoff.on_exception(backoff.expo, Aborted, max_tries=6)
101102
def test_set_project_policy(project_policy: policy_pb2.Policy) -> None:
102103
role = "roles/viewer"
103104
test_binding = policy_pb2.Binding()
@@ -119,7 +120,8 @@ def test_set_project_policy(project_policy: policy_pb2.Policy) -> None:
119120
assert binding_found
120121

121122

122-
def test_modify_policy_add_member(
123+
@backoff.on_exception(backoff.expo, Aborted, max_tries=6)
124+
def test_modify_policy_add_principal(
123125
project_policy: policy_pb2.Policy, service_account: str
124126
) -> None:
125127
role = "roles/viewer"
@@ -141,7 +143,7 @@ def test_modify_policy_add_member(
141143
assert binding_found
142144

143145
member = f"serviceAccount:{service_account}"
144-
policy = execute_wrapped(modify_policy_add_member, PROJECT_ID, role, member)
146+
policy = execute_wrapped(modify_policy_add_principal, PROJECT_ID, role, member)
145147

146148
member_added = False
147149
for bind in policy.bindings:
@@ -151,6 +153,7 @@ def test_modify_policy_add_member(
151153
assert member_added
152154

153155

156+
@backoff.on_exception(backoff.expo, Aborted, max_tries=6)
154157
def test_modify_policy_remove_member(
155158
project_policy: policy_pb2.Policy, service_account: str
156159
) -> None:
@@ -175,7 +178,7 @@ def test_modify_policy_remove_member(
175178
break
176179
assert binding_found
177180

178-
policy = execute_wrapped(modify_policy_remove_member, PROJECT_ID, role, member)
181+
policy = execute_wrapped(modify_policy_remove_principal, PROJECT_ID, role, member)
179182

180183
member_removed = False
181184
for bind in policy.bindings:

iam/cloud-client/snippets/test_service_account.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def test_list_service_accounts(service_account_email: str) -> None:
9898

9999

100100
@backoff.on_exception(backoff.expo, AssertionError, max_tries=6)
101+
@backoff.on_exception(backoff.expo, NotFound, max_tries=6)
101102
def test_disable_service_account(service_account_email: str) -> None:
102103
account_before = get_service_account(PROJECT_ID, service_account_email)
103104
assert not account_before.disabled

iam/cloud-client/snippets/test_service_account_key.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,31 @@
2929
PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "your-google-cloud-project-id")
3030

3131

32+
def delete_service_account_with_backoff(email: str) -> None:
33+
"""Check if the account was deleted correctly using exponential backoff."""
34+
35+
delete_service_account(PROJECT_ID, email)
36+
37+
backoff_delay_secs = 1 # Start wait with delay of 1 second
38+
starting_time = time.time()
39+
timeout_secs = 90
40+
41+
while time.time() < starting_time + timeout_secs:
42+
try:
43+
get_service_account(PROJECT_ID, email)
44+
except (NotFound, InvalidArgument):
45+
# Service account deleted successfully
46+
return
47+
48+
# In case the account still exists, wait again.
49+
print("- Waiting for the service account to be deleted...")
50+
time.sleep(backoff_delay_secs)
51+
# Double the delay to provide exponential backoff
52+
backoff_delay_secs *= 2
53+
54+
pytest.fail(f"The {email} service account was not deleted.")
55+
56+
3257
@pytest.fixture
3358
def service_account(capsys: "pytest.CaptureFixture[str]") -> str:
3459
name = f"test-{uuid.uuid4().hex[:25]}"
@@ -50,14 +75,14 @@ def service_account(capsys: "pytest.CaptureFixture[str]") -> str:
5075
execution_finished = True
5176
created = True
5277
except (NotFound, InvalidArgument):
53-
# Account not created yet, retry
78+
# Account not created yet, retry getting it.
5479
pass
5580

56-
# If we haven't seen the result yet, wait again.
81+
# If account is not found yet, wait again.
5782
if not execution_finished:
5883
print("- Waiting for the service account to be available...")
5984
time.sleep(backoff_delay_secs)
60-
# Double the delay to provide exponential backoff.
85+
# Double the delay to provide exponential backoff
6186
backoff_delay_secs *= 2
6287

6388
if time.time() > starting_time + timeout_secs:
@@ -67,15 +92,7 @@ def service_account(capsys: "pytest.CaptureFixture[str]") -> str:
6792

6893
# Cleanup after running the test
6994
if created:
70-
delete_service_account(PROJECT_ID, email)
71-
time.sleep(5)
72-
73-
try:
74-
get_service_account(PROJECT_ID, email)
75-
except NotFound:
76-
pass
77-
else:
78-
pytest.fail(f"The {email} service account was not deleted.")
95+
delete_service_account_with_backoff(email)
7996

8097

8198
def key_found(project_id: str, account: str, key_id: str) -> bool:

0 commit comments

Comments
 (0)