cloudamatic/mu

View on GitHub
bin/mu-user-manage

Summary

Maintainability
Test Coverage
#!/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