2020-12-03 17:13:40 +01:00
|
|
|
require "json"
|
|
|
|
require "uuid"
|
|
|
|
require "uuid/json"
|
2020-12-09 19:01:33 +01:00
|
|
|
require "baguette-crystal-base"
|
2020-12-03 17:13:40 +01:00
|
|
|
|
|
|
|
require "dodb"
|
|
|
|
|
|
|
|
class DNSManager::Storage
|
2020-12-09 19:01:33 +01:00
|
|
|
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)
|
|
|
|
|
2024-03-13 01:16:09 +01:00
|
|
|
getter tokens : DODB::CachedDataBase(Token)
|
|
|
|
getter tokens_by_uuid : DODB::Index(Token)
|
|
|
|
getter tokens_by_domain : DODB::Partition(Token)
|
|
|
|
|
2024-02-25 04:13:18 +01:00
|
|
|
getter root : String
|
|
|
|
getter zonefiledir : String
|
|
|
|
|
2023-02-15 19:21:49 +01:00
|
|
|
def initialize(@root : String, reindex : Bool = false)
|
2020-12-09 19:01:33 +01:00
|
|
|
@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
|
2024-03-13 01:16:09 +01:00
|
|
|
@tokens = DODB::CachedDataBase(Token).new "#{@root}/tokens"
|
|
|
|
@tokens_by_uuid = @tokens.new_index "uuid", &.uuid
|
|
|
|
@tokens_by_domain = @tokens.new_partition "domain", &.domain
|
2020-12-09 19:01:33 +01:00
|
|
|
|
2024-02-25 04:13:18 +01:00
|
|
|
@zonefiledir = "#{@root}/bind9-zones"
|
|
|
|
Dir.mkdir_p @zonefiledir
|
|
|
|
|
2020-12-09 19:01:33 +01:00
|
|
|
Baguette::Log.info "storage initialized"
|
2023-02-15 19:21:49 +01:00
|
|
|
|
|
|
|
if reindex
|
|
|
|
Baguette::Log.debug "Reindexing user data..."
|
|
|
|
@user_data.reindex_everything!
|
|
|
|
Baguette::Log.debug "Reindexing zones..."
|
|
|
|
@zones.reindex_everything!
|
2024-03-13 01:16:09 +01:00
|
|
|
Baguette::Log.debug "Reindexing tokens..."
|
|
|
|
@tokens.reindex_everything!
|
2023-02-15 19:21:49 +01:00
|
|
|
Baguette::Log.debug "Reindexed!"
|
|
|
|
end
|
2020-12-09 19:01:33 +01:00
|
|
|
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
|
|
|
|
|
2023-05-07 18:32:32 +02:00
|
|
|
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}"
|
2023-07-07 20:23:38 +02:00
|
|
|
user_data = UserData.new user_id
|
|
|
|
@user_data << user_data
|
2023-05-07 18:32:32 +02:00
|
|
|
end
|
|
|
|
|
2023-07-01 16:04:38 +02:00
|
|
|
user_data
|
2023-05-07 18:32:32 +02:00
|
|
|
end
|
2023-05-07 16:45:09 +02:00
|
|
|
|
2024-02-25 04:13:18 +01:00
|
|
|
# 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.
|
2024-02-25 04:13:18 +01:00
|
|
|
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
|
2024-02-25 04:13:18 +01:00
|
|
|
|
|
|
|
Baguette::Log.info "writing zone file #{@zonefiledir}/#{zone.domain}"
|
2024-03-13 03:58:46 +01:00
|
|
|
# TODO: safe write.
|
2024-02-25 04:13:18 +01:00
|
|
|
File.open("#{@zonefiledir}/#{zone.domain}", "w") do |file|
|
|
|
|
zone.to_bind9 file
|
|
|
|
end
|
|
|
|
Response::Success.new
|
|
|
|
end
|
|
|
|
|
2024-02-27 07:42:10 +01:00
|
|
|
def get_generated_zonefile(user_id : Int32, domain : String) : IPC::JSON
|
|
|
|
|
2024-03-14 02:03:26 +01:00
|
|
|
user_data = user_must_exist! user_id
|
|
|
|
zone = zone_must_exist! domain
|
|
|
|
user_should_own! user_data, 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
|
|
|
|
|
2023-06-29 18:29:25 +02:00
|
|
|
def new_domain(accepted_domains : Array(String),
|
|
|
|
template_directory : String,
|
|
|
|
user_id : Int32,
|
|
|
|
domain : String) : IPC::JSON
|
|
|
|
|
2024-03-14 02:03:26 +01:00
|
|
|
user_data = user_must_exist! user_id
|
2023-06-16 18:41:20 +02:00
|
|
|
|
2023-06-29 12:27:15 +02:00
|
|
|
return Response::DomainAlreadyExists.new if zones_by_domain.get? domain
|
2023-06-16 18:41:20 +02:00
|
|
|
|
2023-06-29 18:29:25 +02:00
|
|
|
# Verify if the domain is acceptable.
|
2023-06-29 12:27:15 +02:00
|
|
|
matching_domains = accepted_domains.select { |adomain| domain.ends_with? adomain }
|
2023-06-30 00:01:25 +02:00
|
|
|
unless matching_domains.size > 0
|
2023-06-29 12:27:15 +02:00
|
|
|
Baguette::Log.warning "trying to add an unacceptable domain: #{domain}"
|
|
|
|
return Response::UnacceptableDomain.new
|
|
|
|
end
|
2023-06-16 18:41:20 +02:00
|
|
|
|
2023-06-29 12:27:15 +02:00
|
|
|
matching_domains.each do |md|
|
2023-06-29 18:29:25 +02:00
|
|
|
Baguette::Log.info "Add new domain #{domain} (matching domain #{md})"
|
2023-06-16 18:41:20 +02:00
|
|
|
end
|
2023-06-29 12:27:15 +02:00
|
|
|
|
2023-06-29 18:29:25 +02:00
|
|
|
# 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"
|
2023-06-29 12:27:15 +02:00
|
|
|
|
2023-06-29 18:29:25 +02:00
|
|
|
# Replace domain by the real domain.
|
|
|
|
default_zone.replace_domain domain
|
|
|
|
|
|
|
|
#
|
2023-06-29 12:27:15 +02:00
|
|
|
# Actually write data on-disk.
|
2023-06-29 18:29:25 +02:00
|
|
|
#
|
2023-06-29 12:27:15 +02:00
|
|
|
|
2023-06-29 18:29:25 +02:00
|
|
|
# Add the domain to the user's domain.
|
|
|
|
user_data.domains << domain
|
|
|
|
update_user_data user_data
|
2023-06-29 12:27:15 +02:00
|
|
|
|
2023-06-29 18:29:25 +02:00
|
|
|
# Add the new zone in the database.
|
2023-06-30 00:12:40 +02:00
|
|
|
zones_by_domain.update_or_create domain, default_zone
|
2023-06-29 12:27:15 +02:00
|
|
|
|
2023-07-01 16:04:38 +02:00
|
|
|
Response::DomainAdded.new domain
|
2023-06-16 18:41:20 +02:00
|
|
|
end
|
|
|
|
|
2023-05-07 18:32:32 +02:00
|
|
|
def add_or_update_zone(user_id : Int32, zone : Zone) : IPC::JSON
|
2024-03-14 02:03:26 +01:00
|
|
|
user_data = user_must_exist! user_id
|
|
|
|
|
2023-05-07 16:45:09 +02: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
|
2023-05-07 16:45:09 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
# Does the zone already exist?
|
|
|
|
if z = zones_by_domain.get? zone.domain
|
2024-03-14 02:03:26 +01:00
|
|
|
user_should_own! user_data, z.domain
|
2023-05-07 16:45:09 +02:00
|
|
|
else
|
|
|
|
# Add the domain to the user's domain.
|
2020-12-09 19:01:33 +01:00
|
|
|
user_data.domains << zone.domain
|
2023-05-07 16:45:09 +02:00
|
|
|
|
|
|
|
# Actually write data on-disk.
|
2020-12-09 19:01:33 +01:00
|
|
|
update_user_data user_data
|
|
|
|
end
|
2023-05-07 16:45:09 +02:00
|
|
|
|
|
|
|
# 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
|
2023-05-07 16:45:09 +02:00
|
|
|
|
2023-05-08 17:34:50 +02:00
|
|
|
def add_rr(user_id : Int32, domain : String, rr : Zone::ResourceRecord) : IPC::JSON
|
2024-03-14 02:03:26 +01:00
|
|
|
user_data = user_must_exist! user_id
|
|
|
|
zone = zone_must_exist! domain
|
|
|
|
user_should_own! user_data, 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}"
|
2023-07-11 04:09:46 +02:00
|
|
|
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
|
|
|
|
|
2023-05-08 17:34:50 +02:00
|
|
|
def update_rr(user_id : Int32, domain : String, rr : Zone::ResourceRecord) : IPC::JSON
|
2024-03-14 02:03:26 +01:00
|
|
|
user_data = user_must_exist! user_id
|
|
|
|
zone = zone_must_exist! domain
|
|
|
|
user_should_own! user_data, 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}"
|
2023-07-11 04:09:46 +02:00
|
|
|
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
|
|
|
|
|
2023-05-08 19:23:36 +02:00
|
|
|
def delete_rr(user_id : Int32, domain : String, rrid : UInt32) : IPC::JSON
|
2024-03-14 02:03:26 +01:00
|
|
|
user_data = user_must_exist! user_id
|
|
|
|
zone = zone_must_exist! domain
|
|
|
|
user_should_own! user_data, 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
|
|
|
|
|
2023-05-07 18:32:32 +02:00
|
|
|
def delete_domain(user_id : Int32, domain : String) : IPC::JSON
|
2024-03-14 02:03:26 +01:00
|
|
|
user_data = user_must_exist! user_id
|
|
|
|
zone_must_exist! domain
|
|
|
|
user_should_own! user_data, domain
|
2023-05-07 16:45:09 +02:00
|
|
|
|
2023-05-07 20:23:34 +02:00
|
|
|
# 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
|
|
|
|
|
2024-03-13 01:16:09 +01:00
|
|
|
# Remove the related zone and their registered tokens.
|
2023-05-07 20:23:34 +02:00
|
|
|
zones_by_domain.delete domain
|
2024-03-13 01:16:09 +01:00
|
|
|
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
|
2023-05-07 16:45:09 +02:00
|
|
|
end
|
|
|
|
|
2023-05-07 21:05:53 +02:00
|
|
|
def get_zone(user_id : Int32, domain : String) : IPC::JSON
|
2024-03-14 02:03:26 +01:00
|
|
|
user_data = user_must_exist! user_id
|
|
|
|
zone = zone_must_exist! domain
|
|
|
|
user_should_own! user_data, domain
|
2023-05-07 21:05:53 +02:00
|
|
|
|
|
|
|
Response::Zone.new zone
|
|
|
|
end
|
|
|
|
|
2023-05-07 20:50:56 +02:00
|
|
|
def user_domains(user_id : Int32) : IPC::JSON
|
2024-03-14 02:03:26 +01:00
|
|
|
user_data = user_must_exist! user_id
|
2023-05-07 20:50:56 +02:00
|
|
|
Response::DomainList.new user_data.domains
|
|
|
|
end
|
2024-03-13 01:16:09 +01:00
|
|
|
|
2024-03-14 02:03:26 +01:00
|
|
|
def user_must_exist!(user_id : Int32) : UserData
|
2024-03-13 01:16:09 +01:00
|
|
|
user_data = user_data_by_uid.get? user_id.to_s
|
2024-03-14 02:03:26 +01:00
|
|
|
raise UnknownUserException.new unless user_data
|
|
|
|
user_data
|
|
|
|
end
|
2024-03-13 01:16:09 +01:00
|
|
|
|
2024-03-14 02:43:00 +01:00
|
|
|
def user_must_be_admin!(user_id : Int32) : UserData
|
|
|
|
user_data = user_must_exist! user_id
|
|
|
|
raise AdminAuthorizationException.new unless user_data.admin
|
|
|
|
user_data
|
|
|
|
end
|
|
|
|
|
2024-03-14 02:03:26 +01:00
|
|
|
def zone_must_exist!(domain : String) : Zone
|
2024-03-13 01:16:09 +01:00
|
|
|
zone = zones_by_domain.get? domain
|
2024-03-14 02:03:26 +01:00
|
|
|
raise DomainNotFoundException.new unless zone
|
|
|
|
zone
|
|
|
|
end
|
2024-03-13 01:16:09 +01:00
|
|
|
|
2024-03-14 02:03:26 +01:00
|
|
|
def user_should_own!(user_data : UserData, domain : String)
|
2024-03-13 01:16:09 +01:00
|
|
|
unless user_data.domains.includes?(domain) || user_data.admin
|
2024-03-14 02:03:26 +01:00
|
|
|
raise NoOwnershipException.new
|
2024-03-13 01:16:09 +01:00
|
|
|
end
|
2024-03-14 02:03:26 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
def new_token(user_id : Int32, domain : String, rrid : UInt32) : IPC::JSON
|
|
|
|
user_data = user_must_exist! user_id
|
|
|
|
zone = zone_must_exist! domain
|
|
|
|
user_should_own! user_data, domain
|
2024-03-13 01:16:09 +01:00
|
|
|
|
2024-03-14 02:03:26 +01:00
|
|
|
rr = zone.rr_must_exist! rrid
|
2024-03-13 01:16:09 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2024-03-14 04:36:03 +01:00
|
|
|
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
|
|
|
|
|
|
|
|
# TODO: validate target?
|
|
|
|
|
|
|
|
case rr
|
|
|
|
when Zone::A
|
|
|
|
rr.target = address
|
|
|
|
zone.update_rr rr
|
|
|
|
zones_by_domain.update_or_create zone.domain, zone
|
|
|
|
Response::Success.new
|
|
|
|
when Zone::AAAA
|
|
|
|
rr.target = address
|
|
|
|
zone.update_rr rr
|
|
|
|
zones_by_domain.update_or_create zone.domain, zone
|
|
|
|
Response::Success.new
|
|
|
|
else
|
|
|
|
Response::Error.new "use token on invalid entry (not A or AAAA)"
|
|
|
|
end
|
2024-03-13 01:16:09 +01:00
|
|
|
end
|
2020-12-03 17:13:40 +01:00
|
|
|
end
|
2020-12-09 19:01:33 +01:00
|
|
|
|
|
|
|
require "./storage/*"
|