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

svnpwdadmin

From: Tuncer Ayaz <tuncer.ayaz_at_gmail.com>
Date: 2006-08-30 16:36:09 CEST

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

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@subversion.tigris.org
For additional commands, e-mail: dev-help@subversion.tigris.org
Received on Wed Aug 30 17:14:13 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.