Skip to content

Commit

Permalink
Abstract authenticator (#921)
Browse files Browse the repository at this point in the history
* introduce auth_chain, abstract authenticator and the basic authenticator

---------

Co-authored-by: Anders Bälter <[email protected]>
Co-authored-by: Jon Börjesson <[email protected]>
  • Loading branch information
3 people authored Feb 10, 2025
1 parent 5537a67 commit 2a21b8c
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 1 deletion.
1 change: 1 addition & 0 deletions .ameba.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Lint/NotNil:
- src/lavinmq/shovel/shovel.cr
- src/lavinmq/launcher.cr
- src/lavinmq/vhost.cr
- src/lavinmq/auth/authenticators/basic.cr
- src/lavinmqperf.cr
- spec/api/http_api_spec.cr
- spec/api/exchanges_spec.cr
Expand Down
29 changes: 29 additions & 0 deletions spec/auth_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require "./spec_helper"
require "../src/lavinmq/auth/chain"
require "../src/lavinmq/auth/authenticators/basic"

describe LavinMQ::Auth::Chain do
it "Creates a default authentication chain if not configured" do
with_amqp_server do |s|
chain = LavinMQ::Auth::Chain.create(s.@config, s.@users)
chain.@backends.should be_a Array(LavinMQ::Auth::Authenticator)
chain.@backends.size.should eq 1
end
end

it "Successfully authenticates and returns a basic user" do
with_amqp_server do |s|
chain = LavinMQ::Auth::Chain.create(s.@config, s.@users)
user = chain.authenticate("guest", "guest")
user.should_not be_nil
end
end

it "Does not authenticate when given invalid credentials" do
with_amqp_server do |s|
chain = LavinMQ::Auth::Chain.create(s.@config, s.@users)
user = chain.authenticate("guest", "invalid")
user.should be_nil
end
end
end
4 changes: 3 additions & 1 deletion src/lavinmq/amqp/connection_factory.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ require "../version"
require "../logger"
require "./client"
require "../client/connection_factory"
require "../auth/chain"

module LavinMQ
module AMQP
class ConnectionFactory < LavinMQ::ConnectionFactory
Log = LavinMQ::Log.for "amqp.connection_factory"

def initialize(@users : UserStore, @vhosts : VHostStore)
@auth_chain = LavinMQ::Auth::Chain.create(Config.instance, @users)
end

def start(socket, connection_info) : Client?
Expand Down Expand Up @@ -106,7 +108,7 @@ module LavinMQ
def authenticate(socket, remote_address, start_ok, log)
username, password = credentials(start_ok)
user = @users[username]?
return user if user && user.password && user.password.not_nil!.verify(password) &&
return user if user && @auth_chain.authenticate(username, password) &&
guest_only_loopback?(remote_address, user)

if user.nil?
Expand Down
9 changes: 9 additions & 0 deletions src/lavinmq/auth/authenticator.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module LavinMQ
module Auth
abstract class Authenticator
Log = LavinMQ::Log.for "auth.handler"

abstract def authenticate(username : String, password : String) : User?
end
end
end
18 changes: 18 additions & 0 deletions src/lavinmq/auth/authenticators/basic.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
require "../authenticator"
require "../../server"

module LavinMQ
module Auth
class BasicAuthenticator < Authenticator
def initialize(@users : UserStore)
end

def authenticate(username : String, password : String) : User?
user = @users[username]
return user if user && user.password && user.password.not_nil!.verify(password)
rescue ex : Exception
Log.error { "Basic authentication failed: #{ex.message}" }
end
end
end
end
38 changes: 38 additions & 0 deletions src/lavinmq/auth/chain.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
require "./authenticator"
require "./authenticators/basic"

module LavinMQ
module Auth
class Chain < Authenticator
@backends : Array(Authenticator)

def initialize(backends : Array(Authenticator))
@backends = backends
end

def self.create(config : Config, users : UserStore) : Chain
backends = config.auth_backends
authenticators = Array(Authenticator).new
if backends.nil? || backends.empty?
authenticators << BasicAuthenticator.new(users)
else
backends.each do |backend|
case backend
when "basic"
authenticators << BasicAuthenticator.new(users)
else
raise "Unsupported authentication backend: #{backend}"
end
end
end
self.new(authenticators)
end

def authenticate(username : String, password : String) : User?
@backends.find_value do |backend|
backend.authenticate(username, password)
end
end
end
end
end
1 change: 1 addition & 0 deletions src/lavinmq/config.cr
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ module LavinMQ
property default_consumer_prefetch = UInt16::MAX
property yield_each_received_bytes = 131_072 # max number of bytes to read from a client connection without letting other tasks in the server do any work
property yield_each_delivered_bytes = 1_048_576 # max number of bytes sent to a client without tending to other tasks in the server
property auth_backends : Array(String) = ["basic"]
@@instance : Config = self.new

def self.instance : LavinMQ::Config
Expand Down

0 comments on commit 2a21b8c

Please sign in to comment.