Skip to content

Commit bad3bc7

Browse files
gcp sts access boundary
1 parent 70f51f6 commit bad3bc7

File tree

2 files changed

+200
-6
lines changed

2 files changed

+200
-6
lines changed

sts/sts-client/src/main/java/com/salesforce/multicloudj/sts/driver/AbstractSts.java

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
import com.salesforce.multicloudj.sts.model.AssumeRoleWebIdentityRequest;
55
import com.salesforce.multicloudj.sts.model.AssumedRoleRequest;
66
import com.salesforce.multicloudj.sts.model.CallerIdentity;
7+
import com.salesforce.multicloudj.sts.model.CredentialScope;
78
import com.salesforce.multicloudj.sts.model.GetAccessTokenRequest;
89
import com.salesforce.multicloudj.sts.model.GetCallerIdentityRequest;
910
import com.salesforce.multicloudj.sts.model.StsCredentials;
10-
import lombok.Getter;
11-
1211
import java.net.URI;
1312

13+
import lombok.Getter;
14+
1415
/**
1516
* Abstract base class for Security Token Service (STS) implementations.
1617
* This class is internal for SDK and all the providers for STS implementations
@@ -22,6 +23,7 @@ public abstract class AbstractSts implements Provider {
2223

2324
/**
2425
* Constructs an AbstractSts instance using a Builder.
26+
*
2527
* @param builder The Builder instance to use for construction.
2628
*/
2729
public AbstractSts(Builder<?, ?> builder) {
@@ -30,6 +32,7 @@ public AbstractSts(Builder<?, ?> builder) {
3032

3133
/**
3234
* Constructs an AbstractSts instance with specified provider ID and region.
35+
*
3336
* @param providerId The ID of the provider.
3437
* @param region The region for the STS.
3538
*/
@@ -48,15 +51,62 @@ public String getProviderId() {
4851

4952
/**
5053
* Assumes a role and returns the credentialsOverrider.
54+
*
5155
* @param request The AssumedRoleRequest containing role information.
5256
* @return StsCredentials for the assumed role.
57+
* @throws IllegalArgumentException if credential scope contains non-storage
58+
* permissions or resources
5359
*/
5460
public StsCredentials assumeRole(AssumedRoleRequest request) {
61+
validateCredentialScope(request.getCredentialScope());
5562
return getSTSCredentialsWithAssumeRole(request);
5663
}
5764

65+
/**
66+
* Validates that CredentialScope only contains storage-related permissions and resources.
67+
*
68+
* @param credentialScope The CredentialScope to validate (can be null)
69+
* @throws IllegalArgumentException if scope contains non-storage permissions or resources
70+
*/
71+
private void validateCredentialScope(CredentialScope credentialScope) {
72+
if (credentialScope == null) {
73+
return; // null is valid - no scope restrictions
74+
}
75+
76+
for (CredentialScope.ScopeRule rule : credentialScope.getRules()) {
77+
// Validate resource is storage-only
78+
String resource = rule.getAvailableResource();
79+
if (resource != null && !resource.startsWith("storage://")) {
80+
throw new IllegalArgumentException(
81+
"Credential scope resource must start with 'storage://'. Found: "
82+
+ resource);
83+
}
84+
85+
// Validate all permissions are storage-only
86+
for (String permission : rule.getAvailablePermissions()) {
87+
if (!permission.startsWith("storage:")) {
88+
throw new IllegalArgumentException(
89+
"Credential scope permission must start with 'storage:'. Found: "
90+
+ permission);
91+
}
92+
}
93+
94+
// Validate condition resourcePrefix is storage-only (if present)
95+
if (rule.getAvailabilityCondition() != null) {
96+
String resourcePrefix = rule.getAvailabilityCondition().getResourcePrefix();
97+
if (resourcePrefix != null && !resourcePrefix.startsWith("storage://")) {
98+
throw new IllegalArgumentException(
99+
"Credential scope condition resourcePrefix must start with "
100+
+ "'storage://'. Found: " + resourcePrefix);
101+
}
102+
}
103+
}
104+
}
105+
58106
/**
59107
* Retrieves the caller identity.
108+
*
109+
* @param request The GetCallerIdentityRequest.
60110
* @return The CallerIdentity of the current caller.
61111
*/
62112
public CallerIdentity getCallerIdentity(GetCallerIdentityRequest request) {
@@ -65,6 +115,7 @@ public CallerIdentity getCallerIdentity(GetCallerIdentityRequest request) {
65115

66116
/**
67117
* Retrieves an access token.
118+
*
68119
* @param request The GetAccessTokenRequest containing token request details.
69120
* @return StsCredentials containing the access token.
70121
*/
@@ -74,7 +125,9 @@ public StsCredentials getAccessToken(GetAccessTokenRequest request) {
74125

75126
/**
76127
* Assumes a role with web identity and returns the credentials.
77-
* @param request The AssumeRoleWithWebIdentityRequest containing role and web identity token information.
128+
*
129+
* @param request The AssumeRoleWithWebIdentityRequest containing role and web identity
130+
* token information.
78131
* @return StsCredentials for the assumed role with web identity.
79132
*/
80133
public StsCredentials assumeRoleWithWebIdentity(AssumeRoleWebIdentityRequest request) {
@@ -83,10 +136,12 @@ public StsCredentials assumeRoleWithWebIdentity(AssumeRoleWebIdentityRequest req
83136

84137
/**
85138
* Abstract builder class for AbstractSts implementations.
139+
*
86140
* @param <A> The concrete implementation type of AbstractSts.
87141
* @param <T> The concrete implementation type of Builder.
88142
*/
89-
public abstract static class Builder<A extends AbstractSts, T extends Builder<A, T>> implements Provider.Builder {
143+
public abstract static class Builder<A extends AbstractSts, T extends Builder<A, T>>
144+
implements Provider.Builder {
90145
@Getter
91146
protected String region;
92147
@Getter
@@ -95,6 +150,7 @@ public abstract static class Builder<A extends AbstractSts, T extends Builder<A,
95150

96151
/**
97152
* Sets the region.
153+
*
98154
* @param region The region to set.
99155
* @return This Builder instance.
100156
*/
@@ -105,6 +161,7 @@ public T withRegion(String region) {
105161

106162
/**
107163
* Sets the endpoint to override.
164+
*
108165
* @param endpoint The endpoint to set.
109166
* @return This Builder instance.
110167
*/
@@ -122,39 +179,52 @@ public T providerId(String providerId) {
122179
return self();
123180
}
124181

182+
/**
183+
* Returns the builder instance.
184+
*
185+
* @return This Builder instance.
186+
*/
125187
public abstract T self();
126188

127189
/**
128190
* Builds and returns an instance of AbstractSts.
191+
*
129192
* @return An instance of AbstractSts.
130193
*/
131194
public abstract A build();
132195
}
133196

134197
/**
135198
* Retrieves STS credentialsOverrider with assumed role.
199+
*
136200
* @param request The AssumedRoleRequest.
137201
* @return StsCredentials for the assumed role.
138202
*/
139203
protected abstract StsCredentials getSTSCredentialsWithAssumeRole(AssumedRoleRequest request);
140204

141205
/**
142206
* Retrieves the caller identity from the provider.
207+
*
208+
* @param request The GetCallerIdentityRequest.
143209
* @return The CallerIdentity.
144210
*/
145-
protected abstract CallerIdentity getCallerIdentityFromProvider(GetCallerIdentityRequest request);
211+
protected abstract CallerIdentity getCallerIdentityFromProvider(
212+
GetCallerIdentityRequest request);
146213

147214
/**
148215
* Retrieves an access token from the provider.
216+
*
149217
* @param request The GetAccessTokenRequest.
150218
* @return StsCredentials containing the access token.
151219
*/
152220
protected abstract StsCredentials getAccessTokenFromProvider(GetAccessTokenRequest request);
153221

154222
/**
155223
* Retrieves STS credentials with AssumeRoleWithWebIdentity.
224+
*
156225
* @param request The AssumeRoleWithWebIdentityRequest.
157226
* @return StsCredentials for the assumed role with web identity.
158227
*/
159-
protected abstract StsCredentials getSTSCredentialsWithAssumeRoleWebIdentity(AssumeRoleWebIdentityRequest request);
228+
protected abstract StsCredentials getSTSCredentialsWithAssumeRoleWebIdentity(
229+
AssumeRoleWebIdentityRequest request);
160230
}

sts/sts-client/src/test/java/com/salesforce/multicloudj/sts/driver/AbstractStsTest.java

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.salesforce.multicloudj.common.provider.Provider;
55
import com.salesforce.multicloudj.sts.model.AssumedRoleRequest;
66
import com.salesforce.multicloudj.sts.model.CallerIdentity;
7+
import com.salesforce.multicloudj.sts.model.CredentialScope;
78
import com.salesforce.multicloudj.sts.model.GetAccessTokenRequest;
89
import com.salesforce.multicloudj.sts.model.GetCallerIdentityRequest;
910
import com.salesforce.multicloudj.sts.model.StsCredentials;
@@ -14,6 +15,7 @@
1415

1516
import static org.junit.jupiter.api.Assertions.assertEquals;
1617
import static org.junit.jupiter.api.Assertions.assertNotNull;
18+
import static org.junit.jupiter.api.Assertions.assertThrows;
1719
import static org.mockito.Mockito.mock;
1820

1921
public class AbstractStsTest {
@@ -117,4 +119,126 @@ public void testGetCallerIdentity() {
117119
assertEquals("testResource", identity.getCloudResourceName());
118120
assertEquals("testUser", identity.getUserId());
119121
}
122+
123+
@Test
124+
public void testValidCredentialScope() {
125+
TestSts sts = builder.build();
126+
127+
// Valid storage-only credential scope
128+
CredentialScope.ScopeRule rule = CredentialScope.ScopeRule.builder()
129+
.availableResource("storage://my-bucket")
130+
.availablePermission("storage:GetObject")
131+
.availablePermission("storage:PutObject")
132+
.build();
133+
134+
CredentialScope credentialScope = CredentialScope.builder()
135+
.rule(rule)
136+
.build();
137+
138+
AssumedRoleRequest request = AssumedRoleRequest.newBuilder()
139+
.withRole("test-role")
140+
.withCredentialScope(credentialScope)
141+
.build();
142+
143+
// Should not throw
144+
StsCredentials credentials = sts.assumeRole(request);
145+
assertNotNull(credentials);
146+
}
147+
148+
@Test
149+
public void testInvalidCredentialScopeWithNonStorageResource() {
150+
TestSts sts = builder.build();
151+
152+
// Invalid: non-storage resource
153+
CredentialScope.ScopeRule rule = CredentialScope.ScopeRule.builder()
154+
.availableResource("compute://my-instance")
155+
.availablePermission("storage:GetObject")
156+
.build();
157+
158+
CredentialScope credentialScope = CredentialScope.builder()
159+
.rule(rule)
160+
.build();
161+
162+
AssumedRoleRequest request = AssumedRoleRequest.newBuilder()
163+
.withRole("test-role")
164+
.withCredentialScope(credentialScope)
165+
.build();
166+
167+
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
168+
sts.assumeRole(request);
169+
});
170+
assertEquals("Credential scope resource must start with 'storage://'. Found: compute://my-instance",
171+
exception.getMessage());
172+
}
173+
174+
@Test
175+
public void testInvalidCredentialScopeWithNonStoragePermission() {
176+
TestSts sts = builder.build();
177+
178+
// Invalid: non-storage permission
179+
CredentialScope.ScopeRule rule = CredentialScope.ScopeRule.builder()
180+
.availableResource("storage://my-bucket")
181+
.availablePermission("compute:StartInstance")
182+
.build();
183+
184+
CredentialScope credentialScope = CredentialScope.builder()
185+
.rule(rule)
186+
.build();
187+
188+
AssumedRoleRequest request = AssumedRoleRequest.newBuilder()
189+
.withRole("test-role")
190+
.withCredentialScope(credentialScope)
191+
.build();
192+
193+
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
194+
sts.assumeRole(request);
195+
});
196+
assertEquals("Credential scope permission must start with 'storage:'. Found: compute:StartInstance",
197+
exception.getMessage());
198+
}
199+
200+
@Test
201+
public void testInvalidCredentialScopeWithNonStorageConditionPrefix() {
202+
TestSts sts = builder.build();
203+
204+
// Invalid: non-storage condition resourcePrefix
205+
CredentialScope.AvailabilityCondition condition = CredentialScope.AvailabilityCondition.builder()
206+
.resourcePrefix("compute://my-instance/logs/")
207+
.build();
208+
209+
CredentialScope.ScopeRule rule = CredentialScope.ScopeRule.builder()
210+
.availableResource("storage://my-bucket")
211+
.availablePermission("storage:GetObject")
212+
.availabilityCondition(condition)
213+
.build();
214+
215+
CredentialScope credentialScope = CredentialScope.builder()
216+
.rule(rule)
217+
.build();
218+
219+
AssumedRoleRequest request = AssumedRoleRequest.newBuilder()
220+
.withRole("test-role")
221+
.withCredentialScope(credentialScope)
222+
.build();
223+
224+
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
225+
sts.assumeRole(request);
226+
});
227+
assertEquals("Credential scope condition resourcePrefix must start with 'storage://'. Found: compute://my-instance/logs/",
228+
exception.getMessage());
229+
}
230+
231+
@Test
232+
public void testNullCredentialScopeIsValid() {
233+
TestSts sts = builder.build();
234+
235+
// Null credential scope is valid
236+
AssumedRoleRequest request = AssumedRoleRequest.newBuilder()
237+
.withRole("test-role")
238+
.build();
239+
240+
// Should not throw
241+
StsCredentials credentials = sts.assumeRole(request);
242+
assertNotNull(credentials);
243+
}
120244
}

0 commit comments

Comments
 (0)