require "./authd.cr" require "sodium" extend AuthD require "./configuration" class Array(T) def contains?(value : T) (self.select { |x| x == value }).size > 0 end end # WIP: select (dynamically) messages to mask module AuthD enum MESSAGE KEEPALIVE LOGIN # TODO end end alias IPCMESSAGE = Baguette::Configuration::IPC::MESSAGE alias AUTHMESSAGE = AuthD::MESSAGE # 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::Storage::Common(User) property users_per_uid : DODB::Trigger::IndexCached(User) property users_per_login : DODB::Trigger::IndexCached(User) property users_per_email : DODB::Trigger::IndexCached(User) property logged_users : Hash(Int32, AuthD::User::Public) # #{@configuration.storage_directory}/last_used_uid property last_uid_file : String def initialize(@configuration) super() @users = DODB::Storage::Common(User).new @configuration.storage_directory, 5000 @users_per_uid = @users.new_index "uid", &.uid.to_s @users_per_login = @users.new_index "login", &.login @users_per_email = @users.new_index "email" do |user| if mail = user.contact.email Base64.encode(mail).chomp else DODB.no_index end end @last_uid_file = "#{@configuration.storage_directory}/last_used_uid" @logged_users = Hash(Int32, AuthD::User::Public).new if @configuration.recreate_indexes Baguette::Log.info "Recreate indexes" @users.reindex_everything! end self.timer @configuration.ipc_timer self.service_init @configuration.service_name end def should_display?(value : AUTHMESSAGE) (@configuration.messages_to_mask.select { |x| x == value }).size == 0 end def should_display?(value : IPCMESSAGE) @configuration.ipc_messages_to_show.contains? value end def obsolete_hash_password(password : String) : String digest = OpenSSL::Digest.new "sha256" digest << password digest.hexfinal end def hash_password(password : String) : String pwhash = Sodium::Password::Hash.new hash = pwhash.create password pwhash.verify hash, password Base64.strict_encode hash 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 : UInt32 uid : UInt32 = begin File.read(@last_uid_file).to_u32 rescue 999.to_u32 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) : AuthD::User::Public? @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 # `log_user_info` provides a string composed from either the user # id in case the user was authenticated or the file descriptor of # the connection. def log_user_info(fd : Int32) : String if user = get_logged_user? fd "userid #{user.uid}" else "fd #{"%4d" % fd}" end end def user?(uid_or_login : UserID) if uid_or_login.is_a? UInt32 @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::/, "" connection_info_str = log_user_info event.fd response = begin request.handle self, event.fd rescue e : UserNotFound Baguette::Log.error "(#{connection_info_str}) #{request} user not found" AuthD::Response::Error.new "authorization error" rescue e : AuthenticationInfoLacking Baguette::Log.error "(#{connection_info_str}) #{request} lacking authentication info" AuthD::Response::Error.new "authorization error" rescue e : AdminAuthorizationException Baguette::Log.error "(#{connection_info_str}) #{request} admin authentication failed" AuthD::Response::Error.new "authorization error" rescue e Baguette::Log.error "(#{connection_info_str}) #{request} 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 if response.is_a? AuthD::Response::Error Baguette::Log.warning "(#{connection_info_str}) (#{duration}) #{request} >> #{response}" else if request_name != "KeepAlive" || should_display? AUTHMESSAGE::KEEPALIVE Baguette::Log.debug "(#{connection_info_str}) (#{duration}) #{request} >> #{response}" end 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 #{@configuration.service_name}" Baguette::Log.info "(mailer) Email activation template: #{@configuration.activation_template}" Baguette::Log.info "(mailer) Email recovery template: #{@configuration.recovery_template}" if skf = @configuration.secret_key_file Baguette::Log.info "secret key file is: #{skf}" else Baguette::Log.error "no secret key file" exit 1 end self.loop do |event| case event.type when LibIPC::EventType::Timer Baguette::Log.debug "Timer" if should_display? IPCMESSAGE::TIMER when LibIPC::EventType::MessageRx Baguette::Log.debug "Received message from #{event.fd}" if should_display? IPCMESSAGE::RX 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 should_display? IPCMESSAGE::TX when LibIPC::EventType::Connection Baguette::Log.debug "Connection from #{event.fd}" if should_display? IPCMESSAGE::CONNECTION when LibIPC::EventType::Disconnection Baguette::Log.debug "Disconnection from #{event.fd}" if should_display? IPCMESSAGE::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