]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/kernel/DataHttp.java
Fix broken auth with JCR remoting.
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / internal / kernel / DataHttp.java
1 package org.argeo.cms.internal.kernel;
2
3 import static org.argeo.cms.auth.AuthConstants.LOGIN_CONTEXT_USER;
4
5 import java.io.IOException;
6 import java.io.Serializable;
7 import java.net.URL;
8 import java.security.PrivilegedActionException;
9 import java.security.PrivilegedExceptionAction;
10 import java.util.Properties;
11 import java.util.StringTokenizer;
12
13 import javax.jcr.Repository;
14 import javax.jcr.RepositoryException;
15 import javax.jcr.Session;
16 import javax.security.auth.Subject;
17 import javax.security.auth.callback.Callback;
18 import javax.security.auth.callback.CallbackHandler;
19 import javax.security.auth.callback.NameCallback;
20 import javax.security.auth.callback.PasswordCallback;
21 import javax.security.auth.login.CredentialNotFoundException;
22 import javax.security.auth.login.LoginContext;
23 import javax.security.auth.login.LoginException;
24 import javax.servlet.ServletException;
25 import javax.servlet.http.HttpServletRequest;
26 import javax.servlet.http.HttpServletResponse;
27
28 import org.apache.commons.codec.binary.Base64;
29 import org.apache.commons.logging.Log;
30 import org.apache.commons.logging.LogFactory;
31 import org.apache.jackrabbit.server.SessionProvider;
32 import org.apache.jackrabbit.server.remoting.davex.JcrRemotingServlet;
33 import org.apache.jackrabbit.webdav.simple.SimpleWebdavServlet;
34 import org.argeo.cms.CmsException;
35 import org.argeo.cms.auth.AuthConstants;
36 import org.argeo.cms.auth.CurrentUser;
37 import org.argeo.cms.auth.HttpRequestCallback;
38 import org.argeo.cms.auth.HttpRequestCallbackHandler;
39 import org.argeo.jcr.ArgeoJcrConstants;
40 import org.argeo.jcr.JcrUtils;
41 import org.osgi.service.http.HttpContext;
42 import org.osgi.service.http.HttpService;
43 import org.osgi.service.http.NamespaceException;
44 import org.osgi.service.useradmin.Authorization;
45
46 /**
47 * Intercepts and enriches http access, mainly focusing on security and
48 * transactionality.
49 */
50 class DataHttp implements KernelConstants, ArgeoJcrConstants {
51 private final static Log log = LogFactory.getLog(DataHttp.class);
52
53 // private final static String ATTR_AUTH = "auth";
54 private final static String HEADER_AUTHORIZATION = "Authorization";
55 private final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
56
57 private final HttpService httpService;
58
59 // FIXME Make it more unique
60 private String httpAuthRealm = "Argeo";
61
62 // WebDav / JCR remoting
63 private OpenInViewSessionProvider sessionProvider;
64
65 DataHttp(HttpService httpService) {
66 this.httpService = httpService;
67 sessionProvider = new OpenInViewSessionProvider();
68 // registerRepositoryServlets(ALIAS_NODE, node);
69 }
70
71 public void destroy() {
72 // unregisterRepositoryServlets(ALIAS_NODE);
73 }
74
75 void registerRepositoryServlets(String alias, Repository repository) {
76 try {
77 registerWebdavServlet(alias, repository, true);
78 registerWebdavServlet(alias, repository, false);
79 registerRemotingServlet(alias, repository, true);
80 registerRemotingServlet(alias, repository, false);
81 } catch (Exception e) {
82 throw new CmsException("Could not register servlets for repository " + alias, e);
83 }
84 }
85
86 void unregisterRepositoryServlets(String alias) {
87 // FIXME unregister servlets
88 }
89
90 void registerWebdavServlet(String alias, Repository repository, boolean anonymous)
91 throws NamespaceException, ServletException {
92 WebdavServlet webdavServlet = new WebdavServlet(repository, sessionProvider);
93 String pathPrefix = anonymous ? WEBDAV_PUBLIC : WEBDAV_PRIVATE;
94 String path = pathPrefix + "/" + alias;
95 Properties ip = new Properties();
96 ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_CONFIG, WEBDAV_CONFIG);
97 ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path);
98 httpService.registerServlet(path, webdavServlet, ip, new DataHttpContext(anonymous));
99 }
100
101 void registerRemotingServlet(String alias, Repository repository, boolean anonymous)
102 throws NamespaceException, ServletException {
103 String pathPrefix = anonymous ? REMOTING_PUBLIC : REMOTING_PRIVATE;
104 RemotingServlet remotingServlet = new RemotingServlet(repository, sessionProvider);
105 String path = pathPrefix + "/" + alias;
106 Properties ip = new Properties();
107 ip.setProperty(JcrRemotingServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path);
108
109 // Looks like a bug in Jackrabbit remoting init
110 ip.setProperty(RemotingServlet.INIT_PARAM_HOME, KernelUtils.getOsgiInstanceDir() + "/tmp/jackrabbit");
111 ip.setProperty(RemotingServlet.INIT_PARAM_TMP_DIRECTORY, "remoting");
112 // in order to avoid annoying warning.
113 ip.setProperty(RemotingServlet.INIT_PARAM_PROTECTED_HANDLERS_CONFIG, "");
114 httpService.registerServlet(path, remotingServlet, ip, new DataHttpContext(anonymous));
115 }
116
117 private Subject subjectFromRequest(HttpServletRequest request) {
118 Authorization authorization = (Authorization) request.getAttribute(HttpContext.AUTHORIZATION);
119 if (authorization == null)
120 throw new CmsException("Not authenticated");
121 try {
122 LoginContext lc = new LoginContext(AuthConstants.LOGIN_CONTEXT_USER,
123 new HttpRequestCallbackHandler(request));
124 lc.login();
125 return lc.getSubject();
126 } catch (LoginException e) {
127 throw new CmsException("Cannot login", e);
128 }
129 }
130
131 private class DataHttpContext implements HttpContext {
132 private final boolean anonymous;
133
134 DataHttpContext(boolean anonymous) {
135 this.anonymous = anonymous;
136 }
137
138 @Override
139 public boolean handleSecurity(final HttpServletRequest request, HttpServletResponse response)
140 throws IOException {
141
142 if (anonymous) {
143 Subject subject = KernelUtils.anonymousLogin();
144 Authorization authorization = subject.getPrivateCredentials(Authorization.class).iterator().next();
145 request.setAttribute(REMOTE_USER, AuthConstants.ROLE_ANONYMOUS);
146 request.setAttribute(AUTHORIZATION, authorization);
147 return true;
148 }
149
150 if (log.isTraceEnabled())
151 KernelUtils.logRequestHeaders(log, request);
152 try {
153 new LoginContext(LOGIN_CONTEXT_USER, new HttpRequestCallbackHandler(request)).login();
154 return true;
155 } catch (CredentialNotFoundException e) {
156 CallbackHandler token = basicAuth(request);
157 if (token != null) {
158 try {
159 LoginContext lc = new LoginContext(LOGIN_CONTEXT_USER, token);
160 lc.login();
161 // Note: this is impossible to reliably clear the
162 // authorization header when access from a browser.
163 return true;
164 } catch (LoginException e1) {
165 throw new CmsException("Could not login", e1);
166 }
167 } else {
168 String path = request.getServletPath();
169 if (path.startsWith(REMOTING_PRIVATE))
170 requestBasicAuth(request, response);
171 return false;
172 }
173 } catch (LoginException e) {
174 throw new CmsException("Could not login", e);
175 }
176 }
177
178 @Override
179 public URL getResource(String name) {
180 return Activator.getBundleContext().getBundle().getResource(name);
181 }
182
183 @Override
184 public String getMimeType(String name) {
185 return null;
186 }
187
188 private void requestBasicAuth(HttpServletRequest request, HttpServletResponse response) {
189 response.setStatus(401);
190 response.setHeader(HEADER_WWW_AUTHENTICATE, "basic realm=\"" + httpAuthRealm + "\"");
191 // request.getSession().setAttribute(ATTR_AUTH, Boolean.TRUE);
192 }
193
194 private CallbackHandler basicAuth(final HttpServletRequest httpRequest) {
195 String authHeader = httpRequest.getHeader(HEADER_AUTHORIZATION);
196 if (authHeader != null) {
197 StringTokenizer st = new StringTokenizer(authHeader);
198 if (st.hasMoreTokens()) {
199 String basic = st.nextToken();
200 if (basic.equalsIgnoreCase("Basic")) {
201 try {
202 // TODO manipulate char[]
203 String credentials = new String(Base64.decodeBase64(st.nextToken()), "UTF-8");
204 // log.debug("Credentials: " + credentials);
205 int p = credentials.indexOf(":");
206 if (p != -1) {
207 final String login = credentials.substring(0, p).trim();
208 final char[] password = credentials.substring(p + 1).trim().toCharArray();
209 return new CallbackHandler() {
210 public void handle(Callback[] callbacks) {
211 for (Callback cb : callbacks) {
212 if (cb instanceof NameCallback)
213 ((NameCallback) cb).setName(login);
214 else if (cb instanceof PasswordCallback)
215 ((PasswordCallback) cb).setPassword(password);
216 else if (cb instanceof HttpRequestCallback)
217 ((HttpRequestCallback) cb).setRequest(httpRequest);
218 }
219 }
220 };
221 } else {
222 throw new CmsException("Invalid authentication token");
223 }
224 } catch (Exception e) {
225 throw new CmsException("Couldn't retrieve authentication", e);
226 }
227 }
228 }
229 }
230 return null;
231 }
232
233 }
234
235 /**
236 * Implements an open session in view patter: a new JCR session is created
237 * for each request
238 */
239 private class OpenInViewSessionProvider implements SessionProvider, Serializable {
240 private static final long serialVersionUID = 2270957712453841368L;
241
242 public Session getSession(HttpServletRequest request, Repository rep, String workspace)
243 throws javax.jcr.LoginException, ServletException, RepositoryException {
244 return login(request, rep, workspace);
245 }
246
247 protected Session login(HttpServletRequest request, Repository repository, String workspace)
248 throws RepositoryException {
249 if (log.isTraceEnabled())
250 log.trace("Login to workspace " + (workspace == null ? "<default>" : workspace) + " in web session "
251 + request.getSession().getId());
252 return repository.login(workspace);
253 }
254
255 public void releaseSession(Session session) {
256 JcrUtils.logoutQuietly(session);
257 if (log.isTraceEnabled())
258 log.trace("Logged out remote JCR session " + session);
259 }
260 }
261
262 private class WebdavServlet extends SimpleWebdavServlet {
263 private static final long serialVersionUID = -4687354117811443881L;
264 private final Repository repository;
265
266 public WebdavServlet(Repository repository, SessionProvider sessionProvider) {
267 this.repository = repository;
268 setSessionProvider(sessionProvider);
269 }
270
271 public Repository getRepository() {
272 return repository;
273 }
274
275 @Override
276 protected void service(final HttpServletRequest request, final HttpServletResponse response)
277 throws ServletException, IOException {
278 try {
279 Subject subject = subjectFromRequest(request);
280 if (CurrentUser.isAnonymous(subject) && request.getMethod().equals("GET")) {
281 response.setHeader("Cache-Control", "no-transform, public, max-age=300, s-maxage=900");
282 }
283
284 Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
285 @Override
286 public Void run() throws Exception {
287 WebdavServlet.super.service(request, response);
288 return null;
289 }
290 });
291 } catch (PrivilegedActionException e) {
292 throw new CmsException("Cannot process webdav request", e.getException());
293 }
294 }
295 }
296
297 private class RemotingServlet extends JcrRemotingServlet {
298 private static final long serialVersionUID = 4605238259548058883L;
299 private final Repository repository;
300 private final SessionProvider sessionProvider;
301
302 public RemotingServlet(Repository repository, SessionProvider sessionProvider) {
303 this.repository = repository;
304 this.sessionProvider = sessionProvider;
305 }
306
307 @Override
308 protected Repository getRepository() {
309 return repository;
310 }
311
312 @Override
313 protected SessionProvider getSessionProvider() {
314 return sessionProvider;
315 }
316
317 @Override
318 protected void service(final HttpServletRequest request, final HttpServletResponse response)
319 throws ServletException, IOException {
320 try {
321 Subject subject = subjectFromRequest(request);
322 Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
323 @Override
324 public Void run() throws Exception {
325 RemotingServlet.super.service(request, response);
326 return null;
327 }
328 });
329 } catch (PrivilegedActionException e) {
330 throw new CmsException("Cannot process JCR remoting request", e.getException());
331 }
332 }
333 }
334 }