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.
=== 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
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. +
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({
cloud: {
id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==',
},
auth: {
username: 'elastic',
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`
|`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'
}

4
index.d.ts vendored
View File

@ -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;
}

View File

@ -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',

2
lib/Connection.d.ts vendored
View File

@ -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 {

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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=='
})
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()
})
})
})
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'
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' }))
}
}, (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((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 => {
@ -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,

View File

@ -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()
})

View File

@ -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()