Introduce role based access control

patch by Sam Tunnicliffe; reviewed by Aleksey Yeschenko for
CASSANDRA-7653
This commit is contained in:
Sam Tunnicliffe 2015-01-14 22:48:39 +03:00 committed by Aleksey Yeschenko
parent c65a9f5c63
commit 879b694d34
53 changed files with 2546 additions and 1389 deletions

View File

@ -1,4 +1,5 @@
3.0
* Add role based access control (CASSANDRA-7653)
* Group sstables for anticompaction correctly (CASSANDRA-8578)
* Add ReadFailureException to native protocol, respond
immediately when replicas encounter errors while handling

View File

@ -18,6 +18,14 @@ using the provided 'sstableupgrade' tool.
New features
------------
- Authentication & Authorization APIs have been updated to introduce
roles. Roles and Permissions granted to them are inherited, supporting
role based access control. The role concept supercedes that of users
and CQL constructs such as CREATE USER are deprecated but retained for
compatibility. The requirement to explicitly create Roles in Cassandra
even when auth is handled by an external system has been removed, so
authentication & authorization can be delegated to such systems in their
entirety.
- SSTable file name is changed. Now you don't have Keyspace/CF name
in file name. Also, secondary index has its own directory under parent's
directory.
@ -25,6 +33,18 @@ New features
Upgrading
---------
- IAuthenticator been updated to remove responsibility for user/role
maintenance and is now solely responsible for validating credentials,
This is primarily done via SASL, though an optional method exists for
systems which need support for the Thrift login() method.
- IRoleManager interface has been added which takes over the maintenance
functions from IAuthenticator. IAuthorizer is mainly unchanged. Auth data
in systems using the stock internal implementations PasswordAuthenticator
& CassandraAuthorizer will be automatically converted during upgrade,
with minimal operator intervention required. Custom implementations will
require modification, though these can be used in conjunction with the
stock CassandraRoleManager so providing an IRoleManager implementation
should not usually be necessary.
- Fat client support has been removed since we have push notifications to clients
- cassandra-cli has been removed. Please use cqlsh instead.
- YamlFileNetworkTopologySnitch has been removed; switch to
@ -37,6 +57,7 @@ Upgrading
in the normal order and not anymore in the order in which the column values were
specified in the IN restriction.
2.1.2
=====

View File

@ -108,7 +108,7 @@ except ImportError, e:
from cassandra.cluster import Cluster, PagedResult
from cassandra.query import SimpleStatement, ordered_dict_factory
from cassandra.policies import WhiteListRoundRobinPolicy
from cassandra.metadata import protect_name, protect_names, protect_value
from cassandra.metadata import protect_name, protect_names, protect_value, KeyspaceMetadata, TableMetadata, ColumnMetadata
from cassandra.auth import PlainTextAuthProvider
# cqlsh should run correctly when run out of a Cassandra source tree,
@ -751,9 +751,31 @@ class Shell(cmd.Cmd):
ksmeta = self.get_keyspace_meta(ksname)
if tablename not in ksmeta.tables:
raise ColumnFamilyNotFound("Column family %r not found" % tablename)
if ksname == 'system_auth' and tablename in ['roles','role_permissions']:
self.get_fake_auth_table_meta(ksname, tablename)
else:
raise ColumnFamilyNotFound("Column family %r not found" % tablename)
else:
return ksmeta.tables[tablename]
return ksmeta.tables[tablename]
def get_fake_auth_table_meta(self, ksname, tablename):
# may be using external auth implementation so internal tables
# aren't actually defined in schema. In this case, we'll fake
# them up
if tablename == 'roles':
ks_meta = KeyspaceMetadata(ksname, True, None, None)
table_meta = TableMetadata(ks_meta, 'roles')
table_meta.columns['role'] = ColumnMetadata(table_meta, 'role', cassandra.cqltypes.UTF8Type)
table_meta.columns['is_superuser'] = ColumnMetadata(table_meta, 'is_superuser', cassandra.cqltypes.BooleanType)
table_meta.columns['can_login'] = ColumnMetadata(table_meta, 'can_login', cassandra.cqltypes.BooleanType)
elif tablename == 'role_permissions':
ks_meta = KeyspaceMetadata(ksname, True, None, None)
table_meta = TableMetadata(ks_meta, 'role_permissions')
table_meta.columns['role'] = ColumnMetadata(table_meta, 'role', cassandra.cqltypes.UTF8Type)
table_meta.columns['resource'] = ColumnMetadata(table_meta, 'resource', cassandra.cqltypes.UTF8Type)
table_meta.columns['permission'] = ColumnMetadata(table_meta, 'permission', cassandra.cqltypes.UTF8Type)
else:
raise ColumnFamilyNotFoundException("Column family %r not found" % tablename)
def get_usertypes_meta(self):
data = self.session.execute("select * from system.schema_usertypes")
@ -1006,10 +1028,10 @@ class Shell(cmd.Cmd):
if statement.query_string[:6].lower() == 'select':
self.print_result(rows, self.parse_for_table_meta(statement.query_string))
elif statement.query_string.lower().startswith("list users"):
self.print_result(rows, self.get_table_meta('system_auth','users'))
elif statement.query_string.lower().startswith("list users") or statement.query_string.lower().startswith("list roles"):
self.print_result(rows, self.get_table_meta('system_auth','roles'))
elif statement.query_string.lower().startswith("list"):
self.print_result(rows, self.get_table_meta('system_auth','permissions'))
self.print_result(rows, self.get_table_meta('system_auth','role_permissions'))
elif rows:
# CAS INSERT/UPDATE
self.writeresult("")

View File

@ -62,6 +62,7 @@ batchlog_replay_throttle_in_kb: 1024
# - 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.
# If using PasswordAuthenticator, CassandraRoleManager must also be used (see below)
authenticator: AllowAllAuthenticator
# Authorization backend, implementing IAuthorizer; used to limit access/provide permissions
@ -73,6 +74,25 @@ authenticator: AllowAllAuthenticator
# increase system_auth keyspace replication factor if you use this authorizer.
authorizer: AllowAllAuthorizer
# Part of the Authentication & Authorization backend, implementing IRoleManager; used
# to maintain grants and memberships between roles.
# Out of the box, Cassandra provides org.apache.cassandra.auth.CassandraRoleManager,
# which stores role information in the system_auth keyspace. Most functions of the
# IRoleManager require an authenticated login, so unless the configured IAuthenticator
# actually implements authentication, most of this functionality will be unavailable.
#
# - CassandraRoleManager stores role data in the system_auth keyspace. Please
# increase system_auth keyspace replication factor if you use this role manager.
role_manager: CassandraRoleManager
# Validity period for roles cache (fetching permissions can be an
# expensive operation depending on the authorizer). Granted roles are cached for
# authenticated sessions in AuthenticatedUser and after the period specified
# here, become eligible for (async) reload.
# Defaults to 2000, set to 0 to disable.
# Will be disabled automatically for AllowAllAuthenticator.
roles_validity_in_ms: 2000
# Validity period for permissions cache (fetching permissions can be an
# expensive operation depending on the authorizer, CassandraAuthorizer is
# one example). Defaults to 2000, set to 0 to disable.

View File

@ -254,10 +254,16 @@ JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ;
| <alterUserStatement>
| <dropUserStatement>
| <listUsersStatement>
| <createRoleStatement>
| <alterRoleStatement>
| <dropRoleStatement>
| <listRolesStatement>
;
<authorizationStatement> ::= <grantStatement>
| <grantRoleStatement>
| <revokeStatement>
| <revokeRoleStatement>
| <listPermissionsStatement>
;
@ -1169,14 +1175,49 @@ syntax_rules += r'''
'''
syntax_rules += r'''
<grantStatement> ::= "GRANT" <permissionExpr> "ON" <resource> "TO" <username>
<rolename> ::= <identifier>
| <quotedName>
| <unreservedKeyword>
;
<createRoleStatement> ::= "CREATE" "ROLE" <rolename>
( "WITH" <roleProperty> ("AND" <roleProperty>)*)?
( "SUPERUSER" | "NOSUPERUSER" )?
( "LOGIN" | "NOLOGIN" )?
;
<alterRoleStatement> ::= "ALTER" "ROLE" <rolename>
( "WITH" <roleProperty> ("AND" <roleProperty>)*)?
( "SUPERUSER" | "NOSUPERUSER" )?
( "LOGIN" | "NOLOGIN" )?
;
<roleProperty> ::= "PASSWORD" <stringLiteral>
| "OPTIONS" <mapLiteral>
;
<dropRoleStatement> ::= "DROP" "ROLE" <rolename>
;
<grantRoleStatement> ::= "GRANT" <rolename> "TO" <rolename>
;
<revokeRoleStatement> ::= "REVOKE" <rolename> "FROM" <rolename>
;
<listRolesStatement> ::= "LIST" "ROLES"
( "OF" <rolename> )? "NORECURSIVE"?
;
'''
syntax_rules += r'''
<grantStatement> ::= "GRANT" <permissionExpr> "ON" <resource> "TO" <rolename>
;
<revokeStatement> ::= "REVOKE" <permissionExpr> "ON" <resource> "FROM" <username>
<revokeStatement> ::= "REVOKE" <permissionExpr> "ON" <resource> "FROM" <rolename>
;
<listPermissionsStatement> ::= "LIST" <permissionExpr>
( "ON" <resource> )? ( "OF" <username> )? "NORECURSIVE"?
( "ON" <resource> )? ( "OF" <rolename> )? "NORECURSIVE"?
;
<permission> ::= "AUTHORIZE"
@ -1214,11 +1255,24 @@ def username_name_completer(ctxt, cass):
session = cass.session
return [maybe_quote(row.values()[0].replace("'", "''")) for row in session.execute("LIST USERS")]
@completer_for('rolename', 'role')
def rolename_completer(ctxt, cass):
def maybe_quote(name):
if CqlRuleSet.is_valid_cql3_name(name):
return name
return "'%s'" % name
# disable completion for CREATE ROLE.
if ctxt.matched[0][0] == 'K_CREATE':
return [Hint('<rolename>')]
session = cass.session
return [maybe_quote(row[0].replace("'", "''")) for row in session.execute("LIST ROLES")]
syntax_rules += r'''
<createTriggerStatement> ::= "CREATE" "TRIGGER" ( "IF" "NOT" "EXISTS" )? <cident>
"ON" cf=<columnFamilyName> "USING" class=<stringLiteral>
;
<dropTriggerStatement> ::= "DROP" "TRIGGER" ( "IF" "EXISTS" )? triggername=<cident>
"ON" cf=<columnFamilyName>
;

View File

@ -666,7 +666,9 @@ class CQL3HelpTopics(CQLHelpTopics):
def help_create(self):
super(CQL3HelpTopics, self).help_create()
print " HELP CREATE_USER;\n"
print """ HELP CREATE_USER;
HELP CREATE_ROLE;
"""
def help_alter(self):
print """
@ -702,8 +704,10 @@ class CQL3HelpTopics(CQLHelpTopics):
"""
def help_drop(self):
super(CQL3HelpTopics, self).help_drop()
print " HELP DROP_USER;\n"
super(CQL3HelpTopics, self).help_create()
print """ HELP DROP_USER;
HELP DROP_ROLE;
"""
def help_list(self):
print """
@ -758,10 +762,10 @@ class CQL3HelpTopics(CQLHelpTopics):
ON ALL KEYSPACES
| KEYSPACE <keyspace>
| [TABLE] [<keyspace>.]<table>
TO <username>
TO [ROLE <rolename> | USER <username>]
Grant the specified permission (or all permissions) on a resource
to a user.
to a role or user.
To be able to grant a permission on some resource you have to
have that permission yourself and also AUTHORIZE permission on it,
@ -776,10 +780,10 @@ class CQL3HelpTopics(CQLHelpTopics):
ON ALL KEYSPACES
| KEYSPACE <keyspace>
| [TABLE] [<keyspace>.]<table>
FROM <username>
FROM [ROLE <rolename> | USER <username>]
Revokes the specified permission (or all permissions) on a resource
from a user.
from a role or user.
To be able to revoke a permission on some resource you have to
have that permission yourself and also AUTHORIZE permission on it,
@ -794,12 +798,13 @@ class CQL3HelpTopics(CQLHelpTopics):
[ON ALL KEYSPACES
| KEYSPACE <keyspace>
| [TABLE] [<keyspace>.]<table>]
[OF <username>]
[OF [ROLE <rolename> | USER <username>]
[NORECURSIVE]
Omitting ON <resource> part will list permissions on ALL KEYSPACES,
every keyspace and table.
Omitting OF <username> part will list permissions of all users.
Omitting OF [ROLE <rolename> | USER <username>] part will list permissions
of all roles and users.
Omitting NORECURSIVE specifier will list permissions of the resource
and all its parents (table, table's keyspace and ALL KEYSPACES).
@ -818,3 +823,46 @@ class CQL3HelpTopics(CQLHelpTopics):
MODIFY: required for INSERT, DELETE, UPDATE, TRUNCATE
SELECT: required for SELECT
"""
def help_create_role(self):
print """
CREATE ROLE <rolename>;
CREATE ROLE creates a new Cassandra role.
Only superusers can issue CREATE ROLE requests.
To create a superuser account use SUPERUSER option (NOSUPERUSER is the default).
"""
def help_drop_role(self):
print """
DROP ROLE <rolename>;
DROP ROLE removes an existing role. You have to be logged in as a superuser
to issue a DROP ROLE statement.
"""
def help_list_roles(self):
print """
LIST ROLES [OF [ROLE <rolename> | USER <username>] [NORECURSIVE]];
Only superusers can use the OF clause to list the roles granted to a role or user.
If a superuser omits the OF clause then all the created roles will be listed.
If a non-superuser calls LIST ROLES then the roles granted to that user are listed.
If NORECURSIVE is provided then only directly granted roles are listed.
"""
def help_grant_role(self):
print """
GRANT ROLE <rolename> TO [ROLE <rolename> | USER <username>]
Grant the specified role to another role or user. You have to be logged
in as superuser to issue a GRANT ROLE statement.
"""
def help_revoke_role(self):
print """
REVOKE ROLE <rolename> FROM [ROLE <rolename> | USER <username>]
Revoke the specified role from another role or user. You have to be logged
in as superuser to issue a REVOKE ROLE statement.
"""

View File

@ -23,45 +23,16 @@ import java.util.Set;
import org.apache.cassandra.exceptions.AuthenticationException;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.InvalidRequestException;
public class AllowAllAuthenticator implements IAuthenticator
{
private static final SaslNegotiator AUTHENTICATOR_INSTANCE = new Negotiator();
public boolean requireAuthentication()
{
return false;
}
public Set<Option> supportedOptions()
{
return Collections.emptySet();
}
public Set<Option> alterableOptions()
{
return Collections.emptySet();
}
public AuthenticatedUser authenticate(Map<String, String> credentials) throws AuthenticationException
{
return AuthenticatedUser.ANONYMOUS_USER;
}
public void create(String username, Map<Option, Object> options) throws InvalidRequestException
{
throw new InvalidRequestException("CREATE USER operation is not supported by AllowAllAuthenticator");
}
public void alter(String username, Map<Option, Object> options) throws InvalidRequestException
{
throw new InvalidRequestException("ALTER USER operation is not supported by AllowAllAuthenticator");
}
public void drop(String username) throws InvalidRequestException
{
throw new InvalidRequestException("DROP USER operation is not supported by AllowAllAuthenticator");
}
public Set<IResource> protectedResources()
{
return Collections.emptySet();
@ -74,4 +45,33 @@ public class AllowAllAuthenticator implements IAuthenticator
public void setup()
{
}
public SaslNegotiator newSaslNegotiator()
{
return AUTHENTICATOR_INSTANCE;
}
public AuthenticatedUser legacyAuthenticate(Map<String, String> credentialsData)
{
return AuthenticatedUser.ANONYMOUS_USER;
}
private static class Negotiator implements SaslNegotiator
{
public byte[] evaluateResponse(byte[] clientResponse) throws AuthenticationException
{
return null;
}
public boolean isComplete()
{
return true;
}
public AuthenticatedUser getAuthenticatedUser() throws AuthenticationException
{
return AuthenticatedUser.ANONYMOUS_USER;
}
}
}

View File

@ -1,298 +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.
*/
package org.apache.cassandra.auth;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.ScheduledExecutors;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.KSMetaData;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.cql3.statements.CFStatement;
import org.apache.cassandra.cql3.statements.CreateTableStatement;
import org.apache.cassandra.cql3.statements.SelectStatement;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.locator.SimpleStrategy;
import org.apache.cassandra.service.*;
import org.apache.cassandra.transport.messages.ResultMessage;
import org.apache.cassandra.utils.ByteBufferUtil;
public class Auth
{
private static final Logger logger = LoggerFactory.getLogger(Auth.class);
public static final String DEFAULT_SUPERUSER_NAME = "cassandra";
public static final long SUPERUSER_SETUP_DELAY = Long.getLong("cassandra.superuser_setup_delay_ms", 10000);
public static final String AUTH_KS = "system_auth";
public static final String USERS_CF = "users";
// User-level permissions cache.
private static final PermissionsCache permissionsCache = new PermissionsCache(DatabaseDescriptor.getPermissionsValidity(),
DatabaseDescriptor.getPermissionsUpdateInterval(),
DatabaseDescriptor.getPermissionsCacheMaxEntries(),
DatabaseDescriptor.getAuthorizer());
private static final String USERS_CF_SCHEMA = String.format("CREATE TABLE %s.%s ("
+ "name text,"
+ "super boolean,"
+ "PRIMARY KEY(name)"
+ ") WITH gc_grace_seconds=%d",
AUTH_KS,
USERS_CF,
90 * 24 * 60 * 60); // 3 months.
private static SelectStatement selectUserStatement;
public static Set<Permission> getPermissions(AuthenticatedUser user, IResource resource)
{
return permissionsCache.getPermissions(user, resource);
}
/**
* Checks if the username is stored in AUTH_KS.USERS_CF.
*
* @param username Username to query.
* @return whether or not Cassandra knows about the user.
*/
public static boolean isExistingUser(String username)
{
return !selectUser(username).isEmpty();
}
/**
* Checks if the user is a known superuser.
*
* @param username Username to query.
* @return true is the user is a superuser, false if they aren't or don't exist at all.
*/
public static boolean isSuperuser(String username)
{
UntypedResultSet result = selectUser(username);
return !result.isEmpty() && result.one().getBoolean("super");
}
/**
* Inserts the user into AUTH_KS.USERS_CF (or overwrites their superuser status as a result of an ALTER USER query).
*
* @param username Username to insert.
* @param isSuper User's new status.
* @throws RequestExecutionException
*/
public static void insertUser(String username, boolean isSuper) throws RequestExecutionException
{
QueryProcessor.process(String.format("INSERT INTO %s.%s (name, super) VALUES ('%s', %s)",
AUTH_KS,
USERS_CF,
escape(username),
isSuper),
consistencyForUser(username));
}
/**
* Deletes the user from AUTH_KS.USERS_CF.
*
* @param username Username to delete.
* @throws RequestExecutionException
*/
public static void deleteUser(String username) throws RequestExecutionException
{
QueryProcessor.process(String.format("DELETE FROM %s.%s WHERE name = '%s'",
AUTH_KS,
USERS_CF,
escape(username)),
consistencyForUser(username));
}
/**
* Sets up Authenticator and Authorizer.
*/
public static void setup()
{
if (DatabaseDescriptor.getAuthenticator() instanceof AllowAllAuthenticator)
return;
setupAuthKeyspace();
setupTable(USERS_CF, USERS_CF_SCHEMA);
DatabaseDescriptor.getAuthenticator().setup();
DatabaseDescriptor.getAuthorizer().setup();
// register a custom MigrationListener for permissions cleanup after dropped keyspaces/cfs.
MigrationManager.instance.register(new AuthMigrationListener());
// the delay is here to give the node some time to see its peers - to reduce
// "Skipped default superuser setup: some nodes were not ready" log spam.
// It's the only reason for the delay.
ScheduledExecutors.nonPeriodicTasks.schedule(new Runnable()
{
public void run()
{
setupDefaultSuperuser();
}
}, SUPERUSER_SETUP_DELAY, TimeUnit.MILLISECONDS);
try
{
String query = String.format("SELECT * FROM %s.%s WHERE name = ?", AUTH_KS, USERS_CF);
selectUserStatement = (SelectStatement) QueryProcessor.parseStatement(query).prepare().statement;
}
catch (RequestValidationException e)
{
throw new AssertionError(e); // not supposed to happen
}
}
// Only use QUORUM cl for the default superuser.
private static ConsistencyLevel consistencyForUser(String username)
{
if (username.equals(DEFAULT_SUPERUSER_NAME))
return ConsistencyLevel.QUORUM;
else
return ConsistencyLevel.LOCAL_ONE;
}
private static void setupAuthKeyspace()
{
if (Schema.instance.getKSMetaData(AUTH_KS) == null)
{
try
{
KSMetaData ksm = KSMetaData.newKeyspace(AUTH_KS, SimpleStrategy.class.getName(), ImmutableMap.of("replication_factor", "1"), true);
MigrationManager.announceNewKeyspace(ksm, 0, false);
}
catch (Exception e)
{
throw new AssertionError(e); // shouldn't ever happen.
}
}
}
/**
* Set up table from given CREATE TABLE statement under system_auth keyspace, if not already done so.
*
* @param name name of the table
* @param cql CREATE TABLE statement
*/
public static void setupTable(String name, String cql)
{
if (Schema.instance.getCFMetaData(AUTH_KS, name) == null)
{
try
{
CFStatement parsed = (CFStatement)QueryProcessor.parseStatement(cql);
parsed.prepareKeyspace(AUTH_KS);
CreateTableStatement statement = (CreateTableStatement) parsed.prepare().statement;
CFMetaData cfm = statement.getCFMetaData().copy(CFMetaData.generateLegacyCfId(AUTH_KS, name));
assert cfm.cfName.equals(name);
MigrationManager.announceNewColumnFamily(cfm);
}
catch (Exception e)
{
throw new AssertionError(e);
}
}
}
private static void setupDefaultSuperuser()
{
try
{
// insert a default superuser if AUTH_KS.USERS_CF is empty.
if (!hasExistingUsers())
{
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.ONE);
logger.info("Created default superuser '{}'", DEFAULT_SUPERUSER_NAME);
}
}
catch (RequestExecutionException e)
{
logger.warn("Skipped default superuser setup: some nodes were not ready");
}
}
private static boolean hasExistingUsers() throws RequestExecutionException
{
// Try looking up the 'cassandra' default super user first, to avoid the range query if possible.
String defaultSUQuery = String.format("SELECT * FROM %s.%s WHERE name = '%s'", AUTH_KS, USERS_CF, DEFAULT_SUPERUSER_NAME);
String allUsersQuery = String.format("SELECT * FROM %s.%s LIMIT 1", AUTH_KS, USERS_CF);
return !QueryProcessor.process(defaultSUQuery, ConsistencyLevel.ONE).isEmpty()
|| !QueryProcessor.process(defaultSUQuery, ConsistencyLevel.QUORUM).isEmpty()
|| !QueryProcessor.process(allUsersQuery, ConsistencyLevel.QUORUM).isEmpty();
}
// 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 selectUser(String username)
{
try
{
ResultMessage.Rows rows = selectUserStatement.execute(QueryState.forInternalCalls(),
QueryOptions.forInternalCalls(consistencyForUser(username),
Lists.newArrayList(ByteBufferUtil.bytes(username))));
return UntypedResultSet.create(rows.result);
}
catch (RequestValidationException e)
{
throw new AssertionError(e); // not supposed to happen
}
catch (RequestExecutionException e)
{
throw new RuntimeException(e);
}
}
/**
* MigrationListener implementation that cleans up permissions on dropped resources.
*/
public static class AuthMigrationListener extends MigrationListener
{
public void onDropKeyspace(String ksName)
{
DatabaseDescriptor.getAuthorizer().revokeAll(DataResource.keyspace(ksName));
}
public void onDropColumnFamily(String ksName, String cfName)
{
DatabaseDescriptor.getAuthorizer().revokeAll(DataResource.columnFamily(ksName, cfName));
}
}
}

View File

@ -0,0 +1,90 @@
/*
* 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.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import com.google.common.collect.ImmutableMap;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.KSMetaData;
import org.apache.cassandra.locator.SimpleStrategy;
public class AuthKeyspace
{
public static final String NAME = "system_auth";
public static final String ROLES = "roles";
public static final String ROLE_MEMBERS = "role_members";
public static final String ROLE_PERMISSIONS = "role_permissions";
public static final String RESOURCE_ROLE_INDEX = "resource_role_permissons_index";
public static final long SUPERUSER_SETUP_DELAY = Long.getLong("cassandra.superuser_setup_delay_ms", 10000);
private static final CFMetaData Roles =
compile(ROLES,
"role definitions",
"CREATE TABLE %s ("
+ "role text,"
+ "is_superuser boolean,"
+ "can_login boolean,"
+ "salted_hash text,"
+ "member_of set<text>,"
+ "PRIMARY KEY(role))");
private static final CFMetaData RoleMembers =
compile(ROLE_MEMBERS,
"role memberships lookup table",
"CREATE TABLE %s ("
+ "role text,"
+ "member text,"
+ "PRIMARY KEY(role, member))");
private static final CFMetaData RolePermissions =
compile(ROLE_PERMISSIONS,
"permissions granted to db roles",
"CREATE TABLE %s ("
+ "role text,"
+ "resource text,"
+ "permissions set<text>,"
+ "PRIMARY KEY(role, resource))");
private static final CFMetaData ResourceRoleIndex =
compile(RESOURCE_ROLE_INDEX,
"index of db roles with permissions granted on a resource",
"CREATE TABLE %s ("
+ "resource text,"
+ "role text,"
+ "PRIMARY KEY(resource, role))");
private static CFMetaData compile(String name, String description, String schema)
{
return CFMetaData.compile(String.format(schema, name), NAME)
.comment(description)
.gcGraceSeconds((int) TimeUnit.DAYS.toSeconds(90));
}
public static KSMetaData definition()
{
List<CFMetaData> tables = Arrays.asList(Roles, RoleMembers, RolePermissions, ResourceRoleIndex);
return new KSMetaData(NAME, SimpleStrategy.class, ImmutableMap.of("replication_factor", "1"), true, tables);
}
}

View File

@ -17,25 +17,21 @@
*/
package org.apache.cassandra.auth;
import org.apache.cassandra.exceptions.AuthenticationException;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.service.MigrationListener;
public interface ISaslAwareAuthenticator extends IAuthenticator
/**
* MigrationListener implementation that cleans up permissions on dropped resources.
*/
public class AuthMigrationListener extends MigrationListener
{
/**
* Provide a SaslAuthenticator to be used by the CQL binary protocol server. If
* the configured IAuthenticator requires authentication but does not implement this
* interface we refuse to start the binary protocol server as it will have no way
* of authenticating clients.
* @return SaslAuthenticator implementation
* (see {@link PasswordAuthenticator.PlainTextSaslAuthenticator})
*/
SaslAuthenticator newAuthenticator();
public interface SaslAuthenticator
public void onDropKeyspace(String ksName)
{
public byte[] evaluateResponse(byte[] clientResponse) throws AuthenticationException;
public boolean isComplete();
public AuthenticatedUser getAuthenticatedUser() throws AuthenticationException;
DatabaseDescriptor.getAuthorizer().revokeAll(DataResource.keyspace(ksName));
}
public void onDropColumnFamily(String ksName, String cfName)
{
DatabaseDescriptor.getAuthorizer().revokeAll(DataResource.table(ksName, cfName));
}
}

View File

@ -17,16 +17,46 @@
*/
package org.apache.cassandra.auth;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import com.google.common.base.Objects;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.ScheduledExecutors;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.exceptions.RequestValidationException;
/**
* Returned from IAuthenticator#authenticate(), represents an authenticated user everywhere internally.
*
* Holds the name of the user and the roles that have been granted to the user. The roles will be cached
* for roles_validity_in_ms.
*/
public class AuthenticatedUser
{
private static final Logger logger = LoggerFactory.getLogger(AuthenticatedUser.class);
public static final String ANONYMOUS_USERNAME = "anonymous";
public static final AuthenticatedUser ANONYMOUS_USER = new AuthenticatedUser(ANONYMOUS_USERNAME);
// User-level roles cache
private static final LoadingCache<String, Set<String>> rolesCache = initRolesCache();
// User-level permissions cache.
private static final PermissionsCache permissionsCache = new PermissionsCache(DatabaseDescriptor.getPermissionsValidity(),
DatabaseDescriptor.getPermissionsUpdateInterval(),
DatabaseDescriptor.getPermissionsCacheMaxEntries(),
DatabaseDescriptor.getAuthorizer());
private final String name;
public AuthenticatedUser(String name)
@ -47,7 +77,16 @@ public class AuthenticatedUser
*/
public boolean isSuper()
{
return !isAnonymous() && Auth.isSuperuser(name);
return !isAnonymous() && hasSuperuserRole();
}
private boolean hasSuperuserRole()
{
IRoleManager roleManager = DatabaseDescriptor.getRoleManager();
for (String role : getRoles())
if (roleManager.isSuper(role))
return true;
return false;
}
/**
@ -58,6 +97,80 @@ public class AuthenticatedUser
return this == ANONYMOUS_USER;
}
/**
* Get the roles that have been granted to the user via the IRoleManager
*
* @return a list of roles that have been granted to the user
*/
public Set<String> getRoles()
{
if (rolesCache == null)
return loadRoles(name);
try
{
return rolesCache.get(name);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
public static Set<Permission> getPermissions(AuthenticatedUser user, IResource resource)
{
return permissionsCache.getPermissions(user, resource);
}
private static Set<String> loadRoles(String name)
{
try
{
return DatabaseDescriptor.getRoleManager().getRoles(name, true);
}
catch (RequestValidationException e)
{
throw new AssertionError(e); // not supposed to happen
}
catch (RequestExecutionException e)
{
throw new RuntimeException(e);
}
}
private static LoadingCache<String, Set<String>> initRolesCache()
{
if (DatabaseDescriptor.getAuthenticator() instanceof AllowAllAuthenticator)
return null;
int validityPeriod = DatabaseDescriptor.getRolesValidity();
if (validityPeriod <= 0)
return null;
return CacheBuilder.newBuilder()
.refreshAfterWrite(validityPeriod, TimeUnit.MILLISECONDS)
.build(new CacheLoader<String, Set<String>>()
{
public Set<String> load(String name)
{
return loadRoles(name);
}
public ListenableFuture<Set<String>> reload(final String name, Set<String> oldValue)
{
ListenableFutureTask<Set<String>> task = ListenableFutureTask.create(new Callable<Set<String>>()
{
public Set<String> call()
{
return loadRoles(name);
}
});
ScheduledExecutors.optionalTasks.execute(task);
return task;
}
});
}
@Override
public String toString()
{

View File

@ -18,63 +18,65 @@
package org.apache.cassandra.auth;
import java.util.*;
import java.util.concurrent.TimeUnit;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.concurrent.ScheduledExecutors;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.cql3.*;
import org.apache.cassandra.cql3.statements.BatchStatement;
import org.apache.cassandra.cql3.statements.ModificationStatement;
import org.apache.cassandra.cql3.statements.SelectStatement;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.*;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.messages.ResultMessage;
import org.apache.cassandra.utils.ByteBufferUtil;
/**
* CassandraAuthorizer is an IAuthorizer implementation that keeps
* permissions internally in C* - in system_auth.permissions CQL3 table.
* user permissions internally in C* using the system_auth.role_permissions
* 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 ROLE = "role";
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.
// used during upgrades to perform authz on mixed clusters
public static final String USERNAME = "username";
public static final String USER_PERMISSIONS = "permissions";
private SelectStatement authorizeStatement;
private SelectStatement authorizeRoleStatement;
private SelectStatement legacyAuthorizeRoleStatement;
// Returns every permission on the resource granted to the user.
public CassandraAuthorizer()
{
}
// Returns every permission on the resource granted to the user either directly
// or indirectly via roles granted to the user.
public Set<Permission> authorize(AuthenticatedUser user, IResource resource)
{
if (user.isSuper())
return Permission.ALL;
UntypedResultSet result;
Set<Permission> permissions = EnumSet.noneOf(Permission.class);
try
{
ResultMessage.Rows rows = authorizeStatement.execute(QueryState.forInternalCalls(),
QueryOptions.forInternalCalls(ConsistencyLevel.LOCAL_ONE,
Lists.newArrayList(ByteBufferUtil.bytes(user.getName()),
ByteBufferUtil.bytes(resource.getName()))));
result = UntypedResultSet.create(rows.result);
for (String role: user.getRoles())
addPermissionsForRole(permissions, resource, role);
}
catch (RequestValidationException e)
{
@ -86,53 +88,209 @@ public class CassandraAuthorizer implements IAuthorizer
return Permission.NONE;
}
if (result.isEmpty() || !result.one().has(PERMISSIONS))
return Permission.NONE;
Set<Permission> permissions = EnumSet.noneOf(Permission.class);
for (String perm : result.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
public void grant(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String grantee)
throws RequestValidationException, RequestExecutionException
{
modify(permissions, resource, to, "+");
modifyRolePermissions(permissions, resource, grantee, "+");
addLookupEntry(resource, grantee);
}
public void revoke(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String from)
throws RequestExecutionException
public void revoke(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String revokee)
throws RequestValidationException, RequestExecutionException
{
modify(permissions, resource, from, "-");
modifyRolePermissions(permissions, resource, revokee, "-");
removeLookupEntry(resource, revokee);
}
// 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
// Called prior to deleting the user with DROP USER query.
// Internal hook, so no permission checks are needed here.
// Executes a logged batch removing the granted premissions
// for the role as well as the entries from the reverse index
// table
public void revokeAll(String revokee)
{
process(String.format("UPDATE %s.%s SET permissions = permissions %s {%s} WHERE username = '%s' AND resource = '%s'",
Auth.AUTH_KS,
PERMISSIONS_CF,
try
{
UntypedResultSet rows = process(String.format("SELECT resource FROM %s.%s WHERE role = '%s'",
AuthKeyspace.NAME,
AuthKeyspace.ROLE_PERMISSIONS,
escape(revokee)));
List<CQLStatement> statements = new ArrayList<>();
for (UntypedResultSet.Row row : rows)
{
statements.add(
QueryProcessor.getStatement(String.format("DELETE FROM %s.%s WHERE resource = '%s' AND role = '%s'",
AuthKeyspace.NAME,
AuthKeyspace.RESOURCE_ROLE_INDEX,
escape(row.getString("resource")),
escape(revokee)),
ClientState.forInternalCalls()).statement);
}
statements.add(QueryProcessor.getStatement(String.format("DELETE FROM %s.%s WHERE role = '%s'",
AuthKeyspace.NAME,
AuthKeyspace.ROLE_PERMISSIONS,
escape(revokee)),
ClientState.forInternalCalls()).statement);
executeLoggedBatch(statements);
}
catch (RequestExecutionException | RequestValidationException e)
{
logger.warn("CassandraAuthorizer failed to revoke all permissions of {}: {}", revokee, e);
}
}
// Called after a resource is removed (DROP KEYSPACE, DROP TABLE, etc.).
// Execute a logged batch removing all the permissions for the resource
// as well as the index table entry
public void revokeAll(IResource droppedResource)
{
try
{
UntypedResultSet rows = process(String.format("SELECT role FROM %s.%s WHERE resource = '%s'",
AuthKeyspace.NAME,
AuthKeyspace.RESOURCE_ROLE_INDEX,
escape(droppedResource.getName())));
List<CQLStatement> statements = new ArrayList<>();
for (UntypedResultSet.Row row : rows)
{
statements.add(QueryProcessor.getStatement(String.format("DELETE FROM %s.%s WHERE role = '%s' AND resource = '%s'",
AuthKeyspace.NAME,
AuthKeyspace.ROLE_PERMISSIONS,
escape(row.getString("role")),
escape(droppedResource.getName())),
ClientState.forInternalCalls()).statement);
}
statements.add(QueryProcessor.getStatement(String.format("DELETE FROM %s.%s WHERE resource = '%s'",
AuthKeyspace.NAME,
AuthKeyspace.RESOURCE_ROLE_INDEX,
escape(droppedResource.getName())),
ClientState.forInternalCalls()).statement);
executeLoggedBatch(statements);
}
catch (RequestExecutionException | RequestValidationException e)
{
logger.warn("CassandraAuthorizer failed to revoke all permissions on {}: {}", droppedResource, e);
return;
}
}
private void executeLoggedBatch(List<CQLStatement> statements)
throws RequestExecutionException, RequestValidationException
{
BatchStatement batch = new BatchStatement(0,
BatchStatement.Type.LOGGED,
Lists.newArrayList(Iterables.filter(statements, ModificationStatement.class)),
Attributes.none());
QueryProcessor.instance.processBatch(batch,
QueryState.forInternalCalls(),
BatchQueryOptions.withoutPerStatementVariables(QueryOptions.DEFAULT));
}
// Add every permission on the resource granted to the role
private void addPermissionsForRole(Set<Permission> permissions, IResource resource, String rolename)
throws RequestExecutionException, RequestValidationException
{
QueryOptions options = QueryOptions.forInternalCalls(ConsistencyLevel.LOCAL_ONE,
Lists.newArrayList(ByteBufferUtil.bytes(rolename),
ByteBufferUtil.bytes(resource.getName())));
// If it exists, read from the legacy user permissions table to handle the case where the cluster
// is being upgraded and so is running with mixed versions of the authz schema
SelectStatement statement = Schema.instance.getCFMetaData(AuthKeyspace.NAME, USER_PERMISSIONS) == null
? authorizeRoleStatement
: legacyAuthorizeRoleStatement;
ResultMessage.Rows rows = statement.execute(QueryState.forInternalCalls(), options) ;
UntypedResultSet result = UntypedResultSet.create(rows.result);
if (!result.isEmpty() && result.one().has(PERMISSIONS))
{
for (String perm : result.one().getSet(PERMISSIONS, UTF8Type.instance))
{
permissions.add(Permission.valueOf(perm));
}
}
}
// Adds or removes permissions from a role_permissions table (adds if op is "+", removes if op is "-")
private void modifyRolePermissions(Set<Permission> permissions, IResource resource, String rolename, String op)
throws RequestExecutionException
{
process(String.format("UPDATE %s.%s SET permissions = permissions %s {%s} WHERE role = '%s' AND resource = '%s'",
AuthKeyspace.NAME,
AuthKeyspace.ROLE_PERMISSIONS,
op,
"'" + StringUtils.join(permissions, "','") + "'",
escape(user),
escape(rolename),
escape(resource.getName())));
}
// Removes an entry from the inverted index table (from resource -> role with defined permissions)
private void removeLookupEntry(IResource resource, String rolename) throws RequestExecutionException
{
process(String.format("DELETE FROM %s.%s WHERE resource = '%s' and role = '%s'",
AuthKeyspace.NAME,
AuthKeyspace.RESOURCE_ROLE_INDEX,
escape(resource.getName()),
escape(rolename)));
}
// Adds an entry to the inverted index table (from resource -> role with defined permissions)
private void addLookupEntry(IResource resource, String rolename) throws RequestExecutionException
{
process(String.format("INSERT INTO %s.%s (resource, role) VALUES ('%s','%s')",
AuthKeyspace.NAME,
AuthKeyspace.RESOURCE_ROLE_INDEX,
escape(resource.getName()),
escape(rolename)));
}
// '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
// If the user requesting 'LIST PERMISSIONS' is not a superuser OR their 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)
public Set<PermissionDetails> list(AuthenticatedUser performer,
Set<Permission> permissions,
IResource resource,
String grantee)
throws RequestValidationException, RequestExecutionException
{
if (!performer.isSuper() && !performer.getName().equals(of))
if (!performer.isSuper() && ! performer.getRoles().contains(grantee))
throw new UnauthorizedException(String.format("You are not authorized to view %s's permissions",
of == null ? "everyone" : of));
grantee == null ? "everyone" : grantee));
Set<PermissionDetails> details = new HashSet<PermissionDetails>();
if (null == grantee)
return listPermissionsForRole(permissions, resource, grantee);
for (UntypedResultSet.Row row : process(buildListQuery(resource, of)))
Set<String> roles = DatabaseDescriptor.getRoleManager().getRoles(grantee, true);
Set<PermissionDetails> details = new HashSet<>();
for (String role : roles)
details.addAll(listPermissionsForRole(permissions, resource, role));
return details;
}
private Set<PermissionDetails> listPermissionsForRole(Set<Permission> permissions,
IResource resource,
String rolename)
throws RequestExecutionException
{
Set<PermissionDetails> details = new HashSet<>();
// If it exists, try the legacy user permissions table first. This is to handle the case
// where the cluster is being upgraded and so is running with mixed versions of the perms table
boolean useLegacyTable = Schema.instance.getCFMetaData(AuthKeyspace.NAME, USER_PERMISSIONS) != null;
String entityColumnName = useLegacyTable ? USERNAME : ROLE;
for (UntypedResultSet.Row row : process(buildListQuery(resource, rolename, useLegacyTable)))
{
if (row.has(PERMISSIONS))
{
@ -140,20 +298,21 @@ public class CassandraAuthorizer implements IAuthorizer
{
Permission permission = Permission.valueOf(p);
if (permissions.contains(permission))
details.add(new PermissionDetails(row.getString(USERNAME),
details.add(new PermissionDetails(row.getString(entityColumnName),
DataResource.fromName(row.getString(RESOURCE)),
permission));
}
}
}
return details;
}
private static String buildListQuery(IResource resource, String of)
private String buildListQuery(IResource resource, String grantee, boolean useLegacyTable)
{
List<String> vars = Lists.newArrayList(Auth.AUTH_KS, PERMISSIONS_CF);
List<String> conditions = new ArrayList<String>();
String tableName = useLegacyTable ? USER_PERMISSIONS : AuthKeyspace.ROLE_PERMISSIONS;
String entityName = useLegacyTable ? USERNAME : ROLE;
List<String> vars = Lists.newArrayList(AuthKeyspace.NAME, tableName);
List<String> conditions = new ArrayList<>();
if (resource != null)
{
@ -161,75 +320,27 @@ public class CassandraAuthorizer implements IAuthorizer
vars.add(escape(resource.getName()));
}
if (of != null)
if (grantee != null)
{
conditions.add("username = '%s'");
vars.add(escape(of));
conditions.add(entityName + " = '%s'");
vars.add(escape(grantee));
}
String query = "SELECT username, resource, permissions FROM %s.%s";
String query = "SELECT " + entityName + ", resource, permissions FROM %s.%s";
if (!conditions.isEmpty())
query += " WHERE " + StringUtils.join(conditions, " AND ");
if (resource != null && of == null)
if (resource != null && grantee == 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 (RequestExecutionException 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 (RequestExecutionException 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 (RequestExecutionException 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));
return ImmutableSet.of(DataResource.table(AuthKeyspace.NAME, AuthKeyspace.ROLE_PERMISSIONS));
}
public void validateConfiguration() throws ConfigurationException
@ -238,27 +349,99 @@ public class CassandraAuthorizer implements IAuthorizer
public void setup()
{
Auth.setupTable(PERMISSIONS_CF, PERMISSIONS_CF_SCHEMA);
authorizeRoleStatement = prepare(ROLE, AuthKeyspace.ROLE_PERMISSIONS);
// If old user permissions table exists, migrate the legacy authz data to the new table
// The delay is to give the node a chance to see its peers before attempting the conversion
if (Schema.instance.getCFMetaData(AuthKeyspace.NAME, "permissions") != null)
{
legacyAuthorizeRoleStatement = prepare(USERNAME, USER_PERMISSIONS);
ScheduledExecutors.optionalTasks.schedule(new Runnable()
{
public void run()
{
convertLegacyData();
}
}, AuthKeyspace.SUPERUSER_SETUP_DELAY, TimeUnit.MILLISECONDS);
}
}
private SelectStatement prepare(String entityname, String permissionsTable)
{
try
{
String query = String.format("SELECT permissions FROM %s.%s WHERE username = ? AND resource = ?", Auth.AUTH_KS, PERMISSIONS_CF);
authorizeStatement = (SelectStatement) QueryProcessor.parseStatement(query).prepare().statement;
String query = String.format("SELECT permissions FROM %s.%s WHERE %s = ? AND resource = ?",
AuthKeyspace.NAME,
permissionsTable,
entityname);
return (SelectStatement) QueryProcessor.getStatement(query, ClientState.forInternalCalls()).statement;
}
catch (RequestValidationException e)
{
throw new AssertionError(e); // not supposed to happen
throw new AssertionError(e);
}
}
/**
* Copy legacy authz data from the system_auth.permissions table to the new system_auth.role_permissions table and
* also insert entries into the reverse lookup table.
* In theory, we could simply rename the existing table as the schema is structurally the same, but this would
* break mixed clusters during a rolling upgrade.
* This setup is not performed if AllowAllAuthenticator is configured (see Auth#setup).
*/
private void convertLegacyData()
{
try
{
if (Schema.instance.getCFMetaData("system_auth", "permissions") != null)
{
logger.info("Converting legacy permissions data");
CQLStatement insertStatement =
QueryProcessor.getStatement(String.format("INSERT INTO %s.%s (role, resource, permissions) " +
"VALUES (?, ?, ?)",
AuthKeyspace.NAME,
AuthKeyspace.ROLE_PERMISSIONS),
ClientState.forInternalCalls()).statement;
CQLStatement indexStatement =
QueryProcessor.getStatement(String.format("INSERT INTO %s.%s (resource, role) VALUES (?,?)",
AuthKeyspace.NAME,
AuthKeyspace.RESOURCE_ROLE_INDEX),
ClientState.forInternalCalls()).statement;
UntypedResultSet permissions = process("SELECT * FROM system_auth.permissions");
for (UntypedResultSet.Row row : permissions)
{
insertStatement.execute(QueryState.forInternalCalls(),
QueryOptions.forInternalCalls(ConsistencyLevel.ONE,
Lists.newArrayList(row.getBytes("username"),
row.getBytes("resource"),
row.getBytes("permissions"))));
indexStatement.execute(QueryState.forInternalCalls(),
QueryOptions.forInternalCalls(ConsistencyLevel.ONE,
Lists.newArrayList(row.getBytes("resource"),
row.getBytes("username"))));
}
logger.info("Completed conversion of legacy permissions");
}
}
catch (Exception e)
{
logger.info("Unable to complete conversion of legacy permissions data (perhaps not enough nodes are upgraded yet). " +
"Conversion should not be considered complete");
logger.debug("Conversion error", e);
}
}
// We only worry about one character ('). Make sure it's properly escaped.
private static String escape(String name)
private String escape(String name)
{
return StringUtils.replace(name, "'", "''");
}
private static UntypedResultSet process(String query) throws RequestExecutionException
private UntypedResultSet process(String query) throws RequestExecutionException
{
return QueryProcessor.process(query, ConsistencyLevel.ONE);
return QueryProcessor.process(query, ConsistencyLevel.LOCAL_ONE);
}
}

View File

@ -0,0 +1,586 @@
/*
* 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 java.util.concurrent.TimeUnit;
import com.google.common.base.*;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.ScheduledExecutors;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.cql3.*;
import org.apache.cassandra.cql3.statements.SelectStatement;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.*;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.messages.ResultMessage;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.mindrot.jbcrypt.BCrypt;
/**
* Responsible for the creation, maintainance and delation of roles
* for the purposes of authentication and authorization.
* Role data is stored internally, using the roles and role_members tables
* in the system_auth keyspace.
*
* Additionally, if org.apache.cassandra.auth.PasswordAuthenticator is used,
* encrypted passwords are also stored in the system_auth.roles table. This
* coupling between the IAuthenticator and IRoleManager implementations exists
* because setting a role's password via CQL is done with a CREATE ROLE or
* ALTER ROLE statement, the processing of which is handled by IRoleManager.
* As IAuthenticator is concerned only with credentials checking and has no
* means to modify passwords, PasswordAuthenticator depends on
* CassandraRoleManager for those functions.
*
* Alternative IAuthenticator implementations may be used in conjunction with
* CassandraRoleManager, but WITH PASSWORD = 'password' will not be supported
* in CREATE/ALTER ROLE statements.
*
* Such a configuration could be implemented using a custom IRoleManager that
* extends CassandraRoleManager and which includes Option.PASSWORD in the Set<Option>
* returned from supportedOptions/alterableOptions. Any additional processing
* of the password itself (such as storing it in an alternative location) would
* be added in overriden createRole and alterRole implementations.
*/
public class CassandraRoleManager implements IRoleManager
{
private static final Logger logger = LoggerFactory.getLogger(CassandraRoleManager.class);
static final String DEFAULT_SUPERUSER_NAME = "cassandra";
static final String DEFAULT_SUPERUSER_PASSWORD = "cassandra";
// Transform a row in the AuthKeyspace.ROLES to a Role instance
private static final Function<UntypedResultSet.Row, Role> ROW_TO_ROLE = new Function<UntypedResultSet.Row, Role>()
{
public Role apply(UntypedResultSet.Row row)
{
return new Role(row.getString("role"),
row.getBoolean("is_superuser"),
row.getBoolean("can_login"),
row.has("member_of") ? row.getSet("member_of", UTF8Type.instance)
: Collections.<String>emptySet());
}
};
public static final String LEGACY_USERS_TABLE = "users";
// Transform a row in the legacy system_auth.users table to a Role instance,
// used to fallback to previous schema on a mixed cluster during an upgrade
private static final Function<UntypedResultSet.Row, Role> LEGACY_ROW_TO_ROLE = new Function<UntypedResultSet.Row, Role>()
{
public Role apply(UntypedResultSet.Row row)
{
return new Role(row.getString("name"),
row.getBoolean("super"),
true,
Collections.<String>emptySet());
}
};
// 2 ** GENSALT_LOG2_ROUNS rounds of hashing will be performed.
private static final int GENSALT_LOG2_ROUNDS = 10;
// NullObject returned when a supplied role name not found in AuthKeyspace.ROLES
private static final Role NULL_ROLE = new Role(null, false, false, Collections.<String>emptySet());
private SelectStatement loadRoleStatement;
private SelectStatement legacySelectUserStatement;
private final Set<Option> supportedOptions;
private final Set<Option> alterableOptions;
public CassandraRoleManager()
{
supportedOptions = DatabaseDescriptor.getAuthenticator().getClass() == PasswordAuthenticator.class
? ImmutableSet.of(Option.LOGIN, Option.SUPERUSER, Option.PASSWORD)
: ImmutableSet.of(Option.LOGIN, Option.SUPERUSER);
alterableOptions = DatabaseDescriptor.getAuthenticator().getClass().equals(PasswordAuthenticator.class)
? ImmutableSet.of(Option.PASSWORD)
: ImmutableSet.<Option>of();
}
public void setup()
{
loadRoleStatement = (SelectStatement) prepare("SELECT * from %s.%s WHERE role = ?",
AuthKeyspace.NAME,
AuthKeyspace.ROLES);
// If the old users table exists, we may need to migrate the legacy authn
// data to the new table. We also need to prepare a statement to read from
// it, so we can continue to use the old tables while the cluster is upgraded.
// Otherwise, we may need to create a default superuser role to enable others
// to be added.
if (Schema.instance.getCFMetaData(AuthKeyspace.NAME, "users") != null)
{
legacySelectUserStatement = (SelectStatement) prepare("SELECT * FROM %s.%s WHERE name = ?",
AuthKeyspace.NAME,
LEGACY_USERS_TABLE);
scheduleSetupTask(new Runnable()
{
public void run()
{
convertLegacyData();
}
});
}
else
{
scheduleSetupTask(new Runnable()
{
public void run()
{
setupDefaultRole();
}
});
}
}
public Set<Option> supportedOptions()
{
return supportedOptions;
}
public Set<Option> alterableOptions()
{
return alterableOptions;
}
public void createRole(AuthenticatedUser performer, String role, Map<Option, Object> options)
throws RequestValidationException, RequestExecutionException
{
String insertCql = options.containsKey(Option.PASSWORD)
? String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) VALUES ('%s', %s, %s, '%s')",
AuthKeyspace.NAME,
AuthKeyspace.ROLES,
escape(role),
options.get(Option.SUPERUSER),
options.get(Option.LOGIN),
escape(hashpw(options.get(Option.PASSWORD).toString())))
: String.format("INSERT INTO %s.%s (role, is_superuser, can_login) VALUES ('%s', %s, %s)",
AuthKeyspace.NAME,
AuthKeyspace.ROLES,
escape(role),
options.get(Option.SUPERUSER),
options.get(Option.LOGIN));
process(insertCql, consistencyForRole(role));
}
public void dropRole(AuthenticatedUser performer, String role) throws RequestValidationException, RequestExecutionException
{
process(String.format("DELETE FROM %s.%s WHERE role = '%s'",
AuthKeyspace.NAME,
AuthKeyspace.ROLES,
escape(role)),
consistencyForRole(role));
removeAllMembers(role);
}
public void alterRole(AuthenticatedUser performer, String role, Map<Option, Object> options)
throws RequestValidationException, RequestExecutionException
{
// Unlike most of the other data access methods here, this does not use a
// prepared statement in order to allow the set of assignments to be variable.
String assignments = Joiner.on(',')
.join(Iterables.filter(optionsToAssignments(options),
Predicates.notNull()));
if (!Strings.isNullOrEmpty(assignments))
{
QueryProcessor.process(String.format("UPDATE %s.%s SET %s WHERE role = '%s'",
AuthKeyspace.NAME,
AuthKeyspace.ROLES,
assignments,
escape(role)),
consistencyForRole(role));
}
}
public void grantRole(AuthenticatedUser performer, String role, String grantee)
throws RequestValidationException, RequestExecutionException
{
if (getRoles(grantee, true).contains(role))
throw new InvalidRequestException(String.format("%s is a member of %s", grantee, role));
if (getRoles(role, true).contains(grantee))
throw new InvalidRequestException(String.format("%s is a member of %s", role, grantee));
modifyRoleMembership(grantee, role, "+");
process(String.format("INSERT INTO %s.%s (role, member) values ('%s', '%s')",
AuthKeyspace.NAME,
AuthKeyspace.ROLE_MEMBERS,
escape(role),
escape(grantee)),
consistencyForRole(role));
}
public void revokeRole(AuthenticatedUser performer, String role, String revokee)
throws RequestValidationException, RequestExecutionException
{
if (!getRoles(revokee, false).contains(role))
throw new InvalidRequestException(String.format("%s is not a member of %s", revokee, role));
modifyRoleMembership(revokee, role, "-");
process(String.format("DELETE FROM %s.%s WHERE role = '%s' and member = '%s'",
AuthKeyspace.NAME,
AuthKeyspace.ROLE_MEMBERS,
escape(role),
escape(revokee)),
consistencyForRole(role));
}
public Set<String> getRoles(String grantee, boolean includeInherited) throws RequestValidationException, RequestExecutionException
{
Set<String> roles = new HashSet<>();
Role role = getRole(grantee);
if (!role.equals(NULL_ROLE))
{
roles.add(role.name);
collectRoles(role, roles, includeInherited);
}
return roles;
}
public Set<String> getAllRoles() throws RequestValidationException, RequestExecutionException
{
UntypedResultSet rows = QueryProcessor.process(String.format("SELECT role from %s.%s",
AuthKeyspace.NAME,
AuthKeyspace.ROLES),
ConsistencyLevel.QUORUM);
Iterable<String> roles = Iterables.transform(rows, new Function<UntypedResultSet.Row, String>()
{
public String apply(UntypedResultSet.Row row)
{
return row.getString("role");
}
});
return ImmutableSet.<String>builder().addAll(roles).build();
}
public boolean isSuper(String role)
{
return getRole(role).isSuper;
}
public boolean canLogin(String role)
{
return getRole(role).canLogin;
}
public boolean isExistingRole(String role)
{
return getRole(role) != NULL_ROLE;
}
public Set<? extends IResource> protectedResources()
{
return ImmutableSet.of(DataResource.table(AuthKeyspace.NAME, AuthKeyspace.ROLES),
DataResource.table(AuthKeyspace.NAME, AuthKeyspace.ROLE_MEMBERS));
}
public void validateConfiguration() throws ConfigurationException
{
}
/*
* Create the default superuser role to bootstrap role creation on a clean system. Preemptively
* gives the role the default password so PasswordAuthenticator can be used to log in (if
* configured)
*/
private static void setupDefaultRole()
{
try
{
if (!hasExistingRoles())
{
QueryProcessor.process(String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) " +
"VALUES ('%s', true, true, '%s')",
AuthKeyspace.NAME,
AuthKeyspace.ROLES,
DEFAULT_SUPERUSER_NAME,
escape(hashpw(DEFAULT_SUPERUSER_PASSWORD))),
consistencyForRole(DEFAULT_SUPERUSER_NAME));
logger.info("Created default superuser role '{}'", DEFAULT_SUPERUSER_NAME);
}
}
catch (RequestExecutionException e)
{
logger.warn("CassandraRoleManager skipped default role setup: some nodes were not ready");
}
}
private static boolean hasExistingRoles() throws RequestExecutionException
{
// Try looking up the 'cassandra' default role first, to avoid the range query if possible.
String defaultSUQuery = String.format("SELECT * FROM %s.%s WHERE role = '%s'", AuthKeyspace.NAME, AuthKeyspace.ROLES, DEFAULT_SUPERUSER_NAME);
String allUsersQuery = String.format("SELECT * FROM %s.%s LIMIT 1", AuthKeyspace.NAME, AuthKeyspace.ROLES);
return !process(defaultSUQuery, ConsistencyLevel.ONE).isEmpty()
|| !process(defaultSUQuery, ConsistencyLevel.QUORUM).isEmpty()
|| !process(allUsersQuery, ConsistencyLevel.QUORUM).isEmpty();
}
private void scheduleSetupTask(Runnable runnable)
{
// The delay is to give the node a chance to see its peers before attempting the operation
ScheduledExecutors.optionalTasks.schedule(runnable, AuthKeyspace.SUPERUSER_SETUP_DELAY, TimeUnit.MILLISECONDS);
}
/*
* Copy legacy auth data from the system_auth.users & system_auth.credentials tables to
* the new system_auth.roles table. This setup is not performed if AllowAllAuthenticator
* is configured (see Auth#setup).
*/
private void convertLegacyData()
{
try
{
// read old data at QUORUM as it may contain the data for the default superuser
if (Schema.instance.getCFMetaData("system_auth", "users") != null)
{
logger.info("Converting legacy users");
UntypedResultSet users = QueryProcessor.process("SELECT * FROM system_auth.users",
ConsistencyLevel.QUORUM);
for (UntypedResultSet.Row row : users)
{
Map<Option, Object> options = new HashMap<>();
options.put(Option.SUPERUSER, row.getBoolean("super"));
options.put(Option.LOGIN, true);
createRole(null, row.getString("name"), options);
}
logger.info("Completed conversion of legacy users");
}
if (Schema.instance.getCFMetaData("system_auth", "credentials") != null)
{
logger.info("Migrating legacy credentials data to new system table");
UntypedResultSet credentials = QueryProcessor.process("SELECT * FROM system_auth.credentials",
ConsistencyLevel.QUORUM);
for (UntypedResultSet.Row row : credentials)
{
// Write the password directly into the table to avoid doubly encrypting it
QueryProcessor.process(String.format("UPDATE %s.%s SET salted_hash = '%s' WHERE role = '%s'",
AuthKeyspace.NAME,
AuthKeyspace.ROLES,
row.getString("salted_hash"),
row.getString("username")),
consistencyForRole(row.getString("username")));
}
logger.info("Completed conversion of legacy credentials");
}
}
catch (Exception e)
{
logger.info("Unable to complete conversion of legacy auth data (perhaps not enough nodes are upgraded yet). " +
"Conversion should not be considered complete");
logger.debug("Conversion error", e);
}
}
private CQLStatement prepare(String template, String keyspace, String table)
{
try
{
return QueryProcessor.parseStatement(String.format(template, keyspace, table)).prepare().statement;
}
catch (RequestValidationException e)
{
throw new AssertionError(e); // not supposed to happen
}
}
/*
* Retrieve all roles granted to the given role. includeInherited specifies
* whether to include only those roles granted directly or all inherited roles.
*/
private void collectRoles(Role role, Set<String> collected, boolean includeInherited) throws RequestValidationException, RequestExecutionException
{
for (String memberOf : role.memberOf)
{
Role granted = getRole(memberOf);
if (role.equals(NULL_ROLE))
continue;
collected.add(granted.name);
if (includeInherited)
collectRoles(granted, collected, true);
}
}
/*
* Get a single Role instance given the role name. This never returns null, instead it
* uses the null object NULL_ROLE when a role with the given name cannot be found. So
* it's always safe to call methods on the returned object without risk of NPE.
*/
private Role getRole(String name)
{
try
{
// If it exists, try the legacy users table in case the cluster
// is in the process of being upgraded and so is running with mixed
// versions of the authn schema.
return (Schema.instance.getCFMetaData(AuthKeyspace.NAME, "users") != null)
? getRoleFromTable(name, legacySelectUserStatement, LEGACY_ROW_TO_ROLE)
: getRoleFromTable(name, loadRoleStatement, ROW_TO_ROLE);
}
catch (RequestExecutionException | RequestValidationException e)
{
throw new RuntimeException(e);
}
}
private Role getRoleFromTable(String name, SelectStatement statement, Function<UntypedResultSet.Row, Role> function)
throws RequestExecutionException, RequestValidationException
{
ResultMessage.Rows rows =
statement.execute(QueryState.forInternalCalls(),
QueryOptions.forInternalCalls(consistencyForRole(name),
Collections.singletonList(ByteBufferUtil.bytes(name))));
if (rows.result.isEmpty())
return NULL_ROLE;
return function.apply(UntypedResultSet.create(rows.result).one());
}
/*
* Adds or removes a role name from the membership list of an entry in the roles table table
* (adds if op is "+", removes if op is "-")
*/
private void modifyRoleMembership(String grantee, String role, String op)
throws RequestExecutionException
{
process(String.format("UPDATE %s.%s SET member_of = member_of %s {'%s'} WHERE role = '%s'",
AuthKeyspace.NAME,
AuthKeyspace.ROLES,
op,
escape(role),
escape(grantee)),
consistencyForRole(grantee));
}
/*
* Clear the membership list of the given role
*/
private void removeAllMembers(String role) throws RequestValidationException, RequestExecutionException
{
// Get the membership list of the the given role
UntypedResultSet rows = process(String.format("SELECT member FROM %s.%s WHERE role = '%s'",
AuthKeyspace.NAME,
AuthKeyspace.ROLE_MEMBERS,
escape(role)),
consistencyForRole(role));
if (rows.isEmpty())
return;
// Update each member in the list, removing this role from its own list of granted roles
for (UntypedResultSet.Row row : rows)
modifyRoleMembership(row.getString("member"), role, "-");
// Finally, remove the membership list for the dropped role
process(String.format("DELETE FROM %s.%s WHERE role = '%s'",
AuthKeyspace.NAME,
AuthKeyspace.ROLE_MEMBERS,
escape(role)),
consistencyForRole(role));
}
/*
* Convert a map of Options from a CREATE/ALTER statement into
* assignment clauses used to construct a CQL UPDATE statement
*/
private Iterable<String> optionsToAssignments(Map<Option, Object> options)
{
return Iterables.transform(
options.entrySet(),
new Function<Map.Entry<Option, Object>, String>()
{
public String apply(Map.Entry<Option, Object> entry)
{
switch (entry.getKey())
{
case LOGIN:
return String.format("can_login = %s", entry.getValue());
case SUPERUSER:
return String.format("is_superuser = %s", entry.getValue());
case PASSWORD:
return String.format("salted_hash = '%s'", escape(hashpw((String) entry.getValue())));
default:
return null;
}
}
});
}
protected static ConsistencyLevel consistencyForRole(String role)
{
if (role.equals(DEFAULT_SUPERUSER_NAME))
return ConsistencyLevel.QUORUM;
else
return ConsistencyLevel.LOCAL_ONE;
}
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, ConsistencyLevel consistencyLevel) throws RequestExecutionException
{
return QueryProcessor.process(query, consistencyLevel);
}
private static final class Role
{
private String name;
private final boolean isSuper;
private final boolean canLogin;
private Set<String> memberOf;
private Role(String name, boolean isSuper, boolean canLogin, Set<String> memberOf)
{
this.name = name;
this.isSuper = isSuper;
this.canLogin = canLogin;
this.memberOf = memberOf;
}
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof Role))
return false;
Role r = (Role) o;
return Objects.equal(name, r.name);
}
public int hashCode()
{
return Objects.hashCode(name);
}
}
}

View File

@ -25,16 +25,16 @@ import org.apache.cassandra.config.Schema;
/**
* The primary type of resource in Cassandra.
*
* Used to represent a column family or a keyspace or the root level "data" resource.
* Used to represent a table or a keyspace or the root level "data" resource.
* "data" - the root level data resource.
* "data/keyspace_name" - keyspace-level data resource.
* "data/keyspace_name/column_family_name" - cf-level data resource.
* "data/keyspace_name/table_name" - table-level data resource.
*/
public class DataResource implements IResource
{
enum Level
{
ROOT, KEYSPACE, COLUMN_FAMILY
ROOT, KEYSPACE, TABLE
}
private static final String ROOT_NAME = "data";
@ -42,27 +42,27 @@ public class DataResource implements IResource
private final Level level;
private final String keyspace;
private final String columnFamily;
private final String table;
private DataResource()
{
level = Level.ROOT;
keyspace = null;
columnFamily = null;
table = null;
}
private DataResource(String keyspace)
{
level = Level.KEYSPACE;
this.keyspace = keyspace;
columnFamily = null;
table = null;
}
private DataResource(String keyspace, String columnFamily)
private DataResource(String keyspace, String table)
{
level = Level.COLUMN_FAMILY;
level = Level.TABLE;
this.keyspace = keyspace;
this.columnFamily = columnFamily;
this.table = table;
}
/**
@ -85,15 +85,15 @@ public class DataResource implements IResource
}
/**
* Creates a DataResource instance representing a column family.
* Creates a DataResource instance representing a table.
*
* @param keyspace Name of the keyspace.
* @param columnFamily Name of the column family.
* @param table Name of the table.
* @return DataResource instance representing the column family.
*/
public static DataResource columnFamily(String keyspace, String columnFamily)
public static DataResource table(String keyspace, String table)
{
return new DataResource(keyspace, columnFamily);
return new DataResource(keyspace, table);
}
/**
@ -115,7 +115,7 @@ public class DataResource implements IResource
if (parts.length == 2)
return keyspace(parts[1]);
return columnFamily(parts[1], parts[2]);
return table(parts[1], parts[2]);
}
/**
@ -129,8 +129,8 @@ public class DataResource implements IResource
return ROOT_NAME;
case KEYSPACE:
return String.format("%s/%s", ROOT_NAME, keyspace);
case COLUMN_FAMILY:
return String.format("%s/%s/%s", ROOT_NAME, keyspace, columnFamily);
case TABLE:
return String.format("%s/%s/%s", ROOT_NAME, keyspace, table);
}
throw new AssertionError();
}
@ -144,7 +144,7 @@ public class DataResource implements IResource
{
case KEYSPACE:
return root();
case COLUMN_FAMILY:
case TABLE:
return keyspace(keyspace);
}
throw new IllegalStateException("Root-level resource can't have a parent");
@ -160,9 +160,9 @@ public class DataResource implements IResource
return level == Level.KEYSPACE;
}
public boolean isColumnFamilyLevel()
public boolean isTableLevel()
{
return level == Level.COLUMN_FAMILY;
return level == Level.TABLE;
}
/**
* @return keyspace of the resource. Throws IllegalStateException if it's the root-level resource.
@ -175,13 +175,13 @@ public class DataResource implements IResource
}
/**
* @return column family of the resource. Throws IllegalStateException if it's not a cf-level resource.
* @return column family of the resource. Throws IllegalStateException if it's not a table-level resource.
*/
public String getColumnFamily()
public String getTable()
{
if (!isColumnFamilyLevel())
if (!isTableLevel())
throw new IllegalStateException(String.format("%s data resource has no table", level));
return columnFamily;
return table;
}
/**
@ -205,8 +205,8 @@ public class DataResource implements IResource
return true;
case KEYSPACE:
return Schema.instance.getKeyspaces().contains(keyspace);
case COLUMN_FAMILY:
return Schema.instance.getCFMetaData(keyspace, columnFamily) != null;
case TABLE:
return Schema.instance.getCFMetaData(keyspace, table) != null;
}
throw new AssertionError();
}
@ -220,8 +220,8 @@ public class DataResource implements IResource
return "<all keyspaces>";
case KEYSPACE:
return String.format("<keyspace %s>", keyspace);
case COLUMN_FAMILY:
return String.format("<table %s.%s>", keyspace, columnFamily);
case TABLE:
return String.format("<table %s.%s>", keyspace, table);
}
throw new AssertionError();
}
@ -239,12 +239,12 @@ public class DataResource implements IResource
return Objects.equal(level, ds.level)
&& Objects.equal(keyspace, ds.keyspace)
&& Objects.equal(columnFamily, ds.columnFamily);
&& Objects.equal(table, ds.table);
}
@Override
public int hashCode()
{
return Objects.hashCode(level, keyspace, columnFamily);
return Objects.hashCode(level, keyspace, table);
}
}

View File

@ -22,85 +22,15 @@ import java.util.Set;
import org.apache.cassandra.exceptions.AuthenticationException;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.exceptions.RequestValidationException;
public interface IAuthenticator
{
static final String USERNAME_KEY = "username";
static final String PASSWORD_KEY = "password";
/**
* Supported CREATE USER/ALTER USER options.
* Currently only PASSWORD is available.
*/
enum Option
{
PASSWORD
}
/**
* Whether or not the authenticator requires explicit login.
* If false will instantiate user with AuthenticatedUser.ANONYMOUS_USER.
*/
boolean requireAuthentication();
/**
* Set of options supported by CREATE USER and ALTER USER queries.
* Should never return null - always return an empty set instead.
*/
Set<Option> supportedOptions();
/**
* Subset of supportedOptions that users are allowed to alter when performing ALTER USER [themselves].
* Should never return null - always return an empty set instead.
*/
Set<Option> alterableOptions();
/**
* Authenticates a user given a Map<String, String> of credentials.
* Should never return null - always throw AuthenticationException instead.
* Returning AuthenticatedUser.ANONYMOUS_USER is an option as well if authentication is not required.
*
* @throws AuthenticationException if credentials don't match any known user.
*/
AuthenticatedUser authenticate(Map<String, String> credentials) throws AuthenticationException;
/**
* Called during execution of CREATE USER query (also may be called on startup, see seedSuperuserOptions method).
* If authenticator is static then the body of the method should be left blank, but don't throw an exception.
* options are guaranteed to be a subset of supportedOptions().
*
* @param username Username of the user to create.
* @param options Options the user will be created with.
* @throws RequestValidationException
* @throws RequestExecutionException
*/
void create(String username, Map<Option, Object> options) throws RequestValidationException, RequestExecutionException;
/**
* Called during execution of ALTER USER query.
* options are always guaranteed to be a subset of supportedOptions(). Furthermore, if the user performing the query
* is not a superuser and is altering himself, then options are guaranteed to be a subset of alterableOptions().
* Keep the body of the method blank if your implementation doesn't support any options.
*
* @param username Username of the user that will be altered.
* @param options Options to alter.
* @throws RequestValidationException
* @throws RequestExecutionException
*/
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 RequestValidationException
* @throws RequestExecutionException
*/
void drop(String username) throws RequestValidationException, RequestExecutionException;
/**
* Set of resources that should be made inaccessible to users and only accessible internally.
*
@ -121,4 +51,78 @@ public interface IAuthenticator
* For example, use this method to create any required keyspaces/column families.
*/
void setup();
/**
* Provide a SASL handler to perform authentication for an single connection. SASL
* is a stateful protocol, so a new instance must be used for each authentication
* attempt.
* @return org.apache.cassandra.auth.IAuthenticator.SaslNegotiator implementation
* (see {@link org.apache.cassandra.auth.PasswordAuthenticator.PlainTextSaslAuthenticator})
*/
SaslNegotiator newSaslNegotiator();
/**
* For implementations which support the Thrift login method that accepts arbitrary
* key/value pairs containing credentials data.
* Also used by CQL native protocol v1, in which username and password are sent from
* client to server in a {@link org.apache.cassandra.transport.messages.CredentialsMessage}
* Implementations where support for Thrift and CQL protocol v1 is not required should make
* this an unsupported operation.
*
* Should never return null - always throw AuthenticationException instead.
* Returning AuthenticatedUser.ANONYMOUS_USER is an option as well if authentication is not required.
*
* @param credentials implementation specific key/value pairs
* @return non-null representation of the authenticated subject
* @throws AuthenticationException
*/
AuthenticatedUser legacyAuthenticate(Map<String, String> credentials) throws AuthenticationException;
/**
* Performs the actual SASL negotiation for a single authentication attempt.
* SASL is stateful, so a new instance should be used for each attempt.
* Non-trivial implementations may delegate to an instance of {@link javax.security.sasl.SaslServer}
*/
public interface SaslNegotiator
{
/**
* Evaluates the client response data and generates a byte[] reply which may be a further challenge or purely
* informational in the case that the negotiation is completed on this round.
*
* This method is called each time a {@link org.apache.cassandra.transport.messages.AuthResponse} is received
* from a client. After it is called, {@link isComplete()} is checked to determine whether the negotiation has
* finished. If so, an AuthenticatedUser is obtained by calling {@link getAuthenticatedUser()} and that user
* associated with the active connection and the byte[] sent back to the client via an
* {@link org.apache.cassandra.transport.messages.AuthSuccess} message. If the negotiation is not yet complete,
* the byte[] is returned to the client as a further challenge in an
* {@link org.apache.cassandra.transport.messages.AuthChallenge} message. This continues until the negotiation
* does complete or an error is encountered.
*
* @param clientResponse The non-null (but possibly empty) response sent by the client
* @return The possibly null response to send to the client.
* @throws AuthenticationException
* see {@link javax.security.sasl.SaslServer#evaluateResponse(byte[])}
*/
public byte[] evaluateResponse(byte[] clientResponse) throws AuthenticationException;
/**
* Called after each invocation of {@link evaluateResponse(byte[])} to determine whether the authentication has
* completed successfully or should be continued.
*
* @return true if the authentication exchange has completed; false otherwise.
* see {@link javax.security.sasl.SaslServer#isComplete()}
*/
public boolean isComplete();
/**
* Following a sucessful negotiation, get the AuthenticatedUser representing the logged in subject.
* This method should only be called if {@link isComplete()} returns true.
* Should never return null - always throw AuthenticationException instead.
* Returning AuthenticatedUser.ANONYMOUS_USER is an option if authentication is not required.
*
* @return non-null representation of the authenticated subject
* @throws AuthenticationException
*/
public AuthenticatedUser getAuthenticatedUser() throws AuthenticationException;
}
}

View File

@ -29,7 +29,12 @@ import org.apache.cassandra.exceptions.RequestValidationException;
public interface IAuthorizer
{
/**
* The primary IAuthorizer method. Returns a set of permissions of a user on a resource.
* Returns a set of permissions of a user on a resource.
* Since Roles were introduced in version 3.0, Cassandra does not distinguish in any
* meaningful way between users and roles. A role may or may not have login privileges
* and roles may be granted to other roles. In fact, Cassandra does not really have the
* concept of a user, except to link a client session to role. AuthenticatedUser can be
* thought of as a manifestation of a role, linked to a specific client connection.
*
* @param user Authenticated user requesting authorization.
* @param resource Resource for which the authorization is being requested. @see DataResource.
@ -38,18 +43,18 @@ public interface IAuthorizer
Set<Permission> authorize(AuthenticatedUser user, IResource resource);
/**
* Grants a set of permissions on a resource to a user.
* Grants a set of permissions on a resource to a role.
* The opposite of revoke().
*
* @param performer User who grants the permissions.
* @param permissions Set of permissions to grant.
* @param to Grantee of the permissions.
* @param to Name of the role to which the permissions are to be granted.
* @param resource Resource on which to grant the permissions.
*
* @throws RequestValidationException
* @throws RequestExecutionException
*/
void grant(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String to)
void grant(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String grantee)
throws RequestValidationException, RequestExecutionException;
/**
@ -58,39 +63,41 @@ public interface IAuthorizer
*
* @param performer User who revokes the permissions.
* @param permissions Set of permissions to revoke.
* @param from Revokee of the permissions.
* @param revokee Name of the role from which to the permissions are to be revoked.
* @param resource Resource on which to revoke the permissions.
*
* @throws RequestValidationException
* @throws RequestExecutionException
*/
void revoke(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String from)
void revoke(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String revokee)
throws RequestValidationException, RequestExecutionException;
/**
* Returns a list of permissions on a resource of a user.
* Returns a list of permissions on a resource granted to a role.
*
* @param performer User who wants to see the permissions.
* @param permissions Set of Permission values the user is interested in. The result should only include the matching ones.
* @param resource The resource on which permissions are requested. Can be null, in which case permissions on all resources
* should be returned.
* @param of The user whose permissions are requested. Can be null, in which case permissions of every user should be returned.
* @param permissions Set of Permission values the user is interested in. The result should only include the
* matching ones.
* @param resource The resource on which permissions are requested. Can be null, in which case permissions on all
* resources should be returned.
* @param of The name of the role whose permissions are requested. Can be null, in which case permissions of every
* role should be returned.
*
* @return All of the matching permission that the requesting user is authorized to know about.
*
* @throws RequestValidationException
* @throws RequestExecutionException
*/
Set<PermissionDetails> list(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String of)
Set<PermissionDetails> list(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String grantee)
throws RequestValidationException, RequestExecutionException;
/**
* This method is called before deleting a user with DROP USER query so that a new user with the same
* name wouldn't inherit permissions of the deleted user in the future.
* Called before deleting a role with DROP ROLE statement (or the alias provided for compatibility,
* DROP USER) so that a new role with the same name wouldn't inherit permissions of the deleted one in the future.
*
* @param droppedUser The user to revoke all permissions from.
* @param revokee The role to revoke all permissions from.
*/
void revokeAll(String droppedUser);
void revokeAll(String revokee);
/**
* This method is called after a resource is removed (i.e. keyspace or a table is dropped).

View File

@ -0,0 +1,200 @@
/*
* 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 org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.exceptions.RequestValidationException;
/**
* Responsible for managing roles (which also includes what
* used to be known as users), including creation, deletion,
* alteration and the granting & revoking of roles to other
* roles.
*/
public interface IRoleManager
{
/**
* Supported options for CREATE ROLE/ALTER ROLE (and
* CREATE USER/ALTER USER, which are aliases provided
* for backwards compatibility).
*/
public enum Option
{
SUPERUSER, PASSWORD, LOGIN, OPTIONS
}
/**
* Set of options supported by CREATE ROLE and ALTER ROLE queries.
* Should never return null - always return an empty set instead.
*/
Set<Option> supportedOptions();
/**
* Subset of supportedOptions that users are allowed to alter when performing ALTER ROLE [themselves].
* Should never return null - always return an empty set instead.
*/
Set<Option> alterableOptions();
/**
* Called during execution of a CREATE ROLE statement.
* options are guaranteed to be a subset of supportedOptions().
*
* @param performer User issuing the create role statement.
* @param role Name of the role being created
* @param options Options the role will be created with
* @throws RequestValidationException
* @throws RequestExecutionException
*/
void createRole(AuthenticatedUser performer, String role, Map<Option, Object> options)
throws RequestValidationException, RequestExecutionException;
/**
* Called during execution of DROP ROLE statement, as well we removing any main record of the role from the system
* this implies that we want to revoke this role from all other roles that it has been granted to.
*
* @param performer User issuing the drop role statement.
* @param role The name of the role to be dropped.
* @throws RequestValidationException
* @throws RequestExecutionException
*/
void dropRole(AuthenticatedUser performer, String role) throws RequestValidationException, RequestExecutionException;
/**
* Called during execution of ALTER ROLE statement.
* options are always guaranteed to be a subset of supportedOptions(). Furthermore, if the actor performing the query
* is not a superuser and is altering themself, then options are guaranteed to be a subset of alterableOptions().
* Keep the body of the method blank if your implementation doesn't support modification of any options.
*
* @param performer User issuing the alter role statement.
* @param role Name of the role that will be altered.
* @param options Options to alter.
* @throws RequestValidationException
* @throws RequestExecutionException
*/
void alterRole(AuthenticatedUser performer, String role, Map<Option, Object> options)
throws RequestValidationException, RequestExecutionException;
/**
* Called during execution of GRANT ROLE query.
* Grant an role to another existing role. A grantee that has a role granted to it will inherit any
* permissions of the granted role.
*
* @param performer User issuing the grant statement.
* @param role The name of the role to be granted to the grantee.
* @param grantee The name of the role acting as the grantee.
* @throws RequestValidationException
* @throws RequestExecutionException
*/
void grantRole(AuthenticatedUser performer, String role, String grantee)
throws RequestValidationException, RequestExecutionException;
/**
* Called during the execution of a REVOKE ROLE query.
* Revoke an granted role from an existing role. The revokee will lose any permissions inherited from the role being
* revoked.
*
* @param performer User issuing the revoke statement.
* @param role The name of the role to be revoked.
* @param revokee The name of the role from which the granted role is to be revoked.
* @throws RequestValidationException
* @throws RequestExecutionException
*/
void revokeRole(AuthenticatedUser performer, String role, String revokee)
throws RequestValidationException, RequestExecutionException;
/**
* Called during execution of a LIST ROLES query.
* Returns a set of roles that have been granted to the grantee using GRANT ROLE.
*
* @param grantee Name of the role whose granted roles will be listed.
* @param includeInherited if True will list inherited roles as well as those directly granted to the grantee.
* @return A list containing the granted roles for the user.
* @throws RequestValidationException
* @throws RequestExecutionException
*/
Set<String> getRoles(String grantee, boolean includeInherited) throws RequestValidationException, RequestExecutionException;
/**
* Called during the execution of an unqualified LIST ROLES query.
* Returns the total set of distinct roles in the system.
*
* @return the set of all roles in the system.
* @throws RequestValidationException
* @throws RequestExecutionException
*/
Set<String> getAllRoles() throws RequestValidationException, RequestExecutionException;
/**
* Return true if there exists a Role with the given name that also has
* superuser status. Superuser status may be inherited from another
* granted role, so this method should return true if either the named
* Role, or any other Role it is transitively granted has superuser
* status.
*
* @param role name of the role
* @return true if the role exists and has superuser status, either
* directly or transitively, otherwise false.
*/
boolean isSuper(String role);
/**
* Return true if there exists a Role with the given name which has login
* privileges. Such privileges is not inherited from other granted Roles
* and so must be directly granted to the named Role with the LOGIN option
* of CREATE ROLE or ALTER ROLE
*
* @param role name of the Role
* @return true if the role exists and is permitted to login, otherwise false
*/
boolean canLogin(String role);
/**
* Return true is a Role with the given name exists in the system.
*
* @param role name of the Role.
* @return true if the name identifies an extant Role in the system,
* otherwise false
*/
boolean isExistingRole(String role);
/**
* Set of resources that should be made inaccessible to users and only accessible internally.
*
* @return Keyspaces and column families that will be unmodifiable by users; other resources.
*/
Set<? extends IResource> protectedResources();
/**
* Hook to perform validation of an implementation's configuration (if supported).
*
* @throws ConfigurationException
*/
void validateConfiguration() throws ConfigurationException;
/**
* Hook to perform implementation specific initialization, called once upon system startup.
*
* For example, use this method to create any required keyspaces/column families.
*/
void setup();
}

View File

@ -1,94 +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.
*/
package org.apache.cassandra.auth;
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.RequestExecutionException;
import org.apache.cassandra.exceptions.RequestValidationException;
/**
* Provides a transitional IAuthenticator implementation for old-style (pre-1.2) authenticators.
*
* Comes with default implementation for the all of the new methods.
* Subclass LegacyAuthenticator instead of implementing the old IAuthenticator and your old IAuthenticator
* implementation should continue to work.
*/
public abstract class LegacyAuthenticator implements IAuthenticator
{
/**
* @return The user that a connection is initialized with, or 'null' if a user must call login().
*/
public abstract AuthenticatedUser defaultUser();
/**
* @param credentials An implementation specific collection of identifying information.
* @return A successfully authenticated user: should throw AuthenticationException rather than ever returning null.
*/
public abstract AuthenticatedUser authenticate(Map<String, String> credentials) throws AuthenticationException;
public abstract void validateConfiguration() throws ConfigurationException;
@Override
public boolean requireAuthentication()
{
return defaultUser() == null;
}
@Override
public Set<Option> supportedOptions()
{
return Collections.emptySet();
}
@Override
public Set<Option> alterableOptions()
{
return Collections.emptySet();
}
@Override
public void create(String username, Map<Option, Object> options) throws RequestValidationException, RequestExecutionException
{
}
@Override
public void alter(String username, Map<Option, Object> options) throws RequestValidationException, RequestExecutionException
{
}
@Override
public void drop(String username) throws RequestValidationException, RequestExecutionException
{
}
@Override
public Set<IResource> protectedResources()
{
return Collections.emptySet();
}
@Override
public void setup()
{
}
}

View File

@ -1,114 +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.
*/
package org.apache.cassandra.auth;
import java.util.*;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.UnauthorizedException;
/**
* Provides a transitional IAuthorizer implementation for old-style (pre-1.2) authorizers.
*
* Translates old-style authorize() calls to the new-style, expands Permission.READ and Permission.WRITE
* into the new Permission values, translates the new resource hierarchy into the old hierarchy.
* Stubs the rest of the new methods.
* Subclass LegacyAuthorizer instead of implementing the old IAuthority and your old IAuthority implementation should
* continue to work.
*/
public abstract class LegacyAuthorizer implements IAuthorizer
{
/**
* @param user Authenticated user requesting authorization.
* @param resource List of Objects containing Strings and byte[]s: represents a resource in the old hierarchy.
* @return Set of permissions of the user on the resource. Should never return null. Use Permission.NONE instead.
*/
public abstract EnumSet<Permission> authorize(AuthenticatedUser user, List<Object> resource);
public abstract void validateConfiguration() throws ConfigurationException;
/**
* Translates new-style authorize() method call to the old-style (including permissions and the hierarchy).
*/
@Override
public Set<Permission> authorize(AuthenticatedUser user, IResource resource)
{
if (!(resource instanceof DataResource))
throw new IllegalArgumentException(String.format("%s resource is not supported by LegacyAuthorizer", resource.getName()));
DataResource dr = (DataResource) resource;
List<Object> legacyResource = new ArrayList<Object>();
legacyResource.add(Resources.ROOT);
legacyResource.add(Resources.KEYSPACES);
if (!dr.isRootLevel())
legacyResource.add(dr.getKeyspace());
if (dr.isColumnFamilyLevel())
legacyResource.add(dr.getColumnFamily());
Set<Permission> permissions = authorize(user, legacyResource);
if (permissions.contains(Permission.READ))
permissions.add(Permission.SELECT);
if (permissions.contains(Permission.WRITE))
permissions.addAll(EnumSet.of(Permission.CREATE, Permission.ALTER, Permission.DROP, Permission.MODIFY));
return permissions;
}
@Override
public void grant(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String to)
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
{
throw new InvalidRequestException("REVOKE operation is not supported by LegacyAuthorizer");
}
@Override
public void revokeAll(String droppedUser)
{
}
@Override
public void revokeAll(IResource droppedResource)
{
}
@Override
public Set<PermissionDetails> list(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String of)
throws InvalidRequestException, UnauthorizedException
{
throw new InvalidRequestException("LIST PERMISSIONS operation is not supported by LegacyAuthorizer");
}
@Override
public Set<IResource> protectedResources()
{
return Collections.emptySet();
}
@Override
public void setup()
{
}
}

View File

@ -19,78 +19,107 @@ package org.apache.cassandra.auth;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.ScheduledExecutors;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.cql3.statements.SelectStatement;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.exceptions.*;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.messages.ResultMessage;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.mindrot.jbcrypt.BCrypt;
import static org.apache.cassandra.auth.CassandraRoleManager.consistencyForRole;
/**
* PasswordAuthenticator is an IAuthenticator implementation
* that keeps credentials (usernames and bcrypt-hashed passwords)
* internally in C* - in system_auth.credentials CQL3 table.
* that keeps credentials (rolenames and bcrypt-hashed passwords)
* internally in C* - in system_auth.roles CQL3 table.
* Since 3.0, the management of roles (creation, modification,
* querying etc is the responsibility of IRoleManager. Use of
* PasswordAuthenticator requires the use of CassandraRoleManager
* for storage & retrieval of encryted passwords.
*/
public class PasswordAuthenticator implements ISaslAwareAuthenticator
public class PasswordAuthenticator implements IAuthenticator
{
private static final Logger logger = LoggerFactory.getLogger(PasswordAuthenticator.class);
// 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.
// really this is a rolename now, but as it only matters for Thrift, we leave it for backwards compatibility
public static final String USERNAME_KEY = "username";
public static final String PASSWORD_KEY = "password";
private static final byte NUL = 0;
private SelectStatement authenticateStatement;
public static final String LEGACY_CREDENTIALS_TABLE = "credentials";
private SelectStatement legacyAuthenticateStatement;
// No anonymous access.
public boolean requireAuthentication()
{
return true;
}
public Set<Option> supportedOptions()
private AuthenticatedUser authenticate(String username, String password) throws AuthenticationException
{
return ImmutableSet.of(Option.PASSWORD);
try
{
// If the legacy users table exists try to verify credentials there. This is to handle the case
// where the cluster is being upgraded and so is running with mixed versions of the authn tables
SelectStatement authenticationStatement = Schema.instance.getCFMetaData(AuthKeyspace.NAME, LEGACY_CREDENTIALS_TABLE) == null
? authenticateStatement
: legacyAuthenticateStatement;
return doAuthenticate(username, password, authenticationStatement);
}
catch (RequestExecutionException e)
{
logger.debug("Error performing internal authentication", e);
throw new AuthenticationException(e.toString());
}
}
// Let users alter their own password.
public Set<Option> alterableOptions()
public Set<DataResource> protectedResources()
{
return ImmutableSet.of(Option.PASSWORD);
// Also protected by CassandraRoleManager, but the duplication doesn't hurt and is more explicit
return ImmutableSet.of(DataResource.table(AuthKeyspace.NAME, AuthKeyspace.ROLES));
}
public AuthenticatedUser authenticate(Map<String, String> credentials) throws AuthenticationException
public void validateConfiguration() throws ConfigurationException
{
}
public void setup()
{
String query = String.format("SELECT %s FROM %s.%s WHERE role = ?",
SALTED_HASH,
AuthKeyspace.NAME,
AuthKeyspace.ROLES);
authenticateStatement = prepare(query);
if (Schema.instance.getCFMetaData(AuthKeyspace.NAME, LEGACY_CREDENTIALS_TABLE) != null)
{
query = String.format("SELECT %s from %s.%s WHERE username = ?",
SALTED_HASH,
AuthKeyspace.NAME,
LEGACY_CREDENTIALS_TABLE);
legacyAuthenticateStatement = prepare(query);
}
}
public AuthenticatedUser legacyAuthenticate(Map<String, String> credentials) throws AuthenticationException
{
String username = credentials.get(USERNAME_KEY);
if (username == null)
@ -100,183 +129,71 @@ public class PasswordAuthenticator implements ISaslAwareAuthenticator
if (password == null)
throw new AuthenticationException(String.format("Required key '%s' is missing", PASSWORD_KEY));
return authenticate(username, password);
}
public SaslNegotiator newSaslNegotiator()
{
return new PlainTextSaslAuthenticator();
}
private AuthenticatedUser doAuthenticate(String username, String password, SelectStatement authenticationStatement)
throws RequestExecutionException, AuthenticationException
{
UntypedResultSet result;
try
{
ResultMessage.Rows rows = authenticateStatement.execute(QueryState.forInternalCalls(),
QueryOptions.forInternalCalls(consistencyForUser(username),
Lists.newArrayList(ByteBufferUtil.bytes(username))));
ResultMessage.Rows rows = authenticationStatement.execute(QueryState.forInternalCalls(),
QueryOptions.forInternalCalls(consistencyForRole(username),
Lists.newArrayList(ByteBufferUtil.bytes(username))));
result = UntypedResultSet.create(rows.result);
}
catch (RequestValidationException e)
{
throw new AssertionError(e); // not supposed to happen
}
catch (RequestExecutionException e)
{
throw new AuthenticationException(e.toString());
}
if (result.isEmpty() || !BCrypt.checkpw(password, result.one().getString(SALTED_HASH)))
if ((result.isEmpty() || !result.one().has(SALTED_HASH)) || !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
private SelectStatement prepare(String query)
{
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))),
consistencyForUser(username));
}
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)),
consistencyForUser(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)),
consistencyForUser(username));
}
public Set<DataResource> protectedResources()
{
return ImmutableSet.of(DataResource.columnFamily(Auth.AUTH_KS, CREDENTIALS_CF));
}
public void validateConfiguration() throws ConfigurationException
{
}
public void setup()
{
Auth.setupTable(CREDENTIALS_CF, CREDENTIALS_CF_SCHEMA);
// 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.
ScheduledExecutors.nonPeriodicTasks.schedule(new Runnable()
{
public void run()
{
setupDefaultUser();
}
}, Auth.SUPERUSER_SETUP_DELAY, TimeUnit.MILLISECONDS);
try
{
String query = String.format("SELECT %s FROM %s.%s WHERE username = ?",
SALTED_HASH,
Auth.AUTH_KS,
CREDENTIALS_CF);
authenticateStatement = (SelectStatement) QueryProcessor.parseStatement(query).prepare().statement;
return (SelectStatement) QueryProcessor.getStatement(query, ClientState.forInternalCalls()).statement;
}
catch (RequestValidationException e)
{
throw new AssertionError(e); // not supposed to happen
throw new AssertionError(e);
}
}
public SaslAuthenticator newAuthenticator()
private class PlainTextSaslAuthenticator implements SaslNegotiator
{
return new PlainTextSaslAuthenticator();
}
// if there are no users yet - add default superuser.
private void setupDefaultUser()
{
try
{
// insert the default superuser if AUTH_KS.CREDENTIALS_CF is empty.
if (!hasExistingUsers())
{
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))),
ConsistencyLevel.ONE);
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 boolean hasExistingUsers() throws RequestExecutionException
{
// Try looking up the 'cassandra' default user first, to avoid the range query if possible.
String defaultSUQuery = String.format("SELECT * FROM %s.%s WHERE username = '%s'", Auth.AUTH_KS, CREDENTIALS_CF, DEFAULT_USER_NAME);
String allUsersQuery = String.format("SELECT * FROM %s.%s LIMIT 1", Auth.AUTH_KS, CREDENTIALS_CF);
return !process(defaultSUQuery, ConsistencyLevel.ONE).isEmpty()
|| !process(defaultSUQuery, ConsistencyLevel.QUORUM).isEmpty()
|| !process(allUsersQuery, ConsistencyLevel.QUORUM).isEmpty();
}
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, ConsistencyLevel cl) throws RequestExecutionException
{
return QueryProcessor.process(query, cl);
}
private static ConsistencyLevel consistencyForUser(String username)
{
if (username.equals(DEFAULT_USER_NAME))
return ConsistencyLevel.QUORUM;
else
return ConsistencyLevel.LOCAL_ONE;
}
private class PlainTextSaslAuthenticator implements ISaslAwareAuthenticator.SaslAuthenticator
{
private static final byte NUL = 0;
private boolean complete = false;
private Map<String, String> credentials;
private String username;
private String password;
@Override
public byte[] evaluateResponse(byte[] clientResponse) throws AuthenticationException
{
credentials = decodeCredentials(clientResponse);
decodeCredentials(clientResponse);
complete = true;
return null;
}
@Override
public boolean isComplete()
{
return complete;
}
@Override
public AuthenticatedUser getAuthenticatedUser() throws AuthenticationException
{
return authenticate(credentials);
if (!complete)
throw new AuthenticationException("SASL negotiation not complete");
return authenticate(username, password);
}
/**
@ -285,14 +202,14 @@ public class PasswordAuthenticator implements ISaslAwareAuthenticator
* The form is : {code}authzId<NUL>authnId<NUL>password<NUL>{code}
* authzId is optional, and in fact we don't care about it here as we'll
* set the authzId to match the authnId (that is, there is no concept of
* a user being authorized to act on behalf of another).
* a user being authorized to act on behalf of another with this IAuthenticator).
*
* @param bytes encoded credentials string sent by the client
* @return map containing the username/password pairs in the form an IAuthenticator
* would expect
* @throws javax.security.sasl.SaslException
*/
private Map<String, String> decodeCredentials(byte[] bytes) throws AuthenticationException
private void decodeCredentials(byte[] bytes) throws AuthenticationException
{
logger.debug("Decoding credentials from client token");
byte[] user = null;
@ -315,10 +232,8 @@ public class PasswordAuthenticator implements ISaslAwareAuthenticator
if (pass == null)
throw new AuthenticationException("Password must not be null");
Map<String, String> credentials = new HashMap<String, String>();
credentials.put(IAuthenticator.USERNAME_KEY, new String(user, StandardCharsets.UTF_8));
credentials.put(IAuthenticator.PASSWORD_KEY, new String(pass, StandardCharsets.UTF_8));
return credentials;
username = new String(user, StandardCharsets.UTF_8);
password = new String(pass, StandardCharsets.UTF_8);
}
}
}

View File

@ -26,13 +26,13 @@ import com.google.common.collect.ComparisonChain;
*/
public class PermissionDetails implements Comparable<PermissionDetails>
{
public final String username;
public final String grantee;
public final IResource resource;
public final Permission permission;
public PermissionDetails(String username, IResource resource, Permission permission)
public PermissionDetails(String grantee, IResource resource, Permission permission)
{
this.username = username;
this.grantee = grantee;
this.resource = resource;
this.permission = permission;
}
@ -41,7 +41,7 @@ public class PermissionDetails implements Comparable<PermissionDetails>
public int compareTo(PermissionDetails other)
{
return ComparisonChain.start()
.compare(username, other.username)
.compare(grantee, other.grantee)
.compare(resource.getName(), other.resource.getName())
.compare(permission, other.permission)
.result();
@ -50,8 +50,8 @@ public class PermissionDetails implements Comparable<PermissionDetails>
@Override
public String toString()
{
return String.format("<PermissionDetails username:%s resource:%s permission:%s>",
username,
return String.format("<PermissionDetails grantee:%s resource:%s permission:%s>",
grantee,
resource.getName(),
permission);
}
@ -66,7 +66,7 @@ public class PermissionDetails implements Comparable<PermissionDetails>
return false;
PermissionDetails pd = (PermissionDetails) o;
return Objects.equal(username, pd.username)
return Objects.equal(grantee, pd.grantee)
&& Objects.equal(resource, pd.resource)
&& Objects.equal(permission, pd.permission);
}
@ -74,6 +74,6 @@ public class PermissionDetails implements Comparable<PermissionDetails>
@Override
public int hashCode()
{
return Objects.hashCode(username, resource, permission);
return Objects.hashCode(grantee, resource, permission);
}
}

View File

@ -19,20 +19,19 @@ package org.apache.cassandra.config;
import java.io.IOException;
import java.io.StringReader;
import java.sql.Time;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.google.common.collect.Sets;
import org.supercsv.io.CsvListReader;
import org.supercsv.prefs.CsvPreference;
import org.apache.cassandra.config.EncryptionOptions.ClientEncryptionOptions;
import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.io.util.NativeAllocator;
import org.apache.cassandra.utils.FBUtilities;
import org.supercsv.io.CsvListReader;
import org.supercsv.prefs.CsvPreference;
/**
* A class that contains configuration properties for the cassandra node it runs within.
@ -44,9 +43,11 @@ public class Config
public String cluster_name = "Test Cluster";
public String authenticator;
public String authorizer;
public String role_manager;
public int permissions_validity_in_ms = 2000;
public int permissions_cache_max_entries = 1000;
public int permissions_update_interval_in_ms = -1;
public int roles_validity_in_ms = 2000;
/* Hashing strategy Random or OPHF */
public String partitioner;

View File

@ -81,6 +81,7 @@ public class DatabaseDescriptor
private static IAuthenticator authenticator = new AllowAllAuthenticator();
private static IAuthorizer authorizer = new AllowAllAuthorizer();
private static IRoleManager roleManager = new CassandraRoleManager();
private static IRequestScheduler requestScheduler;
private static RequestSchedulerId requestSchedulerId;
@ -184,7 +185,7 @@ public class DatabaseDescriptor
}
}
/* Authentication and authorization backend, implementing IAuthenticator and IAuthorizer */
/* Authentication, authorization and role management backend, implementing IAuthenticator, IAuthorizer & IRoleMapper*/
if (conf.authenticator != null)
authenticator = FBUtilities.newAuthenticator(conf.authenticator);
@ -194,6 +195,12 @@ public class DatabaseDescriptor
if (authenticator instanceof AllowAllAuthenticator && !(authorizer instanceof AllowAllAuthorizer))
throw new ConfigurationException("AllowAllAuthenticator can't be used with " + conf.authorizer);
if (conf.role_manager != null)
roleManager = FBUtilities.newRoleManager(conf.role_manager);
if (authenticator instanceof PasswordAuthenticator && !(roleManager instanceof CassandraRoleManager))
throw new ConfigurationException("CassandraRoleManager must be used with PasswordAuthenticator");
if (conf.internode_authenticator != null)
internodeAuthenticator = FBUtilities.construct(conf.internode_authenticator, "internode_authenticator");
else
@ -201,6 +208,7 @@ public class DatabaseDescriptor
authenticator.validateConfiguration();
authorizer.validateConfiguration();
roleManager.validateConfiguration();
internodeAuthenticator.validateConfiguration();
/* Hashing strategy */
@ -604,6 +612,11 @@ public class DatabaseDescriptor
return authorizer;
}
public static IRoleManager getRoleManager()
{
return roleManager;
}
public static int getPermissionsValidity()
{
return conf.permissions_validity_in_ms;
@ -621,6 +634,11 @@ public class DatabaseDescriptor
: conf.permissions_update_interval_in_ms;
}
public static int getRolesValidity()
{
return conf.roles_validity_in_ms;
}
public static int getThriftFramedTransportSize()
{
return conf.thrift_framed_transport_size_in_mb * 1024 * 1024;

View File

@ -40,6 +40,7 @@ options {
import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.auth.DataResource;
import org.apache.cassandra.auth.IResource;
import org.apache.cassandra.auth.IRoleManager;
import org.apache.cassandra.cql3.*;
import org.apache.cassandra.cql3.statements.*;
import org.apache.cassandra.cql3.selection.*;
@ -247,6 +248,12 @@ cqlStatement returns [ParsedStatement stmt]
| st29=dropFunctionStatement { $stmt = st29; }
| st30=createAggregateStatement { $stmt = st30; }
| st31=dropAggregateStatement { $stmt = st31; }
| st32=createRoleStatement { $stmt = st32; }
| st33=alterRoleStatement { $stmt = st33; }
| st34=dropRoleStatement { $stmt = st34; }
| st35=listRolesStatement { $stmt = st35; }
| st36=grantRoleStatement { $stmt = st36; }
| st37=revokeRoleStatement { $stmt = st37; }
;
/*
@ -802,7 +809,7 @@ truncateStatement returns [TruncateStatement stmt]
;
/**
* GRANT <permission> ON <resource> TO <username>
* GRANT <permission> ON <resource> TO <rolename>
*/
grantStatement returns [GrantStatement stmt]
: K_GRANT
@ -810,12 +817,12 @@ grantStatement returns [GrantStatement stmt]
K_ON
resource
K_TO
username
{ $stmt = new GrantStatement($permissionOrAll.perms, (DataResource) $resource.res, $username.text); }
grantee=userOrRoleName
{ $stmt = new GrantStatement($permissionOrAll.perms, (DataResource) $resource.res, grantee); }
;
/**
* REVOKE <permission> ON <resource> FROM <username>
* REVOKE <permission> ON <resource> FROM <rolename>
*/
revokeStatement returns [RevokeStatement stmt]
: K_REVOKE
@ -823,22 +830,44 @@ revokeStatement returns [RevokeStatement stmt]
K_ON
resource
K_FROM
username
{ $stmt = new RevokeStatement($permissionOrAll.perms, (DataResource) $resource.res, $username.text); }
revokee=userOrRoleName
{ $stmt = new RevokeStatement($permissionOrAll.perms, (DataResource) $resource.res, revokee); }
;
/**
* GRANT ROLE <rolename> TO <grantee>
*/
grantRoleStatement returns [GrantRoleStatement stmt]
: K_GRANT
role=userOrRoleName
K_TO
grantee=userOrRoleName
{ $stmt = new GrantRoleStatement(role, grantee); }
;
/**
* REVOKE ROLE <rolename> FROM <revokee>
*/
revokeRoleStatement returns [RevokeRoleStatement stmt]
: K_REVOKE
role=userOrRoleName
K_FROM
revokee=userOrRoleName
{ $stmt = new RevokeRoleStatement(role, revokee); }
;
listPermissionsStatement returns [ListPermissionsStatement stmt]
@init {
IResource resource = null;
String username = null;
boolean recursive = true;
RoleName grantee = new RoleName();
}
: K_LIST
permissionOrAll
( K_ON resource { resource = $resource.res; } )?
( K_OF username { username = $username.text; } )?
( K_OF roleName[grantee] )?
( K_NORECURSIVE { recursive = false; } )?
{ $stmt = new ListPermissionsStatement($permissionOrAll.perms, (DataResource) resource, username, recursive); }
{ $stmt = new ListPermissionsStatement($permissionOrAll.perms, (DataResource) resource, grantee, recursive); }
;
permission returns [Permission perm]
@ -859,59 +888,127 @@ dataResource returns [DataResource res]
: K_ALL K_KEYSPACES { $res = DataResource.root(); }
| K_KEYSPACE ks = keyspaceName { $res = DataResource.keyspace($ks.id); }
| ( K_COLUMNFAMILY )? cf = columnFamilyName
{ $res = DataResource.columnFamily($cf.name.getKeyspace(), $cf.name.getColumnFamily()); }
{ $res = DataResource.table($cf.name.getKeyspace(), $cf.name.getColumnFamily()); }
;
/**
* CREATE USER [IF NOT EXISTS] <username> [WITH PASSWORD <password>] [SUPERUSER|NOSUPERUSER]
*/
createUserStatement returns [CreateUserStatement stmt]
createUserStatement returns [CreateRoleStatement stmt]
@init {
UserOptions opts = new UserOptions();
RoleOptions opts = new RoleOptions();
opts.put(IRoleManager.Option.LOGIN.name(), true);
boolean superuser = false;
boolean ifNotExists = false;
RoleName name = new RoleName();
}
: K_CREATE K_USER (K_IF K_NOT K_EXISTS { ifNotExists = true; })? username
( K_WITH userOptions[opts] )?
: K_CREATE K_USER (K_IF K_NOT K_EXISTS { ifNotExists = true; })? u=username { name.setName($u.text, false); }
( K_WITH roleOptions[opts] )?
( K_SUPERUSER { superuser = true; } | K_NOSUPERUSER { superuser = false; } )?
{ $stmt = new CreateUserStatement($username.text, opts, superuser, ifNotExists); }
{ opts.put(IRoleManager.Option.SUPERUSER.name(), superuser);
$stmt = new CreateRoleStatement(name, opts, ifNotExists); }
;
/**
* ALTER USER <username> [WITH PASSWORD <password>] [SUPERUSER|NOSUPERUSER]
*/
alterUserStatement returns [AlterUserStatement stmt]
alterUserStatement returns [AlterRoleStatement stmt]
@init {
UserOptions opts = new UserOptions();
Boolean superuser = null;
RoleOptions opts = new RoleOptions();
RoleName name = new RoleName();
}
: K_ALTER K_USER username
( K_WITH userOptions[opts] )?
( K_SUPERUSER { superuser = true; } | K_NOSUPERUSER { superuser = false; } )?
{ $stmt = new AlterUserStatement($username.text, opts, superuser); }
: K_ALTER K_USER u=username { name.setName($u.text, false); }
( K_WITH roleOptions[opts] )?
( K_SUPERUSER { opts.put(IRoleManager.Option.SUPERUSER.name(), true); }
| K_NOSUPERUSER { opts.put(IRoleManager.Option.SUPERUSER.name(), false); } ) ?
{ $stmt = new AlterRoleStatement(name, opts); }
;
/**
* DROP USER [IF EXISTS] <username>
*/
dropUserStatement returns [DropUserStatement stmt]
@init { boolean ifExists = false; }
: K_DROP K_USER (K_IF K_EXISTS { ifExists = true; })? username { $stmt = new DropUserStatement($username.text, ifExists); }
dropUserStatement returns [DropRoleStatement stmt]
@init {
boolean ifExists = false;
RoleName name = new RoleName();
}
: K_DROP K_USER (K_IF K_EXISTS { ifExists = true; })? u=username { name.setName($u.text, false); $stmt = new DropRoleStatement(name, ifExists); }
;
/**
* LIST USERS
*/
listUsersStatement returns [ListUsersStatement stmt]
listUsersStatement returns [ListRolesStatement stmt]
: K_LIST K_USERS { $stmt = new ListUsersStatement(); }
;
userOptions[UserOptions opts]
: userOption[opts]
/**
* CREATE ROLE [IF NOT EXISTS] <rolename> [WITH PASSWORD <password>] [SUPERUSER|NOSUPERUSER] [LOGIN|NOLOGIN]
*/
createRoleStatement returns [CreateRoleStatement stmt]
@init {
RoleOptions opts = new RoleOptions();
boolean superuser = false;
boolean login = false;
boolean ifNotExists = false;
}
: K_CREATE K_ROLE (K_IF K_NOT K_EXISTS { ifNotExists = true; })? name=userOrRoleName
( K_WITH roleOptions[opts] )?
( K_SUPERUSER { superuser = true; } | K_NOSUPERUSER { superuser = false; } )?
( K_LOGIN { login = true; } | K_NOLOGIN { login = false; } )?
{ opts.put(IRoleManager.Option.SUPERUSER.name(), superuser);
opts.put(IRoleManager.Option.LOGIN.name(), login);
$stmt = new CreateRoleStatement(name, opts, ifNotExists); }
;
userOption[UserOptions opts]
: k=K_PASSWORD v=STRING_LITERAL { opts.put($k.text, $v.text); }
/**
* ALTER ROLE <rolename> [WITH PASSWORD <password>] [SUPERUSER|NOSUPERUSER]
*/
alterRoleStatement returns [AlterRoleStatement stmt]
@init {
RoleOptions opts = new RoleOptions();
}
: K_ALTER K_ROLE name=userOrRoleName
( K_WITH roleOptions[opts] )?
( K_SUPERUSER { opts.put(IRoleManager.Option.SUPERUSER.name(), true); }
| K_NOSUPERUSER { opts.put(IRoleManager.Option.SUPERUSER.name(), false); } ) ?
( K_LOGIN { opts.put(IRoleManager.Option.LOGIN.name(), true); }
| K_NOLOGIN { opts.put(IRoleManager.Option.LOGIN.name(), false); } )?
{ $stmt = new AlterRoleStatement(name, opts); }
;
/**
* DROP ROLE [IF EXISTS] <rolename>
*/
dropRoleStatement returns [DropRoleStatement stmt]
@init {
boolean ifExists = false;
}
: K_DROP K_ROLE (K_IF K_EXISTS { ifExists = true; })? name=userOrRoleName
{ $stmt = new DropRoleStatement(name, ifExists); }
;
/**
* LIST ROLES [OF <rolename>] [NORECURSIVE]
*/
listRolesStatement returns [ListRolesStatement stmt]
@init {
boolean recursive = true;
RoleName grantee = new RoleName();
}
: K_LIST K_ROLES
( K_OF roleName[grantee])?
( K_NORECURSIVE { recursive = false; } )?
{ $stmt = new ListRolesStatement(grantee, recursive); }
;
roleOptions[RoleOptions opts]
: roleOption[opts] (K_AND roleOption[opts])*
;
roleOption[RoleOptions opts]
: k=K_PASSWORD v=STRING_LITERAL { opts.put($k.text, $v.text); }
| k=K_OPTIONS m=mapLiteral { opts.put(IRoleManager.Option.OPTIONS.name(), convertPropertyMap(m)); }
;
/** DEFINITIONS **/
@ -952,6 +1049,11 @@ userTypeName returns [UTName name]
: (ks=ident '.')? ut=non_type_ident { return new UTName(ks, ut); }
;
userOrRoleName returns [RoleName name]
@init { $name = new RoleName(); }
: roleName[name] {return $name;}
;
ksName[KeyspaceElementName name]
: t=IDENT { $name.setKeyspace($t.text, false);}
| t=QUOTED_NAME { $name.setKeyspace($t.text, true);}
@ -973,6 +1075,13 @@ idxName[IndexName name]
| QMARK {addRecognitionError("Bind variables cannot be used for index names");}
;
roleName[RoleName name]
: t=IDENT { $name.setName($t.text, false); }
| t=QUOTED_NAME { $name.setName($t.text, true); }
| k=unreserved_keyword { $name.setName(k, false); }
| QMARK {addRecognitionError("Bind variables cannot be used for role names");}
;
constant returns [Constants.Literal constant]
: t=STRING_LITERAL { $constant = Constants.Literal.string($t.text); }
| t=INTEGER { $constant = Constants.Literal.integer($t.text); }
@ -1326,8 +1435,13 @@ basic_unreserved_keyword returns [String str]
| K_ALL
| K_USER
| K_USERS
| K_ROLE
| K_ROLES
| K_SUPERUSER
| K_NOSUPERUSER
| K_LOGIN
| K_NOLOGIN
| K_OPTIONS
| K_PASSWORD
| K_EXISTS
| K_CUSTOM
@ -1417,9 +1531,14 @@ K_NORECURSIVE: N O R E C U R S I V E;
K_USER: U S E R;
K_USERS: U S E R S;
K_ROLE: R O L E;
K_ROLES: R O L E S;
K_SUPERUSER: S U P E R U S E R;
K_NOSUPERUSER: N O S U P E R U S E R;
K_PASSWORD: P A S S W O R D;
K_LOGIN: L O G I N;
K_NOLOGIN: N O L O G I N;
K_OPTIONS: O P T I O N S;
K_CLUSTERING: C L U S T E R I N G;
K_ASCII: A S C I I;

View File

@ -0,0 +1,41 @@
/*
* 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.cql3;
import java.util.Locale;
public class RoleName
{
private String name;
public void setName(String name, boolean keepCase)
{
this.name = keepCase ? name : name.toLowerCase(Locale.US);
}
public String getName()
{
return name;
}
@Override
public String toString()
{
return name;
}
}

View File

@ -20,18 +20,18 @@ package org.apache.cassandra.cql3;
import java.util.HashMap;
import java.util.Map;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.auth.IRoleManager;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.utils.FBUtilities;
public class UserOptions
public class RoleOptions
{
private final Map<IAuthenticator.Option, Object> options = new HashMap<IAuthenticator.Option, Object>();
private final Map<IRoleManager.Option, Object> options = new HashMap<>();
public void put(String name, Object value)
{
options.put(IAuthenticator.Option.valueOf(name.toUpperCase()), value);
options.put(IRoleManager.Option.valueOf(name.toUpperCase()), value);
}
public boolean isEmpty()
@ -39,18 +39,18 @@ public class UserOptions
return options.isEmpty();
}
public Map<IAuthenticator.Option, Object> getOptions()
public Map<IRoleManager.Option, Object> getOptions()
{
return options;
}
public void validate() throws InvalidRequestException
{
for (IAuthenticator.Option option : options.keySet())
for (IRoleManager.Option option : options.keySet())
{
if (!DatabaseDescriptor.getAuthenticator().supportedOptions().contains(option))
throw new InvalidRequestException(String.format("%s doesn't support %s option",
DatabaseDescriptor.getAuthenticator().getClass().getName(),
if (!DatabaseDescriptor.getRoleManager().supportedOptions().contains(option))
throw new InvalidRequestException(String.format("%s doesn't support %s",
DatabaseDescriptor.getRoleManager().getClass().getName(),
option));
}
}

View File

@ -17,66 +17,60 @@
*/
package org.apache.cassandra.cql3.statements;
import org.apache.cassandra.auth.Auth;
import org.apache.cassandra.auth.AuthenticatedUser;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.auth.IRoleManager.Option;
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.cql3.RoleName;
import org.apache.cassandra.cql3.RoleOptions;
import org.apache.cassandra.exceptions.*;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.transport.messages.ResultMessage;
public class AlterUserStatement extends AuthenticationStatement
public class AlterRoleStatement extends AuthenticationStatement
{
private final String username;
private final UserOptions opts;
private final Boolean superuser;
private final String role;
private final RoleOptions opts;
public AlterUserStatement(String username, UserOptions opts, Boolean superuser)
public AlterRoleStatement(RoleName name, RoleOptions opts)
{
this.username = username;
this.role = name.getName();
this.opts = opts;
this.superuser = superuser;
}
public void validate(ClientState state) throws RequestValidationException
{
opts.validate();
if (superuser == null && opts.isEmpty())
throw new InvalidRequestException("ALTER USER can't be empty");
if (opts.isEmpty())
throw new InvalidRequestException("ALTER [ROLE|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));
if (!DatabaseDescriptor.getRoleManager().isExistingRole(role))
throw new InvalidRequestException(String.format("%s doesn't exist", role));
}
public void checkAccess(ClientState state) throws UnauthorizedException
{
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 (opts.getOptions().containsKey(Option.SUPERUSER) && user.getRoles().contains(role))
throw new UnauthorizedException("You aren't allowed to alter your own superuser " +
"status or that of a role granted to you");
if (superuser != null && !isSuper)
if (opts.getOptions().containsKey(Option.SUPERUSER) && !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() && !user.getName().equals(role))
throw new UnauthorizedException("You aren't allowed to alter this role");
if (!isSuper)
{
for (IAuthenticator.Option option : opts.getOptions().keySet())
for (Option option : opts.getOptions().keySet())
{
if (!DatabaseDescriptor.getAuthenticator().alterableOptions().contains(option))
throw new UnauthorizedException(String.format("You aren't allowed to alter %s option", option));
if (!DatabaseDescriptor.getRoleManager().alterableOptions().contains(option))
throw new UnauthorizedException(String.format("You aren't allowed to alter %s", option));
}
}
}
@ -84,9 +78,7 @@ public class AlterUserStatement extends AuthenticationStatement
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
if (!opts.isEmpty())
DatabaseDescriptor.getAuthenticator().alter(username, opts.getOptions());
if (superuser != null)
Auth.insertUser(username, superuser.booleanValue());
DatabaseDescriptor.getRoleManager().alterRole(state.getUser(), role, opts.getOptions());
return null;
}
}

View File

@ -55,8 +55,8 @@ public abstract class AuthorizationStatement extends ParsedStatement implements
public static DataResource maybeCorrectResource(DataResource resource, ClientState state) throws InvalidRequestException
{
if (resource.isColumnFamilyLevel() && resource.getKeyspace() == null)
return DataResource.columnFamily(state.getKeyspace(), resource.getColumnFamily());
if (resource.isTableLevel() && resource.getKeyspace() == null)
return DataResource.table(state.getKeyspace(), resource.getTable());
return resource;
}
}

View File

@ -17,59 +17,60 @@
*/
package org.apache.cassandra.cql3.statements;
import org.apache.cassandra.auth.Auth;
import org.apache.cassandra.auth.IRoleManager.Option;
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.cql3.RoleName;
import org.apache.cassandra.cql3.RoleOptions;
import org.apache.cassandra.exceptions.*;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.transport.messages.ResultMessage;
public class CreateUserStatement extends AuthenticationStatement
public class CreateRoleStatement extends AuthorizationStatement
{
private final String username;
private final UserOptions opts;
private final boolean superuser;
private final String role;
private final RoleOptions opts;
private final boolean ifNotExists;
public CreateUserStatement(String username, UserOptions opts, boolean superuser, boolean ifNotExists)
public CreateRoleStatement(RoleName name, RoleOptions options, boolean ifNotExists)
{
this.username = username;
this.opts = opts;
this.superuser = superuser;
this.role = name.getName();
this.opts = options;
this.ifNotExists = ifNotExists;
}
public void checkAccess(ClientState state) throws UnauthorizedException, InvalidRequestException
{
if (!state.getUser().isSuper())
throw new UnauthorizedException("Only superusers are allowed to perform CREATE [ROLE|USER] queries");
}
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.
if (role.isEmpty())
throw new InvalidRequestException("Role name can't be an empty string");
// validate login here before checkAccess to avoid leaking role existence to anonymous users.
state.ensureNotAnonymous();
if (!ifNotExists && Auth.isExistingUser(username))
throw new InvalidRequestException(String.format("User %s already exists", username));
if (!ifNotExists && DatabaseDescriptor.getRoleManager().isExistingRole(role))
throw new InvalidRequestException(String.format("%s already exists", role));
for (Option option : opts.getOptions().keySet())
{
if (!DatabaseDescriptor.getRoleManager().supportedOptions().contains(option))
throw new UnauthorizedException(String.format("You aren't allowed to alter %s", option));
}
}
public void checkAccess(ClientState state) throws UnauthorizedException
{
if (!state.getUser().isSuper())
throw new UnauthorizedException("Only superusers are allowed to perform CREATE USER queries");
}
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
public ResultMessage execute(ClientState state) throws RequestExecutionException, RequestValidationException
{
// not rejected in validate()
if (ifNotExists && Auth.isExistingUser(username))
if (ifNotExists && DatabaseDescriptor.getRoleManager().isExistingRole(role))
return null;
DatabaseDescriptor.getAuthenticator().create(username, opts.getOptions());
Auth.insertUser(username, superuser);
DatabaseDescriptor.getRoleManager().createRole(state.getUser(), role, opts.getOptions());
return null;
}
}

View File

@ -17,24 +17,21 @@
*/
package org.apache.cassandra.cql3.statements;
import org.apache.cassandra.auth.Auth;
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.cql3.RoleName;
import org.apache.cassandra.exceptions.*;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.transport.messages.ResultMessage;
public class DropUserStatement extends AuthenticationStatement
public class DropRoleStatement extends AuthenticationStatement
{
private final String username;
private final String role;
private final boolean ifExists;
public DropUserStatement(String username, boolean ifExists)
public DropRoleStatement(RoleName name, boolean ifExists)
{
this.username = username;
this.role = name.getName();
this.ifExists = ifExists;
}
@ -43,30 +40,29 @@ public class DropUserStatement extends AuthenticationStatement
// validate login here before checkAccess to avoid leaking user existence to anonymous users.
state.ensureNotAnonymous();
if (!ifExists && !Auth.isExistingUser(username))
throw new InvalidRequestException(String.format("User %s doesn't exist", username));
if (!ifExists && !DatabaseDescriptor.getRoleManager().isExistingRole(role))
throw new InvalidRequestException(String.format("%s doesn't exist", role));
AuthenticatedUser user = state.getUser();
if (user != null && user.getName().equals(username))
throw new InvalidRequestException("Users aren't allowed to DROP themselves");
if (user != null && user.getName().equals(role))
throw new InvalidRequestException("Cannot DROP primary role for current login");
}
public void checkAccess(ClientState state) throws UnauthorizedException
{
if (!state.getUser().isSuper())
throw new UnauthorizedException("Only superusers are allowed to perform DROP USER queries");
throw new UnauthorizedException("Only superusers are allowed to perform DROP [ROLE|USER] queries" );
}
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
// not rejected in validate()
if (ifExists && !Auth.isExistingUser(username))
if (ifExists && !DatabaseDescriptor.getRoleManager().isExistingRole(role))
return null;
// clean up permissions after the dropped user.
DatabaseDescriptor.getAuthorizer().revokeAll(username);
Auth.deleteUser(username);
DatabaseDescriptor.getAuthenticator().drop(username);
// clean up grants and permissions of the dropped role.
DatabaseDescriptor.getRoleManager().dropRole(state.getUser(), role);
DatabaseDescriptor.getAuthorizer().revokeAll(role);
return null;
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.cql3.statements;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.RoleName;
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;
public class GrantRoleStatement extends RoleManagementStatement
{
public GrantRoleStatement(RoleName name, RoleName grantee)
{
super(name, grantee);
}
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
DatabaseDescriptor.getRoleManager().grantRole(state.getUser(), role, grantee);
return null;
}
}

View File

@ -1,4 +1,4 @@
/**
/*
* 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
@ -7,14 +7,13 @@
* "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
* 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.
* 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.cql3.statements;
@ -23,6 +22,7 @@ import java.util.Set;
import org.apache.cassandra.auth.DataResource;
import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.RoleName;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.service.ClientState;
@ -30,14 +30,14 @@ import org.apache.cassandra.transport.messages.ResultMessage;
public class GrantStatement extends PermissionAlteringStatement
{
public GrantStatement(Set<Permission> permissions, DataResource resource, String username)
public GrantStatement(Set<Permission> permissions, DataResource resource, RoleName grantee)
{
super(permissions, resource, username);
super(permissions, resource, grantee);
}
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
DatabaseDescriptor.getAuthorizer().grant(state.getUser(), permissions, resource, username);
DatabaseDescriptor.getAuthorizer().grant(state.getUser(), permissions, resource, grantee);
return null;
}
}

View File

@ -21,9 +21,7 @@ import java.util.*;
import org.apache.cassandra.auth.*;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.ResultSet;
import org.apache.cassandra.cql3.*;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.RequestExecutionException;
@ -33,7 +31,7 @@ import org.apache.cassandra.transport.messages.ResultMessage;
public class ListPermissionsStatement extends AuthorizationStatement
{
private static final String KS = Auth.AUTH_KS;
private static final String KS = AuthKeyspace.NAME;
private static final String CF = "permissions"; // virtual cf to use for now.
private static final List<ColumnSpecification> metadata;
@ -41,23 +39,24 @@ public class ListPermissionsStatement extends AuthorizationStatement
static
{
List<ColumnSpecification> columns = new ArrayList<ColumnSpecification>(4);
columns.add(new ColumnSpecification(KS, CF, new ColumnIdentifier("role", true), UTF8Type.instance));
columns.add(new ColumnSpecification(KS, CF, new ColumnIdentifier("username", true), UTF8Type.instance));
columns.add(new ColumnSpecification(KS, CF, new ColumnIdentifier("resource", true), UTF8Type.instance));
columns.add(new ColumnSpecification(KS, CF, new ColumnIdentifier("permission", true), UTF8Type.instance));
metadata = Collections.unmodifiableList(columns);
}
private final Set<Permission> permissions;
private DataResource resource;
private final String username;
private final boolean recursive;
protected final Set<Permission> permissions;
protected DataResource resource;
protected final boolean recursive;
private final String grantee;
public ListPermissionsStatement(Set<Permission> permissions, DataResource resource, String username, boolean recursive)
public ListPermissionsStatement(Set<Permission> permissions, DataResource resource, RoleName grantee, boolean recursive)
{
this.permissions = permissions;
this.resource = resource;
this.username = username;
this.recursive = recursive;
this.grantee = grantee.getName();
}
public void validate(ClientState state) throws RequestValidationException
@ -65,16 +64,16 @@ public class ListPermissionsStatement extends AuthorizationStatement
// 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));
if (resource != null)
{
resource = maybeCorrectResource(resource, state);
if (!resource.exists())
throw new InvalidRequestException(String.format("%s doesn't exist", resource));
}
}
if ((grantee != null) && !DatabaseDescriptor.getRoleManager().isExistingRole(grantee))
throw new InvalidRequestException(String.format("%s doesn't exist", grantee));
}
public void checkAccess(ClientState state)
{
@ -100,6 +99,12 @@ public class ListPermissionsStatement extends AuthorizationStatement
return resultMessage(details);
}
private Set<PermissionDetails> list(ClientState state, IResource resource)
throws RequestValidationException, RequestExecutionException
{
return DatabaseDescriptor.getAuthorizer().list(state.getUser(), permissions, resource, grantee);
}
private ResultMessage resultMessage(List<PermissionDetails> details)
{
if (details.isEmpty())
@ -108,16 +113,11 @@ public class ListPermissionsStatement extends AuthorizationStatement
ResultSet result = new ResultSet(metadata);
for (PermissionDetails pd : details)
{
result.addColumnValue(UTF8Type.instance.decompose(pd.username));
result.addColumnValue(UTF8Type.instance.decompose(pd.grantee));
result.addColumnValue(UTF8Type.instance.decompose(pd.grantee));
result.addColumnValue(UTF8Type.instance.decompose(pd.resource.toString()));
result.addColumnValue(UTF8Type.instance.decompose(pd.permission.toString()));
}
return new ResultMessage.Rows(result);
}
private Set<PermissionDetails> list(ClientState state, IResource resource)
throws RequestValidationException, RequestExecutionException
{
return DatabaseDescriptor.getAuthorizer().list(state.getUser(), permissions, resource, username);
}
}

View File

@ -0,0 +1,118 @@
/*
* 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.cql3.statements;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.apache.cassandra.auth.AuthKeyspace;
import org.apache.cassandra.auth.IRoleManager;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.*;
import org.apache.cassandra.db.marshal.BooleanType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.*;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.transport.messages.ResultMessage;
public class ListRolesStatement extends AuthorizationStatement
{
// pseudo-virtual cf as the actual datasource is dependent on the IRoleManager impl
private static final String KS = AuthKeyspace.NAME;
private static final String CF = AuthKeyspace.ROLES;
private static final List<ColumnSpecification> metadata =
ImmutableList.of(new ColumnSpecification(KS, CF, new ColumnIdentifier("role", true), UTF8Type.instance),
new ColumnSpecification(KS, CF, new ColumnIdentifier("super", true), BooleanType.instance),
new ColumnSpecification(KS, CF, new ColumnIdentifier("login", true), BooleanType.instance));
private final String grantee;
private final boolean recursive;
public ListRolesStatement()
{
this(new RoleName(), false);
}
public ListRolesStatement(RoleName grantee, boolean recursive)
{
this.grantee = grantee.getName();
this.recursive = recursive;
}
public void validate(ClientState state) throws UnauthorizedException, InvalidRequestException
{
state.ensureNotAnonymous();
if ((grantee != null) && !DatabaseDescriptor.getRoleManager().isExistingRole(grantee))
throw new InvalidRequestException(String.format("%s doesn't exist", grantee));
}
public void checkAccess(ClientState state) throws InvalidRequestException
{
}
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
if (state.getUser().isSuper())
{
if (grantee == null)
return resultMessage(DatabaseDescriptor.getRoleManager().getAllRoles());
else
return resultMessage(DatabaseDescriptor.getRoleManager().getRoles(grantee, recursive));
}
else
{
if (grantee == null)
return resultMessage(DatabaseDescriptor.getRoleManager().getRoles(state.getUser().getName(), recursive));
if (DatabaseDescriptor.getRoleManager().getRoles(state.getUser().getName(), true).contains(grantee))
return resultMessage(DatabaseDescriptor.getRoleManager().getRoles(grantee, recursive));
else
throw new UnauthorizedException(String.format("You are not authorized to view roles granted to %s ", grantee));
}
}
private ResultMessage resultMessage(Set<String> roles)
{
if (roles.isEmpty())
return new ResultMessage.Void();
List<String> sorted = Lists.newArrayList(roles);
Collections.sort(sorted);
return formatResults(sorted);
}
// overridden in ListUsersStatement to include legacy metadata
protected ResultMessage formatResults(List<String> sortedRoles)
{
ResultSet result = new ResultSet(metadata);
IRoleManager roleManager = DatabaseDescriptor.getRoleManager();
for (String role : sortedRoles)
{
result.addColumnValue(UTF8Type.instance.decompose(role));
result.addColumnValue(BooleanType.instance.decompose(roleManager.isSuper(role)));
result.addColumnValue(BooleanType.instance.decompose(roleManager.canLogin(role)));
}
return new ResultMessage.Rows(result);
}
}

View File

@ -17,31 +17,43 @@
*/
package org.apache.cassandra.cql3.statements;
import org.apache.cassandra.auth.Auth;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.db.ConsistencyLevel;
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.service.QueryState;
import java.util.List;
import com.google.common.collect.ImmutableList;
import org.apache.cassandra.auth.AuthKeyspace;
import org.apache.cassandra.auth.IRoleManager;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.ResultSet;
import org.apache.cassandra.db.marshal.BooleanType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.transport.messages.ResultMessage;
public class ListUsersStatement extends AuthenticationStatement
public class ListUsersStatement extends ListRolesStatement
{
public void validate(ClientState state)
{
}
// pseudo-virtual cf as the actual datasource is dependent on the IRoleManager impl
private static final String KS = AuthKeyspace.NAME;
private static final String CF = "users";
public void checkAccess(ClientState state) throws UnauthorizedException
{
state.ensureNotAnonymous();
}
private static final List<ColumnSpecification> metadata =
ImmutableList.of(new ColumnSpecification(KS, CF, new ColumnIdentifier("name", true), UTF8Type.instance),
new ColumnSpecification(KS, CF, new ColumnIdentifier("super", true), BooleanType.instance));
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
@Override
protected ResultMessage formatResults(List<String> sortedRoles)
{
return QueryProcessor.process(String.format("SELECT * FROM %s.%s", Auth.AUTH_KS, Auth.USERS_CF),
ConsistencyLevel.QUORUM,
QueryState.forInternalCalls());
ResultSet result = new ResultSet(metadata);
IRoleManager roleManager = DatabaseDescriptor.getRoleManager();
for (String role : sortedRoles)
{
if (!roleManager.canLogin(role))
continue;
result.addColumnValue(UTF8Type.instance.decompose(role));
result.addColumnValue(BooleanType.instance.decompose(roleManager.isSuper(role)));
}
return new ResultMessage.Rows(result);
}
}

View File

@ -19,10 +19,10 @@ package org.apache.cassandra.cql3.statements;
import java.util.Set;
import org.apache.cassandra.auth.Auth;
import org.apache.cassandra.auth.DataResource;
import org.apache.cassandra.auth.IResource;
import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.RoleName;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.exceptions.UnauthorizedException;
@ -32,13 +32,13 @@ public abstract class PermissionAlteringStatement extends AuthorizationStatement
{
protected final Set<Permission> permissions;
protected DataResource resource;
protected final String username;
protected final String grantee;
protected PermissionAlteringStatement(Set<Permission> permissions, DataResource resource, String username)
protected PermissionAlteringStatement(Set<Permission> permissions, DataResource resource, RoleName grantee)
{
this.permissions = permissions;
this.resource = resource;
this.username = username;
this.grantee = grantee.getName();
}
public void validate(ClientState state) throws RequestValidationException
@ -46,13 +46,13 @@ public abstract class PermissionAlteringStatement extends AuthorizationStatement
// 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));
if (!DatabaseDescriptor.getRoleManager().isExistingRole(grantee))
throw new InvalidRequestException(String.format("Role %s doesn't exist", grantee));
// if a keyspace is omitted when GRANT/REVOKE ON TABLE <table>, we need to correct the resource.
resource = maybeCorrectResource(resource, state);
if (!resource.exists())
throw new InvalidRequestException(String.format("%s doesn't exist", resource));
throw new InvalidRequestException(String.format("Resource %s doesn't exist", resource));
}
public void checkAccess(ClientState state) throws UnauthorizedException

View File

@ -0,0 +1,40 @@
/*
* 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.cql3.statements;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.RoleName;
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;
public class RevokeRoleStatement extends RoleManagementStatement
{
public RevokeRoleStatement(RoleName name, RoleName grantee)
{
super(name, grantee);
}
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
DatabaseDescriptor.getRoleManager().revokeRole(state.getUser(), role, grantee);
return null;
}
}

View File

@ -1,4 +1,4 @@
/**
/*
* 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
@ -7,14 +7,13 @@
* "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
* 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.
* 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.cql3.statements;
@ -23,6 +22,7 @@ import java.util.Set;
import org.apache.cassandra.auth.DataResource;
import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.RoleName;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.service.ClientState;
@ -30,14 +30,14 @@ import org.apache.cassandra.transport.messages.ResultMessage;
public class RevokeStatement extends PermissionAlteringStatement
{
public RevokeStatement(Set<Permission> permissions, DataResource resource, String username)
public RevokeStatement(Set<Permission> permissions, DataResource resource, RoleName grantee)
{
super(permissions, resource, username);
super(permissions, resource, grantee);
}
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
DatabaseDescriptor.getAuthorizer().revoke(state.getUser(), permissions, resource, username);
DatabaseDescriptor.getAuthorizer().revoke(state.getUser(), permissions, resource, grantee);
return null;
}
}

View File

@ -0,0 +1,54 @@
/*
* 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.cql3.statements;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.RoleName;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.exceptions.UnauthorizedException;
import org.apache.cassandra.service.ClientState;
public abstract class RoleManagementStatement extends AuthorizationStatement
{
protected final String role;
protected final String grantee;
public RoleManagementStatement(RoleName name, RoleName grantee)
{
this.role = name.getName();
this.grantee = grantee.getName();
}
public void checkAccess(ClientState state) throws UnauthorizedException, InvalidRequestException
{
if (!state.getUser().isSuper())
throw new UnauthorizedException("Only superusers are allowed to perform role management queries");
}
public void validate(ClientState state) throws RequestValidationException
{
state.ensureNotAnonymous();
if (!DatabaseDescriptor.getRoleManager().isExistingRole(role))
throw new InvalidRequestException(String.format("%s doesn't exist", role));
if (!DatabaseDescriptor.getRoleManager().isExistingRole(grantee))
throw new InvalidRequestException(String.format("%s doesn't exist", grantee));
}
}

View File

@ -21,18 +21,13 @@ import java.io.Closeable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.*;
import java.util.concurrent.*;
import org.apache.cassandra.auth.IAuthenticator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.PasswordAuthenticator;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
@ -46,8 +41,6 @@ import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.util.Progressable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class AbstractBulkRecordWriter<K, V> extends RecordWriter<K, V>
implements org.apache.hadoop.mapred.RecordWriter<K, V>
@ -191,8 +184,8 @@ implements org.apache.hadoop.mapred.RecordWriter<K, V>
if (username != null)
{
Map<String, String> creds = new HashMap<String, String>();
creds.put(IAuthenticator.USERNAME_KEY, username);
creds.put(IAuthenticator.PASSWORD_KEY, password);
creds.put(PasswordAuthenticator.USERNAME_KEY, username);
creds.put(PasswordAuthenticator.PASSWORD_KEY, password);
AuthenticationRequest authRequest = new AuthenticationRequest(creds);
client.login(authRequest);
}

View File

@ -20,36 +20,22 @@ package org.apache.cassandra.hadoop;
import java.io.IOException;
import java.net.InetAddress;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.*;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.auth.PasswordAuthenticator;
import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.thrift.AuthenticationRequest;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.CfSplit;
import org.apache.cassandra.thrift.InvalidRequestException;
import org.apache.cassandra.thrift.KeyRange;
import org.apache.cassandra.thrift.TokenRange;
import org.apache.commons.lang3.StringUtils;
import org.apache.cassandra.thrift.*;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.mapred.JobConf;
import org.apache.hadoop.mapreduce.InputFormat;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.TaskAttemptID;
import org.apache.hadoop.mapreduce.*;
import org.apache.thrift.TApplicationException;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
@ -106,8 +92,8 @@ public abstract class AbstractColumnFamilyInputFormat<K, Y> extends InputFormat<
if ((ConfigHelper.getInputKeyspaceUserName(conf) != null) && (ConfigHelper.getInputKeyspacePassword(conf) != null))
{
Map<String, String> creds = new HashMap<String, String>();
creds.put(IAuthenticator.USERNAME_KEY, ConfigHelper.getInputKeyspaceUserName(conf));
creds.put(IAuthenticator.PASSWORD_KEY, ConfigHelper.getInputKeyspacePassword(conf));
creds.put(PasswordAuthenticator.USERNAME_KEY, ConfigHelper.getInputKeyspaceUserName(conf));
creds.put(PasswordAuthenticator.PASSWORD_KEY, ConfigHelper.getInputKeyspacePassword(conf));
AuthenticationRequest authRequest = new AuthenticationRequest(creds);
client.login(authRequest);
}

View File

@ -25,8 +25,9 @@ import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.thrift.*;
import org.apache.cassandra.auth.PasswordAuthenticator;
import org.apache.cassandra.thrift.AuthenticationRequest;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.mapreduce.*;
import org.apache.thrift.protocol.TBinaryProtocol;
@ -134,8 +135,8 @@ public abstract class AbstractColumnFamilyOutputFormat<K, Y> extends OutputForma
public static void login(String user, String password, Cassandra.Client client) throws Exception
{
Map<String, String> creds = new HashMap<String, String>();
creds.put(IAuthenticator.USERNAME_KEY, user);
creds.put(IAuthenticator.PASSWORD_KEY, password);
creds.put(PasswordAuthenticator.USERNAME_KEY, user);
creds.put(PasswordAuthenticator.PASSWORD_KEY, password);
AuthenticationRequest authRequest = new AuthenticationRequest(creds);
client.login(authRequest);
}

View File

@ -25,27 +25,28 @@ import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.util.*;
import org.apache.cassandra.db.Cell;
import org.apache.cassandra.schema.LegacySchemaTables;
import org.apache.cassandra.db.SystemKeyspace;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.SyntaxException;
import org.apache.cassandra.auth.IAuthenticator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.PasswordAuthenticator;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.db.Cell;
import org.apache.cassandra.db.SystemKeyspace;
import org.apache.cassandra.db.marshal.*;
import org.apache.cassandra.db.marshal.AbstractCompositeType.CompositeComponent;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.SyntaxException;
import org.apache.cassandra.hadoop.ConfigHelper;
import org.apache.cassandra.schema.LegacySchemaTables;
import org.apache.cassandra.serializers.CollectionSerializer;
import org.apache.cassandra.hadoop.*;
import org.apache.cassandra.thrift.*;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.Hex;
import org.apache.cassandra.utils.UUIDGen;
import org.apache.cassandra.utils.*;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.mapreduce.*;
import org.apache.hadoop.mapreduce.InputFormat;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.OutputFormat;
import org.apache.pig.*;
import org.apache.pig.backend.executionengine.ExecException;
import org.apache.pig.data.*;
@ -54,8 +55,6 @@ import org.apache.thrift.TDeserializer;
import org.apache.thrift.TException;
import org.apache.thrift.TSerializer;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A LoadStoreFunc for retrieving data from and storing data to Cassandra
@ -505,8 +504,8 @@ public abstract class AbstractCassandraStorage extends LoadFunc implements Store
if (username != null && password != null)
{
Map<String, String> credentials = new HashMap<String, String>(2);
credentials.put(IAuthenticator.USERNAME_KEY, username);
credentials.put(IAuthenticator.PASSWORD_KEY, password);
credentials.put(PasswordAuthenticator.USERNAME_KEY, username);
credentials.put(PasswordAuthenticator.PASSWORD_KEY, password);
try
{

View File

@ -18,11 +18,12 @@
package org.apache.cassandra.service;
import java.net.SocketAddress;
import java.util.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -31,13 +32,13 @@ import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.cql3.QueryHandler;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.schema.LegacySchemaTables;
import org.apache.cassandra.db.SystemKeyspace;
import org.apache.cassandra.exceptions.AuthenticationException;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.UnauthorizedException;
import org.apache.cassandra.tracing.TraceKeyspace;
import org.apache.cassandra.schema.LegacySchemaTables;
import org.apache.cassandra.thrift.ThriftValidation;
import org.apache.cassandra.tracing.TraceKeyspace;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.JVMStabilityInspector;
import org.apache.cassandra.utils.SemanticVersion;
@ -52,16 +53,27 @@ public class ClientState
private static final Set<IResource> READABLE_SYSTEM_RESOURCES = new HashSet<>();
private static final Set<IResource> PROTECTED_AUTH_RESOURCES = new HashSet<>();
private static final Set<String> ALTERABLE_SYSTEM_KEYSPACES = new HashSet<>();
private static final Set<IResource> DROPPABLE_SYSTEM_TABLES = new HashSet<>();
static
{
// We want these system cfs to be always readable to authenticated users since many tools rely on them
// (nodetool, cqlsh, bulkloader, etc.)
for (String cf : Iterables.concat(Arrays.asList(SystemKeyspace.LOCAL, SystemKeyspace.PEERS), LegacySchemaTables.ALL))
READABLE_SYSTEM_RESOURCES.add(DataResource.columnFamily(SystemKeyspace.NAME, cf));
READABLE_SYSTEM_RESOURCES.add(DataResource.table(SystemKeyspace.NAME, cf));
PROTECTED_AUTH_RESOURCES.addAll(DatabaseDescriptor.getAuthenticator().protectedResources());
PROTECTED_AUTH_RESOURCES.addAll(DatabaseDescriptor.getAuthorizer().protectedResources());
PROTECTED_AUTH_RESOURCES.addAll(DatabaseDescriptor.getRoleManager().protectedResources());
// allow users with sufficient privileges to alter KS level options on AUTH_KS and
// TRACING_KS, and also to drop legacy tables (users, credentials, permissions) from
// AUTH_KS
ALTERABLE_SYSTEM_KEYSPACES.add(AuthKeyspace.NAME);
ALTERABLE_SYSTEM_KEYSPACES.add(TraceKeyspace.NAME);
DROPPABLE_SYSTEM_TABLES.add(DataResource.table(AuthKeyspace.NAME, PasswordAuthenticator.LEGACY_CREDENTIALS_TABLE));
DROPPABLE_SYSTEM_TABLES.add(DataResource.table(AuthKeyspace.NAME, CassandraRoleManager.LEGACY_USERS_TABLE));
DROPPABLE_SYSTEM_TABLES.add(DataResource.table(AuthKeyspace.NAME, CassandraAuthorizer.USER_PERMISSIONS));
}
// Current user for the session
@ -200,10 +212,13 @@ public class ClientState
*/
public void login(AuthenticatedUser user) throws AuthenticationException
{
if (!user.isAnonymous() && !Auth.isExistingUser(user.getName()))
throw new AuthenticationException(String.format("User %s doesn't exist - create it with CREATE USER query first",
user.getName()));
this.user = user;
// Login privilege is not inherited via granted roles, so just
// verify that the role with the credentials that were actually
// supplied has it
if (user.isAnonymous() || DatabaseDescriptor.getRoleManager().canLogin(user.getName()))
this.user = user;
else
throw new AuthenticationException(String.format("%s is not permitted to log in", user.getName()));
}
public void hasAllKeyspacesAccess(Permission perm) throws UnauthorizedException
@ -223,7 +238,7 @@ public class ClientState
throws UnauthorizedException, InvalidRequestException
{
ThriftValidation.validateColumnFamily(keyspace, columnFamily);
hasAccess(keyspace, perm, DataResource.columnFamily(keyspace, columnFamily));
hasAccess(keyspace, perm, DataResource.table(keyspace, columnFamily));
}
private void hasAccess(String keyspace, Permission perm, DataResource resource)
@ -264,10 +279,15 @@ public class ClientState
if (SystemKeyspace.NAME.equalsIgnoreCase(keyspace))
throw new UnauthorizedException(keyspace + " keyspace is not user-modifiable.");
// we want to allow altering AUTH_KS and TRACING_KS.
Set<String> allowAlter = Sets.newHashSet(Auth.AUTH_KS, TraceKeyspace.NAME);
if (allowAlter.contains(keyspace.toLowerCase()) && !(resource.isKeyspaceLevel() && (perm == Permission.ALTER)))
// allow users with sufficient privileges to alter KS level options on AUTH_KS and
// TRACING_KS, and also to drop legacy tables (users, credentials, permissions) from
// AUTH_KS
if (ALTERABLE_SYSTEM_KEYSPACES.contains(resource.getKeyspace().toLowerCase())
&& ((perm == Permission.ALTER && !resource.isKeyspaceLevel())
|| (perm == Permission.DROP && !DROPPABLE_SYSTEM_TABLES.contains(resource))))
{
throw new UnauthorizedException(String.format("Cannot %s %s", perm, resource));
}
}
public void validateLogin() throws UnauthorizedException
@ -307,6 +327,6 @@ public class ClientState
private Set<Permission> authorize(IResource resource)
{
return Auth.getPermissions(user, resource);
return AuthenticatedUser.getPermissions(user, resource);
}
}

View File

@ -17,10 +17,7 @@
*/
package org.apache.cassandra.service;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.*;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;
@ -29,20 +26,10 @@ import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import javax.management.JMX;
import javax.management.MBeanServer;
import javax.management.Notification;
import javax.management.NotificationBroadcasterSupport;
import javax.management.ObjectName;
import javax.management.*;
import javax.management.openmbean.TabularData;
import javax.management.openmbean.TabularDataSupport;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.jmx.JMXConfiguratorMBean;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
import com.google.common.collect.*;
@ -52,12 +39,17 @@ import org.apache.commons.lang3.time.DurationFormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.Auth;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.jmx.JMXConfiguratorMBean;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import org.apache.cassandra.auth.AuthKeyspace;
import org.apache.cassandra.auth.AuthMigrationListener;
import org.apache.cassandra.concurrent.*;
import org.apache.cassandra.config.*;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.cql3.statements.SelectStatement;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.commitlog.CommitLog;
@ -74,15 +66,9 @@ import org.apache.cassandra.io.sstable.SSTableLoader;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.locator.*;
import org.apache.cassandra.metrics.StorageMetrics;
import org.apache.cassandra.net.AsyncOneResponse;
import org.apache.cassandra.net.MessageOut;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.ResponseVerbHandler;
import org.apache.cassandra.repair.RepairMessageVerbHandler;
import org.apache.cassandra.repair.RepairSessionResult;
import org.apache.cassandra.net.*;
import org.apache.cassandra.repair.*;
import org.apache.cassandra.repair.messages.RepairOption;
import org.apache.cassandra.repair.RepairSession;
import org.apache.cassandra.repair.RepairParallelism;
import org.apache.cassandra.service.paxos.CommitVerbHandler;
import org.apache.cassandra.service.paxos.PrepareVerbHandler;
import org.apache.cassandra.service.paxos.ProposeVerbHandler;
@ -843,7 +829,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
Gossiper.instance.replacedEndpoint(existing);
assert tokenMetadata.sortedTokens().size() > 0;
Auth.setup();
doAuthSetup();
}
else
{
@ -882,10 +868,41 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
logger.info("Leaving write survey mode and joining ring at operator request");
assert tokenMetadata.sortedTokens().size() > 0;
Auth.setup();
doAuthSetup();
}
}
private void doAuthSetup()
{
try
{
// if we don't have system_auth keyspace at this point, then create it manually
// otherwise, create any necessary tables as we may be upgrading in which case
// the ks exists with the only the legacy tables defined
if (Schema.instance.getKSMetaData(AuthKeyspace.NAME) == null)
{
MigrationManager.announceNewKeyspace(AuthKeyspace.definition(), 0, false);
}
else
{
for (Map.Entry<String, CFMetaData> table : AuthKeyspace.definition().cfMetaData().entrySet())
{
if (Schema.instance.getCFMetaData(AuthKeyspace.NAME, table.getKey()) == null)
MigrationManager.announceNewColumnFamily(table.getValue());
}
}
}
catch (Exception e)
{
throw new AssertionError(e); // shouldn't ever happen.
}
DatabaseDescriptor.getRoleManager().setup();
DatabaseDescriptor.getAuthenticator().setup();
DatabaseDescriptor.getAuthorizer().setup();
MigrationManager.instance.register(new AuthMigrationListener());
}
public boolean isJoined()
{
return joined;

View File

@ -29,43 +29,30 @@ import java.util.zip.Inflater;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.*;
import com.google.common.primitives.Longs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.AuthenticatedUser;
import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.KSMetaData;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.config.*;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.statements.ParsedStatement;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.composites.*;
import org.apache.cassandra.db.context.CounterContext;
import org.apache.cassandra.db.filter.ColumnSlice;
import org.apache.cassandra.db.filter.IDiskAtomFilter;
import org.apache.cassandra.db.filter.NamesQueryFilter;
import org.apache.cassandra.db.filter.SliceQueryFilter;
import org.apache.cassandra.db.filter.*;
import org.apache.cassandra.db.marshal.TimeUUIDType;
import org.apache.cassandra.dht.*;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.exceptions.*;
import org.apache.cassandra.io.util.DataOutputBuffer;
import org.apache.cassandra.locator.DynamicEndpointSnitch;
import org.apache.cassandra.metrics.ClientMetrics;
import org.apache.cassandra.scheduler.IRequestScheduler;
import org.apache.cassandra.serializers.MarshalException;
import org.apache.cassandra.service.CASRequest;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.MigrationManager;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.*;
import org.apache.cassandra.service.pager.QueryPagers;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.utils.ByteBufferUtil;
@ -1472,12 +1459,11 @@ public class CassandraServer implements Cassandra.Iface
}
}
public void login(AuthenticationRequest auth_request) throws AuthenticationException, AuthorizationException, TException
public void login(AuthenticationRequest auth_request) throws TException
{
try
{
AuthenticatedUser user = DatabaseDescriptor.getAuthenticator().authenticate(auth_request.getCredentials());
state().login(user);
state().login(DatabaseDescriptor.getAuthenticator().legacyAuthenticate(auth_request.getCredentials()));
}
catch (org.apache.cassandra.exceptions.AuthenticationException e)
{

View File

@ -18,33 +18,33 @@
package org.apache.cassandra.tools;
import java.io.File;
import java.net.*;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.util.*;
import com.google.common.base.Joiner;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import org.apache.commons.cli.*;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TTransport;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.auth.PasswordAuthenticator;
import org.apache.cassandra.config.*;
import org.apache.cassandra.schema.LegacySchemaTables;
import org.apache.cassandra.db.SystemKeyspace;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.io.sstable.SSTableLoader;
import org.apache.cassandra.schema.LegacySchemaTables;
import org.apache.cassandra.streaming.*;
import org.apache.cassandra.thrift.*;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.JVMStabilityInspector;
import org.apache.cassandra.utils.OutputHandler;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TTransport;
public class BulkLoader
{
@ -359,8 +359,8 @@ public class BulkLoader
if (user != null && passwd != null)
{
Map<String, String> credentials = new HashMap<>();
credentials.put(IAuthenticator.USERNAME_KEY, user);
credentials.put(IAuthenticator.PASSWORD_KEY, passwd);
credentials.put(PasswordAuthenticator.USERNAME_KEY, user);
credentials.put(PasswordAuthenticator.PASSWORD_KEY, passwd);
AuthenticationRequest authenticationRequest = new AuthenticationRequest(credentials);
client.login(authenticationRequest);
}

View File

@ -22,16 +22,11 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.*;
import com.google.common.base.Splitter;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.auth.PasswordAuthenticator;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.marshal.Int32Type;
@ -179,7 +174,7 @@ public class Client extends SimpleClient
else if (msgType.equals("AUTHENTICATE"))
{
Map<String, String> credentials = readCredentials(iter);
if(!credentials.containsKey(IAuthenticator.USERNAME_KEY) || !credentials.containsKey(IAuthenticator.PASSWORD_KEY))
if(!credentials.containsKey(PasswordAuthenticator.USERNAME_KEY) || !credentials.containsKey(PasswordAuthenticator.PASSWORD_KEY))
{
System.err.println("[ERROR] Authentication requires both 'username' and 'password'");
return null;
@ -221,8 +216,8 @@ public class Client extends SimpleClient
private byte[] encodeCredentialsForSasl(Map<String, String> credentials)
{
byte[] username = credentials.get(IAuthenticator.USERNAME_KEY).getBytes(StandardCharsets.UTF_8);
byte[] password = credentials.get(IAuthenticator.PASSWORD_KEY).getBytes(StandardCharsets.UTF_8);
byte[] username = credentials.get(PasswordAuthenticator.USERNAME_KEY).getBytes(StandardCharsets.UTF_8);
byte[] password = credentials.get(PasswordAuthenticator.PASSWORD_KEY).getBytes(StandardCharsets.UTF_8);
byte[] initialResponse = new byte[username.length + password.length + 2];
initialResponse[0] = 0;
System.arraycopy(username, 0, initialResponse, 1, username.length);

View File

@ -28,21 +28,24 @@ import java.util.concurrent.atomic.AtomicBoolean;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.util.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.Version;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.GlobalEventExecutor;
import io.netty.util.internal.logging.InternalLoggerFactory;
import io.netty.util.internal.logging.Slf4JLoggerFactory;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.auth.ISaslAwareAuthenticator;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.db.marshal.AbstractType;
@ -50,11 +53,6 @@ import org.apache.cassandra.metrics.ClientMetrics;
import org.apache.cassandra.security.SSLFactory;
import org.apache.cassandra.service.*;
import org.apache.cassandra.transport.messages.EventMessage;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.ssl.SslHandler;
public class Server implements CassandraDaemon.Server
{
@ -132,16 +130,6 @@ public class Server implements CassandraDaemon.Server
private void run()
{
// Check that a SaslAuthenticator can be provided by the configured
// IAuthenticator. If not, don't start the server.
IAuthenticator authenticator = DatabaseDescriptor.getAuthenticator();
if (authenticator.requireAuthentication() && !(authenticator instanceof ISaslAwareAuthenticator))
{
logger.error("Not starting native transport as the configured IAuthenticator is not capable of SASL authentication");
isRunning.compareAndSet(true, false);
return;
}
// Configure the server.
eventExecutorGroup = new RequestThreadPoolExecutor();

View File

@ -20,21 +20,17 @@ package org.apache.cassandra.transport;
import java.util.concurrent.ConcurrentMap;
import io.netty.channel.Channel;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.auth.ISaslAwareAuthenticator;
import org.apache.cassandra.auth.ISaslAwareAuthenticator.SaslAuthenticator;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.QueryState;
import org.cliffc.high_scale_lib.NonBlockingHashMap;
public class ServerConnection extends Connection
{
private enum State { UNINITIALIZED, AUTHENTICATION, READY }
private volatile SaslAuthenticator saslAuthenticator;
private volatile IAuthenticator.SaslNegotiator saslNegotiator;
private final ClientState clientState;
private volatile State state;
@ -104,7 +100,7 @@ public class ServerConnection extends Connection
{
state = State.READY;
// we won't use the authenticator again, null it so that it can be GC'd
saslAuthenticator = null;
saslNegotiator = null;
}
break;
case READY:
@ -114,14 +110,10 @@ public class ServerConnection extends Connection
}
}
public SaslAuthenticator getAuthenticator()
public IAuthenticator.SaslNegotiator getSaslNegotiator()
{
if (saslAuthenticator == null)
{
IAuthenticator authenticator = DatabaseDescriptor.getAuthenticator();
assert authenticator instanceof ISaslAwareAuthenticator : "Configured IAuthenticator does not support SASL authentication";
saslAuthenticator = ((ISaslAwareAuthenticator)authenticator).newAuthenticator();
}
return saslAuthenticator;
if (saslNegotiator == null)
saslNegotiator = DatabaseDescriptor.getAuthenticator().newSaslNegotiator();
return saslNegotiator;
}
}

View File

@ -17,18 +17,14 @@
*/
package org.apache.cassandra.transport.messages;
import org.apache.cassandra.auth.AuthenticatedUser;
import org.apache.cassandra.auth.ISaslAwareAuthenticator.SaslAuthenticator;
import org.apache.cassandra.exceptions.AuthenticationException;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.CBUtil;
import org.apache.cassandra.transport.Message;
import org.apache.cassandra.transport.ProtocolException;
import org.apache.cassandra.transport.ServerConnection;
import java.nio.ByteBuffer;
import io.netty.buffer.ByteBuf;
import java.nio.ByteBuffer;
import org.apache.cassandra.auth.AuthenticatedUser;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.exceptions.AuthenticationException;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.*;
/**
* A SASL token message sent from client to server. Some SASL
@ -61,11 +57,12 @@ public class AuthResponse extends Message.Request
}
};
private byte[] token;
private final byte[] token;
public AuthResponse(byte[] token)
{
super(Message.Type.AUTH_RESPONSE);
assert token != null;
this.token = token;
}
@ -74,11 +71,11 @@ public class AuthResponse extends Message.Request
{
try
{
SaslAuthenticator authenticator = ((ServerConnection) connection).getAuthenticator();
byte[] challenge = authenticator.evaluateResponse(token == null ? new byte[0] : token);
if (authenticator.isComplete())
IAuthenticator.SaslNegotiator negotiator = ((ServerConnection) connection).getSaslNegotiator();
byte[] challenge = negotiator.evaluateResponse(token);
if (negotiator.isComplete())
{
AuthenticatedUser user = authenticator.getAuthenticatedUser();
AuthenticatedUser user = negotiator.getAuthenticatedUser();
queryState.getClientState().login(user);
// authentication is complete, send a ready message to the client
return new AuthSuccess(challenge);

View File

@ -20,15 +20,14 @@ package org.apache.cassandra.transport.messages;
import java.util.HashMap;
import java.util.Map;
import io.netty.buffer.ByteBuf;
import org.apache.cassandra.auth.AuthenticatedUser;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.transport.ProtocolException;
import io.netty.buffer.ByteBuf;
import org.apache.cassandra.exceptions.AuthenticationException;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.CBUtil;
import org.apache.cassandra.transport.Message;
import org.apache.cassandra.transport.ProtocolException;
/**
* Message to indicate that the server is ready to receive requests.
@ -75,14 +74,15 @@ public class CredentialsMessage extends Message.Request
{
try
{
AuthenticatedUser user = DatabaseDescriptor.getAuthenticator().authenticate(credentials);
AuthenticatedUser user = DatabaseDescriptor.getAuthenticator().legacyAuthenticate(credentials);
state.getClientState().login(user);
return new ReadyMessage();
}
catch (AuthenticationException e)
{
return ErrorMessage.fromException(e);
}
return new ReadyMessage();
}
@Override

View File

@ -20,19 +20,12 @@ package org.apache.cassandra.utils;
import java.io.*;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URL;
import java.net.UnknownHostException;
import java.net.*;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.*;
import java.util.zip.Checksum;
import com.google.common.base.Joiner;
@ -43,6 +36,7 @@ import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.auth.IAuthorizer;
import org.apache.cassandra.auth.IRoleManager;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.dht.IPartitioner;
@ -54,10 +48,7 @@ import org.apache.cassandra.io.util.DataOutputBuffer;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.io.util.IAllocator;
import org.apache.cassandra.net.AsyncOneResponse;
import org.apache.thrift.TBase;
import org.apache.thrift.TDeserializer;
import org.apache.thrift.TException;
import org.apache.thrift.TSerializer;
import org.apache.thrift.*;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.map.ObjectMapper;
@ -439,6 +430,13 @@ public class FBUtilities
return FBUtilities.construct(className, "authenticator");
}
public static IRoleManager newRoleManager(String className) throws ConfigurationException
{
if (!className.contains("."))
className = "org.apache.cassandra.auth." + className;
return FBUtilities.construct(className, "role manager");
}
/**
* @return The Class for the given name.
* @param classname Fully qualified classname.