001/* 002 * oauth2-oidc-sdk 003 * 004 * Copyright 2012-2020, Connect2id Ltd and contributors. 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use 007 * this file except in compliance with the License. You may obtain a copy of the 008 * License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software distributed 013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the 015 * specific language governing permissions and limitations under the License. 016 */ 017 018package com.nimbusds.openid.connect.sdk.federation.policy; 019 020 021import com.nimbusds.oauth2.sdk.ParseException; 022import com.nimbusds.oauth2.sdk.util.CollectionUtils; 023import com.nimbusds.oauth2.sdk.util.StringUtils; 024import com.nimbusds.openid.connect.sdk.federation.policy.language.OperationName; 025import com.nimbusds.openid.connect.sdk.federation.policy.language.PolicyOperation; 026import com.nimbusds.openid.connect.sdk.federation.policy.language.PolicyOperationApplication; 027import com.nimbusds.openid.connect.sdk.federation.policy.language.PolicyViolationException; 028import com.nimbusds.openid.connect.sdk.federation.policy.operations.*; 029import net.minidev.json.JSONObject; 030 031import java.util.LinkedHashMap; 032import java.util.LinkedList; 033import java.util.List; 034import java.util.Map; 035 036 037/** 038 * Policy entry for a metadata parameter. 039 * 040 * @see MetadataPolicy 041 * 042 * <p>Related specifications: 043 * 044 * <ul> 045 * <li>OpenID Connect Federation 1.0, section 5.1. 046 * </ul> 047 */ 048public class MetadataPolicyEntry implements Map.Entry<String, List<PolicyOperation>> { 049 050 051 /** 052 * The default policy operation factory. 053 */ 054 public final static PolicyOperationFactory DEFAULT_POLICY_OPERATION_FACTORY = new DefaultPolicyOperationFactory(); 055 056 057 /** 058 * The default policy operation combination validator. 059 */ 060 public final static PolicyOperationCombinationValidator DEFAULT_POLICY_COMBINATION_VALIDATOR = new DefaultPolicyOperationCombinationValidator(); 061 062 063 /** 064 * The parameter name. 065 */ 066 private final String parameterName; 067 068 069 /** 070 * The policy operations, empty list if none. 071 */ 072 private final List<PolicyOperation> policyOperations; 073 074 075 /** 076 * Creates a new policy entry for a metadata parameter. 077 * 078 * @param parameterName The parameter name. Must not be 079 * {@code null}. 080 * @param policyOperations The policy operations, empty list or 081 * {@code null} if none. 082 */ 083 public MetadataPolicyEntry(final String parameterName, final List<PolicyOperation> policyOperations) { 084 if (StringUtils.isBlank(parameterName)) { 085 throw new IllegalArgumentException("The parameter name must not be null or empty"); 086 } 087 this.parameterName = parameterName; 088 this.policyOperations = policyOperations; 089 } 090 091 092 /** 093 * Returns the parameter name. 094 * @see #getKey() 095 * 096 * @return The parameter name. 097 */ 098 public String getParameterName() { 099 return getKey(); 100 } 101 102 103 /** 104 * @see #getParameterName() 105 */ 106 @Override 107 public String getKey() { 108 return parameterName; 109 } 110 111 112 /** 113 * Returns the policy operations. 114 * @see #getValue() 115 * 116 * @return The policy operations, empty list if none. 117 */ 118 public List<PolicyOperation> getPolicyOperations() { 119 return getValue(); 120 } 121 122 123 /** 124 * @see #getPolicyOperations() 125 */ 126 @Override 127 public List<PolicyOperation> getValue() { 128 return policyOperations; 129 } 130 131 132 @Override 133 public List<PolicyOperation> setValue(final List<PolicyOperation> policyOperations) { 134 throw new UnsupportedOperationException(); 135 } 136 137 138 /** 139 * Returns a map of the operations for this policy entry, in their 140 * {@link StandardOperations#NAMES_IN_APPLICATION_ORDER standard 141 * execution order}. Non-standard (custom) policy operations will be 142 * put at the end, in no particular order between them. 143 * 144 * @return The map, empty if no operations. 145 */ 146 public Map<OperationName,PolicyOperation> getOperationsMap() { 147 148 Map<OperationName,PolicyOperation> map = new LinkedHashMap<>(); 149 150 if (getPolicyOperations() == null) { 151 return map; 152 } 153 154 for (OperationName opName: StandardOperations.NAMES_IN_APPLICATION_ORDER) { 155 for (PolicyOperation op: getPolicyOperations()) { 156 if (opName.equals(op.getOperationName())) { 157 map.put(opName, op); 158 } 159 } 160 } 161 162 // Append any custom operations 163 for (PolicyOperation op: getPolicyOperations()) { 164 if (! StandardOperations.NAMES_IN_APPLICATION_ORDER.contains(op.getOperationName())) { 165 map.put(op.getOperationName(), op); 166 } 167 } 168 169 return map; 170 } 171 172 173 /** 174 * Combines this policy entry with another one for the same parameter 175 * name. Uses the {@link DefaultPolicyOperationCombinationValidator 176 * default policy combination validator}. 177 * 178 * @param other The other policy entry. Must not be {@code null}. 179 * 180 * @return The new combined policy entry. 181 * 182 * @throws PolicyViolationException If the parameter names don't match 183 * or another violation was 184 * encountered. 185 */ 186 public MetadataPolicyEntry combine(final MetadataPolicyEntry other) 187 throws PolicyViolationException { 188 189 return combine(other, DEFAULT_POLICY_COMBINATION_VALIDATOR); 190 } 191 192 193 /** 194 * Combines this policy entry with another one for the same parameter 195 * name. 196 * 197 * @param other The other policy entry. Must not be 198 * {@code null}. 199 * @param combinationValidator The policy operation combination 200 * validator. Must not be {@code null}. 201 * 202 * @return The new combined policy entry. 203 * 204 * @throws PolicyViolationException If the parameter names don't match 205 * or another violation was 206 * encountered. 207 */ 208 public MetadataPolicyEntry combine(final MetadataPolicyEntry other, 209 final PolicyOperationCombinationValidator combinationValidator) 210 throws PolicyViolationException { 211 212 if (! getParameterName().equals(other.getParameterName())) { 213 throw new PolicyViolationException("The parameter name of the other policy doesn't match: " + other.getParameterName()); 214 } 215 216 List<PolicyOperation> combinedOperations = new LinkedList<>(); 217 218 Map<OperationName,PolicyOperation> en1Map = getOperationsMap(); 219 Map<OperationName,PolicyOperation> en2Map = other.getOperationsMap(); 220 221 // Copy operations not present in either 222 for (OperationName name: en1Map.keySet()) { 223 if (! en2Map.containsKey(name)) { 224 combinedOperations.add(en1Map.get(name)); 225 } 226 } 227 for (OperationName name: en2Map.keySet()) { 228 if (! en1Map.containsKey(name)) { 229 combinedOperations.add(en2Map.get(name)); 230 } 231 } 232 233 // Merge operations present in both entries 234 for (OperationName opName: en1Map.keySet()) { 235 if (en2Map.containsKey(opName)) { 236 PolicyOperation op1 = en1Map.get(opName); 237 combinedOperations.add(op1.merge(en2Map.get(opName))); 238 } 239 } 240 241 List<PolicyOperation> validatedOperations = combinationValidator.validate(combinedOperations); 242 243 return new MetadataPolicyEntry(getParameterName(), validatedOperations); 244 } 245 246 247 /** 248 * Applies this policy entry for a metadata parameter to the specified 249 * value. 250 * 251 * @param value The parameter value, {@code null} if not specified. 252 * 253 * @return The resulting value, can be {@code null}. 254 * 255 * @throws PolicyViolationException On a policy violation. 256 */ 257 public Object apply(final Object value) 258 throws PolicyViolationException { 259 260 if (CollectionUtils.isEmpty(getValue())) { 261 // no ops 262 return value; 263 } 264 265 // Apply policy operations in list 266 Object updatedValue = value; 267 268 boolean valueIsEssential = false; 269 270 for (Map.Entry<OperationName, PolicyOperation> en: getOperationsMap().entrySet()) { 271 272 if (value == null && ! valueIsEssential) { 273 274 if (OneOfOperation.NAME.equals(en.getKey()) 275 || 276 SubsetOfOperation.NAME.equals(en.getKey()) 277 || 278 SupersetOfOperation.NAME.equals(en.getKey()) 279 ) { 280 continue; // skip 281 } 282 } 283 284 updatedValue = PolicyOperationApplication.apply(en.getValue(), updatedValue); 285 286 if (ValueOperation.NAME.equals(en.getKey())) { 287 // Stop after "value" 288 return updatedValue; 289 } 290 291 if (EssentialOperation.NAME.equals(en.getKey())) { 292 293 if (((EssentialOperation)en.getValue()).getBooleanConfiguration()) { 294 valueIsEssential = true; 295 } else if (updatedValue == null) { 296 // Skip further value checks 297 return null; 298 } 299 } 300 301 if (SubsetOfOperation.NAME.equals(en.getKey()) && valueIsEssential) { 302 303 if (updatedValue == null) { 304 throw new PolicyViolationException("Essential parameter failed subset_of check"); 305 } 306 } 307 } 308 return updatedValue; 309 } 310 311 312 /** 313 * Returns a JSON object representation of the policy operations for 314 * this entry. 315 * 316 * @return The JSON object keeping the ordering of the members. 317 */ 318 public JSONObject toJSONObject() { 319 320 if (CollectionUtils.isEmpty(getValue())) { 321 return null; 322 } 323 324 JSONObject jsonObject = new JSONObject(); 325 for (PolicyOperation operation: getValue()) { 326 // E.g. "subset_of": ["code", "code token", "code id_token"]} 327 Map.Entry<String,Object> en = operation.toJSONObjectEntry(); 328 jsonObject.put(en.getKey(), en.getValue()); 329 } 330 331 return jsonObject; 332 } 333 334 335 /** 336 * Parses a policy entry for a metadata parameter. This method is 337 * intended for policies with standard {@link PolicyOperation}s only. 338 * Uses the default {@link DefaultPolicyOperationFactory policy 339 * operation} and {@link DefaultPolicyOperationCombinationValidator 340 * policy combination validator} factories. 341 * 342 * @param parameterName The parameter name. Must not be {@code null}. 343 * @param entrySpec The JSON object entry specification, must not 344 * be {@code null}. 345 * 346 * @return The policy entry for the metadata parameter. 347 * 348 * @throws ParseException On JSON parsing exception. 349 * @throws PolicyViolationException On a policy violation. 350 */ 351 public static MetadataPolicyEntry parse(final String parameterName, 352 final JSONObject entrySpec) 353 throws ParseException, PolicyViolationException { 354 355 return parse(parameterName, entrySpec, DEFAULT_POLICY_OPERATION_FACTORY, DEFAULT_POLICY_COMBINATION_VALIDATOR); 356 } 357 358 359 /** 360 * Parses a policy entry for a metadata parameter. This method is 361 * intended for policies including non-standard 362 * {@link PolicyOperation}s. 363 * 364 * @param parameterName The parameter name. Must not be 365 * {@code null}. 366 * @param entrySpec The JSON object entry specification, 367 * must not be {@code null}. 368 * @param factory The policy operation factory. Must not 369 * be {@code null}. 370 * @param combinationValidator The policy operation combination 371 * validator. Must not be {@code null}. 372 * 373 * @return The policy entry for the metadata parameter. 374 * 375 * @throws ParseException On JSON parsing exception. 376 * @throws PolicyViolationException On a policy violation. 377 */ 378 public static MetadataPolicyEntry parse(final String parameterName, 379 final JSONObject entrySpec, 380 final PolicyOperationFactory factory, 381 final PolicyOperationCombinationValidator combinationValidator) 382 throws ParseException, PolicyViolationException { 383 384 if (entrySpec == null) { 385 throw new IllegalArgumentException("The entry spec must not be null"); 386 } 387 388 List<PolicyOperation> policyOperations = new LinkedList<>(); 389 390 for (String opName: entrySpec.keySet()) { 391 PolicyOperation op = factory.createForName(new OperationName(opName)); 392 if (op == null) { 393 throw new PolicyViolationException("Unsupported policy operation: " + opName); 394 } 395 op.parseConfiguration(entrySpec.get(opName)); 396 policyOperations.add(op); 397 } 398 399 List<PolicyOperation> validatedPolicyOperations = combinationValidator.validate(policyOperations); 400 401 return new MetadataPolicyEntry(parameterName, validatedPolicyOperations); 402 } 403}