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
This commit is contained in:
committed by
Roy Clarkson
parent
bc75e248c2
commit
29be9d77be
@@ -16,14 +16,8 @@ The tests require the following properties to be set:
|
||||
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.default-org` - The CF organization where the tests are going to run.
|
||||
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.default-space` - The CF space where the tests are going to run.
|
||||
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.skip-ssl-validation` - If SSL validation should be skipped.
|
||||
|
||||
To authenticate to the target CF with a user account, set the following properties:
|
||||
|
||||
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.username` - The CF API username.
|
||||
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.password` - The CF API password.
|
||||
|
||||
To authenticate to the target CF with an OAuth2 client in UAA, set the following properties:
|
||||
|
||||
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.client-id` - The CF API OAuth2 client ID.
|
||||
* `spring.cloud.appbroker.acceptance-test.cloudfoundry.client-secret` - The CF API OAuth2 client secret.
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@ import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
|
||||
import com.jayway.jsonpath.spi.mapper.MappingProvider;
|
||||
import org.cloudfoundry.operations.applications.ApplicationEnvironments;
|
||||
import org.cloudfoundry.operations.applications.ApplicationSummary;
|
||||
import org.cloudfoundry.operations.organizations.OrganizationSummary;
|
||||
import org.cloudfoundry.operations.services.ServiceInstance;
|
||||
import org.cloudfoundry.operations.spaces.SpaceSummary;
|
||||
import org.cloudfoundry.uaa.clients.GetClientResponse;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -54,6 +56,12 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.ACCEPTANCE_TEST_OAUTH_CLIENT_AUTHORITIES;
|
||||
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.ACCEPTANCE_TEST_OAUTH_CLIENT_ID;
|
||||
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET;
|
||||
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.APP_BROKER_CLIENT_AUTHORITIES;
|
||||
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.APP_BROKER_CLIENT_ID;
|
||||
import static org.springframework.cloud.appbroker.acceptance.fixtures.cf.CloudFoundryClientConfiguration.APP_BROKER_CLIENT_SECRET;
|
||||
|
||||
@SpringBootTest(classes = {
|
||||
CloudFoundryClientConfiguration.class,
|
||||
@@ -114,24 +122,41 @@ class CloudFoundryAcceptanceTest {
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
blockingSubscribe(cleanup());
|
||||
blockingSubscribe(cloudFoundryService.getOrCreateDefaultOrganization()
|
||||
.map(OrganizationSummary::getId)
|
||||
.flatMap(orgId -> cloudFoundryService.getOrCreateDefaultSpace()
|
||||
.map(SpaceSummary::getId)
|
||||
.flatMap(spaceId -> cleanup(orgId, spaceId))));
|
||||
}
|
||||
|
||||
private Mono<Void> initializeBroker(String... appBrokerProperties) {
|
||||
return cleanup()
|
||||
.then(
|
||||
cloudFoundryService
|
||||
.getOrCreateDefaultOrganization()
|
||||
.then(cloudFoundryService.getOrCreateDefaultSpace())
|
||||
return cloudFoundryService
|
||||
.getOrCreateDefaultOrganization()
|
||||
.map(OrganizationSummary::getId)
|
||||
.flatMap(orgId -> cloudFoundryService
|
||||
.getOrCreateDefaultSpace()
|
||||
.map(SpaceSummary::getId)
|
||||
.flatMap(spaceId -> cleanup(orgId, spaceId)
|
||||
.then(uaaService.createClient(
|
||||
ACCEPTANCE_TEST_OAUTH_CLIENT_ID,
|
||||
ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET,
|
||||
ACCEPTANCE_TEST_OAUTH_CLIENT_AUTHORITIES))
|
||||
.then(uaaService.createClient(
|
||||
APP_BROKER_CLIENT_ID,
|
||||
APP_BROKER_CLIENT_SECRET,
|
||||
APP_BROKER_CLIENT_AUTHORITIES))
|
||||
.then(cloudFoundryService.associateAppBrokerClientWithOrgAndSpace(orgId, spaceId))
|
||||
.then(cloudFoundryService.pushBrokerApp(TEST_BROKER_APP_NAME, getTestBrokerAppPath(), appBrokerProperties))
|
||||
.then(cloudFoundryService.createServiceBroker(SERVICE_BROKER_NAME, TEST_BROKER_APP_NAME))
|
||||
.then(cloudFoundryService.enableServiceBrokerAccess(APP_SERVICE_NAME))
|
||||
.then(cloudFoundryService.enableServiceBrokerAccess(BACKING_SERVICE_NAME)));
|
||||
.then(cloudFoundryService.enableServiceBrokerAccess(BACKING_SERVICE_NAME))));
|
||||
}
|
||||
|
||||
private Mono<Void> cleanup() {
|
||||
private Mono<Void> cleanup(String orgId, String spaceId) {
|
||||
return cloudFoundryService.deleteServiceBroker(SERVICE_BROKER_NAME)
|
||||
.then(cloudFoundryService.deleteApp(TEST_BROKER_APP_NAME));
|
||||
.then(cloudFoundryService.deleteApp(TEST_BROKER_APP_NAME))
|
||||
.then(cloudFoundryService.removeAppBrokerClientFromOrgAndSpace(orgId, spaceId))
|
||||
.onErrorResume(e -> Mono.empty());
|
||||
}
|
||||
|
||||
void createServiceInstance(String serviceInstanceName) {
|
||||
|
||||
@@ -18,7 +18,6 @@ package org.springframework.cloud.appbroker.acceptance.fixtures.cf;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.cloudfoundry.UnknownCloudFoundryException;
|
||||
import org.cloudfoundry.client.CloudFoundryClient;
|
||||
import org.cloudfoundry.doppler.DopplerClient;
|
||||
import org.cloudfoundry.operations.CloudFoundryOperations;
|
||||
@@ -32,31 +31,39 @@ import org.cloudfoundry.reactor.tokenprovider.ClientCredentialsGrantTokenProvide
|
||||
import org.cloudfoundry.reactor.tokenprovider.PasswordGrantTokenProvider;
|
||||
import org.cloudfoundry.reactor.uaa.ReactorUaaClient;
|
||||
import org.cloudfoundry.uaa.UaaClient;
|
||||
import org.cloudfoundry.uaa.UaaException;
|
||||
import org.cloudfoundry.uaa.clients.Clients;
|
||||
import org.cloudfoundry.uaa.clients.CreateClientRequest;
|
||||
import org.cloudfoundry.uaa.clients.DeleteClientRequest;
|
||||
import org.cloudfoundry.uaa.tokens.GrantType;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(CloudFoundryProperties.class)
|
||||
public class CloudFoundryClientConfiguration {
|
||||
|
||||
static final String ACCEPTANCE_TEST_OAUTH_CLIENT_ID = "acceptance-test-client";
|
||||
static final String ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET = "acceptance-test-client-secret";
|
||||
private static final String[] ACCEPTANCE_TEST_OAUTH_CLIENT_AUTHORITIES = {
|
||||
"openid", "cloud_controller.admin", "cloud_controller.read", "cloud_controller.write",
|
||||
"clients.read", "clients.write"
|
||||
public static final String ACCEPTANCE_TEST_OAUTH_CLIENT_ID = "acceptance-test-client";
|
||||
public static final String ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET = "acceptance-test-client-secret";
|
||||
public static final String[] ACCEPTANCE_TEST_OAUTH_CLIENT_AUTHORITIES = {
|
||||
"openid",
|
||||
"cloud_controller.admin",
|
||||
"cloud_controller.read",
|
||||
"cloud_controller.write",
|
||||
"clients.read",
|
||||
"clients.write"
|
||||
};
|
||||
|
||||
public static final String APP_BROKER_CLIENT_ID = "app-broker-client";
|
||||
public static final String APP_BROKER_CLIENT_SECRET = "app-broker-client-secret";
|
||||
public static final String[] APP_BROKER_CLIENT_AUTHORITIES = {
|
||||
"cloud_controller.read", "cloud_controller.write"
|
||||
};
|
||||
|
||||
@Bean
|
||||
CloudFoundryOperations cloudFoundryOperations(CloudFoundryProperties properties, CloudFoundryClient client,
|
||||
DopplerClient dopplerClient, UaaClient uaaClient) {
|
||||
CloudFoundryOperations cloudFoundryOperations(CloudFoundryProperties properties,
|
||||
CloudFoundryClient client,
|
||||
DopplerClient dopplerClient,
|
||||
UaaClient uaaClient) {
|
||||
return DefaultCloudFoundryOperations.builder()
|
||||
.cloudFoundryClient(client)
|
||||
.dopplerClient(dopplerClient)
|
||||
@@ -67,7 +74,8 @@ public class CloudFoundryClientConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
CloudFoundryClient cloudFoundryClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
|
||||
CloudFoundryClient cloudFoundryClient(ConnectionContext connectionContext,
|
||||
@Qualifier("userCredentials") TokenProvider tokenProvider) {
|
||||
return ReactorCloudFoundryClient.builder()
|
||||
.connectionContext(connectionContext)
|
||||
.tokenProvider(tokenProvider)
|
||||
@@ -85,7 +93,8 @@ public class CloudFoundryClientConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
DopplerClient dopplerClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
|
||||
DopplerClient dopplerClient(ConnectionContext connectionContext,
|
||||
@Qualifier("userCredentials") TokenProvider tokenProvider) {
|
||||
return ReactorDopplerClient.builder()
|
||||
.connectionContext(connectionContext)
|
||||
.tokenProvider(tokenProvider)
|
||||
@@ -93,7 +102,8 @@ public class CloudFoundryClientConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
UaaClient uaaClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
|
||||
UaaClient uaaClient(ConnectionContext connectionContext,
|
||||
@Qualifier("clientCredentials") TokenProvider tokenProvider) {
|
||||
return ReactorUaaClient.builder()
|
||||
.connectionContext(connectionContext)
|
||||
.tokenProvider(tokenProvider)
|
||||
@@ -101,8 +111,11 @@ public class CloudFoundryClientConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty({CloudFoundryProperties.PROPERTY_PREFIX + ".username",
|
||||
CloudFoundryProperties.PROPERTY_PREFIX + ".password"})
|
||||
@Qualifier("userCredentials")
|
||||
@ConditionalOnProperty({
|
||||
CloudFoundryProperties.PROPERTY_PREFIX + ".username",
|
||||
CloudFoundryProperties.PROPERTY_PREFIX + ".password"
|
||||
})
|
||||
PasswordGrantTokenProvider passwordTokenProvider(CloudFoundryProperties properties) {
|
||||
return PasswordGrantTokenProvider.builder()
|
||||
.password(properties.getPassword())
|
||||
@@ -111,42 +124,17 @@ public class CloudFoundryClientConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty({CloudFoundryProperties.PROPERTY_PREFIX + ".client-id",
|
||||
CloudFoundryProperties.PROPERTY_PREFIX + ".client-secret"})
|
||||
ClientCredentialsGrantTokenProvider clientTokenProvider(ConnectionContext connectionContext,
|
||||
CloudFoundryProperties properties) {
|
||||
|
||||
Clients uaaClients = buildTempUaaClient(connectionContext, properties).clients();
|
||||
|
||||
uaaClients.delete(DeleteClientRequest.builder()
|
||||
.clientId(ACCEPTANCE_TEST_OAUTH_CLIENT_ID)
|
||||
.build())
|
||||
.onErrorResume(UaaException.class, e -> Mono.empty())
|
||||
.onErrorResume(UnknownCloudFoundryException.class, e -> Mono.empty())
|
||||
.then(uaaClients.create(CreateClientRequest.builder()
|
||||
.clientId(ACCEPTANCE_TEST_OAUTH_CLIENT_ID)
|
||||
.clientSecret(ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET)
|
||||
.authorizedGrantType(GrantType.CLIENT_CREDENTIALS)
|
||||
.authorities(ACCEPTANCE_TEST_OAUTH_CLIENT_AUTHORITIES)
|
||||
.build()))
|
||||
.block();
|
||||
|
||||
@Qualifier("clientCredentials")
|
||||
@ConditionalOnProperty({
|
||||
CloudFoundryProperties.PROPERTY_PREFIX + ".client-id",
|
||||
CloudFoundryProperties.PROPERTY_PREFIX + ".client-secret"
|
||||
})
|
||||
ClientCredentialsGrantTokenProvider clientTokenProvider(CloudFoundryProperties properties) {
|
||||
return ClientCredentialsGrantTokenProvider.builder()
|
||||
.clientId(ACCEPTANCE_TEST_OAUTH_CLIENT_ID)
|
||||
.clientSecret(ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET)
|
||||
.clientId(properties.getClientId())
|
||||
.clientSecret(properties.getClientSecret())
|
||||
.identityZoneSubdomain(properties.getIdentityZoneSubdomain())
|
||||
.build();
|
||||
}
|
||||
|
||||
private UaaClient buildTempUaaClient(ConnectionContext connectionContext, CloudFoundryProperties properties) {
|
||||
return ReactorUaaClient.builder()
|
||||
.connectionContext(connectionContext)
|
||||
.tokenProvider(ClientCredentialsGrantTokenProvider.builder()
|
||||
.clientId(properties.getClientId())
|
||||
.clientSecret(properties.getClientSecret())
|
||||
.identityZoneSubdomain(properties.getIdentityZoneSubdomain())
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,16 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.cloudfoundry.client.CloudFoundryClient;
|
||||
import org.cloudfoundry.client.v2.organizations.AssociateOrganizationManagerRequest;
|
||||
import org.cloudfoundry.client.v2.organizations.AssociateOrganizationManagerResponse;
|
||||
import org.cloudfoundry.client.v2.organizations.AssociateOrganizationUserRequest;
|
||||
import org.cloudfoundry.client.v2.organizations.AssociateOrganizationUserResponse;
|
||||
import org.cloudfoundry.client.v2.organizations.RemoveOrganizationManagerRequest;
|
||||
import org.cloudfoundry.client.v2.organizations.RemoveOrganizationUserRequest;
|
||||
import org.cloudfoundry.client.v2.spaces.AssociateSpaceDeveloperRequest;
|
||||
import org.cloudfoundry.client.v2.spaces.AssociateSpaceDeveloperResponse;
|
||||
import org.cloudfoundry.client.v2.spaces.RemoveSpaceDeveloperRequest;
|
||||
import org.cloudfoundry.operations.CloudFoundryOperations;
|
||||
import org.cloudfoundry.operations.DefaultCloudFoundryOperations;
|
||||
import org.cloudfoundry.operations.applications.ApplicationDetail;
|
||||
@@ -63,11 +73,16 @@ public class CloudFoundryService {
|
||||
|
||||
private static final String DEPLOYER_PROPERTY_PREFIX = "spring.cloud.appbroker.deployer.cloudfoundry.";
|
||||
|
||||
private final CloudFoundryClient cloudFoundryClient;
|
||||
|
||||
private final CloudFoundryOperations cloudFoundryOperations;
|
||||
|
||||
private final CloudFoundryProperties cloudFoundryProperties;
|
||||
|
||||
public CloudFoundryService(CloudFoundryOperations cloudFoundryOperations,
|
||||
public CloudFoundryService(CloudFoundryClient cloudFoundryClient,
|
||||
CloudFoundryOperations cloudFoundryOperations,
|
||||
CloudFoundryProperties cloudFoundryProperties) {
|
||||
this.cloudFoundryClient = cloudFoundryClient;
|
||||
this.cloudFoundryOperations = cloudFoundryOperations;
|
||||
this.cloudFoundryProperties = cloudFoundryProperties;
|
||||
}
|
||||
@@ -129,7 +144,7 @@ public class CloudFoundryService {
|
||||
.deleteRoutes(true)
|
||||
.build())
|
||||
.doOnSuccess(item -> LOGGER.info("Deleted app " + appName))
|
||||
.doOnError(error -> LOGGER.error("Error deleting app " + appName + ": " + error))
|
||||
.doOnError(error -> LOGGER.warn("Error deleting app " + appName + ": " + error))
|
||||
.onErrorResume(e -> Mono.empty());
|
||||
}
|
||||
|
||||
@@ -139,7 +154,7 @@ public class CloudFoundryService {
|
||||
.name(brokerName)
|
||||
.build())
|
||||
.doOnSuccess(item -> LOGGER.info("Deleted service broker " + brokerName))
|
||||
.doOnError(error -> LOGGER.error("Error deleting service broker " + brokerName + ": " + error))
|
||||
.doOnError(error -> LOGGER.warn("Error deleting service broker " + brokerName + ": " + error))
|
||||
.onErrorResume(e -> Mono.empty());
|
||||
}
|
||||
|
||||
@@ -152,7 +167,7 @@ public class CloudFoundryService {
|
||||
.doOnSuccess(item -> LOGGER.info("Deleted service instance " + serviceInstanceName))
|
||||
.doOnError(error -> LOGGER.error("Error deleting service instance " + serviceInstanceName + ": " + error))
|
||||
.onErrorResume(e -> Mono.empty()))
|
||||
.doOnError(error -> LOGGER.error("Error getting service instance " + serviceInstanceName + ": " + error))
|
||||
.doOnError(error -> LOGGER.warn("Error getting service instance " + serviceInstanceName + ": " + error))
|
||||
.onErrorResume(e -> Mono.empty());
|
||||
}
|
||||
|
||||
@@ -303,6 +318,63 @@ public class CloudFoundryService {
|
||||
.next();
|
||||
}
|
||||
|
||||
public Mono<Void> associateAppBrokerClientWithOrgAndSpace(String orgId, String spaceId) {
|
||||
return Mono.justOrEmpty(CloudFoundryClientConfiguration.APP_BROKER_CLIENT_ID)
|
||||
.flatMap(userId -> associateOrgUser(orgId, userId)
|
||||
.then(associateOrgManager(orgId, userId))
|
||||
.then(associateSpaceDeveloper(spaceId, userId)))
|
||||
.then();
|
||||
}
|
||||
|
||||
public Mono<Void> removeAppBrokerClientFromOrgAndSpace(String orgId, String spaceId) {
|
||||
return Mono.justOrEmpty(CloudFoundryClientConfiguration.APP_BROKER_CLIENT_ID)
|
||||
.flatMap(userId -> removeSpaceDeveloper(spaceId, userId)
|
||||
.then(removeOrgManager(orgId, userId))
|
||||
.then(removeOrgUser(orgId, userId)));
|
||||
}
|
||||
|
||||
private Mono<AssociateOrganizationUserResponse> associateOrgUser(String orgId, String userId) {
|
||||
return cloudFoundryClient.organizations().associateUser(AssociateOrganizationUserRequest.builder()
|
||||
.organizationId(orgId)
|
||||
.userId(userId)
|
||||
.build());
|
||||
}
|
||||
|
||||
private Mono<AssociateOrganizationManagerResponse> associateOrgManager(String orgId, String userId) {
|
||||
return cloudFoundryClient.organizations().associateManager(AssociateOrganizationManagerRequest.builder()
|
||||
.organizationId(orgId)
|
||||
.managerId(userId)
|
||||
.build());
|
||||
}
|
||||
|
||||
private Mono<AssociateSpaceDeveloperResponse> associateSpaceDeveloper(String spaceId, String userId) {
|
||||
return cloudFoundryClient.spaces().associateDeveloper(AssociateSpaceDeveloperRequest.builder()
|
||||
.spaceId(spaceId)
|
||||
.developerId(userId)
|
||||
.build());
|
||||
}
|
||||
|
||||
private Mono<Void> removeOrgUser(String orgId, String userId) {
|
||||
return cloudFoundryClient.organizations().removeUser(RemoveOrganizationUserRequest.builder()
|
||||
.organizationId(orgId)
|
||||
.userId(userId)
|
||||
.build());
|
||||
}
|
||||
|
||||
private Mono<Void> removeOrgManager(String orgId, String userId) {
|
||||
return cloudFoundryClient.organizations().removeManager(RemoveOrganizationManagerRequest.builder()
|
||||
.organizationId(orgId)
|
||||
.managerId(userId)
|
||||
.build());
|
||||
}
|
||||
|
||||
private Mono<Void> removeSpaceDeveloper(String spaceId, String userId) {
|
||||
return cloudFoundryClient.spaces().removeDeveloper(RemoveSpaceDeveloperRequest.builder()
|
||||
.spaceId(spaceId)
|
||||
.developerId(userId)
|
||||
.build());
|
||||
}
|
||||
|
||||
private CloudFoundryOperations createOperationsForSpace(String space) {
|
||||
final String defaultOrg = cloudFoundryProperties.getDefaultOrg();
|
||||
return DefaultCloudFoundryOperations.builder()
|
||||
@@ -325,19 +397,10 @@ public class CloudFoundryService {
|
||||
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "skip-ssl-validation",
|
||||
String.valueOf(cloudFoundryProperties.isSkipSslValidation()));
|
||||
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "properties.memory", "1024M");
|
||||
|
||||
if (cloudFoundryProperties.getUsername() == null || cloudFoundryProperties.getPassword() == null) {
|
||||
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "client-id",
|
||||
CloudFoundryClientConfiguration.ACCEPTANCE_TEST_OAUTH_CLIENT_ID);
|
||||
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "client-secret",
|
||||
CloudFoundryClientConfiguration.ACCEPTANCE_TEST_OAUTH_CLIENT_SECRET);
|
||||
} else {
|
||||
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "username",
|
||||
cloudFoundryProperties.getUsername());
|
||||
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "password",
|
||||
cloudFoundryProperties.getPassword());
|
||||
}
|
||||
|
||||
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "client-id",
|
||||
CloudFoundryClientConfiguration.APP_BROKER_CLIENT_ID);
|
||||
deployerVariables.put(DEPLOYER_PROPERTY_PREFIX + "client-secret",
|
||||
CloudFoundryClientConfiguration.APP_BROKER_CLIENT_SECRET);
|
||||
return deployerVariables;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,22 @@
|
||||
package org.springframework.cloud.appbroker.acceptance.fixtures.uaa;
|
||||
|
||||
import org.cloudfoundry.uaa.UaaClient;
|
||||
import org.cloudfoundry.uaa.clients.CreateClientRequest;
|
||||
import org.cloudfoundry.uaa.clients.DeleteClientRequest;
|
||||
import org.cloudfoundry.uaa.clients.GetClientRequest;
|
||||
import org.cloudfoundry.uaa.clients.GetClientResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.cloudfoundry.uaa.tokens.GrantType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class UaaService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(UaaService.class);
|
||||
|
||||
private final UaaClient uaaClient;
|
||||
|
||||
public UaaService(UaaClient uaaClient) {
|
||||
@@ -31,9 +40,30 @@ public class UaaService {
|
||||
}
|
||||
|
||||
public Mono<GetClientResponse> getUaaClient(String clientId) {
|
||||
return uaaClient.clients().get(GetClientRequest.builder()
|
||||
return uaaClient.clients().get(GetClientRequest
|
||||
.builder()
|
||||
.clientId(clientId)
|
||||
.build())
|
||||
.onErrorResume(e -> Mono.empty());
|
||||
}
|
||||
|
||||
public Mono<Void> createClient(String clientId, String clientSecret, String... authorities) {
|
||||
return uaaClient.clients().delete(DeleteClientRequest
|
||||
.builder()
|
||||
.clientId(clientId)
|
||||
.build())
|
||||
.doOnError(error -> LOGGER.warn("Error deleting client: " + clientId + " with error: " + error))
|
||||
.onErrorResume(e -> Mono.empty())
|
||||
.then(uaaClient.clients().create(CreateClientRequest
|
||||
.builder()
|
||||
.clientId(clientId)
|
||||
.clientSecret(clientSecret)
|
||||
.authorizedGrantType(GrantType.CLIENT_CREDENTIALS)
|
||||
.authorities(authorities)
|
||||
.build())
|
||||
.doOnError(error -> LOGGER.error("Error creating client: " + clientId + " with error: " + error))
|
||||
.onErrorResume(e -> Mono.empty()))
|
||||
.then();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.cloudfoundry.client.v2.organizations.GetOrganizationResponse;
|
||||
import org.cloudfoundry.client.v2.organizations.ListOrganizationSpacesRequest;
|
||||
import org.cloudfoundry.client.v2.organizations.OrganizationEntity;
|
||||
import org.cloudfoundry.client.v2.serviceinstances.ServiceInstanceEntity;
|
||||
import org.cloudfoundry.client.v2.spaces.AssociateSpaceDeveloperRequest;
|
||||
import org.cloudfoundry.client.v2.spaces.CreateSpaceRequest;
|
||||
import org.cloudfoundry.client.v2.spaces.DeleteSpaceRequest;
|
||||
import org.cloudfoundry.client.v2.spaces.SpaceEntity;
|
||||
@@ -80,6 +81,8 @@ import org.cloudfoundry.operations.organizations.OrganizationInfoRequest;
|
||||
import org.cloudfoundry.operations.services.BindServiceInstanceRequest;
|
||||
import org.cloudfoundry.operations.services.ServiceInstance;
|
||||
import org.cloudfoundry.operations.services.UnbindServiceInstanceRequest;
|
||||
import org.cloudfoundry.operations.useradmin.SetSpaceRoleRequest;
|
||||
import org.cloudfoundry.operations.useradmin.SpaceRole;
|
||||
import org.cloudfoundry.util.DelayUtils;
|
||||
import org.cloudfoundry.util.PaginationUtils;
|
||||
import org.cloudfoundry.util.ResourceUtils;
|
||||
@@ -435,26 +438,50 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware
|
||||
.flatMap(cfOperations -> cfOperations.applications().pushManifest(request));
|
||||
}
|
||||
|
||||
private Mono<Void> createSpace(String spaceName) {
|
||||
Mono<String> createSpacePublisher = getDefaultOrganizationId()
|
||||
.flatMap(orgId -> this.client.spaces()
|
||||
.create(CreateSpaceRequest.builder()
|
||||
.organizationId(orgId)
|
||||
.name(spaceName)
|
||||
.build())
|
||||
.doOnSuccess(response -> logger.info("Created space {}", spaceName))
|
||||
.doOnError(e -> logger.warn(String.format("Error creating space %s: %s", spaceName, e.getMessage())))
|
||||
.onErrorResume(e -> Mono.empty())
|
||||
.then(Mono.empty()));
|
||||
return getSpaceIdFromName(spaceName)
|
||||
.switchIfEmpty(createSpacePublisher).then();
|
||||
private Mono<String> createSpace(String spaceName) {
|
||||
return getSpaceId(spaceName)
|
||||
.switchIfEmpty(Mono.justOrEmpty(targetProperties.getDefaultOrg())
|
||||
.flatMap(orgName -> getOrganizationId(orgName)
|
||||
.flatMap(orgId -> client.spaces().create(CreateSpaceRequest.builder()
|
||||
.organizationId(orgId)
|
||||
.name(spaceName)
|
||||
.build())
|
||||
.doOnSuccess(response -> logger.info("Created space {}", spaceName))
|
||||
.doOnError(e -> logger.warn(String.format("Error creating space %s: %s", spaceName, e.getMessage())))
|
||||
.map(response -> response.getMetadata().getId())
|
||||
.flatMap(spaceId -> setSpaceDeveloperRoleForCurrentUser(orgName, spaceName, spaceId)
|
||||
.thenReturn(spaceId)))));
|
||||
}
|
||||
|
||||
private Mono<String> getDefaultOrganizationId() {
|
||||
return this.operations.organizations()
|
||||
.get(OrganizationInfoRequest.builder()
|
||||
.name(targetProperties.getDefaultOrg())
|
||||
.build())
|
||||
private Mono<Void> setSpaceDeveloperRoleForCurrentUser(String orgName, String spaceName, String spaceId) {
|
||||
return Mono.defer(() -> {
|
||||
if (StringUtils.hasText(targetProperties.getClientId())){
|
||||
return client.spaces().associateDeveloper(AssociateSpaceDeveloperRequest.builder()
|
||||
.spaceId(spaceId)
|
||||
.developerId(targetProperties.getClientId())
|
||||
.build())
|
||||
.doOnSuccess(v -> logger.info("Set space developer role for space {}", spaceName))
|
||||
.doOnError(e -> logger.warn(String.format("Error setting space developer role for space %s: %s", spaceName, e.getMessage())))
|
||||
.then();
|
||||
}
|
||||
else if (StringUtils.hasText(targetProperties.getUsername())) {
|
||||
return operations.userAdmin().setSpaceRole(SetSpaceRoleRequest.builder()
|
||||
.spaceRole(SpaceRole.DEVELOPER)
|
||||
.organizationName(orgName)
|
||||
.spaceName(spaceName)
|
||||
.username(targetProperties.getUsername())
|
||||
.build())
|
||||
.doOnSuccess(v -> logger.info("Set space developer role for space {}", spaceName))
|
||||
.doOnError(e -> logger.warn(String.format("Error setting space developer role for space %s: %s", spaceName, e.getMessage())));
|
||||
}
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<String> getOrganizationId(String orgName) {
|
||||
return operations.organizations().get(OrganizationInfoRequest.builder()
|
||||
.name(orgName)
|
||||
.build())
|
||||
.map(OrganizationDetail::getId);
|
||||
}
|
||||
|
||||
@@ -492,7 +519,7 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware
|
||||
}
|
||||
|
||||
private Mono<Void> deleteApplicationInSpace(String name, String spaceName) {
|
||||
return getSpaceIdFromName(spaceName)
|
||||
return getSpaceId(spaceName)
|
||||
.doOnError(error -> logger.warn("Unable to get space name: {} ", spaceName))
|
||||
.then(operationsUtils.getOperationsForSpace(spaceName))
|
||||
.flatMap(cfOperations -> cfOperations.applications().delete(DeleteApplicationRequest.builder()
|
||||
@@ -504,7 +531,7 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware
|
||||
}
|
||||
|
||||
private Mono<Void> deleteSpace(String spaceName) {
|
||||
return getSpaceIdFromName(spaceName)
|
||||
return getSpaceId(spaceName)
|
||||
.doOnError(error -> logger.warn("Unable to get space name: {} ", spaceName))
|
||||
.flatMap(spaceId -> this.client.spaces()
|
||||
.delete(DeleteSpaceRequest.builder()
|
||||
@@ -513,17 +540,18 @@ public class CloudFoundryAppDeployer implements AppDeployer, ResourceLoaderAware
|
||||
.then(Mono.empty()));
|
||||
}
|
||||
|
||||
private Mono<String> getSpaceIdFromName(String spaceName) {
|
||||
return getDefaultOrganizationId()
|
||||
.flatMap(orgId -> PaginationUtils.requestClientV2Resources(page -> client.organizations()
|
||||
.listSpaces(ListOrganizationSpacesRequest.builder()
|
||||
.name(spaceName)
|
||||
.organizationId(orgId)
|
||||
.page(page)
|
||||
.build()))
|
||||
.filter(resource -> resource.getEntity().getName().equals(spaceName))
|
||||
.map(resource -> resource.getMetadata().getId())
|
||||
.next());
|
||||
private Mono<String> getSpaceId(String spaceName) {
|
||||
return Mono.justOrEmpty(targetProperties.getDefaultOrg())
|
||||
.flatMap(orgName -> getOrganizationId(orgName)
|
||||
.flatMap(orgId -> PaginationUtils.requestClientV2Resources(page -> client.organizations()
|
||||
.listSpaces(ListOrganizationSpacesRequest.builder()
|
||||
.name(spaceName)
|
||||
.organizationId(orgId)
|
||||
.page(page)
|
||||
.build()))
|
||||
.filter(resource -> resource.getEntity().getName().equals(spaceName))
|
||||
.map(resource -> resource.getMetadata().getId())
|
||||
.next()));
|
||||
}
|
||||
|
||||
private Map<String, Object> getEnvironmentVariables(Map<String, String> properties,
|
||||
|
||||
@@ -18,16 +18,17 @@ package org.springframework.cloud.appbroker.deployer.cloudfoundry;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.cloudfoundry.client.CloudFoundryClient;
|
||||
import org.cloudfoundry.client.v2.organizations.GetOrganizationRequest;
|
||||
import org.cloudfoundry.client.v2.organizations.GetOrganizationResponse;
|
||||
import org.cloudfoundry.client.v2.organizations.ListOrganizationSpacesRequest;
|
||||
import org.cloudfoundry.client.v2.organizations.OrganizationEntity;
|
||||
import org.cloudfoundry.client.v2.serviceinstances.GetServiceInstanceResponse;
|
||||
import org.cloudfoundry.client.v2.serviceinstances.ServiceInstanceEntity;
|
||||
import org.cloudfoundry.client.v2.serviceinstances.ServiceInstances;
|
||||
import org.cloudfoundry.client.v2.spaces.CreateSpaceRequest;
|
||||
import org.cloudfoundry.client.v2.spaces.GetSpaceRequest;
|
||||
import org.cloudfoundry.client.v2.spaces.GetSpaceResponse;
|
||||
import org.cloudfoundry.client.v2.spaces.SpaceEntity;
|
||||
@@ -36,6 +37,10 @@ import org.cloudfoundry.operations.applications.ApplicationHealthCheck;
|
||||
import org.cloudfoundry.operations.applications.ApplicationManifest;
|
||||
import org.cloudfoundry.operations.applications.Applications;
|
||||
import org.cloudfoundry.operations.applications.PushApplicationManifestRequest;
|
||||
import org.cloudfoundry.operations.organizations.OrganizationDetail;
|
||||
import org.cloudfoundry.operations.organizations.OrganizationInfoRequest;
|
||||
import org.cloudfoundry.operations.organizations.OrganizationQuota;
|
||||
import org.cloudfoundry.operations.organizations.Organizations;
|
||||
import org.cloudfoundry.operations.services.BindServiceInstanceRequest;
|
||||
import org.cloudfoundry.operations.services.GetServiceInstanceRequest;
|
||||
import org.cloudfoundry.operations.services.ServiceInstance;
|
||||
@@ -65,6 +70,7 @@ import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
@@ -74,6 +80,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.cloud.appbroker.deployer.DeploymentProperties.TARGET_PROPERTY_KEY;
|
||||
|
||||
@SuppressWarnings("UnassignedFluxMonoInstance")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -92,6 +99,9 @@ class CloudFoundryAppDeployerTest {
|
||||
@Mock
|
||||
private Services operationsServices;
|
||||
|
||||
@Mock
|
||||
private Organizations operationsOrganizations;
|
||||
|
||||
@Mock
|
||||
private Spaces operationsSpaces;
|
||||
|
||||
@@ -122,6 +132,8 @@ class CloudFoundryAppDeployerTest {
|
||||
void setUp() {
|
||||
deploymentProperties = new CloudFoundryDeploymentProperties();
|
||||
CloudFoundryTargetProperties targetProperties = new CloudFoundryTargetProperties();
|
||||
targetProperties.setDefaultOrg("default-org");
|
||||
targetProperties.setDefaultSpace("default-space");
|
||||
|
||||
when(operationsApplications.pushManifest(any())).thenReturn(Mono.empty());
|
||||
when(resourceLoader.getResource(APP_PATH)).thenReturn(new FileSystemResource(APP_PATH));
|
||||
@@ -129,6 +141,7 @@ class CloudFoundryAppDeployerTest {
|
||||
when(cloudFoundryOperations.spaces()).thenReturn(operationsSpaces);
|
||||
when(cloudFoundryOperations.applications()).thenReturn(operationsApplications);
|
||||
when(cloudFoundryOperations.services()).thenReturn(operationsServices);
|
||||
when(cloudFoundryOperations.organizations()).thenReturn(operationsOrganizations);
|
||||
when(cloudFoundryClient.serviceInstances()).thenReturn(clientServiceInstances);
|
||||
when(cloudFoundryClient.spaces()).thenReturn(clientSpaces);
|
||||
when(cloudFoundryClient.organizations()).thenReturn(clientOrganizations);
|
||||
@@ -434,9 +447,79 @@ class CloudFoundryAppDeployerTest {
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createServiceInstanceWithTarget() {
|
||||
when(operationsOrganizations
|
||||
.get(
|
||||
OrganizationInfoRequest
|
||||
.builder()
|
||||
.name("default-org")
|
||||
.build()))
|
||||
.thenReturn(Mono.just(
|
||||
OrganizationDetail
|
||||
.builder()
|
||||
.id("default-org-id")
|
||||
.name("default-org")
|
||||
.quota(OrganizationQuota
|
||||
.builder()
|
||||
.id("quota-id")
|
||||
.instanceMemoryLimit(0)
|
||||
.organizationId("default-org-id")
|
||||
.name("quota")
|
||||
.paidServicePlans(false)
|
||||
.totalMemoryLimit(0)
|
||||
.totalRoutes(0)
|
||||
.totalServiceInstances(0)
|
||||
.build())
|
||||
.build()));
|
||||
|
||||
when(clientOrganizations
|
||||
.listSpaces(ListOrganizationSpacesRequest
|
||||
.builder()
|
||||
.name("service-instance-id")
|
||||
.organizationId("default-org-id")
|
||||
.page(1)
|
||||
.build()))
|
||||
.thenReturn(Mono.empty());
|
||||
|
||||
when(clientSpaces
|
||||
.create(CreateSpaceRequest
|
||||
.builder()
|
||||
.organizationId("default-org-id")
|
||||
.name("service-instance-id")
|
||||
.build()))
|
||||
.thenReturn(Mono.empty());
|
||||
|
||||
when(operationsServices
|
||||
.createInstance(
|
||||
org.cloudfoundry.operations.services.CreateServiceInstanceRequest
|
||||
.builder()
|
||||
.serviceInstanceName("service-instance-name")
|
||||
.serviceName("db-service")
|
||||
.planName("standard")
|
||||
.parameters(emptyMap())
|
||||
.build()))
|
||||
.thenReturn(Mono.empty());
|
||||
|
||||
CreateServiceInstanceRequest request =
|
||||
CreateServiceInstanceRequest
|
||||
.builder()
|
||||
.serviceInstanceName("service-instance-name")
|
||||
.name("db-service")
|
||||
.plan("standard")
|
||||
.parameters(emptyMap())
|
||||
.properties(singletonMap(TARGET_PROPERTY_KEY, "service-instance-id"))
|
||||
.build();
|
||||
|
||||
StepVerifier.create(
|
||||
appDeployer.createServiceInstance(request))
|
||||
.assertNext(response -> assertThat(response.getName()).isEqualTo("service-instance-name"))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateServiceInstanceUpdatesWithParameters() {
|
||||
Map<String, Object> parameters = Collections.singletonMap("param1", "value");
|
||||
Map<String, Object> parameters = singletonMap("param1", "value");
|
||||
|
||||
when(operationsServices.updateInstance(
|
||||
org.cloudfoundry.operations.services.UpdateServiceInstanceRequest.builder()
|
||||
@@ -643,7 +726,7 @@ class CloudFoundryAppDeployerTest {
|
||||
org.springframework.cloud.appbroker.deployer.GetServiceInstanceRequest request = org.springframework.cloud.appbroker.deployer.GetServiceInstanceRequest
|
||||
.builder()
|
||||
.name("my-foo-service")
|
||||
.properties(Collections.singletonMap(DeploymentProperties.TARGET_PROPERTY_KEY, "foo-space"))
|
||||
.properties(singletonMap(TARGET_PROPERTY_KEY, "foo-space"))
|
||||
.build();
|
||||
|
||||
StepVerifier.create(appDeployer.getServiceInstance(request))
|
||||
@@ -655,7 +738,7 @@ class CloudFoundryAppDeployerTest {
|
||||
.verifyComplete();
|
||||
|
||||
verify(operationsUtils).getOperations(
|
||||
argThat(argument -> "foo-space".equals(argument.get(DeploymentProperties.TARGET_PROPERTY_KEY))));
|
||||
argThat(argument -> "foo-space".equals(argument.get(TARGET_PROPERTY_KEY))));
|
||||
verify(cloudFoundryOperations).services();
|
||||
verify(operationsServices).getInstance(argThat(req -> "my-foo-service".equals(req.getName())));
|
||||
verifyZeroInteractions(cloudFoundryClient);
|
||||
|
||||
@@ -63,8 +63,8 @@ class CreateInstanceWithSpacePerServiceInstanceTargetComponentTest extends Wirem
|
||||
void pushAppWithServicesInSpace() {
|
||||
String serviceInstanceId = "instance-id";
|
||||
|
||||
cloudControllerFixture.stubSpaceDoesNotExist(serviceInstanceId);
|
||||
cloudControllerFixture.stubCreateSpace(serviceInstanceId);
|
||||
cloudControllerFixture.stubAssociatePermissions(serviceInstanceId);
|
||||
cloudControllerFixture.stubPushAppWithHost(APP_NAME, APP_NAME + "-" + serviceInstanceId);
|
||||
|
||||
// given services are available in the marketplace
|
||||
|
||||
@@ -142,13 +142,6 @@ public class CloudControllerStubFixture extends WiremockStubFixture {
|
||||
replace("@org-guid", TEST_ORG_GUID)))));
|
||||
}
|
||||
|
||||
public void stubSpaceDoesNotExist(final String spaceName) {
|
||||
stubFor(get(urlPathEqualTo("/v2/organizations/" + TEST_ORG_GUID + "/spaces"))
|
||||
.withQueryParam("q", equalTo("name:" + spaceName))
|
||||
.willReturn(ok()
|
||||
.withBody(cc("empty-query-results"))));
|
||||
}
|
||||
|
||||
public void stubCreateSpace(final String spaceName) {
|
||||
stubFor(post(urlPathEqualTo("/v2/spaces"))
|
||||
.withRequestBody(matchingJsonPath("$.[?(@.name == '" + spaceName + "')]"))
|
||||
@@ -512,6 +505,26 @@ public class CloudControllerStubFixture extends WiremockStubFixture {
|
||||
.withBody(cc("empty-query-results"))));
|
||||
}
|
||||
|
||||
public void stubAssociatePermissions(final String spaceName) {
|
||||
stubFor(get(urlPathEqualTo("/v2/config/feature_flags/set_roles_by_username"))
|
||||
.willReturn(ok()
|
||||
.withBody(cc("get-feature-flag-roles"))));
|
||||
stubSpaceExists(spaceName);
|
||||
|
||||
stubFor(put(urlPathEqualTo("/v2/organizations/" + TEST_ORG_GUID + "/users"))
|
||||
.willReturn(ok()));
|
||||
}
|
||||
|
||||
private void stubSpaceExists(final String spaceName) {
|
||||
stubFor(get(urlPathEqualTo("/v2/organizations/" + TEST_ORG_GUID + "/spaces"))
|
||||
.withQueryParam("q", equalTo("name:" + spaceName))
|
||||
.willReturn(ok()
|
||||
.withBody(cc("list-spaces",
|
||||
replace("@org-guid", TEST_ORG_GUID),
|
||||
replace("@space-guid", TEST_SPACE_GUID),
|
||||
replace("@name", spaceName)))));
|
||||
}
|
||||
|
||||
private String cc(String fileRoot, StringReplacementPair... replacements) {
|
||||
String response = readResponseFromFile(fileRoot, "cloudcontroller");
|
||||
for (StringReplacementPair pair : replacements) {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "set_roles_by_username",
|
||||
"enabled": true,
|
||||
"error_message": null,
|
||||
"url": "/v2/config/feature_flags/set_roles_by_username"
|
||||
}
|
||||
Reference in New Issue
Block a user