001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2013-2016 ForgeRock AS. 015 */ 016 017package org.forgerock.opendj.ldap; 018 019import static org.forgerock.opendj.ldap.Attributes.renameAttribute; 020 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.Iterator; 026import java.util.Map; 027import java.util.NoSuchElementException; 028 029import org.forgerock.opendj.ldap.schema.AttributeType; 030import org.forgerock.opendj.ldap.schema.ObjectClass; 031import org.forgerock.opendj.ldap.schema.Schema; 032 033import com.forgerock.opendj.util.Iterables; 034 035/** 036 * A configurable factory for filtering the attributes exposed by an entry. An 037 * {@code AttributeFilter} is useful for performing fine-grained access control, 038 * selecting attributes based on search request criteria, and selecting 039 * attributes based on post- and pre- read request control criteria. 040 * <p> 041 * In cases where methods accept a string based list of attribute descriptions, 042 * the following special attribute descriptions are permitted: 043 * <ul> 044 * <li><b>*</b> - include all user attributes 045 * <li><b>+</b> - include all operational attributes 046 * <li><b>1.1</b> - exclude all attributes 047 * <li><b>@<i>objectclass</i></b> - include all attributes identified by the 048 * named object class. 049 * </ul> 050 */ 051public final class AttributeFilter { 052 // TODO: exclude specific attributes, matched values, custom predicates, etc. 053 private boolean includeAllOperationalAttributes; 054 /** Depends on constructor. */ 055 private boolean includeAllUserAttributes; 056 private boolean typesOnly; 057 058 /** 059 * Use a map so that we can perform membership checks as well as recover the 060 * user requested attribute description. 061 */ 062 private Map<AttributeDescription, AttributeDescription> requestedAttributes = Collections 063 .emptyMap(); 064 065 /** 066 * Creates a new attribute filter which will include all user attributes but 067 * no operational attributes. 068 */ 069 public AttributeFilter() { 070 includeAllUserAttributes = true; 071 } 072 073 /** 074 * Creates a new attribute filter which will include the attributes 075 * identified by the provided search request attribute list. Attributes will 076 * be decoded using the default schema. See the class description for 077 * details regarding the types of supported attribute description. 078 * 079 * @param attributeDescriptions 080 * The names of the attributes to be included with each entry. 081 */ 082 public AttributeFilter(final Collection<String> attributeDescriptions) { 083 this(attributeDescriptions, Schema.getDefaultSchema()); 084 } 085 086 /** 087 * Creates a new attribute filter which will include the attributes 088 * identified by the provided search request attribute list. Attributes will 089 * be decoded using the provided schema. See the class description for 090 * details regarding the types of supported attribute description. 091 * 092 * @param attributeDescriptions 093 * The names of the attributes to be included with each entry. 094 * @param schema 095 * The schema The schema to use when parsing attribute 096 * descriptions and object class names. 097 */ 098 public AttributeFilter(final Collection<String> attributeDescriptions, final Schema schema) { 099 if (attributeDescriptions == null || attributeDescriptions.isEmpty()) { 100 // Fast-path for common case. 101 includeAllUserAttributes = true; 102 } else { 103 for (final String attribute : attributeDescriptions) { 104 includeAttribute(attribute, schema); 105 } 106 } 107 } 108 109 /** 110 * Creates a new attribute filter which will include the attributes 111 * identified by the provided search request attribute list. Attributes will 112 * be decoded using the default schema. See the class description for 113 * details regarding the types of supported attribute description. 114 * 115 * @param attributeDescriptions 116 * The names of the attributes to be included with each entry. 117 */ 118 public AttributeFilter(final String... attributeDescriptions) { 119 this(Arrays.asList(attributeDescriptions)); 120 } 121 122 /** 123 * Returns a modifiable filtered copy of the provided entry. 124 * 125 * @param entry 126 * The entry to be filtered and copied. 127 * @return The modifiable filtered copy of the provided entry. 128 */ 129 public Entry filteredCopyOf(final Entry entry) { 130 return new LinkedHashMapEntry(filteredViewOf(entry)); 131 } 132 133 /** 134 * Returns an unmodifiable filtered view of the provided entry. The returned 135 * entry supports all operations except those which modify the contents of 136 * the entry. 137 * 138 * @param entry 139 * The entry to be filtered. 140 * @return The unmodifiable filtered view of the provided entry. 141 */ 142 public Entry filteredViewOf(final Entry entry) { 143 return new AbstractEntry() { 144 145 @Override 146 public boolean addAttribute(final Attribute attribute, 147 final Collection<? super ByteString> duplicateValues) { 148 throw new UnsupportedOperationException(); 149 } 150 151 @Override 152 public Entry clearAttributes() { 153 throw new UnsupportedOperationException(); 154 } 155 156 @Override 157 public Iterable<Attribute> getAllAttributes() { 158 /* 159 * Unfortunately we cannot efficiently re-use the iterators in 160 * {@code Iterators} because we need to transform and filter in 161 * a single step. Transformation is required in order to ensure 162 * that we return an attribute whose name is the same as the one 163 * requested by the user. 164 */ 165 return new Iterable<Attribute>() { 166 private boolean hasNextMustIterate = true; 167 private final Iterator<Attribute> iterator = entry.getAllAttributes().iterator(); 168 private Attribute next = null; 169 170 @Override 171 public Iterator<Attribute> iterator() { 172 return new Iterator<Attribute>() { 173 @Override 174 public boolean hasNext() { 175 if (hasNextMustIterate) { 176 hasNextMustIterate = false; 177 while (iterator.hasNext()) { 178 final Attribute attribute = iterator.next(); 179 final AttributeDescription ad = attribute.getAttributeDescription(); 180 final AttributeType at = ad.getAttributeType(); 181 final AttributeDescription requestedAd = requestedAttributes.get(ad); 182 if (requestedAd != null) { 183 next = renameAttribute(attribute, requestedAd); 184 return true; 185 } else if ((at.isOperational() && includeAllOperationalAttributes) 186 || (!at.isOperational() && includeAllUserAttributes)) { 187 next = attribute; 188 return true; 189 } 190 } 191 next = null; 192 return false; 193 } else { 194 return next != null; 195 } 196 } 197 198 @Override 199 public Attribute next() { 200 if (!hasNext()) { 201 throw new NoSuchElementException(); 202 } 203 hasNextMustIterate = true; 204 return filterAttribute(next); 205 } 206 207 @Override 208 public void remove() { 209 throw new UnsupportedOperationException(); 210 } 211 }; 212 } 213 214 @Override 215 public String toString() { 216 return Iterables.toString(this); 217 } 218 }; 219 } 220 221 @Override 222 public Attribute getAttribute(final AttributeDescription attributeDescription) { 223 /* 224 * It is tempting to filter based on the passed in attribute 225 * description, but we may get inaccurate results due to 226 * placeholder attribute names. 227 */ 228 final Attribute attribute = entry.getAttribute(attributeDescription); 229 if (attribute != null) { 230 final AttributeDescription ad = attribute.getAttributeDescription(); 231 final AttributeType at = ad.getAttributeType(); 232 final AttributeDescription requestedAd = requestedAttributes.get(ad); 233 if (requestedAd != null) { 234 return filterAttribute(renameAttribute(attribute, requestedAd)); 235 } else if ((at.isOperational() && includeAllOperationalAttributes) 236 || (!at.isOperational() && includeAllUserAttributes)) { 237 return filterAttribute(attribute); 238 } 239 } 240 return null; 241 } 242 243 @Override 244 public int getAttributeCount() { 245 return Iterables.size(getAllAttributes()); 246 } 247 248 @Override 249 public DN getName() { 250 return entry.getName(); 251 } 252 253 @Override 254 public Entry setName(final DN dn) { 255 throw new UnsupportedOperationException(); 256 } 257 }; 258 } 259 260 /** 261 * Specifies whether all operational attributes should be included in 262 * filtered entries. By default operational attributes are not included. 263 * 264 * @param include 265 * {@code true} if operational attributes should be included in 266 * filtered entries. 267 * @return A reference to this attribute filter. 268 */ 269 public AttributeFilter includeAllOperationalAttributes(final boolean include) { 270 this.includeAllOperationalAttributes = include; 271 return this; 272 } 273 274 /** 275 * Specifies whether all user attributes should be included in 276 * filtered entries. By default user attributes are included. 277 * 278 * @param include 279 * {@code true} if user attributes should be included in filtered 280 * entries. 281 * @return A reference to this attribute filter. 282 */ 283 public AttributeFilter includeAllUserAttributes(final boolean include) { 284 this.includeAllUserAttributes = include; 285 return this; 286 } 287 288 /** 289 * Specifies that the named attribute should be included in filtered 290 * entries. 291 * 292 * @param attributeDescription 293 * The name of the attribute to be included in filtered entries. 294 * @return A reference to this attribute filter. 295 */ 296 public AttributeFilter includeAttribute(final AttributeDescription attributeDescription) { 297 allocatedRequestedAttributes(); 298 requestedAttributes.put(attributeDescription, attributeDescription); 299 return this; 300 } 301 302 /** 303 * Specifies that the named attribute should be included in filtered 304 * entries. The attribute will be decoded using the default schema. See the 305 * class description for details regarding the types of supported attribute 306 * description. 307 * 308 * @param attributeDescription 309 * The name of the attribute to be included in filtered entries. 310 * @return A reference to this attribute filter. 311 */ 312 public AttributeFilter includeAttribute(final String attributeDescription) { 313 return includeAttribute(attributeDescription, Schema.getDefaultSchema()); 314 } 315 316 /** 317 * Specifies that the named attribute should be included in filtered 318 * entries. The attribute will be decoded using the provided schema. See the 319 * class description for details regarding the types of supported attribute 320 * description. 321 * 322 * @param attributeDescription 323 * The name of the attribute to be included in filtered entries. 324 * @param schema 325 * The schema The schema to use when parsing attribute 326 * descriptions and object class names. 327 * @return A reference to this attribute filter. 328 */ 329 public AttributeFilter includeAttribute(final String attributeDescription, final Schema schema) { 330 if (attributeDescription.equals("*")) { 331 includeAllUserAttributes = true; 332 } else if (attributeDescription.equals("+")) { 333 includeAllOperationalAttributes = true; 334 } else if (attributeDescription.equals("1.1")) { 335 // Ignore - by default no attributes are included. 336 } else if (attributeDescription.startsWith("@") && attributeDescription.length() > 1) { 337 final String objectClassName = attributeDescription.substring(1); 338 final ObjectClass objectClass = schema.getObjectClass(objectClassName); 339 if (!objectClass.isPlaceHolder()) { 340 allocatedRequestedAttributes(); 341 for (final AttributeType at : objectClass.getRequiredAttributes()) { 342 final AttributeDescription ad = AttributeDescription.create(at); 343 requestedAttributes.put(ad, ad); 344 } 345 for (final AttributeType at : objectClass.getOptionalAttributes()) { 346 final AttributeDescription ad = AttributeDescription.create(at); 347 requestedAttributes.put(ad, ad); 348 } 349 } 350 } else { 351 allocatedRequestedAttributes(); 352 final AttributeDescription ad = 353 AttributeDescription.valueOf(attributeDescription, schema); 354 requestedAttributes.put(ad, ad); 355 } 356 return this; 357 } 358 359 @Override 360 public String toString() { 361 if (!includeAllOperationalAttributes 362 && !includeAllUserAttributes 363 && requestedAttributes.isEmpty()) { 364 return "1.1"; 365 } 366 367 final StringBuilder builder = new StringBuilder(); 368 if (includeAllUserAttributes) { 369 builder.append('*'); 370 } 371 if (includeAllOperationalAttributes) { 372 if (builder.length() > 0) { 373 builder.append(", "); 374 } 375 builder.append('+'); 376 } 377 for (final AttributeDescription requestedAttribute : requestedAttributes.keySet()) { 378 if (builder.length() > 0) { 379 builder.append(", "); 380 } 381 builder.append(requestedAttribute); 382 } 383 return builder.toString(); 384 } 385 386 /** 387 * Specifies whether filtered attributes are to contain both 388 * attribute descriptions and values, or just attribute descriptions. 389 * 390 * @param typesOnly 391 * {@code true} if only attribute descriptions (and not values) 392 * are to be included, or {@code false} (the default) if both 393 * attribute descriptions and values are to be included. 394 * @return A reference to this attribute filter. 395 */ 396 public AttributeFilter typesOnly(final boolean typesOnly) { 397 this.typesOnly = typesOnly; 398 return this; 399 } 400 401 private void allocatedRequestedAttributes() { 402 if (requestedAttributes.isEmpty()) { 403 requestedAttributes = new HashMap<>(); 404 } 405 } 406 407 private Attribute filterAttribute(final Attribute attribute) { 408 return typesOnly 409 ? Attributes.emptyAttribute(attribute.getAttributeDescription()) 410 : attribute; 411 } 412}