Crystal bindings: add authd full WIP code.

master
Philippe Pittoli 2023-02-01 11:10:30 +01:00
parent 4b0ca0a59d
commit 79752068c6
24 changed files with 1588 additions and 0 deletions

View File

@ -0,0 +1,31 @@
require "json"
require "jwt"
require "../src/main.cr"
require "baguette-crystal-base"
require "./user.cr"
# Allows get configuration from a provided file.
# See Baguette::Configuration::Base.get
class Baguette::Configuration
class Auth < IPC
include YAML::Serializable
property login : String? = nil
property pass : String? = nil
property shared_key : String = "nico-nico-nii" # Default authd key, as per the specs. :eyes:
property shared_key_file : String? = nil
def initialize
end
end
end
# Requests and responses.
require "./exceptions"
# Requests and responses.
require "./network"
# Functions to request the authd server.
require "./libclient.cr"

View File

@ -0,0 +1,13 @@
module AuthD
class Exception < ::Exception
end
class UserNotFound < ::Exception
end
class AuthenticationInfoLacking < ::Exception
end
class AdminAuthorizationException < ::Exception
end
end

View File

@ -0,0 +1,242 @@
require "ipc/json"
module AuthD
class Client < IPC
property key : String
def initialize
super
@key = ""
end
def get_token?(login : String, password : String) : String?
send_now Request::GetToken.new login, password
response = AuthD.responses.parse_ipc_json read
if response.is_a?(Response::Token)
response.token
else
nil
end
end
def get_user?(login : String, password : String) : AuthD::User::Public?
send_now Request::GetUserByCredentials.new login, password
response = AuthD.responses.parse_ipc_json read
if response.is_a? Response::User
response.user
else
nil
end
end
def get_user?(uid_or_login : Int32 | String) : ::AuthD::User::Public?
send_now Request::GetUser.new uid_or_login
response = AuthD.responses.parse_ipc_json read
if response.is_a? Response::User
response.user
else
nil
end
end
def send_now(type : Request::Type, payload)
send_now @server_fd, type.value.to_u8, payload
end
def decode_token(token)
user, meta = JWT.decode token, @key, JWT::Algorithm::HS256
user = ::AuthD::User::Public.from_json user.to_json
{user, meta}
end
# FIXME: Extra options may be useful to implement here.
def add_user(login : String, password : String,
email : String?,
phone : String?,
profile : Hash(String, JSON::Any)?) : ::AuthD::User::Public | Exception
send_now Request::AddUser.new @key, login, password, email, phone, profile
response = AuthD.responses.parse_ipc_json read
case response
when Response::UserAdded
response.user
when Response::Error
raise Exception.new response.reason
else
# Should not happen in serialized connections, but…
# itll happen if you run several requests at once.
Exception.new
end
end
def validate_user(login : String, activation_key : String) : ::AuthD::User::Public | Exception
send_now Request::ValidateUser.new login, activation_key
response = AuthD.responses.parse_ipc_json read
case response
when Response::UserValidated
response.user
when Response::Error
raise Exception.new response.reason
else
# Should not happen in serialized connections, but…
# itll happen if you run several requests at once.
Exception.new
end
end
def ask_password_recovery(uid_or_login : String | Int32, email : String)
send_now Request::AskPasswordRecovery.new uid_or_login, email
response = AuthD.responses.parse_ipc_json read
case response
when Response::PasswordRecoverySent
when Response::Error
raise Exception.new response.reason
else
Exception.new
end
end
def change_password(uid_or_login : String | Int32, new_pass : String, renew_key : String)
send_now Request::PasswordRecovery.new uid_or_login, renew_key, new_pass
response = AuthD.responses.parse_ipc_json read
case response
when Response::PasswordRecovered
when Response::Error
raise Exception.new response.reason
else
Exception.new
end
end
def register(login : String,
password : String,
email : String?,
phone : String?,
profile : Hash(String, JSON::Any)?) : ::AuthD::User::Public?
send_now Request::Register.new login, password, email, phone, profile
response = AuthD.responses.parse_ipc_json read
case response
when Response::UserAdded
when Response::Error
raise Exception.new response.reason
end
end
def mod_user(uid_or_login : Int32 | String, password : String? = nil, email : String? = nil, phone : String? = nil, avatar : String? = nil) : Bool | Exception
request = Request::ModUser.new @key, uid_or_login
request.password = password if password
request.email = email if email
request.phone = phone if phone
request.avatar = avatar if avatar
send_now request
response = AuthD.responses.parse_ipc_json read
case response
when Response::UserEdited
true
when Response::Error
Exception.new response.reason
else
Exception.new "???"
end
end
def check_permission(user : Int32, service_name : String, resource_name : String) : User::PermissionLevel
request = Request::CheckPermission.new @key, user, service_name, resource_name
send_now request
response = AuthD.responses.parse_ipc_json read
case response
when Response::PermissionCheck
response.permission
when Response
raise Exception.new "unexpected response: #{response.type}"
else
raise Exception.new "unexpected response"
end
end
def set_permission(uid : Int32, service : String, resource : String, permission : User::PermissionLevel)
request = Request::SetPermission.new @key, uid, service, resource, permission
send_now request
response = AuthD.responses.parse_ipc_json read
case response
when Response::PermissionSet
true
when Response
raise Exception.new "unexpected response: #{response.type}"
else
raise Exception.new "unexpected response"
end
end
def search_user(user_login : String)
send_now Request::SearchUser.new user_login
response = AuthD.responses.parse_ipc_json read
case response
when Response::MatchingUsers
response.users
when Response::Error
raise Exception.new response.reason
else
Exception.new
end
end
def edit_profile_content(user : Int32 | String, new_values)
send_now Request::EditProfileContent.new key, user, new_values
response = AuthD.responses.parse_ipc_json read
case response
when Response::User
response.user
when Response::Error
raise Exception.new response.reason
else
raise Exception.new "unexpected response"
end
end
def delete(user : Int32 | String, key : String)
send_now Request::Delete.new user, key
delete_
end
def delete(user : Int32 | String, login : String, pass : String)
send_now Request::Delete.new user, login, pass
delete_
end
def delete_
response = AuthD.responses.parse_ipc_json read
case response
when Response::Error
raise Exception.new response.reason
end
response
end
end
end

View File

@ -0,0 +1,238 @@
require "uuid"
require "option_parser"
require "openssl"
require "colorize"
require "jwt"
require "../src/main.cr"
require "dodb"
require "baguette-crystal-base"
require "grok"
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_url : String? = nil
property field_subject : String? = nil
property field_from : String? = nil
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)
# #{@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"
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
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(event : IPC::Event)
request_start = Time.utc
request = AuthD.requests.parse_ipc_json event.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
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"
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
else
Baguette::Log.error "Not implemented behavior for event: #{event}"
end
end
end
end
begin
simulation, no_configuration, configuration_file = Baguette::Configuration.option_parser
configuration = if no_configuration
Baguette::Log.info "do not load a configuration file."
Baguette::Configuration::Auth.new
else
Baguette::Configuration::Auth.get(configuration_file) ||
Baguette::Configuration::Auth.new
end
Baguette::Context.verbosity = configuration.verbosity
if key_file = configuration.shared_key_file
configuration.shared_key = File.read(key_file).chomp
end
OptionParser.parse do |parser|
parser.banner = "usage: authd [options]"
parser.on "--storage directory", "Directory in which to store users." do |directory|
configuration.storage = directory
end
parser.on "-K file", "--key-file file", "JWT key file" do |file_name|
configuration.shared_key = File.read(file_name).chomp
end
parser.on "-R", "--allow-registrations" do
configuration.registrations = true
end
parser.on "-E", "--require-email" do
configuration.require_email = true
end
parser.on "-t subject", "--subject title", "Subject of the email." do |s|
configuration.field_subject = s
end
parser.on "-f from-email", "--from email", "'From:' field to use in activation email." do |f|
configuration.field_from = f
end
parser.on "-u", "--activation-url url", "Activation URL." do |opt|
configuration.activation_url = opt
end
parser.on "-x key", "--read-only-profile-key key", "Marks a user profile key as being read-only." do |key|
configuration.read_only_profile_keys.push key
end
parser.on "-h", "--help", "Show this help" do
puts parser
exit 0
end
end
if simulation
pp! configuration
exit 0
end
AuthD::Service.new(configuration).run
rescue e : OptionParser::Exception
Baguette::Log.error e.message
rescue e
Baguette::Log.error "exception raised: #{e.message}"
e.backtrace.try &.each do |line|
STDERR << " - " << line << '\n'
end
end

View File

@ -0,0 +1,22 @@
require "../src/main.cr"
require "../src/json"
class IPC::JSON
def handle(service : AuthD::Service)
raise "unimplemented"
end
end
module AuthD
class_getter requests = [] of IPC::JSON.class
class_getter responses = [] of IPC::JSON.class
end
class IPC
def schedule(fd, m : (AuthD::Request | AuthD::Response))
schedule fd, m.type.to_u8, m.to_json
end
end
require "./requests/*"
require "./responses/*"

View File

@ -0,0 +1,100 @@
class AuthD::Request
IPC::JSON.message AddUser, 1 do
# Only clients that have the right shared key will be allowed
# to create users.
property shared_key : String
property login : String
property password : String
property email : String? = nil
property phone : String? = nil
property profile : Hash(String, JSON::Any)? = nil
def initialize(@shared_key, @login, @password, @email, @phone, @profile)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
# No verification of the users' informations when an admin adds it.
# No mail address verification.
if @shared_key != authd.configuration.shared_key
return Response::Error.new "invalid authentication key"
end
if authd.users_per_login.get? @login
return Response::Error.new "login already used"
end
if authd.configuration.require_email && @email.nil?
return Response::Error.new "email required"
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.contact.phone = @phone unless @phone.nil?
@profile.try do |profile|
user.profile = profile
end
# We consider adding the user as a registration
user.date_registration = Time.local
authd.users << user
Response::UserAdded.new user.to_public
end
end
AuthD.requests << AddUser
IPC::JSON.message ModUser, 5 do
property shared_key : String
property user : Int32 | String
property password : String? = nil
property email : String? = nil
property phone : String? = nil
property avatar : String? = nil
def initialize(@shared_key, @user)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
if @shared_key != authd.configuration.shared_key
return Response::Error.new "invalid authentication key"
end
uid_or_login = @user
user = if uid_or_login.is_a? Int32
authd.users_per_uid.get? uid_or_login.to_s
else
authd.users_per_login.get? uid_or_login
end
unless user
return Response::Error.new "user not found"
end
@password.try do |s|
user.password_hash = authd.hash_password s
end
@email.try do |email|
user.contact.email = email
end
@phone.try do |phone|
user.contact.phone = phone
end
authd.users_per_uid.update user.uid.to_s, user
Response::UserEdited.new user.uid
end
end
AuthD.requests << ModUser
end

View File

@ -0,0 +1,46 @@
class AuthD::Request
IPC::JSON.message EditContacts, 16 do
property token : String
property email : String? = nil
property phone : String? = nil
def initialize(@token)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
user = authd.get_user_from_token @token
return Response::Error.new "invalid user" unless user
if email = @email
# FIXME: This *should* require checking the new mail, with
# a new activation key and everything else.
user.contact.email = email
end
authd.users_per_uid.update user
Response::UserEdited.new user.uid
end
end
AuthD.requests << EditContacts
IPC::JSON.message GetContacts, 18 do
property token : String
def initialize(@token)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
user = authd.get_user_from_token @token
return Response::Error.new "invalid user" unless user
_c = user.contact
Response::Contacts.new user.uid, _c.email, _c.phone
end
end
AuthD.requests << GetContacts
end

View File

@ -0,0 +1,69 @@
class AuthD::Request
IPC::JSON.message Delete, 17 do
# Deletion can be triggered by either an admin or the user.
property shared_key : String? = nil
property login : String? = nil
property password : String? = nil
property user : String | Int32
def initialize(@user, @login, @password)
end
def initialize(@user, @shared_key)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
uid_or_login = @user
user_to_delete = if uid_or_login.is_a? Int32
authd.users_per_uid.get? uid_or_login.to_s
else
authd.users_per_login.get? uid_or_login
end
if user_to_delete.nil?
return Response::Error.new "invalid user"
end
# Either the request comes from an admin or the user.
# Shared key == admin, check the key.
if key = @shared_key
return Response::Error.new "unauthorized (wrong shared key)" unless key == authd.configuration.shared_key
else
login = @login
pass = @password
if login.nil? || pass.nil?
return Response::Error.new "authentication failed (no shared key, no login)"
end
# authenticate the user
begin
user = authd.users_per_login.get login
rescue e : DODB::MissingEntry
return Response::Error.new "invalid credentials"
end
if user.nil?
return Response::Error.new "invalid credentials"
end
if user.password_hash != authd.hash_password pass
return Response::Error.new "invalid credentials"
end
# Is the user to delete the requesting user?
if user.uid != user_to_delete.uid
return Response::Error.new "invalid credentials"
end
end
# User or admin is now verified: let's proceed with the user deletion.
authd.users_per_login.delete user_to_delete.login
# TODO: better response
Response::User.new user_to_delete.to_public
end
end
AuthD.requests << Delete
end

View File

@ -0,0 +1,41 @@
class AuthD::Request
IPC::JSON.message ListUsers, 8 do
property token : String? = nil
property key : String? = nil
def initialize(@token, @key)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
# FIXME: Lines too long, repeatedly (>80c with 4c tabs).
@token.try do |token|
user = authd.get_user_from_token token
return Response::Error.new "unauthorized (user not found from token)" unless user
# Test if the user is a moderator.
if permissions = user.permissions["authd"]?
if rights = permissions["*"]?
if rights >= User::PermissionLevel::Read
else
raise AdminAuthorizationException.new "unauthorized (insufficient rights on '*')"
end
else
raise AdminAuthorizationException.new "unauthorized (no rights on '*')"
end
else
raise AdminAuthorizationException.new "unauthorized (user not in authd group)"
end
end
@key.try do |key|
return Response::Error.new "unauthorized (wrong shared key)" unless key == authd.configuration.shared_key
end
return Response::Error.new "unauthorized (no key nor token)" unless @key || @token
Response::UsersList.new authd.users.to_h.map &.[1].to_public
end
end
AuthD.requests << ListUsers
end

View File

@ -0,0 +1,125 @@
class AuthD::Request
IPC::JSON.message UpdatePassword, 7 do
property login : String
property old_password : String
property new_password : String
def initialize(@login, @old_password, @new_password)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
user = authd.users_per_login.get? @login
unless user
return Response::Error.new "invalid credentials"
end
if authd.hash_password(@old_password) != user.password_hash
return Response::Error.new "invalid credentials"
end
user.password_hash = authd.hash_password @new_password
authd.users_per_uid.update user.uid.to_s, user
Response::UserEdited.new user.uid
end
end
AuthD.requests << UpdatePassword
IPC::JSON.message PasswordRecovery, 11 do
property user : Int32 | String
property password_renew_key : String
property new_password : String
def initialize(@user, @password_renew_key, @new_password)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
uid_or_login = @user
user = if uid_or_login.is_a? Int32
authd.users_per_uid.get? uid_or_login.to_s
else
authd.users_per_login.get? uid_or_login
end
if user.nil?
return Response::Error.new "user not found"
end
if user.password_renew_key == @password_renew_key
user.password_hash = authd.hash_password @new_password
else
return Response::Error.new "renew key not valid"
end
user.password_renew_key = nil
authd.users_per_uid.update user.uid.to_s, user
Response::PasswordRecovered.new user.to_public
end
end
AuthD.requests << PasswordRecovery
IPC::JSON.message AskPasswordRecovery, 12 do
property user : Int32 | String
property email : String
def initialize(@user, @email)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
uid_or_login = @user
user = if uid_or_login.is_a? Int32
authd.users_per_uid.get? uid_or_login.to_s
else
authd.users_per_login.get? uid_or_login
end
if user.nil?
return Response::Error.new "no such user"
end
if user.contact.email != @email
# Same error as when users are not found.
return Response::Error.new "no such user"
end
user.password_renew_key = UUID.random.to_s
authd.users_per_uid.update user.uid.to_s, user
unless (activation_url = authd.configuration.activation_url).nil?
field_from = authd.configuration.field_from.not_nil!
activation_url = authd.configuration.activation_url.not_nil!
# Once the user is created and stored, we try to contact him
if authd.configuration.print_password_recovery_parameters
pp! user.login,
user.contact.email.not_nil!,
field_from,
activation_url,
user.password_renew_key.not_nil!
end
unless Process.run("password-recovery-mailer", [
"-l", user.login,
"-e", user.contact.email.not_nil!,
"-t", "Password recovery email",
"-f", field_from,
"-u", 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
end
end
AuthD.requests << AskPasswordRecovery
end

View File

@ -0,0 +1,113 @@
class AuthD::Request
IPC::JSON.message CheckPermission, 9 do
property shared_key : String? = nil
property token : String? = nil
property user : Int32 | String
property service : String
property resource : String
def initialize(@shared_key, @user, @service, @resource)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
authorized = false
if key = @shared_key
if key == authd.configuration.shared_key
authorized = true
else
return Response::Error.new "invalid key provided"
end
end
if token = @token
user = authd.get_user_from_token token
if user.nil?
return Response::Error.new "token does not match user"
end
if user.login != @user && user.uid != @user
return Response::Error.new "token does not match user"
end
authorized = true
end
unless authorized
return Response::Error.new "unauthorized"
end
user = case u = @user
when .is_a? Int32
authd.users_per_uid.get? u.to_s
else
authd.users_per_login.get? u
end
if user.nil?
return Response::Error.new "no such user"
end
service = @service
service_permissions = user.permissions[service]?
if service_permissions.nil?
return Response::PermissionCheck.new service, @resource, user.uid, User::PermissionLevel::None
end
resource_permissions = service_permissions[@resource]?
if resource_permissions.nil?
return Response::PermissionCheck.new service, @resource, user.uid, User::PermissionLevel::None
end
return Response::PermissionCheck.new service, @resource, user.uid, resource_permissions
end
end
AuthD.requests << CheckPermission
IPC::JSON.message SetPermission, 10 do
property shared_key : String
property user : Int32 | String
property service : String
property resource : String
property permission : ::AuthD::User::PermissionLevel
def initialize(@shared_key, @user, @service, @resource, @permission)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
unless @shared_key == authd.configuration.shared_key
return Response::Error.new "unauthorized"
end
user = authd.users_per_uid.get? @user.to_s
if user.nil?
return Response::Error.new "no such user"
end
service = @service
service_permissions = user.permissions[service]?
if service_permissions.nil?
service_permissions = Hash(String, User::PermissionLevel).new
user.permissions[service] = service_permissions
end
if @permission.none?
service_permissions.delete @resource
else
service_permissions[@resource] = @permission
end
authd.users_per_uid.update user.uid.to_s, user
Response::PermissionSet.new user.uid, service, @resource, @permission
end
end
AuthD.requests << SetPermission
end

View File

@ -0,0 +1,93 @@
class AuthD::Request
IPC::JSON.message EditProfile, 14 do
property token : String
property new_profile : Hash(String, JSON::Any)
def initialize(@token, @new_profile)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
user = authd.get_user_from_token @token
return Response::Error.new "invalid user" unless user
new_profile = @new_profile
profile = user.profile || Hash(String, JSON::Any).new
authd.configuration.read_only_profile_keys.each do |key|
if new_profile[key]? != profile[key]?
return Response::Error.new "tried to edit read only key"
end
end
user.profile = new_profile
authd.users_per_uid.update user.uid.to_s, user
Response::User.new user.to_public
end
end
AuthD.requests << EditProfile
# Same as above, but doesnt reset the whole profile, only resets elements
# for which keys are present in `new_profile`.
IPC::JSON.message EditProfileContent, 15 do
property token : String? = nil
property shared_key : String? = nil
property user : Int32 | String | Nil
property new_profile : Hash(String, JSON::Any)
def initialize(@shared_key, @user, @new_profile)
end
def initialize(@token, @new_profile)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
user = if token = @token
u = authd.get_user_from_token token
raise UserNotFound.new unless u
u
elsif shared_key = @shared_key
raise AdminAuthorizationException.new if shared_key != authd.configuration.shared_key
u = @user
raise UserNotFound.new unless u
u = if u.is_a? Int32
authd.users_per_uid.get? u.to_s
else
authd.users_per_login.get? u
end
raise UserNotFound.new unless u
u
else
raise AuthenticationInfoLacking.new
end
new_profile = user.profile || Hash(String, JSON::Any).new
unless @shared_key
authd.configuration.read_only_profile_keys.each do |key|
if @new_profile.has_key? key
return Response::Error.new "tried to edit read only key"
end
end
end
@new_profile.each do |key, value|
new_profile[key] = value
end
user.profile = new_profile
authd.users_per_uid.update user.uid.to_s, user
Response::User.new user.to_public
end
end
AuthD.requests << EditProfileContent
end

View File

@ -0,0 +1,92 @@
class AuthD::Request
IPC::JSON.message Register, 6 do
property login : String
property password : String
property email : String? = nil
property phone : String? = nil
property profile : Hash(String, JSON::Any)? = nil
def initialize(@login, @password, @email, @phone, @profile)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
if ! authd.configuration.registrations
return Response::Error.new "registrations not allowed"
end
if authd.users_per_login.get? @login
return Response::Error.new "login already used"
end
if authd.configuration.require_email && @email.nil?
return Response::Error.new "email required"
end
activation_url = authd.configuration.activation_url
if activation_url.nil?
# In this case we should not accept its registration.
return Response::Error.new "No activation URL were entered. Cannot send activation mails."
end
if ! @email.nil?
# Test on the email address format.
grok = Grok.new [ "%{EMAILADDRESS:email}" ]
result = grok.parse @email.not_nil!
email = result["email"]?
if email.nil?
return Response::Error.new "invalid email format"
end
end
# In this case we should not accept its registration.
if @password.size < 4
return Response::Error.new "password too short"
end
uid = authd.new_uid
password = authd.hash_password @password
user = User.new uid, @login, password
user.contact.email = @email unless @email.nil?
user.contact.phone = @phone unless @phone.nil?
@profile.try do |profile|
user.profile = profile
end
user.date_registration = Time.local
begin
field_subject = authd.configuration.field_subject.not_nil!
field_from = authd.configuration.field_from.not_nil!
activation_url = authd.configuration.activation_url.not_nil!
u_login = user.login
u_email = user.contact.email.not_nil!
u_activation_key = user.contact.activation_key.not_nil!
# Once the user is created and stored, we try to contact him
unless Process.run("activation-mailer", [
"-l", u_login,
"-e", u_email,
"-t", field_subject,
"-f", field_from,
"-u", activation_url,
"-a", u_activation_key
]).success?
raise "cannot contact user #{user.login} address #{user.contact.email}"
end
rescue e
Baguette::Log.error "activation-mailer: #{e}"
return Response::Error.new "cannot contact the user (not registered)"
end
# add the user only if we were able to send the confirmation mail
authd.users << user
Response::UserAdded.new user.to_public
end
end
AuthD.requests << Register
end

View File

@ -0,0 +1,34 @@
class AuthD::Request
IPC::JSON.message SearchUser, 13 do
property user : String
def initialize(@user)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
pattern = Regex.new @user, Regex::Options::IGNORE_CASE
matching_users = Array(AuthD::User::Public).new
users = authd.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
Baguette::Log.debug "#{u.login} matches #{pattern}"
matching_users << u.to_public
else
Baguette::Log.debug "#{u.login} doesn't match #{pattern}"
end
end
Response::MatchingUsers.new matching_users
end
end
AuthD.requests << SearchUser
end

View File

@ -0,0 +1,34 @@
class AuthD::Request
IPC::JSON.message GetToken, 0 do
property login : String
property password : String
def initialize(@login, @password)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
begin
user = authd.users_per_login.get @login
rescue e : DODB::MissingEntry
return Response::Error.new "invalid credentials"
end
if user.nil?
return Response::Error.new "invalid credentials"
end
if user.password_hash != authd.hash_password @password
return Response::Error.new "invalid credentials"
end
user.date_last_connection = Time.local
token = user.to_token
# change the date of the last connection
authd.users_per_uid.update user.uid.to_s, user
Response::Token.new (token.to_s authd.configuration.shared_key), user.uid
end
end
AuthD.requests << GetToken
end

View File

@ -0,0 +1,84 @@
class AuthD::Request
IPC::JSON.message ValidateUser, 2 do
property login : String
property activation_key : String
def initialize(@login, @activation_key)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
user = authd.users_per_login.get? @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 == @activation_key
user.contact.activation_key = nil
else
return Response::Error.new "wrong activation key"
end
authd.users_per_uid.update user.uid.to_s, user
Response::UserValidated.new user.to_public
end
end
AuthD.requests << ValidateUser
IPC::JSON.message GetUser, 3 do
property user : Int32 | String
def initialize(@user)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
uid_or_login = @user
user = if uid_or_login.is_a? Int32
authd.users_per_uid.get? uid_or_login.to_s
else
authd.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
end
end
AuthD.requests << GetUser
IPC::JSON.message GetUserByCredentials, 4 do
property login : String
property password : String
def initialize(@login, @password)
end
def handle(authd : AuthD::Service, event : IPC::Event::Events)
user = authd.users_per_login.get? @login
unless user
return Response::Error.new "invalid credentials"
end
if authd.hash_password(@password) != user.password_hash
return Response::Error.new "invalid credentials"
end
user.date_last_connection = Time.local
# change the date of the last connection
authd.users_per_uid.update user.uid.to_s, user
Response::User.new user.to_public
end
end
AuthD.requests << GetUserByCredentials
end

View File

@ -0,0 +1,10 @@
class AuthD::Response
IPC::JSON.message Contacts, 12 do
property user : Int32
property email : String? = nil
property phone : String? = nil
def initialize(@user, @email, @phone)
end
end
AuthD.responses << Contacts
end

View File

@ -0,0 +1,8 @@
class AuthD::Response
IPC::JSON.message Error, 0 do
property reason : String? = nil
def initialize(@reason)
end
end
AuthD.responses << Error
end

View File

@ -0,0 +1,15 @@
class AuthD::Response
IPC::JSON.message PasswordRecoverySent, 9 do
property user : ::AuthD::User::Public
def initialize(@user)
end
end
AuthD.responses << PasswordRecoverySent
IPC::JSON.message PasswordRecovered, 10 do
property user : ::AuthD::User::Public
def initialize(@user)
end
end
AuthD.responses << PasswordRecovered
end

View File

@ -0,0 +1,21 @@
class AuthD::Response
IPC::JSON.message PermissionCheck, 7 do
property user : Int32
property service : String
property resource : String
property permission : ::AuthD::User::PermissionLevel
def initialize(@service, @resource, @user, @permission)
end
end
AuthD.responses << PermissionCheck
IPC::JSON.message PermissionSet, 8 do
property user : Int32
property service : String
property resource : String
property permission : ::AuthD::User::PermissionLevel
def initialize(@user, @service, @resource, @permission)
end
end
AuthD.responses << PermissionSet
end

View File

@ -0,0 +1,9 @@
class AuthD::Response
IPC::JSON.message Token, 1 do
property uid : Int32
property token : String
def initialize(@token, @uid)
end
end
AuthD.responses << Token
end

View File

@ -0,0 +1,43 @@
class AuthD::Response
IPC::JSON.message User, 2 do
property user : ::AuthD::User::Public
def initialize(@user)
end
end
AuthD.responses << User
IPC::JSON.message UserAdded, 3 do
property user : ::AuthD::User::Public
def initialize(@user)
end
end
AuthD.responses << UserAdded
IPC::JSON.message UserEdited, 4 do
property uid : Int32
def initialize(@uid)
end
end
AuthD.responses << UserEdited
IPC::JSON.message UserValidated, 5 do
property user : ::AuthD::User::Public
def initialize(@user)
end
end
AuthD.responses << UserValidated
IPC::JSON.message UsersList, 6 do
property users : Array(::AuthD::User::Public)
def initialize(@users)
end
end
AuthD.responses << UsersList
IPC::JSON.message MatchingUsers, 11 do
property users : Array(::AuthD::User::Public)
def initialize(@users)
end
end
AuthD.responses << MatchingUsers
end

View File

@ -0,0 +1,29 @@
require "json"
class AuthD::Token
include JSON::Serializable
property login : String
property uid : Int32
def initialize(@login, @uid)
end
def to_h
{
:login => login,
:uid => uid
}
end
def to_s(key)
JWT.encode to_h, key, JWT::Algorithm::HS256
end
def self.from_s(key, str)
payload, meta = JWT.decode str, key, JWT::Algorithm::HS256
self.new payload["login"].as_s, payload["uid"].as_i
end
end

View File

@ -0,0 +1,76 @@
require "json"
require "uuid"
require "./token.cr"
class AuthD::User
include JSON::Serializable
enum PermissionLevel
None
Read
Edit
Admin
def to_json(o)
to_s.downcase.to_json o
end
end
class Contact
include JSON::Serializable
# the activation key is removed once the user is validated
property activation_key : String?
property email : String?
property phone : String?
def initialize(@email = nil, @phone = nil)
@activation_key = UUID.random.to_s
end
end
# Public.
property login : String
property uid : Int32
property profile : Hash(String, JSON::Any)?
# Private.
property contact : Contact
property password_hash : String
property password_renew_key : String?
# service => resource => permission level
property permissions : Hash(String, Hash(String, PermissionLevel))
property configuration : Hash(String, Hash(String, JSON::Any))
property date_last_connection : Time? = nil
property date_registration : Time? = nil
def to_token
Token.new @login, @uid
end
def initialize(@uid, @login, @password_hash)
@contact = Contact.new
@permissions = Hash(String, Hash(String, PermissionLevel)).new
@configuration = Hash(String, Hash(String, JSON::Any)).new
end
class Public
include JSON::Serializable
property login : String
property uid : Int32
property profile : Hash(String, JSON::Any)?
property date_registration : Time?
def initialize(@uid, @login, @profile, @date_registration)
end
end
def to_public : Public
Public.new @uid, @login, @profile, @date_registration
end
end