diff --git a/CHANGES.txt b/CHANGES.txt
index 9edc7f4418..1500c432ce 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -31,6 +31,8 @@
* Simplify auth setup and make system_auth ks alterable (CASSANDRA-5112)
* Stop compactions from hanging during bootstrap (CASSANDRA-5244)
* fix compressed streaming sending extra chunk (CASSANDRA-5105)
+ * Add CQL3-based implementations of IAuthenticator and IAuthorizer
+ (CASSANDRA-4898)
1.2.1
diff --git a/NEWS.txt b/NEWS.txt
index 7e04b2ef85..5b4f478a49 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -21,6 +21,23 @@ Upgrading
favor of blob constants) and its support will be removed in a future
version.
+Features
+--------
+ - Built-in CQL3-based implementations of IAuthenticator (PasswordAuthenticator)
+ and IAuthorizer (CassandraAuthorizer) have been added. PasswordAuthenticator
+ stores usernames and hashed passwords in system_auth.credentials table;
+ CassandraAuthorizer stores permissions in system_auth.permissions table.
+ - system_auth keyspace is now alterable via ALTER KEYSPACE queries.
+ The default is SimpleStrategy with replication_factor of 1, but it's
+ advised to raise RF to at least 3 or 5, since CL.QUORUM is used for all
+ auth-related queries. It's also possible to change the strategy to NTS.
+ - Permissions caching with time-based expiration policy has been added to reduce
+ performance impact of authorization. Permission validity can be configured
+ using 'permissions_validity_in_ms' setting in cassandra.yaml. The default
+ is 2000 (2 seconds).
+ - SimpleAuthenticator and SimpleAuthorizer examples have been removed. Please
+ look at CassandraAuthorizer/PasswordAuthenticator instead.
+
1.2.1
=====
diff --git a/build.xml b/build.xml
index c5dc7ac7f9..9cec6ee321 100644
--- a/build.xml
+++ b/build.xml
@@ -381,6 +381,7 @@
+
@@ -463,6 +464,7 @@
+
@@ -1063,7 +1065,6 @@
-
diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml
index bd148e2618..a7011940e4 100644
--- a/conf/cassandra.yaml
+++ b/conf/cassandra.yaml
@@ -54,15 +54,29 @@ max_hints_delivery_threads: 2
# Defaults to: false
# populate_io_cache_on_flush: false
-# authentication backend, implementing IAuthenticator; used to identify users
+# Authentication backend, implementing IAuthenticator; used to identify users
+# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthenticator,
+# PasswordAuthenticator}.
+#
+# - AllowAllAuthenticator performs no checks - set it to disable authentication.
+# - PasswordAuthenticator relies on username/password pairs to authenticate
+# users. It keeps usernames and hashed passwords in system_auth.credentials table.
+# Please increase system_auth keyspace replication factor if you use this authenticator.
authenticator: org.apache.cassandra.auth.AllowAllAuthenticator
-# authorization backend, implementing IAuthorizer; used to limit access/provide permissions
+# Authorization backend, implementing IAuthorizer; used to limit access/provide permissions
+# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthorizer,
+# CassandraAuthorizer}.
+#
+# - AllowAllAuthorizer allows any action to any user - set it to disable authorization.
+# - CassandraAuthorizer stores permissions in system_auth.permissions table. Please
+# increase system_auth keyspace replication factor if you use this authorizer.
authorizer: org.apache.cassandra.auth.AllowAllAuthorizer
# Validity period for permissions cache (fetching permissions can be an
-# expensive operation depending on the authorizer). Defaults to 2000,
-# set to 0 to disable. Will be disabled automatically for AllowAllAuthorizer.
+# expensive operation depending on the authorizer, CassandraAuthorizer is
+# one example). Defaults to 2000, set to 0 to disable.
+# Will be disabled automatically for AllowAllAuthorizer.
permissions_validity_in_ms: 2000
# The partitioner is responsible for distributing rows (by key) across
diff --git a/examples/simple_authentication/README.txt b/examples/simple_authentication/README.txt
deleted file mode 100644
index f81f5e782d..0000000000
--- a/examples/simple_authentication/README.txt
+++ /dev/null
@@ -1,25 +0,0 @@
-The files in this directory provide a (simplistic) example of how to add
-authentication and resource permissions to Cassandra by implementing the
-org.apache.cassandra.auth.{IAuthenticator, IAuthorizer} interfaces.
-
-To try those examples, copy the two JAVA sources (in src/) into the main
-cassandra sources directory and the two configuration files (in conf/) in the
-main cassandra configuration directory.
-
-You can then set the authenticator and authorizer properties in cassandra.yaml
-to use those classes. See the two configuration files access.properties and
-passwd.properties to configure the authorized users and permissions.
-
-When starting cassandra, you need to specify the location of the passwd.properties
-and access.properties files by adding JVM args similar to the following either
-in cassandra-env.sh or as commandline arguments:
-
- -Dpasswd.properties=conf/passwd.properties
- -Daccess.properties=conf/access.properties
-
-For example, you might invoke cassandra as follows:
-
- bin/cassandra -f -Dpasswd.properties=conf/passwd.properties -Daccess.properties=conf/access.properties
-
-Please note that the code in this directory is for demonstration purposes. In
-particular, it does not provide a high level of security.
diff --git a/examples/simple_authentication/conf/access.properties b/examples/simple_authentication/conf/access.properties
deleted file mode 100644
index f403c2fd09..0000000000
--- a/examples/simple_authentication/conf/access.properties
+++ /dev/null
@@ -1,39 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache 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.apache.org/licenses/LICENSE-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.
-
-# This is a sample access file for SimpleAuthorizer. The format of this file
-# is KEYSPACE[.COLUMNFAMILY].PERMISSION=USERS, where:
-#
-# * KEYSPACE is the keyspace name.
-# * COLUMNFAMILY is the column family name.
-# * PERMISSION is one of or for read-only or read-write respectively.
-# * USERS is a comma delimited list of users from passwd.properties.
-#
-# See below for example entries.
-
-# NOTE: This file contains potentially sensitive information, please keep
-# this in mind when setting its mode and ownership.
-
-# The magical '' property lists users who can modify the
-# list of keyspaces: all users will be able to view the list of keyspaces.
-=jsmith
-
-# Access to Keyspace1 (add/remove column families, etc).
-Keyspace1.=jsmith,Elvis Presley
-Keyspace1.=dilbert
-
-# Access to Standard1 (keyspace Keyspace1)
-Keyspace1.Standard1.=jsmith,Elvis Presley,dilbert
diff --git a/examples/simple_authentication/conf/passwd.properties b/examples/simple_authentication/conf/passwd.properties
deleted file mode 100644
index 3099ba6b94..0000000000
--- a/examples/simple_authentication/conf/passwd.properties
+++ /dev/null
@@ -1,24 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache 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.apache.org/licenses/LICENSE-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.
-#
-# This is a sample password file for SimpleAuthenticator. The format of
-# this file is username=password. If -Dpasswd.mode=MD5 then the password
-# is represented as an md5 digest, otherwise it is cleartext (keep this
-# in mind when setting file mode and ownership). 'cassandra' is the default
-# superuser and can be removed later.
-cassandra=cassandra
-jsmith=havebadpass
-dilbert=nomoovertime
diff --git a/examples/simple_authentication/src/org/apache/cassandra/auth/SimpleAuthenticator.java b/examples/simple_authentication/src/org/apache/cassandra/auth/SimpleAuthenticator.java
deleted file mode 100644
index 0248244e47..0000000000
--- a/examples/simple_authentication/src/org/apache/cassandra/auth/SimpleAuthenticator.java
+++ /dev/null
@@ -1,142 +0,0 @@
-package org.apache.cassandra.auth;
-/*
- *
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache 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.apache.org/licenses/LICENSE-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.
- *
- */
-
-
-import java.io.BufferedInputStream;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.MessageDigest;
-import java.util.Map;
-import java.util.Properties;
-
-import org.apache.cassandra.exceptions.AuthenticationException;
-import org.apache.cassandra.exceptions.ConfigurationException;
-import org.apache.cassandra.io.util.FileUtils;
-import org.apache.cassandra.utils.FBUtilities;
-import org.apache.cassandra.utils.Hex;
-
-public class SimpleAuthenticator extends LegacyAuthenticator
-{
- public final static String PASSWD_FILENAME_PROPERTY = "passwd.properties";
- public final static String PMODE_PROPERTY = "passwd.mode";
-
- public enum PasswordMode
- {
- PLAIN, MD5,
- }
-
- public AuthenticatedUser defaultUser()
- {
- // users must log in
- return null;
- }
-
- public AuthenticatedUser authenticate(Map credentials) throws AuthenticationException
- {
- String pmode_plain = System.getProperty(PMODE_PROPERTY);
- PasswordMode mode = PasswordMode.PLAIN;
-
- if (pmode_plain != null)
- {
- 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 = credentials.get(USERNAME_KEY);
- if (username == null)
- throw new AuthenticationException("Authentication request was missing the required key '" + USERNAME_KEY + "'");
-
- String password = credentials.get(PASSWORD_KEY);
- if (password == null)
- throw new AuthenticationException("Authentication request was missing the required key '" + PASSWORD_KEY + "'");
-
- boolean authenticated = false;
-
- InputStream in = null;
- try
- {
- in = new BufferedInputStream(new FileInputStream(pfilename));
- Properties props = new Properties();
- props.load(in);
-
- // note we keep the message here and for the wrong password exactly the same to prevent attackers from guessing what users are valid
- if (props.getProperty(username) == null) throw new AuthenticationException(authenticationErrorMessage(mode, username));
- switch (mode)
- {
- case PLAIN:
- authenticated = password.equals(props.getProperty(username));
- break;
- case MD5:
- authenticated = MessageDigest.isEqual(FBUtilities.threadLocalMD5Digest().digest(password.getBytes()), Hex.hexToBytes(props.getProperty(username)));
- break;
- default:
- throw new RuntimeException("Unknown PasswordMode " + mode);
- }
- }
- 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);
- }
- finally
- {
- FileUtils.closeQuietly(in);
- }
-
- if (!authenticated) throw new AuthenticationException(authenticationErrorMessage(mode, username));
-
- return new AuthenticatedUser(username);
- }
-
- public void validateConfiguration() throws ConfigurationException
- {
- String pfilename = System.getProperty(SimpleAuthenticator.PASSWD_FILENAME_PROPERTY);
- if (pfilename == null)
- {
- throw new ConfigurationException("When using " + this.getClass().getCanonicalName() + " " +
- SimpleAuthenticator.PASSWD_FILENAME_PROPERTY + " properties must be defined.");
- }
- }
-
- 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);
- }
-}
diff --git a/examples/simple_authentication/src/org/apache/cassandra/auth/SimpleAuthorizer.java b/examples/simple_authentication/src/org/apache/cassandra/auth/SimpleAuthorizer.java
deleted file mode 100644
index df626547a6..0000000000
--- a/examples/simple_authentication/src/org/apache/cassandra/auth/SimpleAuthorizer.java
+++ /dev/null
@@ -1,157 +0,0 @@
-package org.apache.cassandra.auth;
-/*
- *
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache 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.apache.org/licenses/LICENSE-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.
- *
- */
-
-
-import java.io.BufferedInputStream;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Properties;
-
-import org.apache.cassandra.exceptions.ConfigurationException;
-import org.apache.cassandra.io.util.FileUtils;
-
-public class SimpleAuthorizer extends LegacyAuthorizer
-{
- public final static String ACCESS_FILENAME_PROPERTY = "access.properties";
- // magical property for WRITE permissions to the keyspaces list
- public final static String KEYSPACES_WRITE_PROPERTY = "";
-
- public EnumSet authorize(AuthenticatedUser user, List resource)
- {
- if (resource.size() < 2 || !Resources.ROOT.equals(resource.get(0)) || !Resources.KEYSPACES.equals(resource.get(1)))
- return EnumSet.noneOf(Permission.class);
-
- String keyspace, columnFamily = null;
- EnumSet authorized = EnumSet.noneOf(Permission.class);
-
- // /cassandra/keyspaces
- if (resource.size() == 2)
- {
- keyspace = KEYSPACES_WRITE_PROPERTY;
- authorized = EnumSet.of(Permission.READ);
- }
- // /cassandra/keyspaces/
- else if (resource.size() == 3)
- {
- keyspace = (String)resource.get(2);
- }
- // /cassandra/keyspaces//
- else if (resource.size() == 4)
- {
- keyspace = (String)resource.get(2);
- columnFamily = (String)resource.get(3);
- }
- else
- {
- // We don't currently descend any lower in the hierarchy.
- throw new UnsupportedOperationException();
- }
-
- String accessFilename = System.getProperty(ACCESS_FILENAME_PROPERTY);
- InputStream in=null;
- try
- {
- in = new BufferedInputStream(new FileInputStream(accessFilename));
- Properties accessProperties = new Properties();
- accessProperties.load(in);
-
- // Special case access to the keyspace list
- if (keyspace == KEYSPACES_WRITE_PROPERTY)
- {
- String kspAdmins = accessProperties.getProperty(KEYSPACES_WRITE_PROPERTY);
- for (String admin : kspAdmins.split(","))
- if (admin.equals(user.getName()))
- return EnumSet.copyOf(Permission.ALL);
- }
-
- boolean canRead = false, canWrite = false;
- String readers = null, writers = null;
-
- if (columnFamily == null)
- {
- readers = accessProperties.getProperty(keyspace + ".");
- writers = accessProperties.getProperty(keyspace + ".");
- }
- else
- {
- readers = accessProperties.getProperty(keyspace + "." + columnFamily + ".");
- writers = accessProperties.getProperty(keyspace + "." + columnFamily + ".");
- }
-
- if (readers != null)
- {
- for (String reader : readers.split(","))
- {
- if (reader.equals(user.getName()))
- {
- canRead = true;
- break;
- }
- }
- }
-
- if (writers != null)
- {
- for (String writer : writers.split(","))
- {
- if (writer.equals(user.getName()))
- {
- canWrite = true;
- break;
- }
- }
- }
-
- if (canWrite)
- authorized = EnumSet.copyOf(Permission.ALL);
- else if (canRead)
- authorized = EnumSet.of(Permission.READ);
-
- }
- catch (IOException e)
- {
- throw new RuntimeException(String.format("Authorization table file '%s' could not be opened: %s",
- accessFilename,
- e.getMessage()));
- }
- finally
- {
- FileUtils.closeQuietly(in);
- }
-
- return authorized;
- }
-
- public void validateConfiguration() throws ConfigurationException
- {
- String afilename = System.getProperty(ACCESS_FILENAME_PROPERTY);
- if (afilename == null)
- {
- throw new ConfigurationException(String.format("When using %s, '%s' property must be defined.",
- this.getClass().getCanonicalName(),
- ACCESS_FILENAME_PROPERTY));
- }
- }
-}
diff --git a/lib/jbcrypt-0.3m.jar b/lib/jbcrypt-0.3m.jar
new file mode 100644
index 0000000000..ccace8bbcd
Binary files /dev/null and b/lib/jbcrypt-0.3m.jar differ
diff --git a/lib/licenses/jbcrypt-0.3m.txt b/lib/licenses/jbcrypt-0.3m.txt
new file mode 100644
index 0000000000..d332534c06
--- /dev/null
+++ b/lib/licenses/jbcrypt-0.3m.txt
@@ -0,0 +1,17 @@
+jBCrypt is subject to the following license:
+
+/*
+ * Copyright (c) 2006 Damien Miller
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
diff --git a/src/java/org/apache/cassandra/auth/Auth.java b/src/java/org/apache/cassandra/auth/Auth.java
index 399dc26f75..43118e45c3 100644
--- a/src/java/org/apache/cassandra/auth/Auth.java
+++ b/src/java/org/apache/cassandra/auth/Auth.java
@@ -100,6 +100,7 @@ public class Auth
*
* @param username Username to insert.
* @param isSuper User's new status.
+ * @throws RequestExecutionException
*/
public static void insertUser(String username, boolean isSuper) throws RequestExecutionException
{
@@ -115,6 +116,7 @@ public class Auth
* Deletes the user from AUTH_KS.USERS_CF.
*
* @param username Username to delete.
+ * @throws RequestExecutionException
*/
public static void deleteUser(String username) throws RequestExecutionException
{
@@ -140,7 +142,7 @@ public class Auth
MigrationManager.instance.register(new MigrationListener());
// the delay is here to give the node some time to see its peers - to reduce
- // "Skipping default superuser setup: some nodes are not ready" log spam.
+ // "Skipped default superuser setup: some nodes were not ready" log spam.
// It's the only reason for the delay.
StorageService.tasks.schedule(new Runnable()
{
@@ -191,18 +193,18 @@ public class Auth
// insert a default superuser if AUTH_KS.USERS_CF is empty.
if (QueryProcessor.process(String.format("SELECT * FROM %s.%s", AUTH_KS, USERS_CF), ConsistencyLevel.QUORUM).isEmpty())
{
- logger.info("Creating default superuser '{}'", DEFAULT_SUPERUSER_NAME);
QueryProcessor.process(String.format("INSERT INTO %s.%s (name, super) VALUES ('%s', %s) USING TIMESTAMP 0",
AUTH_KS,
USERS_CF,
DEFAULT_SUPERUSER_NAME,
true),
ConsistencyLevel.QUORUM);
+ logger.info("Created default superuser '{}'", DEFAULT_SUPERUSER_NAME);
}
}
catch (RequestExecutionException e)
{
- logger.warn("Skipping default superuser setup: some nodes are not ready");
+ logger.warn("Skipped default superuser setup: some nodes were not ready");
}
}
diff --git a/src/java/org/apache/cassandra/auth/CassandraAuthorizer.java b/src/java/org/apache/cassandra/auth/CassandraAuthorizer.java
new file mode 100644
index 0000000000..2227e5b213
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/CassandraAuthorizer.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache 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.apache.org/licenses/LICENSE-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.apache.cassandra.auth;
+
+import java.util.*;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.exceptions.*;
+
+/**
+ * CassandraAuthorizer is an IAuthorizer implementation that keeps
+ * permissions internally in C* - in system_auth.permissions CQL3 table.
+ */
+public class CassandraAuthorizer implements IAuthorizer
+{
+ private static final Logger logger = LoggerFactory.getLogger(CassandraAuthorizer.class);
+
+ private static final String USERNAME = "username";
+ private static final String RESOURCE = "resource";
+ private static final String PERMISSIONS = "permissions";
+
+ private static final String PERMISSIONS_CF = "permissions";
+ private static final String PERMISSIONS_CF_SCHEMA = String.format("CREATE TABLE %s.%s ("
+ + "username text,"
+ + "resource text,"
+ + "permissions set,"
+ + "PRIMARY KEY(username, resource)"
+ + ") WITH gc_grace_seconds=%d",
+ Auth.AUTH_KS,
+ PERMISSIONS_CF,
+ 90 * 24 * 60 * 60); // 3 months.
+
+ // Returns every permission on the resource granted to the user.
+ public Set authorize(AuthenticatedUser user, IResource resource)
+ {
+ if (user.isSuper())
+ return Permission.ALL;
+
+ UntypedResultSet rows;
+ try
+ {
+ rows = process(String.format("SELECT permissions FROM %s.%s WHERE username = '%s' AND resource = '%s'",
+ Auth.AUTH_KS,
+ PERMISSIONS_CF,
+ escape(user.getName()),
+ escape(resource.getName())));
+ }
+ catch (RequestExecutionException e)
+ {
+ logger.warn("CassandraAuthorizer failed to authorize {} for {}", user, resource);
+ return Permission.NONE;
+ }
+
+ if (rows.isEmpty() || !rows.one().has(PERMISSIONS))
+ return Permission.NONE;
+
+ Set permissions = EnumSet.noneOf(Permission.class);
+ for (String perm : rows.one().getSet(PERMISSIONS, UTF8Type.instance))
+ permissions.add(Permission.valueOf(perm));
+ return permissions;
+ }
+
+ public void grant(AuthenticatedUser performer, Set permissions, IResource resource, String to)
+ throws RequestExecutionException
+ {
+ modify(permissions, resource, to, "+");
+ }
+
+ public void revoke(AuthenticatedUser performer, Set permissions, IResource resource, String from)
+ throws RequestExecutionException
+ {
+ modify(permissions, resource, from, "-");
+ }
+
+ // Adds or removes permissions from user's 'permissions' set (adds if op is "+", removes if op is "-")
+ private void modify(Set permissions, IResource resource, String user, String op) throws RequestExecutionException
+ {
+ process(String.format("UPDATE %s.%s SET permissions = permissions %s {%s} WHERE username = '%s' AND resource = '%s'",
+ Auth.AUTH_KS,
+ PERMISSIONS_CF,
+ op,
+ "'" + StringUtils.join(permissions, "','") + "'",
+ escape(user),
+ escape(resource.getName())));
+ }
+
+ // 'of' can be null - in that case everyone's permissions have been requested. Otherwise only single user's.
+ // If the user requesting 'LIST PERMISSIONS' is not a superuser OR his username doesn't match 'of', we
+ // throw UnauthorizedException. So only a superuser can view everybody's permissions. Regular users are only
+ // allowed to see their own permissions.
+ public Set list(AuthenticatedUser performer, Set permissions, IResource resource, String of)
+ throws RequestValidationException, RequestExecutionException
+ {
+ if (!performer.isSuper() && !performer.getName().equals(of))
+ throw new UnauthorizedException(String.format("You are not authorized to view %s's permissions",
+ of == null ? "everyone" : of));
+
+ Set details = new HashSet();
+
+ for (UntypedResultSet.Row row : process(buildListQuery(resource, of)))
+ {
+ if (row.has(PERMISSIONS))
+ {
+ for (String p : row.getSet(PERMISSIONS, UTF8Type.instance))
+ {
+ Permission permission = Permission.valueOf(p);
+ if (permissions.contains(permission))
+ details.add(new PermissionDetails(row.getString(USERNAME),
+ DataResource.fromName(row.getString(RESOURCE)),
+ permission));
+ }
+ }
+ }
+
+ return details;
+ }
+
+ private static String buildListQuery(IResource resource, String of)
+ {
+ List vars = Lists.newArrayList(Auth.AUTH_KS, PERMISSIONS_CF);
+ List conditions = new ArrayList();
+
+ if (resource != null)
+ {
+ conditions.add("resource = '%s'");
+ vars.add(escape(resource.getName()));
+ }
+
+ if (of != null)
+ {
+ conditions.add("username = '%s'");
+ vars.add(escape(of));
+ }
+
+ String query = "SELECT username, resource, permissions FROM %s.%s";
+
+ if (!conditions.isEmpty())
+ query += " WHERE " + StringUtils.join(conditions, " AND ");
+
+ if (resource != null && of == null)
+ query += " ALLOW FILTERING";
+
+ return String.format(query, vars.toArray());
+ }
+
+ // Called prior to deleting the user with DROP USER query. Internal hook, so no permission checks are needed here.
+ public void revokeAll(String droppedUser)
+ {
+ try
+ {
+ process(String.format("DELETE FROM %s.%s WHERE username = '%s'", Auth.AUTH_KS, PERMISSIONS_CF, escape(droppedUser)));
+ }
+ catch (Throwable e)
+ {
+ logger.warn("CassandraAuthorizer failed to revoke all permissions of {}: {}", droppedUser, e);
+ }
+ }
+
+ // Called after a resource is removed (DROP KEYSPACE, DROP TABLE, etc.).
+ public void revokeAll(IResource droppedResource)
+ {
+
+ UntypedResultSet rows;
+ try
+ {
+ // TODO: switch to secondary index on 'resource' once https://issues.apache.org/jira/browse/CASSANDRA-5125 is resolved.
+ rows = process(String.format("SELECT username FROM %s.%s WHERE resource = '%s' ALLOW FILTERING",
+ Auth.AUTH_KS,
+ PERMISSIONS_CF,
+ escape(droppedResource.getName())));
+ }
+ catch (Throwable e)
+ {
+ logger.warn("CassandraAuthorizer failed to revoke all permissions on {}: {}", droppedResource, e);
+ return;
+ }
+
+ for (UntypedResultSet.Row row : rows)
+ {
+ try
+ {
+ process(String.format("DELETE FROM %s.%s WHERE username = '%s' AND resource = '%s'",
+ Auth.AUTH_KS,
+ PERMISSIONS_CF,
+ escape(row.getString(USERNAME)),
+ escape(droppedResource.getName())));
+ }
+ catch (Throwable e)
+ {
+ logger.warn("CassandraAuthorizer failed to revoke all permissions on {}: {}", droppedResource, e);
+ }
+ }
+ }
+
+ public Set protectedResources()
+ {
+ return ImmutableSet.of(DataResource.columnFamily(Auth.AUTH_KS, PERMISSIONS_CF));
+ }
+
+ public void validateConfiguration() throws ConfigurationException
+ {
+ }
+
+ public void setup()
+ {
+ if (Schema.instance.getCFMetaData(Auth.AUTH_KS, PERMISSIONS_CF) == null)
+ {
+ try
+ {
+ process(PERMISSIONS_CF_SCHEMA);
+ }
+ catch (RequestExecutionException e)
+ {
+ throw new AssertionError(e);
+ }
+ }
+ }
+
+ // We only worry about one character ('). Make sure it's properly escaped.
+ private static String escape(String name)
+ {
+ return StringUtils.replace(name, "'", "''");
+ }
+
+ private static UntypedResultSet process(String query) throws RequestExecutionException
+ {
+ return QueryProcessor.process(query, ConsistencyLevel.QUORUM);
+ }
+}
diff --git a/src/java/org/apache/cassandra/auth/IAuthenticator.java b/src/java/org/apache/cassandra/auth/IAuthenticator.java
index 69a9e8316f..608649078e 100644
--- a/src/java/org/apache/cassandra/auth/IAuthenticator.java
+++ b/src/java/org/apache/cassandra/auth/IAuthenticator.java
@@ -22,7 +22,8 @@ import java.util.Set;
import org.apache.cassandra.exceptions.AuthenticationException;
import org.apache.cassandra.exceptions.ConfigurationException;
-import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.exceptions.RequestValidationException;
public interface IAuthenticator
{
@@ -72,9 +73,10 @@ public interface IAuthenticator
*
* @param username Username of the user to create.
* @param options Options the user will be created with.
- * @throws InvalidRequestException
+ * @throws RequestValidationException
+ * @throws RequestExecutionException
*/
- void create(String username, Map options) throws InvalidRequestException;
+ void create(String username, Map options) throws RequestValidationException, RequestExecutionException;
/**
* Called during execution of ALTER USER query.
@@ -84,23 +86,25 @@ public interface IAuthenticator
*
* @param username Username of the user that will be altered.
* @param options Options to alter.
- * @throws InvalidRequestException
+ * @throws RequestValidationException
+ * @throws RequestExecutionException
*/
- void alter(String username, Map options) throws InvalidRequestException;
+ void alter(String username, Map options) throws RequestValidationException, RequestExecutionException;
/**
* Called during execution of DROP USER query.
*
* @param username Username of the user that will be dropped.
- * @throws InvalidRequestException
+ * @throws RequestValidationException
+ * @throws RequestExecutionException
*/
- void drop(String username) throws InvalidRequestException;
+ void drop(String username) throws RequestValidationException, RequestExecutionException;
/**
* Set of resources that should be made inaccessible to users and only accessible internally.
*
- * @return Keyspaces, column families that will be unreadable and unmodifiable by users; other resources.
+ * @return Keyspaces, column families that will be unmodifiable by users; other resources.
*/
Set extends IResource> protectedResources();
diff --git a/src/java/org/apache/cassandra/auth/IAuthorizer.java b/src/java/org/apache/cassandra/auth/IAuthorizer.java
index de1b3930b5..8ad204f073 100644
--- a/src/java/org/apache/cassandra/auth/IAuthorizer.java
+++ b/src/java/org/apache/cassandra/auth/IAuthorizer.java
@@ -20,8 +20,8 @@ package org.apache.cassandra.auth;
import java.util.Set;
import org.apache.cassandra.exceptions.ConfigurationException;
-import org.apache.cassandra.exceptions.InvalidRequestException;
-import org.apache.cassandra.exceptions.UnauthorizedException;
+import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.exceptions.RequestValidationException;
/**
* Primary Cassandra authorization interface.
@@ -46,11 +46,11 @@ public interface IAuthorizer
* @param to Grantee of the permissions.
* @param resource Resource on which to grant the permissions.
*
- * @throws UnauthorizedException if the granting user isn't allowed to grant (and revoke) the permissions on the resource.
- * @throws InvalidRequestException upon parameter misconfiguration or internal error.
+ * @throws RequestValidationException
+ * @throws RequestExecutionException
*/
void grant(AuthenticatedUser performer, Set permissions, IResource resource, String to)
- throws UnauthorizedException, InvalidRequestException;
+ throws RequestValidationException, RequestExecutionException;
/**
* Revokes a set of permissions on a resource from a user.
@@ -61,11 +61,11 @@ public interface IAuthorizer
* @param from Revokee of the permissions.
* @param resource Resource on which to revoke the permissions.
*
- * @throws UnauthorizedException if the revoking user isn't allowed to revoke the permissions on the resource.
- * @throws InvalidRequestException upon parameter misconfiguration or internal error.
+ * @throws RequestValidationException
+ * @throws RequestExecutionException
*/
void revoke(AuthenticatedUser performer, Set permissions, IResource resource, String from)
- throws UnauthorizedException, InvalidRequestException;
+ throws RequestValidationException, RequestExecutionException;
/**
* Returns a list of permissions on a resource of a user.
@@ -78,11 +78,11 @@ public interface IAuthorizer
*
* @return All of the matching permission that the requesting user is authorized to know about.
*
- * @throws UnauthorizedException if the user isn't allowed to view the requested permissions.
- * @throws InvalidRequestException upon parameter misconfiguration or internal error.
+ * @throws RequestValidationException
+ * @throws RequestExecutionException
*/
Set list(AuthenticatedUser performer, Set permissions, IResource resource, String of)
- throws UnauthorizedException, InvalidRequestException;
+ throws RequestValidationException, RequestExecutionException;
/**
* This method is called before deleting a user with DROP USER query so that a new user with the same
@@ -102,7 +102,7 @@ public interface IAuthorizer
/**
* Set of resources that should be made inaccessible to users and only accessible internally.
*
- * @return Keyspaces, column families that will be unreadable and unmodifiable by users; other resources.
+ * @return Keyspaces, column families that will be unmodifiable by users; other resources.
*/
Set extends IResource> protectedResources();
diff --git a/src/java/org/apache/cassandra/auth/LegacyAuthenticator.java b/src/java/org/apache/cassandra/auth/LegacyAuthenticator.java
index 5210af54a7..c5fd8daab4 100644
--- a/src/java/org/apache/cassandra/auth/LegacyAuthenticator.java
+++ b/src/java/org/apache/cassandra/auth/LegacyAuthenticator.java
@@ -17,11 +17,14 @@
*/
package org.apache.cassandra.auth;
-import java.util.*;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
import org.apache.cassandra.exceptions.AuthenticationException;
import org.apache.cassandra.exceptions.ConfigurationException;
-import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.exceptions.RequestValidationException;
/**
* Provides a transitional IAuthenticator implementation for old-style (pre-1.2) authenticators.
@@ -64,18 +67,17 @@ public abstract class LegacyAuthenticator implements IAuthenticator
}
@Override
- public void create(String username, Map options) throws InvalidRequestException
+ public void create(String username, Map options) throws RequestValidationException, RequestExecutionException
{
}
@Override
- public void alter(String username, Map options) throws InvalidRequestException
+ public void alter(String username, Map options) throws RequestValidationException, RequestExecutionException
{
- throw new InvalidRequestException("ALTER USER operation is not supported by LegacyAuthenticator");
}
@Override
- public void drop(String username) throws InvalidRequestException
+ public void drop(String username) throws RequestValidationException, RequestExecutionException
{
}
diff --git a/src/java/org/apache/cassandra/auth/LegacyAuthorizer.java b/src/java/org/apache/cassandra/auth/LegacyAuthorizer.java
index 5948d34544..f834793c98 100644
--- a/src/java/org/apache/cassandra/auth/LegacyAuthorizer.java
+++ b/src/java/org/apache/cassandra/auth/LegacyAuthorizer.java
@@ -72,14 +72,14 @@ public abstract class LegacyAuthorizer implements IAuthorizer
@Override
public void grant(AuthenticatedUser performer, Set permissions, IResource resource, String to)
- throws InvalidRequestException, UnauthorizedException
+ throws InvalidRequestException
{
throw new InvalidRequestException("GRANT operation is not supported by LegacyAuthorizer");
}
@Override
public void revoke(AuthenticatedUser performer, Set permissions, IResource resource, String from)
- throws InvalidRequestException, UnauthorizedException
+ throws InvalidRequestException
{
throw new InvalidRequestException("REVOKE operation is not supported by LegacyAuthorizer");
}
diff --git a/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java b/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
new file mode 100644
index 0000000000..f8f44d4f24
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
@@ -0,0 +1,223 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache 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.apache.org/licenses/LICENSE-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.apache.cassandra.auth;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.collect.ImmutableSet;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.exceptions.AuthenticationException;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.service.StorageService;
+import org.mindrot.jbcrypt.BCrypt;
+
+/**
+ * PasswordAuthenticator is an IAuthenticator implementation
+ * that keeps credentials (usernames and bcrypt-hashed passwords)
+ * internally in C* - in system_auth.credentials CQL3 table.
+ */
+public class PasswordAuthenticator implements IAuthenticator
+{
+ private static final Logger logger = LoggerFactory.getLogger(PasswordAuthenticator.class);
+
+ private static final long DEFAULT_USER_SETUP_DELAY = 10; // seconds
+
+ // 2 ** GENSALT_LOG2_ROUNS rounds of hashing will be performed.
+ private static final int GENSALT_LOG2_ROUNDS = 10;
+
+ // name of the hash column.
+ private static final String SALTED_HASH = "salted_hash";
+
+ private static final String DEFAULT_USER_NAME = Auth.DEFAULT_SUPERUSER_NAME;
+ private static final String DEFAULT_USER_PASSWORD = Auth.DEFAULT_SUPERUSER_NAME;
+
+ private static final String CREDENTIALS_CF = "credentials";
+ private static final String CREDENTIALS_CF_SCHEMA = String.format("CREATE TABLE %s.%s ("
+ + "username text,"
+ + "salted_hash text," // salt + hash + number of rounds
+ + "options map," // for future extensions
+ + "PRIMARY KEY(username)"
+ + ") WITH gc_grace_seconds=%d",
+ Auth.AUTH_KS,
+ CREDENTIALS_CF,
+ 90 * 24 * 60 * 60); // 3 months.
+
+ // No anonymous access.
+ public boolean requireAuthentication()
+ {
+ return true;
+ }
+
+ public Set supportedOptions()
+ {
+ return ImmutableSet.of(Option.PASSWORD);
+ }
+
+ // Let users alter their own password.
+ public Set alterableOptions()
+ {
+ return ImmutableSet.of(Option.PASSWORD);
+ }
+
+ public AuthenticatedUser authenticate(Map credentials) throws AuthenticationException
+ {
+ String username = credentials.get(USERNAME_KEY);
+ if (username == null)
+ throw new AuthenticationException(String.format("Required key '%s' is missing", USERNAME_KEY));
+
+ String password = credentials.get(PASSWORD_KEY);
+ if (password == null)
+ throw new AuthenticationException(String.format("Required key '%s' is missing", PASSWORD_KEY));
+
+ UntypedResultSet result;
+ try
+ {
+ result = process(String.format("SELECT %s FROM %s.%s WHERE username = '%s'",
+ SALTED_HASH,
+ Auth.AUTH_KS,
+ CREDENTIALS_CF,
+ escape(username)));
+ }
+ catch (RequestExecutionException e)
+ {
+ throw new AuthenticationException(e.toString());
+ }
+
+ if (result.isEmpty() || !BCrypt.checkpw(password, result.one().getString(SALTED_HASH)))
+ throw new AuthenticationException("Username and/or password are incorrect");
+
+ return new AuthenticatedUser(username);
+ }
+
+ public void create(String username, Map options) throws InvalidRequestException, RequestExecutionException
+ {
+ String password = (String) options.get(Option.PASSWORD);
+ if (password == null)
+ throw new InvalidRequestException("PasswordAuthenticator requires PASSWORD option");
+
+ process(String.format("INSERT INTO %s.%s (username, salted_hash) VALUES ('%s', '%s')",
+ Auth.AUTH_KS,
+ CREDENTIALS_CF,
+ escape(username),
+ escape(hashpw(password))));
+ }
+
+ public void alter(String username, Map options) throws RequestExecutionException
+ {
+ process(String.format("UPDATE %s.%s SET salted_hash = '%s' WHERE username = '%s'",
+ Auth.AUTH_KS,
+ CREDENTIALS_CF,
+ escape(hashpw((String) options.get(Option.PASSWORD))),
+ escape(username)));
+ }
+
+ public void drop(String username) throws RequestExecutionException
+ {
+ process(String.format("DELETE FROM %s.%s WHERE username = '%s'", Auth.AUTH_KS, CREDENTIALS_CF, escape(username)));
+ }
+
+ public Set protectedResources()
+ {
+ return ImmutableSet.of(DataResource.columnFamily(Auth.AUTH_KS, CREDENTIALS_CF));
+ }
+
+ public void validateConfiguration() throws ConfigurationException
+ {
+ }
+
+ public void setup()
+ {
+ setupCredentialsTable();
+
+ // the delay is here to give the node some time to see its peers - to reduce
+ // "skipped default user setup: some nodes are were not ready" log spam.
+ // It's the only reason for the delay.
+ StorageService.tasks.schedule(new Runnable()
+ {
+ public void run()
+ {
+ setupDefaultUser();
+ }
+ },
+ DEFAULT_USER_SETUP_DELAY,
+ TimeUnit.SECONDS);
+ }
+
+ private void setupCredentialsTable()
+ {
+ if (Schema.instance.getCFMetaData(Auth.AUTH_KS, CREDENTIALS_CF) == null)
+ {
+ try
+ {
+ process(CREDENTIALS_CF_SCHEMA);
+ }
+ catch (RequestExecutionException e)
+ {
+ throw new AssertionError(e);
+ }
+ }
+ }
+
+ // if there are no users yet - add default superuser.
+ private void setupDefaultUser()
+ {
+ try
+ {
+ // insert a default superuser if AUTH_KS.CREDENTIALS_CF is empty.
+ if (process(String.format("SELECT * FROM %s.%s", Auth.AUTH_KS, CREDENTIALS_CF)).isEmpty())
+ {
+ process(String.format("INSERT INTO %s.%s (username, salted_hash) VALUES ('%s', '%s') USING TIMESTAMP 0",
+ Auth.AUTH_KS,
+ CREDENTIALS_CF,
+ DEFAULT_USER_NAME,
+ escape(hashpw(DEFAULT_USER_PASSWORD))));
+ logger.info("PasswordAuthenticator created default user '{}'", DEFAULT_USER_NAME);
+ }
+ }
+ catch (RequestExecutionException e)
+ {
+ logger.warn("PasswordAuthenticator skipped default user setup: some nodes were not ready");
+ }
+ }
+
+ private static String hashpw(String password)
+ {
+ return BCrypt.hashpw(password, BCrypt.gensalt(GENSALT_LOG2_ROUNDS));
+ }
+
+ private static String escape(String name)
+ {
+ return StringUtils.replace(name, "'", "''");
+ }
+
+ private static UntypedResultSet process(String query) throws RequestExecutionException
+ {
+ return QueryProcessor.process(query, ConsistencyLevel.QUORUM);
+ }
+}
diff --git a/src/java/org/apache/cassandra/cli/CliSessionState.java b/src/java/org/apache/cassandra/cli/CliSessionState.java
index d36b7f3b97..e1a2ebdb14 100644
--- a/src/java/org/apache/cassandra/cli/CliSessionState.java
+++ b/src/java/org/apache/cassandra/cli/CliSessionState.java
@@ -35,8 +35,8 @@ public class CliSessionState
public String hostName; // cassandra server name
public int thriftPort; // cassandra server's thrift port
public boolean debug = false; // print stack traces when errors occur in the CLI
- public String username; // cassandra login name (if SimpleAuthenticator is used)
- public String password; // cassandra login password (if SimpleAuthenticator is used)
+ public String username; // cassandra login name (if password-based authenticator is used)
+ public String password; // cassandra login password (if password-based authenticator is used)
public String keyspace; // cassandra keyspace user is authenticating
public boolean batch = false; // enable/disable batch processing mode
public String filename = ""; // file to read commands from
diff --git a/src/java/org/apache/cassandra/cql3/statements/AlterUserStatement.java b/src/java/org/apache/cassandra/cql3/statements/AlterUserStatement.java
index 68c3d0da0c..7a6e96b030 100644
--- a/src/java/org/apache/cassandra/cql3/statements/AlterUserStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/AlterUserStatement.java
@@ -24,6 +24,7 @@ import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.UserOptions;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.exceptions.UnauthorizedException;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.transport.messages.ResultMessage;
@@ -41,32 +42,36 @@ public class AlterUserStatement extends AuthenticationStatement
this.superuser = superuser;
}
- public void validate(ClientState state) throws InvalidRequestException
+ public void validate(ClientState state) throws RequestValidationException
{
opts.validate();
if (superuser == null && opts.isEmpty())
throw new InvalidRequestException("ALTER USER can't be empty");
+ // validate login here before checkAccess to avoid leaking user existence to anonymous users.
+ state.ensureNotAnonymous();
+
if (!Auth.isExistingUser(username))
throw new InvalidRequestException(String.format("User %s doesn't exist", username));
}
public void checkAccess(ClientState state) throws UnauthorizedException
{
- state.validateLogin();
AuthenticatedUser user = state.getUser();
+ boolean isSuper = user.isSuper();
+
if (superuser != null && user.getName().equals(username))
throw new UnauthorizedException("You aren't allowed to alter your own superuser status");
- if (superuser != null && !user.isSuper())
+ if (superuser != null && !isSuper)
throw new UnauthorizedException("Only superusers are allowed to alter superuser status");
if (!user.isSuper() && !user.getName().equals(username))
throw new UnauthorizedException("You aren't allowed to alter this user");
- if (!user.isSuper())
+ if (!isSuper)
{
for (IAuthenticator.Option option : opts.getOptions().keySet())
{
@@ -76,7 +81,7 @@ public class AlterUserStatement extends AuthenticationStatement
}
}
- public ResultMessage execute(ClientState state) throws InvalidRequestException, RequestExecutionException
+ public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
if (!opts.isEmpty())
DatabaseDescriptor.getAuthenticator().alter(username, opts.getOptions());
diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateUserStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateUserStatement.java
index b1a91cdab9..df3a5e7591 100644
--- a/src/java/org/apache/cassandra/cql3/statements/CreateUserStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/CreateUserStatement.java
@@ -22,6 +22,7 @@ import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.UserOptions;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.exceptions.UnauthorizedException;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.transport.messages.ResultMessage;
@@ -39,24 +40,27 @@ public class CreateUserStatement extends AuthenticationStatement
this.superuser = superuser;
}
- public void validate(ClientState state) throws InvalidRequestException
+ public void validate(ClientState state) throws RequestValidationException
{
if (username.isEmpty())
throw new InvalidRequestException("Username can't be an empty string");
+
opts.validate();
+
+ // validate login here before checkAccess to avoid leaking user existence to anonymous users.
+ state.ensureNotAnonymous();
+
if (Auth.isExistingUser(username))
throw new InvalidRequestException(String.format("User %s already exists", username));
}
public void checkAccess(ClientState state) throws UnauthorizedException
{
- state.validateLogin();
-
if (!state.getUser().isSuper())
throw new UnauthorizedException("Only superusers are allowed to perfrom CREATE USER queries");
}
- public ResultMessage execute(ClientState state) throws InvalidRequestException, RequestExecutionException
+ public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
DatabaseDescriptor.getAuthenticator().create(username, opts.getOptions());
Auth.insertUser(username, superuser);
diff --git a/src/java/org/apache/cassandra/cql3/statements/DropUserStatement.java b/src/java/org/apache/cassandra/cql3/statements/DropUserStatement.java
index 493cd231aa..ae6cf67435 100644
--- a/src/java/org/apache/cassandra/cql3/statements/DropUserStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/DropUserStatement.java
@@ -22,6 +22,7 @@ import org.apache.cassandra.auth.AuthenticatedUser;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.exceptions.UnauthorizedException;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.transport.messages.ResultMessage;
@@ -35,8 +36,11 @@ public class DropUserStatement extends AuthenticationStatement
this.username = username;
}
- public void validate(ClientState state) throws InvalidRequestException
+ public void validate(ClientState state) throws RequestValidationException
{
+ // validate login here before checkAccess to avoid leaking user existence to anonymous users.
+ state.ensureNotAnonymous();
+
if (!Auth.isExistingUser(username))
throw new InvalidRequestException(String.format("User %s doesn't exists", username));
@@ -47,12 +51,11 @@ public class DropUserStatement extends AuthenticationStatement
public void checkAccess(ClientState state) throws UnauthorizedException
{
- state.validateLogin();
if (!state.getUser().isSuper())
throw new UnauthorizedException("Only superusers are allowed to perfrom DROP USER queries");
}
- public ResultMessage execute(ClientState state) throws InvalidRequestException, RequestExecutionException
+ public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
// clean up permissions after the dropped user.
DatabaseDescriptor.getAuthorizer().revokeAll(username);
diff --git a/src/java/org/apache/cassandra/cql3/statements/GrantStatement.java b/src/java/org/apache/cassandra/cql3/statements/GrantStatement.java
index f4f35da02a..67b651ceca 100644
--- a/src/java/org/apache/cassandra/cql3/statements/GrantStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/GrantStatement.java
@@ -23,8 +23,8 @@ import java.util.Set;
import org.apache.cassandra.auth.IResource;
import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.exceptions.InvalidRequestException;
-import org.apache.cassandra.exceptions.UnauthorizedException;
+import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.transport.messages.ResultMessage;
@@ -35,7 +35,7 @@ public class GrantStatement extends PermissionAlteringStatement
super(permissions, resource, username);
}
- public ResultMessage execute(ClientState state) throws UnauthorizedException, InvalidRequestException
+ public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
DatabaseDescriptor.getAuthorizer().grant(state.getUser(), permissions, resource, username);
return null;
diff --git a/src/java/org/apache/cassandra/cql3/statements/ListPermissionsStatement.java b/src/java/org/apache/cassandra/cql3/statements/ListPermissionsStatement.java
index 2b9624dcec..d8db871a96 100644
--- a/src/java/org/apache/cassandra/cql3/statements/ListPermissionsStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/ListPermissionsStatement.java
@@ -26,7 +26,8 @@ import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.ResultSet;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.InvalidRequestException;
-import org.apache.cassandra.exceptions.UnauthorizedException;
+import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.transport.messages.ResultMessage;
@@ -59,9 +60,11 @@ public class ListPermissionsStatement extends AuthorizationStatement
this.recursive = recursive;
}
- public void validate(ClientState state) throws InvalidRequestException
+ public void validate(ClientState state) throws RequestValidationException
{
// a check to ensure the existence of the user isn't being leaked by user existence check.
+ state.ensureNotAnonymous();
+
if (username != null && !Auth.isExistingUser(username))
throw new InvalidRequestException(String.format("User %s doesn't exist", username));
@@ -73,13 +76,13 @@ public class ListPermissionsStatement extends AuthorizationStatement
}
}
- public void checkAccess(ClientState state) throws UnauthorizedException
+ public void checkAccess(ClientState state)
{
- state.ensureNotAnonymous();
+ // checked in validate
}
// TODO: Create a new ResultMessage type (?). Rows will do for now.
- public ResultMessage execute(ClientState state) throws UnauthorizedException, InvalidRequestException
+ public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
List details = new ArrayList();
@@ -112,7 +115,8 @@ public class ListPermissionsStatement extends AuthorizationStatement
return new ResultMessage.Rows(result);
}
- private Set list(ClientState state, IResource resource) throws UnauthorizedException, InvalidRequestException
+ private Set list(ClientState state, IResource resource)
+ throws RequestValidationException, RequestExecutionException
{
return DatabaseDescriptor.getAuthorizer().list(state.getUser(), permissions, resource, username);
}
diff --git a/src/java/org/apache/cassandra/cql3/statements/PermissionAlteringStatement.java b/src/java/org/apache/cassandra/cql3/statements/PermissionAlteringStatement.java
index b7d899d03c..5f3b70c35d 100644
--- a/src/java/org/apache/cassandra/cql3/statements/PermissionAlteringStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/PermissionAlteringStatement.java
@@ -24,6 +24,7 @@ import org.apache.cassandra.auth.DataResource;
import org.apache.cassandra.auth.IResource;
import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.exceptions.UnauthorizedException;
import org.apache.cassandra.service.ClientState;
@@ -40,17 +41,11 @@ public abstract class PermissionAlteringStatement extends AuthorizationStatement
this.username = username;
}
- public void checkAccess(ClientState state) throws UnauthorizedException
+ public void validate(ClientState state) throws RequestValidationException
{
- // check that the user has AUTHORIZE permission on the resource or its parents, otherwise reject GRANT/REVOKE.
- state.ensureHasPermission(Permission.AUTHORIZE, resource);
- // check that the user has [a single permission or all in case of ALL] on the resource or its parents.
- for (Permission p : permissions)
- state.ensureHasPermission(p, resource);
- }
+ // validate login here before checkAccess to avoid leaking user existence to anonymous users.
+ state.ensureNotAnonymous();
- public void validate(ClientState state) throws InvalidRequestException
- {
if (!Auth.isExistingUser(username))
throw new InvalidRequestException(String.format("User %s doesn't exist", username));
@@ -59,4 +54,13 @@ public abstract class PermissionAlteringStatement extends AuthorizationStatement
if (!resource.exists())
throw new InvalidRequestException(String.format("%s doesn't exist", resource));
}
+
+ public void checkAccess(ClientState state) throws UnauthorizedException
+ {
+ // check that the user has AUTHORIZE permission on the resource or its parents, otherwise reject GRANT/REVOKE.
+ state.ensureHasPermission(Permission.AUTHORIZE, resource);
+ // check that the user has [a single permission or all in case of ALL] on the resource or its parents.
+ for (Permission p : permissions)
+ state.ensureHasPermission(p, resource);
+ }
}
diff --git a/src/java/org/apache/cassandra/cql3/statements/RevokeStatement.java b/src/java/org/apache/cassandra/cql3/statements/RevokeStatement.java
index 2b103420ed..e81a385651 100644
--- a/src/java/org/apache/cassandra/cql3/statements/RevokeStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/RevokeStatement.java
@@ -23,8 +23,8 @@ import java.util.Set;
import org.apache.cassandra.auth.IResource;
import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.exceptions.InvalidRequestException;
-import org.apache.cassandra.exceptions.UnauthorizedException;
+import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.transport.messages.ResultMessage;
@@ -35,7 +35,7 @@ public class RevokeStatement extends PermissionAlteringStatement
super(permissions, resource, username);
}
- public ResultMessage execute(ClientState state) throws UnauthorizedException, InvalidRequestException
+ public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
DatabaseDescriptor.getAuthorizer().revoke(state.getUser(), permissions, resource, username);
return null;
diff --git a/src/java/org/apache/cassandra/service/ClientState.java b/src/java/org/apache/cassandra/service/ClientState.java
index 48129e1846..bc544c70c1 100644
--- a/src/java/org/apache/cassandra/service/ClientState.java
+++ b/src/java/org/apache/cassandra/service/ClientState.java
@@ -156,7 +156,8 @@ public class ClientState
if (perm.equals(Permission.SELECT) && READABLE_SYSTEM_RESOURCES.contains(resource))
return;
if (PROTECTED_AUTH_RESOURCES.contains(resource))
- throw new UnauthorizedException(String.format("Resource %s is inaccessible", resource));
+ if (perm.equals(Permission.CREATE) || perm.equals(Permission.ALTER) || perm.equals(Permission.DROP))
+ throw new UnauthorizedException(String.format("%s schema is protected", resource));
ensureHasPermission(perm, resource);
}
@@ -197,7 +198,7 @@ public class ClientState
{
validateLogin();
if (user.isAnonymous())
- throw new UnauthorizedException("You have to be logged in to perform this query");
+ throw new UnauthorizedException("You have to be logged in and not anonymous to perform this request");
}
private static void validateKeyspace(String keyspace) throws InvalidRequestException
diff --git a/test/conf/access.properties b/test/conf/access.properties
deleted file mode 100644
index f7c5e07a96..0000000000
--- a/test/conf/access.properties
+++ /dev/null
@@ -1,29 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache 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.apache.org/licenses/LICENSE-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.
-
-# This is a sample access file for SimpleAuthorizer. The format of
-# this file is keyspace=users, where users is a comma delimited list of
-# authenticatable users from passwd.properties. This file contains
-# potentially sensitive information, keep this in mind when setting its
-# mode and ownership.
-#
-# The magical '' property lists users who can modify the
-# list of keyspaces: all users will be able to view the list of keyspaces.
-=user1
-
-Keyspace1.=user1,user2
-Keyspace1.Standard1.=user2
-Keyspace1.Standard1.=user1