Skip to content

Commit 7a0ee0c

Browse files
committed
Add support for AUTH and SELECT protocols.
1 parent 8f4cc55 commit 7a0ee0c

File tree

6 files changed

+185
-6
lines changed

6 files changed

+185
-6
lines changed

async-redis.gemspec

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ Gem::Specification.new do |spec|
2323

2424
spec.required_ruby_version = ">= 3.1"
2525

26-
spec.add_dependency "async", [">= 1.8", "< 3.0"]
26+
spec.add_dependency "async", "~> 2.10"
2727
spec.add_dependency "async-pool", "~> 0.2"
2828
spec.add_dependency "io-endpoint", "~> 0.10"
2929
spec.add_dependency "io-stream", "~> 0.4"
30-
spec.add_dependency "protocol-redis", "~> 0.8.0"
30+
spec.add_dependency "protocol-redis", "~> 0.9"
3131
end

guides/getting-started/readme.md

+33-4
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,42 @@ $ bundle add async-redis
1717
``` ruby
1818
require 'async/redis'
1919

20-
endpoint = Async::Redis.local_endpoint
21-
client = Async::Redis::Client.new(endpoint)
20+
Async do
21+
endpoint = Async::Redis.local_endpoint
22+
client = Async::Redis::Client.new(endpoint)
23+
puts client.info
24+
end
25+
```
26+
27+
### Authenticated Protocol
28+
29+
In order to authenticate, it is necessary to issue an `AUTH` command after connecting to the server. The `Async::Redis::Protocol::Authenticated` protocol class does this for you:
30+
31+
``` ruby
32+
require 'async/redis'
33+
require 'async/redis/protocol/authenticated'
2234

2335
Async do
36+
endpoint = Async::Redis.local_endpoint
37+
protocol = Async::Redis::Protocol::Authenticated.new(["username", "password"])
38+
client = Async::Redis::Client.new(endpoint, protocol: protocol)
2439
puts client.info
25-
ensure
26-
client.close
40+
end
41+
```
42+
43+
### Selected Database
44+
45+
In order to select a database, it is necessary to issue a `SELECT` command after connecting to the server. The `Async::Redis::Protocol::Selected` protocol class does this for you:
46+
47+
``` ruby
48+
require 'async/redis'
49+
require 'async/redis/protocol/selected'
50+
51+
Async do
52+
endpoint = Async::Redis.local_endpoint
53+
protocol = Async::Redis::Protocol::Selected.new(1)
54+
client = Async::Redis::Client.new(endpoint, protocol: protocol)
55+
puts client.client_info
2756
end
2857
```
2958

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2024, by Samuel Williams.
5+
6+
require 'protocol/redis'
7+
8+
module Async
9+
module Redis
10+
module Protocol
11+
# Executes AUTH after the user has established a connection.
12+
class Authenticated
13+
# Authentication has failed for some reason.
14+
class AuthenticationError < StandardError
15+
end
16+
17+
# Create a new authenticated protocol.
18+
#
19+
# @parameter credentials [Array] The credentials to use for authentication.
20+
# @parameter protocol [Object] The delegated protocol for connecting.
21+
def initialize(credentials, protocol: Async::Redis::Protocol::RESP2)
22+
@credentials = credentials
23+
@protocol = protocol
24+
end
25+
26+
# Create a new client and authenticate it.
27+
def client(stream)
28+
client = @protocol.client(stream)
29+
30+
client.write_request(["AUTH", *@credentials])
31+
response = client.read_response
32+
33+
if response != "OK"
34+
raise AuthenticationError, "Could not authenticate: #{response}"
35+
end
36+
37+
return client
38+
end
39+
end
40+
end
41+
end
42+
end

lib/async/redis/protocol/selected.rb

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2024, by Samuel Williams.
5+
6+
require 'protocol/redis'
7+
8+
module Async
9+
module Redis
10+
module Protocol
11+
# Executes AUTH after the user has established a connection.
12+
class Selected
13+
# Authentication has failed for some reason.
14+
class SelectionError < StandardError
15+
end
16+
17+
# Create a new authenticated protocol.
18+
#
19+
# @parameter index [Integer] The database index to select.
20+
# @parameter protocol [Object] The delegated protocol for connecting.
21+
def initialize(index, protocol: Async::Redis::Protocol::RESP2)
22+
@index = index
23+
@protocol = protocol
24+
end
25+
26+
# Create a new client and authenticate it.
27+
def client(stream)
28+
client = @protocol.client(stream)
29+
30+
client.write_request(["SELECT", @index])
31+
response = client.read_response
32+
33+
if response != "OK"
34+
raise SelectionError, "Could not select database: #{response}"
35+
end
36+
37+
return client
38+
end
39+
end
40+
end
41+
end
42+
end
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2024, by Samuel Williams.
5+
6+
require 'async/redis/client'
7+
require 'async/redis/protocol/authenticated'
8+
require 'sus/fixtures/async'
9+
10+
describe Async::Redis::Protocol::Authenticated do
11+
include Sus::Fixtures::Async::ReactorContext
12+
13+
let(:endpoint) {Async::Redis.local_endpoint}
14+
let(:credentials) {["testuser", "testpassword"]}
15+
let(:protocol) {subject.new(credentials)}
16+
let(:client) {Async::Redis::Client.new(endpoint, protocol: protocol)}
17+
18+
before do
19+
# Setup ACL user with limited permissions for testing.
20+
admin_client = Async::Redis::Client.new(endpoint)
21+
admin_client.call("ACL", "SETUSER", "testuser", "on", ">" + credentials[1], "+ping", "+auth")
22+
ensure
23+
admin_client.close
24+
end
25+
26+
after do
27+
# Cleanup ACL user after tests.
28+
admin_client = Async::Redis::Client.new(endpoint)
29+
admin_client.call("ACL", "DELUSER", "testuser")
30+
admin_client.close
31+
end
32+
33+
it "can authenticate and send allowed commands" do
34+
response = client.call("PING")
35+
expect(response).to be == "PONG"
36+
end
37+
38+
it "rejects commands not allowed by ACL" do
39+
expect do
40+
client.call("SET", "key", "value")
41+
end.to raise_exception(Protocol::Redis::ServerError, message: be =~ /NOPERM/)
42+
end
43+
end

test/async/redis/protocol/selected.rb

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
# frozen_string_literal: true
3+
4+
# Released under the MIT License.
5+
# Copyright, 2024, by Samuel Williams.
6+
7+
require 'async/redis/client'
8+
require 'async/redis/protocol/selected'
9+
require 'sus/fixtures/async'
10+
11+
describe Async::Redis::Protocol::Selected do
12+
include Sus::Fixtures::Async::ReactorContext
13+
14+
let(:endpoint) {Async::Redis.local_endpoint}
15+
let(:index) {1}
16+
let(:protocol) {subject.new(index)}
17+
let(:client) {Async::Redis::Client.new(endpoint, protocol: protocol)}
18+
19+
it "can select a specific database" do
20+
response = client.client_info
21+
expect(response[:db].to_i).to be == index
22+
end
23+
end

0 commit comments

Comments
 (0)