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.operations;
019
020
021import java.util.LinkedList;
022import java.util.List;
023
024import com.nimbusds.oauth2.sdk.util.CollectionUtils;
025import com.nimbusds.openid.connect.sdk.federation.policy.language.OperationName;
026import com.nimbusds.openid.connect.sdk.federation.policy.language.PolicyOperation;
027import com.nimbusds.openid.connect.sdk.federation.policy.language.PolicyViolationException;
028
029
030/**
031 * Validates the permitted combinations of known policy operations for a given
032 * metadata parameter.
033 *
034 * <p>Supports all standard OpenID Connect federation policy operations:
035 *
036 * <ul>
037 *     <li>{@link SubsetOfOperation subset_of}
038 *     <li>{@link OneOfOperation one_of}
039 *     <li>{@link SupersetOfOperation superset_of}
040 *     <li>{@link AddOperation add}
041 *     <li>{@link ValueOperation value}
042 *     <li>{@link DefaultOperation default}
043 *     <li>{@link EssentialOperation essential}
044 * </ul>
045 *
046 * <p>Override the {@link #validate(List)} method to support additional custom
047 * policies.
048 *
049 * <p>Related specifications:
050 *
051 * <ul>
052 *     <li>OpenID Connect Federation 1.0, section 4.2.
053 * </ul>
054 */
055public class DefaultPolicyOperationCombinationValidator implements PolicyOperationCombinationValidator {
056        
057        
058        @Override
059        public List<PolicyOperation> validate(final List<PolicyOperation> policyOperations)
060                throws PolicyViolationException {
061                
062                if (CollectionUtils.isEmpty(policyOperations) || policyOperations.size() == 1) {
063                        // Empty or one policy operation always pass
064                        return policyOperations;
065                }
066                
067                List<PolicyOperation> currentOpList = new LinkedList<>(policyOperations);
068                
069                currentOpList = validateCombinationsOfEssential(currentOpList);
070                currentOpList = validateCombinationsOfAdd(currentOpList);
071                currentOpList = validateCombinationsOfDefault(currentOpList);
072                currentOpList = validateCombinationsOfSupersetOf(currentOpList);
073                currentOpList = validateCombinationsOfSubsetOf(currentOpList);
074                currentOpList = validateCombinationsOfValue(currentOpList);
075                
076                return currentOpList;
077        }
078        
079        
080        private static List<PolicyOperation> validateCombinationsOfEssential(final List<PolicyOperation> ops) {
081                // Can be combined with all the others. If essential is not present
082                // that is the same as stating essential=true.
083                return ops;
084        }
085        
086        
087        private static List<PolicyOperation> validateCombinationsOfAdd(final List<PolicyOperation> ops) {
088                // No official spec about "add"
089                return ops;
090        }
091        
092        
093        private static List<PolicyOperation> validateCombinationsOfDefault(final List<PolicyOperation> ops)
094                throws PolicyViolationException {
095                // Can be combined with one_of, subset_of and superset_of. If a
096                // default policy is combined with one_of, subset_of or superset_of
097                // and it is not a subset of the subset_of policy or the one_of
098                // policy or a superset of the superset_of policy then an error MUST
099                // be raised.
100                DefaultOperation o = Utils.getPolicyOperationByType(ops, DefaultOperation.class);
101                
102                if (o == null) {
103                        return ops;
104                }
105                
106                if (o.getStringListConfiguration() != null) {
107                        ensureSatisfiedBySubsetOf(ops, o.getStringListConfiguration());
108                        ensureSatisfiedBySupersetOf(ops, o.getStringListConfiguration());
109                } else if (o.getStringConfiguration() != null) {
110                        ensureSatisfiedByOneOf(ops, o.getStringConfiguration());
111                }
112                
113                if (Utils.getPolicyOperationByType(ops, ValueOperation.class) != null) {
114                        throw new PolicyViolationException("Policies default and value cannot be combined");
115                }
116                
117                return ops;
118        }
119        
120        
121        private static List<PolicyOperation> validateCombinationsOfSupersetOf(final List<PolicyOperation> ops)
122                throws PolicyViolationException {
123                // Can be combined with subset_of. If subset_of and superset_of both
124                // appears in a metadata_policy statement subset_of MUST be a superset
125                // of superset_of.
126                SupersetOfOperation o = Utils.getPolicyOperationByType(ops, SupersetOfOperation.class);
127                if (o == null) {
128                        return ops;
129                }
130                
131                SubsetOfOperation subsetOfOperation = Utils.getPolicyOperationByType(ops, SubsetOfOperation.class);
132                if (subsetOfOperation != null) {
133                        ensureSatisfied(o, subsetOfOperation.getStringListConfiguration());
134                }
135                
136                return ops;
137        }
138        
139        
140        private static List<PolicyOperation> validateCombinationsOfSubsetOf(final List<PolicyOperation> ops)
141                throws PolicyViolationException {
142                // Can be combined with superset_of. If superset_of and subset_of both
143                // appears in a metadata_policy statement for a claim subset_of MUST be
144                // a superset of superset_of.
145                SubsetOfOperation o = Utils.getPolicyOperationByType(ops, SubsetOfOperation.class);
146                if (o == null) {
147                        return ops;
148                }
149                
150                SupersetOfOperation supersetOfOperation = Utils.getPolicyOperationByType(ops, SupersetOfOperation.class);
151                if (supersetOfOperation != null) {
152                        ensureSatisfied(supersetOfOperation, o.getStringListConfiguration());
153                }
154                
155                return ops;
156        }
157        
158        
159        // See https://bitbucket.org/openid/connect/issues/1163/federation-metadata-policy-current-spec
160        private static List<PolicyOperation> validateCombinationsOfValue(final List<PolicyOperation> ops)
161                throws PolicyViolationException {
162                // https://bitbucket.org/openid/connect/issues/1163/federation-metadata-policy-current-spec
163                // value must not be combined with other policy types, raise an error if this condition is detected.
164                ValueOperation o = Utils.getPolicyOperationByType(ops, ValueOperation.class);
165                if (o == null) {
166                        return ops;
167                }
168                
169                List<OperationName> violating = new LinkedList<>();
170                for (PolicyOperation op: ops) {
171                        if (op instanceof ValueOperation) {
172                                // ok
173                        } else if (op instanceof EssentialOperation) {
174                                // ok
175                        } else {
176                                violating.add(op.getOperationName());
177                        }
178                }
179                
180                if (! violating.isEmpty()) {
181                        throw new PolicyViolationException("Policy operation " + ValueOperation.NAME + " must not be combined with: " + violating);
182                }
183                
184                return ops;
185        }
186        
187        
188        private static void ensureSatisfied(final SubsetOfOperation op, final List<String> values)
189                throws PolicyViolationException {
190                if (! op.getStringListConfiguration().containsAll(values))
191                        throw new PolicyViolationException("Not in " + SubsetOfOperation.NAME + " " + op.getStringListConfiguration() + ": " + values);
192        }
193        
194        
195        private static void ensureSatisfiedBySubsetOf(final List<PolicyOperation> policyOperations, final List<String> values)
196                throws PolicyViolationException {
197                SubsetOfOperation op = Utils.getPolicyOperationByType(policyOperations, SubsetOfOperation.class);
198                if (op != null) {
199                        ensureSatisfied(op, values);
200                }
201        }
202        
203        
204        private static void ensureSatisfied(final SupersetOfOperation op, final List<String> values)
205                throws PolicyViolationException {
206                if (! values.containsAll(op.getStringListConfiguration()))
207                        throw new PolicyViolationException("Not in " + SupersetOfOperation.NAME + " " + op.getStringListConfiguration() + ": " + values);
208        }
209        
210        
211        private static void ensureSatisfiedBySupersetOf(final List<PolicyOperation> policyOperations, final List<String> values)
212                throws PolicyViolationException {
213                SupersetOfOperation op = Utils.getPolicyOperationByType(policyOperations, SupersetOfOperation.class);
214                if (op != null) {
215                        ensureSatisfied(op, values);
216                }
217        }
218        
219        
220        private static void ensureSatisfiedByOneOf(final List<PolicyOperation> policyOperations, final String value)
221                throws PolicyViolationException {
222                OneOfOperation op = Utils.getPolicyOperationByType(policyOperations, OneOfOperation.class);
223                if (op == null) return;
224                if (! op.getStringListConfiguration().contains(value))
225                        throw new PolicyViolationException("Not in " + OneOfOperation.NAME + " " + op.getStringListConfiguration() + ": " + value);
226        }
227}