bin/mu-user-manage
#!/usr/local/ruby-current/bin/ruby
# Copyright:: Copyright (c) 2014 eGlobalTech, Inc., all rights reserved
#
# Licensed under the BSD-3 license (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License in the root of the project or at
#
# http://egt-labs.com/mu/LICENSE.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require File.realpath(File.expand_path(File.dirname(__FILE__)+"/mu-load-config.rb"))
# now we have our global config available as the read-only hash $MU_CFG
require 'mu'
require 'optimist'
require 'simple-password-gen'
require 'net/smtp'
if Etc.getpwuid(Process.uid).name != "root"
MU.log "#{$0} can only be run as root", MU::ERR
exit 1
end
$opts = Optimist::options do
banner <<-EOS
Listing users:
#{$0}
Show details for a specific user:
#{$0} <username>
Adding/modifying users:
#{$0} [-a|-r] [-e <email>] [-n '<Real Name>'] [-i|-p <password>|-g] [-o <chef_org>] [-v <chef_org>] [-m <email>] [-l <chef_user>] <username>
Deleting users:
#{$0} [-i] -d <username>
EOS
opt :delete, "Delete the user and all of their Chef and filesystem artifacts.", :require => false, :default => false, :type => :boolean
opt :skipupload, "Do not upload Chef artifacts to new users' orgs for them. The user's dotfiles will be configured to do so automatically on their first interactive login.", :require => false, :default => false, :type => :boolean
opt :monitoring_alerts_to, "Send this user's monitoring alerts to an alternate address. Set to 'none' to disable monitoring alerts to this user.", :require => false, :type => :string
opt :name, "The user's real name. Required when creating a new user.", :require => false, :type => :string
opt :email, "The user's email address. Required when creating a new user.", :require => false, :type => :string
opt :admin, "Flag the user as a Mu admin. They will be granted sudo access to the 'mu' (root's) Chef organization.", :require => false, :type => :boolean
opt :revoke_admin, "Revoke the user's status as a Mu admin. Access to the 'mu' (root) Chef organization and sudoers will be removed.", :require => false, :type => :boolean
opt :orgs, "Add the user to the named Chef organization, in addition to their default org or orgs.", :require => false, :type => :strings
opt :remove_from_orgs, "Remove the user to the named Chef organization.", :require => false, :type => :strings
opt :password, "Set a specific password for this user.", :require => false, :type => :string
opt :generate_password, "Generate and set a random password for this user.", :require => false, :type => :boolean, :default => false
opt :link_to_chef_user, "Link to an existing Chef user. Chef's naming restrictions sometimes necessitate having a different account name than everything else. Also useful for linking a pre-existing Chef user to the rest of a Mu account.", :require => false, :type => :string
opt :interactive, "Interactive prompt to set a password.", :require => false, :type => :boolean
opt :scratchpad, "Use Mu's Scratchpad to securely share user passwords instead of printing the password directly to the terminal.", :require => false, :type => :boolean, :default => true
opt :notify_user, "Share the Scratchpad link for new passwords to users via email, instead of printing to the screen.", :require => false, :type => :boolean, :default => false
opt :force_uid, "Change a user's uid, or request a specific uid for a new user. Not valid for Active Directory.", :require => false, :type => :integer, :default => -1
end
def mailUser(to, subject, message)
from = "root@#{$MU_CFG['host_name']}"
fullmsg = <<MESSAGE_END
From: Mu <#{from}>
To: #{to}
MIME-Version: 1.0
Content-type: text/html
Subject: #{subject}
<br>
<pre>#{message}</pre>
MESSAGE_END
Net::SMTP.start('localhost') do |smtp|
smtp.send_message(fullmsg, from, to)
end
end
def sendPassword(username, password, scratchpad: true, notify: true)
users = MU::Master::LDAP.findUsers
if scratchpad
scratchitem = MU::Master.storeScratchPadSecret("Mu password for user #{username}: #{password}")
url = "https://#{$MU_CFG['public_address']}/scratchpad/#{scratchitem}"
MU.log "Stored in scratchpad, public URL: #{url}", MU::NOTICE
if users[username]["mail"] and
users[username]["mail"].match(/^[A-Z0-9\._%\+\-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}$/i)
if notify
message = "Your Mu development credentials have been set.\nYou can access your new password ONCE by visiting the following url:\n\n<a href='#{url}'>#{url}</a>"
mailUser(users[username]["mail"], "Your Mu password password", message)
MU.log "Sent new password notification to #{users[username]["mail"]}."
MU.log "IMPORTANT: Be sure that your Mu Master is able to send mail (see /var/log/maillog)", MU::NOTICE
else
MU.log "Email notification disabled by default. Don't forget to share the Scratchpad URL with the user.", MU::WARN
end
else
MU.log "No email address found for #{username}, you will have to share the Scratchpad URL some other way.", MU::WARN
end
else
# XXX skip this message if we read the password interactively
MU.log "Password for #{username}: #{password}", MU::NOTICE
end
end
Dir.mkdir($MU_CFG['datadir']+"/users", 0755) if !Dir.exist?($MU_CFG['datadir']+"/users")
if $opts[:admin] and $opts[:revoke_admin]
MU.log "Cannot both add and revoke admin access", MU::ERR
Optimist::educate
end
if $opts[:password] and $opts[:generate_password]
MU.log "Cannot both specify a password and generate a password", MU::ERR
Optimist::educate
end
if $opts[:orgs] and $opts[:remove_from_orgs] and ($opts[:orgs] & $opts[:remove_from_orgs]).size > 0
MU.log "Cannot both add and remove from the same Chef org", MU::ERR
exit 1
end
$password = nil
if $opts[:generate_password]
$password = MU.generateWindowsPassword
elsif $opts[:password]
$password = $opts[:password]
elsif $opts[:interactive]
STDOUT.print "Enter password for #{$username}: "
$password = STDIN.noecho(&:gets)
puts
MU.log "Note: If this password does not comply with complexity requirements, you may get an 'Unwilling to perform' response", MU::NOTICE
end
$cur_users = MU::Master.listUsers
$opts.select { |opt| opt =~ /_given$/ }.size == 0
if !ARGV[0] or ARGV[0].empty?
bail = false
$opts.each_key { |opt|
if $opts[opt] and !opt.to_s.match(/_given$/) and !["notify_user", "scratchpad", "force_uid"].include?(opt.to_s)
MU.log "Must specify a username with the '#{opt.to_s}' option", MU::ERR
bail = true
end
}
Optimist::educate if bail
MU::Master.printUsersToTerminal
exit 0
elsif $opts.select { |opt| opt =~ /_given$/ }.size == 0
MU::Master.printUserDetails(ARGV[0])
exit 0
end
$username = ARGV[0]
[:orgs, :remove_from_orgs].each { |org_field|
bail = false
if $opts[org_field]
$opts[org_field].each { |org|
if !org.match(/^[a-z_][a-z0-9_]{0,30}$/i)
MU.log "'#{org}' is not a valid Chef org name", MU::ERR
bail = true
end
}
end
exit 1 if bail
}
[:email, :monitoring_alerts_to].each { |email_field|
bail = false
if $opts[email_field] and !$opts[email_field].match(/^[A-Z0-9\._%\+\-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}$/i) and !(email_field == :monitoring_alerts_to and $opts[email_field] == "none")
MU.log "'#{$opts[email_field]}' is not a valid email address", MU::ERR
bail = true
end
exit 1 if bail
}
if $opts[:name] and !$opts[:name].match(/ /)
MU.log "'name' field must consist of at least two words (saw '#{$opts[:name]}')", MU::ERR
exit 1
end
if $opts[:link_to_chef_user] and !MU::Master::Chef.getUser($opts[:link_to_chef_user])
MU.log "Requested link to Chef user '#{$opts[:link_to_chef_user]}', but that user doesn't exist", MU::ERR
exit 1
end
# Delete an existing account
if $opts[:delete]
bail = false
$opts.each_key { |opt|
if !["delete", "scratchpad", "notify_user"].include?(opt.to_s) and
$opts[opt] and !opt.to_s.match(/_given$/)
MU.log "Ignoring extraneous option '#{opt.to_s}' in delete", MU::WARN
end
}
exit 1 if bail
MU::Master.deleteUser($username)
else
create = false
if !$cur_users.has_key?($username)
$cur_users[$username] = {} if !$cur_users.has_key?($username)
create = true
end
$cur_users[$username]['realname'] = $opts[:name] if $opts[:name]
$cur_users[$username]['email'] = $opts[:email] if $opts[:email]
$cur_users[$username]['admin'] = true if $opts[:admin]
$cur_users[$username]['admin'] = false if $opts[:revoke_admin]
if $opts[:link_to_chef_user]
$cur_users[$username]['chef_user'] = $opts[:link_to_chef_user].dup
else
$cur_users[$username]['chef_user'] = $username.dup
end
# Validate for modifying an existing account
if !create
bail = false
if !$cur_users[$username].has_key?("email") and !$opts[:email]
MU.log "#{$username} does not have an email address set in LDAP, must supply one with -e to modify this account.", MU::ERR
bail = true
end
if !$cur_users[$username].has_key?("realname") and !$opts[:name]
MU.log "#{$username} does not have a display name set in LDAP, must supply one with -n to modify this account.", MU::ERR
bail = true
end
exit 1 if bail
# Validate for creating a new account
else
bail = false
if !$opts[:email]
MU.log "#{$username} does not have an email address set in LDAP, must supply one with -e.", MU::ERR
bail = true
end
if !$opts[:name]
MU.log "#{$username} does not have a display name set in LDAP, must supply one with -n.", MU::ERR
bail = true
end
if $password.nil?
$password = MU.generateWindowsPassword
MU.log "Creating a new account but no password supplied, invoking -g (generate) behavior.", MU::NOTICE
end
exit 1 if bail
end
if !$cur_users[$username]['realname'] or $cur_users[$username]['realname'].empty?
$cur_users[$username]['realname'] = $username
end
if !MU::Master.manageUser(
$username,
chef_username: $cur_users[$username]['chef_user'],
name: $cur_users[$username]['realname'],
email: $cur_users[$username]['email'],
admin: $cur_users[$username]['admin'],
password: $password,
change_uid: $opts[:force_uid],
orgs: $opts[:orgs],
remove_orgs: $opts[:remove_from_orgs]
)
exit 1
end
if create and !$opts[:skipupload]
home = Etc.getpwnam($username).dir
MU.log "Uploading Chef artifacts to the new '#{$username}' organization. This may take a while.", MU::NOTICE
%x{/bin/su - #{$username} -c "#{$MU_CFG['installdir']}/bin/mu-upload-chef-artifacts -n 2>&1 > /dev/null && touch #{home}/.first_chef_upload"}
end
end
if $password
if $opts[:notify_user] or $opts[:scratchpad]
sendPassword($username, $password, scratchpad: $opts[:scratchpad], notify: $opts[:notify_user])
elsif $opts[:generate_password]
MU.log "Generated password for #{$username}: #{$password}", MU::NOTICE
end
end
if File.exist?("/sbin/sss_cache")
%x{/sbin/sss_cache -E}
end
MU::Master.printUsersToTerminal