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.trust.constraints;
019
020
021import java.util.Collections;
022import java.util.LinkedList;
023import java.util.List;
024import java.util.Objects;
025
026import net.jcip.annotations.Immutable;
027import net.minidev.json.JSONAware;
028import net.minidev.json.JSONObject;
029
030import com.nimbusds.oauth2.sdk.ParseException;
031import com.nimbusds.oauth2.sdk.util.CollectionUtils;
032import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
033import com.nimbusds.openid.connect.sdk.federation.entities.EntityID;
034
035
036/**
037 * Trust chain constraints.
038 *
039 * <p>Example JSON object:
040 *
041 * <pre>
042 * {
043 *   "max_path_length"    : 2,
044 *   "naming_constraints" : {
045 *      "permitted" : [ "https://example.com" ],
046 *      "excluded"  : [ "https://east.example.com" ]
047 *   }
048 * }
049 * </pre>
050 *
051 * <p>Related specifications:
052 *
053 * <ul>
054 *     <li>OpenID Connect Federation 1.0, section 7.2.
055 *     <li>RFC 5280, section 4.2.1.10.
056 * </ul>
057 */
058@Immutable
059public final class TrustChainConstraints implements JSONAware {
060        
061        
062        /**
063         * No constraint instance.
064         */
065        public static final TrustChainConstraints NO_CONSTRAINTS = new TrustChainConstraints();
066        
067        
068        /**
069         * The max path length, -1 if not specified.
070         */
071        private final int maxPathLength;
072        
073        
074        /**
075         * The permitted entities.
076         */
077        private final List<EntityIDConstraint> permittedEntities;
078        
079        
080        /**
081         * The excluded entities.
082         */
083        private final List<EntityIDConstraint> excludedEntities;
084        
085        
086        /**
087         * Creates a new no constraints instance.
088         */
089        public TrustChainConstraints() {
090                this(-1, null, null);
091        }
092        
093        
094        /**
095         * Creates a new trust chain constraints instance.
096         *
097         * @param maxPathLength The maximum number of entities between this and
098         *                      the last one in the chain, -1 if not specified.
099         */
100        public TrustChainConstraints(final int maxPathLength) {
101                this(maxPathLength, null, null);
102        }
103        
104        
105        /**
106         * Creates a new trust chain constraints instance.
107         *
108         * @param maxPathLength     The maximum number of entities between this
109         *                          and the last one in the chain, -1 if not
110         *                          specified.
111         * @param permittedEntities The permitted entities, {@code null} if not
112         *                          specified.
113         * @param excludedEntities  The excluded entities, {@code null} if not
114         *                          specified.
115         */
116        public TrustChainConstraints(final int maxPathLength, final List<EntityIDConstraint> permittedEntities, final List<EntityIDConstraint> excludedEntities) {
117                this.maxPathLength = maxPathLength;
118                this.permittedEntities = permittedEntities != null ? permittedEntities : Collections.<EntityIDConstraint>emptyList();
119                this.excludedEntities = excludedEntities != null ? excludedEntities : Collections.<EntityIDConstraint>emptyList();
120        }
121        
122        
123        /**
124         * Checks if the given number of intermediates is permitted.
125         *
126         * @param numIntermediatesInPath The number of intermediate entities
127         *                               between the entity specifying the
128         *                               constraints and the specified entity.
129         *                               Must be zero or greater.
130         *
131         * @return {@code true} if permitted, else {@code false}.
132         */
133        public boolean isPermitted(final int numIntermediatesInPath) {
134                
135                if (numIntermediatesInPath < 0) {
136                        throw new IllegalArgumentException("The path length must not be negative");
137                }
138                
139                return getMaxPathLength() <= -1 || numIntermediatesInPath <= getMaxPathLength();
140        }
141        
142        
143        /**
144         * Checks if the entity ID is permitted.
145         *
146         * @param entityID The entity ID. Must not be {@code null}.
147         *
148         * @return {@code true} if permitted, else {@code false}.
149         */
150        public boolean isPermitted(final EntityID entityID) {
151                
152                if (getExcludedEntities().isEmpty() && getPermittedEntities().isEmpty()) {
153                        return true;
154                }
155                
156                if (! getExcludedEntities().isEmpty()) {
157                        
158                        for (EntityIDConstraint constraint: getExcludedEntities()) {
159                                if (constraint.matches(entityID)) {
160                                        return false;
161                                }
162                        }
163                }
164                
165                if (! getPermittedEntities().isEmpty()) {
166                        
167                        for (EntityIDConstraint constraint: getPermittedEntities()) {
168                                if (constraint.matches(entityID)) {
169                                        return true;
170                                }
171                        }
172                } else {
173                        // If passed so far - always permitted
174                        return true;
175                }
176                
177                return false;
178        }
179        
180        
181        /**
182         * Checks if the entity ID with the given number of intermediates is
183         * permitted.
184         *
185         * @param numIntermediatesInPath The number of intermediate entities
186         *                               between the entity specifying the
187         *                               constraints and the specified entity.
188         *                               Must be zero or greater.
189         *
190         * @param entityID               The entity ID. Must not be
191         *                               {@code null}.
192         * @return {@code true} if permitted, else {@code false}.
193         */
194        public boolean isPermitted(final int numIntermediatesInPath, final EntityID entityID) {
195                
196                return isPermitted(numIntermediatesInPath) && isPermitted(entityID);
197        }
198        
199        
200        /**
201         * Returns the maximum number of entities between this and the last one
202         * in the chain.
203         *
204         * @return The maximum number of entities between this and the last one
205         *         in the chain, -1 if not specified.
206         */
207        public int getMaxPathLength() {
208                return maxPathLength;
209        }
210        
211        
212        /**
213         * Returns the permitted entities.
214         *
215         * @return The permitted entities, empty list if not specified.
216         */
217        public List<EntityIDConstraint> getPermittedEntities() {
218                return permittedEntities;
219        }
220        
221        
222        /**
223         * Returns the excluded entities.
224         *
225         * @return The excluded entities, empty list if not specified.
226         */
227        public List<EntityIDConstraint> getExcludedEntities() {
228                return excludedEntities;
229        }
230        
231        
232        /**
233         * Returns a JSON object representation of this trust chain
234         * constraints.
235         *
236         * @return The JSON object.
237         */
238        public JSONObject toJSONObject() {
239        
240                JSONObject o = new JSONObject();
241                
242                if (maxPathLength > -1) {
243                        o.put("max_path_length", maxPathLength);
244                }
245                
246                JSONObject namingConstraints = new JSONObject();
247                
248                if (CollectionUtils.isNotEmpty(permittedEntities)) {
249                        List<String> vals = new LinkedList<>();
250                        for (EntityIDConstraint v: permittedEntities) {
251                                vals.add(v.toString());
252                        }
253                        namingConstraints.put("permitted", vals);
254                }
255                
256                if (CollectionUtils.isNotEmpty(excludedEntities)) {
257                        List<String> vals = new LinkedList<>();
258                        for (EntityIDConstraint v: excludedEntities) {
259                                vals.add(v.toString());
260                        }
261                        namingConstraints.put("excluded", vals);
262                }
263                
264                if (! namingConstraints.isEmpty()) {
265                        o.put("naming_constraints", namingConstraints);
266                }
267                
268                return o;
269        }
270        
271        
272        @Override
273        public String toJSONString() {
274                return toJSONObject().toJSONString();
275        }
276        
277        
278        @Override
279        public boolean equals(Object o) {
280                if (this == o) return true;
281                if (!(o instanceof TrustChainConstraints)) return false;
282                TrustChainConstraints that = (TrustChainConstraints) o;
283                return getMaxPathLength() == that.getMaxPathLength() &&
284                        Objects.equals(getPermittedEntities(), that.getPermittedEntities()) &&
285                        Objects.equals(getExcludedEntities(), that.getExcludedEntities());
286        }
287        
288        
289        @Override
290        public int hashCode() {
291                return Objects.hash(getMaxPathLength(), getPermittedEntities(), getExcludedEntities());
292        }
293        
294        
295        /**
296         * Parses a trust chain constraints instance from the specified JSON
297         * object.
298         *
299         * @param jsonObject The JSON object. Must not be {@code null}.
300         *
301         * @return The trust chain constraints.
302         *
303         * @throws ParseException If parsing failed.
304         */
305        public static TrustChainConstraints parse(final JSONObject jsonObject)
306                throws ParseException {
307                
308                int maxPathLength = JSONObjectUtils.getInt(jsonObject, "max_path_length", -1);
309                
310                JSONObject namingConstraints = JSONObjectUtils.getJSONObject(jsonObject, "naming_constraints", new JSONObject());
311                
312                List<EntityIDConstraint> permitted = null;
313                List<String> values = JSONObjectUtils.getStringList(namingConstraints, "permitted", null);
314                if (values != null) {
315                        permitted = new LinkedList<>();
316                        for (String v: values) {
317                                if (v != null) {
318                                        permitted.add(EntityIDConstraint.parse(v));
319                                }
320                        }
321                }
322                
323                List<EntityIDConstraint> excluded = null;
324                values = JSONObjectUtils.getStringList(namingConstraints, "excluded", null);
325                if (values != null) {
326                        excluded = new LinkedList<>();
327                        for (String v: values) {
328                                if (v != null) {
329                                        excluded.add(EntityIDConstraint.parse(v));
330                                }
331                        }
332                }
333                
334                return new TrustChainConstraints(maxPathLength, permitted, excluded);
335        }
336}