fix(lwpreflight): credit GCP basic roles with storage permissions - #1858
Merged
lokesh-vadlamudi merged 2 commits intoSep 2, 2026
Merged
Conversation
The GCP preflight permission check expands every role bound to the caller via roles.get and looks for the required permission strings inside includedPermissions. Five of the Agentless requirements are bucket-scoped (storage.buckets.get, storage.buckets.getIamPolicy, storage.buckets.setIamPolicy, storage.objects.delete and storage.objects.list) and so are absent from the project-scoped basic roles. Any caller whose access came from roles/owner therefore failed preflight, deterministically, even though the deployment itself succeeds. Cloud Storage grants those permissions separately: every bucket created in a project gets default IAM bindings for the projectOwner, projectEditor and projectViewer convenience values, which map to roles/storage.legacyBucketOwner and roles/storage.legacyBucketReader. Expand that implied legacy role alongside any basic role bound to the caller so the permission set reflects what the caller can actually do. CAD-2290
fetchProjectPolicies asks for RequestedPolicyVersion 3 but fetchOrgPolicies sent an empty GetIamPolicyRequest, which returns a version 1 policy. When the policy carries conditional bindings, IAM does not reject a v1 request: it rewrites each conditional binding's role to roles/<name>_withcond_<hash> and drops the condition. Verified against a live project, where roles/browser came back as roles/browser_withcond_d6d85a888910b56287b5. So if the caller sat in a conditional binding, rolesForCaller collected the mangled name and the following iam.roles.get returned 404, failing the whole org preflight. Bindings belonging to other principals were unaffected. Ask for version 3 on both paths so the org path sees real role names, matching what the project path already does. CAD-2290
lokesh-vadlamudi
force-pushed
the
lvadlamudi/cad-2290-gcp-basic-role-storage-perms
branch
from
September 1, 2026 22:27
143b671 to
b6614d2
Compare
lokesh-vadlamudi
marked this pull request as ready for review
September 2, 2026 15:23
lokesh-vadlamudi
enabled auto-merge (squash)
September 2, 2026 15:23
PengyuanZhao
approved these changes
Sep 2, 2026
lokesh-vadlamudi
deleted the
lvadlamudi/cad-2290-gcp-basic-role-storage-perms
branch
September 2, 2026 15:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
JIRA https://lacework.atlassian.net/browse/CAD-2290
GCP preflight rejected any caller whose access came from
roles/owner, with five errors, on every Agentless onboarding attempt:Root cause
FetchPoliciesexpands each role bound to the caller viaiam.Roles.Get(role).IncludedPermissions, andCheckPermissionslooks for the required permission strings in that set. It never evaluates the caller's real access.Those five requirements are bucket-scoped.
roles/owneris project-scoped and genuinely does not list them:Cloud Storage grants the bucket-scoped ones separately. Every bucket created in a project gets default IAM bindings for the project convenience values, confirmed against a live bucket:
roles/storage.legacyBucketOwnercontains all five. So an Owner can complete the deployment but can never pass the check, regardless of identity type or configuration. Agentless is the only integration whose requirements include bucket-scoped permissions, which is why Configuration and the Audit Log integrations onboard fine under the same identity.Fix
Map the basic roles to the legacy storage role Cloud Storage implies, and expand it alongside any basic role bound to the caller. Binding collection is extracted into a pure
rolesForCallerso it can be unit tested; role expansion now happens once per distinct role rather than inline.Effect on the check, simulated against the live role definitions:
The seven remaining at org scope are
resourcemanager.folders.*andresourcemanager.organizations.*, whichroles/ownergenuinely does not grant. Those are correctly reported and are out of scope here.Second commit: request policy version 3 on the org path
fetchProjectPolicieshas always asked forRequestedPolicyVersion: 3, butfetchOrgPoliciessent an emptyGetIamPolicyRequest, which returns a version 1 policy.IAM does not reject a v1 request against a policy that carries conditional bindings. It silently rewrites each conditional binding's role name and drops the condition, confirmed against a live project:
So if the caller sat in a conditional binding on the organization,
rolesForCallercollected the mangled name and the followingiam.roles.getreturned 404, failing the whole org preflight. Conditional bindings belonging to other principals were unaffected.Asking for version 3 on both paths makes the org path see real role names. Pre-existing issue, included here because it is the same function and the org path is where a hard failure would otherwise hide behind this fix.
Known ceiling, unchanged by this PR:
rolesForCallerdoes not inspectBinding.Condition, so a conditional grant is credited as if unconditional. That is how the project path has always behaved; this commit only makes the org path consistent with it.Test plan
go test ./lwpreflight/gcp/...covers owner, editor, viewer, custom roles, multi-policy dedupe, and bindings belonging to other principals.roles/ownerandroles/storage.legacyBucketOwnercontents, and the default bucket bindings, against live GCP.Fixes CAD-2290.
Live verification
Run against
abc-demo-project-123in org121868925203, as a caller holdingroles/owneron theproject. That project also granted the caller
roles/storage.admin, which contains all fivepermissions and so masks the bug; it is the only role on the project that supplies them. It was
removed for the duration of the test and restored afterwards, leaving
roles/owneras the caller'ssole source of storage access, which is the situation the ticket describes.
Same command, one binary per commit:
Both runs resolved the caller, walked the real three-policy ancestry (project, folder, org) and
discovered 30 Cloud Scheduler regions, so this is the whole pipeline and not a harness.
Driving
rolesForCallerinto a liveiam.Roles.Getand thenCheckPermissions, over policiesfetched by the production code paths:
roles/ownerbinding onlyOrg scope is unchanged because this caller holds no basic role at the organization, so the mapping
never fires. That is the negative control: the change is inert for a caller without a basic role and
does not blanket-grant storage permissions to everyone.
Completing the evidence for the second commit: the mangled name a v1 request returns is not a role
that exists, so the
iam.roles.getthat follows it cannot succeed.Unit side: with
basicRoleStorageRolesemptied, 6 of the 8TestRolesForCallercases fail. The twothat do not touch the mapping, custom roles and other principals, stay green in both states, so the
failures are the change's doing rather than test breakage.