2018-09-22 19:08:28 +02:00
|
|
|
require "uuid"
|
2018-11-12 18:51:21 +01:00
|
|
|
require "option_parser"
|
2019-02-19 20:45:19 +01:00
|
|
|
require "openssl"
|
2018-09-22 19:08:28 +02:00
|
|
|
|
|
|
|
require "jwt"
|
2018-11-12 18:51:21 +01:00
|
|
|
require "ipc"
|
2019-12-12 00:45:12 +01:00
|
|
|
require "dodb"
|
2018-11-12 18:51:21 +01:00
|
|
|
|
2020-01-27 13:16:16 +01:00
|
|
|
require "grok"
|
|
|
|
|
2018-11-12 18:51:21 +01:00
|
|
|
require "./authd.cr"
|
|
|
|
|
|
|
|
extend AuthD
|
2018-09-22 21:23:50 +02:00
|
|
|
|
2019-11-23 01:08:05 +01:00
|
|
|
class AuthD::Service
|
2019-12-07 00:53:31 +01:00
|
|
|
property registrations_allowed = false
|
2020-01-22 01:55:57 +01:00
|
|
|
property require_email = false
|
2019-12-07 00:53:31 +01:00
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
@users_per_login : DODB::Index(User)
|
2020-01-04 08:40:13 +01:00
|
|
|
@users_per_uid : DODB::Index(User)
|
2019-12-15 23:38:49 +01:00
|
|
|
|
|
|
|
def initialize(@storage_root : String, @jwt_key : String)
|
2020-01-04 08:40:13 +01:00
|
|
|
@users = DODB::DataBase(User).new @storage_root
|
|
|
|
@users_per_uid = @users.new_index "uid", &.uid.to_s
|
2019-12-15 23:38:49 +01:00
|
|
|
@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
|
2018-12-19 13:54:19 +01:00
|
|
|
end
|
2019-11-03 13:17:24 +01:00
|
|
|
|
2019-11-23 01:08:05 +01:00
|
|
|
def handle_request(request : AuthD::Request?, connection : IPC::Connection)
|
|
|
|
case request
|
2019-11-22 18:14:52 +01:00
|
|
|
when Request::GetToken
|
2020-02-26 14:36:54 +01:00
|
|
|
begin
|
|
|
|
user = @users_per_login.get request.login
|
|
|
|
rescue e : DODB::MissingEntry
|
|
|
|
return Response::Error.new "invalid credentials"
|
|
|
|
end
|
2019-12-15 23:38:49 +01:00
|
|
|
|
|
|
|
if user.password_hash != hash_password request.password
|
|
|
|
return Response::Error.new "invalid credentials"
|
|
|
|
end
|
2018-11-12 18:51:21 +01:00
|
|
|
|
|
|
|
if user.nil?
|
2019-11-23 01:08:05 +01:00
|
|
|
return Response::Error.new "invalid credentials"
|
2018-11-12 18:51:21 +01:00
|
|
|
end
|
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
token = user.to_token
|
2019-11-22 22:55:12 +01:00
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
Response::Token.new token.to_s @jwt_key
|
2019-11-22 18:14:52 +01:00
|
|
|
when Request::AddUser
|
2020-01-27 13:16:16 +01:00
|
|
|
# No verification of the users' informations when an admin adds it.
|
|
|
|
# No mail address verification.
|
2019-11-23 01:08:05 +01:00
|
|
|
if request.shared_key != @jwt_key
|
|
|
|
return Response::Error.new "invalid authentication key"
|
2019-10-10 20:58:44 +02:00
|
|
|
end
|
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
if @users_per_login.get? request.login
|
2019-11-23 01:08:05 +01:00
|
|
|
return Response::Error.new "login already used"
|
2018-12-19 13:54:19 +01:00
|
|
|
end
|
|
|
|
|
2020-01-22 01:55:57 +01:00
|
|
|
if @require_email && request.email.nil?
|
|
|
|
return Response::Error.new "email required"
|
|
|
|
end
|
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
password_hash = hash_password request.password
|
|
|
|
|
|
|
|
uid = new_uid
|
|
|
|
|
|
|
|
user = User.new uid, request.login, password_hash
|
2020-01-27 13:16:16 +01:00
|
|
|
user.contact.email = request.email unless request.email.nil?
|
2020-01-22 01:55:57 +01:00
|
|
|
user.contact.phone = request.phone unless request.phone.nil?
|
2019-12-15 23:38:49 +01:00
|
|
|
|
|
|
|
request.profile.try do |profile|
|
|
|
|
user.profile = profile
|
|
|
|
end
|
|
|
|
|
2020-01-04 08:40:13 +01:00
|
|
|
@users << user
|
2018-12-19 13:54:19 +01:00
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
Response::UserAdded.new user.to_public
|
2020-01-22 14:43:58 +01:00
|
|
|
when Request::ValidateUser
|
|
|
|
if request.shared_key != @jwt_key
|
|
|
|
return Response::Error.new "invalid authentication key"
|
|
|
|
end
|
|
|
|
|
|
|
|
user = @users_per_login.get? request.login
|
|
|
|
|
|
|
|
if user.nil?
|
|
|
|
return Response::Error.new "user not found"
|
|
|
|
end
|
|
|
|
|
2020-01-23 15:45:05 +01:00
|
|
|
if user.contact.activation_key.nil?
|
|
|
|
return Response::Error.new "user already validated"
|
|
|
|
end
|
|
|
|
|
2020-01-22 14:43:58 +01:00
|
|
|
# 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
|
2019-11-22 18:14:52 +01:00
|
|
|
when Request::GetUserByCredentials
|
2019-12-17 13:40:10 +01:00
|
|
|
user = @users_per_login.get? request.login
|
2019-02-16 22:06:56 +01:00
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
unless user
|
|
|
|
return Response::Error.new "invalid credentials"
|
|
|
|
end
|
|
|
|
|
|
|
|
if hash_password(request.password) != user.password_hash
|
|
|
|
return Response::Error.new "invalid credentials"
|
2019-02-16 22:06:56 +01:00
|
|
|
end
|
2019-01-07 17:04:20 +01:00
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
Response::User.new user.to_public
|
|
|
|
when Request::GetUser
|
|
|
|
uid_or_login = request.user
|
|
|
|
user = if uid_or_login.is_a? Int32
|
2020-01-04 08:40:13 +01:00
|
|
|
@users_per_uid.get? uid_or_login.to_s
|
2019-01-07 17:04:20 +01:00
|
|
|
else
|
2019-12-15 23:38:49 +01:00
|
|
|
@users_per_login.get? uid_or_login
|
|
|
|
end
|
|
|
|
|
|
|
|
if user.nil?
|
|
|
|
return Response::Error.new "user not found"
|
2019-01-07 17:04:20 +01:00
|
|
|
end
|
2019-12-15 23:38:49 +01:00
|
|
|
|
|
|
|
Response::User.new user.to_public
|
2019-11-22 18:14:52 +01:00
|
|
|
when Request::ModUser
|
2019-11-23 01:08:05 +01:00
|
|
|
if request.shared_key != @jwt_key
|
|
|
|
return Response::Error.new "invalid authentication key"
|
2019-10-10 20:58:44 +02:00
|
|
|
end
|
|
|
|
|
2020-02-09 15:47:44 +01:00
|
|
|
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
|
2019-12-15 23:38:49 +01:00
|
|
|
|
|
|
|
unless user
|
|
|
|
return Response::Error.new "user not found"
|
|
|
|
end
|
|
|
|
|
2020-02-07 03:54:26 +01:00
|
|
|
request.password.try do |s|
|
2019-12-15 23:38:49 +01:00
|
|
|
user.password_hash = hash_password s
|
2019-05-29 16:06:11 +02:00
|
|
|
end
|
|
|
|
|
2020-02-09 15:47:44 +01:00
|
|
|
request.email.try do |email|
|
|
|
|
user.contact.email = email
|
|
|
|
end
|
|
|
|
|
|
|
|
request.phone.try do |phone|
|
|
|
|
user.contact.phone = phone
|
|
|
|
end
|
|
|
|
|
2020-01-04 08:40:13 +01:00
|
|
|
@users_per_uid.update user.uid.to_s, user
|
2019-05-29 16:06:11 +02:00
|
|
|
|
2020-02-09 15:47:44 +01:00
|
|
|
Response::UserEdited.new user.uid
|
2019-12-07 00:53:31 +01:00
|
|
|
when Request::Register
|
|
|
|
if ! @registrations_allowed
|
|
|
|
return Response::Error.new "registrations not allowed"
|
|
|
|
end
|
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
if @users_per_login.get? request.login
|
2019-12-07 00:53:31 +01:00
|
|
|
return Response::Error.new "login already used"
|
|
|
|
end
|
|
|
|
|
2020-01-27 13:16:16 +01:00
|
|
|
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
|
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
uid = new_uid
|
|
|
|
password = hash_password request.password
|
2019-12-07 21:09:17 +01:00
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
user = User.new uid, request.login, password
|
2020-01-27 13:16:16 +01:00
|
|
|
user.contact.email = request.email unless request.email.nil?
|
|
|
|
user.contact.phone = request.phone unless request.phone.nil?
|
2019-12-07 21:09:17 +01:00
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
request.profile.try do |profile|
|
|
|
|
user.profile = profile
|
|
|
|
end
|
2019-12-07 21:09:17 +01:00
|
|
|
|
2020-01-20 13:44:48 +01:00
|
|
|
@users << user
|
2019-12-07 21:09:17 +01:00
|
|
|
|
2020-01-27 13:16:16 +01:00
|
|
|
# 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", "Activation email",
|
|
|
|
"-f", "karchnu@localhost",
|
|
|
|
"-a", user.contact.activation_key.not_nil!
|
|
|
|
]).success?
|
|
|
|
return Response::Error.new "cannot contact the user (but still registered)"
|
|
|
|
end
|
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
Response::UserAdded.new user.to_public
|
2019-12-07 23:57:40 +01:00
|
|
|
when Request::UpdatePassword
|
2019-12-15 23:38:49 +01:00
|
|
|
user = @users_per_login.get? request.login
|
2019-12-07 23:57:40 +01:00
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
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
|
2019-12-07 23:57:40 +01:00
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
user.password_hash = hash_password request.new_password
|
2019-12-07 23:57:40 +01:00
|
|
|
|
2020-01-04 08:40:13 +01:00
|
|
|
@users_per_uid.update user.uid.to_s, user
|
2019-12-07 23:57:40 +01:00
|
|
|
|
|
|
|
Response::UserEdited.new user.uid
|
2019-12-09 21:57:38 +01:00
|
|
|
when Request::ListUsers
|
2019-12-15 23:38:49 +01:00
|
|
|
# FIXME: Lines too long, repeatedly (>80c with 4c tabs).
|
2019-12-09 21:57:38 +01:00
|
|
|
request.token.try do |token|
|
|
|
|
user = get_user_from_token token
|
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
return Response::Error.new "unauthorized (user not found from token)"
|
2019-12-09 21:57:38 +01:00
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
return Response::Error.new "unauthorized (user not in authd group)" unless user.permissions["authd"]?.try(&.["*"].>=(User::PermissionLevel::Read))
|
2019-12-09 21:57:38 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
request.key.try do |key|
|
2019-12-10 04:50:36 +01:00
|
|
|
return Response::Error.new "unauthorized (wrong shared key)" unless key == @jwt_key
|
2019-12-09 21:57:38 +01:00
|
|
|
end
|
|
|
|
|
2019-12-10 04:50:36 +01:00
|
|
|
return Response::Error.new "unauthorized (no key nor token)" unless request.key || request.token
|
2019-12-09 21:57:38 +01:00
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
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
|
|
|
|
|
2020-01-04 08:40:13 +01:00
|
|
|
user = @users_per_uid.get? request.user.to_s
|
2019-12-15 23:38:49 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2020-01-04 08:40:13 +01:00
|
|
|
user = @users_per_uid.get? request.user.to_s
|
2019-12-15 23:38:49 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2020-01-04 08:40:13 +01:00
|
|
|
@users_per_uid.update user.uid.to_s, user
|
2019-12-15 23:38:49 +01:00
|
|
|
|
|
|
|
Response::PermissionSet.new user.uid, service, request.resource, request.permission
|
2019-11-22 23:13:34 +01:00
|
|
|
else
|
|
|
|
Response::Error.new "unhandled request type"
|
2018-11-12 18:51:21 +01:00
|
|
|
end
|
2019-11-23 01:08:05 +01:00
|
|
|
end
|
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
def get_user_from_token(token : String)
|
|
|
|
token_payload = Token.from_s(token, @jwt_key)
|
2019-12-07 21:09:17 +01:00
|
|
|
|
2020-01-04 08:40:13 +01:00
|
|
|
@users_per_uid.get? token_payload.uid.to_s
|
2019-12-07 21:09:17 +01:00
|
|
|
end
|
|
|
|
|
2019-11-23 01:08:05 +01:00
|
|
|
def run
|
|
|
|
##
|
|
|
|
# Provides a JWT-based authentication scheme for service-specific users.
|
|
|
|
IPC::Service.new "auth" do |event|
|
|
|
|
if event.is_a? IPC::Exception
|
|
|
|
puts "oh no"
|
|
|
|
pp! event
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
|
|
|
case event
|
|
|
|
when IPC::Event::Message
|
2019-12-07 00:53:31 +01:00
|
|
|
begin
|
|
|
|
request = Request.from_ipc event.message
|
2019-11-23 01:08:05 +01:00
|
|
|
|
2019-12-07 00:53:31 +01:00
|
|
|
response = handle_request request, event.connection
|
2019-11-23 01:08:05 +01:00
|
|
|
|
2019-12-07 00:53:31 +01:00
|
|
|
event.connection.send response
|
|
|
|
rescue e
|
|
|
|
STDERR.puts "error: #{e.message}"
|
|
|
|
end
|
2019-11-23 01:08:05 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-12-15 23:38:49 +01:00
|
|
|
authd_storage = "storage"
|
2019-11-23 01:08:05 +01:00
|
|
|
authd_jwt_key = "nico-nico-nii"
|
2019-12-07 00:53:31 +01:00
|
|
|
authd_registrations = false
|
2020-01-22 01:55:57 +01:00
|
|
|
authd_require_email = false
|
2019-11-23 01:08:05 +01:00
|
|
|
|
2020-01-04 09:02:31 +01:00
|
|
|
begin
|
|
|
|
OptionParser.parse do |parser|
|
|
|
|
parser.banner = "usage: authd [options]"
|
2019-12-17 15:56:42 +01:00
|
|
|
|
2020-01-04 09:02:31 +01:00
|
|
|
parser.on "-s directory", "--storage directory", "Directory in which to store users." do |directory|
|
|
|
|
authd_storage = directory
|
|
|
|
end
|
2019-11-23 01:08:05 +01:00
|
|
|
|
2020-01-04 09:02:31 +01:00
|
|
|
parser.on "-K file", "--key-file file", "JWT key file" do |file_name|
|
|
|
|
authd_jwt_key = File.read(file_name).chomp
|
|
|
|
end
|
2019-11-23 01:08:05 +01:00
|
|
|
|
2020-01-04 09:02:31 +01:00
|
|
|
parser.on "-R", "--allow-registrations" do
|
|
|
|
authd_registrations = true
|
|
|
|
end
|
2019-12-07 00:53:31 +01:00
|
|
|
|
2020-01-22 01:55:57 +01:00
|
|
|
parser.on "-E", "--require-email" do
|
|
|
|
authd_require_email = true
|
|
|
|
end
|
|
|
|
|
2020-01-04 09:02:31 +01:00
|
|
|
parser.on "-h", "--help", "Show this help" do
|
|
|
|
puts parser
|
2019-11-22 23:13:34 +01:00
|
|
|
|
2020-01-04 09:02:31 +01:00
|
|
|
exit 0
|
|
|
|
end
|
2018-09-23 16:17:48 +02:00
|
|
|
end
|
2018-11-12 18:51:21 +01:00
|
|
|
|
2020-01-04 09:02:31 +01:00
|
|
|
AuthD::Service.new(authd_storage, authd_jwt_key).tap do |authd|
|
|
|
|
authd.registrations_allowed = authd_registrations
|
2020-01-22 01:55:57 +01:00
|
|
|
authd.require_email = authd_require_email
|
2020-01-04 09:02:31 +01:00
|
|
|
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
|
2019-11-23 01:08:05 +01:00
|
|
|
|