diff --git a/spec/auth_spec.cr b/spec/auth_spec.cr new file mode 100644 index 0000000000..25efe4334a --- /dev/null +++ b/spec/auth_spec.cr @@ -0,0 +1,62 @@ +require "./spec_helper" + +class MockAuthService < LavinMQ::AuthenticationService + property? should_succeed : Bool + property last_username : String? + property last_password : String? + + def initialize(@should_succeed = false) + end + + def authorize?(username : String, password : String) + @last_username = username + @last_password = password + + if @should_succeed + "allow" + else + try_next(username, password) + end + end +end + +describe LavinMQ::AuthenticationChain do + describe "#authorize?" do + it "returns nil when no services are configured" do + chain = LavinMQ::AuthenticationChain.new + chain.authorize?("user", "pass").should be_nil + end + + it "tries services in order until success" do + # Arrange + chain = LavinMQ::AuthenticationChain.new + service1 = MockAuthService.new(should_succeed: false) + service2 = MockAuthService.new(should_succeed: true) + service3 = MockAuthService.new(should_succeed: true) + + chain.add_service(service1) + chain.add_service(service2) + chain.add_service(service3) + + # Act + user = chain.authorize?("test_user", "test_pass") + + # Assert + user.should_not be_nil + service1.last_username.should eq("test_user") + service2.last_username.should eq("test_user") + service3.last_username.should be_nil # Ne devrait pas ĂȘtre appelĂ© + end + + it "returns nil if all services fail" do + chain = LavinMQ::AuthenticationChain.new + service1 = MockAuthService.new(should_succeed: false) + service2 = MockAuthService.new(should_succeed: false) + + chain.add_service(service1) + chain.add_service(service2) + + chain.authorize?("user", "pass").should be_nil + end + end +end diff --git a/src/lavinmq/auth/auth_chain.cr b/src/lavinmq/auth/auth_chain.cr new file mode 100644 index 0000000000..db5d4ad7c0 --- /dev/null +++ b/src/lavinmq/auth/auth_chain.cr @@ -0,0 +1,32 @@ +require "./services/auth_service" +require "./services/local_auth_service" +require "./services/http_service" + +module LavinMQ + class AuthenticationChain + @first_service : AuthenticationService? + + def initialize + @first_service = nil + end + + def add_service(service : AuthenticationService) + if first = @first_service + current = first + while next_service = current.next_service + current = next_service + end + current.then(service) + else + @first_service = service + end + self + end + + def authorize?(username : String, password : String) + if service = @first_service + service.authorize?(username, password) + end + end + end +end diff --git a/src/lavinmq/auth/services/auth_service.cr b/src/lavinmq/auth/services/auth_service.cr new file mode 100644 index 0000000000..802da72731 --- /dev/null +++ b/src/lavinmq/auth/services/auth_service.cr @@ -0,0 +1,18 @@ +module LavinMQ + abstract class AuthenticationService + property next_service : AuthenticationService? + + abstract def authorize?(username : String, password : String) + + def then(service : AuthenticationService) : AuthenticationService + @next_service = service + service + end + + protected def try_next(username : String, password : String) + if next_service = @next_service + next_service.authorize?(username, password) + end + end + end +end diff --git a/src/lavinmq/auth/services/http_service.cr b/src/lavinmq/auth/services/http_service.cr new file mode 100644 index 0000000000..a1e9ad7323 --- /dev/null +++ b/src/lavinmq/auth/services/http_service.cr @@ -0,0 +1,28 @@ +require "http/client" +require "json" +require "./auth_service" + +module LavinMQ + class HttpAuthService < AuthenticationService + def initialize(@method : String, @user_path : String, @whost_path : String, @resource_path : String, @topic_path : String) + end + + def authorize?(username : String, password : String) + + payload = { + "username" => username, + "password" => password + }.to_json + + success = ::HTTP::Client.post(@user_path, + headers: ::HTTP::Headers{"Content-Type" => "application/json"}, + body: payload).success? + + if success + "allow" + else + try_next(username, password) + end + end + end +end diff --git a/src/lavinmq/auth/services/local_auth_service.cr b/src/lavinmq/auth/services/local_auth_service.cr new file mode 100644 index 0000000000..4fa5c910b7 --- /dev/null +++ b/src/lavinmq/auth/services/local_auth_service.cr @@ -0,0 +1,20 @@ +require "./auth_service" + +module LavinMQ + class LocalAuthService < AuthenticationService + def initialize(@users_store : UserStore) + end + + def authorize?(username : String, password : String) + if user = @users_store[username]? + if user.password && user.password.not_nil!.verify(password) + "allow" + else + try_next(username, password) + end + else + try_next(username, password) + end + end + end +end diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index 832bd7a4f5..912110e2a7 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -10,6 +10,8 @@ require "./exchange" require "./amqp/queue" require "./parameter" require "./config" +require "./cache" +require "./auth/auth_chain" require "./connection_info" require "./proxy_protocol" require "./client/client"