diff --git a/CHANGES.txt b/CHANGES.txt index 39b1948ec4..8ecf5946cb 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -20,6 +20,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 ea0bbd72a7..1fd2bf92b4 100644 --- a/build.xml +++ b/build.xml @@ -379,6 +379,7 @@ + @@ -461,6 +462,7 @@ + @@ -1041,7 +1043,6 @@ - diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml index f027c15ae0..8e910d841c 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 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 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