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