mirror of https://github.com/apache/cassandra
Add CQL3-based implementations of IAuthenticator and IAuthorizer
Patch by Aleksey Yeschenko; reviewed by Jonathan Ellis for CASSANDRA-4898
This commit is contained in:
parent
ee0be0624b
commit
0b83682b40
|
|
@ -20,6 +20,8 @@
|
|||
* Simplify auth setup and make system_auth ks alterable (CASSANDRA-5112)
|
||||
* Stop compactions from hanging during bootstrap (CASSANDRA-5244)
|
||||
* fix compressed streaming sending extra chunk (CASSANDRA-5105)
|
||||
* Add CQL3-based implementations of IAuthenticator and IAuthorizer
|
||||
(CASSANDRA-4898)
|
||||
|
||||
|
||||
1.2.1
|
||||
|
|
|
|||
17
NEWS.txt
17
NEWS.txt
|
|
@ -21,6 +21,23 @@ Upgrading
|
|||
favor of blob constants) and its support will be removed in a future
|
||||
version.
|
||||
|
||||
Features
|
||||
--------
|
||||
- Built-in CQL3-based implementations of IAuthenticator (PasswordAuthenticator)
|
||||
and IAuthorizer (CassandraAuthorizer) have been added. PasswordAuthenticator
|
||||
stores usernames and hashed passwords in system_auth.credentials table;
|
||||
CassandraAuthorizer stores permissions in system_auth.permissions table.
|
||||
- system_auth keyspace is now alterable via ALTER KEYSPACE queries.
|
||||
The default is SimpleStrategy with replication_factor of 1, but it's
|
||||
advised to raise RF to at least 3 or 5, since CL.QUORUM is used for all
|
||||
auth-related queries. It's also possible to change the strategy to NTS.
|
||||
- Permissions caching with time-based expiration policy has been added to reduce
|
||||
performance impact of authorization. Permission validity can be configured
|
||||
using 'permissions_validity_in_ms' setting in cassandra.yaml. The default
|
||||
is 2000 (2 seconds).
|
||||
- SimpleAuthenticator and SimpleAuthorizer examples have been removed. Please
|
||||
look at CassandraAuthorizer/PasswordAuthenticator instead.
|
||||
|
||||
|
||||
1.2.1
|
||||
=====
|
||||
|
|
|
|||
|
|
@ -379,6 +379,7 @@
|
|||
<dependency groupId="org.apache.cassandra" artifactId="cassandra-thrift" version="${version}" />
|
||||
<dependency groupId="com.yammer.metrics" artifactId="metrics-core" version="2.0.3" />
|
||||
<dependency groupId="edu.stanford.ppl" artifactId="snaptree" version="0.1" />
|
||||
<dependency groupId="org.mindrot" artifactId="jbcrypt" version="0.3m" />
|
||||
</dependencyManagement>
|
||||
<developer id="alakshman" name="Avinash Lakshman"/>
|
||||
<developer id="antelder" name="Anthony Elder"/>
|
||||
|
|
@ -461,6 +462,7 @@
|
|||
<dependency groupId="com.github.stephenc.high-scale-lib" artifactId="high-scale-lib"/>
|
||||
<dependency groupId="org.yaml" artifactId="snakeyaml"/>
|
||||
<dependency groupId="edu.stanford.ppl" artifactId="snaptree"/>
|
||||
<dependency groupId="org.mindrot" artifactId="jbcrypt"/>
|
||||
<dependency groupId="com.yammer.metrics" artifactId="metrics-core"/>
|
||||
|
||||
<dependency groupId="log4j" artifactId="log4j"/>
|
||||
|
|
@ -1041,7 +1043,6 @@
|
|||
<formatter type="xml" usefile="true"/>
|
||||
<formatter type="brief" usefile="false"/>
|
||||
<jvmarg value="-Dstorage-config=${test.conf}"/>
|
||||
<jvmarg value="-Daccess.properties=${test.conf}/access.properties"/>
|
||||
<jvmarg value="-Dlog4j.configuration=log4j-junit.properties" />
|
||||
<jvmarg value="-javaagent:${basedir}/lib/jamm-0.2.5.jar" />
|
||||
<jvmarg value="-ea"/>
|
||||
|
|
|
|||
|
|
@ -54,15 +54,29 @@ max_hints_delivery_threads: 2
|
|||
# Defaults to: false
|
||||
# populate_io_cache_on_flush: false
|
||||
|
||||
# authentication backend, implementing IAuthenticator; used to identify users
|
||||
# Authentication backend, implementing IAuthenticator; used to identify users
|
||||
# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthenticator,
|
||||
# PasswordAuthenticator}.
|
||||
#
|
||||
# - AllowAllAuthenticator performs no checks - set it to disable authentication.
|
||||
# - PasswordAuthenticator relies on username/password pairs to authenticate
|
||||
# users. It keeps usernames and hashed passwords in system_auth.credentials table.
|
||||
# Please increase system_auth keyspace replication factor if you use this authenticator.
|
||||
authenticator: org.apache.cassandra.auth.AllowAllAuthenticator
|
||||
|
||||
# authorization backend, implementing IAuthorizer; used to limit access/provide permissions
|
||||
# Authorization backend, implementing IAuthorizer; used to limit access/provide permissions
|
||||
# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthorizer,
|
||||
# CassandraAuthorizer}.
|
||||
#
|
||||
# - AllowAllAuthorizer allows any action to any user - set it to disable authorization.
|
||||
# - CassandraAuthorizer stores permissions in system_auth.permissions table. Please
|
||||
# increase system_auth keyspace replication factor if you use this authorizer.
|
||||
authorizer: org.apache.cassandra.auth.AllowAllAuthorizer
|
||||
|
||||
# Validity period for permissions cache (fetching permissions can be an
|
||||
# expensive operation depending on the authorizer). Defaults to 2000,
|
||||
# set to 0 to disable. Will be disabled automatically for AllowAllAuthorizer.
|
||||
# expensive operation depending on the authorizer, CassandraAuthorizer is
|
||||
# one example). Defaults to 2000, set to 0 to disable.
|
||||
# Will be disabled automatically for AllowAllAuthorizer.
|
||||
permissions_validity_in_ms: 2000
|
||||
|
||||
# The partitioner is responsible for distributing rows (by key) across
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
The files in this directory provide a (simplistic) example of how to add
|
||||
authentication and resource permissions to Cassandra by implementing the
|
||||
org.apache.cassandra.auth.{IAuthenticator, IAuthorizer} interfaces.
|
||||
|
||||
To try those examples, copy the two JAVA sources (in src/) into the main
|
||||
cassandra sources directory and the two configuration files (in conf/) in the
|
||||
main cassandra configuration directory.
|
||||
|
||||
You can then set the authenticator and authorizer properties in cassandra.yaml
|
||||
to use those classes. See the two configuration files access.properties and
|
||||
passwd.properties to configure the authorized users and permissions.
|
||||
|
||||
When starting cassandra, you need to specify the location of the passwd.properties
|
||||
and access.properties files by adding JVM args similar to the following either
|
||||
in cassandra-env.sh or as commandline arguments:
|
||||
|
||||
-Dpasswd.properties=conf/passwd.properties
|
||||
-Daccess.properties=conf/access.properties
|
||||
|
||||
For example, you might invoke cassandra as follows:
|
||||
|
||||
bin/cassandra -f -Dpasswd.properties=conf/passwd.properties -Daccess.properties=conf/access.properties
|
||||
|
||||
Please note that the code in this directory is for demonstration purposes. In
|
||||
particular, it does not provide a high level of security.
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# This is a sample access file for SimpleAuthorizer. The format of this file
|
||||
# is KEYSPACE[.COLUMNFAMILY].PERMISSION=USERS, where:
|
||||
#
|
||||
# * KEYSPACE is the keyspace name.
|
||||
# * COLUMNFAMILY is the column family name.
|
||||
# * PERMISSION is one of <ro> or <rw> for read-only or read-write respectively.
|
||||
# * USERS is a comma delimited list of users from passwd.properties.
|
||||
#
|
||||
# See below for example entries.
|
||||
|
||||
# NOTE: This file contains potentially sensitive information, please keep
|
||||
# this in mind when setting its mode and ownership.
|
||||
|
||||
# The magical '<modify-keyspaces>' property lists users who can modify the
|
||||
# list of keyspaces: all users will be able to view the list of keyspaces.
|
||||
<modify-keyspaces>=jsmith
|
||||
|
||||
# Access to Keyspace1 (add/remove column families, etc).
|
||||
Keyspace1.<ro>=jsmith,Elvis Presley
|
||||
Keyspace1.<rw>=dilbert
|
||||
|
||||
# Access to Standard1 (keyspace Keyspace1)
|
||||
Keyspace1.Standard1.<rw>=jsmith,Elvis Presley,dilbert
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# This is a sample password file for SimpleAuthenticator. The format of
|
||||
# this file is username=password. If -Dpasswd.mode=MD5 then the password
|
||||
# is represented as an md5 digest, otherwise it is cleartext (keep this
|
||||
# in mind when setting file mode and ownership). 'cassandra' is the default
|
||||
# superuser and can be removed later.
|
||||
cassandra=cassandra
|
||||
jsmith=havebadpass
|
||||
dilbert=nomoovertime
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
package org.apache.cassandra.auth;
|
||||
/*
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.cassandra.exceptions.AuthenticationException;
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.io.util.FileUtils;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
import org.apache.cassandra.utils.Hex;
|
||||
|
||||
public class SimpleAuthenticator extends LegacyAuthenticator
|
||||
{
|
||||
public final static String PASSWD_FILENAME_PROPERTY = "passwd.properties";
|
||||
public final static String PMODE_PROPERTY = "passwd.mode";
|
||||
|
||||
public enum PasswordMode
|
||||
{
|
||||
PLAIN, MD5,
|
||||
}
|
||||
|
||||
public AuthenticatedUser defaultUser()
|
||||
{
|
||||
// users must log in
|
||||
return null;
|
||||
}
|
||||
|
||||
public AuthenticatedUser authenticate(Map<String, String> credentials) throws AuthenticationException
|
||||
{
|
||||
String pmode_plain = System.getProperty(PMODE_PROPERTY);
|
||||
PasswordMode mode = PasswordMode.PLAIN;
|
||||
|
||||
if (pmode_plain != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
mode = PasswordMode.valueOf(pmode_plain);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// this is not worth a StringBuffer
|
||||
String mode_values = "";
|
||||
for (PasswordMode pm : PasswordMode.values())
|
||||
mode_values += "'" + pm + "', ";
|
||||
|
||||
mode_values += "or leave it unspecified.";
|
||||
throw new AuthenticationException("The requested password check mode '" + pmode_plain + "' is not a valid mode. Possible values are " + mode_values);
|
||||
}
|
||||
}
|
||||
|
||||
String pfilename = System.getProperty(PASSWD_FILENAME_PROPERTY);
|
||||
|
||||
String username = credentials.get(USERNAME_KEY);
|
||||
if (username == null)
|
||||
throw new AuthenticationException("Authentication request was missing the required key '" + USERNAME_KEY + "'");
|
||||
|
||||
String password = credentials.get(PASSWORD_KEY);
|
||||
if (password == null)
|
||||
throw new AuthenticationException("Authentication request was missing the required key '" + PASSWORD_KEY + "'");
|
||||
|
||||
boolean authenticated = false;
|
||||
|
||||
InputStream in = null;
|
||||
try
|
||||
{
|
||||
in = new BufferedInputStream(new FileInputStream(pfilename));
|
||||
Properties props = new Properties();
|
||||
props.load(in);
|
||||
|
||||
// note we keep the message here and for the wrong password exactly the same to prevent attackers from guessing what users are valid
|
||||
if (props.getProperty(username) == null) throw new AuthenticationException(authenticationErrorMessage(mode, username));
|
||||
switch (mode)
|
||||
{
|
||||
case PLAIN:
|
||||
authenticated = password.equals(props.getProperty(username));
|
||||
break;
|
||||
case MD5:
|
||||
authenticated = MessageDigest.isEqual(FBUtilities.threadLocalMD5Digest().digest(password.getBytes()), Hex.hexToBytes(props.getProperty(username)));
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unknown PasswordMode " + mode);
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException("Authentication table file given by property " + PASSWD_FILENAME_PROPERTY + " could not be opened: " + e.getMessage());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException("Unexpected authentication problem", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
FileUtils.closeQuietly(in);
|
||||
}
|
||||
|
||||
if (!authenticated) throw new AuthenticationException(authenticationErrorMessage(mode, username));
|
||||
|
||||
return new AuthenticatedUser(username);
|
||||
}
|
||||
|
||||
public void validateConfiguration() throws ConfigurationException
|
||||
{
|
||||
String pfilename = System.getProperty(SimpleAuthenticator.PASSWD_FILENAME_PROPERTY);
|
||||
if (pfilename == null)
|
||||
{
|
||||
throw new ConfigurationException("When using " + this.getClass().getCanonicalName() + " " +
|
||||
SimpleAuthenticator.PASSWD_FILENAME_PROPERTY + " properties must be defined.");
|
||||
}
|
||||
}
|
||||
|
||||
static String authenticationErrorMessage(PasswordMode mode, String username)
|
||||
{
|
||||
return String.format("Given password in password mode %s could not be validated for user %s", mode, username);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
package org.apache.cassandra.auth;
|
||||
/*
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.io.util.FileUtils;
|
||||
|
||||
public class SimpleAuthorizer extends LegacyAuthorizer
|
||||
{
|
||||
public final static String ACCESS_FILENAME_PROPERTY = "access.properties";
|
||||
// magical property for WRITE permissions to the keyspaces list
|
||||
public final static String KEYSPACES_WRITE_PROPERTY = "<modify-keyspaces>";
|
||||
|
||||
public EnumSet<Permission> authorize(AuthenticatedUser user, List<Object> resource)
|
||||
{
|
||||
if (resource.size() < 2 || !Resources.ROOT.equals(resource.get(0)) || !Resources.KEYSPACES.equals(resource.get(1)))
|
||||
return EnumSet.noneOf(Permission.class);
|
||||
|
||||
String keyspace, columnFamily = null;
|
||||
EnumSet<Permission> authorized = EnumSet.noneOf(Permission.class);
|
||||
|
||||
// /cassandra/keyspaces
|
||||
if (resource.size() == 2)
|
||||
{
|
||||
keyspace = KEYSPACES_WRITE_PROPERTY;
|
||||
authorized = EnumSet.of(Permission.READ);
|
||||
}
|
||||
// /cassandra/keyspaces/<keyspace name>
|
||||
else if (resource.size() == 3)
|
||||
{
|
||||
keyspace = (String)resource.get(2);
|
||||
}
|
||||
// /cassandra/keyspaces/<keyspace name>/<cf name>
|
||||
else if (resource.size() == 4)
|
||||
{
|
||||
keyspace = (String)resource.get(2);
|
||||
columnFamily = (String)resource.get(3);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We don't currently descend any lower in the hierarchy.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
String accessFilename = System.getProperty(ACCESS_FILENAME_PROPERTY);
|
||||
InputStream in=null;
|
||||
try
|
||||
{
|
||||
in = new BufferedInputStream(new FileInputStream(accessFilename));
|
||||
Properties accessProperties = new Properties();
|
||||
accessProperties.load(in);
|
||||
|
||||
// Special case access to the keyspace list
|
||||
if (keyspace == KEYSPACES_WRITE_PROPERTY)
|
||||
{
|
||||
String kspAdmins = accessProperties.getProperty(KEYSPACES_WRITE_PROPERTY);
|
||||
for (String admin : kspAdmins.split(","))
|
||||
if (admin.equals(user.getName()))
|
||||
return EnumSet.copyOf(Permission.ALL);
|
||||
}
|
||||
|
||||
boolean canRead = false, canWrite = false;
|
||||
String readers = null, writers = null;
|
||||
|
||||
if (columnFamily == null)
|
||||
{
|
||||
readers = accessProperties.getProperty(keyspace + ".<ro>");
|
||||
writers = accessProperties.getProperty(keyspace + ".<rw>");
|
||||
}
|
||||
else
|
||||
{
|
||||
readers = accessProperties.getProperty(keyspace + "." + columnFamily + ".<ro>");
|
||||
writers = accessProperties.getProperty(keyspace + "." + columnFamily + ".<rw>");
|
||||
}
|
||||
|
||||
if (readers != null)
|
||||
{
|
||||
for (String reader : readers.split(","))
|
||||
{
|
||||
if (reader.equals(user.getName()))
|
||||
{
|
||||
canRead = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (writers != null)
|
||||
{
|
||||
for (String writer : writers.split(","))
|
||||
{
|
||||
if (writer.equals(user.getName()))
|
||||
{
|
||||
canWrite = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (canWrite)
|
||||
authorized = EnumSet.copyOf(Permission.ALL);
|
||||
else if (canRead)
|
||||
authorized = EnumSet.of(Permission.READ);
|
||||
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException(String.format("Authorization table file '%s' could not be opened: %s",
|
||||
accessFilename,
|
||||
e.getMessage()));
|
||||
}
|
||||
finally
|
||||
{
|
||||
FileUtils.closeQuietly(in);
|
||||
}
|
||||
|
||||
return authorized;
|
||||
}
|
||||
|
||||
public void validateConfiguration() throws ConfigurationException
|
||||
{
|
||||
String afilename = System.getProperty(ACCESS_FILENAME_PROPERTY);
|
||||
if (afilename == null)
|
||||
{
|
||||
throw new ConfigurationException(String.format("When using %s, '%s' property must be defined.",
|
||||
this.getClass().getCanonicalName(),
|
||||
ACCESS_FILENAME_PROPERTY));
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
|
@ -0,0 +1,17 @@
|
|||
jBCrypt is subject to the following license:
|
||||
|
||||
/*
|
||||
* Copyright (c) 2006 Damien Miller <djm@mindrot.org>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
|
@ -100,6 +100,7 @@ public class Auth
|
|||
*
|
||||
* @param username Username to insert.
|
||||
* @param isSuper User's new status.
|
||||
* @throws RequestExecutionException
|
||||
*/
|
||||
public static void insertUser(String username, boolean isSuper) throws RequestExecutionException
|
||||
{
|
||||
|
|
@ -115,6 +116,7 @@ public class Auth
|
|||
* Deletes the user from AUTH_KS.USERS_CF.
|
||||
*
|
||||
* @param username Username to delete.
|
||||
* @throws RequestExecutionException
|
||||
*/
|
||||
public static void deleteUser(String username) throws RequestExecutionException
|
||||
{
|
||||
|
|
@ -140,7 +142,7 @@ public class Auth
|
|||
MigrationManager.instance.register(new MigrationListener());
|
||||
|
||||
// the delay is here to give the node some time to see its peers - to reduce
|
||||
// "Skipping default superuser setup: some nodes are not ready" log spam.
|
||||
// "Skipped default superuser setup: some nodes were not ready" log spam.
|
||||
// It's the only reason for the delay.
|
||||
StorageService.tasks.schedule(new Runnable()
|
||||
{
|
||||
|
|
@ -191,18 +193,18 @@ public class Auth
|
|||
// insert a default superuser if AUTH_KS.USERS_CF is empty.
|
||||
if (QueryProcessor.process(String.format("SELECT * FROM %s.%s", AUTH_KS, USERS_CF), ConsistencyLevel.QUORUM).isEmpty())
|
||||
{
|
||||
logger.info("Creating default superuser '{}'", DEFAULT_SUPERUSER_NAME);
|
||||
QueryProcessor.process(String.format("INSERT INTO %s.%s (name, super) VALUES ('%s', %s) USING TIMESTAMP 0",
|
||||
AUTH_KS,
|
||||
USERS_CF,
|
||||
DEFAULT_SUPERUSER_NAME,
|
||||
true),
|
||||
ConsistencyLevel.QUORUM);
|
||||
logger.info("Created default superuser '{}'", DEFAULT_SUPERUSER_NAME);
|
||||
}
|
||||
}
|
||||
catch (RequestExecutionException e)
|
||||
{
|
||||
logger.warn("Skipping default superuser setup: some nodes are not ready");
|
||||
logger.warn("Skipped default superuser setup: some nodes were not ready");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.cassandra.auth;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.config.Schema;
|
||||
import org.apache.cassandra.cql3.UntypedResultSet;
|
||||
import org.apache.cassandra.cql3.QueryProcessor;
|
||||
import org.apache.cassandra.db.ConsistencyLevel;
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
import org.apache.cassandra.exceptions.*;
|
||||
|
||||
/**
|
||||
* CassandraAuthorizer is an IAuthorizer implementation that keeps
|
||||
* permissions internally in C* - in system_auth.permissions CQL3 table.
|
||||
*/
|
||||
public class CassandraAuthorizer implements IAuthorizer
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(CassandraAuthorizer.class);
|
||||
|
||||
private static final String USERNAME = "username";
|
||||
private static final String RESOURCE = "resource";
|
||||
private static final String PERMISSIONS = "permissions";
|
||||
|
||||
private static final String PERMISSIONS_CF = "permissions";
|
||||
private static final String PERMISSIONS_CF_SCHEMA = String.format("CREATE TABLE %s.%s ("
|
||||
+ "username text,"
|
||||
+ "resource text,"
|
||||
+ "permissions set<text>,"
|
||||
+ "PRIMARY KEY(username, resource)"
|
||||
+ ") WITH gc_grace_seconds=%d",
|
||||
Auth.AUTH_KS,
|
||||
PERMISSIONS_CF,
|
||||
90 * 24 * 60 * 60); // 3 months.
|
||||
|
||||
// Returns every permission on the resource granted to the user.
|
||||
public Set<Permission> authorize(AuthenticatedUser user, IResource resource)
|
||||
{
|
||||
if (user.isSuper())
|
||||
return Permission.ALL;
|
||||
|
||||
UntypedResultSet rows;
|
||||
try
|
||||
{
|
||||
rows = process(String.format("SELECT permissions FROM %s.%s WHERE username = '%s' AND resource = '%s'",
|
||||
Auth.AUTH_KS,
|
||||
PERMISSIONS_CF,
|
||||
escape(user.getName()),
|
||||
escape(resource.getName())));
|
||||
}
|
||||
catch (RequestExecutionException e)
|
||||
{
|
||||
logger.warn("CassandraAuthorizer failed to authorize {} for {}", user, resource);
|
||||
return Permission.NONE;
|
||||
}
|
||||
|
||||
if (rows.isEmpty() || !rows.one().has(PERMISSIONS))
|
||||
return Permission.NONE;
|
||||
|
||||
Set<Permission> permissions = EnumSet.noneOf(Permission.class);
|
||||
for (String perm : rows.one().getSet(PERMISSIONS, UTF8Type.instance))
|
||||
permissions.add(Permission.valueOf(perm));
|
||||
return permissions;
|
||||
}
|
||||
|
||||
public void grant(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String to)
|
||||
throws RequestExecutionException
|
||||
{
|
||||
modify(permissions, resource, to, "+");
|
||||
}
|
||||
|
||||
public void revoke(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String from)
|
||||
throws RequestExecutionException
|
||||
{
|
||||
modify(permissions, resource, from, "-");
|
||||
}
|
||||
|
||||
// Adds or removes permissions from user's 'permissions' set (adds if op is "+", removes if op is "-")
|
||||
private void modify(Set<Permission> permissions, IResource resource, String user, String op) throws RequestExecutionException
|
||||
{
|
||||
process(String.format("UPDATE %s.%s SET permissions = permissions %s {%s} WHERE username = '%s' AND resource = '%s'",
|
||||
Auth.AUTH_KS,
|
||||
PERMISSIONS_CF,
|
||||
op,
|
||||
"'" + StringUtils.join(permissions, "','") + "'",
|
||||
escape(user),
|
||||
escape(resource.getName())));
|
||||
}
|
||||
|
||||
// 'of' can be null - in that case everyone's permissions have been requested. Otherwise only single user's.
|
||||
// If the user requesting 'LIST PERMISSIONS' is not a superuser OR his username doesn't match 'of', we
|
||||
// throw UnauthorizedException. So only a superuser can view everybody's permissions. Regular users are only
|
||||
// allowed to see their own permissions.
|
||||
public Set<PermissionDetails> list(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String of)
|
||||
throws RequestValidationException, RequestExecutionException
|
||||
{
|
||||
if (!performer.isSuper() && !performer.getName().equals(of))
|
||||
throw new UnauthorizedException(String.format("You are not authorized to view %s's permissions",
|
||||
of == null ? "everyone" : of));
|
||||
|
||||
Set<PermissionDetails> details = new HashSet<PermissionDetails>();
|
||||
|
||||
for (UntypedResultSet.Row row : process(buildListQuery(resource, of)))
|
||||
{
|
||||
if (row.has(PERMISSIONS))
|
||||
{
|
||||
for (String p : row.getSet(PERMISSIONS, UTF8Type.instance))
|
||||
{
|
||||
Permission permission = Permission.valueOf(p);
|
||||
if (permissions.contains(permission))
|
||||
details.add(new PermissionDetails(row.getString(USERNAME),
|
||||
DataResource.fromName(row.getString(RESOURCE)),
|
||||
permission));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
private static String buildListQuery(IResource resource, String of)
|
||||
{
|
||||
List<String> vars = Lists.newArrayList(Auth.AUTH_KS, PERMISSIONS_CF);
|
||||
List<String> conditions = new ArrayList<String>();
|
||||
|
||||
if (resource != null)
|
||||
{
|
||||
conditions.add("resource = '%s'");
|
||||
vars.add(escape(resource.getName()));
|
||||
}
|
||||
|
||||
if (of != null)
|
||||
{
|
||||
conditions.add("username = '%s'");
|
||||
vars.add(escape(of));
|
||||
}
|
||||
|
||||
String query = "SELECT username, resource, permissions FROM %s.%s";
|
||||
|
||||
if (!conditions.isEmpty())
|
||||
query += " WHERE " + StringUtils.join(conditions, " AND ");
|
||||
|
||||
if (resource != null && of == null)
|
||||
query += " ALLOW FILTERING";
|
||||
|
||||
return String.format(query, vars.toArray());
|
||||
}
|
||||
|
||||
// Called prior to deleting the user with DROP USER query. Internal hook, so no permission checks are needed here.
|
||||
public void revokeAll(String droppedUser)
|
||||
{
|
||||
try
|
||||
{
|
||||
process(String.format("DELETE FROM %s.%s WHERE username = '%s'", Auth.AUTH_KS, PERMISSIONS_CF, escape(droppedUser)));
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
logger.warn("CassandraAuthorizer failed to revoke all permissions of {}: {}", droppedUser, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Called after a resource is removed (DROP KEYSPACE, DROP TABLE, etc.).
|
||||
public void revokeAll(IResource droppedResource)
|
||||
{
|
||||
|
||||
UntypedResultSet rows;
|
||||
try
|
||||
{
|
||||
// TODO: switch to secondary index on 'resource' once https://issues.apache.org/jira/browse/CASSANDRA-5125 is resolved.
|
||||
rows = process(String.format("SELECT username FROM %s.%s WHERE resource = '%s' ALLOW FILTERING",
|
||||
Auth.AUTH_KS,
|
||||
PERMISSIONS_CF,
|
||||
escape(droppedResource.getName())));
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
logger.warn("CassandraAuthorizer failed to revoke all permissions on {}: {}", droppedResource, e);
|
||||
return;
|
||||
}
|
||||
|
||||
for (UntypedResultSet.Row row : rows)
|
||||
{
|
||||
try
|
||||
{
|
||||
process(String.format("DELETE FROM %s.%s WHERE username = '%s' AND resource = '%s'",
|
||||
Auth.AUTH_KS,
|
||||
PERMISSIONS_CF,
|
||||
escape(row.getString(USERNAME)),
|
||||
escape(droppedResource.getName())));
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
logger.warn("CassandraAuthorizer failed to revoke all permissions on {}: {}", droppedResource, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Set<DataResource> protectedResources()
|
||||
{
|
||||
return ImmutableSet.of(DataResource.columnFamily(Auth.AUTH_KS, PERMISSIONS_CF));
|
||||
}
|
||||
|
||||
public void validateConfiguration() throws ConfigurationException
|
||||
{
|
||||
}
|
||||
|
||||
public void setup()
|
||||
{
|
||||
if (Schema.instance.getCFMetaData(Auth.AUTH_KS, PERMISSIONS_CF) == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
process(PERMISSIONS_CF_SCHEMA);
|
||||
}
|
||||
catch (RequestExecutionException e)
|
||||
{
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We only worry about one character ('). Make sure it's properly escaped.
|
||||
private static String escape(String name)
|
||||
{
|
||||
return StringUtils.replace(name, "'", "''");
|
||||
}
|
||||
|
||||
private static UntypedResultSet process(String query) throws RequestExecutionException
|
||||
{
|
||||
return QueryProcessor.process(query, ConsistencyLevel.QUORUM);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,8 @@ import java.util.Set;
|
|||
|
||||
import org.apache.cassandra.exceptions.AuthenticationException;
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||
import org.apache.cassandra.exceptions.RequestValidationException;
|
||||
|
||||
public interface IAuthenticator
|
||||
{
|
||||
|
|
@ -72,9 +73,10 @@ public interface IAuthenticator
|
|||
*
|
||||
* @param username Username of the user to create.
|
||||
* @param options Options the user will be created with.
|
||||
* @throws InvalidRequestException
|
||||
* @throws RequestValidationException
|
||||
* @throws RequestExecutionException
|
||||
*/
|
||||
void create(String username, Map<Option, Object> options) throws InvalidRequestException;
|
||||
void create(String username, Map<Option, Object> options) throws RequestValidationException, RequestExecutionException;
|
||||
|
||||
/**
|
||||
* Called during execution of ALTER USER query.
|
||||
|
|
@ -84,23 +86,25 @@ public interface IAuthenticator
|
|||
*
|
||||
* @param username Username of the user that will be altered.
|
||||
* @param options Options to alter.
|
||||
* @throws InvalidRequestException
|
||||
* @throws RequestValidationException
|
||||
* @throws RequestExecutionException
|
||||
*/
|
||||
void alter(String username, Map<Option, Object> options) throws InvalidRequestException;
|
||||
void alter(String username, Map<Option, Object> options) throws RequestValidationException, RequestExecutionException;
|
||||
|
||||
|
||||
/**
|
||||
* Called during execution of DROP USER query.
|
||||
*
|
||||
* @param username Username of the user that will be dropped.
|
||||
* @throws InvalidRequestException
|
||||
* @throws RequestValidationException
|
||||
* @throws RequestExecutionException
|
||||
*/
|
||||
void drop(String username) throws InvalidRequestException;
|
||||
void drop(String username) throws RequestValidationException, RequestExecutionException;
|
||||
|
||||
/**
|
||||
* Set of resources that should be made inaccessible to users and only accessible internally.
|
||||
*
|
||||
* @return Keyspaces, column families that will be unreadable and unmodifiable by users; other resources.
|
||||
* @return Keyspaces, column families that will be unmodifiable by users; other resources.
|
||||
*/
|
||||
Set<? extends IResource> protectedResources();
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ package org.apache.cassandra.auth;
|
|||
import java.util.Set;
|
||||
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.exceptions.UnauthorizedException;
|
||||
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||
import org.apache.cassandra.exceptions.RequestValidationException;
|
||||
|
||||
/**
|
||||
* Primary Cassandra authorization interface.
|
||||
|
|
@ -46,11 +46,11 @@ public interface IAuthorizer
|
|||
* @param to Grantee of the permissions.
|
||||
* @param resource Resource on which to grant the permissions.
|
||||
*
|
||||
* @throws UnauthorizedException if the granting user isn't allowed to grant (and revoke) the permissions on the resource.
|
||||
* @throws InvalidRequestException upon parameter misconfiguration or internal error.
|
||||
* @throws RequestValidationException
|
||||
* @throws RequestExecutionException
|
||||
*/
|
||||
void grant(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String to)
|
||||
throws UnauthorizedException, InvalidRequestException;
|
||||
throws RequestValidationException, RequestExecutionException;
|
||||
|
||||
/**
|
||||
* Revokes a set of permissions on a resource from a user.
|
||||
|
|
@ -61,11 +61,11 @@ public interface IAuthorizer
|
|||
* @param from Revokee of the permissions.
|
||||
* @param resource Resource on which to revoke the permissions.
|
||||
*
|
||||
* @throws UnauthorizedException if the revoking user isn't allowed to revoke the permissions on the resource.
|
||||
* @throws InvalidRequestException upon parameter misconfiguration or internal error.
|
||||
* @throws RequestValidationException
|
||||
* @throws RequestExecutionException
|
||||
*/
|
||||
void revoke(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String from)
|
||||
throws UnauthorizedException, InvalidRequestException;
|
||||
throws RequestValidationException, RequestExecutionException;
|
||||
|
||||
/**
|
||||
* Returns a list of permissions on a resource of a user.
|
||||
|
|
@ -78,11 +78,11 @@ public interface IAuthorizer
|
|||
*
|
||||
* @return All of the matching permission that the requesting user is authorized to know about.
|
||||
*
|
||||
* @throws UnauthorizedException if the user isn't allowed to view the requested permissions.
|
||||
* @throws InvalidRequestException upon parameter misconfiguration or internal error.
|
||||
* @throws RequestValidationException
|
||||
* @throws RequestExecutionException
|
||||
*/
|
||||
Set<PermissionDetails> list(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String of)
|
||||
throws UnauthorizedException, InvalidRequestException;
|
||||
throws RequestValidationException, RequestExecutionException;
|
||||
|
||||
/**
|
||||
* This method is called before deleting a user with DROP USER query so that a new user with the same
|
||||
|
|
@ -102,7 +102,7 @@ public interface IAuthorizer
|
|||
/**
|
||||
* Set of resources that should be made inaccessible to users and only accessible internally.
|
||||
*
|
||||
* @return Keyspaces, column families that will be unreadable and unmodifiable by users; other resources.
|
||||
* @return Keyspaces, column families that will be unmodifiable by users; other resources.
|
||||
*/
|
||||
Set<? extends IResource> protectedResources();
|
||||
|
||||
|
|
|
|||
|
|
@ -17,11 +17,14 @@
|
|||
*/
|
||||
package org.apache.cassandra.auth;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.cassandra.exceptions.AuthenticationException;
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||
import org.apache.cassandra.exceptions.RequestValidationException;
|
||||
|
||||
/**
|
||||
* Provides a transitional IAuthenticator implementation for old-style (pre-1.2) authenticators.
|
||||
|
|
@ -64,18 +67,17 @@ public abstract class LegacyAuthenticator implements IAuthenticator
|
|||
}
|
||||
|
||||
@Override
|
||||
public void create(String username, Map<Option, Object> options) throws InvalidRequestException
|
||||
public void create(String username, Map<Option, Object> options) throws RequestValidationException, RequestExecutionException
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void alter(String username, Map<Option, Object> options) throws InvalidRequestException
|
||||
public void alter(String username, Map<Option, Object> options) throws RequestValidationException, RequestExecutionException
|
||||
{
|
||||
throw new InvalidRequestException("ALTER USER operation is not supported by LegacyAuthenticator");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drop(String username) throws InvalidRequestException
|
||||
public void drop(String username) throws RequestValidationException, RequestExecutionException
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,14 +72,14 @@ public abstract class LegacyAuthorizer implements IAuthorizer
|
|||
|
||||
@Override
|
||||
public void grant(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String to)
|
||||
throws InvalidRequestException, UnauthorizedException
|
||||
throws InvalidRequestException
|
||||
{
|
||||
throw new InvalidRequestException("GRANT operation is not supported by LegacyAuthorizer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revoke(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, String from)
|
||||
throws InvalidRequestException, UnauthorizedException
|
||||
throws InvalidRequestException
|
||||
{
|
||||
throw new InvalidRequestException("REVOKE operation is not supported by LegacyAuthorizer");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.cassandra.auth;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.config.Schema;
|
||||
import org.apache.cassandra.cql3.UntypedResultSet;
|
||||
import org.apache.cassandra.cql3.QueryProcessor;
|
||||
import org.apache.cassandra.db.ConsistencyLevel;
|
||||
import org.apache.cassandra.exceptions.AuthenticationException;
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
|
||||
/**
|
||||
* PasswordAuthenticator is an IAuthenticator implementation
|
||||
* that keeps credentials (usernames and bcrypt-hashed passwords)
|
||||
* internally in C* - in system_auth.credentials CQL3 table.
|
||||
*/
|
||||
public class PasswordAuthenticator implements IAuthenticator
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(PasswordAuthenticator.class);
|
||||
|
||||
private static final long DEFAULT_USER_SETUP_DELAY = 10; // seconds
|
||||
|
||||
// 2 ** GENSALT_LOG2_ROUNS rounds of hashing will be performed.
|
||||
private static final int GENSALT_LOG2_ROUNDS = 10;
|
||||
|
||||
// name of the hash column.
|
||||
private static final String SALTED_HASH = "salted_hash";
|
||||
|
||||
private static final String DEFAULT_USER_NAME = Auth.DEFAULT_SUPERUSER_NAME;
|
||||
private static final String DEFAULT_USER_PASSWORD = Auth.DEFAULT_SUPERUSER_NAME;
|
||||
|
||||
private static final String CREDENTIALS_CF = "credentials";
|
||||
private static final String CREDENTIALS_CF_SCHEMA = String.format("CREATE TABLE %s.%s ("
|
||||
+ "username text,"
|
||||
+ "salted_hash text," // salt + hash + number of rounds
|
||||
+ "options map<text,text>," // for future extensions
|
||||
+ "PRIMARY KEY(username)"
|
||||
+ ") WITH gc_grace_seconds=%d",
|
||||
Auth.AUTH_KS,
|
||||
CREDENTIALS_CF,
|
||||
90 * 24 * 60 * 60); // 3 months.
|
||||
|
||||
// No anonymous access.
|
||||
public boolean requireAuthentication()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public Set<Option> supportedOptions()
|
||||
{
|
||||
return ImmutableSet.of(Option.PASSWORD);
|
||||
}
|
||||
|
||||
// Let users alter their own password.
|
||||
public Set<Option> alterableOptions()
|
||||
{
|
||||
return ImmutableSet.of(Option.PASSWORD);
|
||||
}
|
||||
|
||||
public AuthenticatedUser authenticate(Map<String, String> credentials) throws AuthenticationException
|
||||
{
|
||||
String username = credentials.get(USERNAME_KEY);
|
||||
if (username == null)
|
||||
throw new AuthenticationException(String.format("Required key '%s' is missing", USERNAME_KEY));
|
||||
|
||||
String password = credentials.get(PASSWORD_KEY);
|
||||
if (password == null)
|
||||
throw new AuthenticationException(String.format("Required key '%s' is missing", PASSWORD_KEY));
|
||||
|
||||
UntypedResultSet result;
|
||||
try
|
||||
{
|
||||
result = process(String.format("SELECT %s FROM %s.%s WHERE username = '%s'",
|
||||
SALTED_HASH,
|
||||
Auth.AUTH_KS,
|
||||
CREDENTIALS_CF,
|
||||
escape(username)));
|
||||
}
|
||||
catch (RequestExecutionException e)
|
||||
{
|
||||
throw new AuthenticationException(e.toString());
|
||||
}
|
||||
|
||||
if (result.isEmpty() || !BCrypt.checkpw(password, result.one().getString(SALTED_HASH)))
|
||||
throw new AuthenticationException("Username and/or password are incorrect");
|
||||
|
||||
return new AuthenticatedUser(username);
|
||||
}
|
||||
|
||||
public void create(String username, Map<Option, Object> options) throws InvalidRequestException, RequestExecutionException
|
||||
{
|
||||
String password = (String) options.get(Option.PASSWORD);
|
||||
if (password == null)
|
||||
throw new InvalidRequestException("PasswordAuthenticator requires PASSWORD option");
|
||||
|
||||
process(String.format("INSERT INTO %s.%s (username, salted_hash) VALUES ('%s', '%s')",
|
||||
Auth.AUTH_KS,
|
||||
CREDENTIALS_CF,
|
||||
escape(username),
|
||||
escape(hashpw(password))));
|
||||
}
|
||||
|
||||
public void alter(String username, Map<Option, Object> options) throws RequestExecutionException
|
||||
{
|
||||
process(String.format("UPDATE %s.%s SET salted_hash = '%s' WHERE username = '%s'",
|
||||
Auth.AUTH_KS,
|
||||
CREDENTIALS_CF,
|
||||
escape(hashpw((String) options.get(Option.PASSWORD))),
|
||||
escape(username)));
|
||||
}
|
||||
|
||||
public void drop(String username) throws RequestExecutionException
|
||||
{
|
||||
process(String.format("DELETE FROM %s.%s WHERE username = '%s'", Auth.AUTH_KS, CREDENTIALS_CF, escape(username)));
|
||||
}
|
||||
|
||||
public Set<DataResource> protectedResources()
|
||||
{
|
||||
return ImmutableSet.of(DataResource.columnFamily(Auth.AUTH_KS, CREDENTIALS_CF));
|
||||
}
|
||||
|
||||
public void validateConfiguration() throws ConfigurationException
|
||||
{
|
||||
}
|
||||
|
||||
public void setup()
|
||||
{
|
||||
setupCredentialsTable();
|
||||
|
||||
// the delay is here to give the node some time to see its peers - to reduce
|
||||
// "skipped default user setup: some nodes are were not ready" log spam.
|
||||
// It's the only reason for the delay.
|
||||
StorageService.tasks.schedule(new Runnable()
|
||||
{
|
||||
public void run()
|
||||
{
|
||||
setupDefaultUser();
|
||||
}
|
||||
},
|
||||
DEFAULT_USER_SETUP_DELAY,
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void setupCredentialsTable()
|
||||
{
|
||||
if (Schema.instance.getCFMetaData(Auth.AUTH_KS, CREDENTIALS_CF) == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
process(CREDENTIALS_CF_SCHEMA);
|
||||
}
|
||||
catch (RequestExecutionException e)
|
||||
{
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if there are no users yet - add default superuser.
|
||||
private void setupDefaultUser()
|
||||
{
|
||||
try
|
||||
{
|
||||
// insert a default superuser if AUTH_KS.CREDENTIALS_CF is empty.
|
||||
if (process(String.format("SELECT * FROM %s.%s", Auth.AUTH_KS, CREDENTIALS_CF)).isEmpty())
|
||||
{
|
||||
process(String.format("INSERT INTO %s.%s (username, salted_hash) VALUES ('%s', '%s') USING TIMESTAMP 0",
|
||||
Auth.AUTH_KS,
|
||||
CREDENTIALS_CF,
|
||||
DEFAULT_USER_NAME,
|
||||
escape(hashpw(DEFAULT_USER_PASSWORD))));
|
||||
logger.info("PasswordAuthenticator created default user '{}'", DEFAULT_USER_NAME);
|
||||
}
|
||||
}
|
||||
catch (RequestExecutionException e)
|
||||
{
|
||||
logger.warn("PasswordAuthenticator skipped default user setup: some nodes were not ready");
|
||||
}
|
||||
}
|
||||
|
||||
private static String hashpw(String password)
|
||||
{
|
||||
return BCrypt.hashpw(password, BCrypt.gensalt(GENSALT_LOG2_ROUNDS));
|
||||
}
|
||||
|
||||
private static String escape(String name)
|
||||
{
|
||||
return StringUtils.replace(name, "'", "''");
|
||||
}
|
||||
|
||||
private static UntypedResultSet process(String query) throws RequestExecutionException
|
||||
{
|
||||
return QueryProcessor.process(query, ConsistencyLevel.QUORUM);
|
||||
}
|
||||
}
|
||||
|
|
@ -35,8 +35,8 @@ public class CliSessionState
|
|||
public String hostName; // cassandra server name
|
||||
public int thriftPort; // cassandra server's thrift port
|
||||
public boolean debug = false; // print stack traces when errors occur in the CLI
|
||||
public String username; // cassandra login name (if SimpleAuthenticator is used)
|
||||
public String password; // cassandra login password (if SimpleAuthenticator is used)
|
||||
public String username; // cassandra login name (if password-based authenticator is used)
|
||||
public String password; // cassandra login password (if password-based authenticator is used)
|
||||
public String keyspace; // cassandra keyspace user is authenticating
|
||||
public boolean batch = false; // enable/disable batch processing mode
|
||||
public String filename = ""; // file to read commands from
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import org.apache.cassandra.config.DatabaseDescriptor;
|
|||
import org.apache.cassandra.cql3.UserOptions;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||
import org.apache.cassandra.exceptions.RequestValidationException;
|
||||
import org.apache.cassandra.exceptions.UnauthorizedException;
|
||||
import org.apache.cassandra.service.ClientState;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
|
|
@ -41,32 +42,36 @@ public class AlterUserStatement extends AuthenticationStatement
|
|||
this.superuser = superuser;
|
||||
}
|
||||
|
||||
public void validate(ClientState state) throws InvalidRequestException
|
||||
public void validate(ClientState state) throws RequestValidationException
|
||||
{
|
||||
opts.validate();
|
||||
|
||||
if (superuser == null && opts.isEmpty())
|
||||
throw new InvalidRequestException("ALTER USER can't be empty");
|
||||
|
||||
// validate login here before checkAccess to avoid leaking user existence to anonymous users.
|
||||
state.ensureNotAnonymous();
|
||||
|
||||
if (!Auth.isExistingUser(username))
|
||||
throw new InvalidRequestException(String.format("User %s doesn't exist", username));
|
||||
}
|
||||
|
||||
public void checkAccess(ClientState state) throws UnauthorizedException
|
||||
{
|
||||
state.validateLogin();
|
||||
AuthenticatedUser user = state.getUser();
|
||||
|
||||
boolean isSuper = user.isSuper();
|
||||
|
||||
if (superuser != null && user.getName().equals(username))
|
||||
throw new UnauthorizedException("You aren't allowed to alter your own superuser status");
|
||||
|
||||
if (superuser != null && !user.isSuper())
|
||||
if (superuser != null && !isSuper)
|
||||
throw new UnauthorizedException("Only superusers are allowed to alter superuser status");
|
||||
|
||||
if (!user.isSuper() && !user.getName().equals(username))
|
||||
throw new UnauthorizedException("You aren't allowed to alter this user");
|
||||
|
||||
if (!user.isSuper())
|
||||
if (!isSuper)
|
||||
{
|
||||
for (IAuthenticator.Option option : opts.getOptions().keySet())
|
||||
{
|
||||
|
|
@ -76,7 +81,7 @@ public class AlterUserStatement extends AuthenticationStatement
|
|||
}
|
||||
}
|
||||
|
||||
public ResultMessage execute(ClientState state) throws InvalidRequestException, RequestExecutionException
|
||||
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
|
||||
{
|
||||
if (!opts.isEmpty())
|
||||
DatabaseDescriptor.getAuthenticator().alter(username, opts.getOptions());
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import org.apache.cassandra.config.DatabaseDescriptor;
|
|||
import org.apache.cassandra.cql3.UserOptions;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||
import org.apache.cassandra.exceptions.RequestValidationException;
|
||||
import org.apache.cassandra.exceptions.UnauthorizedException;
|
||||
import org.apache.cassandra.service.ClientState;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
|
|
@ -39,24 +40,27 @@ public class CreateUserStatement extends AuthenticationStatement
|
|||
this.superuser = superuser;
|
||||
}
|
||||
|
||||
public void validate(ClientState state) throws InvalidRequestException
|
||||
public void validate(ClientState state) throws RequestValidationException
|
||||
{
|
||||
if (username.isEmpty())
|
||||
throw new InvalidRequestException("Username can't be an empty string");
|
||||
|
||||
opts.validate();
|
||||
|
||||
// validate login here before checkAccess to avoid leaking user existence to anonymous users.
|
||||
state.ensureNotAnonymous();
|
||||
|
||||
if (Auth.isExistingUser(username))
|
||||
throw new InvalidRequestException(String.format("User %s already exists", username));
|
||||
}
|
||||
|
||||
public void checkAccess(ClientState state) throws UnauthorizedException
|
||||
{
|
||||
state.validateLogin();
|
||||
|
||||
if (!state.getUser().isSuper())
|
||||
throw new UnauthorizedException("Only superusers are allowed to perfrom CREATE USER queries");
|
||||
}
|
||||
|
||||
public ResultMessage execute(ClientState state) throws InvalidRequestException, RequestExecutionException
|
||||
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
|
||||
{
|
||||
DatabaseDescriptor.getAuthenticator().create(username, opts.getOptions());
|
||||
Auth.insertUser(username, superuser);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import org.apache.cassandra.auth.AuthenticatedUser;
|
|||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||
import org.apache.cassandra.exceptions.RequestValidationException;
|
||||
import org.apache.cassandra.exceptions.UnauthorizedException;
|
||||
import org.apache.cassandra.service.ClientState;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
|
|
@ -35,8 +36,11 @@ public class DropUserStatement extends AuthenticationStatement
|
|||
this.username = username;
|
||||
}
|
||||
|
||||
public void validate(ClientState state) throws InvalidRequestException
|
||||
public void validate(ClientState state) throws RequestValidationException
|
||||
{
|
||||
// validate login here before checkAccess to avoid leaking user existence to anonymous users.
|
||||
state.ensureNotAnonymous();
|
||||
|
||||
if (!Auth.isExistingUser(username))
|
||||
throw new InvalidRequestException(String.format("User %s doesn't exists", username));
|
||||
|
||||
|
|
@ -47,12 +51,11 @@ public class DropUserStatement extends AuthenticationStatement
|
|||
|
||||
public void checkAccess(ClientState state) throws UnauthorizedException
|
||||
{
|
||||
state.validateLogin();
|
||||
if (!state.getUser().isSuper())
|
||||
throw new UnauthorizedException("Only superusers are allowed to perfrom DROP USER queries");
|
||||
}
|
||||
|
||||
public ResultMessage execute(ClientState state) throws InvalidRequestException, RequestExecutionException
|
||||
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
|
||||
{
|
||||
// clean up permissions after the dropped user.
|
||||
DatabaseDescriptor.getAuthorizer().revokeAll(username);
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ import java.util.Set;
|
|||
import org.apache.cassandra.auth.IResource;
|
||||
import org.apache.cassandra.auth.Permission;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.exceptions.UnauthorizedException;
|
||||
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||
import org.apache.cassandra.exceptions.RequestValidationException;
|
||||
import org.apache.cassandra.service.ClientState;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ public class GrantStatement extends PermissionAlteringStatement
|
|||
super(permissions, resource, username);
|
||||
}
|
||||
|
||||
public ResultMessage execute(ClientState state) throws UnauthorizedException, InvalidRequestException
|
||||
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
|
||||
{
|
||||
DatabaseDescriptor.getAuthorizer().grant(state.getUser(), permissions, resource, username);
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ import org.apache.cassandra.cql3.ColumnSpecification;
|
|||
import org.apache.cassandra.cql3.ResultSet;
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.exceptions.UnauthorizedException;
|
||||
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||
import org.apache.cassandra.exceptions.RequestValidationException;
|
||||
import org.apache.cassandra.service.ClientState;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
|
||||
|
|
@ -59,9 +60,11 @@ public class ListPermissionsStatement extends AuthorizationStatement
|
|||
this.recursive = recursive;
|
||||
}
|
||||
|
||||
public void validate(ClientState state) throws InvalidRequestException
|
||||
public void validate(ClientState state) throws RequestValidationException
|
||||
{
|
||||
// a check to ensure the existence of the user isn't being leaked by user existence check.
|
||||
state.ensureNotAnonymous();
|
||||
|
||||
if (username != null && !Auth.isExistingUser(username))
|
||||
throw new InvalidRequestException(String.format("User %s doesn't exist", username));
|
||||
|
||||
|
|
@ -73,13 +76,13 @@ public class ListPermissionsStatement extends AuthorizationStatement
|
|||
}
|
||||
}
|
||||
|
||||
public void checkAccess(ClientState state) throws UnauthorizedException
|
||||
public void checkAccess(ClientState state)
|
||||
{
|
||||
state.ensureNotAnonymous();
|
||||
// checked in validate
|
||||
}
|
||||
|
||||
// TODO: Create a new ResultMessage type (?). Rows will do for now.
|
||||
public ResultMessage execute(ClientState state) throws UnauthorizedException, InvalidRequestException
|
||||
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
|
||||
{
|
||||
List<PermissionDetails> details = new ArrayList<PermissionDetails>();
|
||||
|
||||
|
|
@ -112,7 +115,8 @@ public class ListPermissionsStatement extends AuthorizationStatement
|
|||
return new ResultMessage.Rows(result);
|
||||
}
|
||||
|
||||
private Set<PermissionDetails> list(ClientState state, IResource resource) throws UnauthorizedException, InvalidRequestException
|
||||
private Set<PermissionDetails> list(ClientState state, IResource resource)
|
||||
throws RequestValidationException, RequestExecutionException
|
||||
{
|
||||
return DatabaseDescriptor.getAuthorizer().list(state.getUser(), permissions, resource, username);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import org.apache.cassandra.auth.DataResource;
|
|||
import org.apache.cassandra.auth.IResource;
|
||||
import org.apache.cassandra.auth.Permission;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.exceptions.RequestValidationException;
|
||||
import org.apache.cassandra.exceptions.UnauthorizedException;
|
||||
import org.apache.cassandra.service.ClientState;
|
||||
|
||||
|
|
@ -40,17 +41,11 @@ public abstract class PermissionAlteringStatement extends AuthorizationStatement
|
|||
this.username = username;
|
||||
}
|
||||
|
||||
public void checkAccess(ClientState state) throws UnauthorizedException
|
||||
public void validate(ClientState state) throws RequestValidationException
|
||||
{
|
||||
// check that the user has AUTHORIZE permission on the resource or its parents, otherwise reject GRANT/REVOKE.
|
||||
state.ensureHasPermission(Permission.AUTHORIZE, resource);
|
||||
// check that the user has [a single permission or all in case of ALL] on the resource or its parents.
|
||||
for (Permission p : permissions)
|
||||
state.ensureHasPermission(p, resource);
|
||||
}
|
||||
// validate login here before checkAccess to avoid leaking user existence to anonymous users.
|
||||
state.ensureNotAnonymous();
|
||||
|
||||
public void validate(ClientState state) throws InvalidRequestException
|
||||
{
|
||||
if (!Auth.isExistingUser(username))
|
||||
throw new InvalidRequestException(String.format("User %s doesn't exist", username));
|
||||
|
||||
|
|
@ -59,4 +54,13 @@ public abstract class PermissionAlteringStatement extends AuthorizationStatement
|
|||
if (!resource.exists())
|
||||
throw new InvalidRequestException(String.format("%s doesn't exist", resource));
|
||||
}
|
||||
|
||||
public void checkAccess(ClientState state) throws UnauthorizedException
|
||||
{
|
||||
// check that the user has AUTHORIZE permission on the resource or its parents, otherwise reject GRANT/REVOKE.
|
||||
state.ensureHasPermission(Permission.AUTHORIZE, resource);
|
||||
// check that the user has [a single permission or all in case of ALL] on the resource or its parents.
|
||||
for (Permission p : permissions)
|
||||
state.ensureHasPermission(p, resource);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ import java.util.Set;
|
|||
import org.apache.cassandra.auth.IResource;
|
||||
import org.apache.cassandra.auth.Permission;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.exceptions.UnauthorizedException;
|
||||
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||
import org.apache.cassandra.exceptions.RequestValidationException;
|
||||
import org.apache.cassandra.service.ClientState;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ public class RevokeStatement extends PermissionAlteringStatement
|
|||
super(permissions, resource, username);
|
||||
}
|
||||
|
||||
public ResultMessage execute(ClientState state) throws UnauthorizedException, InvalidRequestException
|
||||
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
|
||||
{
|
||||
DatabaseDescriptor.getAuthorizer().revoke(state.getUser(), permissions, resource, username);
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -156,7 +156,8 @@ public class ClientState
|
|||
if (perm.equals(Permission.SELECT) && READABLE_SYSTEM_RESOURCES.contains(resource))
|
||||
return;
|
||||
if (PROTECTED_AUTH_RESOURCES.contains(resource))
|
||||
throw new UnauthorizedException(String.format("Resource %s is inaccessible", resource));
|
||||
if (perm.equals(Permission.CREATE) || perm.equals(Permission.ALTER) || perm.equals(Permission.DROP))
|
||||
throw new UnauthorizedException(String.format("%s schema is protected", resource));
|
||||
ensureHasPermission(perm, resource);
|
||||
}
|
||||
|
||||
|
|
@ -197,7 +198,7 @@ public class ClientState
|
|||
{
|
||||
validateLogin();
|
||||
if (user.isAnonymous())
|
||||
throw new UnauthorizedException("You have to be logged in to perform this query");
|
||||
throw new UnauthorizedException("You have to be logged in and not anonymous to perform this request");
|
||||
}
|
||||
|
||||
private static void validateKeyspace(String keyspace) throws InvalidRequestException
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# This is a sample access file for SimpleAuthorizer. The format of
|
||||
# this file is keyspace=users, where users is a comma delimited list of
|
||||
# authenticatable users from passwd.properties. This file contains
|
||||
# potentially sensitive information, keep this in mind when setting its
|
||||
# mode and ownership.
|
||||
#
|
||||
# The magical '<modify-keyspaces>' property lists users who can modify the
|
||||
# list of keyspaces: all users will be able to view the list of keyspaces.
|
||||
<modify-keyspaces>=user1
|
||||
|
||||
Keyspace1.<rw>=user1,user2
|
||||
Keyspace1.Standard1.<ro>=user2
|
||||
Keyspace1.Standard1.<rw>=user1
|
||||
Loading…
Reference in New Issue