I've created a Subversion passwd management tool which
solves different problems than the already existing scripts.
With my script all passwords are generated by calling an external
tool like pwgen from Ted T'so or any other tool which prints the
generated password to stdout.
The script handles password lifetime which is a common policy in
many companies, although the achieved security gain is debatable
according to some experts. If you have such a policy being able
to automate the task helps. This is ideal for a cron job to check
for expired passwords and send the users new ones.
I'm thankful for any name suggestions other then svnpwdadmin
which also does not clash with svnpasswd(.pl).
Actually I might change the script's name to be more generic
if enough users like the password lifetime and auto-generation
idea and want the script to for example also manage htpasswd files.
In case this script is useful to someone else it might be an
option to include it in Subversion's contrib directory.
here it is:
#!/usr/bin/env ruby
$COPYRIGHT = <<EOC
Copyright (c) 2006, Tuncer Ayaz <tuncer.ayaz@[cycos|gmail].com>
Copyright (c) 2006, Cycos AG
Idea and original implementation of FileHash class plus Ruby help by:
Karl Czisch <prog.kcz@gmail com>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above
copyright notice, this list of conditions and the following
disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials
provided with the distribution.
* Neither the name of the Cycos AG nor the names of
its contributors may be used to endorse or promote products
derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
EOC
require 'optparse'
require 'ostruct'
require 'net/smtp'
$Version = "0.9"
# Default values for command line options
$settings = OpenStruct.new
$settings.verbose = false
$settings.max_days = 90
$settings.check_all = false
$settings.user = nil
$settings.user_email = nil
$settings.remove_user = nil
$settings.db_file = nil
$settings.passwd_file = nil
$settings.pwgen = nil
$settings.hostname = nil
$settings.mx = nil
$settings.from = nil
ARGV.options do |o|
o.program_name = "svnpwdadmin"
o.banner = "Usage: #{o.program_name} [options] passwd-file"
o.release = $Version
o.separator("Update Subversion passwd file")
o.separator("")
o.on("-m", "--max-days [DAYS]", Integer, \
"passwords expiry days [default: #{$settings.max_days}]") { |m|
$settings.max_days = m
}
o.on("-u", "--user [USER]", String, "update user's password") { |u|
$settings.user = u
}
o.on("-e", "--user-email [EMAIL ADDRESS]", String, \
"update user's password") { |e|
$settings.user_email = e
}
o.on("-r", "--remove-user [USER]", String, "remove user") { |r|
$settings.remove_user = r
}
o.on("-c", "--check-all", "check and update passwords for all users") {
$settings.check_all = true
}
o.on("-d", "--db-file [PATH]", String, "input database") { |d|
$settings.db_file = d
}
o.on("-s", "--smtp-server [HOSTNAME/IP]", String, "SMTP server") { |s|
$settings.mx = s
}
o.on("-f", "--from-email [EMAIL ADDRESS]", String, "FROM: address") { |f|
$settings.from = f
}
o.on("--hostname [HOSTNAME]", String, "HOSTNAME for email content") { |h|
$settings.hostname = h
}
o.on("-p", "--pwgen [COMMAND]", String,\
"command to execute for password generation") { |p|
$settings.pwgen = p
}
o.on("-v", "--verbose", "explain what is being done") {
$settings.verbose = true
}
o.separator("")
o.on_tail("-h", "--help", "Show help") {
puts o
puts <<EOX
db-file format:
user_id;user_email_address;password_creation_timestamp
You can either add each new user with a single call to #{o.program_name}
or prepare a db-file with missing timestamps and do it with one
#{o.program_name} --check-all call to.
Such a db-file with only two users would look like this before:
userone;userone@example.com
usertwo;usertwo@example.com
and so after running #{o.program_name}:
userone;userone@example.com;1156618141
userone;usertwo@example.com;1156618142
Update password for one user:
$ #{o.program_name} -u uid -e uid@srv1.com -d db -s mx -f admin@mx1.com passwd
Check all existing user's passwords for expiry date and update expired ones:
$ #{o.program_name} -c -d passwd.dbfile -s mx -f admin@example.com passwd-file
This is ideal for a cron job to check for expired passwords and automatically
update and notify the affected user(s).
Add the new user u3 with email-address usr3@srv1.com:
$ #{o.program_name} -u u3 -e usr3@srv1.com -d db -s mx -f admin@mx1.com passwd
EOX
exit
}
o.on_tail("--version", "Show version") {
puts <<EOV
#{o.program_name} #{o.release}
pyright (c) 2006, Tuncer Ayaz <tuncer.ayaz@[cycos|gmail].com>
Copyright (c) 2006, Cycos AG
Idea and original implementation of FileHash class plus Ruby help by:
Karl Czisch <prog.kcz@gmail com>
EOV
exit
}
end
def putsv(msg)
puts msg if $settings.verbose
end
class FileHash
attr_writer :key
attr_accessor :data
def initialize(filename, separator, ignore_pattern, *names)
@filename, @separator = filename, separator
@ignore_pattern = ignore_pattern
@data = Hash.new
@names = Hash.new
count = 0
# save positions relating to column titles
if names
names.each { |n|
@names[n] = count
count += 1
}
end
@key = nil
load()
end
def load
File.open(@filename, File::RDONLY) do |f|
f.each_line do |line|
# skip lines matching ignore pattern
next if @ignore_pattern and line =~ @ignore_pattern
key, value = line.chomp.split(@separator.strip,2\
).collect { |x| x.strip }
# split once again if more than 2 columns
value = value.split(@separator.strip, @names.length\
).collect { |x| x.strip } unless @names.empty?
# save entry
@data[key] = value unless key.empty?
end
end
end
def save
File.open(@filename, File::WRONLY|File::TRUNC) do |f|
@data.each do |key, value|
value = value.join(@separator) if value.kind_of?(Array)
f.puts "#{key}#{@separator}#{value}"
end
end
end
def delete(user)
@data.delete user
end
def method_missing(mid, *args)
if @names and @key
mname = mid.id2name
# is this an assignment?
if mname =~ /=$/
assign = true
mname.chop!
end
# based on column position assign/retrieve value
pos = @names[mname]
if pos
if assign and args.length == 1
@data[@key] = Array.new unless @data.has_key?(@key)
return @data[@key][pos] = args[0]
else
return @data.has_key?(@key) ? @data[@key][pos] : nil
end
end
end
raise NoMethodError, "undefined method `#{mname}' for #{self}",\
caller(1)
end
end
def mail_pwd(mx,from,to,uid,pwd)
putsv "Mailing new password for user<#{uid}> to <#{to}>"
Net::SMTP.start(mx) do |smtp|
if $settings.hostname
hostname = $settings.hostname
else
hostname = `hostname`.chomp
end
msg = "Subject: new SVN password\n"
msg << "To: #{to}\n"
msg << "Your new password to be used for the Subversion account"
msg << " #{uid}@#{hostname} is:\n\n"
msg << pwd
smtp.send_message msg, from, to
end
end
def gen_pwd
if $settings.pwgen
`#{$settings.pwgen}`
else
`pwgen --capitalize --numerals --ambiguous 15 1`
end
end
def load_dbfile
FileHash.new($settings.db_file, ';', nil, 'smtp', 'timestamp')
end
def load_passwd_file
FileHash.new($settings.passwd_file, " = ", /\[users\]/, 'passwd')
end
def remove_user
putsv "Going to remove user <" + $settings.remove_user + ">"
# remove from db-file
dbfile = load_dbfile
dbfile.delete $settings.remove_user
# remove from passwd file
passwd = load_passwd_file
passwd.delete $settings.remove_user
# save changes
passwd.save
dbfile.save
end
def update_one_user
putsv "Going to update password for <" + $settings.user \
+ "> in <" + $settings.passwd_file + ">"
# load dbfile and passwd file
dbfile = load_dbfile
dbfile.key = $settings.user
passwd = load_passwd_file
passwd.key = $settings.user
# change entries
newpwd = gen_pwd
passwd.passwd = newpwd
dbfile.smtp = $settings.user_email if $settings.user_email
dbfile.timestamp = Time.now.to_i.to_s
# save changes
passwd.save
dbfile.save
# mail password to user
mail_pwd $settings.mx, $settings.from, dbfile.smtp, $settings.user, newpwd
end
def pwd_expired(max_days, timestamp)
now = Time.now.to_i
diff_sec = now.to_i - timestamp.to_i
diff_day = diff_sec / 3600 / 24
putsv "now(#{now}) - timestamp(#{timestamp.to_i}) = #{diff_sec}s = " \
+ "#{diff_day} days"
return diff_day >= max_days
end
def check_and_update_all_users
putsv "Going to check and update passwords for all users in <" \
+ $settings.passwd_file + ">"
# load dbfile and passwd file
dbfile = load_dbfile
passwd = load_passwd_file
# check each entry for missing or expired timestamp
# and where needed create new password
dbfile.data.each_key do |key|
dbfile.key = key
smtp, timestamp = dbfile.smtp, dbfile.timestamp
if timestamp == nil or pwd_expired($settings.max_days,timestamp)
passwd.key = key
putsv "Generating new password for user <#{key}>"
newpwd = gen_pwd
dbfile.timestamp = Time.now.to_i.to_s
passwd.passwd = newpwd
mail_pwd $settings.mx, $settings.from, smtp, key, newpwd
else
putsv "Password still valid for user <#{key}>, nothing to do"
end
end
# save changes
dbfile.save
passwd.save
end
def exitl(msg, retcode)
puts msg
exit retcode
end
#
# main routine block
#
ARGV.parse!
$settings.passwd_file = ARGV[0]
# check for mandatory params/conditions
exitl("missing passwd-file param\n#{ARGV.options.to_s}", 1)\
if $settings.passwd_file == nil
exitl("missing db-file param\n#{ARGV.options.to_s}",1)\
if $settings.db_file == nil
exitl("missing SMTP server param\n#{ARGV.options.to_s}",1)\
if $settings.remove_user == nil and $settings.mx == nil
exitl("missing FROM: address param\n#{ARGV.options.to_s}",1)\
if $settings.remove_user == nil and $settings.from == nil
exitl("passwd-file <#{$settings.passwd_file}> does not exist", 1)\
unless File.exist?($settings.passwd_file)
exitl("db-file <#{$settings.db_file}> does not exist", 1)\
unless File.exist?($settings.db_file)
# log settings
putsv "max-days: #{$settings.max_days.to_s}"
putsv "passwd-file: #{$settings.passwd_file}"
putsv "check-all: #{$settings.check_all}"
putsv "user: #{$settings.user}"
putsv "user-email: #{$settings.user_email}"
putsv "remove-user: #{$settings.remove_user}"
putsv "db-file: #{$settings.db_file}"
putsv "mx: #{$settings.mx}"
putsv "from: #{$settings.from}"
putsv "hostname: #{$settings.hostname}"
putsv "pwgen: #{$settings.pwgen}"
putsv "verbose: #{$settings.verbose}"
putsv ""
# either update a single user only or check & update all users
if $settings.remove_user
remove_user
elsif $settings.user
update_one_user
elsif $settings.check_all
check_and_update_all_users
end
---------------------------------------------------------------------
To unsubscribe, e-mail: users-unsubscribe@subversion.tigris.org
For additional commands, e-mail: users-help@subversion.tigris.org
Received on Tue Aug 29 19:29:50 2006