Compare commits

...

96 Commits

Author SHA1 Message Date
Philippe PITTOLI d790caa4e1 Client: do not raise exceptions on expected possible errors. 2024-05-07 10:54:25 +02:00
Philippe PITTOLI a3368d0228 Use libsodium. Cryptographic configuration is WIP. 2024-05-02 01:16:01 +02:00
Philippe PITTOLI d0a058e0fb PasswordTooLong 2024-05-01 00:30:49 +02:00
Philippe PITTOLI c2f5c5e98c Log when indexes are being recreated. 2024-04-28 23:22:07 +02:00
Philippe Pittoli 1987316df0 Add "HOME" envvar while invoking the mailer for password recovery. 2024-03-24 01:00:02 +01:00
Philippe Pittoli c40fd13193 Only compile the server by default. 2024-03-23 11:47:30 +01:00
Philippe Pittoli 6c352cf5ae Chomp Base64 emails. 2024-03-19 02:03:55 +01:00
Philippe Pittoli af696b8768 AskPasswordRecovery now compliant with the netlib.re interface. New email index. 2024-03-19 01:49:30 +01:00
Philippe Pittoli 2df24a583e (minor) change the way logs are printed. 2024-03-17 23:16:18 +01:00
Philippe Pittoli 9ff86ae846 Makefile: add `wipe-db` and `release` rules + OPTS contains `--progress` by default. 2024-03-17 23:09:14 +01:00
Philippe Pittoli ab18c0005d Compile with options (release mode). 2024-03-17 05:40:48 +01:00
Philippe Pittoli 5ac733cbc3 UserID: from Int32 to UInt32. 2024-03-16 05:45:15 +01:00
Philippe Pittoli c462bbafa1 Makefile: compile the applications only when a file changed. 2024-03-13 14:40:10 +01:00
Philippe Pittoli 95b7b76f6c (minor) put some comments to show how to use the Process API. 2024-02-20 23:26:33 +01:00
Philippe Pittoli 4b21c8bec1 Do not print KeepAlive messages by default. 2024-02-20 23:24:52 +01:00
Philippe Pittoli 4c07699082 Implement Keep Alive messages. 2024-02-20 17:14:37 +01:00
Philippe Pittoli 66ebcb662d Add authentication by token. 2024-02-17 18:26:23 +01:00
Philippe Pittoli 4a10456f27 Search: try to match the email address too. 2024-02-17 18:13:12 +01:00
Philippe Pittoli 494b0b3719 Running processes: add a "HOME" envvar (may be required in some environments). 2024-02-16 02:04:12 +01:00
Philippe Pittoli 4f84b4c5d6 Forbid the authentication of a user until he validates his email address. 2024-02-11 20:25:08 +01:00
Philippe Pittoli a2f5442565 Print which file descriptor we are talking to (or are receiving from). 2023-07-02 12:11:31 +02:00
Philippe Pittoli 95c4af02f0 Small contribution. 2023-06-16 01:28:10 +02:00
Philippe Pittoli e3f52ff088 Client: validation + get user. 2023-06-15 02:43:24 +02:00
Philippe Pittoli ecc2290a09 Client: better error messages for register operation. 2023-06-15 01:11:16 +02:00
Philippe Pittoli f24926fe1a makefile: register command. 2023-06-15 00:41:56 +02:00
Philippe Pittoli 03b0b7a95b Client can now register users. 2023-06-15 00:41:22 +02:00
Philippe Pittoli 719e47a28d Register: fix login regex. 2023-06-15 00:40:59 +02:00
Philippe Pittoli 8bcae74efc makefile: permissions implemented. 2023-06-15 00:03:12 +02:00
Philippe Pittoli 9ce976afc6 Client: permissions implemented. 2023-06-15 00:02:53 +02:00
Philippe Pittoli bfbfa79b62 makefile: now with login! 2023-06-14 21:00:15 +02:00
Philippe Pittoli 9c75522d01 Can now authenticated from the client. 2023-06-14 20:16:42 +02:00
Philippe Pittoli 2024325092 DeleteUser now returns a UserDeleted response. 2023-06-14 18:39:36 +02:00
Philippe Pittoli 00c202a63a New DecodeToken request. 2023-06-14 18:39:23 +02:00
Philippe Pittoli a441431e05 makefile creates documentation and serves it thanks to darkhttpd. 2023-06-14 18:38:31 +02:00
Philippe Pittoli 538faf5004 Client now is simpler (a single function parses responses). 2023-06-14 18:37:16 +02:00
Philippe Pittoli da641a6d3d Remove redundant error messages. 2023-06-14 03:25:19 +02:00
Philippe Pittoli b047a3fd6f All explicit errors in requests are now dedicated errors. 2023-06-14 02:07:03 +02:00
Philippe Pittoli 71cbb1d519 Invalid credentials error message. 2023-06-14 01:51:55 +02:00
Philippe Pittoli 7dfbeacd68 makefile: print error messages 2023-06-14 01:47:40 +02:00
Philippe Pittoli d66afffc15 Errors now have dedicated messages. 2023-06-14 01:46:38 +02:00
Philippe Pittoli 33b47766e5 s/shared key/secret key/ + new bootstrap request + some cleaning. 2023-06-13 23:40:34 +02:00
Philippe Pittoli 186edd2ca0 s/UserID | Nil/UserID?/ and allow simple users to read their permissions. 2023-06-13 18:37:58 +02:00
Philippe Pittoli 7324bdb619 README: talk about permissions. 2023-06-13 18:34:51 +02:00
Philippe Pittoli dab3a70e3e README: add + update + fix most explanations. Sill very much WIP. 2023-06-13 18:15:47 +02:00
Philippe Pittoli f58de2ce36 Load a file to get the AuthD::Service definition. 2023-06-13 03:24:08 +02:00
Philippe Pittoli 0d904a5d72 make print-message-numbers 2023-06-13 03:16:59 +02:00
Philippe Pittoli f6311be77f Change message numbers. 2023-06-13 03:15:08 +02:00
Philippe Pittoli fb65c32848 Remove messages: GetContacts ListUsers UpdatePassword ReplaceProfile. 2023-06-13 02:32:15 +02:00
Philippe Pittoli 3d8d74e8b7 Some inconsistencies have been dealt with. Authorization rules are documented. 2023-06-13 01:32:54 +02:00
Philippe Pittoli 67adb6ef51 Removing "phone" and EditContact message (ModUser could be used instead). 2023-06-12 23:24:49 +02:00
Philippe Pittoli 641d89dd43 make print-messages 2023-06-12 20:54:41 +02:00
Philippe Pittoli 3e807ad63e Fix ModUser logic. 2023-06-12 20:54:04 +02:00
Philippe Pittoli cb358ef1de More fine-grained authorizations and remove useless message GetUserByCredentials. 2023-06-12 14:40:03 +02:00
Philippe Pittoli 3e40a4ce3d TODO: remind that the current client doesn't have much features. 2023-06-12 01:59:33 +02:00
Philippe Pittoli b98399e030 Simplification is done. Still some minor inconsistencies to fix. 2023-06-12 01:56:31 +02:00
Philippe Pittoli af22ea8d18 Register: require at least 20-byte passwords. 2023-06-12 01:55:06 +02:00
Philippe Pittoli f9ad5e1d93 TODO: some more inconsistencies. 2023-06-12 01:54:34 +02:00
Philippe Pittoli 0621d21ce0 Simplification continues with the Profile class. 2023-06-12 01:03:52 +02:00
Philippe Pittoli 5f3f208798 Permissions: code simplification. 2023-06-11 21:27:52 +02:00
Philippe Pittoli 52ee731921 TODO.md: document some inconsistencies to fix. 2023-06-11 21:27:18 +02:00
Philippe Pittoli cf97fab773 New TODO.md + improved password management (simpler code and messages). 2023-06-11 21:10:03 +02:00
Philippe Pittoli 2786e2f7ff Refactoring in progress. Still a few classes to go. 2023-06-11 18:59:41 +02:00
Philippe Pittoli 7958e7812e Slowly embrace the logged-authenticated-user logic. 2023-06-11 16:39:16 +02:00
Philippe Pittoli 4989218a79 WIP: remove "shared keys logic" and use logged user hash. 2023-06-10 18:27:50 +02:00
Philippe Pittoli 2a267ea7a2 Authd: users are now logged and have an 'admin' attribute. 2023-06-10 17:26:12 +02:00
Philippe Pittoli 683bcb5970 makefile: setup and run-authd rules. 2023-06-08 17:33:43 +02:00
Philippe Pittoli 1f2f72fba8 Add a makefile. 2023-05-29 20:41:12 +02:00
Philippe Pittoli d9e0e6b09b Authd now uses '-k' instead of '-K' (like other services). 2023-02-15 15:19:22 +01:00
Philippe Pittoli 78838f5ab9 New file structure: authd can now be used as a simple library. 2023-02-10 09:51:53 +01:00
Philippe Pittoli 34f1077757 Update whole structure + new LibIPC API. 2023-02-09 17:55:34 +01:00
Philippe Pittoli 3ddcd181dd hexdigest => hexfinal 2023-01-25 05:53:35 +01:00
Karchnu 841d2dc7c2 Filling tho responses array. 2020-12-13 03:00:45 +01:00
Karchnu c39e639ab1 Add a reference to the AuthD::Service definition in the client. 2020-12-04 00:08:24 +01:00
Karchnu ccd288a156 Take into account connections and disconnections. 2020-11-26 06:56:20 +01:00
Karchnu 9b33f686bd Client bugfix. 2020-11-26 04:02:19 +01:00
Karchnu dbcfa4880b New authd code structure. 2020-11-22 13:49:34 +01:00
Karchnu b717be649f Configuration simplification. 2020-11-08 22:50:24 +01:00
Luka Vandervelden b70443409b GetContacts request. 2020-10-31 00:46:14 +01:00
Luka Vandervelden b69afedbd2 Merge branch 'master' of ssh://git.baguette.netlib.re:2299/Baguette/authd 2020-10-30 18:59:38 +01:00
Karchnu f335be76de Parser: now gives a better understanding of the error. 2020-10-26 19:24:57 +01:00
Karchnu df22fe8a9b recreate_indexes 2020-10-26 00:16:47 +01:00
Karchnu bf0571c31b password too short 2020-10-25 23:54:37 +01:00
Karchnu d12c125c22 fix key management dedup 2020-10-25 19:36:28 +01:00
Karchnu 9524f969c4 print_password_recovery_parameters 2020-10-25 19:09:13 +01:00
Karchnu 4b68daf016 Server: simpler configuration system. 2020-10-25 03:57:58 +01:00
Karchnu 21480c5866 Two stage option parsing. 2020-10-25 02:03:07 +01:00
Karchnu cf9c6220e6 Fix: Baguette::Configuration::Auth in the wrong file. 2020-10-23 19:16:49 +02:00
Karchnu 43668b8e5d Baguette::Configuration::Auth 2020-10-23 16:30:35 +02:00
Karchnu 9a921b6ac7 Display the target class on JSON parsing error. 2020-10-14 17:02:06 +02:00
Luka Vandervelden ecd6b555a1 Merge branch 'master' of ssh://git.baguette.netlib.re:2299/Baguette/authd into master 2020-10-14 13:30:17 +02:00
Luka Vandervelden 7c7c5641e1 Debug message removed. 2020-10-14 13:28:07 +02:00
Karchnu 624289a112 Merge branch 'master' of ssh://git.baguette.netlib.re:2299/Baguette/authd 2020-10-13 18:05:41 +02:00
Karchnu f8d98ab1a1 authc CLI for authd 2020-10-13 18:01:59 +02:00
Luka Vandervelden 585fee5b9f Some debug messages removed. 2020-10-11 21:00:41 +02:00
Luka Vandervelden b90facdb82 Merge branch 'master' of ssh://git.baguette.netlib.re:2299/Baguette/authd into master 2020-10-09 15:52:46 +02:00
Luka Vandervelden 851a3accfe Permission checking can be done by users. 2020-10-09 15:51:36 +02:00
43 changed files with 2206 additions and 1842 deletions

View File

@ -1,43 +1,32 @@
# authd # authd
authd is a token-based authentication micro-service. authd is a token-based authentication micro-service.
> TODO: explain basic concepts behind `authd`.
## Build ## Build
`authd` is written in Crystal and uses `build.zsh` as Makefile generator, as `authd` is written in Crystal.
well as shards to fetch dependencies. Youll need the following tools:
Youll need the following tools to build authd:
- crystal - crystal
- shards - shards
- build.zsh
- make - make
To build authd, run the following commands: To build authd, run the following commands:
``` ```
shards install
make make
``` ```
Note that if you clone authd from its repository, its `Makefile` may be missing.
In such situations, run `build.zsh -c` to generate it, after which `make` should run fine.
## Deployment ## Deployment
``` ```
$ authd --help $ authd --help
usage: authd [options]
-s directory, --storage directory
Directory in which to store users.
-K file, --key-file file JWT key file
-R --allow-registrations
-h, --help Show this help
$
``` ```
> TODO: documentation on how to deploy, including with the mail server and tools.
### Users storage ### Users storage
The storage directory will default to `./storage`. The storage directory will default to `./storage`.
@ -45,29 +34,11 @@ The storage directory will default to `./storage`.
No SQL database, database management system or other kind of setup is required to run authd and store users. No SQL database, database management system or other kind of setup is required to run authd and store users.
To migrate an instance of authd, a simple copy of the storage directory will be enough. To migrate an instance of authd, a simple copy of the storage directory will be enough.
Make sure your copy preserves symlinks, as those are extensively used.
### Administrating users ### Administrating users
The `authd-user-add` and `authd-user-allow` are tools to add users to authds database and to edit their permissions. > TODO: document how to manage users through `authc`.
The permission level `none` can be used in `authd-user-allow` to remove a permission.
### Key file
authd will provide users with cryptographically signed tokens.
To sign and check those tokens, a shared key is required between authd and services using authd.
authd reads that key from a file to prevent it being visible on the command line when running authd.
Any content is acceptable as a key file.
Example:
```
$ echo "I am a key." > key-file
$ authd -K ./key-file
```
## APIs ## APIs
@ -75,22 +46,34 @@ $ authd -K ./key-file
authds protocol is still subject to change. authds protocol is still subject to change.
> TODO: document messages.
### Libraries ### Libraries
> TODO: document basic functions in the `AuthD::Client` class to exchange messages with `authd`.
A `AuthD::Client` Crystal class is available to build synchronous clients in Crystal. A `AuthD::Client` Crystal class is available to build synchronous clients in Crystal.
```crystal # Authorization rules
require "authd"
authd = AuthD::Client.new Logged users can:
authd.key = File.read("./some-file").chomp - retrieve public data of any user **individually**
- change their own data: password, email address, profile entries (except the read-only ones)
- delete their account
- check their own permissions
pp! r = authd.get_token?("login", "password") Admins with 'Read' permission on the '*' resource can:
- list users
- check permissions of other users
pp! r = authd.add_user("login", "password") Admins with 'Edit' permission on the '*' resource can:
- change data of another user
pp! u = authd.get_user?("login", "password").not_nil! Admins with 'Admin' permission on the '*' resource (or the 'admin' boolean) can:
``` - change read-only profile entries
- change permissions
- delete a user
- uprank and downrank admins
## Contributing ## Contributing
@ -98,3 +81,15 @@ Pull requests are welcome. For major changes, please open an issue first to disc
Please make sure to update tests as appropriate. Please make sure to update tests as appropriate.
# WIP: design choices
An user has a number, a login, an email address, a profile (`Hash(String, JSON::Any)`) and permissions.
An `admin` boolean also tells weither or not the user is an administrator.
**Requests work mostly on current user**.
Some take a *UserID* to identify another user (its number or its login, both are valid), which often implies admin permissions.
**Permissions** are: None, Read, Edit, Admin.
Plus, the `admin` boolean value in the `AuthD::User` class.
> TODO: continue explaining design choices.

22
TODO.md Normal file
View File

@ -0,0 +1,22 @@
### Consistency in error management
**Both exceptions and error reponses are used**.
A choice should be made between the two options.
A combinaison of both is fine as long as the logic is comprehensively documented.
**Response::Error** class is overused.
A simple error message is given instead of specific messages for each recurring error.
In the same time, some exceptions (such as **AdminAuthenticationException**) are used a few times for the same kind of errors.
### Structures, not classes
Maybe in some cases, it could be great to use structures instead of classes.
They are simpler, use less memory and computation.
### CLI client
Current client **authc** lacks most requests.
### Documentation
Documentation isn't started, yet. TODO!

26
bin/get-messages.awk Executable file
View File

@ -0,0 +1,26 @@
#!/bin/awk -f
# Provides message parameters and numbers.
# Use: cat src/requests/*.cr | ./bin/get-messages.awk
BEGIN {
OFS="\t"
should_print = 0
}
/def initialize/ {
should_print = 0
print ""
}
# Print line only when we should:
# - when in a message class
# - when the line isn't empty
should_print == 1 && /[0-9a-zA-Z]/ {
print
}
/IPC::JSON.message/ || /IPC::CBOR.message/ {
print $3, $2
should_print = 1
}

17
bin/should-update Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh
# Should we run the build?
if [ $# -lt 1 ]; then
exec >& 2
echo "Usage: $0 <exe>"
exit 1
fi
exe=$1
# If the binary hasn't already be compiled.
[ -f "${exe}" ] || exit 0
v=`find src/ -type f -newer "${exe}" | wc -l`
test "${v}" != "0"

101
makefile Normal file
View File

@ -0,0 +1,101 @@
all: build-server
Q ?= @
SHOULD_UPDATE = ./bin/should-update
OPTS ?= --progress
NAME ?= John
EMAIL ?= john@example.com
# For requests where authentication is required.
LOGIN ?=
ifeq ($(LOGIN),)
LOGIN_OPT =
else
LOGIN_OPT = -l $(LOGIN)
endif
##################
### SETUP COMMANDS
##################
PASSWORD_FILE ?= /tmp/PASSWORD
setup:
@[ -f $(PASSWORD_FILE) ] || echo -n "coucou" > $(PASSWORD_FILE)
DATA_DIRECTORY ?= /tmp/DATA-AUTHD
run-authd: setup
./bin/authd -k $(PASSWORD_FILE) -R -E --storage $(DATA_DIRECTORY)
# First user always is the admin.
add-first-user:
./bin/authc bootstrap $(NAME) $(EMAIL)
####################
### REQUEST EXAMPLES
####################
add-user:
./bin/authc user add $(NAME) $(EMAIL) $(LOGIN_OPT)
register:
./bin/authc user register $(NAME) $(EMAIL)
ACTIVATION_KEY ?= put-your-key-here
validate:
./bin/authc user validate $(NAME) $(ACTIVATION_KEY)
get-user:
./bin/authc user get $(NAME) $(LOGIN_OPT)
SERVICE ?= 'auth'
RESOURCE ?= '*'
UID ?= 1000
permission-check:
./bin/authc permission check $(UID) $(SERVICE) $(RESOURCE) $(LOGIN_OPT)
PERMISSION ?= Read
permission-set:
./bin/authc permission set $(UID) $(SERVICE) $(RESOURCE) $(PERMISSION) $(LOGIN_OPT)
###################
### DEVELOPER TOOLS
###################
build-server:
$(Q)-$(SHOULD_UPDATE) bin/authd && shards build authd $(OPTS)
build-client:
$(Q)-$(SHOULD_UPDATE) bin/authc && shards build authc $(OPTS)
build: build-server build-client
doc:
crystal docs
HTTPD_ACCESS_LOGS ?= /tmp/access-authd-docs.log
HTTPD_ADDR ?= 127.0.0.1
HTTPD_PORT ?= 9000
DIR ?= docs
serve-doc:
darkhttpd $(DIR) --addr $(HTTPD_ADDR) --port $(HTTPD_PORT) --log $(HTTPD_ACCESS_LOGS)
print-messages:
cat src/requests/*.cr | ./bin/get-messages.awk
print-message-numbers:
make -s print-messages | grep -E "^[0-9]" | sort -n
print-messages-without-comments:
make -s print-messages | grep -vE '^[[:blank:]]+#'
print-response-messages:
cat src/responses/*.cr | ./bin/get-messages.awk
print-response-message-numbers:
make -s print-response-messages | grep -E "^[0-9]" | sort -n
print-response-messages-without-comments:
make -s print-response-messages | grep -vE '^[[:blank:]]+#'
wipe-db:
rm -r $(DATA_DIRECTORY)
release:
make build-server OPTS="--progress --release"

View File

@ -1,52 +1,43 @@
name: authd name: authd
version: 0.2.0 version: 0.1.0
authors:
- Karchnu <karchnu@karchnu.fr>
- Luka Vandervelden <lukc@upyum.com>
description: | description: |
JWT-based authentication daemon. JWT-based authentication daemon.
authors:
- Philippe Pittoli <karchnu@karchnu.fr>
targets: targets:
authd: authd:
main: src/main.cr main: src/server.cr
auth-user-perms: authc:
main: utils/authd-user-perms.cr main: src/client.cr
auth-user-add:
main: utils/authd-user-add.cr
auth-user-allow:
main: utils/authd-user-allow.cr
auth-user-ask-for-new-password:
main: utils/authd-user-ask-for-new-password.cr
auth-user-get:
main: utils/authd-user-get.cr
auth-user-mod:
main: utils/authd-user-mod.cr
auth-user-validate:
main: utils/authd-user-validate.cr
auth-user-search:
main: utils/authd-user-search.cr
crystal: 0.35.1 crystal: 1.7.1
dependencies: dependencies:
grok: sodium:
github: spinscale/grok.cr branch: master
passwd: github: didactic-drunk/sodium.cr
git: https://git.baguette.netlib.re/Baguette/passwd.cr grok:
branch: master github: spinscale/grok.cr
ipc: passwd:
git: https://git.baguette.netlib.re/Baguette/ipc.cr git: https://git.baguette.netlib.re/Baguette/passwd.cr
branch: master branch: master
jwt: jwt:
github: crystal-community/jwt github: crystal-community/jwt
branch: master branch: master
baguette-crystal-base: baguette-crystal-base:
git: https://git.baguette.netlib.re/Baguette/baguette-crystal-base git: https://git.baguette.netlib.re/Baguette/baguette-crystal-base
branch: master branch: master
dodb: dodb:
git: https://git.baguette.netlib.re/Baguette/dodb.cr git: https://git.baguette.netlib.re/Baguette/dodb.cr
branch: master branch: master
cbor:
git: https://git.baguette.netlib.re/Baguette/crystal-cbor
branch: master
ipc:
git: https://git.baguette.netlib.re/Baguette/ipc.cr
branch: master
license: EUPL license: ISC

View File

@ -1,629 +1,43 @@
require "json" require "uuid"
require "option_parser"
require "openssl"
require "colorize"
require "jwt" require "jwt"
require "grok"
require "dodb"
require "ipc" require "ipc"
require "./user.cr" require "baguette-crystal-base"
class AuthD::Exception < Exception # In any message, a user can be referred by its UInt32 uid or its login.
end alias UserID = UInt32 | String
class AuthD::MalformedRequest < Exception # Allows get configuration from a provided file.
getter ipc_type : Int32 # See Baguette::Configuration::Base.get
getter payload : String class Baguette::Configuration
class Auth < IPC
include YAML::Serializable
def initialize(@ipc_type, @payload) property login : String? = nil
@message = "malformed payload" property pass : String? = nil
end property secret_key : String = "nico-nico-nii" # Default authd key, as per the specs. :eyes:
end property secret_key_file : String? = nil
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
# 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 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
# 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 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)
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)
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 end
end end
end end
class IPC::Context # Token and user classes.
def send(fd, response : AuthD::Response) require "./authd/token.cr"
send fd, response.type.to_u8, response.to_json require "./authd/user.cr"
end
end
class IPC::Client # Requests and responses.
def send(request : AuthD::Request) require "./authd/exceptions"
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
# Requests and responses.
require "./network"
# Functions to request the authd server.
require "./authd/client.cr"

192
src/authd/client.cr Normal file
View File

@ -0,0 +1,192 @@
require "ipc/json"
require "json"
module AuthD
class Client < IPC
property server_fd : Int32 = -1
def initialize
super
fd = self.connect "auth"
if fd.nil?
raise "couldn't connect to 'auth' IPC service"
end
@server_fd = fd
end
def read
slice = self.read @server_fd
m = IPCMessage::TypedMessage.deserialize slice
m.not_nil!
end
# `parse_message` only raises exception in case the response could not be anticipated.
# Any possibly expected error for a request should be in `expected_messages`.
def parse_message(expected_messages, message)
em = Array(IPC::JSON.class).new
expected_messages.each do |e|
em << e
end
# response = AuthD.responses.parse_ipc_json read
em.parse_ipc_json message
end
def login?(login : String, password : String)
send_now Request::Login.new login, password
parse_message [Response::Login, Response::ErrorInvalidCredentials], read
end
def get_user?(uid_or_login : UserID)
send_now Request::GetUser.new uid_or_login
parse_message [
Response::User,
Response::ErrorMustBeAuthenticated,
Response::ErrorUserNotFound
], read
end
def send_now(msg : IPC::JSON)
m = IPCMessage::TypedMessage.new msg.type.to_u8, msg.to_json
write @server_fd, m
end
def send_now(type : Request::Type, payload)
m = IPCMessage::TypedMessage.new type.value.to_u8, payload
write @server_fd, m
end
# FIXME: Extra options may be useful to implement here.
def add_user(login : String, password : String,
admin : Bool,
email : String?,
profile : Hash(String, ::JSON::Any)?)
send_now Request::AddUser.new login, password, admin, email, profile
parse_message [
Response::UserAdded,
Response::ErrorMustBeAuthenticated,
Response::ErrorAlreadyUsedLogin,
Response::ErrorMailRequired
], read
end
def bootstrap(login : String,
password : String,
email : String,
profile : Hash(String, ::JSON::Any)? = nil)
send_now Request::BootstrapFirstAdmin.new login, password, email, profile
parse_message [Response::UserAdded,Response::ErrorAlreadyUsersInDB], read
end
def decode_token(token)
send_now Request::DecodeToken.new token
parse_message [
Response::User,
Response::ErrorMustBeAuthenticated,
Response::ErrorUserNotFound
], read
end
def validate_user(login : String, activation_key : String)
send_now Request::ValidateUser.new login, activation_key
parse_message [
Response::UserValidated,
Response::ErrorUserNotFound,
Response::ErrorUserAlreadyValidated,
Response::ErrorInvalidActivationKey
], read
end
def ask_password_recovery(uid_or_login : UserID)
send_now Request::AskPasswordRecovery.new uid_or_login
parse_message [Response::PasswordRecoverySent, Response::ErrorUserNotFound], read
end
def change_password(uid_or_login : UserID, new_pass : String, renew_key : String)
send_now Request::PasswordRecovery.new uid_or_login, renew_key, new_pass
parse_message [
Response::PasswordRecovered,
Response::ErrorUserNotFound,
Response::ErrorInvalidRenewKey
], read
end
def register(login : String,
password : String,
email : String?,
profile : Hash(String, ::JSON::Any)?)
send_now Request::Register.new login, password, email, profile
parse_message [
Response::UserAdded,
Response::ErrorRegistrationsClosed,
Response::ErrorAlreadyUsedLogin,
Response::ErrorInvalidLoginFormat,
Response::ErrorMailRequired,
Response::ErrorInvalidEmailFormat,
Response::ErrorPasswordTooShort,
Response::ErrorPasswordTooLong,
Response::ErrorCannotContactUser
], read
end
def mod_user(uid_or_login : UserID, password : String? = nil, email : String? = nil)
request = Request::ModUser.new uid_or_login
request.password = password if password
request.email = email if email
send_now request
parse_message [
Response::ErrorMustBeAuthenticated,
Response::ErrorUserNotFound,
Response::UserEdited
], read
end
def check_permission(user : UserID, service_name : String, resource_name : String)
request = Request::CheckPermission.new user, service_name, resource_name
send_now request
parse_message [
Response::PermissionCheck,
Response::ErrorMustBeAuthenticated,
Response::ErrorUserNotFound
], read
end
def set_permission(user : UserID, service : String, resource : String, permission : User::PermissionLevel)
request = Request::SetPermission.new user, service, resource, permission
send_now request
parse_message [
Response::PermissionSet,
Response::ErrorMustBeAuthenticated,
Response::ErrorUserNotFound
], read
end
def search_user(user_login : String)
send_now Request::SearchUser.new user_login
parse_message [Response::MatchingUsers, Response::ErrorMustBeAuthenticated], read
end
def edit_profile_content(user : UserID, new_values)
send_now Request::EditProfileEntries.new user, new_values
parse_message [
Response::User,
Response::ErrorMustBeAuthenticated,
Response::ErrorUserNotFound,
Response::ErrorReadOnlyProfileKeys
], read
end
def delete(user : UserID)
send_now Request::Delete.new user
parse_message [
Response::ErrorMustBeAuthenticated,
Response::ErrorUserNotFound,
Response::UserDeleted
], read
end
end
end

13
src/authd/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

View File

@ -4,7 +4,7 @@ class AuthD::Token
include JSON::Serializable include JSON::Serializable
property login : String property login : String
property uid : Int32 property uid : UInt32
def initialize(@login, @uid) def initialize(@login, @uid)
end end
@ -23,7 +23,7 @@ class AuthD::Token
def self.from_s(key, str) def self.from_s(key, str)
payload, meta = JWT.decode str, key, JWT::Algorithm::HS256 payload, meta = JWT.decode str, key, JWT::Algorithm::HS256
self.new payload["login"].as_s, payload["uid"].as_i self.new payload["login"].as_s, payload["uid"].as_i64.to_u32
end end
end end

View File

@ -1,9 +1,6 @@
require "json" require "json"
require "uuid" require "uuid"
require "./token.cr"
class AuthD::User class AuthD::User
include JSON::Serializable include JSON::Serializable
@ -22,18 +19,21 @@ class AuthD::User
include JSON::Serializable include JSON::Serializable
# the activation key is removed once the user is validated # the activation key is removed once the user is validated
property activation_key : String? property activation_key : String? = nil
property email : String? property email : String?
property phone : String?
def initialize(@email = nil, @phone = nil) def initialize(@email = nil)
end
def new_activation_key
@activation_key = UUID.random.to_s @activation_key = UUID.random.to_s
end end
end end
# Public. # Public.
property login : String property login : String
property uid : Int32 property uid : UInt32
property admin : Bool = false
property profile : Hash(String, JSON::Any)? property profile : Hash(String, JSON::Any)?
# Private. # Private.
@ -50,7 +50,7 @@ class AuthD::User
Token.new @login, @uid Token.new @login, @uid
end end
def initialize(@uid, @login, @password_hash) def initialize(@uid, @login, @password_hash, @admin = false)
@contact = Contact.new @contact = Contact.new
@permissions = Hash(String, Hash(String, PermissionLevel)).new @permissions = Hash(String, Hash(String, PermissionLevel)).new
@configuration = Hash(String, Hash(String, JSON::Any)).new @configuration = Hash(String, Hash(String, JSON::Any)).new
@ -60,17 +60,35 @@ class AuthD::User
include JSON::Serializable include JSON::Serializable
property login : String property login : String
property uid : Int32 property uid : UInt32
property admin : Bool = false
property profile : Hash(String, JSON::Any)? property profile : Hash(String, JSON::Any)?
property date_registration : Time? property date_registration : Time?
def initialize(@uid, @login, @profile, @date_registration) def initialize(@uid, @login, @admin, @profile, @date_registration)
end end
end end
def to_public : Public def to_public : Public
Public.new @uid, @login, @profile, @date_registration Public.new @uid, @login, @admin, @profile, @date_registration
end
def assert_permission(service : String, resource : String, level : User::PermissionLevel)
return if @admin # skip if admin
permissions = @permissions[service]?
unless permissions
raise AdminAuthorizationException.new "unauthorized (not admin nor in #{service} group)"
end
rights = permissions[resource]?
unless rights
raise AdminAuthorizationException.new "unauthorized (no rights on '#{service}/#{resource}')"
end
if rights < level
raise AdminAuthorizationException.new "unauthorized (insufficient rights on '#{service}/#{resource}')"
end
end end
end end

218
src/better-parser.cr Normal file
View File

@ -0,0 +1,218 @@
require "option_parser"
# frequently used functions
opt_authd_login = -> (parser : OptionParser) {
parser.on "-l LOGIN", "--login LOGIN", "Authd user login." do |login|
Context.authd_login = login
Baguette::Log.info "User login for authd: #{Context.authd_login}."
end
parser.on "-p PASSWORD", "--password PASSWORD", "Authd user password." do |password|
Context.authd_pass = password
Baguette::Log.info "User password for authd: #{Context.authd_pass}."
end
}
opt_help = -> (parser : OptionParser) {
parser.on "help", "Prints this help message." do
puts parser
exit 0
end
}
opt_profile = -> (parser : OptionParser) {
parser.on "-P file", "--profile file", "Read the user profile from a file." do |file|
Context.user_profile = JSON.parse(File.read file).as_h
Baguette::Log.info "Reading the user profile: #{Context.user_profile}."
end
}
opt_email = -> (parser : OptionParser) {
parser.on "-e email", "--email address", "Email address." do |email|
Context.email = email
Baguette::Log.info "Reading the user email address: #{Context.email}."
end
}
# Unrecognized parameters are used to create commands with multiple arguments.
# Example: user add login email
# Here, login and email are unrecognized arguments.
# Still, the "user add" command expect them.
unrecognized_args_to_context_args = -> (parser : OptionParser, n_expected_args : Int32) {
# With the right args, these will be interpreted as serialized data.
parser.unknown_args do |args|
if args.size != n_expected_args
Baguette::Log.error "expected number of arguments: #{n_expected_args}, received: #{args.size}"
Baguette::Log.error "args: #{args}"
Baguette::Log.error "#{parser}"
exit 1
end
args.each do |arg|
Baguette::Log.debug "Unrecognized argument: #{arg} (adding to Context.args)"
if Context.args.nil?
Context.args = Array(String).new
end
Context.args.not_nil! << arg
end
end
}
parser = OptionParser.new do |parser|
parser.banner = "usage: #{PROGRAM_NAME} command help"
parser.on "-v verbosity", "--verbosity v", "Verbosity. From 0 to 4 (debug)." do |v|
Baguette::Context.verbosity = v.to_i
Baguette::Log.info "verbosity = #{v}"
end
parser.on "-h", "--help", "Prints this help message." do
puts "usage: #{PROGRAM_NAME} command help"
puts parser
exit 0
end
parser.on "bootstrap", "Add the first user (an admin)." do
parser.banner = "Usage: bootstrap login email [-P profile]"
Baguette::Log.info "Bootstrapping the first user (admin) to the DB."
Context.command = "bootstrap"
opt_profile.call parser
opt_help.call parser
# login email
unrecognized_args_to_context_args.call parser, 2
end
parser.on "user", "Operations on users." do
parser.banner = "Usage: user [add | mod | delete | validate | search | get | recover | register ]"
parser.on "add", "Adding a user to the DB." do
parser.banner = "usage: user add login email [-P profile] [opt]"
Baguette::Log.info "Adding a user to the DB."
Context.command = "user-add"
opt_authd_login.call parser
opt_profile.call parser
opt_help.call parser
# login email
unrecognized_args_to_context_args.call parser, 2
end
parser.on "mod", "Modify a user account." do
parser.banner = "Usage: user mod userid [-e email|-P profile] [opt]"
Baguette::Log.info "Modify a user account."
Context.command = "user-mod"
opt_authd_login.call parser
opt_email.call parser
opt_profile.call parser
opt_help.call parser
# userid
unrecognized_args_to_context_args.call parser, 1
end
parser.on "delete", "Remove user." do
parser.banner = "Usage: user delete userid [opt]"
Baguette::Log.info "Remove user."
Context.command = "user-delete"
# You can either be the owner of the account, or an admin.
opt_authd_login.call parser
opt_help.call parser
# userid
unrecognized_args_to_context_args.call parser, 1
end
parser.on "validate", "Validate user." do
parser.banner = "Usage: user validate login activation-key [opt]"
Baguette::Log.info "Validate user."
Context.command = "user-validation"
# No need to be authenticated.
opt_help.call parser
# login activation-key
unrecognized_args_to_context_args.call parser, 2
end
parser.on "get", "Get user info." do
parser.banner = "Usage: user get login [opt]"
Baguette::Log.info "Get user info."
Context.command = "user-get"
opt_authd_login.call parser
opt_help.call parser
# login
unrecognized_args_to_context_args.call parser, 1
end
parser.on "search", "Search user." do
parser.banner = "Usage: user recover login [opt]"
Baguette::Log.info "Search user."
Context.command = "user-search"
opt_authd_login.call parser
opt_help.call parser
# login
unrecognized_args_to_context_args.call parser, 1
end
parser.on "recover", "Recover user password." do
parser.banner = "Usage: user recover login [opt]"
Baguette::Log.info "Recover user password."
Context.command = "user-recovery"
# No need to be authenticated.
opt_help.call parser
# login email
unrecognized_args_to_context_args.call parser, 1
end
# Do not require to be admin.
parser.on "register", "Register a user (requires activation)." do
parser.banner = "Usage: user register login email [-P profile] [opt]"
Baguette::Log.info "Register a user (requires activation)."
Context.command = "user-registration"
opt_profile.call parser
opt_help.call parser
# login email
unrecognized_args_to_context_args.call parser, 2
end
end
parser.on "permission", "Permissions management." do
parser.banner = "Usage: permissions [check | set]"
parser.on "set", "Set permissions." do
parser.banner = <<-END
usage: permission set user application resource permission
example: permission set 1002 my-application chat read
permission list: none read edit admin
END
Baguette::Log.info "Set permissions."
Context.command = "permission-set"
opt_authd_login.call parser
opt_help.call parser
# userid application resource permission
unrecognized_args_to_context_args.call parser, 4
end
parser.on "check", "Check permissions." do
parser.banner = <<-END
usage: permission check user application resource
example: permission check 1002 my-application chat
permission list: none read edit admin
END
Baguette::Log.info "Check permissions."
Context.command = "permission-check"
opt_authd_login.call parser
opt_help.call parser
# userid application resource
unrecognized_args_to_context_args.call parser, 3
end
end
parser.unknown_args do |args|
if args.size > 0
Baguette::Log.warning "Unknown args: #{args}"
end
end
# parser.on "-X user-password", "--user-password pass", "Read the new user password." do |pass|
# password = pass
# end
end
parser.parse

278
src/client.cr Normal file
View File

@ -0,0 +1,278 @@
require "option_parser"
require "yaml"
require "./authd.cr"
class Context
class_property simulation = false # do not perform the action
class_property authd_login : String? = nil
class_property authd_pass : String? = nil
# # Properties to select what to display when printing a deal.
# class_property print_title = true
# class_property print_description = true
# class_property print_owner = true
# class_property print_nb_comments = true
class_property command = "not-implemented"
class_property user_profile : Hash(String,JSON::Any)?
class_property email : String?
# Will be parsed later, with a specific parser.
class_property args : Array(String)? = nil
end
require "./better-parser"
class Actions
def self.ask_password
STDOUT << "password: "
STDOUT << `stty -echo`
STDOUT.flush
password = STDIN.gets.try &.chomp
STDOUT << '\n'
STDOUT << `stty echo`
password
end
def self.ask_something(str : String) : String?
STDOUT << "#{str} "
STDOUT.flush
answer = STDIN.gets.try &.chomp
answer
end
property the_call = {} of String => Proc(Nil)
property authd : AuthD::Client
def initialize(@authd)
@the_call["user-registration"] = ->user_registration
@the_call["user-validation"] = ->user_validation # Do not require authentication.
@the_call["user-recovery"] = ->user_recovery # Do not require authentication.
@the_call["user-delete"] = ->user_deletion # Do not require admin priviledges.
@the_call["user-get"] = ->user_get
@the_call["user-search"] = ->user_search
@the_call["bootstrap"] = ->bootstrap
# Require admin privileges.
@the_call["user-add"] = ->user_add
@the_call["user-mod"] = ->user_mod
@the_call["permission-set"] = ->permission_set
@the_call["permission-check"] = ->permission_check
end
#
# For all functions: the number of arguments is already tested.
#
def user_add
args = Context.args.not_nil!
login, email = args[0..1]
profile = Context.user_profile
password = Actions.ask_password
exit 1 unless password
# By default: not admin.
pp! authd.add_user login, password.not_nil!, false, email, profile: profile
rescue e : AuthD::Exception
puts "error: #{e.message}"
end
def user_registration
args = Context.args.not_nil!
login, email = args[0..1]
profile = Context.user_profile
password = Actions.ask_password
unless password
Baguette::Log.error "no password!"
exit 1
end
res = authd.register login, password.not_nil!, email, profile: profile
case res
when Response::UserAdded
Baguette::Log.info "user registered, mail sent"
exit 0
when Response::ErrorRegistrationsClosed
Baguette::Log.error "registrations are closed (only admins can add users)"
exit 1
when Response::ErrorAlreadyUsedLogin
Baguette::Log.error "login already used"
exit 1
when Response::ErrorMailRequired
Baguette::Log.error "an email address is required"
exit 1
when Response::ErrorInvalidEmailFormat
Baguette::Log.error "provided email address has an invalid format"
exit 1
when Response::ErrorCannotContactUser
Baguette::Log.error "an error occured while contacting the user with this email address"
exit 1
when Response::ErrorInvalidLoginFormat
Baguette::Log.error "invalid login"
exit 1
when Response::ErrorPasswordTooShort
Baguette::Log.error "password too short"
exit 1
end
rescue e
puts "error: #{e.message}"
end
def bootstrap
puts "Bootstrap"
args = Context.args.not_nil!
login, email = args[0..1]
profile = Context.user_profile
password = Actions.ask_password
exit 1 unless password
pp! authd.bootstrap login, password.not_nil!, email, profile
rescue e : AuthD::Exception
puts "error: #{e.message}"
end
# TODO
def user_mod
args = Context.args.not_nil!
userid = args[0]
password : String? = nil
should_ask_password = Actions.ask_something "Should we change the password (Yn) ?" || "n"
case should_ask_password
when /y/i
Baguette::Log.debug "Ok let's change the password!"
password = Actions.ask_password
exit 1 unless password
else
Baguette::Log.debug "Ok no change in password."
end
email = Context.email
Baguette::Log.error "This function shouldn't be used for now."
Baguette::Log.error "It is way too cumbersome."
# res = authd.add_user login, password, email, profile: profile
# puts res
end
def user_deletion
args = Context.args.not_nil!
userid = args[0].to_u32
res = authd.delete userid
puts res
end
def user_validation
args = Context.args.not_nil!
login, activation_key = args[0..1]
pp! authd.validate_user login, activation_key
end
def user_search
args = Context.args.not_nil!
login = args[0]
pp! authd.search_user login
end
def user_get
args = Context.args.not_nil!
login = args[0]
pp! authd.get_user? login
end
def user_recovery
args = Context.args.not_nil!
login = args[0]
pp! authd.ask_password_recovery login
end
def permission_check
args = Context.args.not_nil!
user, application, resource = args[0..2]
res = @authd.check_permission user.to_u32, application, resource
case res
when Response::PermissionCheck
s = res.service
r = res.resource
u = res.user
p = res.permission
Baguette::Log.info "app #{s} resource #{r} user #{u}: #{p}"
end
end
def permission_set
args = Context.args.not_nil!
user, application, resource, permission = args[0..3]
perm = AuthD::User::PermissionLevel.parse(permission)
res = @authd.set_permission user.to_u32, application, resource, perm
case res
when Response::PermissionSet
s = res.service
r = res.resource
u = res.user
p = res.permission
Baguette::Log.info "app #{s} resource #{r} user #{u}: #{p}"
end
end
end
def main
# Authd connection.
authd = AuthD::Client.new
if login = Context.authd_login
pass = if p = Context.authd_pass
p
else
password = Actions.ask_password
raise "cannot get a password" unless password
password
end
response = authd.login? login, pass
case response
when Response::Login
uid = response.uid
token = response.token
Baguette::Log.info "Authenticated as #{login} #{uid}, token: #{token}"
else
raise "Cannot authenticate to authd with login #{login}: #{response}."
end
end
actions = Actions.new authd
# Now we did read the intent, we should proceed doing what was asked.
begin
actions.the_call[Context.command].call
rescue e
Baguette::Log.info "The command is not recognized (or implemented)."
Baguette::Log.info "Exception: #{e}."
pp! e
end
# authd disconnection
authd.close
rescue e
Baguette::Log.info "Exception: #{e}"
end
# Command line:
# tool [options] command [options-for-command]
main

View File

@ -1,649 +0,0 @@
require "uuid"
require "option_parser"
require "openssl"
require "colorize"
require "jwt"
require "ipc"
require "dodb"
require "baguette-crystal-base"
require "grok"
require "./authd.cr"
extend AuthD
class AuthD::Service
property registrations_allowed = false
property require_email = false
property mailer_activation_url : String? = nil
property mailer_field_from : String? = nil
property mailer_field_subject : String? = nil
property read_only_profile_keys = Array(String).new
@users_per_login : DODB::Index(User)
@users_per_uid : DODB::Index(User)
def initialize(@storage_root : String, @jwt_key : String)
@users = DODB::DataBase(User).new @storage_root
@users_per_uid = @users.new_index "uid", &.uid.to_s
@users_per_login = @users.new_index "login", &.login
@last_uid_file = "#{@storage_root}/last_used_uid"
end
def hash_password(password : String) : String
digest = OpenSSL::Digest.new "sha256"
digest << password
digest.hexdigest
end
def new_uid
begin
uid = File.read(@last_uid_file).to_i
rescue
uid = 999
end
uid += 1
File.write @last_uid_file, uid.to_s
uid
end
def handle_request(request : AuthD::Request?)
case request
when Request::GetToken
begin
user = @users_per_login.get request.login
rescue e : DODB::MissingEntry
return Response::Error.new "invalid credentials"
end
if user.password_hash != hash_password request.password
return Response::Error.new "invalid credentials"
end
if user.nil?
return Response::Error.new "invalid credentials"
end
user.date_last_connection = Time.local
token = user.to_token
# change the date of the last connection
@users_per_uid.update user.uid.to_s, user
Response::Token.new (token.to_s @jwt_key), user.uid
when Request::AddUser
# No verification of the users' informations when an admin adds it.
# No mail address verification.
if request.shared_key != @jwt_key
return Response::Error.new "invalid authentication key"
end
if @users_per_login.get? request.login
return Response::Error.new "login already used"
end
if @require_email && request.email.nil?
return Response::Error.new "email required"
end
password_hash = hash_password request.password
uid = new_uid
user = User.new uid, request.login, password_hash
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 != @jwt_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 ! @registrations_allowed
return Response::Error.new "registrations not allowed"
end
if @users_per_login.get? request.login
return Response::Error.new "login already used"
end
if @require_email && request.email.nil?
return Response::Error.new "email required"
end
mailer_activation_url = @mailer_activation_url
if mailer_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
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
mailer_field_subject = @mailer_field_subject.not_nil!
mailer_field_from = @mailer_field_from.not_nil!
mailer_activation_url = @mailer_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", mailer_field_subject,
"-f", mailer_field_from,
"-u", mailer_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 == @jwt_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
unless request.shared_key == @jwt_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?
return Response::PermissionCheck.new service, request.resource, user.uid, User::PermissionLevel::None
end
resource_permissions = service_permissions[request.resource]?
if resource_permissions.nil?
return Response::PermissionCheck.new service, request.resource, user.uid, User::PermissionLevel::None
end
return Response::PermissionCheck.new service, request.resource, user.uid, resource_permissions
when Request::SetPermission
unless request.shared_key == @jwt_key
return Response::Error.new "unauthorized"
end
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 (mailer_activation_url = @mailer_activation_url).nil?
mailer_field_from = @mailer_field_from.not_nil!
mailer_activation_url = @mailer_activation_url.not_nil!
# Once the user is created and stored, we try to contact him
unless Process.run("password-recovery-mailer", [
"-l", user.login,
"-e", user.contact.email.not_nil!,
"-t", "Password recovery email",
"-f", mailer_field_from,
"-u", mailer_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
@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 != @jwt_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
@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
else
Response::Error.new "unhandled request type"
end
end
def get_user_from_token(token : String)
token_payload = Token.from_s(@jwt_key, token)
@users_per_uid.get? token_payload.uid.to_s
end
def run
##
# Provides a JWT-based authentication scheme for service-specific users.
server = IPC::Server.new "auth"
server.base_timer = 30000 # 30 seconds
server.timer = 30000 # 30 seconds
server.loop do |event|
if event.is_a? IPC::Exception
Baguette::Log.error "IPC::Exception"
pp! event
next
end
case event
when IPC::Event::Timer
Baguette::Log.debug "Timer"
when IPC::Event::MessageReceived
begin
request = Request.from_ipc(event.message).not_nil!
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 " .. payload was: #{e.payload}"
response = Response::Error.new e.message
rescue e
Baguette::Log.error "#{e.message}"
response = Response::Error.new e.message
end
Baguette::Log.info ">> #{response.class.name.sub /^Response::/, ""}"
end
end
end
end
authd_storage = "storage"
authd_jwt_key = "nico-nico-nii"
authd_registrations = false
authd_require_email = false
activation_url : String? = nil
field_subject : String? = nil
field_from : String? = nil
read_only_profile_keys = Array(String).new
begin
OptionParser.parse do |parser|
parser.banner = "usage: authd [options]"
parser.on "-s directory", "--storage directory", "Directory in which to store users." do |directory|
authd_storage = directory
end
parser.on "-K file", "--key-file file", "JWT key file" do |file_name|
authd_jwt_key = File.read(file_name).chomp
end
parser.on "-R", "--allow-registrations" do
authd_registrations = true
end
parser.on "-E", "--require-email" do
authd_require_email = true
end
parser.on "-t subject", "--subject title", "Subject of the email." do |s|
field_subject = s
end
parser.on "-f from-email", "--from email", "'From:' field to use in activation email." do |f|
field_from = f
end
parser.on "-u", "--activation-url url", "Activation URL." do |opt|
activation_url = opt
end
parser.on "-x key", "--read-only-profile-key key", "Marks a user profile key as being read-only." do |key|
read_only_profile_keys.push key
end
parser.on "-v verbosity",
"--verbosity level",
"Verbosity level. From 0 to 3. Default: 1" do |v|
Baguette::Context.verbosity = v.to_i
end
parser.on "-h", "--help", "Show this help" do
puts parser
exit 0
end
end
AuthD::Service.new(authd_storage, authd_jwt_key).tap do |authd|
authd.registrations_allowed = authd_registrations
authd.require_email = authd_require_email
authd.mailer_activation_url = activation_url
authd.mailer_field_subject = field_subject
authd.mailer_field_from = field_from
authd.read_only_profile_keys = read_only_profile_keys
end.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

24
src/network.cr Normal file
View File

@ -0,0 +1,24 @@
require "ipc"
require "ipc/json"
require "./authd.cr"
require "./service.cr" # To load AuthD::Service definition.
class IPC::JSON
def handle(service : AuthD::Service, fd : Int32)
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/*"

108
src/requests/admin.cr Normal file
View File

@ -0,0 +1,108 @@
class AuthD::Request
IPC::JSON.message AddUser, 9 do
property login : String
property password : String
property admin : Bool = false
property email : String? = nil
property profile : Hash(String, JSON::Any)? = nil
def initialize(@login, @password, @admin, @email, @profile)
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user_full? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?
logged_user.assert_permission("authd", "*", User::PermissionLevel::Admin)
if authd.users_per_login.get? @login
return Response::ErrorAlreadyUsedLogin.new
end
# No verification of the user's informations when an admin adds it.
# No mail address verification.
if authd.configuration.require_email && @email.nil?
return Response::ErrorMailRequired.new
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.admin = @admin
@profile.try do |profile|
user.profile = profile
end
# We consider adding the user as a registration.
user.date_registration = Time.local
authd.users << user
authd.new_uid_commit uid
Response::UserAdded.new user.to_public
end
end
AuthD.requests << AddUser
IPC::JSON.message BootstrapFirstAdmin, 13 do
property login : String
property password : String
property email : String? = nil
property profile : Hash(String, JSON::Any)? = nil
def initialize(@login, @password, @email, @profile = nil)
end
def handle(authd : AuthD::Service, fd : Int32)
# Check if there already is a registered user.
if authd.users.to_a.size > 0
return Response::ErrorAlreadyUsersInDB.new
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.admin = true
@profile.try do |profile|
user.profile = profile
end
# We consider adding the user as a registration.
user.date_registration = Time.local
authd.users << user
authd.new_uid_commit uid
Response::UserAdded.new user.to_public
end
end
AuthD.requests << BootstrapFirstAdmin
IPC::JSON.message DecodeToken, 14 do
property token : String
def initialize(@token)
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user_full? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?
logged_user.assert_permission("authd", "*", User::PermissionLevel::Read)
token_payload = AuthD::Token.from_s authd.configuration.secret_key, token
user = authd.users_per_uid.get? token_payload.uid.to_s
if user
Response::User.new user.to_public
else
Response::ErrorUserNotFound.new
end
end
end
AuthD.requests << DecodeToken
end

34
src/requests/delete.cr Normal file
View File

@ -0,0 +1,34 @@
class AuthD::Request
IPC::JSON.message Delete, 8 do
# Deletion can be triggered by either an admin or the related user.
property user : UserID? = nil
def initialize(@user = nil)
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user_full? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?
user_to_delete = if u = @user
logged_user.assert_permission("authd", "*", User::PermissionLevel::Admin)
authd.user? u
else
logged_user
end
return Response::ErrorUserNotFound.new if user_to_delete.nil?
# User or admin is now verified: let's proceed with the user deletion.
authd.users_per_login.delete user_to_delete.login
# If the current user is deleted, unlog!
if logged_user.uid == user_to_delete.uid
authd.close fd
authd.logged_users.delete fd
end
Response::UserDeleted.new user_to_delete.uid
end
end
AuthD.requests << Delete
end

11
src/requests/keepalive.cr Normal file
View File

@ -0,0 +1,11 @@
class AuthD::Request
IPC::JSON.message KeepAlive, 250 do
def initialize()
end
def handle(authd : AuthD::Service, fd : Int32)
Response::KeepAlive.new
end
end
AuthD.requests << KeepAlive
end

73
src/requests/login.cr Normal file
View File

@ -0,0 +1,73 @@
require "sodium"
class AuthD::Request
def self.perform_login(authd : AuthD::Service, fd : Int32, user : AuthD::User)
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
# On successuful connection: store the authenticated user in a hash.
authd.logged_users[fd] = user.to_public
Response::Login.new (token.to_s authd.configuration.secret_key), user.uid
end
IPC::JSON.message Login, 0 do
property login : String
property password : String
def initialize(@login, @password)
end
def handle(authd : AuthD::Service, fd : Int32)
begin
user = authd.users_per_login.get @login
rescue e : DODB::MissingEntry
# This lack of proper error message is intentional.
# Let attackers try to authenticate themselves with a wrong login.
return Response::ErrorInvalidCredentials.new
end
# This line is basically just to please the Crystal's type system.
# No user means DODB::MissingEntry, so it's already covered.
return Response::ErrorInvalidCredentials.new if user.nil?
# In case the user hasn't validated his email address,
# authentication shouldn't be possible.
if user.contact.activation_key
return Response::ErrorInvalidCredentials.new
end
pwhash = Sodium::Password::Hash.new
hash = Base64.decode user.password_hash
begin
pwhash.verify hash, @password
rescue
return Response::ErrorInvalidCredentials.new
end
AuthD::Request.perform_login authd, fd, user.not_nil!
end
end
AuthD.requests << Login
IPC::JSON.message AuthByToken, 15 do
property token : String
def initialize(@token)
end
def handle(authd : AuthD::Service, fd : Int32)
token_payload = AuthD::Token.from_s authd.configuration.secret_key, token
user = authd.users_per_uid.get? token_payload.uid.to_s
return Response::ErrorUserNotFound.new if user.nil?
AuthD::Request.perform_login authd, fd, user
end
end
AuthD.requests << AuthByToken
end

43
src/requests/moduser.cr Normal file
View File

@ -0,0 +1,43 @@
class AuthD::Request
IPC::JSON.message ModUser, 6 do
property user : UserID? = nil
property admin : Bool? = nil
property password : String? = nil
property email : String? = nil
def initialize(@user, @admin, @password, @email)
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user_full? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?
user = if u = @user
logged_user.assert_permission("authd", "*", User::PermissionLevel::Edit)
authd.user? u
else
logged_user
end
return Response::ErrorUserNotFound.new if user.nil?
# Only an admin can uprank or downrank someone.
if admin = @admin
logged_user.assert_permission("authd", "*", User::PermissionLevel::Admin)
user.admin = admin
end
@password.try do |s|
user.password_hash = authd.hash_password s
end
@email.try do |email|
user.contact.email = email
end
authd.users_per_uid.update user.uid.to_s, user
Response::UserEdited.new user.uid
end
end
AuthD.requests << ModUser
end

92
src/requests/password.cr Normal file
View File

@ -0,0 +1,92 @@
class AuthD::Request
IPC::JSON.message AskPasswordRecovery, 3 do
property login : String? = nil
property email : String? = nil
def initialize(@login = nil, @email = nil)
end
def handle(authd : AuthD::Service, fd : Int32)
if @login.nil? && @email.nil?
return Response::ErrorUserNotFound.new
end
user = if l = @login
authd.user? l
elsif mail = @email
authd.users_per_email.get? Base64.encode(mail).chomp
else
nil
end
# This is a way for an attacker to know what are the valid logins.
# Not sure I care enough to fix this.
return Response::ErrorUserNotFound.new if user.nil?
# Create a new random key for password renewal.
user.password_renew_key = UUID.random.to_s
authd.users_per_uid.update user.uid.to_s, user
# TODO: this is debug information. Should be removed once tested.
# 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!,
user.password_renew_key.not_nil!
end
mailer_exe = authd.configuration.mailer_exe
template_name = authd.configuration.recovery_template
u_login = user.login
u_email = user.contact.email.not_nil!
u_token = user.password_renew_key.not_nil!
# Once the user is created and stored, we try to contact him.
unless Process.run(mailer_exe,
# PARAMETERS
[ "send", template_name, u_email ],
# ENV
{ "HOME" => "/", "LOGIN" => u_login, "TOKEN" => u_token },
true # clear environment
# input: Process::Redirect::Inherit,
# output: Process::Redirect::Inherit,
# error: Process::Redirect::Inherit
).success?
raise "cannot contact user #{u_login} address #{u_email}"
end
Response::PasswordRecoverySent.new
end
end
AuthD.requests << AskPasswordRecovery
IPC::JSON.message PasswordRecovery, 4 do
property user : UserID
property password_renew_key : String
property new_password : String
def initialize(@user, @password_renew_key, @new_password)
end
def handle(authd : AuthD::Service, fd : Int32)
user = authd.user? @user
# This is a way for an attacker to know what are the valid logins.
# Not sure I care enough to fix this.
return Response::ErrorUserNotFound.new if user.nil?
if user.password_renew_key == @password_renew_key
user.password_hash = authd.hash_password @new_password
else
return Response::ErrorInvalidRenewKey.new
end
user.password_renew_key = nil
authd.users_per_uid.update user.uid.to_s, user
Response::PasswordRecovered.new
end
end
AuthD.requests << PasswordRecovery
end

View File

@ -0,0 +1,76 @@
class AuthD::Request
IPC::JSON.message CheckPermission, 10 do
property user : UserID? = nil
property service : String
property resource : String
def initialize(@user, @service, @resource)
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user_full? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?
user = if u = @user
logged_user.assert_permission("authd", "*", User::PermissionLevel::Read)
authd.user? u
else
logged_user
end
return Response::ErrorUserNotFound.new if user.nil?
service_permissions = user.permissions[@service]?
resource_permissions = if service_permissions.nil?
User::PermissionLevel::None
elsif p = service_permissions[@resource]?
p
else
User::PermissionLevel::None
end
return Response::PermissionCheck.new @service, @resource, user.uid, resource_permissions
end
end
AuthD.requests << CheckPermission
IPC::JSON.message SetPermission, 11 do
property user : UserID
property service : String
property resource : String
property permission : ::AuthD::User::PermissionLevel
def initialize(@user, @service, @resource, @permission)
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user_full? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?
logged_user.assert_permission("authd", "*", User::PermissionLevel::Admin)
user = if u = @user
authd.user? u
else
logged_user
end
return Response::ErrorUserNotFound.new if user.nil?
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

50
src/requests/profile.cr Normal file
View File

@ -0,0 +1,50 @@
class AuthD::Request
# Reset elements for which keys are present in `new_profile_entries`.
IPC::JSON.message EditProfileEntries, 7 do
property user : UserID? = nil
property new_profile_entries : Hash(String, JSON::Any)
def initialize(@new_profile_entries, @user = nil)
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user_full? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?
user = if u = @user
logged_user.assert_permission("authd", "*", User::PermissionLevel::Edit)
authd.user? u
else
logged_user
end
return Response::ErrorUserNotFound.new if user.nil?
new_profile_entries = user.profile || Hash(String, JSON::Any).new
invalid_profile_keys = Array(String).new
unless logged_user.admin
authd.configuration.read_only_profile_keys.each do |key|
if @new_profile_entries.has_key? key
invalid_profile_keys << key
end
end
end
if invalid_profile_keys.size > 0
return Response::ErrorReadOnlyProfileKeys.new invalid_profile_keys
end
@new_profile_entries.each do |key, value|
new_profile_entries[key] = value
end
user.profile = new_profile_entries
authd.users_per_uid.update user.uid.to_s, user
Response::User.new user.to_public
end
end
AuthD.requests << EditProfileEntries
end

87
src/requests/register.cr Normal file
View File

@ -0,0 +1,87 @@
class AuthD::Request
IPC::JSON.message Register, 1 do
property login : String
property password : String
property email : String? = nil
property profile : Hash(String, JSON::Any)? = nil
def initialize(@login, @password, @email, @profile)
end
def handle(authd : AuthD::Service, fd : Int32)
unless authd.configuration.registrations
return Response::ErrorRegistrationsClosed.new
end
if authd.users_per_login.get? @login
return Response::ErrorAlreadyUsedLogin.new
end
acceptable_login_regex = "[a-zA-Z][-_ a-zA-Z0-9']+"
pattern = Regex.new acceptable_login_regex, Regex::Options::IGNORE_CASE
return Response::ErrorInvalidLoginFormat.new unless pattern =~ @login
if authd.configuration.require_email && @email.nil?
return Response::ErrorMailRequired.new
end
if ! @email.nil?
# Test on the email address format.
grok = Grok.new [ "%{EMAILADDRESS:email}" ]
result = grok.parse @email.not_nil!
email = result["email"]?
return Response::ErrorInvalidEmailFormat.new if email.nil?
end
# In this case we should not accept its registration.
return Response::ErrorPasswordTooShort.new if @password.size < 20
return Response::ErrorPasswordTooLong.new if @password.size > 100
uid = authd.new_uid
password = authd.hash_password @password
user = User.new uid, @login, password
user.contact.email = @email unless @email.nil?
user.contact.new_activation_key
@profile.try do |profile|
user.profile = profile
end
user.date_registration = Time.local
begin
mailer_exe = authd.configuration.mailer_exe
template_name = authd.configuration.activation_template
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(mailer_exe,
# PARAMETERS
[ "send", template_name, u_email ],
# ENV
{ "HOME" => "/", "LOGIN" => u_login, "TOKEN" => u_activation_key },
true # clear environment
# input: Process::Redirect::Inherit,
# output: Process::Redirect::Inherit,
# error: Process::Redirect::Inherit
).success?
raise "cannot contact user #{u_login} address #{u_email}"
end
rescue e
Baguette::Log.error "mailer: #{e}"
return Response::ErrorCannotContactUser.new
end
# add the user only if we were able to send the confirmation mail
authd.users << user
authd.new_uid_commit uid
Response::UserAdded.new user.to_public
end
end
AuthD.requests << Register
end

52
src/requests/search.cr Normal file
View File

@ -0,0 +1,52 @@
class AuthD::Request
IPC::JSON.message SearchUser, 12 do
property regex : String? = nil
# Since the list could be long, here is a way to get it at a reasonable pace.
property offset : Int32 = 0
# By default, authd will send 10 users at a time.
def initialize(@regex = nil, @offset = 0)
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user_full? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?
logged_user.assert_permission("authd", "*", User::PermissionLevel::Read)
users = authd.users.to_a
matching_users = Array(AuthD::User::Public).new
# FIXME: could be optimized.
result = if regex = @regex
pattern = Regex.new regex, Regex::Options::IGNORE_CASE
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 || u.contact.email.try do |email|
pattern =~ email
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
matching_users[offset..offset+10]
else
users[offset..offset+10].each do |u|
matching_users << u.to_public
end
matching_users
end
Response::MatchingUsers.new result
end
end
AuthD.requests << SearchUser
end

52
src/requests/users.cr Normal file
View File

@ -0,0 +1,52 @@
class AuthD::Request
IPC::JSON.message ValidateUser, 2 do
property user : UserID
property activation_key : String
def initialize(@user, @activation_key)
end
def handle(authd : AuthD::Service, fd : Int32)
user = authd.user? @user
# This is a way for an attacker to know what are the valid logins.
# Not sure I care enough to fix this.
return Response::ErrorUserNotFound.new if user.nil?
if user.contact.activation_key.nil?
return Response::ErrorUserAlreadyValidated.new
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::ErrorInvalidActivationKey.new
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, 5 do
property user : UserID
def initialize(@user)
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?
user = authd.user? @user
# This is a way for an attacker to know what are the valid logins.
# Not sure I care enough to fix this.
return Response::ErrorUserNotFound.new if user.nil?
Response::User.new user.to_public
end
end
AuthD.requests << GetUser
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 : UInt32
property email : String? = nil
def initialize(@user, @email)
end
end
AuthD.responses << Contacts
end

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

@ -0,0 +1,105 @@
class AuthD::Response
IPC::JSON.message Error, 0 do
property reason : String? = nil
def initialize(@reason)
end
end
AuthD.responses << Error
IPC::JSON.message ErrorMustBeAuthenticated, 20 do
def initialize()
end
end
AuthD.responses << ErrorMustBeAuthenticated
IPC::JSON.message ErrorAlreadyUsedLogin, 21 do
def initialize()
end
end
AuthD.responses << ErrorAlreadyUsedLogin
IPC::JSON.message ErrorMailRequired, 22 do
def initialize()
end
end
AuthD.responses << ErrorMailRequired
IPC::JSON.message ErrorUserNotFound, 23 do
def initialize()
end
end
AuthD.responses << ErrorUserNotFound
IPC::JSON.message ErrorPasswordTooShort, 24 do
def initialize()
end
end
AuthD.responses << ErrorPasswordTooShort
IPC::JSON.message ErrorInvalidCredentials, 25 do
def initialize()
end
end
AuthD.responses << ErrorInvalidCredentials
IPC::JSON.message ErrorRegistrationsClosed, 26 do
def initialize()
end
end
AuthD.responses << ErrorRegistrationsClosed
IPC::JSON.message ErrorInvalidLoginFormat, 27 do
def initialize()
end
end
AuthD.responses << ErrorInvalidLoginFormat
IPC::JSON.message ErrorInvalidEmailFormat, 28 do
def initialize()
end
end
AuthD.responses << ErrorInvalidEmailFormat
IPC::JSON.message ErrorAlreadyUsersInDB, 29 do
def initialize()
end
end
AuthD.responses << ErrorAlreadyUsersInDB
IPC::JSON.message ErrorReadOnlyProfileKeys, 30 do
property read_only_keys : Array(String)
def initialize(@read_only_keys)
end
end
AuthD.responses << ErrorReadOnlyProfileKeys
IPC::JSON.message ErrorInvalidActivationKey, 31 do
def initialize()
end
end
AuthD.responses << ErrorInvalidActivationKey
IPC::JSON.message ErrorUserAlreadyValidated, 32 do
def initialize()
end
end
AuthD.responses << ErrorUserAlreadyValidated
IPC::JSON.message ErrorCannotContactUser, 33 do
def initialize()
end
end
AuthD.responses << ErrorCannotContactUser
IPC::JSON.message ErrorInvalidRenewKey, 34 do
def initialize()
end
end
AuthD.responses << ErrorInvalidRenewKey
IPC::JSON.message ErrorPasswordTooLong, 35 do
def initialize()
end
end
AuthD.responses << ErrorPasswordTooLong
end

View File

@ -0,0 +1,7 @@
class AuthD::Response
IPC::JSON.message KeepAlive, 250 do
def initialize()
end
end
AuthD.responses << KeepAlive
end

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

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

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

@ -0,0 +1,13 @@
class AuthD::Response
IPC::JSON.message PasswordRecoverySent, 9 do
def initialize
end
end
AuthD.responses << PasswordRecoverySent
IPC::JSON.message PasswordRecovered, 10 do
def initialize
end
end
AuthD.responses << PasswordRecovered
end

View File

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

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

@ -0,0 +1,50 @@
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 : UInt32
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
IPC::JSON.message UserDeleted, 12 do
property uid : UInt32
def initialize(@uid)
end
end
AuthD.responses << UserDeleted
end

76
src/server.cr Normal file
View File

@ -0,0 +1,76 @@
require "./service.cr"
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.secret_key_file
configuration.secret_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.secret_key = File.read(file_name).chomp
end
parser.on "-R", "--allow-registrations", "Allow user registration." do
configuration.registrations = true
end
parser.on "-E", "--require-email", "Require an email." do
configuration.require_email = true
end
parser.on "-t activation-template-name", "--activation-template name", "Email activation template." do |opt|
configuration.activation_template = opt
end
parser.on "-r recovery-template-name", "--recovery-template name", "Email recovery template." do |opt|
configuration.recovery_template = opt
end
parser.on "-m mailer-exe", "--mailer mailer-exe", "Application to send registration emails." do |opt|
configuration.mailer_exe = 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

215
src/service.cr Normal file
View File

@ -0,0 +1,215 @@
require "./authd.cr"
require "sodium"
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_template : String = "email-activation"
property recovery_template : String = "email-recovery"
property mailer_exe : String = "mailer"
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)
property users_per_email : DODB::Index(User)
property logged_users : Hash(Int32, AuthD::User::Public)
# #{@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
@users_per_email = @users.new_index "email" do |user|
if mail = user.contact.email
Base64.encode(mail).chomp
else
""
end
end
@last_uid_file = "#{@configuration.storage}/last_used_uid"
@logged_users = Hash(Int32, AuthD::User::Public).new
if @configuration.recreate_indexes
Baguette::Log.info "Recreate indexes"
@users.reindex_everything!
end
self.timer @configuration.ipc_timer
self.service_init "auth"
end
def obsolete_hash_password(password : String) : String
digest = OpenSSL::Digest.new "sha256"
digest << password
digest.hexfinal
end
def hash_password(password : String) : String
pwhash = Sodium::Password::Hash.new
hash = pwhash.create password
pwhash.verify hash, password
Base64.strict_encode hash
end
# new_uid reads the last given UID and returns it incremented.
# Splitting the retrieval and record of new user ids allows to
# only increment when an user fully registers, thus avoiding a
# Denial of Service attack.
#
# WARNING: to record this new UID, new_uid_commit must be called.
# WARNING: new_uid isn't thread safe.
def new_uid : UInt32
uid : UInt32 = begin
File.read(@last_uid_file).to_u32
rescue
999.to_u32
end
uid += 1
end
# new_uid_commit records the new UID.
# WARNING: new_uid_commit isn't thread safe.
def new_uid_commit(uid : Int)
File.write @last_uid_file, uid.to_s
end
def get_logged_user?(fd : Int32)
@logged_users[fd]?
end
# Instead of just getting the public view of a logged user,
# get the actual User instance.
def get_logged_user_full?(fd : Int32)
if u = @logged_users[fd]?
user? u.uid
end
end
def user?(uid_or_login : UserID)
if uid_or_login.is_a? UInt32
@users_per_uid.get? uid_or_login.to_s
else
@users_per_login.get? uid_or_login
end
end
def handle_request(event : IPC::Event)
request_start = Time.utc
array = event.message.not_nil!
slice = Slice.new array.to_unsafe, array.size
message = IPCMessage::TypedMessage.deserialize slice
request = AuthD.requests.parse_ipc_json message.not_nil!
if request.nil?
raise "unknown request type"
end
request_name = request.class.name.sub /^AuthD::Request::/, ""
response = begin
request.handle self, event.fd
rescue e : UserNotFound
Baguette::Log.error "(fd #{ "%4d" % event.fd}) #{request_name} user not found"
AuthD::Response::Error.new "authorization error"
rescue e : AuthenticationInfoLacking
Baguette::Log.error "(fd #{ "%4d" % event.fd}) #{request_name} lacking authentication info"
AuthD::Response::Error.new "authorization error"
rescue e : AdminAuthorizationException
Baguette::Log.error "(fd #{ "%4d" % event.fd}) #{request_name} admin authentication failed"
AuthD::Response::Error.new "authorization error"
rescue e
Baguette::Log.error "(fd #{ "%4d" % event.fd}) #{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 "fd #{ "%4d" % event.fd} (#{duration}) #{request_name} >> #{response_name} (#{response.reason})"
else
if request_name != "KeepAlive" || @configuration.print_keepalive
Baguette::Log.debug "fd #{ "%4d" % event.fd} (#{duration}) #{request_name} >> #{response_name}"
end
end
end
def get_user_from_token(token : String)
token_payload = Token.from_s(@configuration.secret_key, token)
@users_per_uid.get? token_payload.uid.to_s
end
def run
Baguette::Log.title "Starting authd"
Baguette::Log.info "(mailer) Email activation template: #{@configuration.activation_template}"
Baguette::Log.info "(mailer) Email recovery template: #{@configuration.recovery_template}"
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
@logged_users.delete event.fd
else
Baguette::Log.error "Not implemented behavior for event: #{event}"
if event.responds_to?(:fd)
fd = event.fd
Baguette::Log.warning "closing #{fd}"
close fd
@logged_users.delete fd
end
end
end
end
end

View File

@ -1,95 +0,0 @@
require "option_parser"
require "../src/authd.cr"
key_file : String? = nil
cli_login : String? = nil
profile_file : String? = nil
register = false
email = nil
phone = nil
password : String? = nil
OptionParser.parse do |parser|
parser.unknown_args do |args|
if args.size != 3
puts "usage: #{PROGRAM_NAME} <login> <email> <phone> [options]"
puts parser
exit 1
end
cli_login, email, phone = args[0..2]
end
parser.on "-p file", "--profile file", "Read the user profile from a file." do |file|
profile_file = file
end
parser.on "-X user-password", "--user-password pass", "Read the new user password." do |pass|
password = pass
end
parser.on "-K file", "--key-file file", "Read the authd shared key from a file." do |file|
key_file = file
end
parser.on "-R", "--register", "Use a registration request instead of a add-user one." do
register = true
end
parser.on "-h", "--help", "Prints this help message." do
puts "usage: #{PROGRAM_NAME} <login> <email> <phone> [options]"
puts parser
exit 0
end
end
if cli_login.nil?
STDERR.puts "no login provided"
exit 1
end
login = cli_login.not_nil! # not_nil!? O RLY?
profile = profile_file.try do |file|
begin
JSON.parse(File.read file).as_h
rescue e
STDERR.puts e.message
exit 1
end
end
if password.nil?
STDOUT << "password: "
STDOUT << `stty -echo`
STDOUT.flush
password = STDIN.gets.try &.chomp
STDOUT << '\n'
STDOUT << `stty echo`
end
exit 1 unless password
authd = AuthD::Client.new
email = nil if email == ""
phone = nil if phone == ""
begin
if register
pp! authd.register login, password.not_nil!, email, phone, profile: profile
else
key_file.try do |file| # FIXME: fail if missing?
authd.key = File.read(file).chomp
end
pp! authd.add_user login, password.not_nil!, email, phone, profile: profile
end
rescue e : AuthD::Exception
puts "error: #{e.message}"
end
authd.close

View File

@ -1,70 +0,0 @@
require "option_parser"
require "../src/authd.cr"
key_file : String? = nil
login : String? = nil
service : String? = nil
resource : String? = nil
register = false
level = AuthD::User::PermissionLevel::Read
OptionParser.parse do |parser|
parser.unknown_args do |args|
if args.size != 3
puts "usage: #{PROGRAM_NAME} <user> <service> <resource> [options]"
puts parser
exit 1
end
login, service, resource = args
end
parser.on "-K file", "--key-file file", "Read the authd shared key from a file." do |file|
key_file = file
end
parser.on "-L level", "--level level", "Sets the permission level to give the user." do |l|
begin
level = AuthD::User::PermissionLevel.parse l
rescue
STDERR.puts "Could not parse permission level '#{l}'"
exit 1
end
end
parser.on "-R", "--register", "Use a registration request instead of a add-user one." do
register = true
end
parser.on "-h", "--help", "Prints this help message." do
puts "usage: #{PROGRAM_NAME} <user> <service> <resource> [options]"
puts parser
exit 0
end
end
if key_file.nil?
STDERR.puts "you need to provide the shared key"
exit 1
end
authd = AuthD::Client.new
authd.key = File.read(key_file.not_nil!).chomp
begin
user = authd.get_user? login.not_nil!
if user.nil?
raise AuthD::Exception.new "#{login}: no such user"
end
# FIXME: make a “disallow” variant.
authd.set_permission user.uid, service.not_nil!, resource.not_nil!, level
rescue e : AuthD::Exception
puts "error: #{e.message}"
end
authd.close

View File

@ -1,43 +0,0 @@
require "option_parser"
require "../src/authd.cr"
key_file : String? = nil
cli_login : String? = nil
OptionParser.parse do |parser|
parser.unknown_args do |args|
if args.size != 1
puts "usage: #{PROGRAM_NAME} <login> [options]"
puts parser
exit 1
end
cli_login = args[0]
end
parser.on "-K file", "--key-file file", "Read the authd shared key from a file." do |file|
key_file = file
end
parser.on "-h", "--help", "Prints this help message." do
puts "usage: #{PROGRAM_NAME} <login> [options]"
puts parser
exit 0
end
end
begin
authd = AuthD::Client.new
authd.key = File.read(key_file.not_nil!).chomp
login = cli_login.not_nil!
# AskPasswordRecovery => PasswordRecoverySent
# PasswordRecovery =>
pp! authd.ask_password_recovery login
rescue e
puts "Error: #{e}"
exit 1
end

View File

@ -1,40 +0,0 @@
require "option_parser"
require "../src/authd.cr"
key_file : String? = nil
cli_login : String? = nil
OptionParser.parse do |parser|
parser.unknown_args do |args|
if args.size != 1
puts "usage: #{PROGRAM_NAME} <login> [options]"
puts parser
exit 1
end
cli_login = args[0]
end
parser.on "-K file", "--key-file file", "Read the authd shared key from a file." do |file|
key_file = file
end
parser.on "-h", "--help", "Prints this help message." do
puts "usage: #{PROGRAM_NAME} <login> <email> <phone> [options]"
puts parser
exit 0
end
end
begin
authd = AuthD::Client.new
authd.key = File.read(key_file.not_nil!).chomp
login = cli_login.not_nil!
pp! authd.get_user? login
rescue e
puts "Error: #{e}"
exit 1
end

View File

@ -1,88 +0,0 @@
require "option_parser"
require "../src/authd.cr"
key_file : String? = nil
cli_login : String? = nil
profile_file : String? = nil
register = false
email = nil
phone = nil
OptionParser.parse do |parser|
parser.unknown_args do |args|
if args.size != 3
puts "usage: #{PROGRAM_NAME} <login> <email> <phone> [options]"
puts parser
exit 1
end
cli_login, email, phone = args[0..2]
end
parser.on "-p file", "--profile file", "Read the user profile from a file." do |file|
profile_file = file
end
parser.on "-K file", "--key-file file", "Read the authd shared key from a file." do |file|
key_file = file
end
parser.on "-R", "--register", "Use a registration request instead of a add-user one." do
register = true
end
parser.on "-h", "--help", "Prints this help message." do
puts "usage: #{PROGRAM_NAME} <login> <email> <phone> [options]"
puts parser
exit 0
end
end
if cli_login.nil?
STDERR.puts "no login provided"
exit 1
end
login = cli_login.not_nil! # not_nil!? O RLY?
profile = profile_file.try do |file|
begin
JSON.parse File.read file
rescue e
STDERR.puts e.message
exit 1
end
end
STDOUT << "password: "
STDOUT << `stty -echo`
STDOUT.flush
password = STDIN.gets.try &.chomp
STDOUT << '\n'
STDOUT << `stty echo`
exit 1 unless password
authd = AuthD::Client.new
email = nil if email == ""
phone = nil if phone == ""
begin
if register
pp! authd.register login, password, email, phone, profile: profile
else
key_file.try do |file| # FIXME: fail if missing?
authd.key = File.read(file).chomp
end
pp! authd.add_user login, password, email, phone, profile: profile
end
rescue e : AuthD::Exception
puts "error: #{e.message}"
end
authd.close

View File

@ -1,67 +0,0 @@
require "option_parser"
require "../src/authd.cr"
key_file : String? = nil
cli_login : String? = nil
cli_service : String? = nil
cli_resource : String? = nil
cli_permlvl : String? = nil
OptionParser.parse do |parser|
parser.unknown_args do |args|
if 3 < args.size > 4
puts "usage: #{PROGRAM_NAME} <uid> <service> <resource> <permlevel> [options]"
puts parser
exit 1
end
cli_login = args[0]
cli_service = args[1]
cli_resource = args[2] if args.size > 2
cli_permlvl = args[3] if args.size > 3
end
parser.on "-K file", "--key-file file", "Read the authd shared key from a file." do |file|
key_file = file
end
parser.on "-h", "--help", "Prints this help message." do
puts "usage: #{PROGRAM_NAME} <uid> <service> <resource> [permission] [options]"
puts "example: #{PROGRAM_NAME} 1002 my-application chat read"
puts
puts "permission list: none read edit admin"
puts parser
exit 0
end
end
if cli_login.nil?
STDERR.puts "no login provided"
exit 1
end
login = cli_login.not_nil!.to_i # not_nil!? O RLY?
service = cli_service.not_nil! # not_nil!
resource = cli_resource.not_nil! # not_nil!
authd = AuthD::Client.new
begin
key_file.try do |file| # FIXME: fail if missing?
authd.key = File.read(file).chomp
end
if cli_permlvl.nil?
pp! authd.check_permission login, service, resource
else
permlvl = cli_permlvl.not_nil!
perm = AuthD::User::PermissionLevel.parse(permlvl)
pp! authd.set_permission login, service, resource, perm
end
rescue e : AuthD::Exception
puts "error: #{e.message}"
end
authd.close

View File

@ -1,38 +0,0 @@
require "option_parser"
require "../src/authd.cr"
# key_file : String? = nil
login : String? = nil
activation_key : String? = nil
OptionParser.parse do |parser|
parser.unknown_args do |args|
if args.size != 1
puts "usage: #{PROGRAM_NAME} login-to-search [options]"
exit 1
end
login = args[0]
end
#parser.on "-K file", "--key-file file", "Read the authd shared key from a file." do |file|
# key_file = file
#end
parser.on "-h", "--help", "Prints this help message." do
puts "usage: #{PROGRAM_NAME} login-to-search [options]"
puts parser
exit 0
end
end
begin
authd = AuthD::Client.new
# authd.key = File.read(key_file.not_nil!).chomp
pp! r = authd.search_user login.not_nil!
rescue e
puts "Error: #{e}"
exit 1
end

View File

@ -1,38 +0,0 @@
require "option_parser"
require "../src/authd.cr"
key_file : String? = nil
login : String? = nil
activation_key : String? = nil
OptionParser.parse do |parser|
parser.unknown_args do |args|
if args.size != 2
puts "usage: #{PROGRAM_NAME} login activation_key [options]"
exit 1
end
login, activation_key = args[0..1]
end
parser.on "-K file", "--key-file file", "Read the authd shared key from a file." do |file|
key_file = file
end
parser.on "-h", "--help", "Prints this help message." do
puts "usage: #{PROGRAM_NAME} <login> <email> <phone> [options]"
puts parser
exit 0
end
end
begin
authd = AuthD::Client.new
authd.key = File.read(key_file.not_nil!).chomp
pp! r = authd.validate_user login.not_nil!, activation_key.not_nil!
rescue e
puts "Error: #{e}"
exit 1
end