Class: RightScale::LoginUserManager

Inherits:
Object
  • Object
show all
Includes:
RightSupport::Ruby::EasySingleton
Defined in:
lib/instance/login_user_manager.rb

Constant Summary collapse

PROFILE_CHECKSUM =
"profile.md5"
MIN_UID =
10_000
MAX_UID =
2**32 - 1
MAX_UUID =
MAX_UID - MIN_UID
SBIN_PATHS =

List of directories that commonly contain user and group management utilities

['/usr/bin', '/usr/sbin', '/bin', '/sbin']
DEFAULT_SHELLS =

List of viable default shells. Useful because Ubuntu’s adduser seems to require a -s parameter.

['/bin/bash', '/usr/bin/bash', '/bin/sh', '/usr/bin/sh', '/bin/dash', '/bin/tcsh']

Instance Method Summary collapse

Instance Method Details

#add_user(username, uid, shell = nil) ⇒ Object

Create a Unix user with the “useradd” command.

Parameters

username(String)

username

uid(String)

account’s UID

expired_at(Time)

account’s expiration date; default nil

shell(String)

account’s login shell; default nil (use systemwide default)

Raise

(RightScale::LoginManager::SystemConflict)

if the user could not be created for some reason

Return

true

always returns true



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/instance/login_user_manager.rb', line 170

def add_user(username, uid, shell=nil)
  uid = Integer(uid)
  shell ||= DEFAULT_SHELLS.detect { |sh| File.exists?(sh) }

  useradd = find_sbin('useradd')

  unless shell.nil?
    dash_s = "-s #{Shellwords.escape(shell)}"
  end

  result = sudo("#{useradd} #{dash_s} -u #{uid} -m #{Shellwords.escape(username)}")

  case result.exitstatus
  when 0
    home_dir = Shellwords.escape(Etc.getpwnam(username).dir)

    sudo("chmod 0771 #{Shellwords.escape(home_dir)}")

    RightScale::Log.info "LoginUserManager created #{username} successfully"
  else
    raise RightScale::LoginManager::SystemConflict, "Failed to create user #{username}"
  end

  true
end

#create_user(username, uuid, superuser) ⇒ Object

Ensure that a given user exists and that his group membership is correct.

Parameters

username(String)

preferred username of RightScale user

uuid(String)

RightScale user’s UUID

superuser(Boolean)

whether the user should have sudo privileges

Block

If a block is given AND the user needs to be created, yields to the block with the to-be-created account’s username, before creating it. This gives the caller a chance to provide interactive feedback to the user.

Return

username(String)

user’s actual username (may vary from preferred username)

Raise

(LoginManager::SystemConflict)

if an existing non-RightScale-managed UID prevents us from creating a user



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/instance/login_user_manager.rb', line 88

def create_user(username, uuid, superuser)
  uid = LoginUserManager.uuid_to_uid(uuid)

  if uid_exists?(uid, ['rightscale'])
    username = uid_to_username(uid)
  elsif !uid_exists?(uid)
    username  = pick_username(username)
    yield(username) if block_given?
    add_user(username, uid)
    modify_group('rightscale', :add, username)

    # NB it is SUPER IMPORTANT to pass :force=>true here. Due to an oddity in Ruby's Etc
    # extension, a user who has recently been added, won't seem to be a member of
    # any groups until the SECOND time we enumerate his group membership.
    manage_user(uuid, superuser, :force=>true)
  else
    raise RightScale::LoginManager::SystemConflict, "A user with UID #{uid} already exists and is " +
                                                    "not managed by RightScale"
  end

  username
end

#group_exists?(name) ⇒ Boolean

Check if group with specified name exists in the system.

Parameters

name(String)

group’s name

Block

If a block is given, it will be yielded to with various status messages suitable for display to the user.

Return

exist_status(Boolean)

true if exists; otherwise false

Returns:

  • (Boolean)


334
335
336
337
338
# File 'lib/instance/login_user_manager.rb', line 334

def group_exists?(name)
  groups = Set.new
  Etc.group { |g| groups << g.name }
  groups.include?(name)
end

#manage_user(uuid, superuser, options = {}) ⇒ Object

If the given user exists and is RightScale-managed, then ensure his login information and group membership are correct. If force == true, then management tasks are performed irrespective of the user’s group membership status.

Parameters

uuid(String)

RightScale user’s UUID

superuser(Boolean)

whether the user should have sudo privileges

force(Boolean)

if true, performs group management even if the user does NOT belong to ‘rightscale’

Options

:force

if true, then the user will be updated even if they do not belong to the RightScale group

:disable

if true, then the user will be prevented from logging in

Return

username(String)

if the user exists, returns his actual username

false

if the user does not exist



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/instance/login_user_manager.rb', line 127

def manage_user(uuid, superuser, options={})
  uid      = LoginUserManager.uuid_to_uid(uuid)
  username = uid_to_username(uid)
  force    = options[:force] || false
  disable  = options[:disable] || false

  if ( force && uid_exists?(uid) ) || uid_exists?(uid, ['rightscale'])
    modify_user(username, disable)
    action = superuser ? :add : :remove
    modify_group('rightscale_sudo', action, username) if group_exists?('rightscale_sudo')

    username
  else
    false
  end
end

#modify_group(group, operation, username) ⇒ Object

Adds or removes a user from an OS group; does nothing if the user is already in the correct membership state.

Parameters

group(String)

group name

operation(Symbol)

:add or :remove

username(String)

username to add/remove

Raise

Raises ArgumentError

Return

result(Boolean)

true if user was added/removed; false if



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/instance/login_user_manager.rb', line 249

def modify_group(group, operation, username)
  #Ensure group/user exist; this raises ArgumentError if either does not exist
  Etc.getgrnam(group)
  Etc.getpwnam(username)

  groups = Set.new
  Etc.group { |g| groups << g.name if g.mem.include?(username) }

  case operation
    when :add
      return false if groups.include?(group)
      groups << group
    when :remove
      return false unless groups.include?(group)
      groups.delete(group)
    else
      raise ArgumentError, "Unknown operation #{operation}; expected :add or :remove"
  end

  groups   = Shellwords.escape(groups.to_a.join(','))
  username = Shellwords.escape(username)

  usermod = find_sbin('usermod')

  result = sudo("#{usermod} -G #{groups} #{username}")

  case result.exitstatus
  when 0
    RightScale::Log.info "Successfully performed group-#{operation} of #{username} to #{group}"
    return true
  else
    RightScale::Log.error "Failed group-#{operation} of #{username} to #{group}"
    return false
  end
end

#modify_user(username, locked = false, shell = nil) ⇒ Object

Modify a user with the “usermod” command.

Parameters

username(String)

username

uid(String)

account’s UID

locked(true,false)

if true, prevent the user from logging in

shell(String)

account’s login shell; default nil (use systemwide default)

Return

true

always returns true



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/instance/login_user_manager.rb', line 206

def modify_user(username, locked=false, shell=nil)
  shell ||= DEFAULT_SHELLS.detect { |sh| File.exists?(sh) }

  usermod = find_sbin('usermod')

  if locked
    # the man page claims that "1" works here, but testing proves that it doesn't.
    # use 1970 instead.
    dash_e = "-e 1970-01-01 -L"
  else
    dash_e = "-e 99999 -U"
  end

  unless shell.nil?
    dash_s = "-s #{Shellwords.escape(shell)}"
  end

  result = sudo("#{usermod} #{dash_e} #{dash_s} #{Shellwords.escape(username)}")

  case result.exitstatus
  when 0
    RightScale::Log.info "LoginUserManager modified #{username} successfully"
  else
    RightScale::Log.error "Failed to modify user #{username}"
  end

  true
end

#pick_username(ideal) ⇒ Object

Pick a username that does not yet exist on the system. If the given username does not exist, it is returned; else we add a “_1” suffix and continue incrementing the number until we arrive at a username that does not yet exist.

Parameters

ideal(String)

the user’s ideal (chosen) username

Return

username(String)

username with possible postfix



59
60
61
62
63
64
65
66
67
68
69
# File 'lib/instance/login_user_manager.rb', line 59

def pick_username(ideal)
  name = ideal
  i = 0

  while user_exists?(name)
    i += 1
    name = "#{ideal}_#{i}"
  end

  name
end

#setup_profile(username, home_dir, custom_data, force) ⇒ Object



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/instance/login_user_manager.rb', line 340

def setup_profile(username, home_dir, custom_data, force)
  return false if custom_data.nil? || custom_data.empty?

  checksum_path = File.join('.rightscale', PROFILE_CHECKSUM)
  return false if !force && File.exists?(File.join(home_dir, checksum_path))

  t0 = Time.now.to_i
  yield("Performing profile setup for #{username}...") if block_given?

  tmpdir = Dir.mktmpdir
  file_path = File.join(tmpdir, File.basename(custom_data))
  if download_files(custom_data, file_path) && extract_files(username, file_path, home_dir)
    save_checksum(username, file_path, checksum_path, home_dir)
    t1 = Time.now.to_i
    yield("Setup complete (#{t1 - t0} sec)") if block_given? && (t1 - t0 >= 2)
  end

  return true
rescue Exception => e
  yield("Failed to create profile for #{username}; continuing") if block_given?
  yield("#{e.class.name}: #{e.message} - #{e.backtrace.first}") if block_given?
  Log.error("#{e.class.name}: #{e.message} - #{e.backtrace.first}")
  return false
ensure
  FileUtils.rm_rf(tmpdir) if tmpdir && File.exists?(tmpdir)
end

#simulate_login(username) ⇒ Object

Set some of the environment variables that would normally be set if a user were to login to an interactive shell. This is useful when simulating an interactive login, e.g. for purposes of running a user-specified command via SSH.

Parameters

username(String)

user’s name

Return

true

always returns true



377
378
379
380
381
382
383
# File 'lib/instance/login_user_manager.rb', line 377

def (username)
  info = Etc.getpwnam(username)
  ENV['USER']  = info.name
  ENV['HOME']  = info.dir
  ENV['SHELL'] = info.shell
  true
end

#uid_exists?(uid, groups = []) ⇒ Boolean

Check if user with specified Unix UID exists in the system, and optionally whether he belongs to all of the specified groups.

Parameters

uid(String)

account’s UID

Return

exist_status(Boolean)

true if exists; otherwise false

Returns:

  • (Boolean)


306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/instance/login_user_manager.rb', line 306

def uid_exists?(uid, groups=[])
  uid = Integer(uid)
  user_exists = Etc.getpwuid(uid).uid == uid
  if groups.empty?
    user_belongs = true
  else
    mem = Set.new
    username = Etc.getpwuid(uid).name
    Etc.group { |g| mem << g.name if g.mem.include?(username) }
    user_belongs = groups.all? { |g| mem.include?(g) }
  end

  user_exists && user_belongs
rescue ArgumentError
  false
end

#uid_to_username(uid) ⇒ Object

uid(String)

linux account UID

Return

username(String)

account’s username or empty string



151
152
153
154
# File 'lib/instance/login_user_manager.rb', line 151

def uid_to_username(uid)
  uid = Integer(uid)
  Etc.getpwuid(uid).name
end

#user_exists?(name) ⇒ Boolean

Checks if user with specified name exists in the system.

Parameter

name(String)

username

Return

exist_status(Boolean)

true if user exists; otherwise false

Returns:

  • (Boolean)


292
293
294
295
296
# File 'lib/instance/login_user_manager.rb', line 292

def user_exists?(name)
  Etc.getpwnam(name).name == name
rescue ArgumentError
  false
end

#uuid_to_uid(uuid) ⇒ Object

Map a universally-unique integer RightScale user ID to a locally-unique Unix UID.



40
41
42
43
44
45
46
47
# File 'lib/instance/login_user_manager.rb', line 40

def uuid_to_uid(uuid)
  uuid = Integer(uuid)
  if uuid >= 0 && uuid <= MAX_UUID
    10_000 + uuid
  else
    raise RangeError, "#{uuid} is not within (0..#{MAX_UUID})"
  end
end