James Moger
2011-09-26 2179fb76bbbd2021c350a7c28d6901389ed50b2b
commit | author | age
8c9a20 1 /*
JM 2  * Copyright 2011 gitblit.com.
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 com.gitblit;
17
18 import java.io.IOException;
5450d0 19 import java.nio.charset.Charset;
8c9a20 20 import java.security.Principal;
JM 21 import java.text.MessageFormat;
22 import java.util.Enumeration;
23 import java.util.HashMap;
24 import java.util.Map;
25
26 import javax.servlet.Filter;
27 import javax.servlet.FilterChain;
28 import javax.servlet.FilterConfig;
29 import javax.servlet.ServletException;
30 import javax.servlet.ServletRequest;
31 import javax.servlet.ServletResponse;
32 import javax.servlet.http.HttpServletRequest;
33 import javax.servlet.http.HttpServletResponse;
34 import javax.servlet.http.HttpSession;
35
5450d0 36 import org.eclipse.jgit.util.Base64;
8c9a20 37 import org.slf4j.Logger;
JM 38 import org.slf4j.LoggerFactory;
39
40 import com.gitblit.models.RepositoryModel;
41 import com.gitblit.models.UserModel;
42 import com.gitblit.utils.StringUtils;
43
44 /**
892570 45  * The AccessRestrictionFilter is a servlet filter that preprocesses requests
JM 46  * that match its url pattern definition in the web.xml file.
47  * 
48  * The filter extracts the name of the repository from the url and determines if
49  * the requested action for the repository requires a Basic authentication
50  * prompt. If authentication is required and no credentials are stored in the
51  * "Authorization" header, then a basic authentication challenge is issued.
8c9a20 52  * 
JM 53  * http://en.wikipedia.org/wiki/Basic_access_authentication
892570 54  * 
JM 55  * @author James Moger
56  * 
8c9a20 57  */
JM 58 public abstract class AccessRestrictionFilter implements Filter {
59
60     private static final String BASIC = "Basic";
61
62     private static final String CHALLENGE = BASIC + " realm=\"" + Constants.NAME + "\"";
63
64     private static final String SESSION_SECURED = "com.gitblit.secured";
65
66     protected transient Logger logger;
67
68     public AccessRestrictionFilter() {
69         logger = LoggerFactory.getLogger(getClass());
70     }
71
892570 72     /**
JM 73      * Extract the repository name from the url.
74      * 
75      * @param url
76      * @return repository name
77      */
8c9a20 78     protected abstract String extractRepositoryName(String url);
JM 79
892570 80     /**
JM 81      * Analyze the url and returns the action of the request.
82      * 
83      * @param url
84      * @return action of the request
85      */
86     protected abstract String getUrlRequestAction(String url);
8c9a20 87
892570 88     /**
JM 89      * Determine if the repository requires authentication.
90      * 
91      * @param repository
92      * @return true if authentication required
93      */
8c9a20 94     protected abstract boolean requiresAuthentication(RepositoryModel repository);
JM 95
892570 96     /**
JM 97      * Determine if the user can access the repository and perform the specified
98      * action.
99      * 
100      * @param repository
101      * @param user
102      * @param action
103      * @return true if user may execute the action on the repository
104      */
105     protected abstract boolean canAccess(RepositoryModel repository, UserModel user, String action);
8c9a20 106
892570 107     /**
JM 108      * doFilter does the actual work of preprocessing the request to ensure that
109      * the user may proceed.
110      * 
88598b 111      * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
JM 112      *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
892570 113      */
8c9a20 114     @Override
JM 115     public void doFilter(final ServletRequest request, final ServletResponse response,
116             final FilterChain chain) throws IOException, ServletException {
117
118         HttpServletRequest httpRequest = (HttpServletRequest) request;
119         HttpServletResponse httpResponse = (HttpServletResponse) response;
120
121         // Wrap the HttpServletRequest with the AccessRestrictionRequest which
122         // overrides the servlet container user principal methods.
123         // JGit requires either:
124         //
125         // 1. servlet container authenticated user
126         // 2. http.receivepack = true in each repository's config
127         //
128         // Gitblit must conditionally authenticate users per-repository so just
129         // enabling http.receivepack is insufficient.
130
131         AccessRestrictionRequest accessRequest = new AccessRestrictionRequest(httpRequest);
132
2179fb 133         String servletUrl = httpRequest.getContextPath() + httpRequest.getServletPath();
JM 134         String url = httpRequest.getRequestURI().substring(servletUrl.length());
8c9a20 135         String params = httpRequest.getQueryString();
JM 136         if (url.length() > 0 && url.charAt(0) == '/') {
137             url = url.substring(1);
138         }
139         String fullUrl = url + (StringUtils.isEmpty(params) ? "" : ("?" + params));
140
141         String repository = extractRepositoryName(url);
142
143         // Determine if the request URL is restricted
144         String fullSuffix = fullUrl.substring(repository.length());
892570 145         String urlRequestType = getUrlRequestAction(fullSuffix);
8c9a20 146
JM 147         // Load the repository model
148         RepositoryModel model = GitBlit.self().getRepositoryModel(repository);
149         if (model == null) {
150             // repository not found. send 404.
151             logger.info("ARF: " + fullUrl + " (" + HttpServletResponse.SC_NOT_FOUND + ")");
152             httpResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
153             return;
154         }
155
156         // BASIC authentication challenge and response processing
157         if (!StringUtils.isEmpty(urlRequestType) && requiresAuthentication(model)) {
158             // look for client authorization credentials in header
159             final String authorization = httpRequest.getHeader("Authorization");
160             if (authorization != null && authorization.startsWith(BASIC)) {
161                 // Authorization: Basic base64credentials
162                 String base64Credentials = authorization.substring(BASIC.length()).trim();
5450d0 163                 String credentials = new String(Base64.decode(base64Credentials),
JM 164                         Charset.forName("UTF-8"));
8c9a20 165                 // credentials = username:password
JM 166                 final String[] values = credentials.split(":");
167
168                 if (values.length == 2) {
169                     String username = values[0];
170                     char[] password = values[1].toCharArray();
171                     UserModel user = GitBlit.self().authenticate(username, password);
172                     if (user != null) {
173                         accessRequest.setUser(user);
174                         if (user.canAdmin || canAccess(model, user, urlRequestType)) {
175                             // authenticated request permitted.
176                             // pass processing to the restricted servlet.
177                             newSession(accessRequest, httpResponse);
5450d0 178                             logger.info("ARF: " + fullUrl + " (" + HttpServletResponse.SC_CONTINUE
JM 179                                     + ") authenticated");
8c9a20 180                             chain.doFilter(accessRequest, httpResponse);
JM 181                             return;
182                         }
183                         // valid user, but not for requested access. send 403.
184                         if (GitBlit.isDebugMode()) {
185                             logger.info("ARF: " + fullUrl + " (" + HttpServletResponse.SC_FORBIDDEN
186                                     + ")");
187                             logger.info(MessageFormat.format("AUTH: {0} forbidden to access {1}",
188                                     user.username, url));
189                         }
190                         httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
191                         return;
192                     }
193                 }
194                 if (GitBlit.isDebugMode()) {
195                     logger.info(MessageFormat
196                             .format("AUTH: invalid credentials ({0})", credentials));
197                 }
198             }
199
200             // challenge client to provide credentials. send 401.
201             if (GitBlit.isDebugMode()) {
202                 logger.info("ARF: " + fullUrl + " (" + HttpServletResponse.SC_UNAUTHORIZED + ")");
203                 logger.info("AUTH: Challenge " + CHALLENGE);
204             }
205             httpResponse.setHeader("WWW-Authenticate", CHALLENGE);
206             httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
207             return;
208         }
209
210         if (GitBlit.isDebugMode()) {
5450d0 211             logger.info("ARF: " + fullUrl + " (" + HttpServletResponse.SC_CONTINUE
JM 212                     + ") unauthenticated");
8c9a20 213         }
JM 214         // unauthenticated request permitted.
215         // pass processing to the restricted servlet.
216         chain.doFilter(accessRequest, httpResponse);
217     }
218
219     /**
220      * Taken from Jetty's LoginAuthenticator.renewSessionOnAuthentication()
221      */
222     protected void newSession(HttpServletRequest request, HttpServletResponse response) {
223         HttpSession oldSession = request.getSession(false);
224         if (oldSession != null && oldSession.getAttribute(SESSION_SECURED) == null) {
225             synchronized (this) {
226                 Map<String, Object> attributes = new HashMap<String, Object>();
227                 Enumeration<String> e = oldSession.getAttributeNames();
228                 while (e.hasMoreElements()) {
229                     String name = e.nextElement();
230                     attributes.put(name, oldSession.getAttribute(name));
231                     oldSession.removeAttribute(name);
232                 }
233                 oldSession.invalidate();
234
235                 HttpSession newSession = request.getSession(true);
236                 newSession.setAttribute(SESSION_SECURED, Boolean.TRUE);
237                 for (Map.Entry<String, Object> entry : attributes.entrySet()) {
238                     newSession.setAttribute(entry.getKey(), entry.getValue());
239                 }
240             }
241         }
242     }
243
892570 244     /**
JM 245      * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
246      */
8c9a20 247     @Override
JM 248     public void init(final FilterConfig config) throws ServletException {
249     }
250
892570 251     /**
JM 252      * @see javax.servlet.Filter#destroy()
253      */
8c9a20 254     @Override
JM 255     public void destroy() {
256     }
5450d0 257
8c9a20 258     /**
JM 259      * Wraps a standard HttpServletRequest and overrides user principal methods.
260      */
261     public static class AccessRestrictionRequest extends ServletRequestWrapper {
262
263         private UserModel user;
5450d0 264
8c9a20 265         public AccessRestrictionRequest(HttpServletRequest req) {
JM 266             super(req);
267             user = new UserModel("anonymous");
268         }
5450d0 269
8c9a20 270         void setUser(UserModel user) {
JM 271             this.user = user;
272         }
273
274         @Override
275         public String getRemoteUser() {
276             return user.username;
277         }
278
279         @Override
280         public boolean isUserInRole(String role) {
281             if (role.equals(Constants.ADMIN_ROLE)) {
282                 return user.canAdmin;
283             }
284             return user.canAccessRepository(role);
285         }
286
287         @Override
288         public Principal getUserPrincipal() {
289             return user;
290         }
291     }
292 }