Merge remote-tracking branch 'mitre/master'

pull/105/merge
Mike Derryberry 2012-06-07 14:29:13 -04:00
commit 3e810cb5dc
28 changed files with 304 additions and 126 deletions

View File

@ -21,4 +21,22 @@ Managing OAuth clients:
Modules
-------
The project uses a multi-level Maven and git repository sutrcture. The main project is split into the following modules:
- openid-connect-common: common classes, service and repository interfaces, and model code. Also includes full JWT library.
- openid-connect-server: IdP/server implementation, includes implementations of services and repositories for use by server.
- openid-connect-client: RP/client implementation, built around spring security filters.
- spring-security-oauth: Git submodule that points to the Spring Security OAuth Git repository. Will be removed once a reliable milestone is reached upstream (see note above).
Maven War Overlay
-----------------
One of the best ways to build a custom deployment of this system is to use the Maven War Overlay mechanism. In essence, you make a new Maven project with a "war" disposition and make it depend on the openid-connect-server module with the Maven Overlay plugin configured. Any files in your new project will be built and injected into the war from the other project. This action will also overwrite any existing files.
For instance, to overwrite the data source configuration in the main server war file, create a file named src/main/webapp/WEB-INF/data-context.xml that contains the dataSource bean. This file will completely replace the one that's in the originally built war.

View File

@ -88,20 +88,18 @@ public class JwtSigningAndValidationServiceDefault extends AbstractJwtSigningAnd
Map<String, PublicKey> map = new HashMap<String, PublicKey>();
PublicKey publicKey;
for (String signerId : signers.keySet()) {
for (JwtSigner signer : signers.values()) {
JwtSigner signer = signers.get(signerId);
if (signer instanceof RsaSigner) {
publicKey = ((RsaSigner) signer).getPublicKey();
RsaSigner rsa = (RsaSigner)signer;
PublicKey publicKey = rsa.getPublicKey();
if (publicKey != null) {
// what's the index of this map for?
map.put(((RSAPublicKey) publicKey).getModulus()
.toString(16).toUpperCase()
+ ((RSAPublicKey) publicKey).getPublicExponent()
.toString(16).toUpperCase(), publicKey);
map.put(signerId, publicKey);
}
}

View File

@ -20,12 +20,13 @@ import java.util.List;
import org.mitre.oauth2.model.ClientDetailsEntity;
import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
import org.mitre.oauth2.model.OAuth2RefreshTokenEntity;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
public interface OAuth2TokenEntityService extends AuthorizationServerTokenServices, ResourceServerTokenServices {
public OAuth2AccessTokenEntity getAccessToken(String accessTokenValue);
public OAuth2AccessTokenEntity readAccessToken(String accessTokenValue);
public OAuth2RefreshTokenEntity getRefreshToken(String refreshTokenValue);
@ -43,4 +44,6 @@ public interface OAuth2TokenEntityService extends AuthorizationServerTokenServic
public OAuth2RefreshTokenEntity saveRefreshToken(OAuth2RefreshTokenEntity refreshToken);
public OAuth2AccessTokenEntity getAccessToken(OAuth2Authentication authentication);
}

View File

@ -1,7 +1,6 @@
CREATE TABLE clientdetails (
clientId VARCHAR(256),
clientSecret VARCHAR(2000),
registeredRedirectUri VARCHAR(2000),
clientName VARCHAR(256),
clientDescription VARCHAR(2000),
allowRefresh TINYINT,

View File

@ -1,4 +1,4 @@
CREATE TABLE redirect_uris (
owner_id VARCHAR(256),
registeredRedirectUri VARCHAR(256)
registeredRedirectUri VARCHAR(2000)
);

View File

@ -222,8 +222,11 @@ public class DefaultOAuth2ProviderTokenService implements OAuth2TokenEntityServi
}
/**
* Get an access token from its token value.
*/
@Override
public OAuth2AccessTokenEntity getAccessToken(String accessTokenValue) throws AuthenticationException {
public OAuth2AccessTokenEntity readAccessToken(String accessTokenValue) throws AuthenticationException {
OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenByValue(accessTokenValue);
if (accessToken == null) {
throw new InvalidTokenException("Access token for value " + accessTokenValue + " was not found");
@ -233,6 +236,9 @@ public class DefaultOAuth2ProviderTokenService implements OAuth2TokenEntityServi
}
}
/**
* Get an access token by its authentication object.
*/
@Override
public OAuth2AccessTokenEntity getAccessToken(OAuth2Authentication authentication) {
@ -241,6 +247,9 @@ public class DefaultOAuth2ProviderTokenService implements OAuth2TokenEntityServi
return accessToken;
}
/**
* Get a refresh token by its token value.
*/
@Override
public OAuth2RefreshTokenEntity getRefreshToken(String refreshTokenValue) throws AuthenticationException {
OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenByValue(refreshTokenValue);
@ -252,12 +261,18 @@ public class DefaultOAuth2ProviderTokenService implements OAuth2TokenEntityServi
}
}
/**
* Revoke a refresh token and all access tokens issued to it.
*/
@Override
public void revokeRefreshToken(OAuth2RefreshTokenEntity refreshToken) {
tokenRepository.clearAccessTokensForRefreshToken(refreshToken);
tokenRepository.removeRefreshToken(refreshToken);
}
/**
* Revoke an access token.
*/
@Override
public void revokeAccessToken(OAuth2AccessTokenEntity accessToken) {
tokenRepository.removeAccessToken(accessToken);
@ -341,12 +356,6 @@ public class DefaultOAuth2ProviderTokenService implements OAuth2TokenEntityServi
}
}
@Override
public OAuth2AccessToken readAccessToken(String accessToken) {
// TODO Auto-generated method stub
return null;
}
/* (non-Javadoc)
* @see org.mitre.oauth2.service.OAuth2TokenEntityService#saveAccessToken(org.mitre.oauth2.model.OAuth2AccessTokenEntity)
*/
@ -363,6 +372,4 @@ public class DefaultOAuth2ProviderTokenService implements OAuth2TokenEntityServi
return tokenRepository.saveRefreshToken(refreshToken);
}
}

View File

@ -41,7 +41,7 @@ public class IntrospectionEndpoint {
@RequestMapping("/oauth/verify")
public ModelAndView verify(@RequestParam("token") String tokenValue,
ModelAndView modelAndView) {
OAuth2AccessTokenEntity token = tokenServices.getAccessToken(tokenValue);
OAuth2AccessTokenEntity token = tokenServices.readAccessToken(tokenValue);
if (token == null) {
// if it's not a valid token, we'll print a 404

View File

@ -15,6 +15,8 @@
******************************************************************************/
package org.mitre.oauth2.web;
import java.security.Principal;
import org.mitre.oauth2.exception.PermissionDeniedException;
import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
import org.mitre.oauth2.model.OAuth2RefreshTokenEntity;
@ -47,23 +49,35 @@ public class RevocationEndpoint {
// TODO
@PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')")
@RequestMapping("/oauth/revoke")
public ModelAndView revoke(@RequestParam("token") String tokenValue,
public ModelAndView revoke(@RequestParam("token") String tokenValue, Principal principal,
ModelAndView modelAndView) {
OAuth2RefreshTokenEntity refreshToken = tokenServices.getRefreshToken(tokenValue);
OAuth2AccessTokenEntity accessToken = tokenServices.getAccessToken(tokenValue);
OAuth2RefreshTokenEntity refreshToken = null;
OAuth2AccessTokenEntity accessToken = null;
try {
refreshToken = tokenServices.getRefreshToken(tokenValue);
} catch (InvalidTokenException e) {
// it's OK if either of these tokens are bad
}
try {
accessToken = tokenServices.readAccessToken(tokenValue);
} catch (InvalidTokenException e) {
// it's OK if either of these tokens are bad
}
if (refreshToken == null && accessToken == null) {
// TODO: this should throw a 400 with a JSON error code
throw new InvalidTokenException("Invalid OAuth token: " + tokenValue);
}
// TODO: there should be a way to do this in SPEL, right?
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof OAuth2Authentication) {
if (principal instanceof OAuth2Authentication) {
OAuth2AccessTokenEntity tok = tokenServices.getAccessToken((OAuth2Authentication) principal);
// we've got a client acting on its own behalf, not an admin
//ClientAuthentication clientAuth = (ClientAuthenticationToken) ((OAuth2Authentication) auth).getClientAuthentication();
AuthorizationRequest clientAuth = ((OAuth2Authentication) auth).getAuthorizationRequest();
AuthorizationRequest clientAuth = ((OAuth2Authentication) principal).getAuthorizationRequest();
if (refreshToken != null) {
if (!refreshToken.getClient().getClientId().equals(clientAuth.getClientId())) {

View File

@ -33,10 +33,12 @@ import org.apache.commons.codec.binary.Base64;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.web.servlet.view.AbstractView;
import com.google.common.collect.BiMap;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
@ -68,11 +70,22 @@ public class JwkKeyListView extends AbstractView {
}
})
.registerTypeHierarchyAdapter(PublicKey.class, new JsonSerializer<PublicKey>() {
.create();
@Override
public JsonElement serialize(PublicKey src, Type typeOfSrc, JsonSerializationContext context) {
response.setContentType("application/json");
Writer out = response.getWriter();
BiMap<String, PublicKey> keyMap = (BiMap<String, PublicKey>) model.get("keys");
JsonObject obj = new JsonObject();
JsonArray keys = new JsonArray();
obj.add("keys", keys);
for (String keyId : keyMap.keySet()) {
PublicKey src = keyMap.get(keyId);
if (src instanceof RSAPublicKey) {
@ -87,41 +100,14 @@ public class JwkKeyListView extends AbstractView {
JsonObject o = new JsonObject();
o.addProperty("use", "sig");
o.addProperty("alg", "RSA");
o.addProperty("use", "sig"); // since we don't do encryption yet
o.addProperty("alg", "RSA"); // we know this is RSA
o.addProperty("mod", m64);
o.addProperty("exp", e64);
// TODO: get the key ID from the map
return o;
} else if (src instanceof ECPublicKey) {
o.addProperty("kid", keyId);
@SuppressWarnings("unused")
ECPublicKey ec = (ECPublicKey)src;
// TODO: serialize the EC
return null;
} else {
// skip this class ... we shouldn't have any keys in here that aren't encodable by this serializer
return null;
keys.add(o);
}
}
})
.create();
response.setContentType("application/json");
Writer out = response.getWriter();
Object obj = model.get("entity");
if (obj == null) {
obj = model;
}
gson.toJson(obj, out);

View File

@ -27,6 +27,10 @@ import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Maps;
@Controller
public class JsonWebKeyEndpoint {
@ -36,14 +40,16 @@ public class JsonWebKeyEndpoint {
@RequestMapping("/jwk")
public ModelAndView getJwk() {
Collection<PublicKey> keys = jwtService.getAllPublicKeys().values();
// get all public keys for display
// map from key id to public key for that signer
Map<String, PublicKey> keys = jwtService.getAllPublicKeys();
// put them into a bidirectional map to get at key IDs
BiMap<String, PublicKey> biKeys = HashBiMap.create(keys);
// TODO: check if keys are empty, return a 404 here or just an empty list?
Map<String, Object> jwk = new HashMap<String, Object>();
jwk.put("jwk", keys);
return new ModelAndView("jwkKeyList", "entity", jwk);
return new ModelAndView("jwkKeyList", "keys", biKeys);
}
}

View File

@ -0,0 +1,90 @@
/*******************************************************************************
* Copyright 2012 The MITRE Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
/**
*
*/
package org.mitre.swd.view;
import java.io.Writer;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.web.servlet.view.AbstractView;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
/**
* @author jricher
*
*/
public class XrdJsonResponse extends AbstractView {
/* (non-Javadoc)
* @see org.springframework.web.servlet.view.AbstractView#renderMergedOutputModel(java.util.Map, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
Gson gson = new GsonBuilder().setExclusionStrategies(new ExclusionStrategy() {
@Override
public boolean shouldSkipField(FieldAttributes f) {
return false;
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
// skip the JPA binding wrapper
if (clazz.equals(BeanPropertyBindingResult.class)) {
return true;
} else {
return false;
}
}
})
.create();
response.setContentType("application/json");
Writer out = response.getWriter();
Map<String, String> links = (Map<String, String>) model.get("links");
JsonObject obj = new JsonObject();
JsonArray linksList = new JsonArray();
obj.add("links", linksList);
// map of "rel" -> "link" values
for (Map.Entry<String, String> link : links.entrySet()) {
JsonObject l = new JsonObject();
l.addProperty("rel", link.getKey());
l.addProperty("link", link.getValue());
linksList.add(l);
}
gson.toJson(obj, out);
}
}

View File

@ -20,7 +20,9 @@ import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.mitre.openid.connect.config.ConfigurationPropertiesBean;
import org.mitre.util.Utility;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@ -31,11 +33,14 @@ import com.google.common.collect.Lists;
@Controller
public class SimpleWebDiscoveryEndpoint {
@Autowired
ConfigurationPropertiesBean config;
@RequestMapping(value="/.well-known/simple-web-discovery",
params={"principal", "service=http://openid.net/specs/connect/1.0/issuer"})
public ModelAndView openIdConnectIssuerDiscovery(@RequestParam("principal") String principal, ModelAndView modelAndView, HttpServletRequest request) {
public ModelAndView openIdConnectIssuerDiscovery(@RequestParam("principal") String principal, ModelAndView modelAndView) {
String baseUrl = Utility.findBaseUrl(request);
String baseUrl = config.getIssuer();
// look up user, see if they're local
// if so, return this server
@ -51,15 +56,26 @@ public class SimpleWebDiscoveryEndpoint {
return modelAndView;
}
@RequestMapping(value="/.well-known/host-meta",
params={"resource", "rel=http://openid.net/specs/connect/1.0/issuer"})
public ModelAndView xrdDiscovery(@RequestParam("resource") String resource, ModelAndView modelAndView) {
Map<String, String> relMap = new HashMap<String, String>();
relMap.put("http://openid.net/specs/connect/1.0/issuer", config.getIssuer());
modelAndView.getModel().put("links", relMap);
modelAndView.setViewName("jsonXrdResponseView");
return modelAndView;
}
@RequestMapping("/.well-known/openid-configuration")
public ModelAndView providerConfiguration(ModelAndView modelAndView, HttpServletRequest request) {
String baseUrl = Utility.findBaseUrl(request);
public ModelAndView providerConfiguration(ModelAndView modelAndView) {
/*
* version string Version of the provider response. "3.0" is the default.
* issuer string The https: URL with no path component that the OP asserts as its Issuer Identifier
* issuer string The https: URL that the OP asserts as its Issuer Identifier
* authorization_endpoint string URL of the OP's Authentication and Authorization Endpoint [OpenID.Messages]
* token_endpoint string URL of the OP's OAuth 2.0 Token Endpoint [OpenID.Messages]
* userinfo_endpoint string URL of the OP's UserInfo Endpoint [OpenID.Messages]
@ -81,18 +97,24 @@ public class SimpleWebDiscoveryEndpoint {
* token_endpoint_auth_types_supported array A JSON array containing a list of authentication types supported by this Token Endpoint. The options are client_secret_post, client_secret_basic, client_secret_jwt, and private_key_jwt, as described in Section 2.2.1 of OpenID Connect Messages 1.0 [OpenID.Messages]. Other Authentication types may be defined by extension. If unspecified or omitted, the default is client_secret_basic HTTP Basic Authentication Scheme as specified in section 2.3.1 of OAuth 2.0 [OAuth2.0].
* token_endpoint_auth_algs_supported array A JSON array containing a list of the JWS [JWS] signing algorithms supported by the Token Endpoint for the private_key_jwt method to encode the JWT [JWT]. Servers SHOULD support RS256.
*/
String baseUrl = config.getIssuer();
if (!baseUrl.endsWith("/")) {
baseUrl = baseUrl.concat("/");
}
Map<String, Object> m = new HashMap<String, Object>();
m.put("version", "3.0");
m.put("issuer", baseUrl);
m.put("authorization_endpoint", baseUrl + "/authorize");
m.put("token_endpoint", baseUrl + "/oauth");
m.put("userinfo_endpoint", baseUrl + "/userinfo");
m.put("check_id_endpoint", baseUrl + "/checkid");
m.put("refresh_session_endpoint", baseUrl + "/refresh_session");
m.put("end_session_endpoint", baseUrl + "/end_session");
m.put("jwk_url", baseUrl + "/jwk");
m.put("registration_endpoint", baseUrl + "/register_client");
m.put("scopes_supported", Lists.newArrayList("openid"));
m.put("issuer", config.getIssuer());
m.put("authorization_endpoint", baseUrl + "openidconnect/auth");
m.put("token_endpoint", baseUrl + "openidconnect/token");
m.put("userinfo_endpoint", baseUrl + "userinfo");
m.put("check_id_endpoint", baseUrl + "checkid");
//m.put("refresh_session_endpoint", baseUrl + "/refresh_session");
//m.put("end_session_endpoint", baseUrl + "/end_session");
m.put("jwk_url", baseUrl + "jwk");
//m.put("registration_endpoint", baseUrl + "/register_client");
m.put("scopes_supported", Lists.newArrayList("openid", "email", "profile", "address", "phone"));
m.put("response_types_supported", Lists.newArrayList("code"));

View File

@ -190,8 +190,11 @@
<!-- </bean> -->
<!-- JSON views for each type of model object -->
<bean id="jsonOpenIdConfigurationView" class="org.mitre.swd.view.JsonOpenIdConfigurationView" />
<bean id="jsonSwdResponseView" class="org.mitre.swd.view.SwdResponse" />
<bean id="jsonXrdResponseView" class="org.mitre.swd.view.XrdJsonResponse" />
<bean id="jwkKeyList" class="org.mitre.openid.connect.view.JwkKeyListView" />
<bean id="jsonUserInfoView" class="org.mitre.openid.connect.view.JSONUserInfoView" />

View File

@ -21,14 +21,56 @@
<authz:authorize ifAnyGranted="ROLE_USER">
<div class="hero-unit" style="text-align:center">
<h1>Please Confirm!</h1>
<div class="well" style="text-align:center">
<h1>Approve New Site</h1>
<p>I hereby authorize "<c:out value="${client.clientId}"/>" to access my protected resources.</p>
<form name="confirmationForm" style="display:inline" action="<%=request.getContextPath()%>/oauth/authorize"
method="post">
<div class="row">
<div class="span4 offset2 well-small" style="text-align:left">Do you authorize
"<c:choose>
<c:when test="${empty client.clientName}">
<c:out value="${client.clientId}"/>
</c:when>
<c:otherwise>
<c:out value="${client.clientName}"/>
</c:otherwise>
</c:choose>" to sign you into their site
using your identity?
<a class="small" href="#" onclick="$('#description').toggle('fast')">more information</a>
<p>
<blockquote id="description" style="display: none">
<c:choose>
<c:when test="${empty client.clientDescription}">
No additional information available.
</c:when>
<c:otherwise>
<c:out value="${client.clientDescription}"/>
</c:otherwise>
</c:choose>
</blockquote>
</p>
</div>
<div class="span4">
<fieldset style="text-align:left" class="well">
<legend style="margin-bottom: 0;">Access to:</legend>
<label for="option1"></label>
<input type="checkbox" name="option1" id="option1" checked="checked"> basic profile information
<label for="option2"></label>
<input type="checkbox" name="option1" id="option2" checked="checked"> email address
<label for="option3"></label>
<input type="checkbox" name="option3" id="option3" checked="checked"> address
<label for="option4"></label>
<input type="checkbox" name="option4" id="option4" checked="checked"> phone number
<label for="option5"></label>
<input type="checkbox" name="option5" id="option5" checked="checked"> offline access
</fieldset>
</div>
</div>
<form name="confirmationForm" style="display:inline" action="<%=request.getContextPath()%>/oauth/authorize" method="post">
<div class="row">
<input id="user_oauth_approval" name="user_oauth_approval" value="true" type="hidden"/>
@ -39,18 +81,8 @@
<input name="deny" value="Deny" type="submit" onclick="$('#user_oauth_approval').attr('value',false)"
class="btn btn-secondary btn-large"/>
</div>
<div class="row control-group">
<label for="option1"></label>
<input name="option1" id="option1" type="checkbox"> Check me out
<label for="option2"></label>
<input name="option1" id="option2" type="checkbox"> Check me out
</div>
</form>
<div>
<a href="#" class="small">learn more</a>
</div>
</authz:authorize>

View File

@ -12,19 +12,19 @@
<property name="scriptLocations" >
<list>
<!-- OpenID Connect Data model -->
<value>file:src/main/webapp/db/tables/accesstoken.sql</value>
<value>file:src/main/webapp/db/tables/address.sql</value>
<value>file:src/main/webapp/db/tables/approvedsite.sql</value>
<value>file:src/main/webapp/db/tables/authorities.sql</value>
<value>file:src/main/webapp/db/tables/clientdetails.sql</value>
<value>file:src/main/webapp/db/tables/event.sql</value>
<value>file:src/main/webapp/db/tables/granttypes.sql</value>
<value>file:src/main/webapp/db/tables/idtoken.sql</value>
<value>file:src/main/webapp/db/tables/idtokenclaims.sql</value>
<value>file:src/main/webapp/db/tables/refreshtoken.sql</value>
<value>file:src/main/webapp/db/tables/scope.sql</value>
<value>file:src/main/webapp/db/tables/userinfo.sql</value>
<value>file:src/main/webapp/db/tables/whitelistedsite.sql</value>
<value>file:db/tables/accesstoken.sql</value>
<value>file:db/tables/address.sql</value>
<value>file:db/tables/approvedsite.sql</value>
<value>file:db/tables/authorities.sql</value>
<value>file:db/tables/clientdetails.sql</value>
<value>file:db/tables/event.sql</value>
<value>file:db/tables/granttypes.sql</value>
<value>file:db/tables/idtoken.sql</value>
<value>file:db/tables/idtokenclaims.sql</value>
<value>file:db/tables/refreshtoken.sql</value>
<value>file:db/tables/scope.sql</value>
<value>file:db/tables/userinfo.sql</value>
<value>file:db/tables/whitelistedsite.sql</value>
<!-- Preloaded data -->
<value>classpath:test-data.sql</value>
</list>