Improve authentication handling (#908)

This commit is contained in:
Tomas Della Vedova
2019-07-18 10:33:11 +02:00
committed by delvedor
parent a55856c60b
commit 708f9abe3e
12 changed files with 602 additions and 142 deletions

View File

@ -3,53 +3,11 @@
This document contains code snippets to show you how to connect to various Elasticsearch providers. 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.
[source,js]
----
const { Client } = require('@elastic/elasticsearch')
const client = new Client({
node: 'https://username:password@localhost:9200'
})
----
Or you can use the full node declaration.
[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',
...
}
})
----
=== SSL configuration
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: 'http://username:password@localhost:9200',
ssl: {
ca: fs.readFileSync('./cacert.pem'),
rejectUnauthorized: true
}
})
----
=== Elastic Cloud === 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. + 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. 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. + 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. Moreover, the client will also set the ssl option `secureProtocol` to `TLSv1_2_method` unless specified otherwise.
@ -63,8 +21,87 @@ const { Client } = require('@elastic/elasticsearch')
const client = new Client({ const client = new Client({
cloud: { cloud: {
id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==', id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==',
},
auth: {
username: 'elastic', username: 'elastic',
password: 'changeme' password: 'changeme'
} }
}) })
---- ----
=== Basic authentication
You can provide your credentials by passing the `username` and `password` parameters via the `auth` option.
[source,js]
----
const { Client } = require('@elastic/elasticsearch')
const client = new Client({
node: 'https://localhost:9200',
auth: {
username: 'elastic',
password: 'changeme'
}
})
----
Otherwise, you can provide your credentials in the node(s) URL.
[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: 'https://localhost:9200',
auth: {
apiKey: 'base64EncodedKey'
}
})
----
[source,js]
----
const { Client } = require('@elastic/elasticsearch')
const client = new Client({
node: 'https://localhost:9200',
auth: {
apiKey: {
id: 'foo',
api_key: 'bar'
}
}
})
----
=== SSL configuration
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: 'http://localhost:9200',
auth: {
username: 'elastic',
password: 'changeme'
},
ssl: {
ca: fs.readFileSync('./cacert.pem'),
rejectUnauthorized: true
}
})
----

View File

@ -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` |`maxRetries`
|`number` - Max number of retries for each request. + |`number` - Max number of retries for each request. +
_Default:_ `3` _Default:_ `3`
@ -163,7 +185,9 @@ _Cloud configuration example:_
---- ----
const client = new Client({ const client = new Client({
cloud: { cloud: {
id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==', id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA=='
},
auth: {
username: 'elastic', username: 'elastic',
password: 'changeme' password: 'changeme'
} }

4
index.d.ts vendored
View File

@ -33,7 +33,7 @@ import Transport, {
} from './lib/Transport'; } from './lib/Transport';
import { URL } from 'url'; import { URL } from 'url';
import Connection, { AgentOptions, agentFn } from './lib/Connection'; 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 Serializer from './lib/Serializer';
import * as RequestParams from './api/requestParams'; import * as RequestParams from './api/requestParams';
import * as errors from './lib/errors'; import * as errors from './lib/errors';
@ -111,8 +111,10 @@ interface ClientOptions {
headers?: anyObject; headers?: anyObject;
generateRequestId?: generateRequestIdFn; generateRequestId?: generateRequestIdFn;
name?: string; name?: string;
auth?: BasicAuth | ApiKeyAuth;
cloud?: { cloud?: {
id: string; id: string;
// TODO: remove username and password here in 8
username: string; username: string;
password: string; password: string;
} }

View File

@ -20,6 +20,7 @@
'use strict' 'use strict'
const { EventEmitter } = require('events') const { EventEmitter } = require('events')
const { URL } = require('url')
const debug = require('debug')('elasticsearch') const debug = require('debug')('elasticsearch')
const Transport = require('./lib/Transport') const Transport = require('./lib/Transport')
const Connection = require('./lib/Connection') 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 url is a string divided by two '$', the first is the cloud url
// the second the elasticsearch instance, the third the kibana instance // the second the elasticsearch instance, the third the kibana instance
const cloudUrls = Buffer.from(id.split(':')[1], 'base64').toString().split('$') 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 // Cloud has better performances with compression enabled
// see https://github.com/elastic/elasticsearch-py/pull/704. // see https://github.com/elastic/elasticsearch-py/pull/704.
@ -61,6 +67,11 @@ class Client extends EventEmitter {
throw new ConfigurationError('Missing node(s) option') 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({}, { const options = Object.assign({}, {
Connection, Connection,
ConnectionPool, ConnectionPool,
@ -82,7 +93,8 @@ class Client extends EventEmitter {
nodeFilter: null, nodeFilter: null,
nodeSelector: 'round-robin', nodeSelector: 'round-robin',
generateRequestId: null, generateRequestId: null,
name: 'elasticsearch-js' name: 'elasticsearch-js',
auth: null
}, opts) }, opts)
this[kInitialOptions] = options this[kInitialOptions] = options
@ -96,6 +108,7 @@ class Client extends EventEmitter {
ssl: options.ssl, ssl: options.ssl,
agent: options.agent, agent: options.agent,
Connection: options.Connection, Connection: options.Connection,
auth: options.auth,
emit: this.emit.bind(this), emit: this.emit.bind(this),
sniffEnabled: options.sniffInterval !== false || sniffEnabled: options.sniffInterval !== false ||
options.sniffOnStart !== 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 = { const events = {
RESPONSE: 'response', RESPONSE: 'response',
REQUEST: 'request', REQUEST: 'request',

2
lib/Connection.d.ts vendored
View File

@ -21,6 +21,7 @@
import { URL } from 'url'; import { URL } from 'url';
import { inspect, InspectOptions } from 'util'; import { inspect, InspectOptions } from 'util';
import { ApiKeyAuth, BasicAuth } from './ConnectionPool'
import * as http from 'http'; import * as http from 'http';
import { ConnectionOptions as TlsConnectionOptions } from 'tls'; import { ConnectionOptions as TlsConnectionOptions } from 'tls';
@ -34,6 +35,7 @@ interface ConnectionOptions {
agent?: AgentOptions | agentFn; agent?: AgentOptions | agentFn;
status?: string; status?: string;
roles?: any; roles?: any;
auth?: BasicAuth | ApiKeyAuth;
} }
interface RequestOptions extends http.ClientRequestArgs { interface RequestOptions extends http.ClientRequestArgs {

View File

@ -34,8 +34,7 @@ class Connection {
this.url = opts.url this.url = opts.url
this.ssl = opts.ssl || null this.ssl = opts.ssl || null
this.id = opts.id || stripAuth(opts.url.href) this.id = opts.id || stripAuth(opts.url.href)
this.headers = opts.headers || null this.headers = prepareHeaders(opts.headers, opts.auth)
this.auth = opts.auth || { username: null, password: null }
this.deadCount = 0 this.deadCount = 0
this.resurrectTimeout = 0 this.resurrectTimeout = 0
@ -181,7 +180,6 @@ class Connection {
buildRequestObject (params) { buildRequestObject (params) {
const url = this.url const url = this.url
const { username, password } = this.auth
const request = { const request = {
protocol: url.protocol, protocol: url.protocol,
hostname: url.hostname[0] === '[' hostname: url.hostname[0] === '['
@ -196,9 +194,6 @@ class Connection {
// https://github.com/elastic/elasticsearch-js/issues/843 // https://github.com/elastic/elasticsearch-js/issues/843
port: url.port !== '' ? url.port : undefined, port: url.port !== '' ? url.port : undefined,
headers: this.headers, headers: this.headers,
auth: username != null && password != null
? `${username}:${password}`
: undefined,
agent: this.agent agent: this.agent
} }
@ -230,10 +225,15 @@ class Connection {
// the logs very hard to read. The user can still // the logs very hard to read. The user can still
// access them with `instance.agent` and `instance.ssl`. // access them with `instance.agent` and `instance.ssl`.
[inspect.custom] (depth, options) { [inspect.custom] (depth, options) {
const {
authorization,
...headers
} = this.headers
return { return {
url: stripAuth(this.url.toString()), url: stripAuth(this.url.toString()),
id: this.id, id: this.id,
headers: this.headers, headers,
deadCount: this.deadCount, deadCount: this.deadCount,
resurrectTimeout: this.resurrectTimeout, resurrectTimeout: this.resurrectTimeout,
_openRequests: this._openRequests, _openRequests: this._openRequests,
@ -243,10 +243,15 @@ class Connection {
} }
toJSON () { toJSON () {
const {
authorization,
...headers
} = this.headers
return { return {
url: stripAuth(this.url.toString()), url: stripAuth(this.url.toString()),
id: this.id, id: this.id,
headers: this.headers, headers,
deadCount: this.deadCount, deadCount: this.deadCount,
resurrectTimeout: this.resurrectTimeout, resurrectTimeout: this.resurrectTimeout,
_openRequests: this._openRequests, _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 module.exports = Connection

View File

@ -26,6 +26,7 @@ import { nodeFilterFn, nodeSelectorFn } from './Transport';
interface ConnectionPoolOptions { interface ConnectionPoolOptions {
ssl?: SecureContextOptions; ssl?: SecureContextOptions;
agent?: AgentOptions; agent?: AgentOptions;
auth: BasicAuth | ApiKeyAuth;
pingTimeout?: number; pingTimeout?: number;
Connection: typeof Connection; Connection: typeof Connection;
resurrectStrategy?: string; resurrectStrategy?: string;
@ -36,6 +37,20 @@ export interface getConnectionOptions {
selector?: nodeSelectorFn; selector?: nodeSelectorFn;
} }
export interface ApiKeyAuth {
apiKey:
| string
| {
id: string;
api_key: string;
}
}
export interface BasicAuth {
username: string;
password: string;
}
export interface resurrectOptions { export interface resurrectOptions {
now?: number; now?: number;
requestId: string; requestId: string;
@ -66,6 +81,7 @@ export default class ConnectionPool {
resurrectTimeout: number; resurrectTimeout: number;
resurrectTimeoutCutoff: number; resurrectTimeoutCutoff: number;
pingTimeout: number; pingTimeout: number;
auth: BasicAuth | ApiKeyAuth;
Connection: typeof Connection; Connection: typeof Connection;
resurrectStrategy: number; resurrectStrategy: number;
constructor(opts?: ConnectionPoolOptions); constructor(opts?: ConnectionPoolOptions);

View File

@ -30,7 +30,7 @@ class ConnectionPool {
this.connections = new Map() this.connections = new Map()
this.dead = [] this.dead = []
this.selector = opts.selector this.selector = opts.selector
this._auth = null this.auth = opts.auth || null
this._ssl = opts.ssl this._ssl = opts.ssl
this._agent = opts.agent this._agent = opts.agent
// the resurrect timeout is 60s // the resurrect timeout is 60s
@ -217,23 +217,14 @@ class ConnectionPool {
if (typeof opts === 'string') { if (typeof opts === 'string') {
opts = this.urlToHost(opts) 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 !== '') { if (opts.url.username !== '' && opts.url.password !== '') {
this._auth = { opts.auth = {
username: decodeURIComponent(opts.url.username), username: decodeURIComponent(opts.url.username),
password: decodeURIComponent(opts.url.password) password: decodeURIComponent(opts.url.password)
} }
opts.auth = this._auth } else if (this.auth !== null) {
} 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
}
} }
if (opts.ssl == null) opts.ssl = this._ssl if (opts.ssl == null) opts.ssl = this._ssl

View File

@ -50,6 +50,26 @@ const nodeOpts: NodeOptions = {
const client2 = new Client({ node: nodeOpts }) 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) => { client.on(events.RESPONSE, (err: errors.ElasticsearchClientError | null, request: RequestEvent) => {
if (err) console.log(err) if (err) console.log(err)
const { body, statusCode } = request const { body, statusCode } = request

View File

@ -198,63 +198,293 @@ test('Configure host', t => {
t.end() t.end()
}) })
test('Node with auth data in the url', t => { test('Authentication', t => {
t.plan(3) t.test('Basic', t => {
t.test('Node with basic auth data in the url', t => {
t.plan(3)
function handler (req, res) { function handler (req, res) {
t.match(req.headers, { t.match(req.headers, {
authorization: 'Basic Zm9vOmJhcg==' authorization: 'Basic Zm9vOmJhcg=='
}) })
res.setHeader('Content-Type', 'application/json;utf=8') res.setHeader('Content-Type', 'application/json;utf=8')
res.end(JSON.stringify({ hello: 'world' })) 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()
})
})
})
test('Custom 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 }) => { buildServer(handler, ({ port }, server) => {
t.error(err) const client = new Client({
t.deepEqual(body, { hello: 'world' }) node: `http://foo:bar@localhost:${port}`
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 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()
})
})
})
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()
}) })
t.test('ApiKey', t => {
t.test('Node with ApiKey auth data in the options as string', 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: 'Zm9vOmJhcg=='
}
})
client.info((err, { body }) => {
t.error(err)
t.deepEqual(body, { hello: 'world' })
server.stop()
})
})
})
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()
})
})
})
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' }))
}
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 => { test('Custom headers per request', t => {
@ -554,6 +784,45 @@ test('Elastic cloud config', t => {
t.match(pool.connections.get('https://abcd.localhost/'), { t.match(pool.connections.get('https://abcd.localhost/'), {
url: new URL('https://elastic:changeme@abcd.localhost'), url: new URL('https://elastic:changeme@abcd.localhost'),
id: 'https://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' }, ssl: { secureProtocol: 'TLSv1_2_method' },
deadCount: 0, deadCount: 0,
resurrectTimeout: 0, resurrectTimeout: 0,

View File

@ -50,25 +50,6 @@ test('API', t => {
t.end() 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 => { t.test('addConnection should handle not-friendly url parameters for user and password', t => {
const pool = new ConnectionPool({ Connection }) const pool = new ConnectionPool({ Connection })
const href = 'http://us"er:p@assword@localhost:9200/' const href = 'http://us"er:p@assword@localhost:9200/'
@ -76,8 +57,9 @@ test('API', t => {
const conn = pool.getConnection() const conn = pool.getConnection()
t.strictEqual(conn.url.username, 'us%22er') t.strictEqual(conn.url.username, 'us%22er')
t.strictEqual(conn.url.password, 'p%40assword') t.strictEqual(conn.url.password, 'p%40assword')
t.strictEqual(conn.auth.username, 'us"er') t.match(conn.headers, {
t.strictEqual(conn.auth.password, 'p@assword') authorization: 'Basic ' + Buffer.from('us"er:p@assword').toString('base64')
})
t.end() t.end()
}) })

View File

@ -811,6 +811,53 @@ test('Port handling', t => {
t.end() 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 => { test('Should not add agent and ssl to the serialized connection', t => {
const connection = new Connection({ const connection = new Connection({
url: new URL('http://localhost:9200') url: new URL('http://localhost:9200')
@ -818,7 +865,7 @@ test('Should not add agent and ssl to the serialized connection', t => {
t.strictEqual( t.strictEqual(
JSON.stringify(connection), 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() t.end()