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><security-constraint></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