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 is a token-based authentication micro-service.
> TODO: explain basic concepts behind `authd`.
## Build
`authd` is written in Crystal and uses `build.zsh` as Makefile generator, as
well as shards to fetch dependencies.
Youll need the following tools to build authd:
`authd` is written in Crystal.
Youll need the following tools:
- crystal
- shards
- build.zsh
- make
To build authd, run the following commands:
```
shards install
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
```
$ 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
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.
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
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
@ -75,22 +46,34 @@ $ authd -K ./key-file
authds protocol is still subject to change.
> TODO: document messages.
### 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.
```crystal
require "authd"
# Authorization rules
authd = AuthD::Client.new
authd.key = File.read("./some-file").chomp
Logged users can:
- 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
@ -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.
# 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
version: 0.2.0
authors:
- Karchnu <karchnu@karchnu.fr>
- Luka Vandervelden <lukc@upyum.com>
version: 0.1.0
description: |
JWT-based authentication daemon.
JWT-based authentication daemon.
authors:
- Philippe Pittoli <karchnu@karchnu.fr>
targets:
authd:
main: src/main.cr
auth-user-perms:
main: utils/authd-user-perms.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
main: src/server.cr
authc:
main: src/client.cr
crystal: 0.35.1
crystal: 1.7.1
dependencies:
grok:
github: spinscale/grok.cr
passwd:
git: https://git.baguette.netlib.re/Baguette/passwd.cr
branch: master
ipc:
git: https://git.baguette.netlib.re/Baguette/ipc.cr
branch: master
jwt:
github: crystal-community/jwt
branch: master
baguette-crystal-base:
git: https://git.baguette.netlib.re/Baguette/baguette-crystal-base
branch: master
dodb:
git: https://git.baguette.netlib.re/Baguette/dodb.cr
branch: master
sodium:
branch: master
github: didactic-drunk/sodium.cr
grok:
github: spinscale/grok.cr
passwd:
git: https://git.baguette.netlib.re/Baguette/passwd.cr
branch: master
jwt:
github: crystal-community/jwt
branch: master
baguette-crystal-base:
git: https://git.baguette.netlib.re/Baguette/baguette-crystal-base
branch: master
dodb:
git: https://git.baguette.netlib.re/Baguette/dodb.cr
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 "grok"
require "dodb"
require "ipc"
require "./user.cr"
require "baguette-crystal-base"
class AuthD::Exception < Exception
end
# In any message, a user can be referred by its UInt32 uid or its login.
alias UserID = UInt32 | String
class AuthD::MalformedRequest < Exception
getter ipc_type : Int32
getter payload : String
# Allows get configuration from a provided file.
# See Baguette::Configuration::Base.get
class Baguette::Configuration
class Auth < IPC
include YAML::Serializable
def initialize(@ipc_type, @payload)
@message = "malformed payload"
end
end
class AuthD::Response
include JSON::Serializable
property id : JSON::Any?
annotation MessageType
end
class_getter type = -1
def type
@@type
end
macro inherited
def self.type
::AuthD::Response::Type::{{ @type.name.split("::").last.id }}
end
end
macro initialize(*properties)
def initialize(
{% for value in properties %}
@{{value.id}}{% if value != properties.last %},{% end %}
{% end %}
)
end
def type
Type::{{ @type.name.split("::").last.id }}
end
end
class Error < Response
property reason : String?
initialize :reason
end
class Token < Response
property uid : Int32
property token : String
initialize :token, :uid
end
class User < Response
property user : ::AuthD::User::Public
initialize :user
end
class UserAdded < Response
property user : ::AuthD::User::Public
initialize :user
end
class UserEdited < Response
property uid : Int32
initialize :uid
end
class UserValidated < Response
property user : ::AuthD::User::Public
initialize :user
end
class UsersList < Response
property users : Array(::AuthD::User::Public)
initialize :users
end
class PermissionCheck < Response
property user : Int32
property service : String
property resource : String
property permission : ::AuthD::User::PermissionLevel
initialize :service, :resource, :user, :permission
end
class PermissionSet < Response
property user : Int32
property service : String
property resource : String
property permission : ::AuthD::User::PermissionLevel
initialize :user, :service, :resource, :permission
end
class PasswordRecoverySent < Response
property user : ::AuthD::User::Public
initialize :user
end
class PasswordRecovered < Response
property user : ::AuthD::User::Public
initialize :user
end
class MatchingUsers < Response
property users : Array(::AuthD::User::Public)
initialize :users
end
# 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
property login : String? = nil
property pass : String? = nil
property secret_key : String = "nico-nico-nii" # Default authd key, as per the specs. :eyes:
property secret_key_file : String? = nil
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
class IPC::Context
def send(fd, response : AuthD::Response)
send fd, response.type.to_u8, response.to_json
end
end
# Token and user classes.
require "./authd/token.cr"
require "./authd/user.cr"
class IPC::Client
def send(request : AuthD::Request)
unless (fd = @server_fd).nil?
send_now fd, request.type.to_u8, request.to_json
else
raise "Client not connected to the server"
end
end
end
# Requests and responses.
require "./authd/exceptions"
# 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
property login : String
property uid : Int32
property uid : UInt32
def initialize(@login, @uid)
end
@ -23,7 +23,7 @@ class AuthD::Token
def self.from_s(key, str)
payload, meta = JWT.decode str, key, JWT::Algorithm::HS256
self.new payload["login"].as_s, payload["uid"].as_i
self.new payload["login"].as_s, payload["uid"].as_i64.to_u32
end
end

View File

@ -1,9 +1,6 @@
require "json"
require "uuid"
require "./token.cr"
class AuthD::User
include JSON::Serializable
@ -22,18 +19,21 @@ class AuthD::User
include JSON::Serializable
# the activation key is removed once the user is validated
property activation_key : String?
property activation_key : String? = nil
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
end
end
# Public.
property login : String
property uid : Int32
property uid : UInt32
property admin : Bool = false
property profile : Hash(String, JSON::Any)?
# Private.
@ -50,7 +50,7 @@ class AuthD::User
Token.new @login, @uid
end
def initialize(@uid, @login, @password_hash)
def initialize(@uid, @login, @password_hash, @admin = false)
@contact = Contact.new
@permissions = Hash(String, Hash(String, PermissionLevel)).new
@configuration = Hash(String, Hash(String, JSON::Any)).new
@ -60,17 +60,35 @@ class AuthD::User
include JSON::Serializable
property login : String
property uid : Int32
property uid : UInt32
property admin : Bool = false
property profile : Hash(String, JSON::Any)?
property date_registration : Time?
def initialize(@uid, @login, @profile, @date_registration)
def initialize(@uid, @login, @admin, @profile, @date_registration)
end
end
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

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