Skip to content

Commit ceb29dc

Browse files
authored
Support DSE auth transitional mode
1 parent b11a515 commit ceb29dc

File tree

10 files changed

+161
-46
lines changed

10 files changed

+161
-46
lines changed

lib/auth/index.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,26 @@
1414
* limitations under the License.
1515
*/
1616
'use strict';
17+
1718
/**
1819
* DSE Authentication module.
1920
* <p>
2021
* Contains the classes used for connecting to a DSE cluster secured with DseAuthenticator.
2122
* </p>
2223
* @module auth
2324
*/
24-
const baseProvider = require('./provider.js');
25-
exports.AuthProvider = baseProvider.AuthProvider;
26-
exports.Authenticator = baseProvider.Authenticator;
27-
exports.PlainTextAuthProvider = require('./plain-text-auth-provider.js');
28-
exports.DseGssapiAuthProvider = require('./dse-gssapi-auth-provider');
29-
exports.DsePlainTextAuthProvider = require('./dse-plain-text-auth-provider');
25+
26+
const { Authenticator, AuthProvider } = require('./provider');
27+
const { PlainTextAuthProvider } = require('./plain-text-auth-provider');
28+
const DseGssapiAuthProvider = require('./dse-gssapi-auth-provider');
29+
const DsePlainTextAuthProvider = require('./dse-plain-text-auth-provider');
30+
const NoAuthProvider = require('./no-auth-provider');
31+
32+
module.exports = {
33+
Authenticator,
34+
AuthProvider,
35+
DseGssapiAuthProvider,
36+
DsePlainTextAuthProvider,
37+
NoAuthProvider,
38+
PlainTextAuthProvider
39+
};

lib/auth/no-auth-provider.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright DataStax, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
'use strict';
18+
19+
const { AuthProvider, Authenticator } = require('./provider');
20+
const { PlainTextAuthenticator } = require('./plain-text-auth-provider');
21+
const errors = require('../errors');
22+
23+
const dseAuthenticator = 'com.datastax.bdp.cassandra.auth.DseAuthenticator';
24+
25+
/**
26+
* Internal authentication provider that is used when no provider has been set by the user.
27+
* @ignore
28+
*/
29+
class NoAuthProvider extends AuthProvider {
30+
newAuthenticator(endpoint, name) {
31+
if (name === dseAuthenticator) {
32+
// Try to use transitional mode
33+
return new TransitionalModePlainTextAuthenticator();
34+
}
35+
36+
// Use an authenticator that doesn't allow auth flow
37+
return new NoAuthAuthenticator(endpoint);
38+
}
39+
}
40+
41+
/**
42+
* An authenticator throws an error when authentication flow is started.
43+
* @ignore
44+
*/
45+
class NoAuthAuthenticator extends Authenticator {
46+
constructor(endpoint) {
47+
super();
48+
this.endpoint = endpoint;
49+
}
50+
51+
initialResponse(callback) {
52+
callback(new errors.AuthenticationError(
53+
`Host ${this.endpoint} requires authentication, but no authenticator found in the options`));
54+
}
55+
}
56+
57+
/**
58+
* Authenticator that accounts for DSE authentication configured with transitional mode: normal.
59+
*
60+
* In this situation, the client is allowed to connect without authentication, but DSE
61+
* would still send an AUTHENTICATE response. This Authenticator handles this situation
62+
* by sending back a dummy credential.
63+
*/
64+
class TransitionalModePlainTextAuthenticator extends PlainTextAuthenticator {
65+
constructor() {
66+
super('', '');
67+
}
68+
}
69+
70+
module.exports = NoAuthProvider;

lib/auth/plain-text-auth-provider.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,7 @@ PlainTextAuthenticator.prototype.evaluateChallenge = function (challenge, callba
7575
callback();
7676
};
7777

78-
module.exports = PlainTextAuthProvider;
78+
module.exports = {
79+
PlainTextAuthenticator,
80+
PlainTextAuthProvider,
81+
};

lib/client-options.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -257,17 +257,18 @@ function validateSocketOptions(socketOptions) {
257257
* @private
258258
*/
259259
function validateAuthenticationOptions(options) {
260-
const credentials = options.credentials;
260+
if (!options.authProvider) {
261+
const credentials = options.credentials;
262+
if (credentials) {
263+
if (typeof credentials.username !== 'string' || typeof credentials.password !== 'string') {
264+
throw new TypeError('credentials username and password must be a string');
265+
}
261266

262-
if (credentials && !options.authProvider) {
263-
if (typeof credentials.username !== 'string' || typeof credentials.password !== 'string') {
264-
throw new TypeError('credentials username and password must be a string');
267+
options.authProvider = new auth.PlainTextAuthProvider(credentials.username, credentials.password);
268+
} else {
269+
options.authProvider = new auth.NoAuthProvider();
265270
}
266-
267-
options.authProvider = new auth.PlainTextAuthProvider(credentials.username, credentials.password);
268-
}
269-
270-
if (options.authProvider && !(options.authProvider instanceof auth.AuthProvider)) {
271+
} else if (!(options.authProvider instanceof auth.AuthProvider)) {
271272
throw new TypeError('options.authProvider must be an instance of AuthProvider');
272273
}
273274
}

lib/datastax/cloud/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const { URL } = require('url');
2424

2525
const errors = require('../../errors');
2626
const utils = require('../../utils');
27-
const { DsePlainTextAuthProvider } = require('../../auth');
27+
const { DsePlainTextAuthProvider, NoAuthProvider } = require('../../auth');
2828

2929
// Use the callback-based method fs.readFile() instead of fs.promises as we have to support Node.js 8+
3030
const readFile = util.promisify(fs.readFile);
@@ -91,7 +91,7 @@ class CloudOptions {
9191
return;
9292
}
9393

94-
if (this.clientOptions.authProvider) {
94+
if (this.clientOptions.authProvider && !(this.clientOptions.authProvider instanceof NoAuthProvider)) {
9595
// There is an auth provider set by the user
9696
return;
9797
}

lib/insights-client.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const requests = require('./requests');
2626
const { ExecutionOptions } = require('./execution-options');
2727
const packageInfo = require('../package.json');
2828
const VersionNumber = require('./types/version-number');
29+
const { NoAuthProvider } = require('./auth');
2930

3031
let kerberosModule;
3132

@@ -223,7 +224,7 @@ class InsightsClient {
223224
certValidation: options.sslOptions ? !!options.sslOptions.rejectUnauthorized : undefined
224225
},
225226
authProvider: {
226-
type: getConstructor(options.authProvider),
227+
type: !(options.authProvider instanceof NoAuthProvider) ? getConstructor(options.authProvider) : undefined,
227228
},
228229
otherOptions: {
229230
coalescingThreshold: options.socketOptions.coalescingThreshold,

test/integration/short/auth/dse-plain-text-auth-provider-tests.js

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,25 @@
1414
* limitations under the License.
1515
*/
1616
'use strict';
17-
const assert = require('assert');
17+
const { assert } = require('chai');
1818
const helper = require('../../../test-helper');
1919
const DsePlainTextAuthProvider = require('../../../../lib/auth/dse-plain-text-auth-provider');
2020
const Client = require('../../../../lib/client');
2121
const vdescribe = helper.vdescribe;
2222

2323
vdescribe('dse-5.0', 'DsePlainTextAuthProvider @SERVER_API', function () {
2424
this.timeout(180000);
25-
it('should authenticate against DSE daemon instance', function (done) {
26-
const testClusterOptions = {
25+
26+
context('with Cassandra PasswordAuthenticator', () => {
27+
helper.setup(1, { initClient: false, ccmOptions: {
2728
yaml: ['authenticator:PasswordAuthenticator'],
2829
jvmArgs: ['-Dcassandra.superuser_setup_delay_ms=0']
29-
};
30-
helper.ccm.startAll(1, testClusterOptions, function (err) {
31-
assert.ifError(err);
30+
}});
31+
32+
it('should authenticate against DSE daemon instance', function (done) {
3233
const authProvider = new DsePlainTextAuthProvider('cassandra', 'cassandra');
3334
const clientOptions = helper.getOptions({ authProvider: authProvider });
34-
const client = new Client(clientOptions);
35+
const client = helper.shutdownAfterThisTest(new Client(clientOptions));
3536
client.connect(function (err) {
3637
assert.ifError(err);
3738
assert.notEqual(client.hosts.length, 0);
@@ -40,22 +41,38 @@ vdescribe('dse-5.0', 'DsePlainTextAuthProvider @SERVER_API', function () {
4041
});
4142
});
4243

43-
it('should authenticate against DSE 5+ DseAuthenticator', function (done) {
44-
const testClusterOptions = {
44+
context('with DSE 5+ DseAuthenticator', () => {
45+
helper.setup(1, { initClient: false, ccmOptions: {
4546
yaml: ['authenticator:com.datastax.bdp.cassandra.auth.DseAuthenticator'],
4647
jvmArgs: ['-Dcassandra.superuser_setup_delay_ms=0'],
4748
dseYaml: ['authentication_options.enabled:true', 'authentication_options.default_scheme:internal']
48-
};
49-
helper.ccm.startAll(1, testClusterOptions, function (err) {
50-
assert.ifError(err);
49+
}});
50+
51+
it('should authenticate against DSE 5+ DseAuthenticator', async () => {
5152
const authProvider = new DsePlainTextAuthProvider('cassandra', 'cassandra');
52-
const clientOptions = helper.getOptions({ authProvider: authProvider });
53-
const client = new Client(clientOptions);
54-
client.connect(function (err) {
55-
assert.ifError(err);
56-
assert.notEqual(client.hosts.length, 0);
57-
client.shutdown(done);
58-
});
53+
const clientOptions = helper.getOptions({ authProvider });
54+
const client = helper.shutdownAfterThisTest(new Client(clientOptions));
55+
await client.connect();
56+
// There is an open connection
57+
assert.strictEqual(client.getState().getOpenConnections(client.hosts.values()[0]), 1);
58+
await client.shutdown();
59+
});
60+
});
61+
62+
context('with transitional mode normal', () => {
63+
helper.setup(1, { initClient: false, ccmOptions: {
64+
yaml: ['authenticator:com.datastax.bdp.cassandra.auth.DseAuthenticator'],
65+
jvmArgs: ['-Dcassandra.superuser_setup_delay_ms=0'],
66+
dseYaml: ['authentication_options.enabled:true', 'authentication_options.default_scheme:internal', 'authentication_options.transitional_mode:normal']
67+
}});
68+
69+
it('should support transitional mode', async () => {
70+
// Without setting an authenticator
71+
const clientOptions = helper.getOptions({});
72+
const client = helper.shutdownAfterThisTest(new Client(clientOptions));
73+
await client.connect();
74+
await client.execute('SELECT * FROM system.local');
75+
await client.shutdown();
5976
});
6077
});
61-
});
78+
});

test/integration/short/client-pool-tests.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
* limitations under the License.
1515
*/
1616
'use strict';
17-
const assert = require('chai').assert;
17+
18+
const { assert } = require('chai');
1819
const dns = require('dns');
1920
const util = require('util');
2021

@@ -27,7 +28,7 @@ const types = require('../../../lib/types');
2728
const policies = require('../../../lib/policies');
2829
const RoundRobinPolicy = require('../../../lib/policies/load-balancing.js').RoundRobinPolicy;
2930
const Murmur3Tokenizer = require('../../../lib/tokenizer.js').Murmur3Tokenizer;
30-
const PlainTextAuthProvider = require('../../../lib/auth/plain-text-auth-provider.js');
31+
const { PlainTextAuthProvider } = require('../../../lib/auth');
3132
const ConstantSpeculativeExecutionPolicy = policies.speculativeExecution.ConstantSpeculativeExecutionPolicy;
3233
const OrderedLoadBalancingPolicy = helper.OrderedLoadBalancingPolicy;
3334
const vit = helper.vit;
@@ -384,6 +385,13 @@ describe('Client', function () {
384385
}, helper.finish(client, done));
385386
});
386387

388+
it('should return an AuthenticationError when authProvider is not set', async () => {
389+
const client = newInstance();
390+
const err = await helper.assertThrowsAsync(client.connect());
391+
assertAuthError(err, /requires authentication, but no authenticator found in the options/);
392+
await client.shutdown();
393+
});
394+
387395
context('with credentials', () => {
388396

389397
vit('3.0', 'should support authenticating', () => {
@@ -1094,8 +1102,13 @@ function createRole(client, role, password) {
10941102
return client.execute(`CREATE ROLE IF NOT EXISTS ${role} WITH PASSWORD = '${password}' AND LOGIN = true`);
10951103
}
10961104

1097-
function assertAuthError(err) {
1105+
function assertAuthError(err, message) {
10981106
helper.assertInstanceOf(err, errors.NoHostAvailableError);
10991107
assert.ok(err.innerErrors);
1100-
helper.assertInstanceOf(Object.values(err.innerErrors)[0], errors.AuthenticationError);
1101-
}
1108+
const firstErr = Object.values(err.innerErrors)[0];
1109+
helper.assertInstanceOf(firstErr, errors.AuthenticationError);
1110+
1111+
if (message) {
1112+
assert.match(firstErr.message, message);
1113+
}
1114+
}

test/integration/short/cloud/cloud-tests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ vdescribe('dse-6.7', 'Cloud support', function () {
152152
// Ignore auth error
153153
}
154154

155-
assert.strictEqual(client.options.authProvider, null);
155+
assert.instanceOf(client.options.authProvider, auth.NoAuthProvider);
156156
});
157157

158158
it('should support overriding the auth provider', async () => {

test/unit/typescript/api-generation-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export async function generatedFn() {
6464
printClasses(root, 'root', new Set([ 'Encoder' ]));
6565
printObjects(root, 'root', new Set([ 'token' ]));
6666

67-
printClasses(auth, 'auth');
67+
printClasses(auth, 'auth', new Set(['NoAuthProvider']));
6868
printClasses(errors, 'errors');
6969
printFunctions(concurrent, 'concurrent');
7070
printClasses(concurrent, 'concurrent');

0 commit comments

Comments
 (0)