mirror of https://github.com/apache/cassandra
add plugin support for CQLSH
patch by Brian Houser; reviewed by Stefan Miklosovic and Brandon Williams for CASSANDRA-16456
This commit is contained in:
parent
f444c40286
commit
381c2a4fa8
|
|
@ -14,6 +14,7 @@ lib/
|
||||||
pylib/src/
|
pylib/src/
|
||||||
**/cqlshlib.xml
|
**/cqlshlib.xml
|
||||||
!lib/cassandra-driver-internal-only-*.zip
|
!lib/cassandra-driver-internal-only-*.zip
|
||||||
|
!lib/puresasl-*.zip
|
||||||
|
|
||||||
# C* debs
|
# C* debs
|
||||||
build-stamp
|
build-stamp
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
4.1
|
4.1
|
||||||
|
* Add plugin support for CQLSH (CASSANDRA-16456)
|
||||||
* Add guardrail to disallow querying with ALLOW FILTERING (CASSANDRA-17370)
|
* Add guardrail to disallow querying with ALLOW FILTERING (CASSANDRA-17370)
|
||||||
* Enhance SnakeYAML properties to be reusable outside of YAML parsing, support camel case conversion to snake case, and add support to ignore properties (CASSANDRA-17166)
|
* Enhance SnakeYAML properties to be reusable outside of YAML parsing, support camel case conversion to snake case, and add support to ignore properties (CASSANDRA-17166)
|
||||||
* nodetool compact should support using a key string to find the range to avoid operators having to manually do this (CASSANDRA-17537)
|
* nodetool compact should support using a key string to find the range to avoid operators having to manually do this (CASSANDRA-17537)
|
||||||
|
|
|
||||||
68
bin/cqlsh.py
68
bin/cqlsh.py
|
|
@ -113,7 +113,7 @@ if cql_zip:
|
||||||
sys.path.insert(0, os.path.join(cql_zip, 'cassandra-driver-' + ver))
|
sys.path.insert(0, os.path.join(cql_zip, 'cassandra-driver-' + ver))
|
||||||
|
|
||||||
# the driver needs dependencies
|
# the driver needs dependencies
|
||||||
third_parties = ('six-')
|
third_parties = ('six-', 'puresasl-')
|
||||||
|
|
||||||
for lib in third_parties:
|
for lib in third_parties:
|
||||||
lib_zip = find_zip(lib)
|
lib_zip = find_zip(lib)
|
||||||
|
|
@ -145,7 +145,7 @@ cqlshlibdir = os.path.join(CASSANDRA_PATH, 'pylib')
|
||||||
if os.path.isdir(cqlshlibdir):
|
if os.path.isdir(cqlshlibdir):
|
||||||
sys.path.insert(0, cqlshlibdir)
|
sys.path.insert(0, cqlshlibdir)
|
||||||
|
|
||||||
from cqlshlib import cql3handling, pylexotron, sslhandling, cqlshhandling
|
from cqlshlib import cql3handling, pylexotron, sslhandling, cqlshhandling, authproviderhandling
|
||||||
from cqlshlib.copyutil import ExportTask, ImportTask
|
from cqlshlib.copyutil import ExportTask, ImportTask
|
||||||
from cqlshlib.displaying import (ANSI_RESET, BLUE, COLUMN_NAME_COLORS, CYAN,
|
from cqlshlib.displaying import (ANSI_RESET, BLUE, COLUMN_NAME_COLORS, CYAN,
|
||||||
RED, WHITE, FormattedValue, colorme)
|
RED, WHITE, FormattedValue, colorme)
|
||||||
|
|
@ -154,6 +154,8 @@ from cqlshlib.formatting import (DEFAULT_DATE_FORMAT, DEFAULT_NANOTIME_FORMAT,
|
||||||
format_by_type)
|
format_by_type)
|
||||||
from cqlshlib.tracing import print_trace, print_trace_session
|
from cqlshlib.tracing import print_trace, print_trace_session
|
||||||
from cqlshlib.util import get_file_encoding_bomsize
|
from cqlshlib.util import get_file_encoding_bomsize
|
||||||
|
from cqlshlib.util import is_file_secure
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_HOST = '127.0.0.1'
|
DEFAULT_HOST = '127.0.0.1'
|
||||||
DEFAULT_PORT = 9042
|
DEFAULT_PORT = 9042
|
||||||
|
|
@ -426,7 +428,7 @@ class Shell(cmd.Cmd):
|
||||||
default_page_size = 100
|
default_page_size = 100
|
||||||
|
|
||||||
def __init__(self, hostname, port, color=False,
|
def __init__(self, hostname, port, color=False,
|
||||||
username=None, password=None, encoding=None, stdin=None, tty=True,
|
username=None, encoding=None, stdin=None, tty=True,
|
||||||
completekey=DEFAULT_COMPLETEKEY, browser=None, use_conn=None,
|
completekey=DEFAULT_COMPLETEKEY, browser=None, use_conn=None,
|
||||||
cqlver=None, keyspace=None,
|
cqlver=None, keyspace=None,
|
||||||
tracing_enabled=False, expand_enabled=False,
|
tracing_enabled=False, expand_enabled=False,
|
||||||
|
|
@ -442,16 +444,21 @@ class Shell(cmd.Cmd):
|
||||||
request_timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
request_timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
||||||
protocol_version=None,
|
protocol_version=None,
|
||||||
connect_timeout=DEFAULT_CONNECT_TIMEOUT_SECONDS,
|
connect_timeout=DEFAULT_CONNECT_TIMEOUT_SECONDS,
|
||||||
is_subshell=False):
|
is_subshell=False,
|
||||||
|
auth_provider=None):
|
||||||
cmd.Cmd.__init__(self, completekey=completekey)
|
cmd.Cmd.__init__(self, completekey=completekey)
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.port = port
|
self.port = port
|
||||||
self.auth_provider = None
|
self.auth_provider = auth_provider
|
||||||
if username:
|
|
||||||
if not password:
|
|
||||||
password = getpass.getpass()
|
|
||||||
self.auth_provider = PlainTextAuthProvider(username=username, password=password)
|
|
||||||
self.username = username
|
self.username = username
|
||||||
|
|
||||||
|
if isinstance(auth_provider, PlainTextAuthProvider):
|
||||||
|
self.username = auth_provider.username
|
||||||
|
if not auth_provider.password:
|
||||||
|
# if no password is provided, we need to query the user to get one.
|
||||||
|
password = getpass.getpass()
|
||||||
|
self.auth_provider = PlainTextAuthProvider(username=auth_provider.username, password=password)
|
||||||
|
|
||||||
self.keyspace = keyspace
|
self.keyspace = keyspace
|
||||||
self.ssl = ssl
|
self.ssl = ssl
|
||||||
self.tracing_enabled = tracing_enabled
|
self.tracing_enabled = tracing_enabled
|
||||||
|
|
@ -1613,10 +1620,8 @@ class Shell(cmd.Cmd):
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
self.printerr('Could not open %r: %s' % (fname, e))
|
self.printerr('Could not open %r: %s' % (fname, e))
|
||||||
return
|
return
|
||||||
username = self.auth_provider.username if self.auth_provider else None
|
|
||||||
password = self.auth_provider.password if self.auth_provider else None
|
|
||||||
subshell = Shell(self.hostname, self.port, color=self.color,
|
subshell = Shell(self.hostname, self.port, color=self.color,
|
||||||
username=username, password=password,
|
username=self.username,
|
||||||
encoding=self.encoding, stdin=f, tty=False, use_conn=self.conn,
|
encoding=self.encoding, stdin=f, tty=False, use_conn=self.conn,
|
||||||
cqlver=self.cql_version, keyspace=self.current_keyspace,
|
cqlver=self.cql_version, keyspace=self.current_keyspace,
|
||||||
tracing_enabled=self.tracing_enabled,
|
tracing_enabled=self.tracing_enabled,
|
||||||
|
|
@ -1629,7 +1634,8 @@ class Shell(cmd.Cmd):
|
||||||
max_trace_wait=self.max_trace_wait, ssl=self.ssl,
|
max_trace_wait=self.max_trace_wait, ssl=self.ssl,
|
||||||
request_timeout=self.session.default_timeout,
|
request_timeout=self.session.default_timeout,
|
||||||
connect_timeout=self.conn.connect_timeout,
|
connect_timeout=self.conn.connect_timeout,
|
||||||
is_subshell=True)
|
is_subshell=True,
|
||||||
|
auth_provider=self.auth_provider)
|
||||||
# duplicate coverage related settings in subshell
|
# duplicate coverage related settings in subshell
|
||||||
if self.coverage:
|
if self.coverage:
|
||||||
subshell.coverage = True
|
subshell.coverage = True
|
||||||
|
|
@ -2077,21 +2083,6 @@ def should_use_color():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def is_file_secure(filename):
|
|
||||||
try:
|
|
||||||
st = os.stat(filename)
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno != errno.ENOENT:
|
|
||||||
raise
|
|
||||||
return True # the file doesn't exists, the security of it is irrelevant
|
|
||||||
|
|
||||||
uid = os.getuid()
|
|
||||||
|
|
||||||
# Skip enforcing the file owner and UID matching for the root user (uid == 0).
|
|
||||||
# This is to allow "sudo cqlsh" to work with user owned credentials file.
|
|
||||||
return (uid == 0 or st.st_uid == uid) and stat.S_IMODE(st.st_mode) & (stat.S_IRGRP | stat.S_IROTH) == 0
|
|
||||||
|
|
||||||
|
|
||||||
def read_options(cmdlineargs, environment):
|
def read_options(cmdlineargs, environment):
|
||||||
configs = configparser.ConfigParser()
|
configs = configparser.ConfigParser()
|
||||||
configs.read(CONFIG_FILE)
|
configs.read(CONFIG_FILE)
|
||||||
|
|
@ -2102,13 +2093,14 @@ def read_options(cmdlineargs, environment):
|
||||||
username_from_cqlshrc = option_with_default(configs.get, 'authentication', 'username')
|
username_from_cqlshrc = option_with_default(configs.get, 'authentication', 'username')
|
||||||
password_from_cqlshrc = option_with_default(rawconfigs.get, 'authentication', 'password')
|
password_from_cqlshrc = option_with_default(rawconfigs.get, 'authentication', 'password')
|
||||||
if username_from_cqlshrc or password_from_cqlshrc:
|
if username_from_cqlshrc or password_from_cqlshrc:
|
||||||
if password_from_cqlshrc and not is_file_secure(CONFIG_FILE):
|
if password_from_cqlshrc and not is_file_secure(os.path.expanduser(CONFIG_FILE)):
|
||||||
print("\nWarning: Password is found in an insecure cqlshrc file. The file is owned or readable by other users on the system.",
|
print("\nWarning: Password is found in an insecure cqlshrc file. The file is owned or readable by other users on the system.",
|
||||||
end='', file=sys.stderr)
|
end='', file=sys.stderr)
|
||||||
print("\nNotice: Credentials in the cqlshrc file is deprecated and will be ignored in the future."
|
print("\nNotice: Credentials in the cqlshrc file is deprecated and will be ignored in the future."
|
||||||
"\nPlease use a credentials file to specify the username and password.\n", file=sys.stderr)
|
"\nPlease use a credentials file to specify the username and password.\n", file=sys.stderr)
|
||||||
|
|
||||||
optvalues = optparse.Values()
|
optvalues = optparse.Values()
|
||||||
|
|
||||||
optvalues.username = None
|
optvalues.username = None
|
||||||
optvalues.password = None
|
optvalues.password = None
|
||||||
optvalues.credentials = os.path.expanduser(option_with_default(configs.get, 'authentication', 'credentials',
|
optvalues.credentials = os.path.expanduser(option_with_default(configs.get, 'authentication', 'credentials',
|
||||||
|
|
@ -2153,6 +2145,13 @@ def read_options(cmdlineargs, environment):
|
||||||
|
|
||||||
(options, arguments) = parser.parse_args(cmdlineargs, values=optvalues)
|
(options, arguments) = parser.parse_args(cmdlineargs, values=optvalues)
|
||||||
|
|
||||||
|
# Credentials from cqlshrc will be expanded,
|
||||||
|
# credentials from the command line are also expanded if there is a space...
|
||||||
|
# we need the following so that these two scenarios will work
|
||||||
|
# cqlsh --credentials=~/.cassandra/creds
|
||||||
|
# cqlsh --credentials ~/.cassandra/creds
|
||||||
|
options.credentials = os.path.expanduser(options.credentials)
|
||||||
|
|
||||||
if not is_file_secure(options.credentials):
|
if not is_file_secure(options.credentials):
|
||||||
print("\nWarning: Credentials file '{0}' exists but is not used, because:"
|
print("\nWarning: Credentials file '{0}' exists but is not used, because:"
|
||||||
"\n a. the file owner is not the current user; or"
|
"\n a. the file owner is not the current user; or"
|
||||||
|
|
@ -2169,7 +2168,7 @@ def read_options(cmdlineargs, environment):
|
||||||
credentials.read(options.credentials)
|
credentials.read(options.credentials)
|
||||||
|
|
||||||
# use the username from credentials file but fallback to cqlshrc if username is absent from the command line parameters
|
# use the username from credentials file but fallback to cqlshrc if username is absent from the command line parameters
|
||||||
options.username = option_with_default(credentials.get, 'plain_text_auth', 'username', username_from_cqlshrc)
|
options.username = username_from_cqlshrc
|
||||||
|
|
||||||
if not options.password:
|
if not options.password:
|
||||||
rawcredentials = configparser.RawConfigParser()
|
rawcredentials = configparser.RawConfigParser()
|
||||||
|
|
@ -2177,6 +2176,7 @@ def read_options(cmdlineargs, environment):
|
||||||
|
|
||||||
# handling password in the same way as username, priority cli > credentials > cqlshrc
|
# handling password in the same way as username, priority cli > credentials > cqlshrc
|
||||||
options.password = option_with_default(rawcredentials.get, 'plain_text_auth', 'password', password_from_cqlshrc)
|
options.password = option_with_default(rawcredentials.get, 'plain_text_auth', 'password', password_from_cqlshrc)
|
||||||
|
options.password = password_from_cqlshrc
|
||||||
elif not options.insecure_password_without_warning:
|
elif not options.insecure_password_without_warning:
|
||||||
print("\nWarning: Using a password on the command line interface can be insecure."
|
print("\nWarning: Using a password on the command line interface can be insecure."
|
||||||
"\nRecommendation: use the credentials file to securely provide the password.\n", file=sys.stderr)
|
"\nRecommendation: use the credentials file to securely provide the password.\n", file=sys.stderr)
|
||||||
|
|
@ -2330,7 +2330,6 @@ def main(options, hostname, port):
|
||||||
port,
|
port,
|
||||||
color=options.color,
|
color=options.color,
|
||||||
username=options.username,
|
username=options.username,
|
||||||
password=options.password,
|
|
||||||
stdin=stdin,
|
stdin=stdin,
|
||||||
tty=options.tty,
|
tty=options.tty,
|
||||||
completekey=options.completekey,
|
completekey=options.completekey,
|
||||||
|
|
@ -2349,7 +2348,12 @@ def main(options, hostname, port):
|
||||||
single_statement=options.execute,
|
single_statement=options.execute,
|
||||||
request_timeout=options.request_timeout,
|
request_timeout=options.request_timeout,
|
||||||
connect_timeout=options.connect_timeout,
|
connect_timeout=options.connect_timeout,
|
||||||
encoding=options.encoding)
|
encoding=options.encoding,
|
||||||
|
auth_provider=authproviderhandling.load_auth_provider(
|
||||||
|
config_file=CONFIG_FILE,
|
||||||
|
cred_file=options.credentials,
|
||||||
|
username=options.username,
|
||||||
|
password=options.password))
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
sys.exit('Connection aborted.')
|
sys.exit('Connection aborted.')
|
||||||
except CQL_ERRORS as e:
|
except CQL_ERRORS as e:
|
||||||
|
|
|
||||||
|
|
@ -392,7 +392,7 @@
|
||||||
|
|
||||||
<target name="realclean" depends="clean" description="Remove the entire build directory and all downloaded artifacts">
|
<target name="realclean" depends="clean" description="Remove the entire build directory and all downloaded artifacts">
|
||||||
<delete>
|
<delete>
|
||||||
<fileset dir="${build.lib}" excludes="cassandra-driver-internal-only-*"/>
|
<fileset dir="${build.lib}" excludes="cassandra-driver-internal-only-*,puresasl-internal-only-*"/>
|
||||||
</delete>
|
</delete>
|
||||||
<delete dir="${build.dir}" />
|
<delete dir="${build.dir}" />
|
||||||
<delete dir="${doc.dir}/build" />
|
<delete dir="${doc.dir}/build" />
|
||||||
|
|
@ -1333,9 +1333,10 @@
|
||||||
<exclude name="ide/nbproject/private/**" />
|
<exclude name="ide/nbproject/private/**" />
|
||||||
</tarfileset>
|
</tarfileset>
|
||||||
|
|
||||||
<!-- python driver -->
|
<!-- python driver, puresasl for SASL / GSSAPI -->
|
||||||
<tarfileset dir="${basedir}" prefix="${final.name}-src">
|
<tarfileset dir="${basedir}" prefix="${final.name}-src">
|
||||||
<include name="lib/cassandra-driver-internal-only-**" />
|
<include name="lib/cassandra-driver-internal-only-**" />
|
||||||
|
<include name="lib/puresasl-internal-only-**" />
|
||||||
</tarfileset>
|
</tarfileset>
|
||||||
|
|
||||||
<!-- Shell includes in bin/ and tools/bin/ -->
|
<!-- Shell includes in bin/ and tools/bin/ -->
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,14 @@
|
||||||
; keyspace = ks1
|
; keyspace = ks1
|
||||||
|
|
||||||
|
|
||||||
|
[auth_provider]
|
||||||
|
;; you can specify any auth provider found in your python environment
|
||||||
|
;; module and class will be used to dynamically load the class
|
||||||
|
;; all other properties found here and in the credentials file under the class name
|
||||||
|
;; will be passed to the constructor
|
||||||
|
; module = cassandra.auth
|
||||||
|
; classname = PlainTextAuthProvider
|
||||||
|
; username = user1
|
||||||
|
|
||||||
[ui]
|
[ui]
|
||||||
;; Whether or not to display query results with colors
|
;; Whether or not to display query results with colors
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
;
|
;
|
||||||
; Please ensure this file is owned by the user and is not readable by group and other users
|
; Please ensure this file is owned by the user and is not readable by group and other users
|
||||||
|
|
||||||
[plain_text_auth]
|
[PlainTextAuthProvider]
|
||||||
; username = fred
|
; username = fred
|
||||||
; password = !!bang!!$
|
; password = !!bang!!$
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,176 @@
|
||||||
|
# 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.
|
||||||
|
"""
|
||||||
|
Handles loading of AuthProvider for CQLSH authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
import sys
|
||||||
|
from importlib import import_module
|
||||||
|
from cqlshlib.util import is_file_secure
|
||||||
|
|
||||||
|
|
||||||
|
def _warn_for_plain_text_security(config_file, provider_settings):
|
||||||
|
"""
|
||||||
|
Call when using PlainTextAuthProvider
|
||||||
|
check to see if password appears in the basic provider settings
|
||||||
|
as this is a security risk
|
||||||
|
|
||||||
|
Will write errors to stderr
|
||||||
|
"""
|
||||||
|
if 'password' in provider_settings:
|
||||||
|
if not is_file_secure(config_file):
|
||||||
|
print("""\nWarning: Password is found in an insecure cqlshrc file.
|
||||||
|
The file is owned or readable by other users on the system.""",
|
||||||
|
end='',
|
||||||
|
file=sys.stderr)
|
||||||
|
print("""\nNotice: Credentials in the cqlshrc file is deprecated and
|
||||||
|
will be ignored in the future.\n
|
||||||
|
Please use a credentials file to
|
||||||
|
specify the username and password.\n""",
|
||||||
|
file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def load_auth_provider(config_file=None, cred_file=None, username=None, password=None):
|
||||||
|
"""
|
||||||
|
Function which loads an auth provider from available config.
|
||||||
|
|
||||||
|
Params:
|
||||||
|
* config_file ..: path to cqlsh config file (usually ~/.cassandra/cqlshrc).
|
||||||
|
* cred_file ....: path to cqlsh credentials file (default is ~/.cassandra/credentials).
|
||||||
|
* username .....: override used to return PlainTextAuthProvider according to legacy case
|
||||||
|
* password .....: override used to return PlainTextAuthProvider according to legacy case
|
||||||
|
|
||||||
|
Will attempt to load an auth provider from available config file, using what's found in
|
||||||
|
credentials file as an override.
|
||||||
|
|
||||||
|
Config file is expected to list module name /class in the *auth_provider*
|
||||||
|
section for dynamic loading (which is to be of type auth_provider)
|
||||||
|
|
||||||
|
Additional params passed to the constructor of class should be specified
|
||||||
|
in the *auth_provider* section and can be freely named to match
|
||||||
|
auth provider's expectation.
|
||||||
|
|
||||||
|
If passed username and password these will be overridden and passed to auth provider
|
||||||
|
|
||||||
|
None is returned if no possible auth provider is found, and no username/password can be
|
||||||
|
returned. If a username is found, system will assume that PlainTextAuthProvider was
|
||||||
|
specified
|
||||||
|
|
||||||
|
EXAMPLE CQLSHRC:
|
||||||
|
# .. inside cqlshrc file
|
||||||
|
|
||||||
|
[auth_provider]
|
||||||
|
module = cassandra.auth
|
||||||
|
classname = PlainTextAuthProvider
|
||||||
|
username = user1
|
||||||
|
password = password1
|
||||||
|
|
||||||
|
if credentials file is specified put relevant properties under the class name
|
||||||
|
EXAMPLE
|
||||||
|
# ... inside credentials file for above example
|
||||||
|
[PlainTextAuthProvider]
|
||||||
|
password = password2
|
||||||
|
|
||||||
|
Credential attributes will override found in the cqlshrc.
|
||||||
|
in the above example, PlainTextAuthProvider would be used with a password of 'password2',
|
||||||
|
and username of 'user1'
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_settings_from_config(section_name,
|
||||||
|
conf_file,
|
||||||
|
interpolation=configparser.BasicInterpolation()):
|
||||||
|
"""
|
||||||
|
Returns dict from section_name, and ini based conf_file
|
||||||
|
|
||||||
|
* section_name ..: Section to read map of properties from (ex: [auth_provider])
|
||||||
|
* conf_file .....: Ini based config file to read. Will return empty dict if None.
|
||||||
|
* interpolation .: Interpolation to use.
|
||||||
|
|
||||||
|
If section is not found, or conf_file is None, function will return an empty dictionary.
|
||||||
|
"""
|
||||||
|
conf = configparser.ConfigParser(interpolation=interpolation)
|
||||||
|
if conf_file is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
conf.read(conf_file)
|
||||||
|
if section_name in conf.sections():
|
||||||
|
return dict(conf.items(section_name))
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_cred_file_settings(classname, creds_file):
|
||||||
|
# Since this is the credentials file we may be encountering raw strings
|
||||||
|
# as these are what passwords, or security tokens may inadvertently fall into
|
||||||
|
# we don't want interpolation to mess with them.
|
||||||
|
return get_settings_from_config(
|
||||||
|
section_name=classname,
|
||||||
|
conf_file=creds_file,
|
||||||
|
interpolation=None)
|
||||||
|
|
||||||
|
def get_auth_provider_settings(conf_file):
|
||||||
|
return get_settings_from_config(
|
||||||
|
section_name='auth_provider',
|
||||||
|
conf_file=conf_file)
|
||||||
|
|
||||||
|
def get_legacy_settings(legacy_username, legacy_password):
|
||||||
|
result = {}
|
||||||
|
if legacy_username is not None:
|
||||||
|
result['username'] = legacy_username
|
||||||
|
if legacy_password is not None:
|
||||||
|
result['password'] = legacy_password
|
||||||
|
return result
|
||||||
|
|
||||||
|
provider_settings = get_auth_provider_settings(config_file)
|
||||||
|
|
||||||
|
module_name = provider_settings.pop('module', None)
|
||||||
|
class_name = provider_settings.pop('classname', None)
|
||||||
|
|
||||||
|
if module_name is None and class_name is None:
|
||||||
|
# not specified, default to plaintext auth provider
|
||||||
|
module_name = 'cassandra.auth'
|
||||||
|
class_name = 'PlainTextAuthProvider'
|
||||||
|
elif module_name is None or class_name is None:
|
||||||
|
# then this was PARTIALLY specified.
|
||||||
|
return None
|
||||||
|
|
||||||
|
credential_settings = get_cred_file_settings(class_name, cred_file)
|
||||||
|
|
||||||
|
if module_name == 'cassandra.auth' and class_name == 'PlainTextAuthProvider':
|
||||||
|
# merge credential settings as overrides on top of provider settings.
|
||||||
|
|
||||||
|
# we need to ensure that password property gets "set" in all cases.
|
||||||
|
# this is to support the ability to give the user a prompt in other parts
|
||||||
|
# of the code.
|
||||||
|
_warn_for_plain_text_security(config_file, provider_settings)
|
||||||
|
ctor_args = {'password': None,
|
||||||
|
**provider_settings,
|
||||||
|
**credential_settings,
|
||||||
|
**get_legacy_settings(username, password)}
|
||||||
|
# if no username, we can't create PlainTextAuthProvider
|
||||||
|
if 'username' not in ctor_args:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
# merge credential settings as overrides on top of provider settings.
|
||||||
|
ctor_args = {**provider_settings,
|
||||||
|
**credential_settings,
|
||||||
|
**get_legacy_settings(username, password)}
|
||||||
|
|
||||||
|
# Load class definitions
|
||||||
|
module = import_module(module_name)
|
||||||
|
auth_provider_klass = getattr(module, class_name)
|
||||||
|
|
||||||
|
# instantiate the class
|
||||||
|
return auth_provider_klass(**ctor_args)
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
# 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 unittest
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cassandra.auth import PlainTextAuthProvider
|
||||||
|
from cqlshlib.authproviderhandling import load_auth_provider
|
||||||
|
|
||||||
|
|
||||||
|
def construct_config_path(config_file_name):
|
||||||
|
return os.path.join(os.path.dirname(__file__),
|
||||||
|
'test_authproviderhandling_config',
|
||||||
|
config_file_name)
|
||||||
|
|
||||||
|
|
||||||
|
# Simple class to help verify AuthProviders that don't need arguments.
|
||||||
|
class NoUserNamePlainTextAuthProvider(PlainTextAuthProvider):
|
||||||
|
def __init__(self):
|
||||||
|
super(NoUserNamePlainTextAuthProvider, self).__init__('', '')
|
||||||
|
|
||||||
|
|
||||||
|
class ComplexTextAuthProvider(PlainTextAuthProvider):
|
||||||
|
def __init__(self, username, password='default_pass', extra_flag=None):
|
||||||
|
super(ComplexTextAuthProvider, self).__init__(username, password)
|
||||||
|
self.extra_flag = extra_flag
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_auth_provider_matches(actual, klass, expected_props):
|
||||||
|
"""
|
||||||
|
Assert that the provider matches class and properties
|
||||||
|
* actual ..........: Thing to compare with it
|
||||||
|
* klass ...........: Class to ensure this matches to (ie PlainTextAuthProvider)
|
||||||
|
* expected_props ..: Dict of var properties to match
|
||||||
|
"""
|
||||||
|
assert isinstance(actual, klass)
|
||||||
|
assert expected_props == vars(actual)
|
||||||
|
|
||||||
|
class CustomAuthProviderTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self._captured_std_err = io.StringIO()
|
||||||
|
sys.stderr = self._captured_std_err
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self._captured_std_err.close()
|
||||||
|
sys.stdout = sys.__stderr__
|
||||||
|
|
||||||
|
def test_no_warning_insecure_if_no_pass(self):
|
||||||
|
load_auth_provider(construct_config_path('plain_text_partial_example'))
|
||||||
|
err_msg = self._captured_std_err.getvalue()
|
||||||
|
assert err_msg == ''
|
||||||
|
|
||||||
|
def test_insecure_creds(self):
|
||||||
|
load_auth_provider(construct_config_path('full_plain_text_example'))
|
||||||
|
err_msg = self._captured_std_err.getvalue()
|
||||||
|
assert "Notice:" in err_msg
|
||||||
|
assert "Warning:" in err_msg
|
||||||
|
|
||||||
|
def test_creds_not_checked_for_non_plaintext(self):
|
||||||
|
load_auth_provider(construct_config_path('complex_auth_provider_with_pass'))
|
||||||
|
err_msg = self._captured_std_err.getvalue()
|
||||||
|
assert err_msg == ''
|
||||||
|
|
||||||
|
def test_partial_property_example(self):
|
||||||
|
actual = load_auth_provider(construct_config_path('partial_example'))
|
||||||
|
_assert_auth_provider_matches(
|
||||||
|
actual,
|
||||||
|
NoUserNamePlainTextAuthProvider,
|
||||||
|
{"username": '',
|
||||||
|
"password": ''})
|
||||||
|
|
||||||
|
def test_full_property_example(self):
|
||||||
|
actual = load_auth_provider(construct_config_path('full_plain_text_example'))
|
||||||
|
_assert_auth_provider_matches(
|
||||||
|
actual,
|
||||||
|
PlainTextAuthProvider,
|
||||||
|
{"username": 'user1',
|
||||||
|
"password": 'pass1'})
|
||||||
|
|
||||||
|
def test_empty_example(self):
|
||||||
|
actual = load_auth_provider(construct_config_path('empty_example'))
|
||||||
|
assert actual is None
|
||||||
|
|
||||||
|
def test_plaintextauth_when_not_defined(self):
|
||||||
|
creds_file = construct_config_path('plain_text_full_creds')
|
||||||
|
actual = load_auth_provider(cred_file=creds_file)
|
||||||
|
_assert_auth_provider_matches(
|
||||||
|
actual,
|
||||||
|
PlainTextAuthProvider,
|
||||||
|
{"username": 'user2',
|
||||||
|
"password": 'pass2'})
|
||||||
|
|
||||||
|
def test_no_cqlshrc_file(self):
|
||||||
|
actual = load_auth_provider()
|
||||||
|
assert actual is None
|
||||||
|
|
||||||
|
def test_no_classname_example(self):
|
||||||
|
actual = load_auth_provider(construct_config_path('no_classname_example'))
|
||||||
|
assert actual is None
|
||||||
|
|
||||||
|
def test_improper_config_example(self):
|
||||||
|
with pytest.raises(ModuleNotFoundError) as error:
|
||||||
|
load_auth_provider(construct_config_path('illegal_example'))
|
||||||
|
assert error is not None
|
||||||
|
|
||||||
|
def test_username_password_passed_from_commandline(self):
|
||||||
|
creds_file = construct_config_path('complex_auth_provider_creds')
|
||||||
|
cqlshrc = construct_config_path('complex_auth_provider')
|
||||||
|
|
||||||
|
actual = load_auth_provider(cqlshrc, creds_file, 'user-from-legacy', 'pass-from-legacy')
|
||||||
|
_assert_auth_provider_matches(
|
||||||
|
actual,
|
||||||
|
ComplexTextAuthProvider,
|
||||||
|
{"username": 'user-from-legacy',
|
||||||
|
"password": 'pass-from-legacy',
|
||||||
|
"extra_flag": 'flag2'})
|
||||||
|
|
||||||
|
def test_creds_example(self):
|
||||||
|
creds_file = construct_config_path('complex_auth_provider_creds')
|
||||||
|
cqlshrc = construct_config_path('complex_auth_provider')
|
||||||
|
|
||||||
|
actual = load_auth_provider(cqlshrc, creds_file)
|
||||||
|
_assert_auth_provider_matches(
|
||||||
|
actual,
|
||||||
|
ComplexTextAuthProvider,
|
||||||
|
{"username": 'user1',
|
||||||
|
"password": 'pass2',
|
||||||
|
"extra_flag": 'flag2'})
|
||||||
|
|
||||||
|
def test_legacy_example_use_passed_username(self):
|
||||||
|
creds_file = construct_config_path('plain_text_partial_creds')
|
||||||
|
cqlshrc = construct_config_path('plain_text_partial_example')
|
||||||
|
|
||||||
|
actual = load_auth_provider(cqlshrc, creds_file, 'user3')
|
||||||
|
_assert_auth_provider_matches(
|
||||||
|
actual,
|
||||||
|
PlainTextAuthProvider,
|
||||||
|
{"username": 'user3',
|
||||||
|
"password": 'pass2'})
|
||||||
|
|
||||||
|
def test_legacy_example_no_auth_provider_given(self):
|
||||||
|
cqlshrc = construct_config_path('empty_example')
|
||||||
|
creds_file = construct_config_path('complex_auth_provider_creds')
|
||||||
|
|
||||||
|
actual = load_auth_provider(cqlshrc, creds_file, 'user3', 'pass3')
|
||||||
|
_assert_auth_provider_matches(
|
||||||
|
actual,
|
||||||
|
PlainTextAuthProvider,
|
||||||
|
{"username": 'user3',
|
||||||
|
"password": 'pass3'})
|
||||||
|
|
||||||
|
def test_shouldnt_pass_no_password_when_alt_auth_provider(self):
|
||||||
|
cqlshrc = construct_config_path('complex_auth_provider')
|
||||||
|
creds_file = None
|
||||||
|
|
||||||
|
actual = load_auth_provider(cqlshrc, creds_file, 'user3')
|
||||||
|
_assert_auth_provider_matches(
|
||||||
|
actual,
|
||||||
|
ComplexTextAuthProvider,
|
||||||
|
{"username": 'user3',
|
||||||
|
"password": 'default_pass',
|
||||||
|
"extra_flag": 'flag1'})
|
||||||
|
|
||||||
|
def test_legacy_example_no_password(self):
|
||||||
|
cqlshrc = construct_config_path('plain_text_partial_example')
|
||||||
|
creds_file = None
|
||||||
|
|
||||||
|
actual = load_auth_provider(cqlshrc, creds_file, 'user3')
|
||||||
|
_assert_auth_provider_matches(
|
||||||
|
actual,
|
||||||
|
PlainTextAuthProvider,
|
||||||
|
{"username": 'user3',
|
||||||
|
"password": None})
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
; Config for a custom auth provider that uses the auth_provider field
|
||||||
|
; ComplexTextAuthProvider is a PlainTextAuthProvider in the driver which
|
||||||
|
; takes an extra field (extra_flag).
|
||||||
|
; used by unit testing
|
||||||
|
|
||||||
|
[auth_provider]
|
||||||
|
module = cqlshlib.test.test_authproviderhandling
|
||||||
|
classname = ComplexTextAuthProvider
|
||||||
|
username = user1
|
||||||
|
extra_flag = flag1
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
[ComplexTextAuthProvider]
|
||||||
|
extra_flag = flag2
|
||||||
|
password = pass2
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
; Config for a custom auth provider that uses the auth_provider field
|
||||||
|
; ComplexTextAuthProvider is a PlainTextAuthProvider in the driver which
|
||||||
|
; takes an extra field (extra_flag).
|
||||||
|
; used by unit testing
|
||||||
|
|
||||||
|
[auth_provider]
|
||||||
|
module = cqlshlib.test.test_authproviderhandling
|
||||||
|
classname = ComplexTextAuthProvider
|
||||||
|
username = user1
|
||||||
|
password = pass1
|
||||||
|
extra_flag = flag1
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
; Config for a custom auth provider that uses only the auth_provider field
|
||||||
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
; Config for a custom auth provider that uses all possible fields
|
||||||
|
; This example loads the PlainTextAuthProvider and passes username and password to constructor
|
||||||
|
; dynamically.
|
||||||
|
; used by unit testing
|
||||||
|
|
||||||
|
[auth_provider]
|
||||||
|
module = cassandra.auth
|
||||||
|
classname = PlainTextAuthProvider
|
||||||
|
username = user1
|
||||||
|
password = pass1
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
; Example that shouldn't work
|
||||||
|
|
||||||
|
[auth_provider]
|
||||||
|
module = nowhere.illegal.wrong
|
||||||
|
classname = badclass
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
; Config for a custom auth provider that uses only the auth_provider field
|
||||||
|
; this version doesn't have a classname, but has a module name.
|
||||||
|
|
||||||
|
[auth_provider]
|
||||||
|
module = cqlshlib.test
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
; Config for a custom auth provider that uses only the auth_provider field
|
||||||
|
; NoUserNamePlainTextAuthProvider is a PlainTextAuthProvider in the driver which
|
||||||
|
; doesn't take a username or password.
|
||||||
|
; used by unit testing
|
||||||
|
|
||||||
|
[auth_provider]
|
||||||
|
module = cqlshlib.test.test_authproviderhandling
|
||||||
|
classname = NoUserNamePlainTextAuthProvider
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
[PlainTextAuthProvider]
|
||||||
|
password = pass2
|
||||||
|
username = user2
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
[PlainTextAuthProvider]
|
||||||
|
password = pass2
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
; Config for a custom auth provider that uses some possible fields
|
||||||
|
; validate that the partial breakdown works successfully
|
||||||
|
; used by unit testing
|
||||||
|
|
||||||
|
[auth_provider]
|
||||||
|
module = cassandra.auth
|
||||||
|
classname = PlainTextAuthProvider
|
||||||
|
username = user1
|
||||||
|
|
@ -18,7 +18,9 @@
|
||||||
import cProfile
|
import cProfile
|
||||||
import codecs
|
import codecs
|
||||||
import pstats
|
import pstats
|
||||||
|
import os
|
||||||
|
import errno
|
||||||
|
import stat
|
||||||
|
|
||||||
from datetime import timedelta, tzinfo
|
from datetime import timedelta, tzinfo
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
@ -112,6 +114,21 @@ def trim_if_present(s, prefix):
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def is_file_secure(filename):
|
||||||
|
try:
|
||||||
|
st = os.stat(filename)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.ENOENT:
|
||||||
|
raise
|
||||||
|
# the file doesn't exist, the security of it is irrelevant
|
||||||
|
return True
|
||||||
|
uid = os.getuid()
|
||||||
|
|
||||||
|
# Skip enforcing the file owner and UID matching for the root user (uid == 0).
|
||||||
|
# This is to allow "sudo cqlsh" to work with user owned credentials file.
|
||||||
|
return (uid == 0 or st.st_uid == uid) and stat.S_IMODE(st.st_mode) & (stat.S_IRGRP | stat.S_IROTH) == 0
|
||||||
|
|
||||||
|
|
||||||
def get_file_encoding_bomsize(filename):
|
def get_file_encoding_bomsize(filename):
|
||||||
"""
|
"""
|
||||||
Checks the beginning of a file for a Unicode BOM. Based on this check,
|
Checks the beginning of a file for a Unicode BOM. Based on this check,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue