From 61bace491cc60e81e24f725ba37fd056906a8e33 Mon Sep 17 00:00:00 2001 From: Karchnu Date: Sat, 27 Apr 2024 05:48:28 +0200 Subject: [PATCH] Ownership: full overhaul in progress. --- src/main.cr | 2 +- src/requests/admin.cr | 7 +- src/storage.cr | 142 ++++++++++++++++++--------------------- src/storage/domain.cr | 17 +++++ src/storage/user_data.cr | 19 ------ 5 files changed, 87 insertions(+), 100 deletions(-) create mode 100644 src/storage/domain.cr delete mode 100644 src/storage/user_data.cr diff --git a/src/main.cr b/src/main.cr index 26abe52..46d7619 100644 --- a/src/main.cr +++ b/src/main.cr @@ -53,7 +53,7 @@ class DNSManager::Service < IPC def initialize(@configuration) super() - @storage = DNSManager::Storage.new @configuration.storage_directory, @configuration.recreate_indexes + @storage = DNSManager::Storage.new self, @configuration.storage_directory, @configuration.recreate_indexes @logged_users = Hash(Int32, AuthD::User::Public).new diff --git a/src/requests/admin.cr b/src/requests/admin.cr index afc26f9..6861996 100644 --- a/src/requests/admin.cr +++ b/src/requests/admin.cr @@ -77,9 +77,10 @@ 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.generate_zonefile @domain + # TODO: verify user must be admin. + #udata = dnsmanagerd.storage.get_user_data user + #return Response::InsufficientRights.new unless udata.admin + #dnsmanagerd.storage.generate_zonefile @domain end end DNSManager.requests << GenerateZoneFile diff --git a/src/storage.cr b/src/storage.cr index 8c56419..6742a15 100644 --- a/src/storage.cr +++ b/src/storage.cr @@ -5,9 +5,14 @@ require "baguette-crystal-base" require "dodb" +alias UserDataID = UInt32 + class DNSManager::Storage - getter user_data : DODB::CachedDataBase(UserData) - getter user_data_by_uid : DODB::Index(UserData) + 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) @@ -19,9 +24,26 @@ class DNSManager::Storage getter root : String getter zonefiledir : String - def initialize(@root : String, reindex : Bool = false) - @user_data = DODB::CachedDataBase(UserData).new "#{@root}/user-data" - @user_data_by_uid = @user_data.new_index "uid", &.uid.to_s + getter dnsmanagerd : DNSManager::Service + + def initialize(@dnsmanagerd, @root : String, reindex : Bool = false) + @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" @@ -34,8 +56,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,32 +66,6 @@ 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 get_user_data(user : ::AuthD::User::Public) - get_user_data user.uid - end - - def update_user_data(user_data : UserData) - user_data_by_uid.update_or_create user_data.uid.to_s, user_data - end - - def ensure_user_data(user_id : 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 - end - - user_data - end - # Only an admin can access this function. def generate_all_zonefiles() : IPC::JSON Baguette::Log.info "writing all zone files in #{@zonefiledir}/" @@ -96,9 +92,9 @@ class DNSManager::Storage def get_generated_zonefile(user_id : UserDataID, domain : String) : IPC::JSON - user_data = user_must_exist! user_id + 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,7 +106,7 @@ class DNSManager::Storage user_id : UserDataID, domain : String) : IPC::JSON - user_data = user_must_exist! user_id + user_must_exist! user_id # Prevent future very confusing errors. domain = domain.downcase @@ -142,9 +138,8 @@ 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. + @domains << DomainInfo.new domain, owners: [user_id] # Add the new zone in the database. zones_by_domain.update_or_create domain, default_zone @@ -153,7 +148,7 @@ class DNSManager::Storage end def add_or_update_zone(user_id : UserDataID, zone : Zone) : IPC::JSON - user_data = user_must_exist! user_id + user_must_exist! user_id # Test zone validity. if errors = zone.get_errors? @@ -163,13 +158,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 << DomainInfo.new zone.domain end # Add -or replace- the zone. @@ -179,9 +171,9 @@ class DNSManager::Storage end def add_rr(user_id : UserDataID, domain : String, rr : Zone::ResourceRecord) : IPC::JSON - user_data = user_must_exist! user_id + 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,9 +198,9 @@ class DNSManager::Storage end def update_rr(user_id : UserDataID, domain : String, rr : Zone::ResourceRecord) : IPC::JSON - user_data = user_must_exist! user_id + 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 @@ -228,9 +220,9 @@ class DNSManager::Storage end def delete_rr(user_id : UserDataID, domain : String, rrid : UInt32) : IPC::JSON - user_data = user_must_exist! user_id + 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_not_readonly! rrid @@ -246,15 +238,12 @@ class DNSManager::Storage end def delete_domain(user_id : UserDataID, domain : String) : IPC::JSON - user_data = user_must_exist! user_id + user_must_exist! user_id zone_must_exist! domain - user_should_own! user_data, domain + user_should_own! user_id, domain # 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 + domains_by_name.delete domain # Remove the related zone and their registered tokens. zones_by_domain.delete domain @@ -264,14 +253,14 @@ class DNSManager::Storage 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 + @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" @@ -289,12 +278,14 @@ class DNSManager::Storage 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) + domains_by_owners.get(user_id.to_s).each do |d| + end # Remove the user's domains. user_data.domains.each do |domain| tokens_by_domain.delete domain @@ -302,15 +293,12 @@ class DNSManager::Storage rescue e Baguette::Log.error "while removing a domain: #{e}" end - - # Remove the user. - user_data_by_uid.delete user_data.uid.to_s end - def delete_user_data(dnsmanagerd : DNSManager::Service, user_id : UserDataID, user_to_delete : UserDataID?) : IPC::JSON + def delete_user_data(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 + user_must_be_admin! user_id Baguette::Log.info "Admin #{user_id} removes data of user #{u}." user_must_exist! u else @@ -328,16 +316,13 @@ class DNSManager::Storage Response::DomainList.new user_data.domains 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.uid 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,8 +331,11 @@ class DNSManager::Storage zone end - def user_should_own!(user_data : UserData, domain : String) - unless user_data.domains.includes?(domain) || user_data.admin + # 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) + d = domains_by_name.get domain + unless d.includes? user_id raise NoOwnershipException.new end end @@ -355,7 +343,7 @@ class DNSManager::Storage 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 diff --git a/src/storage/domain.cr b/src/storage/domain.cr new file mode 100644 index 0000000..63f6039 --- /dev/null +++ b/src/storage/domain.cr @@ -0,0 +1,17 @@ +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 +end diff --git a/src/storage/user_data.cr b/src/storage/user_data.cr deleted file mode 100644 index 76bd587..0000000 --- a/src/storage/user_data.cr +++ /dev/null @@ -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