Ownership: full overhaul in progress.

This commit is contained in:
Karchnu 2024-04-27 05:48:28 +02:00
parent 9b6457f575
commit 61bace491c
5 changed files with 87 additions and 100 deletions

View File

@ -53,7 +53,7 @@ class DNSManager::Service < IPC
def initialize(@configuration) def initialize(@configuration)
super() 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 @logged_users = Hash(Int32, AuthD::User::Public).new

View File

@ -77,9 +77,10 @@ class DNSManager::Request
user = dnsmanagerd.get_logged_user event user = dnsmanagerd.get_logged_user event
return Response::ErrorUserNotLogged.new unless user return Response::ErrorUserNotLogged.new unless user
udata = dnsmanagerd.storage.get_user_data user # TODO: verify user must be admin.
return Response::InsufficientRights.new unless udata.admin #udata = dnsmanagerd.storage.get_user_data user
dnsmanagerd.storage.generate_zonefile @domain #return Response::InsufficientRights.new unless udata.admin
#dnsmanagerd.storage.generate_zonefile @domain
end end
end end
DNSManager.requests << GenerateZoneFile DNSManager.requests << GenerateZoneFile

View File

@ -5,9 +5,14 @@ require "baguette-crystal-base"
require "dodb" require "dodb"
alias UserDataID = UInt32
class DNSManager::Storage class DNSManager::Storage
getter user_data : DODB::CachedDataBase(UserData) getter domains : DODB::CachedDataBase(Domain)
getter user_data_by_uid : DODB::Index(UserData) 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 : DODB::CachedDataBase(Zone)
getter zones_by_domain : DODB::Index(Zone) getter zones_by_domain : DODB::Index(Zone)
@ -19,9 +24,26 @@ class DNSManager::Storage
getter root : String getter root : String
getter zonefiledir : String getter zonefiledir : String
def initialize(@root : String, reindex : Bool = false) getter dnsmanagerd : DNSManager::Service
@user_data = DODB::CachedDataBase(UserData).new "#{@root}/user-data"
@user_data_by_uid = @user_data.new_index "uid", &.uid.to_s 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 = DODB::CachedDataBase(Zone).new "#{@root}/zones"
@zones_by_domain = @zones.new_index "domain", &.domain @zones_by_domain = @zones.new_index "domain", &.domain
@tokens = DODB::CachedDataBase(Token).new "#{@root}/tokens" @tokens = DODB::CachedDataBase(Token).new "#{@root}/tokens"
@ -34,8 +56,8 @@ class DNSManager::Storage
Baguette::Log.info "storage initialized" Baguette::Log.info "storage initialized"
if reindex if reindex
Baguette::Log.debug "Reindexing user data..." Baguette::Log.debug "Reindexing domains..."
@user_data.reindex_everything! @domains.reindex_everything!
Baguette::Log.debug "Reindexing zones..." Baguette::Log.debug "Reindexing zones..."
@zones.reindex_everything! @zones.reindex_everything!
Baguette::Log.debug "Reindexing tokens..." Baguette::Log.debug "Reindexing tokens..."
@ -44,32 +66,6 @@ class DNSManager::Storage
end end
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. # Only an admin can access this function.
def generate_all_zonefiles() : IPC::JSON def generate_all_zonefiles() : IPC::JSON
Baguette::Log.info "writing all zone files in #{@zonefiledir}/" 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 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 zone = zone_must_exist! domain
user_should_own! user_data, zone.domain user_should_own! user_id, zone.domain
io = IO::Memory.new io = IO::Memory.new
zone.to_bind9 io zone.to_bind9 io
@ -110,7 +106,7 @@ class DNSManager::Storage
user_id : UserDataID, user_id : UserDataID,
domain : String) : IPC::JSON domain : String) : IPC::JSON
user_data = user_must_exist! user_id user_must_exist! user_id
# Prevent future very confusing errors. # Prevent future very confusing errors.
domain = domain.downcase domain = domain.downcase
@ -142,9 +138,8 @@ class DNSManager::Storage
# Actually write data on-disk. # Actually write data on-disk.
# #
# Add the domain to the user's domain. # Add the new domain.
user_data.domains << domain @domains << DomainInfo.new domain, owners: [user_id]
update_user_data user_data
# Add the new zone in the database. # Add the new zone in the database.
zones_by_domain.update_or_create domain, default_zone zones_by_domain.update_or_create domain, default_zone
@ -153,7 +148,7 @@ class DNSManager::Storage
end end
def add_or_update_zone(user_id : UserDataID, zone : Zone) : IPC::JSON 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. # Test zone validity.
if errors = zone.get_errors? if errors = zone.get_errors?
@ -163,13 +158,10 @@ class DNSManager::Storage
# Does the zone already exist? # Does the zone already exist?
if z = zones_by_domain.get? zone.domain if z = zones_by_domain.get? zone.domain
user_should_own! user_data, z.domain user_should_own! user_id, z.domain
else else
# Add the domain to the user's domain. # Add the domain to the user's domain.
user_data.domains << zone.domain domains << DomainInfo.new zone.domain
# Actually write data on-disk.
update_user_data user_data
end end
# Add -or replace- the zone. # Add -or replace- the zone.
@ -179,9 +171,9 @@ class DNSManager::Storage
end end
def add_rr(user_id : UserDataID, domain : String, rr : Zone::ResourceRecord) : IPC::JSON 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 zone = zone_must_exist! domain
user_should_own! user_data, domain user_should_own! user_id, domain
# Test RR validity. # Test RR validity.
rr.get_errors.tap do |errors| rr.get_errors.tap do |errors|
@ -206,9 +198,9 @@ class DNSManager::Storage
end end
def update_rr(user_id : UserDataID, domain : String, rr : Zone::ResourceRecord) : IPC::JSON 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 zone = zone_must_exist! domain
user_should_own! user_data, domain user_should_own! user_id, domain
zone.rr_not_readonly! rr.rrid zone.rr_not_readonly! rr.rrid
@ -228,9 +220,9 @@ class DNSManager::Storage
end end
def delete_rr(user_id : UserDataID, domain : String, rrid : UInt32) : IPC::JSON 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 zone = zone_must_exist! domain
user_should_own! user_data, domain user_should_own! user_id, domain
rr = zone.rr_not_readonly! rrid rr = zone.rr_not_readonly! rrid
@ -246,15 +238,12 @@ class DNSManager::Storage
end end
def delete_domain(user_id : UserDataID, domain : String) : IPC::JSON 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 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. # Remove this domain from the list of user's domains.
user_data.domains.delete domain domains_by_name.delete domain
# Update on-disk user data.
update_user_data user_data
# Remove the related zone and their registered tokens. # Remove the related zone and their registered tokens.
zones_by_domain.delete domain zones_by_domain.delete domain
@ -264,14 +253,14 @@ class DNSManager::Storage
end end
# Get all removed users from `authd`, list all their domains and remove their data from `dnsmanagerd`. # 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 def get_orphan_domains(user_id : UserDataID) : IPC::JSON
user_must_be_admin! dnsmanagerd, user_id user_must_be_admin! user_id
Baguette::Log.debug "list all orphan domains (long computation)" Baguette::Log.debug "list all orphan domains (long computation)"
orphans = [] of String orphans = [] of String
user_data.each do |user| user_data.each do |user|
begin begin
dnsmanagerd.authd.get_user? user.uid @dnsmanagerd.authd.get_user? user.uid
rescue e rescue e
Baguette::Log.warning "no authd info on user #{user.uid}: #{e} (removing this user)" 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" 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 def get_zone(user_id : UserDataID, domain : String) : IPC::JSON
user_data = user_must_exist! user_id user_data = user_must_exist! user_id
zone = zone_must_exist! domain zone = zone_must_exist! domain
user_should_own! user_data, domain user_should_own! user_id, domain
Response::Zone.new zone Response::Zone.new zone
end end
def wipe_user_data(user_data : UserData) def wipe_user_data(user_data : UserData)
domains_by_owners.get(user_id.to_s).each do |d|
end
# Remove the user's domains. # Remove the user's domains.
user_data.domains.each do |domain| user_data.domains.each do |domain|
tokens_by_domain.delete domain tokens_by_domain.delete domain
@ -302,15 +293,12 @@ class DNSManager::Storage
rescue e rescue e
Baguette::Log.error "while removing a domain: #{e}" Baguette::Log.error "while removing a domain: #{e}"
end end
# Remove the user.
user_data_by_uid.delete user_data.uid.to_s
end 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 = user_must_exist! user_id
user_data_to_delete = if u = user_to_delete 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}." Baguette::Log.info "Admin #{user_id} removes data of user #{u}."
user_must_exist! u user_must_exist! u
else else
@ -328,16 +316,13 @@ class DNSManager::Storage
Response::DomainList.new user_data.domains Response::DomainList.new user_data.domains
end end
def user_must_exist!(user_id : UserDataID) : UserData # TODO: is the user known from authd?
user_data = user_data_by_uid.get? user_id.to_s def user_must_exist!(user_id : UserDataID)
raise UnknownUserException.new unless user_data @dnsmanagerd.authd.get_user? user.uid
user_data
end end
def user_must_be_admin!(dnsmanagerd : DNSManager::Service, user_id : UserDataID) : UserData def user_must_be_admin!(user_id : UserDataID) : Nil
user_data = user_must_exist! user_id @dnsmanagerd.assert_permissions! user_id, "*", AuthD::User::PermissionLevel::Admin
dnsmanagerd.assert_permissions! user_id, "*", AuthD::User::PermissionLevel::Admin
user_data
end end
def zone_must_exist!(domain : String) : Zone def zone_must_exist!(domain : String) : Zone
@ -346,8 +331,11 @@ class DNSManager::Storage
zone zone
end end
def user_should_own!(user_data : UserData, domain : String) # Owning a domain means to be in the owners' list of the domain.
unless user_data.domains.includes?(domain) || user_data.admin # 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 raise NoOwnershipException.new
end end
end end
@ -355,7 +343,7 @@ class DNSManager::Storage
def new_token(user_id : UserDataID, domain : String, rrid : UInt32) : IPC::JSON def new_token(user_id : UserDataID, domain : String, rrid : UInt32) : IPC::JSON
user_data = user_must_exist! user_id user_data = user_must_exist! user_id
zone = zone_must_exist! domain zone = zone_must_exist! domain
user_should_own! user_data, domain user_should_own! user_id, domain
rr = zone.rr_must_exist! rrid rr = zone.rr_must_exist! rrid

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

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

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