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 2016 ForgeRock AS. 015 * Portions Copyright 2022 Wren Security. 016 */ 017package org.forgerock.opendj.rest2ldap.schema; 018 019import static com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_COMMENTS; 020import static com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_SINGLE_QUOTES; 021import static com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS; 022import static java.util.Collections.emptyList; 023import static org.forgerock.opendj.ldap.schema.Schema.getCoreSchema; 024import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_JSON_IO_ERROR; 025import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_JSON_PARSE_ERROR; 026import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.ValidationPolicy.LENIENT; 027import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.ValidationPolicy.STRICT; 028import static org.forgerock.util.Options.defaultOptions; 029 030import java.io.IOException; 031import java.io.InputStream; 032import java.util.Collection; 033 034import org.forgerock.i18n.LocalizableMessage; 035import org.forgerock.i18n.LocalizedIllegalArgumentException; 036import org.forgerock.opendj.ldap.ByteString; 037import org.forgerock.opendj.ldap.schema.MatchingRule; 038import org.forgerock.opendj.ldap.schema.MatchingRuleImpl; 039import org.forgerock.opendj.ldap.schema.Schema; 040import org.forgerock.opendj.ldap.schema.SchemaBuilder; 041import org.forgerock.opendj.ldap.schema.Syntax; 042import org.forgerock.util.Function; 043import org.forgerock.util.Option; 044import org.forgerock.util.Options; 045 046import com.fasterxml.jackson.core.JsonFactory; 047import com.fasterxml.jackson.core.JsonProcessingException; 048import com.fasterxml.jackson.databind.ObjectMapper; 049 050/** 051 * Utility methods for obtaining JSON syntaxes and matching rules. See the package documentation for more detail. 052 */ 053public final class JsonSchema { 054 /** JSON value validation policies. */ 055 public enum ValidationPolicy { 056 /** JSON validation policy requiring strict conformance to RFC 7159. */ 057 STRICT(new ObjectMapper()), 058 /** 059 * JSON validation policy requiring conformance to RFC 7159 with the following exceptions: 1) comments are 060 * allowed, 2) single quotes may be used instead of double quotes, and 3) unquoted control characters are 061 * allowed in strings. 062 */ 063 LENIENT(new ObjectMapper().enable(ALLOW_COMMENTS) 064 .enable(ALLOW_SINGLE_QUOTES) 065 .enable(ALLOW_UNQUOTED_CONTROL_CHARS)), 066 /** JSON validation policy which does not perform any validation. */ 067 DISABLED(null); 068 069 private final ObjectMapper objectMapper; 070 071 ValidationPolicy(final ObjectMapper objectMapper) { 072 this.objectMapper = objectMapper; 073 } 074 075 final JsonFactory getJsonFactory() { 076 return objectMapper.getFactory(); 077 } 078 079 final ObjectMapper getObjectMapper() { 080 return objectMapper; 081 } 082 } 083 084 /** 085 * Schema option controlling syntax validation for JSON based attributes. By default this compatibility option 086 * is set to {@link ValidationPolicy#STRICT}. 087 */ 088 public static final Option<ValidationPolicy> VALIDATION_POLICY = Option.withDefault(STRICT); 089 /** 090 * Matching rule option controlling whether JSON string comparisons should be case-sensitive. By default this 091 * compatibility option is set to {@code false} meaning that case will be ignored. 092 * <p> 093 * This option must be provided when constructing a JSON matching rule using {@link 094 * #newJsonQueryEqualityMatchingRuleImpl}, and cannot be overridden at the schema level. 095 */ 096 public static final Option<Boolean> CASE_SENSITIVE_STRINGS = Option.withDefault(false); 097 /** 098 * Matching rule option controlling whether JSON string comparisons should ignore white-space. By default this 099 * compatibility option is set to {@code true} meaning that leading and trailing white-space will be ignored and 100 * intermediate white-space will be reduced to a single white-space character. 101 * <p> 102 * This option must be provided when constructing a JSON matching rule using {@link 103 * #newJsonQueryEqualityMatchingRuleImpl}, and cannot be overridden at the schema level. 104 */ 105 public static final Option<Boolean> IGNORE_WHITE_SPACE = Option.withDefault(true); 106 /** 107 * Matching rule option controlling which JSON fields should be indexed by the matching rule. By default all 108 * fields will be indexed. To restrict the set of indexed fields specify a list whose values are wild-card 109 * patterns for matching against JSON pointers. Patterns are JSON pointers where "*" represents zero or more 110 * characters in a single path element, and "**" represents any number of path elements. For example: 111 * 112 * <table valign="top"> 113 * <caption></caption> 114 * <tr><th>Pattern</th><th>Matches</th><th>Doesn't match</th></tr> 115 * <tr><td>/aaa/bbb/ccc</td><td>/aaa/bbb/ccc</td><td>/aaa/bbb/ccc/ddd<br/>/aaa/bbb/cccc</td></tr> 116 * <tr><td>/aaa/b*/ccc</td><td>/aaa/bbb/ccc<br/>/aaa/bxx/ccc</td><td>/aaa/xxx/ccc<br/>/aaa/bbb</td></tr> 117 * <tr><td>/aaa/**/ddd</td><td>/aaa/ddd<br/>/aaa/xxx/yyy/ddd</td><td>/aaa/bbb/ccc</td></tr> 118 * <tr><td>/aaa/**</td><td>/aaa<br/>/aaa/bbb<br/>/aaa/bbb/ccc<br/></td><td>/aa</td></tr> 119 * </table> 120 */ 121 @SuppressWarnings("unchecked") 122 public static final Option<Collection<String>> INDEXED_FIELD_PATTERNS = 123 (Option) Option.of(Collection.class, emptyList()); 124 /** The OID of the JSON attribute syntax. */ 125 static final String SYNTAX_JSON_OID = "1.3.6.1.4.1.36733.2.1.3.1"; 126 /** The description of the JSON attribute syntax. */ 127 static final String SYNTAX_JSON_DESCRIPTION = "Json"; 128 /** The OID of the JSON query attribute syntax. */ 129 static final String SYNTAX_JSON_QUERY_OID = "1.3.6.1.4.1.36733.2.1.3.2"; 130 /** The description of the JSON query attribute syntax. */ 131 static final String SYNTAX_JSON_QUERY_DESCRIPTION = "Json Query"; 132 /** The OID of the case insensitive JSON query equality matching rule. */ 133 static final String EMR_CASE_IGNORE_JSON_QUERY_OID = "1.3.6.1.4.1.36733.2.1.4.1"; 134 /** The name of the case insensitive JSON query equality matching rule. */ 135 static final String EMR_CASE_IGNORE_JSON_QUERY_NAME = "caseIgnoreJsonQueryMatch"; 136 /** The OID of the case sensitive JSON query equality matching rule. */ 137 static final String EMR_CASE_EXACT_JSON_QUERY_OID = "1.3.6.1.4.1.36733.2.1.4.2"; 138 /** The name of the case sensitive JSON query equality matching rule. */ 139 static final String EMR_CASE_EXACT_JSON_QUERY_NAME = "caseExactJsonQueryMatch"; 140 private static final Syntax JSON_SYNTAX; 141 private static final Syntax JSON_QUERY_SYNTAX; 142 private static final MatchingRule CASE_IGNORE_JSON_QUERY_MATCHING_RULE; 143 private static final MatchingRule CASE_EXACT_JSON_QUERY_MATCHING_RULE; 144 private static final Function<ByteString, Object, LocalizedIllegalArgumentException> BYTESTRING_TO_JSON = 145 new Function<ByteString, Object, LocalizedIllegalArgumentException>() { 146 @Override 147 public Object apply(final ByteString value) { 148 try (final InputStream inputStream = value.asReader().asInputStream()) { 149 return LENIENT.getObjectMapper().readValue(inputStream, Object.class); 150 } catch (final IOException e) { 151 throw new LocalizedIllegalArgumentException(jsonParsingException(e)); 152 } 153 } 154 }; 155 156 static LocalizableMessage jsonParsingException(final IOException e) { 157 if (e instanceof JsonProcessingException) { 158 final JsonProcessingException jpe = (JsonProcessingException) e; 159 if (jpe.getLocation() != null) { 160 return ERR_JSON_PARSE_ERROR.get(jpe.getLocation().getLineNr(), 161 jpe.getLocation().getColumnNr(), 162 jpe.getOriginalMessage()); 163 } 164 } 165 return ERR_JSON_IO_ERROR.get(e.getMessage()); 166 } 167 168 private static final Function<Object, ByteString, JsonProcessingException> JSON_TO_BYTESTRING = 169 new Function<Object, ByteString, JsonProcessingException>() { 170 @Override 171 public ByteString apply(final Object value) throws JsonProcessingException { 172 return ByteString.wrap(LENIENT.getObjectMapper().writeValueAsBytes(value)); 173 } 174 }; 175 176 static { 177 final Schema schema = addJsonSyntaxesAndMatchingRulesToSchema(new SchemaBuilder(getCoreSchema())).toSchema(); 178 JSON_SYNTAX = schema.getSyntax(SYNTAX_JSON_OID); 179 JSON_QUERY_SYNTAX = schema.getSyntax(SYNTAX_JSON_QUERY_OID); 180 CASE_IGNORE_JSON_QUERY_MATCHING_RULE = schema.getMatchingRule(EMR_CASE_IGNORE_JSON_QUERY_OID); 181 CASE_EXACT_JSON_QUERY_MATCHING_RULE = schema.getMatchingRule(EMR_CASE_EXACT_JSON_QUERY_OID); 182 } 183 184 /** 185 * Returns the JSON attribute syntax having the OID 1.3.6.1.4.1.36733.2.1.3.1. Attribute values of this syntax 186 * must be valid JSON. Use the {@link #VALIDATION_POLICY} schema option to control the degree of syntax 187 * enforcement. By default JSON attributes will support equality matching using the 188 * {@link #getCaseIgnoreJsonQueryMatchingRule() jsonQueryMatch} matching rule, although this may be overridden 189 * when defining individual attribute types. 190 * 191 * @return The JSON attribute syntax having the OID 1.3.6.1.4.1.36733.2.1.3.1. 192 */ 193 public static Syntax getJsonSyntax() { 194 return JSON_SYNTAX; 195 } 196 197 /** 198 * Returns the JSON Query attribute syntax having the OID 1.3.6.1.4.1.36733.2.1.3.2. Attribute values of this 199 * syntax must be valid CREST JSON {@link org.forgerock.util.query.QueryFilter query filter} strings as 200 * defined in {@link org.forgerock.util.query.QueryFilterParser}. 201 * 202 * @return The JSON Query attribute syntax having the OID 1.3.6.1.4.1.36733.2.1.3.2. 203 */ 204 public static Syntax getJsonQuerySyntax() { 205 return JSON_QUERY_SYNTAX; 206 } 207 208 /** 209 * Returns the {@code jsonQueryMatch} matching rule having the OID 1.3.6.1.4.1.36733.2.1.4.1. The 210 * matching rule's assertion syntax is a {@link #getJsonQuerySyntax() CREST JSON query filter}. This matching 211 * rule will ignore case when comparing JSON strings as well as ignoring white-space. In addition, all JSON 212 * fields will be indexed if indexing is enabled. 213 * 214 * @return The @code jsonQueryMatch} matching rule having the OID 1.3.6.1.4.1.36733.2.1.4.1. 215 */ 216 public static MatchingRule getCaseIgnoreJsonQueryMatchingRule() { 217 return CASE_IGNORE_JSON_QUERY_MATCHING_RULE; 218 } 219 220 /** 221 * Returns the {@code jsonQueryMatch} matching rule having the OID 1.3.6.1.4.1.36733.2.1.4.2. The 222 * matching rule's assertion syntax is a {@link #getJsonQuerySyntax() CREST JSON query filter}. This matching 223 * rule will ignore case when comparing JSON strings as well as ignoring white-space. In addition, all JSON 224 * fields will be indexed if indexing is enabled. 225 * 226 * @return The @code jsonQueryMatch} matching rule having the OID 1.3.6.1.4.1.36733.2.1.4.2. 227 */ 228 public static MatchingRule getCaseExactJsonQueryMatchingRule() { 229 return CASE_EXACT_JSON_QUERY_MATCHING_RULE; 230 } 231 232 /** 233 * Creates a new custom JSON query equality matching rule implementation with the provided matching rule name and 234 * options. This method should be used when creating custom JSON matching rules whose behavior differs from 235 * {@link #getCaseIgnoreJsonQueryMatchingRule()}. 236 * 237 * @param matchingRuleName 238 * The name of the matching rule. This will be used as the index ID in attribute indexes so it must not 239 * collide with other indexes identifiers. 240 * @param options 241 * The options controlling the behavior of the matching rule. 242 * @return The new custom JSON query equality matching rule implementation. 243 * @see #CASE_SENSITIVE_STRINGS 244 * @see #IGNORE_WHITE_SPACE 245 */ 246 public static MatchingRuleImpl newJsonQueryEqualityMatchingRuleImpl(final String matchingRuleName, 247 final Options options) { 248 return new JsonQueryEqualityMatchingRuleImpl(matchingRuleName, options); 249 } 250 251 /** 252 * Adds the syntaxes and matching rules required by for JSON attribute support to the provided schema builder. 253 * 254 * @param builder 255 * The schema builder to which the schema elements should be added. 256 * @return The schema builder. 257 */ 258 public static SchemaBuilder addJsonSyntaxesAndMatchingRulesToSchema(final SchemaBuilder builder) { 259 builder.buildSyntax(SYNTAX_JSON_OID) 260 .description(SYNTAX_JSON_DESCRIPTION) 261 .implementation(new JsonSyntaxImpl()) 262 .extraProperties("X-ORIGIN", "OpenDJ Directory Server") 263 .addToSchema(); 264 265 builder.buildSyntax(SYNTAX_JSON_QUERY_OID) 266 .description(SYNTAX_JSON_QUERY_DESCRIPTION) 267 .implementation(new JsonQuerySyntaxImpl()) 268 .extraProperties("X-ORIGIN", "OpenDJ Directory Server") 269 .addToSchema(); 270 271 final JsonQueryEqualityMatchingRuleImpl caseIgnoreImpl = new JsonQueryEqualityMatchingRuleImpl( 272 EMR_CASE_IGNORE_JSON_QUERY_NAME, 273 defaultOptions().set(CASE_SENSITIVE_STRINGS, false).set(IGNORE_WHITE_SPACE, true)); 274 builder.buildMatchingRule(EMR_CASE_IGNORE_JSON_QUERY_OID) 275 .names(EMR_CASE_IGNORE_JSON_QUERY_NAME) 276 .syntaxOID(SYNTAX_JSON_QUERY_OID) 277 .extraProperties("X-ORIGIN", "OpenDJ Directory Server") 278 .implementation(caseIgnoreImpl) 279 .addToSchema(); 280 281 final JsonQueryEqualityMatchingRuleImpl caseExactImpl = new JsonQueryEqualityMatchingRuleImpl( 282 EMR_CASE_EXACT_JSON_QUERY_NAME, 283 defaultOptions().set(CASE_SENSITIVE_STRINGS, true).set(IGNORE_WHITE_SPACE, true)); 284 builder.buildMatchingRule(EMR_CASE_EXACT_JSON_QUERY_OID) 285 .names(EMR_CASE_EXACT_JSON_QUERY_NAME) 286 .syntaxOID(SYNTAX_JSON_QUERY_OID) 287 .extraProperties("X-ORIGIN", "OpenDJ Directory Server") 288 .implementation(caseExactImpl) 289 .addToSchema(); 290 291 return builder; 292 } 293 294 /** 295 * Returns a function which parses {@code JSON} values. Invalid values will result in a 296 * {@code LocalizedIllegalArgumentException}. 297 * 298 * @return A function which parses {@code JSON} values. 299 */ 300 public static Function<ByteString, Object, LocalizedIllegalArgumentException> byteStringToJson() { 301 return BYTESTRING_TO_JSON; 302 } 303 304 /** 305 * Returns a function which converts a JSON {@code Object} to a {@code ByteString}. 306 * 307 * @return A function which converts a JSON {@code Object} to a {@code ByteString}. 308 */ 309 public static Function<Object, ByteString, JsonProcessingException> jsonToByteString() { 310 return JSON_TO_BYTESTRING; 311 } 312 313 private JsonSchema() { 314 // Prevent instantiation. 315 } 316}