Add network authz

Patch by Blake Eggleston; Reviewed by Sam Tunnicliffe for CASSANDRA-13985
This commit is contained in:
Blake Eggleston 2017-12-13 13:17:05 -08:00
parent 63945228fc
commit 54de771e64
36 changed files with 1246 additions and 54 deletions

View File

@ -1,4 +1,5 @@
4.0
* Add network authz (CASSANDRA-13985)
* Use the correct IP/Port for Streaming when localAddress is left unbound (CASSANDAR-14389)
* nodetool listsnapshots is missing local system keyspace snapshots (CASSANDRA-14381)
* Remove StreamCoordinator.streamExecutor thread pool (CASSANDRA-14402)

View File

@ -72,6 +72,10 @@ New features
See CASSANDRA-13848 for more detail
- Metric for coordinator writes per table has been added. See CASSANDRA-14232
- Nodetool cfstats now has options to sort by various metrics as well as limit results.
- Operators can restrict login user activity to one or more datacenters. See `network_authorizer`
in cassandra.yaml, and the docs for create and alter role statements. CASSANDRA-13985
- Roles altered from login=true to login=false will prevent existing connections from executing any
statements after the cache has been refreshed. CASSANDRA-13985
Upgrading
---------

View File

@ -122,6 +122,16 @@ authorizer: AllowAllAuthorizer
# increase system_auth keyspace replication factor if you use this role manager.
role_manager: CassandraRoleManager
# Network authorization backend, implementing INetworkAuthorizer; used to restrict user
# access to certain DCs
# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllNetworkAuthorizer,
# CassandraNetworkAuthorizer}.
#
# - AllowAllNetworkAuthorizer allows access to any DC to any user - set it to disable authorization.
# - CassandraNetworkAuthorizer stores permissions in system_auth.network_permissions table. Please
# increase system_auth keyspace replication factor if you use this authorizer.
network_authorizer: AllowAllNetworkAuthorizer
# Validity period for roles cache (fetching granted roles can be an expensive
# operation depending on the role manager, CassandraRoleManager is one example)
# Granted roles are cached for authenticated sessions in AuthenticatedUser and

View File

@ -46,6 +46,8 @@ Creating a role uses the ``CREATE ROLE`` statement:
:| LOGIN '=' `boolean`
:| SUPERUSER '=' `boolean`
:| OPTIONS '=' `map_literal`
:| ACCESS TO DATACENTERS `set_literal`
:| ACCESS TO ALL DATACENTERS
For instance::
@ -53,6 +55,8 @@ For instance::
CREATE ROLE alice WITH PASSWORD = 'password_a' AND LOGIN = true;
CREATE ROLE bob WITH PASSWORD = 'password_b' AND LOGIN = true AND SUPERUSER = true;
CREATE ROLE carlos WITH OPTIONS = { 'custom_option1' : 'option1_value', 'custom_option2' : 99 };
CREATE ROLE alice WITH PASSWORD = 'password_a' AND LOGIN = true AND ACCESS TO DATACENTERS {'DC1', 'DC3'};
CREATE ROLE alice WITH PASSWORD = 'password_a' AND LOGIN = true AND ACCESS TO ALL DATACENTERS;
By default roles do not possess ``LOGIN`` privileges or ``SUPERUSER`` status.
@ -81,6 +85,14 @@ quotation marks.
If internal authentication has not been set up or the role does not have ``LOGIN`` privileges, the ``WITH PASSWORD``
clause is not necessary.
Restricting connections to specific datacenters
```````````````````````````````````````````````
If a ``network_authorizer`` has been configured, you can restrict login roles to specific datacenters with the
``ACCESS TO DATACENTERS`` clause followed by a set literal of datacenters the user can access. Not specifiying
datacenters implicitly grants access to all datacenters. The clause ``ACCESS TO ALL DATACENTERS`` can be used for
explicitness, but there's no functional difference.
Creating a role conditionally
`````````````````````````````
@ -105,6 +117,13 @@ For instance::
ALTER ROLE bob WITH PASSWORD = 'PASSWORD_B' AND SUPERUSER = false;
Restricting connections to specific datacenters
```````````````````````````````````````````````
If a ``network_authorizer`` has been configured, you can restrict login roles to specific datacenters with the
``ACCESS TO DATACENTERS`` clause followed by a set literal of datacenters the user can access. To remove any
data center restrictions, use the ``ACCESS TO ALL DATACENTERS`` clause.
Conditions on executing ``ALTER ROLE`` statements:
- A client must have ``SUPERUSER`` status to alter the ``SUPERUSER`` status of another role

View File

@ -1468,6 +1468,8 @@ syntax_rules += r'''
| "OPTIONS" "=" <mapLiteral>
| "SUPERUSER" "=" <boolean>
| "LOGIN" "=" <boolean>
| "ACCESS" "TO" "DATACENTERS" <setLiteral>
| "ACCESS" "TO" "ALL" "DATACENTERS"
;
<dropRoleStatement> ::= "DROP" "ROLE" <rolename>

View File

@ -145,6 +145,8 @@ 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_ACCESS: A C C E S S;
K_DATACENTERS: D A T A C E N T E R S;
K_CLUSTERING: C L U S T E R I N G;
K_ASCII: A S C I I;

View File

@ -1160,7 +1160,7 @@ createUserStatement returns [CreateRoleStatement stmt]
( K_WITH userPassword[opts] )?
( K_SUPERUSER { superuser = true; } | K_NOSUPERUSER { superuser = false; } )?
{ opts.setOption(IRoleManager.Option.SUPERUSER, superuser);
$stmt = new CreateRoleStatement(name, opts, ifNotExists); }
$stmt = new CreateRoleStatement(name, opts, DCPermissions.all(), ifNotExists); }
;
/**
@ -1175,7 +1175,7 @@ alterUserStatement returns [AlterRoleStatement stmt]
( K_WITH userPassword[opts] )?
( K_SUPERUSER { opts.setOption(IRoleManager.Option.SUPERUSER, true); }
| K_NOSUPERUSER { opts.setOption(IRoleManager.Option.SUPERUSER, false); } ) ?
{ $stmt = new AlterRoleStatement(name, opts); }
{ $stmt = new AlterRoleStatement(name, opts, null); }
;
/**
@ -1208,10 +1208,11 @@ listUsersStatement returns [ListRolesStatement stmt]
createRoleStatement returns [CreateRoleStatement stmt]
@init {
RoleOptions opts = new RoleOptions();
DCPermissions.Builder dcperms = DCPermissions.builder();
boolean ifNotExists = false;
}
: K_CREATE K_ROLE (K_IF K_NOT K_EXISTS { ifNotExists = true; })? name=userOrRoleName
( K_WITH roleOptions[opts] )?
( K_WITH roleOptions[opts, dcperms] )?
{
// set defaults if they weren't explictly supplied
if (!opts.getLogin().isPresent())
@ -1222,7 +1223,7 @@ createRoleStatement returns [CreateRoleStatement stmt]
{
opts.setOption(IRoleManager.Option.SUPERUSER, false);
}
$stmt = new CreateRoleStatement(name, opts, ifNotExists);
$stmt = new CreateRoleStatement(name, opts, dcperms.build(), ifNotExists);
}
;
@ -1238,10 +1239,11 @@ createRoleStatement returns [CreateRoleStatement stmt]
alterRoleStatement returns [AlterRoleStatement stmt]
@init {
RoleOptions opts = new RoleOptions();
DCPermissions.Builder dcperms = DCPermissions.builder();
}
: K_ALTER K_ROLE name=userOrRoleName
( K_WITH roleOptions[opts] )?
{ $stmt = new AlterRoleStatement(name, opts); }
( K_WITH roleOptions[opts, dcperms] )?
{ $stmt = new AlterRoleStatement(name, opts, dcperms.isModified() ? dcperms.build() : null); }
;
/**
@ -1269,15 +1271,21 @@ listRolesStatement returns [ListRolesStatement stmt]
{ $stmt = new ListRolesStatement(grantee, recursive); }
;
roleOptions[RoleOptions opts]
: roleOption[opts] (K_AND roleOption[opts])*
roleOptions[RoleOptions opts, DCPermissions.Builder dcperms]
: roleOption[opts, dcperms] (K_AND roleOption[opts, dcperms])*
;
roleOption[RoleOptions opts]
roleOption[RoleOptions opts, DCPermissions.Builder dcperms]
: K_PASSWORD '=' v=STRING_LITERAL { opts.setOption(IRoleManager.Option.PASSWORD, $v.text); }
| K_OPTIONS '=' m=fullMapLiteral { opts.setOption(IRoleManager.Option.OPTIONS, convertPropertyMap(m)); }
| K_SUPERUSER '=' b=BOOLEAN { opts.setOption(IRoleManager.Option.SUPERUSER, Boolean.valueOf($b.text)); }
| K_LOGIN '=' b=BOOLEAN { opts.setOption(IRoleManager.Option.LOGIN, Boolean.valueOf($b.text)); }
| K_ACCESS K_TO K_ALL K_DATACENTERS { dcperms.all(); }
| K_ACCESS K_TO K_DATACENTERS '{' dcPermission[dcperms] (',' dcPermission[dcperms])* '}'
;
dcPermission[DCPermissions.Builder builder]
: dc=STRING_LITERAL { builder.add($dc.text); }
;
// for backwards compatibility in CREATE/ALTER USER, this has no '='

View File

@ -0,0 +1,47 @@
/*
* 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 org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.InvalidRequestException;
public class AllowAllNetworkAuthorizer implements INetworkAuthorizer
{
public void setup() {}
public DCPermissions authorize(RoleResource role)
{
return DCPermissions.all();
}
public void setRoleDatacenters(RoleResource role, DCPermissions permissions)
{
throw new InvalidRequestException("ACCESS TO DATACENTERS operations not supported by AllowAllNetworkAuthorizer");
}
public void drop(RoleResource role) {}
public void validateConfiguration() throws ConfigurationException {}
@Override
public boolean requireAuthorization()
{
return false;
}
}

View File

@ -25,7 +25,6 @@ import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.utils.FBUtilities;
import org.hsqldb.Database;
/**
* Only purpose is to Initialize authentication/authorization via {@link #applyAuth()}.
@ -98,12 +97,21 @@ public final class AuthConfig
if (conf.internode_authenticator != null)
DatabaseDescriptor.setInternodeAuthenticator(FBUtilities.construct(conf.internode_authenticator, "internode_authenticator"));
// network authorizer
INetworkAuthorizer networkAuthorizer = FBUtilities.newNetworkAuthorizer(conf.network_authorizer);
DatabaseDescriptor.setNetworkAuthorizer(networkAuthorizer);
if (networkAuthorizer.requireAuthorization() && !authenticator.requireAuthentication())
{
throw new ConfigurationException(conf.network_authorizer + " can't be used with " + conf.authenticator, false);
}
// Validate at last to have authenticator, authorizer, role-manager and internode-auth setup
// in case these rely on each other.
authenticator.validateConfiguration();
authorizer.validateConfiguration();
roleManager.validateConfiguration();
networkAuthorizer.validateConfiguration();
DatabaseDescriptor.getInternodeAuthenticator().validateConfiguration();
}
}

View File

@ -39,6 +39,7 @@ public final class AuthKeyspace
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 String NETWORK_PERMISSIONS = "network_permissions";
public static final long SUPERUSER_SETUP_DELAY = Long.getLong("cassandra.superuser_setup_delay_ms", 10000);
@ -78,6 +79,13 @@ public final class AuthKeyspace
+ "role text,"
+ "PRIMARY KEY(resource, role))");
private static final TableMetadata NetworkPermissions =
parse(NETWORK_PERMISSIONS,
"user network permissions",
"CREATE TABLE %s ("
+ "role text, "
+ "dcs frozen<set<text>>, "
+ "PRIMARY KEY(role))");
private static TableMetadata parse(String name, String description, String cql)
{
@ -92,6 +100,6 @@ public final class AuthKeyspace
{
return KeyspaceMetadata.create(SchemaConstants.AUTH_KEYSPACE_NAME,
KeyspaceParams.simple(1),
Tables.of(Roles, RoleMembers, RolePermissions, ResourceRoleIndex));
Tables.of(Roles, RoleMembers, RolePermissions, ResourceRoleIndex, NetworkPermissions));
}
}

View File

@ -22,6 +22,7 @@ import java.util.Set;
import com.google.common.base.Objects;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.dht.Datacenters;
/**
* Returned from IAuthenticator#authenticate(), represents an authenticated user everywhere internally.
@ -39,6 +40,7 @@ public class AuthenticatedUser
// User-level permissions cache.
private static final PermissionsCache permissionsCache = new PermissionsCache(DatabaseDescriptor.getAuthorizer());
private static final NetworkAuthCache networkAuthCache = new NetworkAuthCache(DatabaseDescriptor.getNetworkAuthorizer());
private final String name;
// primary Role of the logged in user
@ -104,6 +106,11 @@ public class AuthenticatedUser
return permissionsCache.getPermissions(this, resource);
}
public boolean hasLocalAccess()
{
return networkAuthCache.get(this.getPrimaryRole()).canAccess(Datacenters.thisDatacenter());
}
@Override
public String toString()
{

View File

@ -57,7 +57,7 @@ public class CassandraAuthorizer implements IAuthorizer
private static final String RESOURCE = "resource";
private static final String PERMISSIONS = "permissions";
private SelectStatement authorizeRoleStatement;
SelectStatement authorizeRoleStatement;
public CassandraAuthorizer()
{
@ -180,11 +180,7 @@ public class CassandraAuthorizer implements IAuthorizer
BatchStatement.Type.LOGGED,
Lists.newArrayList(Iterables.filter(statements, ModificationStatement.class)),
Attributes.none());
QueryProcessor.instance.processBatch(batch,
QueryState.forInternalCalls(),
BatchQueryOptions.withoutPerStatementVariables(QueryOptions.DEFAULT),
System.nanoTime());
processBatch(batch);
}
// Add every permission on the resource granted to the role
@ -195,7 +191,8 @@ public class CassandraAuthorizer implements IAuthorizer
Lists.newArrayList(ByteBufferUtil.bytes(role.getRoleName()),
ByteBufferUtil.bytes(resource.getName())));
ResultMessage.Rows rows = authorizeRoleStatement.execute(QueryState.forInternalCalls(), options, System.nanoTime());
ResultMessage.Rows rows = select(authorizeRoleStatement, options);
UntypedResultSet result = UntypedResultSet.create(rows.result);
if (!result.isEmpty() && result.one().has(PERMISSIONS))
@ -346,8 +343,21 @@ public class CassandraAuthorizer implements IAuthorizer
return StringUtils.replace(name, "'", "''");
}
private UntypedResultSet process(String query) throws RequestExecutionException
ResultMessage.Rows select(SelectStatement statement, QueryOptions options)
{
return statement.execute(QueryState.forInternalCalls(), options, System.nanoTime());
}
UntypedResultSet process(String query) throws RequestExecutionException
{
return QueryProcessor.process(query, ConsistencyLevel.LOCAL_ONE);
}
void processBatch(BatchStatement statement)
{
QueryProcessor.instance.processBatch(statement,
QueryState.forInternalCalls(),
BatchQueryOptions.withoutPerStatementVariables(QueryOptions.DEFAULT),
System.nanoTime());
}
}

View File

@ -0,0 +1,157 @@
/*
* 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 com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import org.apache.cassandra.config.DatabaseDescriptor;
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.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.schema.SchemaConstants;
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;
public class CassandraNetworkAuthorizer implements INetworkAuthorizer
{
private SelectStatement authorizeUserStatement = null;
public void setup()
{
String query = String.format("SELECT dcs FROM %s.%s WHERE role = ?",
SchemaConstants.AUTH_KEYSPACE_NAME,
AuthKeyspace.NETWORK_PERMISSIONS);
authorizeUserStatement = (SelectStatement) QueryProcessor.getStatement(query, ClientState.forInternalCalls()).statement;
}
@VisibleForTesting
ResultMessage.Rows select(SelectStatement statement, QueryOptions options)
{
return statement.execute(QueryState.forInternalCalls(), options, System.nanoTime());
}
@VisibleForTesting
void process(String query)
{
QueryProcessor.process(query, ConsistencyLevel.LOCAL_ONE);
}
private Set<String> getAuthorizedDcs(String name)
{
QueryOptions options = QueryOptions.forInternalCalls(ConsistencyLevel.LOCAL_ONE,
Lists.newArrayList(ByteBufferUtil.bytes(name)));
ResultMessage.Rows rows = select(authorizeUserStatement, options);
UntypedResultSet result = UntypedResultSet.create(rows.result);
Set<String> dcs = null;
if (!result.isEmpty() && result.one().has("dcs"))
{
dcs = result.one().getFrozenSet("dcs", UTF8Type.instance);
}
return dcs;
}
public DCPermissions authorize(RoleResource role)
{
if (!DatabaseDescriptor.getRoleManager().canLogin(role))
{
return DCPermissions.none();
}
if (Roles.hasSuperuserStatus(role))
{
return DCPermissions.all();
}
Set<String> dcs = getAuthorizedDcs(role.getName());
if (dcs == null || dcs.isEmpty())
{
return DCPermissions.all();
}
else
{
return DCPermissions.subset(dcs);
}
}
private static String getSetString(DCPermissions permissions)
{
if (permissions.restrictsAccess())
{
StringBuilder builder = new StringBuilder();
builder.append('{');
boolean first = true;
for (String dc: permissions.allowedDCs())
{
if (first)
{
first = false;
}
else
{
builder.append(", ");
}
builder.append('\'');
builder.append(dc);
builder.append('\'');
}
builder.append('}');
return builder.toString();
}
else
{
return "{}";
}
}
public void setRoleDatacenters(RoleResource role, DCPermissions permissions)
{
String query = String.format("UPDATE %s.%s SET dcs = %s WHERE role = '%s'",
SchemaConstants.AUTH_KEYSPACE_NAME,
AuthKeyspace.NETWORK_PERMISSIONS,
getSetString(permissions),
role.getName());
process(query);
}
public void drop(RoleResource role)
{
String query = String.format("DELETE FROM %s.%s WHERE role = '%s'",
SchemaConstants.AUTH_KEYSPACE_NAME,
AuthKeyspace.NETWORK_PERMISSIONS,
role.getName());
process(query);
}
public void validateConfiguration() throws ConfigurationException
{
// noop
}
}

View File

@ -21,6 +21,7 @@ import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.*;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableSet;
@ -388,6 +389,12 @@ public class CassandraRoleManager implements IRoleManager
}
}
@VisibleForTesting
ResultMessage.Rows select(SelectStatement statement, QueryOptions options)
{
return statement.execute(QueryState.forInternalCalls(), options, System.nanoTime());
}
/*
* 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
@ -395,11 +402,9 @@ public class CassandraRoleManager implements IRoleManager
*/
private Role getRole(String name)
{
ResultMessage.Rows rows =
loadRoleStatement.execute(QueryState.forInternalCalls(),
QueryOptions.forInternalCalls(consistencyForRole(name),
Collections.singletonList(ByteBufferUtil.bytes(name))),
System.nanoTime());
QueryOptions options = QueryOptions.forInternalCalls(consistencyForRole(name),
Collections.singletonList(ByteBufferUtil.bytes(name)));
ResultMessage.Rows rows = select(loadRoleStatement, options);
if (rows.result.isEmpty())
return NULL_ROLE;
@ -498,7 +503,8 @@ public class CassandraRoleManager implements IRoleManager
* This shouldn't be used during setup as this will directly return an error if the manager is not setup yet. Setup tasks
* should use QueryProcessor.process directly.
*/
private UntypedResultSet process(String query, ConsistencyLevel consistencyLevel) throws RequestValidationException, RequestExecutionException
@VisibleForTesting
UntypedResultSet process(String query, ConsistencyLevel consistencyLevel) throws RequestValidationException, RequestExecutionException
{
if (!isClusterReady)
throw new InvalidRequestException("Cannot process role related query as the role manager isn't yet setup. "

View File

@ -0,0 +1,217 @@
/*
* 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.HashSet;
import java.util.Set;
import java.util.StringJoiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import org.apache.cassandra.dht.Datacenters;
import org.apache.cassandra.exceptions.InvalidRequestException;
public abstract class DCPermissions
{
public abstract boolean canAccess(String dc);
public abstract boolean restrictsAccess();
public abstract Set<String> allowedDCs();
public abstract void validate();
private static class SubsetPermissions extends DCPermissions
{
private final Set<String> subset;
public SubsetPermissions(Set<String> subset)
{
Preconditions.checkNotNull(subset);
this.subset = subset;
}
public boolean canAccess(String dc)
{
return subset.contains(dc);
}
public boolean restrictsAccess()
{
return true;
}
public Set<String> allowedDCs()
{
return ImmutableSet.copyOf(subset);
}
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SubsetPermissions that = (SubsetPermissions) o;
return subset.equals(that.subset);
}
public int hashCode()
{
return subset.hashCode();
}
public String toString()
{
StringJoiner joiner = new StringJoiner(", ");
subset.forEach(joiner::add);
return joiner.toString();
}
public void validate()
{
Datacenters.getValidDatacenters();
Set<String> unknownDcs = Sets.difference(subset, Datacenters.getValidDatacenters());
if (!unknownDcs.isEmpty())
{
throw new InvalidRequestException(String.format("Invalid value(s) for DATACENTERS '%s'," +
"All values must be valid datacenters", subset));
}
}
}
private static final DCPermissions ALL = new DCPermissions()
{
public boolean canAccess(String dc)
{
return true;
}
public boolean restrictsAccess()
{
return false;
}
public Set<String> allowedDCs()
{
throw new UnsupportedOperationException();
}
public String toString()
{
return "ALL";
}
public void validate()
{
}
};
private static final DCPermissions NONE = new DCPermissions()
{
public boolean canAccess(String dc)
{
return false;
}
public boolean restrictsAccess()
{
return true;
}
public Set<String> allowedDCs()
{
throw new UnsupportedOperationException();
}
public String toString()
{
return "n/a";
}
public void validate()
{
throw new UnsupportedOperationException();
}
};
public static DCPermissions all()
{
return ALL;
}
public static DCPermissions none()
{
return NONE;
}
public static DCPermissions subset(Set<String> dcs)
{
return new SubsetPermissions(dcs);
}
public static DCPermissions subset(String... dcs)
{
return subset(Sets.newHashSet(dcs));
}
public static class Builder
{
private Set<String> dcs = new HashSet<>();
private boolean isAll = false;
private boolean modified = false;
public void add(String dc)
{
Preconditions.checkArgument(!isAll, "All has been set");
dcs.add(dc);
modified = true;
}
public void all()
{
Preconditions.checkArgument(dcs.isEmpty(), "DCs have already been set");
isAll = true;
modified = true;
}
public boolean isModified()
{
return modified;
}
public DCPermissions build()
{
if (dcs.isEmpty())
{
return DCPermissions.all();
}
else
{
return subset(dcs);
}
}
}
public static Builder builder()
{
return new Builder();
}
}

View File

@ -0,0 +1,63 @@
/*
* 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 org.apache.cassandra.exceptions.ConfigurationException;
/**
* Not part of the roles hierarchy?? How would that even work?
*/
public interface INetworkAuthorizer
{
/**
* Whether or not the authorizer will attempt authorization.
* If false the authorizer will not be called for authorization of resources.
*/
default boolean requireAuthorization()
{
return true;
}
/**
* Setup is called once upon system startup to initialize the INetworkAuthorizer.
*
* For example, use this method to create any required keyspaces/column families.
*/
void setup();
/**
* Returns the dc permissions associated with the given role
*/
DCPermissions authorize(RoleResource role);
void setRoleDatacenters(RoleResource role, DCPermissions permissions);
/**
* Called when a role is deleted, so any corresponding network auth
* data can also be cleaned up
*/
void drop(RoleResource role);
/**
* Validates configuration of IAuthorizer implementation (if configurable).
*
* @throws ConfigurationException when there is a configuration error.
*/
void validateConfiguration() throws ConfigurationException;
}

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.auth;
import org.apache.cassandra.config.DatabaseDescriptor;
/**
* Created by blakeeggleston on 12/14/17.
*/
public class NetworkAuthCache extends AuthCache<RoleResource, DCPermissions> implements AuthCacheMBean
{
public NetworkAuthCache(INetworkAuthorizer authorizer)
{
super("NetworkAuthCache",
DatabaseDescriptor::setRolesValidity,
DatabaseDescriptor::getRolesValidity,
DatabaseDescriptor::setRolesUpdateInterval,
DatabaseDescriptor::getRolesUpdateInterval,
DatabaseDescriptor::setRolesCacheMaxEntries,
DatabaseDescriptor::getRolesCacheMaxEntries,
authorizer::authorize,
() -> DatabaseDescriptor.getAuthenticator().requireAuthentication());
}
}

View File

@ -52,6 +52,7 @@ public class Config
public String authenticator;
public String authorizer;
public String role_manager;
public String network_authorizer;
public volatile int permissions_validity_in_ms = 2000;
public volatile int permissions_cache_max_entries = 1000;
public volatile int permissions_update_interval_in_ms = -1;

View File

@ -41,6 +41,7 @@ import org.apache.cassandra.auth.AuthConfig;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.auth.IAuthorizer;
import org.apache.cassandra.auth.IInternodeAuthenticator;
import org.apache.cassandra.auth.INetworkAuthorizer;
import org.apache.cassandra.auth.IRoleManager;
import org.apache.cassandra.config.Config.CommitLogSync;
import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption;
@ -101,6 +102,7 @@ public class DatabaseDescriptor
private static IAuthenticator authenticator;
private static IAuthorizer authorizer;
private static INetworkAuthorizer networkAuthorizer;
// Don't initialize the role manager until applying config. The options supported by CassandraRoleManager
// depend on the configured IAuthenticator, so defer creating it until that's been set.
private static IRoleManager roleManager;
@ -1066,6 +1068,16 @@ public class DatabaseDescriptor
DatabaseDescriptor.authorizer = authorizer;
}
public static INetworkAuthorizer getNetworkAuthorizer()
{
return networkAuthorizer;
}
public static void setNetworkAuthorizer(INetworkAuthorizer networkAuthorizer)
{
DatabaseDescriptor.networkAuthorizer = networkAuthorizer;
}
public static IRoleManager getRoleManager()
{
return roleManager;

View File

@ -31,18 +31,30 @@ public class AlterRoleStatement extends AuthenticationStatement
{
private final RoleResource role;
private final RoleOptions opts;
final DCPermissions dcPermissions;
public AlterRoleStatement(RoleName name, RoleOptions opts)
{
this(name, opts, null);
}
public AlterRoleStatement(RoleName name, RoleOptions opts, DCPermissions dcPermissions)
{
this.role = RoleResource.role(name.getName());
this.opts = opts;
this.dcPermissions = dcPermissions;
}
public void validate(ClientState state) throws RequestValidationException
{
opts.validate();
if (opts.isEmpty())
if (dcPermissions != null)
{
dcPermissions.validate();
}
if (opts.isEmpty() && dcPermissions == null)
throw new InvalidRequestException("ALTER [ROLE|USER] can't be empty");
// validate login here before checkAccess to avoid leaking user existence to anonymous users.
@ -87,6 +99,8 @@ public class AlterRoleStatement extends AuthenticationStatement
{
if (!opts.isEmpty())
DatabaseDescriptor.getRoleManager().alterRole(state.getUser(), role, opts);
if (dcPermissions != null)
DatabaseDescriptor.getNetworkAuthorizer().setRoleDatacenters(role, dcPermissions);
return null;
}

View File

@ -30,12 +30,14 @@ public class CreateRoleStatement extends AuthenticationStatement
{
private final RoleResource role;
private final RoleOptions opts;
final DCPermissions dcPermissions;
private final boolean ifNotExists;
public CreateRoleStatement(RoleName name, RoleOptions options, boolean ifNotExists)
public CreateRoleStatement(RoleName name, RoleOptions options, DCPermissions dcPermissions, boolean ifNotExists)
{
this.role = RoleResource.role(name.getName());
this.opts = options;
this.dcPermissions = dcPermissions;
this.ifNotExists = ifNotExists;
}
@ -53,6 +55,11 @@ public class CreateRoleStatement extends AuthenticationStatement
{
opts.validate();
if (dcPermissions != null)
{
dcPermissions.validate();
}
if (role.getRoleName().isEmpty())
throw new InvalidRequestException("Role name can't be an empty string");
@ -70,6 +77,10 @@ public class CreateRoleStatement extends AuthenticationStatement
return null;
DatabaseDescriptor.getRoleManager().createRole(state.getUser(), role, opts);
if (dcPermissions.restrictsAccess())
{
DatabaseDescriptor.getNetworkAuthorizer().setRoleDatacenters(role, dcPermissions);
}
grantPermissionsToCreator(state);
return null;
}

View File

@ -72,6 +72,7 @@ public class DropRoleStatement extends AuthenticationStatement
DatabaseDescriptor.getRoleManager().dropRole(state.getUser(), role);
DatabaseDescriptor.getAuthorizer().revokeAllFrom(role);
DatabaseDescriptor.getAuthorizer().revokeAllOn(role);
DatabaseDescriptor.getNetworkAuthorizer().drop(role);
return null;
}

View File

@ -48,7 +48,8 @@ public class ListRolesStatement extends AuthorizationStatement
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),
new ColumnSpecification(KS, CF, new ColumnIdentifier("options", true), optionsType));
new ColumnSpecification(KS, CF, new ColumnIdentifier("options", true), optionsType),
new ColumnSpecification(KS, CF, new ColumnIdentifier("datacenters", true), UTF8Type.instance));
private final RoleResource grantee;
private final boolean recursive;
@ -118,12 +119,14 @@ public class ListRolesStatement extends AuthorizationStatement
ResultSet result = new ResultSet(resultMetadata);
IRoleManager roleManager = DatabaseDescriptor.getRoleManager();
INetworkAuthorizer networkAuthorizer = DatabaseDescriptor.getNetworkAuthorizer();
for (RoleResource role : sortedRoles)
{
result.addColumnValue(UTF8Type.instance.decompose(role.getRoleName()));
result.addColumnValue(BooleanType.instance.decompose(roleManager.isSuper(role)));
result.addColumnValue(BooleanType.instance.decompose(roleManager.canLogin(role)));
result.addColumnValue(optionsType.decompose(roleManager.getCustomOptions(role)));
result.addColumnValue(UTF8Type.instance.decompose(networkAuthorizer.authorize(role).toString()));
}
return new ResultMessage.Rows(result);
}

View File

@ -41,7 +41,8 @@ public class ListUsersStatement extends ListRolesStatement
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));
new ColumnSpecification(KS, CF, new ColumnIdentifier("super", true), BooleanType.instance),
new ColumnSpecification(KS, CF, new ColumnIdentifier("datacenters", true), UTF8Type.instance));
@Override
protected ResultMessage formatResults(List<RoleResource> sortedRoles)
@ -50,12 +51,14 @@ public class ListUsersStatement extends ListRolesStatement
ResultSet result = new ResultSet(resultMetadata);
IRoleManager roleManager = DatabaseDescriptor.getRoleManager();
INetworkAuthorizer networkAuthorizer = DatabaseDescriptor.getNetworkAuthorizer();
for (RoleResource role : sortedRoles)
{
if (!roleManager.canLogin(role))
continue;
result.addColumnValue(UTF8Type.instance.decompose(role.getRoleName()));
result.addColumnValue(BooleanType.instance.decompose(Roles.hasSuperuserStatus(role)));
result.addColumnValue(UTF8Type.instance.decompose(networkAuthorizer.authorize(role).toString()));
}
return new ResultMessage.Rows(result);

View File

@ -0,0 +1,63 @@
/*
* 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.dht;
import java.util.HashSet;
import java.util.Set;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.locator.IEndpointSnitch;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.utils.FBUtilities;
public class Datacenters
{
private static class DCHandle
{
private static final String thisDc = DatabaseDescriptor.getEndpointSnitch().getDatacenter(FBUtilities.getBroadcastAddressAndPort());
}
public static String thisDatacenter()
{
return DCHandle.thisDc;
}
/*
* (non-javadoc) Method to generate list of valid data center names to be used to validate the replication parameters during CREATE / ALTER keyspace operations.
* All peers of current node are fetched from {@link TokenMetadata} and then a set is build by fetching DC name of each peer.
* @return a set of valid DC names
*/
public static Set<String> getValidDatacenters()
{
final Set<String> validDataCenters = new HashSet<>();
final IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch();
// Add data center of localhost.
validDataCenters.add(thisDatacenter());
// Fetch and add DCs of all peers.
for (InetAddressAndPort peer : StorageService.instance.getTokenMetadata().getAllEndpoints())
{
validDataCenters.add(snitch.getDatacenter(peer));
}
return validDataCenters;
}
}

View File

@ -23,11 +23,10 @@ import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.dht.Datacenters;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.locator.TokenMetadata.Topology;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.Pair;
@ -215,31 +214,10 @@ public class NetworkTopologyStrategy extends AbstractReplicationStrategy
return datacenters.keySet();
}
/*
* (non-javadoc) Method to generate list of valid data center names to be used to validate the replication parameters during CREATE / ALTER keyspace operations.
* All peers of current node are fetched from {@link TokenMetadata} and then a set is build by fetching DC name of each peer.
* @return a set of valid DC names
*/
private static Set<String> buildValidDataCentersSet()
{
final Set<String> validDataCenters = new HashSet<>();
final IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch();
// Add data center of localhost.
validDataCenters.add(snitch.getDatacenter(FBUtilities.getBroadcastAddressAndPort()));
// Fetch and add DCs of all peers.
for (final InetAddressAndPort peer : StorageService.instance.getTokenMetadata().getAllEndpoints())
{
validDataCenters.add(snitch.getDatacenter(peer));
}
return validDataCenters;
}
public Collection<String> recognizedOptions()
{
// only valid options are valid DC names.
return buildValidDataCentersSet();
return Datacenters.getValidDatacenters();
}
protected void validateExpectedOptions() throws ConfigurationException

View File

@ -435,7 +435,13 @@ public class ClientState
public void validateLogin() throws UnauthorizedException
{
if (user == null)
{
throw new UnauthorizedException("You have not logged in");
}
else if (!user.hasLocalAccess())
{
throw new UnauthorizedException("You do not have access to this datacenter");
}
}
public void ensureNotAnonymous() throws UnauthorizedException

View File

@ -1093,6 +1093,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
DatabaseDescriptor.getRoleManager().setup();
DatabaseDescriptor.getAuthenticator().setup();
DatabaseDescriptor.getAuthorizer().setup();
DatabaseDescriptor.getNetworkAuthorizer().setup();
Schema.instance.registerListener(new AuthSchemaChangeListener());
authSetupComplete = true;
}

View File

@ -39,8 +39,10 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.AllowAllNetworkAuthorizer;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.auth.IAuthorizer;
import org.apache.cassandra.auth.INetworkAuthorizer;
import org.apache.cassandra.auth.IRoleManager;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.DecoratedKey;
@ -512,6 +514,19 @@ public class FBUtilities
return FBUtilities.construct(className, "role manager");
}
public static INetworkAuthorizer newNetworkAuthorizer(String className)
{
if (className == null)
{
return new AllowAllNetworkAuthorizer();
}
if (!className.contains("."))
{
className = "org.apache.cassandra.auth." + className;
}
return FBUtilities.construct(className, "network authorizer");
}
/**
* @return The Class for the given name.
* @param classname Fully qualified classname.

View File

@ -21,6 +21,12 @@ import java.io.File;
import java.io.IOException;
import java.util.*;
import org.apache.cassandra.auth.AuthKeyspace;
import org.apache.cassandra.auth.AuthSchemaChangeListener;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.auth.IAuthorizer;
import org.apache.cassandra.auth.INetworkAuthorizer;
import org.apache.cassandra.auth.IRoleManager;
import org.apache.cassandra.cql3.statements.CreateTableStatement;
import org.apache.cassandra.dht.Murmur3Partitioner;
import org.apache.cassandra.index.sasi.SASIIndex;
@ -272,6 +278,20 @@ public class SchemaLoader
MigrationManager.announceNewKeyspace(KeyspaceMetadata.create(name, params, tables, Views.none(), types, Functions.none()), true);
}
public static void setupAuth(IRoleManager roleManager, IAuthenticator authenticator, IAuthorizer authorizer, INetworkAuthorizer networkAuthorizer)
{
DatabaseDescriptor.setRoleManager(roleManager);
DatabaseDescriptor.setAuthenticator(authenticator);
DatabaseDescriptor.setAuthorizer(authorizer);
DatabaseDescriptor.setNetworkAuthorizer(networkAuthorizer);
MigrationManager.announceNewKeyspace(AuthKeyspace.metadata(), true);
DatabaseDescriptor.getRoleManager().setup();
DatabaseDescriptor.getAuthenticator().setup();
DatabaseDescriptor.getAuthorizer().setup();
DatabaseDescriptor.getNetworkAuthorizer().setup();
Schema.instance.registerListener(new AuthSchemaChangeListener());
}
public static ColumnMetadata integerColumn(String ksName, String cfName)
{
return new ColumnMetadata(ksName,

View File

@ -0,0 +1,259 @@
/*
* 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 com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import org.apache.commons.lang.RandomStringUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.apache.cassandra.SchemaLoader;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.cql3.statements.AlterRoleStatement;
import org.apache.cassandra.cql3.statements.AuthenticationStatement;
import org.apache.cassandra.cql3.statements.BatchStatement;
import org.apache.cassandra.cql3.statements.CreateRoleStatement;
import org.apache.cassandra.cql3.statements.DropRoleStatement;
import org.apache.cassandra.cql3.statements.SelectStatement;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.messages.ResultMessage;
import static org.apache.cassandra.auth.AuthKeyspace.NETWORK_PERMISSIONS;
import static org.apache.cassandra.schema.SchemaConstants.AUTH_KEYSPACE_NAME;
public class CassandraNetworkAuthorizerTest
{
private static class LocalCassandraAuthorizer extends CassandraAuthorizer
{
ResultMessage.Rows select(SelectStatement statement, QueryOptions options)
{
return statement.executeInternal(QueryState.forInternalCalls(), options);
}
UntypedResultSet process(String query) throws RequestExecutionException
{
return QueryProcessor.executeInternal(query);
}
@Override
void processBatch(BatchStatement statement)
{
statement.executeInternal(QueryState.forInternalCalls(), QueryOptions.DEFAULT);
}
}
private static class LocalCassandraRoleManager extends CassandraRoleManager
{
ResultMessage.Rows select(SelectStatement statement, QueryOptions options)
{
return statement.executeInternal(QueryState.forInternalCalls(), options);
}
UntypedResultSet process(String query, ConsistencyLevel consistencyLevel) throws RequestValidationException, RequestExecutionException
{
return QueryProcessor.executeInternal(query);
}
}
private static class LocalCassandraNetworkAuthorizer extends CassandraNetworkAuthorizer
{
ResultMessage.Rows select(SelectStatement statement, QueryOptions options)
{
return statement.executeInternal(QueryState.forInternalCalls(), options);
}
void process(String query)
{
QueryProcessor.executeInternal(query);
}
}
private static void setupSuperUser()
{
QueryProcessor.executeInternal(String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) "
+ "VALUES ('%s', true, true, '%s')",
AUTH_KEYSPACE_NAME,
AuthKeyspace.ROLES,
CassandraRoleManager.DEFAULT_SUPERUSER_NAME,
"xxx"));
}
@BeforeClass
public static void defineSchema() throws ConfigurationException
{
SchemaLoader.prepareServer();
SchemaLoader.setupAuth(new LocalCassandraRoleManager(),
new PasswordAuthenticator(),
new LocalCassandraAuthorizer(),
new LocalCassandraNetworkAuthorizer());
setupSuperUser();
}
@Before
public void clear() throws Exception
{
Keyspace.open(AUTH_KEYSPACE_NAME).getColumnFamilyStore(NETWORK_PERMISSIONS).truncateBlocking();
}
private static UntypedResultSet query(String q)
{
return QueryProcessor.executeInternal(q);
}
private static void assertNoDcPermRow(String username)
{
String query = String.format("SELECT dcs FROM %s.%s WHERE role = '%s'",
AUTH_KEYSPACE_NAME,
NETWORK_PERMISSIONS,
RoleResource.role(username).getName());
UntypedResultSet results = QueryProcessor.executeInternal(query);
Assert.assertTrue(results.isEmpty());
}
private static void assertDcPermRow(String username, String... dcs)
{
Set<String> expected = Sets.newHashSet(dcs);
String query = String.format("SELECT dcs FROM %s.%s WHERE role = '%s'",
AUTH_KEYSPACE_NAME,
NETWORK_PERMISSIONS,
RoleResource.role(username).getName());
UntypedResultSet results = QueryProcessor.executeInternal(query);
UntypedResultSet.Row row = Iterables.getOnlyElement(results);
Set<String> actual = row.getFrozenSet("dcs", UTF8Type.instance);
Assert.assertEquals(expected, actual);
}
private static String createName()
{
return RandomStringUtils.randomAlphabetic(8).toLowerCase();
}
private static ClientState getClientState()
{
ClientState state = ClientState.forInternalCalls();
state.login(new AuthenticatedUser(CassandraRoleManager.DEFAULT_SUPERUSER_NAME));
return state;
}
private static void auth(String query, Object... args)
{
CQLStatement statement = QueryProcessor.parseStatement(String.format(query, args)).prepare().statement;
assert statement instanceof CreateRoleStatement
|| statement instanceof AlterRoleStatement
|| statement instanceof DropRoleStatement;
AuthenticationStatement authStmt = (AuthenticationStatement) statement;
authStmt.execute(getClientState());
}
private static DCPermissions dcPerms(String username)
{
AuthenticatedUser user = new AuthenticatedUser(username);
return DatabaseDescriptor.getNetworkAuthorizer().authorize(user.getPrimaryRole());
}
@Test
public void create() throws Exception
{
String username = createName();
// user should implicitly have access to all datacenters
assertNoDcPermRow(username);
auth("CREATE ROLE %s WITH password = 'password' AND LOGIN = true AND ACCESS TO DATACENTERS {'dc1', 'dc2'}", username);
Assert.assertEquals(DCPermissions.subset("dc1", "dc2"), dcPerms(username));
assertDcPermRow(username, "dc1", "dc2");
}
@Test
public void alter() throws Exception
{
String username = createName();
assertNoDcPermRow(username);
// user should implicitly have access to all datacenters
auth("CREATE ROLE %s WITH password = 'password' AND LOGIN = true", username);
Assert.assertEquals(DCPermissions.all(), dcPerms(username));
assertNoDcPermRow(username);
// unless explicitly restricted
auth("ALTER ROLE %s WITH ACCESS TO DATACENTERS {'dc1', 'dc2'}", username);
Assert.assertEquals(DCPermissions.subset("dc1", "dc2"), dcPerms(username));
assertDcPermRow(username, "dc1", "dc2");
auth("ALTER ROLE %s WITH ACCESS TO DATACENTERS {'dc1'}", username);
Assert.assertEquals(DCPermissions.subset("dc1"), dcPerms(username));
assertDcPermRow(username, "dc1");
auth("ALTER ROLE %s WITH ACCESS TO ALL DATACENTERS", username);
Assert.assertEquals(DCPermissions.all(), dcPerms(username));
assertDcPermRow(username);
}
@Test
public void drop()
{
String username = createName();
assertNoDcPermRow(username);
// user should implicitly have access to all datacenters
auth("CREATE ROLE %s WITH password = 'password' AND LOGIN = true AND ACCESS TO DATACENTERS {'dc1'}", username);
assertDcPermRow(username, "dc1");
auth("DROP ROLE %s", username);
assertNoDcPermRow(username);
}
@Test
public void superUser() throws Exception
{
String username = createName();
auth("CREATE ROLE %s WITH password = 'password' AND LOGIN = true AND ACCESS TO DATACENTERS {'dc1'}", username);
Assert.assertEquals(DCPermissions.subset("dc1"), dcPerms(username));
assertDcPermRow(username, "dc1");
auth("ALTER ROLE %s WITH superuser = true", username);
Assert.assertEquals(DCPermissions.all(), dcPerms(username));
}
@Test
public void cantLogin() throws Exception
{
String username = createName();
auth("CREATE ROLE %s", username);
Assert.assertEquals(DCPermissions.none(), dcPerms(username));
}
}

View File

@ -59,6 +59,7 @@ public class DatabaseDescriptorRefTest
"org.apache.cassandra.auth.IAuthenticator",
"org.apache.cassandra.auth.IAuthorizer",
"org.apache.cassandra.auth.IRoleManager",
"org.apache.cassandra.auth.INetworkAuthorizer",
"org.apache.cassandra.config.DatabaseDescriptor",
"org.apache.cassandra.config.ConfigurationLoader",
"org.apache.cassandra.config.Config",

View File

@ -0,0 +1,73 @@
/*
* 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 com.google.common.collect.Sets;
import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.auth.DCPermissions;
import org.apache.cassandra.cql3.QueryProcessor;
public class AlterRoleStatementTest
{
private static AlterRoleStatement parse(String query)
{
ParsedStatement stmt = QueryProcessor.parseStatement(query);
Assert.assertTrue(stmt instanceof AlterRoleStatement);
return (AlterRoleStatement) stmt;
}
private static DCPermissions dcPerms(String query)
{
return parse(query).dcPermissions;
}
@Test
public void dcsNotSpecified() throws Exception
{
Assert.assertNull(dcPerms("ALTER ROLE r1 WITH PASSWORD = 'password'"));
}
@Test
public void dcsAllSpecified() throws Exception
{
DCPermissions dcPerms = dcPerms("ALTER ROLE r1 WITH ACCESS TO ALL DATACENTERS");
Assert.assertNotNull(dcPerms);
Assert.assertFalse(dcPerms.restrictsAccess());
}
@Test
public void singleDc() throws Exception
{
DCPermissions dcPerms = dcPerms("ALTER ROLE r1 WITH ACCESS TO DATACENTERS {'dc1'}");
Assert.assertNotNull(dcPerms);
Assert.assertTrue(dcPerms.restrictsAccess());
Assert.assertEquals(Sets.newHashSet("dc1"), dcPerms.allowedDCs());
}
@Test
public void multiDcs() throws Exception
{
DCPermissions dcPerms = dcPerms("ALTER ROLE r1 WITH ACCESS TO DATACENTERS {'dc1', 'dc2'}");
Assert.assertNotNull(dcPerms);
Assert.assertTrue(dcPerms.restrictsAccess());
Assert.assertEquals(Sets.newHashSet("dc1", "dc2"), dcPerms.allowedDCs());
}
}

View File

@ -0,0 +1,72 @@
/*
* 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 com.google.common.collect.Sets;
import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.auth.DCPermissions;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.QueryProcessor;
public class CreateRoleStatementTest extends CQLTester
{
private static CreateRoleStatement parse(String query)
{
ParsedStatement stmt = QueryProcessor.parseStatement(query);
Assert.assertTrue(stmt instanceof CreateRoleStatement);
return (CreateRoleStatement) stmt;
}
private static DCPermissions dcPerms(String query)
{
return parse(query).dcPermissions;
}
@Test
public void allDcsImplicit() throws Exception
{
Assert.assertFalse(dcPerms("CREATE ROLE role").restrictsAccess());
}
@Test
public void allDcsExplicit() throws Exception
{
Assert.assertFalse(dcPerms("CREATE ROLE role WITH ACCESS TO ALL DATACENTERS").restrictsAccess());
}
@Test
public void singleDc() throws Exception
{
DCPermissions perms = dcPerms("CREATE ROLE role WITH ACCESS TO DATACENTERS {'dc1'}");
Assert.assertTrue(perms.restrictsAccess());
Assert.assertEquals(Sets.newHashSet("dc1"), perms.allowedDCs());
}
@Test
public void multiDcs() throws Exception
{
DCPermissions perms = dcPerms("CREATE ROLE role WITH ACCESS TO DATACENTERS {'dc1', 'dc2'}");
Assert.assertTrue(perms.restrictsAccess());
Assert.assertEquals(Sets.newHashSet("dc1", "dc2"), perms.allowedDCs());
}
}

View File

@ -0,0 +1,46 @@
/*
* 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.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.auth.DCPermissions;
import org.apache.cassandra.cql3.QueryProcessor;
public class CreateUserStatementTest
{
private static CreateRoleStatement parse(String query)
{
ParsedStatement stmt = QueryProcessor.parseStatement(query);
Assert.assertTrue(stmt instanceof CreateRoleStatement);
return (CreateRoleStatement) stmt;
}
private static DCPermissions dcPerms(String query)
{
return parse(query).dcPermissions;
}
@Test
public void allDcsImplicit() throws Exception
{
Assert.assertFalse(dcPerms("CREATE USER u1").restrictsAccess());
}
}

View File

@ -40,6 +40,7 @@ import org.apache.cassandra.locator.AbstractEndpointSnitch;
import org.apache.cassandra.locator.IEndpointSnitch;
import org.apache.cassandra.io.util.DataOutputBuffer;
import org.apache.cassandra.schema.SchemaKeyspace;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.triggers.ITrigger;
import org.apache.cassandra.utils.ByteBufferUtil;
@ -49,7 +50,6 @@ import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import static org.apache.cassandra.cql3.Duration.*;
import static org.junit.Assert.assertEquals;
public class CreateTest extends CQLTester
{
@ -523,6 +523,9 @@ public class CreateTest extends CQLTester
public int compareEndpoints(InetAddressAndPort target, InetAddressAndPort a1, InetAddressAndPort a2) { return 0; }
});
// this forces the dc above to be added to the list of known datacenters (fixes static init problem
// with this group of tests), ok to remove at some point if doing so doesn't break the test
StorageService.instance.getTokenMetadata().updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.0.0.255"));
execute("CREATE KEYSPACE Foo WITH replication = { 'class' : 'NetworkTopologyStrategy', 'us-east-1' : 1 };");
// Restore the previous EndpointSnitch