From 33b47766e56360e754f09e799925e8a76e50e4f6 Mon Sep 17 00:00:00 2001 From: Philippe Pittoli Date: Tue, 13 Jun 2023 23:40:34 +0200 Subject: [PATCH] s/shared key/secret key/ + new bootstrap request + some cleaning. --- makefile | 10 ++- src/authd.cr | 4 +- src/authd/client.cr | 19 ++++ src/better-parser.cr | 22 +++-- src/client.cr | 33 +++++-- src/network.cr | 2 +- src/requests/admin.cr | 37 ++++++++ src/requests/login.cr | 2 +- src/server.cr | 204 +----------------------------------------- src/service.cr | 196 ++++++++++++++++++++++++++++++++++++++++ 10 files changed, 303 insertions(+), 226 deletions(-) create mode 100644 src/service.cr diff --git a/makefile b/makefile index ed9a79b..18c8af0 100644 --- a/makefile +++ b/makefile @@ -5,7 +5,6 @@ build: NAME ?= John EMAIL ?= john@example.com -PHONE ?= 0707070707 PASSWORD_FILE ?= /tmp/PASSWORD DATA_DIRECTORY ?= /tmp/DATA-AUTHD @@ -16,11 +15,18 @@ setup: run-authd: setup ./bin/authd -k /tmp/PASSWORD -R -E --storage $(DATA_DIRECTORY) +# First user always is the admin. +add-first-user: + ./bin/authc bootstrap $(NAME) $(EMAIL) + add-user: - ./bin/authc user add $(NAME) $(EMAIL) $(PHONE) -k $(PASSWORD_FILE) + ./bin/authc user add $(NAME) $(EMAIL) print-messages: cat src/requests/*.cr | ./bin/get-messages.awk print-message-numbers: make print-messages | grep -E "^[0-9]" | sort -n + +print-messages-without-comments: + make print-messages | grep -vE '^[[:blank:]]+#' diff --git a/src/authd.cr b/src/authd.cr index 776603f..9e4ea3f 100644 --- a/src/authd.cr +++ b/src/authd.cr @@ -21,8 +21,8 @@ class Baguette::Configuration property login : String? = nil property pass : String? = nil - property shared_key : String = "nico-nico-nii" # Default authd key, as per the specs. :eyes: - property shared_key_file : String? = nil + property secret_key : String = "nico-nico-nii" # Default authd key, as per the specs. :eyes: + property secret_key_file : String? = nil def initialize end diff --git a/src/authd/client.cr b/src/authd/client.cr index de2e9f6..ccb3b18 100644 --- a/src/authd/client.cr +++ b/src/authd/client.cr @@ -98,6 +98,25 @@ module AuthD end end + def bootstrap(login : String, + password : String, + email : String, + profile : Hash(String, ::JSON::Any)? = nil) : ::AuthD::User::Public | Exception + + send_now Request::BootstrapFirstAdmin.new login, password, email, profile + + response = AuthD.responses.parse_ipc_json read + + case response + when Response::UserAdded + response.user + when Response::Error + raise Exception.new response.reason + else + Exception.new + end + end + def validate_user(login : String, activation_key : String) : ::AuthD::User::Public | Exception send_now Request::ValidateUser.new login, activation_key diff --git a/src/better-parser.cr b/src/better-parser.cr index 6562aa4..120926b 100644 --- a/src/better-parser.cr +++ b/src/better-parser.cr @@ -1,12 +1,5 @@ require "option_parser" -opt_authd_admin = -> (parser : OptionParser) { - parser.on "-k file", "--key-file file", "Read the authd shared key from a file." do |file| - Context.shared_key = File.read(file).chomp - Baguette::Log.info "Key for admin operations: #{Context.shared_key}." - end -} - # frequently used functions opt_authd_login = -> (parser : OptionParser) { parser.on "-l LOGIN", "--login LOGIN", "Authd user login." do |login| @@ -76,6 +69,16 @@ parser = OptionParser.new do |parser| exit 0 end + parser.on "bootstrap", "Add the first user (an admin)." do + parser.banner = "Usage: bootstrap login email [-P profile]" + Baguette::Log.info "Bootstrapping the first user (admin) to the DB." + Context.command = "bootstrap" + opt_profile.call parser + opt_help.call parser + # login email + unrecognized_args_to_context_args.call parser, 2 + end + parser.on "user", "Operations on users." do parser.banner = "Usage: user [add | mod | delete | validate | search | get | recover | register ]" @@ -83,7 +86,6 @@ parser = OptionParser.new do |parser| parser.banner = "usage: user add login email [-P profile] [opt]" Baguette::Log.info "Adding a user to the DB." Context.command = "user-add" - opt_authd_admin.call parser opt_profile.call parser opt_help.call parser # login email @@ -94,7 +96,6 @@ parser = OptionParser.new do |parser| parser.banner = "Usage: user mod userid [-e email|-P profile] [opt]" Baguette::Log.info "Modify a user account." Context.command = "user-mod" - opt_authd_admin.call parser opt_email.call parser opt_profile.call parser opt_help.call parser @@ -108,7 +109,6 @@ parser = OptionParser.new do |parser| Context.command = "user-delete" # You can either be the owner of the account, or an admin. opt_authd_login.call parser - opt_authd_admin.call parser opt_help.call parser # userid unrecognized_args_to_context_args.call parser, 1 @@ -178,7 +178,6 @@ permission list: none read edit admin END Baguette::Log.info "Set permissions." Context.command = "permission-set" - opt_authd_admin.call parser opt_help.call parser # userid application resource permission unrecognized_args_to_context_args.call parser, 4 @@ -193,7 +192,6 @@ permission list: none read edit admin END Baguette::Log.info "Check permissions." Context.command = "permission-check" - opt_authd_admin.call parser opt_help.call parser # userid application resource unrecognized_args_to_context_args.call parser, 3 diff --git a/src/client.cr b/src/client.cr index 87ec462..c24b0ed 100644 --- a/src/client.cr +++ b/src/client.cr @@ -7,7 +7,6 @@ class Context class_property authd_login = "undef" # undef authd user class_property authd_pass = "undef" # undef authd user password - class_property shared_key = "undef" # undef authd user password # # Properties to select what to display when printing a deal. # class_property print_title = true @@ -52,14 +51,18 @@ class Actions property authd : AuthD::Client def initialize(@authd) - @the_call["user-add"] = ->user_add - @the_call["user-mod"] = ->user_mod - @the_call["user-registration"] = ->user_registration # Do not require admin priviledges. - @the_call["user-delete"] = ->user_deletion # Do not require admin priviledges. - @the_call["user-get"] = ->user_get # Do not require authentication. + @the_call["user-registration"] = ->user_registration @the_call["user-validation"] = ->user_validation # Do not require authentication. @the_call["user-recovery"] = ->user_recovery # Do not require authentication. - @the_call["user-search"] = ->user_search # Do not require authentication. + @the_call["user-delete"] = ->user_deletion # Do not require admin priviledges. + @the_call["user-get"] = ->user_get + @the_call["user-search"] = ->user_search + + @the_call["bootstrap"] = ->bootstrap + + # Require admin privileges. + @the_call["user-add"] = ->user_add + @the_call["user-mod"] = ->user_mod @the_call["permission-set"] = ->permission_set @the_call["permission-check"] = ->permission_check @@ -101,6 +104,21 @@ class Actions puts "error: #{e.message}" end + def bootstrap + puts "Bootstrap" + args = Context.args.not_nil! + login, email = args[0..1] + profile = Context.user_profile + + password = Actions.ask_password + exit 1 unless password + + pp! authd.bootstrap login, password.not_nil!, email, profile + rescue e : AuthD::Exception + puts "error: #{e.message}" + end + + # TODO def user_mod args = Context.args.not_nil! @@ -188,7 +206,6 @@ def main # Authd connection. authd = AuthD::Client.new - authd.key = Context.shared_key if Context.shared_key != "undef" # Authd token. # FIXME: not sure about getting the token, it seems not used elsewhere. diff --git a/src/network.cr b/src/network.cr index af88e99..d988e26 100644 --- a/src/network.cr +++ b/src/network.cr @@ -1,7 +1,7 @@ require "ipc" require "ipc/json" require "./authd.cr" -require "./server.cr" # To load AuthD::Service definition. +require "./service.cr" # To load AuthD::Service definition. class IPC::JSON def handle(service : AuthD::Service, fd : Int32) diff --git a/src/requests/admin.cr b/src/requests/admin.cr index 6e5d861..82e799c 100644 --- a/src/requests/admin.cr +++ b/src/requests/admin.cr @@ -46,4 +46,41 @@ class AuthD::Request end end AuthD.requests << AddUser + + IPC::JSON.message BootstrapFirstAdmin, 13 do + property login : String + property password : String + property email : String? = nil + property profile : Hash(String, JSON::Any)? = nil + + def initialize(@login, @password, @email, @profile = nil) + end + + def handle(authd : AuthD::Service, fd : Int32) + # Check if there already is a registered user. + if authd.users.to_a.size > 0 + return Response::Error.new "already users in the database" + end + + password_hash = authd.hash_password @password + + uid = authd.new_uid + + user = User.new uid, @login, password_hash + user.contact.email = @email unless @email.nil? + user.admin = true + + @profile.try do |profile| + user.profile = profile + end + + # We consider adding the user as a registration. + user.date_registration = Time.local + + authd.users << user + authd.new_uid_commit uid + Response::UserAdded.new user.to_public + end + end + AuthD.requests << BootstrapFirstAdmin end diff --git a/src/requests/login.cr b/src/requests/login.cr index 56bbb4f..3397d8d 100644 --- a/src/requests/login.cr +++ b/src/requests/login.cr @@ -32,7 +32,7 @@ class AuthD::Request # On successuful connection: store the authenticated user in a hash. authd.logged_users[fd] = user.to_public - Response::Login.new (token.to_s authd.configuration.shared_key), user.uid + Response::Login.new (token.to_s authd.configuration.secret_key), user.uid end end AuthD.requests << Login diff --git a/src/server.cr b/src/server.cr index 544c472..7546618 100644 --- a/src/server.cr +++ b/src/server.cr @@ -1,200 +1,4 @@ -require "./authd.cr" - -extend AuthD - -class Baguette::Configuration - class Auth < IPC - property recreate_indexes : Bool = false - property storage : String = "storage" - property registrations : Bool = false - property require_email : Bool = false - property activation_template : String = "email-activation" - property recovery_template : String = "email-recovery" - property mailer_exe : String = "mailer" - property read_only_profile_keys : Array(String) = Array(String).new - - property print_password_recovery_parameters : Bool = false - end -end - -# Provides a JWT-based authentication scheme for service-specific users. -class AuthD::Service < IPC - property configuration : Baguette::Configuration::Auth - - # DB and its indexes. - property users : DODB::DataBase(User) - property users_per_uid : DODB::Index(User) - property users_per_login : DODB::Index(User) - - property logged_users : Hash(Int32, AuthD::User::Public) - - # #{@configuration.storage}/last_used_uid - property last_uid_file : String - - def initialize(@configuration) - super() - - @users = DODB::DataBase(User).new @configuration.storage - @users_per_uid = @users.new_index "uid", &.uid.to_s - @users_per_login = @users.new_index "login", &.login - - @last_uid_file = "#{@configuration.storage}/last_used_uid" - - @logged_users = Hash(Int32, AuthD::User::Public).new - - if @configuration.recreate_indexes - @users.reindex_everything! - end - - self.timer @configuration.ipc_timer - self.service_init "auth" - end - - def hash_password(password : String) : String - digest = OpenSSL::Digest.new "sha256" - digest << password - digest.hexfinal - end - - # new_uid reads the last given UID and returns it incremented. - # Splitting the retrieval and record of new user ids allows to - # only increment when an user fully registers, thus avoiding a - # Denial of Service attack. - # - # WARNING: to record this new UID, new_uid_commit must be called. - # WARNING: new_uid isn't thread safe. - def new_uid - begin - uid = File.read(@last_uid_file).to_i - rescue - uid = 999 - end - - uid += 1 - end - - # new_uid_commit records the new UID. - # WARNING: new_uid_commit isn't thread safe. - def new_uid_commit(uid : Int) - File.write @last_uid_file, uid.to_s - end - - def get_logged_user?(fd : Int32) - @logged_users[fd]? - end - - # Instead of just getting the public view of a logged user, - # get the actual User instance. - def get_logged_user_full?(fd : Int32) - if u = @logged_users[fd]? - user? u.uid - end - end - - def user?(uid_or_login : UserID) - if uid_or_login.is_a? Int32 - @users_per_uid.get? uid_or_login.to_s - else - @users_per_login.get? uid_or_login - end - end - - def handle_request(event : IPC::Event) - request_start = Time.utc - - array = event.message.not_nil! - slice = Slice.new array.to_unsafe, array.size - message = IPCMessage::TypedMessage.deserialize slice - request = AuthD.requests.parse_ipc_json message.not_nil! - - if request.nil? - raise "unknown request type" - end - - request_name = request.class.name.sub /^AuthD::Request::/, "" - Baguette::Log.debug "<< #{request_name}" - - response = begin - request.handle self, event.fd - rescue e : UserNotFound - Baguette::Log.error "#{request_name} user not found" - AuthD::Response::Error.new "authorization error" - rescue e : AuthenticationInfoLacking - Baguette::Log.error "#{request_name} lacking authentication info" - AuthD::Response::Error.new "authorization error" - rescue e : AdminAuthorizationException - Baguette::Log.error "#{request_name} admin authentication failed" - AuthD::Response::Error.new "authorization error" - rescue e - Baguette::Log.error "#{request_name} generic error #{e}" - AuthD::Response::Error.new "unknown error" - end - - # If clients sent requests with an “id” field, it is copied - # in the responses. Allows identifying responses easily. - response.id = request.id - - schedule event.fd, response - - duration = Time.utc - request_start - - response_name = response.class.name.sub /^AuthD::Response::/, "" - - if response.is_a? AuthD::Response::Error - Baguette::Log.warning ">> #{response_name} (#{response.reason})" - else - Baguette::Log.debug ">> #{response_name} (Total duration: #{duration})" - end - end - - def get_user_from_token(token : String) - token_payload = Token.from_s(@configuration.shared_key, token) - - @users_per_uid.get? token_payload.uid.to_s - end - - def run - Baguette::Log.title "Starting authd" - - Baguette::Log.info "(mailer) Email activation template: #{@configuration.activation_template}" - Baguette::Log.info "(mailer) Email recovery template: #{@configuration.recovery_template}" - - self.loop do |event| - case event.type - when LibIPC::EventType::Timer - Baguette::Log.debug "Timer" if @configuration.print_ipc_timer - - when LibIPC::EventType::MessageRx - Baguette::Log.debug "Received message from #{event.fd}" if @configuration.print_ipc_message_received - begin - handle_request event - rescue e - Baguette::Log.error "#{e.message}" - # send event.fd, Response::Error.new e.message - end - - when LibIPC::EventType::MessageTx - Baguette::Log.debug "Message sent to #{event.fd}" if @configuration.print_ipc_message_sent - - when LibIPC::EventType::Connection - Baguette::Log.debug "Connection from #{event.fd}" if @configuration.print_ipc_connection - when LibIPC::EventType::Disconnection - Baguette::Log.debug "Disconnection from #{event.fd}" if @configuration.print_ipc_disconnection - @logged_users.delete event.fd - else - Baguette::Log.error "Not implemented behavior for event: #{event}" - if event.responds_to?(:fd) - fd = event.fd - Baguette::Log.warning "closing #{fd}" - close fd - @logged_users.delete fd - end - end - end - - end -end - +require "./service.cr" begin simulation, no_configuration, configuration_file = Baguette::Configuration.option_parser @@ -209,8 +13,8 @@ begin Baguette::Context.verbosity = configuration.verbosity - if key_file = configuration.shared_key_file - configuration.shared_key = File.read(key_file).chomp + if key_file = configuration.secret_key_file + configuration.secret_key = File.read(key_file).chomp end OptionParser.parse do |parser| @@ -221,7 +25,7 @@ begin end parser.on "-k file", "--key-file file", "JWT key file" do |file_name| - configuration.shared_key = File.read(file_name).chomp + configuration.secret_key = File.read(file_name).chomp end parser.on "-R", "--allow-registrations", "Allow user registration." do diff --git a/src/service.cr b/src/service.cr new file mode 100644 index 0000000..fe5ef6c --- /dev/null +++ b/src/service.cr @@ -0,0 +1,196 @@ +require "./authd.cr" + +extend AuthD + +class Baguette::Configuration + class Auth < IPC + property recreate_indexes : Bool = false + property storage : String = "storage" + property registrations : Bool = false + property require_email : Bool = false + property activation_template : String = "email-activation" + property recovery_template : String = "email-recovery" + property mailer_exe : String = "mailer" + property read_only_profile_keys : Array(String) = Array(String).new + + property print_password_recovery_parameters : Bool = false + end +end + +# Provides a JWT-based authentication scheme for service-specific users. +class AuthD::Service < IPC + property configuration : Baguette::Configuration::Auth + + # DB and its indexes. + property users : DODB::DataBase(User) + property users_per_uid : DODB::Index(User) + property users_per_login : DODB::Index(User) + + property logged_users : Hash(Int32, AuthD::User::Public) + + # #{@configuration.storage}/last_used_uid + property last_uid_file : String + + def initialize(@configuration) + super() + + @users = DODB::DataBase(User).new @configuration.storage + @users_per_uid = @users.new_index "uid", &.uid.to_s + @users_per_login = @users.new_index "login", &.login + + @last_uid_file = "#{@configuration.storage}/last_used_uid" + + @logged_users = Hash(Int32, AuthD::User::Public).new + + if @configuration.recreate_indexes + @users.reindex_everything! + end + + self.timer @configuration.ipc_timer + self.service_init "auth" + end + + def hash_password(password : String) : String + digest = OpenSSL::Digest.new "sha256" + digest << password + digest.hexfinal + end + + # new_uid reads the last given UID and returns it incremented. + # Splitting the retrieval and record of new user ids allows to + # only increment when an user fully registers, thus avoiding a + # Denial of Service attack. + # + # WARNING: to record this new UID, new_uid_commit must be called. + # WARNING: new_uid isn't thread safe. + def new_uid + begin + uid = File.read(@last_uid_file).to_i + rescue + uid = 999 + end + + uid += 1 + end + + # new_uid_commit records the new UID. + # WARNING: new_uid_commit isn't thread safe. + def new_uid_commit(uid : Int) + File.write @last_uid_file, uid.to_s + end + + def get_logged_user?(fd : Int32) + @logged_users[fd]? + end + + # Instead of just getting the public view of a logged user, + # get the actual User instance. + def get_logged_user_full?(fd : Int32) + if u = @logged_users[fd]? + user? u.uid + end + end + + def user?(uid_or_login : UserID) + if uid_or_login.is_a? Int32 + @users_per_uid.get? uid_or_login.to_s + else + @users_per_login.get? uid_or_login + end + end + + def handle_request(event : IPC::Event) + request_start = Time.utc + + array = event.message.not_nil! + slice = Slice.new array.to_unsafe, array.size + message = IPCMessage::TypedMessage.deserialize slice + request = AuthD.requests.parse_ipc_json message.not_nil! + + if request.nil? + raise "unknown request type" + end + + request_name = request.class.name.sub /^AuthD::Request::/, "" + Baguette::Log.debug "<< #{request_name}" + + response = begin + request.handle self, event.fd + rescue e : UserNotFound + Baguette::Log.error "#{request_name} user not found" + AuthD::Response::Error.new "authorization error" + rescue e : AuthenticationInfoLacking + Baguette::Log.error "#{request_name} lacking authentication info" + AuthD::Response::Error.new "authorization error" + rescue e : AdminAuthorizationException + Baguette::Log.error "#{request_name} admin authentication failed" + AuthD::Response::Error.new "authorization error" + rescue e + Baguette::Log.error "#{request_name} generic error #{e}" + AuthD::Response::Error.new "unknown error" + end + + # If clients sent requests with an “id” field, it is copied + # in the responses. Allows identifying responses easily. + response.id = request.id + + schedule event.fd, response + + duration = Time.utc - request_start + + response_name = response.class.name.sub /^AuthD::Response::/, "" + + if response.is_a? AuthD::Response::Error + Baguette::Log.warning ">> #{response_name} (#{response.reason})" + else + Baguette::Log.debug ">> #{response_name} (Total duration: #{duration})" + end + end + + def get_user_from_token(token : String) + token_payload = Token.from_s(@configuration.secret_key, token) + + @users_per_uid.get? token_payload.uid.to_s + end + + def run + Baguette::Log.title "Starting authd" + + Baguette::Log.info "(mailer) Email activation template: #{@configuration.activation_template}" + Baguette::Log.info "(mailer) Email recovery template: #{@configuration.recovery_template}" + + self.loop do |event| + case event.type + when LibIPC::EventType::Timer + Baguette::Log.debug "Timer" if @configuration.print_ipc_timer + + when LibIPC::EventType::MessageRx + Baguette::Log.debug "Received message from #{event.fd}" if @configuration.print_ipc_message_received + begin + handle_request event + rescue e + Baguette::Log.error "#{e.message}" + # send event.fd, Response::Error.new e.message + end + + when LibIPC::EventType::MessageTx + Baguette::Log.debug "Message sent to #{event.fd}" if @configuration.print_ipc_message_sent + + when LibIPC::EventType::Connection + Baguette::Log.debug "Connection from #{event.fd}" if @configuration.print_ipc_connection + when LibIPC::EventType::Disconnection + Baguette::Log.debug "Disconnection from #{event.fd}" if @configuration.print_ipc_disconnection + @logged_users.delete event.fd + else + Baguette::Log.error "Not implemented behavior for event: #{event}" + if event.responds_to?(:fd) + fd = event.fd + Baguette::Log.warning "closing #{fd}" + close fd + @logged_users.delete fd + end + end + end + + end +end