/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-impl/src/main/java/org/sakaiproject/authz/impl/SakaiSecurity.java $ * $Id: SakaiSecurity.java 126062 2013-06-20 21:32:44Z ottenhoff@longsight.com $ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **********************************************************************************/ package org.sakaiproject.authz.impl; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.Stack; import java.util.Vector; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.authz.api.AuthzGroupService; import org.sakaiproject.authz.api.SecurityAdvisor; import org.sakaiproject.authz.api.SecurityService; import org.sakaiproject.entity.api.Entity; import org.sakaiproject.entity.api.EntityManager; import org.sakaiproject.entity.api.Reference; import org.sakaiproject.event.api.EventTrackingService; import org.sakaiproject.memory.api.MemoryService; import org.sakaiproject.memory.api.MultiRefCache; import org.sakaiproject.site.api.SiteService; import org.sakaiproject.thread_local.api.ThreadLocalManager; import org.sakaiproject.tool.api.Session; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.user.api.User; import org.sakaiproject.user.api.UserDirectoryService; /** * <p> * SakaiSecurity is a Sakai security service. * </p> */ public abstract class SakaiSecurity implements SecurityService { /** Our logger. */ private static Log M_log = LogFactory.getLog(SakaiSecurity.class); /** A cache of calls to the service and the results. */ protected MultiRefCache m_callCache = null; /** ThreadLocalManager key for our SecurityAdvisor Stack. */ protected final static String ADVISOR_STACK = "SakaiSecurity.advisor.stack"; /** Session attribute to store roleswap state **/ protected final static String ROLESWAP_PREFIX = "roleswap"; /** The update event to post to clear cached security lookups involving the authz group **/ protected final static String EVENT_ROLESWAP_CLEAR = "realm.clear.cache"; /********************************************************************************************************************************************************************************************************************************************************** * Dependencies, configuration, and their setter methods *********************************************************************************************************************************************************************************************************************************************************/ /** * @return the ThreadLocalManager collaborator. */ protected abstract ThreadLocalManager threadLocalManager(); /** * @return the AuthzGroupService collaborator. */ protected abstract AuthzGroupService authzGroupService(); /** * @return the UserDirectoryService collaborator. */ protected abstract UserDirectoryService userDirectoryService(); /** * @return the MemoryService collaborator. */ protected abstract MemoryService memoryService(); /** * @return the EntityManager collaborator. */ protected abstract EntityManager entityManager(); /** * @return the SessionManager collaborator. */ protected abstract SessionManager sessionManager(); /** * @return the EventTrackingService collaborator. */ protected abstract EventTrackingService eventTrackingService(); /********************************************************************************************************************************************************************************************************************************************************** * Configuration *********************************************************************************************************************************************************************************************************************************************************/ /** The # minutes to cache the security answers. 0 disables the cache. */ protected int m_cacheMinutes = 3; /** * Set the # minutes to cache a security answer. * * @param time * The # minutes to cache a security answer (as an integer string). */ public void setCacheMinutes(String time) { m_cacheMinutes = Integer.parseInt(time); } /********************************************************************************************************************************************************************************************************************************************************** * Init and Destroy *********************************************************************************************************************************************************************************************************************************************************/ /** * Final initialization, once all dependencies are set. */ public void init() { // <= 0 minutes indicates no caching desired if (m_cacheMinutes > 0) { m_callCache = memoryService().newMultiRefCache( "org.sakaiproject.authz.api.SecurityService.cache"); } } /** * Final cleanup. */ public void destroy() { M_log.info("destroy()"); } /********************************************************************************************************************************************************************************************************************************************************** * SecurityService implementation *********************************************************************************************************************************************************************************************************************************************************/ /** * {@inheritDoc} */ public boolean isSuperUser() { User user = userDirectoryService().getCurrentUser(); if (user == null) return false; return isSuperUser(user.getId()); } /** * {@inheritDoc} */ public boolean isSuperUser(String userId) { // if no user or the no-id user (i.e. the anon user) if ((userId == null) || (userId.length() == 0)) return false; // check the cache String command = "super@" + userId; if (m_callCache != null) { final Boolean value = (Boolean) m_callCache.get(command); if(value != null) return value.booleanValue(); } boolean rv = false; // these known ids are super if (UserDirectoryService.ADMIN_ID.equalsIgnoreCase(userId)) { rv = true; } else if ("postmaster".equalsIgnoreCase(userId)) { rv = true; } // if the user has site modification rights in the "!admin" site, welcome aboard! else { if (authzGroupService().isAllowed(userId, SiteService.SECURE_UPDATE_SITE, "/site/!admin")) { rv = true; } } // cache if (m_callCache != null) { Collection<String> azgIds = new Vector<String>(); azgIds.add("/site/!admin"); m_callCache.put(command, Boolean.valueOf(rv), m_cacheMinutes * 60, null, azgIds); } return rv; } /** * {@inheritDoc} */ public boolean unlock(String lock, String resource) { return unlock(userDirectoryService().getCurrentUser(), lock, resource); } /** * {@inheritDoc} */ public boolean unlock(User u, String function, String entityRef) { // pick up the current user if needed User user = u; if (user == null) { user = userDirectoryService().getCurrentUser(); } return unlock(user.getId(), function, entityRef); } /** * {@inheritDoc} */ public boolean unlock(String userId, String function, String entityRef) { return unlock(userId, function, entityRef, null); } /** * {@inheritDoc} */ public boolean unlock(String userId, String function, String entityRef, Collection<String> azgs) { // make sure we have complete parameters (azgs is optional) if (userId == null || function == null || entityRef == null) { M_log.warn("unlock(): null: " + userId + " " + function + " " + entityRef); return false; } // if super, grant if (isSuperUser(userId)) { return true; } // let the advisors have a crack at it, if we have any // Note: this cannot be cached without taking into consideration the exact advisor configuration -ggolden if (hasAdvisors()) { SecurityAdvisor.SecurityAdvice advice = adviseIsAllowed(userId, function, entityRef); if (advice != SecurityAdvisor.SecurityAdvice.PASS) { return advice == SecurityAdvisor.SecurityAdvice.ALLOWED; } } // check with the AuthzGroups appropriate for this entity return checkAuthzGroups(userId, function, entityRef, azgs); } /** * Check the appropriate AuthzGroups for the answer - this may be cached * * @param userId * The user id. * @param function * The security function. * @param entityRef * The entity reference string. * @return true if allowed, false if not. */ protected boolean checkAuthzGroups(String userId, String function, String entityRef, Collection<String> azgs) { // check the cache String command = "unlock@" + userId + "@" + function + "@" + entityRef; if (m_callCache != null) { final Boolean value = (Boolean) m_callCache.get(command); if(value != null) return value.booleanValue(); } // get this entity's AuthzGroups if needed if (azgs == null) { // make a reference for the entity Reference ref = entityManager().newReference(entityRef); azgs = ref.getAuthzGroups(userId); } boolean rv = authzGroupService().isAllowed(userId, function, azgs); // cache if (m_callCache != null) m_callCache.put(command, Boolean.valueOf(rv), m_cacheMinutes * 60, entityRef, azgs); return rv; } /** * Access the List the Users who can unlock the lock for use with this resource. * * @param lock * The lock id string. * @param reference * The resource reference string. * @return A List (User) of the users can unlock the lock (may be empty). */ @SuppressWarnings("unchecked") public List<User> unlockUsers(String lock, String reference) { if (reference == null) { M_log.warn("unlockUsers(): null resource: " + lock); return new Vector<User>(); } // make a reference for the resource Reference ref = entityManager().newReference(reference); // get this resource's Realms Collection<String> realms = ref.getAuthzGroups(); // get the users who can unlock in these realms List<String> ids = new Vector<String>(); ids.addAll(authzGroupService().getUsersIsAllowed(lock, realms)); // convert the set of Users into a sorted list of users List<User> users = userDirectoryService().getUsers(ids); Collections.sort(users); return users; } /********************************************************************************************************************************************************************************************************************************************************** * SecurityAdvisor Support *********************************************************************************************************************************************************************************************************************************************************/ /** * Get the thread-local security advisor stack, possibly creating it * * @param force * if true, create if missing */ @SuppressWarnings("unchecked") protected Stack<SecurityAdvisor> getAdvisorStack(boolean force) { Stack<SecurityAdvisor> advisors = (Stack<SecurityAdvisor>) threadLocalManager().get(ADVISOR_STACK); if ((advisors == null) && force) { advisors = new Stack<SecurityAdvisor>(); threadLocalManager().set(ADVISOR_STACK, advisors); } return advisors; } /** * Remove the thread-local security advisor stack */ protected void dropAdvisorStack() { threadLocalManager().set(ADVISOR_STACK, null); } /** * Check the advisor stack - if anyone declares ALLOWED or NOT_ALLOWED, stop and return that, else, while they PASS, keep checking. * * @param userId * The user id. * @param function * The security function. * @param reference * The Entity reference. * @return ALLOWED or NOT_ALLOWED if an advisor makes a decision, or PASS if there are no advisors or they cannot make a decision. */ protected SecurityAdvisor.SecurityAdvice adviseIsAllowed(String userId, String function, String reference) { Stack<SecurityAdvisor> advisors = getAdvisorStack(false); if ((advisors == null) || (advisors.isEmpty())) return SecurityAdvisor.SecurityAdvice.PASS; // a Stack grows to the right - process from top to bottom for (int i = advisors.size() - 1; i >= 0; i--) { SecurityAdvisor advisor = (SecurityAdvisor) advisors.elementAt(i); SecurityAdvisor.SecurityAdvice advice = advisor.isAllowed(userId, function, reference); if (advice != SecurityAdvisor.SecurityAdvice.PASS) { return advice; } } return SecurityAdvisor.SecurityAdvice.PASS; } /** * {@inheritDoc} */ public void pushAdvisor(SecurityAdvisor advisor) { Stack<SecurityAdvisor> advisors = getAdvisorStack(true); advisors.push(advisor); } /** * {@inheritDoc} */ public SecurityAdvisor popAdvisor(SecurityAdvisor advisor) { Stack<SecurityAdvisor> advisors = getAdvisorStack(false); if (advisors == null) return null; SecurityAdvisor rv = null; if (advisors.size() > 0) { if (advisor == null) { rv = (SecurityAdvisor) advisors.pop(); } else { SecurityAdvisor sa = advisors.firstElement(); if (advisor.equals(sa)) { rv = (SecurityAdvisor) advisors.pop(); } } } if (advisors.isEmpty()) { dropAdvisorStack(); } return rv; } public SecurityAdvisor popAdvisor() { return popAdvisor(null); } /** * {@inheritDoc} */ public boolean hasAdvisors() { Stack<SecurityAdvisor> advisors = getAdvisorStack(false); if (advisors == null) return false; return !advisors.isEmpty(); } /** * {@inheritDoc} */ public void clearAdvisors() { dropAdvisorStack(); } /** * {@inheritDoc} */ public boolean setUserEffectiveRole(String azGroupId, String role) { if (!unlock(SiteService.SITE_ROLE_SWAP, azGroupId)) return false; // set the session attribute with the roleid sessionManager().getCurrentSession().setAttribute(ROLESWAP_PREFIX + azGroupId, role); resetSecurityCache(azGroupId); return true; } /** * {@inheritDoc} */ public String getUserEffectiveRole(String azGroupId) { if (azGroupId == null || "".equals(azGroupId)) return null; return (String) sessionManager().getCurrentSession().getAttribute(ROLESWAP_PREFIX + azGroupId); } /** * {@inheritDoc} */ public void clearUserEffectiveRole(String azGroupId) { // remove the attribute from the session sessionManager().getCurrentSession().removeAttribute(ROLESWAP_PREFIX + azGroupId); resetSecurityCache(azGroupId); return; } /** * {@inheritDoc} */ public void clearUserEffectiveRoles() { // get all the roleswaps from the session and clear them Session session = sessionManager().getCurrentSession(); for (Enumeration<String> e = session.getAttributeNames(); e.hasMoreElements();) { String name = e.nextElement(); if (name.startsWith(ROLESWAP_PREFIX)) { clearUserEffectiveRole(name.substring(ROLESWAP_PREFIX.length())); } } return; } /** * Clear the results of security lookups involving the given authz group from the security lookup cache. * * @param azGroupId * The authz group id. */ protected void resetSecurityCache(String azGroupId) { // This will clear all cached security lookups involving this realm, thereby forcing the permissions to be rechecked. // We could turn this into a SessionStateBindingListener so it gets called automatically when // the session is cleared. eventTrackingService().post(eventTrackingService().newEvent(EVENT_ROLESWAP_CLEAR, org.sakaiproject.authz.api.AuthzGroupService.REFERENCE_ROOT + Entity.SEPARATOR + azGroupId, true)); return; } }