dnsmanager/src/storage.cr

489 lines
14 KiB
Crystal
Raw Normal View History

2020-12-03 17:13:40 +01:00
require "json"
require "uuid"
require "uuid/json"
require "baguette-crystal-base"
2020-12-03 17:13:40 +01:00
require "dodb"
2024-04-27 05:48:28 +02:00
alias UserDataID = UInt32
2020-12-03 17:13:40 +01:00
class DNSManager::Storage
2024-04-27 05:48:28 +02:00
getter domains : DODB::CachedDataBase(Domain)
getter domains_by_name : DODB::Index(Domain)
getter domains_by_share_key : DODB::Index(Domain)
getter domains_by_transfer_key : DODB::Index(Domain)
getter domains_by_owners : DODB::Tags(Domain)
getter zones : DODB::CachedDataBase(Zone)
getter zones_by_domain : DODB::Index(Zone)
getter tokens : DODB::CachedDataBase(Token)
getter tokens_by_uuid : DODB::Index(Token)
getter tokens_by_domain : DODB::Partition(Token)
getter root : String
getter zonefiledir : String
property dnsmanagerd : DNSManager::Service? = nil
2024-04-27 05:48:28 +02:00
def dnsmanagerd() : DNSManager::Service
@dnsmanagerd.not_nil!
rescue
raise Exception.new "dnsmanagerd not defined"
end
def initialize(@root : String, reindex : Bool = false)
2024-04-27 05:48:28 +02:00
@domains = DODB::CachedDataBase(Domain).new "#{@root}/domains"
@domains_by_name = @domains.new_index "name", &.name
@domains_by_share_key = @domains.new_nilable_index "share-key", do |d|
if k = d.share_key
k
else
DODB.no_index
end
end
@domains_by_transfer_key = @domains.new_nilable_index "transfer-key", do |d|
if k = d.transfer_key
k
else
DODB.no_index
end
end
@domains_by_owners = @domains.new_tags "owners", &.owners.map &.to_s
@zones = DODB::CachedDataBase(Zone).new "#{@root}/zones"
@zones_by_domain = @zones.new_index "domain", &.domain
@tokens = DODB::CachedDataBase(Token).new "#{@root}/tokens"
@tokens_by_uuid = @tokens.new_index "uuid", &.uuid
@tokens_by_domain = @tokens.new_partition "domain", &.domain
@zonefiledir = "#{@root}/bind9-zones"
Dir.mkdir_p @zonefiledir
Baguette::Log.info "storage initialized"
if reindex
2024-04-27 05:48:28 +02:00
Baguette::Log.debug "Reindexing domains..."
@domains.reindex_everything!
Baguette::Log.debug "Reindexing zones..."
@zones.reindex_everything!
Baguette::Log.debug "Reindexing tokens..."
@tokens.reindex_everything!
Baguette::Log.debug "Reindexed!"
end
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|
2024-03-13 03:58:46 +01:00
# TODO: safe write.
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
2024-03-14 02:43:00 +01:00
zone = zone_must_exist! domain
Baguette::Log.info "writing zone file #{@zonefiledir}/#{zone.domain}"
2024-03-13 03:58:46 +01:00
# TODO: safe write.
File.open("#{@zonefiledir}/#{zone.domain}", "w") do |file|
zone.to_bind9 file
end
Response::Success.new
end
def get_generated_zonefile(user_id : UserDataID, domain : String) : IPC::JSON
2024-02-27 07:42:10 +01:00
2024-04-27 05:48:28 +02:00
user_must_exist! user_id
2024-03-14 02:03:26 +01:00
zone = zone_must_exist! domain
2024-04-27 05:48:28 +02:00
user_should_own! user_id, zone.domain
2024-02-27 07:42:10 +01:00
io = IO::Memory.new
zone.to_bind9 io
Response::GeneratedZone.new domain, (String.new io.buffer, io.pos)
end
def new_domain(accepted_domains : Array(String),
template_directory : String,
user_id : UserDataID,
domain : String) : IPC::JSON
2024-04-27 05:48:28 +02:00
user_must_exist! user_id
2023-06-16 18:41:20 +02:00
2024-03-31 20:59:46 +02:00
# Prevent future very confusing errors.
domain = domain.downcase
return Response::DomainAlreadyExists.new if zones_by_domain.get? domain
2023-06-16 18:41:20 +02:00
# Verify if the domain is acceptable.
matching_domains = accepted_domains.select { |adomain| domain.ends_with? adomain }
2023-06-30 00:01:25 +02:00
unless matching_domains.size > 0
Baguette::Log.warning "trying to add an unacceptable domain: #{domain}"
return Response::UnacceptableDomain.new
end
2023-06-16 18:41:20 +02:00
matching_domains.each do |md|
Baguette::Log.info "Add new domain #{domain} (matching domain #{md})"
2023-06-16 18:41:20 +02:00
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.
#
2024-04-27 05:48:28 +02:00
# Add the new domain.
the_new_domain = Domain.new domain
the_new_domain.owners = [user_id]
@domains << the_new_domain
# Add the new zone in the database.
zones_by_domain.update_or_create domain, default_zone
Response::DomainAdded.new domain
2023-06-16 18:41:20 +02:00
end
2024-04-27 23:10:01 +02:00
def ask_share_token(user_id : UserDataID, domain_name : String)
user_must_exist! user_id
user_should_own! user_id, domain_name
domain = @domains_by_name.get domain_name
if domain.share_key.nil?
domain.share_key = UUID.random.to_s
@domains_by_name.update_or_create domain_name, domain
Response::DomainChanged.new domain
else
Response::Error.new "The domain already have a share key."
end
end
def ask_transfer_token(user_id : UserDataID, domain_name : String)
user_must_exist! user_id
user_should_own! user_id, domain_name
domain = @domains_by_name.get domain_name
if domain.transfer_key.nil?
domain.transfer_key = UUID.random.to_s
@domains_by_name.update_or_create domain_name, domain
Response::DomainChanged.new domain
else
Response::Error.new "The domain already have a transfer key."
end
end
# Check the domain owners.
# In case there's only the requesting user, allow him to gain full control.
def ask_unshare_domain(user_id : UserDataID, domain_name : String)
user_must_exist! user_id
user_should_own! user_id, domain_name
domain = @domains_by_name.get domain_name
if domain.owners.size == 1 && domain.owners[0] == user_id
domain.share_key = nil
@domains_by_name.update_or_create domain_name, domain
Response::DomainChanged.new domain
else
Response::Error.new "You are't the only owner."
end
end
def gain_ownership(user_id : UserDataID, uuid : String)
user_must_exist! user_id
if domain = @domains_by_share_key.get uuid
if domain.owners.includes? user_id
return Response::Error.new "You already own this domain."
end
domain.owners << user_id
@domains_by_name.update_or_create domain.name, domain
Response::DomainChanged.new domain
elsif domain = @domains_by_transfer_key.get uuid
if domain.owners.includes? user_id
return Response::Error.new "You already own this domain."
end
domain.transfer_key = nil
domain.owners = [user_id]
@domains_by_name.update_or_create domain.name, domain
Response::DomainChanged.new domain
else
Response::Error.new "There is no key with this UUID."
end
end
def add_or_update_zone(user_id : UserDataID, zone : Zone) : IPC::JSON
2024-04-27 05:48:28 +02:00
user_must_exist! user_id
2024-03-14 02:03:26 +01:00
# Test zone validity.
if errors = zone.get_errors?
Baguette::Log.warning "zone #{zone.domain} update with errors: #{errors}"
2023-05-08 17:34:50 +02:00
return Response::InvalidZone.new errors
end
# Does the zone already exist?
if z = zones_by_domain.get? zone.domain
2024-04-27 05:48:28 +02:00
user_should_own! user_id, z.domain
else
# Add the domain to the user's domain.
domains << Domain.new zone.domain
end
# Add -or replace- the zone.
zones_by_domain.update_or_create zone.domain, zone
Response::Success.new
2020-12-03 17:13:40 +01:00
end
def add_rr(user_id : UserDataID, domain : String, rr : Zone::ResourceRecord) : IPC::JSON
2024-04-27 05:48:28 +02:00
user_must_exist! user_id
2024-03-14 02:03:26 +01:00
zone = zone_must_exist! domain
2024-04-27 05:48:28 +02:00
user_should_own! user_id, domain
2023-05-08 17:34:50 +02:00
2023-05-08 19:07:20 +02:00
# Test RR validity.
rr.get_errors.tap do |errors|
unless errors.empty?
2023-05-08 19:34:57 +02:00
Baguette::Log.warning "add RR with errors: #{errors}"
return Response::InvalidRR.new errors
2023-05-08 19:07:20 +02:00
end
end
2023-05-08 17:34:50 +02:00
zone << rr
2024-03-07 02:52:59 +01:00
update_zone zone
2023-05-08 17:34:50 +02:00
2023-07-11 02:15:37 +02:00
Response::RRAdded.new zone.domain, rr
2023-05-08 17:34:50 +02:00
end
2024-03-07 02:52:59 +01:00
# Any modification of the zone must be performed here.
# This function updates the SOA serial before storing the modified zone.
def update_zone(zone : Zone)
zone.update_serial
zones_by_domain.update_or_create zone.domain, zone
end
def update_rr(user_id : UserDataID, domain : String, rr : Zone::ResourceRecord) : IPC::JSON
2024-04-27 05:48:28 +02:00
user_must_exist! user_id
2024-03-14 02:03:26 +01:00
zone = zone_must_exist! domain
2024-04-27 05:48:28 +02:00
user_should_own! user_id, domain
2023-05-08 17:34:50 +02:00
2024-03-14 02:03:26 +01:00
zone.rr_not_readonly! rr.rrid
2024-03-13 03:58:46 +01:00
2023-05-08 19:07:20 +02:00
# 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
2023-05-08 19:07:20 +02:00
end
end
2024-03-13 03:58:46 +01:00
zone.update_rr rr
2023-07-12 16:10:25 +02:00
2024-03-07 02:52:59 +01:00
update_zone zone
2023-05-08 17:34:50 +02:00
2023-07-12 16:10:25 +02:00
Response::RRUpdated.new domain, rr
2023-05-08 17:34:50 +02:00
end
def delete_rr(user_id : UserDataID, domain : String, rrid : UInt32) : IPC::JSON
2024-04-27 05:48:28 +02:00
user_must_exist! user_id
2024-03-14 02:03:26 +01:00
zone = zone_must_exist! domain
2024-04-27 05:48:28 +02:00
user_should_own! user_id, domain
2024-03-13 03:58:46 +01:00
2024-03-14 02:03:26 +01:00
rr = zone.rr_not_readonly! rrid
2024-03-13 03:58:46 +01:00
# Remove token from the db.
if token_uuid = rr.token
tokens_by_uuid.delete token_uuid
end
2023-05-08 19:23:36 +02:00
2024-03-14 02:03:26 +01:00
zone.delete_rr rrid
2024-03-07 02:52:59 +01:00
update_zone zone
2023-05-08 19:23:36 +02:00
2023-07-10 03:34:06 +02:00
Response::RRDeleted.new rrid
2023-05-08 19:23:36 +02:00
end
def delete_domain(user_id : UserDataID, domain : String) : IPC::JSON
2024-04-27 05:48:28 +02:00
user_must_exist! user_id
2024-03-14 02:03:26 +01:00
zone_must_exist! domain
2024-04-27 05:48:28 +02:00
user_should_own! user_id, domain
2023-05-07 20:23:34 +02:00
# Remove this domain from the list of user's domains.
2024-04-27 05:48:28 +02:00
domains_by_name.delete domain
2023-05-07 20:23:34 +02:00
# Remove the related zone and their registered tokens.
2023-05-07 20:23:34 +02:00
zones_by_domain.delete domain
tokens_by_domain.delete domain
2023-05-07 20:23:34 +02:00
2023-07-03 12:23:18 +02:00
Response::DomainDeleted.new domain
end
2024-03-17 22:45:19 +01:00
# Get all removed users from `authd`, list all their domains and remove their data from `dnsmanagerd`.
2024-04-27 05:48:28 +02:00
def get_orphan_domains(user_id : UserDataID) : IPC::JSON
user_must_be_admin! user_id
2024-03-17 05:15:50 +01:00
# Baguette::Log.debug "list all orphan domains (long computation)"
# orphans = [] of String
# user_data.each do |user|
# begin
# dnsmanagerd().authd.get_user? user.uid
# rescue e
# Baguette::Log.warning "no authd info on user #{user.uid}: #{e} (removing this user)"
# Baguette::Log.debug "user #{user.uid} had #{user.domains.size} domains"
# user.domains.each do |domain|
# orphans << domain
# end
# wipe_user_data user
# end
# end
# Baguette::Log.debug "total: #{orphans.size} orphans"
#
# Response::OrphanDomainList.new orphans
Response::Error.new "Not implemented."
2024-03-17 05:15:50 +01:00
end
def get_zone(user_id : UserDataID, domain : String) : IPC::JSON
user_must_exist! user_id
2024-03-14 02:03:26 +01:00
zone = zone_must_exist! domain
2024-04-27 05:48:28 +02:00
user_should_own! user_id, domain
2023-05-07 21:05:53 +02:00
Response::Zone.new zone
end
def wipe_user_data(user_id : UserDataID)
domains_by_owners.get(user_id.to_s).each do |domain|
domain.owners.delete user_id
# Remove the user's domain when he is the only owner.
if domain.owners.empty?
@domains_by_name.delete domain.name
@tokens_by_domain.delete domain.name
@zones_by_domain.delete domain.name
else
@domains_by_name.update_or_create domain.name, domain
end
end
rescue e
Baguette::Log.error "while removing a domain: #{e}"
end
2024-04-27 05:48:28 +02:00
def delete_user_data(user_id : UserDataID, user_to_delete : UserDataID?) : IPC::JSON
user_must_exist! user_id
user_id_to_delete = if u = user_to_delete
2024-04-27 05:48:28 +02:00
user_must_be_admin! user_id
Baguette::Log.info "Admin #{user_id} removes data of user #{u}."
user_must_exist! u
u
else
Baguette::Log.info "User #{user_id} terminates his account."
user_id
end
wipe_user_data user_id_to_delete
Response::Success.new
end
def user_domains(user_id : UserDataID) : Array(Domain)
user_must_exist! user_id
domains_by_owners.get(user_id.to_s)
2023-05-07 20:50:56 +02:00
end
2024-04-27 05:48:28 +02:00
# TODO: is the user known from authd?
def user_must_exist!(user_id : UserDataID)
dnsmanagerd().authd.get_user? user_id
2024-03-14 02:03:26 +01:00
end
2024-04-27 05:48:28 +02:00
def user_must_be_admin!(user_id : UserDataID) : Nil
dnsmanagerd().assert_permissions! user_id, "*", AuthD::User::PermissionLevel::Admin
2024-03-14 02:43:00 +01:00
end
2024-03-14 02:03:26 +01:00
def zone_must_exist!(domain : String) : Zone
zone = zones_by_domain.get? domain
2024-03-14 02:03:26 +01:00
raise DomainNotFoundException.new unless zone
zone
end
2024-04-27 05:48:28 +02:00
# Owning a domain means to be in the owners' list of the domain.
# TODO: accept admin users to override this test.
def user_should_own!(user_id : UserDataID, domain : String) : Nil
2024-04-27 05:48:28 +02:00
d = domains_by_name.get domain
unless d.owners.includes? user_id
2024-03-14 02:03:26 +01:00
raise NoOwnershipException.new
end
2024-03-14 02:03:26 +01:00
end
def new_token(user_id : UserDataID, domain : String, rrid : UInt32) : IPC::JSON
user_must_exist! user_id
2024-03-14 02:03:26 +01:00
zone = zone_must_exist! domain
2024-04-27 05:48:28 +02:00
user_should_own! user_id, domain
2024-03-14 02:03:26 +01:00
rr = zone.rr_must_exist! rrid
old_token = rr.token
# 1. create token
token = Token.new domain, rrid
# 2. add token to the RR.
rr.token = token.uuid
zone.update_rr rr
# 3. update the zone (no need to call `update_zone` to change the zone serial).
zones_by_domain.update_or_create zone.domain, zone
# 4. if there is an old token, remove it.
if old_token_uuid = old_token
tokens_by_uuid.delete old_token_uuid
end
# 5. add token to the db.
@tokens << token
Response::RRUpdated.new domain, rr
end
2024-03-14 02:43:00 +01:00
def token_must_exist!(token_uuid : String) : Token
token = tokens_by_uuid.get? token_uuid
raise TokenNotFoundException.new unless token
token
end
def use_token(token_uuid : String, address : String) : IPC::JSON
2024-03-14 02:43:00 +01:00
token = token_must_exist! token_uuid
zone = zone_must_exist! token.domain
rr = zone.rr_must_exist! token.rrid
# Same address, no need to change anything.
return Response::Success.new if rr.target == address
2024-03-14 02:43:00 +01:00
case rr
when Zone::A
return Response::Error.new "invalid ipv4" unless Zone.is_ipv4_address_valid? address
2024-03-14 02:43:00 +01:00
rr.target = address
zone.update_rr rr
zones_by_domain.update_or_create zone.domain, zone
Response::Success.new
when Zone::AAAA
return Response::Error.new "invalid ipv6" unless Zone.is_ipv6_address_valid? address
2024-03-14 02:43:00 +01:00
rr.target = address
zone.update_rr rr
update_zone zone
2024-03-14 02:43:00 +01:00
Response::Success.new
else
Response::Error.new "use token on invalid entry (not A or AAAA)"
end
end
2020-12-03 17:13:40 +01:00
end
require "./storage/*"