Compare commits

...

10 commits

28 changed files with 389 additions and 67 deletions

15
apparmor.d/boilerplate Normal file
View file

@ -0,0 +1,15 @@
# This file is related to the `Baguette` project (authd, dnsmanagerd…).
# It is a way to avoid the long and complex default configuration files provided
# by the system. Allowed operations can be known in a matter of seconds.
# Accept basically all available libraries.
@{BASE_LIBS}=/{,usr/,usr/local/}lib{,32,64}/*.so* /usr/lib/x86_64*/*.so* /etc/ld*
# Enable reading files from different places required by the libraries I use,
# which may be the Crystal standard library itself.
@{BASE_RO}=/dev/{,u}random /dev/pts/* /proc/** /etc/localtime /usr/share/zoneinfo/**
@{BASE_RW}=/dev/{null,zero,full}
# Found in other profiles:
# Recent glibc uses /dev/full in preference to /dev/null for programs
# that don't have open fds at exec().

27
apparmor.d/dnsmanager Normal file
View file

@ -0,0 +1,27 @@
# Main configuration directory.
@{MAIN_CONF_DIR}=@{HOME}/.config/baguette
# Main configuration files.
@{AUTHD_CONFIG}=@{MAIN_CONF_DIR}/auth.yml
@{DNSMANAGERD_CONFIG}=@{MAIN_CONF_DIR}/dnsmanager.yml
# Databases.
@{AUTHD_DB_PATH}=@{HOME}/tmp/db-authd
@{DNSMANAGERD_DB_PATH}=@{HOME}/tmp/db-dnsmanagerd
# Key to encrypt passwords.
@{AUTHD_DB_KEY}=@{MAIN_CONF_DIR}/authd-db-key
# DNS templates (read-only entries).
@{DNSMANAGERD_TEMPLATES}=@{MAIN_CONF_DIR}/templates/*.json
# Logs.
@{LOGS_DIR}=@{HOME}/tmp/logs
@{AUTHD_LOGS}=@{LOGS_DIR}/auth
@{DNSMANAGERD_LOGS}=@{LOGS_DIR}/dnsmanager
# Mailer for authd.
@{MAILER}=/{usr,usr/local}/bin/mailer
# IPC-related directory (see libipc(7)).
@{LIBIPC_DIR}=/tmp/.libipc-run/

View file

@ -0,0 +1,33 @@
abi <abi/3.0>,
include <tunables/global>
include <dnsmanager>
include <boilerplate>
/usr/local/bin/authd flags=(enforce) {
# See the file `boilerplate`.
@{BASE_LIBS} mr,
@{BASE_RO} r,
@{BASE_RW} rw,
# Allow IPC-related unix sockets.
owner @{LIBIPC_DIR}/* rwk,
# Enable all unix socket operations. TODO: restrict this even further?
unix,
# Deny networking (udp and tcp).
deny network tcp,
deny network udp,
# Enable to read the configuration (and the database key).
owner @{AUTHD_CONFIG} r,
owner @{AUTHD_DB_KEY} r,
# Database and logs.
owner @{AUTHD_DB_PATH}/** rwkl,
owner @{AUTHD_LOGS} w,
# Enable authd to send mails.
@{MAILER} ux,
}

View file

@ -4,6 +4,9 @@
login: admin
pass: secret
# Path to the log file. By default, everything is just printed on screen.
log_file: /var/log/baguette/auth.log
# In case you have a special `mailer` application.
# Read the manual to understand how it is invoked.
mailer_exe: /usr/local/bin/my-special-mailer
@ -40,16 +43,10 @@ recovery_template: netlibre-email-recovery
#verbosity: 4
# By default, print everything except KEEPALIVE messages.
#messages_to_mask: [KEEPALIVE]
#print_password_recovery_parameters: false
# IPC-related variables. By default, only print errors and exceptions.
#print_ipc_timer: false
#print_ipc_connection: false
#print_ipc_disconnection: false
#print_ipc_extra_socket: false
#print_ipc_message_received: false
#print_ipc_message_sent: false
#print_ipc_switch: false
#print_ipc_error: true
#print_ipc_exception: true
#print_keepalive: false
# IPC-related messages. By default, only print errors and exceptions.
#ipc_messages_to_show: [ERROR, EXCEPTION]

View file

@ -33,6 +33,8 @@ can be:
.Bl -tag -width " print functions" -compact
.It Li bootstrap
Add the first user (an admin).
.It Li exit
Kill the service.
.It Li migration-script
Adding a batch of users from old code base.
.It Li user
@ -113,7 +115,13 @@ Add the first user (will automatically be an admin).
.br
.Nm authctl
.Ar migration-script No
.br
Kill the service.
.br
.Nm authctl
.Ar exit No
.br
Operations on users.
.br
@ -259,13 +267,13 @@ to handle users (authentication, authorization, preferences and profile)
.It
.Xr mailer 1
a simple executable to send mails based on templates
.
.It
.Xr dodb 7
a document database library used in
.Xr authd .
.El
The Document-oriented DataBase (DoDB) library used in
.Xr authd .
.br
.Lk https://git.baguette.netlib.re/Baguette/dodb.cr dodb
.Sh Limitations
WARNING:
.Xr authctl

View file

@ -58,8 +58,8 @@ to encrypt users's password is read from the file
.Pa /var/authd/secret-password .
.br
Finally, indexes are recreated, which is related to the
.Xr dodb 7
document database, see the related manual page to learn more.
.Xr DoDB
document database, see the related documentation to learn more.
.Sh Configuration file variables
The following presents the complete list of configuration file variables.
@ -70,50 +70,72 @@ related variables:
.
.Bl -tag -width " print functions" -compact
.It Li ipc_timer
Int32, 30_000 (30 seconds)
.
Int32, 30_000
.br
The IPC timer wakes the process by default every 30 seconds.
There is no much point changing this value since nothing is executed periodically anyway, at least for now.
.It Li verbosity
Int32, 4 ([0-4],
Int32, 4
.br
[0-4],
.Dq 0
being quiet and
.Dq 4
meaning printing debug values)
.
.It Li print functions
Print functions enable to select messages to print, for example by printing a message each time a message is received while ignoring keepalive messages.
meaning printing debug values.
.Bl -tag -width " print_ipc_message_received" -compact
.It Li print_ipc_timer
Bool, false
.It Li print_ipc_connection
Bool, false
.It Li print_ipc_disconnection
Bool, false
.It Li print_ipc_extra_socket
Bool, false
.It Li print_ipc_message_received
Bool, false
.It Li print_ipc_message_sent
Bool, false
.It Li print_ipc_switch
Bool, false
.It Li print_ipc_error
Bool, true
.It Li print_ipc_exception
Bool, true
.It Li print_keepalive
Bool, false
.It Li ipc_messages_to_show
Array of
.Vt Baguette::Configuration::IPC::MESSAGE ,
[ERROR, EXCEPTION]
Types of IPC messages to print, for example all connections.
This is mainly for debug since it is very low-level.
High-level messages are more relevant to log.
By default, errors and exceptions are logged.
See
.Xr Baguette-crystal-base ,
which includes the
.Vt Baguette::Configuration::IPC::MESSAGE
definition.
This type has an alias in
.Xr authd :
.Vt IPCMESSAGE .
.It Li service_name
String,
.Dq auth
.br
.Xr libipc 7
unix socket name.
.El
.
.El
Specific
.Xr authd
variables:
.Bl -tag -width " print_password_recovery_parameters" -compact
.It Li service_name
String,
.Dq auth
.Bl -tag -width " print_password_recovery" -compact
.It Li log_file
String?,
.Em none
.br
Path to the log file.
.It Li messages_to_mask
Array of
.Vt AuthD::MESSAGE ,
.Em [ KEEPALIVE ]
.br
List of high-level
.Em authd
messages to mask in the logs.
.br
The type
.Vt AuthD::MESSAGE
has an alias:
.Vt AUTHMESSAGE .
.It Li recreate_indexes
Bool, false
.It Li storage_directory
@ -250,13 +272,27 @@ to handle users (authentication, authorization, preferences and profile)
.It
.Xr mailer 1
a simple executable to send mails based on templates
.
.It
.Xr dodb 7
a document database library used in
.Xr authd .
.El
The Document-oriented DataBase (DoDB) library used in
.Xr authd .
.br
.Lk https://git.baguette.netlib.re/Baguette/dodb.cr dodb
The online service
.Dq netlib.re
is the first one to use the
.Xr authd
daemon.
.br
.Lk https://www.netlib.re netlib.re
The logging and configuration library for the whole
.Dq baguette
project.
.br
.Lk https://git.baguette.netlib.re/Baguette/baguette-crystal-base baguette-crystal-base
.Sh Limitations
WARNING:
.Xr authd

View file

@ -99,6 +99,10 @@ module AuthD
], read
end
def exit
send_now Request::Exit.new
end
def validate_user(login : String, activation_key : String)
send_now Request::ValidateUser.new login, activation_key
parse_message [

View file

@ -79,6 +79,15 @@ parser = OptionParser.new do |parser|
unrecognized_args_to_context_args.call parser, 2
end
parser.on "exit", "Kill the service." do
parser.banner = "Usage: exit"
Baguette::Log.info "Kill the service."
Context.command = "exit"
opt_authd_login.call parser
opt_help.call parser
unrecognized_args_to_context_args.call parser, 0
end
parser.on "migration-script", "Add a batch of users from old code base." do
parser.banner = "usage: migration-script user-db.txt"
Baguette::Log.info "Add a batch of users."

View file

@ -66,6 +66,7 @@ class Actions
@the_call["user-migrate"] = ->user_migrate
@the_call["migration-script"] = ->migration_script
@the_call["user-mod"] = ->user_mod
@the_call["exit"] = ->kill_service
@the_call["permission-set"] = ->permission_set
@the_call["permission-check"] = ->permission_check
@ -192,6 +193,14 @@ class Actions
puts "error: #{e.message}"
end
def kill_service
puts "Kill the service."
authd.exit
rescue e : AuthD::Exception
puts "error: #{e.message}"
end
def user_change_password
args = Context.args.not_nil!
login = args[0]

View file

@ -7,6 +7,10 @@ class IPC::JSON
def handle(service : AuthD::Service, fd : Int32)
raise "unimplemented"
end
def to_s(io : IO)
io << self.class.name.sub /[^:]+::[^:]+::/, ""
end
end
module AuthD

View file

@ -9,6 +9,11 @@ class AuthD::Request
def initialize(@login, @password, @admin, @email, @profile)
end
def to_s(io : IO)
super io
io << " (login: #{@login}, email: #{@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?
@ -56,6 +61,11 @@ class AuthD::Request
def initialize(@login, @password, @email, @profile = nil)
end
def to_s(io : IO)
super io
io << " (login: #{@login}, email: #{email})"
end
def handle(authd : AuthD::Service, fd : Int32)
# Check if there already is a registered user.
if authd.users.to_a.size > 0
@ -90,6 +100,11 @@ class AuthD::Request
def initialize(@token)
end
def to_s(io : IO)
super io
io << " (token size: #{@token.size})"
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user_full? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?
@ -105,4 +120,23 @@ class AuthD::Request
end
end
AuthD.requests << DecodeToken
IPC::JSON.message Exit, 248 do
def initialize
end
def to_s(io : IO)
super io
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)
Baguette::Log.warning "exit requested, bye"
exit 0
end
end
AuthD.requests << Exit
end

View file

@ -6,6 +6,11 @@ class AuthD::Request
def initialize(@user = nil)
end
def to_s(io : IO)
super io
io << " (user: #{@user})"
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user_full? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?

View file

@ -22,6 +22,11 @@ class AuthD::Request
def initialize(@login, @password)
end
def to_s(io : IO)
super io
io << " (login: #{@login})"
end
def handle(authd : AuthD::Service, fd : Int32)
begin
user = authd.users_per_login.get @login

View file

@ -10,6 +10,11 @@ class AuthD::Request
def initialize(@login, @password_hash_brkn, @admin = false, @email = nil, @profile = nil)
end
def to_s(io : IO)
super io
io << " (login: #{@login}, admin: #{@admin})"
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user_full? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?

View file

@ -8,6 +8,11 @@ class AuthD::Request
def initialize(@user, @admin, @password, @email)
end
def to_s(io : IO)
super io
io << " (user: #{@user}, admin: #{@admin})"
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user_full? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?

View file

@ -6,6 +6,11 @@ class AuthD::Request
def initialize(@login = nil, @email = nil)
end
def to_s(io : IO)
super io
io << " (login: #{@login})"
end
def handle(authd : AuthD::Service, fd : Int32)
if @login.nil? && @email.nil?
return Response::ErrorUserNotFound.new
@ -54,6 +59,11 @@ class AuthD::Request
def initialize(@user, @password_renew_key, @new_password)
end
def to_s(io : IO)
super io
io << " (user: #{@user}, password_renew_key: #{@password_renew_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.

View file

@ -7,6 +7,11 @@ class AuthD::Request
def initialize(@user, @service, @resource)
end
def to_s(io : IO)
super io
io << " (user: #{@user}, service: #{@service}, resource: #{@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?
@ -42,6 +47,11 @@ class AuthD::Request
def initialize(@user, @service, @resource, @permission)
end
def to_s(io : IO)
super io
io << " (user: #{@user}, service: #{@service}, resource: #{@resource}, permission: #{@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?

View file

@ -8,6 +8,11 @@ class AuthD::Request
def initialize(@new_profile_entries, @user = nil)
end
def to_s(io : IO)
super io
io << " (user: #{@user})"
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user_full? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?

View file

@ -8,6 +8,11 @@ class AuthD::Request
def initialize(@login, @password, @email, @profile)
end
def to_s(io : IO)
super io
io << " (login: #{@login})"
end
def handle(authd : AuthD::Service, fd : Int32)
unless authd.configuration.registrations
return Response::ErrorRegistrationsClosed.new

View file

@ -9,6 +9,11 @@ class AuthD::Request
def initialize(@regex = nil, @offset = 0)
end
def to_s(io : IO)
super io
io << " (regex: #{@regex}, offset: #{@offset})"
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user_full? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?

View file

@ -6,6 +6,11 @@ class AuthD::Request
def initialize(@user, @activation_key)
end
def to_s(io : IO)
super io
io << " (user: #{@user}, activation_key: #{@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.
@ -40,6 +45,11 @@ class AuthD::Request
def initialize(@user)
end
def to_s(io : IO)
super io
io << " (user: #{@user})"
end
def handle(authd : AuthD::Service, fd : Int32)
logged_user = authd.get_logged_user? fd
return Response::ErrorMustBeAuthenticated.new if logged_user.nil?

View file

@ -4,6 +4,12 @@ class AuthD::Response
property email : String? = nil
def initialize(@user, @email)
end
def to_s(io : IO)
super io
io << " (user: #{@user}, email: #{@email})"
end
end
AuthD.responses << Contacts
end

View file

@ -9,6 +9,11 @@ class AuthD::Response
property email : String
def initialize(@email)
end
def to_s(io : IO)
super io
io << " (email: #{@email})"
end
end
AuthD.responses << NewEmailAddressValidated
end

View file

@ -3,6 +3,11 @@ class AuthD::Response
property reason : String? = nil
def initialize(@reason)
end
def to_s(io : IO)
super io
io << " (reason: #{@reason})"
end
end
AuthD.responses << Error
@ -70,6 +75,11 @@ class AuthD::Response
property read_only_keys : Array(String)
def initialize(@read_only_keys)
end
def to_s(io : IO)
super io
io << " (read_only_keys: #{@read_only_keys.join(",")})"
end
end
AuthD.responses << ErrorReadOnlyProfileKeys

View file

@ -6,6 +6,11 @@ class AuthD::Response
property pending_email : String? = nil
def initialize(@token, @uid, @current_email, @pending_email)
end
def to_s(io : IO)
super io
io << " (uid: #{@uid})"
end
end
AuthD.responses << Login
end

View file

@ -6,6 +6,11 @@ class AuthD::Response
property permission : ::AuthD::User::PermissionLevel
def initialize(@service, @resource, @user, @permission)
end
def to_s(io : IO)
super io
io << " (user: #{@user}, service: #{@service}, resource: #{@resource}, permission: #{@permission})"
end
end
AuthD.responses << PermissionCheck
@ -16,6 +21,11 @@ class AuthD::Response
property permission : ::AuthD::User::PermissionLevel
def initialize(@user, @service, @resource, @permission)
end
def to_s(io : IO)
super io
io << " (user: #{@user}, service: #{@service}, resource: #{@resource}, permission: #{@permission})"
end
end
AuthD.responses << PermissionSet
end

View file

@ -3,6 +3,11 @@ class AuthD::Response
property user : ::AuthD::User::Public
def initialize(@user)
end
def to_s(io : IO)
super io
io << " (user public data: #{@user})"
end
end
AuthD.responses << User
@ -10,6 +15,11 @@ class AuthD::Response
property user : ::AuthD::User::Public
def initialize(@user)
end
def to_s(io : IO)
super io
io << " (user public data: #{@user})"
end
end
AuthD.responses << UserAdded
@ -17,6 +27,11 @@ class AuthD::Response
property uid : UInt32
def initialize(@uid)
end
def to_s(io : IO)
super io
io << " (uid: #{@uid})"
end
end
AuthD.responses << UserEdited
@ -24,6 +39,11 @@ class AuthD::Response
property user : ::AuthD::User::Public
def initialize(@user)
end
def to_s(io : IO)
super io
io << " (user public data: #{@user})"
end
end
AuthD.responses << UserValidated
@ -31,6 +51,11 @@ class AuthD::Response
property users : Array(::AuthD::User::Public)
def initialize(@users)
end
def to_s(io : IO)
super io
io << " (users public data: #{(@users.map &.uid).join ","})"
end
end
AuthD.responses << UsersList
@ -38,6 +63,11 @@ class AuthD::Response
property users : Array(::AuthD::User::Public)
def initialize(@users)
end
def to_s(io : IO)
super io
io << " (users public data: #{(@users.map &.uid).join ","})"
end
end
AuthD.responses << MatchingUsers
@ -45,6 +75,11 @@ class AuthD::Response
property uid : UInt32
def initialize(@uid)
end
def to_s(io : IO)
super io
io << " (uid: #{@uid})"
end
end
AuthD.responses << UserDeleted
end

View file

@ -29,7 +29,7 @@ class AuthD::Service < IPC
property configuration : Baguette::Configuration::Auth
# DB and its indexes.
property users : DODB::Storage::Cached(User)
property users : DODB::Storage::Common(User)
property users_per_uid : DODB::Trigger::IndexCached(User)
property users_per_login : DODB::Trigger::IndexCached(User)
property users_per_email : DODB::Trigger::IndexCached(User)
@ -42,7 +42,7 @@ class AuthD::Service < IPC
def initialize(@configuration)
super()
@users = DODB::Storage::Cached(User).new @configuration.storage_directory
@users = DODB::Storage::Common(User).new @configuration.storage_directory, 5000
@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|
@ -111,7 +111,7 @@ class AuthD::Service < IPC
File.write @last_uid_file, uid.to_s
end
def get_logged_user?(fd : Int32)
def get_logged_user?(fd : Int32) : AuthD::User::Public?
@logged_users[fd]?
end
@ -123,6 +123,17 @@ class AuthD::Service < IPC
end
end
# `log_user_info` provides a string composed from either the user
# id in case the user was authenticated or the file descriptor of
# the connection.
def log_user_info(fd : Int32) : String
if user = get_logged_user? fd
"userid #{user.uid}"
else
"fd #{"%4d" % fd}"
end
end
def user?(uid_or_login : UserID)
if uid_or_login.is_a? UInt32
@users_per_uid.get? uid_or_login.to_s
@ -144,20 +155,21 @@ class AuthD::Service < IPC
end
request_name = request.class.name.sub /^AuthD::Request::/, ""
connection_info_str = log_user_info event.fd
response = begin
request.handle self, event.fd
rescue e : UserNotFound
Baguette::Log.error "(fd #{ "%4d" % event.fd}) #{request_name} user not found"
Baguette::Log.error "(#{connection_info_str}) #{request} user not found"
AuthD::Response::Error.new "authorization error"
rescue e : AuthenticationInfoLacking
Baguette::Log.error "(fd #{ "%4d" % event.fd}) #{request_name} lacking authentication info"
Baguette::Log.error "(#{connection_info_str}) #{request} lacking authentication info"
AuthD::Response::Error.new "authorization error"
rescue e : AdminAuthorizationException
Baguette::Log.error "(fd #{ "%4d" % event.fd}) #{request_name} admin authentication failed"
Baguette::Log.error "(#{connection_info_str}) #{request} admin authentication failed"
AuthD::Response::Error.new "authorization error"
rescue e
Baguette::Log.error "(fd #{ "%4d" % event.fd}) #{request_name} generic error #{e}"
Baguette::Log.error "(#{connection_info_str}) #{request} generic error #{e}"
AuthD::Response::Error.new "unknown error"
end
@ -169,13 +181,11 @@ class AuthD::Service < IPC
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})"
Baguette::Log.warning "(#{connection_info_str}) (#{duration}) #{request} >> #{response}"
else
if request_name != "KeepAlive" || should_display? AUTHMESSAGE::KEEPALIVE
Baguette::Log.debug "fd #{ "%4d" % event.fd} (#{duration}) #{request_name} >> #{response_name}"
Baguette::Log.debug "(#{connection_info_str}) (#{duration}) #{request} >> #{response}"
end
end
end