Skip to content

Commit 29be9d7

Browse files
committed
Dynamically add space developer role for non-admin clients
When using the space per service instance strategy, currently the OAuth client requires `cloud_controller.admin` privileges to deploy apps and services to a new space. This commit adds support for dynamically granting the space developer role to a client which has the org manager role but not admin privileges. Resolves #203 Requiring client credentials to run ATs
1 parent bc75e24 commit 29be9d7

File tree

10 files changed

+361
-131
lines changed

10 files changed

+361
-131
lines changed

spring-cloud-app-broker-acceptance-tests/README.adoc

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,8 @@ The tests require the following properties to be set:
1616
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.default-org` - The CF organization where the tests are going to run.
1717
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.default-space` - The CF space where the tests are going to run.
1818
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.skip-ssl-validation` - If SSL validation should be skipped.
19-
20-
To authenticate to the target CF with a user account, set the following properties:
21-
2219
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.username` - The CF API username.
2320
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.password` - The CF API password.
24-
25-
To authenticate to the target CF with an OAuth2 client in UAA, set the following properties:
26-
2721
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.client-id` - The CF API OAuth2 client ID.
2822
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.client-secret` - The CF API OAuth2 client secret.
2923

spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/CloudFoundryAcceptanceTest.java

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
import com.jayway.jsonpath.spi.mapper.MappingProvider;
3737
import org.cloudfoundry.operations.applications.ApplicationEnvironments;
3838
import org.cloudfoundry.operations.applications.ApplicationSummary;
39+
import org.cloudfoundry.operations.organizations.OrganizationSummary;
3940
import org.cloudfoundry.operations.services.ServiceInstance;
41+
import org.cloudfoundry.operations.spaces.SpaceSummary;
4042
import org.cloudfoundry.uaa.clients.GetClientResponse;
4143
import org.junit.jupiter.api.AfterEach;
4244
import org.junit.jupiter.api.BeforeEach;
@@ -54,6 +56,12 @@
5456
import org.springframework.web.client.RestTemplate;
5557

5658
import static org.assertj.core.api.Assertions.assertThat;
59+
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.ACCEPTANCE_TEST_OAUTH_CLIENT_AUTHORITIES;
60+
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.ACCEPTANCE_TEST_OAUTH_CLIENT_ID;
61+
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET;
62+
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.APP_BROKER_CLIENT_AUTHORITIES;
63+
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.APP_BROKER_CLIENT_ID;
64+
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.APP_BROKER_CLIENT_SECRET;
5765

5866
@SpringBootTest(classes = {
5967
CloudFoundryClientConfiguration.class,
@@ -114,24 +122,41 @@ public Set<Option> options() {
114122

115123
@AfterEach
116124
void tearDown() {
117-
blockingSubscribe(cleanup());
125+
blockingSubscribe(cloudFoundryService.getOrCreateDefaultOrganization()
126+
.map(OrganizationSummary::getId)
127+
.flatMap(orgId -> cloudFoundryService.getOrCreateDefaultSpace()
128+
.map(SpaceSummary::getId)
129+
.flatMap(spaceId -> cleanup(orgId, spaceId))));
118130
}
119131

120132
private Mono<Void> initializeBroker(String... appBrokerProperties) {
121-
return cleanup()
122-
.then(
123-
cloudFoundryService
124-
.getOrCreateDefaultOrganization()
125-
.then(cloudFoundryService.getOrCreateDefaultSpace())
133+
return cloudFoundryService
134+
.getOrCreateDefaultOrganization()
135+
.map(OrganizationSummary::getId)
136+
.flatMap(orgId -> cloudFoundryService
137+
.getOrCreateDefaultSpace()
138+
.map(SpaceSummary::getId)
139+
.flatMap(spaceId -> cleanup(orgId, spaceId)
140+
.then(uaaService.createClient(
141+
ACCEPTANCE_TEST_OAUTH_CLIENT_ID,
142+
ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET,
143+
ACCEPTANCE_TEST_OAUTH_CLIENT_AUTHORITIES))
144+
.then(uaaService.createClient(
145+
APP_BROKER_CLIENT_ID,
146+
APP_BROKER_CLIENT_SECRET,
147+
APP_BROKER_CLIENT_AUTHORITIES))
148+
.then(cloudFoundryService.associateAppBrokerClientWithOrgAndSpace(orgId, spaceId))
126149
.then(cloudFoundryService.pushBrokerApp(TEST_BROKER_APP_NAME, getTestBrokerAppPath(), appBrokerProperties))
127150
.then(cloudFoundryService.createServiceBroker(SERVICE_BROKER_NAME, TEST_BROKER_APP_NAME))
128151
.then(cloudFoundryService.enableServiceBrokerAccess(APP_SERVICE_NAME))
129-
.then(cloudFoundryService.enableServiceBrokerAccess(BACKING_SERVICE_NAME)));
152+
.then(cloudFoundryService.enableServiceBrokerAccess(BACKING_SERVICE_NAME))));
130153
}
131154

132-
private Mono<Void> cleanup() {
155+
private Mono<Void> cleanup(String orgId, String spaceId) {
133156
return cloudFoundryService.deleteServiceBroker(SERVICE_BROKER_NAME)
134-
.then(cloudFoundryService.deleteApp(TEST_BROKER_APP_NAME));
157+
.then(cloudFoundryService.deleteApp(TEST_BROKER_APP_NAME))
158+
.then(cloudFoundryService.removeAppBrokerClientFromOrgAndSpace(orgId, spaceId))
159+
.onErrorResume(e -> Mono.empty());
135160
}
136161

137162
void createServiceInstance(String serviceInstanceName) {

spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryClientConfiguration.java

Lines changed: 40 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import java.util.Optional;
2020

21-
import org.cloudfoundry.UnknownCloudFoundryException;
2221
import org.cloudfoundry.client.CloudFoundryClient;
2322
import org.cloudfoundry.doppler.DopplerClient;
2423
import org.cloudfoundry.operations.CloudFoundryOperations;
@@ -32,31 +31,39 @@
3231
import org.cloudfoundry.reactor.tokenprovider.PasswordGrantTokenProvider;
3332
import org.cloudfoundry.reactor.uaa.ReactorUaaClient;
3433
import org.cloudfoundry.uaa.UaaClient;
35-
import org.cloudfoundry.uaa.UaaException;
36-
import org.cloudfoundry.uaa.clients.Clients;
37-
import org.cloudfoundry.uaa.clients.CreateClientRequest;
38-
import org.cloudfoundry.uaa.clients.DeleteClientRequest;
39-
import org.cloudfoundry.uaa.tokens.GrantType;
34+
35+
import org.springframework.beans.factory.annotation.Qualifier;
4036
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
4137
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4238
import org.springframework.context.annotation.Bean;
4339
import org.springframework.context.annotation.Configuration;
44-
import reactor.core.publisher.Mono;
4540

4641
@Configuration
4742
@EnableConfigurationProperties(CloudFoundryProperties.class)
4843
public class CloudFoundryClientConfiguration {
4944

50-
static final String ACCEPTANCE_TEST_OAUTH_CLIENT_ID = "acceptance-test-client";
51-
static final String ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET = "acceptance-test-client-secret";
52-
private static final String[] ACCEPTANCE_TEST_OAUTH_CLIENT_AUTHORITIES = {
53-
"openid", "cloud_controller.admin", "cloud_controller.read", "cloud_controller.write",
54-
"clients.read", "clients.write"
45+
public static final String ACCEPTANCE_TEST_OAUTH_CLIENT_ID = "acceptance-test-client";
46+
public static final String ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET = "acceptance-test-client-secret";
47+
public static final String[] ACCEPTANCE_TEST_OAUTH_CLIENT_AUTHORITIES = {
48+
"openid",
49+
"cloud_controller.admin",
50+
"cloud_controller.read",
51+
"cloud_controller.write",
52+
"clients.read",
53+
"clients.write"
54+
};
55+
56+
public static final String APP_BROKER_CLIENT_ID = "app-broker-client";
57+
public static final String APP_BROKER_CLIENT_SECRET = "app-broker-client-secret";
58+
public static final String[] APP_BROKER_CLIENT_AUTHORITIES = {
59+
"cloud_controller.read", "cloud_controller.write"
5560
};
5661

5762
@Bean
58-
CloudFoundryOperations cloudFoundryOperations(CloudFoundryProperties properties, CloudFoundryClient client,
59-
DopplerClient dopplerClient, UaaClient uaaClient) {
63+
CloudFoundryOperations cloudFoundryOperations(CloudFoundryProperties properties,
64+
CloudFoundryClient client,
65+
DopplerClient dopplerClient,
66+
UaaClient uaaClient) {
6067
return DefaultCloudFoundryOperations.builder()
6168
.cloudFoundryClient(client)
6269
.dopplerClient(dopplerClient)
@@ -67,7 +74,8 @@ CloudFoundryOperations cloudFoundryOperations(CloudFoundryProperties properties,
6774
}
6875

6976
@Bean
70-
CloudFoundryClient cloudFoundryClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
77+
CloudFoundryClient cloudFoundryClient(ConnectionContext connectionContext,
78+
@Qualifier("userCredentials") TokenProvider tokenProvider) {
7179
return ReactorCloudFoundryClient.builder()
7280
.connectionContext(connectionContext)
7381
.tokenProvider(tokenProvider)
@@ -85,24 +93,29 @@ ConnectionContext connectionContext(CloudFoundryProperties properties) {
8593
}
8694

8795
@Bean
88-
DopplerClient dopplerClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
96+
DopplerClient dopplerClient(ConnectionContext connectionContext,
97+
@Qualifier("userCredentials") TokenProvider tokenProvider) {
8998
return ReactorDopplerClient.builder()
9099
.connectionContext(connectionContext)
91100
.tokenProvider(tokenProvider)
92101
.build();
93102
}
94103

95104
@Bean
96-
UaaClient uaaClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
105+
UaaClient uaaClient(ConnectionContext connectionContext,
106+
@Qualifier("clientCredentials") TokenProvider tokenProvider) {
97107
return ReactorUaaClient.builder()
98108
.connectionContext(connectionContext)
99109
.tokenProvider(tokenProvider)
100110
.build();
101111
}
102112

103113
@Bean
104-
@ConditionalOnProperty({CloudFoundryProperties.PROPERTY_PREFIX + ".username",
105-
CloudFoundryProperties.PROPERTY_PREFIX + ".password"})
114+
@Qualifier("userCredentials")
115+
@ConditionalOnProperty({
116+
CloudFoundryProperties.PROPERTY_PREFIX + ".username",
117+
CloudFoundryProperties.PROPERTY_PREFIX + ".password"
118+
})
106119
PasswordGrantTokenProvider passwordTokenProvider(CloudFoundryProperties properties) {
107120
return PasswordGrantTokenProvider.builder()
108121
.password(properties.getPassword())
@@ -111,42 +124,17 @@ PasswordGrantTokenProvider passwordTokenProvider(CloudFoundryProperties properti
111124
}
112125

113126
@Bean
114-
@ConditionalOnProperty({CloudFoundryProperties.PROPERTY_PREFIX + ".client-id",
115-
CloudFoundryProperties.PROPERTY_PREFIX + ".client-secret"})
116-
ClientCredentialsGrantTokenProvider clientTokenProvider(ConnectionContext connectionContext,
117-
CloudFoundryProperties properties) {
118-
119-
Clients uaaClients = buildTempUaaClient(connectionContext, properties).clients();
120-
121-
uaaClients.delete(DeleteClientRequest.builder()
122-
.clientId(ACCEPTANCE_TEST_OAUTH_CLIENT_ID)
123-
.build())
124-
.onErrorResume(UaaException.class, e -> Mono.empty())
125-
.onErrorResume(UnknownCloudFoundryException.class, e -> Mono.empty())
126-
.then(uaaClients.create(CreateClientRequest.builder()
127-
.clientId(ACCEPTANCE_TEST_OAUTH_CLIENT_ID)
128-
.clientSecret(ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET)
129-
.authorizedGrantType(GrantType.CLIENT_CREDENTIALS)
130-
.authorities(ACCEPTANCE_TEST_OAUTH_CLIENT_AUTHORITIES)
131-
.build()))
132-
.block();
133-
127+
@Qualifier("clientCredentials")
128+
@ConditionalOnProperty({
129+
CloudFoundryProperties.PROPERTY_PREFIX + ".client-id",
130+
CloudFoundryProperties.PROPERTY_PREFIX + ".client-secret"
131+
})
132+
ClientCredentialsGrantTokenProvider clientTokenProvider(CloudFoundryProperties properties) {
134133
return ClientCredentialsGrantTokenProvider.builder()
135-
.clientId(ACCEPTANCE_TEST_OAUTH_CLIENT_ID)
136-
.clientSecret(ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET)
134+
.clientId(properties.getClientId())
135+
.clientSecret(properties.getClientSecret())
137136
.identityZoneSubdomain(properties.getIdentityZoneSubdomain())
138137
.build();
139138
}
140139

141-
private UaaClient buildTempUaaClient(ConnectionContext connectionContext, CloudFoundryProperties properties) {
142-
return ReactorUaaClient.builder()
143-
.connectionContext(connectionContext)
144-
.tokenProvider(ClientCredentialsGrantTokenProvider.builder()
145-
.clientId(properties.getClientId())
146-
.clientSecret(properties.getClientSecret())
147-
.identityZoneSubdomain(properties.getIdentityZoneSubdomain())
148-
.build())
149-
.build();
150-
}
151-
152140
}

spring-cloud-app-broker-acceptance-tests/src/test/java/org.springframework.cloud.appbroker.acceptance/fixtures/cf/CloudFoundryService.java

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@
2222
import java.util.List;
2323
import java.util.Map;
2424

25+
import org.cloudfoundry.client.CloudFoundryClient;
26+
import org.cloudfoundry.client.v2.organizations.AssociateOrganizationManagerRequest;
27+
import org.cloudfoundry.client.v2.organizations.AssociateOrganizationManagerResponse;
28+
import org.cloudfoundry.client.v2.organizations.AssociateOrganizationUserRequest;
29+
import org.cloudfoundry.client.v2.organizations.AssociateOrganizationUserResponse;
30+
import org.cloudfoundry.client.v2.organizations.RemoveOrganizationManagerRequest;
31+
import org.cloudfoundry.client.v2.organizations.RemoveOrganizationUserRequest;
32+
import org.cloudfoundry.client.v2.spaces.AssociateSpaceDeveloperRequest;
33+
import org.cloudfoundry.client.v2.spaces.AssociateSpaceDeveloperResponse;
34+
import org.cloudfoundry.client.v2.spaces.RemoveSpaceDeveloperRequest;
2535
import org.cloudfoundry.operations.CloudFoundryOperations;
2636
import org.cloudfoundry.operations.DefaultCloudFoundryOperations;
2737
import org.cloudfoundry.operations.applications.ApplicationDetail;
@@ -63,11 +73,16 @@ public class CloudFoundryService {
6373

6474
private static final String DEPLOYER_PROPERTY_PREFIX = "spring.cloud.appbroker.deployer.cloudfoundry.";
6575

76+
private final CloudFoundryClient cloudFoundryClient;
77+
6678
private final CloudFoundryOperations cloudFoundryOperations;
79+
6780
private final CloudFoundryProperties cloudFoundryProperties;
6881

69-
public CloudFoundryService(CloudFoundryOperations cloudFoundryOperations,
82+
public CloudFoundryService(CloudFoundryClient cloudFoundryClient,
83+
CloudFoundryOperations cloudFoundryOperations,
7084
CloudFoundryProperties cloudFoundryProperties) {
85+
this.cloudFoundryClient = cloudFoundryClient;
7186
this.cloudFoundryOperations = cloudFoundryOperations;
7287
this.cloudFoundryProperties = cloudFoundryProperties;
7388
}
@@ -129,7 +144,7 @@ public Mono<Void> deleteApp(String appName) {
129144
.deleteRoutes(true)
130145
.build())
131146
.doOnSuccess(item -> LOGGER.info("Deleted app " + appName))
132-
.doOnError(error -> LOGGER.error("Error deleting app " + appName + ": " + error))
147+
.doOnError(error -> LOGGER.warn("Error deleting app " + appName + ": " + error))
133148
.onErrorResume(e -> Mono.empty());
134149
}
135150

@@ -139,7 +154,7 @@ public Mono<Void> deleteServiceBroker(String brokerName) {
139154
.name(brokerName)
140155
.build())
141156
.doOnSuccess(item -> LOGGER.info("Deleted service broker " + brokerName))
142-
.doOnError(error -> LOGGER.error("Error deleting service broker " + brokerName + ": " + error))
157+
.doOnError(error -> LOGGER.warn("Error deleting service broker " + brokerName + ": " + error))
143158
.onErrorResume(e -> Mono.empty());
144159
}
145160

@@ -152,7 +167,7 @@ public Mono<Void> deleteServiceInstance(String serviceInstanceName) {
152167
.doOnSuccess(item -> LOGGER.info("Deleted service instance " + serviceInstanceName))
153168
.doOnError(error -> LOGGER.error("Error deleting service instance " + serviceInstanceName + ": " + error))
154169
.onErrorResume(e -> Mono.empty()))
155-
.doOnError(error -> LOGGER.error("Error getting service instance " + serviceInstanceName + ": " + error))
170+
.doOnError(error -> LOGGER.warn("Error getting service instance " + serviceInstanceName + ": " + error))
156171
.onErrorResume(e -> Mono.empty());
157172
}
158173

@@ -303,6 +318,63 @@ private Mono<SpaceSummary> getDefaultSpace(Spaces spaceOperations) {
303318
.next();
304319
}
305320

321+
public Mono<Void> associateAppBrokerClientWithOrgAndSpace(String orgId, String spaceId) {
322+
return Mono.justOrEmpty(CloudFoundryClientConfiguration.APP_BROKER_CLIENT_ID)
323+
.flatMap(userId -> associateOrgUser(orgId, userId)
324+
.then(associateOrgManager(orgId, userId))
325+
.then(associateSpaceDeveloper(spaceId, userId)))
326+
.then();
327+
}
328+
329+
public Mono<Void> removeAppBrokerClientFromOrgAndSpace(String orgId, String spaceId) {
330+
return Mono.justOrEmpty(CloudFoundryClientConfiguration.APP_BROKER_CLIENT_ID)
331+
.flatMap(userId -> removeSpaceDeveloper(spaceId, userId)
332+
.then(removeOrgManager(orgId, userId))
333+
.then(removeOrgUser(orgId, userId)));
334+
}
335+
336+
private Mono<AssociateOrganizationUserResponse> associateOrgUser(String orgId, String userId) {
337+
return cloudFoundryClient.organizations().associateUser(AssociateOrganizationUserRequest.builder()
338+
.organizationId(orgId)
339+
.userId(userId)
340+
.build());
341+
}
342+
343+
private Mono<AssociateOrganizationManagerResponse> associateOrgManager(String orgId, String userId) {
344+
return cloudFoundryClient.organizations().associateManager(AssociateOrganizationManagerRequest.builder()
345+
.organizationId(orgId)
346+
.managerId(userId)
347+
.build());
348+
}
349+
350+
private Mono<AssociateSpaceDeveloperResponse> associateSpaceDeveloper(String spaceId, String userId) {
351+
return cloudFoundryClient.spaces().associateDeveloper(AssociateSpaceDeveloperRequest.builder()
352+
.spaceId(spaceId)
353+
.developerId(userId)
354+
.build());
355+
}
356+
357+
private Mono<Void> removeOrgUser(String orgId, String userId) {
358+
return cloudFoundryClient.organizations().removeUser(RemoveOrganizationUserRequest.builder()
359+
.organizationId(orgId)
360+
.userId(userId)
361+
.build());
362+
}
363+
364+
private Mono<Void> removeOrgManager(String orgId, String userId) {
365+
return cloudFoundryClient.organizations().removeManager(RemoveOrganizationManagerRequest.builder()
366+
.organizationId(orgId)
367+
.managerId(userId)
368+
.build());
369+
}
370+
371+
private Mono<Void> removeSpaceDeveloper(String spaceId, String userId) {
372+
return cloudFoundryClient.spaces().removeDeveloper(RemoveSpaceDeveloperRequest.builder()
373+
.spaceId(spaceId)
374+
.developerId(userId)
375+
.build());
376+
}
377+
306378
private CloudFoundryOperations createOperationsForSpace(String space) {
307379
final String defaultOrg = cloudFoundryProperties.getDefaultOrg();
308380
return DefaultCloudFoundryOperations.builder()
@@ -325,19 +397,10 @@ private Map<String, String> appBrokerDeployerEnvironmentVariables() {
325397
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "skip-ssl-validation",
326398
String.valueOf(cloudFoundryProperties.isSkipSslValidation()));
327399
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "properties.memory", "1024M");
328-
329-
if (cloudFoundryProperties.getUsername() == null || cloudFoundryProperties.getPassword() == null) {
330-
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "client-id",
331-
CloudFoundryClientConfiguration.ACCEPTANCE_TEST_OAUTH_CLIENT_ID);
332-
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "client-secret",
333-
CloudFoundryClientConfiguration.ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET);
334-
} else {
335-
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "username",
336-
cloudFoundryProperties.getUsername());
337-
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "password",
338-
cloudFoundryProperties.getPassword());
339-
}
340-
400+
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "client-id",
401+
CloudFoundryClientConfiguration.APP_BROKER_CLIENT_ID);
402+
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "client-secret",
403+
CloudFoundryClientConfiguration.APP_BROKER_CLIENT_SECRET);
341404
return deployerVariables;
342405
}
343406

0 commit comments

Comments
 (0)