diff --git a/docs/authentication.asciidoc b/docs/authentication.asciidoc index bd6a0fa30..3fa7e9641 100644 --- a/docs/authentication.asciidoc +++ b/docs/authentication.asciidoc @@ -3,68 +3,105 @@ This document contains code snippets to show you how to connect to various Elasticsearch providers. -=== Basic Auth -You can provide your credentials in the node(s) URL. +=== Elastic Cloud + +If you are using https://www.elastic.co/cloud[Elastic Cloud], the client offers a easy way to connect to it via the `cloud` option. + +You must pass the Cloud ID that you can find in the cloud console, then your username and password inside the `auth` option. + +NOTE: When connecting to Elastic Cloud, the client will automatically enable both request and response compression by default, since it yields significant throughput improvements. + +Moreover, the client will also set the ssl option `secureProtocol` to `TLSv1_2_method` unless specified otherwise. +You can still override this option by configuring them. + +IMPORTANT: Do not enable sniffing when using Elastic Cloud, since the nodes are behind a load balancer, Elastic Cloud will take care of everything for you. [source,js] ---- const { Client } = require('@elastic/elasticsearch') const client = new Client({ - node: 'https://username:password@localhost:9200' + cloud: { + id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==', + }, + auth: { + username: 'elastic', + password: 'changeme' + } }) ---- -Or you can use the full node declaration. +=== Basic authentication + +You can provide your credentials by passing the `username` and `password` parameters via the `auth` option. [source,js] ---- -const { URL } = require('url') const { Client } = require('@elastic/elasticsearch') const client = new Client({ - node: { - url: new URL('https://username:password@localhost:9200'), - id: 'node-1', - ... + node: 'https://localhost:9200', + auth: { + username: 'elastic', + password: 'changeme' } }) ---- -=== SSL configuration +Otherwise, you can provide your credentials in the node(s) URL. -Without any additional configuration you can specify `https://` node urls, but the certificates used to sign these requests will not verified (`rejectUnauthorized: false`). To turn on certificate verification you must specify an `ssl` object either in the top level config or in each host config object and set `rejectUnauthorized: true`. The ssl config object can contain many of the same configuration options that https://nodejs.org/api/tls.html#tls_tls_connect_options_callback[tls.connect()] accepts. +[source,js] +---- +const { Client } = require('@elastic/elasticsearch') +const client = new Client({ + node: 'https://username:password@localhost:9200' +}) +---- + +=== ApiKey authentication + +You can use the https://www.elastic.co/guide/en/elasticsearch/reference/7.x/security-api-create-api-key.html[ApiKey] authentication by passing the `apiKey` parameter via the `auth` option. + +The `apiKey` parameter can be either a base64 encoded string or an object with the values that you can obtain from the https://www.elastic.co/guide/en/elasticsearch/reference/7.x/security-api-create-api-key.html[create api key endpoint]. [source,js] ---- const { Client } = require('@elastic/elasticsearch') const client = new Client({ - node: 'http://username:password@localhost:9200', - ssl: { - ca: fs.readFileSync('./cacert.pem'), - rejectUnauthorized: true + node: 'https://localhost:9200', + auth: { + apiKey: 'base64EncodedKey' } }) ---- -=== Elastic Cloud +[source,js] +---- +const { Client } = require('@elastic/elasticsearch') +const client = new Client({ + node: 'https://localhost:9200', + auth: { + apiKey: { + id: 'foo', + api_key: 'bar' + } + } +}) +---- -If you are using https://www.elastic.co/cloud[Elastic Cloud], the client offers a easy way to connect to it via the `cloud` option. + -You must pass the Cloud ID that you can find in the cloud console, then your username and password. -NOTE: When connecting to Elastic Cloud, the client will automatically enable both request and response compression by default, since it yields significant throughput improvements. + -Moreover, the client will also set the ssl option `secureProtocol` to `TLSv1_2_method` unless specified otherwise. -You can still override this option by configuring them. +=== SSL configuration -IMPORTANT: Do not enable sniffing when using Elastic Cloud, since the nodes are behind a load balancer, Elastic Cloud will take care of everything for you. +Without any additional configuration you can specify `https://` node urls, but the certificates used to sign these requests will not verified (`rejectUnauthorized: false`). To turn on certificate verification you must specify an `ssl` object either in the top level config or in each host config object and set `rejectUnauthorized: true`. The ssl config object can contain many of the same configuration options that https://nodejs.org/api/tls.html#tls_tls_connect_options_callback[tls.connect()] accepts. [source,js] ---- const { Client } = require('@elastic/elasticsearch') const client = new Client({ - cloud: { - id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==', + node: 'http://localhost:9200', + auth: { username: 'elastic', password: 'changeme' + }, + ssl: { + ca: fs.readFileSync('./cacert.pem'), + rejectUnauthorized: true } }) ---- \ No newline at end of file diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index cab52c193..c32575c45 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -43,6 +43,28 @@ node: { } ---- +|`auth` +a|Your authentication data. You can use both Basic authentication and https://www.elastic.co/guide/en/elasticsearch/reference/7.x/security-api-create-api-key.html[ApiKey]. + +See https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/auth-reference.html[Authentication] for more details. + +_Default:_ `null` + +Basic authentication: +[source,js] +---- +auth: { + username: 'elastic', + password: 'changeme' +} +---- +https://www.elastic.co/guide/en/elasticsearch/reference/7.x/security-api-create-api-key.html[ApiKey] authentication: +[source,js] +---- +auth: { + apiKey: 'base64EncodedKey' +} +---- + + |`maxRetries` |`number` - Max number of retries for each request. + _Default:_ `3` @@ -163,7 +185,9 @@ _Cloud configuration example:_ ---- const client = new Client({ cloud: { - id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==', + id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==' + }, + auth: { username: 'elastic', password: 'changeme' } diff --git a/index.d.ts b/index.d.ts index 84acfeaf8..caee77c8e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -33,7 +33,7 @@ import Transport, { } from './lib/Transport'; import { URL } from 'url'; import Connection, { AgentOptions, agentFn } from './lib/Connection'; -import ConnectionPool, { ResurrectEvent } from './lib/ConnectionPool'; +import ConnectionPool, { ResurrectEvent, BasicAuth, ApiKeyAuth } from './lib/ConnectionPool'; import Serializer from './lib/Serializer'; import * as RequestParams from './api/requestParams'; import * as errors from './lib/errors'; @@ -111,8 +111,10 @@ interface ClientOptions { headers?: anyObject; generateRequestId?: generateRequestIdFn; name?: string; + auth?: BasicAuth | ApiKeyAuth; cloud?: { id: string; + // TODO: remove username and password here in 8 username: string; password: string; } diff --git a/index.js b/index.js index 7f5b3c1c3..742b50c9e 100644 --- a/index.js +++ b/index.js @@ -20,6 +20,7 @@ 'use strict' const { EventEmitter } = require('events') +const { URL } = require('url') const debug = require('debug')('elasticsearch') const Transport = require('./lib/Transport') const Connection = require('./lib/Connection') @@ -43,7 +44,12 @@ class Client extends EventEmitter { // the url is a string divided by two '$', the first is the cloud url // the second the elasticsearch instance, the third the kibana instance const cloudUrls = Buffer.from(id.split(':')[1], 'base64').toString().split('$') - opts.node = `https://${username}:${password}@${cloudUrls[1]}.${cloudUrls[0]}` + + // TODO: remove username and password here in 8 + if (username && password) { + opts.auth = Object.assign({}, opts.auth, { username, password }) + } + opts.node = `https://${cloudUrls[1]}.${cloudUrls[0]}` // Cloud has better performances with compression enabled // see https://github.com/elastic/elasticsearch-py/pull/704. @@ -61,6 +67,11 @@ class Client extends EventEmitter { throw new ConfigurationError('Missing node(s) option') } + const checkAuth = getAuth(opts.node || opts.nodes) + if (checkAuth && checkAuth.username && checkAuth.password) { + opts.auth = Object.assign({}, opts.auth, { username: checkAuth.username, password: checkAuth.password }) + } + const options = Object.assign({}, { Connection, ConnectionPool, @@ -82,7 +93,8 @@ class Client extends EventEmitter { nodeFilter: null, nodeSelector: 'round-robin', generateRequestId: null, - name: 'elasticsearch-js' + name: 'elasticsearch-js', + auth: null }, opts) this[kInitialOptions] = options @@ -96,6 +108,7 @@ class Client extends EventEmitter { ssl: options.ssl, agent: options.agent, Connection: options.Connection, + auth: options.auth, emit: this.emit.bind(this), sniffEnabled: options.sniffInterval !== false || options.sniffOnStart !== false || @@ -209,6 +222,41 @@ class Client extends EventEmitter { } } +function getAuth (node) { + if (Array.isArray(node)) { + for (const url of node) { + const auth = getUsernameAndPassword(url) + if (auth.username !== '' && auth.password !== '') { + return auth + } + } + + return null + } + + const auth = getUsernameAndPassword(node) + if (auth.username !== '' && auth.password !== '') { + return auth + } + + return null + + function getUsernameAndPassword (node) { + if (typeof node === 'string') { + const { username, password } = new URL(node) + return { + username: decodeURIComponent(username), + password: decodeURIComponent(password) + } + } else if (node.url instanceof URL) { + return { + username: decodeURIComponent(node.url.username), + password: decodeURIComponent(node.url.password) + } + } + } +} + const events = { RESPONSE: 'response', REQUEST: 'request', diff --git a/lib/Connection.d.ts b/lib/Connection.d.ts index f2ab8532a..4d1e4c0b4 100644 --- a/lib/Connection.d.ts +++ b/lib/Connection.d.ts @@ -21,6 +21,7 @@ import { URL } from 'url'; import { inspect, InspectOptions } from 'util'; +import { ApiKeyAuth, BasicAuth } from './ConnectionPool' import * as http from 'http'; import { ConnectionOptions as TlsConnectionOptions } from 'tls'; @@ -34,6 +35,7 @@ interface ConnectionOptions { agent?: AgentOptions | agentFn; status?: string; roles?: any; + auth?: BasicAuth | ApiKeyAuth; } interface RequestOptions extends http.ClientRequestArgs { diff --git a/lib/Connection.js b/lib/Connection.js index 1501d65d7..8a6a97700 100644 --- a/lib/Connection.js +++ b/lib/Connection.js @@ -34,8 +34,7 @@ class Connection { this.url = opts.url this.ssl = opts.ssl || null this.id = opts.id || stripAuth(opts.url.href) - this.headers = opts.headers || null - this.auth = opts.auth || { username: null, password: null } + this.headers = prepareHeaders(opts.headers, opts.auth) this.deadCount = 0 this.resurrectTimeout = 0 @@ -181,7 +180,6 @@ class Connection { buildRequestObject (params) { const url = this.url - const { username, password } = this.auth const request = { protocol: url.protocol, hostname: url.hostname[0] === '[' @@ -196,9 +194,6 @@ class Connection { // https://github.com/elastic/elasticsearch-js/issues/843 port: url.port !== '' ? url.port : undefined, headers: this.headers, - auth: username != null && password != null - ? `${username}:${password}` - : undefined, agent: this.agent } @@ -230,10 +225,15 @@ class Connection { // the logs very hard to read. The user can still // access them with `instance.agent` and `instance.ssl`. [inspect.custom] (depth, options) { + const { + authorization, + ...headers + } = this.headers + return { url: stripAuth(this.url.toString()), id: this.id, - headers: this.headers, + headers, deadCount: this.deadCount, resurrectTimeout: this.resurrectTimeout, _openRequests: this._openRequests, @@ -243,10 +243,15 @@ class Connection { } toJSON () { + const { + authorization, + ...headers + } = this.headers + return { url: stripAuth(this.url.toString()), id: this.id, - headers: this.headers, + headers, deadCount: this.deadCount, resurrectTimeout: this.resurrectTimeout, _openRequests: this._openRequests, @@ -302,4 +307,21 @@ function resolve (host, path) { } } +function prepareHeaders (headers = {}, auth) { + if (auth != null && headers.authorization == null) { + if (auth.username && auth.password) { + headers.authorization = 'Basic ' + Buffer.from(`${auth.username}:${auth.password}`).toString('base64') + } + + if (auth.apiKey) { + if (typeof auth.apiKey === 'object') { + headers.authorization = 'ApiKey ' + Buffer.from(`${auth.apiKey.id}:${auth.apiKey.api_key}`).toString('base64') + } else { + headers.authorization = `ApiKey ${auth.apiKey}` + } + } + } + return headers +} + module.exports = Connection diff --git a/lib/ConnectionPool.d.ts b/lib/ConnectionPool.d.ts index 5b68d7d36..669ec7824 100644 --- a/lib/ConnectionPool.d.ts +++ b/lib/ConnectionPool.d.ts @@ -26,6 +26,7 @@ import { nodeFilterFn, nodeSelectorFn } from './Transport'; interface ConnectionPoolOptions { ssl?: SecureContextOptions; agent?: AgentOptions; + auth: BasicAuth | ApiKeyAuth; pingTimeout?: number; Connection: typeof Connection; resurrectStrategy?: string; @@ -36,6 +37,20 @@ export interface getConnectionOptions { selector?: nodeSelectorFn; } +export interface ApiKeyAuth { + apiKey: + | string + | { + id: string; + api_key: string; + } +} + +export interface BasicAuth { + username: string; + password: string; +} + export interface resurrectOptions { now?: number; requestId: string; @@ -66,6 +81,7 @@ export default class ConnectionPool { resurrectTimeout: number; resurrectTimeoutCutoff: number; pingTimeout: number; + auth: BasicAuth | ApiKeyAuth; Connection: typeof Connection; resurrectStrategy: number; constructor(opts?: ConnectionPoolOptions); diff --git a/lib/ConnectionPool.js b/lib/ConnectionPool.js index 60191d3ba..e467e2b01 100644 --- a/lib/ConnectionPool.js +++ b/lib/ConnectionPool.js @@ -30,7 +30,7 @@ class ConnectionPool { this.connections = new Map() this.dead = [] this.selector = opts.selector - this._auth = null + this.auth = opts.auth || null this._ssl = opts.ssl this._agent = opts.agent // the resurrect timeout is 60s @@ -217,23 +217,14 @@ class ConnectionPool { if (typeof opts === 'string') { opts = this.urlToHost(opts) } - // if a given node has auth data we store it in the connection pool, - // so if we add new nodes without auth data (after a sniff for example) - // we can add it to them once the connection instance has been created + if (opts.url.username !== '' && opts.url.password !== '') { - this._auth = { + opts.auth = { username: decodeURIComponent(opts.url.username), password: decodeURIComponent(opts.url.password) } - opts.auth = this._auth - } - - if (this._auth != null) { - if (opts.auth == null || (opts.auth.username == null && opts.auth.password == null)) { - opts.auth = this._auth - opts.url.username = this._auth.username - opts.url.password = this._auth.password - } + } else if (this.auth !== null) { + opts.auth = this.auth } if (opts.ssl == null) opts.ssl = this._ssl diff --git a/test/types/index.ts b/test/types/index.ts index 2aa0526e9..6289ee889 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -50,6 +50,26 @@ const nodeOpts: NodeOptions = { const client2 = new Client({ node: nodeOpts }) +const clientBasicAuth = new Client({ + node: 'http://localhost:9200', + auth: { username: 'foo', password: 'bar' } +}) + +const clientApiKeyString = new Client({ + node: 'http://localhost:9200', + auth: { apiKey: 'foobar' } +}) + +const clientApiKeyObject = new Client({ + node: 'http://localhost:9200', + auth: { + apiKey: { + id: 'foo', + api_key: 'bar' + } + } +}) + client.on(events.RESPONSE, (err: errors.ElasticsearchClientError | null, request: RequestEvent) => { if (err) console.log(err) const { body, statusCode } = request diff --git a/test/unit/client.test.js b/test/unit/client.test.js index d92f765b6..cac05919f 100644 --- a/test/unit/client.test.js +++ b/test/unit/client.test.js @@ -198,63 +198,293 @@ test('Configure host', t => { t.end() }) -test('Node with auth data in the url', t => { - t.plan(3) +test('Authentication', t => { + t.test('Basic', t => { + t.test('Node with basic auth data in the url', t => { + t.plan(3) - function handler (req, res) { - t.match(req.headers, { - authorization: 'Basic Zm9vOmJhcg==' + function handler (req, res) { + t.match(req.headers, { + authorization: 'Basic Zm9vOmJhcg==' + }) + res.setHeader('Content-Type', 'application/json;utf=8') + res.end(JSON.stringify({ hello: 'world' })) + } + + buildServer(handler, ({ port }, server) => { + const client = new Client({ + node: `http://foo:bar@localhost:${port}` + }) + + client.info((err, { body }) => { + t.error(err) + t.deepEqual(body, { hello: 'world' }) + server.stop() + }) + }) }) - res.setHeader('Content-Type', 'application/json;utf=8') - res.end(JSON.stringify({ hello: 'world' })) - } - buildServer(handler, ({ port }, server) => { - const client = new Client({ - node: `http://foo:bar@localhost:${port}` + t.test('Node with basic auth data in the url (array of nodes)', t => { + t.plan(3) + + function handler (req, res) { + t.match(req.headers, { + authorization: 'Basic Zm9vOmJhcg==' + }) + res.setHeader('Content-Type', 'application/json;utf=8') + res.end(JSON.stringify({ hello: 'world' })) + } + + buildServer(handler, ({ port }, server) => { + const client = new Client({ + nodes: [`http://foo:bar@localhost:${port}`] + }) + + client.info((err, { body }) => { + t.error(err) + t.deepEqual(body, { hello: 'world' }) + server.stop() + }) + }) }) - client.info((err, { body }) => { - t.error(err) - t.deepEqual(body, { hello: 'world' }) - server.stop() + t.test('Node with basic auth data in the options', t => { + t.plan(3) + + function handler (req, res) { + t.match(req.headers, { + authorization: 'Basic Zm9vOmJhcg==' + }) + res.setHeader('Content-Type', 'application/json;utf=8') + res.end(JSON.stringify({ hello: 'world' })) + } + + buildServer(handler, ({ port }, server) => { + const client = new Client({ + node: `http://localhost:${port}`, + auth: { + username: 'foo', + password: 'bar' + } + }) + + client.info((err, { body }) => { + t.error(err) + t.deepEqual(body, { hello: 'world' }) + server.stop() + }) + }) }) + + t.test('Custom basic authentication per request', t => { + t.plan(6) + + var first = true + function handler (req, res) { + t.match(req.headers, { + authorization: first ? 'hello' : 'Basic Zm9vOmJhcg==' + }) + res.setHeader('Content-Type', 'application/json;utf=8') + res.end(JSON.stringify({ hello: 'world' })) + } + + buildServer(handler, ({ port }, server) => { + const client = new Client({ + node: `http://foo:bar@localhost:${port}` + }) + + client.info({}, { + headers: { + authorization: 'hello' + } + }, (err, { body }) => { + t.error(err) + t.deepEqual(body, { hello: 'world' }) + first = false + + client.info((err, { body }) => { + t.error(err) + t.deepEqual(body, { hello: 'world' }) + server.stop() + }) + }) + }) + }) + + t.test('Override default basic authentication per request', t => { + t.plan(6) + + var first = true + function handler (req, res) { + t.match(req.headers, { + authorization: first ? 'hello' : 'Basic Zm9vOmJhcg==' + }) + res.setHeader('Content-Type', 'application/json;utf=8') + res.end(JSON.stringify({ hello: 'world' })) + } + + buildServer(handler, ({ port }, server) => { + const client = new Client({ + node: `http://localhost:${port}`, + auth: { + username: 'foo', + password: 'bar' + } + }) + + client.info({}, { + headers: { + authorization: 'hello' + } + }, (err, { body }) => { + t.error(err) + t.deepEqual(body, { hello: 'world' }) + first = false + + client.info((err, { body }) => { + t.error(err) + t.deepEqual(body, { hello: 'world' }) + server.stop() + }) + }) + }) + }) + + t.end() }) -}) -test('Custom authentication per request', t => { - t.plan(6) + t.test('ApiKey', t => { + t.test('Node with ApiKey auth data in the options as string', t => { + t.plan(3) - var first = true - function handler (req, res) { - t.match(req.headers, { - authorization: first ? 'hello' : 'Basic Zm9vOmJhcg==' + function handler (req, res) { + t.match(req.headers, { + authorization: 'ApiKey Zm9vOmJhcg==' + }) + res.setHeader('Content-Type', 'application/json;utf=8') + res.end(JSON.stringify({ hello: 'world' })) + } + + buildServer(handler, ({ port }, server) => { + const client = new Client({ + node: `http://localhost:${port}`, + auth: { + apiKey: 'Zm9vOmJhcg==' + } + }) + + client.info((err, { body }) => { + t.error(err) + t.deepEqual(body, { hello: 'world' }) + server.stop() + }) + }) }) - res.setHeader('Content-Type', 'application/json;utf=8') - res.end(JSON.stringify({ hello: 'world' })) - } - buildServer(handler, ({ port }, server) => { - const client = new Client({ - node: `http://foo:bar@localhost:${port}` + t.test('Node with ApiKey auth data in the options as object', t => { + t.plan(3) + + function handler (req, res) { + t.match(req.headers, { + authorization: 'ApiKey Zm9vOmJhcg==' + }) + res.setHeader('Content-Type', 'application/json;utf=8') + res.end(JSON.stringify({ hello: 'world' })) + } + + buildServer(handler, ({ port }, server) => { + const client = new Client({ + node: `http://localhost:${port}`, + auth: { + apiKey: { id: 'foo', api_key: 'bar' } + } + }) + + client.info((err, { body }) => { + t.error(err) + t.deepEqual(body, { hello: 'world' }) + server.stop() + }) + }) }) - client.info({}, { - headers: { - authorization: 'hello' + t.test('Custom ApiKey authentication per request', t => { + t.plan(6) + + var first = true + function handler (req, res) { + t.match(req.headers, { + authorization: first ? 'ApiKey Zm9vOmJhcg==' : 'Basic Zm9vOmJhcg==' + }) + res.setHeader('Content-Type', 'application/json;utf=8') + res.end(JSON.stringify({ hello: 'world' })) } - }, (err, { body }) => { - t.error(err) - t.deepEqual(body, { hello: 'world' }) - first = false - client.info((err, { body }) => { - t.error(err) - t.deepEqual(body, { hello: 'world' }) - server.stop() + buildServer(handler, ({ port }, server) => { + const client = new Client({ + node: `http://foo:bar@localhost:${port}` + }) + + client.info({}, { + headers: { + authorization: 'ApiKey Zm9vOmJhcg==' + } + }, (err, { body }) => { + t.error(err) + t.deepEqual(body, { hello: 'world' }) + first = false + + client.info((err, { body }) => { + t.error(err) + t.deepEqual(body, { hello: 'world' }) + server.stop() + }) + }) }) }) + + t.test('Override default ApiKey authentication per request', t => { + t.plan(6) + + var first = true + function handler (req, res) { + t.match(req.headers, { + authorization: first ? 'hello' : 'ApiKey Zm9vOmJhcg==' + }) + res.setHeader('Content-Type', 'application/json;utf=8') + res.end(JSON.stringify({ hello: 'world' })) + } + + buildServer(handler, ({ port }, server) => { + const client = new Client({ + node: `http://localhost:${port}`, + auth: { + apiKey: 'Zm9vOmJhcg==' + } + }) + + client.info({}, { + headers: { + authorization: 'hello' + } + }, (err, { body }) => { + t.error(err) + t.deepEqual(body, { hello: 'world' }) + first = false + + client.info((err, { body }) => { + t.error(err) + t.deepEqual(body, { hello: 'world' }) + server.stop() + }) + }) + }) + }) + + t.end() }) + + t.end() }) test('Custom headers per request', t => { @@ -554,6 +784,45 @@ test('Elastic cloud config', t => { t.match(pool.connections.get('https://abcd.localhost/'), { url: new URL('https://elastic:changeme@abcd.localhost'), id: 'https://abcd.localhost/', + headers: { + authorization: 'Basic ' + Buffer.from('elastic:changeme').toString('base64') + }, + ssl: { secureProtocol: 'TLSv1_2_method' }, + deadCount: 0, + resurrectTimeout: 0, + roles: { + master: true, + data: true, + ingest: true, + ml: false + } + }) + + t.strictEqual(client.transport.compression, 'gzip') + t.strictEqual(client.transport.suggestCompression, true) + t.deepEqual(pool._ssl, { secureProtocol: 'TLSv1_2_method' }) + }) + + t.test('Auth as separate option', t => { + t.plan(4) + const client = new Client({ + cloud: { + // 'localhost$abcd$efgh' + id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==' + }, + auth: { + username: 'elastic', + password: 'changeme' + } + }) + + const pool = client.connectionPool + t.match(pool.connections.get('https://abcd.localhost/'), { + url: new URL('https://elastic:changeme@abcd.localhost'), + id: 'https://abcd.localhost/', + headers: { + authorization: 'Basic ' + Buffer.from('elastic:changeme').toString('base64') + }, ssl: { secureProtocol: 'TLSv1_2_method' }, deadCount: 0, resurrectTimeout: 0, diff --git a/test/unit/connection-pool.test.js b/test/unit/connection-pool.test.js index c2510a2aa..2136d83dc 100644 --- a/test/unit/connection-pool.test.js +++ b/test/unit/connection-pool.test.js @@ -50,25 +50,6 @@ test('API', t => { t.end() }) - t.test('addConnection (should store the auth data)', t => { - const pool = new ConnectionPool({ Connection }) - const href = 'http://localhost:9200/' - pool.addConnection('http://foo:bar@localhost:9200') - - t.ok(pool.connections.get(href) instanceof Connection) - t.strictEqual(pool.connections.get(href).status, Connection.statuses.ALIVE) - t.deepEqual(pool.dead, []) - t.deepEqual(pool._auth, { username: 'foo', password: 'bar' }) - - pool.addConnection('http://localhost:9201') - const conn = pool.connections.get('http://localhost:9201/') - t.strictEqual(conn.url.username, 'foo') - t.strictEqual(conn.url.password, 'bar') - t.strictEqual(conn.auth.username, 'foo') - t.strictEqual(conn.auth.password, 'bar') - t.end() - }) - t.test('addConnection should handle not-friendly url parameters for user and password', t => { const pool = new ConnectionPool({ Connection }) const href = 'http://us"er:p@assword@localhost:9200/' @@ -76,8 +57,9 @@ test('API', t => { const conn = pool.getConnection() t.strictEqual(conn.url.username, 'us%22er') t.strictEqual(conn.url.password, 'p%40assword') - t.strictEqual(conn.auth.username, 'us"er') - t.strictEqual(conn.auth.password, 'p@assword') + t.match(conn.headers, { + authorization: 'Basic ' + Buffer.from('us"er:p@assword').toString('base64') + }) t.end() }) diff --git a/test/unit/connection.test.js b/test/unit/connection.test.js index ea9a2503b..9029afdea 100644 --- a/test/unit/connection.test.js +++ b/test/unit/connection.test.js @@ -811,6 +811,53 @@ test('Port handling', t => { t.end() }) +test('Authorization header', t => { + t.test('None', t => { + const connection = new Connection({ + url: new URL('http://localhost:9200') + }) + + t.deepEqual(connection.headers, {}) + + t.end() + }) + + t.test('Basic', t => { + const connection = new Connection({ + url: new URL('http://localhost:9200'), + auth: { username: 'foo', password: 'bar' } + }) + + t.deepEqual(connection.headers, { authorization: 'Basic Zm9vOmJhcg==' }) + + t.end() + }) + + t.test('ApiKey (string)', t => { + const connection = new Connection({ + url: new URL('http://localhost:9200'), + auth: { apiKey: 'Zm9vOmJhcg==' } + }) + + t.deepEqual(connection.headers, { authorization: 'ApiKey Zm9vOmJhcg==' }) + + t.end() + }) + + t.test('ApiKey (object)', t => { + const connection = new Connection({ + url: new URL('http://localhost:9200'), + auth: { apiKey: { id: 'foo', api_key: 'bar' } } + }) + + t.deepEqual(connection.headers, { authorization: 'ApiKey Zm9vOmJhcg==' }) + + t.end() + }) + + t.end() +}) + test('Should not add agent and ssl to the serialized connection', t => { const connection = new Connection({ url: new URL('http://localhost:9200') @@ -818,7 +865,7 @@ test('Should not add agent and ssl to the serialized connection', t => { t.strictEqual( JSON.stringify(connection), - '{"url":"http://localhost:9200/","id":"http://localhost:9200/","headers":null,"deadCount":0,"resurrectTimeout":0,"_openRequests":0,"status":"alive","roles":{"master":true,"data":true,"ingest":true,"ml":false}}' + '{"url":"http://localhost:9200/","id":"http://localhost:9200/","headers":{},"deadCount":0,"resurrectTimeout":0,"_openRequests":0,"status":"alive","roles":{"master":true,"data":true,"ingest":true,"ml":false}}' ) t.end()