Overview | Index by: file name |
procedure name |
procedure call |
annotation
auth.tcl
(annotations | original source)
#
# Copyright (C) 2010 Alexandros Stergiakis <alsterg@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#//#
# User and password management.
#
# Including procedures to:
# * Change & validate user paswords
# * Add & delete a user accounts
# * Login a user
#
#//#
namespace eval auth {
namespace import ::helper::sgets ::helper::sputs ::helper::sflush ::helper::sread
namespace export login verify passwd adduser deluser pass_is_ok name_is_ok crypt lock unlock makeroot user_exists
# Verify that supplied username and password is correct.
#
# @param user The username string
# @param pass The password string
# @return 1 for successful verification, 0 otherwise and log reason
# @error
proc verify {user pass} {
Global ETC_DIR
if {! [name_is_ok $user] || ! [pass_is_ok $pass]} {
log::Debug "Illegal username or password string"
return 0
}
# Note: We don't use sed because encrypted password contains special chars
# that cause problems to sed, and makes using sed error-prone.
set fd 0
if {[catch {
set fd [open [file join $ETC_DIR shadow] "r"]
} errMsg errStack]} {
log::Critical -error -stack $errStack "Failed to open shadow file for reading"
}
while {[gets $fd line] >= 0} {
set temp [split $line ":"]
if {$user eq [lindex $temp 0]} {
set storedpass [lindex $temp 1]
set salt [lindex [split $storedpass \$] 2]
set cryptpass [crypt $pass $salt]
if {$storedpass eq $cryptpass} {
close $fd
return 1
} else {
break
}
}
}
close $fd
# If we reach here, it means that username was not listed in 'shadow'
log::Info "Password verification failed"
return 0
}
# Verify that username string is syntactically correct (length, character
# composition, etc). It logs if something is wrong with the input.
#
# @param user The username string
# @return 1 if username is ok, otherwise 0 and log reason
proc name_is_ok {user} {
Global USERNAME_MAXLEN
if {[lempty $user]} {
log::Debug "Empty username"
return 0
}
if {[string length $user] > $USERNAME_MAXLEN} {
log::Debug "Username too long"
return 0
}
if {[regexp [format {^%s+$} {[_a-z0-9]}] $user] == 0} { ;# (@magic-number)
log::Debug "Usersame contains invalid characters"
return 0
}
return 1
}
# Verify that password string is syntactically correct (length, character
# composition, etc). It logs if something is wrong with the input.
# This procedure also makes sure that the password string does not
# start with the magic prefix $1$, which signifies within MikroConf
# an encrypted password.
#
# @param pass The password string
# @return 1 if username is ok, otherwise 0 and log reason
proc pass_is_ok {pass} {
Global PASSWORD_MAXLEN
if {[lempty $pass]} {
log::Debug "Empty password"
return 0
}
if {[string length $pass] > $PASSWORD_MAXLEN} {
log::Debug "Password too long"
return 0
}
if {[regexp [format {^%s+$} {[a-zA-Z0-9\.\*\?\+\[\]\(\)\^\$\|\-\\~`@#%&_=+":;\}\{<>,!]}] $pass] == 0} {
log::Debug "Password contains invalid characters"
return 0
}
if {[string match {$1$*} $pass]} {
log::Debug "Password contains illegal prefix"
}
return 1
}
# Ask user for login credentials. Username can be provided before hand.
# Log failures, and accept up to MAX_LOGINS failed attempts before
# returning failure.
#
# @param user The username string to ask password for, if empty then \
username is also asked from the terminal
# @return 1 on successful login, 0 otherwise and reason is logged
proc login {{user {}}} {
Global MAX_LOGINS
if {! [lempty $user]} {
if {[name_is_ok $user]} {
set username $user
} else { ;# error is logged
log::Info -error "Illegal username string"
}
}
set tries 0
for {set tries 0} {$tries < $MAX_LOGINS} {incr tries} {
if {[lempty $user]} {
# Print the username prompt
sputs -nonewline "Username: "
sflush stdout
# Read the username
set username [sgets -noecho stdin]
}
# Print the password prompt
sputs -nonewline "Password: "
sflush stdout
# Read the password
set password [sgets -noecho stdin]
# Verify username/password
if {[catch {
verify $username $password
} result errStack]} { ;# error is logged
return 0
}
if {$result} {
log::Info "$username logged in at [clock format [clock seconds]]"
return 1
}
}
# Max number of tries reached
log::Warning "Login failed for user \"$username\" after $MAX_LOGINS login attempts"
# Emitted after a user fails to log-in consecutively for MAX_LOGINS number of times.
# Useful to lock the user account after some failed logins.
#
# @param The username.
# @param The number of failed logins in this session.
event generate AUTHENTICATION MAX_LOGIN_ATTEMPTS [list $username $MAX_LOGINS]
return 0
}
# Compute the MD5 checksum of a string. The resultant form is appropriate for the shadow
# file: $1$8ch_salt$24ch_encrypted_pass
#
# @param pass The password cleartext string
# @param salt A string to use as a salt as per 'cryptpw'. If not specified 'cryptpw' will select one randomly.
# @return The encrypted and appropriate formated password
# @error
proc crypt {pass {salt {}}} {
if {! [pass_is_ok $pass]} {
log::Info -error "Illegal password string"
}
if {[catch {
if {[lempty $salt]} {
exec cryptpw -m md5 $pass
} else {
exec cryptpw -S $salt -m md5 $pass
}
} result errStack]} {
log::Error -error -stack $errStack "Failed to encrypt password string: " $result
}
return $result
}
# Check if a username is used in the system
#
# @param user The username string
# @return '1' if username exist, otherwise '0 and log (including error conditions)
proc user_exists {user} {
Global ETC_DIR
if {! [name_is_ok $user]} {
log::Info "Usersame \"$user\" contains invalid characters"
return 0
}
set matches [::fileutil::grep [string map "user $user" {^user:.*$}] [file join $ETC_DIR passwd]]
if {[lempty $matches]} {
return 0
}
return 1
}
# Change user password, without asking for the old one.
# No need to unlock a locked account.
#
# @assume We use shadow passwords
#
# @param user The username string.
# @param newpass The new password string (cleartext) to change to.
# @param nocrypt When set to something then password is assumed to be already encrypted.
# @return The MD5 checksum of the new password.
# @error
proc passwd {user newpass {nocrypt {}}} {
Global ETC_DIR
if {! [user_exists $user]} {
log::Warning -error "user account \"$user\" does not exist"
}
if {[lempty $nocrypt]} {
if {! [pass_is_ok $newpass]} {
log::Info -error "illegal password string"
}
set cryptpass [crypt $newpass]
} else {
set cryptpass $newpass
}
# Substitute old with new pass.
# Note: We don't use sed because encrypted password contains special chars
# that cause problems to sed, and makes using sed error-prone.
set fd 0
if {[catch {
set fd [open [file join $ETC_DIR shadow] "r"]
} errMsg errStack]} {
log::Critical -error -stack $errStack "Failed to open shadow file for reading"
}
# To store the modified file in memory
set newlines [list]
while {[gets $fd line] >= 0} {
set temp [split $line ":"]
if {$user eq [lindex $temp 0]} {
lappend newlines [join [lreplace $temp 1 1 $cryptpass] ":"]
} else {
lappend newlines $line
}
}
catch {close $fd}
# Now write the in-memory modified file back to disk
if {[catch {
set fd [open [file join $ETC_DIR shadow] "w"]
} errMsg errStack]} {
log::Critical -error -stack $errStack "Failed to open shadow file for writing: " $errMsg
}
for {set i 0} {$i < [llength $newlines]} {incr i} {
puts $fd [lindex $newlines $i]
}
catch {close $fd}
return $cryptpass ;# Success
}
# Give root privileges to a user account.
#
# @assume The requested user belongs to a group with same name as the username.
#
# @param user The username string.
# @error
proc makeroot {user} {
Global ETC_DIR
if {! [user_exists $user]} {
log::Warning -error "user account \"$user\" does not exist"
}
if {[catch {
exec sed -i -r [string map "user $user" {s/^(user:[^:]*):[^:]*:[^:]*:(.*)$/\1:0:0:\2/g}] [file join $ETC_DIR passwd]
} errMsg errStack]} {
log::Error -error -stack $errStack "Failed to give user \"$user\" root privileges: " $result
}
if {[catch {
exec sed -i -r [string map "user $user" {s/^(user:[^:]*):[^:]*:(.*)$/\1:0:\2/g}] [file join $ETC_DIR group]
} errMsg errStack]} {
log::Error -error -stack $errStack "Failed to give group \"$user\" root privileges: " $result
}
}
# Lock a user account: Cannot be used to login.
# Ignore if user account already locked.
#
# @depend on 'passwd' output.
# @param user User name.
# @error
proc lock {user} {
if {! [user_exists $user]} {
log::Warning -error "User does not exist"
}
if {[islocked $user]} { return }
if {[catch {
exec passwd -l $user
} result options]} {
if {$result ne "passwd: password for $user is already locked"} {
return -options $options $result
}
}
}
# Unlock a user account: Can be used to login.
# Ignore if user account already unlocked.
#
# @depend on 'passwd' output.
# @param user User name.
# @error
proc unlock {user} {
if {! [user_exists $user]} {
log::Warning -error "User does not exist"
}
if {! [islocked $user]} { return }
if {[catch {
exec passwd -u $user
} result options]} {
if {$result ne "passwd: password for $user is already unlocked"} {
return -options $options $result
}
}
}
# Find if a user account is locked/unlocked.
#
# @param user User name.
# @return true if locked, false if unlocked or non-existent.
proc islocked {user} {
Global ETC_DIR
set matches [::fileutil::grep [string map "user $user" {^user:!:.*$}] [file join $ETC_DIR shadow]]
if {[lempty $matches]} {
return 0
}
return 1
}
# Create a new user account
#
# @param user The username string.
# @param shell The user shell. MikroConf shell is used if not provided.
# @param pass The cleartext password string for the new user. Login is dissabled if not provided.
# @return The MD5 checksum of the new password, or nothing if no password was provided.
# @error
proc adduser {user shell {pass {}}} {
Global HOME_DIR
if {! [name_is_ok $user]} {
log::Notice -error "illegal username"
}
if {[user_exists $user]} {
log::Notice -error "A user with same name already exists"
}
if {[catch {
exec adduser -h $HOME_DIR -g $user -s $shell -D -H $user
} errMsg errStack]} {
log::Error -error -stack $errStack "Failed to add user \"$user\": " $errMsg
}
if {! [lempty $pass]} {
return [passwd $user $pass]
}
return ;# Success
}
# Delete a user account from the system
#
# @param user The username string
# @return nothing for success
# @error
proc deluser {user} {
if {! [user_exists $user]} {
log::Warning -error "Cannot remove user \"$user\". User does not exist"
}
if {[catch {
exec deluser $user
} errMsg errStack]} {
log::Error -error -stack $errStack "Failed to delete user \"$user\": " $errMsg
}
return ;# Success
}
namespace ensemble create
} ;# namespace ends
Overview | Index by: file name |
procedure name |
procedure call |
annotation
File generated 2010-03-13 at 22:28.