554 lines
14 KiB
Crystal
554 lines
14 KiB
Crystal
require "uuid"
|
|
require "option_parser"
|
|
require "openssl"
|
|
require "colorize"
|
|
|
|
require "jwt"
|
|
require "ipc"
|
|
require "dodb"
|
|
|
|
require "grok"
|
|
|
|
require "./authd.cr"
|
|
|
|
extend AuthD
|
|
|
|
class AuthD::Service
|
|
property registrations_allowed = false
|
|
property require_email = false
|
|
property mailer_activation_url : String? = nil
|
|
property mailer_field_from : String? = nil
|
|
property mailer_field_subject : String? = nil
|
|
|
|
@users_per_login : DODB::Index(User)
|
|
@users_per_uid : DODB::Index(User)
|
|
|
|
def initialize(@storage_root : String, @jwt_key : String)
|
|
@users = DODB::DataBase(User).new @storage_root
|
|
@users_per_uid = @users.new_index "uid", &.uid.to_s
|
|
@users_per_login = @users.new_index "login", &.login
|
|
|
|
@last_uid_file = "#{@storage_root}/last_used_uid"
|
|
end
|
|
|
|
def hash_password(password : String) : String
|
|
digest = OpenSSL::Digest.new "sha256"
|
|
digest << password
|
|
digest.hexdigest
|
|
end
|
|
|
|
def new_uid
|
|
begin
|
|
uid = File.read(@last_uid_file).to_i
|
|
rescue
|
|
uid = 999
|
|
end
|
|
|
|
uid += 1
|
|
|
|
File.write @last_uid_file, uid.to_s
|
|
|
|
uid
|
|
end
|
|
|
|
def handle_request(request : AuthD::Request?)
|
|
case request
|
|
when Request::GetToken
|
|
begin
|
|
user = @users_per_login.get request.login
|
|
rescue e : DODB::MissingEntry
|
|
return Response::Error.new "invalid credentials"
|
|
end
|
|
|
|
if user.password_hash != hash_password request.password
|
|
return Response::Error.new "invalid credentials"
|
|
end
|
|
|
|
if user.nil?
|
|
return Response::Error.new "invalid credentials"
|
|
end
|
|
|
|
user.date_last_connection = Time.local
|
|
token = user.to_token
|
|
|
|
# change the date of the last connection
|
|
@users_per_uid.update user.uid.to_s, user
|
|
|
|
Response::Token.new (token.to_s @jwt_key), user.uid
|
|
when Request::AddUser
|
|
# No verification of the users' informations when an admin adds it.
|
|
# No mail address verification.
|
|
if request.shared_key != @jwt_key
|
|
return Response::Error.new "invalid authentication key"
|
|
end
|
|
|
|
if @users_per_login.get? request.login
|
|
return Response::Error.new "login already used"
|
|
end
|
|
|
|
if @require_email && request.email.nil?
|
|
return Response::Error.new "email required"
|
|
end
|
|
|
|
password_hash = hash_password request.password
|
|
|
|
uid = new_uid
|
|
|
|
user = User.new uid, request.login, password_hash
|
|
user.contact.email = request.email unless request.email.nil?
|
|
user.contact.phone = request.phone unless request.phone.nil?
|
|
|
|
request.profile.try do |profile|
|
|
user.profile = profile
|
|
end
|
|
|
|
# We consider adding the user as a registration
|
|
user.date_registration = Time.local
|
|
|
|
@users << user
|
|
|
|
Response::UserAdded.new user.to_public
|
|
when Request::ValidateUser
|
|
user = @users_per_login.get? request.login
|
|
|
|
if user.nil?
|
|
return Response::Error.new "user not found"
|
|
end
|
|
|
|
if user.contact.activation_key.nil?
|
|
return Response::Error.new "user already validated"
|
|
end
|
|
|
|
# remove the user contact activation key: the email is validated
|
|
if user.contact.activation_key == request.activation_key
|
|
user.contact.activation_key = nil
|
|
else
|
|
return Response::Error.new "wrong activation key"
|
|
end
|
|
|
|
@users_per_uid.update user.uid.to_s, user
|
|
|
|
Response::UserValidated.new user.to_public
|
|
when Request::GetUserByCredentials
|
|
user = @users_per_login.get? request.login
|
|
|
|
unless user
|
|
return Response::Error.new "invalid credentials"
|
|
end
|
|
|
|
if hash_password(request.password) != user.password_hash
|
|
return Response::Error.new "invalid credentials"
|
|
end
|
|
|
|
user.date_last_connection = Time.local
|
|
|
|
# change the date of the last connection
|
|
@users_per_uid.update user.uid.to_s, user
|
|
|
|
Response::User.new user.to_public
|
|
when Request::GetUser
|
|
uid_or_login = request.user
|
|
user = 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
|
|
|
|
if user.nil?
|
|
return Response::Error.new "user not found"
|
|
end
|
|
|
|
Response::User.new user.to_public
|
|
when Request::ModUser
|
|
if request.shared_key != @jwt_key
|
|
return Response::Error.new "invalid authentication key"
|
|
end
|
|
|
|
uid_or_login = request.user
|
|
user = 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
|
|
|
|
unless user
|
|
return Response::Error.new "user not found"
|
|
end
|
|
|
|
request.password.try do |s|
|
|
user.password_hash = hash_password s
|
|
end
|
|
|
|
request.email.try do |email|
|
|
user.contact.email = email
|
|
end
|
|
|
|
request.phone.try do |phone|
|
|
user.contact.phone = phone
|
|
end
|
|
|
|
@users_per_uid.update user.uid.to_s, user
|
|
|
|
Response::UserEdited.new user.uid
|
|
when Request::Register
|
|
if ! @registrations_allowed
|
|
return Response::Error.new "registrations not allowed"
|
|
end
|
|
|
|
if @users_per_login.get? request.login
|
|
return Response::Error.new "login already used"
|
|
end
|
|
|
|
if @require_email && request.email.nil?
|
|
return Response::Error.new "email required"
|
|
end
|
|
|
|
if ! request.email.nil?
|
|
# Test on the email address format.
|
|
grok = Grok.new [ "%{EMAILADDRESS:email}" ]
|
|
result = grok.parse request.email.not_nil!
|
|
email = result["email"]?
|
|
|
|
if email.nil?
|
|
return Response::Error.new "invalid email format"
|
|
end
|
|
end
|
|
|
|
uid = new_uid
|
|
password = hash_password request.password
|
|
|
|
user = User.new uid, request.login, password
|
|
user.contact.email = request.email unless request.email.nil?
|
|
user.contact.phone = request.phone unless request.phone.nil?
|
|
|
|
request.profile.try do |profile|
|
|
user.profile = profile
|
|
end
|
|
|
|
user.date_registration = Time.local
|
|
|
|
unless (mailer_activation_url = @mailer_activation_url).nil?
|
|
|
|
mailer_field_subject = @mailer_field_subject.not_nil!
|
|
mailer_field_from = @mailer_field_from.not_nil!
|
|
mailer_activation_url = @mailer_activation_url.not_nil!
|
|
|
|
# Once the user is created and stored, we try to contact him
|
|
unless Process.run("activation-mailer", [
|
|
"-l", user.login,
|
|
"-e", user.contact.email.not_nil!,
|
|
"-t", mailer_field_subject,
|
|
"-f", mailer_field_from,
|
|
"-u", mailer_activation_url,
|
|
"-a", user.contact.activation_key.not_nil!
|
|
]).success?
|
|
return Response::Error.new "cannot contact the user (but still registered)"
|
|
end
|
|
end
|
|
|
|
# add the user only if we were able to send the confirmation mail
|
|
@users << user
|
|
|
|
Response::UserAdded.new user.to_public
|
|
when Request::UpdatePassword
|
|
user = @users_per_login.get? request.login
|
|
|
|
unless user
|
|
return Response::Error.new "invalid credentials"
|
|
end
|
|
|
|
if hash_password(request.old_password) != user.password_hash
|
|
return Response::Error.new "invalid credentials"
|
|
end
|
|
|
|
user.password_hash = hash_password request.new_password
|
|
|
|
@users_per_uid.update user.uid.to_s, user
|
|
|
|
Response::UserEdited.new user.uid
|
|
when Request::ListUsers
|
|
# FIXME: Lines too long, repeatedly (>80c with 4c tabs).
|
|
request.token.try do |token|
|
|
user = get_user_from_token token
|
|
|
|
return Response::Error.new "unauthorized (user not found from token)"
|
|
|
|
return Response::Error.new "unauthorized (user not in authd group)" unless user.permissions["authd"]?.try(&.["*"].>=(User::PermissionLevel::Read))
|
|
end
|
|
|
|
request.key.try do |key|
|
|
return Response::Error.new "unauthorized (wrong shared key)" unless key == @jwt_key
|
|
end
|
|
|
|
return Response::Error.new "unauthorized (no key nor token)" unless request.key || request.token
|
|
|
|
Response::UsersList.new @users.to_h.map &.[1].to_public
|
|
when Request::CheckPermission
|
|
unless request.shared_key == @jwt_key
|
|
return Response::Error.new "unauthorized"
|
|
end
|
|
|
|
user = @users_per_uid.get? request.user.to_s
|
|
|
|
if user.nil?
|
|
return Response::Error.new "no such user"
|
|
end
|
|
|
|
service = request.service
|
|
service_permissions = user.permissions[service]?
|
|
|
|
if service_permissions.nil?
|
|
return Response::PermissionCheck.new service, request.resource, user.uid, User::PermissionLevel::None
|
|
end
|
|
|
|
resource_permissions = service_permissions[request.resource]?
|
|
|
|
if resource_permissions.nil?
|
|
return Response::PermissionCheck.new service, request.resource, user.uid, User::PermissionLevel::None
|
|
end
|
|
|
|
return Response::PermissionCheck.new service, request.resource, user.uid, resource_permissions
|
|
when Request::SetPermission
|
|
unless request.shared_key == @jwt_key
|
|
return Response::Error.new "unauthorized"
|
|
end
|
|
|
|
user = @users_per_uid.get? request.user.to_s
|
|
|
|
if user.nil?
|
|
return Response::Error.new "no such user"
|
|
end
|
|
|
|
service = request.service
|
|
service_permissions = user.permissions[service]?
|
|
|
|
if service_permissions.nil?
|
|
service_permissions = Hash(String, User::PermissionLevel).new
|
|
user.permissions[service] = service_permissions
|
|
end
|
|
|
|
if request.permission.none?
|
|
service_permissions.delete request.resource
|
|
else
|
|
service_permissions[request.resource] = request.permission
|
|
end
|
|
|
|
@users_per_uid.update user.uid.to_s, user
|
|
|
|
Response::PermissionSet.new user.uid, service, request.resource, request.permission
|
|
when Request::AskPasswordRecovery
|
|
|
|
uid_or_login = request.user
|
|
user = 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
|
|
|
|
if user.nil?
|
|
return Response::Error.new "user not found"
|
|
end
|
|
|
|
user.password_renew_key = UUID.random.to_s
|
|
|
|
@users_per_uid.update user.uid.to_s, user
|
|
|
|
unless (mailer_activation_url = @mailer_activation_url).nil?
|
|
|
|
mailer_field_from = @mailer_field_from.not_nil!
|
|
mailer_activation_url = @mailer_activation_url.not_nil!
|
|
|
|
# Once the user is created and stored, we try to contact him
|
|
unless Process.run("password-recovery-mailer", [
|
|
"-l", user.login,
|
|
"-e", user.contact.email.not_nil!,
|
|
"-t", "Password recovery email",
|
|
"-f", mailer_field_from,
|
|
"-u", mailer_activation_url,
|
|
"-a", user.password_renew_key.not_nil!
|
|
]).success?
|
|
return Response::Error.new "cannot contact the user for password recovery"
|
|
end
|
|
end
|
|
|
|
Response::PasswordRecoverySent.new user.to_public
|
|
when Request::PasswordRecovery
|
|
if request.shared_key != @jwt_key
|
|
return Response::Error.new "invalid authentication key"
|
|
end
|
|
|
|
uid_or_login = request.user
|
|
user = 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
|
|
|
|
if user.nil?
|
|
return Response::Error.new "user not found"
|
|
end
|
|
|
|
if user.password_renew_key == request.password_renew_key
|
|
user.password_hash = hash_password request.new_password
|
|
else
|
|
return Response::Error.new "renew key not valid"
|
|
end
|
|
|
|
user.password_renew_key = nil
|
|
|
|
@users_per_uid.update user.uid.to_s, user
|
|
|
|
Response::PasswordRecoverySent.new user.to_public
|
|
when Request::SearchUser
|
|
pattern = Regex.new request.user
|
|
|
|
matching_users = Array(AuthD::User::Public).new
|
|
|
|
users = @users.to_a
|
|
users.each do |u|
|
|
if pattern =~ u.login || u.profile.try do |profile|
|
|
full_name = profile["full_name"]?
|
|
if full_name.nil?
|
|
false
|
|
else
|
|
pattern =~ full_name.as_s
|
|
end
|
|
end
|
|
puts "#{u.login} matches #{pattern}"
|
|
matching_users << u.to_public
|
|
else
|
|
puts "#{u.login} doesn't match #{pattern}"
|
|
end
|
|
end
|
|
|
|
Response::MatchingUsers.new matching_users
|
|
when Request::EditProfile
|
|
user = get_user_from_token request.token
|
|
|
|
return Response::Error.new "invalid user" unless user
|
|
|
|
user.profile = request.new_profile
|
|
|
|
@users_per_uid.update user.uid.to_s, user
|
|
|
|
Response::User.new user.to_public
|
|
else
|
|
Response::Error.new "unhandled request type"
|
|
end
|
|
end
|
|
|
|
def get_user_from_token(token : String)
|
|
token_payload = Token.from_s(@jwt_key, token)
|
|
|
|
@users_per_uid.get? token_payload.uid.to_s
|
|
end
|
|
|
|
def info(message)
|
|
STDOUT << ":: ".colorize(:green) << message.colorize(:white) << "\n"
|
|
end
|
|
|
|
def error(message)
|
|
STDOUT << "!! ".colorize(:red) << message.colorize(:red) << "\n"
|
|
end
|
|
|
|
def run
|
|
##
|
|
# Provides a JWT-based authentication scheme for service-specific users.
|
|
server = IPC::Server.new "auth"
|
|
server.loop do |event|
|
|
if event.is_a? IPC::Exception
|
|
puts "oh no"
|
|
pp! event
|
|
next
|
|
end
|
|
|
|
case event
|
|
when IPC::Event::MessageReceived
|
|
begin
|
|
request = Request.from_ipc event.message
|
|
|
|
info "<< #{request.class.name.sub /^Request::/, ""}"
|
|
|
|
response = handle_request request
|
|
|
|
server.send event.fd, response
|
|
rescue e : MalformedRequest
|
|
error "#{e.message}"
|
|
error " .. type was: #{e.ipc_type}"
|
|
error " .. payload was: #{e.payload}"
|
|
response = Response::Error.new e.message
|
|
rescue e
|
|
error "#{e.message}"
|
|
response = Response::Error.new e.message
|
|
end
|
|
|
|
info ">> #{response.class.name.sub /^Response::/, ""}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
authd_storage = "storage"
|
|
authd_jwt_key = "nico-nico-nii"
|
|
authd_registrations = false
|
|
authd_require_email = false
|
|
activation_url : String? = nil
|
|
field_subject : String? = nil
|
|
field_from : String? = nil
|
|
|
|
begin
|
|
OptionParser.parse do |parser|
|
|
parser.banner = "usage: authd [options]"
|
|
|
|
parser.on "-s directory", "--storage directory", "Directory in which to store users." do |directory|
|
|
authd_storage = directory
|
|
end
|
|
|
|
parser.on "-K file", "--key-file file", "JWT key file" do |file_name|
|
|
authd_jwt_key = File.read(file_name).chomp
|
|
end
|
|
|
|
parser.on "-R", "--allow-registrations" do
|
|
authd_registrations = true
|
|
end
|
|
|
|
parser.on "-E", "--require-email" do
|
|
authd_require_email = true
|
|
end
|
|
|
|
parser.on "-t subject", "--subject title", "Subject of the email." do |s|
|
|
field_subject = s
|
|
end
|
|
|
|
parser.on "-f from-email", "--from email", "'From:' field to use in activation email." do |f|
|
|
field_from = f
|
|
end
|
|
|
|
parser.on "-u", "--activation-url url", "Activation URL." do |opt|
|
|
activation_url = opt
|
|
end
|
|
|
|
parser.on "-h", "--help", "Show this help" do
|
|
puts parser
|
|
|
|
exit 0
|
|
end
|
|
end
|
|
|
|
AuthD::Service.new(authd_storage, authd_jwt_key).tap do |authd|
|
|
authd.registrations_allowed = authd_registrations
|
|
authd.require_email = authd_require_email
|
|
authd.mailer_activation_url = activation_url
|
|
authd.mailer_field_subject = field_subject
|
|
authd.mailer_field_from = field_from
|
|
end.run
|
|
rescue e : OptionParser::Exception
|
|
STDERR.puts e.message
|
|
rescue e
|
|
STDERR.puts "exception raised: #{e.message}"
|
|
e.backtrace.try &.each do |line|
|
|
STDERR << " - " << line << '\n'
|
|
end
|
|
end
|
|
|