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}