diff --git a/.github/workflows/test-spec.yml b/.github/workflows/test-spec.yml index c63dde722..fff6fa61a 100644 --- a/.github/workflows/test-spec.yml +++ b/.github/workflows/test-spec.yml @@ -53,12 +53,16 @@ jobs: tests: plugins/workload-management - version: 2.18.0 tests: plugins/analysis + - version: 2.18.0 + tests: plugins/security + cert: plugins/security/kirk.pem + key: plugins/security/kirk-key.pem - version: 2.19.0 hub: opensearchstaging - ref: '@sha256:4da23e0137b2b67206d23b36fcf0914cc39b3bf19310c782f536e4934b86f6cc' + ref: "@sha256:4da23e0137b2b67206d23b36fcf0914cc39b3bf19310c782f536e4934b86f6cc" - version: 3.0.0 hub: opensearchstaging - ref: '@sha256:727643acdfebed77bfdb26362dbcff536b7ea02a0cc4ae2da2521729171333de' + ref: "@sha256:727643acdfebed77bfdb26362dbcff536b7ea02a0cc4ae2da2521729171333de" name: test-opensearch-spec (version=${{ matrix.entry.version }}, hub=${{ matrix.entry.hub || 'opensearchproject' }}, tests=${{ matrix.entry.tests || 'default' }}) runs-on: ubuntu-latest @@ -77,7 +81,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: '20' + node-version: "20" - name: Install Dependencies run: npm ci @@ -97,6 +101,8 @@ jobs: --opensearch-version=${{ matrix.entry.version }} \ --coverage coverage/test-spec-coverage-${{ steps.tests.outputs.hash }}.json \ --opensearch-url=${{ matrix.entry.url || 'https://localhost:9200'}} \ + --opensearch-cert=${{ matrix.entry.cert }} \ + --opensearch-key=${{ matrix.entry.key }} \ --tests=tests/${{ matrix.entry.tests || 'default' }} - name: Get Container Logs diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 9709132fc..ffdc55b65 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -243,6 +243,8 @@ The dump-cluster-spec tool connects to an OpenSearch cluster which has the [open - `--opensearch-insecure`: Disable SSL/TLS certificate verification, defaults to performing verification. - `--opensearch-username `: The username to authenticate with the cluster, defaults to `admin`, only used when `--opensearch-password` is set. - `--opensearch-password `: The password to authenticate with the cluster, also settable via the `OPENSEARCH_PASSWORD` environment variable. +- `--opensearch-cert `: The OpenSSL certificate file, also settable via the `OPENSEARCH_CERT` environment variable. +- `--opensearch-key `: The OpenSSL certificate private key, also settable via the `OPENSEARCH_KEY` environment variable. - `--output `: The path to write the dumped spec to, defaults to `/build/opensearch-openapi-CLUSTER.yaml`. **Example** diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 362f6359f..2812b37b2 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -5,6 +5,7 @@ - [Prerequisites](#prerequisites) - [OpenSearch Cluster](#opensearch-cluster) - [Run Tests](#run-tests) + - [Running Spec Tests that Require an Admin Certificate](#running-spec-tests-that-require-an-admin-certificate) - [Running Spec Tests with Amazon OpenSearch](#running-spec-tests-with-amazon-opensearch) - [Common Errors](#common-errors) - [401 Unauthorized](#401-unauthorized) @@ -76,6 +77,19 @@ Want to help with some missing tests? Choose from the remaining paths in the tes npm run test:spec -- --opensearch-insecure --coverage-report ``` +### Running Spec Tests that Require an Admin Certificate + +Some tests may require an admin certificate for authorization. The certificate can be provided wth `--opensearch-cert` and the key with `opensearch-key`. + +For example, run tests in [plugins/security](tests/plugins/security) as follows: +```bash +npm run test:spec--insecure -- \ + --tests tests/plugins/security/api/nodesdn.yaml \ + --opensearch-key tests/plugins/security/kirk-key.pem \ + --opensearch-cert tests/plugins/security/kirk.pem \ + --verbose +``` + ### Running Spec Tests with Amazon OpenSearch Use an Amazon OpenSearch service instance. diff --git a/tests/plugins/security/api/nodesdn.yaml b/tests/plugins/security/api/nodesdn.yaml new file mode 100644 index 000000000..730477f39 --- /dev/null +++ b/tests/plugins/security/api/nodesdn.yaml @@ -0,0 +1,7 @@ +$schema: ../../../../json_schemas/test_story.schema.yaml + +description: Test retrieving `nodesdn` dynamically. +chapters: + - synopsis: Retrieve `nodesdn`. + path: /_plugins/_security/api/nodesdn + method: GET diff --git a/tests/plugins/security/docker-compose.yml b/tests/plugins/security/docker-compose.yml new file mode 100644 index 000000000..d16327a6f --- /dev/null +++ b/tests/plugins/security/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3' + +services: + opensearch-cluster: + image: ${OPENSEARCH_DOCKER_HUB_PROJECT:-opensearchproject}/opensearch:${OPENSEARCH_VERSION:-latest}${OPENSEARCH_DOCKER_REF} + ports: + - 9200:9200 + - 9600:9600 + environment: + - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_PASSWORD:-myStrongPassword123!} + - OPENSEARCH_JAVA_OPTS=${OPENSEARCH_JAVA_OPTS} + - discovery.type=single-node + - plugins.security.nodes_dn_dynamic_config_enabled=true + volumes: + - ./kirk-key.pem:/usr/share/opensearch/config/kirk-key.pem + - ./kirk.pem:/usr/share/opensearch/config/kirk.pem diff --git a/tests/plugins/security/kirk-key.pem b/tests/plugins/security/kirk-key.pem new file mode 100644 index 000000000..fd1728cda --- /dev/null +++ b/tests/plugins/security/kirk-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCVXDgEJQorgfXp +gpY0TgF55bD2xuzxN5Dc9rDfgWxrsOvOloMpd7k6FR71bKWjJi1KptSmM/cDElky +AWYKSfYWGiGxsQ+EQW+6kwCfEOHXQldn+0+JcWqP+osSPjtJfwRvRN5kRqP69MPo +7U0N2kdqenqMWjmG1chDGLRSOEGU5HIBiDxsZtOcvMaJ8b1eaW0lvS+6gFQ80AvB +GBkDDCOHHLtDXBylrZk2CQP8AzxNicIZ4B8G3CG3OHA8+nBtEtxZoIihrrkqlMt+ +b/5N8u8zB0Encew0kdrc4R/2wS//ahr6U+9Siq8T7WsUtGwKj3BJClg6OyDJRhlu +y2gFnxoPAgMBAAECggEAP5TOycDkx+megAWVoHV2fmgvgZXkBrlzQwUG/VZQi7V4 +ZGzBMBVltdqI38wc5MtbK3TCgHANnnKgor9iq02Z4wXDwytPIiti/ycV9CDRKvv0 +TnD2hllQFjN/IUh5n4thHWbRTxmdM7cfcNgX3aZGkYbLBVVhOMtn4VwyYu/Mxy8j +xClZT2xKOHkxqwmWPmdDTbAeZIbSv7RkIGfrKuQyUGUaWhrPslvYzFkYZ0umaDgQ +OAthZew5Bz3OfUGOMPLH61SVPuJZh9zN1hTWOvT65WFWfsPd2yStI+WD/5PU1Doo +1RyeHJO7s3ug8JPbtNJmaJwHe9nXBb/HXFdqb976yQKBgQDNYhpu+MYSYupaYqjs +9YFmHQNKpNZqgZ4ceRFZ6cMJoqpI5dpEMqToFH7tpor72Lturct2U9nc2WR0HeEs +/6tiptyMPTFEiMFb1opQlXF2ae7LeJllntDGN0Q6vxKnQV+7VMcXA0Y8F7tvGDy3 +qJu5lfvB1mNM2I6y/eMxjBuQhwKBgQC6K41DXMFro0UnoO879pOQYMydCErJRmjG +/tZSy3Wj4KA/QJsDSViwGfvdPuHZRaG9WtxdL6kn0w1exM9Rb0bBKl36lvi7o7xv +M+Lw9eyXMkww8/F5d7YYH77gIhGo+RITkKI3+5BxeBaUnrGvmHrpmpgRXWmINqr0 +0jsnN3u0OQKBgCf45vIgItSjQb8zonLz2SpZjTFy4XQ7I92gxnq8X0Q5z3B+o7tQ +K/4rNwTju/sGFHyXAJlX+nfcK4vZ4OBUJjP+C8CTjEotX4yTNbo3S6zjMyGQqDI5 +9aIOUY4pb+TzeUFJX7If5gR+DfGyQubvvtcg1K3GHu9u2l8FwLj87sRzAoGAflQF +RHuRiG+/AngTPnZAhc0Zq0kwLkpH2Rid6IrFZhGLy8AUL/O6aa0IGoaMDLpSWUJp +nBY2S57MSM11/MVslrEgGmYNnI4r1K25xlaqV6K6ztEJv6n69327MS4NG8L/gCU5 +3pEm38hkUi8pVYU7in7rx4TCkrq94OkzWJYurAkCgYATQCL/rJLQAlJIGulp8s6h +mQGwy8vIqMjAdHGLrCS35sVYBXG13knS52LJHvbVee39AbD5/LlWvjJGlQMzCLrw +F7oILW5kXxhb8S73GWcuMbuQMFVHFONbZAZgn+C9FW4l7XyRdkrbR1MRZ2km8YMs +/AHmo368d4PSNRMMzLHw8Q== +-----END PRIVATE KEY----- diff --git a/tests/plugins/security/kirk.pem b/tests/plugins/security/kirk.pem new file mode 100644 index 000000000..b89edfe18 --- /dev/null +++ b/tests/plugins/security/kirk.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEmDCCA4CgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLcwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzA0MjRaFw0zNDAyMTcxNzA0MjRaME0xCzAJBgNVBAYT +AmRlMQ0wCwYDVQQHDAR0ZXN0MQ8wDQYDVQQKDAZjbGllbnQxDzANBgNVBAsMBmNs +aWVudDENMAsGA1UEAwwEa2lyazCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAJVcOAQlCiuB9emCljROAXnlsPbG7PE3kNz2sN+BbGuw686Wgyl3uToVHvVs +paMmLUqm1KYz9wMSWTIBZgpJ9hYaIbGxD4RBb7qTAJ8Q4ddCV2f7T4lxao/6ixI+ +O0l/BG9E3mRGo/r0w+jtTQ3aR2p6eoxaOYbVyEMYtFI4QZTkcgGIPGxm05y8xonx +vV5pbSW9L7qAVDzQC8EYGQMMI4ccu0NcHKWtmTYJA/wDPE2JwhngHwbcIbc4cDz6 +cG0S3FmgiKGuuSqUy35v/k3y7zMHQSdx7DSR2tzhH/bBL/9qGvpT71KKrxPtaxS0 +bAqPcEkKWDo7IMlGGW7LaAWfGg8CAwEAAaOCASswggEnMAwGA1UdEwEB/wQCMAAw +DgYDVR0PAQH/BAQDAgXgMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMB0GA1UdDgQW +BBSjMS8tgguX/V7KSGLoGg7K6XMzIDCBzwYDVR0jBIHHMIHEgBQXh9+gWutmEqfV +0Pi6EkU8tysAnKGBlaSBkjCBjzETMBEGCgmSJomT8ixkARkWA2NvbTEXMBUGCgmS +JomT8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1wbGUgQ29tIEluYy4xITAf +BgNVBAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEhMB8GA1UEAwwYRXhhbXBs +ZSBDb20gSW5jLiBSb290IENBghQNZAmZZn3EFOxBR4630XlhI+mo4jANBgkqhkiG +9w0BAQsFAAOCAQEACEUPPE66/Ot3vZqRGpjDjPHAdtOq+ebaglQhvYcnDw8LOZm8 +Gbh9M88CiO6UxC8ipQLTPh2yyeWArkpJzJK/Pi1eoF1XLiAa0sQ/RaJfQWPm9dvl +1ZQeK5vfD4147b3iBobwEV+CR04SKow0YeEEzAJvzr8YdKI6jqr+2GjjVqzxvRBy +KRVHWCFiR7bZhHGLq3br8hSu0hwjb3oGa1ZI8dui6ujyZt6nm6BoEkau3G/6+zq9 +E6vX3+8Fj4HKCAL6i0SwfGmEpTNp5WUhqibK/fMhhmMT4Mx6MxkT+OFnIjdUU0S/ +e3kgnG8qjficUr38CyEli1U0M7koIXUZI7r+LQ== +-----END CERTIFICATE----- diff --git a/tools/src/OpenSearchHttpClient.ts b/tools/src/OpenSearchHttpClient.ts index 314a73d45..c4eda6032 100644 --- a/tools/src/OpenSearchHttpClient.ts +++ b/tools/src/OpenSearchHttpClient.ts @@ -10,6 +10,7 @@ import { Option } from '@commander-js/extra-typings' import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type ResponseType } from 'axios' import * as https from 'node:https' +import fs from 'fs' import { sleep } from './helpers' import { Logger } from './Logger' import { aws4Interceptor } from 'aws4-axios' @@ -29,6 +30,12 @@ export const OPENSEARCH_USERNAME_OPTION = new Option('--opensearch-username ', 'password to use when authenticating with OpenSearch') .env('OPENSEARCH_PASSWORD') +export const OPENSEARCH_CERT_OPTION = new Option('--opensearch-cert ', 'client certificate file to use when authenticating with OpenSearch') + .env('OPENSEARCH_CERT') + +export const OPENSEARCH_KEY_OPTION = new Option('--opensearch-key ', 'client certificate private key file name to use when authenticating with OpenSearch') + .env('OPENSEARCH_KEY') + export const OPENSEARCH_INSECURE_OPTION = new Option('--opensearch-insecure', 'disable SSL/TLS certificate verification when connecting to OpenSearch') .default(DEFAULT_INSECURE) @@ -65,6 +72,8 @@ export interface AwsAuth { export interface OpenSearchHttpClientOptions { url?: string insecure?: boolean + cert?: string, + key?: string, responseType?: ResponseType logger?: Logger, basic_auth?: BasicAuth @@ -77,6 +86,8 @@ export type OpenSearchHttpClientCliOptions = { opensearchUsername?: string opensearchPassword?: string opensearchInsecure?: boolean + opensearchCert?: string, + opensearchKey?: string, awsAccessKeyId?: string awsSecretAccessKey?: string awsSessionToken?: string @@ -90,6 +101,8 @@ export function get_opensearch_opts_from_cli (opts: OpenSearchHttpClientCliOptio return { url: opts.opensearchUrl, insecure: opts.opensearchInsecure, + cert: opts.opensearchCert, + key: opts.opensearchKey, basic_auth: opts.opensearchUsername !== undefined && opts.opensearchPassword !== undefined ? { username: opts.opensearchUsername, password: opts.opensearchPassword @@ -164,7 +177,11 @@ export class OpenSearchHttpClient { this._axios = axios.create({ baseURL: opts?.url ?? DEFAULT_URL, - httpsAgent: new https.Agent({ rejectUnauthorized: !(opts?.insecure ?? DEFAULT_INSECURE) }), + httpsAgent: new https.Agent({ + rejectUnauthorized: !(opts?.insecure ?? DEFAULT_INSECURE), + cert: opts?.cert !== undefined ? fs.readFileSync(opts?.cert) : undefined, + key: opts?.key !== undefined ? fs.readFileSync(opts?.key) : undefined, + }), responseType: opts?.responseType, }) diff --git a/tools/src/dump-cluster-spec/dump-cluster-spec.ts b/tools/src/dump-cluster-spec/dump-cluster-spec.ts index 02dd77972..bdf8ea05a 100644 --- a/tools/src/dump-cluster-spec/dump-cluster-spec.ts +++ b/tools/src/dump-cluster-spec/dump-cluster-spec.ts @@ -13,7 +13,9 @@ import * as process from 'node:process' import { write_yaml } from '../helpers' import { get_opensearch_opts_from_cli, + OPENSEARCH_CERT_OPTION, OPENSEARCH_INSECURE_OPTION, + OPENSEARCH_KEY_OPTION, OPENSEARCH_PASSWORD_OPTION, OPENSEARCH_URL_OPTION, OPENSEARCH_USERNAME_OPTION, OpenSearchHttpClient, @@ -42,6 +44,8 @@ const command = new Command() .addOption(OPENSEARCH_USERNAME_OPTION) .addOption(OPENSEARCH_PASSWORD_OPTION) .addOption(OPENSEARCH_INSECURE_OPTION) + .addOption(OPENSEARCH_CERT_OPTION) + .addOption(OPENSEARCH_KEY_OPTION) .addOption(new Option('--output ', 'path to the output file').default(resolve(__dirname, '../../../build/opensearch-openapi-CLUSTER.yaml'))) .allowExcessArguments(false) .parse() diff --git a/tools/src/tester/test.ts b/tools/src/tester/test.ts index 77351fe75..8a69185fb 100644 --- a/tools/src/tester/test.ts +++ b/tools/src/tester/test.ts @@ -17,7 +17,9 @@ import { AWS_SERVICE_OPTION, AWS_SESSION_TOKEN_OPTION, get_opensearch_opts_from_cli, + OPENSEARCH_CERT_OPTION, OPENSEARCH_INSECURE_OPTION, + OPENSEARCH_KEY_OPTION, OPENSEARCH_PASSWORD_OPTION, OPENSEARCH_URL_OPTION, OPENSEARCH_USERNAME_OPTION, @@ -54,6 +56,8 @@ const command = new Command() .addOption(OPENSEARCH_USERNAME_OPTION) .addOption(OPENSEARCH_PASSWORD_OPTION) .addOption(OPENSEARCH_INSECURE_OPTION) + .addOption(OPENSEARCH_CERT_OPTION) + .addOption(OPENSEARCH_KEY_OPTION) .addOption(AWS_ACCESS_KEY_ID_OPTION) .addOption(AWS_SECRET_ACCESS_KEY_OPTION) .addOption(AWS_SESSION_TOKEN_OPTION) diff --git a/tools/tests/tester/OpenSearchHttpClient.test.ts b/tools/tests/tester/OpenSearchHttpClient.test.ts index 85c62af05..0493488c4 100644 --- a/tools/tests/tester/OpenSearchHttpClient.test.ts +++ b/tools/tests/tester/OpenSearchHttpClient.test.ts @@ -73,4 +73,47 @@ describe('OpenSearchHttpClient', () => { expect((await client.get('/')).data).toEqual({ called: true }) }) + + it('defaults to rejectUnauthorized', async () => { + let client = new OpenSearchHttpClient({ + url: 'https://localhost:9200' + }) + + mock.onAny().reply((config) => { + expect(config.httpsAgent.options.rejectUnauthorized).toBe(true) + return [200, { called: true }] + }) + + expect((await client.get('/')).data).toEqual({ called: true }) + }) + + it('sets rejectUnauthorized to false', async () => { + let client = new OpenSearchHttpClient({ + url: 'https://localhost:9200', + insecure: true + }) + + mock.onAny().reply((config) => { + expect(config.httpsAgent.options.rejectUnauthorized).toEqual(false) + return [200, { called: true }] + }) + + expect((await client.get('/')).data).toEqual({ called: true }) + }) + + it('adds a certificate file and key', async () => { + let client = new OpenSearchHttpClient({ + url: 'https://localhost:9200', + cert: './tools/tests/tester/fixtures/keys/kirk.pem', + key: './tools/tests/tester/fixtures/keys/kirk-key.pem' + }) + + mock.onAny().reply((config) => { + expect(config.httpsAgent.options.cert.toString()).toEqual("-----BEGIN CERTIFICATE-----\ncertificate\n-----END CERTIFICATE-----\n") + expect(config.httpsAgent.options.key.toString()).toEqual("-----BEGIN PRIVATE KEY-----\nprivate key\n-----END PRIVATE KEY-----\n") + return [200, { called: true }] + }) + + expect((await client.get('/')).data).toEqual({ called: true }) + }) }) diff --git a/tools/tests/tester/fixtures/keys/kirk-key.pem b/tools/tests/tester/fixtures/keys/kirk-key.pem new file mode 100644 index 000000000..5af21d7eb --- /dev/null +++ b/tools/tests/tester/fixtures/keys/kirk-key.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +private key +-----END PRIVATE KEY----- diff --git a/tools/tests/tester/fixtures/keys/kirk.pem b/tools/tests/tester/fixtures/keys/kirk.pem new file mode 100644 index 000000000..24c83bcd0 --- /dev/null +++ b/tools/tests/tester/fixtures/keys/kirk.pem @@ -0,0 +1,3 @@ +-----BEGIN CERTIFICATE----- +certificate +-----END CERTIFICATE-----