Compare commits

...

16 Commits
master ... dev

Author SHA1 Message Date
Philippe PITTOLI b53b31b584 CAA entries. 2024-06-08 03:55:52 +02:00
Philippe PITTOLI 5cbcbdaa8f Removing a domain: domain may not have tokens. 2024-06-02 01:58:56 +02:00
Philippe PITTOLI de8beb0df8 New DODB API. 2024-06-01 03:35:53 +02:00
Philippe PITTOLI 7bee53d625 Adapt to the DODB cached mode. 2024-05-07 21:00:01 +02:00
Philippe PITTOLI 234259a9d0 Check if the user is admin with `is_admin?` function in `dnsmanagerd`. 2024-05-07 12:48:52 +02:00
Philippe PITTOLI 4923fb34f9 Use cached indexes. 2024-05-07 01:32:37 +02:00
Philippe PITTOLI 158d772727 User authentication: limit the number of domains sent in "Logged" to a hundred. 2024-04-28 17:16:06 +02:00
Philippe PITTOLI 723c1a83a0 Update expected messages. 2024-04-28 16:17:47 +02:00
Philippe PITTOLI ebd9bd75a5 Prevent a basic hack. 2024-04-28 16:17:28 +02:00
Philippe PITTOLI 4606f852cc Client: can be built again. Add a makefile rule to generate all zones. 2024-04-28 12:56:45 +02:00
Philippe PITTOLI 1f69839333 Only delete the domain when the user is the last owner. 2024-04-28 01:24:55 +02:00
Philippe PITTOLI a188d28f1d Ownership is now managed. 2024-04-27 23:10:01 +02:00
Philippe PITTOLI 8cafca13be WIP: ownership management. Some empty functions. 2024-04-27 20:08:34 +02:00
Karchnu a888551bbc Compatible with the webclient again. Full audit REQUIRED. 2024-04-27 18:30:49 +02:00
Karchnu 40fcc9c66e Complete overhaul of ownership management: add a domain db, remove user data db. 2024-04-27 17:32:22 +02:00
Karchnu 61bace491c Ownership: full overhaul in progress. 2024-04-27 05:48:28 +02:00
17 changed files with 454 additions and 183 deletions

View File

@ -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"

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

61
src/requests/ownership.cr Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

5
src/service.cr Normal file
View File

@ -0,0 +1,5 @@
require "ipc"
# Useful to enable the client to be built.
class DNSManager::Service < IPC
end

View File

@ -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

19
src/storage/domain.cr Normal file
View File

@ -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

View File

@ -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

View File

@ -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

1
src/storage/userid.cr Normal file
View File

@ -0,0 +1 @@
alias UserDataID = UInt32

View File

@ -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