diff --git a/packages/osd-opensearch/src/cli_commands/archive.js b/packages/osd-opensearch/src/cli_commands/archive.js index ebcdbb98b7a8..b9970cbaece9 100644 --- a/packages/osd-opensearch/src/cli_commands/archive.js +++ b/packages/osd-opensearch/src/cli_commands/archive.js @@ -48,7 +48,6 @@ exports.help = (defaults = {}) => { --base-path Path containing cache/installations [default: ${basePath}] --install-path Installation path, defaults to 'source' within base-path --password Sets password for opensearch user [default: ${password}] - --password.[user] Sets password for native realm user [default: ${password}] --ssl Sets up SSL on OpenSearch -E Additional key=value settings to pass to OpenSearch diff --git a/packages/osd-opensearch/src/cli_commands/snapshot.js b/packages/osd-opensearch/src/cli_commands/snapshot.js index 341768572d29..73ca17736322 100644 --- a/packages/osd-opensearch/src/cli_commands/snapshot.js +++ b/packages/osd-opensearch/src/cli_commands/snapshot.js @@ -48,7 +48,6 @@ exports.help = (defaults = {}) => { --install-path Installation path, defaults to 'source' within base-path --data-archive Path to zip or tarball containing an OpenSearch data directory to seed the cluster with. --password Sets password for opensearch user [default: ${password}] - --password.[user] Sets password for native realm user [default: ${password}] -E Additional key=value settings to pass to OpenSearch --download-only Download the snapshot but don't actually start it --ssl Sets up SSL on OpenSearch diff --git a/packages/osd-opensearch/src/cli_commands/source.js b/packages/osd-opensearch/src/cli_commands/source.js index bdf84cac8cc7..727179d7e056 100644 --- a/packages/osd-opensearch/src/cli_commands/source.js +++ b/packages/osd-opensearch/src/cli_commands/source.js @@ -48,7 +48,6 @@ exports.help = (defaults = {}) => { --install-path Installation path, defaults to 'source' within base-path --data-archive Path to zip or tarball containing an OpenSearch data directory to seed the cluster with. --password Sets password for opensearch user [default: ${password}] - --password.[user] Sets password for native realm user [default: ${password}] --ssl Sets up SSL on OpenSearch -E Additional key=value settings to pass to OpenSearch diff --git a/packages/osd-opensearch/src/cluster.js b/packages/osd-opensearch/src/cluster.js index 2114867bcba4..e678526737df 100644 --- a/packages/osd-opensearch/src/cluster.js +++ b/packages/osd-opensearch/src/cluster.js @@ -37,13 +37,7 @@ const chalk = require('chalk'); const path = require('path'); const { downloadSnapshot, installSnapshot, installSource, installArchive } = require('./install'); const { OPENSEARCH_BIN } = require('./paths'); -const { - log: defaultLog, - parseOpenSearchLog, - extractConfigFiles, - decompress, - NativeRealm, -} = require('./utils'); +const { log: defaultLog, parseOpenSearchLog, extractConfigFiles, decompress } = require('./utils'); const { createCliError } = require('./errors'); const { promisify } = require('util'); const treeKillAsync = promisify(require('tree-kill')); @@ -191,14 +185,13 @@ exports.Cluster = class Cluster { this._exec(installPath, options); await Promise.race([ - // wait for native realm to be setup and opensearch to be started + // wait for opensearch to be started Promise.all([ first(this._process.stdout, (data) => { if (/started/.test(data)) { return true; } }), - this._nativeRealmSetup, ]), // await the outcome of the process in case it exits before starting @@ -219,12 +212,6 @@ exports.Cluster = class Cluster { async run(installPath, options = {}) { this._exec(installPath, options); - // log native realm setup errors so they aren't uncaught - this._nativeRealmSetup.catch((error) => { - this._log.error(error); - this.stop(); - }); - // await the final outcome of the process await this._outcome; } @@ -314,28 +301,6 @@ exports.Cluster = class Cluster { stdio: ['ignore', 'pipe', 'pipe'], }); - // parse log output to find http port - const httpPort = first(this._process.stdout, (data) => { - const match = data.toString('utf8').match(/HttpServer.+publish_address {[0-9.]+:([0-9]+)/); - - if (match) { - return match[1]; - } - }); - - // once the http port is available setup the native realm - this._nativeRealmSetup = httpPort.then(async (port) => { - const caCert = await this._caCertPromise; - const nativeRealm = new NativeRealm({ - port, - caCert, - log: this._log, - opensearchPassword: options.password, - ssl: this._ssl, - }); - await nativeRealm.setPasswords(options); - }); - // parse and forward opensearch stdout to the log this._process.stdout.on('data', (data) => { const lines = parseOpenSearchLog(data.toString()); diff --git a/packages/osd-opensearch/src/utils/index.js b/packages/osd-opensearch/src/utils/index.js index e5728dc6c656..846e6115f122 100644 --- a/packages/osd-opensearch/src/utils/index.js +++ b/packages/osd-opensearch/src/utils/index.js @@ -36,6 +36,5 @@ exports.parseOpenSearchLog = require('./parse_opensearch_log').parseOpenSearchLo exports.findMostRecentlyChanged = require('./find_most_recently_changed').findMostRecentlyChanged; exports.extractConfigFiles = require('./extract_config_files').extractConfigFiles; exports.decompress = require('./decompress').decompress; -exports.NativeRealm = require('./native_realm').NativeRealm; exports.buildSnapshot = require('./build_snapshot').buildSnapshot; exports.archiveForPlatform = require('./build_snapshot').archiveForPlatform; diff --git a/packages/osd-opensearch/src/utils/native_realm.js b/packages/osd-opensearch/src/utils/native_realm.js deleted file mode 100644 index 31e4ab1f8525..000000000000 --- a/packages/osd-opensearch/src/utils/native_realm.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -const { Client } = require('@elastic/elasticsearch'); -const chalk = require('chalk'); - -const { log: defaultLog } = require('./log'); - -exports.NativeRealm = class NativeRealm { - constructor({ opensearchPassword, port, log = defaultLog, ssl = false, caCert }) { - this._client = new Client({ - node: `${ssl ? 'https' : 'http'}://opensearch:${opensearchPassword}@localhost:${port}`, - ssl: ssl - ? { - ca: caCert, - rejectUnauthorized: true, - } - : undefined, - }); - this.opensearchPassword = opensearchPassword; - this._log = log; - } - - async setPassword(username, password = this._opensearchPassword, retryOpts = {}) { - await this._autoRetry(retryOpts, async () => { - try { - await this._client.security.changePassword({ - username, - refresh: 'wait_for', - body: { - password, - }, - }); - } catch (err) { - const isAnonymousUserPasswordChangeError = - err.statusCode === 400 && - err.body && - err.body.error && - err.body.error.reason.startsWith(`user [${username}] is anonymous`); - if (!isAnonymousUserPasswordChangeError) { - throw err; - } else { - this._log.info( - `cannot set password for anonymous user ${chalk.bold(username)}, skipping` - ); - } - } - }); - } - - async setPasswords(options) { - if (!(await this.isSecurityEnabled())) { - this._log.info('security is not enabled, unable to set native realm passwords'); - return; - } - - const reservedUsers = await this.getReservedUsers(); - await Promise.all( - reservedUsers.map(async (user) => { - await this.setPassword(user, options[`password.${user}`]); - }) - ); - } - - async getReservedUsers(retryOpts = {}) { - return await this._autoRetry(retryOpts, async () => { - const resp = await this._client.security.getUser(); - const usernames = Object.keys(resp.body).filter( - (user) => resp.body[user].metadata._reserved === true - ); - - if (!usernames?.length) { - throw new Error('no reserved users found, unable to set native realm passwords'); - } - - return usernames; - }); - } - - async isSecurityEnabled(retryOpts = {}) { - try { - return await this._autoRetry(retryOpts, async () => { - const { - body: { features }, - } = await this._client.xpack.info({ categories: 'features' }); - return features.security && features.security.enabled && features.security.available; - }); - } catch (error) { - if (error.meta && error.meta.statusCode === 400) { - return false; - } - - throw error; - } - } - - async _autoRetry(opts, fn) { - const { attempt = 1, maxAttempts = 3 } = opts; - - try { - return await fn(attempt); - } catch (error) { - if (attempt >= maxAttempts) { - throw error; - } - - const sec = 1.5 * attempt; - this._log.warning( - `assuming OpenSearch isn't initialized completely, trying again in ${sec} seconds` - ); - await new Promise((resolve) => setTimeout(resolve, sec * 1000)); - - const nextOpts = { - ...opts, - attempt: attempt + 1, - }; - return await this._autoRetry(nextOpts, fn); - } - } -}; diff --git a/packages/osd-opensearch/src/utils/native_realm.test.js b/packages/osd-opensearch/src/utils/native_realm.test.js deleted file mode 100644 index cd372e4174b7..000000000000 --- a/packages/osd-opensearch/src/utils/native_realm.test.js +++ /dev/null @@ -1,248 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -const { NativeRealm } = require('./native_realm'); - -jest.genMockFromModule('@elastic/elasticsearch'); -jest.mock('@elastic/elasticsearch'); - -const { ToolingLog } = require('@osd/dev-utils'); -const { Client } = require('@elastic/elasticsearch'); - -const mockClient = { - xpack: { - info: jest.fn(), - }, - security: { - changePassword: jest.fn(), - getUser: jest.fn(), - }, -}; -Client.mockImplementation(() => mockClient); - -const log = new ToolingLog(); -let nativeRealm; - -beforeEach(() => { - nativeRealm = new NativeRealm({ opensearchPassword: 'changeme', port: '9200', log }); -}); - -afterAll(() => { - jest.clearAllMocks(); -}); - -function mockXPackInfo(available, enabled) { - mockClient.xpack.info.mockImplementation(() => ({ - body: { - features: { - security: { - available, - enabled, - }, - }, - }, - })); -} - -describe('isSecurityEnabled', () => { - test('returns true if enabled and available', async () => { - mockXPackInfo(true, true); - expect(await nativeRealm.isSecurityEnabled()).toBe(true); - }); - - test('returns false if not available', async () => { - mockXPackInfo(false, true); - expect(await nativeRealm.isSecurityEnabled()).toBe(false); - }); - - test('returns false if not enabled', async () => { - mockXPackInfo(true, false); - expect(await nativeRealm.isSecurityEnabled()).toBe(false); - }); - - test('returns false if 400 error returned', async () => { - mockClient.xpack.info.mockImplementation(() => { - const error = new Error('ResponseError'); - error.meta = { - statusCode: 400, - }; - throw error; - }); - - expect(await nativeRealm.isSecurityEnabled({ maxAttempts: 1 })).toBe(false); - }); - - test('rejects if unexpected error is thrown', async () => { - mockClient.xpack.info.mockImplementation(() => { - const error = new Error('ResponseError'); - error.meta = { - statusCode: 500, - }; - throw error; - }); - - await expect( - nativeRealm.isSecurityEnabled({ maxAttempts: 1 }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"ResponseError"`); - }); -}); - -describe('setPasswords', () => { - it('uses provided passwords', async () => { - mockXPackInfo(true, true); - - mockClient.security.getUser.mockImplementation(() => ({ - body: { - opensearch_dashboards_system: { - metadata: { - _reserved: true, - }, - }, - non_native: { - metadata: { - _reserved: false, - }, - }, - logstash_system: { - metadata: { - _reserved: true, - }, - }, - opensearch: { - metadata: { - _reserved: true, - }, - }, - beats_system: { - metadata: { - _reserved: true, - }, - }, - }, - })); - - await nativeRealm.setPasswords({ - 'password.opensearch_dashboards_system': 'bar', - }); - - expect(mockClient.security.changePassword.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "body": Object { - "password": "bar", - }, - "refresh": "wait_for", - "username": "opensearch_dashboards_system", - }, - ], - Array [ - Object { - "body": Object { - "password": undefined, - }, - "refresh": "wait_for", - "username": "logstash_system", - }, - ], - Array [ - Object { - "body": Object { - "password": undefined, - }, - "refresh": "wait_for", - "username": "opensearch", - }, - ], - Array [ - Object { - "body": Object { - "password": undefined, - }, - "refresh": "wait_for", - "username": "beats_system", - }, - ], - ] - `); - }); -}); - -describe('getReservedUsers', () => { - it('returns array of reserved usernames', async () => { - mockClient.security.getUser.mockImplementation(() => ({ - body: { - opensearch_dashboards_system: { - metadata: { - _reserved: true, - }, - }, - non_native: { - metadata: { - _reserved: false, - }, - }, - logstash_system: { - metadata: { - _reserved: true, - }, - }, - }, - })); - - expect(await nativeRealm.getReservedUsers()).toEqual([ - 'opensearch_dashboards_system', - 'logstash_system', - ]); - }); -}); - -describe('setPassword', () => { - it('sets password for provided user', async () => { - await nativeRealm.setPassword('opensearch_dashboards_system', 'foo'); - expect(mockClient.security.changePassword).toHaveBeenCalledWith({ - body: { password: 'foo' }, - refresh: 'wait_for', - username: 'opensearch_dashboards_system', - }); - }); - - it('rejects with errors', async () => { - mockClient.security.changePassword.mockImplementation(() => { - throw new Error('SomeError'); - }); - - await expect( - nativeRealm.setPassword('opensearch_dashboards_system', 'foo', { maxAttempts: 1 }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"SomeError"`); - }); -});