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 java.util.HashMap;
022import java.util.LinkedList;
023import java.util.List;
024import java.util.Map;
025
026import net.minidev.json.JSONObject;
027
028import com.nimbusds.oauth2.sdk.ParseException;
029import com.nimbusds.oauth2.sdk.util.CollectionUtils;
030import com.nimbusds.oauth2.sdk.util.StringUtils;
031import com.nimbusds.openid.connect.sdk.federation.policy.language.OperationName;
032import com.nimbusds.openid.connect.sdk.federation.policy.language.PolicyOperation;
033import com.nimbusds.openid.connect.sdk.federation.policy.language.PolicyOperationApplication;
034import com.nimbusds.openid.connect.sdk.federation.policy.language.PolicyViolationException;
035import com.nimbusds.openid.connect.sdk.federation.policy.operations.DefaultPolicyOperationCombinationValidator;
036import com.nimbusds.openid.connect.sdk.federation.policy.operations.DefaultPolicyOperationFactory;
037import com.nimbusds.openid.connect.sdk.federation.policy.operations.PolicyOperationCombinationValidator;
038import com.nimbusds.openid.connect.sdk.federation.policy.operations.PolicyOperationFactory;
039
040
041/**
042 * Policy entry for a metadata parameter.
043 *
044 * @see MetadataPolicy
045 *
046 * <p>Related specifications:
047 *
048 * <ul>
049 *     <li>OpenID Connect Federation 1.0, section 4.1.
050 * </ul>
051 */
052public class MetadataPolicyEntry implements Map.Entry<String, List<PolicyOperation>> {
053        
054        
055        /**
056         * The default policy operation factory.
057         */
058        public final static PolicyOperationFactory DEFAULT_POLICY_OPERATION_FACTORY = new DefaultPolicyOperationFactory();
059        
060        
061        /**
062         * The default policy operation combination validator.
063         */
064        public final static PolicyOperationCombinationValidator DEFAULT_POLICY_COMBINATION_VALIDATOR = new DefaultPolicyOperationCombinationValidator();
065        
066        
067        /**
068         * The parameter name.
069         */
070        private final String parameterName;
071        
072        
073        /**
074         * The policy operations, empty list if none.
075         */
076        private final List<PolicyOperation> policyOperations;
077        
078        
079        /**
080         * Creates a new policy entry for a metadata parameter.
081         *
082         * @param parameterName    The parameter name. Must not be
083         *                         {@code null}.
084         * @param policyOperations The policy operations, empty list or
085         *                         {@code null} if none.
086         */
087        public MetadataPolicyEntry(final String parameterName, final List<PolicyOperation> policyOperations) {
088                if (StringUtils.isBlank(parameterName)) {
089                        throw new IllegalArgumentException("The parameter name must not be null or empty");
090                }
091                this.parameterName = parameterName;
092                this.policyOperations = policyOperations;
093        }
094        
095        
096        /**
097         * Returns the parameter name.
098         * @see #getKey()
099         *
100         * @return The parameter name.
101         */
102        public String getParameterName() {
103                return getKey();
104        }
105        
106        
107        /**
108         * @see #getParameterName()
109         */
110        @Override
111        public String getKey() {
112                return parameterName;
113        }
114        
115        
116        /**
117         * Returns the policy operations.
118         * @see #getValue()
119         *
120         * @return The policy operations, empty list if none.
121         */
122        public List<PolicyOperation> getPolicyOperations() {
123                return getValue();
124        }
125        
126        
127        /**
128         * @see #getPolicyOperations()
129         */
130        @Override
131        public List<PolicyOperation> getValue() {
132                return policyOperations;
133        }
134        
135        
136        @Override
137        public List<PolicyOperation> setValue(final List<PolicyOperation> policyOperations) {
138                throw new UnsupportedOperationException();
139        }
140        
141        
142        /**
143         * Returns a map of the operations for this policy entry.
144         *
145         * @return The map, empty if no operations.
146         */
147        public Map<OperationName,PolicyOperation> getOperationsMap() {
148                
149                Map<OperationName,PolicyOperation> map = new HashMap<>();
150                
151                if (getPolicyOperations() == null) {
152                        return map;
153                }
154                
155                for (PolicyOperation op: getPolicyOperations()) {
156                        map.put(op.getOperationName(), op);
157                }
158                
159                return map;
160        }
161        
162        
163        /**
164         * Combines this policy entry with another one for the same parameter
165         * name. Uses the {@link DefaultPolicyOperationCombinationValidator
166         * default policy combination validator}.
167         *
168         * @param other The other policy entry. Must not be {@code null}.
169         *
170         * @return The new combined policy entry.
171         *
172         * @throws PolicyViolationException If the parameter names don't match
173         *                                  or another violation was
174         *                                  encountered.
175         */
176        public MetadataPolicyEntry combine(final MetadataPolicyEntry other)
177                throws PolicyViolationException {
178                
179                return combine(other, DEFAULT_POLICY_COMBINATION_VALIDATOR);
180        }
181        
182        
183        /**
184         * Combines this policy entry with another one for the same parameter
185         * name.
186         *
187         * @param other                The other policy entry. Must not be
188         *                             {@code null}.
189         * @param combinationValidator The policy operation combination
190         *                             validator. Must not be {@code null}.
191         *
192         * @return The new combined policy entry.
193         *
194         * @throws PolicyViolationException If the parameter names don't match
195         *                                  or another violation was
196         *                                  encountered.
197         */
198        public MetadataPolicyEntry combine(final MetadataPolicyEntry other,
199                                           final PolicyOperationCombinationValidator combinationValidator)
200                throws PolicyViolationException {
201                
202                if (! getParameterName().equals(other.getParameterName())) {
203                        throw new PolicyViolationException("The parameter name of the other policy doesn't match: " + other.getParameterName());
204                }
205                
206                List<PolicyOperation> combinedOperations = new LinkedList<>();
207                
208                Map<OperationName,PolicyOperation> en1Map = getOperationsMap();
209                Map<OperationName,PolicyOperation> en2Map = other.getOperationsMap();
210                
211                // Copy operations not present in either
212                for (OperationName name: en1Map.keySet()) {
213                        if (! en2Map.containsKey(name)) {
214                                combinedOperations.add(en1Map.get(name));
215                        }
216                }
217                for (OperationName name: en2Map.keySet()) {
218                        if (! en1Map.containsKey(name)) {
219                                combinedOperations.add(en2Map.get(name));
220                        }
221                }
222                
223                // Merge operations present in both entries
224                for (OperationName opName: en1Map.keySet()) {
225                        if (en2Map.containsKey(opName)) {
226                                PolicyOperation op1 = en1Map.get(opName);
227                                combinedOperations.add(op1.merge(en2Map.get(opName)));
228                        }
229                }
230                
231                List<PolicyOperation> validatedOperations = combinationValidator.validate(combinedOperations);
232                
233                return new MetadataPolicyEntry(getParameterName(), validatedOperations);
234        }
235        
236        
237        /**
238         * Applies this policy entry for a metadata parameter to the specified
239         * value.
240         *
241         * @param value The parameter value, {@code null} if not specified.
242         *
243         * @return The resulting value, can be {@code null}.
244         *
245         * @throws PolicyViolationException On a policy violation.
246         */
247        public Object apply(final Object value)
248                throws PolicyViolationException {
249                
250                if (CollectionUtils.isEmpty(getValue())) {
251                        // no ops
252                        return value;
253                }
254                
255                // Apply policy operations in list
256                Object updatedValue = value;
257                for (PolicyOperation op: getValue()) {
258                        updatedValue = PolicyOperationApplication.apply(op, updatedValue);
259                }
260                return updatedValue;
261        }
262        
263        
264        /**
265         * Returns a JSON object representation of the policy operations for
266         * this entry.
267         *
268         * @return The JSON object keeping the ordering of the members.
269         */
270        public JSONObject toJSONObject() {
271                
272                if (CollectionUtils.isEmpty(getValue())) {
273                        return null;
274                }
275                
276                JSONObject jsonObject = new JSONObject();
277                for (PolicyOperation operation: getValue()) {
278                        // E.g. "subset_of": ["code", "code token", "code id_token"]}
279                        Map.Entry<String,Object> en = operation.toJSONObjectEntry();
280                        jsonObject.put(en.getKey(), en.getValue());
281                }
282                
283                return jsonObject;
284        }
285        
286        
287        /**
288         * Parses a policy entry for a metadata parameter. This method is
289         * intended for policies with standard {@link PolicyOperation}s only.
290         * Uses the default {@link DefaultPolicyOperationFactory policy
291         * operation} and {@link DefaultPolicyOperationCombinationValidator
292         * policy combination validator} factories.
293         *
294         * @param parameterName The parameter name. Must not be {@code null}.
295         * @param entrySpec     The JSON object entry specification, must not
296         *                      be {@code null}.
297         *
298         * @return The policy entry for the metadata parameter.
299         *
300         * @throws ParseException           On JSON parsing exception.
301         * @throws PolicyViolationException On a policy violation.
302         */
303        public static MetadataPolicyEntry parse(final String parameterName,
304                                                final JSONObject entrySpec)
305                throws ParseException, PolicyViolationException {
306                
307                return parse(parameterName, entrySpec, DEFAULT_POLICY_OPERATION_FACTORY, DEFAULT_POLICY_COMBINATION_VALIDATOR);
308        }
309        
310        
311        /**
312         * Parses a policy entry for a metadata parameter. This method is
313         * intended for policies including non-standard
314         * {@link PolicyOperation}s.
315         *
316         * @param parameterName        The parameter name. Must not be
317         *                             {@code null}.
318         * @param entrySpec            The JSON object entry specification,
319         *                             must not be {@code null}.
320         * @param factory              The policy operation factory. Must not
321         *                             be {@code null}.
322         * @param combinationValidator The policy operation combination
323         *                             validator. Must not be {@code null}.
324         *
325         * @return The policy entry for the metadata parameter.
326         *
327         * @throws ParseException           On JSON parsing exception.
328         * @throws PolicyViolationException On a policy violation.
329         */
330        public static MetadataPolicyEntry parse(final String parameterName,
331                                                final JSONObject entrySpec,
332                                                final PolicyOperationFactory factory,
333                                                final PolicyOperationCombinationValidator combinationValidator)
334                throws ParseException, PolicyViolationException {
335                
336                if (entrySpec == null) {
337                        throw new IllegalArgumentException("The entry spec must not be null");
338                }
339                
340                List<PolicyOperation> policyOperations = new LinkedList<>();
341                
342                for (String opName: entrySpec.keySet()) {
343                        PolicyOperation op = factory.createForName(new OperationName(opName));
344                        if (op == null) {
345                                throw new PolicyViolationException("Unsupported policy operation: " + opName);
346                        }
347                        op.parseConfiguration(entrySpec.get(opName));
348                        policyOperations.add(op);
349                }
350                
351                List<PolicyOperation> validatedPolicyOperations = combinationValidator.validate(policyOperations);
352                
353                return new MetadataPolicyEntry(parameterName, validatedPolicyOperations);
354        }
355}