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

Configurable default user & password #919

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion extras/lavinmq.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[main]
data_dir = /var/lib/lavinmq
guest_only_loopback = true
default_user_only_loopback = true
log_level = info
;log_file = /var/log/lavinmq.log
;socket_buffer_size = 16384
Expand Down
24 changes: 24 additions & 0 deletions spec/users_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,30 @@ describe LavinMQ::Server do
end
end
end

it "allows changing default user" do
LavinMQ::Config.instance.default_user = "spec"
LavinMQ::Config.instance.default_password = LavinMQ::User.hash_password("spec", "SHA256").to_s
with_amqp_server do |s|
with_channel(s, user: "spec", password: "spec") { }
end
ensure
LavinMQ::Config.instance.default_user = "guest"
LavinMQ::Config.instance.default_password = LavinMQ::User.hash_password("guest", "SHA256").to_s
end

it "disallows 'guest' if default user is changed" do
LavinMQ::Config.instance.default_user = "spec"
LavinMQ::Config.instance.default_password = LavinMQ::User.hash_password("spec", "SHA256").to_s
with_amqp_server do |s|
expect_raises(AMQP::Client::Connection::ClosedException) do
with_channel(s, user: "guest", password: "guest") { }
end
end
ensure
LavinMQ::Config.instance.default_user = "guest"
LavinMQ::Config.instance.default_password = LavinMQ::User.hash_password("guest", "SHA256").to_s
end
end

describe LavinMQ::Tag do
Expand Down
8 changes: 4 additions & 4 deletions src/lavinmq/amqp/connection_factory.cr
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ module LavinMQ
username, password = credentials(start_ok)
user = users[username]?
return user if user && user.password && user.password.not_nil!.verify(password) &&
guest_only_loopback?(remote_address, user)
default_user_only_loopback?(remote_address, user)

if user.nil?
log.warn { "User \"#{username}\" not found" }
Expand Down Expand Up @@ -182,9 +182,9 @@ module LavinMQ
nil
end

private def guest_only_loopback?(remote_address, user) : Bool
return true unless user.name == "guest"
return true unless Config.instance.guest_only_loopback?
private def default_user_only_loopback?(remote_address, user) : Bool
return true unless user.name == Config.instance.default_user
return true unless Config.instance.default_user_only_loopback?
remote_address.loopback?
end
end
Expand Down
67 changes: 39 additions & 28 deletions src/lavinmq/config.cr
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ require "./in_memory_backend"

module LavinMQ
class Config
DEFAULT_LOG_LEVEL = ::Log::Severity::Info
DEFAULT_LOG_LEVEL = ::Log::Severity::Info
DEFAULT_PASSWORD_HASH = "+pHuxkR9fCyrrwXjOD4BP4XbzO3l8LJr8YkThMgJ0yVHFRE+" # Hash of 'guest'

property data_dir : String = ENV.fetch("STATE_DIRECTORY", "/var/lib/lavinmq")
property config_file = File.exists?(File.join(ENV.fetch("CONFIGURATION_DIRECTORY", "/etc/lavinmq"), "lavinmq.ini")) ? File.join(ENV.fetch("CONFIGURATION_DIRECTORY", "/etc/lavinmq"), "lavinmq.ini") : ""
Expand Down Expand Up @@ -44,7 +45,7 @@ module LavinMQ
property tcp_keepalive : Tuple(Int32, Int32, Int32)? = {60, 10, 3} # idle, interval, probes/count
property tcp_recv_buffer_size : Int32? = nil
property tcp_send_buffer_size : Int32? = nil
property? guest_only_loopback : Bool = true
property? default_user_only_loopback : Bool = true
property max_message_size = 128 * 1024**2
property? log_exchange : Bool = false
property free_disk_min : Int64 = 0 # bytes
Expand All @@ -62,6 +63,8 @@ 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 default_user : String = ENV.fetch("LAVINMQ_DEFAULT_USER", "guest")
property default_password : String = ENV.fetch("LAVINMQ_DEFAULT_PASSWORD", DEFAULT_PASSWORD_HASH) # Hashed password for default user
@@instance : Config = self.new

def self.instance : LavinMQ::Config
Expand Down Expand Up @@ -118,8 +121,8 @@ module LavinMQ
p.on("-h", "--help", "Show this help") { puts p; exit 0 }
p.on("-v", "--version", "Show version") { puts LavinMQ::VERSION; exit 0 }
p.on("--build-info", "Show build information") { puts LavinMQ::BUILD_INFO; exit 0 }
p.on("--guest-only-loopback=BOOL", "Limit guest user to only connect from loopback address") do |v|
@guest_only_loopback = {"true", "yes", "y", "1"}.includes? v.to_s
p.on("--default-user-only-loopback=BOOL", "Limit default user to only connect from loopback address") do |v|
@default_user_only_loopback = {"true", "yes", "y", "1"}.includes? v.to_s
end
p.on("--clustering", "Enable clustering") do
@clustering = true
Expand All @@ -145,6 +148,12 @@ module LavinMQ
p.on("--default-consumer-prefetch=NUMBER", "Default consumer prefetch (default 65535)") do |v|
@default_consumer_prefetch = v.to_u16
end
p.on("--default-user=USER", "Default user (default: guest)") do |v|
@default_user = v
end
p.on("--default-password=PASSWORD", "Hashed password for default user (default: '+pHuxkR9fCyrrwXjOD4BP4XbzO3l8LJr8YkThMgJ0yVHFRE+' (guest))") do |v|
@default_password = v
end
p.invalid_option { |arg| abort "Invalid argument: #{arg}" }
end
parser.parse(ARGV.dup) # only parse args to get config_file
Expand Down Expand Up @@ -210,30 +219,32 @@ module LavinMQ
private def parse_main(settings)
settings.each do |config, v|
case config
when "data_dir" then @data_dir = v
when "data_dir_lock" then @data_dir_lock = true?(v)
when "log_level" then @log_level = ::Log::Severity.parse(v)
when "log_file" then @log_file = v
when "stats_interval" then @stats_interval = v.to_i32
when "stats_log_size" then @stats_log_size = v.to_i32
when "segment_size" then @segment_size = v.to_i32
when "set_timestamp" then @set_timestamp = true?(v)
when "socket_buffer_size" then @socket_buffer_size = v.to_i32
when "tcp_nodelay" then @tcp_nodelay = true?(v)
when "tcp_keepalive" then @tcp_keepalive = tcp_keepalive?(v)
when "tcp_recv_buffer_size" then @tcp_recv_buffer_size = v.to_i32?
when "tcp_send_buffer_size" then @tcp_send_buffer_size = v.to_i32?
when "tls_cert" then @tls_cert_path = v
when "tls_key" then @tls_key_path = v
when "tls_ciphers" then @tls_ciphers = v
when "tls_min_version" then @tls_min_version = v
when "guest_only_loopback" then @guest_only_loopback = true?(v)
when "log_exchange" then @log_exchange = true?(v)
when "free_disk_min" then @free_disk_min = v.to_i64
when "free_disk_warn" then @free_disk_warn = v.to_i64
when "max_deleted_definitions" then @max_deleted_definitions = v.to_i
when "consumer_timeout" then @consumer_timeout = v.to_u64
when "default_consumer_prefetch" then @default_consumer_prefetch = v.to_u16
when "data_dir" then @data_dir = v
when "data_dir_lock" then @data_dir_lock = true?(v)
when "log_level" then @log_level = ::Log::Severity.parse(v)
when "log_file" then @log_file = v
when "stats_interval" then @stats_interval = v.to_i32
when "stats_log_size" then @stats_log_size = v.to_i32
when "segment_size" then @segment_size = v.to_i32
when "set_timestamp" then @set_timestamp = true?(v)
when "socket_buffer_size" then @socket_buffer_size = v.to_i32
when "tcp_nodelay" then @tcp_nodelay = true?(v)
when "tcp_keepalive" then @tcp_keepalive = tcp_keepalive?(v)
when "tcp_recv_buffer_size" then @tcp_recv_buffer_size = v.to_i32?
when "tcp_send_buffer_size" then @tcp_send_buffer_size = v.to_i32?
when "tls_cert" then @tls_cert_path = v
when "tls_key" then @tls_key_path = v
when "tls_ciphers" then @tls_ciphers = v
when "tls_min_version" then @tls_min_version = v
when "default_user_only_loopback" then @default_user_only_loopback = true?(v)
when "log_exchange" then @log_exchange = true?(v)
when "free_disk_min" then @free_disk_min = v.to_i64
when "free_disk_warn" then @free_disk_warn = v.to_i64
when "max_deleted_definitions" then @max_deleted_definitions = v.to_i
when "consumer_timeout" then @consumer_timeout = v.to_u64
when "default_consumer_prefetch" then @default_consumer_prefetch = v.to_u16
when "default_user" then @default_user = v
when "default_password" then @default_password = v
else
STDERR.puts "WARNING: Unrecognized configuration 'main/#{config}'"
end
Expand Down
7 changes: 7 additions & 0 deletions src/lavinmq/http/controller/users.cr
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ module LavinMQ
end
context
end

put "/api/auth/hash_password/:password" do |context, params|
password = params["password"]
hash = User.hash_password(password, "SHA256")
{password_hash: hash.to_s}.to_json(context.response)
context
end
end
end
end
Expand Down
8 changes: 4 additions & 4 deletions src/lavinmq/http/handler/auth_handler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ module LavinMQ
if user = @server.users[username]?
if user_password = user.password
if user_password.verify(password)
if guest_only_loopback?(remote_address, username)
if default_user_only_loopback?(remote_address, username)
return false if user.tags.empty?
return true
end
Expand All @@ -80,9 +80,9 @@ module LavinMQ
context.response.status_code = 401
end

private def guest_only_loopback?(remote_address, username) : Bool
return true unless Config.instance.guest_only_loopback?
return true unless username == "guest"
private def default_user_only_loopback?(remote_address, username) : Bool
return true unless Config.instance.default_user_only_loopback?
return true unless username == Config.instance.default_user
case remote_address
when Socket::IPAddress then remote_address.loopback?
when Socket::UNIXAddress then true
Expand Down
11 changes: 7 additions & 4 deletions src/lavinmq/user_store.cr
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,19 @@ module LavinMQ
@replicator.register_file f
end
else
tags = [Tag::Administrator]
Log.debug { "Loading default users" }
create("guest", "guest", tags, save: false)
add_permission("guest", "/", /.*/, /.*/, /.*/)
save!
create_default_user
end
create_direct_user
Log.debug { "#{size} users loaded" }
end

private def create_default_user
add(Config.instance.default_user, Config.instance.default_password, "SHA256", tags: [Tag::Administrator], save: false)
add_permission(Config.instance.default_user, "/", /.*/, /.*/, /.*/)
save!
end

private def create_direct_user
@users[DIRECT_USER] = User.create_hidden_user(DIRECT_USER)
perm = {config: /.*/, read: /.*/, write: /.*/}
Expand Down
10 changes: 10 additions & 0 deletions src/lavinmqctl.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ require "./lavinmq/definitions_generator"
require "http/client"
require "json"
require "option_parser"
require "./lavinmq/user"

class LavinMQCtl
@options = {} of String => String
Expand Down Expand Up @@ -41,6 +42,8 @@ class LavinMQCtl
{"delete_exchange", "Delete exchange", "<name>"},
{"set_vhost_limits", "Set VHost limits (max-connections, max-queues)", "<json>"},
{"set_permissions", "Set permissions for a user", "<username> <configure> <write> <read>"},
{"hash_password", "Hash a password", "<password>"},

}

def initialize
Expand Down Expand Up @@ -178,6 +181,7 @@ class LavinMQCtl
when "set_vhost_limits" then set_vhost_limits
when "set_permissions" then set_permissions
when "definitions" then definitions
when "hash_password" then hash_password
when "stop_app"
when "start_app"
else
Expand Down Expand Up @@ -679,6 +683,12 @@ class LavinMQCtl
data_dir = ARGV.shift? || abort "definitions <datadir>"
DefinitionsGenerator.new(data_dir).generate(STDOUT)
end

private def hash_password
password = ARGV.shift?
abort @banner unless password
output LavinMQ::User.hash_password(password, "SHA256")
end
end

cli = LavinMQCtl.new
Expand Down
Loading