Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for EXTERNAL auth #213

Merged
merged 9 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,18 @@ jobs:
services:
rabbitmq:
image: rabbitmq:3.13-rc-management
options: --hostname test-node
options: --hostname test-node --name test-node
env:
RABBITMQ_DEFAULT_USER: "test-user"
RABBITMQ_DEFAULT_PASS: "test-password"
volumes:
# these directories will be empty until checkout, but they will be
# populated by the time we restart the service
- ${{ github.workspace }}/conf:/etc/rabbitmq
- ${{ github.workspace }}/certs:/certs
ports:
- 5552:5552
- 5551:5551
- 5672:5672
- 15672:15672
- 1883:1883
Expand All @@ -41,17 +47,32 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- name: Enable RabbitMQ Plugins
run: docker exec $(docker ps --filter ancestor=rabbitmq:3.13-rc-management -q) rabbitmq-plugins enable rabbitmq_stream rabbitmq_stream_management
- name: Generate certificates
env:
CN: test-node
run: |
git clone https://github.com/rabbitmq/tls-gen tls-gen
cd tls-gen/basic
make
cd ../..
cp -a tls-gen/basic/result certs/
sudo chown -R 999:999 certs
sudo mv certs/server_test-node_certificate.pem certs/server_rabbitmq_certificate.pem
sudo mv certs/server_test-node_key.pem certs/server_rabbitmq_key.pem
- name: Restart RabbitMQ
run: docker restart $(docker ps --filter ancestor=rabbitmq:3.13-rc-management -q)
- name: Wait for rabbit instance restart
run: sleep 10
run: |
docker restart test-node
sleep 2
docker exec test-node rabbitmqctl await_startup
- name: Create SuperStream
run: docker exec $(docker ps --filter ancestor=rabbitmq:3.13-rc-management -q) rabbitmq-streams add_super_stream super-stream-test --partitions 2
run: docker exec test-node rabbitmq-streams add_super_stream super-stream-test --partitions 2
- run: npm ci
- run: npm run check
- run: npm run build --if-present
- run: |
docker exec test-node rabbitmqctl add_user 'O=client,CN=test-node' ''
docker exec test-node rabbitmqctl clear_password 'O=client,CN=test-node'
docker exec test-node rabbitmqctl set_permissions 'O=client,CN=test-node' '.*' '.*' '.*'
- run: npm test
env:
RABBITMQ_USER: "test-user"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ dist/
node_modules/
performance_test/node_modules
.envrc
tls-gen/
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,16 @@ rabbitmq-cluster:
cd cluster; docker build -t haproxy-rabbitmq-cluster .
cd cluster; chmod 755 -R tls-gen
cd cluster; docker compose down
cd cluster; docker compose up -d
cd cluster; docker compose up -d

rabbitmq-test:
rm -rf tls-gen;
git clone https://github.com/rabbitmq/tls-gen tls-gen; cd tls-gen/basic; CN=rabbitmq make
chmod 755 -R tls-gen
docker compose down
docker compose up -d
sleep 5
docker exec rabbitmq-stream rabbitmqctl await_startup
docker exec rabbitmq-stream rabbitmqctl add_user 'O=client,CN=rabbitmq' ''
docker exec rabbitmq-stream rabbitmqctl clear_password 'O=client,CN=rabbitmq'
docker exec rabbitmq-stream rabbitmqctl set_permissions 'O=client,CN=rabbitmq' '.*' '.*' '.*'
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ npm i
run the docker-compose to launch a rabbit instance already stream enabled

```shell
docker-compose up -d
make rabbitmq-test
```

add this line to your host file (on linux `/etc/hosts`) to correctly resolve rabbitmq
Expand All @@ -400,8 +400,8 @@ npm run build
Test:

```shell
docker-compose up -d
npm run test
make rabbitmq-test
RABBIT_MQ_TEST_NODES=rabbitmq:5552 npm run test
```

Check everything:
Expand Down
2 changes: 1 addition & 1 deletion conf/enabled_plugins
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[rabbitmq_management,rabbitmq_prometheus,rabbitmq_stream_management].
[rabbitmq_management,rabbitmq_prometheus,rabbitmq_stream_management,rabbitmq_auth_mechanism_ssl].
15 changes: 15 additions & 0 deletions conf/rabbitmq.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
loopback_users.guest = false

ssl_options.cacertfile = /certs/ca_certificate.pem
ssl_options.certfile = /certs/server_rabbitmq_certificate.pem
ssl_options.keyfile = /certs/server_rabbitmq_key.pem
listeners.ssl.default = 5671
listeners.tcp.default = 5672
stream.listeners.tcp.default = 5552
stream.listeners.ssl.default = 5551
auth_mechanisms.1 = PLAIN
auth_mechanisms.2 = EXTERNAL
ssl_options.verify = verify_peer
ssl_options.fail_if_no_peer_cert = false
log.file.level = debug
log.console = true
7 changes: 4 additions & 3 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: "2"

services:
rabbitmq-stream:
image: rabbitmq:3.13-rc-management
Expand All @@ -8,10 +6,13 @@ services:
hostname: "rabbitmq"
ports:
- "15672:15672"
- "5671:5671"
- "5672:5672"
- "5551:5551"
- "5552:5552"
environment:
RABBITMQ_DEFAULT_USER: "rabbit"
RABBITMQ_DEFAULT_PASS: "rabbit"
volumes:
- ./conf/enabled_plugins:/etc/rabbitmq/enabled_plugins
- ./conf/:/etc/rabbitmq/
- "./tls-gen/basic/result/:/certs"
1 change: 1 addition & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,7 @@ export interface ClientParams {
port: number
username: string
password: string
mechanism?: "PLAIN" | "EXTERNAL"
vhost: string
frameMax?: number
heartbeat?: number
Expand Down
18 changes: 10 additions & 8 deletions src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,11 @@ export class Connection {
this.logger.info(`Connected to RabbitMQ ${this.params.hostname}:${this.params.port}`)
this.peerProperties = (await this.exchangeProperties()).properties
this.filteringEnabled = lt(coerce(this.rabbitManagementVersion)!, REQUIRED_MANAGEMENT_VERSION) ? false : true
await this.auth({ username: this.params.username, password: this.params.password })
await this.auth({
username: this.params.username,
password: this.params.password,
mechanism: this.params.mechanism ?? "PLAIN",
})
const { heartbeat } = await this.tune(this.params.heartbeat ?? 0)
await this.open({ virtualHost: this.params.vhost })
if (!this.heartbeat.started) this.heartbeat.start(heartbeat)
Expand Down Expand Up @@ -436,19 +440,17 @@ export class Connection {
return this.setupCompleted
}

private async auth(params: { username: string; password: string }) {
private async auth(params: { username: string; password: string; mechanism: string }) {
this.logger.debug(`Start authentication process ...`)
this.logger.debug(`Start SASL handshake ...`)
const handshakeResponse = await this.sendAndWait<SaslHandshakeResponse>(new SaslHandshakeRequest())
this.logger.debug(`Mechanisms: ${handshakeResponse.mechanisms}`)
if (!handshakeResponse.mechanisms.find((m) => m === "PLAIN")) {
throw new Error(`Unable to find PLAIN mechanism in ${handshakeResponse.mechanisms}`)
if (!handshakeResponse.mechanisms.find((m) => m === params.mechanism)) {
throw new Error(`Unable to find ${params.mechanism} mechanism in ${handshakeResponse.mechanisms}`)
}

this.logger.debug(`Start SASL PLAIN authentication ...`)
const authResponse = await this.sendAndWait<SaslAuthenticateResponse>(
new SaslAuthenticateRequest({ ...params, mechanism: "PLAIN" })
)
this.logger.debug(`Start SASL ${params.mechanism} authentication ...`)
const authResponse = await this.sendAndWait<SaslAuthenticateResponse>(new SaslAuthenticateRequest(params))
this.logger.debug(`Authentication: ${authResponse.ok} - '${authResponse.data}'`)
if (!authResponse.ok) {
throw new Error(`Unable Authenticate -> ${authResponse.code}`)
Expand Down
23 changes: 18 additions & 5 deletions src/requests/sasl_authenticate_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { SaslAuthenticateResponse } from "../responses/sasl_authenticate_respons
import { AbstractRequest } from "./abstract_request"
import { DataWriter } from "./data_writer"

function assertUnreachable(mechanism: string): never {
throw new Error(`Auth mechanism '${mechanism}' not implemented`)
}

export class SaslAuthenticateRequest extends AbstractRequest {
readonly responseKey = SaslAuthenticateResponse.key
static readonly Key = 0x0013
Expand All @@ -14,10 +18,19 @@ export class SaslAuthenticateRequest extends AbstractRequest {

protected writeContent(writer: DataWriter): void {
writer.writeString(this.params.mechanism)
writer.writeUInt32(this.params.password.length + this.params.username.length + 2)
writer.writeUInt8(0)
writer.writeData(this.params.username)
writer.writeUInt8(0)
writer.writeData(this.params.password)
switch (this.params.mechanism) {
case "PLAIN":
writer.writeUInt32(this.params.password.length + this.params.username.length + 2)
writer.writeUInt8(0)
writer.writeData(this.params.username)
writer.writeUInt8(0)
writer.writeData(this.params.password)
break
case "EXTERNAL":
writer.writeUInt32(0)
break
default:
assertUnreachable(this.params.mechanism)
}
}
}
30 changes: 28 additions & 2 deletions test/e2e/connect.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import { expect } from "chai"
import { Client } from "../../src"
import { Client, connect } from "../../src"
import { createClient } from "../support/fake_data"
import { Rabbit } from "../support/rabbit"
import { eventually, username, password } from "../support/util"
import { eventually, username, password, getTestNodesFromEnv } from "../support/util"
import { Version } from "../../src/versions"
import { randomUUID } from "node:crypto"
import { readFile } from "node:fs/promises"

async function createTlsClient(): Promise<Client> {
const [firstNode] = getTestNodesFromEnv()
return connect({
hostname: firstNode.host,
port: 5551,
mechanism: "EXTERNAL",
ssl: {
ca: await readFile("./tls-gen/basic/result/ca_certificate.pem", "utf8"),
cert: await readFile(`./tls-gen/basic/result/client_${firstNode.host}_certificate.pem`, "utf8"),
key: await readFile(`./tls-gen/basic/result/client_${firstNode.host}_key.pem`, "utf8"),
},
username: "",
password: "",
vhost: "/",
})
}

describe("connect", () => {
let client: Client
Expand All @@ -28,6 +46,14 @@ describe("connect", () => {
}, 5000)
}).timeout(10000)

it("using EXTERNAL auth", async () => {
client = await createTlsClient()

await eventually(async () => {
expect(await rabbit.getConnections()).lengthOf(1)
}, 5000)
}).timeout(10000)

it("declaring connection name", async () => {
const connectionName = `connection-name-${randomUUID()}`
client = await createClient(username, password, undefined, undefined, undefined, undefined, connectionName)
Expand Down
Loading