New authd code structure.

This commit is contained in:
Karchnu 2020-11-22 13:49:34 +01:00
parent b717be649f
commit dbcfa4880b
22 changed files with 1291 additions and 1266 deletions

View File

@ -13,7 +13,7 @@ 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 shared_key : String = "nico-nico-nii" # Default authd key, as per the specs. :eyes:
property shared_key_file : String? = nil property shared_key_file : String? = nil
def initialize def initialize
@ -21,667 +21,11 @@ class Baguette::Configuration
end end
end end
class AuthD::Exception < Exception # Requests and responses.
end require "./exceptions"
class AuthD::MalformedRequest < Exception # Requests and responses.
getter ipc_type : Int32 require "./network"
getter payload : String
def initialize(@ipc_type, @payload)
@message = "malformed payload"
end
end
class AuthD::Response
include JSON::Serializable
property id : JSON::Any?
annotation MessageType
end
class_getter type = -1
def type
@@type
end
macro inherited
def self.type
::AuthD::Response::Type::{{ @type.name.split("::").last.id }}
end
end
macro initialize(*properties)
def initialize(
{% for value in properties %}
@{{value.id}}{% if value != properties.last %},{% end %}
{% end %}
)
end
def type
Type::{{ @type.name.split("::").last.id }}
end
end
class Error < Response
property reason : String?
initialize :reason
end
class Token < Response
property uid : Int32
property token : String
initialize :token, :uid
end
class User < Response
property user : ::AuthD::User::Public
initialize :user
end
class UserAdded < Response
property user : ::AuthD::User::Public
initialize :user
end
class UserEdited < Response
property uid : Int32
initialize :uid
end
class UserValidated < Response
property user : ::AuthD::User::Public
initialize :user
end
class UsersList < Response
property users : Array(::AuthD::User::Public)
initialize :users
end
class PermissionCheck < Response
property user : Int32
property service : String
property resource : String
property permission : ::AuthD::User::PermissionLevel
initialize :service, :resource, :user, :permission
end
class PermissionSet < Response
property user : Int32
property service : String
property resource : String
property permission : ::AuthD::User::PermissionLevel
initialize :user, :service, :resource, :permission
end
class PasswordRecoverySent < Response
property user : ::AuthD::User::Public
initialize :user
end
class PasswordRecovered < Response
property user : ::AuthD::User::Public
initialize :user
end
class MatchingUsers < Response
property users : Array(::AuthD::User::Public)
initialize :users
end
class Contacts < Response
property user : Int32
property email : String?
property phone : String?
initialize user, email, phone
end
# This creates a Request::Type enumeration. One entry for each request type.
{% begin %}
enum Type
{% for ivar in @type.subclasses %}
{% klass = ivar.name %}
{% name = ivar.name.split("::").last.id %}
{% a = ivar.annotation(MessageType) %}
{% if a %}
{% value = a[0] %}
{{ name }} = {{ value }}
{% else %}
{{ name }}
{% end %}
{% end %}
end
{% end %}
# This is an array of all requests types.
{% begin %}
class_getter requests = [
{% for ivar in @type.subclasses %}
{% klass = ivar.name %}
{{klass}},
{% end %}
]
{% end %}
def self.from_ipc(message : IPC::Message) : Response?
payload = String.new message.payload
type = Type.new message.utype.to_i
begin
requests.find(&.type.==(type)).try &.from_json(payload)
rescue e : JSON::ParseException
raise MalformedRequest.new message.utype.to_i, payload
end
end
end
class AuthD::Request
include JSON::Serializable
property id : JSON::Any?
annotation MessageType
end
class_getter type = -1
macro inherited
def self.type
::AuthD::Request::Type::{{ @type.name.split("::").last.id }}
end
end
macro initialize(*properties)
def initialize(
{% for value in properties %}
@{{value.id}}{% if value != properties.last %},{% end %}
{% end %}
)
end
def type
Type::{{ @type.name.split("::").last.id }}
end
end
class GetToken < Request
property login : String
property password : String
initialize :login, :password
end
class AddUser < Request
# 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?
property phone : String?
property profile : Hash(String, JSON::Any)?
initialize :shared_key, :login, :password, :email, :phone, :profile
end
class ValidateUser < Request
property login : String
property activation_key : String
initialize :login, :activation_key
end
class GetUser < Request
property user : Int32 | String
initialize :user
end
class GetUserByCredentials < Request
property login : String
property password : String
initialize :login, :password
end
class ModUser < Request
property shared_key : String
property user : Int32 | String
property password : String?
property email : String?
property phone : String?
property avatar : String?
initialize :shared_key, :user
end
class Register < Request
property login : String
property password : String
property email : String?
property phone : String?
property profile : Hash(String, JSON::Any)?
initialize :login, :password, :email, :phone, :profile
end
class UpdatePassword < Request
property login : String
property old_password : String
property new_password : String
end
class ListUsers < Request
property token : String?
property key : String?
end
class CheckPermission < Request
property shared_key : String?
property token : String?
property user : Int32 | String
property service : String
property resource : String
initialize :shared_key, :user, :service, :resource
end
class SetPermission < Request
property shared_key : String
property user : Int32 | String
property service : String
property resource : String
property permission : ::AuthD::User::PermissionLevel
initialize :shared_key, :user, :service, :resource, :permission
end
class PasswordRecovery < Request
property user : Int32 | String
property password_renew_key : String
property new_password : String
initialize :user, :password_renew_key, :new_password
end
class AskPasswordRecovery < Request
property user : Int32 | String
property email : String
initialize :user, :email
end
class SearchUser < Request
property user : String
initialize :user
end
class EditProfile < Request
property token : String
property new_profile : Hash(String, JSON::Any)
initialize :token, :new_profile
end
# Same as above, but doesnt reset the whole profile, only resets elements
# for which keys are present in `new_profile`.
class EditProfileContent < Request
property token : String?
property shared_key : String?
property user : Int32 | String | Nil
property new_profile : Hash(String, JSON::Any)
initialize :shared_key, :user, :new_profile
initialize :token, :new_profile
end
class EditContacts < Request
property token : String
property email : String?
property phone : String?
end
class Delete < Request
# Deletion can be triggered by either an admin or the user.
property shared_key : String?
property login : String?
property password : String?
property user : String | Int32
initialize :user, :login, :password
initialize :user, :shared_key
end
class GetContacts < Request
property token : String
end
# This creates a Request::Type enumeration. One entry for each request type.
{% begin %}
enum Type
{% for ivar in @type.subclasses %}
{% klass = ivar.name %}
{% name = ivar.name.split("::").last.id %}
{% a = ivar.annotation(MessageType) %}
{% if a %}
{% value = a[0] %}
{{ name }} = {{ value }}
{% else %}
{{ name }}
{% end %}
{% end %}
end
{% end %}
# This is an array of all requests types.
{% begin %}
class_getter requests = [
{% for ivar in @type.subclasses %}
{% klass = ivar.name %}
{{klass}},
{% end %}
]
{% end %}
def self.from_ipc(message : IPC::Message) : Request?
payload = String.new message.payload
type = Type.new message.utype.to_i
begin
requests.find(&.type.==(type)).try &.from_json(payload)
rescue e : JSON::ParseException
raise MalformedRequest.new message.utype.to_i, payload
end
end
end
module AuthD
class Client < IPC::Client
property key : String
def initialize
@key = ""
initialize "auth"
end
def get_token?(login : String, password : String) : String?
send Request::GetToken.new login, password
response = Response.from_ipc read
if response.is_a?(Response::Token)
response.token
else
nil
end
end
def get_user?(login : String, password : String) : AuthD::User::Public?
send Request::GetUserByCredentials.new login, password
response = Response.from_ipc 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 Request::GetUser.new uid_or_login
response = Response.from_ipc read
if response.is_a? Response::User
response.user
else
nil
end
end
def send(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 Request::AddUser.new @key, login, password, email, phone, profile
response = Response.from_ipc 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 Request::ValidateUser.new login, activation_key
response = Response.from_ipc 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 Request::AskPasswordRecovery.new uid_or_login, email
response = Response.from_ipc 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 Request::PasswordRecovery.new uid_or_login, renew_key, new_pass
response = Response.from_ipc 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 Request::Register.new login, password, email, phone, profile
response = Response.from_ipc 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 request
response = Response.from_ipc 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 request
response = Response.from_ipc 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 request
response = Response.from_ipc 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 Request::SearchUser.new user_login
response = Response.from_ipc 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 Request::EditProfileContent.new key, user, new_values
response = Response.from_ipc 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 Request::Delete.new user, key
delete_
end
def delete(user : Int32 | String, login : String, pass : String)
send Request::Delete.new user, login, pass
delete_
end
def delete_
response = Response.from_ipc read
case response
when Response::Error
raise Exception.new response.reason
end
response
end
end
end
class IPC::Context
def send(fd, response : AuthD::Response)
send fd, response.type.to_u8, response.to_json
end
end
class IPC::Client
def send(request : AuthD::Request)
unless (fd = @server_fd).nil?
send_now fd, request.type.to_u8, request.to_json
else
raise "Client not connected to the server"
end
end
end
# Functions to request the authd server.
require "./libclient.cr"

13
src/exceptions.cr Normal file
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

242
src/libclient.cr Normal file
View File

@ -0,0 +1,242 @@
module AuthD
class Client < IPC::Client
property key : String
def initialize
@key = ""
initialize "auth"
end
def get_token?(login : String, password : String) : String?
send Request::GetToken.new login, password
response = Response.from_ipc read
if response.is_a?(Response::Token)
response.token
else
nil
end
end
def get_user?(login : String, password : String) : AuthD::User::Public?
send Request::GetUserByCredentials.new login, password
response = Response.from_ipc 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 Request::GetUser.new uid_or_login
response = Response.from_ipc read
if response.is_a? Response::User
response.user
else
nil
end
end
def send(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 Request::AddUser.new @key, login, password, email, phone, profile
response = Response.from_ipc 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 Request::ValidateUser.new login, activation_key
response = Response.from_ipc 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 Request::AskPasswordRecovery.new uid_or_login, email
response = Response.from_ipc 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 Request::PasswordRecovery.new uid_or_login, renew_key, new_pass
response = Response.from_ipc 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 Request::Register.new login, password, email, phone, profile
response = Response.from_ipc 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 request
response = Response.from_ipc 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 request
response = Response.from_ipc 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 request
response = Response.from_ipc 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 Request::SearchUser.new user_login
response = Response.from_ipc 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 Request::EditProfileContent.new key, user, new_values
response = Response.from_ipc 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 Request::Delete.new user, key
delete_
end
def delete(user : Int32 | String, login : String, pass : String)
send Request::Delete.new user, login, pass
delete_
end
def delete_
response = Response.from_ipc read
case response
when Response::Error
raise Exception.new response.reason
end
response
end
end
end

View File

@ -29,11 +29,17 @@ class Baguette::Configuration
end end
end end
class AuthD::Service # Provides a JWT-based authentication scheme for service-specific users.
property configuration : Baguette::Configuration::Auth class AuthD::Service < IPC::Server
property configuration : Baguette::Configuration::Auth
@users_per_login : DODB::Index(User) # DB and its indexes.
@users_per_uid : DODB::Index(User) 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) def initialize(@configuration)
@users = DODB::DataBase(User).new @configuration.storage @users = DODB::DataBase(User).new @configuration.storage
@ -45,6 +51,8 @@ class AuthD::Service
if @configuration.recreate_indexes if @configuration.recreate_indexes
@users.reindex_everything! @users.reindex_everything!
end end
super "auth"
end end
def hash_password(password : String) : String def hash_password(password : String) : String
@ -67,573 +75,48 @@ class AuthD::Service
uid uid
end end
def handle_request(request : AuthD::Request?) def handle_request(event : IPC::Event::MessageReceived)
case request request_start = Time.utc
when Request::GetToken
begin request = AuthD.requests.parse_ipc_json event.message
user = @users_per_login.get request.login
rescue e : DODB::MissingEntry if request.nil?
return Response::Error.new "invalid credentials" raise "unknown request type"
end end
if user.nil? request_name = request.class.name.sub /^AuthD::Request::/, ""
return Response::Error.new "invalid credentials" Baguette::Log.debug "<< #{request_name}"
end
response = begin
if user.password_hash != hash_password request.password request.handle self, event
return Response::Error.new "invalid credentials" rescue e : UserNotFound
end Baguette::Log.error "#{request_name} user not found"
AuthD::Response::Error.new "authorization error"
user.date_last_connection = Time.local rescue e : AuthenticationInfoLacking
token = user.to_token Baguette::Log.error "#{request_name} lacking authentication info"
AuthD::Response::Error.new "authorization error"
# change the date of the last connection rescue e : AdminAuthorizationException
@users_per_uid.update user.uid.to_s, user Baguette::Log.error "#{request_name} admin authentication failed"
AuthD::Response::Error.new "authorization error"
Response::Token.new (token.to_s @configuration.shared_key), user.uid rescue e
when Request::AddUser Baguette::Log.error "#{request_name} generic error #{e}"
# No verification of the users' informations when an admin adds it. AuthD::Response::Error.new "unknown error"
# No mail address verification. end
if request.shared_key != @configuration.shared_key
return Response::Error.new "invalid authentication key" # If clients sent requests with an “id” field, it is copied
end # in the responses. Allows identifying responses easily.
response.id = request.id
if @users_per_login.get? request.login
return Response::Error.new "login already used" send event.fd, response
end
duration = Time.utc - request_start
if @configuration.require_email && request.email.nil?
return Response::Error.new "email required" response_name = response.class.name.sub /^AuthD::Response::/, ""
end
if response.is_a? AuthD::Response::Error
password_hash = hash_password request.password Baguette::Log.warning ">> #{response_name} (#{response.reason})"
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 != @configuration.shared_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 ! @configuration.registrations
return Response::Error.new "registrations not allowed"
end
if @users_per_login.get? request.login
return Response::Error.new "login already used"
end
if @configuration.require_email && request.email.nil?
return Response::Error.new "email required"
end
activation_url = @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 ! 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
# In this case we should not accept its registration.
if request.password.size < 4
return Response::Error.new "password too short"
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
begin
field_subject = @configuration.field_subject.not_nil!
field_from = @configuration.field_from.not_nil!
activation_url = @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
@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 == @configuration.shared_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
authorized = false
if key = request.shared_key
if key == @configuration.shared_key
authorized = true
else
return Response::Error.new "invalid key provided"
end
end
if token = request.token
user = get_user_from_token token
if user.nil?
return Response::Error.new "token does not match user"
end
if user.login != request.user && user.uid != request.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 = request.user
when .is_a? Int32
@users_per_uid.get? u.to_s
else
@users_per_login.get? u
end
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 == @configuration.shared_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 "no such user"
end
if user.contact.email != request.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
@users_per_uid.update user.uid.to_s, user
unless (activation_url = @configuration.activation_url).nil?
field_from = @configuration.field_from.not_nil!
activation_url = @configuration.activation_url.not_nil!
# Once the user is created and stored, we try to contact him
if @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
when Request::PasswordRecovery
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::PasswordRecovered.new user.to_public
when Request::SearchUser
pattern = Regex.new request.user, Regex::Options::IGNORE_CASE
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
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
when Request::EditProfile
user = get_user_from_token request.token
return Response::Error.new "invalid user" unless user
new_profile = request.new_profile
profile = user.profile || Hash(String, JSON::Any).new
@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
@users_per_uid.update user.uid.to_s, user
Response::User.new user.to_public
when Request::EditProfileContent
user = if token = request.token
user = get_user_from_token token
return Response::Error.new "invalid user" unless user
user
elsif shared_key = request.shared_key
return Response::Error.new "invalid shared key" if shared_key != @configuration.shared_key
user = request.user
return Response::Error.new "invalid user" unless user
user = if user.is_a? Int32
@users_per_uid.get? user.to_s
else
@users_per_login.get? user
end
return Response::Error.new "invalid user" unless user
user
else
return Response::Error.new "no token or shared_key/user pair"
end
new_profile = user.profile || Hash(String, JSON::Any).new
unless request.shared_key
@configuration.read_only_profile_keys.each do |key|
if request.new_profile.has_key? key
return Response::Error.new "tried to edit read only key"
end
end
end
request.new_profile.each do |key, value|
new_profile[key] = value
end
user.profile = new_profile
@users_per_uid.update user.uid.to_s, user
Response::User.new user.to_public
when Request::EditContacts
user = get_user_from_token request.token
return Response::Error.new "invalid user" unless user
if email = request.email
# FIXME: This *should* require checking the new mail, with
# a new activation key and everything else.
user.contact.email = email
end
@users_per_uid.update user
Response::UserEdited.new user.uid
when Request::Delete
uid_or_login = request.user
user_to_delete = 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_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 = request.shared_key
return Response::Error.new "unauthorized (wrong shared key)" unless key == @configuration.shared_key
else
login = request.login
pass = request.password
if login.nil? || pass.nil?
return Response::Error.new "authentication failed (no shared key, no login)"
end
# authenticate the user
begin
user = @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 != 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.
@users_per_login.delete user_to_delete.login
# TODO: better response
Response::User.new user_to_delete.to_public
when Request::GetContacts
user = get_user_from_token request.token
return Response::Error.new "invalid user" unless user
_c = user.contact
Response::Contacts.new user.uid, _c.email, _c.phone
else else
Response::Error.new "unhandled request type" Baguette::Log.debug ">> #{response_name} (Total duration: #{duration})"
end end
end end
@ -644,51 +127,36 @@ class AuthD::Service
end end
def run def run
## Baguette::Log.title "Starting authd"
# Provides a JWT-based authentication scheme for service-specific users.
server = IPC::Server.new "auth"
server.base_timer = @configuration.ipc_timer
server.timer = @configuration.ipc_timer
server.loop do |event|
if event.is_a? IPC::Exception
Baguette::Log.error "IPC::Exception"
pp! event
next
end
@base_timer = @configuration.ipc_timer
@timer = @configuration.ipc_timer
self.loop do |event|
case event case event
when IPC::Event::Timer when IPC::Event::Timer
Baguette::Log.debug "Timer" if @configuration.print_ipc_timer Baguette::Log.debug "Timer" if @configuration.print_ipc_timer
when IPC::Event::MessageReceived when IPC::Event::MessageReceived
Baguette::Log.debug "Received message from #{event.fd}" if @configuration.print_ipc_message_received
begin begin
request = Request.from_ipc(event.message) handle_request event
if request.nil?
raise "Unknown request (#{event.message.utype.to_i})"
end
Baguette::Log.info "<< #{request.class.name.sub /^Request::/, ""}"
response = handle_request request
response.id = request.id
server.send event.fd, response
rescue e : MalformedRequest
Baguette::Log.error "#{e.message}"
Baguette::Log.error " .. type was: #{e.ipc_type}"
Baguette::Log.error " .. tried class was: #{Request.requests.find(&.type.==(e.ipc_type)).to_s}"
Baguette::Log.error " .. payload was: #{e.payload}"
Baguette::Log.error " .. tried class was: #{Request.requests.find(&.type.==(e.ipc_type)).to_s}"
response = Response::Error.new e.message
rescue e rescue e
Baguette::Log.error "#{e.message}" Baguette::Log.error "#{e.message}"
response = Response::Error.new e.message # send event.fd, Response::Error.new e.message
end end
Baguette::Log.info ">> #{response.class.name.sub /^Response::/, ""}" when IPC::Event::MessageSent
Baguette::Log.debug "Message sent to #{event.fd}" if @configuration.print_ipc_message_sent
when IPC::Exception
Baguette::Log.error "IPC::Exception"
pp! event
else
Baguette::Log.error "Not implemented behavior for event: #{event}"
end end
end end
end end
end end

34
src/network.cr Normal file
View File

@ -0,0 +1,34 @@
require "ipc"
require "json"
require "ipc/json"
class IPC::JSON
def handle(service : AuthD::Service, event : IPC::Event::Events)
raise "unimplemented"
end
end
module AuthD
class_getter requests = [] of IPC::JSON.class
class_getter responses = [] of IPC::JSON.class
end
class IPC::Context
def send(fd, response : AuthD::Response)
send fd, response.type.to_u8, response.to_json
end
end
class IPC::Client
def send(request : AuthD::Request)
unless (fd = @server_fd).nil?
send_now fd, request.type.to_u8, request.to_json
else
raise "Client not connected to the server"
end
end
end
require "./requests/*"
require "./responses/*"

100
src/requests/admin.cr Normal file
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

46
src/requests/contact.cr Normal file
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

69
src/requests/delete.cr Normal file
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

41
src/requests/list.cr Normal file
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

125
src/requests/password.cr Normal file
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

113
src/requests/permissions.cr Normal file
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

93
src/requests/profile.cr Normal file
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

92
src/requests/register.cr Normal file
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

34
src/requests/search.cr Normal file
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

34
src/requests/token.cr Normal file
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

84
src/requests/users.cr Normal file
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

9
src/responses/contact.cr Normal file
View File

@ -0,0 +1,9 @@
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
end

7
src/responses/errors.cr Normal file
View File

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

13
src/responses/password.cr Normal file
View File

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

View File

@ -0,0 +1,19 @@
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
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
end

8
src/responses/token.cr Normal file
View File

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

37
src/responses/users.cr Normal file
View File

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