Skip to content

Commit

Permalink
Add support for authenticating a user with an X.509 cert
Browse files Browse the repository at this point in the history
* Rename `privateKey` -> `certKeyFile` and `certChain` -> `certFile`
  • Loading branch information
w1am committed Apr 10, 2024
1 parent 3543b83 commit 5ca51e8
Show file tree
Hide file tree
Showing 83 changed files with 422 additions and 127 deletions.
54 changes: 54 additions & 0 deletions .github/workflows/test_EE.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: "EE"

on:
pull_request:
push:
branches:
- master
schedule:
- cron: "0 * * * 0" # Every day at 3am UTC.

jobs:
tests:
name: "${{ matrix.group.name }}"
strategy:
fail-fast: false
matrix:
group:
- name: plugins
path: ./src/__test__/plugins
env:
# Github only passes secrets to the main repo, so we need to skip some things if they are unavailable
SECRETS_AVAILABLE: ${{ secrets.EVENTSTORE_CLOUD_ID != null }}
EVENTSTORE_VERSION: "24.2.0-jammy"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Connect to tailscale
if: ${{ matrix.group.tailscale && env.SECRETS_AVAILABLE == 'true' }}
run: |
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/eoan.gpg | sudo apt-key add -
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/eoan.list | sudo tee /etc/apt/sources.list.d/tailscale.list
sudo apt-get update
sudo apt-get install tailscale
sudo tailscale up --authkey ${{ secrets.TAILSCALE_AUTH }} --hostname "node-client-ci-${{ env.EVENTSTORE_VERSION }}-$(date +'%Y-%m-%d-%H-%M-%S')" --advertise-tags=tag:ci --accept-routes
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: "14.x"
- name: Login to Cloudsmith
uses: docker/login-action@v3
with:
registry: docker.eventstore.com
username: ${{ secrets.CLOUDSMITH_CICD_USER }}
password: ${{ secrets.CLOUDSMITH_CICD_TOKEN }}
- name: Install
run: yarn
- name: Run Tests
run: yarn test ${{ matrix.group.path }} --ci --run-in-band --forceExit
env:
EVENTSTORE_IMAGE: eventstore-ee:${{ env.EVENTSTORE_VERSION }}
EVENTSTORE_CLOUD_ID: ${{ secrets.EVENTSTORE_CLOUD_ID }}
- name: Disconnect from tailscale
if: ${{ always() && matrix.group.tailscale && env.SECRETS_AVAILABLE == 'true' }}
run: sudo tailscale down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,6 @@ dist
.npmrc
package-lock.json

src/__test__/utils/instances
src/__test__/utils/instances

.vscode
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"cross-env": "^7.0.3",
"docker-compose": "^0.23.19",
"docker-compose": "^0.24.7",
"dotenv": "^10.0.0",
"eslint": "^7.32.0",
"eslint-plugin-jsdoc": "^40.3.0",
Expand Down
2 changes: 1 addition & 1 deletion samples/appending-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe("[sample] appending-events", () => {
await node.up();
client = new EventStoreDBClient(
{ endpoint: node.uri },
{ rootCertificate: node.rootCertificate },
{ rootCertificate: node.certs.root },
{ username: "admin", password: "changeit" }
);
});
Expand Down
2 changes: 1 addition & 1 deletion samples/persistent-subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe("[sample] persistent-subscriptions", () => {

client = new EventStoreDBClient(
{ endpoint: node.uri },
{ rootCertificate: node.rootCertificate },
{ rootCertificate: node.certs.root },
{ username: "admin", password: "changeit" }
);

Expand Down
2 changes: 1 addition & 1 deletion samples/projection-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe("[sample] projection-management", () => {

client = new EventStoreDBClient(
{ endpoint: node.uri },
{ rootCertificate: node.rootCertificate },
{ rootCertificate: node.certs.root },
{ username: "admin", password: "changeit" }
);

Expand Down
2 changes: 1 addition & 1 deletion samples/reading-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe("[sample] reading-events", () => {
await node.up();
client = new EventStoreDBClient(
{ endpoint: node.uri },
{ rootCertificate: node.rootCertificate },
{ rootCertificate: node.certs.root },
{ username: "admin", password: "changeit" }
);

Expand Down
2 changes: 1 addition & 1 deletion samples/server-side-filtering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe("[sample] server-side-filtering", () => {
await node.up();
client = new EventStoreDBClient(
{ endpoint: node.uri },
{ rootCertificate: node.rootCertificate },
{ rootCertificate: node.certs.root },
{ username: "admin", password: "changeit" }
);

Expand Down
2 changes: 1 addition & 1 deletion samples/subscribing-to-streams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe("[sample] server-side-filtering", () => {
await node.up();
client = new EventStoreDBClient(
{ endpoint: node.uri },
{ rootCertificate: node.rootCertificate },
{ rootCertificate: node.certs.root },
{ username: "admin", password: "changeit" }
);

Expand Down
52 changes: 48 additions & 4 deletions src/Client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,25 @@ type ConnectionSettings =
| SingleNodeOptions;

export interface ChannelCredentialOptions {
/**
* Whether to use an insecure connection.
*/
insecure?: boolean;
/**
* The root certificate data.
*/
rootCertificate?: Buffer;
privateKey?: Buffer;
certChain?: Buffer;
/**
* The x.509 private key, if available.
*/
certKeyFile?: Buffer;
/**
* The x.509 key chain, if available.
*/
certFile?: Buffer;
/**
* Additional options to modify certificate verification.
*/
verifyOptions?: Parameters<typeof ChannelCredentials.createSsl>[3];
}

Expand Down Expand Up @@ -186,6 +201,35 @@ export class Client {
}
}

if (options.certFile || options.certKeyFile) {
if (!options.certFile || !options.certKeyFile) {
throw new Error(
"Both certPath and certKeyPath must be provided together"
);
}

const certPathResolved = isAbsolute(options.certFile)
? options.certFile
: resolve(process.cwd(), options.certFile);

const certKeyPathResolved = isAbsolute(options.certKeyFile)
? options.certKeyFile
: resolve(process.cwd(), options.certKeyFile);

if (!existsSync(certPathResolved)) {
throw new Error("Failed to load certificate file. File was not found.");
}

if (!existsSync(certKeyPathResolved)) {
throw new Error(
"Failed to load certificate key file. File was not found."
);
}

channelCredentials.certKeyFile = readFileSync(certKeyPathResolved);
channelCredentials.certFile = readFileSync(certPathResolved);
}

if (options.dnsDiscover) {
const [discover] = options.hosts;

Expand Down Expand Up @@ -317,8 +361,8 @@ export class Client {
);
this.#channelCredentials = grpcCredentials.createSsl(
channelCredentials.rootCertificate,
channelCredentials.privateKey,
channelCredentials.certChain,
channelCredentials.certKeyFile,
channelCredentials.certFile,
channelCredentials.verifyOptions
);
}
Expand Down
10 changes: 10 additions & 0 deletions src/Client/parseConnectionString.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { Credentials, EndPoint, NodePreference } from "../types";
import { debug } from "../utils";

export interface QueryOptions {
certFile?: string;
certKeyFile?: string;
maxDiscoverAttempts?: number;
connectionName?: string;
defaultDeadline?: number;
Expand Down Expand Up @@ -54,6 +56,8 @@ const mapToNodePreference = caseMap<NodePreference>({
});

const mapToQueryOption = caseMap<keyof QueryOptions>({
certFile: "certFile",
certKeyFile: "certKeyFile",
maxDiscoverAttempts: "maxDiscoverAttempts",
connectionName: "connectionName",
defaultDeadline: "defaultDeadline",
Expand Down Expand Up @@ -291,6 +295,12 @@ const verifyKeyValuePair = (
case "tlsCAFile": {
return { key, value: decodeURIComponent(value) };
}
case "certFile": {
return { key, value: decodeURIComponent(value) };
}
case "certKeyFile": {
return { key, value: decodeURIComponent(value) };
}
case "maxDiscoverAttempts":
case "defaultDeadline":
case "discoveryInterval":
Expand Down
2 changes: 1 addition & 1 deletion src/__test__/connection/Channel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe("Channel", () => {
endpoints: cluster.endpoints,
nodePreference: "random",
},
{ rootCertificate: cluster.rootCertificate },
{ rootCertificate: cluster.certs.root },
{ username: "admin", password: "changeit" }
);

Expand Down
4 changes: 2 additions & 2 deletions src/__test__/connection/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe("cluster", () => {
test("should successfully connect", async () => {
const client = new EventStoreDBClient(
{ endpoints: cluster.endpoints },
{ rootCertificate: cluster.rootCertificate },
{ rootCertificate: cluster.certs.root },
{ username: "admin", password: "changeit" }
);

Expand All @@ -42,7 +42,7 @@ describe("cluster", () => {
],
maxDiscoverAttempts,
},
{ rootCertificate: cluster.rootCertificate }
{ rootCertificate: cluster.certs.root }
);

await expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ connectionStringTests({
createServer: createTestCluster,
createUri: ({ endpoints }) =>
endpoints.map(({ address, port }) => `${address}:${port}`).join(","),
createQueryString: ({ certPath }) => `tlsCAFile=${certPath}`,
createQueryString: ({ certPath }) => `tlsCAFile=${certPath.root}`,
streamPrefix: "secure-cluster",
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ connectionStringTests({
title: "Secure Single Node",
createServer: createTestNode,
createUri: ({ uri }) => uri,
createQueryString: ({ certPath }) => `tlsCAFile=${certPath}`,
createQueryString: ({ certPath }) => `tlsCAFile=${certPath.root}`,
streamPrefix: "secure-single-node",
});
8 changes: 4 additions & 4 deletions src/__test__/connection/deadline/deadline-effects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe("deadline", () => {
() =>
new EventStoreDBClient(
{ endpoints: cluster.endpoints, defaultDeadline: 1 },
{ rootCertificate: cluster.rootCertificate },
{ rootCertificate: cluster.certs.root },
{ username: "admin", password: "changeit" }
).listProjections(),
],
Expand All @@ -31,7 +31,7 @@ describe("deadline", () => {
() =>
new EventStoreDBClient(
{ endpoints: cluster.endpoints },
{ rootCertificate: cluster.rootCertificate },
{ rootCertificate: cluster.certs.root },
{ username: "admin", password: "changeit" }
).listProjections({
deadline: 1,
Expand All @@ -42,7 +42,7 @@ describe("deadline", () => {
() =>
new EventStoreDBClient(
{ endpoints: cluster.endpoints, defaultDeadline: 200_000 },
{ rootCertificate: cluster.rootCertificate },
{ rootCertificate: cluster.certs.root },
{ username: "admin", password: "changeit" }
).listProjections({
deadline: 1,
Expand All @@ -53,7 +53,7 @@ describe("deadline", () => {
() =>
new EventStoreDBClient(
{ endpoints: cluster.endpoints, defaultDeadline: 200_000 },
{ rootCertificate: cluster.rootCertificate },
{ rootCertificate: cluster.certs.root },
{ username: "admin", password: "changeit" }
).appendToStream("deadline", jsonTestEvents(), {
deadline: 1,
Expand Down
4 changes: 2 additions & 2 deletions src/__test__/connection/defaultCredentials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe("defaultCredentials", () => {
test("bad override", async () => {
const client = new EventStoreDBClient(
{ endpoint: node.uri },
{ rootCertificate: node.rootCertificate },
{ rootCertificate: node.certs.root },
{ username: "admin", password: "changeit" }
);
await expect(
Expand All @@ -35,7 +35,7 @@ describe("defaultCredentials", () => {
test("good override", async () => {
const client = new EventStoreDBClient(
{ endpoint: node.uri },
{ rootCertificate: node.rootCertificate },
{ rootCertificate: node.certs.root },
{ username: "AzureDiamond", password: "hunter2" }
);
await expect(
Expand Down
4 changes: 2 additions & 2 deletions src/__test__/connection/not-leader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe("not-leader", () => {
endpoints: cluster.endpoints,
nodePreference: FOLLOWER,
},
{ rootCertificate: cluster.rootCertificate },
{ rootCertificate: cluster.certs.root },
{ username: "admin", password: "changeit" }
);

Expand Down Expand Up @@ -66,7 +66,7 @@ describe("not-leader", () => {
{
endpoint: error.leader,
},
{ rootCertificate: cluster.rootCertificate },
{ rootCertificate: cluster.certs.root },
{ username: "admin", password: "changeit" }
);

Expand Down
14 changes: 14 additions & 0 deletions src/__test__/connection/parseConnectionStringMockups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,20 @@ export const valid: Array<
],
},
],
[
"esdb://host?certFile=/home/user/dev/cert.ca&certKeyFile=/home/user/dev/cert.key",
{
dnsDiscover: false,
certFile: "/home/user/dev/cert.ca",
certKeyFile: "/home/user/dev/cert.key",
hosts: [
{
address: "host",
port: 2113,
},
],
},
],
];

export const invalid: string[] = [
Expand Down
2 changes: 1 addition & 1 deletion src/__test__/connection/reconnect/NotLeaderError.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe("reconnect", () => {
// so it's better not to have deadlines here to force the errors we are testing.
defaultDeadline: Infinity,
},
{ rootCertificate: cluster.rootCertificate },
{ rootCertificate: cluster.certs.root },
{ username: "admin", password: "changeit" }
);

Expand Down
2 changes: 1 addition & 1 deletion src/__test__/connection/reconnect/UnavailableError.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe("reconnect", () => {
// so it's better not to have deadlines here to force the errors we are testing.
defaultDeadline: Infinity,
},
{ rootCertificate: cluster.rootCertificate },
{ rootCertificate: cluster.certs.root },
{ username: "admin", password: "changeit" }
);

Expand Down
2 changes: 1 addition & 1 deletion src/__test__/connection/reconnect/all-nodes-down.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe("reconnect", () => {
// so it's better not to have deadlines here to force the errors we are testing.
defaultDeadline: Infinity,
},
{ rootCertificate: cluster.rootCertificate },
{ rootCertificate: cluster.certs.root },
{ username: "admin", password: "changeit" }
);
});
Expand Down
Loading

0 comments on commit 5ca51e8

Please sign in to comment.