Minor change in the code structure.

migration
Philippe PITTOLI 2024-07-01 11:56:50 +02:00
parent 519533c02d
commit 1e66262884
6 changed files with 295 additions and 286 deletions

View File

@ -9,6 +9,10 @@ TODO:
* (server) Resource Records to add, del and modify * (server) Resource Records to add, del and modify
* (server) Zone validity on modification * (server) Zone validity on modification
* (client) check for errors in the list of possible returned messages * (client) check for errors in the list of possible returned messages
- optimization: RAMOnly-DB for connected users
1. dnsmanagerd should be able to (un)subscribe for a user data
2. avoid requests to AuthD (don't ask for user data twice)
3. AuthD should send updates on a user in real-time
DONE: DONE:

39
src/exceptions.cr Normal file
View File

@ -0,0 +1,39 @@
module DNSManager
class Exception < ::Exception
end
class NotLoggedException < ::Exception
end
class NoOwnershipException < ::Exception
end
class AuthorizationException < ::Exception
end
class UnknownUserException < ::Exception
end
class RRReadOnlyException < ::Exception
property domain : String
property rr : DNSManager::Storage::Zone::ResourceRecord
def initialize(@domain, @rr)
end
end
class CannotCheckPermissionsException < ::Exception
property uid : UserDataID
property resource : String
def initialize(@uid, @resource)
end
end
class DomainNotFoundException < ::Exception
end
class RRNotFoundException < ::Exception
end
class TokenNotFoundException < ::Exception
end
end

View File

@ -3,235 +3,28 @@ require "option_parser"
require "ipc" require "ipc"
require "ipc/json" require "ipc/json"
require "authd" require "authd"
require "baguette-crystal-base" require "./service"
require "./config"
module DNSManager
class Exception < ::Exception
end
class DomainNotFoundException < ::Exception
end
class UnknownUserException < ::Exception
end
class RRReadOnlyException < ::Exception
property domain : String
property rr : DNSManager::Storage::Zone::ResourceRecord
def initialize(@domain, @rr)
end
end
class CannotCheckPermissionsException < ::Exception
property uid : UserDataID
property resource : String
def initialize(@uid, @resource)
end
end
class AuthorizationException < ::Exception
end
class NoOwnershipException < ::Exception
end
class NotLoggedException < ::Exception
end
class RRNotFoundException < ::Exception
end
class TokenNotFoundException < ::Exception
end
end
require "./storage.cr" require "./storage.cr"
require "./network.cr" require "./network.cr"
# First option parsing, same with all Baguette (service) applications.
simulation, no_configuration, configuration_file = Baguette::Configuration.option_parser
class DNSManager::Service < IPC # DNSManagerd configuration.
property configuration : Baguette::Configuration::DNSManager configuration = if no_configuration
getter storage : DNSManager::Storage
getter logged_users : Hash(Int32, AuthD::User::Public)
property authd : AuthD::Client
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}"
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(event : IPC::Event)
@logged_users[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
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::/, ""
response = begin
request.handle self, event
rescue e : AuthorizationException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} authorization error"
Response::Error.new "authorization error"
rescue e : DomainNotFoundException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} domain not found"
Response::DomainNotFound.new
rescue e : CannotCheckPermissionsException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} cannot check permissions of user '#{e.uid}' on resource '#{e.resource}'"
Response::InsufficientRights.new
rescue e : UnknownUserException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} unknown user"
Response::UnknownUser.new
rescue e : NoOwnershipException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} no ownership error"
Response::NoOwnership.new
rescue e : NotLoggedException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} user not logged"
Response::Error.new "user not logged"
rescue e : RRNotFoundException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} RR not found"
Response::RRNotFound.new
rescue e : TokenNotFoundException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} Token not found"
Response::Error.new "token not found"
rescue e : RRReadOnlyException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} RR is read only"
Response::RRReadOnly.new e.domain, e.rr
rescue e # Generic case
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} generic error #{e}"
Response::Error.new "generic error"
end
# 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
response_name = response.class.name.sub /^DNSManager::Response::/, ""
if response.is_a? DNSManager::Response::Error
Baguette::Log.warning "fd #{"%4d" % event.fd} (#{duration}) #{reqname} >> #{response_name} (#{response.reason})"
else
if reqname != "KeepAlive" || @configuration.print_keepalive
Baguette::Log.debug "fd #{"%4d" % event.fd} (#{duration}) #{reqname} >> #{response_name}"
end
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 @configuration.print_ipc_timer
when LibIPC::EventType::Connection
Baguette::Log.debug "New connection!" if @configuration.print_ipc_connection
when LibIPC::EventType::Disconnection
Baguette::Log.debug "Disconnection from #{event.fd}." if @configuration.print_ipc_disconnection
@logged_users.delete event.fd
when LibIPC::EventType::MessageTx
Baguette::Log.debug "Message sent to #{event.fd}." if @configuration.print_ipc_message_sent
when LibIPC::EventType::MessageRx
Baguette::Log.debug "Message received from #{event.fd}." if @configuration.print_ipc_message_received
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
def main
# First option parsing, same with all Baguette (service) applications.
simulation, no_configuration, configuration_file = Baguette::Configuration.option_parser
# DNSManagerd configuration.
configuration = if no_configuration
Baguette::Log.info "do not load a configuration file." Baguette::Log.info "do not load a configuration file."
Baguette::Configuration::DNSManager.new Baguette::Configuration::DNSManager.new
else else
# In case there is a configuration file helping with the parameters. # In case there is a configuration file helping with the parameters.
Baguette::Configuration::DNSManager.get(configuration_file) || Baguette::Configuration::DNSManager.get(configuration_file) ||
Baguette::Configuration::DNSManager.new Baguette::Configuration::DNSManager.new
end end
OptionParser.parse do |parser| OptionParser.parse do |parser|
parser.on "-v verbosity-level", "--verbosity level", "Verbosity." do |opt| parser.on "-v verbosity-level", "--verbosity level", "Verbosity." do |opt|
Baguette::Log.info "Verbosity level: #{opt}" Baguette::Log.info "Verbosity level: #{opt}"
configuration.verbosity = opt.to_i configuration.verbosity = opt.to_i
@ -262,9 +55,9 @@ def main
puts parser puts parser
exit 0 exit 0
end end
end end
unless File.directory? configuration.template_directory unless File.directory? configuration.template_directory
Baguette::Log.warning "template directory '#{configuration.template_directory}' doesn't exist" Baguette::Log.warning "template directory '#{configuration.template_directory}' doesn't exist"
if File.directory? "./templates" if File.directory? "./templates"
Baguette::Log.info "using template directory './templates'" Baguette::Log.info "using template directory './templates'"
@ -274,38 +67,35 @@ def main
Baguette::Log.error "no template directory detected, quitting" Baguette::Log.error "no template directory detected, quitting"
exit 1 exit 1
end end
end end
dir = configuration.template_directory dir = configuration.template_directory
accepted_domains = configuration.accepted_domains accepted_domains = configuration.accepted_domains
unless accepted_domains unless accepted_domains
Baguette::Log.error "Not even a single accepted domain configured. Probably an error." Baguette::Log.error "Not even a single accepted domain configured. Probably an error."
exit 1 exit 1
end end
accepted_domains.each do |domain| accepted_domains.each do |domain|
template_file = "#{dir}/#{domain}.json" template_file = "#{dir}/#{domain}.json"
zone = DNSManager::Storage::Zone.from_json File.read "#{template_file}" zone = DNSManager::Storage::Zone.from_json File.read "#{template_file}"
puts "default zone for #{domain}: #{zone}" puts "default zone for #{domain}: #{zone}"
rescue e rescue e
Baguette::Log.error "error reading template #{template_file}: #{e}" Baguette::Log.error "error reading template #{template_file}: #{e}"
exit 1 exit 1
end end
if simulation if simulation
pp! configuration pp! configuration
exit 0 exit 0
end end
unless configuration.pass unless configuration.pass
Baguette::Log.error "no pass found" Baguette::Log.error "no pass found"
Baguette::Log.error "Should be present in dnsmanager.yml or via command line arguments (-p)" Baguette::Log.error "Should be present in dnsmanager.yml or via command line arguments (-p)"
exit 1 exit 1
end
service = DNSManager::Service.new configuration
service.run
end end
main service = DNSManager::Service.new configuration
service.run

View File

@ -1,5 +1,175 @@
require "ipc" require "ipc"
# Useful to enable the client to be built. require "baguette-crystal-base"
require "./config"
require "./exceptions"
class DNSManager::Service < IPC 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 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}"
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(event : IPC::Event)
@logged_users[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
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::/, ""
response = begin
request.handle self, event
rescue e : AuthorizationException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} authorization error"
Response::Error.new "authorization error"
rescue e : DomainNotFoundException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} domain not found"
Response::DomainNotFound.new
rescue e : CannotCheckPermissionsException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} cannot check permissions of user '#{e.uid}' on resource '#{e.resource}'"
Response::InsufficientRights.new
rescue e : UnknownUserException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} unknown user"
Response::UnknownUser.new
rescue e : NoOwnershipException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} no ownership error"
Response::NoOwnership.new
rescue e : NotLoggedException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} user not logged"
Response::Error.new "user not logged"
rescue e : RRNotFoundException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} RR not found"
Response::RRNotFound.new
rescue e : TokenNotFoundException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} Token not found"
Response::Error.new "token not found"
rescue e : RRReadOnlyException
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} RR is read only"
Response::RRReadOnly.new e.domain, e.rr
rescue e # Generic case
Baguette::Log.error "(fd #{"%4d" % event.fd}) #{reqname} generic error #{e}"
Response::Error.new "generic error"
end
# 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
response_name = response.class.name.sub /^DNSManager::Response::/, ""
if response.is_a? DNSManager::Response::Error
Baguette::Log.warning "fd #{"%4d" % event.fd} (#{duration}) #{reqname} >> #{response_name} (#{response.reason})"
else
if reqname != "KeepAlive" || @configuration.print_keepalive
Baguette::Log.debug "fd #{"%4d" % event.fd} (#{duration}) #{reqname} >> #{response_name}"
end
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 @configuration.print_ipc_timer
when LibIPC::EventType::Connection
Baguette::Log.debug "New connection!" if @configuration.print_ipc_connection
when LibIPC::EventType::Disconnection
Baguette::Log.debug "Disconnection from #{event.fd}." if @configuration.print_ipc_disconnection
@logged_users.delete event.fd
when LibIPC::EventType::MessageTx
Baguette::Log.debug "Message sent to #{event.fd}." if @configuration.print_ipc_message_sent
when LibIPC::EventType::MessageRx
Baguette::Log.debug "Message received from #{event.fd}." if @configuration.print_ipc_message_received
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 end

View File

@ -71,6 +71,9 @@ class DNSManager::Storage
end end
end end
# Generate Bind9 zone files.
# The file is written in a temporary file then moved, enabling safe manipulation of the file
# since its content always will be consistent even if not up-to-date.
def generate_bind9_zonefile(domain : String) : Nil def generate_bind9_zonefile(domain : String) : Nil
zone = zone_must_exist! domain zone = zone_must_exist! domain
@ -87,7 +90,8 @@ class DNSManager::Storage
File.rename filename_wip, filename_final File.rename filename_wip, filename_final
end end
# Only an admin can access this function. # Request to generate a zone file.
# Only an admin can access this function, so there is no need to verify user's authorizations a second time.
def generate_zonefile(domain : String) : IPC::JSON def generate_zonefile(domain : String) : IPC::JSON
generate_bind9_zonefile domain generate_bind9_zonefile domain
Response::Success.new Response::Success.new
@ -102,6 +106,7 @@ class DNSManager::Storage
Response::Success.new Response::Success.new
end end
# Provides the generated zone file to a user.
def get_generated_zonefile(user_id : UserDataID, domain : String) : IPC::JSON def get_generated_zonefile(user_id : UserDataID, domain : String) : IPC::JSON
zone = zone_must_exist! domain zone = zone_must_exist! domain
user_should_own! user_id, zone.domain user_should_own! user_id, zone.domain
@ -111,8 +116,8 @@ class DNSManager::Storage
Response::GeneratedZone.new domain, (String.new io.buffer, io.pos) Response::GeneratedZone.new domain, (String.new io.buffer, io.pos)
end end
# Adds a new domain.
def new_domain(user_id : UserDataID, domain : String) : IPC::JSON def new_domain(user_id : UserDataID, domain : String) : IPC::JSON
accepted_domains = dnsmanagerd.configuration.accepted_domains.not_nil! accepted_domains = dnsmanagerd.configuration.accepted_domains.not_nil!
template_directory = dnsmanagerd.configuration.template_directory template_directory = dnsmanagerd.configuration.template_directory

View File

@ -1,5 +1,6 @@
require "authd" require "authd"
require "ipc" require "ipc"
require "../src/client.cr" require "../src/client.cr"
require "http/server" require "http/server"