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