/**
* Copyright 2008 Sakaiproject 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.osedu.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.adminsiteperms.service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.authz.api.AuthzGroup;
import org.sakaiproject.authz.api.AuthzGroupService;
import org.sakaiproject.authz.api.AuthzPermissionException;
import org.sakaiproject.authz.api.FunctionManager;
import org.sakaiproject.authz.api.GroupNotDefinedException;
import org.sakaiproject.authz.api.Role;
import org.sakaiproject.authz.api.SecurityService;
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.site.api.SiteService.SelectionType;
import org.sakaiproject.site.api.SiteService.SortType;
import org.sakaiproject.tool.api.Session;
import org.sakaiproject.tool.api.SessionManager;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.api.UserDirectoryService;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;
/**
* Handles the processing related to the permissions handler
*
* @author Aaron Zeckoski (azeckoski @ unicon.net) (azeckoski @ vt.edu)
*/
public class SitePermsService {
final protected Log log = LogFactory.getLog(getClass());
private static final String STATUS_COMPLETE = "COMPLETE";
private static int DEFAULT_PAUSE_TIME_MS = 1001; // just over 1 second
private static int DEFAULT_MAX_UPDATE_TIME_SECS = 60*60; // 1 hour
private static int DEFAULT_SITES_BEFORE_PAUSE = 10;
private int pauseTimeMS = DEFAULT_PAUSE_TIME_MS;
private long maxUpdateTimeMS = DEFAULT_MAX_UPDATE_TIME_SECS * 1000l;
private int sitesUntilPause = DEFAULT_SITES_BEFORE_PAUSE;
public static String[] templates = {
"!site.template",
"!site.template.course",
"!site.template.portfolio",
"!site.user"
};
/*
* NOTE: we do NOT want to allow 2 sets of admin perms updates to run at the same time on a server
* so we will use these service variables to basically lock the updates processing on a single server,
* ideally we want to lock this for the entire cluster though...
* We don't want to allow 2 users to do this at the same time so not locking it to the session.
*/
private String updateStatus = null;
private String updateMessage = null;
/**
* Timestamp of the start of the update,
* used to ensure we can unlock this if it died
*/
private long updateStarted = 0;
/**
* Set permissions (perms) in a set of site types (types) for a set of roles (roles)
*
* @param perms a list of permission keys
* @param types a list of site types (course/project/workspace/etc.)
* @param roles a list of site roles
* @param add if true then add the permissions, if false then remove them
*/
public void setSiteRolePerms(final String[] perms, final String[] types, final String[] roles, final boolean add) {
if (! securityService.isSuperUser()) {
throw new SecurityException("setSiteRolePerms is only usable by super users");
}
if (isLockedForUpdates()) {
throw new IllegalStateException("Cannot start new perms update, one is already in progress");
}
// get the configurable values
pauseTimeMS = serverConfigurationService.getConfig("site.adminperms.pause.ms", pauseTimeMS);
int maxUpdateTimeS = serverConfigurationService.getConfig("site.adminperms.maxrun.secs", DEFAULT_MAX_UPDATE_TIME_SECS);
maxUpdateTimeMS = 1000l * maxUpdateTimeS; // covert to milliseconds
sitesUntilPause = serverConfigurationService.getConfig("site.adminperms.sitesuntilpause", sitesUntilPause);
// get the current state
final User currentUser = userDirectoryService.getCurrentUser();
final Session currentSession = sessionManager.getCurrentSession();
// run this in a new thread
Runnable backgroundRunner = new Runnable() {
public void run() {
try {
initiateSitePermsThread(currentUser, currentSession, perms, types, roles, add);
} catch (IllegalStateException e) {
throw e; // rethrow this back out
} catch (Exception e) {
log.error("SitePerms background perms runner thread died: "+e, e);
}
}
};
Thread bgThread = new Thread(backgroundRunner);
bgThread.setDaemon(true); // important, otherwise JVM cannot exit
bgThread.start();
}
/**
* Returns a current status message from the session if there is one to display for a running admin perms process,
* is the status is complete then the session data will be removed
*/
public synchronized String getCurrentStatusMessage() {
String msg = updateMessage;
isLockedForUpdates(); // just run this
if (STATUS_COMPLETE.equals(updateStatus)) {
updateStatus = null;
updateMessage = null;
}
return msg;
}
/**
* @return true if this update is currently locking the permissions processor, false if another permissions processor can be started
*/
public synchronized boolean isLockedForUpdates() {
boolean locked = false;
if (updateStarted > 0) {
if (System.currentTimeMillis() > (updateStarted + maxUpdateTimeMS)) {
// max time reached for this update so reset
updateStarted = 0;
updateStatus = STATUS_COMPLETE;
updateMessage = "Max time exceeded for this update";
} else {
locked = true;
}
}
return locked;
}
private void initiateSitePermsThread(final User currentUser, final Session currentSession, final String[] perms, final String[] types, final String[] roles, final boolean add) throws InterruptedException {
String permsString = makeStringFromArray(perms);
String typesString = makeStringFromArray(types);
String rolesString = makeStringFromArray(roles);
// exit if we are locked for updates
if (isLockedForUpdates()) {
throw new IllegalStateException("Cannot start new perms update, one is already in progress");
}
updateStarted = System.currentTimeMillis();
// update the session with a status message
String msg = getMessage("siterole.message.processing."+(add?"add":"remove"),
new Object[] {permsString, typesString, rolesString, 0});
log.info("STARTED: "+msg+" :: pauseTimeMS="+pauseTimeMS+", sitesUntilPause="+sitesUntilPause+", maxUpdateTimeMS="+maxUpdateTimeMS);
updateStatus = "RUNNING";
updateMessage = msg;
// set the current user in this thread so they can perform the operations
Session threadSession = setCurrentUser(currentUser.getId());
try {
List<String> permsList = Arrays.asList(perms);
// now add the perms to all matching roles in all matching sites
// switched site listing to using ids only - KNL-1125
List<String> siteIds = siteService.getSiteIds(SelectionType.ANY, types, null, null, SortType.NONE, null);
int pauseTime = 0;
int sitesCounter = 0;
int updatesCount = 0;
int successCount = 0;
for (String siteId : siteIds) {
String siteRef = siteService.siteReference(siteId);
try {
AuthzGroup ag = authzGroupService.getAuthzGroup(siteRef);
if (authzGroupService.allowUpdate(ag.getId())) {
boolean updated = false;
for (String role : roles) {
Role r = ag.getRole(role);
// if role not found in this group then move on
if (r != null) {
// get the current perms so we can possibly avoid an update
Set<String> current = r.getAllowedFunctions();
if (add) {
if (! current.containsAll(permsList)) {
// only update if the perms are not already there
r.allowFunctions(permsList);
updated = true;
}
} else {
boolean found = false;
for (String perm : permsList) {
if (current.contains(perm)) {
found = true;
break;
}
}
if (found) {
// only update if at least one perm needs to be removed
r.disallowFunctions(permsList);
updated = true;
}
}
}
}
if (updated) {
// only save if the group was updated
authzGroupService.save(ag);
updatesCount++;
log.info("Added Permissions ("+permsString+") for roles ("+rolesString+") to group:" + siteRef);
}
successCount++;
if (updatesCount > 0 && updatesCount % sitesUntilPause == 0) {
// pause every 10 (default) sites updated or so for about 1 second (default)
Thread.sleep(pauseTimeMS);
pauseTime += pauseTimeMS;
// make sure the sessions do not timeout
threadSession.setActive();
currentSession.setActive();
}
} else {
log.warn("Cannot update authz group: " + siteRef + ", unable to apply any perms change");
}
} catch (GroupNotDefinedException e) {
log.error("Could not find authz group: " + siteRef + ", unable to apply any perms change");
} catch (AuthzPermissionException e) {
log.error("Could not save authz group: " + siteRef + ", unable to apply any perms change");
}
sitesCounter++;
if (!isLockedForUpdates()) {
// if we are no longer locked for updates then we have a timeout failure
throw new RuntimeException("Timeout occurred while running site permissions update");
} else if (sitesCounter % 4 == 0) {
// update the processor status every few sites processed
int percentComplete = (int) (sitesCounter * 100) / siteIds.size();
msg = getMessage("siterole.message.processing."+(add?"add":"remove"),
new Object[] {permsString, typesString, rolesString, percentComplete});
updateMessage = msg;
}
}
int failureCount = siteIds.size() - successCount;
long totalTime = System.currentTimeMillis() - updateStarted;
int totalSecs = totalTime > 0 ? (int)(totalTime/1000) : 0;
int pauseSecs = pauseTime > 0 ? (int)(pauseTime/1000) : 0;
msg = getMessage("siterole.message.permissions."+(add ? "added" : "removed"),
new Object[] {permsString, typesString, rolesString, siteIds.size(), updatesCount, successCount, failureCount, totalSecs, pauseSecs});
log.info(msg);
updateMessage = msg;
} finally {
// reset the update status
updateStatus = STATUS_COMPLETE;
updateStarted = 0;
// cleanup the session associated with this thread
threadSession.clear();
threadSession.invalidate();
}
}
/**
* Set a current user for the current thread, create session if needed
* @param userId the userId to set
* @return the new Session
*/
private Session setCurrentUser(String userId) {
if (userId == null) {
throw new IllegalArgumentException("userId cannot be null");
}
Session currentSession = sessionManager.getCurrentSession();
if (currentSession == null) {
// start a session if none is around
currentSession = sessionManager.startSession(userId);
}
currentSession.setUserId(userId);
currentSession.setActive();
sessionManager.setCurrentSession(currentSession);
authzGroupService.refreshUser(userId);
return currentSession;
}
/**
* @return a list of all site types
*/
public List<String> getSiteTypes() {
List<String> siteTypes = siteService.getSiteTypes();
Collections.sort(siteTypes);
return siteTypes;
}
/**
* @return a list of all permissions
*/
public List<String> getPermissions() {
List<String> perms = functionManager.getRegisteredFunctions();
Collections.sort(perms);
return perms;
}
/**
* @return a list of all valid roles names
*/
public List<String> getValidRoles() {
HashSet<String> roleIds = new HashSet<String>();
for (String templateRef : templates) {
AuthzGroup ag;
try {
ag = authzGroupService.getAuthzGroup(templateRef);
Set<Role> agRoles = ag.getRoles();
for (Role role : agRoles) {
roleIds.add(role.getId());
}
} catch (GroupNotDefinedException e) {
// nothing to do here but continue really
}
}
ArrayList<String> roles = new ArrayList<String>(roleIds);
Collections.sort(roles);
return roles;
}
/**
* @return true if current user is super admin
*/
public boolean isSuperAdmin() {
return securityService.isSuperUser();
}
/**
* Get a translated string from a code and replacement args
*
* @param code
* @param args
* @return the translated string
*/
public String getMessage(String code, Object[] args) {
String msg;
try {
msg = getMessageSource().getMessage(code, args, null);
} catch (NoSuchMessageException e) {
msg = "Missing message for code: "+code;
}
return msg;
}
public static String makeStringFromArray(String[] array) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < array.length; i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(array[i]);
}
return sb.toString();
}
private AuthzGroupService authzGroupService;
public void setAuthzGroupService(AuthzGroupService authzGroupService) {
this.authzGroupService = authzGroupService;
}
private FunctionManager functionManager;
public void setFunctionManager(FunctionManager functionManager) {
this.functionManager = functionManager;
}
private SessionManager sessionManager;
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
private SecurityService securityService;
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
private ServerConfigurationService serverConfigurationService;
public void setServerConfigurationService(ServerConfigurationService serverConfigurationService) {
this.serverConfigurationService = serverConfigurationService;
}
private SiteService siteService;
public void setSiteService(SiteService siteService) {
this.siteService = siteService;
}
UserDirectoryService userDirectoryService;
public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
this.userDirectoryService = userDirectoryService;
}
private MessageSource messageSource;
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
public MessageSource getMessageSource() {
return messageSource;
}
}