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