View Javadoc
1   /*
2    * Copyright 2013–2019 Michael Osipov
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package net.sf.michaelo.tomcat.extras.listeners;
17  
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.util.Map.Entry;
21  import java.util.Objects;
22  import java.util.Properties;
23  
24  import org.apache.catalina.Context;
25  import org.apache.catalina.Lifecycle;
26  import org.apache.catalina.LifecycleEvent;
27  import org.apache.catalina.LifecycleListener;
28  import org.apache.juli.logging.Log;
29  import org.apache.juli.logging.LogFactory;
30  
31  /**
32   * A listener which populates the context's role mapping from a properties file. <br>
33   * Consider your external role store does not allow changing names to developer-friendly names,
34   * you'd need to map them from cryptic ones, e.g, DNs, IDs, etc.
35   * <p>
36   * The following options can be configured:
37   * <ul>
38   * <li>{@code roleMappingFile}: the properties file containing the mapping from a technical role
39   * (key) to an application role (value). The option can either point to a classpath resource with
40   * {@code classpath:} or with {@code webapp:} to a webapp resource. Omitting a prefix will look in
41   * the webapp by default. The default value is {@code webapp:/WEB-INF/role-mapping.properties}.</li>
42   * <li>{@code keyPrefix}: scans all properties with this prefix (e.g., {@code my.roles.}).</li>
43   * </ul>
44   *
45   * <b>Note</b>: The used realm has to call {@link Context#findRoleMapping(String)} to make use of
46   * the populated mapping.
47   *
48   * @version $Id: PropertiesRoleMappingListener.java 123 2019-03-09 22:29:27Z michael-o $
49   */
50  public class PropertiesRoleMappingListener implements LifecycleListener {
51  
52  	private static final String WEBAPP_RESOURCE_PREFIX = "webapp:";
53  	private static final String CLASSPATH_RESOURCE_PREFIX = "classpath:";
54  
55  	private static final Log logger = LogFactory.getLog(PropertiesRoleMappingListener.class);
56  	private Context context;
57  
58  	private String roleMappingFile = "webapp:/WEB-INF/role-mapping.properties";
59  	private String keyPrefix;
60  
61  	public void setRoleMappingFile(String roleMappingFile) {
62  		this.roleMappingFile = Objects.requireNonNull(roleMappingFile,
63  				"roleMappingFile cannot be null");
64  	}
65  
66  	public void setKeyPrefix(String keyPrefix) {
67  		this.keyPrefix = keyPrefix;
68  	}
69  
70  	@Override
71  	public void lifecycleEvent(LifecycleEvent le) {
72  
73  		if (le.getLifecycle() instanceof Context)
74  			context = (Context) le.getLifecycle();
75  		else
76  			return;
77  
78  		if (le.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
79  			InputStream is;
80  
81  			if (roleMappingFile.startsWith(WEBAPP_RESOURCE_PREFIX)) {
82  				String path = roleMappingFile.substring(WEBAPP_RESOURCE_PREFIX.length());
83  				is = context.getServletContext().getResourceAsStream(path);
84  			} else if (roleMappingFile.startsWith(CLASSPATH_RESOURCE_PREFIX)) {
85  				String path = roleMappingFile.substring(CLASSPATH_RESOURCE_PREFIX.length());
86  				is = context.getLoader().getClassLoader().getResourceAsStream(path);
87  			} else {
88  				is = context.getServletContext().getResourceAsStream(roleMappingFile);
89  			}
90  
91  			if (is == null) {
92  				logger.warn(String.format(
93  						"Role mapping file '%s' not found, no role mapping can be performed",
94  						roleMappingFile));
95  				return;
96  			}
97  
98  			Properties props = new Properties();
99  
100 			try {
101 				props.load(is);
102 			} catch (IOException e) {
103 				logger.error(
104 						String.format("Failed to load role mapping file '%s'", roleMappingFile), e);
105 				return;
106 			} finally {
107 				try {
108 					is.close();
109 					is = null;
110 				} catch (IOException e) {
111 					// ignore
112 				}
113 			}
114 
115 			int linkCount = 0;
116 			for (Entry<Object, Object> prop : props.entrySet()) {
117 				String role = (String) prop.getKey();
118 
119 				if (keyPrefix != null) {
120 					if (role.startsWith(keyPrefix))
121 						role = role.substring(keyPrefix.length());
122 					else
123 						continue;
124 				}
125 
126 				String link = (String) prop.getValue();
127 
128 				if (logger.isTraceEnabled())
129 					logger.trace(String.format(
130 							"Successfully linked application role '%s' to technical role '%s'",
131 							role, link));
132 				context.addRoleMapping(role, link);
133 				linkCount++;
134 			}
135 
136 			if (logger.isDebugEnabled())
137 				logger.debug(
138 						String.format("Linked %s application roles to technical roles", linkCount));
139 		}
140 
141 	}
142 
143 }