Skip to content

Commit 026faff

Browse files
chore: IAM Validation added for fail-safe.
1 parent 301cd41 commit 026faff

File tree

10 files changed

+485
-0
lines changed

10 files changed

+485
-0
lines changed

v1/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,12 @@
559559
<artifactId>protobuf-java</artifactId>
560560
<version>4.33.1</version>
561561
</dependency>
562+
<dependency>
563+
<groupId>com.google.cloud.teleport.v2</groupId>
564+
<artifactId>common</artifactId>
565+
<version>1.0-SNAPSHOT</version>
566+
<scope>compile</scope>
567+
</dependency>
562568
</dependencies>
563569

564570
<build>

v1/src/main/java/com/google/cloud/teleport/spanner/ImportPipeline.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,15 @@
2424
import com.google.cloud.teleport.metadata.TemplateParameter.TemplateEnumOption;
2525
import com.google.cloud.teleport.spanner.ImportPipeline.Options;
2626
import com.google.cloud.teleport.spanner.spannerio.SpannerConfig;
27+
import com.google.cloud.teleport.v2.iam.IAMCheckResult;
28+
import com.google.cloud.teleport.v2.iam.IAMPermissionsChecker;
29+
import com.google.cloud.teleport.v2.iam.IAMRequirementsCreator;
30+
import com.google.cloud.teleport.v2.iam.IAMResourceRequirements;
31+
import java.util.Collections;
2732
import org.apache.beam.runners.dataflow.options.DataflowPipelineOptions;
2833
import org.apache.beam.sdk.Pipeline;
2934
import org.apache.beam.sdk.PipelineResult;
35+
import org.apache.beam.sdk.extensions.gcp.options.GcpOptions;
3036
import org.apache.beam.sdk.options.Default;
3137
import org.apache.beam.sdk.options.Description;
3238
import org.apache.beam.sdk.options.PipelineOptions;
@@ -241,6 +247,8 @@ public static void main(String[] args) {
241247
.withDatabaseId(options.getDatabaseId())
242248
.withRpcPriority(options.getSpannerPriority());
243249

250+
validateRequiredPermissions(options);
251+
244252
p.apply(
245253
new ImportTransform(
246254
spannerConfig,
@@ -264,4 +272,25 @@ public static void main(String[] args) {
264272
result.waitUntilFinish();
265273
}
266274
}
275+
276+
private static void validateRequiredPermissions(Options options) {
277+
IAMResourceRequirements spannerRequirements =
278+
IAMRequirementsCreator.createSpannerResourceRequirement();
279+
280+
IAMPermissionsChecker iamPermissionsChecker =
281+
new IAMPermissionsChecker(
282+
options.getSpannerProjectId().get(), options.as(GcpOptions.class));
283+
IAMCheckResult missingPermission =
284+
iamPermissionsChecker.check(Collections.singletonList(spannerRequirements));
285+
if (missingPermission.isSuccess()) {
286+
return;
287+
}
288+
String errorString =
289+
"For resource: "
290+
+ missingPermission.getResourceName()
291+
+ ", missing permissions: "
292+
+ missingPermission.getMissingPermissions()
293+
+ ";";
294+
throw new RuntimeException(errorString);
295+
}
267296
}

v2/common/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@
128128
<groupId>com.google.protobuf</groupId>
129129
<artifactId>protobuf-java</artifactId>
130130
</dependency>
131+
<dependency>
132+
<groupId>com.google.apis</groupId>
133+
<artifactId>google-api-services-cloudresourcemanager</artifactId>
134+
<version>v3-rev20251103-2.0.0</version>
135+
</dependency>
131136

132137
<!-- UDFs -->
133138
<dependency>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (C) 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.google.cloud.teleport.v2.iam;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
21+
/** Represents the result of an IAM permission check on a specific resource. */
22+
public class IAMCheckResult {
23+
private final String resourceName;
24+
private final List<String> missingPermissions;
25+
26+
public IAMCheckResult(String resourceName, List<String> missingPermissions) {
27+
this.resourceName = resourceName;
28+
this.missingPermissions = new ArrayList<>(missingPermissions);
29+
}
30+
31+
public String getResourceName() {
32+
return resourceName;
33+
}
34+
35+
public List<String> getMissingPermissions() {
36+
return new ArrayList<>(missingPermissions);
37+
}
38+
39+
public boolean isSuccess() {
40+
return missingPermissions.isEmpty();
41+
}
42+
43+
@Override
44+
public String toString() {
45+
return "IAMCheckResult{"
46+
+ "resourceName='"
47+
+ resourceName
48+
+ '\''
49+
+ ", missingPermissions="
50+
+ missingPermissions
51+
+ ", success="
52+
+ isSuccess()
53+
+ '}';
54+
}
55+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright (C) 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.google.cloud.teleport.v2.iam;
17+
18+
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
19+
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
20+
import com.google.api.client.http.HttpRequestInitializer;
21+
import com.google.api.client.http.HttpTransport;
22+
import com.google.api.client.json.JsonFactory;
23+
import com.google.api.client.json.jackson2.JacksonFactory;
24+
import com.google.api.services.cloudresourcemanager.v3.CloudResourceManager;
25+
import com.google.api.services.cloudresourcemanager.v3.model.TestIamPermissionsRequest;
26+
import com.google.api.services.cloudresourcemanager.v3.model.TestIamPermissionsResponse;
27+
import com.google.auth.Credentials;
28+
import com.google.auth.http.HttpCredentialsAdapter;
29+
import com.google.common.annotations.VisibleForTesting;
30+
import java.io.IOException;
31+
import java.security.GeneralSecurityException;
32+
import java.util.Collection;
33+
import java.util.Collections;
34+
import java.util.HashSet;
35+
import java.util.List;
36+
import java.util.stream.Collectors;
37+
import org.apache.beam.sdk.extensions.gcp.auth.NullCredentialInitializer;
38+
import org.apache.beam.sdk.extensions.gcp.options.GcpOptions;
39+
import org.slf4j.Logger;
40+
import org.slf4j.LoggerFactory;
41+
42+
/** Utility to check IAM permissions for various GCP resources. */
43+
public class IAMPermissionsChecker {
44+
private static final Logger LOG = LoggerFactory.getLogger(IAMPermissionsChecker.class);
45+
private final Credentials credential;
46+
private static final String RESOURCE_NAME_FORMAT = "projects/%s";
47+
private final String projectIdResource;
48+
49+
@VisibleForTesting static CloudResourceManager resourceManagerForTesting;
50+
51+
public IAMPermissionsChecker(String projectId, GcpOptions gcpOptions) {
52+
this.credential = gcpOptions.getGcpCredential();
53+
this.projectIdResource = String.format("projects/%s", projectId);
54+
}
55+
56+
@VisibleForTesting
57+
static void setResourceManagerForTesting(CloudResourceManager resourceManager) {
58+
resourceManagerForTesting = resourceManager;
59+
}
60+
61+
/**
62+
* Checks IAM permissions for a list of requirements.
63+
*
64+
* @param requirements List of resources and required permissions.
65+
* @return List of results, only missing permissions are included. Empty list indicate all the
66+
* requirements are met.
67+
*/
68+
public IAMCheckResult check(List<IAMResourceRequirements> requirements) {
69+
try {
70+
CloudResourceManager resourceManager = createCloudResourceManagerService();
71+
72+
List<String> permissionList =
73+
requirements.stream()
74+
.map(IAMResourceRequirements::getPermissions)
75+
.flatMap(Collection::stream)
76+
.toList();
77+
return check(resourceManager, permissionList);
78+
} catch (IOException | GeneralSecurityException e) {
79+
throw new RuntimeException(e);
80+
}
81+
}
82+
83+
/**
84+
* Checks IAM permissions for a single requirement list.
85+
*
86+
* @param requiredPermissions required permissions.
87+
* @return Result of the check. It contains list of missing permissions.
88+
*/
89+
private IAMCheckResult check(
90+
CloudResourceManager resourceManager, List<String> requiredPermissions) {
91+
HashSet<String> grantedPermissions =
92+
new HashSet<>(checkPermission(resourceManager, projectIdResource, requiredPermissions));
93+
94+
List<String> missingPermissions =
95+
requiredPermissions.stream()
96+
.filter(p -> !grantedPermissions.contains(p))
97+
.collect(Collectors.toList());
98+
99+
return new IAMCheckResult(projectIdResource, missingPermissions);
100+
}
101+
102+
private List<String> checkPermission(
103+
CloudResourceManager resourceManager, String resourceName, List<String> permissions) {
104+
try {
105+
106+
TestIamPermissionsRequest requestBody =
107+
new TestIamPermissionsRequest().setPermissions(permissions);
108+
109+
TestIamPermissionsResponse testIamPermissionsResponse =
110+
resourceManager.projects().testIamPermissions(resourceName, requestBody).execute();
111+
112+
List<String> granted = testIamPermissionsResponse.getPermissions();
113+
return granted == null ? Collections.emptyList() : granted;
114+
} catch (IOException e) {
115+
throw new RuntimeException("Failed to check project permissions", e);
116+
}
117+
}
118+
119+
private CloudResourceManager createCloudResourceManagerService()
120+
throws IOException, GeneralSecurityException {
121+
if (resourceManagerForTesting != null) {
122+
return resourceManagerForTesting;
123+
}
124+
HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
125+
JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
126+
HttpRequestInitializer initializer = getHttpRequestInitializer(this.credential);
127+
CloudResourceManager service =
128+
new CloudResourceManager.Builder(httpTransport, jsonFactory, initializer)
129+
.setApplicationName("service-accounts")
130+
.build();
131+
return service;
132+
}
133+
134+
private static HttpRequestInitializer getHttpRequestInitializer(Credentials credential)
135+
throws IOException {
136+
if (credential == null) {
137+
try {
138+
return GoogleCredential.getApplicationDefault();
139+
} catch (Exception e) {
140+
return new NullCredentialInitializer();
141+
}
142+
} else {
143+
return new HttpCredentialsAdapter(credential);
144+
}
145+
}
146+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright (C) 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.google.cloud.teleport.v2.iam;
17+
18+
import com.google.common.collect.ImmutableList;
19+
import java.util.List;
20+
21+
public class IAMRequirementsCreator {
22+
private static final List<String> SPANNER_PERMISSIONS =
23+
ImmutableList.of(
24+
"spanner.databases.beginOrRollbackReadWriteTransaction",
25+
"spanner.databases.beginPartitionedDmlTransaction",
26+
"spanner.databases.beginReadOnlyTransaction",
27+
"spanner.databases.create",
28+
"spanner.databases.drop",
29+
"spanner.databases.get",
30+
"spanner.databases.getDdl",
31+
"spanner.databases.list",
32+
"spanner.databases.partitionQuery",
33+
"spanner.databases.partitionRead",
34+
"spanner.databases.read",
35+
"spanner.databases.select",
36+
"spanner.databases.update",
37+
"spanner.databases.updateDdl",
38+
"spanner.databases.write",
39+
"spanner.instances.get",
40+
"spanner.instances.list");
41+
42+
public static IAMResourceRequirements createSpannerResourceRequirement() {
43+
return new IAMResourceRequirements(SPANNER_PERMISSIONS);
44+
}
45+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (C) 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.google.cloud.teleport.v2.iam;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
21+
/** Represents the IAM permissions required on a specific GCP resource. */
22+
public class IAMResourceRequirements {
23+
private final List<String> permissions;
24+
25+
public IAMResourceRequirements(List<String> permissions) {
26+
if (permissions == null || permissions.isEmpty()) {
27+
throw new IllegalArgumentException("Permissions list must not be empty");
28+
}
29+
this.permissions = new ArrayList<>(permissions);
30+
}
31+
32+
public List<String> getPermissions() {
33+
return new ArrayList<>(permissions);
34+
}
35+
36+
@Override
37+
public String toString() {
38+
return getClass().getSimpleName() + "{" + "permissions=" + permissions + '}';
39+
}
40+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright (C) 2020 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
/** IAM validatory Utility classes for templates. */
18+
package com.google.cloud.teleport.v2.iam;

0 commit comments

Comments
 (0)