dnsmanager/src/service.cr

216 lines
6.9 KiB
Crystal

require "ipc"
require "baguette-crystal-base"
require "./config"
require "./exceptions"
class Array(T)
def contains?(value : T)
(self.select { |x| x == value }).size > 0
end
end
module DNSManager
# Select messages to mask in the logs (when everything goes well, of course).
# No need to flood the logs with keepalive messages or periodic token use, for instance.
enum MESSAGE
KEEPALIVE
USETOKEN
end
end
alias DNSMESSAGE = DNSManager::MESSAGE
class DNSManager::Service < IPC
property configuration : Baguette::Configuration::DNSManager
getter storage : DNSManager::Storage
getter logged_users : Hash(Int32, AuthD::User::Public)
property authd : AuthD::Client
def should_display?(value : DNSMESSAGE)
(@configuration.messages_to_mask.select { |x| x == value }).size == 0
end
def should_display?(value : IPCMESSAGE)
@configuration.ipc_messages_to_show.contains? value
end
def initialize(@configuration)
super()
@storage = DNSManager::Storage.new @configuration.storage_directory, @configuration.recreate_indexes
@logged_users = Hash(Int32, AuthD::User::Public).new
# TODO: auth service isn't in the FDs pool.
# If the service crashes, dnsmanagerd won't know it.
@authd = AuthD::Client.new
response = authd.login? @configuration.login, @configuration.pass.not_nil!
case response
when AuthD::Response::Login
uid = response.uid
token = response.token
Baguette::Log.info "Authenticated as #{@configuration.login} #{uid}, token: #{token[0..15]}..."
else
@authd.close
raise "Cannot authenticate to authd with login #{@configuration.login}: #{response}."
end
@storage.dnsmanagerd = self
self.timer @configuration.ipc_timer
self.service_init @configuration.service_name
end
def get_logged_user(fd : Int32) : AuthD::User::Public?
@logged_users[fd]?
end
def get_logged_user(event : IPC::Event) : AuthD::User::Public?
get_logged_user event.fd
end
def decode_token(token : String)
@authd.decode_token token
end
def is_admin?(uid : UInt32) : Bool
perms = check_permissions uid, "*"
(perms == AuthD::User::PermissionLevel::Admin)
end
def check_permissions(uid : UInt32, resource : String) : AuthD::User::PermissionLevel
response = @authd.check_permission uid, "dnsmanager", resource
case response
when AuthD::Response::PermissionCheck
return response.permission
end
raise CannotCheckPermissionsException.new uid, resource
rescue e
Baguette::Log.error "error while checking permissions: #{e}"
raise CannotCheckPermissionsException.new uid, resource
end
def assert_permissions!(uid : UInt32, resource : String, perms : AuthD::User::PermissionLevel)
if check_permissions(uid, resource) < perms
raise AuthorizationException.new
end
end
# `log_user_info` provides a string composed from either the user
# id in case the user was authenticated or the file descriptor of
# the connection.
def log_user_info(fd : Int32) : String
if user = get_logged_user fd
"userid #{user.uid}"
else
"fd #{"%4d" % fd}"
end
end
def handle_request(event : IPC::Event)
request_start = Time.utc
array = event.message.not_nil!
slice = Slice.new array.to_unsafe, array.size
message = IPCMessage::TypedMessage.deserialize slice
request = DNSManager.requests.parse_ipc_json message.not_nil!
if request.nil?
raise "unknown request type"
end
reqname = request.class.name.sub /^DNSManager::Request::/, ""
connection_info_str = log_user_info event.fd
response = begin
request.handle self, event
rescue e : AuthorizationException
Baguette::Log.error "(#{connection_info_str}) #{request} authorization error"
Response::Error.new "authorization error"
rescue e : DomainNotFoundException
Baguette::Log.error "(#{connection_info_str}) #{request} domain not found"
Response::DomainNotFound.new
rescue e : CannotCheckPermissionsException
Baguette::Log.error "(#{connection_info_str}) #{request} cannot check permissions of user '#{e.uid}' on resource '#{e.resource}'"
Response::InsufficientRights.new
rescue e : UnknownUserException
Baguette::Log.error "(#{connection_info_str}) #{request} unknown user"
Response::UnknownUser.new
rescue e : NoOwnershipException
Baguette::Log.error "(#{connection_info_str}) #{request} no ownership error"
Response::NoOwnership.new
rescue e : NotLoggedException
Baguette::Log.error "(#{connection_info_str}) #{request} user not logged"
Response::Error.new "user not logged"
rescue e : RRNotFoundException
Baguette::Log.error "(#{connection_info_str}) #{request} RR not found"
Response::RRNotFound.new
rescue e : TokenNotFoundException
Baguette::Log.error "(#{connection_info_str}) #{request} Token not found"
Response::Error.new "token not found"
rescue e : RRReadOnlyException
Baguette::Log.error "(#{connection_info_str}) #{request} RR is read only"
Response::RRReadOnly.new e.domain, e.rr
rescue e # Generic case
Baguette::Log.error "(#{connection_info_str}) #{request} generic error #{e}"
Response::Error.new "generic error"
end
respname = response.class.name.sub /^DNSManager::Response::/, ""
# If clients sent requests with an “id” field, it is copied
# in the responses. Allows identifying responses easily.
response.id = request.id
schedule event.fd, response
duration = Time.utc - request_start
if response.is_a? DNSManager::Response::Error
Baguette::Log.warning "(#{connection_info_str}) (#{duration}) #{request} >> #{response}"
else
# Different cases where we want to simply avoid logging redundant messages.
return if reqname == "KeepAlive" && ! should_display? DNSMESSAGE::KEEPALIVE
return if reqname == "UseToken" && respname == "Success" && ! should_display? DNSMESSAGE::USETOKEN
Baguette::Log.debug "(#{connection_info_str}) (#{duration}) #{request} >> #{response}"
end
end
def run
Baguette::Log.title "Starting #{@configuration.service_name}"
self.loop do |event|
begin
case event.type
when LibIPC::EventType::Timer
Baguette::Log.debug "Timer." if should_display? IPCMESSAGE::TIMER
when LibIPC::EventType::Connection
Baguette::Log.debug "New connection!" if should_display? IPCMESSAGE::CONNECTION
when LibIPC::EventType::Disconnection
Baguette::Log.debug "Disconnection from #{event.fd}." if should_display? IPCMESSAGE::DISCONNECTION
@logged_users.delete event.fd
when LibIPC::EventType::MessageTx
Baguette::Log.debug "Message sent to #{event.fd}." if should_display? IPCMESSAGE::TX
when LibIPC::EventType::MessageRx
Baguette::Log.debug "Message received from #{event.fd}." if should_display? IPCMESSAGE::RX
handle_request event
else
Baguette::Log.warning "Unhandled IPC event: #{event.class}."
if event.responds_to?(:fd)
fd = event.fd
Baguette::Log.warning "closing #{fd}"
close fd
@logged_users.delete fd
end
end
rescue exception
Baguette::Log.error "exception: #{typeof(exception)} - #{exception.message}"
end
end
end
end