| /* |
| * 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.Callable; |
| 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.Config; |
| 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.net.MessagingService; |
| 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, maintenance and deletion 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 overridden 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_ROUNDS rounds of hashing will be performed. |
| private static final String GENSALT_LOG2_ROUNDS_PROPERTY = Config.PROPERTY_PREFIX + "auth_bcrypt_gensalt_log2_rounds"; |
| private static final int GENSALT_LOG2_ROUNDS = getGensaltLogRounds(); |
| |
| static int getGensaltLogRounds() |
| { |
| int rounds = Integer.getInteger(GENSALT_LOG2_ROUNDS_PROPERTY, 10); |
| if (rounds < 4 || rounds > 31) |
| throw new ConfigurationException(String.format("Bad value for system property -D%s." + |
| "Please use a value between 4 and 31 inclusively", |
| GENSALT_LOG2_ROUNDS_PROPERTY)); |
| return rounds; |
| } |
| |
| // 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; |
| |
| // Will be set to true when all nodes in the cluster are on a version which supports roles (i.e. 2.2+) |
| private volatile boolean isClusterReady = false; |
| |
| 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(() -> { |
| convertLegacyData(); |
| return null; |
| }); |
| } |
| else |
| { |
| scheduleSetupTask(() -> { |
| setupDefaultRole(); |
| return null; |
| }); |
| } |
| } |
| |
| public Set<Option> supportedOptions() |
| { |
| return supportedOptions; |
| } |
| |
| public Set<Option> alterableOptions() |
| { |
| return alterableOptions; |
| } |
| |
| public void createRole(AuthenticatedUser performer, RoleResource role, RoleOptions options) |
| throws RequestValidationException, RequestExecutionException |
| { |
| String insertCql = options.getPassword().isPresent() |
| ? String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) VALUES ('%s', %s, %s, '%s')", |
| AuthKeyspace.NAME, |
| AuthKeyspace.ROLES, |
| escape(role.getRoleName()), |
| options.getSuperuser().or(false), |
| options.getLogin().or(false), |
| escape(hashpw(options.getPassword().get()))) |
| : String.format("INSERT INTO %s.%s (role, is_superuser, can_login) VALUES ('%s', %s, %s)", |
| AuthKeyspace.NAME, |
| AuthKeyspace.ROLES, |
| escape(role.getRoleName()), |
| options.getSuperuser().or(false), |
| options.getLogin().or(false)); |
| process(insertCql, consistencyForRole(role.getRoleName())); |
| } |
| |
| public void dropRole(AuthenticatedUser performer, RoleResource role) throws RequestValidationException, RequestExecutionException |
| { |
| process(String.format("DELETE FROM %s.%s WHERE role = '%s'", |
| AuthKeyspace.NAME, |
| AuthKeyspace.ROLES, |
| escape(role.getRoleName())), |
| consistencyForRole(role.getRoleName())); |
| removeAllMembers(role.getRoleName()); |
| } |
| |
| public void alterRole(AuthenticatedUser performer, RoleResource role, RoleOptions options) |
| { |
| // 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.getOptions()), |
| Predicates.notNull())); |
| if (!Strings.isNullOrEmpty(assignments)) |
| { |
| process(String.format("UPDATE %s.%s SET %s WHERE role = '%s'", |
| AuthKeyspace.NAME, |
| AuthKeyspace.ROLES, |
| assignments, |
| escape(role.getRoleName())), |
| consistencyForRole(role.getRoleName())); |
| } |
| } |
| |
| public void grantRole(AuthenticatedUser performer, RoleResource role, RoleResource grantee) |
| throws RequestValidationException, RequestExecutionException |
| { |
| if (getRoles(grantee, true).contains(role)) |
| throw new InvalidRequestException(String.format("%s is a member of %s", |
| grantee.getRoleName(), |
| role.getRoleName())); |
| if (getRoles(role, true).contains(grantee)) |
| throw new InvalidRequestException(String.format("%s is a member of %s", |
| role.getRoleName(), |
| grantee.getRoleName())); |
| |
| modifyRoleMembership(grantee.getRoleName(), role.getRoleName(), "+"); |
| process(String.format("INSERT INTO %s.%s (role, member) values ('%s', '%s')", |
| AuthKeyspace.NAME, |
| AuthKeyspace.ROLE_MEMBERS, |
| escape(role.getRoleName()), |
| escape(grantee.getRoleName())), |
| consistencyForRole(role.getRoleName())); |
| } |
| |
| public void revokeRole(AuthenticatedUser performer, RoleResource role, RoleResource revokee) |
| throws RequestValidationException, RequestExecutionException |
| { |
| if (!getRoles(revokee, false).contains(role)) |
| throw new InvalidRequestException(String.format("%s is not a member of %s", |
| revokee.getRoleName(), |
| role.getRoleName())); |
| |
| modifyRoleMembership(revokee.getRoleName(), role.getRoleName(), "-"); |
| process(String.format("DELETE FROM %s.%s WHERE role = '%s' and member = '%s'", |
| AuthKeyspace.NAME, |
| AuthKeyspace.ROLE_MEMBERS, |
| escape(role.getRoleName()), |
| escape(revokee.getRoleName())), |
| consistencyForRole(role.getRoleName())); |
| } |
| |
| public Set<RoleResource> getRoles(RoleResource grantee, boolean includeInherited) throws RequestValidationException, RequestExecutionException |
| { |
| Set<RoleResource> roles = new HashSet<>(); |
| Role role = getRole(grantee.getRoleName()); |
| if (!role.equals(NULL_ROLE)) |
| { |
| roles.add(RoleResource.role(role.name)); |
| collectRoles(role, roles, includeInherited); |
| } |
| return roles; |
| } |
| |
| public Set<RoleResource> getAllRoles() throws RequestValidationException, RequestExecutionException |
| { |
| UntypedResultSet rows = process(String.format("SELECT role from %s.%s", AuthKeyspace.NAME, AuthKeyspace.ROLES), ConsistencyLevel.QUORUM); |
| Iterable<RoleResource> roles = Iterables.transform(rows, new Function<UntypedResultSet.Row, RoleResource>() |
| { |
| public RoleResource apply(UntypedResultSet.Row row) |
| { |
| return RoleResource.role(row.getString("role")); |
| } |
| }); |
| return ImmutableSet.<RoleResource>builder().addAll(roles).build(); |
| } |
| |
| public boolean isSuper(RoleResource role) |
| { |
| return getRole(role.getRoleName()).isSuper; |
| } |
| |
| public boolean canLogin(RoleResource role) |
| { |
| return getRole(role.getRoleName()).canLogin; |
| } |
| |
| public Map<String, String> getCustomOptions(RoleResource role) |
| { |
| return Collections.emptyMap(); |
| } |
| |
| public boolean isExistingRole(RoleResource role) |
| { |
| return getRole(role.getRoleName()) != 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"); |
| throw e; |
| } |
| } |
| |
| 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 !QueryProcessor.process(defaultSUQuery, ConsistencyLevel.ONE).isEmpty() |
| || !QueryProcessor.process(defaultSUQuery, ConsistencyLevel.QUORUM).isEmpty() |
| || !QueryProcessor.process(allUsersQuery, ConsistencyLevel.QUORUM).isEmpty(); |
| } |
| |
| private void scheduleSetupTask(final Callable<Void> setupTask) |
| { |
| // The delay is to give the node a chance to see its peers before attempting the operation |
| ScheduledExecutors.optionalTasks.schedule(new Runnable() |
| { |
| public void run() |
| { |
| // If not all nodes are on 2.2, we don't want to initialize the role manager as this will confuse 2.1 |
| // nodes (see CASSANDRA-9761 for details). So we re-schedule the setup for later, hoping that the upgrade |
| // will be finished by then. |
| if (!MessagingService.instance().areAllNodesAtLeast22()) |
| { |
| logger.trace("Not all nodes are upgraded to a version that supports Roles yet, rescheduling setup task"); |
| scheduleSetupTask(setupTask); |
| return; |
| } |
| |
| isClusterReady = true; |
| try |
| { |
| setupTask.call(); |
| } |
| catch (Exception e) |
| { |
| logger.info("Setup task failed with error, rescheduling"); |
| scheduleSetupTask(setupTask); |
| } |
| } |
| }, 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() throws Exception |
| { |
| 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) |
| { |
| RoleOptions options = new RoleOptions(); |
| options.setOption(Option.SUPERUSER, row.getBoolean("super")); |
| options.setOption(Option.LOGIN, true); |
| createRole(null, RoleResource.role(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.trace("Conversion error", e); |
| throw 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<RoleResource> collected, boolean includeInherited) throws RequestValidationException, RequestExecutionException |
| { |
| for (String memberOf : role.memberOf) |
| { |
| Role granted = getRole(memberOf); |
| if (granted.equals(NULL_ROLE)) |
| continue; |
| collected.add(RoleResource.role(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, "'", "''"); |
| } |
| |
| /** |
| * Executes the provided query. |
| * 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 |
| { |
| if (!isClusterReady) |
| throw new InvalidRequestException("Cannot process role related query as the role manager isn't yet setup. " |
| + "This is likely because some of nodes in the cluster are on version 2.1 or earlier. " |
| + "You need to upgrade all nodes to Cassandra 2.2 or more to use roles."); |
| |
| 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); |
| } |
| } |
| } |