Added: incubator/cassandra/trunk/src/java/org/apache/cassandra/auth/SimpleAuthenticator.java URL: http://svn.apache.org/viewvc/incubator/cassandra/trunk/src/java/org/apache/cassandra/auth/SimpleAuthenticator.java?rev=900644&view=auto ============================================================================== --- incubator/cassandra/trunk/src/java/org/apache/cassandra/auth/SimpleAuthenticator.java (added) +++ incubator/cassandra/trunk/src/java/org/apache/cassandra/auth/SimpleAuthenticator.java Tue Jan 19 02:10:41 2010 @@ -0,0 +1,141 @@ +package org.apache.cassandra.auth; + +import java.io.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Properties; + +import org.apache.cassandra.service.*; + +public class SimpleAuthenticator implements IAuthenticator +{ + public final static String PASSWD_FILENAME_PROPERTY = "passwd.properties"; + public final static String AUTHORIZATION_FILENAME_PROPERTY = "authorization.properties"; + public final static String PMODE_PROPERTY = "passwd.mode"; + public static final String USERNAME_KEY = "username"; + public static final String PASSWORD_KEY = "password"; + + public enum PasswordMode + { + PLAIN, MD5, + }; + + @Override + public void login(String keyspace, AuthenticationRequest authRequest) throws AuthenticationException, AuthorizationException + { + String pmode_plain = System.getProperty(PMODE_PROPERTY); + PasswordMode mode = PasswordMode.PLAIN; + + if (null != pmode_plain) + { + try + { + mode = PasswordMode.valueOf(pmode_plain); + } + catch (Exception e) + { + // this is not worth a StringBuffer + String mode_values = ""; + for (PasswordMode pm : PasswordMode.values()) + mode_values += "'" + pm + "', "; + + mode_values += "or leave it unspecified."; + throw new AuthenticationException("The requested password check mode '" + pmode_plain + "' is not a valid mode. Possible values are " + mode_values); + } + } + + String pfilename = System.getProperty(PASSWD_FILENAME_PROPERTY); + + String username = authRequest.getCredentials().get(USERNAME_KEY); + if (null == username) throw new AuthenticationException("Authentication request was missing the required key '" + USERNAME_KEY + "'"); + + String password = authRequest.getCredentials().get(PASSWORD_KEY); + if (null == password) throw new AuthenticationException("Authentication request was missing the required key '" + PASSWORD_KEY + "'"); + + try + { + FileInputStream in = new FileInputStream(pfilename); + Properties props = new Properties(); + props.load(in); + in.close(); + + // note we keep the message here and for the wrong password exactly the same to prevent attackers from guessing what users are valid + if (null == props.getProperty(username)) throw new AuthenticationException(authenticationErrorMessage(mode, username)); + boolean authenticated = false; + switch (mode) + { + case PLAIN: + authenticated = password.equals(props.getProperty(username)); + break; + case MD5: + authenticated = MessageDigest.isEqual(password.getBytes(), MessageDigest.getInstance("MD5").digest(props.getProperty(username).getBytes())); + break; + } + + if (!authenticated) throw new AuthenticationException(authenticationErrorMessage(mode, username)); + } + catch (NoSuchAlgorithmException e) + { + throw new AuthenticationException("You requested MD5 checking but the MD5 digest algorithm is not available: " + e.getMessage()); + } + catch (FileNotFoundException e) + { + throw new RuntimeException("Authentication table file given by property " + PASSWD_FILENAME_PROPERTY + " could not be found: " + e.getMessage()); + } + catch (IOException e) + { + throw new RuntimeException("Authentication table file given by property " + PASSWD_FILENAME_PROPERTY + " could not be opened: " + e.getMessage()); + } + catch (Exception e) + { + throw new RuntimeException("Unexpected authentication problem: " + e.getMessage()); + } + + // if we're here, the authentication succeeded. Now let's see if the user is authorized for this keyspace. + + String afilename = System.getProperty(AUTHORIZATION_FILENAME_PROPERTY); + boolean authorized = false; + try + { + FileInputStream in = new FileInputStream(afilename); + Properties props = new Properties(); + props.load(in); + in.close(); + + // structure: + // given keyspace X, users A B and C can be authorized like this (separate their names with spaces): + // X = A B C + + // note we keep the message here and for other authorization problems exactly the same to prevent attackers from guessing what keyspaces are valid + if (null == props.getProperty(keyspace)) throw new AuthorizationException(authorizationErrorMessage(keyspace, username)); + for (String allow : props.getProperty(keyspace).split(",")) + { + if (allow.equals(username)) authorized = true; + } + + if (!authorized) throw new AuthorizationException(authorizationErrorMessage(keyspace, username)); + } + catch (FileNotFoundException e) + { + throw new RuntimeException("Authorization table file given by property " + AUTHORIZATION_FILENAME_PROPERTY + " could not be found: " + e.getMessage()); + } + catch (IOException e) + { + throw new RuntimeException("Authorization table file given by property " + AUTHORIZATION_FILENAME_PROPERTY + " could not be opened: " + e.getMessage()); + } + catch (Exception e) + { + throw new RuntimeException("Unexpected authorization problem: " + e.getMessage()); + } + } + + static String authorizationErrorMessage(String keyspace, String username) + { + return String.format("User %s could not be authorized to use keyspace %s", username, keyspace); + } + + static String authenticationErrorMessage(PasswordMode mode, String username) + { + return String.format("Given password in password mode %s could not be validated for user %s", mode, username); + } +}
Modified: incubator/cassandra/trunk/src/java/org/apache/cassandra/config/DatabaseDescriptor.java URL: http://svn.apache.org/viewvc/incubator/cassandra/trunk/src/java/org/apache/cassandra/config/DatabaseDescriptor.java?rev=900644&r1=900643&r2=900644&view=diff ============================================================================== --- incubator/cassandra/trunk/src/java/org/apache/cassandra/config/DatabaseDescriptor.java (original) +++ incubator/cassandra/trunk/src/java/org/apache/cassandra/config/DatabaseDescriptor.java Tue Jan 19 02:10:41 2010 @@ -18,6 +18,8 @@ package org.apache.cassandra.config; +import org.apache.cassandra.auth.AllowAllAuthenticator; +import org.apache.cassandra.auth.IAuthenticator; import org.apache.cassandra.db.*; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.BytesType; @@ -137,6 +139,8 @@ private static boolean snapshotBeforeCompaction_; private static boolean autoBootstrap_ = false; + private static IAuthenticator authenticator = new AllowAllAuthenticator(); + static { try @@ -221,6 +225,21 @@ indexAccessMode_ = diskAccessMode_; } + /* Authentication and authorization backend, implementing IAuthenticator */ + String authenticatorClassName = xmlUtils.getNodeValue("/Storage/Authenticator"); + if (authenticatorClassName != null) + { + try + { + Class cls = Class.forName(authenticatorClassName); + authenticator = (IAuthenticator) cls.getConstructor().newInstance(); + } + catch (ClassNotFoundException e) + { + throw new ConfigurationException("Invalid authenticator class " + authenticatorClassName); + } + } + /* Hashing strategy */ String partitionerClassName = xmlUtils.getNodeValue("/Storage/Partitioner"); if (partitionerClassName == null) @@ -599,6 +618,11 @@ } } + public static IAuthenticator getAuthenticator() + { + return authenticator; + } + public static boolean isThriftFramed() { return thriftFramed_; Modified: incubator/cassandra/trunk/src/java/org/apache/cassandra/service/CassandraServer.java URL: http://svn.apache.org/viewvc/incubator/cassandra/trunk/src/java/org/apache/cassandra/service/CassandraServer.java?rev=900644&r1=900643&r2=900644&view=diff ============================================================================== --- incubator/cassandra/trunk/src/java/org/apache/cassandra/service/CassandraServer.java (original) +++ incubator/cassandra/trunk/src/java/org/apache/cassandra/service/CassandraServer.java Tue Jan 19 02:10:41 2010 @@ -28,6 +28,7 @@ import org.apache.commons.lang.ArrayUtils; +import org.apache.cassandra.auth.*; import org.apache.cassandra.config.CFMetaData; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.*; @@ -49,6 +50,16 @@ private final static List<ColumnOrSuperColumn> EMPTY_COLUMNS = Collections.emptyList(); private final static List<Column> EMPTY_SUBCOLUMNS = Collections.emptyList(); + // will be set only by login() + private ThreadLocal<Boolean> loginDone = new ThreadLocal<Boolean>() + { + @Override + protected Boolean initialValue() + { + return false; + } + }; + /* * Handle to the storage service to interact with the other machines in the * cluster. @@ -59,7 +70,7 @@ { storageService = StorageService.instance; } - + protected Map<String, ColumnFamily> readColumnFamily(List<ReadCommand> commands, ConsistencyLevel consistency_level) throws InvalidRequestException, UnavailableException, TimedOutException { @@ -198,6 +209,9 @@ { if (logger.isDebugEnabled()) logger.debug("get_slice"); + + checkLoginDone(); + return multigetSliceInternal(keyspace, Arrays.asList(key), column_parent, predicate, consistency_level).get(key); } @@ -206,6 +220,9 @@ { if (logger.isDebugEnabled()) logger.debug("multiget_slice"); + + checkLoginDone(); + return multigetSliceInternal(keyspace, keys, column_parent, predicate, consistency_level); } @@ -242,6 +259,9 @@ { if (logger.isDebugEnabled()) logger.debug("get"); + + checkLoginDone(); + ColumnOrSuperColumn column = multigetInternal(table, Arrays.asList(key), column_path, consistency_level).get(key); if (!column.isSetColumn() && !column.isSetSuper_column()) { @@ -291,6 +311,9 @@ { if (logger.isDebugEnabled()) logger.debug("multiget"); + + checkLoginDone(); + return multigetInternal(table, keys, column_path, consistency_level); } @@ -349,6 +372,9 @@ { if (logger.isDebugEnabled()) logger.debug("get_count"); + + checkLoginDone(); + return multigetCountInternal(table, Arrays.asList(key), column_parent, consistency_level).get(key); } @@ -394,6 +420,9 @@ { if (logger.isDebugEnabled()) logger.debug("insert"); + + checkLoginDone(); + ThriftValidation.validateKey(key); ThriftValidation.validateColumnPath(table, column_path); @@ -414,6 +443,9 @@ { if (logger.isDebugEnabled()) logger.debug("batch_insert"); + + checkLoginDone(); + ThriftValidation.validateKey(key); for (String cfName : cfmap.keySet()) @@ -432,7 +464,9 @@ { if (logger.isDebugEnabled()) logger.debug("batch_mutate"); - + + checkLoginDone(); + List<RowMutation> rowMutations = new ArrayList<RowMutation>(); for (Map.Entry<String, Map<String, List<Mutation>>> mutationEntry: mutation_map.entrySet()) { @@ -473,6 +507,9 @@ { if (logger.isDebugEnabled()) logger.debug("remove"); + + checkLoginDone(); + ThriftValidation.validateKey(key); ThriftValidation.validateColumnPathOrParent(table, column_path); @@ -586,6 +623,8 @@ if (logger.isDebugEnabled()) logger.debug("range_slice"); + checkLoginDone(); + ThriftValidation.validatePredicate(keyspace, column_parent, predicate); if (!StorageService.getPartitioner().preservesOrder()) { @@ -629,6 +668,9 @@ { if (logger.isDebugEnabled()) logger.debug("get_key_range"); + + checkLoginDone(); + ThriftValidation.validateCommand(tablename, columnFamily); if (!StorageService.getPartitioner().preservesOrder()) { @@ -653,5 +695,18 @@ } } + @Override + public void login(String keyspace, AuthenticationRequest auth_request) throws AuthenticationException, AuthorizationException, TException + { + DatabaseDescriptor.getAuthenticator().login(keyspace, auth_request); + loginDone.set(true); + } + + protected void checkLoginDone() throws InvalidRequestException + { + if (!loginDone.get()) throw new InvalidRequestException("Login is required before any other API calls"); + } + + // main method moved to CassandraDaemon }
