Compare commits
16 Commits
Author | SHA1 | Date |
---|---|---|
|
b53b31b584 | |
|
5cbcbdaa8f | |
|
de8beb0df8 | |
|
7bee53d625 | |
|
234259a9d0 | |
|
4923fb34f9 | |
|
158d772727 | |
|
723c1a83a0 | |
|
ebd9bd75a5 | |
|
4606f852cc | |
|
1f69839333 | |
|
a188d28f1d | |
|
8cafca13be | |
|
a888551bbc | |
|
40fcc9c66e | |
|
61bace491c |
5
Makefile
5
Makefile
|
@ -64,6 +64,9 @@ run-client-rr-update-a:
|
|||
run-client-rr-del:
|
||||
$(Q)$(LD_P) ./bin/dnsmanager-client user rr del $(DOMAIN) $(RRID) $(LOGIN_OPT)
|
||||
|
||||
run-admin-generate-all-zonefiles:
|
||||
$(Q)$(LD_P) ./bin/dnsmanager-client admin genall $(LOGIN_OPT)
|
||||
|
||||
##################
|
||||
### SETUP COMMANDS
|
||||
##################
|
||||
|
@ -105,4 +108,4 @@ wipe-db:
|
|||
rm -r $(DBDIR)
|
||||
|
||||
release:
|
||||
make build-server OPTS="--release --progress"
|
||||
make build-server OPTS="--release --progress --no-debug"
|
||||
|
|
|
@ -11,6 +11,7 @@ dependencies:
|
|||
ipc:
|
||||
git: https://git.baguette.netlib.re/Baguette/ipc.cr
|
||||
dodb:
|
||||
branch: master
|
||||
git: https://git.baguette.netlib.re/Baguette/dodb.cr
|
||||
authd:
|
||||
git: https://git.baguette.netlib.re/Baguette/authd
|
||||
|
|
|
@ -51,7 +51,11 @@ class DNSManager::Client < IPC
|
|||
def user_domain_add(domain : String)
|
||||
request = Request::NewDomain.new domain
|
||||
send_now request
|
||||
parse_message [ Response::Success ], read
|
||||
parse_message [ Response::DomainAdded,
|
||||
Response::DomainAlreadyExists,
|
||||
Response::InvalidDomainName,
|
||||
Response::UnacceptableDomain
|
||||
], read
|
||||
end
|
||||
|
||||
# Remove a domain.
|
||||
|
|
|
@ -59,11 +59,13 @@ def parsing_cli(authd_config : Baguette::Configuration::Auth)
|
|||
# either we test with the exact expected number of arguments or the least.
|
||||
if exact = nexact
|
||||
if args.size != exact
|
||||
Baguette::Log.error "Wrong number of parameters: expected #{exact}, got #{args.size}"
|
||||
Baguette::Log.error "#{parser}"
|
||||
exit 1
|
||||
end
|
||||
elsif least = at_least
|
||||
if args.size < least
|
||||
Baguette::Log.error "Wrong number of parameters: expected at least #{least}, got #{args.size}"
|
||||
Baguette::Log.error "#{parser}"
|
||||
exit 1
|
||||
end
|
||||
|
|
|
@ -71,6 +71,7 @@ class DNSManager::Service < IPC
|
|||
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
|
||||
|
@ -83,6 +84,11 @@ class DNSManager::Service < IPC
|
|||
@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
|
||||
|
|
|
@ -7,7 +7,7 @@ class DNSManager::Request
|
|||
def handle(dnsmanagerd : DNSManager::Service, event : IPC::Event) : IPC::JSON
|
||||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
dnsmanagerd.storage.get_orphan_domains dnsmanagerd, user.uid
|
||||
dnsmanagerd.storage.get_orphan_domains user.uid
|
||||
end
|
||||
end
|
||||
DNSManager.requests << GetOrphanDomains
|
||||
|
@ -59,8 +59,7 @@ class DNSManager::Request
|
|||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
|
||||
udata = dnsmanagerd.storage.get_user_data user
|
||||
return Response::InsufficientRights.new unless udata.admin
|
||||
dnsmanagerd.storage.user_must_be_admin! user.uid
|
||||
dnsmanagerd.storage.generate_all_zonefiles
|
||||
end
|
||||
end
|
||||
|
@ -77,8 +76,7 @@ class DNSManager::Request
|
|||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
|
||||
udata = dnsmanagerd.storage.get_user_data user
|
||||
return Response::InsufficientRights.new unless udata.admin
|
||||
dnsmanagerd.storage.user_must_be_admin! user.uid
|
||||
dnsmanagerd.storage.generate_zonefile @domain
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
class DNSManager::Request
|
||||
IPC::JSON.message AskShareToken, 20 do
|
||||
property domain : String
|
||||
|
||||
def initialize(@domain)
|
||||
end
|
||||
|
||||
def handle(dnsmanagerd : DNSManager::Service, event : IPC::Event)
|
||||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
|
||||
dnsmanagerd.storage.ask_share_token user.uid, @domain
|
||||
end
|
||||
end
|
||||
DNSManager.requests << AskShareToken
|
||||
|
||||
IPC::JSON.message AskTransferToken, 21 do
|
||||
property domain : String
|
||||
|
||||
def initialize(@domain)
|
||||
end
|
||||
|
||||
def handle(dnsmanagerd : DNSManager::Service, event : IPC::Event)
|
||||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
|
||||
dnsmanagerd.storage.ask_transfer_token user.uid, @domain
|
||||
end
|
||||
end
|
||||
DNSManager.requests << AskTransferToken
|
||||
|
||||
IPC::JSON.message AskUnShareDomain, 22 do
|
||||
property domain : String
|
||||
|
||||
def initialize(@domain)
|
||||
end
|
||||
|
||||
def handle(dnsmanagerd : DNSManager::Service, event : IPC::Event)
|
||||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
|
||||
dnsmanagerd.storage.ask_unshare_domain user.uid, @domain
|
||||
end
|
||||
end
|
||||
DNSManager.requests << AskUnShareDomain
|
||||
|
||||
IPC::JSON.message GainOwnership, 23 do
|
||||
property uuid : String
|
||||
|
||||
def initialize(@uuid)
|
||||
end
|
||||
|
||||
def handle(dnsmanagerd : DNSManager::Service, event : IPC::Event)
|
||||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
|
||||
dnsmanagerd.storage.gain_ownership user.uid, @uuid
|
||||
end
|
||||
end
|
||||
DNSManager.requests << GainOwnership
|
||||
end
|
|
@ -14,13 +14,19 @@ class DNSManager::Request
|
|||
#dnsmanagerd.auth.edit_profile_content user.uid, {
|
||||
# "dnsmanager-last-connection" => JSON::Any.new Time.utc.to_s
|
||||
#}
|
||||
user_data = dnsmanagerd.storage.ensure_user_data response.user.uid
|
||||
return Response::Error.new "invalid user" unless user_data
|
||||
user_id = response.user.uid
|
||||
|
||||
accepted_domains = dnsmanagerd.configuration.accepted_domains.not_nil!
|
||||
user_domains = user_data.domains
|
||||
perms = dnsmanagerd.check_permissions response.user.uid, "*"
|
||||
Response::Logged.new (perms == AuthD::User::PermissionLevel::Admin), accepted_domains, user_domains
|
||||
|
||||
# Limit the number of domains in this message.
|
||||
# Pagination will be required beyond a hundred domains.
|
||||
user_domains = dnsmanagerd.storage.user_domains(user_id).[0..100]
|
||||
is_admin = dnsmanagerd.is_admin? user_id
|
||||
|
||||
Response::Logged.new is_admin, accepted_domains, user_domains
|
||||
when AuthD::Response::ErrorUserNotFound
|
||||
Baguette::Log.error "Trying to authenticate an unknown user."
|
||||
Response::ErrorInvalidToken.new
|
||||
else
|
||||
Response::ErrorInvalidToken.new
|
||||
end
|
||||
|
@ -42,7 +48,7 @@ class DNSManager::Request
|
|||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
|
||||
dnsmanagerd.storage.delete_user_data dnsmanagerd, user.uid, user_id
|
||||
dnsmanagerd.storage.delete_user_data user.uid, user_id
|
||||
end
|
||||
end
|
||||
DNSManager.requests << DeleteUser
|
||||
|
|
|
@ -69,7 +69,8 @@ class DNSManager::Request
|
|||
def handle(dnsmanagerd : DNSManager::Service, event : IPC::Event) : IPC::JSON
|
||||
user = dnsmanagerd.get_logged_user event
|
||||
return Response::ErrorUserNotLogged.new unless user
|
||||
dnsmanagerd.storage.user_domains user.uid
|
||||
user_domain_names = dnsmanagerd.storage.user_domains(user.uid).map &.name
|
||||
Response::DomainList.new user_domain_names
|
||||
end
|
||||
end
|
||||
DNSManager.requests << UserDomains
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
class DNSManager::Response
|
||||
IPC::JSON.message DomainDeleted, 9 do
|
||||
property domain : String
|
||||
|
@ -15,9 +14,9 @@ class DNSManager::Response
|
|||
end
|
||||
DNSManager.responses << InvalidZone
|
||||
|
||||
# Domain of a zone cannot change, for security reasons.
|
||||
IPC::JSON.message DomainChanged, 11 do
|
||||
def initialize
|
||||
property domain : DNSManager::Storage::Domain
|
||||
def initialize(@domain)
|
||||
end
|
||||
end
|
||||
DNSManager.responses << DomainChanged
|
||||
|
@ -52,7 +51,7 @@ class DNSManager::Response
|
|||
IPC::JSON.message Logged, 16 do
|
||||
property admin : Bool
|
||||
property accepted_domains : Array(String)
|
||||
property my_domains : Array(String)
|
||||
property my_domains : Array(DNSManager::Storage::Domain)
|
||||
def initialize(@admin, @accepted_domains, @my_domains)
|
||||
end
|
||||
end
|
||||
|
@ -119,4 +118,3 @@ class DNSManager::Response
|
|||
end
|
||||
DNSManager.responses << OrphanDomainList
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
require "ipc"
|
||||
|
||||
# Useful to enable the client to be built.
|
||||
class DNSManager::Service < IPC
|
||||
end
|
382
src/storage.cr
382
src/storage.cr
|
@ -2,29 +2,56 @@ require "json"
|
|||
require "uuid"
|
||||
require "uuid/json"
|
||||
require "baguette-crystal-base"
|
||||
require "./service.cr"
|
||||
|
||||
require "dodb"
|
||||
|
||||
class DNSManager::Storage
|
||||
getter user_data : DODB::CachedDataBase(UserData)
|
||||
getter user_data_by_uid : DODB::Index(UserData)
|
||||
getter domains : DODB::Storage::Cached(Domain)
|
||||
getter domains_by_name : DODB::Trigger::IndexCached(Domain)
|
||||
getter domains_by_share_key : DODB::Trigger::IndexCached(Domain)
|
||||
getter domains_by_transfer_key : DODB::Trigger::IndexCached(Domain)
|
||||
getter domains_by_owners : DODB::Trigger::Tags(Domain)
|
||||
|
||||
getter zones : DODB::CachedDataBase(Zone)
|
||||
getter zones_by_domain : DODB::Index(Zone)
|
||||
getter zones : DODB::Storage::Cached(Zone)
|
||||
getter zones_by_domain : DODB::Trigger::IndexCached(Zone)
|
||||
|
||||
getter tokens : DODB::CachedDataBase(Token)
|
||||
getter tokens_by_uuid : DODB::Index(Token)
|
||||
getter tokens_by_domain : DODB::Partition(Token)
|
||||
getter tokens : DODB::Storage::Cached(Token)
|
||||
getter tokens_by_uuid : DODB::Trigger::IndexCached(Token)
|
||||
getter tokens_by_domain : DODB::Trigger::Partition(Token)
|
||||
|
||||
getter root : String
|
||||
getter zonefiledir : String
|
||||
|
||||
property dnsmanagerd : DNSManager::Service? = nil
|
||||
|
||||
def dnsmanagerd() : DNSManager::Service
|
||||
@dnsmanagerd.not_nil!
|
||||
rescue
|
||||
raise Exception.new "dnsmanagerd not defined"
|
||||
end
|
||||
|
||||
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"
|
||||
@domains = DODB::Storage::Cached(Domain).new "#{@root}/domains"
|
||||
@domains_by_name = @domains.new_index "name", &.name
|
||||
@domains_by_share_key = @domains.new_index "share-key", do |d|
|
||||
if k = d.share_key
|
||||
k
|
||||
else
|
||||
DODB.no_index
|
||||
end
|
||||
end
|
||||
@domains_by_transfer_key = @domains.new_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::Storage::Cached(Zone).new "#{@root}/zones"
|
||||
@zones_by_domain = @zones.new_index "domain", &.domain
|
||||
@tokens = DODB::CachedDataBase(Token).new "#{@root}/tokens"
|
||||
@tokens = DODB::Storage::Cached(Token).new "#{@root}/tokens"
|
||||
@tokens_by_uuid = @tokens.new_index "uuid", &.uuid
|
||||
@tokens_by_domain = @tokens.new_partition "domain", &.domain
|
||||
|
||||
|
@ -34,8 +61,8 @@ class DNSManager::Storage
|
|||
Baguette::Log.info "storage initialized"
|
||||
|
||||
if reindex
|
||||
Baguette::Log.debug "Reindexing user data..."
|
||||
@user_data.reindex_everything!
|
||||
Baguette::Log.debug "Reindexing domains..."
|
||||
@domains.reindex_everything!
|
||||
Baguette::Log.debug "Reindexing zones..."
|
||||
@zones.reindex_everything!
|
||||
Baguette::Log.debug "Reindexing tokens..."
|
||||
|
@ -44,61 +71,40 @@ class DNSManager::Storage
|
|||
end
|
||||
end
|
||||
|
||||
def get_user_data(uid : UserDataID)
|
||||
user_data_by_uid.get uid.to_s
|
||||
rescue e : DODB::MissingEntry
|
||||
entry = UserData.new uid
|
||||
entry
|
||||
end
|
||||
def generate_zonefile_(domain : String) : Nil
|
||||
zone = zone_must_exist! domain
|
||||
|
||||
def get_user_data(user : ::AuthD::User::Public)
|
||||
get_user_data user.uid
|
||||
end
|
||||
# Safe write.
|
||||
filename_final = "#{@zonefiledir}/#{zone.domain}"
|
||||
filename_wip = "#{filename_final}.wip"
|
||||
Baguette::Log.info "writing zone file #{filename_final}"
|
||||
|
||||
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 : UserDataID)
|
||||
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
|
||||
File.open(filename_wip, "w") do |file|
|
||||
zone.to_bind9 file
|
||||
end
|
||||
|
||||
user_data
|
||||
# Rename WIP filename to final file name.
|
||||
File.rename filename_wip, filename_final
|
||||
end
|
||||
|
||||
# Only an admin can access this function.
|
||||
def generate_zonefile(domain : String) : IPC::JSON
|
||||
generate_zonefile_ domain
|
||||
Response::Success.new
|
||||
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|
|
||||
# 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
|
||||
zone = zone_must_exist! domain
|
||||
|
||||
Baguette::Log.info "writing zone file #{@zonefiledir}/#{zone.domain}"
|
||||
# TODO: safe write.
|
||||
File.open("#{@zonefiledir}/#{zone.domain}", "w") do |file|
|
||||
zone.to_bind9 file
|
||||
generate_zonefile_ zone.domain
|
||||
end
|
||||
Response::Success.new
|
||||
end
|
||||
|
||||
def get_generated_zonefile(user_id : UserDataID, domain : String) : IPC::JSON
|
||||
|
||||
user_data = user_must_exist! user_id
|
||||
zone = zone_must_exist! domain
|
||||
user_should_own! user_data, zone.domain
|
||||
user_should_own! user_id, zone.domain
|
||||
|
||||
io = IO::Memory.new
|
||||
zone.to_bind9 io
|
||||
|
@ -110,8 +116,6 @@ class DNSManager::Storage
|
|||
user_id : UserDataID,
|
||||
domain : String) : IPC::JSON
|
||||
|
||||
user_data = user_must_exist! user_id
|
||||
|
||||
# Prevent future very confusing errors.
|
||||
domain = domain.downcase
|
||||
|
||||
|
@ -125,6 +129,8 @@ class DNSManager::Storage
|
|||
end
|
||||
|
||||
matching_domains.each do |md|
|
||||
# Prevent empty domains (from crafted requests) to be accepted.
|
||||
return Response::InvalidDomainName.new unless (domain.chomp md).size > 1
|
||||
Baguette::Log.info "Add new domain #{domain} (matching domain #{md})"
|
||||
end
|
||||
|
||||
|
@ -142,9 +148,10 @@ class DNSManager::Storage
|
|||
# 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 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
|
||||
|
@ -152,9 +159,78 @@ class DNSManager::Storage
|
|||
Response::DomainAdded.new domain
|
||||
end
|
||||
|
||||
def add_or_update_zone(user_id : UserDataID, zone : Zone) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
def ask_share_token(user_id : UserDataID, domain_name : String)
|
||||
user_should_own! user_id, domain_name
|
||||
|
||||
domain = @domains_by_name.get domain_name
|
||||
if domain.share_key.nil?
|
||||
# Clone the domain so the database doesn't try to remove the new `shared_key`.
|
||||
domain_cloned = domain.clone
|
||||
domain_cloned.share_key = UUID.random.to_s
|
||||
@domains_by_name.update domain_name, domain_cloned
|
||||
Response::DomainChanged.new domain_cloned
|
||||
else
|
||||
Response::Error.new "The domain already have a share key."
|
||||
end
|
||||
end
|
||||
|
||||
def ask_transfer_token(user_id : UserDataID, domain_name : String)
|
||||
user_should_own! user_id, domain_name
|
||||
|
||||
domain = @domains_by_name.get domain_name
|
||||
if domain.transfer_key.nil?
|
||||
# Clone the domain so the database doesn't try to remove the new `transfer_key`.
|
||||
domain_cloned = domain.clone
|
||||
domain_cloned.transfer_key = UUID.random.to_s
|
||||
@domains_by_name.update domain_name, domain_cloned
|
||||
Response::DomainChanged.new domain_cloned
|
||||
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_should_own! user_id, domain_name
|
||||
|
||||
domain = @domains_by_name.get domain_name
|
||||
if domain.owners.size == 1 && domain.owners[0] == user_id
|
||||
# Clone the domain so the old `shared_key` still exists in the old version.
|
||||
domain_cloned = domain.clone
|
||||
domain_cloned.share_key = nil
|
||||
@domains_by_name.update domain_cloned
|
||||
Response::DomainChanged.new domain_cloned
|
||||
else
|
||||
Response::Error.new "You are not the only owner."
|
||||
end
|
||||
end
|
||||
|
||||
def gain_ownership(user_id : UserDataID, uuid : String)
|
||||
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_cloned = domain.clone
|
||||
domain_cloned.owners << user_id
|
||||
@domains_by_name.update domain_cloned
|
||||
Response::DomainChanged.new domain_cloned
|
||||
elsif domain = @domains_by_transfer_key.get? uuid
|
||||
if domain.owners.includes? user_id
|
||||
return Response::Error.new "You already own this domain."
|
||||
end
|
||||
# Clone the domain so the old `transfer_key` still exists in the old version.
|
||||
domain_cloned = domain.clone
|
||||
domain_cloned.transfer_key = nil
|
||||
domain_cloned.owners = [user_id]
|
||||
@domains_by_name.update domain_cloned
|
||||
Response::DomainChanged.new domain_cloned
|
||||
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
|
||||
# Test zone validity.
|
||||
if errors = zone.get_errors?
|
||||
Baguette::Log.warning "zone #{zone.domain} update with errors: #{errors}"
|
||||
|
@ -163,13 +239,10 @@ class DNSManager::Storage
|
|||
|
||||
# Does the zone already exist?
|
||||
if z = zones_by_domain.get? zone.domain
|
||||
user_should_own! user_data, z.domain
|
||||
user_should_own! user_id, z.domain
|
||||
else
|
||||
# Add the domain to the user's domain.
|
||||
user_data.domains << zone.domain
|
||||
|
||||
# Actually write data on-disk.
|
||||
update_user_data user_data
|
||||
domains << Domain.new zone.domain
|
||||
end
|
||||
|
||||
# Add -or replace- the zone.
|
||||
|
@ -179,9 +252,8 @@ class DNSManager::Storage
|
|||
end
|
||||
|
||||
def add_rr(user_id : UserDataID, domain : String, rr : Zone::ResourceRecord) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
zone = zone_must_exist! domain
|
||||
user_should_own! user_data, domain
|
||||
user_should_own! user_id, domain
|
||||
|
||||
# Test RR validity.
|
||||
rr.get_errors.tap do |errors|
|
||||
|
@ -206,12 +278,13 @@ class DNSManager::Storage
|
|||
end
|
||||
|
||||
def update_rr(user_id : UserDataID, domain : String, rr : Zone::ResourceRecord) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
zone = zone_must_exist! domain
|
||||
user_should_own! user_data, domain
|
||||
user_should_own! user_id, domain
|
||||
|
||||
zone.rr_not_readonly! rr.rrid
|
||||
|
||||
zone_clone = zone.clone
|
||||
|
||||
# Test RR validity.
|
||||
rr.get_errors.tap do |errors|
|
||||
unless errors.empty?
|
||||
|
@ -220,17 +293,16 @@ class DNSManager::Storage
|
|||
end
|
||||
end
|
||||
|
||||
zone.update_rr rr
|
||||
zone_clone.update_rr rr
|
||||
|
||||
update_zone zone
|
||||
update_zone zone_clone
|
||||
|
||||
Response::RRUpdated.new domain, rr
|
||||
end
|
||||
|
||||
def delete_rr(user_id : UserDataID, domain : String, rrid : UInt32) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
zone = zone_must_exist! domain
|
||||
user_should_own! user_data, domain
|
||||
zone = zone_must_exist! domain
|
||||
user_should_own! user_id, domain
|
||||
|
||||
rr = zone.rr_not_readonly! rrid
|
||||
|
||||
|
@ -239,105 +311,121 @@ class DNSManager::Storage
|
|||
tokens_by_uuid.delete token_uuid
|
||||
end
|
||||
|
||||
zone.delete_rr rrid
|
||||
update_zone zone
|
||||
zone_clone = zone.clone
|
||||
|
||||
zone_clone.delete_rr rrid
|
||||
update_zone zone_clone
|
||||
|
||||
Response::RRDeleted.new rrid
|
||||
end
|
||||
|
||||
def delete_domain(user_id : UserDataID, domain : String) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
zone_must_exist! domain
|
||||
user_should_own! user_data, domain
|
||||
def delete_domain(user_id : UserDataID, domain_name : String) : IPC::JSON
|
||||
zone_must_exist! domain_name
|
||||
user_should_own! user_id, domain_name
|
||||
|
||||
# Remove this domain from the list of user's domains.
|
||||
user_data.domains.delete domain
|
||||
domain = @domains_by_name.get domain_name
|
||||
# The user isn't the only owner, just remove him from the list of owners.
|
||||
if domain.owners.size > 1
|
||||
domain_cloned = domain.clone
|
||||
domain_cloned.owners.select! { |o| o != user_id }
|
||||
@domains_by_name.update domain_cloned
|
||||
|
||||
# Update on-disk user data.
|
||||
update_user_data user_data
|
||||
# Not really a domain deletion, but that'll do the trick.
|
||||
return Response::DomainDeleted.new domain_name
|
||||
end
|
||||
|
||||
# Remove the related zone and their registered tokens.
|
||||
zones_by_domain.delete domain
|
||||
tokens_by_domain.delete domain
|
||||
zones_by_domain.delete domain_name
|
||||
|
||||
Response::DomainDeleted.new domain
|
||||
# The domain may not have tokens.
|
||||
tokens_by_domain.delete? domain_name
|
||||
|
||||
# Remove this domain_name from the list of user's domains.
|
||||
domains_by_name.delete domain_name
|
||||
|
||||
Response::DomainDeleted.new domain_name
|
||||
end
|
||||
|
||||
# Get all removed users from `authd`, list all their domains and remove their data from `dnsmanagerd`.
|
||||
def get_orphan_domains(dnsmanagerd : DNSManager::Service, user_id : UserDataID) : IPC::JSON
|
||||
user_must_be_admin! dnsmanagerd, user_id
|
||||
def get_orphan_domains(user_id : UserDataID) : IPC::JSON
|
||||
user_must_be_admin! user_id
|
||||
|
||||
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
|
||||
# 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."
|
||||
end
|
||||
|
||||
def get_zone(user_id : UserDataID, domain : String) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
zone = zone_must_exist! domain
|
||||
user_should_own! user_data, domain
|
||||
user_should_own! user_id, domain
|
||||
|
||||
Response::Zone.new zone
|
||||
end
|
||||
|
||||
def wipe_user_data(user_data : UserData)
|
||||
# Remove the user's domains.
|
||||
user_data.domains.each do |domain|
|
||||
tokens_by_domain.delete domain
|
||||
zones_by_domain.delete domain
|
||||
rescue e
|
||||
Baguette::Log.error "while removing a domain: #{e}"
|
||||
end
|
||||
def wipe_user_data(user_id : UserDataID)
|
||||
domains_by_owners.get(user_id.to_s).each do |domain|
|
||||
domain_cloned = domain.clone
|
||||
domain_cloned.owners.delete user_id
|
||||
|
||||
# Remove the user.
|
||||
user_data_by_uid.delete user_data.uid.to_s
|
||||
# Remove the user's domain when he is the only owner.
|
||||
if domain_cloned.owners.empty?
|
||||
# The domain may not have tokens.
|
||||
@tokens_by_domain.delete? domain_cloned.name
|
||||
@zones_by_domain.delete domain_cloned.name
|
||||
@domains_by_name.delete domain_cloned.name
|
||||
else
|
||||
@domains_by_name.update domain_cloned
|
||||
end
|
||||
end
|
||||
rescue e
|
||||
Baguette::Log.error "while removing a domain: #{e}"
|
||||
end
|
||||
|
||||
def delete_user_data(dnsmanagerd : DNSManager::Service, user_id : UserDataID, user_to_delete : UserDataID?) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
user_data_to_delete = if u = user_to_delete
|
||||
user_must_be_admin! dnsmanagerd, user_id
|
||||
def delete_user_data(user_id : UserDataID, user_to_delete : UserDataID?) : IPC::JSON
|
||||
user_id_to_delete = if u = user_to_delete
|
||||
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_data.uid} terminates his account."
|
||||
user_data
|
||||
Baguette::Log.info "User #{user_id} terminates his account."
|
||||
user_id
|
||||
end
|
||||
|
||||
wipe_user_data user_data_to_delete
|
||||
wipe_user_data user_id_to_delete
|
||||
|
||||
Response::Success.new
|
||||
end
|
||||
|
||||
def user_domains(user_id : UserDataID) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
Response::DomainList.new user_data.domains
|
||||
def user_domains(user_id : UserDataID) : Array(Domain)
|
||||
if doms = domains_by_owners.get? user_id.to_s
|
||||
doms
|
||||
else
|
||||
Array(Domain).new
|
||||
end
|
||||
end
|
||||
|
||||
def user_must_exist!(user_id : UserDataID) : UserData
|
||||
user_data = user_data_by_uid.get? user_id.to_s
|
||||
raise UnknownUserException.new unless user_data
|
||||
user_data
|
||||
# TODO: is the user known from authd?
|
||||
def user_must_exist!(user_id : UserDataID)
|
||||
dnsmanagerd().authd.get_user? user_id
|
||||
end
|
||||
|
||||
def user_must_be_admin!(dnsmanagerd : DNSManager::Service, user_id : UserDataID) : UserData
|
||||
user_data = user_must_exist! user_id
|
||||
dnsmanagerd.assert_permissions! user_id, "*", AuthD::User::PermissionLevel::Admin
|
||||
user_data
|
||||
def user_must_be_admin!(user_id : UserDataID) : Nil
|
||||
dnsmanagerd().assert_permissions! user_id, "*", AuthD::User::PermissionLevel::Admin
|
||||
end
|
||||
|
||||
def zone_must_exist!(domain : String) : Zone
|
||||
|
@ -346,16 +434,21 @@ class DNSManager::Storage
|
|||
zone
|
||||
end
|
||||
|
||||
def user_should_own!(user_data : UserData, domain : String)
|
||||
unless user_data.domains.includes?(domain) || user_data.admin
|
||||
raise NoOwnershipException.new
|
||||
# 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
|
||||
if d = domains_by_name.get? domain
|
||||
unless d.owners.includes? user_id
|
||||
raise NoOwnershipException.new
|
||||
end
|
||||
else
|
||||
raise DomainNotFoundException.new
|
||||
end
|
||||
end
|
||||
|
||||
def new_token(user_id : UserDataID, domain : String, rrid : UInt32) : IPC::JSON
|
||||
user_data = user_must_exist! user_id
|
||||
zone = zone_must_exist! domain
|
||||
user_should_own! user_data, domain
|
||||
user_should_own! user_id, domain
|
||||
|
||||
rr = zone.rr_must_exist! rrid
|
||||
|
||||
|
@ -366,10 +459,11 @@ class DNSManager::Storage
|
|||
|
||||
# 2. add token to the RR.
|
||||
rr.token = token.uuid
|
||||
zone.update_rr rr
|
||||
zone_clone = zone.clone
|
||||
zone_clone.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
|
||||
zones_by_domain.update zone_clone
|
||||
|
||||
# 4. if there is an old token, remove it.
|
||||
if old_token_uuid = old_token
|
||||
|
@ -401,7 +495,7 @@ class DNSManager::Storage
|
|||
return Response::Error.new "invalid ipv4" unless Zone.is_ipv4_address_valid? address
|
||||
rr.target = address
|
||||
zone.update_rr rr
|
||||
zones_by_domain.update_or_create zone.domain, zone
|
||||
update_zone zone
|
||||
Response::Success.new
|
||||
when Zone::AAAA
|
||||
return Response::Error.new "invalid ipv6" unless Zone.is_ipv6_address_valid? address
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
require "json"
|
||||
require "uuid"
|
||||
require "uuid/json"
|
||||
|
||||
class DNSManager::Storage::Domain
|
||||
include JSON::Serializable
|
||||
|
||||
property name : String
|
||||
property share_key : String? = nil
|
||||
property transfer_key : String? = nil
|
||||
|
||||
# Users may have many domains, and a domain may have many owners.
|
||||
property owners = [] of UserDataID
|
||||
|
||||
def initialize(@name, share_key = nil, transfer_key = nil, owners = [] of UserDataID)
|
||||
end
|
||||
|
||||
def_clone
|
||||
end
|
|
@ -7,6 +7,8 @@ class DNSManager::Storage::Token
|
|||
property domain : String
|
||||
property rrid : UInt32
|
||||
|
||||
def_clone
|
||||
|
||||
def initialize(@domain, @rrid)
|
||||
@uuid = UUID.random.to_s
|
||||
end
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
require "json"
|
||||
require "uuid"
|
||||
require "uuid/json"
|
||||
|
||||
alias UserDataID = UInt32
|
||||
|
||||
class DNSManager::Storage::UserData
|
||||
include JSON::Serializable
|
||||
|
||||
property uid : UserDataID
|
||||
|
||||
# Users may have many domains, and a domain can have many owners.
|
||||
property domains = [] of String
|
||||
|
||||
property admin : Bool = false
|
||||
|
||||
def initialize(@uid)
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
alias UserDataID = UInt32
|
|
@ -35,6 +35,8 @@ class DNSManager::Storage::Zone
|
|||
def initialize(@domain)
|
||||
end
|
||||
|
||||
def_clone
|
||||
|
||||
alias Error = String
|
||||
|
||||
# Store a Resource Record: A, AAAA, TXT, PTR, CNAME…
|
||||
|
@ -50,6 +52,7 @@ class DNSManager::Storage::Zone
|
|||
NS: NS,
|
||||
CNAME: CNAME,
|
||||
MX: MX,
|
||||
CAA: CAA,
|
||||
SRV: SRV,
|
||||
|
||||
# Special resource records, which actually are TXT records.
|
||||
|
@ -96,6 +99,8 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class SOA < ResourceRecord
|
||||
def_clone
|
||||
|
||||
# Start of Authority
|
||||
property mname : String # Master Name Server for the zone.
|
||||
property rname : String # admin email address john.doe@example.com => john\.doe.example.com
|
||||
|
@ -159,6 +164,8 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class A < ResourceRecord
|
||||
def_clone
|
||||
|
||||
def get_errors : Array(Error)
|
||||
errors = [] of Error
|
||||
|
||||
|
@ -179,6 +186,8 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class AAAA < ResourceRecord
|
||||
def_clone
|
||||
|
||||
def get_errors : Array(Error)
|
||||
errors = [] of Error
|
||||
|
||||
|
@ -199,6 +208,8 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class TXT < ResourceRecord
|
||||
def_clone
|
||||
|
||||
def get_errors : Array(Error)
|
||||
errors = [] of Error
|
||||
|
||||
|
@ -224,6 +235,8 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class PTR < ResourceRecord
|
||||
def_clone
|
||||
|
||||
def get_errors : Array(Error)
|
||||
errors = [] of Error
|
||||
|
||||
|
@ -252,6 +265,8 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class NS < ResourceRecord
|
||||
def_clone
|
||||
|
||||
def get_errors : Array(Error)
|
||||
errors = [] of Error
|
||||
|
||||
|
@ -268,6 +283,8 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class CNAME < ResourceRecord
|
||||
def_clone
|
||||
|
||||
def get_errors : Array(Error)
|
||||
errors = [] of Error
|
||||
|
||||
|
@ -289,7 +306,7 @@ class DNSManager::Storage::Zone
|
|||
# TODO: verifications + print.
|
||||
# baguette.netlib.re. 3600 IN TXT "v=spf1 a mx ip4:<IP> a:mail.baguette.netlib.re ~all"
|
||||
class SPF < ResourceRecord
|
||||
|
||||
def_clone
|
||||
|
||||
# SPF mechanisms are about policy, which comes with the form of a single character:
|
||||
# - '?' means no policy (neutral),
|
||||
|
@ -332,6 +349,8 @@ class DNSManager::Storage::Zone
|
|||
property t : Type # type of modifier
|
||||
property v : String # value
|
||||
|
||||
def_clone
|
||||
|
||||
def initialize(@t, @v)
|
||||
end
|
||||
|
||||
|
@ -362,6 +381,8 @@ class DNSManager::Storage::Zone
|
|||
INCLUDE # include foreign SPF policy
|
||||
end
|
||||
|
||||
def_clone
|
||||
|
||||
property q : Qualifier = Qualifier::Pass
|
||||
property t : Type # type of mechanism
|
||||
property v : String # value
|
||||
|
@ -484,6 +505,7 @@ class DNSManager::Storage::Zone
|
|||
|
||||
# TODO
|
||||
class DKIM < ResourceRecord
|
||||
def_clone
|
||||
enum Version
|
||||
DKIM1
|
||||
end
|
||||
|
@ -508,6 +530,8 @@ class DNSManager::Storage::Zone
|
|||
io << "v=#{v};h=#{h.to_s.downcase};k=#{k.to_s.downcase};p=#{p}"
|
||||
io << ";n=#{n}" unless n == ""
|
||||
end
|
||||
|
||||
def_clone
|
||||
end
|
||||
|
||||
property dkim : DKIMProperties
|
||||
|
@ -542,6 +566,7 @@ class DNSManager::Storage::Zone
|
|||
|
||||
# TODO
|
||||
class DMARC < ResourceRecord
|
||||
def_clone
|
||||
enum Version
|
||||
DMARC1
|
||||
end
|
||||
|
@ -551,6 +576,8 @@ class DNSManager::Storage::Zone
|
|||
property mail : String
|
||||
property limit : UInt32? = nil
|
||||
|
||||
def_clone
|
||||
|
||||
def to_s(io : IO)
|
||||
io << "#{mail}"
|
||||
if l = @limit
|
||||
|
@ -596,6 +623,8 @@ class DNSManager::Storage::Zone
|
|||
property rf : Array(ReportFormat)? = nil
|
||||
property ri : UInt32
|
||||
|
||||
def_clone
|
||||
|
||||
def initialize(@p, @sp, @v, @adkim, @aspf, @pct, @fo, @rua, @ruf, @rf, @ri)
|
||||
end
|
||||
|
||||
|
@ -693,7 +722,66 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
end
|
||||
|
||||
class CAA < ResourceRecord
|
||||
def_clone
|
||||
|
||||
enum Tag
|
||||
ISSUE
|
||||
ISSUEWILD
|
||||
IODEF
|
||||
CONTACTEMAIL
|
||||
CONTACTPHONE
|
||||
end
|
||||
|
||||
class CAAProperties
|
||||
include JSON::Serializable
|
||||
|
||||
property flag : UInt8 = 0
|
||||
property tag : Tag = Tag::ISSUE
|
||||
property value : String = ""
|
||||
|
||||
def_clone
|
||||
|
||||
def initialize(@flag, @tag, @value)
|
||||
end
|
||||
end
|
||||
|
||||
property caa : CAAProperties
|
||||
|
||||
def initialize(@name, @ttl, @target, flag, tag, value)
|
||||
@rrtype = "CAA"
|
||||
@caa = CAAProperties.new flag, tag, value
|
||||
end
|
||||
|
||||
def to_s(io : IO)
|
||||
io << "(#{ "%4d" % @rrid }) "
|
||||
io << "#{ "%30s" % @name} #{ "%6d" % @ttl} CAA "
|
||||
io << "#{ "%3s" % @caa.flag} #{ "%15s" % @caa.tag} #{@caa.value}\n"
|
||||
end
|
||||
|
||||
def to_bind9(io : IO)
|
||||
io << "#{@name} #{@ttl} IN CAA #{@caa.flag} #{@caa.tag.to_s.downcase} #{@caa.value}\n"
|
||||
end
|
||||
|
||||
def get_errors : Array(Error)
|
||||
errors = [] of Error
|
||||
|
||||
unless Zone.is_subdomain_valid? @name
|
||||
errors << "CAA invalid subdomain: #{@name}"
|
||||
end
|
||||
|
||||
if @ttl < Zone.ttl_limit_min
|
||||
errors << "CAA invalid ttl: #{@ttl}, shouldn't be less than #{Zone.ttl_limit_min}"
|
||||
end
|
||||
|
||||
# TODO: rest of the errors.
|
||||
|
||||
errors
|
||||
end
|
||||
end
|
||||
|
||||
class MX < ResourceRecord
|
||||
def_clone
|
||||
property priority : UInt32 = 10
|
||||
def initialize(@name, @ttl, @target, @priority = 10)
|
||||
@rrtype = "MX"
|
||||
|
@ -729,6 +817,7 @@ class DNSManager::Storage::Zone
|
|||
end
|
||||
|
||||
class SRV < ResourceRecord
|
||||
def_clone
|
||||
property port : UInt16
|
||||
property protocol : String = "tcp"
|
||||
property priority : UInt32 = 10
|
||||
|
|
Loading…
Reference in New Issue