You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by se...@apache.org on 2016/07/20 19:02:50 UTC

cxf git commit: Making it possible to use client_credentials without pre-registering clients

Repository: cxf
Updated Branches:
  refs/heads/master ce2d945bf -> c7ebda63c


Making it possible to use client_credentials without pre-registering clients


Project: http://git-wip-us.apache.org/repos/asf/cxf/repo
Commit: http://git-wip-us.apache.org/repos/asf/cxf/commit/c7ebda63
Tree: http://git-wip-us.apache.org/repos/asf/cxf/tree/c7ebda63
Diff: http://git-wip-us.apache.org/repos/asf/cxf/diff/c7ebda63

Branch: refs/heads/master
Commit: c7ebda63ccb53b4d246fe886dfe492e1ffd78ac8
Parents: ce2d945
Author: Sergey Beryozkin <sb...@gmail.com>
Authored: Wed Jul 20 22:02:32 2016 +0300
Committer: Sergey Beryozkin <sb...@gmail.com>
Committed: Wed Jul 20 22:02:32 2016 +0300

----------------------------------------------------------------------
 .../clientcred/ClientCredentialsGrant.java      | 25 +++++++++++++++-
 .../provider/AbstractOAuthDataProvider.java     |  6 ++++
 .../oauth2/services/AbstractOAuthService.java   |  8 ++---
 .../oauth2/services/AbstractTokenService.java   | 31 ++++++++++++--------
 .../services/DirectAuthorizationService.java    |  5 +---
 .../services/RedirectionBasedGrantService.java  | 11 +++----
 .../oauth2/common/OAuthDataProviderImpl.java    | 21 ++++++++++++-
 .../security/oauth2/grants/JAXRSOAuth2Test.java | 17 ++++++++++-
 .../jaxrs/security/oauth2/grants/server.xml     |  4 +++
 9 files changed, 97 insertions(+), 31 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cxf/blob/c7ebda63/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/clientcred/ClientCredentialsGrant.java
----------------------------------------------------------------------
diff --git a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/clientcred/ClientCredentialsGrant.java b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/clientcred/ClientCredentialsGrant.java
index f98e935..caf7920 100644
--- a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/clientcred/ClientCredentialsGrant.java
+++ b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/clientcred/ClientCredentialsGrant.java
@@ -18,13 +18,16 @@
  */
 package org.apache.cxf.rs.security.oauth2.grants.clientcred;
 
+import javax.ws.rs.core.MultivaluedMap;
+
 import org.apache.cxf.rs.security.oauth2.grants.AbstractGrant;
 import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
 
 public class ClientCredentialsGrant extends AbstractGrant {
     
     private static final long serialVersionUID = 5586488165697954347L;
-
+    private String clientId;
+    private String clientSecret;
     public ClientCredentialsGrant() {
         this(null);
     }
@@ -36,4 +39,24 @@ public class ClientCredentialsGrant extends AbstractGrant {
     public ClientCredentialsGrant(String scope, String audience) {
         super(OAuthConstants.CLIENT_CREDENTIALS_GRANT, scope, audience);
     }
+    
+    public MultivaluedMap<String, String> toMap() {
+        MultivaluedMap<String, String> map = super.toMap();
+        if (clientId != null) {
+            map.putSingle(OAuthConstants.CLIENT_ID, clientId);
+            if (clientSecret != null) {
+                map.putSingle(OAuthConstants.CLIENT_SECRET, clientSecret);
+                
+            }    
+        }
+        return map;
+    }
+
+    public void setClientId(String clientId) {
+        this.clientId = clientId;
+    }
+
+    public void setClientSecret(String clientSecret) {
+        this.clientSecret = clientSecret;
+    }
 }

http://git-wip-us.apache.org/repos/asf/cxf/blob/c7ebda63/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProvider.java
----------------------------------------------------------------------
diff --git a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProvider.java b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProvider.java
index 4169f4b..860fa7d 100644
--- a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProvider.java
+++ b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProvider.java
@@ -214,6 +214,12 @@ public abstract class AbstractOAuthDataProvider implements OAuthDataProvider, Cl
         return new BearerAccessToken(client, accessTokenLifetime);
     }
      
+    protected String getCurrentRequestedGrantType() {
+        return (String)messageContext.get(OAuthConstants.GRANT_TYPE);
+    }
+    protected String getCurrentClientSecret() {
+        return (String)messageContext.get(OAuthConstants.CLIENT_SECRET);
+    }
     protected RefreshToken updateRefreshToken(RefreshToken rt, ServerAccessToken at) {
         linkAccessTokenToRefreshToken(rt, at);
         saveRefreshToken(rt);

http://git-wip-us.apache.org/repos/asf/cxf/blob/c7ebda63/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/AbstractOAuthService.java
----------------------------------------------------------------------
diff --git a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/AbstractOAuthService.java b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/AbstractOAuthService.java
index 7873aa8..c8bd408 100644
--- a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/AbstractOAuthService.java
+++ b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/AbstractOAuthService.java
@@ -90,17 +90,17 @@ public abstract class AbstractOAuthService {
         return getMessageContext().getUriInfo().getQueryParameters();
     }
     
-    protected Client getValidClient(MultivaluedMap<String, String> params) {
-        return getValidClient(params.getFirst(OAuthConstants.CLIENT_ID));
-    }
     /**
      * Get the {@link Client} reference
      * @param clientId the provided client id
      * @return Client the client reference 
      * @throws {@link OAuthServiceExcepption} if no matching Client is found
      */
-    protected Client getValidClient(String clientId) throws OAuthServiceException {
+    protected Client getValidClient(String clientId, MultivaluedMap<String, String> params)
+        throws OAuthServiceException {
         if (clientId != null) {
+            mc.put(OAuthConstants.CLIENT_SECRET, params.getFirst(OAuthConstants.CLIENT_SECRET));
+            mc.put(OAuthConstants.GRANT_TYPE, params.getFirst(OAuthConstants.GRANT_TYPE));
             return dataProvider.getClient(clientId);
         }
         LOG.fine("No valid client found as the given clientId is null");

http://git-wip-us.apache.org/repos/asf/cxf/blob/c7ebda63/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/AbstractTokenService.java
----------------------------------------------------------------------
diff --git a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/AbstractTokenService.java b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/AbstractTokenService.java
index 238ae98..05fdefb 100644
--- a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/AbstractTokenService.java
+++ b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/AbstractTokenService.java
@@ -63,21 +63,22 @@ public class AbstractTokenService extends AbstractOAuthService {
             String clientId = retrieveClientId(params);
             if (clientId != null) {
                 client = getAndValidateClientFromIdAndSecret(clientId,
-                                              params.getFirst(OAuthConstants.CLIENT_SECRET));
+                                              params.getFirst(OAuthConstants.CLIENT_SECRET),
+                                                             params);
             }
         } else {
             String clientId = retrieveClientId(params);
             if (clientId != null) {
-                client = getClient(clientId);
+                client = getClient(clientId, params);
             } else if (principal.getName() != null) {
-                client = getClient(principal.getName());
+                client = getClient(principal.getName(), params);
             } 
         }
         if (client == null) {
-            client = getClientFromTLSCertificates(sc, getTlsSessionInfo());
+            client = getClientFromTLSCertificates(sc, getTlsSessionInfo(), params);
             if (client == null) {
                 // Basic Authentication is expected by default
-                client = getClientFromBasicAuthScheme();
+                client = getClientFromBasicAuthScheme(params);
             }
         }
         if (client != null && !client.getApplicationCertificates().isEmpty()) {
@@ -107,8 +108,10 @@ public class AbstractTokenService extends AbstractOAuthService {
     }
     
     // Get the Client and check the id and secret
-    protected Client getAndValidateClientFromIdAndSecret(String clientId, String providedClientSecret) {
-        Client client = getClient(clientId);
+    protected Client getAndValidateClientFromIdAndSecret(String clientId, 
+                                                         String providedClientSecret,
+                                                         MultivaluedMap<String, String> params) {
+        Client client = getClient(clientId, params);
         if (!client.getClientId().equals(clientId)) {
             reportInvalidClient();
         }
@@ -136,22 +139,24 @@ public class AbstractTokenService extends AbstractOAuthService {
             && clientSecret == null;
     }
     
-    protected Client getClientFromBasicAuthScheme() {
+    protected Client getClientFromBasicAuthScheme(MultivaluedMap<String, String> params) {
         String[] userInfo = AuthorizationUtils.getBasicAuthUserInfo(getMessageContext());
         if (userInfo != null && userInfo.length == 2) {
-            return getAndValidateClientFromIdAndSecret(userInfo[0], userInfo[1]);
+            return getAndValidateClientFromIdAndSecret(userInfo[0], userInfo[1], params);
         } else {
             return null;
         }
     }
     
-    protected Client getClientFromTLSCertificates(SecurityContext sc, TLSSessionInfo tlsSessionInfo) {
+    protected Client getClientFromTLSCertificates(SecurityContext sc, 
+                                                  TLSSessionInfo tlsSessionInfo,
+                                                  MultivaluedMap<String, String> params) {
         Client client = null;
         if (tlsSessionInfo != null && StringUtils.isEmpty(sc.getAuthenticationScheme())) {
             // Pure 2-way TLS authentication
             String clientId = getClientIdFromTLSCertificates(sc, tlsSessionInfo);
             if (!StringUtils.isEmpty(clientId)) {
-                client = getClient(clientId);
+                client = getClient(clientId, params);
             }
         }
         return client;
@@ -219,14 +224,14 @@ public class AbstractTokenService extends AbstractOAuthService {
      * @return Client the client reference 
      * @throws {@link javax.ws.rs.WebApplicationException} if no matching Client is found
      */
-    protected Client getClient(String clientId) {
+    protected Client getClient(String clientId, MultivaluedMap<String, String> params) {
         if (clientId == null) {
             reportInvalidRequestError("Client ID is null");
             return null;
         }
         Client client = null;
         try {
-            client = getValidClient(clientId);
+            client = getValidClient(clientId, params);
         } catch (OAuthServiceException ex) {
             LOG.warning("No valid client found for clientId: " + clientId);
             if (ex.getError() != null) {

http://git-wip-us.apache.org/repos/asf/cxf/blob/c7ebda63/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/DirectAuthorizationService.java
----------------------------------------------------------------------
diff --git a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/DirectAuthorizationService.java b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/DirectAuthorizationService.java
index a9fa8be..aa59063 100644
--- a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/DirectAuthorizationService.java
+++ b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/DirectAuthorizationService.java
@@ -109,13 +109,10 @@ public class DirectAuthorizationService extends AbstractOAuthService {
         this.subjectCreator = subjectCreator;
     }
     protected Client getClient(MultivaluedMap<String, String> params) {
-        return getClient(params.getFirst(OAuthConstants.CLIENT_ID));
-    }
-    protected Client getClient(String clientId) {
         Client client = null;
         
         try {
-            client = getValidClient(clientId);
+            client = getValidClient(params.getFirst(OAuthConstants.CLIENT_ID), params);
         } catch (OAuthServiceException ex) {
             if (ex.getError() != null) {
                 reportInvalidRequestError(ex.getError(), null);

http://git-wip-us.apache.org/repos/asf/cxf/blob/c7ebda63/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/RedirectionBasedGrantService.java
----------------------------------------------------------------------
diff --git a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/RedirectionBasedGrantService.java b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/RedirectionBasedGrantService.java
index 92f7c1c..efd832a 100644
--- a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/RedirectionBasedGrantService.java
+++ b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/RedirectionBasedGrantService.java
@@ -122,7 +122,7 @@ public abstract class RedirectionBasedGrantService extends AbstractOAuthService
     protected Response startAuthorization(MultivaluedMap<String, String> params) {
         // Make sure the end user has authenticated, check if HTTPS is used
         SecurityContext sc = getAndValidateSecurityContext(params);
-        Client client = getClient(params);
+        Client client = getClient(params.getFirst(OAuthConstants.CLIENT_ID), params);
         // Create a UserSubject representing the end user 
         UserSubject userSubject = createUserSubject(sc, params);
         
@@ -351,7 +351,7 @@ public abstract class RedirectionBasedGrantService extends AbstractOAuthService
             state = recreateRedirectionStateFromParams(params); 
         }
         
-        Client client = getClient(state.getClientId());
+        Client client = getClient(state.getClientId(), params);
         String redirectUri = validateRedirectUri(client, state.getRedirectUri());
         
         // Get the end user decision value
@@ -502,11 +502,11 @@ public abstract class RedirectionBasedGrantService extends AbstractOAuthService
      *         the error is returned directly to the end user without 
      *         following the redirect URI if any
      */
-    protected Client getClient(String clientId) {
+    protected Client getClient(String clientId, MultivaluedMap<String, String> params) {
         Client client = null;
         
         try {
-            client = getValidClient(clientId);
+            client = getValidClient(clientId, params);
         } catch (OAuthServiceException ex) {
             if (ex.getError() != null) {
                 reportInvalidRequestError(ex.getError(), null);
@@ -519,9 +519,6 @@ public abstract class RedirectionBasedGrantService extends AbstractOAuthService
         return client;
         
     }
-    protected Client getClient(MultivaluedMap<String, String> params) {
-        return this.getClient(params.getFirst(OAuthConstants.CLIENT_ID));
-    }
     protected String getSupportedGrantType() {
         return this.supportedGrantType;
     }

http://git-wip-us.apache.org/repos/asf/cxf/blob/c7ebda63/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/oauth2/common/OAuthDataProviderImpl.java
----------------------------------------------------------------------
diff --git a/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/oauth2/common/OAuthDataProviderImpl.java b/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/oauth2/common/OAuthDataProviderImpl.java
index 6e2cd29..75e7d17 100644
--- a/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/oauth2/common/OAuthDataProviderImpl.java
+++ b/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/oauth2/common/OAuthDataProviderImpl.java
@@ -22,7 +22,9 @@ import java.io.InputStream;
 import java.security.cert.Certificate;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import org.apache.cxf.common.util.Base64Utility;
 import org.apache.cxf.rs.security.oauth2.common.Client;
@@ -37,7 +39,7 @@ import org.apache.xml.security.utils.ClassLoaderUtils;
  * Extend the DefaultEHCacheCodeDataProvider to allow refreshing of tokens
  */
 public class OAuthDataProviderImpl extends DefaultEHCacheCodeDataProvider {
-    
+    private Set<String> externalClients = new HashSet<String>();
     public OAuthDataProviderImpl(String servicePort) throws Exception {
         // filters/grants test client
         Client client = new Client("consumer-id", "this-is-a-secret", true);
@@ -119,6 +121,10 @@ public class OAuthDataProviderImpl extends DefaultEHCacheCodeDataProvider {
         client2.getAllowedGrantTypes().add("custom_grant");
         client2.setApplicationCertificates(Collections.singletonList(encodedCert));
         this.setClient(client2);
+        
+        // external clients (in LDAP/etc) which can be used for client cred
+        externalClients.add("bob:bobPassword");
+        
     }
     
     private Certificate loadCert() throws Exception {
@@ -128,6 +134,19 @@ public class OAuthDataProviderImpl extends DefaultEHCacheCodeDataProvider {
     }
     
     @Override
+    public Client getClient(String clientId) {
+        Client c = super.getClient(clientId);
+        if (c == null) {
+            String clientSecret = super.getCurrentClientSecret(); 
+            if (externalClients.contains(clientId + ":" + clientSecret)) {
+                c = new Client(clientId, clientSecret, true);
+            }
+        }
+        return c;
+        
+    }
+    
+    @Override
     protected boolean isRefreshTokenSupported(List<String> theScopes) {
         return true;
     }

http://git-wip-us.apache.org/repos/asf/cxf/blob/c7ebda63/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/oauth2/grants/JAXRSOAuth2Test.java
----------------------------------------------------------------------
diff --git a/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/oauth2/grants/JAXRSOAuth2Test.java b/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/oauth2/grants/JAXRSOAuth2Test.java
index d21dc9b..7adb545 100644
--- a/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/oauth2/grants/JAXRSOAuth2Test.java
+++ b/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/oauth2/grants/JAXRSOAuth2Test.java
@@ -42,6 +42,7 @@ import org.apache.cxf.rs.security.oauth2.client.Consumer;
 import org.apache.cxf.rs.security.oauth2.client.OAuthClientUtils;
 import org.apache.cxf.rs.security.oauth2.common.AccessTokenGrant;
 import org.apache.cxf.rs.security.oauth2.common.ClientAccessToken;
+import org.apache.cxf.rs.security.oauth2.grants.clientcred.ClientCredentialsGrant;
 import org.apache.cxf.rs.security.oauth2.grants.jwt.JwtBearerGrant;
 import org.apache.cxf.rs.security.oauth2.grants.saml.Saml2BearerGrant;
 import org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException;
@@ -58,6 +59,7 @@ import org.apache.wss4j.common.saml.SAMLUtil;
 import org.apache.wss4j.common.saml.SamlAssertionWrapper;
 import org.apache.wss4j.common.saml.builder.SAML2Constants;
 import org.apache.wss4j.common.util.DOM2Writer;
+
 import org.junit.BeforeClass;
 import org.junit.Test;
 
@@ -128,7 +130,7 @@ public class JAXRSOAuth2Test extends AbstractBusClientServerTestBase {
     }
     
     @Test
-    public void testTwoWayTLSAuthentication() throws Exception {
+    public void testTwoWayTLSAuthenticationCustomGrant() throws Exception {
         String address = "https://localhost:" + PORT + "/oauth2/token";
         WebClient wc = createWebClient(address);
         
@@ -137,6 +139,19 @@ public class JAXRSOAuth2Test extends AbstractBusClientServerTestBase {
     }
     
     @Test
+    public void testBasicAuthClientCred() throws Exception {
+        String address = "https://localhost:" + PORT + "/oauth2/token";
+        WebClient wc = createWebClient(address);
+        ClientCredentialsGrant grant = new ClientCredentialsGrant();
+        // Pass client_id & client_secret as form properties
+        // (instead WebClient can be initialized with username & password)
+        grant.setClientId("bob");
+        grant.setClientSecret("bobPassword");
+        ClientAccessToken at = OAuthClientUtils.getAccessToken(wc, grant);
+        assertNotNull(at.getTokenKey());
+    }
+    
+    @Test
     public void testSAML2BearerAuthenticationInterceptor() throws Exception {
         String address = "https://localhost:" + PORT + "/oauth2-auth/token";
         WebClient wc = createWebClientWithProps(address);

http://git-wip-us.apache.org/repos/asf/cxf/blob/c7ebda63/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oauth2/grants/server.xml
----------------------------------------------------------------------
diff --git a/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oauth2/grants/server.xml b/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oauth2/grants/server.xml
index 60bce6f..410a2c1 100644
--- a/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oauth2/grants/server.xml
+++ b/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oauth2/grants/server.xml
@@ -74,6 +74,9 @@ under the License.
     <bean id="customGrantHandler" class="org.apache.cxf.systest.jaxrs.security.oauth2.grants.CustomGrantHandler">
         <property name="dataProvider" ref="dataProvider"/>
     </bean>
+    <bean id="clientCredGrantHandler" class="org.apache.cxf.rs.security.oauth2.grants.clientcred.ClientCredentialsGrantHandler">
+        <property name="dataProvider" ref="dataProvider"/>
+    </bean>
     <bean id="oauthJson" class="org.apache.cxf.rs.security.oauth2.provider.OAuthJSONProvider"/>
     <bean id="serviceBean" class="org.apache.cxf.rs.security.oauth2.services.AccessTokenService">
         <property name="dataProvider" ref="dataProvider"/>
@@ -82,6 +85,7 @@ under the License.
                 <ref bean="samlGrantHandler"/>
                 <ref bean="jwtGrantHandler"/>
                 <ref bean="customGrantHandler"/>
+                <ref bean="clientCredGrantHandler"/>
             </list>
         </property>
     </bean>