Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ lib/
pylib/src/
**/cqlshlib.xml
!lib/cassandra-driver-internal-only-*.zip
!lib/puresasl-*.zip

# C* debs
build-stamp
Expand Down
57 changes: 39 additions & 18 deletions bin/cqlsh.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@
if os.environ.get('CQLSH_NO_BUNDLED', ''):
ZIPLIB_DIRS = ()


def find_zip(libprefix):
for ziplibdir in ZIPLIB_DIRS:
zips = glob(os.path.join(ziplibdir, libprefix + '*.zip'))
Expand All @@ -113,7 +112,7 @@ def find_zip(libprefix):
sys.path.insert(0, os.path.join(cql_zip, 'cassandra-driver-' + ver))

# the driver needs dependencies
third_parties = ('six-')
third_parties = ('six-','puresasl-')

for lib in third_parties:
lib_zip = find_zip(lib)
Expand Down Expand Up @@ -145,7 +144,7 @@ def find_zip(libprefix):
if os.path.isdir(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.displaying import (ANSI_RESET, BLUE, COLUMN_NAME_COLORS, CYAN,
RED, WHITE, FormattedValue, colorme)
Expand All @@ -155,6 +154,7 @@ def find_zip(libprefix):
from cqlshlib.tracing import print_trace, print_trace_session
from cqlshlib.util import get_file_encoding_bomsize


DEFAULT_HOST = '127.0.0.1'
DEFAULT_PORT = 9042
DEFAULT_SSL = False
Expand Down Expand Up @@ -426,7 +426,7 @@ class Shell(cmd.Cmd):
default_page_size = 100

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,
cqlver=None, keyspace=None,
tracing_enabled=False, expand_enabled=False,
Expand All @@ -442,15 +442,19 @@ def __init__(self, hostname, port, color=False,
request_timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
protocol_version=None,
connect_timeout=DEFAULT_CONNECT_TIMEOUT_SECONDS,
is_subshell=False):
is_subshell=False,
auth_provider=None):
cmd.Cmd.__init__(self, completekey=completekey)
self.hostname = hostname
self.port = port
self.auth_provider = None
if username:
if not password:
self.auth_provider = auth_provider

if isinstance(auth_provider, PlainTextAuthProvider):
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=username, password=password)
self.auth_provider = PlainTextAuthProvider(username=auth_provider.username, password=password)

self.username = username
self.keyspace = keyspace
self.ssl = ssl
Expand Down Expand Up @@ -1613,10 +1617,8 @@ def do_source(self, parsed):
except IOError as e:
self.printerr('Could not open %r: %s' % (fname, e))
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,
username=username, password=password,
username=self.username,
encoding=self.encoding, stdin=f, tty=False, use_conn=self.conn,
cqlver=self.cql_version, keyspace=self.current_keyspace,
tracing_enabled=self.tracing_enabled,
Expand All @@ -1629,7 +1631,8 @@ def do_source(self, parsed):
max_trace_wait=self.max_trace_wait, ssl=self.ssl,
request_timeout=self.session.default_timeout,
connect_timeout=self.conn.connect_timeout,
is_subshell=True)
is_subshell=True,
auth_provider=self.auth_provider)
# duplicate coverage related settings in subshell
if self.coverage:
subshell.coverage = True
Expand Down Expand Up @@ -2091,7 +2094,6 @@ def is_file_secure(filename):
# 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):
configs = configparser.ConfigParser()
configs.read(CONFIG_FILE)
Expand All @@ -2109,6 +2111,7 @@ def read_options(cmdlineargs, environment):
"\nPlease use a credentials file to specify the username and password.\n", file=sys.stderr)

optvalues = optparse.Values()

optvalues.username = None
optvalues.password = None
optvalues.credentials = os.path.expanduser(option_with_default(configs.get, 'authentication', 'credentials',
Expand Down Expand Up @@ -2152,6 +2155,13 @@ def read_options(cmdlineargs, environment):
optvalues.insecure_password_without_warning = False

(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):
print("\nWarning: Credentials file '{0}' exists but is not used, because:"
Expand All @@ -2164,19 +2174,26 @@ def read_options(cmdlineargs, environment):
file=sys.stderr)
options.credentials = '' # ConfigParser.read() will ignore unreadable files

provider = 'PlainTextAuthProvider'
if not options.username or not options.password:
if "auth_provider" in configs.sections():
auth_provider = dict(configs.items('auth_provider'))
if "classname" in auth_provider:
provider = auth_provider["classname"]

if not options.username:
credentials = configparser.ConfigParser()
credentials.read(options.credentials)

# 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 = option_with_default(credentials.get, provider, 'username', username_from_cqlshrc)

if not options.password:
rawcredentials = configparser.RawConfigParser()
rawcredentials.read(options.credentials)

# 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, provider, 'password', password_from_cqlshrc)
elif not options.insecure_password_without_warning:
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)
Expand Down Expand Up @@ -2330,7 +2347,6 @@ def main(options, hostname, port):
port,
color=options.color,
username=options.username,
password=options.password,
stdin=stdin,
tty=options.tty,
completekey=options.completekey,
Expand All @@ -2349,7 +2365,12 @@ def main(options, hostname, port):
single_statement=options.execute,
request_timeout=options.request_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:
sys.exit('Connection aborted.')
except CQL_ERRORS as e:
Expand Down
5 changes: 3 additions & 2 deletions build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@

<target name="realclean" depends="clean" description="Remove the entire build directory and all downloaded artifacts">
<delete>
<fileset dir="${build.lib}" excludes="cassandra-driver-internal-only-*"/>
<fileset dir="${build.lib}" excludes="cassandra-driver-internal-only-*,puresasl-internal-only-*"/>
</delete>
<delete dir="${build.dir}" />
<delete dir="${doc.dir}/build" />
Expand Down Expand Up @@ -1328,9 +1328,10 @@
<exclude name="ide/nbproject/private/**" />
</tarfileset>

<!-- python driver -->
<!-- python driver, puresasl for SASL / GSSAPI -->
<tarfileset dir="${basedir}" prefix="${final.name}-src">
<include name="lib/cassandra-driver-internal-only-**" />
<include name="lib/puresasl-internal-only-**" />
</tarfileset>

<!-- Shell includes in bin/ and tools/bin/ -->
Expand Down
2 changes: 1 addition & 1 deletion conf/credentials.sample
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
;
; Please ensure this file is owned by the user and is not readable by group and other users

[plain_text_auth]
[PlainTextAuthProvider]
; username = fred
; password = !!bang!!$

Binary file added lib/puresasl-internal-only-0.6.2.zip
Binary file not shown.
148 changes: 148 additions & 0 deletions pylib/cqlshlib/authproviderhandling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# 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
from importlib import import_module


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, function will return a PlainTextAuthProvider using the
traditional logic. It will try to use properties specified in [Auth_provider] section if
the PlainTextAuthProvider is the specified class.

None is returned if no possible auth provider is found.

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 a legacy username or password is passed to us
# regardless of what the module / class is specified, we have been overridden to using
# PlainTextAuthProvider
if username is not None or password is not None:
module_name = 'cassandra.auth'
class_name = 'PlainTextAuthProvider'

if module_name is None or class_name is None:
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.
ctor_args = {'password': None,
**provider_settings,
**credential_settings,
**get_legacy_settings(username, password)}
else:
# merge credential settings as overrides on top of provider settings.
ctor_args = {**provider_settings, **credential_settings}

# Load class definitions
module = import_module(module_name)
auth_provider_klass = getattr(module, class_name)

# instantiate the class
return auth_provider_klass(**ctor_args)
Loading