authd/src/main.cr

324 lines
7.8 KiB
Crystal
Raw Normal View History

2018-09-22 19:08:28 +02:00
require "uuid"
require "option_parser"
2019-02-19 20:45:19 +01:00
require "openssl"
2018-09-22 19:08:28 +02:00
require "jwt"
require "ipc"
require "dodb"
require "./authd.cr"
extend AuthD
2019-11-23 01:08:05 +01:00
class AuthD::Service
property registrations_allowed = false
2020-01-22 01:55:57 +01:00
property require_email = false
@users_per_login : DODB::Index(User)
2020-01-04 08:40:13 +01:00
@users_per_uid : DODB::Index(User)
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
@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
user = @users_per_login.get request.login
if user.password_hash != hash_password request.password
return Response::Error.new "invalid credentials"
end
if user.nil?
2019-11-23 01:08:05 +01:00
return Response::Error.new "invalid credentials"
end
token = user.to_token
2019-11-22 22:55:12 +01:00
Response::Token.new token.to_s @jwt_key
2019-11-22 18:14:52 +01:00
when Request::AddUser
2019-11-23 01:08:05 +01:00
if request.shared_key != @jwt_key
return Response::Error.new "invalid authentication key"
end
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
password_hash = hash_password request.password
uid = new_uid
user = User.new uid, request.login, password_hash
2020-01-22 01:55:57 +01:00
user.contact.email = request.email
user.contact.phone = request.phone unless request.phone.nil?
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
Response::UserAdded.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
unless user
return Response::Error.new "invalid credentials"
end
if hash_password(request.password) != user.password_hash
return Response::Error.new "invalid credentials"
end
2019-01-07 17:04:20 +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
@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
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"
end
2020-01-04 08:40:13 +01:00
user = @users_per_uid.get? request.uid.to_s
unless user
return Response::Error.new "user not found"
end
2019-05-29 16:06:11 +02:00
password_hash = request.password.try do |s|
user.password_hash = hash_password s
2019-05-29 16:06:11 +02:00
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
2019-11-22 23:13:34 +01:00
Response::UserEdited.new request.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
uid = new_uid
password = hash_password request.password
user = User.new uid, request.login, password
request.profile.try do |profile|
user.profile = profile
end
2020-01-20 13:44:48 +01:00
@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
2020-01-04 08:40:13 +01:00
@users_per_uid.update user.uid.to_s, user
Response::UserEdited.new user.uid
2019-12-09 21:57:38 +01:00
when Request::ListUsers
# 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
return Response::Error.new "unauthorized (user not found from token)"
2019-12-09 21:57:38 +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|
return Response::Error.new "unauthorized (wrong shared key)" unless key == @jwt_key
2019-12-09 21:57:38 +01:00
end
return Response::Error.new "unauthorized (no key nor token)" unless request.key || request.token
2019-12-09 21:57:38 +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
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
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
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"
end
2019-11-23 01:08:05 +01:00
end
def get_user_from_token(token : String)
token_payload = Token.from_s(token, @jwt_key)
2020-01-04 08:40:13 +01:00
@users_per_uid.get? token_payload.uid.to_s
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
begin
request = Request.from_ipc event.message
2019-11-23 01:08:05 +01:00
response = handle_request request, event.connection
2019-11-23 01:08:05 +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
authd_storage = "storage"
2019-11-23 01:08:05 +01:00
authd_jwt_key = "nico-nico-nii"
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
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
end
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