Skip to content

Commit

Permalink
Add tests ensuring YamlConfigurationValidator is present and validati…
Browse files Browse the repository at this point in the history
…ng config

- The YamlConfigurationValidator is wired in the application context through spring-servlet.xml.
- It is supposed to check the configuration values when the "strict" profile is present. When
  there is no "strict" profile, it performs the validation but swallows any validation error
  without a trace.
- This commit adds two tests, one for valid configurations, and one for invalid configurations.
- The "valid" configuration test did not pass because the `UaaConfiguration` object is out of
  date with the actual possible configuration, so this commit also updates the model to get the
  test to pass.
- Ported the latest `cargo/uaa.yml` in the integration tests config so that validation also works
  with cargo. It required fixing one test to use the new key format.
  • Loading branch information
Kehrlann committed Feb 7, 2025
1 parent cc21ea2 commit 1023b99
Show file tree
Hide file tree
Showing 6 changed files with 308 additions and 31 deletions.
23 changes: 23 additions & 0 deletions scripts/cargo/uaa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,19 +177,22 @@ oauth:
override: true
clients:
admin:
id: admin
authorized-grant-types: client_credentials
scope: uaa.none
authorities: 'uaa.admin,clients.read,clients.write,clients.secret,clients.trust,scim.read,scim.write,clients.admin'
secret: "adminsecret"
jwks: '{"alg":"RS256","e":"AQAB","kid":"cUiuzP1rw1zm9MV8F0vtrws7BLc","kty":"RSA","n":"rWuIqrVV8kuqeorvRuLio1_pdQm_z7HZJKIcCD5SQqGO0AsKyf1xa5TPzHM0lqEh2GcPTer4u7MYQZzXAAvzOsSaTmgSlenLKDYCDZy2bwOjK0izVLbJwYqiiqyiMGhKeWsYokyDNoYaefjz8izDrp47XDHnwC2eeyJ43cE8GP0JJXRyxIPFecO8rfpe3AzTrHszJ9lPSX9E8QGppSFmcnUDUQYDRipNMzXXp2FHdR7T2MZkvxzjFhVSSMiaDTmAca-Wv_Uct2HpOfC3IuKSy1jpu8yr_GT6aBsDkt1XC1iARuFf9dE83R39oNgvVMICPjeWgNoyhK-ddQAUnRDeqw"}'
cf:
id: cf
secret: ''
authorized-grant-types: 'implicit,password,refresh_token'
scope: 'uaa.user,cloud_controller.read,cloud_controller.write,openid,password.write,scim.userids,cloud_controller.admin,scim.read,scim.write'
redirect-uri: 'http://localhost:8080/**,http://localhost:7000/**'
authorities: uaa.none
autoapprove: 'true'
app:
id: app
secret: appclientsecret
authorized-grant-types: password,implicit,authorization_code,client_credentials,refresh_token
scope: cloud_controller.read,cloud_controller.write,openid,password.write,scim.userids,organizations.acme
Expand All @@ -200,6 +203,7 @@ oauth:
change_email_redirect_url: http://localhost:8080/app/
name: The Ultimate Oauth App
appspecial:
id: appspecial
secret: appclient|secret!
authorized-grant-types: password,implicit,authorization_code,client_credentials,refresh_token
scope: cloud_controller.read,cloud_controller.write,openid,password.write,scim.userids,organizations.acme
Expand All @@ -210,6 +214,7 @@ oauth:
change_email_redirect_url: http://localhost:8080/app/
name: The Ultimate Oauth App - Special
login:
id: login
secret: loginsecret
scope: 'openid,oauth.approvals'
authorized-grant-types: 'client_credentials,authorization_code'
Expand All @@ -218,97 +223,114 @@ oauth:
autoapprove: 'true'
allowpublic: 'true'
dashboard:
id: dashboard
secret: dashboardsecret
scope: 'dashboard.user,openid'
authorized-grant-types: authorization_code
authorities: uaa.resource
redirect-uri: 'http://localhost:8080/uaa/'
notifications:
id: notifications
secret: notificationssecret
authorized-grant-types: client_credentials
authorities: 'cloud_controller.admin,scim.read'
identity:
id: identity
secret: identitysecret
authorized-grant-types: 'authorization_code,client_credentials,refresh_token,password'
scope: 'cloud_controller.admin,cloud_controller.read,cloud_controller.write,openid,zones.*.*,zones.*.*.*,zones.read,zones.write'
authorities: 'scim.zones,zones.read,cloud_controller.read,uaa.resource,zones.write'
autoapprove: 'true'
redirect-uri: 'http://localhost/*,http://localhost:8080/**,http://oidcloginit.localhost:8080/uaa/**'
oauth_showcase_authorization_code:
id: oauth_showcase_authorization_code
secret: secret
authorized-grant-types: authorization_code
scope: openid
authorities: uaa.resource
redirect-uri: http://localhost:8080/uaa/
allowedproviders: [ uaa ]
oauth_showcase_client_credentials:
id: oauth_showcase_client_credentials
secret: secret
authorized-grant-types: client_credentials
scope: uaa.none
authorities: 'uaa.resource,clients.read'
oauth_showcase_password_grant:
id: oauth_showcase_password_grant
secret: secret
authorized-grant-types: password
scope: openid
authorities: uaa.resource
oauth_showcase_implicit_grant:
id: oauth_showcase_implicit_grant
authorized-grant-types: implicit
scope: openid
authorities: uaa.resource
redirect-uri: 'http://localhost:8080/uaa/'
oauth_showcase_user_token:
id: oauth_showcase_user_token
authorized-grant-types: 'user_token,password,refresh_token'
scope: 'openid,uaa.user'
secret: secret
oauth_showcase_user_token_public:
id: oauth_showcase_user_token_public
secret: ''
authorized-grant-types: 'user_token,password,authorization_code'
scope: 'openid,uaa.user'
redirect-uri: 'http://localhost:8080/uaa/'
allowpublic: 'true'
oauth_showcase_saml2_bearer:
id: oauth_showcase_saml2_bearer
authorized-grant-types: 'password,urn:ietf:params:oauth:grant-type:saml2-bearer'
scope: 'openid,uaa.user'
secret: secret
some_client_that_contains_redirect_uri_matching_request_param:
id: some_client_that_contains_redirect_uri_matching_request_param
authorized-grant-types: 'uaa.admin,clients.read,clients.write,clients.secret,scim.read,scim.write,clients.admin'
scope: openid
authorities: uaa.resource
redirect-uri: 'http://redirect.localhost'
client_with_bcrypt_prefix:
id: client_with_bcrypt_prefix
secret: password
authorized-grant-types: client_credentials
authorities: uaa.none
use-bcrypt-prefix: 'true'
jku_test:
id: jku_test
secret: secret
authorized-grant-types: 'password,client_credentials,refresh_token,authorization_code'
authorities: uaa.none
autoapprove: 'true'
scope: 'openid,oauth.approvals,user_attributes'
redirect-uri: 'http://localhost/**'
jku_test_without_autoapprove:
id: jku_test_without_autoapprove
secret: secret
authorized-grant-types: 'password,client_credentials,refresh_token,authorization_code'
authorities: uaa.none
autoapprove: 'false'
scope: 'openid,oauth.approvals,user_attributes'
redirect-uri: 'http://localhost/**'
client_without_openid:
id: client_without_openid
secret: secret
authorized-grant-types: 'password,client_credentials,refresh_token,authorization_code'
authorities: uaa.none
autoapprove: 'true'
scope: password.write
redirect-uri: 'http://localhost/**'
client_with_jwks_trust:
id: client_with_jwks_trust
authorized-grant-types: 'authorization_code,client_credentials,refresh_token,password'
scope: 'openid,password.write,scim.userids,cloud_controller.read,cloud_controller.write'
authorities: 'password.write,scim.userids,cloud_controller.read,cloud_controller.write,uaa.resource'
autoapprove: 'true'
redirect-uri: 'http://localhost/*,http://localhost:8080/**,http://localhost:7000/**'
jwks: '{"kty":"RSA","e":"AQAB","use":"sig","kid":"key-id-1","alg":"RS256","n":"qMClJXznycV2bQ1pFbN8W-AWSYhpS2MVAGhkWNlmxv2Ix0_-n6zjivjdoxcq7RJR4kVycoVeD07DiWElYSnQLdeQPgKAcBiwilR30UyyDTKcqDQQ5rkCg2ONlwV0aMsg74KaXeXsV653ASs3FYEtuS1aD_Db5-FyXF8HkHo8xy19NUnqsDWQnh1Hhklynxu2tvW0fw2oDE1pwNl-WLEVPtlcpCtf4VSv-GawtBiI6xmYsGBMC9w29ESHFqPw0NSCRhlyJf6rDBNH_766mzK_vEzA4rzGTBEUqDxTg_8JpRhh9D3qljSsmqCtpQoloOAaUKCqSJb_hKPspe-7r9cYmw"}'
client_with_allowpublic_and_jwks_uri_trust:
id: client_with_allowpublic_and_jwks_uri_trust
authorized-grant-types: 'authorization_code,client_credentials,refresh_token,password,urn:ietf:params:oauth:grant-type:jwt-bearer'
scope: 'openid,password.write,scim.userids,cloud_controller.read,cloud_controller.write'
authorities: 'password.write,scim.userids,cloud_controller.read,cloud_controller.write,uaa.resource'
Expand All @@ -317,6 +339,7 @@ oauth:
redirect-uri: 'http://localhost/*,http://localhost:8080/**,http://localhost:7000/**'
jwks_uri: 'http://localhost:8080/uaa/token_keys'
client_federated_jwt_trust:
id: client_federated_jwt_trust
authorized-grant-types: 'authorization_code,client_credentials,refresh_token,password,urn:ietf:params:oauth:grant-type:jwt-bearer'
scope: 'openid,password.write,scim.userids,cloud_controller.read,cloud_controller.write'
authorities: 'password.write,scim.userids,cloud_controller.read,cloud_controller.write,uaa.resource'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.cloudfoundry.identity.uaa.impl.config.UaaConfiguration.Jwt.Token.Policy.KeySpec;
import org.cloudfoundry.identity.uaa.impl.config.UaaConfiguration.OAuth.Client;
import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants;
import org.cloudfoundry.identity.uaa.ratelimiting.core.config.LimiterMapping;
import org.hibernate.validator.constraints.URL;
import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.Yaml;
Expand Down Expand Up @@ -59,6 +60,7 @@ public class UaaConfiguration {

@URL(message = "issuer.uri must be a valid URL")
public String issuerUri;
public Map<String, Object> issuer;
public boolean dump_requests;
public boolean require_https;
public boolean loginAddnew;
Expand Down Expand Up @@ -109,6 +111,13 @@ public class UaaConfiguration {
@Valid
public Map<String, Object> cors;

public Encryption encryption;

public Integer userMaxCount;
public Integer groupMaxCount;
public Integer clientMaxCount;
public RateLimit ratelimit;

public static class Zones {
@Valid
public InternalZone internal;
Expand Down Expand Up @@ -159,6 +168,8 @@ public static class Token {
public String signingAlg;
public Claims claims;
public Policy policy;
public Boolean revocable;
public Refresh refresh;

public static class Claims {
public Set<String> exclusions;
Expand All @@ -174,8 +185,15 @@ public static class Policy {
public static class KeySpec {
public String signingKey;
public String signingKeyPassword;
public String signingAlg;
}
}

public static class Refresh {
public String format;
public Boolean rotate;
public Boolean unique;
}
}
}

Expand Down Expand Up @@ -227,23 +245,51 @@ public static class OAuthClient {
public String refreshTokenValidity;
@URL(message = "'redirect-uri' must be a valid URL")
public String redirectUri;
public String jwks;
public String signup_redirect_url;
public String change_email_redirect_url;
public String name;
public List<String> allowedproviders;
public String useBcryptPrefix;
public String jwks_uri;
public String jwt_creds;
}

public static class Scim {
public boolean userids_enabled;
public boolean userOverride;
public List<String> users;
public List<String> external_groups;
public Object groups;
}

public static class PasswordPolicy {
public int requiredScore;
}

public static class Encryption {
public String active_key_label;
public String passkey;
public List<EncryptionKey> encryption_keys;

public static class EncryptionKey {
public String label;
}
}

public static class RateLimit {
public String loggingOption;
public String credentialID;
public List<LimiterMapping> limiterMappings;
}


public static class UaaConfigConstructor extends CustomPropertyConstructor {

public UaaConfigConstructor() {
super(UaaConfiguration.class);
var uaaDesc = typeDefinitions.get(UaaConfiguration.class);
uaaDesc.putMapPropertyType("issuer", String.class, Object.class);

TypeDescription oauthDesc = createTypeDescription(OAuth.class);
oauthDesc.putMapPropertyType("clients", String.class, OAuthClient.class);
Expand Down Expand Up @@ -280,6 +326,7 @@ public UaaConfigConstructor() {
addPropertyAlias("access-token-validity", OAuthClient.class, "accessTokenValidity");
addPropertyAlias("refresh-token-validity", OAuthClient.class, "refreshTokenValidity");
addPropertyAlias("user.override", Scim.class, "userOverride");
addPropertyAlias("use-bcrypt-prefix", OAuthClient.class, "useBcryptPrefix");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package org.cloudfoundry.identity.uaa;

import org.cloudfoundry.identity.uaa.impl.config.YamlConfigurationValidator;
import org.cloudfoundry.identity.uaa.impl.config.YamlServletProfileInitializer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.ResourceEntityResolver;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.mock.web.MockServletConfig;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.support.AbstractRefreshableWebApplicationContext;

import javax.validation.ConstraintViolationException;
import java.util.EventListener;

import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* This component-level test verifies that {@link YamlConfigurationValidator} is actually
* wired into the application context and does validate the configuration.
*/
class YamlConfigurationValidationTests {

@AfterEach
void tearDown() {
System.clearProperty("UAA_CONFIG_URL");
}

@Test
void validConfiguration() {
System.setProperty("UAA_CONFIG_URL", "classpath:integration_test_properties.yml");
var applicationContext = createApplicationContext();
assertThatNoException().isThrownBy(applicationContext::refresh);
}

@Test
void invalidConfiguration() {
System.setProperty("UAA_CONFIG_URL", "classpath:invalid_configuration.yml");
var applicationContext = createApplicationContext();
assertThatThrownBy(applicationContext::refresh)
.isInstanceOf(BeansException.class)
.getRootCause()
.isInstanceOf(ConstraintViolationException.class)
.hasMessageContaining("database.url: Database url is required");
}

private static TestApplicationContext createApplicationContext() {
var applicationContext = new TestApplicationContext();
var servletContext = new TestMockContext();
applicationContext.setServletContext(servletContext);
MockServletConfig servletConfig = new MockServletConfig(servletContext);
applicationContext.setServletConfig(servletConfig);

YamlServletProfileInitializer initializer = new YamlServletProfileInitializer();
initializer.initialize(applicationContext);
applicationContext.getEnvironment().setActiveProfiles("strict");
return applicationContext;
}


static class TestApplicationContext extends AbstractRefreshableWebApplicationContext {

@Override
protected void loadBeanDefinitions(@NonNull DefaultListableBeanFactory beanFactory) throws BeansException {
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

// Configure the bean definition reader with this context's
// resource loading environment.
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

beanDefinitionReader.loadBeanDefinitions("file:./src/main/webapp/WEB-INF/spring-servlet.xml");
}
}

;

static class TestMockContext extends MockServletContext {
@Override
public <T extends EventListener> void addListener(@Nullable T t) {
//no op
}
}

;

}
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ void acceptInvitationEmailWithCompanyName() throws Exception {
branding.setCompanyName("Best Company");
IdentityZoneConfiguration config = new IdentityZoneConfiguration();
config.setBranding(branding);
config.setTokenPolicy(IdentityZoneHolder.getUaaZone().getConfig().getTokenPolicy());
IdentityZone defaultZone = IdentityZoneHolder.getUaaZone();
defaultZone.setConfig(config);
identityZoneProvisioning.update(defaultZone);
Expand Down
Loading

0 comments on commit 1023b99

Please sign in to comment.