s/shared key/secret key/ + new bootstrap request + some cleaning.
This commit is contained in:
parent
186edd2ca0
commit
33b47766e5
10
makefile
10
makefile
@ -5,7 +5,6 @@ build:
|
|||||||
|
|
||||||
NAME ?= John
|
NAME ?= John
|
||||||
EMAIL ?= john@example.com
|
EMAIL ?= john@example.com
|
||||||
PHONE ?= 0707070707
|
|
||||||
|
|
||||||
PASSWORD_FILE ?= /tmp/PASSWORD
|
PASSWORD_FILE ?= /tmp/PASSWORD
|
||||||
DATA_DIRECTORY ?= /tmp/DATA-AUTHD
|
DATA_DIRECTORY ?= /tmp/DATA-AUTHD
|
||||||
@ -16,11 +15,18 @@ setup:
|
|||||||
run-authd: setup
|
run-authd: setup
|
||||||
./bin/authd -k /tmp/PASSWORD -R -E --storage $(DATA_DIRECTORY)
|
./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:
|
add-user:
|
||||||
./bin/authc user add $(NAME) $(EMAIL) $(PHONE) -k $(PASSWORD_FILE)
|
./bin/authc user add $(NAME) $(EMAIL)
|
||||||
|
|
||||||
print-messages:
|
print-messages:
|
||||||
cat src/requests/*.cr | ./bin/get-messages.awk
|
cat src/requests/*.cr | ./bin/get-messages.awk
|
||||||
|
|
||||||
print-message-numbers:
|
print-message-numbers:
|
||||||
make print-messages | grep -E "^[0-9]" | sort -n
|
make print-messages | grep -E "^[0-9]" | sort -n
|
||||||
|
|
||||||
|
print-messages-without-comments:
|
||||||
|
make print-messages | grep -vE '^[[:blank:]]+#'
|
||||||
|
@ -21,8 +21,8 @@ class Baguette::Configuration
|
|||||||
|
|
||||||
property login : String? = nil
|
property login : String? = nil
|
||||||
property pass : String? = nil
|
property pass : String? = nil
|
||||||
property shared_key : String = "nico-nico-nii" # Default authd key, as per the specs. :eyes:
|
property secret_key : String = "nico-nico-nii" # Default authd key, as per the specs. :eyes:
|
||||||
property shared_key_file : String? = nil
|
property secret_key_file : String? = nil
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
end
|
end
|
||||||
|
@ -98,6 +98,25 @@ module AuthD
|
|||||||
end
|
end
|
||||||
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
|
def validate_user(login : String, activation_key : String) : ::AuthD::User::Public | Exception
|
||||||
send_now Request::ValidateUser.new login, activation_key
|
send_now Request::ValidateUser.new login, activation_key
|
||||||
|
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
require "option_parser"
|
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
|
# frequently used functions
|
||||||
opt_authd_login = -> (parser : OptionParser) {
|
opt_authd_login = -> (parser : OptionParser) {
|
||||||
parser.on "-l LOGIN", "--login LOGIN", "Authd user login." do |login|
|
parser.on "-l LOGIN", "--login LOGIN", "Authd user login." do |login|
|
||||||
@ -76,6 +69,16 @@ parser = OptionParser.new do |parser|
|
|||||||
exit 0
|
exit 0
|
||||||
end
|
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.on "user", "Operations on users." do
|
||||||
parser.banner = "Usage: user [add | mod | delete | validate | search | get | recover | register ]"
|
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]"
|
parser.banner = "usage: user add login email [-P profile] [opt]"
|
||||||
Baguette::Log.info "Adding a user to the DB."
|
Baguette::Log.info "Adding a user to the DB."
|
||||||
Context.command = "user-add"
|
Context.command = "user-add"
|
||||||
opt_authd_admin.call parser
|
|
||||||
opt_profile.call parser
|
opt_profile.call parser
|
||||||
opt_help.call parser
|
opt_help.call parser
|
||||||
# login email
|
# login email
|
||||||
@ -94,7 +96,6 @@ parser = OptionParser.new do |parser|
|
|||||||
parser.banner = "Usage: user mod userid [-e email|-P profile] [opt]"
|
parser.banner = "Usage: user mod userid [-e email|-P profile] [opt]"
|
||||||
Baguette::Log.info "Modify a user account."
|
Baguette::Log.info "Modify a user account."
|
||||||
Context.command = "user-mod"
|
Context.command = "user-mod"
|
||||||
opt_authd_admin.call parser
|
|
||||||
opt_email.call parser
|
opt_email.call parser
|
||||||
opt_profile.call parser
|
opt_profile.call parser
|
||||||
opt_help.call parser
|
opt_help.call parser
|
||||||
@ -108,7 +109,6 @@ parser = OptionParser.new do |parser|
|
|||||||
Context.command = "user-delete"
|
Context.command = "user-delete"
|
||||||
# You can either be the owner of the account, or an admin.
|
# You can either be the owner of the account, or an admin.
|
||||||
opt_authd_login.call parser
|
opt_authd_login.call parser
|
||||||
opt_authd_admin.call parser
|
|
||||||
opt_help.call parser
|
opt_help.call parser
|
||||||
# userid
|
# userid
|
||||||
unrecognized_args_to_context_args.call parser, 1
|
unrecognized_args_to_context_args.call parser, 1
|
||||||
@ -178,7 +178,6 @@ permission list: none read edit admin
|
|||||||
END
|
END
|
||||||
Baguette::Log.info "Set permissions."
|
Baguette::Log.info "Set permissions."
|
||||||
Context.command = "permission-set"
|
Context.command = "permission-set"
|
||||||
opt_authd_admin.call parser
|
|
||||||
opt_help.call parser
|
opt_help.call parser
|
||||||
# userid application resource permission
|
# userid application resource permission
|
||||||
unrecognized_args_to_context_args.call parser, 4
|
unrecognized_args_to_context_args.call parser, 4
|
||||||
@ -193,7 +192,6 @@ permission list: none read edit admin
|
|||||||
END
|
END
|
||||||
Baguette::Log.info "Check permissions."
|
Baguette::Log.info "Check permissions."
|
||||||
Context.command = "permission-check"
|
Context.command = "permission-check"
|
||||||
opt_authd_admin.call parser
|
|
||||||
opt_help.call parser
|
opt_help.call parser
|
||||||
# userid application resource
|
# userid application resource
|
||||||
unrecognized_args_to_context_args.call parser, 3
|
unrecognized_args_to_context_args.call parser, 3
|
||||||
|
@ -7,7 +7,6 @@ class Context
|
|||||||
|
|
||||||
class_property authd_login = "undef" # undef authd user
|
class_property authd_login = "undef" # undef authd user
|
||||||
class_property authd_pass = "undef" # undef authd user password
|
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.
|
# # Properties to select what to display when printing a deal.
|
||||||
# class_property print_title = true
|
# class_property print_title = true
|
||||||
@ -52,14 +51,18 @@ class Actions
|
|||||||
property authd : AuthD::Client
|
property authd : AuthD::Client
|
||||||
|
|
||||||
def initialize(@authd)
|
def initialize(@authd)
|
||||||
@the_call["user-add"] = ->user_add
|
@the_call["user-registration"] = ->user_registration
|
||||||
@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-validation"] = ->user_validation # Do not require authentication.
|
@the_call["user-validation"] = ->user_validation # Do not require authentication.
|
||||||
@the_call["user-recovery"] = ->user_recovery # 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-set"] = ->permission_set
|
||||||
@the_call["permission-check"] = ->permission_check
|
@the_call["permission-check"] = ->permission_check
|
||||||
@ -101,6 +104,21 @@ class Actions
|
|||||||
puts "error: #{e.message}"
|
puts "error: #{e.message}"
|
||||||
end
|
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
|
# TODO
|
||||||
def user_mod
|
def user_mod
|
||||||
args = Context.args.not_nil!
|
args = Context.args.not_nil!
|
||||||
@ -188,7 +206,6 @@ def main
|
|||||||
|
|
||||||
# Authd connection.
|
# Authd connection.
|
||||||
authd = AuthD::Client.new
|
authd = AuthD::Client.new
|
||||||
authd.key = Context.shared_key if Context.shared_key != "undef"
|
|
||||||
|
|
||||||
# Authd token.
|
# Authd token.
|
||||||
# FIXME: not sure about getting the token, it seems not used elsewhere.
|
# FIXME: not sure about getting the token, it seems not used elsewhere.
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
require "ipc"
|
require "ipc"
|
||||||
require "ipc/json"
|
require "ipc/json"
|
||||||
require "./authd.cr"
|
require "./authd.cr"
|
||||||
require "./server.cr" # To load AuthD::Service definition.
|
require "./service.cr" # To load AuthD::Service definition.
|
||||||
|
|
||||||
class IPC::JSON
|
class IPC::JSON
|
||||||
def handle(service : AuthD::Service, fd : Int32)
|
def handle(service : AuthD::Service, fd : Int32)
|
||||||
|
@ -46,4 +46,41 @@ class AuthD::Request
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
AuthD.requests << AddUser
|
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
|
end
|
||||||
|
@ -32,7 +32,7 @@ class AuthD::Request
|
|||||||
# On successuful connection: store the authenticated user in a hash.
|
# On successuful connection: store the authenticated user in a hash.
|
||||||
authd.logged_users[fd] = user.to_public
|
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
|
||||||
end
|
end
|
||||||
AuthD.requests << Login
|
AuthD.requests << Login
|
||||||
|
204
src/server.cr
204
src/server.cr
@ -1,200 +1,4 @@
|
|||||||
require "./authd.cr"
|
require "./service.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
|
|
||||||
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
simulation, no_configuration, configuration_file = Baguette::Configuration.option_parser
|
simulation, no_configuration, configuration_file = Baguette::Configuration.option_parser
|
||||||
@ -209,8 +13,8 @@ begin
|
|||||||
|
|
||||||
Baguette::Context.verbosity = configuration.verbosity
|
Baguette::Context.verbosity = configuration.verbosity
|
||||||
|
|
||||||
if key_file = configuration.shared_key_file
|
if key_file = configuration.secret_key_file
|
||||||
configuration.shared_key = File.read(key_file).chomp
|
configuration.secret_key = File.read(key_file).chomp
|
||||||
end
|
end
|
||||||
|
|
||||||
OptionParser.parse do |parser|
|
OptionParser.parse do |parser|
|
||||||
@ -221,7 +25,7 @@ begin
|
|||||||
end
|
end
|
||||||
|
|
||||||
parser.on "-k file", "--key-file file", "JWT key file" do |file_name|
|
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
|
end
|
||||||
|
|
||||||
parser.on "-R", "--allow-registrations", "Allow user registration." do
|
parser.on "-R", "--allow-registrations", "Allow user registration." do
|
||||||
|
196
src/service.cr
Normal file
196
src/service.cr
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user