diff --git a/config/alfresco/core-services-context.xml b/config/alfresco/core-services-context.xml index 0becf55a20..ea666d9cb8 100644 --- a/config/alfresco/core-services-context.xml +++ b/config/alfresco/core-services-context.xml @@ -135,6 +135,8 @@ + ${server.maxusers} + ${server.singleuseronly.name} diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index b22c1bcbff..918f295f72 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -44,6 +44,18 @@ index.tracking.maxRecordSetSize=1000 # Change the failure behaviour of the configuration checker system.bootstrap.config_check.strict=true +# Server Single User Mode +# note: +# only allow named user (note: if blank or not set then will allow all users) +# assuming maxusers is not set to 0 +#server.singleuseronly.name=admin + +# Server Max Users - limit number of users with non-expired tickets +# note: +# -1 allows any number of users, assuming not in single-user mode +# 0 prevents further logins, including the ability to enter single-user mode +server.maxusers=-1 + # # Properties to limit resources spent on individual searches # diff --git a/source/java/org/alfresco/repo/admin/RepoServerMgmt.java b/source/java/org/alfresco/repo/admin/RepoServerMgmt.java index cd4689483b..030eac63c8 100644 --- a/source/java/org/alfresco/repo/admin/RepoServerMgmt.java +++ b/source/java/org/alfresco/repo/admin/RepoServerMgmt.java @@ -27,7 +27,10 @@ package org.alfresco.repo.admin; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.security.authentication.AuthenticationServiceImpl; import org.alfresco.repo.transaction.TransactionServiceImpl; import org.alfresco.service.license.LicenseService; @@ -47,6 +50,8 @@ public class RepoServerMgmt implements RepoServerMgmtMBean, ApplicationContextAw private TransactionServiceImpl transactionService; private AuthenticationServiceImpl authenticationService; + // property key should be the same as the one in core-services-context.xml (to allow repo to start in multi-user mode even if the property is not set) + private final static String PROPERTY_KEY_SINGLE_USER_ONLY = "${server.singleuseronly.name}"; public void setTransactionService(TransactionServiceImpl transactionService) { @@ -71,13 +76,13 @@ public class RepoServerMgmt implements RepoServerMgmtMBean, ApplicationContextAw { if (readOnly && isReadOnly()) { - log.info("Alfresco Repository is already READONLY"); + log.info("Alfresco is already read-only"); return; } if (!readOnly && !isReadOnly()) { - log.info("Alfresco Repository is already WRITABLE"); + log.info("Alfresco is already read-write"); return; } @@ -100,11 +105,11 @@ public class RepoServerMgmt implements RepoServerMgmtMBean, ApplicationContextAw if (readOnly) { - log.info("Alfresco Repository set to READONLY"); + log.info("Alfresco set to be read-only"); } else { - log.info("Alfresco Repository set to WRITABLE"); + log.info("Alfresco set to be read-write"); } } @@ -118,7 +123,7 @@ public class RepoServerMgmt implements RepoServerMgmtMBean, ApplicationContextAw } // Note: implementing counts as managed attributes (without params) means that - // certain JMX consoles can create graphs + // certain JMX consoles can monitor /* * (non-Javadoc) @@ -165,7 +170,8 @@ public class RepoServerMgmt implements RepoServerMgmtMBean, ApplicationContextAw public String[] listUserNamesNonExpired() { Set userSet = authenticationService.getUsersWithTickets(true); - return userSet.toArray(new String[0]); + SortedSet sorted = new TreeSet(userSet); + return sorted.toArray(new String[0]); } /* @@ -175,7 +181,8 @@ public class RepoServerMgmt implements RepoServerMgmtMBean, ApplicationContextAw public String[] listUserNamesAll() { Set userSet = authenticationService.getUsersWithTickets(false); - return userSet.toArray(new String[0]); + SortedSet sorted = new TreeSet(userSet); + return sorted.toArray(new String[0]); } /* @@ -184,7 +191,9 @@ public class RepoServerMgmt implements RepoServerMgmtMBean, ApplicationContextAw */ public int invalidateTicketsExpired() { - return authenticationService.invalidateTickets(true); + int count = authenticationService.invalidateTickets(true); + log.info("Expired tickets invalidated: " + count); + return count; } /* @@ -193,24 +202,132 @@ public class RepoServerMgmt implements RepoServerMgmtMBean, ApplicationContextAw */ public int invalidateTicketsAll() { - return authenticationService.invalidateTickets(false); + int count = authenticationService.invalidateTickets(false); + log.info("All tickets invalidated: " + count); + return count; } /* * (non-Javadoc) - * @see org.alfresco.mbeans.RepoServerMgmtMBean#allowSingleUserOnly(java.lang.String) + * @see org.alfresco.repo.admin.RepoServerMgmtMBean#invalidateUser(java.lang.String) */ - public void allowSingleUserOnly(String allowedUsername) + public void invalidateUser(String username) { + authenticationService.invalidateUserSession(username); + log.info("User invalidated: " + username); + } + + /* + * (non-Javadoc) + * @see org.alfresco.repo.admin.RepoServerMgmtMBean#setSingleUserOnly(java.lang.String) + */ + public void setSingleUserOnly(String allowedUsername) + { + int maxUsers = getMaxUsers(); + List allowedUsers = null; - if (allowedUsername != null) + if ((allowedUsername != null) && (! allowedUsername.equals(""))) { - allowedUsers = new ArrayList(0); - allowedUsers.add(allowedUsername); - - invalidateTicketsAll(); + if (! allowedUsername.equals(PROPERTY_KEY_SINGLE_USER_ONLY)) + { + allowedUsers = new ArrayList(0); + allowedUsers.add(allowedUsername); + + invalidateTicketsAll(); + + if (maxUsers != 0) + { + log.info("Alfresco set to allow single-user (" + allowedUsername + ") logins"); + } + else + { + log.info("Alfresco set to allow single-user (" + allowedUsername + ") logins - although further logins are currently prevented (limit = 0)"); + } + } + } + else + { + if (maxUsers == -1) + { + log.info("Alfresco set to allow logins (no limit set)"); + } + else if (maxUsers == 0) + { + log.info("Alfresco set to allow logins - although further logins are currently prevented (limit = 0)"); + } + else if (maxUsers != 0) + { + log.info("Alfresco set to allow logins (limit = " + maxUsers + ")"); + } } authenticationService.setAllowedUsers(allowedUsers); } + + /* + * (non-Javadoc) + * @see org.alfresco.repo.admin.RepoServerMgmtMBean#getSingleUserOnly() + */ + public String getSingleUserOnly() + { + List allowedUsers = authenticationService.getAllowedUsers(); + if (allowedUsers != null) + { + if (allowedUsers.size() > 1) + { + throw new AlfrescoRuntimeException("Unexpected: more than one user allowed"); + } + if (allowedUsers.size() == 1) + { + return allowedUsers.get(0); + } + } + return null; + } + + /* + * (non-Javadoc) + * @see org.alfresco.repo.admin.RepoServerMgmtMBean#setMaxUsers(int) + */ + public void setMaxUsers(int maxUsers) + { + authenticationService.setMaxUsers(maxUsers); + + String singleUserOnlyName = getSingleUserOnly(); + if (maxUsers == -1) + { + if ((singleUserOnlyName != null) && (! singleUserOnlyName.equals(""))) + { + log.info("Alfresco set to allow logins (no limit set) - although currently restricted to single-user (" + singleUserOnlyName + ")"); + } + else + { + log.info("Alfresco set to allow logins (no limit set)"); + } + } + else if (maxUsers == 0) + { + log.info("Alfresco set to prevent further logins (limit = 0)"); + } + else + { + if ((singleUserOnlyName != null) && (! singleUserOnlyName.equals(""))) + { + log.info("Alfresco set to allow logins (limit = " + maxUsers + ") - although currently restricted to single-user (" + singleUserOnlyName + ")"); + } + else + { + log.info("Alfresco set to allow logins (limit = " + maxUsers + ")"); + } + } + } + + /* + * (non-Javadoc) + * @see org.alfresco.repo.admin.RepoServerMgmtMBean#getMaxUsers() + */ + public int getMaxUsers() + { + return authenticationService.getMaxUsers(); + } } diff --git a/source/java/org/alfresco/repo/admin/RepoServerMgmtMBean.java b/source/java/org/alfresco/repo/admin/RepoServerMgmtMBean.java index 17e49d223e..ae2961167b 100644 --- a/source/java/org/alfresco/repo/admin/RepoServerMgmtMBean.java +++ b/source/java/org/alfresco/repo/admin/RepoServerMgmtMBean.java @@ -24,14 +24,17 @@ */ package org.alfresco.repo.admin; - +/** + * Repository Server Management + * + * Note: The attributes/operations below can be clustered (ie. when configured all servers in the cluster will be affected) + * + */ public interface RepoServerMgmtMBean { /** * Set whether Repository allows writes or not * - * Note: This operation can be clustered (ie. all servers in the cluster will be affected) - * * @param readOnly true is READONLY, false is WRITEABLE */ public void setReadOnly(boolean readOnly); @@ -39,8 +42,6 @@ public interface RepoServerMgmtMBean /** * Does the Repository allows writes or not ? * - * Note: This operation can be clustered (ie. all servers in the cluster will be affected) - * * @return boolean true is READONLY, false is WRITEABLE */ public boolean isReadOnly(); @@ -50,8 +51,6 @@ public interface RepoServerMgmtMBean * * This may be higher than the user count, since a user can have more than one ticket/session * - * Note: This operation can be clustered (ie. all servers in the cluster will be affected) - * * @return int number of non-expired tickets */ public int getTicketCountNonExpired(); @@ -61,8 +60,6 @@ public interface RepoServerMgmtMBean * * This may be higher than the user count, since a user can have more than one ticket/session * - * Note: This operation can be clustered (ie. all servers in the cluster will be affected) - * * @return int number of tickets (non-expired and expired) */ public int getTicketCountAll(); @@ -72,8 +69,6 @@ public interface RepoServerMgmtMBean * * This may be lower than the ticket count, since a user can have more than one ticket/session * - * Note: This operation can be clustered (ie. all servers in the cluster will be affected) - * * @return int number of non-expired users */ public int getUserCountNonExpired(); @@ -83,16 +78,12 @@ public interface RepoServerMgmtMBean * * This may be lower than the ticket count, since a user can have more than one ticket/session * - * Note: This operation can be clustered (ie. all servers in the cluster will be affected) - * * @return int number of users (non-expired and expired) */ public int getUserCountAll(); /** * Get set of unique non-expired usernames - * - * Note: This operation can be clustered (ie. all servers in the cluster will be affected) * * @return String[] array of non-expired usernames */ @@ -101,16 +92,12 @@ public interface RepoServerMgmtMBean /** * Get set of all unique usernames * - * Note: This operation can be clustered (ie. all servers in the cluster will be affected) - * * @return String[] array of all usernames (non-expired and expired) */ public String[] listUserNamesAll(); /** * Invalidate expired tickets - * - * Note: This operation can be clustered (ie. all servers in the cluster will be affected) * * @return int count of expired invalidated tickets */ @@ -125,15 +112,57 @@ public interface RepoServerMgmtMBean */ public int invalidateTicketsAll(); + /** + * Invalidate given users tickets + */ + public void invalidateUser(String username); + /** * Set whether Repository allows single user mode or not * * If single user mode is set then all tickets will be invalidated first before allowing the - * named user to login (with one or more sessions) + * named user to login (with one or more sessions) assuming maxUsers is not set to 0 * - * Note: This operation can be clustered (ie. all servers in the cluster will be affected) + * Note: This can also be configured at startup. Refer to repository property (server.singleuseronly.name). * * @param String allowed username (eg. 'admin') or null to unset (ie. allow all users) */ - public void allowSingleUserOnly(String allowedUsername); + public void setSingleUserOnly(String allowedUsername); + + /** + * If Repository is in single user mode then return the name of the allowed user else return null + * + * @param String allowed username (eg. 'admin') or null (ie. allow all users) + */ + public String getSingleUserOnly(); + + /** + * Set limit for max users and/or prevent further logins + * + * If number of non-expired logins is greater or equal to the limit then further logins will be prevented + * otherwise valid login attempt will be permitted, unless the system is in single-user mode. + * + * Note: + * + * Max users = 0 prevents further logins (will also prevent single-user mode login) + * Max users = -1 allow logins (without a max limit) + * + * Note: This can also be configured at startup. Refer to repository property (server.maxusers). + * + * @param maxUsers + */ + public void setMaxUsers(int maxUsers); + + /** + * Get limit for max users + * + * If number of non-expired logins is greater or equal to the limit then further logins will be prevented + * otherwise valid login attempt will be permitted. However, single-user mode will take precedence. + * + * Max users = 0 prevents further logins + * Max users = -1 allow logins (without a max limit) + * + * @param int maxUsers + */ + public int getMaxUsers(); } diff --git a/source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java b/source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java index 4c912d3584..d3702393e2 100644 --- a/source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java +++ b/source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java @@ -30,6 +30,7 @@ import java.util.Set; import org.alfresco.repo.cache.SimpleCache; import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.PermissionService; public class AuthenticationServiceImpl implements AuthenticationService { @@ -49,7 +50,8 @@ public class AuthenticationServiceImpl implements AuthenticationService // SysAdmin cache - used to cluster certain JMX operations private SimpleCache sysAdminCache; - private final static String KEY_SYSADMIN_ALLOWED_USERS = "sysAdminCache.authAllowedUsers"; + private final static String KEY_SYSADMIN_ALLOWED_USERS = "sysAdminCache.authAllowedUsers"; // List + private final static String KEY_SYSADMIN_MAX_USERS = "sysAdminCache.authMaxUsers"; // Integer public AuthenticationServiceImpl() @@ -130,7 +132,15 @@ public class AuthenticationServiceImpl implements AuthenticationService { throw new AuthenticationException("Username not allowed: " + userName); } - authenticationComponent.authenticate(userName, password); + + Integer maxUsers = (Integer)sysAdminCache.get(KEY_SYSADMIN_MAX_USERS); + + if ((maxUsers != null) && (maxUsers != -1) && (ticketComponent.getUsersWithTickets(true).size() >= maxUsers)) + { + throw new AuthenticationException("Max users exceeded: " + maxUsers); + } + + authenticationComponent.authenticate(userName, password); } catch(AuthenticationException ae) { @@ -166,6 +176,24 @@ public class AuthenticationServiceImpl implements AuthenticationService { sysAdminCache.put(KEY_SYSADMIN_ALLOWED_USERS, allowedUsers); } + + @SuppressWarnings("unchecked") + public List getAllowedUsers() + { + return (List)sysAdminCache.get(KEY_SYSADMIN_ALLOWED_USERS); + } + + public void setMaxUsers(int maxUsers) + { + sysAdminCache.put(KEY_SYSADMIN_MAX_USERS, new Integer(maxUsers)); + } + + @SuppressWarnings("unchecked") + public int getMaxUsers() + { + Integer maxUsers = (Integer)sysAdminCache.get(KEY_SYSADMIN_MAX_USERS); + return (maxUsers == null ? -1 : maxUsers.intValue()); + } public void invalidateTicket(String ticket) throws AuthenticationException { @@ -219,8 +247,16 @@ public class AuthenticationServiceImpl implements AuthenticationService return authenticationComponent.isSystemUserName(getCurrentUserName()); } + @SuppressWarnings("unchecked") public void authenticateAsGuest() throws AuthenticationException { + List allowedUsers = (List)sysAdminCache.get(KEY_SYSADMIN_ALLOWED_USERS); + + if ((allowedUsers != null) && (! allowedUsers.contains(PermissionService.GUEST_AUTHORITY))) + { + throw new AuthenticationException("Guest authentication is not allowed"); + } + authenticationComponent.setGuestUserAsCurrentUser(); ticketComponent.clearCurrentTicket(); }