#!/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} Adding/modifying users: #{$0} [-a|-r] [-e ] [-n ''] [-i|-p |-g] [-o ] [-v ] [-m ] [-l ] Deleting users: #{$0} [-i] -d 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 = < To: #{to} MIME-Version: 1.0 Content-type: text/html Subject: #{subject}
#{message}
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#{url}" 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 MU::Master.printUsersToTerminal