Compare commits
8 Commits
Author | SHA1 | Date |
---|---|---|
Karchnu | 4c1b4c43c2 | |
Karchnu | 194abdd1f9 | |
Karchnu | baa86bf667 | |
Karchnu | 90ccd50c80 | |
Karchnu | c3d5aef951 | |
Karchnu | e88c19c892 | |
Karchnu | 62cef72fb2 | |
Karchnu | 88f87ef6f2 |
83
README.md
83
README.md
|
@ -1,32 +1,43 @@
|
||||||
|
|
||||||
# authd
|
# authd
|
||||||
|
|
||||||
authd is a token-based authentication micro-service.
|
authd is a token-based authentication micro-service.
|
||||||
|
|
||||||
> TODO: explain basic concepts behind `authd`.
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
`authd` is written in Crystal.
|
`authd` is written in Crystal and uses `build.zsh` as Makefile generator, as
|
||||||
You’ll need the following tools:
|
well as shards to fetch dependencies.
|
||||||
|
|
||||||
|
You’ll need the following tools to build authd:
|
||||||
|
|
||||||
- crystal
|
- crystal
|
||||||
- shards
|
- shards
|
||||||
|
- build.zsh
|
||||||
- make
|
- make
|
||||||
|
|
||||||
To build authd, run the following commands:
|
To build authd, run the following commands:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
shards install
|
||||||
make
|
make
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note that if you clone authd from its repository, its `Makefile` may be missing.
|
||||||
|
In such situations, run `build.zsh -c` to generate it, after which `make` should run fine.
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
```
|
```
|
||||||
$ authd --help
|
$ authd --help
|
||||||
|
usage: authd [options]
|
||||||
|
-s directory, --storage directory
|
||||||
|
Directory in which to store users.
|
||||||
|
-K file, --key-file file JWT key file
|
||||||
|
-R --allow-registrations
|
||||||
|
-h, --help Show this help
|
||||||
|
$
|
||||||
```
|
```
|
||||||
|
|
||||||
> TODO: documentation on how to deploy, including with the mail server and tools.
|
|
||||||
|
|
||||||
### Users storage
|
### Users storage
|
||||||
|
|
||||||
The storage directory will default to `./storage`.
|
The storage directory will default to `./storage`.
|
||||||
|
@ -34,11 +45,29 @@ The storage directory will default to `./storage`.
|
||||||
No SQL database, database management system or other kind of setup is required to run authd and store users.
|
No SQL database, database management system or other kind of setup is required to run authd and store users.
|
||||||
|
|
||||||
To migrate an instance of authd, a simple copy of the storage directory will be enough.
|
To migrate an instance of authd, a simple copy of the storage directory will be enough.
|
||||||
|
Make sure your copy preserves symlinks, as those are extensively used.
|
||||||
|
|
||||||
### Administrating users
|
### Administrating users
|
||||||
|
|
||||||
> TODO: document how to manage users through `authc`.
|
The `authd-user-add` and `authd-user-allow` are tools to add users to authd’s database and to edit their permissions.
|
||||||
|
|
||||||
|
The permission level `none` can be used in `authd-user-allow` to remove a permission.
|
||||||
|
|
||||||
|
### Key file
|
||||||
|
|
||||||
|
authd will provide users with cryptographically signed tokens.
|
||||||
|
To sign and check those tokens, a shared key is required between authd and services using authd.
|
||||||
|
|
||||||
|
authd reads that key from a file to prevent it being visible on the command line when running authd.
|
||||||
|
|
||||||
|
Any content is acceptable as a key file.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ echo "I am a key." > key-file
|
||||||
|
$ authd -K ./key-file
|
||||||
|
```
|
||||||
|
|
||||||
## APIs
|
## APIs
|
||||||
|
|
||||||
|
@ -46,34 +75,22 @@ To migrate an instance of authd, a simple copy of the storage directory will be
|
||||||
|
|
||||||
authd’s protocol is still subject to change.
|
authd’s protocol is still subject to change.
|
||||||
|
|
||||||
> TODO: document messages.
|
|
||||||
|
|
||||||
### Libraries
|
### Libraries
|
||||||
|
|
||||||
> TODO: document basic functions in the `AuthD::Client` class to exchange messages with `authd`.
|
|
||||||
|
|
||||||
A `AuthD::Client` Crystal class is available to build synchronous clients in Crystal.
|
A `AuthD::Client` Crystal class is available to build synchronous clients in Crystal.
|
||||||
|
|
||||||
# Authorization rules
|
```crystal
|
||||||
|
require "authd"
|
||||||
|
|
||||||
Logged users can:
|
authd = AuthD::Client.new
|
||||||
- retrieve public data of any user **individually**
|
authd.key = File.read("./some-file").chomp
|
||||||
- change their own data: password, email address, profile entries (except the read-only ones)
|
|
||||||
- delete their account
|
|
||||||
- check their own permissions
|
|
||||||
|
|
||||||
Admins with 'Read' permission on the '*' resource can:
|
pp! r = authd.get_token?("login", "password")
|
||||||
- list users
|
|
||||||
- check permissions of other users
|
|
||||||
|
|
||||||
Admins with 'Edit' permission on the '*' resource can:
|
pp! r = authd.add_user("login", "password")
|
||||||
- change data of another user
|
|
||||||
|
|
||||||
Admins with 'Admin' permission on the '*' resource (or the 'admin' boolean) can:
|
pp! u = authd.get_user?("login", "password").not_nil!
|
||||||
- change read-only profile entries
|
```
|
||||||
- change permissions
|
|
||||||
- delete a user
|
|
||||||
- uprank and downrank admins
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
@ -81,15 +98,3 @@ Pull requests are welcome. For major changes, please open an issue first to disc
|
||||||
|
|
||||||
Please make sure to update tests as appropriate.
|
Please make sure to update tests as appropriate.
|
||||||
|
|
||||||
# WIP: design choices
|
|
||||||
|
|
||||||
An user has a number, a login, an email address, a profile (`Hash(String, JSON::Any)`) and permissions.
|
|
||||||
An `admin` boolean also tells weither or not the user is an administrator.
|
|
||||||
|
|
||||||
**Requests work mostly on current user**.
|
|
||||||
Some take a *UserID* to identify another user (its number or its login, both are valid), which often implies admin permissions.
|
|
||||||
|
|
||||||
**Permissions** are: None, Read, Edit, Admin.
|
|
||||||
Plus, the `admin` boolean value in the `AuthD::User` class.
|
|
||||||
|
|
||||||
> TODO: continue explaining design choices.
|
|
||||||
|
|
22
TODO.md
22
TODO.md
|
@ -1,22 +0,0 @@
|
||||||
### 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!
|
|
|
@ -1,26 +0,0 @@
|
||||||
#!/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
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
#!/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
101
makefile
|
@ -1,101 +0,0 @@
|
||||||
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"
|
|
59
shard.yml
59
shard.yml
|
@ -1,43 +1,38 @@
|
||||||
name: authd
|
name: authd
|
||||||
version: 0.1.0
|
version: 0.2.0
|
||||||
|
|
||||||
description: |
|
|
||||||
JWT-based authentication daemon.
|
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Philippe Pittoli <karchnu@karchnu.fr>
|
- Karchnu <karchnu@karchnu.fr>
|
||||||
|
- Luka Vandervelden <lukc@upyum.com>
|
||||||
|
|
||||||
|
description: |
|
||||||
|
JWT-based authentication daemon.
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
authd:
|
authd:
|
||||||
main: src/server.cr
|
main: src/main.cr
|
||||||
authc:
|
authc:
|
||||||
main: src/client.cr
|
main: utils/authc.cr
|
||||||
|
|
||||||
crystal: 1.7.1
|
crystal: 0.35.1
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
sodium:
|
grok:
|
||||||
branch: master
|
github: spinscale/grok.cr
|
||||||
github: didactic-drunk/sodium.cr
|
passwd:
|
||||||
grok:
|
git: https://git.baguette.netlib.re/Baguette/passwd.cr
|
||||||
github: spinscale/grok.cr
|
branch: master
|
||||||
passwd:
|
ipc:
|
||||||
git: https://git.baguette.netlib.re/Baguette/passwd.cr
|
git: https://git.baguette.netlib.re/Baguette/ipc.cr
|
||||||
branch: master
|
branch: master
|
||||||
jwt:
|
jwt:
|
||||||
github: crystal-community/jwt
|
github: crystal-community/jwt
|
||||||
branch: master
|
branch: master
|
||||||
baguette-crystal-base:
|
baguette-crystal-base:
|
||||||
git: https://git.baguette.netlib.re/Baguette/baguette-crystal-base
|
git: https://git.baguette.netlib.re/Baguette/baguette-crystal-base
|
||||||
branch: master
|
branch: master
|
||||||
dodb:
|
dodb:
|
||||||
git: https://git.baguette.netlib.re/Baguette/dodb.cr
|
git: https://git.baguette.netlib.re/Baguette/dodb.cr
|
||||||
branch: master
|
branch: master
|
||||||
cbor:
|
|
||||||
git: https://git.baguette.netlib.re/Baguette/crystal-cbor
|
|
||||||
branch: master
|
|
||||||
ipc:
|
|
||||||
git: https://git.baguette.netlib.re/Baguette/ipc.cr
|
|
||||||
branch: master
|
|
||||||
|
|
||||||
license: ISC
|
license: EUPL
|
||||||
|
|
674
src/authd.cr
674
src/authd.cr
|
@ -1,43 +1,659 @@
|
||||||
require "uuid"
|
require "json"
|
||||||
require "option_parser"
|
|
||||||
require "openssl"
|
require "jwt"
|
||||||
require "colorize"
|
|
||||||
require "jwt"
|
|
||||||
require "grok"
|
|
||||||
|
|
||||||
require "dodb"
|
|
||||||
require "ipc"
|
require "ipc"
|
||||||
|
|
||||||
require "baguette-crystal-base"
|
require "./user.cr"
|
||||||
|
|
||||||
# In any message, a user can be referred by its UInt32 uid or its login.
|
class AuthD::Exception < Exception
|
||||||
alias UserID = UInt32 | String
|
end
|
||||||
|
|
||||||
# Allows get configuration from a provided file.
|
class AuthD::MalformedRequest < Exception
|
||||||
# See Baguette::Configuration::Base.get
|
getter ipc_type : Int32
|
||||||
class Baguette::Configuration
|
getter payload : String
|
||||||
class Auth < IPC
|
|
||||||
include YAML::Serializable
|
|
||||||
|
|
||||||
property login : String? = nil
|
def initialize(@ipc_type, @payload)
|
||||||
property pass : String? = nil
|
@message = "malformed payload"
|
||||||
property secret_key : String = "nico-nico-nii" # Default authd key, as per the specs. :eyes:
|
end
|
||||||
property secret_key_file : String? = nil
|
end
|
||||||
|
|
||||||
def initialize
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Token and user classes.
|
class AuthD::Request
|
||||||
require "./authd/token.cr"
|
include JSON::Serializable
|
||||||
require "./authd/user.cr"
|
|
||||||
|
|
||||||
# Requests and responses.
|
property id : JSON::Any?
|
||||||
require "./authd/exceptions"
|
|
||||||
|
|
||||||
# Requests and responses.
|
annotation MessageType
|
||||||
require "./network"
|
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 doesn’t reset the whole profile, only resets elements
|
||||||
|
# for which keys are present in `new_profile`.
|
||||||
|
class EditProfileContent < Request
|
||||||
|
property token : String?
|
||||||
|
|
||||||
|
property shared_key : String?
|
||||||
|
property user : Int32 | String | Nil
|
||||||
|
|
||||||
|
property new_profile : Hash(String, JSON::Any)
|
||||||
|
|
||||||
|
initialize :shared_key, :user, :new_profile
|
||||||
|
initialize :token, :new_profile
|
||||||
|
end
|
||||||
|
|
||||||
|
class EditContacts < Request
|
||||||
|
property token : String
|
||||||
|
|
||||||
|
property email : String?
|
||||||
|
property phone : String?
|
||||||
|
end
|
||||||
|
|
||||||
|
class Delete < Request
|
||||||
|
# Deletion can be triggered by either an admin or the user.
|
||||||
|
property shared_key : String?
|
||||||
|
|
||||||
|
property login : String?
|
||||||
|
property password : String?
|
||||||
|
|
||||||
|
property user : String | Int32
|
||||||
|
|
||||||
|
initialize :user, :login, :password
|
||||||
|
initialize :user, :shared_key
|
||||||
|
end
|
||||||
|
|
||||||
|
# This creates a Request::Type enumeration. One entry for each request type.
|
||||||
|
{% begin %}
|
||||||
|
enum Type
|
||||||
|
{% for ivar in @type.subclasses %}
|
||||||
|
{% klass = ivar.name %}
|
||||||
|
{% name = ivar.name.split("::").last.id %}
|
||||||
|
|
||||||
|
{% a = ivar.annotation(MessageType) %}
|
||||||
|
|
||||||
|
{% if a %}
|
||||||
|
{% value = a[0] %}
|
||||||
|
{{ name }} = {{ value }}
|
||||||
|
{% else %}
|
||||||
|
{{ name }}
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
# This is an array of all requests types.
|
||||||
|
{% begin %}
|
||||||
|
class_getter requests = [
|
||||||
|
{% for ivar in @type.subclasses %}
|
||||||
|
{% klass = ivar.name %}
|
||||||
|
|
||||||
|
{{klass}},
|
||||||
|
{% end %}
|
||||||
|
]
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
def self.from_ipc(message : IPC::Message) : Request?
|
||||||
|
payload = String.new message.payload
|
||||||
|
type = Type.new message.utype.to_i
|
||||||
|
|
||||||
|
begin
|
||||||
|
requests.find(&.type.==(type)).try &.from_json(payload)
|
||||||
|
rescue e : JSON::ParseException
|
||||||
|
raise MalformedRequest.new message.utype.to_i, payload
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module AuthD
|
||||||
|
class Client < IPC::Client
|
||||||
|
property key : String
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@key = ""
|
||||||
|
|
||||||
|
initialize "auth"
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_token?(login : String, password : String) : String?
|
||||||
|
send Request::GetToken.new login, password
|
||||||
|
|
||||||
|
response = Response.from_ipc read
|
||||||
|
|
||||||
|
if response.is_a?(Response::Token)
|
||||||
|
response.token
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_user?(login : String, password : String) : AuthD::User::Public?
|
||||||
|
send Request::GetUserByCredentials.new login, password
|
||||||
|
|
||||||
|
response = Response.from_ipc read
|
||||||
|
|
||||||
|
if response.is_a? Response::User
|
||||||
|
response.user
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_user?(uid_or_login : Int32 | String) : ::AuthD::User::Public?
|
||||||
|
send Request::GetUser.new uid_or_login
|
||||||
|
|
||||||
|
response = Response.from_ipc read
|
||||||
|
|
||||||
|
if response.is_a? Response::User
|
||||||
|
response.user
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def send(type : Request::Type, payload)
|
||||||
|
send_now @server_fd, type.value.to_u8, payload
|
||||||
|
end
|
||||||
|
|
||||||
|
def decode_token(token)
|
||||||
|
user, meta = JWT.decode token, @key, JWT::Algorithm::HS256
|
||||||
|
|
||||||
|
user = ::AuthD::User::Public.from_json user.to_json
|
||||||
|
|
||||||
|
{user, meta}
|
||||||
|
end
|
||||||
|
|
||||||
|
# FIXME: Extra options may be useful to implement here.
|
||||||
|
def add_user(login : String, password : String,
|
||||||
|
email : String?,
|
||||||
|
phone : String?,
|
||||||
|
profile : Hash(String, JSON::Any)?) : ::AuthD::User::Public | Exception
|
||||||
|
|
||||||
|
send Request::AddUser.new @key, login, password, email, phone, profile
|
||||||
|
|
||||||
|
response = Response.from_ipc read
|
||||||
|
|
||||||
|
case response
|
||||||
|
when Response::UserAdded
|
||||||
|
response.user
|
||||||
|
when Response::Error
|
||||||
|
raise Exception.new response.reason
|
||||||
|
else
|
||||||
|
# Should not happen in serialized connections, but…
|
||||||
|
# it’ll 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…
|
||||||
|
# it’ll happen if you run several requests at once.
|
||||||
|
Exception.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ask_password_recovery(uid_or_login : String | Int32, email : String)
|
||||||
|
send Request::AskPasswordRecovery.new uid_or_login, email
|
||||||
|
response = Response.from_ipc read
|
||||||
|
|
||||||
|
case response
|
||||||
|
when Response::PasswordRecoverySent
|
||||||
|
when Response::Error
|
||||||
|
raise Exception.new response.reason
|
||||||
|
else
|
||||||
|
Exception.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def change_password(uid_or_login : String | Int32, new_pass : String, renew_key : String)
|
||||||
|
send Request::PasswordRecovery.new uid_or_login, renew_key, new_pass
|
||||||
|
response = Response.from_ipc read
|
||||||
|
|
||||||
|
case response
|
||||||
|
when Response::PasswordRecovered
|
||||||
|
when Response::Error
|
||||||
|
raise Exception.new response.reason
|
||||||
|
else
|
||||||
|
Exception.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def register(login : String,
|
||||||
|
password : String,
|
||||||
|
email : String?,
|
||||||
|
phone : String?,
|
||||||
|
profile : Hash(String, JSON::Any)?) : ::AuthD::User::Public?
|
||||||
|
|
||||||
|
send Request::Register.new login, password, email, phone, profile
|
||||||
|
response = Response.from_ipc read
|
||||||
|
|
||||||
|
case response
|
||||||
|
when Response::UserAdded
|
||||||
|
when Response::Error
|
||||||
|
raise Exception.new response.reason
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def mod_user(uid_or_login : Int32 | String, password : String? = nil, email : String? = nil, phone : String? = nil, avatar : String? = nil) : Bool | Exception
|
||||||
|
request = Request::ModUser.new @key, uid_or_login
|
||||||
|
|
||||||
|
request.password = password if password
|
||||||
|
request.email = email if email
|
||||||
|
request.phone = phone if phone
|
||||||
|
request.avatar = avatar if avatar
|
||||||
|
|
||||||
|
send request
|
||||||
|
|
||||||
|
response = Response.from_ipc read
|
||||||
|
|
||||||
|
case response
|
||||||
|
when Response::UserEdited
|
||||||
|
true
|
||||||
|
when Response::Error
|
||||||
|
Exception.new response.reason
|
||||||
|
else
|
||||||
|
Exception.new "???"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_permission(user : Int32, service_name : String, resource_name : String) : User::PermissionLevel
|
||||||
|
request = Request::CheckPermission.new @key, user, service_name, resource_name
|
||||||
|
|
||||||
|
send request
|
||||||
|
|
||||||
|
response = Response.from_ipc read
|
||||||
|
|
||||||
|
case response
|
||||||
|
when Response::PermissionCheck
|
||||||
|
response.permission
|
||||||
|
when Response
|
||||||
|
raise Exception.new "unexpected response: #{response.type}"
|
||||||
|
else
|
||||||
|
raise Exception.new "unexpected response"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_permission(uid : Int32, service : String, resource : String, permission : User::PermissionLevel)
|
||||||
|
request = Request::SetPermission.new @key, uid, service, resource, permission
|
||||||
|
|
||||||
|
send request
|
||||||
|
|
||||||
|
response = Response.from_ipc read
|
||||||
|
|
||||||
|
case response
|
||||||
|
when Response::PermissionSet
|
||||||
|
true
|
||||||
|
when Response
|
||||||
|
raise Exception.new "unexpected response: #{response.type}"
|
||||||
|
else
|
||||||
|
raise Exception.new "unexpected response"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def search_user(user_login : String)
|
||||||
|
send Request::SearchUser.new user_login
|
||||||
|
response = Response.from_ipc read
|
||||||
|
|
||||||
|
case response
|
||||||
|
when Response::MatchingUsers
|
||||||
|
response.users
|
||||||
|
when Response::Error
|
||||||
|
raise Exception.new response.reason
|
||||||
|
else
|
||||||
|
Exception.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit_profile_content(user : Int32 | String, new_values)
|
||||||
|
send Request::EditProfileContent.new key, user, new_values
|
||||||
|
response = Response.from_ipc read
|
||||||
|
|
||||||
|
case response
|
||||||
|
when Response::User
|
||||||
|
response.user
|
||||||
|
when Response::Error
|
||||||
|
raise Exception.new response.reason
|
||||||
|
else
|
||||||
|
raise Exception.new "unexpected response"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(user : Int32 | String, key : String)
|
||||||
|
send Request::Delete.new user, key
|
||||||
|
delete_
|
||||||
|
end
|
||||||
|
def delete(user : Int32 | String, login : String, pass : String)
|
||||||
|
send Request::Delete.new user, login, pass
|
||||||
|
delete_
|
||||||
|
end
|
||||||
|
def delete_
|
||||||
|
response = Response.from_ipc read
|
||||||
|
case response
|
||||||
|
when Response::Error
|
||||||
|
raise Exception.new response.reason
|
||||||
|
end
|
||||||
|
response
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class IPC::Context
|
||||||
|
def send(fd, response : AuthD::Response)
|
||||||
|
send fd, response.type.to_u8, response.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class IPC::Client
|
||||||
|
def send(request : AuthD::Request)
|
||||||
|
unless (fd = @server_fd).nil?
|
||||||
|
send_now fd, request.type.to_u8, request.to_json
|
||||||
|
else
|
||||||
|
raise "Client not connected to the server"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Functions to request the authd server.
|
|
||||||
require "./authd/client.cr"
|
|
||||||
|
|
|
@ -1,192 +0,0 @@
|
||||||
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
|
|
|
@ -1,13 +0,0 @@
|
||||||
module AuthD
|
|
||||||
class Exception < ::Exception
|
|
||||||
end
|
|
||||||
|
|
||||||
class UserNotFound < ::Exception
|
|
||||||
end
|
|
||||||
|
|
||||||
class AuthenticationInfoLacking < ::Exception
|
|
||||||
end
|
|
||||||
|
|
||||||
class AdminAuthorizationException < ::Exception
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -0,0 +1,698 @@
|
||||||
|
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.nil?
|
||||||
|
return Response::Error.new "invalid credentials"
|
||||||
|
end
|
||||||
|
|
||||||
|
if user.password_hash != hash_password request.password
|
||||||
|
return Response::Error.new "invalid credentials"
|
||||||
|
end
|
||||||
|
|
||||||
|
user.date_last_connection = Time.local
|
||||||
|
token = user.to_token
|
||||||
|
|
||||||
|
# change the date of the last connection
|
||||||
|
@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
|
||||||
|
when Request::Delete
|
||||||
|
uid_or_login = request.user
|
||||||
|
user_to_delete = if uid_or_login.is_a? Int32
|
||||||
|
@users_per_uid.get? uid_or_login.to_s
|
||||||
|
else
|
||||||
|
@users_per_login.get? uid_or_login
|
||||||
|
end
|
||||||
|
|
||||||
|
if user_to_delete.nil?
|
||||||
|
return Response::Error.new "invalid user"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Either the request comes from an admin or the user.
|
||||||
|
# Shared key == admin, check the key.
|
||||||
|
if key = request.shared_key
|
||||||
|
return Response::Error.new "unauthorized (wrong shared key)" unless key == @jwt_key
|
||||||
|
else
|
||||||
|
login = request.login
|
||||||
|
pass = request.password
|
||||||
|
if login.nil? || pass.nil?
|
||||||
|
return Response::Error.new "authentication failed (no shared key, no login)"
|
||||||
|
end
|
||||||
|
|
||||||
|
# authenticate the user
|
||||||
|
begin
|
||||||
|
user = @users_per_login.get login
|
||||||
|
rescue e : DODB::MissingEntry
|
||||||
|
return Response::Error.new "invalid credentials"
|
||||||
|
end
|
||||||
|
|
||||||
|
if user.nil?
|
||||||
|
return Response::Error.new "invalid credentials"
|
||||||
|
end
|
||||||
|
|
||||||
|
if user.password_hash != hash_password pass
|
||||||
|
return Response::Error.new "invalid credentials"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Is the user to delete the requesting user?
|
||||||
|
if user.uid != user_to_delete.uid
|
||||||
|
return Response::Error.new "invalid credentials"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# User or admin is now verified: let's proceed with the user deletion.
|
||||||
|
@users_per_login.delete user_to_delete.login
|
||||||
|
|
||||||
|
# TODO: better response
|
||||||
|
Response::User.new user_to_delete.to_public
|
||||||
|
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
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
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/*"
|
|
|
@ -1,108 +0,0 @@
|
||||||
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
|
|
|
@ -1,34 +0,0 @@
|
||||||
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
|
|
|
@ -1,11 +0,0 @@
|
||||||
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
|
|
|
@ -1,73 +0,0 @@
|
||||||
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
|
|
|
@ -1,43 +0,0 @@
|
||||||
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
|
|
|
@ -1,92 +0,0 @@
|
||||||
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
|
|
|
@ -1,76 +0,0 @@
|
||||||
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
|
|
|
@ -1,50 +0,0 @@
|
||||||
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
|
|
|
@ -1,87 +0,0 @@
|
||||||
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
|
|
|
@ -1,52 +0,0 @@
|
||||||
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
|
|
|
@ -1,52 +0,0 @@
|
||||||
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
|
|
|
@ -1,9 +0,0 @@
|
||||||
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
|
|
|
@ -1,105 +0,0 @@
|
||||||
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
|
|
|
@ -1,7 +0,0 @@
|
||||||
class AuthD::Response
|
|
||||||
IPC::JSON.message KeepAlive, 250 do
|
|
||||||
def initialize()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
AuthD.responses << KeepAlive
|
|
||||||
end
|
|
|
@ -1,9 +0,0 @@
|
||||||
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
|
|
|
@ -1,13 +0,0 @@
|
||||||
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
|
|
|
@ -1,21 +0,0 @@
|
||||||
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
|
|
|
@ -1,50 +0,0 @@
|
||||||
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
|
|
|
@ -1,76 +0,0 @@
|
||||||
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
215
src/service.cr
|
@ -1,215 +0,0 @@
|
||||||
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
|
|
|
@ -4,7 +4,7 @@ class AuthD::Token
|
||||||
include JSON::Serializable
|
include JSON::Serializable
|
||||||
|
|
||||||
property login : String
|
property login : String
|
||||||
property uid : UInt32
|
property uid : Int32
|
||||||
|
|
||||||
def initialize(@login, @uid)
|
def initialize(@login, @uid)
|
||||||
end
|
end
|
||||||
|
@ -23,7 +23,7 @@ class AuthD::Token
|
||||||
def self.from_s(key, str)
|
def self.from_s(key, str)
|
||||||
payload, meta = JWT.decode str, key, JWT::Algorithm::HS256
|
payload, meta = JWT.decode str, key, JWT::Algorithm::HS256
|
||||||
|
|
||||||
self.new payload["login"].as_s, payload["uid"].as_i64.to_u32
|
self.new payload["login"].as_s, payload["uid"].as_i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
require "json"
|
require "json"
|
||||||
|
|
||||||
require "uuid"
|
require "uuid"
|
||||||
|
|
||||||
|
require "./token.cr"
|
||||||
|
|
||||||
class AuthD::User
|
class AuthD::User
|
||||||
include JSON::Serializable
|
include JSON::Serializable
|
||||||
|
|
||||||
|
@ -19,21 +22,18 @@ class AuthD::User
|
||||||
include JSON::Serializable
|
include JSON::Serializable
|
||||||
|
|
||||||
# the activation key is removed once the user is validated
|
# the activation key is removed once the user is validated
|
||||||
property activation_key : String? = nil
|
property activation_key : String?
|
||||||
property email : String?
|
property email : String?
|
||||||
|
property phone : String?
|
||||||
|
|
||||||
def initialize(@email = nil)
|
def initialize(@email = nil, @phone = nil)
|
||||||
end
|
|
||||||
|
|
||||||
def new_activation_key
|
|
||||||
@activation_key = UUID.random.to_s
|
@activation_key = UUID.random.to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Public.
|
# Public.
|
||||||
property login : String
|
property login : String
|
||||||
property uid : UInt32
|
property uid : Int32
|
||||||
property admin : Bool = false
|
|
||||||
property profile : Hash(String, JSON::Any)?
|
property profile : Hash(String, JSON::Any)?
|
||||||
|
|
||||||
# Private.
|
# Private.
|
||||||
|
@ -50,7 +50,7 @@ class AuthD::User
|
||||||
Token.new @login, @uid
|
Token.new @login, @uid
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(@uid, @login, @password_hash, @admin = false)
|
def initialize(@uid, @login, @password_hash)
|
||||||
@contact = Contact.new
|
@contact = Contact.new
|
||||||
@permissions = Hash(String, Hash(String, PermissionLevel)).new
|
@permissions = Hash(String, Hash(String, PermissionLevel)).new
|
||||||
@configuration = Hash(String, Hash(String, JSON::Any)).new
|
@configuration = Hash(String, Hash(String, JSON::Any)).new
|
||||||
|
@ -60,35 +60,17 @@ class AuthD::User
|
||||||
include JSON::Serializable
|
include JSON::Serializable
|
||||||
|
|
||||||
property login : String
|
property login : String
|
||||||
property uid : UInt32
|
property uid : Int32
|
||||||
property admin : Bool = false
|
|
||||||
property profile : Hash(String, JSON::Any)?
|
property profile : Hash(String, JSON::Any)?
|
||||||
|
|
||||||
property date_registration : Time?
|
property date_registration : Time?
|
||||||
|
|
||||||
def initialize(@uid, @login, @admin, @profile, @date_registration)
|
def initialize(@uid, @login, @profile, @date_registration)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_public : Public
|
def to_public : Public
|
||||||
Public.new @uid, @login, @admin, @profile, @date_registration
|
Public.new @uid, @login, @profile, @date_registration
|
||||||
end
|
|
||||||
|
|
||||||
def assert_permission(service : String, resource : String, level : User::PermissionLevel)
|
|
||||||
return if @admin # skip if admin
|
|
||||||
|
|
||||||
permissions = @permissions[service]?
|
|
||||||
unless permissions
|
|
||||||
raise AdminAuthorizationException.new "unauthorized (not admin nor in #{service} group)"
|
|
||||||
end
|
|
||||||
|
|
||||||
rights = permissions[resource]?
|
|
||||||
unless rights
|
|
||||||
raise AdminAuthorizationException.new "unauthorized (no rights on '#{service}/#{resource}')"
|
|
||||||
end
|
|
||||||
|
|
||||||
if rights < level
|
|
||||||
raise AdminAuthorizationException.new "unauthorized (insufficient rights on '#{service}/#{resource}')"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,12 +1,23 @@
|
||||||
require "option_parser"
|
require "option_parser"
|
||||||
|
|
||||||
|
require "ipc"
|
||||||
require "yaml"
|
require "yaml"
|
||||||
require "./authd.cr"
|
|
||||||
|
require "baguette-crystal-base"
|
||||||
|
|
||||||
|
require "../src/authd.cr"
|
||||||
|
|
||||||
|
# require "./altideal-client.cr"
|
||||||
|
# require "./yaml_uuid.cr" # YAML UUID parser
|
||||||
|
# require "./authd_api.cr" # Authd interface functions
|
||||||
|
|
||||||
|
|
||||||
class Context
|
class Context
|
||||||
class_property simulation = false # do not perform the action
|
class_property simulation = false # do not perform the action
|
||||||
|
|
||||||
class_property authd_login : String? = nil
|
class_property authd_login = "undef" # undef authd user
|
||||||
class_property authd_pass : String? = nil
|
class_property authd_pass = "undef" # undef authd user password
|
||||||
|
class_property shared_key = "undef" # undef authd user password
|
||||||
|
|
||||||
# # Properties to select what to display when printing a deal.
|
# # Properties to select what to display when printing a deal.
|
||||||
# class_property print_title = true
|
# class_property print_title = true
|
||||||
|
@ -17,12 +28,14 @@ class Context
|
||||||
class_property command = "not-implemented"
|
class_property command = "not-implemented"
|
||||||
|
|
||||||
class_property user_profile : Hash(String,JSON::Any)?
|
class_property user_profile : Hash(String,JSON::Any)?
|
||||||
|
class_property phone : String?
|
||||||
class_property email : String?
|
class_property email : String?
|
||||||
|
|
||||||
# Will be parsed later, with a specific parser.
|
# Will be parsed later, with a specific parser.
|
||||||
class_property args : Array(String)? = nil
|
class_property args : Array(String)? = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# require "./parse-me"
|
||||||
require "./better-parser"
|
require "./better-parser"
|
||||||
|
|
||||||
class Actions
|
class Actions
|
||||||
|
@ -51,18 +64,14 @@ class Actions
|
||||||
property authd : AuthD::Client
|
property authd : AuthD::Client
|
||||||
|
|
||||||
def initialize(@authd)
|
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-add"] = ->user_add
|
||||||
@the_call["user-mod"] = ->user_mod
|
@the_call["user-mod"] = ->user_mod
|
||||||
|
@the_call["user-registration"] = ->user_registration # Do not require admin priviledges.
|
||||||
|
@the_call["user-delete"] = ->user_deletion # Do not require admin priviledges.
|
||||||
|
@the_call["user-get"] = ->user_get # Do not require authentication.
|
||||||
|
@the_call["user-validation"] = ->user_validation # Do not require authentication.
|
||||||
|
@the_call["user-recovery"] = ->user_recovery # Do not require authentication.
|
||||||
|
@the_call["user-search"] = ->user_search # Do not require authentication.
|
||||||
|
|
||||||
@the_call["permission-set"] = ->permission_set
|
@the_call["permission-set"] = ->permission_set
|
||||||
@the_call["permission-check"] = ->permission_check
|
@the_call["permission-check"] = ->permission_check
|
||||||
|
@ -74,76 +83,33 @@ class Actions
|
||||||
#
|
#
|
||||||
|
|
||||||
def user_add
|
def user_add
|
||||||
|
puts "User add!!!"
|
||||||
args = Context.args.not_nil!
|
args = Context.args.not_nil!
|
||||||
login, email = args[0..1]
|
login, email, phone = args[0..2]
|
||||||
profile = Context.user_profile
|
profile = Context.user_profile
|
||||||
|
|
||||||
password = Actions.ask_password
|
password = Actions.ask_password
|
||||||
exit 1 unless password
|
exit 1 unless password
|
||||||
|
|
||||||
# By default: not admin.
|
pp! authd.add_user login, password.not_nil!, email, phone, profile: profile
|
||||||
pp! authd.add_user login, password.not_nil!, false, email, profile: profile
|
|
||||||
rescue e : AuthD::Exception
|
rescue e : AuthD::Exception
|
||||||
puts "error: #{e.message}"
|
puts "error: #{e.message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_registration
|
def user_registration
|
||||||
args = Context.args.not_nil!
|
args = Context.args.not_nil!
|
||||||
login, email = args[0..1]
|
login, email, phone = args[0..2]
|
||||||
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
|
profile = Context.user_profile
|
||||||
|
|
||||||
password = Actions.ask_password
|
password = Actions.ask_password
|
||||||
exit 1 unless password
|
exit 1 unless password
|
||||||
|
|
||||||
pp! authd.bootstrap login, password.not_nil!, email, profile
|
res = authd.register login, password.not_nil!, email, phone, profile: profile
|
||||||
|
puts res
|
||||||
rescue e : AuthD::Exception
|
rescue e : AuthD::Exception
|
||||||
puts "error: #{e.message}"
|
puts "error: #{e.message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
def user_mod
|
def user_mod
|
||||||
args = Context.args.not_nil!
|
args = Context.args.not_nil!
|
||||||
|
@ -162,19 +128,25 @@ class Actions
|
||||||
end
|
end
|
||||||
|
|
||||||
email = Context.email
|
email = Context.email
|
||||||
|
phone = Context.phone
|
||||||
|
|
||||||
Baguette::Log.error "This function shouldn't be used for now."
|
Baguette::Log.error "This function shouldn't be used for now."
|
||||||
Baguette::Log.error "It is way too cumbersome."
|
Baguette::Log.error "It is way too cumbersome."
|
||||||
|
|
||||||
# res = authd.add_user login, password, email, profile: profile
|
# res = authd.add_user login, password, email, phone, profile: profile
|
||||||
# puts res
|
# puts res
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_deletion
|
def user_deletion
|
||||||
args = Context.args.not_nil!
|
args = Context.args.not_nil!
|
||||||
userid = args[0].to_u32
|
userid = args[0].to_i
|
||||||
|
|
||||||
res = authd.delete userid
|
# Check if the request comes from an admin or the user.
|
||||||
|
res = if Context.shared_key.nil?
|
||||||
|
authd.delete userid, Context.authd_login, Context.authd_pass
|
||||||
|
else
|
||||||
|
authd.delete userid, Context.shared_key
|
||||||
|
end
|
||||||
|
|
||||||
puts res
|
puts res
|
||||||
end
|
end
|
||||||
|
@ -196,37 +168,27 @@ class Actions
|
||||||
end
|
end
|
||||||
def user_recovery
|
def user_recovery
|
||||||
args = Context.args.not_nil!
|
args = Context.args.not_nil!
|
||||||
login = args[0]
|
login, email = args[0..1]
|
||||||
pp! authd.ask_password_recovery login
|
pp! authd.ask_password_recovery login, email
|
||||||
end
|
end
|
||||||
|
|
||||||
def permission_check
|
def permission_check
|
||||||
args = Context.args.not_nil!
|
args = Context.args.not_nil!
|
||||||
user, application, resource = args[0..2]
|
user, application, resource = args[0..2]
|
||||||
res = @authd.check_permission user.to_u32, application, resource
|
# pp! user, application, resource
|
||||||
case res
|
|
||||||
when Response::PermissionCheck
|
res = @authd.check_permission user.to_i, application, resource
|
||||||
s = res.service
|
puts res
|
||||||
r = res.resource
|
|
||||||
u = res.user
|
|
||||||
p = res.permission
|
|
||||||
Baguette::Log.info "app #{s} resource #{r} user #{u}: #{p}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def permission_set
|
def permission_set
|
||||||
args = Context.args.not_nil!
|
args = Context.args.not_nil!
|
||||||
user, application, resource, permission = args[0..3]
|
user, application, resource, permission = args[0..3]
|
||||||
|
# pp! user, application, resource, permission
|
||||||
|
|
||||||
perm = AuthD::User::PermissionLevel.parse(permission)
|
perm = AuthD::User::PermissionLevel.parse(permission)
|
||||||
res = @authd.set_permission user.to_u32, application, resource, perm
|
res = @authd.set_permission user.to_i, application, resource, perm
|
||||||
case res
|
puts 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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -234,25 +196,18 @@ def main
|
||||||
|
|
||||||
# Authd connection.
|
# Authd connection.
|
||||||
authd = AuthD::Client.new
|
authd = AuthD::Client.new
|
||||||
|
authd.key = Context.shared_key if Context.shared_key != "undef"
|
||||||
|
|
||||||
if login = Context.authd_login
|
# Authd token.
|
||||||
pass = if p = Context.authd_pass
|
# FIXME: not sure about getting the token, it seems not used elsewhere.
|
||||||
p
|
# If login == pass == "undef": do not even try.
|
||||||
else
|
#unless Context.authd_login == Context.authd_pass && Context.authd_login == "undef"
|
||||||
password = Actions.ask_password
|
# login = Context.authd_login
|
||||||
raise "cannot get a password" unless password
|
# pass = Context.authd_pass
|
||||||
password
|
# token = authd.get_token? login, pass
|
||||||
end
|
# raise "cannot get a token" if token.nil?
|
||||||
response = authd.login? login, pass
|
# # authd.login token
|
||||||
case response
|
#end
|
||||||
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
|
actions = Actions.new authd
|
||||||
|
|
||||||
|
@ -261,8 +216,6 @@ def main
|
||||||
actions.the_call[Context.command].call
|
actions.the_call[Context.command].call
|
||||||
rescue e
|
rescue e
|
||||||
Baguette::Log.info "The command is not recognized (or implemented)."
|
Baguette::Log.info "The command is not recognized (or implemented)."
|
||||||
Baguette::Log.info "Exception: #{e}."
|
|
||||||
pp! e
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# authd disconnection
|
# authd disconnection
|
||||||
|
@ -276,3 +229,4 @@ end
|
||||||
# tool [options] command [options-for-command]
|
# tool [options] command [options-for-command]
|
||||||
|
|
||||||
main
|
main
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
require "option_parser"
|
require "option_parser"
|
||||||
|
|
||||||
|
opt_authd_admin = -> (parser : OptionParser) {
|
||||||
|
parser.on "-k file", "--key-file file", "Read the authd shared key from a file." do |file|
|
||||||
|
Context.shared_key = File.read(file).chomp
|
||||||
|
Baguette::Log.info "Key for admin operations: #{Context.shared_key}."
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
# frequently used functions
|
# frequently used functions
|
||||||
opt_authd_login = -> (parser : OptionParser) {
|
opt_authd_login = -> (parser : OptionParser) {
|
||||||
parser.on "-l LOGIN", "--login LOGIN", "Authd user login." do |login|
|
parser.on "-l LOGIN", "--login LOGIN", "Authd user login." do |login|
|
||||||
|
@ -26,6 +33,13 @@ opt_profile = -> (parser : OptionParser) {
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
opt_phone = -> (parser : OptionParser) {
|
||||||
|
parser.on "-n phone", "--phone-number num", "Phone number." do |phone|
|
||||||
|
Context.phone = phone
|
||||||
|
Baguette::Log.info "Reading the user phone number: #{Context.phone}."
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
opt_email = -> (parser : OptionParser) {
|
opt_email = -> (parser : OptionParser) {
|
||||||
parser.on "-e email", "--email address", "Email address." do |email|
|
parser.on "-e email", "--email address", "Email address." do |email|
|
||||||
Context.email = email
|
Context.email = email
|
||||||
|
@ -35,15 +49,13 @@ opt_email = -> (parser : OptionParser) {
|
||||||
|
|
||||||
|
|
||||||
# Unrecognized parameters are used to create commands with multiple arguments.
|
# Unrecognized parameters are used to create commands with multiple arguments.
|
||||||
# Example: user add login email
|
# Example: user add _login email phone_
|
||||||
# Here, login and email are unrecognized arguments.
|
# Here, login, email and phone are unrecognized arguments.
|
||||||
# Still, the "user add" command expect them.
|
# Still, the "user add" command expect them.
|
||||||
unrecognized_args_to_context_args = -> (parser : OptionParser, n_expected_args : Int32) {
|
unrecognized_args_to_context_args = -> (parser : OptionParser, n_expected_args : Int32) {
|
||||||
# With the right args, these will be interpreted as serialized data.
|
# With the right args, these will be interpreted as serialized data.
|
||||||
parser.unknown_args do |args|
|
parser.unknown_args do |args|
|
||||||
if args.size != n_expected_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}"
|
Baguette::Log.error "#{parser}"
|
||||||
exit 1
|
exit 1
|
||||||
end
|
end
|
||||||
|
@ -69,36 +81,29 @@ parser = OptionParser.new do |parser|
|
||||||
exit 0
|
exit 0
|
||||||
end
|
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.on "user", "Operations on users." do
|
||||||
parser.banner = "Usage: user [add | mod | delete | validate | search | get | recover | register ]"
|
parser.banner = "Usage: user [add | mod | delete | validate | search | get | recover | register ]"
|
||||||
|
|
||||||
parser.on "add", "Adding a user to the DB." do
|
parser.on "add", "Adding a user to the DB." do
|
||||||
parser.banner = "usage: user add login email [-P profile] [opt]"
|
parser.banner = "usage: user add login email phone [-P profile] [opt]"
|
||||||
Baguette::Log.info "Adding a user to the DB."
|
Baguette::Log.info "Adding a user to the DB."
|
||||||
Context.command = "user-add"
|
Context.command = "user-add"
|
||||||
opt_authd_login.call parser
|
opt_authd_admin.call parser
|
||||||
opt_profile.call parser
|
opt_profile.call parser
|
||||||
|
opt_email.call parser
|
||||||
|
opt_phone.call parser
|
||||||
opt_help.call parser
|
opt_help.call parser
|
||||||
# login email
|
# login email phone
|
||||||
unrecognized_args_to_context_args.call parser, 2
|
unrecognized_args_to_context_args.call parser, 3
|
||||||
end
|
end
|
||||||
|
|
||||||
parser.on "mod", "Modify a user account." do
|
parser.on "mod", "Modify a user account." do
|
||||||
parser.banner = "Usage: user mod userid [-e email|-P profile] [opt]"
|
parser.banner = "Usage: user mod userid [-e email|-n phone|-P profile] [opt]"
|
||||||
Baguette::Log.info "Modify a user account."
|
Baguette::Log.info "Modify a user account."
|
||||||
Context.command = "user-mod"
|
Context.command = "user-mod"
|
||||||
opt_authd_login.call parser
|
opt_authd_admin.call parser
|
||||||
opt_email.call parser
|
opt_email.call parser
|
||||||
|
opt_phone.call parser
|
||||||
opt_profile.call parser
|
opt_profile.call parser
|
||||||
opt_help.call parser
|
opt_help.call parser
|
||||||
# userid
|
# userid
|
||||||
|
@ -111,6 +116,7 @@ parser = OptionParser.new do |parser|
|
||||||
Context.command = "user-delete"
|
Context.command = "user-delete"
|
||||||
# You can either be the owner of the account, or an admin.
|
# You can either be the owner of the account, or an admin.
|
||||||
opt_authd_login.call parser
|
opt_authd_login.call parser
|
||||||
|
opt_authd_admin.call parser
|
||||||
opt_help.call parser
|
opt_help.call parser
|
||||||
# userid
|
# userid
|
||||||
unrecognized_args_to_context_args.call parser, 1
|
unrecognized_args_to_context_args.call parser, 1
|
||||||
|
@ -119,7 +125,7 @@ parser = OptionParser.new do |parser|
|
||||||
parser.on "validate", "Validate user." do
|
parser.on "validate", "Validate user." do
|
||||||
parser.banner = "Usage: user validate login activation-key [opt]"
|
parser.banner = "Usage: user validate login activation-key [opt]"
|
||||||
Baguette::Log.info "Validate user."
|
Baguette::Log.info "Validate user."
|
||||||
Context.command = "user-validation"
|
Context.command = "user-validate"
|
||||||
# No need to be authenticated.
|
# No need to be authenticated.
|
||||||
opt_help.call parser
|
opt_help.call parser
|
||||||
# login activation-key
|
# login activation-key
|
||||||
|
@ -130,7 +136,7 @@ parser = OptionParser.new do |parser|
|
||||||
parser.banner = "Usage: user get login [opt]"
|
parser.banner = "Usage: user get login [opt]"
|
||||||
Baguette::Log.info "Get user info."
|
Baguette::Log.info "Get user info."
|
||||||
Context.command = "user-get"
|
Context.command = "user-get"
|
||||||
opt_authd_login.call parser
|
# No need to be authenticated.
|
||||||
opt_help.call parser
|
opt_help.call parser
|
||||||
# login
|
# login
|
||||||
unrecognized_args_to_context_args.call parser, 1
|
unrecognized_args_to_context_args.call parser, 1
|
||||||
|
@ -140,32 +146,36 @@ parser = OptionParser.new do |parser|
|
||||||
parser.banner = "Usage: user recover login [opt]"
|
parser.banner = "Usage: user recover login [opt]"
|
||||||
Baguette::Log.info "Search user."
|
Baguette::Log.info "Search user."
|
||||||
Context.command = "user-search"
|
Context.command = "user-search"
|
||||||
opt_authd_login.call parser
|
# No need to be authenticated.
|
||||||
opt_help.call parser
|
opt_help.call parser
|
||||||
# login
|
# login
|
||||||
unrecognized_args_to_context_args.call parser, 1
|
unrecognized_args_to_context_args.call parser, 1
|
||||||
end
|
end
|
||||||
|
|
||||||
parser.on "recover", "Recover user password." do
|
parser.on "recover", "Recover user password." do
|
||||||
parser.banner = "Usage: user recover login [opt]"
|
parser.banner = "Usage: user recover login email [opt]"
|
||||||
Baguette::Log.info "Recover user password."
|
Baguette::Log.info "Recover user password."
|
||||||
Context.command = "user-recovery"
|
Context.command = "user-recovery"
|
||||||
# No need to be authenticated.
|
# No need to be authenticated.
|
||||||
opt_help.call parser
|
opt_help.call parser
|
||||||
# login email
|
# login email
|
||||||
unrecognized_args_to_context_args.call parser, 1
|
unrecognized_args_to_context_args.call parser, 2
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# Do not require to be admin.
|
# Do not require to be admin.
|
||||||
parser.on "register", "Register a user (requires activation)." do
|
parser.on "register", "Register a user (requires activation)." do
|
||||||
parser.banner = "Usage: user register login email [-P profile] [opt]"
|
parser.banner = "Usage: user register login email phone [-P profile] [opt]"
|
||||||
Baguette::Log.info "Register a user (requires activation)."
|
Baguette::Log.info "Register a user (requires activation)."
|
||||||
Context.command = "user-registration"
|
Context.command = "user-registration"
|
||||||
|
# These options shouldn't be used here,
|
||||||
|
# email and phone parameters are mandatory.
|
||||||
|
# opt_email.call parser
|
||||||
|
# opt_phone.call parser
|
||||||
opt_profile.call parser
|
opt_profile.call parser
|
||||||
opt_help.call parser
|
opt_help.call parser
|
||||||
# login email
|
# login email phone
|
||||||
unrecognized_args_to_context_args.call parser, 2
|
unrecognized_args_to_context_args.call parser, 3
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -180,7 +190,7 @@ permission list: none read edit admin
|
||||||
END
|
END
|
||||||
Baguette::Log.info "Set permissions."
|
Baguette::Log.info "Set permissions."
|
||||||
Context.command = "permission-set"
|
Context.command = "permission-set"
|
||||||
opt_authd_login.call parser
|
opt_authd_admin.call parser
|
||||||
opt_help.call parser
|
opt_help.call parser
|
||||||
# userid application resource permission
|
# userid application resource permission
|
||||||
unrecognized_args_to_context_args.call parser, 4
|
unrecognized_args_to_context_args.call parser, 4
|
||||||
|
@ -195,7 +205,7 @@ permission list: none read edit admin
|
||||||
END
|
END
|
||||||
Baguette::Log.info "Check permissions."
|
Baguette::Log.info "Check permissions."
|
||||||
Context.command = "permission-check"
|
Context.command = "permission-check"
|
||||||
opt_authd_login.call parser
|
opt_authd_admin.call parser
|
||||||
opt_help.call parser
|
opt_help.call parser
|
||||||
# userid application resource
|
# userid application resource
|
||||||
unrecognized_args_to_context_args.call parser, 3
|
unrecognized_args_to_context_args.call parser, 3
|
Loading…
Reference in New Issue