# # 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