[svn.haxx.se] · SVN Dev · SVN Users · SVN Org · TSVN Dev · TSVN Users · Subclipse Dev · Subclipse Users · this month's index

Re: svnpwdadmin

From: Tuncer Ayaz <tuncer.ayaz_at_gmail.com>
Date: 2006-09-01 12:07:26 CEST

On 8/30/06, Tuncer Ayaz <tuncer.ayaz@gmail.com> wrote:
> This was originally posted to users@subversion.tigris.org, hope
> I'm not violating the netiquette with re-posting to the dev@ list.
>
> I've created a Subversion passwd management tool which
> solves different problems than the already existing scripts.
>
> All passwords are generated by calling an external tool like
> pwgen from Ted Ts'o 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 than svnpwdadmin
> which also do 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.
>
>
> #!/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
>

as it is open source I've put it in a Google code repo
http://code.google.com/p/svnpwdadmin/
the version on Google code fixed the issue that I forgot
to add the [users] header to the passwd file.

I named the repo the same as the script because it
is a trivial tool.

actually, once SASL is included in a stable release
the script may not be of much use but for now we need
it at Cycos and share it with the world just in case
someone has the same problems to solve with svnserve
usage.

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@subversion.tigris.org
For additional commands, e-mail: dev-help@subversion.tigris.org
Received on Fri Sep 1 12:07:58 2006

This is an archived mail posted to the Subversion Dev mailing list.

This site is subject to the Apache Privacy Policy and the Apache Public Forum Archive Policy.