Skip to content

Commit

Permalink
Add support for EXTERNAL auth (#213)
Browse files Browse the repository at this point in the history
* feat: add support for EXTERNAL auth

Co-authored-by: GPad <642430+gpad@users.noreply.github.com>

* refactor: throw from a function

---------

Co-authored-by: GPad <642430+gpad@users.noreply.github.com>
  • Loading branch information
david-mohr and gpad authored Nov 7, 2024
1 parent 701e1d6 commit d8083e5
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 30 deletions.
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

0 comments on commit d8083e5

Please sign in to comment.