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