require "json" require "uuid" require "uuid/json" require "baguette-crystal-base" require "dodb" class DNSManager::Storage getter user_data : DODB::CachedDataBase(UserData) getter user_data_by_uid : DODB::Index(UserData) getter zones : DODB::CachedDataBase(Zone) getter zones_by_domain : DODB::Index(Zone) getter root : String getter zonefiledir : String def initialize(@root : String, reindex : Bool = false) @user_data = DODB::CachedDataBase(UserData).new "#{@root}/user-data" @user_data_by_uid = @user_data.new_index "uid", &.uid.to_s @zones = DODB::CachedDataBase(Zone).new "#{@root}/zones" @zones_by_domain = @zones.new_index "domain", &.domain @zonefiledir = "#{@root}/bind9-zones" # TODO: create the directory Dir.mkdir_p @zonefiledir Baguette::Log.info "storage initialized" if reindex Baguette::Log.debug "Reindexing user data..." @user_data.reindex_everything! Baguette::Log.debug "Reindexing zones..." @zones.reindex_everything! Baguette::Log.debug "Reindexed!" end end def get_user_data(uid : Int32) user_data_by_uid.get uid.to_s rescue e : DODB::MissingEntry entry = UserData.new uid entry end def get_user_data(user : ::AuthD::User::Public) get_user_data user.uid end def update_user_data(user_data : UserData) user_data_by_uid.update_or_create user_data.uid.to_s, user_data end def ensure_user_data(user_id : Int32) user_data = user_data_by_uid.get? user_id.to_s unless user_data Baguette::Log.info "New user #{user_id}" user_data = UserData.new user_id @user_data << user_data end user_data end # Only an admin can access this function. def generate_all_zonefiles() : IPC::JSON Baguette::Log.info "writing all zone files in #{@zonefiledir}/" zones.each do |zone| File.open("#{@zonefiledir}/#{zone.domain}", "w") do |file| zone.to_bind9 file end end Response::Success.new end # Only an admin can access this function. def generate_zonefile(domain : String) : IPC::JSON zone = zones_by_domain.get? domain return Response::DomainNotFound.new unless zone Baguette::Log.info "writing zone file #{@zonefiledir}/#{zone.domain}" File.open("#{@zonefiledir}/#{zone.domain}", "w") do |file| zone.to_bind9 file end Response::Success.new end def new_domain(accepted_domains : Array(String), template_directory : String, user_id : Int32, domain : String) : IPC::JSON # User must exist. user_data = user_data_by_uid.get? user_id.to_s unless user_data Baguette::Log.warning "unknown user #{user_id} tries to add domain #{domain}" return Response::UnknownUser.new end return Response::DomainAlreadyExists.new if zones_by_domain.get? domain # Verify if the domain is acceptable. matching_domains = accepted_domains.select { |adomain| domain.ends_with? adomain } unless matching_domains.size > 0 Baguette::Log.warning "trying to add an unacceptable domain: #{domain}" return Response::UnacceptableDomain.new end matching_domains.each do |md| Baguette::Log.info "Add new domain #{domain} (matching domain #{md})" end # Verify the domain name validity. return Response::InvalidDomainName.new unless Zone.is_domain_valid? domain # Fill a template zone. tld = matching_domains.pop default_zone = Zone.from_json File.read "#{template_directory}/#{tld}.json" # Replace domain by the real domain. default_zone.replace_domain domain # # Actually write data on-disk. # # Add the domain to the user's domain. user_data.domains << domain update_user_data user_data # Add the new zone in the database. zones_by_domain.update_or_create domain, default_zone Response::DomainAdded.new domain end def add_or_update_zone(user_id : Int32, zone : Zone) : IPC::JSON # Test zone validity. if errors = zone.get_errors? Baguette::Log.warning "zone #{zone.domain} update with errors: #{errors}" return Response::InvalidZone.new errors end # User must exist. user_data = user_data_by_uid.get? user_id.to_s unless user_data Baguette::Log.warning "unknown user #{user_id} tries to add -or update- zone #{zone.domain}" return Response::UnknownUser.new end # Does the zone already exist? if z = zones_by_domain.get? zone.domain # User must own the zone. unless user_data.domains.includes?(zone.domain) || user_data.admin Baguette::Log.warning "user #{user_id} doesn't own domain #{zone.domain}" return Response::NoOwnership.new end else # Add the domain to the user's domain. user_data.domains << zone.domain # Actually write data on-disk. update_user_data user_data end # Add -or replace- the zone. zones_by_domain.update_or_create zone.domain, zone Response::Success.new rescue e Baguette::Log.error "trying to add -or update- zone #{zone.domain}: #{e}" Response::Error.new "error while adding or updating the domain #{zone.domain}" end def add_rr(user_id : Int32, domain : String, rr : Zone::ResourceRecord) : IPC::JSON # User must exist. user_data = user_data_by_uid.get? user_id.to_s unless user_data Baguette::Log.warning "unknown user #{user_id} tries to add a RR in domain #{domain}" return Response::UnknownUser.new end # Zone must exist. zone = zones_by_domain.get? domain return Response::DomainNotFound.new unless zone # User must own the zone. unless user_data.domains.includes?(domain) || user_data.admin Baguette::Log.warning "user #{user_id} doesn't own domain #{domain}" return Response::NoOwnership.new end # Test RR validity. rr.get_errors.tap do |errors| unless errors.empty? Baguette::Log.warning "add RR with errors: #{errors}" return Response::InvalidRR.new errors end end zone << rr # Update the zone. zones_by_domain.update_or_create zone.domain, zone Response::RRAdded.new zone.domain, rr rescue e Baguette::Log.error "trying to add a resource record in domain #{domain}: #{e}" Response::Error.new "error while adding a resource record in domain #{domain}" end def update_rr(user_id : Int32, domain : String, rr : Zone::ResourceRecord) : IPC::JSON # User must exist. user_data = user_data_by_uid.get? user_id.to_s unless user_data Baguette::Log.warning "unknown user #{user_id} tries to update a RR in domain #{domain}" return Response::UnknownUser.new end # Zone must exist. zone = zones_by_domain.get? domain return Response::DomainNotFound.new unless zone # User must own the zone. unless user_data.domains.includes?(domain) || user_data.admin Baguette::Log.warning "user #{user_id} doesn't own domain #{domain}" return Response::NoOwnership.new end # Test RR validity. rr.get_errors.tap do |errors| unless errors.empty? Baguette::Log.warning "update RR with errors: #{errors}" return Response::InvalidRR.new errors end end stored_rrs = zone.resources.select { |x| x.rrid == rr.rrid } unless stored_rrs.size > 0 Baguette::Log.warning "modifying a RR that doesn't exist (#{rr.rrid}) in domain #{domain}" return Response::RRNotFound.new end # Verify that this resource isn't ReadOnly. stored_rrs.each do |stored_rr| return Response::RRReadOnly.new domain, rr if stored_rr.readonly end zone.resources = zone.resources.map { |x| x.rrid == rr.rrid ? rr : x } # Update the zone. zones_by_domain.update_or_create zone.domain, zone Response::RRUpdated.new domain, rr rescue e Baguette::Log.error "trying to replace a resource record in domain #{domain}: #{e}" Response::Error.new "error while replacing a resource record in domain #{domain}" end def delete_rr(user_id : Int32, domain : String, rrid : UInt32) : IPC::JSON # User must exist. user_data = user_data_by_uid.get? user_id.to_s unless user_data Baguette::Log.warning "unknown user #{user_id} tries to delete a RR in domain #{domain}" return Response::UnknownUser.new end # Zone must exist. zone = zones_by_domain.get? domain return Response::DomainNotFound.new unless zone # User must own the zone. unless user_data.domains.includes?(domain) || user_data.admin Baguette::Log.warning "user #{user_id} doesn't own domain #{domain}" return Response::NoOwnership.new end zone.resources.select! { |x| x.rrid != rrid } # Update the zone. zones_by_domain.update_or_create zone.domain, zone Response::RRDeleted.new rrid rescue e Baguette::Log.error "trying to remove a resource record in domain #{domain}: #{e}" Response::Error.new "error while removing a resource record in domain #{domain}" end def delete_domain(user_id : Int32, domain : String) : IPC::JSON # User must exist. user_data = user_data_by_uid.get? user_id.to_s unless user_data Baguette::Log.warning "unknown user #{user_id} tries to delete domain #{domain}" return Response::UnknownUser.new end # User must own the domain. unless user_data.domains.includes?(domain) || user_data.admin Baguette::Log.warning "user #{user_id} tries to delete domain #{domain} but doesn't own it" return Response::NoOwnership.new end # Remove this domain from the list of user's domains. user_data.domains.delete domain # Update on-disk user data. update_user_data user_data # Remove the related zone. zones_by_domain.delete domain Response::DomainDeleted.new domain rescue e Baguette::Log.error "trying to delete a domain #{domain}: #{e}" Response::Error.new "error while deleting the domain #{domain}" end def get_zone(user_id : Int32, domain : String) : IPC::JSON # User must exist. user_data = user_data_by_uid.get? user_id.to_s unless user_data Baguette::Log.warning "unknown user #{user_id} tries to get zone #{domain}" return Response::UnknownUser.new end # User must own the domain. unless user_data.domains.includes?(domain) || user_data.admin Baguette::Log.warning "user #{user_id} tries to get zone #{domain} but doesn't own it" return Response::NoOwnership.new end zone = zones_by_domain.get? domain unless zone return Response::UnknownZone.new end Response::Zone.new zone rescue e Baguette::Log.error "trying to get a zone #{domain}: #{e}" Response::Error.new "error while accessing a zone #{domain}" end def user_domains(user_id : Int32) : IPC::JSON # User must exist. user_data = user_data_by_uid.get? user_id.to_s unless user_data Baguette::Log.warning "unknown user #{user_id} tries to list its domains" return Response::UnknownUser.new end Response::DomainList.new user_data.domains rescue e Baguette::Log.error "trying to list all user #{user_id} domains: #{e}" Response::Error.new "error while listing domains" end end require "./storage/*"