← Professional Cloud Security Engineer: controls and evidence
09 / 23 · 135 MIN

Identities, credentials and access evidence

Review directories, workload trust and grant paths without confusing inventory with effective access.

Start with the request and the identity

In an access review, first write down who wants to do what, to which resource and in which context. “The operator has project access” is too vague to guide a decision. A useful request identifies the person or workload, full resource, operation and approved purpose. Add the observation time, data origin and decision owner. Failure of one operation does not demonstrate that every other operation is blocked. Success does not demonstrate that the granted scope is appropriate either. Consider a fictional APS team preparing a funds closing window. One person reviews results, a job produces files and an administrator maintains the platform. These three activities should not automatically share one identity. If the job fails, granting the person a broader role might not change the identity actually making the request. Identify the observed caller before proposing permissions. This example does not describe internal BNP Paribas procedures. Organize investigation around four questions: how identity is established, which credential is presented, which grants exist and which limits apply. Record uncertainties as uncertainties. The review handover should let another operator repeat the analysis without relying on a private conversation or ambiguous display names. Include the evidence needed to tell a wrong-identity problem from a missing-permission problem before choosing a remediation owner.

Synchronize directories with explicit scope

GCDS compares entities within its configured scope. An account disappearing from an LDAP search might represent a departure, but it might also have been excluded by an accidental filter change. Do not turn absence from a search into a business decision without reconciliation. In the exercise, the approved request contains four departures and simulation proposes 286 suspensions. The additional 282 accounts are a discrepancy to investigate, not a margin to accept. Compare affected identifiers with the approved list and previous search. When a non-administrative account must exist only in Google, evaluate a Google-side exclusion. Excluding a name only from LDAP does not protect an account already absent from that source. When an exception requires preserving local attributes of an entity present on both sides, the documented configuration can use exclusions on both sides. Assign an exception owner and the next review date; an unattended exclusion can become a blind spot in departure processing. For production handover, provide the intended scope, simulation result and explanation of differences. A successful LDAP connection confirms connectivity, not correct selection. If evidence is missing, keep the change pending until the affected set is understood. The aim is to explain each proposed change before executing it, including accounts that are intentionally outside routine synchronization.

Protect administration and recovery

Separating administration from the everyday account reduces exposure of privileged identity during tasks that do not require it. Pair that separation with strong authentication, identified owners and exercised recovery. An emergency account whose authentication factors are unavailable during the on-call window can exist in inventory while remaining unusable. A password shared across the team might facilitate intervention, but it weakens individual attribution and increases exposure. Also draw the paths that do not traverse the primary IdP. A network mask in SSO configuration can redirect only some origins to the IdP; a Google password path outside that set needs its own protection. Super admin recovery deserves the same analysis. The claim “MFA required” should state which identities and paths were actually covered, including those used when the normal service is unavailable. In a fictional exercise, ask a second authorized operator to follow the procedure with the defined owners, without receiving secrets through improvised messages. Record what they could execute, what was missing and how exceptional access is closed. Then check whether an IdP departure left orphaned Google accounts. Before reusing names or removing data, coordinate entitlements, identity and retention needs with the appropriate owners. Recovery and departure evidence should be understandable to the next shift.

Distinguish credentials and delegated operations

A service account can be the identity used by a workload and also a resource on which somebody receives administrative permissions. Always state which meaning you are analyzing. Service Account User provides actAs but does not include direct access-token issuance. When an integration only needs an OIDC ID token, compare the dedicated role with Token Creator and examine the latter’s additional capabilities. Granting a role at minimum scope remains a separate decision from choosing the role. In a fictional pipeline, the team wants to replace a long-lived key with short-lived credentials. The plan must identify the consumer, identity origin, issuance, destination and how successful operation will be confirmed. “No key in the repository” does not demonstrate that an old key stopped working. A restriction on new creation does not automatically revoke previously issued credentials either. Keep the old inventory in the plan until migration and retirement are evidenced. If a key was exposed, removing the file does not eliminate external copies: handle the credential in the service and investigate use. For an apparently inactive key, compare observation with the consumer cycle. Seven days without events do not cover a quarterly process. Use that discrepancy to request evidence instead of concluding the key is unnecessary or granting a permanent exception.

Build trust without recyclable names

Workload Identity Federation requires examining who can issue an accepted identity and which attributes determine the destination principal. An issuer shared by several customers can sign legitimate tokens for all of them. A signature confirms origin but does not prove that the tenant belongs to the organization you intend to authorize. Define restrictions using trustworthy attributes and verify who can change the mapping. A seemingly small change there can modify which workloads reach the same grant. In the closing pipeline case, the repository name can be reused. A new resource with the old name should not silently inherit the trust relationship. Use stable, non-reusable identifiers when the source supplies them. If two providers in one pool produce the same subject for different entities, investigate the collision; short lifetimes do not make those subjects distinct. Configuration needs an identity namespace without that ambiguity. Prepare evidence with a table of four fictional attempts: expected tenant and repository; different tenant; different repository; old name associated with another identifier. Write the intended result before an authorized exercise and retain relevant configuration. Do not accept only the successful case: trust must exclude origins that look similar but do not satisfy the criteria. Keep complete tokens and secrets out of the acceptance report so the review itself does not create another exposure.

Separate grants, boundaries and hierarchy

A review should distinguish an operation grant, an applicable denial and resource eligibility. PAB does not grant permissions. When multiple PABs apply, a resource eligible under one can remain eligible; do not assume intersection by intuition. Also confirm the enforcement version and coverage of the permission being analyzed. A design relying on blocking an operation outside that coverage has a gap even if its boundary diagram looks correct. Conditions are not interchangeable across every mechanism either. A time expression accepted in an allow policy should not be copied into deny without checking supported functions. Documented deny conditions use resource-tag functions. In review, connect control intent to actual mechanism capability rather than approving an expression because its syntax looks familiar. For Organization Policy, identify constraint type and inheritance behavior. An explicit descendant boolean policy can override the inherited setting in the applicable model. For a list configured to merge policies, an explicit denial takes precedence over the same allowed value. These are different models. Record organization, folder, project, explicit configuration and relevant defaults. Use abstract exercise values to practice evaluation; before configuring a real constraint, confirm its particular rules. A locally plausible policy file is insufficient evidence when an ancestor policy changes the effective result.

Conclude only what evidence demonstrates

Removing a direct binding is a concrete action, but “all access was removed” is a broader conclusion. Look for group paths, inherited grants and other authorized identities within review scope. Identify when and how each dataset was collected. An incomplete inventory can reveal a real problem without being sufficient to demonstrate absence of other problems. Preserve both ideas in the report: the path found and the limits of the search. Access changes propagate over time. Nested groups can take longer than a direct policy change; do not promise convergence in a fixed number of minutes without support. If an operation continues to succeed, record principal, resource, operation and time. Investigate propagation and independent paths in parallel without immediately concluding either that the change failed or that waiting alone is sufficient. If urgency requires containment, follow the environment’s authorized process. A useful APS handover might say: “direct binding removed; group path identified; inherited policy not yet collected; operational validation pending.” Each statement has evidence and an owner. Avoid turning a green script result into production authorization. Review ends when agreed criteria are demonstrated and gaps have explicit treatment, not when names disappear from a partial list. Distinguish an observation of configuration from evidence that a particular request succeeded or was rejected.

Practice with a local inventory

The Python exercise uses only fictional data included in the file. Run it locally with python3 run.py and read its JSON output. Before running, predict the first case: the direct grant is inactive, but u belongs to a, a belongs to b and b has an active reviewer/funds grant. The group path should appear as known. This is not proof of effective access because the exercise does not evaluate deny, PAB, permission expansion, hierarchy, sessions or service-specific behavior. Next compare three membership states: true, false and null. True confirms the link in supplied data; false excludes that link; null preserves an unconfirmed possibility. When a path includes unknown information, output should preserve that uncertainty. When groups form a cycle, traversal should terminate. Record order should not change the conclusion. Role and resource are compared as exact identifiers; the script does not infer that one role includes another. The program checks eight cases, state combinations, permutations and invalid inputs without changing the snapshot. Pay particular attention to incomplete inventory with no paths: absence of results does not prove revocation. Write a handover note for each case, stating the fact, limitation and next evidence needed. Then modify a membership in the fictional data and justify the difference before consulting output. Use the exercise to improve review reasoning, not to approve actual account changes.

"""Offline membership inventory exercise. This is not an IAM policy evaluator."""
import copy
import hashlib
import itertools
import json
from collections import deque
from pathlib import Path


def review(snapshot, principal, role, resource):
 def label(value):
 return isinstance(value, str) and bool(value.strip) and value == value.strip

 def state(value):
 return value is None or type(value) is bool

 if not isinstance(snapshot, dict) or set(snapshot)!= {"actors", "memberships", "grants", "inventoryComplete"}:
 raise ValueError("Expected actors, memberships, grants and inventoryComplete")
 if type(snapshot["inventoryComplete"]) is not bool:
 raise ValueError("inventoryComplete must be an explicit boolean")
 if not all(label(x) for x in (principal, role, resource)):
 raise ValueError("Query identifiers must be nonempty trimmed strings")
 actors = snapshot["actors"]
 if not isinstance(actors, list) or not all(isinstance(a, dict) and set(a) == {"id", "kind"} and label(a["id"]) and a["kind"] in ("principal", "group") for a in actors):
 raise ValueError("Invalid actors")
 kinds = {a["id"]: a["kind"] for a in actors}
 if len(kinds)!= len(actors) or kinds.get(principal)!= "principal":
 raise ValueError("Duplicate actors or undeclared query principal")
 edges, grants = snapshot["memberships"], snapshot["grants"]
 if not isinstance(edges, list) or not isinstance(grants, list):
 raise ValueError("Memberships and grants must be lists")
 ids = set
 for edge in edges:
 if not isinstance(edge, dict) or set(edge)!= {"id", "member", "group", "state"} or not label(edge["id"]):
 raise ValueError("Invalid membership record")
 if not isinstance(edge["member"], str) or not isinstance(edge["group"], str) or edge["member"] not in kinds or kinds.get(edge["group"])!= "group" or not state(edge["state"]) or edge["id"] in ids:
 raise ValueError("Invalid membership reference, state or duplicate ID")
 ids.add(edge["id"])
 for grant in grants:
 if not isinstance(grant, dict) or set(grant)!= {"id", "principal", "role", "resource", "active"} or not all(label(grant[k]) for k in ("id", "principal", "role", "resource")):
 raise ValueError("Invalid grant record")
 if grant["principal"] not in kinds or not state(grant["active"]) or grant["id"] in ids:
 raise ValueError("Invalid grant reference, state or duplicate ID")
 ids.add(grant["id"])

 def reachable(include_unknown):
 # Sorting makes the chosen shortest witness deterministic for a snapshot.
 found, pending = {principal: []}, deque([principal])
 while pending:
 member = pending.popleft
 for edge in sorted(edges, key=lambda e: e["id"]):
 if edge["member"]!= member or edge["state"] is False or (edge["state"] is None and not include_unknown):
 continue
 if edge["group"] not in found:
 found[edge["group"]] = found[member] + [edge["id"]]
 pending.append(edge["group"])
 return found

 known, possible = reachable(False), reachable(True)
 confirmed, uncertain = [], []
 for grant in sorted(grants, key=lambda g: g["id"]):
 if grant["role"]!= role or grant["resource"]!= resource or grant["active"] is False:
 continue
 actor = grant["principal"]
 if actor in known and grant["active"] is True:
 confirmed.append({"grant": grant["id"], "membershipPath": known[actor]})
 elif actor in possible:
 uncertain.append({"grant": grant["id"], "membershipPath": possible[actor]})
 return {"knownGrantPaths": confirmed, "possibleButUnconfirmedPaths": uncertain,
 "inventoryComplete": snapshot["inventoryComplete"], "effectiveAccessProven": False,
 "revocationProven": False, "productionAuthorized": False}


def fixture:
 return {"actors": [{"id": "u", "kind": "principal"}, {"id": "a", "kind": "group"}, {"id": "b", "kind": "group"}],
 "memberships": [{"id": "m1", "member": "u", "group": "a", "state": True}, {"id": "m2", "member": "a", "group": "b", "state": True}],
 "grants": [{"id": "direct", "principal": "u", "role": "reviewer", "resource": "funds", "active": False}, {"id": "nested", "principal": "b", "role": "reviewer", "resource": "funds", "active": True}],
 "inventoryComplete": True}


def evidence:
 result, snapshots = [], []
 for name in ("nested-remains", "unknown-membership", "removed-membership", "unknown-grant", "different-resource", "cycle", "incomplete-empty", "direct-and-nested"):
 s = fixture
 if name == "unknown-membership": s["memberships"][1]["state"] = None
 if name == "removed-membership": s["memberships"][0]["state"] = False
 if name == "unknown-grant": s["grants"][1]["active"] = None
 if name == "different-resource": s["grants"][1]["resource"] = "payments"
 if name == "cycle": s["memberships"].append({"id": "m3", "member": "b", "group": "a", "state": True})
 if name == "incomplete-empty": s["memberships"] = []; s["inventoryComplete"] = False
 if name == "direct-and-nested": s["grants"][0]["active"] = True
 before = copy.deepcopy(s)
 r = review(s, "u", "reviewer", "funds")
 assert s == before
 counts = {"nested-remains": (1, 0), "unknown-membership": (0, 1), "removed-membership": (0, 0), "unknown-grant": (0, 1), "different-resource": (0, 0), "cycle": (1, 0), "incomplete-empty": (0, 0), "direct-and-nested": (2, 0)}
 assert (len(r["knownGrantPaths"]), len(r["possibleButUnconfirmedPaths"])) == counts[name]
 result.append({"id": name, **r}); snapshots.append(s)
 combinations = 0
 for first, second, active in itertools.product((False, None, True), repeat=3):
 s = fixture; s["memberships"][0]["state"] = first; s["memberships"][1]["state"] = second; s["grants"][1]["active"] = active
 r = review(s, "u", "reviewer", "funds")
 expected_known = all(x is True for x in (first, second, active))
 expected_possible = not any(x is False for x in (first, second, active)) and not expected_known
 assert bool(r["knownGrantPaths"]) == expected_known
 assert bool(r["possibleButUnconfirmedPaths"]) == expected_possible
 combinations += 1
 cycle = snapshots[5]; permutations = 0
 for actors in itertools.permutations(cycle["actors"]):
 for edges in itertools.permutations(cycle["memberships"]):
 for grants in itertools.permutations(cycle["grants"]):
 s = {**cycle, "actors": list(actors), "memberships": list(edges), "grants": list(grants)}
 assert review(s, "u", "reviewer", "funds") == review(cycle, "u", "reviewer", "funds")
 permutations += 1
 invalid = []
 def bad(change):
 s = fixture; change(s); invalid.append(s)
 bad(lambda s: s.update(inventoryComplete=None))
 bad(lambda s: s.update(inventoryComplete=1))
 bad(lambda s: s["actors"].append(dict(s["actors"][0])))
 bad(lambda s: s["actors"][0].update(kind="group"))
 bad(lambda s: s["memberships"][0].update(state=1))
 bad(lambda s: s["memberships"][0].update(state="true"))
 bad(lambda s: s["memberships"][0].update(member="missing"))
 bad(lambda s: s["memberships"][0].update(group="u"))
 bad(lambda s: s["memberships"][0].update(id="direct"))
 bad(lambda s: s["grants"][1].update(active=0))
 bad(lambda s: s["grants"][1].update(principal="missing"))
 bad(lambda s: s["grants"][1].update(role=" "))
 bad(lambda s: s["grants"][1].update(extra="unexpected"))
 bad(lambda s: s.update(memberships={}))
 for s in invalid:
 try: review(s, "u", "reviewer", "funds")
 except ValueError: pass
 else: raise AssertionError("Invalid input accepted")
 return {"fixtures": result, "stateCombinations": combinations, "recordPermutations": permutations, "invalidInputs": len(invalid), "inputPreserved": True, "orderIndependent": True,
 "network": False, "cloudExecuted": False, "iamPoliciesRead": False, "persistentWrites": False,
 "scriptSha256": hashlib.sha256(Path(__file__).read_bytes).hexdigest}


if __name__ == "__main__":
 print(json.dumps(evidence, ensure_ascii=False, indent=2))
IN PRACTICE

A direct grant disappeared, but two groups still connect the contractor to the funds-review grant; the snapshot does not contain all policies.

Common pitfalls

Confusing authentication with authorization, removing only the direct binding, treating recyclable names as stable identity or concluding revocation from partial inventory.

Related topics: Workload federation and pipelines · Privileged identity governance · Inheritance and authorization review

Take this idea with you

Tie the conclusion to the concrete request and actual evidence coverage; keep unknown paths visible until investigated.

Create account

Reference: Policy types · Current linked guide; edition date unconfirmed (2026-09-30 inspection)

Google Cloud is a trademark of Google LLC. bigsavant.com is an independent preparation platform and is not affiliated with, associated with, sponsored, authorised or endorsed by Google. Content and questions are original, are not official exam questions, and completing our tests does not award or guarantee any certification. Names are used only to identify the subject. All other trademarks belong to their respective owners.