001: /**********************************************************************************
002: * $URL: https://source.sakaiproject.org/svn/access/tags/sakai_2-4-1/access-impl/impl/src/java/org/sakaiproject/access/tool/AccessServlet.java $
003: * $Id: AccessServlet.java 17063 2006-10-11 19:48:42Z jimeng@umich.edu $
004: ***********************************************************************************
005: *
006: * Copyright (c) 2003, 2004, 2005, 2006 The Sakai Foundation.
007: *
008: * Licensed under the Educational Community License, Version 1.0 (the "License");
009: * you may not use this file except in compliance with the License.
010: * You may obtain a copy of the License at
011: *
012: * http://www.opensource.org/licenses/ecl1.php
013: *
014: * Unless required by applicable law or agreed to in writing, software
015: * distributed under the License is distributed on an "AS IS" BASIS,
016: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017: * See the License for the specific language governing permissions and
018: * limitations under the License.
019: *
020: **********************************************************************************/package org.sakaiproject.access.tool;
021:
022: import java.io.IOException;
023: import java.util.Collection;
024: import java.util.Enumeration;
025: import java.util.Properties;
026: import java.util.Vector;
027:
028: import javax.servlet.ServletConfig;
029: import javax.servlet.ServletException;
030: import javax.servlet.http.HttpServletRequest;
031: import javax.servlet.http.HttpServletResponse;
032:
033: import org.apache.commons.logging.Log;
034: import org.apache.commons.logging.LogFactory;
035: import org.sakaiproject.authz.api.SecurityAdvisor;
036: import org.sakaiproject.authz.cover.SecurityService;
037: import org.sakaiproject.cheftool.VmServlet;
038: import org.sakaiproject.entity.api.Entity;
039: import org.sakaiproject.entity.api.EntityAccessOverloadException;
040: import org.sakaiproject.entity.api.EntityCopyrightException;
041: import org.sakaiproject.entity.api.EntityNotDefinedException;
042: import org.sakaiproject.entity.api.EntityPermissionException;
043: import org.sakaiproject.entity.api.EntityProducer;
044: import org.sakaiproject.entity.api.HttpAccess;
045: import org.sakaiproject.entity.api.Reference;
046: import org.sakaiproject.entity.api.ResourceProperties;
047: import org.sakaiproject.entity.cover.EntityManager;
048: import org.sakaiproject.tool.api.ActiveTool;
049: import org.sakaiproject.tool.api.Session;
050: import org.sakaiproject.tool.api.Tool;
051: import org.sakaiproject.tool.api.ToolException;
052: import org.sakaiproject.tool.cover.ActiveToolManager;
053: import org.sakaiproject.tool.cover.SessionManager;
054: import org.sakaiproject.util.BaseResourceProperties;
055: import org.sakaiproject.util.BasicAuth;
056: import org.sakaiproject.util.ParameterParser;
057: import org.sakaiproject.util.ResourceLoader;
058: import org.sakaiproject.util.Validator;
059: import org.sakaiproject.util.Web;
060:
061: /**
062: * <p>
063: * Access is a servlet that provides a portal to entity access by URL for Sakai.<br />
064: * The servlet takes the requests and dispatches to the appropriate EntityProducer for the response.<br />
065: * Any error handling is done here.<br />
066: * If the user has not yet logged in and need to for permission, the login process is handled here, too.
067: * </p>
068: *
069: * @author Sakai Software Development Team
070: */
071: public class AccessServlet extends VmServlet {
072: /** Our log (commons). */
073: private static Log M_log = LogFactory.getLog(AccessServlet.class);
074:
075: /** Resource bundle using current language locale */
076: protected static ResourceLoader rb = new ResourceLoader("access");
077:
078: /** stream content requests if true, read all into memory and send if false. */
079: protected static final boolean STREAM_CONTENT = true;
080:
081: /** The chunk size used when streaming (100k). */
082: protected static final int STREAM_BUFFER_SIZE = 102400;
083:
084: /** delimiter for form multiple values */
085: protected static final String FORM_VALUE_DELIMETER = "^";
086:
087: /** set to true when init'ed. */
088: protected boolean m_ready = false;
089:
090: /** copyright path -- MUST have same value as ResourcesAction.COPYRIGHT_PATH */
091: protected static final String COPYRIGHT_PATH = Entity.SEPARATOR
092: + "copyright";
093:
094: /** Path used when forcing the user to accept the copyright agreement . */
095: protected static final String COPYRIGHT_REQUIRE = Entity.SEPARATOR
096: + "require";
097:
098: /** Path used when the user has accepted the copyright agreement . */
099: protected static final String COPYRIGHT_ACCEPT = Entity.SEPARATOR
100: + "accept";
101:
102: /** Ref accepted, request parameter for COPYRIGHT_ACCEPT request. */
103: protected static final String COPYRIGHT_ACCEPT_REF = "ref";
104:
105: /** Return URL, request parameter for COPYRIGHT_ACCEPT request. */
106: protected static final String COPYRIGHT_ACCEPT_URL = "url";
107:
108: /** Session attribute holding copyright-accepted references (a collection of Strings). */
109: protected static final String COPYRIGHT_ACCEPTED_REFS_ATTR = "Access.Copyright.Accepted";
110:
111: protected BasicAuth basicAuth = null;
112:
113: /** init thread - so we don't wait in the actual init() call */
114: public class AccessServletInit extends Thread {
115: /**
116: * construct and start the init activity
117: */
118: public AccessServletInit() {
119: m_ready = false;
120: start();
121: }
122:
123: /**
124: * run the init
125: */
126: public void run() {
127: m_ready = true;
128: }
129: }
130:
131: /**
132: * initialize the AccessServlet servlet
133: *
134: * @param config
135: * the servlet config parameter
136: * @exception ServletException
137: * in case of difficulties
138: */
139: public void init(ServletConfig config) throws ServletException {
140: super .init(config);
141: startInit();
142: basicAuth = new BasicAuth();
143: basicAuth.init();
144:
145: }
146:
147: /**
148: * Start the initialization process
149: */
150: public void startInit() {
151: new AccessServletInit();
152: }
153:
154: /**
155: * respond to an HTTP GET request
156: *
157: * @param req
158: * HttpServletRequest object with the client request
159: * @param res
160: * HttpServletResponse object back to the client
161: * @exception ServletException
162: * in case of difficulties
163: * @exception IOException
164: * in case of difficulties
165: */
166: public void doGet(HttpServletRequest req, HttpServletResponse res)
167: throws ServletException, IOException {
168: // process any login that might be present
169: basicAuth.doLogin(req);
170: // catch the login helper requests
171: String option = req.getPathInfo();
172: String[] parts = option.split("/");
173: if ((parts.length == 2) && ((parts[1].equals("login")))) {
174: doLogin(req, res, null);
175: } else {
176: dispatch(req, res);
177: }
178: }
179:
180: /**
181: * respond to an HTTP POST request; only to handle the login process
182: *
183: * @param req
184: * HttpServletRequest object with the client request
185: * @param res
186: * HttpServletResponse object back to the client
187: * @exception ServletException
188: * in case of difficulties
189: * @exception IOException
190: * in case of difficulties
191: */
192: public void doPost(HttpServletRequest req, HttpServletResponse res)
193: throws ServletException, IOException {
194: // process any login that might be present
195: basicAuth.doLogin(req);
196: // catch the login helper posts
197: String option = req.getPathInfo();
198: String[] parts = option.split("/");
199: if ((parts.length == 2) && ((parts[1].equals("login")))) {
200: doLogin(req, res, null);
201: }
202:
203: else {
204: sendError(res, HttpServletResponse.SC_NOT_FOUND);
205: }
206: }
207:
208: /**
209: * handle get and post communication from the user
210: *
211: * @param req
212: * HttpServletRequest object with the client request
213: * @param res
214: * HttpServletResponse object back to the client
215: */
216: public void dispatch(HttpServletRequest req, HttpServletResponse res)
217: throws ServletException {
218: ParameterParser params = (ParameterParser) req
219: .getAttribute(ATTR_PARAMS);
220:
221: // get the path info
222: String path = params.getPath();
223: if (path == null)
224: path = "";
225:
226: if (!m_ready) {
227: sendError(res, HttpServletResponse.SC_SERVICE_UNAVAILABLE);
228: return;
229: }
230:
231: // send the sample copyright screen
232: if (COPYRIGHT_PATH.equals(path)) {
233: respondCopyrightAlertDemo(req, res);
234: return;
235: }
236:
237: // send the real copyright screen for some entity (encoded in the request parameter)
238: if (COPYRIGHT_REQUIRE.equals(path)) {
239: String acceptedRef = req.getParameter(COPYRIGHT_ACCEPT_REF);
240: String returnPath = req.getParameter(COPYRIGHT_ACCEPT_URL);
241:
242: Reference aRef = EntityManager.newReference(acceptedRef);
243:
244: // get the properties - but use a security advisor to avoid needing end-user permission to the resource
245: SecurityService.pushAdvisor(new SecurityAdvisor() {
246: public SecurityAdvice isAllowed(String userId,
247: String function, String reference) {
248: return SecurityAdvice.ALLOWED;
249: }
250: });
251: ResourceProperties props = aRef.getProperties();
252: SecurityService.clearAdvisors();
253:
254: // send the copyright agreement interface
255: if (props == null) {
256: sendError(res, HttpServletResponse.SC_NOT_FOUND);
257: }
258:
259: setVmReference("validator", new Validator(), req);
260: setVmReference("props", props, req);
261: setVmReference("tlang", rb, req);
262:
263: String acceptPath = Web.returnUrl(req, COPYRIGHT_ACCEPT
264: + "?" + COPYRIGHT_ACCEPT_REF + "="
265: + Validator.escapeUrl(aRef.getReference()) + "&"
266: + COPYRIGHT_ACCEPT_URL + "="
267: + Validator.escapeUrl(returnPath));
268:
269: setVmReference("accept", acceptPath, req);
270: res.setContentType("text/html; charset=UTF-8");
271: includeVm("/vm/access/copyrightAlert.vm", req, res);
272: return;
273: }
274:
275: // make sure we have a collection for accepted copyright agreements
276: Collection accepted = (Collection) SessionManager
277: .getCurrentSession().getAttribute(
278: COPYRIGHT_ACCEPTED_REFS_ATTR);
279: if (accepted == null) {
280: accepted = new Vector();
281: SessionManager.getCurrentSession().setAttribute(
282: COPYRIGHT_ACCEPTED_REFS_ATTR, accepted);
283: }
284:
285: // for accepted copyright, mark it and redirect to the entity's access URL
286: if (COPYRIGHT_ACCEPT.equals(path)) {
287: String acceptedRef = req.getParameter(COPYRIGHT_ACCEPT_REF);
288: Reference aRef = EntityManager.newReference(acceptedRef);
289:
290: // save this with the session's other accepted refs
291: accepted.add(aRef.getReference());
292:
293: // redirect to the original URL
294: String returnPath = req.getParameter(COPYRIGHT_ACCEPT_URL);
295:
296: try {
297: res.sendRedirect(Web.returnUrl(req, returnPath));
298: } catch (IOException e) {
299: }
300: return;
301: }
302:
303: // pre-process the path
304: String origPath = path;
305: path = preProcessPath(path, req);
306:
307: // what is being requested?
308: Reference ref = EntityManager.newReference(path);
309:
310: // get the incoming information
311: AccessServletInfo info = newInfo(req);
312:
313: // let the entity producer handle it
314: try {
315: // make sure we have a valid reference with an entity producer we can talk to
316: EntityProducer service = ref.getEntityProducer();
317: if (service == null)
318: throw new EntityNotDefinedException(ref.getReference());
319:
320: // get the producer's HttpAccess helper, it might not support one
321: HttpAccess access = service.getHttpAccess();
322: if (access == null)
323: throw new EntityNotDefinedException(ref.getReference());
324:
325: // let the helper do the work
326: access.handleAccess(req, res, ref, accepted);
327: } catch (EntityNotDefinedException e) {
328: // the request was not valid in some way
329: sendError(res, HttpServletResponse.SC_NOT_FOUND);
330: return;
331: }
332:
333: catch (EntityPermissionException e) {
334: // the end user does not have permission - offer a login if there is no user id yet established
335: // if not permitted, and the user is the anon user, let them login
336: if (SessionManager.getCurrentSessionUserId() == null) {
337: try {
338: doLogin(req, res, origPath);
339: } catch (IOException ioex) {
340: }
341: return;
342: }
343:
344: // otherwise reject the request
345: sendError(res, HttpServletResponse.SC_FORBIDDEN);
346: }
347:
348: catch (EntityAccessOverloadException e) {
349: M_log.info("dispatch(): ref: " + ref.getReference() + e);
350: sendError(res, HttpServletResponse.SC_SERVICE_UNAVAILABLE);
351: }
352:
353: catch (EntityCopyrightException e) {
354: // redirect to the copyright agreement interface for this entity
355: try {
356: // TODO: send back using a form of the request URL, encoding the real reference, and the requested reference
357: // Note: refs / requests with servlet parameters (?x=y...) are NOT supported -ggolden
358: String redirPath = COPYRIGHT_REQUIRE + "?"
359: + COPYRIGHT_ACCEPT_REF + "="
360: + Validator.escapeUrl(e.getReference()) + "&"
361: + COPYRIGHT_ACCEPT_URL + "="
362: + Validator.escapeUrl(req.getPathInfo());
363: res.sendRedirect(Web.returnUrl(req, redirPath));
364: } catch (IOException ee) {
365: }
366: return;
367: }
368:
369: catch (Throwable e) {
370: M_log.warn("dispatch(): exception: ", e);
371: sendError(res, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
372: }
373:
374: finally {
375: // log
376: if (M_log.isDebugEnabled())
377: M_log.debug("from:" + req.getRemoteAddr() + " path:"
378: + params.getPath() + " options: "
379: + info.optionsString() + " time: "
380: + info.getElapsedTime());
381: }
382: }
383:
384: /**
385: * Make any changes needed to the path before final "ref" processing.
386: *
387: * @param path
388: * The path from the request.
389: * @req The request object.
390: * @return The path to use to make the Reference for further processing.
391: */
392: protected String preProcessPath(String path, HttpServletRequest req) {
393: return path;
394: }
395:
396: /**
397: * Make the Sample Copyright Alert response.
398: *
399: * @param req
400: * HttpServletRequest object with the client request.
401: * @param res
402: * HttpServletResponse object back to the client.
403: */
404: protected void respondCopyrightAlertDemo(HttpServletRequest req,
405: HttpServletResponse res) throws ServletException {
406: // the context wraps our real vm attribute set
407: ResourceProperties props = new BaseResourceProperties();
408: setVmReference("props", props, req);
409: setVmReference("validator", new Validator(), req);
410: setVmReference("sample", Boolean.TRUE.toString(), req);
411: setVmReference("tlang", rb, req);
412: res.setContentType("text/html; charset=UTF-8");
413: includeVm("/vm/access/copyrightAlert.vm", req, res);
414: }
415:
416: /**
417: * Make a redirect to the login url.
418: *
419: * @param req
420: * HttpServletRequest object with the client request.
421: * @param res
422: * HttpServletResponse object back to the client.
423: * @param path
424: * The current request path, set ONLY if we want this to be where to redirect the user after successfull login
425: * @throws IOException
426: */
427: protected void doLogin(HttpServletRequest req,
428: HttpServletResponse res, String path) throws ToolException,
429: IOException {
430: // if basic auth is valid do that
431: if (basicAuth.doAuth(req, res)) {
432: //System.err.println("BASIC Auth Request Sent to the Browser ");
433: return;
434: }
435:
436: // get the Sakai session
437: Session session = SessionManager.getCurrentSession();
438:
439: // set the return path for after login if needed (Note: in session, not tool session, special for Login helper)
440: if (path != null) {
441: // where to go after
442: session.setAttribute(Tool.HELPER_DONE_URL, Web.returnUrl(
443: req, path));
444: }
445:
446: // check that we have a return path set; might have been done earlier
447: if (session.getAttribute(Tool.HELPER_DONE_URL) == null) {
448: M_log
449: .warn("doLogin - proceeding with null HELPER_DONE_URL");
450: }
451:
452: // map the request to the helper, leaving the path after ".../options" for the helper
453: ActiveTool tool = ActiveToolManager
454: .getActiveTool("sakai.login");
455: String context = req.getContextPath() + req.getServletPath()
456: + "/login";
457: tool.help(req, res, context, "/login");
458: }
459:
460: /** create the info */
461: protected AccessServletInfo newInfo(HttpServletRequest req) {
462: return new AccessServletInfo(req);
463: }
464:
465: protected void sendError(HttpServletResponse res, int code) {
466: try {
467: res.sendError(code);
468: } catch (Throwable t) {
469: M_log.warn("sendError: " + t);
470: }
471: }
472:
473: public class AccessServletInfo {
474: // elapsed time start
475: protected long m_startTime = System.currentTimeMillis();
476:
477: public long getStartTime() {
478: return m_startTime;
479: }
480:
481: public long getElapsedTime() {
482: return System.currentTimeMillis() - m_startTime;
483: }
484:
485: // all properties from the request
486: protected Properties m_options = null;
487:
488: /** construct from the req */
489: public AccessServletInfo(HttpServletRequest req) {
490: m_options = new Properties();
491: String type = req.getContentType();
492:
493: Enumeration e = req.getParameterNames();
494: while (e.hasMoreElements()) {
495: String key = (String) e.nextElement();
496: String[] values = req.getParameterValues(key);
497: if (values.length == 1) {
498: m_options.put(key, values[0]);
499: } else {
500: StringBuffer buf = new StringBuffer();
501: for (int i = 0; i < values.length; i++) {
502: buf.append(values[i] + FORM_VALUE_DELIMETER);
503: }
504: m_options.put(key, buf.toString());
505: }
506: }
507: }
508:
509: /** return the m_options as a string - obscure any "password" fields */
510: public String optionsString() {
511: StringBuffer buf = new StringBuffer(1024);
512: Enumeration e = m_options.keys();
513: while (e.hasMoreElements()) {
514: String key = (String) e.nextElement();
515: Object o = m_options.getProperty(key);
516: if (o instanceof String) {
517: buf.append(key);
518: buf.append("=");
519: if (key.equals("password")) {
520: buf.append("*****");
521: } else {
522: buf.append(o.toString());
523: }
524: buf.append("&");
525: }
526: }
527:
528: return buf.toString();
529: }
530: }
531:
532: /**
533: * A simple SecurityAdviser that can be used to override permissions on one reference string for one user for one function.
534: */
535: public class SimpleSecurityAdvisor implements SecurityAdvisor {
536: protected String m_userId;
537:
538: protected String m_function;
539:
540: protected String m_reference;
541:
542: public SimpleSecurityAdvisor(String userId, String function,
543: String reference) {
544: m_userId = userId;
545: m_function = function;
546: m_reference = reference;
547: }
548:
549: public SecurityAdvice isAllowed(String userId, String function,
550: String reference) {
551: SecurityAdvice rv = SecurityAdvice.PASS;
552: if (m_userId.equals(userId) && m_function.equals(function)
553: && m_reference.equals(reference)) {
554: rv = SecurityAdvice.ALLOWED;
555: }
556: return rv;
557: }
558: }
559: }
|