Add CQL3-based implementations of IAuthenticator and IAuthorizer

Patch by Aleksey Yeschenko; reviewed by Jonathan Ellis for
CASSANDRA-4898
This commit is contained in:
Aleksey Yeschenko 2013-02-15 01:26:10 +03:00
parent ee0be0624b
commit 0b83682b40
28 changed files with 630 additions and 489 deletions

View File

@ -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

View File

@ -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
=====

View File

@ -379,6 +379,7 @@
<dependency groupId="org.apache.cassandra" artifactId="cassandra-thrift" version="${version}" />
<dependency groupId="com.yammer.metrics" artifactId="metrics-core" version="2.0.3" />
<dependency groupId="edu.stanford.ppl" artifactId="snaptree" version="0.1" />
<dependency groupId="org.mindrot" artifactId="jbcrypt" version="0.3m" />
</dependencyManagement>
<developer id="alakshman" name="Avinash Lakshman"/>
<developer id="antelder" name="Anthony Elder"/>
@ -461,6 +462,7 @@
<dependency groupId="com.github.stephenc.high-scale-lib" artifactId="high-scale-lib"/>
<dependency groupId="org.yaml" artifactId="snakeyaml"/>
<dependency groupId="edu.stanford.ppl" artifactId="snaptree"/>
<dependency groupId="org.mindrot" artifactId="jbcrypt"/>
<dependency groupId="com.yammer.metrics" artifactId="metrics-core"/>
<dependency groupId="log4j" artifactId="log4j"/>
@ -1041,7 +1043,6 @@
<formatter type="xml" usefile="true"/>
<formatter type="brief" usefile="false"/>
<jvmarg value="-Dstorage-config=${test.conf}"/>
<jvmarg value="-Daccess.properties=${test.conf}/access.properties"/>
<jvmarg value="-Dlog4j.configuration=log4j-junit.properties" />
<jvmarg value="-javaagent:${basedir}/lib/jamm-0.2.5.jar" />
<jvmarg value="-ea"/>

View File

@ -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

View File

@ -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.

View File

@ -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 <ro> or <rw> 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 '<modify-keyspaces>' property lists users who can modify the
# list of keyspaces: all users will be able to view the list of keyspaces.
<modify-keyspaces>=jsmith
# Access to Keyspace1 (add/remove column families, etc).
Keyspace1.<ro>=jsmith,Elvis Presley
Keyspace1.<rw>=dilbert
# Access to Standard1 (keyspace Keyspace1)
Keyspace1.Standard1.<rw>=jsmith,Elvis Presley,dilbert

View File

@ -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

View File

@ -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<String, String> 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);
}
}

View File

@ -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 = "<modify-keyspaces>";
public EnumSet<Permission> authorize(AuthenticatedUser user, List<Object> 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<Permission> authorized = EnumSet.noneOf(Permission.class);
// /cassandra/keyspaces
if (resource.size() == 2)
{
keyspace = KEYSPACES_WRITE_PROPERTY;
authorized = EnumSet.of(Permission.READ);
}
// /cassandra/keyspaces/<keyspace name>
else if (resource.size() == 3)
{
keyspace = (String)resource.get(2);
}
// /cassandra/keyspaces/<keyspace name>/<cf name>
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 + ".<ro>");
writers = accessProperties.getProperty(keyspace + ".<rw>");
}
else
{
readers = accessProperties.getProperty(keyspace + "." + columnFamily + ".<ro>");
writers = accessProperties.getProperty(keyspace + "." + columnFamily + ".<rw>");
}
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));
}
}
}

BIN
lib/jbcrypt-0.3m.jar Normal file

Binary file not shown.

View File

@ -0,0 +1,17 @@
jBCrypt is subject to the following license:
/*
* Copyright (c) 2006 Damien Miller <djm@mindrot.org>
*
* 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.
*/

View File

@ -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");
}
}

View File

@ -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<text>,"
+ "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<Permission> 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<Permission> 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<Permission> permissions, IResource resource, String to)
throws RequestExecutionException
{
modify(permissions, resource, to, "+");
}
public void revoke(AuthenticatedUser performer, Set<Permission> 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<Permission> 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<PermissionDetails> list(AuthenticatedUser performer, Set<Permission> 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<PermissionDetails> details = new HashSet<PermissionDetails>();
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<String> vars = Lists.newArrayList(Auth.AUTH_KS, PERMISSIONS_CF);
List<String> conditions = new ArrayList<String>();
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<DataResource> 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);
}
}

View File

@ -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<Option, Object> options) throws InvalidRequestException;
void create(String username, Map<Option, Object> 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<Option, Object> options) throws InvalidRequestException;
void alter(String username, Map<Option, Object> 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();

View File

@ -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<Permission> 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<Permission> 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<PermissionDetails> list(AuthenticatedUser performer, Set<Permission> 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();

View File

@ -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<Option, Object> options) throws InvalidRequestException
public void create(String username, Map<Option, Object> options) throws RequestValidationException, RequestExecutionException
{
}
@Override
public void alter(String username, Map<Option, Object> options) throws InvalidRequestException
public void alter(String username, Map<Option, Object> 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
{
}

View File

@ -72,14 +72,14 @@ public abstract class LegacyAuthorizer implements IAuthorizer
@Override
public void grant(AuthenticatedUser performer, Set<Permission> 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<Permission> permissions, IResource resource, String from)
throws InvalidRequestException, UnauthorizedException
throws InvalidRequestException
{
throw new InvalidRequestException("REVOKE operation is not supported by LegacyAuthorizer");
}

View File

@ -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<text,text>," // 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<Option> supportedOptions()
{
return ImmutableSet.of(Option.PASSWORD);
}
// Let users alter their own password.
public Set<Option> alterableOptions()
{
return ImmutableSet.of(Option.PASSWORD);
}
public AuthenticatedUser authenticate(Map<String, String> 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<Option, Object> 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<Option, Object> 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<DataResource> 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);
}
}

View File

@ -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

View File

@ -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());

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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<PermissionDetails> details = new ArrayList<PermissionDetails>();
@ -112,7 +115,8 @@ public class ListPermissionsStatement extends AuthorizationStatement
return new ResultMessage.Rows(result);
}
private Set<PermissionDetails> list(ClientState state, IResource resource) throws UnauthorizedException, InvalidRequestException
private Set<PermissionDetails> list(ClientState state, IResource resource)
throws RequestValidationException, RequestExecutionException
{
return DatabaseDescriptor.getAuthorizer().list(state.getUser(), permissions, resource, username);
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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

View File

@ -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 '<modify-keyspaces>' property lists users who can modify the
# list of keyspaces: all users will be able to view the list of keyspaces.
<modify-keyspaces>=user1
Keyspace1.<rw>=user1,user2
Keyspace1.Standard1.<ro>=user2
Keyspace1.Standard1.<rw>=user1