001: /******************************************************************************
002: * JBoss, a division of Red Hat *
003: * Copyright 2006, Red Hat Middleware, LLC, and individual *
004: * contributors as indicated by the @authors tag. See the *
005: * copyright.txt in the distribution for a full listing of *
006: * individual contributors. *
007: * *
008: * This is free software; you can redistribute it and/or modify it *
009: * under the terms of the GNU Lesser General Public License as *
010: * published by the Free Software Foundation; either version 2.1 of *
011: * the License, or (at your option) any later version. *
012: * *
013: * This software is distributed in the hope that it will be useful, *
014: * but WITHOUT ANY WARRANTY; without even the implied warranty of *
015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
016: * Lesser General Public License for more details. *
017: * *
018: * You should have received a copy of the GNU Lesser General Public *
019: * License along with this software; if not, write to the Free *
020: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA *
021: * 02110-1301 USA, or see the FSF site: http://www.fsf.org. *
022: ******************************************************************************/package org.jboss.portal.cms.impl.interceptors;
023:
024: import org.apache.log4j.Logger;
025: import org.hibernate.Query;
026: import org.hibernate.Session;
027: import org.hibernate.SessionFactory;
028: import org.hibernate.Transaction;
029: import org.jboss.portal.cms.CMSException;
030: import org.jboss.portal.cms.CMSInterceptor;
031: import org.jboss.portal.cms.hibernate.state.Tools;
032: import org.jboss.portal.cms.impl.jcr.JCRCMS;
033: import org.jboss.portal.cms.impl.jcr.JCRCommand;
034: import org.jboss.portal.cms.impl.jcr.command.SearchCommand;
035: import org.jboss.portal.cms.model.File;
036: import org.jboss.portal.cms.model.Folder;
037: import org.jboss.portal.cms.security.AuthorizationManager;
038: import org.jboss.portal.cms.security.CMSPermission;
039: import org.jboss.portal.cms.security.Criteria;
040: import org.jboss.portal.cms.security.PermRoleAssoc;
041: import org.jboss.portal.cms.security.Permission;
042: import org.jboss.portal.cms.security.PortalCMSSecurityContext;
043: import org.jboss.portal.common.invocation.InvocationException;
044: import org.jboss.portal.identity.Role;
045: import org.jboss.portal.identity.RoleModule;
046: import org.jboss.portal.identity.User;
047: import org.jboss.portal.jems.as.JNDI;
048: import org.jboss.portal.security.PortalPermission;
049: import org.w3c.dom.Document;
050: import org.w3c.dom.Element;
051: import org.w3c.dom.NodeList;
052:
053: import javax.naming.InitialContext;
054: import javax.naming.NamingException;
055: import javax.xml.parsers.DocumentBuilderFactory;
056: import java.io.ByteArrayInputStream;
057: import java.io.InputStream;
058: import java.util.ArrayList;
059: import java.util.Collection;
060: import java.util.HashSet;
061: import java.util.Iterator;
062: import java.util.List;
063: import java.util.Set;
064:
065: /**
066: * ACLInterceptor is plugged into the CMS system to enforce fine grained security access control on resources stored in
067: * the CMS system.
068: *
069: * @author Sohil Shah - sohil.shah@jboss.com - Nov 27, 2006
070: */
071: public class ACLInterceptor extends CMSInterceptor {
072:
073: private static Logger log = Logger.getLogger(ACLInterceptor.class);
074:
075: /** default security policy that the cms service should be booted with */
076: private String defaultPolicy = null;
077:
078: /** . */
079: private RoleModule roleModule = null;
080:
081: /** . */
082: private String jndiName = null;
083:
084: /** . */
085: private JNDI.Binding jndiBinding = null;
086:
087: /** . */
088: private AuthorizationManager authorizationManager = null;
089:
090: /** . */
091: private String cmsSessionFactory = null;
092:
093: /** . */
094: private String identitySessionFactory = null;
095:
096: /** @return */
097: public AuthorizationManager getAuthorizationManager() {
098: return this .authorizationManager;
099: }
100:
101: /** @param authorizationManager */
102: public void setAuthorizationManager(
103: AuthorizationManager authorizationManager) {
104: this .authorizationManager = authorizationManager;
105: }
106:
107: /** @return */
108: public String getDefaultPolicy() {
109: return this .defaultPolicy;
110: }
111:
112: /** @param defaultPolicy */
113: public void setDefaultPolicy(String defaultPolicy) {
114: this .defaultPolicy = defaultPolicy;
115: }
116:
117: /** @return */
118: public RoleModule getRoleModule() {
119: return this .roleModule;
120: }
121:
122: /** @param roleModule */
123: public void setRoleModule(RoleModule roleModule) {
124: this .roleModule = roleModule;
125: }
126:
127: /** @return */
128: public String getJNDIName() {
129: return this .jndiName;
130: }
131:
132: /** @param jndiName */
133: public void setJNDIName(String jndiName) {
134: this .jndiName = jndiName;
135: }
136:
137: /** @return */
138: public String getIdentitySessionFactory() {
139: return this .identitySessionFactory;
140: }
141:
142: /** @param identitySessionFactory */
143: public void setIdentitySessionFactory(String identitySessionFactory) {
144: this .identitySessionFactory = identitySessionFactory;
145: }
146:
147: /** @return */
148: public String getCmsSessionFactory() {
149: return cmsSessionFactory;
150: }
151:
152: /** @param cmsSessionFactory */
153: public void setCmsSessionFactory(String cmsSessionFactory) {
154: this .cmsSessionFactory = cmsSessionFactory;
155: }
156:
157: /**
158: *
159: */
160: protected Object invoke(JCRCommand invocation) throws Exception,
161: InvocationException {
162: if (ACLInterceptor.turnOff.get() == null) {
163: //make the acl check before this command is executed
164:
165: // Get the user executing the command
166: User user = (User) JCRCMS.getUserInfo().get();
167:
168: //setup the security context with enough information for the authorization provider
169: //to be able to make an enforcement decision
170: PortalCMSSecurityContext securityContext = new PortalCMSSecurityContext(
171: user);
172: securityContext.setAttribute("command", invocation);
173:
174: //perform access check
175: PortalPermission cmsPermission = new CMSPermission(
176: securityContext);
177: boolean allowAccess = this .authorizationManager
178: .checkPermission(cmsPermission);
179: if (allowAccess) {
180: Object response = invocation.invokeNext();
181:
182: //also filter lists of files and folders based on access allowed on these resources
183: response = this .applyFilter(response, securityContext);
184:
185: return response;
186: } else {
187: String username = null;
188: if (user == null) {
189: username = "Anonymous";
190: } else {
191: username = user.getUserName();
192: }
193: log.debug("Unauthorized command (" + invocation
194: + ") for user: " + username);
195: throw new CMSException(
196: "Access to this resource is denied");
197: }
198: } else {
199: //this is turned off for this thread that is trying to integrate with the CMS
200: return invocation.invokeNext();
201: }
202: }
203:
204: /**
205: * Filters any files/folders based on the user's access. The filter is applied to folders/files returned by invoking
206: * a CMS command
207: *
208: * @param response
209: * @return
210: */
211: private Object applyFilter(Object response,
212: PortalCMSSecurityContext securityContext) {
213: Object filteredResponse = response;
214: JCRCommand command = (JCRCommand) securityContext
215: .getAttribute("command");
216: try {
217: if (filteredResponse instanceof Folder) {
218: Folder folder = (Folder) filteredResponse;
219: List filteredFolders = new ArrayList();
220: List filteredFiles = new ArrayList();
221: securityContext.removeAttribute("command");
222: if (folder.getFolders() != null) {
223: for (Iterator itr = folder.getFolders().iterator(); itr
224: .hasNext();) {
225: Folder cour = (Folder) itr.next();
226: securityContext.setAttribute("applyFilter",
227: cour.getBasePath());
228: PortalPermission cmsPermission = new CMSPermission(
229: securityContext);
230: boolean allow = this .authorizationManager
231: .checkPermission(cmsPermission);
232: if (allow) {
233: filteredFolders.add(cour);
234: }
235: }
236: }
237: if (folder.getFiles() != null) {
238: for (Iterator itr = folder.getFiles().iterator(); itr
239: .hasNext();) {
240: File cour = (File) itr.next();
241: securityContext.setAttribute("applyFilter",
242: cour.getBasePath());
243: PortalPermission cmsPermission = new CMSPermission(
244: securityContext);
245: boolean allow = this .authorizationManager
246: .checkPermission(cmsPermission);
247: if (allow) {
248: filteredFiles.add(cour);
249: }
250: }
251: }
252: folder.setFolders(filteredFolders);
253: folder.setFiles(filteredFiles);
254: } else if ((filteredResponse instanceof List)
255: && (command instanceof SearchCommand)) {
256: List list = (List) filteredResponse;
257: List filteredFiles = new ArrayList();
258: for (Iterator itr = list.iterator(); itr.hasNext();) {
259: File cour = (File) itr.next();
260: securityContext.setAttribute("path", cour
261: .getBasePath());
262: PortalPermission cmsPermission = new CMSPermission(
263: securityContext);
264: boolean allow = this .authorizationManager
265: .checkPermission(cmsPermission);
266: if (allow) {
267: filteredFiles.add(cour);
268: }
269: }
270: filteredResponse = filteredFiles;
271: }
272: } catch (Exception e) {
273: throw new RuntimeException(e);
274: }
275: return filteredResponse;
276: }
277:
278: /**
279: *
280: */
281: public void start() throws Exception {
282: log.info("AuthorizationManager initialized="
283: + this .authorizationManager);
284:
285: if (this .jndiName != null) {
286: this .jndiBinding = new JNDI.Binding(jndiName, this );
287: this .jndiBinding.bind();
288: }
289:
290: Tools.init(this .cmsSessionFactory);
291:
292: try {
293: roleModule = (RoleModule) new InitialContext()
294: .lookup("java:portal/RoleModule");
295: } catch (NamingException e) {
296: log.error("Cannot obtain RoleModule from JNDI: ", e);
297: throw e;
298: }
299:
300: //check and see if cms permissions exist...if not, boot it with the default policy
301: //specified in the configuration
302: if (!this .isBootRequired()) {
303: return;
304: }
305:
306: //go ahead and boot the cms access policy with default policy specified in the configuration
307: InputStream is = null;
308: try {
309: //process the specified defaultPolicy
310: is = new ByteArrayInputStream(this .defaultPolicy.getBytes());
311: Document document = DocumentBuilderFactory.newInstance()
312: .newDocumentBuilder().parse(is);
313:
314: NodeList criteria = document
315: .getElementsByTagName("criteria");
316: if (criteria != null) {
317: for (int i = 0; i < criteria.getLength(); i++) {
318: Element criteriaElem = (Element) criteria.item(i);
319: String name = criteriaElem.getAttribute("name");
320: String value = criteriaElem.getAttribute("value");
321:
322: //permission setup
323: NodeList permissions = criteriaElem
324: .getElementsByTagName("permission");
325: if (permissions != null) {
326: Session session = null;
327: Transaction tx = null;
328: Collection parsedPermissions = this
329: .parseDefaultPermissions(permissions);
330: try {
331: session = Tools.getOpenSession();
332: tx = session.beginTransaction();
333: for (Iterator itr = parsedPermissions
334: .iterator(); itr.hasNext();) {
335: Permission permission = (Permission) itr
336: .next();
337: permission.addCriteria(new Criteria(
338: name, value));
339: Set securityBinding = new HashSet();
340: securityBinding.add(permission);
341: this .authorizationManager.getProvider()
342: .setSecurityBindings(null,
343: securityBinding);
344: }
345: tx.commit();
346: } catch (Exception e) {
347: tx.rollback();
348: } finally {
349: Tools.closeSession(session);
350: }
351: }
352: }
353: }
354: } finally {
355: if (is != null) {
356: is.close();
357: }
358: }
359: }
360:
361: /** @throws Exception */
362: public void stop() throws Exception {
363: if (this .jndiBinding != null) {
364: this .jndiBinding.unbind();
365: this .jndiBinding = null;
366: }
367: Tools.destroy();
368: }
369:
370: /**
371: * Parses and produces Permission objects for the default policy
372: *
373: * @param permissions
374: * @return
375: */
376: private Collection parseDefaultPermissions(NodeList permissions)
377: throws Exception {
378: Collection parsedPermissions = new ArrayList();
379: for (int i = 0; i < permissions.getLength(); i++) {
380: Element permissionElement = (Element) permissions.item(i);
381: String name = permissionElement.getAttribute("name");
382: String action = permissionElement.getAttribute("action");
383: Permission permission = new Permission(name, action);
384:
385: //parse the roles listed under this permission element
386: NodeList roles = permissionElement
387: .getElementsByTagName("role");
388: for (int j = 0; j < roles.getLength(); j++) {
389: Element roleElement = (Element) roles.item(j);
390: String roleName = roleElement.getAttribute("name");
391: Role role = this .getRole(roleName);
392: PermRoleAssoc roleAssoc = new PermRoleAssoc();
393: if (role != null) {
394: //makes sure this is not Anonymous
395: roleAssoc.setRoleId(roleName);
396: } else {
397: roleAssoc.setRoleId(AuthorizationManager.Anonymous);
398: }
399: permission.addRoleAssoc(roleAssoc);
400: }
401:
402: parsedPermissions.add(permission);
403: }
404: return parsedPermissions;
405: }
406:
407: /**
408: * Returns the Role object specified in the default policy
409: *
410: * @param name
411: * @return
412: */
413: private Role getRole(String name) throws Exception {
414: Role role = null;
415:
416: //since this is at app start up and not on user thread...need to create a transaction context.
417: InitialContext context = new InitialContext();
418: SessionFactory sessionFactory = (SessionFactory) context
419: .lookup(this .identitySessionFactory);
420: Session session = sessionFactory.openSession();
421: Transaction tx = session.beginTransaction();
422: try {
423: role = this .roleModule.findRoleByName(name);
424: tx.commit();
425: } catch (Exception e) {
426: tx.rollback();
427: role = null;
428: } finally {
429: session.close();
430: }
431:
432: return role;
433: }
434:
435: /**
436: * Returns if cms permissions need to be booted with the default policy from configuration
437: *
438: * @return
439: */
440: private boolean isBootRequired() {
441: boolean bootRequired = false;
442:
443: String hsqlQuery = "select count(permission) from org.jboss.portal.cms.security.Permission as permission";
444: Session session = Tools.getOpenSession();
445: Transaction tx = session.beginTransaction();
446: try {
447: Query query = session.createQuery(hsqlQuery);
448: long count = ((Long) query.list().get(0)).longValue();
449: if (count <= 0) {
450: bootRequired = true;
451: }
452: tx.commit();
453: } catch (Exception e) {
454: tx.rollback();
455: } finally {
456: Tools.closeSession(session);
457: }
458:
459: return bootRequired;
460: }
461:
462: /**
463: * This turns off acl security only for a particular thread. This is used by system level operations that need to
464: * integrate with the CMS
465: * <p/>
466: * Example is: the workflow daemon that publishes a content as live when a manager approves it. Without turning this
467: * off, the daemon thread is running in Anonymous mode which obviously does not have the rights to publish the
468: * content
469: */
470: private static ThreadLocal turnOff = new ThreadLocal();
471:
472: public static void turnOff() {
473: ACLInterceptor.turnOff.set(new Boolean(true));
474: }
475:
476: public static void turnOn() {
477: ACLInterceptor.turnOff.set(null);
478: }
479: }
|