001: /* ====================================================================
002: * The Jcorporate Apache Style Software License, Version 1.2 05-07-2002
003: *
004: * Copyright (c) 1995-2002 Jcorporate Ltd. All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions
008: * are met:
009: *
010: * 1. Redistributions of source code must retain the above copyright
011: * notice, this list of conditions and the following disclaimer.
012: *
013: * 2. Redistributions in binary form must reproduce the above copyright
014: * notice, this list of conditions and the following disclaimer in
015: * the documentation and/or other materials provided with the
016: * distribution.
017: *
018: * 3. The end-user documentation included with the redistribution,
019: * if any, must include the following acknowledgment:
020: * "This product includes software developed by Jcorporate Ltd.
021: * (http://www.jcorporate.com/)."
022: * Alternately, this acknowledgment may appear in the software itself,
023: * if and wherever such third-party acknowledgments normally appear.
024: *
025: * 4. "Jcorporate" and product names such as "Expresso" must
026: * not be used to endorse or promote products derived from this
027: * software without prior written permission. For written permission,
028: * please contact info@jcorporate.com.
029: *
030: * 5. Products derived from this software may not be called "Expresso",
031: * or other Jcorporate product names; nor may "Expresso" or other
032: * Jcorporate product names appear in their name, without prior
033: * written permission of Jcorporate Ltd.
034: *
035: * 6. No product derived from this software may compete in the same
036: * market space, i.e. framework, without prior written permission
037: * of Jcorporate Ltd. For written permission, please contact
038: * partners@jcorporate.com.
039: *
040: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
041: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
042: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
043: * DISCLAIMED. IN NO EVENT SHALL JCORPORATE LTD OR ITS CONTRIBUTORS
044: * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
045: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
046: * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
047: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
048: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
049: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
050: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
051: * SUCH DAMAGE.
052: * ====================================================================
053: *
054: * This software consists of voluntary contributions made by many
055: * individuals on behalf of the Jcorporate Ltd. Contributions back
056: * to the project(s) are encouraged when you make modifications.
057: * Please send them to support@jcorporate.com. For more information
058: * on Jcorporate Ltd. and its products, please see
059: * <http://www.jcorporate.com/>.
060: *
061: * Portions of this software are based upon other open source
062: * products and are subject to their respective licenses.
063: */
064:
065: /**
066: *
067: * Copyright 1999, 2000, 2001 Jcorporate Ltd.
068: */package com.jcorporate.expresso.core.controller;
069:
070: import com.jcorporate.expresso.core.cache.CacheException;
071: import com.jcorporate.expresso.core.cache.CacheManager;
072: import com.jcorporate.expresso.core.cache.CacheSystem;
073: import com.jcorporate.expresso.core.db.DBException;
074: import com.jcorporate.expresso.core.dbobj.ValidValue;
075: import com.jcorporate.expresso.core.i18n.Messages;
076: import com.jcorporate.expresso.core.security.User;
077: import com.jcorporate.expresso.kernel.util.FastStringBuffer;
078: import com.jcorporate.expresso.services.dbobj.ControllerSecurity;
079: import com.jcorporate.expresso.services.dbobj.DefaultUserInfo;
080: import com.jcorporate.expresso.services.dbobj.GroupMembers;
081: import com.jcorporate.expresso.services.dbobj.UserGroup;
082: import org.apache.log4j.Logger;
083:
084: import java.util.Enumeration;
085: import java.util.Hashtable;
086: import java.util.Iterator;
087: import java.util.Stack;
088: import java.util.StringTokenizer;
089: import java.util.Vector;
090:
091: /**
092: * An extension of the basic Controller object that knows how to
093: * connect to a database, and how to read it's own security
094: * information for each state
095: */
096: public abstract class DBController extends Controller {
097:
098: private static final String CACHE_NAME = DBController.class
099: + "securityCache"; // includes class name and hashcode
100:
101: /**
102: * Default TTY for the security cache = 30 minutes
103: */
104: private static final long CACHE_TTY = 60 * 1000 * 30;
105:
106: private static boolean listenersSetup = false;
107: /**
108: * sLog for static methods in this class alone
109: */
110: private static Logger sLog = Logger.getLogger(DBController.class);
111:
112: /**
113: *
114: */
115: public DBController() {
116: if (!listenersSetup) {
117: CacheManager.addListener(CACHE_NAME, UserGroup.class
118: .getName());
119: CacheManager.addListener(CACHE_NAME,
120: ControllerSecurity.class.getName());
121: CacheManager.addListener(CACHE_NAME, DefaultUserInfo.class
122: .getName());
123: CacheManager.addListener(CACHE_NAME, GroupMembers.class
124: .getName());
125:
126: listenersSetup = true;
127: }
128: } /* DBController() */
129:
130: /**
131: * Revised method of cached security. Stores keys based upon group name
132: * rather than uid. This allows users to share the same cache entry without
133: * additional round-trips to the database.
134: *
135: * @param group The group name to add
136: * @param dataContext the datacontext to set this value for
137: * @param className the classname as the controller key
138: * @param states the states that are allowed for this group and this class
139: * @throws CacheException upon error working with the cache system.
140: */
141: private static synchronized void addCachedSecurity(String group,
142: String dataContext, String className, String states)
143: throws CacheException {
144: CacheSystem cs = CacheManager.getCacheSystem(dataContext);
145: if (cs == null) {
146: //We aren't caching for this context.
147: return;
148: }
149:
150: if (sLog.isDebugEnabled()) {
151: sLog.debug("Adding cached access for Group:'" + group
152: + "' for state(s) '" + states + "' controller "
153: + className);
154: }
155: if (!cs.existsCache(CACHE_NAME)) {
156: cs.createCache(CACHE_NAME, false);
157: cs.addListener(CACHE_NAME, UserGroup.class.getName());
158: cs.addListener(CACHE_NAME, ControllerSecurity.class
159: .getName());
160: cs.addListener(CACHE_NAME, DefaultUserInfo.class.getName());
161: cs.addListener(CACHE_NAME, GroupMembers.class.getName());
162:
163: }
164:
165: /* If there is already an entry for this user, add the given */
166:
167: /* state(s) to the list of allowed things - no dups */
168: ValidValue existingVal = (ValidValue) cs.getItem(CACHE_NAME,
169: group + "|" + className);
170:
171: if (existingVal != null) {
172: String existing = existingVal.getDescription();
173:
174: /* Using the two hashtable below "merges" the entries, creatiing */
175:
176: /* one hash with all of the states in it */
177: Hashtable existingHash = new Hashtable(3);
178: String oneState = null;
179: StringTokenizer stk = new StringTokenizer(existing, " ");
180:
181: while (stk.hasMoreElements()) {
182: oneState = stk.nextToken();
183: existingHash.put(oneState, oneState);
184: }
185:
186: StringTokenizer stk2 = new StringTokenizer(states, " ");
187:
188: while (stk2.hasMoreElements()) {
189: oneState = stk2.nextToken();
190: existingHash.put(oneState, oneState);
191: }
192: /* Time-saver: If we have "*", don't bother with the rest */
193: if (existingHash.get("*") != null) {
194: existing = ("*");
195: } else {
196:
197: FastStringBuffer existingBuffer = FastStringBuffer
198: .getInstance();
199: try {
200: for (Enumeration e = existingHash.keys(); e
201: .hasMoreElements();) {
202: existingBuffer.append((String) e.nextElement());
203: existingBuffer.append(" ");
204: }
205:
206: existing = existingBuffer.toString();
207: } finally {
208: existingBuffer.release();
209: existingBuffer = null;
210: }
211: }
212:
213: cs.removeItem(CACHE_NAME, existingVal);
214: cs.addItem(CACHE_NAME, new ValidValue(group + "|"
215: + className, existing), CACHE_TTY);
216:
217: if (sLog.isDebugEnabled()) {
218: sLog.debug("Cache updated for group '" + group
219: + "', controller " + className
220: + ", permissions now '" + existing + "'");
221: }
222:
223: return;
224: } else {
225: if (sLog.isDebugEnabled()) {
226: sLog.debug("There was no existing security in cache");
227: }
228:
229: cs.addItem(CACHE_NAME, new ValidValue(group + "|"
230: + className, states), CACHE_TTY);
231: }
232:
233: }
234:
235: /**
236: * Checks a 'state field' string and checks to see if it's allowed. Searches
237: * for either a match in a comma delimited version OR a wildcard "*"
238: *
239: * @param searchString the description string to parse
240: * @param state the state we're looking to match.
241: * @return boolean, true if we've found an allowed state.
242: */
243: private static boolean containsAllowedState(String searchString,
244: String state) {
245: if (searchString.equals("*")) {
246: return true;
247: }
248:
249: if (searchString == null || searchString.length() == 0) {
250: return false;
251: }
252:
253: StringTokenizer stok = new StringTokenizer(searchString, ",");
254:
255: while (stok.hasMoreTokens()) {
256: String oneState = stok.nextToken().trim();
257: if (oneState.equals(state)) {
258: return true;
259: }
260: }
261:
262: return false;
263: }
264:
265: /**
266: * For database controllers, we check if the new state is allowed
267: * against the database objects for that purpose
268: *
269: * @param newState The name of the new state that is being requested; controller class is assumed to be 'this'
270: * @param myRequest the <code>ControllerRequest</code> object
271: * @return True if the state is permitted for this user, else false
272: * @throws ControllerException if another undefined error takes place while
273: * checking security.
274: */
275: public boolean stateAllowed(String newState,
276: ControllerRequest myRequest) throws ControllerException {
277: try {
278: boolean allowed = isAllowed(myRequest, this , newState);
279:
280: return allowed;
281: } catch (DBException de) {
282: throw new ControllerException("Unable to check Controller "
283: + "security", de);
284: } catch (CacheException ce) {
285: throw new ControllerException("Cache exception checking "
286: + "Controller security", ce);
287: }
288: } /* stateAllowed(String) */
289:
290: /**
291: * for the given controller class and state, can the user in this request access this state?
292: *
293: * @param request current request
294: * @param controller controller in question
295: * @param newState state in question
296: * @return true if state is allowed for user specified in request
297: * @throws DBException upon database error
298: * @throws CacheException upon error working with the cache system.
299: */
300: public static boolean isAllowed(ControllerRequest request,
301: DBController controller, String newState)
302: throws DBException, CacheException {
303: boolean allowed = false;
304:
305: if (controller == null) {
306: return false;
307: }
308:
309: //Grab the user
310: User this User = new User();
311: this User.setDataContext(request.getDataContext());
312: this User.setUid(request.getUid());
313: this User.retrieve();
314:
315: // admin is always allowed
316: if (this User.isAdmin()) {
317: return true;
318: }
319:
320: String controllerClassName = controller.getClass().getName();
321: //This gets filled with any group names that weren't in the cache
322: //
323: String missingGroupNames[] = null;
324:
325: //Group the user's groups
326: Vector usersGroups = this User.getGroups();
327: int groupSize = usersGroups.size();
328:
329: //Precalc to save time
330: String keySuffix = "|" + controllerClassName;
331:
332: CacheSystem cs = CacheManager.getCacheSystem(request
333: .getDataContext());
334:
335: //Ok, here's the critical section
336: //If we get a batch of users logging in at once, we don't want
337: //to crash the security system. So! Only allow one thread
338: //at a time to check the cache system. Otherwise, we'll get
339: //100 database hits when we only need one.
340: synchronized (DBController.class) {
341: //Make sure there's at least some sort of upper bound in what the cache
342: //holds so that we don't run out of memory.
343: if (!cs.existsCache(CACHE_NAME)) {
344: cs.createCache(CACHE_NAME, false, 500);
345: //We'll need to change things if somebody manually changes the
346: //controller security OR group members are added or deleted
347: //If the other caches are modified, it clears the dbobject
348: //security cache
349: cs.addListener(CACHE_NAME, UserGroup.class.getName());
350: cs.addListener(CACHE_NAME, ControllerSecurity.class
351: .getName());
352: cs.addListener(CACHE_NAME, DefaultUserInfo.class
353: .getName());
354: cs
355: .addListener(CACHE_NAME, GroupMembers.class
356: .getName());
357: }
358:
359: //
360: //We're looking for the 'best case' here. If we find any group
361: //that it is valid for, then we're set. If we don't find anything
362: //positive in the cache and there are groups that aren't currently
363: //cached, then we need to go to the database and find the remaining
364: //items.
365: //
366: int insertGroupIndex = 0;
367: for (int i = 0; i < groupSize; i++) {
368: String oneGroup = (String) usersGroups.get(i);
369: String key = oneGroup + keySuffix;
370: ValidValue secVal = (ValidValue) cs.getItem(CACHE_NAME,
371: key);
372:
373: if (secVal == null) {
374: //No cache item exists in this value. If we fail to find
375: //a positive we have to go to the database to check for this
376: //group
377: if (missingGroupNames == null) {
378: missingGroupNames = new String[groupSize];
379: }
380: missingGroupNames[insertGroupIndex] = oneGroup;
381: insertGroupIndex++;
382: } else {
383: //Ok we found something. If it's a positive, then we
384: //return true, otherwise we keep searching.
385: String sec = secVal.getDescription();
386:
387: if (containsAllowedState(sec, newState)) {
388: if (sLog.isDebugEnabled()) {
389: sLog.debug("User '" + request.getUid()
390: + "' allowed state '" + newState
391: + "' via cache");
392: }
393:
394: allowed = true;
395: break;
396: }
397: }
398: }
399:
400: if (!allowed && missingGroupNames != null) {
401: //
402: //Ok. Getting here means that there are group names we haven't
403: //covered all groups so we have to go to the database to check them.
404: //If missingGroupNames == null then we haven't found a match and
405: //should return false
406: //
407: ControllerSecurity csec = new ControllerSecurity();
408: csec.setDataContext(request.getDataContext());
409:
410: ControllerSecurity oneSecurityEntry = null;
411: String allowedStates = null;
412:
413: for (int i = 0; i < missingGroupNames.length; i++) {
414: String oneGroupName = missingGroupNames[i];
415: if (oneGroupName != null) {
416:
417: csec.clear();
418: csec.setField(
419: ControllerSecurity.CONTROLLER_CLASS,
420: controllerClassName);
421: csec.setField(ControllerSecurity.GROUP_NAME,
422: oneGroupName);
423:
424: for (Iterator cse = csec
425: .searchAndRetrieveList().iterator(); cse
426: .hasNext();) {
427:
428: oneSecurityEntry = (ControllerSecurity) cse
429: .next();
430: allowedStates = oneSecurityEntry
431: .getField(ControllerSecurity.STATES);
432:
433: if (containsAllowedState(allowedStates,
434: newState)) {
435: allowed = true;
436: }
437: addCachedSecurity(oneGroupName, request
438: .getDataContext(),
439: controllerClassName, allowedStates);
440:
441: } /* for */
442: } else {
443: break; // we don't fill array necessarily
444: }
445: } /* for each user group */
446: }
447: } //end synchronized
448: return allowed;
449: }
450:
451: /**
452: * Override the superclass getString, as now we know the user
453: * and the DB, and thus can use the language preferences of the user
454: * if available.
455: *
456: * @param stringCode The code in the MessagesBundle to retrieve
457: * @param args the i18n formatting arguments
458: * @param myRequest the controllerRequest object
459: * @return the properly formatted string
460: * @deprecated The getString method from ControllerRequest should
461: * be used in *all* cases - this method is not reliable, as the
462: * same DBController instance is shared between users.
463: */
464: protected String getString(String stringCode, Object[] args,
465: ControllerRequest myRequest) {
466: if (myRequest.getUser().equals("")) {
467: return super .getString(stringCode, args);
468: } else {
469: // return Messages.getStringForUser(myRequest.getUid(),
470: // myRequest.getDBName(),
471: // getSchema(), stringCode, args);
472:
473: Stack s = this .getSchemaStack();
474: if (s == null || s.isEmpty()) {
475: return Messages.getStringForUser(myRequest.getUid(),
476: myRequest.getDataContext(), getSchema(),
477: stringCode, args);
478: } else {
479: for (Iterator i = s.iterator(); i.hasNext();) {
480: try {
481: String schema = (String) i.next();
482: return Messages.getStringForUser(myRequest
483: .getUid(), myRequest.getDataContext(),
484: schema, stringCode, args);
485: } catch (IllegalArgumentException ex) {
486: if (!i.hasNext()) {
487: throw ex;
488: }
489: }
490:
491: }
492:
493: throw new IllegalArgumentException(
494: "Unable to locate string " + stringCode
495: + " for any schema");
496: }
497: }
498: } /* getString(String, Object[]) */
499:
500: } /* DBController */
|