1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */

17 package org.apache.tomcat.util.descriptor.web;
18
19 import java.io.Serializable;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.Collection;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Set;
28
29 import javax.servlet.HttpConstraintElement;
30 import javax.servlet.HttpMethodConstraintElement;
31 import javax.servlet.ServletSecurityElement;
32 import javax.servlet.annotation.ServletSecurity;
33 import javax.servlet.annotation.ServletSecurity.EmptyRoleSemantic;
34
35 import org.apache.juli.logging.Log;
36 import org.apache.tomcat.util.res.StringManager;
37
38
39 /**
40  * Representation of a security constraint element for a web application,
41  * as represented in a <code>&lt;security-constraint&gt;</code> element in the
42  * deployment descriptor.
43  * <p>
44  * <b>WARNING</b>:  It is assumed that instances of this class will be created
45  * and modified only within the context of a single thread, before the instance
46  * is made visible to the remainder of the application.  After that, only read
47  * access is expected.  Therefore, none of the read and write access within
48  * this class is synchronized.
49  *
50  * @author Craig R. McClanahan
51  */

52 public class SecurityConstraint extends XmlEncodingBase implements Serializable {
53
54     private static final long serialVersionUID = 1L;
55
56     public static final String ROLE_ALL_ROLES = "*";
57     public static final String ROLE_ALL_AUTHENTICATED_USERS = "**";
58
59     private static final StringManager sm =
60             StringManager.getManager(Constants.PACKAGE_NAME);
61
62
63     // ----------------------------------------------------------- Constructors
64
65     /**
66      * Construct a new security constraint instance with default values.
67      */

68     public SecurityConstraint() {
69         super();
70     }
71
72
73     // ----------------------------------------------------- Instance Variables
74
75
76     /**
77      * Was the "all roles" wildcard - {@link #ROLE_ALL_ROLES} - included in the
78      * authorization constraints for this security constraint?
79      */

80     private boolean allRoles = false;
81
82
83     /**
84      * Was the "all authenticated users" wildcard -
85      * {@link #ROLE_ALL_AUTHENTICATED_USERS} - included in the authorization
86      * constraints for this security constraint?
87      */

88     private boolean authenticatedUsers = false;
89
90
91     /**
92      * Was an authorization constraint included in this security constraint?
93      * This is necessary to distinguish the case where an auth-constraint with
94      * no roles (signifying no direct access at all) was requested, versus
95      * a lack of auth-constraint which implies no access control checking.
96      */

97     private boolean authConstraint = false;
98
99
100     /**
101      * The set of roles permitted to access resources protected by this
102      * security constraint.
103      */

104     private String authRoles[] = new String[0];
105
106
107     /**
108      * The set of web resource collections protected by this security
109      * constraint.
110      */

111     private SecurityCollection collections[] = new SecurityCollection[0];
112
113
114     /**
115      * The display name of this security constraint.
116      */

117     private String displayName = null;
118
119
120     /**
121      * The user data constraint for this security constraint.  Must be NONE,
122      * INTEGRAL, or CONFIDENTIAL.
123      */

124     private String userConstraint = "NONE";
125
126
127     // ------------------------------------------------------------- Properties
128
129
130     /**
131      * Was the "all roles" wildcard included in this authentication
132      * constraint?
133      * @return <code>true</code> if all roles
134      */

135     public boolean getAllRoles() {
136
137         return this.allRoles;
138
139     }
140
141
142     /**
143      * Was the "all authenticated users" wildcard included in this
144      * authentication constraint?
145      * @return <code>true</code> if all authenticated users
146      */

147     public boolean getAuthenticatedUsers() {
148         return this.authenticatedUsers;
149     }
150
151
152     /**
153      * Return the authorization constraint present flag for this security
154      * constraint.
155      * @return <code>true</code> if this needs authorization
156      */

157     public boolean getAuthConstraint() {
158
159         return this.authConstraint;
160
161     }
162
163
164     /**
165      * Set the authorization constraint present flag for this security
166      * constraint.
167      * @param authConstraint The new value
168      */

169     public void setAuthConstraint(boolean authConstraint) {
170
171         this.authConstraint = authConstraint;
172
173     }
174
175
176     /**
177      * @return the display name of this security constraint.
178      */

179     public String getDisplayName() {
180
181         return this.displayName;
182
183     }
184
185
186     /**
187      * Set the display name of this security constraint.
188      * @param displayName The new value
189      */

190     public void setDisplayName(String displayName) {
191
192         this.displayName = displayName;
193
194     }
195
196
197     /**
198      * Return the user data constraint for this security constraint.
199      * @return the user constraint
200      */

201     public String getUserConstraint() {
202
203         return userConstraint;
204
205     }
206
207
208     /**
209      * Set the user data constraint for this security constraint.
210      *
211      * @param userConstraint The new user data constraint
212      */

213     public void setUserConstraint(String userConstraint) {
214
215         if (userConstraint != null)
216             this.userConstraint = userConstraint;
217
218     }
219
220
221     /**
222      * Called in the unlikely event that an application defines a role named
223      * "**".
224      */

225     public void treatAllAuthenticatedUsersAsApplicationRole() {
226         if (authenticatedUsers) {
227             authenticatedUsers = false;
228
229             String[] results = Arrays.copyOf(authRoles, authRoles.length + 1);
230             results[authRoles.length] = ROLE_ALL_AUTHENTICATED_USERS;
231             authRoles = results;
232             authConstraint = true;
233         }
234     }
235
236
237     // --------------------------------------------------------- Public Methods
238
239
240     /**
241      * Add an authorization role, which is a role name that will be
242      * permitted access to the resources protected by this security constraint.
243      *
244      * @param authRole Role name to be added
245      */

246     public void addAuthRole(String authRole) {
247
248         if (authRole == null)
249             return;
250
251         if (ROLE_ALL_ROLES.equals(authRole)) {
252             allRoles = true;
253             return;
254         }
255
256         if (ROLE_ALL_AUTHENTICATED_USERS.equals(authRole)) {
257             authenticatedUsers = true;
258             return;
259         }
260
261         String[] results = Arrays.copyOf(authRoles, authRoles.length + 1);
262         results[authRoles.length] = authRole;
263         authRoles = results;
264         authConstraint = true;
265     }
266
267
268     /**
269      * Add a new web resource collection to those protected by this
270      * security constraint.
271      *
272      * @param collection The new web resource collection
273      */

274     public void addCollection(SecurityCollection collection) {
275
276         if (collection == null)
277             return;
278
279         collection.setCharset(getCharset());
280
281         SecurityCollection results[] = Arrays.copyOf(collections, collections.length + 1);
282         results[collections.length] = collection;
283         collections = results;
284
285     }
286
287
288     /**
289      * Check a role.
290      *
291      * @param role Role name to be checked
292      * @return <code>true</code> if the specified role is permitted access to
293      * the resources protected by this security constraint.
294      */

295     public boolean findAuthRole(String role) {
296
297         if (role == null)
298             return false;
299         for (int i = 0; i < authRoles.length; i++) {
300             if (role.equals(authRoles[i]))
301                 return true;
302         }
303         return false;
304
305     }
306
307
308     /**
309      * Return the set of roles that are permitted access to the resources
310      * protected by this security constraint.  If none have been defined,
311      * a zero-length array is returned (which implies that all authenticated
312      * users are permitted access).
313      * @return the roles array
314      */

315     public String[] findAuthRoles() {
316         return authRoles;
317     }
318
319
320     /**
321      * Return the web resource collection for the specified name, if any;
322      * otherwise, return <code>null</code>.
323      *
324      * @param name Web resource collection name to return
325      * @return the collection
326      */

327     public SecurityCollection findCollection(String name) {
328         if (name == null)
329             return null;
330         for (int i = 0; i < collections.length; i++) {
331             if (name.equals(collections[i].getName()))
332                 return collections[i];
333         }
334         return null;
335     }
336
337
338     /**
339      * Return all of the web resource collections protected by this
340      * security constraint.  If there are none, a zero-length array is
341      * returned.
342      * @return the collections array
343      */

344     public SecurityCollection[] findCollections() {
345         return collections;
346     }
347
348
349     /**
350      * Check if the constraint applies to a URI and method.
351      * @param uri Context-relative URI to check
352      * @param method Request method being used
353      * @return <code>true</code> if the specified context-relative URI (and
354      * associated HTTP method) are protected by this security constraint.
355      */

356     public boolean included(String uri, String method) {
357
358         // We cannot match without a valid request method
359         if (method == null)
360             return false;
361
362         // Check all of the collections included in this constraint
363         for (int i = 0; i < collections.length; i++) {
364             if (!collections[i].findMethod(method))
365                 continue;
366             String patterns[] = collections[i].findPatterns();
367             for (int j = 0; j < patterns.length; j++) {
368                 if (matchPattern(uri, patterns[j]))
369                     return true;
370             }
371         }
372
373         // No collection included in this constraint matches this request
374         return false;
375
376     }
377
378
379     /**
380      * Remove the specified role from the set of roles permitted to access
381      * the resources protected by this security constraint.
382      *
383      * @param authRole Role name to be removed
384      */

385     public void removeAuthRole(String authRole) {
386
387         if (authRole == null)
388             return;
389
390         if (ROLE_ALL_ROLES.equals(authRole)) {
391             allRoles = false;
392             return;
393         }
394
395         if (ROLE_ALL_AUTHENTICATED_USERS.equals(authRole)) {
396             authenticatedUsers = false;
397             return;
398         }
399
400         int n = -1;
401         for (int i = 0; i < authRoles.length; i++) {
402             if (authRoles[i].equals(authRole)) {
403                 n = i;
404                 break;
405             }
406         }
407         if (n >= 0) {
408             int j = 0;
409             String results[] = new String[authRoles.length - 1];
410             for (int i = 0; i < authRoles.length; i++) {
411                 if (i != n)
412                     results[j++] = authRoles[i];
413             }
414             authRoles = results;
415         }
416     }
417
418
419     /**
420      * Remove the specified web resource collection from those protected by
421      * this security constraint.
422      *
423      * @param collection Web resource collection to be removed
424      */

425     public void removeCollection(SecurityCollection collection) {
426
427         if (collection == null)
428             return;
429         int n = -1;
430         for (int i = 0; i < collections.length; i++) {
431             if (collections[i].equals(collection)) {
432                 n = i;
433                 break;
434             }
435         }
436         if (n >= 0) {
437             int j = 0;
438             SecurityCollection results[] =
439                 new SecurityCollection[collections.length - 1];
440             for (int i = 0; i < collections.length; i++) {
441                 if (i != n)
442                     results[j++] = collections[i];
443             }
444             collections = results;
445         }
446
447     }
448
449
450     /**
451      * Return a String representation of this security constraint.
452      */

453     @Override
454     public String toString() {
455         StringBuilder sb = new StringBuilder("SecurityConstraint[");
456         for (int i = 0; i < collections.length; i++) {
457             if (i > 0)
458                 sb.append(", ");
459             sb.append(collections[i].getName());
460         }
461         sb.append("]");
462         return sb.toString();
463     }
464
465
466     // -------------------------------------------------------- Private Methods
467
468
469     /**
470      * Does the specified request path match the specified URL pattern?
471      * This method follows the same rules (in the same order) as those used
472      * for mapping requests to servlets.
473      *
474      * @param path Context-relative request path to be checked
475      *  (must start with '/')
476      * @param pattern URL pattern to be compared against
477      */

478     private boolean matchPattern(String path, String pattern) {
479
480         // Normalize the argument strings
481         if ((path == null) || (path.length() == 0))
482             path = "/";
483         if ((pattern == null) || (pattern.length() == 0))
484             pattern = "/";
485
486         // Check for exact match
487         if (path.equals(pattern))
488             return true;
489
490         // Check for path prefix matching
491         if (pattern.startsWith("/") && pattern.endsWith("/*")) {
492             pattern = pattern.substring(0, pattern.length() - 2);
493             if (pattern.length() == 0)
494                 return true;  // "/*" is the same as "/"
495             if (path.endsWith("/"))
496                 path = path.substring(0, path.length() - 1);
497             while (true) {
498                 if (pattern.equals(path))
499                     return true;
500                 int slash = path.lastIndexOf('/');
501                 if (slash <= 0)
502                     break;
503                 path = path.substring(0, slash);
504             }
505             return false;
506         }
507
508         // Check for suffix matching
509         if (pattern.startsWith("*.")) {
510             int slash = path.lastIndexOf('/');
511             int period = path.lastIndexOf('.');
512             if ((slash >= 0) && (period > slash) &&
513                 path.endsWith(pattern.substring(1))) {
514                 return true;
515             }
516             return false;
517         }
518
519         // Check for universal mapping
520         if (pattern.equals("/"))
521             return true;
522
523         return false;
524
525     }
526
527
528     /**
529      * Convert a {@link ServletSecurityElement} to an array of
530      * {@link SecurityConstraint}(s).
531      *
532      * @param element       The element to be converted
533      * @param urlPattern    The url pattern that the element should be applied
534      *                      to
535      * @return              The (possibly zero length) array of constraints that
536      *                      are the equivalent to the input
537      */

538     public static SecurityConstraint[] createConstraints(
539             ServletSecurityElement element, String urlPattern) {
540         Set<SecurityConstraint> result = new HashSet<>();
541
542         // Add the per method constraints
543         Collection<HttpMethodConstraintElement> methods =
544             element.getHttpMethodConstraints();
545         for (HttpMethodConstraintElement methodElement : methods) {
546             SecurityConstraint constraint =
547                 createConstraint(methodElement, urlPattern, true);
548             // There will always be a single collection
549             SecurityCollection collection = constraint.findCollections()[0];
550             collection.addMethod(methodElement.getMethodName());
551             result.add(constraint);
552         }
553
554         // Add the constraint for all the other methods
555         SecurityConstraint constraint = createConstraint(element, urlPattern, false);
556         if (constraint != null) {
557             // There will always be a single collection
558             SecurityCollection collection = constraint.findCollections()[0];
559             for (String name : element.getMethodNames()) {
560                 collection.addOmittedMethod(name);
561             }
562
563             result.add(constraint);
564
565         }
566
567         return result.toArray(new SecurityConstraint[result.size()]);
568     }
569
570     private static SecurityConstraint createConstraint(
571             HttpConstraintElement element, String urlPattern, boolean alwaysCreate) {
572
573         SecurityConstraint constraint = new SecurityConstraint();
574         SecurityCollection collection = new SecurityCollection();
575         boolean create = alwaysCreate;
576
577         if (element.getTransportGuarantee() !=
578                 ServletSecurity.TransportGuarantee.NONE) {
579             constraint.setUserConstraint(element.getTransportGuarantee().name());
580             create = true;
581         }
582         if (element.getRolesAllowed().length > 0) {
583             String[] roles = element.getRolesAllowed();
584             for (String role : roles) {
585                 constraint.addAuthRole(role);
586             }
587             create = true;
588         }
589         if (element.getEmptyRoleSemantic() != EmptyRoleSemantic.PERMIT) {
590             constraint.setAuthConstraint(true);
591             create = true;
592         }
593
594         if (create) {
595             collection.addPattern(urlPattern);
596             constraint.addCollection(collection);
597             return constraint;
598         }
599
600         return null;
601     }
602
603
604     public static SecurityConstraint[] findUncoveredHttpMethods(
605             SecurityConstraint[] constraints,
606             boolean denyUncoveredHttpMethods, Log log) {
607
608         Set<String> coveredPatterns = new HashSet<>();
609         Map<String,Set<String>> urlMethodMap = new HashMap<>();
610         Map<String,Set<String>> urlOmittedMethodMap = new HashMap<>();
611
612         List<SecurityConstraint> newConstraints = new ArrayList<>();
613
614         // First build the lists of covered patterns and those patterns that
615         // might be uncovered
616         for (SecurityConstraint constraint : constraints) {
617             SecurityCollection[] collections = constraint.findCollections();
618             for (SecurityCollection collection : collections) {
619                 String[] patterns = collection.findPatterns();
620                 String[] methods = collection.findMethods();
621                 String[] omittedMethods = collection.findOmittedMethods();
622                 // Simple case: no methods
623                 if (methods.length == 0 && omittedMethods.length == 0) {
624                     for (String pattern : patterns) {
625                         coveredPatterns.add(pattern);
626                     }
627                     continue;
628                 }
629
630                 // Pre-calculate so we don't do this for every iteration of the
631                 // following loop
632                 List<String> omNew = null;
633                 if (omittedMethods.length != 0) {
634                     omNew = Arrays.asList(omittedMethods);
635                 }
636
637                 // Only need to process uncovered patterns
638                 for (String pattern : patterns) {
639                     if (!coveredPatterns.contains(pattern)) {
640                         if (methods.length == 0) {
641                             // Build the interset of omitted methods for this
642                             // pattern
643                             Set<String> om = urlOmittedMethodMap.get(pattern);
644                             if (om == null) {
645                                 om = new HashSet<>();
646                                 urlOmittedMethodMap.put(pattern, om);
647                                 om.addAll(omNew);
648                             } else {
649                                 om.retainAll(omNew);
650                             }
651                         } else {
652                             // Build the union of methods for this pattern
653                             Set<String> m = urlMethodMap.get(pattern);
654                             if (m == null) {
655                                 m = new HashSet<>();
656                                 urlMethodMap.put(pattern, m);
657                             }
658                             for (String method : methods) {
659                                 m.add(method);
660                             }
661                         }
662                     }
663                 }
664             }
665         }
666
667         // Now check the potentially uncovered patterns
668         for (Map.Entry<String, Set<String>> entry : urlMethodMap.entrySet()) {
669             String pattern = entry.getKey();
670             if (coveredPatterns.contains(pattern)) {
671                 // Fully covered. Ignore any partial coverage
672                 urlOmittedMethodMap.remove(pattern);
673                 continue;
674             }
675
676             Set<String> omittedMethods = urlOmittedMethodMap.remove(pattern);
677             Set<String> methods = entry.getValue();
678
679             if (omittedMethods == null) {
680                 StringBuilder msg = new StringBuilder();
681                 for (String method : methods) {
682                     msg.append(method);
683                     msg.append(' ');
684                 }
685                 if (denyUncoveredHttpMethods) {
686                     log.info(sm.getString(
687                             "securityConstraint.uncoveredHttpMethodFix",
688                             pattern, msg.toString().trim()));
689                     SecurityCollection collection = new SecurityCollection();
690                     for (String method : methods) {
691                         collection.addOmittedMethod(method);
692                     }
693                     collection.addPatternDecoded(pattern);
694                     collection.setName("deny-uncovered-http-methods");
695                     SecurityConstraint constraint = new SecurityConstraint();
696                     constraint.setAuthConstraint(true);
697                     constraint.addCollection(collection);
698                     newConstraints.add(constraint);
699                 } else {
700                     log.error(sm.getString(
701                             "securityConstraint.uncoveredHttpMethod",
702                             pattern, msg.toString().trim()));
703                 }
704                 continue;
705             }
706
707             // As long as every omitted method as a corresponding method the
708             // pattern is fully covered.
709             omittedMethods.removeAll(methods);
710
711             handleOmittedMethods(omittedMethods, pattern, denyUncoveredHttpMethods,
712                     newConstraints, log);
713         }
714         for (Map.Entry<String, Set<String>> entry :
715                 urlOmittedMethodMap.entrySet()) {
716             String pattern = entry.getKey();
717             if (coveredPatterns.contains(pattern)) {
718                 // Fully covered. Ignore any partial coverage
719                 continue;
720             }
721
722             handleOmittedMethods(entry.getValue(), pattern, denyUncoveredHttpMethods,
723                     newConstraints, log);
724         }
725
726         return newConstraints.toArray(new SecurityConstraint[newConstraints.size()]);
727     }
728
729
730     private static void handleOmittedMethods(Set<String> omittedMethods, String pattern,
731             boolean denyUncoveredHttpMethods, List<SecurityConstraint> newConstraints, Log log) {
732         if (omittedMethods.size() > 0) {
733             StringBuilder msg = new StringBuilder();
734             for (String method : omittedMethods) {
735                 msg.append(method);
736                 msg.append(' ');
737             }
738             if (denyUncoveredHttpMethods) {
739                 log.info(sm.getString(
740                         "securityConstraint.uncoveredHttpOmittedMethodFix",
741                         pattern, msg.toString().trim()));
742                 SecurityCollection collection = new SecurityCollection();
743                 for (String method : omittedMethods) {
744                     collection.addMethod(method);
745                 }
746                 collection.addPatternDecoded(pattern);
747                 collection.setName("deny-uncovered-http-methods");
748                 SecurityConstraint constraint = new SecurityConstraint();
749                 constraint.setAuthConstraint(true);
750                 constraint.addCollection(collection);
751                 newConstraints.add(constraint);
752             } else {
753                 log.error(sm.getString(
754                         "securityConstraint.uncoveredHttpOmittedMethod",
755                         pattern, msg.toString().trim()));
756             }
757         }
758     }
759 }
760