Coverage Report - org.openpermis.repository.basic.LdapSubjectRepository
 
Classes in this File Line Coverage Branch Coverage Complexity
LdapSubjectRepository
32%
38/118
25%
9/36
4.2
 
 1  
 /*
 2  
  * Copyright (c) 2009, Swiss Federal Department of Defence Civil Protection and Sport
 3  
  *                     (http://www.vbs.admin.ch)
 4  
  * Copyright (c) 2009, Ergon Informatik AG (http://www.ergon.ch)
 5  
  * All rights reserved.
 6  
  *
 7  
  * Licensed under the Open Permis License which accompanies this distribution,
 8  
  * and is available at http://www.openpermis.org/BSDlicenceKent.txt
 9  
  */
 10  
 package org.openpermis.repository.basic;
 11  
 
 12  
 import java.io.ByteArrayOutputStream;
 13  
 import java.io.IOException;
 14  
 import java.io.InputStream;
 15  
 import java.net.URI;
 16  
 import java.security.NoSuchAlgorithmException;
 17  
 import java.security.NoSuchProviderException;
 18  
 import java.util.ArrayList;
 19  
 import java.util.Enumeration;
 20  
 import java.util.HashMap;
 21  
 import java.util.Hashtable;
 22  
 import java.util.List;
 23  
 import java.util.Map;
 24  
 
 25  
 import javax.naming.Context;
 26  
 import javax.naming.NamingEnumeration;
 27  
 import javax.naming.NamingException;
 28  
 import javax.naming.directory.Attribute;
 29  
 import javax.naming.directory.SearchControls;
 30  
 import javax.naming.directory.SearchResult;
 31  
 import javax.naming.ldap.InitialLdapContext;
 32  
 
 33  
 import org.bouncycastle.util.encoders.Base64;
 34  
 
 35  
 import org.openpermis.Subject;
 36  
 import org.openpermis.basic.InternalSubject;
 37  
 import org.openpermis.cert.AttributeCertificate;
 38  
 import org.openpermis.cert.CertificateVerifier;
 39  
 import org.openpermis.repository.SubjectRepositoryException;
 40  
 
 41  
 
 42  
 /**
 43  
  * Loads certificate attributes from an LDAP directory and builds up a subject repository from
 44  
  * them.
 45  
  *
 46  
  * This implementation of {@link org.openpermis.repository.SubjectRepository}
 47  
  * connects to an LDAP directory
 48  
  * (either anonymously or using username and password) and looks at all nodes of the specified
 49  
  * search context (node or subtree). For each node, it looks for attributes containing role
 50  
  * assignment attribute certificates. The subject repository is build from all found and
 51  
  * successfully validated role assignment attribute certificates.
 52  
  * Note that it is not relevant in what nodes a role assignment is found. This means that a role
 53  
  * assignment does not necessarily have to be stored in role holders node.
 54  
  *
 55  
  * The search scope can be limited to the specified search context or to the whole subtree it
 56  
  * defines. An arbitrary search filter can be specified in order to limit the number of nodes or
 57  
  * in order to get better performance in large directories.
 58  
  *
 59  
  * This implementation keeps the subject repository in an internal cache. It is therefore not
 60  
  * necessary to query the directory for every call to {@link #retrieveSubject(URI)}. The validity
 61  
  * period of data in the cache can be controlled using method {@link #setCacheTimeout(long)} and
 62  
  * defaults to five seconds. The cache can be disabled leading to a potentially expensive LDAP
 63  
  * query for every call to {@link #retrieveSubject(URI)}.
 64  
  *
 65  
  * @since 0.1.0
 66  
  */
 67  
 public class LdapSubjectRepository extends AbstractSubjectRepository {
 68  
         //---- Static
 69  
 
 70  
         /** The attribute name under which attribute certificates are stored in a directory */
 71  
         private static final String ACE_ATTRIBUTE_NAME = "attributeCertificateAttribute";
 72  
 
 73  
         /** The default cache timeout */
 74  
         private static final long DEFAULT_CACHE_TIMEOUT_MILLIS = 5000L;
 75  
 
 76  
         /** Don't limit ldap result size by default */
 77  
         private static final long DEFAULT_LDAP_SEARCH_LIMIT = 0L;
 78  
 
 79  
         /** Default LDAP version string */
 80  
         private static final String DEFAULT_LDAP_VERSION = "3";
 81  
 
 82  
         /** Default is not to use SSL */
 83  
         private static final boolean DEFAULT_LDAP_USE_SSL = false;
 84  
 
 85  
         /** Default initial context factory */
 86  
         private static final String DEFAULT_LDAP_INITIAL_CONTEXT_FACTORY =
 87  
                 "com.sun.jndi.ldap.LdapCtxFactory";
 88  
 
 89  
         /** LDAP search conditino that is always true */
 90  
         private static final String DEFAULT_LDAP_SEARCH_FILTER = "objectClass=*";
 91  
 
 92  
         /** The default search scope is subtree */
 93  
         private static final int DEFAULT_SEARCH_SCOPE = SearchControls.SUBTREE_SCOPE;
 94  
 
 95  
         /** Buffer size */
 96  
         private static final int BUFFER_SIZE = 1024;
 97  
 
 98  
         //---- State
 99  
 
 100  
         /**
 101  
          * The maximum number of results an LDAP directory may return.
 102  
          * 0 = no limit.
 103  
          * Note that there may be a server side limit that cannot be influenced by the client.
 104  
          */
 105  
         private long ldapSearchLimit;
 106  
 
 107  
         /**
 108  
          * The number of milliseconds the data loaded from the LDAP may be cached.
 109  
          */
 110  
         private long cacheTimeout;
 111  
 
 112  
         /**
 113  
          * Internal timestamp of latest refresh of the cache.
 114  
          */
 115  
         private long latestRefresh;
 116  
 
 117  
         /**
 118  
          * The internal data cache.
 119  
          */
 120  
         private final Map<URI, InternalSubject> subjectsByIdentity;
 121  
 
 122  
         /**
 123  
          * The following state is used in the LDAP queries.
 124  
          */
 125  
         private String ldapVersion;
 126  
         private boolean useSsl;
 127  
         private String initialContextFactory;
 128  
         private String ldapFilter;
 129  
         private Object[] ldapFilterArgs;
 130  
         private int ldapSearchScope;
 131  
 
 132  
         private String ldapUrl;
 133  
         private String bindPrincipal;
 134  
         private String bindPrincipalPassword;
 135  
         private String ldapSearchBase;
 136  
 
 137  
         /** Attribute certificate data user in unit tests */
 138  
         private List<byte[]> testAcData;
 139  
         /** Set to true in the case of a unit test without LDAP */
 140  
         private final boolean unitTestMode;
 141  
 
 142  
         //---- Constructors
 143  
         /**
 144  
          * Creates an LDAP subject repository and internally stores the specified
 145  
          * SOA certificate. It is used for validating the attribute certificates of a subject.
 146  
          * <p>
 147  
          * The public key of the subject in the SOA certificate is considered to be authentic. It is
 148  
          * the callers duty to ensure that it really is authentic. Further, by specifiying the SOA
 149  
          * certificate, its subject is trusted.
 150  
          * </p>
 151  
          *
 152  
          * @param certificateVerifier The certificate verifier used to verify the attribute
 153  
          * certificates.
 154  
          * Must not be <code>null</code>.
 155  
          *
 156  
          * @param ldapUrl The LDAP URL. Example: "ldap://foo.host.com:389"
 157  
          * @param bindPrincipal The distinguished name of the principal used to bind at the directory
 158  
          *        to perform the search. Use <code>null</code> as value to anonymously bind.
 159  
          * @param bindPrincipalPassword The password of the principal to bind at the directory to
 160  
          *        perform the search. Use <code>null</code> as value to anonymously bind.
 161  
          * @param ldapSearchBase The search base.
 162  
          * @param ldapSearchScope The search level. Must be either {@link SearchControls#ONELEVEL_SCOPE}
 163  
          *        or {@link SearchControls#SUBTREE_SCOPE}.
 164  
 
 165  
          *
 166  
          *
 167  
          * @throws SubjectRepositoryException Thrown if an error occurs reading or interpreting the
 168  
          * SOA certificate.
 169  
          * @since 0.1.0
 170  
          */
 171  
         public LdapSubjectRepository (
 172  
                 CertificateVerifier certificateVerifier,
 173  
                 String ldapUrl,
 174  
                 String bindPrincipal,
 175  
                 String bindPrincipalPassword,
 176  
                 String ldapSearchBase,
 177  
                 int ldapSearchScope
 178  
         ) throws SubjectRepositoryException {
 179  0
                 super(certificateVerifier);
 180  0
                 this.unitTestMode = false;
 181  0
                 this.subjectsByIdentity = new HashMap<URI, InternalSubject>();
 182  
                 // init state with default values
 183  0
                 this.ldapSearchLimit = DEFAULT_LDAP_SEARCH_LIMIT;
 184  0
                 this.cacheTimeout = DEFAULT_CACHE_TIMEOUT_MILLIS;
 185  0
                 this.ldapVersion = DEFAULT_LDAP_VERSION;
 186  0
                 this.useSsl = DEFAULT_LDAP_USE_SSL;
 187  0
                 this.initialContextFactory = DEFAULT_LDAP_INITIAL_CONTEXT_FACTORY;
 188  0
                 this.ldapFilter = DEFAULT_LDAP_SEARCH_FILTER;
 189  0
                 this.ldapFilterArgs = null;
 190  0
                 this.ldapSearchScope = DEFAULT_SEARCH_SCOPE;
 191  
 
 192  
                 // check constructor data
 193  0
                 if (ldapUrl == null) {
 194  0
                         throw new IllegalArgumentException("ldap URL is null");
 195  
                 }
 196  0
                 this.ldapUrl = ldapUrl;
 197  
                 // user / pwd may be null for anonymous bind
 198  0
                 this.bindPrincipal = bindPrincipal;
 199  0
                 this.bindPrincipalPassword = bindPrincipalPassword;
 200  
                 // search base may be empty if it was already part of the url but it may not be null
 201  0
                 this.ldapSearchBase = ldapSearchBase == null ? "" : ldapSearchBase;
 202  
                 // the search scope value must be one of the following three
 203  0
                 if (
 204  
                         ldapSearchScope != SearchControls.OBJECT_SCOPE &&
 205  
                         ldapSearchScope != SearchControls.SUBTREE_SCOPE &&
 206  
                         ldapSearchScope != SearchControls.ONELEVEL_SCOPE
 207  
                 ) {
 208  0
                         throw new IllegalArgumentException("search scope has an invalid value");
 209  
                 }
 210  0
                 this.ldapSearchScope = ldapSearchScope;
 211  
 
 212  
                 // invalidate cache
 213  0
                 invalidateCache();
 214  0
         }
 215  
 
 216  
         /**
 217  
          * Package private constructor used for unit testing. It loads the role assignments given
 218  
          * in the specified files and uses them as input instead of querying the LDAP directory.
 219  
          *
 220  
          * In this mode, the verification of attribute certificates, the creation of subjects and
 221  
          * the caching may be tested without an LDAP directory.
 222  
          *
 223  
          * @param certificateVerifier The certificate verifier user to verify the attribute
 224  
          * certificates.
 225  
          * Must not be <code>null</code>.
 226  
          * Must not be <code>null</code>.
 227  
          * @param acsFileNames One or more files names pointing to attribute certificates on the
 228  
          * classpath.
 229  
          * @throws SubjectRepositoryException Thrown if an error occurs reading or interpreting the
 230  
          * SOA certificate.
 231  
          * @since 0.1.0
 232  
          */
 233  
         LdapSubjectRepository (CertificateVerifier certificateVerifier, String... acsFileNames)
 234  
                 throws SubjectRepositoryException
 235  
         {
 236  3
                 super(certificateVerifier);
 237  3
                 this.unitTestMode = true;
 238  
                 // init ac data list
 239  3
                 this.testAcData = new ArrayList<byte[]>();
 240  
                 try {
 241  
 
 242  9
                         for (String fn : acsFileNames) {
 243  6
                                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
 244  6
                                 InputStream is = LdapSubjectRepository.class.getResourceAsStream(fn);
 245  6
                                 byte[] buffer = new byte[BUFFER_SIZE];
 246  6
                                 int readBytes = 0;
 247  12
                                 while ((readBytes = is.read(buffer)) > -1) {
 248  6
                                         baos.write(buffer, 0, readBytes);
 249  
                                 }
 250  6
                                 is.close();
 251  6
                                 this.testAcData.add(Base64.encode(baos.toByteArray()));
 252  
                         }
 253  0
                 } catch (IOException e) {
 254  0
                         throw new SubjectRepositoryException(
 255  
                                 "Cannot setup ldap subject repostiory in test mode.", e
 256  
                         );
 257  3
                 }
 258  3
                 this.subjectsByIdentity = new HashMap<URI, InternalSubject>();
 259  3
                 this.cacheTimeout = DEFAULT_CACHE_TIMEOUT_MILLIS;
 260  3
                 invalidateCache();
 261  3
         }
 262  
 
 263  
         //---- Methods
 264  
 
 265  
         /**
 266  
          * Private method that refreshes the internal data cache by quering the LDAP.
 267  
          * @throws SubjectRepositoryException Thrown if the subject repository cannot be built up.
 268  
 
 269  
          */
 270  
         private void refreshDataCache () throws SubjectRepositoryException
 271  
         {
 272  
                 List<byte[]> acsData;
 273  3
                 if (this.unitTestMode) {
 274  3
                         acsData = this.testAcData;
 275  
                 } else {
 276  
                         try {
 277  0
                                 acsData = getAttributeCertificateData(
 278  
                                         this.ldapUrl,
 279  
                                         this.bindPrincipal,
 280  
                                         this.bindPrincipalPassword,
 281  
                                         this.ldapVersion,
 282  
                                         this.ldapSearchLimit,
 283  
                                         this.useSsl,
 284  
                                         this.initialContextFactory,
 285  
                                         this.ldapSearchBase,
 286  
                                         this.ldapSearchScope,
 287  
                                         this.ldapFilter,
 288  
                                         this.ldapFilterArgs
 289  
                                 );
 290  0
                         } catch (NamingException e) {
 291  0
                                 throw new SubjectRepositoryException("Cannot load data from LDAP.", e);
 292  0
                         }
 293  
                 }
 294  
                 // try to decode them as attribute certificates and verify them
 295  3
                 synchronized (this.subjectsByIdentity) {
 296  3
                         for (byte[] acData : acsData) {
 297  
                                 try {
 298  
                                         
 299  6
                                         updateSubjectMap(this.subjectsByIdentity, new AttributeCertificate(
 300  
                                                 Base64.decode(acData))
 301  
                                         );
 302  0
                                 } catch (NoSuchAlgorithmException e) {
 303  0
                                         throw new SubjectRepositoryException(
 304  
                                                 "A cryptographic algorithm used for signature verification cannot " +
 305  
                                                 "be retrieved from the crypto provider(s).",
 306  
                                                 e
 307  
                                         );
 308  0
                                 } catch (NoSuchProviderException e) {
 309  0
                                         throw new SubjectRepositoryException("There is no default crypto provider.", e);
 310  0
                                 } catch (IOException e) {
 311  
                                         // ignore this certificate
 312  12
                                 }
 313  
                         }
 314  3
                         this.latestRefresh = System.currentTimeMillis();
 315  3
                 }
 316  3
         }
 317  
 
 318  
 
 319  
         /**
 320  
          * Allows specifying an LDAP filter expression that is used to search attribute certificates.
 321  
          * Setting a filter also invalidates the cache such that data is re-read from the directory
 322  
          * the next time {@link #retrieveSubject(URI)} is called.
 323  
          * @param filterExpression An LDAP filter expression (following RFC 2254).
 324  
          * @param filterArguments A list of arguments for the filter expressions. May be null.
 325  
          * @return Returns the object this method was called on (fluent interface).
 326  
          * @since 0.1.0
 327  
          */
 328  
         public LdapSubjectRepository setLdapFilter (String filterExpression, Object[] filterArguments) {
 329  0
                 if (filterExpression != null) {
 330  0
                         this.ldapFilter = filterExpression;
 331  0
                         this.ldapFilterArgs = filterArguments.clone();
 332  0
                         invalidateCache();
 333  
                 }
 334  0
                 return this;
 335  
         }
 336  
 
 337  
         /**
 338  
          * Allows specifying the initial LDAP context factory that is used to search attribute
 339  
          * certificates.
 340  
          * Setting the factory also invalidates the cache such that data is re-read from the directory
 341  
          * the next time {@link #retrieveSubject(URI)} is called.
 342  
          * @param initialLdapContextFactory The class name of the initial LDAP context factory.
 343  
          * @return Returns the object this method was called on (fluent interface).
 344  
          * @since 0.1.0
 345  
          */
 346  
         public LdapSubjectRepository setInitialLdapContextFactory (String initialLdapContextFactory) {
 347  0
                 if (initialLdapContextFactory == null) {
 348  0
                         throw new IllegalArgumentException("inital LDAP context factory is null");
 349  
                 }
 350  0
                 this.initialContextFactory = initialLdapContextFactory;
 351  0
                 invalidateCache();
 352  0
                 return this;
 353  
         }
 354  
 
 355  
         /**
 356  
          * Sets the maximum number of results an LDAP directory may return in a search.
 357  
          * Zero (0) means "no limit" which is the default.
 358  
          * Note that there may be a server side limit that cannot be influenced by the client.
 359  
          * @param maxResults The maximum number of results.
 360  
          * @return Returns the object this method was called on (fluent interface).
 361  
          * @since 0.1.0
 362  
          */
 363  
         public LdapSubjectRepository setLdapSearchLimit (long maxResults) {
 364  0
                 this.ldapSearchLimit = maxResults;
 365  0
                 return this;
 366  
         }
 367  
 
 368  
         /**
 369  
          * Sets the cache timeout in milliseconds. The cache timeout defines how long data loaded
 370  
          * from the directory is considered valid before reloading it again.
 371  
          * Use the value zero to disable caching and use Long.MAX_VALUE to cache the data for ever.
 372  
          * @param milliseconds The number of milliseconds of the cache timeout.
 373  
          * @return Returns the object this method was called on (fluent interface).
 374  
          * @since 0.1.0
 375  
          */
 376  
         public LdapSubjectRepository setCacheTimeout (long milliseconds) {
 377  0
                 synchronized (this.subjectsByIdentity) {
 378  0
                         this.cacheTimeout = milliseconds < 0 ? 0 : milliseconds;
 379  0
                 }
 380  0
                 return this;
 381  
         }
 382  
 
 383  
         /**
 384  
          * Invalidates the internal data cache. Forces the data to be reload the next time
 385  
          * {@link #retrieveSubject(URI)} is called.
 386  
          * @since 0.1.0
 387  
          */
 388  
         public void invalidateCache () {
 389  3
                 synchronized (this.subjectsByIdentity) {
 390  3
                         this.latestRefresh = -1L;
 391  3
                 }
 392  
 
 393  3
         }
 394  
 
 395  
         /**
 396  
          * Searches in on specified directory tree (context) for entries with attribute certificates
 397  
          * and returns a list of byte arrays containing the data.
 398  
          *
 399  
          * @param ldapUrl The LDAP URL. Example: "ldap://foo.host.com:389"
 400  
          * @param bindPrincipal The distinguished name of the principal used to bind at the directory
 401  
          *        to perform the search. Use <code>null</code> as value to anonymously bind.
 402  
          * @param bindPrincipalPassword The password of the principal to bind at the directory to
 403  
          *        perform the search. Use <code>null</code> as value to anonymously bind.
 404  
          * @param ldapVersion The LDAP version string. Normally, this is "3".
 405  
          * @param ldapSearchLimit The maximum number of results to return in an LDAP query.
 406  
          * @param useSsl Set to true if SSL should be used to connect to the ldap server. If set to true
 407  
          * the SSL factory provided by the JVM is used and a corresponding trust- and keystore must be
 408  
          * configured in the JVM.
 409  
          * @param initialContextFactory The factory used to create initial LDAP context object.
 410  
          *        This depends on the used JVM. To use the SUN implementation use the value
 411  
          *        "com.sun.jndi.ldap.LdapCtxFactory".
 412  
          * @param ldapSearchBase The search base.
 413  
          * @param ldapSearchLevel The search level. Must be either {@link SearchControls#ONELEVEL_SCOPE}
 414  
          *        or {@link SearchControls#SUBTREE_SCOPE}.
 415  
          * @param filter An LDAP filter expression (following RFC 2254).
 416  
          * @param filterArgs The filter arguments.
 417  
          * @return A possibly empty list of byte arrays containing attribute certificates.
 418  
          * @throws NamingException If there is an error searching the directory or decoding the response
 419  
          *         from the directory.
 420  
          */
 421  
         private static List<byte[]> getAttributeCertificateData (
 422  
                 String ldapUrl,
 423  
                 String bindPrincipal,
 424  
                 String bindPrincipalPassword,
 425  
                 String ldapVersion,
 426  
                 long ldapSearchLimit,
 427  
                 boolean useSsl,
 428  
                 String initialContextFactory,
 429  
                 String ldapSearchBase,
 430  
                 int ldapSearchLevel,
 431  
                 String filter,
 432  
                 Object[] filterArgs
 433  
         ) throws NamingException {
 434  
                 // setup the ldap context for searching
 435  0
                 Hashtable<String, String> env = new Hashtable<String, String>();
 436  0
                 env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
 437  0
                 env.put(Context.PROVIDER_URL, ldapUrl);
 438  0
                 env.put(Context.SECURITY_AUTHENTICATION, "simple");
 439  0
                 if (bindPrincipal != null) {
 440  0
                         env.put(Context.SECURITY_PRINCIPAL, bindPrincipal);
 441  0
                         env.put(Context.SECURITY_CREDENTIALS, bindPrincipalPassword);
 442  
                 }
 443  0
                 env.put("java.naming.ldap.attributes.binary", ACE_ATTRIBUTE_NAME);
 444  0
                 env.put("java.naming.ldap.version", ldapVersion);
 445  0
                 if (useSsl) {
 446  0
                         env.put(Context.SECURITY_PROTOCOL, "ssl");
 447  
                 }
 448  
                 // create search context
 449  0
                 InitialLdapContext context = new InitialLdapContext(env, null);
 450  
 
 451  
                 // setup LDAP search controls
 452  0
                 SearchControls searchControls = new SearchControls();
 453  0
                 searchControls.setReturningAttributes(new String[] {ACE_ATTRIBUTE_NAME});
 454  0
                 searchControls.setCountLimit(ldapSearchLimit);
 455  0
                 searchControls.setSearchScope(ldapSearchLevel);
 456  
                 // perform the LDAP query
 457  0
                 Enumeration<SearchResult> resultFromLdap =
 458  
                         context.search(ldapSearchBase, filter, filterArgs, searchControls);
 459  
                 // prepare result structure
 460  0
                 final List<byte[]> res = new ArrayList<byte[]>();
 461  
 
 462  
                 // process the result from the LDAP, i.e. extract the attribute certificates
 463  0
                 while (resultFromLdap.hasMoreElements()) {
 464  0
                         final SearchResult searchResult = resultFromLdap.nextElement();
 465  0
                         final NamingEnumeration<? extends Attribute> names =
 466  
                                 searchResult.getAttributes().getAll();
 467  0
                         while (names.hasMore()) {
 468  0
                                 Attribute at = names.next();
 469  
                                 // note that there may be more than one value for this attribute !!
 470  0
                                 NamingEnumeration<?> values = at.getAll();
 471  0
                                 while (values.hasMore()) {
 472  
                                         // the cast to byte[] is ok because the context was setup such that the
 473  
                                         //requested attribute is returned as binary
 474  0
                                         res.add((byte[]) values.next());
 475  
                                 }
 476  0
                         }
 477  0
                 }
 478  0
                 return res;
 479  
         }
 480  
 
 481  
         //---- SubjectRepository
 482  
 
 483  
         /**
 484  
          * @since 0.1.0
 485  
          */
 486  
         // TODO Any Function does not correctly implement interface (Should never return null).
 487  
         public Subject retrieveSubject (URI identity) throws SubjectRepositoryException {
 488  
                 final Subject result;
 489  4
                 synchronized (this.subjectsByIdentity) {
 490  4
                         if (System.currentTimeMillis() < this.latestRefresh + this.cacheTimeout) {
 491  1
                                 result = this.subjectsByIdentity.get(identity);
 492  
                         } else {
 493  
                                 try {
 494  3
                                         refreshDataCache();
 495  0
                                 } catch (SubjectRepositoryException e) {
 496  0
                                         throw new SubjectRepositoryException("Cannot update subject repository.", e);
 497  3
                                 }
 498  3
                                 result = this.subjectsByIdentity.get(identity);
 499  
                         }
 500  4
                 }
 501  4
                 return result;
 502  
         }
 503  
 }