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