]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/kernel/DataHttp.java
Rename doAs with exception in tryAs
[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 java.io.IOException;
4 import java.io.Serializable;
5 import java.net.URL;
6 import java.security.PrivilegedActionException;
7 import java.security.PrivilegedExceptionAction;
8 import java.util.Properties;
9 import java.util.StringTokenizer;
10
11 import javax.jcr.Repository;
12 import javax.jcr.RepositoryException;
13 import javax.jcr.Session;
14 import javax.security.auth.Subject;
15 import javax.security.auth.callback.Callback;
16 import javax.security.auth.callback.CallbackHandler;
17 import javax.security.auth.callback.NameCallback;
18 import javax.security.auth.callback.PasswordCallback;
19 import javax.security.auth.login.CredentialNotFoundException;
20 import javax.security.auth.login.LoginContext;
21 import javax.security.auth.login.LoginException;
22 import javax.servlet.ServletException;
23 import javax.servlet.http.HttpServletRequest;
24 import javax.servlet.http.HttpServletResponse;
25
26 import org.apache.commons.codec.binary.Base64;
27 import org.apache.commons.logging.Log;
28 import org.apache.commons.logging.LogFactory;
29 import org.apache.jackrabbit.server.SessionProvider;
30 import org.apache.jackrabbit.server.remoting.davex.JcrRemotingServlet;
31 import org.apache.jackrabbit.webdav.simple.SimpleWebdavServlet;
32 import org.argeo.cms.CmsException;
33 import org.argeo.cms.auth.HttpRequestCallback;
34 import org.argeo.cms.auth.HttpRequestCallbackHandler;
35 import org.argeo.jcr.JcrUtils;
36 import org.argeo.node.NodeConstants;
37 import org.osgi.framework.BundleContext;
38 import org.osgi.framework.FrameworkUtil;
39 import org.osgi.framework.ServiceReference;
40 import org.osgi.service.http.HttpContext;
41 import org.osgi.service.http.HttpService;
42 import org.osgi.service.http.NamespaceException;
43 import org.osgi.service.useradmin.Authorization;
44 import org.osgi.util.tracker.ServiceTracker;
45 import org.osgi.util.tracker.ServiceTrackerCustomizer;
46
47 /**
48 * Intercepts and enriches http access, mainly focusing on security and
49 * transactionality.
50 */
51 class DataHttp implements KernelConstants {
52 private final static Log log = LogFactory.getLog(DataHttp.class);
53
54 // private final static String ATTR_AUTH = "auth";
55 private final static String HEADER_AUTHORIZATION = "Authorization";
56 private final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
57
58 private final static String DEFAULT_PROTECTED_HANDLERS = "/org/argeo/cms/internal/kernel/protectedHandlers.xml";
59
60 private final BundleContext bc;
61 private final HttpService httpService;
62 private final ServiceTracker<Repository, Repository> repositories;
63
64 // FIXME Make it more unique
65 private String httpAuthRealm = "Argeo";
66
67 DataHttp(HttpService httpService) {
68 this.bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
69 this.httpService = httpService;
70 repositories = new ServiceTracker<>(bc, Repository.class, new RepositoriesStc());
71 repositories.open();
72 }
73
74 public void destroy() {
75 repositories.close();
76 }
77
78 void registerRepositoryServlets(String alias, Repository repository) {
79 try {
80 registerWebdavServlet(alias, repository);
81 registerRemotingServlet(alias, repository);
82 if (log.isDebugEnabled())
83 log.debug("Registered servlets for repository '" + alias + "'");
84 } catch (Exception e) {
85 throw new CmsException("Could not register servlets for repository '" + alias + "'", e);
86 }
87 }
88
89 void unregisterRepositoryServlets(String alias) {
90 try {
91 httpService.unregister(webdavPath(alias));
92 httpService.unregister(remotingPath(alias));
93 if (log.isDebugEnabled())
94 log.debug("Unregistered servlets for repository '" + alias + "'");
95 } catch (Exception e) {
96 log.error("Could not unregister servlets for repository '" + alias + "'", e);
97 }
98 }
99
100 void registerWebdavServlet(String alias, Repository repository) throws NamespaceException, ServletException {
101 WebdavServlet webdavServlet = new WebdavServlet(repository, new OpenInViewSessionProvider(alias));
102 String path = webdavPath(alias);
103 Properties ip = new Properties();
104 ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_CONFIG, WEBDAV_CONFIG);
105 ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path);
106 httpService.registerServlet(path, webdavServlet, ip, new DataHttpContext());
107 }
108
109 void registerRemotingServlet(String alias, Repository repository) throws NamespaceException, ServletException {
110 RemotingServlet remotingServlet = new RemotingServlet(repository, new OpenInViewSessionProvider(alias));
111 String path = remotingPath(alias);
112 Properties ip = new Properties();
113 ip.setProperty(JcrRemotingServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path);
114
115 // Looks like a bug in Jackrabbit remoting init
116 ip.setProperty(RemotingServlet.INIT_PARAM_HOME, KernelUtils.getOsgiInstanceDir() + "/tmp/remoting_" + alias);
117 ip.setProperty(RemotingServlet.INIT_PARAM_TMP_DIRECTORY, "remoting_" + alias);
118 ip.setProperty(RemotingServlet.INIT_PARAM_PROTECTED_HANDLERS_CONFIG, DEFAULT_PROTECTED_HANDLERS);
119 ip.setProperty(RemotingServlet.INIT_PARAM_CREATE_ABSOLUTE_URI, "false");
120 httpService.registerServlet(path, remotingServlet, ip, new RemotingHttpContext());
121 }
122
123 private String webdavPath(String alias) {
124 return NodeConstants.PATH_DATA + "/" + alias;
125 }
126
127 private String remotingPath(String alias) {
128 return NodeConstants.PATH_JCR + "/" + alias;
129 }
130
131 private Subject subjectFromRequest(HttpServletRequest request) {
132 Authorization authorization = (Authorization) request.getAttribute(HttpContext.AUTHORIZATION);
133 if (authorization == null)
134 throw new CmsException("Not authenticated");
135 try {
136 LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER,
137 new HttpRequestCallbackHandler(request));
138 lc.login();
139 return lc.getSubject();
140 } catch (LoginException e) {
141 throw new CmsException("Cannot login", e);
142 }
143 }
144
145 private void requestBasicAuth(HttpServletRequest request, HttpServletResponse response) {
146 response.setStatus(401);
147 response.setHeader(HEADER_WWW_AUTHENTICATE, "basic realm=\"" + httpAuthRealm + "\"");
148 }
149
150 private CallbackHandler basicAuth(final HttpServletRequest httpRequest) {
151 String authHeader = httpRequest.getHeader(HEADER_AUTHORIZATION);
152 if (authHeader != null) {
153 StringTokenizer st = new StringTokenizer(authHeader);
154 if (st.hasMoreTokens()) {
155 String basic = st.nextToken();
156 if (basic.equalsIgnoreCase("Basic")) {
157 try {
158 // TODO manipulate char[]
159 String credentials = new String(Base64.decodeBase64(st.nextToken()), "UTF-8");
160 // log.debug("Credentials: " + credentials);
161 int p = credentials.indexOf(":");
162 if (p != -1) {
163 final String login = credentials.substring(0, p).trim();
164 final char[] password = credentials.substring(p + 1).trim().toCharArray();
165 return new CallbackHandler() {
166 public void handle(Callback[] callbacks) {
167 for (Callback cb : callbacks) {
168 if (cb instanceof NameCallback)
169 ((NameCallback) cb).setName(login);
170 else if (cb instanceof PasswordCallback)
171 ((PasswordCallback) cb).setPassword(password);
172 else if (cb instanceof HttpRequestCallback)
173 ((HttpRequestCallback) cb).setRequest(httpRequest);
174 }
175 }
176 };
177 } else {
178 throw new CmsException("Invalid authentication token");
179 }
180 } catch (Exception e) {
181 throw new CmsException("Couldn't retrieve authentication", e);
182 }
183 }
184 }
185 }
186 return null;
187 }
188
189 private class RepositoriesStc implements ServiceTrackerCustomizer<Repository, Repository> {
190
191 @Override
192 public Repository addingService(ServiceReference<Repository> reference) {
193 Repository repository = bc.getService(reference);
194 Object jcrRepoAlias = reference.getProperty(NodeConstants.CN);
195 if (jcrRepoAlias != null) {
196 String alias = jcrRepoAlias.toString();
197 registerRepositoryServlets(alias, repository);
198 }
199 return repository;
200 }
201
202 @Override
203 public void modifiedService(ServiceReference<Repository> reference, Repository service) {
204 }
205
206 @Override
207 public void removedService(ServiceReference<Repository> reference, Repository service) {
208 Object jcrRepoAlias = reference.getProperty(NodeConstants.CN);
209 if (jcrRepoAlias != null) {
210 String alias = jcrRepoAlias.toString();
211 unregisterRepositoryServlets(alias);
212 }
213 }
214 }
215
216 private class DataHttpContext implements HttpContext {
217 @Override
218 public boolean handleSecurity(final HttpServletRequest request, HttpServletResponse response)
219 throws IOException {
220 if (log.isTraceEnabled())
221 KernelUtils.logRequestHeaders(log, request);
222 LoginContext lc;
223 try {
224 lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, new HttpRequestCallbackHandler(request));
225 lc.login();
226 // return true;
227 } catch (LoginException e) {
228 CallbackHandler token = basicAuth(request);
229 if (token != null) {
230 try {
231 lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, token);
232 lc.login();
233 // Note: this is impossible to reliably clear the
234 // authorization header when access from a browser.
235 return true;
236 } catch (LoginException e1) {
237 throw new CmsException("Could not login", e1);
238 }
239 } else {
240 // anonymous
241 try {
242 lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER);
243 lc.login();
244 } catch (LoginException e1) {
245 if (log.isDebugEnabled())
246 log.error("Cannot log in anonynous", e1);
247 return false;
248 }
249 }
250 }
251 request.setAttribute(NodeConstants.LOGIN_CONTEXT_USER, lc);
252 return true;
253 }
254
255 @Override
256 public URL getResource(String name) {
257 return KernelUtils.getBundleContext(DataHttp.class).getBundle().getResource(name);
258 }
259
260 @Override
261 public String getMimeType(String name) {
262 return null;
263 }
264
265 }
266
267 private class RemotingHttpContext implements HttpContext {
268 // private final boolean anonymous;
269
270 RemotingHttpContext() {
271 // this.anonymous = anonymous;
272 }
273
274 @Override
275 public boolean handleSecurity(final HttpServletRequest request, HttpServletResponse response)
276 throws IOException {
277
278 // if (anonymous) {
279 // Subject subject = KernelUtils.anonymousLogin();
280 // Authorization authorization =
281 // subject.getPrivateCredentials(Authorization.class).iterator().next();
282 // request.setAttribute(REMOTE_USER, NodeConstants.ROLE_ANONYMOUS);
283 // request.setAttribute(AUTHORIZATION, authorization);
284 // return true;
285 // }
286
287 if (log.isTraceEnabled())
288 KernelUtils.logRequestHeaders(log, request);
289 LoginContext lc;
290 try {
291 lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, new HttpRequestCallbackHandler(request));
292 lc.login();
293 } catch (CredentialNotFoundException e) {
294 CallbackHandler token = basicAuth(request);
295 if (token != null) {
296 try {
297 lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, token);
298 lc.login();
299 // Note: this is impossible to reliably clear the
300 // authorization header when access from a browser.
301 } catch (LoginException e1) {
302 throw new CmsException("Could not login", e1);
303 }
304 } else {
305 requestBasicAuth(request, response);
306 lc = null;
307 }
308 } catch (LoginException e) {
309 throw new CmsException("Could not login", e);
310 }
311
312 if (lc != null) {
313 request.setAttribute(NodeConstants.LOGIN_CONTEXT_USER, lc);
314 return true;
315 } else {
316 return false;
317 }
318 }
319
320 @Override
321 public URL getResource(String name) {
322 return KernelUtils.getBundleContext(DataHttp.class).getBundle().getResource(name);
323 }
324
325 @Override
326 public String getMimeType(String name) {
327 return null;
328 }
329
330 }
331
332 /**
333 * Implements an open session in view patter: a new JCR session is created
334 * for each request
335 */
336 private class OpenInViewSessionProvider implements SessionProvider, Serializable {
337 private static final long serialVersionUID = 2270957712453841368L;
338 private final String alias;
339
340 public OpenInViewSessionProvider(String alias) {
341 this.alias = alias;
342 }
343
344 public Session getSession(HttpServletRequest request, Repository rep, String workspace)
345 throws javax.jcr.LoginException, ServletException, RepositoryException {
346 return login(request, rep, workspace);
347 }
348
349 protected Session login(HttpServletRequest request, Repository repository, String workspace)
350 throws RepositoryException {
351 if (log.isTraceEnabled())
352 log.trace("Repo " + alias + ", login to workspace " + (workspace == null ? "<default>" : workspace)
353 + " in web session " + request.getSession().getId());
354 LoginContext lc = (LoginContext) request.getAttribute(NodeConstants.LOGIN_CONTEXT_USER);
355 if (lc == null)
356 throw new CmsException("No login context available");
357 try {
358 // LoginContext lc = new
359 // LoginContext(NodeConstants.LOGIN_CONTEXT_USER,
360 // new HttpRequestCallbackHandler(request));
361 // lc.login();
362 return Subject.doAs(lc.getSubject(), new PrivilegedExceptionAction<Session>() {
363 @Override
364 public Session run() throws Exception {
365 return repository.login(workspace);
366 }
367 });
368 } catch (Exception e) {
369 throw new CmsException("Cannot log in to JCR", e);
370 }
371 // return repository.login(workspace);
372 }
373
374 public void releaseSession(Session session) {
375 JcrUtils.logoutQuietly(session);
376 if (log.isTraceEnabled())
377 log.trace("Logged out remote JCR session " + session);
378 }
379 }
380
381 private class WebdavServlet extends SimpleWebdavServlet {
382 private static final long serialVersionUID = -4687354117811443881L;
383 private final Repository repository;
384
385 public WebdavServlet(Repository repository, SessionProvider sessionProvider) {
386 this.repository = repository;
387 setSessionProvider(sessionProvider);
388 }
389
390 public Repository getRepository() {
391 return repository;
392 }
393
394 @Override
395 protected void service(final HttpServletRequest request, final HttpServletResponse response)
396 throws ServletException, IOException {
397 WebdavServlet.super.service(request, response);
398 // try {
399 // Subject subject = subjectFromRequest(request);
400 // // TODO make it stronger, with eTags.
401 // // if (CurrentUser.isAnonymous(subject) &&
402 // // request.getMethod().equals("GET")) {
403 // // response.setHeader("Cache-Control", "no-transform, public,
404 // // max-age=300, s-maxage=900");
405 // // }
406 //
407 // Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
408 // @Override
409 // public Void run() throws Exception {
410 // WebdavServlet.super.service(request, response);
411 // return null;
412 // }
413 // });
414 // } catch (PrivilegedActionException e) {
415 // throw new CmsException("Cannot process webdav request",
416 // e.getException());
417 // }
418 }
419 }
420
421 private class RemotingServlet extends JcrRemotingServlet {
422 private static final long serialVersionUID = 4605238259548058883L;
423 private final Repository repository;
424 private final SessionProvider sessionProvider;
425
426 public RemotingServlet(Repository repository, SessionProvider sessionProvider) {
427 this.repository = repository;
428 this.sessionProvider = sessionProvider;
429 }
430
431 @Override
432 protected Repository getRepository() {
433 return repository;
434 }
435
436 @Override
437 protected SessionProvider getSessionProvider() {
438 return sessionProvider;
439 }
440
441 @Override
442 protected void service(final HttpServletRequest request, final HttpServletResponse response)
443 throws ServletException, IOException {
444 try {
445 Subject subject = subjectFromRequest(request);
446 Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
447 @Override
448 public Void run() throws Exception {
449 RemotingServlet.super.service(request, response);
450 return null;
451 }
452 });
453 } catch (PrivilegedActionException e) {
454 throw new CmsException("Cannot process JCR remoting request", e.getException());
455 }
456 }
457 }
458 }