001 package hirondelle.web4j.security;
002
003 import static hirondelle.web4j.util.Consts.NOT_FOUND;
004 import hirondelle.web4j.action.Operation;
005 import hirondelle.web4j.request.RequestParser;
006 import hirondelle.web4j.util.Util;
007 import hirondelle.web4j.util.WebUtil;
008 import java.util.ArrayList;
009 import java.util.LinkedHashMap;
010 import java.util.List;
011 import java.util.Map;
012 import java.util.StringTokenizer;
013 import java.util.logging.Logger;
014 import javax.servlet.ServletConfig;
015
016 /**
017 Default implementation of {@link UntrustedProxyForUserId}.
018
019 <P>This implementation depends on settings in <tt>web.xml</tt>, which are read in by {@link #init(ServletConfig)}.
020 Later, each request URL is parsed by {@link #usesUntrustedIdentifier(RequestParser)},
021 and an attempt is made to find a match to the aforementioned settings in <tt>web.xml</tt>.
022 */
023 public final class UntrustedProxyForUserIdImpl implements UntrustedProxyForUserId {
024
025 /**
026 Read in the values of an optional <tt>init-param</tt> in <tt>web.xml</tt> named <tt>UntrustedProxyForUserId</tt>.
027
028 <P>This class uses settings in <tt>web.xml</tt> to define requests having ownership constraints that use an untrusted proxy
029 for the user id. It uses a <i>roughly</i> similar style as used for role-based constraints.
030 Here is an example of a number of several such ownership constraints defined in <tt>web.xml</tt>:<PRE><init-param>
031 <description>
032 Operations having an ownership constraint that uses an untrusted identifier.
033 </description>
034 <param-name>UntrustedProxyForUserId</param-name>
035 <param-value>
036 FoodAction.*
037 VacationAction.add
038 VacationAction.delete
039 </param-value>
040 </init-param>
041 </PRE>
042
043 <P>Each line is treated as a separate constraint, one per line. You can define as many as required.
044 The period character separates the 'noun' (the Action) from the 'verb' (the {@link Operation}).
045
046 <P>The special '*' character refers to all verbs/operations attached to a given noun/action.
047 */
048 public static void init(ServletConfig aConfig){
049 String rawValue = aConfig.getInitParameter(INIT_PARAM_NAME);
050 if( Util.textHasContent(rawValue) ) {
051 parseSettings(rawValue);
052 fLogger.fine(Util.logOnePerLine(fRestrictedOperations));
053 }
054 else {
055 fLogger.config("No ownership constraints have been set in web.xml. No init-param named " + Util.quote(INIT_PARAM_NAME));
056 }
057 }
058
059 /**
060 Return <tt>true</tt> only if the given request matches one of the items defined by the <tt>UntrustedProxyForUserId</tt> setting
061 in <tt>web.xml</tt>.
062
063 <P>For example, given the URL :
064 <PRE>'.../VacationAction.list?X=Y'</PRE>
065 this method will parse the URL into a 'noun' and a 'verb' :
066 <PRE>noun: 'VacationAction'
067 verb: 'list'</PRE>
068
069 It will then compare the noun-and-verb to the settings defined in <tt>web.xml</tt> (see {@link #init(ServletConfig)}).
070 If there is a match, then this method returns <tt>true</tt>.
071 */
072 public boolean usesUntrustedIdentifier(RequestParser aRequestParser) {
073 boolean result = false; //by default, there is no ownership constraint
074 String noun = extractNoun(aRequestParser); //WebUtil needs response too! servlet path + path info?
075 if( isRestrictedRequest(noun) ){
076 List<String> restrictedVerbs = fRestrictedOperations.get(noun);
077 if ( hasAllOperationsRestricted(restrictedVerbs) ) {
078 result = true;
079 }
080 else {
081 String verb = extractVerb(aRequestParser);
082 if ( restrictedVerbs.contains(verb) ) {
083 result = true;
084 }
085 }
086 }
087 return result;
088 }
089
090 // PRIVATE
091
092 /** Holds all restricted operations in memory. Populated upon startup. */
093 private static Map<String, List<String>> fRestrictedOperations = new LinkedHashMap<String, List<String>>();
094
095 /** Name used in web.xml. */
096 private static final String INIT_PARAM_NAME = "UntrustedProxyForUserId";
097
098 /** Special character denoting all operations/verbs. */
099 private static final String ALL_OPERATIONS = "*";
100
101 private static final Logger fLogger = Util.getLogger(UntrustedProxyForUserIdImpl.class);
102
103 private static void parseSettings(String aRawValue){
104 fLogger.fine("Parsing ownership constraints defined in web.xml.");
105 List<String> lines = parseSeparateLines(aRawValue);
106 for(String line: lines){
107 parseNounsAndVerbs(line);
108 }
109 }
110
111 private static List<String> parseSeparateLines(String aRawValue){
112 List<String> result = new ArrayList<String>();
113 StringTokenizer parser = new StringTokenizer(aRawValue, "\n\r");
114 while ( parser.hasMoreTokens() ) {
115 result.add( parser.nextToken().trim() );
116 }
117 return result;
118 }
119
120 private static void parseNounsAndVerbs(String aLine){
121 String verb = WebUtil.getFileExtension(aLine);
122 int verbStart = aLine.indexOf(".");
123 String noun = aLine.substring(0,verbStart);
124 if( isMissing(verb) || isMissing(noun) ){
125 throw new RuntimeException(
126 "This line for the " + INIT_PARAM_NAME + " setting in web.xml does not have the expected form: " + Util.quote(aLine)
127 );
128 }
129 add(noun.trim(), verb.trim());
130 }
131
132 private static boolean isMissing(String aText){
133 return ! Util.textHasContent(aText);
134 }
135
136 private static void add(String aNoun, String aVerb){
137 if( fRestrictedOperations.containsKey(aNoun) ) {
138 addAnotherVerb(aNoun, aVerb);
139 }
140 else {
141 addNewNounAndVerb(aNoun, aVerb);
142 }
143 }
144
145 private static void addNewNounAndVerb(String aNoun, String aVerb){
146 List<String> verbs = new ArrayList<String>();
147 verbs.add(aVerb);
148 fRestrictedOperations.put(aNoun, verbs);
149 }
150
151 private static void addAnotherVerb(String aNoun, String aVerb){
152 if( ALL_OPERATIONS.equals(aVerb) ){
153 fLogger.fine(Util.logOnePerLine(fRestrictedOperations));
154 throw new RuntimeException(
155 "When you use the '" + ALL_OPERATIONS + "' character to represent ALL operations, then only one line can be present for that item. " +
156 "In web.xml, you have a redundant setting for the init-param named " + INIT_PARAM_NAME +
157 " which needs to be removed. It is related to " + Util.quote(aNoun) + " and " + Util.quote(aVerb)
158 );
159 }
160 List<String> verbs = fRestrictedOperations.get(aNoun);
161 verbs.add(aVerb);
162 }
163
164 private boolean isRestrictedRequest(String aNoun){
165 return fRestrictedOperations.containsKey(aNoun);
166 }
167
168 private boolean hasAllOperationsRestricted(List<String> aVerbs){
169 return aVerbs.contains(ALL_OPERATIONS);
170 }
171
172 /**
173 For the example URL '.../BlahAction.do?X=Y', this method returns 'BlahAction' as the noun.
174 Relies on the presence of '/' and '.' characters.
175 */
176 private String extractNoun(RequestParser aRequestParser){
177 String uri = getURI(aRequestParser);
178 int firstPeriod = uri.indexOf(".");
179 if( firstPeriod == NOT_FOUND ) {
180 throw new RuntimeException("Cannot find '.' character in URL: " + Util.quote(uri));
181 }
182 int lastSlash = uri.lastIndexOf("/");
183 if( lastSlash == NOT_FOUND ) {
184 throw new RuntimeException("Cannot find '/' character in URL: " + Util.quote(uri));
185 }
186 return uri.substring(lastSlash + 1, firstPeriod);
187 }
188
189 /** Return the part of the URL after the '.' character, but before any '?' character (if present). */
190 private String extractVerb(RequestParser aRequestParser){
191 return WebUtil.getFileExtension(getURI(aRequestParser));
192 }
193
194 private String getURI(RequestParser aRequestParser){
195 return aRequestParser.getRequest().getRequestURI();
196 }
197 }