From 299b4d34348d1319aad69f212f098c142c44f010 Mon Sep 17 00:00:00 2001 From: Andrew Hind Date: Fri, 15 Jun 2007 16:05:43 +0000 Subject: [PATCH] Improvements to the ticket service git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@5984 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../AuthenticationServiceImpl.java | 7 +- .../authentication/AuthenticationTest.java | 31 ++- .../ChainingAuthenticationServiceImpl.java | 16 ++ .../InMemoryTicketComponentImpl.java | 205 ++++++++++++++---- .../TestAuthenticationServiceImpl.java | 13 ++ .../authentication/TicketComponent.java | 38 +++- .../cmr/security/AuthenticationService.java | 7 + 7 files changed, 263 insertions(+), 54 deletions(-) diff --git a/source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java b/source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java index bb0a452650..b9ca07a1fd 100644 --- a/source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java +++ b/source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java @@ -153,9 +153,14 @@ public class AuthenticationServiceImpl implements AuthenticationService public String getCurrentTicket() { - return ticketComponent.getTicket(getCurrentUserName()); + return ticketComponent.getCurrentTicket(getCurrentUserName()); } + public String getNewTicket() + { + return ticketComponent.getNewTicket(getCurrentUserName()); + } + public void clearCurrentSecurityContext() { authenticationComponent.clearCurrentSecurityContext(); diff --git a/source/java/org/alfresco/repo/security/authentication/AuthenticationTest.java b/source/java/org/alfresco/repo/security/authentication/AuthenticationTest.java index b915d96884..9d1538b023 100644 --- a/source/java/org/alfresco/repo/security/authentication/AuthenticationTest.java +++ b/source/java/org/alfresco/repo/security/authentication/AuthenticationTest.java @@ -453,8 +453,9 @@ public class AuthenticationTest extends TestCase Authentication result = authenticationManager.authenticate(token); result.setAuthenticated(true); - String ticket = ticketComponent.getTicket(getUserName(result)); + String ticket = ticketComponent.getNewTicket(getUserName(result)); String user = ticketComponent.validateTicket(ticket); + assertEquals(ticketComponent.getCurrentTicket("Andy"), ticket); user = null; try @@ -471,6 +472,7 @@ public class AuthenticationTest extends TestCase try { user = ticketComponent.validateTicket(ticket); + assertEquals(ticketComponent.getCurrentTicket("Andy"), ticket); assertNotNull(null); } catch (AuthenticationException e) @@ -499,9 +501,11 @@ public class AuthenticationTest extends TestCase Authentication result = authenticationManager.authenticate(token); result.setAuthenticated(true); - String ticket = tc.getTicket(getUserName(result)); + String ticket = tc.getNewTicket(getUserName(result)); tc.validateTicket(ticket); + assertEquals(ticketComponent.getCurrentTicket("Andy"), ticket); tc.validateTicket(ticket); + assertEquals(ticketComponent.getCurrentTicket("Andy"), ticket); dao.deleteUser("Andy"); // assertNull(dao.getUserOrNull("Andy")); @@ -523,8 +527,9 @@ public class AuthenticationTest extends TestCase Authentication result = authenticationManager.authenticate(token); result.setAuthenticated(true); - String ticket = tc.getTicket(getUserName(result)); + String ticket = tc.getNewTicket(getUserName(result)); tc.validateTicket(ticket); + assertTrue(!ticketComponent.getCurrentTicket("Andy").equals(ticket)); try { tc.validateTicket(ticket); @@ -555,10 +560,13 @@ public class AuthenticationTest extends TestCase Authentication result = authenticationManager.authenticate(token); result.setAuthenticated(true); - String ticket = tc.getTicket(getUserName(result)); + String ticket = tc.getNewTicket(getUserName(result)); tc.validateTicket(ticket); + assertEquals(ticketComponent.getCurrentTicket("Andy"), ticket); tc.validateTicket(ticket); + assertEquals(ticketComponent.getCurrentTicket("Andy"), ticket); tc.validateTicket(ticket); + assertEquals(ticketComponent.getCurrentTicket("Andy"), ticket); synchronized (this) { @@ -645,10 +653,13 @@ public class AuthenticationTest extends TestCase Authentication result = authenticationManager.authenticate(token); result.setAuthenticated(true); - String ticket = tc.getTicket(getUserName(result)); + String ticket = tc.getNewTicket(getUserName(result)); tc.validateTicket(ticket); + assertEquals(ticketComponent.getCurrentTicket("Andy"), ticket); tc.validateTicket(ticket); + assertEquals(ticketComponent.getCurrentTicket("Andy"), ticket); tc.validateTicket(ticket); + assertEquals(ticketComponent.getCurrentTicket("Andy"), ticket); synchronized (this) { try @@ -663,7 +674,8 @@ public class AuthenticationTest extends TestCase } tc.validateTicket(ticket); - + assertEquals(ticketComponent.getCurrentTicket("Andy"), ticket); + dao.deleteUser("Andy"); // assertNull(dao.getUserOrNull("Andy")); @@ -772,6 +784,7 @@ public class AuthenticationTest extends TestCase String ticket = authenticationService.getCurrentTicket(); // validate our ticket is still valid authenticationService.validate(ticket); + assertEquals(ticket, authenticationService.getCurrentTicket()); // destroy the ticket instance authenticationService.invalidateTicket(ticket); @@ -820,6 +833,7 @@ public class AuthenticationTest extends TestCase authenticationService.clearCurrentSecurityContext(); authenticationService.validate(ticket); + assertEquals(ticket, authenticationService.getCurrentTicket()); // destroy the ticket instance authenticationService.invalidateTicket(ticket); @@ -878,6 +892,7 @@ public class AuthenticationTest extends TestCase String ticket = authenticationService.getCurrentTicket(); // validate our ticket is still valid authenticationService.validate(ticket); + assertEquals(ticket, authenticationService.getCurrentTicket()); // destroy the ticket instance authenticationService.invalidateTicket(ticket); @@ -937,6 +952,7 @@ public class AuthenticationTest extends TestCase String ticket = authenticationService.getCurrentTicket(); // validate our ticket is still valid authenticationService.validate(ticket); + assertEquals(ticket, authenticationService.getCurrentTicket()); // destroy the ticket instance authenticationService.invalidateTicket(ticket); @@ -1098,6 +1114,7 @@ public class AuthenticationTest extends TestCase // validate our ticket is still valid pubAuthenticationService.validate(ticket); + assertEquals(ticket, authenticationService.getCurrentTicket()); // destroy the ticket instance pubAuthenticationService.invalidateTicket(ticket); @@ -1166,6 +1183,7 @@ public class AuthenticationTest extends TestCase String ticket = pubAuthenticationService.getCurrentTicket(); // validate our ticket is still valid pubAuthenticationService.validate(ticket); + assertEquals(ticket, authenticationService.getCurrentTicket()); // destroy the ticket instance pubAuthenticationService.invalidateTicket(ticket); @@ -1226,6 +1244,7 @@ public class AuthenticationTest extends TestCase String ticket = pubAuthenticationService.getCurrentTicket(); // validate our ticket is still valid pubAuthenticationService.validate(ticket); + assertEquals(ticket, authenticationService.getCurrentTicket()); // destroy the ticket instance pubAuthenticationService.invalidateTicket(ticket); diff --git a/source/java/org/alfresco/repo/security/authentication/ChainingAuthenticationServiceImpl.java b/source/java/org/alfresco/repo/security/authentication/ChainingAuthenticationServiceImpl.java index 2e40d32c5a..68b1d40f29 100644 --- a/source/java/org/alfresco/repo/security/authentication/ChainingAuthenticationServiceImpl.java +++ b/source/java/org/alfresco/repo/security/authentication/ChainingAuthenticationServiceImpl.java @@ -301,6 +301,22 @@ public class ChainingAuthenticationServiceImpl implements AuthenticationService } return null; } + + public String getNewTicket() + { + for (AuthenticationService authService : getUsableAuthenticationServices()) + { + try + { + return authService.getNewTicket(); + } + catch (AuthenticationException e) + { + // Ignore and chain + } + } + return null; + } public void clearCurrentSecurityContext() { diff --git a/source/java/org/alfresco/repo/security/authentication/InMemoryTicketComponentImpl.java b/source/java/org/alfresco/repo/security/authentication/InMemoryTicketComponentImpl.java index e259c80f25..84ae4fc5d6 100644 --- a/source/java/org/alfresco/repo/security/authentication/InMemoryTicketComponentImpl.java +++ b/source/java/org/alfresco/repo/security/authentication/InMemoryTicketComponentImpl.java @@ -25,57 +25,76 @@ package org.alfresco.repo.security.authentication; import java.io.Serializable; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.HashSet; import java.util.Set; +import java.util.zip.CRC32; import org.alfresco.repo.cache.SimpleCache; import org.alfresco.service.cmr.repository.datatype.Duration; -import org.alfresco.util.GUID; +import org.apache.commons.codec.binary.Hex; +import org.doomdark.uuid.UUIDGenerator; + +/** + * Store tickets in memory. They can be distributed in a cluster via the cache + * + * @author andyh + */ public class InMemoryTicketComponentImpl implements TicketComponent { + /** + * Ticket prefix + */ public static final String GRANTED_AUTHORITY_TICKET_PREFIX = "TICKET_"; + + private static ThreadLocal currentTicket = new ThreadLocal(); private boolean ticketsExpire; private Duration validDuration; - + private boolean oneOff; - private SimpleCache ticketsCache; // Can't use Ticket as it's private + private SimpleCache ticketsCache; // Can't use Ticket as it's private + /** + * IOC constructor + * + */ public InMemoryTicketComponentImpl() { super(); } + /** + * Set the ticket cache to support clustering + * + * @param ticketsCache + */ public void setTicketsCache(SimpleCache ticketsCache) { - this.ticketsCache = ticketsCache; + this.ticketsCache = ticketsCache; } - public String getTicket(String userName) throws AuthenticationException + public String getNewTicket(String userName) throws AuthenticationException { - Date expiryDate = null; - if (ticketsExpire) - { - expiryDate = Duration.add(new Date(), validDuration); - } - Ticket ticket = new Ticket(ticketsExpire, expiryDate, userName); - ticketsCache.put(ticket.getTicketId(), ticket); - - return GRANTED_AUTHORITY_TICKET_PREFIX + ticket.getTicketId(); + Date expiryDate = null; + if (ticketsExpire) + { + expiryDate = Duration.add(new Date(), validDuration); + } + Ticket ticket = new Ticket(ticketsExpire, expiryDate, userName); + ticketsCache.put(ticket.getTicketId(), ticket); + String ticketString = GRANTED_AUTHORITY_TICKET_PREFIX + ticket.getTicketId(); + currentTicket.set(ticketString); + return ticketString; } public String validateTicket(String ticketString) throws AuthenticationException { - if (ticketString.length() < GRANTED_AUTHORITY_TICKET_PREFIX.length()) - { - throw new AuthenticationException(ticketString + " is an invalid ticket format"); - } - - String key = ticketString.substring(GRANTED_AUTHORITY_TICKET_PREFIX.length()); - Ticket ticket = ticketsCache.get(key); + Ticket ticket = getTicketByTicketString(ticketString); if (ticket == null) { throw new AuthenticationException("Missing ticket for " + ticketString); @@ -86,40 +105,70 @@ public class InMemoryTicketComponentImpl implements TicketComponent } // TODO: Recheck the user details here // TODO: Strengthen ticket as GUID is predicatble - if(oneOff) + if (oneOff) { - ticketsCache.remove(key); + ticketsCache.remove(getTicketKey(ticketString)); } + currentTicket.set(ticketString); return ticket.getUserName(); } - + + /** + * Helper method to find a ticket + * @param ticketString + * @return - the ticket + */ + private Ticket getTicketByTicketString(String ticketString) + { + Ticket ticket = ticketsCache.get(getTicketKey(ticketString)); + return ticket; + } + + /** + * Helper method to extract the ticket id from the ticket string + * @param ticketString + * @return - the ticket key + */ + private String getTicketKey(String ticketString) + { + if (ticketString.length() < GRANTED_AUTHORITY_TICKET_PREFIX.length()) + { + throw new AuthenticationException(ticketString + " is an invalid ticket format"); + } + String key = ticketString.substring(GRANTED_AUTHORITY_TICKET_PREFIX.length()); + return key; + } + public void invalidateTicketById(String ticketString) { String key = ticketString.substring(GRANTED_AUTHORITY_TICKET_PREFIX.length()); ticketsCache.remove(key); } - + public void invalidateTicketByUser(String userName) { Set toRemove = new HashSet(); - - for(String key: ticketsCache.getKeys()) + + for (String key : ticketsCache.getKeys()) { Ticket ticket = ticketsCache.get(key); - if(ticket.getUserName().equals(userName)) + if (ticket.getUserName().equals(userName)) { toRemove.add(ticket.getTicketId()); } } - - for(String id: toRemove) + + for (String id : toRemove) { ticketsCache.remove(id); } } - - - + + /** + * Ticket + * @author andyh + * + */ public static class Ticket implements Serializable { private static final long serialVersionUID = -5904510560161261049L; @@ -132,18 +181,54 @@ public class InMemoryTicketComponentImpl implements TicketComponent private String ticketId; + private String guid; + Ticket(boolean expires, Date expiryDate, String userName) { this.expires = expires; this.expiryDate = expiryDate; this.userName = userName; - this.ticketId = GUID.generate(); + this.guid = UUIDGenerator.getInstance().generateRandomBasedUUID().toString(); + + + String encode = (expires ? "T" : "F") + + ((expiryDate == null) ? new Date().toString(): expiryDate.toString()) + + userName + guid; + MessageDigest digester; + try + { + digester = MessageDigest.getInstance("SHA-1"); + this.ticketId = new String(Hex.encodeHex(digester.digest(encode.getBytes()))); + } + catch (NoSuchAlgorithmException e) + { + try + { + digester = MessageDigest.getInstance("MD5"); + this.ticketId = new String(Hex.encodeHex(digester.digest(encode.getBytes()))); + } + catch (NoSuchAlgorithmException e1) + { + CRC32 crc = new CRC32(); + crc.update(encode.getBytes()); + byte[] bytes = new byte[4]; + long value = crc.getValue(); + bytes[0] = (byte)(value & 0xFF); + value >>>= 4; + bytes[1] = (byte)(value & 0xFF); + value >>>= 4; + bytes[2] = (byte)(value & 0xFF); + value >>>= 4; + bytes[3] = (byte)(value & 0xFF); + this.ticketId = new String(Hex.encodeHex(bytes)); + } + } } /** * Has the tick expired * - * @return + * @return - if expired */ boolean hasExpired() { @@ -168,7 +253,9 @@ public class InMemoryTicketComponentImpl implements TicketComponent return false; } Ticket t = (Ticket) o; - return (this.expires == t.expires) && this.expiryDate.equals(t.expiryDate) && this.userName.equals(t.userName) && this.ticketId.equals(t.ticketId); + return (this.expires == t.expires) + && this.expiryDate.equals(t.expiryDate) && this.userName.equals(t.userName) + && this.ticketId.equals(t.ticketId); } public int hashCode() @@ -198,23 +285,59 @@ public class InMemoryTicketComponentImpl implements TicketComponent } - - + /** + * Are tickets single use + * @param oneOff + */ public void setOneOff(boolean oneOff) { this.oneOff = oneOff; } - + /** + * Do tickets expire + * @param ticketsExpire + */ public void setTicketsExpire(boolean ticketsExpire) { this.ticketsExpire = ticketsExpire; } - + /** + * How long are tickets valid (XML duration as a string) + * @param validDuration + */ public void setValidDuration(String validDuration) { this.validDuration = new Duration(validDuration); } - + + public String getAuthorityForTicket(String ticketString) + { + Ticket ticket = getTicketByTicketString(ticketString); + if (ticket == null) + { + return null; + } + return ticket.getUserName(); + } + + public String getCurrentTicket(String userName) + { + String ticket = currentTicket.get(); + if(ticket == null) + { + return getNewTicket(userName); + } + String ticketUser = getAuthorityForTicket(ticket); + if(userName.equals(ticketUser)) + { + return ticket; + } + else + { + return getNewTicket(userName); + } + } + } diff --git a/source/java/org/alfresco/repo/security/authentication/TestAuthenticationServiceImpl.java b/source/java/org/alfresco/repo/security/authentication/TestAuthenticationServiceImpl.java index 4a03194558..14bd082549 100644 --- a/source/java/org/alfresco/repo/security/authentication/TestAuthenticationServiceImpl.java +++ b/source/java/org/alfresco/repo/security/authentication/TestAuthenticationServiceImpl.java @@ -315,6 +315,7 @@ public class TestAuthenticationServiceImpl implements AuthenticationService } + // TODO: Fix this up public String getCurrentTicket() { String currentUser = getCurrentUserName(); @@ -326,6 +327,18 @@ public class TestAuthenticationServiceImpl implements AuthenticationService } return ticket; } + + public String getNewTicket() + { + String currentUser = getCurrentUserName(); + String ticket = userToTicket.get(currentUser); + if (ticket == null) + { + ticket = GUID.generate(); + userToTicket.put(currentUser, ticket); + } + return ticket; + } public void clearCurrentSecurityContext() { diff --git a/source/java/org/alfresco/repo/security/authentication/TicketComponent.java b/source/java/org/alfresco/repo/security/authentication/TicketComponent.java index 520838711a..26d968ceac 100644 --- a/source/java/org/alfresco/repo/security/authentication/TicketComponent.java +++ b/source/java/org/alfresco/repo/security/authentication/TicketComponent.java @@ -34,14 +34,23 @@ package org.alfresco.repo.security.authentication; public interface TicketComponent { /** - * Register a ticket + * Register a new ticket * - * @param authentication - * @return + * @param userName + * @return - the ticket * @throws AuthenticationException */ - public String getTicket(String userName) throws AuthenticationException; + public String getNewTicket(String userName) throws AuthenticationException; + /** + * Get the current ticket + * + * @param userName + * @return - the ticket + */ + + public String getCurrentTicket(String userName); + /** * Check that a certificate is valid and can be used in place of a login. * @@ -62,13 +71,30 @@ public interface TicketComponent * * * - * @param authentication - * @return + * @param ticket + * @return - the user name * @throws AuthenticationException */ public String validateTicket(String ticket) throws AuthenticationException; + /** + * Invalidate the tickets by id + * @param ticket + */ public void invalidateTicketById(String ticket); + /** + * Invalidate all user tickets + * + * @param userName + */ public void invalidateTicketByUser(String userName); + + /** + * Get the authority for the given ticket + * + * @param ticket + * @return the authority + */ + public String getAuthorityForTicket(String ticket); } diff --git a/source/java/org/alfresco/service/cmr/security/AuthenticationService.java b/source/java/org/alfresco/service/cmr/security/AuthenticationService.java index fd4023c42b..b5cde4e515 100644 --- a/source/java/org/alfresco/service/cmr/security/AuthenticationService.java +++ b/source/java/org/alfresco/service/cmr/security/AuthenticationService.java @@ -178,6 +178,13 @@ public interface AuthenticationService @Auditable public String getCurrentTicket(); + /** + * Get the current ticket as a string + * @return + */ + @Auditable + public String getNewTicket(); + /** * Remove the current security information *