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