Simplify auth setup and make system_auth ks alterable

Patch by Aleksey Yeschenko; reviewed by Jonathan Ellis for
CASSANDRA-5112
This commit is contained in:
Aleksey Yeschenko 2013-02-14 01:19:04 +03:00
parent e531be77a4
commit 265964064b
14 changed files with 153 additions and 57 deletions

View File

@ -18,6 +18,7 @@
* Implement caching of authorization results (CASSANDRA-4295)
* Add support for LZ4 compression (CASSANDRA-5038)
* Fix missing columns in wide rows queries (CASSANDRA-5225)
* Simplify auth setup and make system_auth ks alterable (CASSANDRA-5112)
1.2.1

View File

@ -173,7 +173,7 @@ else:
debug_completion = bool(os.environ.get('CQLSH_DEBUG_COMPLETION', '') == 'YES')
SYSTEM_KEYSPACES = ('system', 'system_traces', 'system_auth')
SYSTEM_KEYSPACES = ('system', 'system_traces')
# we want the cql parser to understand our cqlsh-specific commands too
my_commands_ending_with_newline = (

View File

@ -39,6 +39,7 @@ class UnexpectedTableStructure(UserWarning):
return 'Unexpected table structure; may not translate correctly to CQL. ' + self.msg
SYSTEM_KEYSPACES = ('system', 'system_traces', 'system_auth')
NONALTERBALE_KEYSPACES = ('system', 'system_traces')
class Cql3ParsingRuleSet(CqlParsingRuleSet):
keywords = set((
@ -306,6 +307,8 @@ JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ;
<nonSystemKeyspaceName> ::= ksname=<cfOrKsName> ;
<alterableKeyspaceName> ::= ksname=<cfOrKsName> ;
<cfOrKsName> ::= <identifier>
| <quotedName>
| <unreservedKeyword>;
@ -686,6 +689,11 @@ def ks_name_completer(ctxt, cass):
ksnames = [n for n in cass.get_keyspace_names() if n not in SYSTEM_KEYSPACES]
return map(maybe_escape_name, ksnames)
@completer_for('alterableKeyspaceName', 'ksname')
def ks_name_completer(ctxt, cass):
ksnames = [n for n in cass.get_keyspace_names() if n not in NONALTERBALE_KEYSPACES]
return map(maybe_escape_name, ksnames)
@completer_for('columnFamilyName', 'ksname')
def cf_ks_name_completer(ctxt, cass):
return [maybe_escape_name(ks) + '.' for ks in cass.get_keyspace_names()]
@ -1242,7 +1250,7 @@ def alter_table_col_completer(ctxt, cass):
explain_completion('alterInstructions', 'newcol', '<new_column_name>')
syntax_rules += r'''
<alterKeyspaceStatement> ::= "ALTER" ( "KEYSPACE" | "SCHEMA" ) ks=<nonSystemKeyspaceName>
<alterKeyspaceStatement> ::= "ALTER" ( "KEYSPACE" | "SCHEMA" ) ks=<alterableKeyspaceName>
"WITH" <newPropSpec> ( "AND" <newPropSpec> )*
;
'''
@ -1295,7 +1303,7 @@ syntax_rules += r'''
;
<dataResource> ::= ( "ALL" "KEYSPACES" )
| ( "KEYSPACE" <nonSystemKeyspaceName> )
| ( "KEYSPACE" <keyspaceName> )
| ( "TABLE"? <columnFamilyName> )
;
'''

View File

@ -17,15 +17,24 @@
*/
package org.apache.cassandra.auth;
import java.util.concurrent.TimeUnit;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.KSMetaData;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.locator.SimpleStrategy;
import org.apache.cassandra.service.IMigrationListener;
import org.apache.cassandra.service.MigrationManager;
import org.apache.cassandra.service.StorageService;
public class Auth
{
@ -33,9 +42,20 @@ public class Auth
public static final String DEFAULT_SUPERUSER_NAME = "cassandra";
private static final long SUPERUSER_SETUP_DELAY = 10; // seconds.
public static final String AUTH_KS = "system_auth";
public static final String USERS_CF = "users";
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.
/**
* Checks if the username is stored in AUTH_KS.USERS_CF.
*
@ -47,7 +67,7 @@ public class Auth
String query = String.format("SELECT * FROM %s.%s WHERE name = '%s'", AUTH_KS, USERS_CF, escape(username));
try
{
return !QueryProcessor.process(query).isEmpty();
return !QueryProcessor.process(query, ConsistencyLevel.QUORUM).isEmpty();
}
catch (RequestExecutionException e)
{
@ -66,7 +86,7 @@ public class Auth
String query = String.format("SELECT super FROM %s.%s WHERE name = '%s'", AUTH_KS, USERS_CF, escape(username));
try
{
UntypedResultSet result = QueryProcessor.process(query);
UntypedResultSet result = QueryProcessor.process(query, ConsistencyLevel.QUORUM);
return !result.isEmpty() && result.one().getBoolean("super");
}
catch (RequestExecutionException e)
@ -87,7 +107,8 @@ public class Auth
AUTH_KS,
USERS_CF,
escape(username),
isSuper));
isSuper),
ConsistencyLevel.QUORUM);
}
/**
@ -100,7 +121,8 @@ public class Auth
QueryProcessor.process(String.format("DELETE FROM %s.%s WHERE name = '%s'",
AUTH_KS,
USERS_CF,
escape(username)));
escape(username)),
ConsistencyLevel.QUORUM);
}
/**
@ -108,23 +130,75 @@ public class Auth
*/
public static void setup()
{
authenticator().setup();
authorizer().setup();
setupAuthKeyspace();
setupUsersTable();
DatabaseDescriptor.getAuthenticator().setup();
DatabaseDescriptor.getAuthorizer().setup();
// register a custom MigrationListener for permissions cleanup after dropped keyspaces/cfs.
MigrationManager.instance.register(new MigrationListener());
// the delay is here to give the node some time to see its peers - to reduce
// "Skipping default superuser setup: some nodes are not ready" log spam.
// It's the only reason for the delay.
StorageService.tasks.schedule(new Runnable()
{
public void run()
{
setupDefaultSuperuser();
}
},
SUPERUSER_SETUP_DELAY,
TimeUnit.SECONDS);
}
/**
* Sets up default superuser.
*/
public static void setupSuperuser()
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);
}
catch (Exception e)
{
throw new AssertionError(e); // shouldn't ever happen.
}
}
}
private static void setupUsersTable()
{
if (Schema.instance.getCFMetaData(AUTH_KS, USERS_CF) == null)
{
try
{
QueryProcessor.process(USERS_CF_SCHEMA, ConsistencyLevel.ONE);
}
catch (RequestExecutionException e)
{
throw new AssertionError(e);
}
}
}
private static void setupDefaultSuperuser()
{
try
{
// insert a default superuser if AUTH_KS.USERS_CF is empty.
if (QueryProcessor.process(String.format("SELECT * FROM %s.%s", AUTH_KS, USERS_CF)).isEmpty())
insertUser(DEFAULT_SUPERUSER_NAME, true);
if (QueryProcessor.process(String.format("SELECT * FROM %s.%s", AUTH_KS, USERS_CF), ConsistencyLevel.QUORUM).isEmpty())
{
logger.info("Creating default superuser '{}'", DEFAULT_SUPERUSER_NAME);
QueryProcessor.process(String.format("INSERT INTO %s.%s (name, super) VALUES ('%s', %s) USING TIMESTAMP 0",
AUTH_KS,
USERS_CF,
DEFAULT_SUPERUSER_NAME,
true),
ConsistencyLevel.QUORUM);
}
}
catch (RequestExecutionException e)
{
@ -138,13 +212,35 @@ public class Auth
return StringUtils.replace(name, "'", "''");
}
private static IAuthenticator authenticator()
/**
* IMigrationListener implementation that cleans up permissions on dropped resources.
*/
public static class MigrationListener implements IMigrationListener
{
return DatabaseDescriptor.getAuthenticator();
}
public void onDropKeyspace(String ksName)
{
DatabaseDescriptor.getAuthorizer().revokeAll(DataResource.keyspace(ksName));
}
private static IAuthorizer authorizer()
{
return DatabaseDescriptor.getAuthorizer();
public void onDropColumnFamily(String ksName, String cfName)
{
DatabaseDescriptor.getAuthorizer().revokeAll(DataResource.columnFamily(ksName, cfName));
}
public void onCreateKeyspace(String ksName)
{
}
public void onCreateColumnFamily(String ksName, String cfName)
{
}
public void onUpdateKeyspace(String ksName)
{
}
public void onUpdateColumnFamily(String ksName, String cfName)
{
}
}
}

View File

@ -33,7 +33,6 @@ import org.apache.commons.lang.builder.ToStringBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.Auth;
import org.apache.cassandra.cql3.CFDefinition;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.UntypedResultSet;
@ -227,12 +226,7 @@ public final class CFMetaData
+ "requested_at timestamp"
+ ") WITH COMMENT='ranges requested for transfer here'");
public static final CFMetaData AuthUsersCf = compile(18, "CREATE TABLE " + Auth.USERS_CF + " ("
+ "name text PRIMARY KEY,"
+ "super boolean"
+ ") WITH gc_grace_seconds=864000;", Auth.AUTH_KS);
public static final CFMetaData CompactionLogCF = compile(19, "CREATE TABLE " + SystemTable.COMPACTION_LOG + " ("
public static final CFMetaData CompactionLogCF = compile(18, "CREATE TABLE " + SystemTable.COMPACTION_LOG + " ("
+ "id uuid PRIMARY KEY,"
+ "keyspace_name text,"
+ "columnfamily_name text,"

View File

@ -470,9 +470,7 @@ public class DatabaseDescriptor
}
// Hardcoded system tables
List<KSMetaData> systemKeyspaces = Arrays.asList(KSMetaData.systemKeyspace(),
KSMetaData.traceKeyspace(),
KSMetaData.authKeyspace());
List<KSMetaData> systemKeyspaces = Arrays.asList(KSMetaData.systemKeyspace(), KSMetaData.traceKeyspace());
assert systemKeyspaces.size() == Schema.systemKeyspaceNames.size();
for (KSMetaData ksmd : systemKeyspaces)
{

View File

@ -23,7 +23,6 @@ import com.google.common.collect.ImmutableMap;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.cassandra.auth.Auth;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.db.*;
@ -104,12 +103,6 @@ public final class KSMetaData
return new KSMetaData(Tracing.TRACE_KS, SimpleStrategy.class, ImmutableMap.of("replication_factor", "1"), true, cfDefs);
}
public static KSMetaData authKeyspace()
{
List<CFMetaData> cfDefs = Arrays.asList(CFMetaData.AuthUsersCf);
return new KSMetaData(Auth.AUTH_KS, SimpleStrategy.class, ImmutableMap.of("replication_factor", "1"), true, cfDefs);
}
public static KSMetaData testMetadata(String name, Class<? extends AbstractReplicationStrategy> strategyClass, Map<String, String> strategyOptions, CFMetaData... cfDefs)
{
return new KSMetaData(name, strategyClass, strategyOptions, true, Arrays.asList(cfDefs));

View File

@ -27,7 +27,6 @@ import com.google.common.collect.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.Auth;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.Table;
import org.apache.cassandra.db.marshal.AbstractType;
@ -67,7 +66,7 @@ public class Schema
// 59adb24e-f3cd-3e02-97f0-5b395827453f
public static final UUID emptyVersion;
public static final ImmutableSet<String> systemKeyspaceNames = ImmutableSet.of(Table.SYSTEM_KS, Tracing.TRACE_KS, Auth.AUTH_KS);
public static final ImmutableSet<String> systemKeyspaceNames = ImmutableSet.of(Table.SYSTEM_KS, Tracing.TRACE_KS);
static
{

View File

@ -143,12 +143,12 @@ public class QueryProcessor
return processStatement(prepared, cl, queryState, Collections.<ByteBuffer>emptyList());
}
public static UntypedResultSet process(String query) throws RequestExecutionException
public static UntypedResultSet process(String query, ConsistencyLevel cl) throws RequestExecutionException
{
try
{
QueryState state = new QueryState(new ClientState(true));
ResultMessage result = process(query, ConsistencyLevel.ONE, state);
ResultMessage result = process(query, cl, state);
if (result instanceof ResultMessage.Rows)
return new UntypedResultSet(((ResultMessage.Rows)result).result);
else

View File

@ -42,7 +42,7 @@ public class ListUsersStatement extends AuthenticationStatement
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
return QueryProcessor.process(String.format("SELECT * FROM %s.%s", Auth.AUTH_KS, Auth.USERS_CF),
ConsistencyLevel.ONE,
ConsistencyLevel.QUORUM,
new QueryState(new ClientState(true)));
}
}

View File

@ -32,7 +32,6 @@ import org.apache.log4j.PropertyConfigurator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.Auth;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.config.DatabaseDescriptor;
@ -195,9 +194,6 @@ public class CassandraDaemon
System.exit(100);
}
// setup Authenticator and Authorizer.
Auth.setup();
// clean up debris in the rest of the tables
for (String table : Schema.instance.getTables())
{

View File

@ -152,7 +152,7 @@ public class ClientState
if (internalCall)
return;
validateLogin();
preventSystemKSSchemaModification(keyspace, perm);
preventSystemKSSchemaModification(keyspace, resource, perm);
if (perm.equals(Permission.SELECT) && READABLE_SYSTEM_RESOURCES.contains(resource))
return;
if (PROTECTED_AUTH_RESOURCES.contains(resource))
@ -173,10 +173,18 @@ public class ClientState
resource));
}
private void preventSystemKSSchemaModification(String keyspace, Permission perm) throws UnauthorizedException
private void preventSystemKSSchemaModification(String keyspace, DataResource resource, Permission perm) throws UnauthorizedException
{
if (Schema.systemKeyspaceNames.contains(keyspace.toLowerCase()) && !(perm.equals(Permission.SELECT) || perm.equals(Permission.MODIFY)))
// we only care about schema modification.
if (!(perm.equals(Permission.ALTER) || perm.equals(Permission.DROP) || perm.equals(Permission.CREATE)))
return;
if (Schema.systemKeyspaceNames.contains(keyspace.toLowerCase()))
throw new UnauthorizedException(keyspace + " keyspace is not user-modifiable.");
// we want to allow altering AUTH_KS itself.
if (keyspace.equals(Auth.AUTH_KS) && !(resource.isKeyspaceLevel() && perm.equals(Permission.ALTER)))
throw new UnauthorizedException(String.format("Cannot %s %s", perm, resource));
}
public void validateLogin() throws UnauthorizedException

View File

@ -197,6 +197,11 @@ public class MigrationManager implements IEndpointStateChangeSubscriber
}
public static void announceNewKeyspace(KSMetaData ksm) throws ConfigurationException
{
announceNewKeyspace(ksm, FBUtilities.timestampMicros());
}
public static void announceNewKeyspace(KSMetaData ksm, long timestamp) throws ConfigurationException
{
ksm.validate();
@ -204,7 +209,7 @@ public class MigrationManager implements IEndpointStateChangeSubscriber
throw new AlreadyExistsException(ksm.name);
logger.info(String.format("Create new Keyspace: %s", ksm));
announce(ksm.toSchema(FBUtilities.timestampMicros()));
announce(ksm.toSchema(timestamp));
}
public static void announceNewColumnFamily(CFMetaData cfm) throws ConfigurationException

View File

@ -746,8 +746,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
logger.info("Startup completed! Now serving reads.");
assert tokenMetadata.sortedTokens().size() > 0;
// setup default superuser (if needed).
Auth.setupSuperuser();
Auth.setup();
}
else
{
@ -786,8 +785,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
logger.info("Leaving write survey mode and joining ring at operator request");
assert tokenMetadata.sortedTokens().size() > 0;
// setup default superuser (if needed).
Auth.setupSuperuser();
Auth.setup();
}
}
@ -2308,7 +2306,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
public int forceRepairAsync(final String keyspace, final boolean isSequential, final boolean isLocal, final boolean primaryRange, final String... columnFamilies)
{
if (Table.SYSTEM_KS.equals(keyspace) || Tracing.TRACE_KS.equals(keyspace) || Auth.AUTH_KS.equals(keyspace))
if (Table.SYSTEM_KS.equals(keyspace) || Tracing.TRACE_KS.equals(keyspace))
return 0;
final int cmd = nextRepairCommand.incrementAndGet();
@ -2348,7 +2346,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
public void forceTableRepairRange(final String tableName, final Collection<Range<Token>> ranges, boolean isSequential, boolean isLocal, final String... columnFamilies) throws IOException
{
if (Table.SYSTEM_KS.equals(tableName) || Tracing.TRACE_KS.equals(tableName) || Auth.AUTH_KS.equals(tableName))
if (Table.SYSTEM_KS.equals(tableName) || Tracing.TRACE_KS.equals(tableName))
return;
createRepairTask(nextRepairCommand.incrementAndGet(), tableName, ranges, isSequential, isLocal, columnFamilies).run();
}