authd/src/authd.cr

631 lines
13 KiB
Crystal
Raw Normal View History

2019-11-22 18:14:52 +01:00
require "json"
require "jwt"
require "ipc"
require "./user.cr"
class AuthD::Exception < Exception
end
class AuthD::MalformedRequest < Exception
getter ipc_type : Int32
getter payload : String
def initialize(@ipc_type, @payload)
@message = "malformed payload"
end
end
2019-11-22 22:55:12 +01:00
class AuthD::Response
include JSON::Serializable
2020-08-12 18:33:32 +02:00
property id : JSON::Any?
2019-11-22 22:55:12 +01:00
annotation MessageType
end
class_getter type = -1
2019-11-22 23:13:34 +01:00
def type
@@type
end
2019-11-22 22:55:12 +01:00
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
2020-04-25 10:34:07 +02:00
property uid : Int32
2019-11-22 22:55:12 +01:00
property token : String
2020-04-25 10:34:07 +02:00
initialize :token, :uid
2019-11-22 22:55:12 +01:00
end
class User < Response
property user : ::AuthD::User::Public
2019-11-22 22:55:12 +01:00
initialize :user
end
class UserAdded < Response
property user : ::AuthD::User::Public
2019-11-22 22:55:12 +01:00
initialize :user
end
class UserEdited < Response
property uid : Int32
initialize :uid
end
2020-01-22 10:13:59 +01:00
class UserValidated < Response
2020-01-22 14:43:58 +01:00
property user : ::AuthD::User::Public
2020-01-22 10:13:59 +01:00
2020-01-22 14:43:58 +01:00
initialize :user
2020-01-22 10:13:59 +01:00
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
2019-12-09 21:57:38 +01:00
initialize :user, :service, :resource, :permission
2019-12-09 21:57:38 +01:00
end
2020-02-23 20:37:50 +01:00
class PasswordRecoverySent < Response
property user : ::AuthD::User::Public
initialize :user
end
class PasswordRecovered < Response
property user : ::AuthD::User::Public
initialize :user
end
2020-03-23 15:06:54 +01:00
class MatchingUsers < Response
property users : Array(::AuthD::User::Public)
initialize :users
end
2019-11-22 22:55:12 +01:00
# This creates a Request::Type enumeration. One entry for each request type.
{% begin %}
2019-11-22 17:31:56 +01:00
enum Type
2019-11-22 22:55:12 +01:00
{% 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 %}
2019-11-22 17:31:56 +01:00
end
2019-11-22 22:55:12 +01:00
{% end %}
# This is an array of all requests types.
{% begin %}
class_getter requests = [
{% for ivar in @type.subclasses %}
{% klass = ivar.name %}
{{klass}},
{% end %}
]
{% end %}
2019-11-23 01:08:05 +01:00
def self.from_ipc(message : IPC::Message) : Response?
2019-11-22 22:55:12 +01:00
payload = String.new message.payload
type = Type.new message.utype.to_i
2019-11-22 22:55:12 +01:00
begin
requests.find(&.type.==(type)).try &.from_json(payload)
rescue e : JSON::ParseException
raise MalformedRequest.new message.utype.to_i, payload
end
2019-11-22 22:55:12 +01:00
end
end
2019-11-22 18:14:52 +01:00
class AuthD::Request
include JSON::Serializable
2020-08-12 18:33:32 +02:00
property id : JSON::Any?
2019-11-22 18:14:52 +01:00
annotation MessageType
end
2019-11-22 18:14:52 +01:00
class_getter type = -1
macro inherited
def self.type
::AuthD::Request::Type::{{ @type.name.split("::").last.id }}
2019-11-22 17:31:56 +01:00
end
2019-11-22 18:14:52 +01:00
end
2018-12-19 13:54:19 +01:00
2019-11-22 18:14:52 +01:00
macro initialize(*properties)
def initialize(
{% for value in properties %}
@{{value.id}}{% if value != properties.last %},{% end %}
{% end %}
)
2019-11-22 17:31:56 +01:00
end
2019-01-07 17:04:20 +01:00
2019-11-22 18:14:52 +01:00
def type
Type::{{ @type.name.split("::").last.id }}
2019-11-22 17:31:56 +01:00
end
2019-11-22 18:14:52 +01:00
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
2020-01-22 01:55:57 +01:00
property email : String?
property phone : String?
property profile : Hash(String, JSON::Any)?
2019-11-22 18:14:52 +01:00
2020-01-22 01:55:57 +01:00
initialize :shared_key, :login, :password, :email, :phone, :profile
2019-11-22 18:14:52 +01:00
end
2020-01-22 10:13:59 +01:00
class ValidateUser < Request
2020-01-22 14:43:58 +01:00
property login : String
2020-01-22 10:13:59 +01:00
property activation_key : String
initialize :login, :activation_key
2020-01-22 10:13:59 +01:00
end
2019-11-22 18:14:52 +01:00
class GetUser < Request
property user : Int32 | String
2019-11-22 18:14:52 +01:00
initialize :user
2019-11-22 18:14:52 +01:00
end
2019-11-22 18:14:52 +01:00
class GetUserByCredentials < Request
property login : String
property password : String
2019-11-22 18:14:52 +01:00
initialize :login, :password
end
class ModUser < Request
property shared_key : String
property user : Int32 | String
2019-11-22 18:14:52 +01:00
property password : String?
property email : String?
property phone : String?
2019-11-22 18:14:52 +01:00
property avatar : String?
initialize :shared_key, :user
2019-11-22 18:14:52 +01:00
end
2020-08-12 18:33:32 +02:00
class Register < Request
property login : String
property password : String
2020-01-22 01:55:57 +01:00
property email : String?
property phone : String?
property profile : Hash(String, JSON::Any)?
2020-01-22 01:55:57 +01:00
initialize :login, :password, :email, :phone, :profile
end
2020-08-12 18:33:32 +02:00
class UpdatePassword < Request
property login : String
property old_password : String
property new_password : String
end
2020-08-12 18:33:32 +02:00
class ListUsers < Request
2019-12-09 21:57:38 +01:00
property token : String?
property key : String?
end
class CheckPermission < Request
property shared_key : String?
property token : String?
2020-09-05 02:46:03 +02:00
property user : Int32 | String
property service : String
property resource : String
initialize :shared_key, :user, :service, :resource
end
class SetPermission < Request
property shared_key : String
2020-09-05 02:46:03 +02:00
property user : Int32 | String
property service : String
property resource : String
property permission : ::AuthD::User::PermissionLevel
initialize :shared_key, :user, :service, :resource, :permission
end
2020-02-23 20:37:50 +01:00
class PasswordRecovery < Request
property user : Int32 | String
property password_renew_key : String
property new_password : String
2020-08-12 18:33:32 +02:00
initialize :user, :password_renew_key, :new_password
2020-02-23 20:37:50 +01:00
end
class AskPasswordRecovery < Request
property user : Int32 | String
2020-08-12 18:33:32 +02:00
property email : String
2020-02-23 20:37:50 +01:00
2020-08-12 18:33:32 +02:00
initialize :user, :email
2020-02-23 20:37:50 +01:00
end
2020-03-23 15:06:54 +01:00
class SearchUser < Request
property user : String
initialize :user
end
2020-04-18 21:21:17 +02:00
class EditProfile < Request
property token : String
property new_profile : Hash(String, JSON::Any)
2020-04-18 21:21:17 +02:00
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
2020-08-27 18:14:44 +02:00
class EditContacts < Request
property token : String
property email : String?
property phone : String?
end
2019-11-22 18:14:52 +01:00
# 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 %}
2019-11-22 17:31:56 +01:00
end
2019-11-22 18:14:52 +01:00
{% end %}
# This is an array of all requests types.
{% begin %}
class_getter requests = [
{% for ivar in @type.subclasses %}
{% klass = ivar.name %}
{{klass}},
{% end %}
]
{% end %}
2019-11-23 01:08:05 +01:00
def self.from_ipc(message : IPC::Message) : Request?
2019-11-22 18:14:52 +01:00
payload = String.new message.payload
type = Type.new message.utype.to_i
2019-11-22 18:14:52 +01:00
begin
requests.find(&.type.==(type)).try &.from_json(payload)
rescue e : JSON::ParseException
raise MalformedRequest.new message.utype.to_i, payload
end
2019-05-29 16:06:11 +02:00
end
2019-11-22 18:14:52 +01:00
end
module AuthD
2020-07-13 14:43:19 +02:00
class Client < IPC::Client
property key : String
def initialize
@key = ""
initialize "auth"
end
2019-06-06 01:16:52 +02:00
def get_token?(login : String, password : String) : String?
2019-11-22 18:14:52 +01:00
send Request::GetToken.new login, password
2019-11-22 22:55:12 +01:00
response = Response.from_ipc read
2019-11-22 22:55:12 +01:00
if response.is_a?(Response::Token)
response.token
else
nil
end
end
2019-12-19 03:58:00 +01:00
def get_user?(login : String, password : String) : AuthD::User::Public?
2019-11-22 18:14:52 +01:00
send Request::GetUserByCredentials.new login, password
2019-11-22 22:55:12 +01:00
response = Response.from_ipc read
2019-11-22 22:55:12 +01:00
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
2019-01-07 17:04:20 +01:00
response = Response.from_ipc read
2019-01-07 17:04:20 +01:00
if response.is_a? Response::User
response.user
2019-01-07 17:04:20 +01:00
else
nil
end
end
2019-11-22 17:31:56 +01:00
def send(type : Request::Type, payload)
2020-07-23 19:39:58 +02:00
send_now @server_fd, type.value.to_u8, payload
2018-12-19 13:54:19 +01:00
end
def decode_token(token)
2019-06-28 18:20:34 +02:00
user, meta = JWT.decode token, @key, JWT::Algorithm::HS256
user = ::AuthD::User::Public.from_json user.to_json
{user, meta}
end
2018-12-19 13:54:19 +01:00
# FIXME: Extra options may be useful to implement here.
2020-01-22 01:55:57 +01:00
def add_user(login : String, password : String,
email : String?,
phone : String?,
2020-09-26 14:27:47 +02:00
profile : Hash(String, JSON::Any)?) : ::AuthD::User::Public | Exception
2020-01-22 01:55:57 +01:00
2020-01-22 14:43:58 +01:00
send Request::AddUser.new @key, login, password, email, phone, profile
2018-12-19 13:54:19 +01:00
2019-11-22 22:55:12 +01:00
response = Response.from_ipc read
2018-12-19 13:54:19 +01:00
2019-11-22 22:55:12 +01:00
case response
when Response::UserAdded
response.user
when Response::Error
raise Exception.new response.reason
2018-12-19 13:54:19 +01:00
else
2019-11-22 22:55:12 +01:00
# Should not happen in serialized connections, but…
# itll happen if you run several requests at once.
Exception.new
2018-12-19 13:54:19 +01:00
end
end
2019-05-29 16:06:11 +02:00
2020-01-22 14:43:58 +01:00
def validate_user(login : String, activation_key : String) : ::AuthD::User::Public | Exception
send Request::ValidateUser.new login, activation_key
2020-01-22 10:13:59 +01:00
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
2020-02-23 20:37:50 +01:00
def ask_password_recovery(uid_or_login : String | Int32)
send Request::AskPasswordRecovery.new uid_or_login
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)
2020-08-12 18:33:32 +02:00
send Request::PasswordRecovery.new uid_or_login, renew_key, new_pass
2020-02-23 20:37:50 +01:00
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?,
2020-09-26 14:27:47 +02:00
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
2019-05-29 16:06:11 +02:00
2019-11-22 18:14:52 +01:00
request.password = password if password
request.email = email if email
request.phone = phone if phone
2019-11-22 18:14:52 +01:00
request.avatar = avatar if avatar
2019-05-29 19:45:03 +02:00
2019-11-22 18:14:52 +01:00
send request
2019-05-29 16:06:11 +02:00
2019-11-22 22:55:12 +01:00
response = Response.from_ipc read
2019-05-29 16:06:11 +02:00
2019-11-22 22:55:12 +01:00
case response
when Response::UserEdited
2019-05-29 16:06:11 +02:00
true
2019-11-22 22:55:12 +01:00
when Response::Error
Exception.new response.reason
2019-05-29 16:06:11 +02:00
else
2019-11-22 22:55:12 +01:00
Exception.new "???"
2019-05-29 16:06:11 +02:00
end
end
2020-09-05 02:46:03 +02:00
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
2020-03-23 15:06:54 +01:00
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
end
end
2020-07-14 17:48:16 +02:00
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?
2020-07-23 19:39:58 +02:00
send_now fd, request.type.to_u8, request.to_json
2020-07-14 17:48:16 +02:00
else
raise "Client not connected to the server"
end
end
end