001/* 002 * Copyright 2017 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.datasourcesexplorer; 017 018import java.nio.charset.StandardCharsets; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Map; 027import java.util.stream.Collectors; 028 029import javax.naming.NamingEnumeration; 030import javax.naming.NamingException; 031import javax.naming.PartialResultException; 032import javax.naming.SizeLimitExceededException; 033import javax.naming.directory.Attribute; 034import javax.naming.directory.Attributes; 035import javax.naming.directory.SearchControls; 036import javax.naming.directory.SearchResult; 037import javax.naming.ldap.InitialLdapContext; 038import javax.naming.ldap.LdapContext; 039 040import org.apache.avalon.framework.component.Component; 041import org.apache.cocoon.ProcessingException; 042import org.apache.commons.lang3.StringUtils; 043 044import org.ametys.core.util.ldap.AbstractLDAPConnector; 045import org.ametys.core.util.ldap.IncompleteLDAPResultException; 046 047/** 048 * Connection stuff to read ldap 049 */ 050public class LDAPConnector extends AbstractLDAPConnector implements Component 051{ 052 /** The compoent role */ 053 public static final String ROLE = LDAPConnector.class.getName(); 054 055 private static final int __LEVEL_SIZE = 100; 056 057 /** 058 * Get the ldap attributes of a node 059 * @param ldapDatasourceId The ldap datasource id 060 * @param dn The dn of the ldap node to get 061 * @return The attributes 062 * @throws ProcessingException If an error occurred with ldap 063 */ 064 public List<Map<String, String>> getAttributes(String ldapDatasourceId, String dn) throws ProcessingException 065 { 066 try 067 { 068 _delayedInitialize(ldapDatasourceId); 069 } 070 catch (Exception e) 071 { 072 throw new ProcessingException(e); 073 } 074 075 LdapContext context = null; 076 NamingEnumeration<String> ids = null; 077 try 078 { 079 // Connect to the LDAP server. 080 context = new InitialLdapContext(_getContextEnv(), null); 081 Attributes attributes = context.getAttributes(StringUtils.substringBefore(dn, "," + _ldapBaseDN)); 082 083 List<Map<String, String>> returnedAttributes = new ArrayList<>(); 084 085 ids = attributes.getIDs(); 086 while (ids.hasMore()) 087 { 088 String attrId = ids.next(); 089 Attribute attribute = attributes.get(attrId); 090 for (int i = 0; i < attribute.size(); i++) 091 { 092 String valueAsString; 093 094 try 095 { 096 Object value = attribute.get(i); 097 if (value instanceof byte[]) 098 { 099 byte[] valueAsBytes = (byte[]) value; 100 valueAsString = new String(valueAsBytes, StandardCharsets.UTF_8); 101 } 102 else 103 { 104 valueAsString = value.toString(); 105 } 106 107 if (valueAsString.length() > 255) 108 { 109 valueAsString = valueAsString.substring(0, 255) + "…"; 110 } 111 } 112 catch (Throwable t) 113 { 114 valueAsString = "Error retrieving: " + t.getMessage(); 115 getLogger().error("Cannot display value n°" + i + " of LDAP attribute '" + attrId + "' at " + (dn + "," + _ldapBaseDN), t); 116 } 117 118 Map<String, String> returnedAttribute = new HashMap<>(); 119 returnedAttribute.put("name", attrId); 120 returnedAttribute.put("value", valueAsString); 121 returnedAttributes.add(returnedAttribute); 122 } 123 } 124 125 return returnedAttributes; 126 } 127 catch (NamingException e) 128 { 129 throw new ProcessingException(e); 130 } 131 finally 132 { 133 // Close connection resources 134 _cleanup(context, ids); 135 } 136 } 137 138 /** 139 * Get children DN 140 * @param ldapDatasourceId The ldap datasource id 141 * @param dn The parent DN. Can be empty to get root DN. 142 * @return The children DNs 143 * @throws ProcessingException If an exception occurred while reading the ldap 144 */ 145 public Collection<DN> getChildren(String ldapDatasourceId, String dn) throws ProcessingException 146 { 147 try 148 { 149 _delayedInitialize(ldapDatasourceId); 150 } 151 catch (Exception e) 152 { 153 throw new ProcessingException(e); 154 } 155 156 String filter = "(objectClass=*)"; 157 SearchControls constraints = new SearchControls(); 158 constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); 159 160 try 161 { 162 if (StringUtils.isEmpty(dn)) 163 { 164 int count; 165 boolean incomplete; 166 167 try 168 { 169 count = _count("", filter, new Object[0], constraints); 170 incomplete = false; 171 } 172 catch (IncompleteLDAPResultException e) 173 { 174 count = e.getPartialResults(); 175 incomplete = true; 176 getLogger().warn("LDAP refused to return more than " + count + " results"); 177 } 178 return Collections.singletonList(new DN(_ldapBaseDN, _ldapBaseDN + (count > 0 ? " (" + count + (incomplete ? "+" : "") + ")" : ""), count > 0)); 179 } 180 else 181 { 182 Collection<DN> childrenDN = new ArrayList<>(); 183 184 int offset = 0; 185 int limit = Integer.MAX_VALUE; 186 if (dn.startsWith("#")) 187 { 188 // limited subpart 189 String count = StringUtils.substringBefore(dn, ",").substring(1); 190 offset = Integer.parseInt(StringUtils.substringBefore(count, "-")) - 1; 191 limit = Integer.parseInt(StringUtils.substringAfter(count, "-")) - offset; 192 } 193 194 // Remove the pseudo-nodes 195 String finalDN = Arrays.stream(StringUtils.split(dn, ",")).filter(s -> !s.startsWith("#")).collect(Collectors.joining(",")); 196 197 String subDN = finalDN.length() > _ldapBaseDN.length() ? finalDN.substring(0, finalDN.length() - (_ldapBaseDN.length() + 1)) : ""; 198 String prefixedSubDN = subDN.length() > 0 ? "," + subDN : ""; 199 200 int nodeSize; 201 if (limit < Integer.MAX_VALUE) 202 { 203 nodeSize = limit; 204 } 205 else 206 { 207 try 208 { 209 nodeSize = _count(subDN, filter, new Object[0], constraints); 210 } 211 catch (IncompleteLDAPResultException e) 212 { 213 nodeSize = e.getPartialResults(); 214 getLogger().warn("LDAP refused to return more than " + nodeSize + " results"); 215 } 216 } 217 218 final int levelNeeded = (int) Math.floor(Math.log10(nodeSize - 1) / Math.log10(__LEVEL_SIZE)); 219 220 if (levelNeeded <= 0) 221 { 222 // a few entries are sent directly 223 // TODO sort 224 List<SearchResult> results; 225 try 226 { 227 results = _search(subDN, filter, new Object[0], constraints, offset, limit); 228 } 229 catch (IncompleteLDAPResultException e) 230 { 231 results = e.getPartialResults(); 232 getLogger().warn("LDAP refused to return more than " + results.size() + " results"); 233 e.printStackTrace(); 234 } 235 for (Iterator<SearchResult> iterator = results.iterator(); iterator.hasNext();) 236 { 237 SearchResult searchResult = iterator.next(); 238 int count; 239 boolean incomplete; 240 try 241 { 242 count = _count(searchResult.getName() + prefixedSubDN, filter, new Object[0], constraints); 243 incomplete = false; 244 } 245 catch (IncompleteLDAPResultException e) 246 { 247 count = e.getPartialResults(); 248 incomplete = true; 249 getLogger().warn("LDAP refused to return more than " + count + " results"); 250 } 251 childrenDN.add(new DN(searchResult.getName(), searchResult.getName() + (count > 0 ? " (" + count + (incomplete ? "+" : "") + ")" : ""), count > 0)); 252 } 253 } 254 else 255 { 256 final int step = (int) Math.pow(__LEVEL_SIZE, levelNeeded); 257 int done = 0; 258 while (done < nodeSize) 259 { 260 int grow = Math.min(step, nodeSize - done); 261 childrenDN.add(new DN("#" + (done + 1) + "-" + (done + grow), "[" + (done + 1) + "..." + (done + grow) + "]", true)); 262 done += grow; 263 } 264 } 265 266 return childrenDN; 267 } 268 } 269 catch (NamingException e) 270 { 271 throw new ProcessingException(e); 272 } 273 } 274 275 /** 276 * Count the number of results 277 * @param name the name of the context or object to search 278 * @param filter the filter expression to use for the search 279 * @param filterArgs the array of arguments to substitute for the variables in filter. Can be null. 280 * @param searchControls the search controls that control the search. 281 * @return The number of results 282 * @throws NamingException If an error occurred 283 */ 284 protected boolean _hasResults(String name, String filter, Object[] filterArgs, SearchControls searchControls) throws NamingException 285 { 286 long countLimit = searchControls.getCountLimit(); 287 searchControls.setCountLimit(1); 288 289 try 290 { 291 return _count(name, filter, filterArgs, searchControls) > 0; 292 } 293 catch (IncompleteLDAPResultException e) 294 { 295 // Should never happened since we ask for only 1 result 296 getLogger().error("Truncated", e); 297 return false; 298 } 299 finally 300 { 301 searchControls.setCountLimit(countLimit); 302 } 303 } 304 305 /** 306 * Count the number of results 307 * @param name the name of the context or object to search 308 * @param filter the filter expression to use for the search 309 * @param filterArgs the array of arguments to substitute for the variables in filter. Can be null. 310 * @param searchControls the search controls that control the search. 311 * @return The number of results 312 * @throws NamingException If an error occurred 313 * @throws IncompleteLDAPResultException If results are truncated 314 */ 315 protected int _count(String name, String filter, Object[] filterArgs, SearchControls searchControls) throws NamingException, IncompleteLDAPResultException 316 { 317 int count = 0; 318 319 LdapContext context = null; 320 NamingEnumeration<SearchResult> tmpResults = null; 321 322 boolean returningObjFlag = searchControls.getReturningObjFlag(); 323 String[] attrs = searchControls.getReturningAttributes(); 324 searchControls.setReturningObjFlag(false); 325 searchControls.setReturningAttributes(new String[0]); 326 327 try 328 { 329 // Connect to the LDAP server. 330 context = new InitialLdapContext(_getContextEnv(), null); 331 332 _setResultsControls(context, false); 333 334 do 335 { 336 // Perform the search 337 tmpResults = context.search(name, filter, filterArgs, searchControls); 338 339 // Iterate over a batch of search results 340 try 341 { 342 while (tmpResults != null && tmpResults.hasMore()) 343 { 344 tmpResults.next(); 345 346 count++; 347 } 348 } 349 catch (PartialResultException e) 350 { 351 // ignore 352 } 353 } 354 while (_hasMoreEntries(context)); 355 } 356 catch (SizeLimitExceededException e) 357 { 358 throw new IncompleteLDAPResultException(count, e); 359 } 360 finally 361 { 362 // Close connection resources 363 _cleanup(context, tmpResults); 364 searchControls.setReturningObjFlag(returningObjFlag); 365 searchControls.setReturningAttributes(attrs); 366 } 367 368 return count; 369 } 370 371 /** 372 * A DN 373 */ 374 public static class DN 375 { 376 private String _dn; 377 private boolean _hasChild; 378 private String _label; 379 380 /** 381 * Create a DN 382 * @param dn The ldap dn 383 * @param label The label for the dn 384 * @param hasChild Has this DN children 385 */ 386 public DN(String dn, String label, boolean hasChild) 387 { 388 _dn = dn; 389 _label = label; 390 _hasChild = hasChild; 391 } 392 393 /** 394 * Get the dn 395 * @return the dn 396 */ 397 public String getDN() 398 { 399 return _dn; 400 } 401 402 /** 403 * Get the label 404 * @return the label 405 */ 406 public String getLabel() 407 { 408 return _label; 409 } 410 411 /** 412 * Has child 413 * @return true if has child 414 */ 415 public boolean hasChild() 416 { 417 return _hasChild; 418 } 419 } 420}