2015-10-03 11:32:19 +00:00
|
|
|
# encoding: utf-8
|
|
|
|
# author: Christoph Hartmann
|
2015-10-06 16:55:44 +00:00
|
|
|
# author: Dominik Richter
|
2015-10-03 11:32:19 +00:00
|
|
|
|
|
|
|
# Usage:
|
|
|
|
#
|
|
|
|
# describe user('root') do
|
|
|
|
# it { should exist }
|
|
|
|
# its(:uid) { should eq 0 }
|
|
|
|
# its(:gid) { should eq 0 }
|
|
|
|
# its(:group) { should eq 'root' }
|
|
|
|
# its(:groups) { should eq ['root', 'wheel']}
|
|
|
|
# its(:home) { should eq '/root' }
|
|
|
|
# its(:shell) { should eq '/bin/bash' }
|
|
|
|
# its(:mindays) { should eq 0 }
|
|
|
|
# its(:maxdays) { should eq 99 }
|
|
|
|
# its(:warndays) { should eq 5 }
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# The following Serverspec matchers are deprecated in favor for direct value access
|
|
|
|
#
|
|
|
|
# describe user('root') do
|
|
|
|
# it { should belong_to_group 'root' }
|
|
|
|
# it { should have_uid 0 }
|
|
|
|
# it { should have_home_directory '/root' }
|
|
|
|
# it { should have_login_shell '/bin/bash' }
|
|
|
|
# its(:minimum_days_between_password_change) { should eq 0 }
|
|
|
|
# its(:maximum_days_between_password_change) { should eq 99 }
|
|
|
|
# end
|
|
|
|
|
|
|
|
# ServerSpec tests that are not supported:
|
|
|
|
#
|
|
|
|
# describe user('root') do
|
|
|
|
# it { should have_authorized_key 'ssh-rsa ADg54...3434 user@example.local' }
|
|
|
|
# its(:encrypted_password) { should eq 1234 }
|
|
|
|
# end
|
|
|
|
|
2015-10-04 16:07:45 +00:00
|
|
|
require 'utils/parser'
|
2015-10-05 09:40:34 +00:00
|
|
|
require 'utils/convert'
|
2015-10-04 16:07:45 +00:00
|
|
|
|
2015-10-26 03:04:18 +00:00
|
|
|
class User < Inspec.resource(1)
|
2015-10-03 11:32:19 +00:00
|
|
|
name 'user'
|
|
|
|
|
|
|
|
def initialize(user)
|
|
|
|
@user = user
|
|
|
|
|
|
|
|
# select package manager
|
|
|
|
@user_provider = nil
|
2015-10-26 03:04:18 +00:00
|
|
|
case inspec.os[:family]
|
2015-10-22 21:53:50 +00:00
|
|
|
when 'ubuntu', 'debian', 'redhat', 'fedora', 'centos', 'arch', 'opensuse'
|
2015-10-26 03:04:18 +00:00
|
|
|
@user_provider = LinuxUser.new(inspec)
|
2015-10-04 15:20:59 +00:00
|
|
|
when 'windows'
|
2015-10-26 03:04:18 +00:00
|
|
|
@user_provider = WindowsUser.new(inspec)
|
2015-10-03 11:32:19 +00:00
|
|
|
when 'darwin'
|
2015-10-26 03:04:18 +00:00
|
|
|
@user_provider = DarwinUser.new(inspec)
|
2015-10-03 11:58:45 +00:00
|
|
|
when 'freebsd'
|
2015-10-26 03:04:18 +00:00
|
|
|
@user_provider = FreeBSDUser.new(inspec)
|
2015-10-03 11:32:19 +00:00
|
|
|
else
|
2015-10-21 17:32:25 +00:00
|
|
|
return skip_resource 'The `user` resource is not supported on your OS yet.'
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def exists?
|
2015-10-05 09:22:03 +00:00
|
|
|
!identiy.nil? && !identiy[:user].nil?
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def uid
|
2015-10-05 09:22:03 +00:00
|
|
|
identiy.nil? ? nil : identiy[:uid]
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def gid
|
2015-10-05 09:22:03 +00:00
|
|
|
identiy.nil? ? nil : identiy[:gid]
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def group
|
2015-10-05 09:22:03 +00:00
|
|
|
identiy.nil? ? nil : identiy[:group]
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def groups
|
2015-10-05 09:22:03 +00:00
|
|
|
identiy.nil? ? nil : identiy[:groups]
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def home
|
2015-10-05 09:22:03 +00:00
|
|
|
meta_info.nil? ? nil : meta_info[:home]
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def shell
|
2015-10-05 09:22:03 +00:00
|
|
|
meta_info.nil? ? nil : meta_info[:shell]
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# returns the minimum days between password changes
|
|
|
|
def mindays
|
2015-10-05 09:40:34 +00:00
|
|
|
credentials.nil? ? nil : credentials[:mindays]
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# returns the maximum days between password changes
|
|
|
|
def maxdays
|
2015-10-05 09:40:34 +00:00
|
|
|
credentials.nil? ? nil : credentials[:maxdays]
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# returns the days for password change warning
|
|
|
|
def warndays
|
2015-10-05 09:40:34 +00:00
|
|
|
credentials.nil? ? nil : credentials[:warndays]
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# implement 'mindays' method to be compatible with serverspec
|
|
|
|
def minimum_days_between_password_change
|
|
|
|
deprecated('minimum_days_between_password_change', "Please use 'its(:mindays)'")
|
|
|
|
mindays
|
|
|
|
end
|
|
|
|
|
|
|
|
# implement 'maxdays' method to be compatible with serverspec
|
|
|
|
def maximum_days_between_password_change
|
|
|
|
deprecated('maximum_days_between_password_change', "Please use 'its(:maxdays)'")
|
|
|
|
maxdays
|
|
|
|
end
|
|
|
|
|
|
|
|
# implements rspec has matcher, to be compatible with serverspec
|
|
|
|
# @see: https://github.com/rspec/rspec-expectations/blob/master/lib/rspec/matchers/built_in/has.rb
|
|
|
|
def has_uid?(compare_uid)
|
|
|
|
deprecated('has_uid?')
|
|
|
|
uid == compare_uid
|
|
|
|
end
|
|
|
|
|
|
|
|
def has_home_directory?(compare_home)
|
|
|
|
deprecated('has_home_directory?', "Please use 'its(:home)'")
|
|
|
|
home == compare_home
|
|
|
|
end
|
|
|
|
|
|
|
|
def has_login_shell?(compare_shell)
|
|
|
|
deprecated('has_login_shell?', "Please use 'its(:shell)'")
|
|
|
|
shell == compare_shell
|
|
|
|
end
|
|
|
|
|
|
|
|
def has_authorized_key?(_compare_key)
|
|
|
|
deprecated('has_authorized_key?')
|
|
|
|
fail NotImplementedError
|
|
|
|
end
|
|
|
|
|
|
|
|
def deprecated(name, alternative = nil)
|
|
|
|
warn "[DEPRECATION] #{name} is deprecated. #{alternative}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_s
|
2015-10-12 11:01:58 +00:00
|
|
|
"User #{@user}"
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
2015-10-05 09:22:03 +00:00
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def identiy
|
2015-10-07 23:02:59 +00:00
|
|
|
return @id_cache if defined?(@id_cache)
|
2015-10-05 09:46:48 +00:00
|
|
|
@id_cache = @user_provider.identity(@user) if !@user_provider.nil?
|
2015-10-05 09:22:03 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def meta_info
|
2015-10-07 23:02:59 +00:00
|
|
|
return @meta_cache if defined?(@meta_cache)
|
2015-10-05 09:46:48 +00:00
|
|
|
@meta_cache = @user_provider.meta_info(@user) if !@user_provider.nil?
|
2015-10-05 09:22:03 +00:00
|
|
|
end
|
2015-10-05 09:40:34 +00:00
|
|
|
|
|
|
|
def credentials
|
2015-10-07 23:02:59 +00:00
|
|
|
return @cred_cache if defined?(@cred_cache)
|
2015-10-05 09:46:48 +00:00
|
|
|
@cred_cache = @user_provider.credentials(@user) if !@user_provider.nil?
|
2015-10-05 09:40:34 +00:00
|
|
|
end
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
class UserInfo
|
2015-10-05 09:40:34 +00:00
|
|
|
include Converter
|
|
|
|
|
2015-10-26 03:04:18 +00:00
|
|
|
attr_reader :inspec
|
|
|
|
def initialize(inspec)
|
|
|
|
@inspec = inspec
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
2015-10-05 09:40:34 +00:00
|
|
|
|
|
|
|
def credentials(_username)
|
|
|
|
end
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# implements generic unix id handling
|
|
|
|
class UnixUser < UserInfo
|
|
|
|
# parse one id entry like '0(wheel)''
|
|
|
|
def parse_value(line)
|
|
|
|
SimpleConfig.new(
|
|
|
|
line,
|
2015-10-03 12:24:13 +00:00
|
|
|
line_separator: ',',
|
2015-10-03 11:32:19 +00:00
|
|
|
assignment_re: /^\s*([^\(]*?)\s*\(\s*(.*?)\)*$/,
|
|
|
|
group_re: nil,
|
|
|
|
multiple_values: false,
|
|
|
|
).params
|
|
|
|
end
|
|
|
|
|
|
|
|
# extracts the identity
|
|
|
|
def identity(username)
|
2015-10-26 03:04:18 +00:00
|
|
|
cmd = inspec.command("id #{username}")
|
2015-10-03 11:32:19 +00:00
|
|
|
return nil if cmd.exit_status != 0
|
|
|
|
|
|
|
|
# parse words
|
|
|
|
params = SimpleConfig.new(
|
2015-10-03 12:24:13 +00:00
|
|
|
cmd.stdout.chomp,
|
|
|
|
line_separator: ' ',
|
2015-10-03 11:32:19 +00:00
|
|
|
assignment_re: /^\s*([^=]*?)\s*=\s*(.*?)\s*$/,
|
|
|
|
group_re: nil,
|
|
|
|
multiple_values: false,
|
|
|
|
).params
|
|
|
|
|
|
|
|
{
|
|
|
|
uid: convert_to_i(parse_value(params['uid']).keys[0]),
|
|
|
|
user: parse_value(params['uid']).values[0],
|
|
|
|
gid: convert_to_i(parse_value(params['gid']).keys[0]),
|
|
|
|
group: parse_value(params['gid']).values[0],
|
|
|
|
groups: parse_value(params['groups']).values,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class LinuxUser < UnixUser
|
2015-10-04 16:07:45 +00:00
|
|
|
include ContentParser
|
|
|
|
|
2015-10-03 11:58:45 +00:00
|
|
|
def meta_info(username)
|
2015-10-26 03:04:18 +00:00
|
|
|
cmd = inspec.command("getent passwd #{username}")
|
2015-10-04 16:07:45 +00:00
|
|
|
return nil if cmd.exit_status != 0
|
|
|
|
# returns: root:x:0:0:root:/root:/bin/bash
|
|
|
|
passwd = parse_passwd_line(cmd.stdout.chomp)
|
2015-10-03 11:32:19 +00:00
|
|
|
{
|
2015-10-04 16:07:45 +00:00
|
|
|
home: passwd['home'],
|
|
|
|
shell: passwd['shell'],
|
2015-10-03 11:32:19 +00:00
|
|
|
}
|
|
|
|
end
|
2015-10-05 09:40:34 +00:00
|
|
|
|
|
|
|
def credentials(username)
|
2015-10-26 03:04:18 +00:00
|
|
|
cmd = inspec.command("chage -l #{username}")
|
2015-10-05 09:40:34 +00:00
|
|
|
return nil if cmd.exit_status != 0
|
|
|
|
|
|
|
|
params = SimpleConfig.new(
|
|
|
|
cmd.stdout.chomp,
|
|
|
|
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
|
|
|
|
group_re: nil,
|
|
|
|
multiple_values: false,
|
|
|
|
).params
|
|
|
|
|
|
|
|
{
|
|
|
|
mindays: convert_to_i(params['Minimum number of days between password change']),
|
|
|
|
maxdays: convert_to_i(params['Maximum number of days between password change']),
|
|
|
|
warndays: convert_to_i(params['Number of days of warning before password expires']),
|
|
|
|
}
|
|
|
|
end
|
2015-10-03 11:32:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# we do not use 'finger' for MacOS, because it is harder to parse data with it
|
|
|
|
# @see https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/fingerd.8.html
|
|
|
|
# instead we use 'dscl' to request user data
|
|
|
|
# @see https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/dscl.1.html
|
|
|
|
# @see http://superuser.com/questions/592921/mac-osx-users-vs-dscl-command-to-list-user
|
|
|
|
class DarwinUser < UnixUser
|
|
|
|
def meta_info(username)
|
2015-10-26 03:04:18 +00:00
|
|
|
cmd = inspec.command("dscl -q . -read /Users/#{username} NFSHomeDirectory PrimaryGroupID RecordName UniqueID UserShell")
|
2015-10-03 11:32:19 +00:00
|
|
|
return nil if cmd.exit_status != 0
|
|
|
|
|
|
|
|
params = SimpleConfig.new(
|
|
|
|
cmd.stdout.chomp,
|
|
|
|
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
|
|
|
|
group_re: nil,
|
|
|
|
multiple_values: false,
|
|
|
|
).params
|
|
|
|
|
|
|
|
{
|
|
|
|
home: params['NFSHomeDirectory'],
|
|
|
|
shell: params['UserShell'],
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
2015-10-03 11:58:45 +00:00
|
|
|
|
|
|
|
# FreeBSD recommends to use the 'pw' command for user management
|
|
|
|
# @see: https://www.freebsd.org/doc/handbook/users-synopsis.html
|
|
|
|
# @see: https://www.freebsd.org/cgi/man.cgi?pw(8)
|
|
|
|
# It offers the following commands:
|
|
|
|
# - adduser(8) The recommended command-line application for adding new users.
|
|
|
|
# - rmuser(8) The recommended command-line application for removing users.
|
|
|
|
# - chpass(1) A flexible tool for changing user database information.
|
|
|
|
# - passwd(1) The command-line tool to change user passwords.
|
|
|
|
class FreeBSDUser < UnixUser
|
2015-10-04 16:07:45 +00:00
|
|
|
include ContentParser
|
|
|
|
|
|
|
|
def meta_info(username)
|
2015-10-26 03:04:18 +00:00
|
|
|
cmd = inspec.command("pw usershow #{username} -7")
|
2015-10-04 16:07:45 +00:00
|
|
|
return nil if cmd.exit_status != 0
|
|
|
|
# returns: root:*:0:0:Charlie &:/root:/bin/csh
|
|
|
|
passwd = parse_passwd_line(cmd.stdout.chomp)
|
2015-10-04 15:20:59 +00:00
|
|
|
{
|
2015-10-04 16:07:45 +00:00
|
|
|
home: passwd['home'],
|
|
|
|
shell: passwd['shell'],
|
2015-10-04 15:20:59 +00:00
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# For now, we stick with WMI Win32_UserAccount
|
|
|
|
# @see https://msdn.microsoft.com/en-us/library/aa394507(v=vs.85).aspx
|
|
|
|
# @see https://msdn.microsoft.com/en-us/library/aa394153(v=vs.85).aspx
|
|
|
|
#
|
|
|
|
# using Get-AdUser would be the best command for domain machines, but it will not be installed
|
|
|
|
# on client machines by default
|
|
|
|
# @see https://technet.microsoft.com/en-us/library/ee617241.aspx
|
|
|
|
# @see https://technet.microsoft.com/en-us/library/hh509016(v=WS.10).aspx
|
|
|
|
# @see http://woshub.com/get-aduser-getting-active-directory-users-data-via-powershell/
|
|
|
|
# @see http://stackoverflow.com/questions/17548523/the-term-get-aduser-is-not-recognized-as-the-name-of-a-cmdlet
|
|
|
|
#
|
|
|
|
# Just for reference, we could also use ADSI (Active Directory Service Interfaces)
|
|
|
|
# @see https://mcpmag.com/articles/2015/04/15/reporting-on-local-accounts.aspx
|
|
|
|
class WindowsUser < UserInfo
|
|
|
|
# parse windows account name
|
|
|
|
def parse_windows_account(username)
|
|
|
|
account = username.split('\\')
|
|
|
|
name = account.pop
|
|
|
|
domain = account.pop if account.size > 0
|
|
|
|
[name, domain]
|
|
|
|
end
|
|
|
|
|
|
|
|
def identity(username)
|
|
|
|
# extract domain/user information
|
2015-10-05 12:57:09 +00:00
|
|
|
account, domain = parse_windows_account(username)
|
2015-10-04 15:20:59 +00:00
|
|
|
|
|
|
|
# TODO: escape content
|
|
|
|
if !domain.nil?
|
2015-10-05 12:57:09 +00:00
|
|
|
filter = "Name = '#{account}' and Domain = '#{domain}'"
|
2015-10-04 15:20:59 +00:00
|
|
|
else
|
2015-10-05 12:57:09 +00:00
|
|
|
filter = "Name = '#{account}' and LocalAccount = true"
|
2015-10-04 15:20:59 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
script = <<-EOH
|
|
|
|
# find user
|
|
|
|
$user = Get-WmiObject Win32_UserAccount -filter "#{filter}"
|
|
|
|
# get related groups
|
|
|
|
$groups = $user.GetRelated('Win32_Group') | Select-Object -Property Caption, Domain, Name, LocalAccount, SID, SIDType, Status
|
|
|
|
# filter user information
|
|
|
|
$user = $user | Select-Object -Property Caption, Description, Domain, Name, LocalAccount, Lockout, PasswordChangeable, PasswordExpires, PasswordRequired, SID, SIDType, Status
|
|
|
|
# build response object
|
|
|
|
New-Object -Type PSObject | `
|
|
|
|
Add-Member -MemberType NoteProperty -Name User -Value ($user) -PassThru | `
|
|
|
|
Add-Member -MemberType NoteProperty -Name Groups -Value ($groups) -PassThru | `
|
|
|
|
ConvertTo-Json
|
|
|
|
EOH
|
|
|
|
|
2015-10-26 03:04:18 +00:00
|
|
|
cmd = inspec.script(script)
|
2015-10-04 15:20:59 +00:00
|
|
|
|
|
|
|
# cannot rely on exit code for now, successful command returns exit code 1
|
|
|
|
# return nil if cmd.exit_status != 0, try to parse json
|
|
|
|
begin
|
|
|
|
params = JSON.parse(cmd.stdout)
|
|
|
|
rescue JSON::ParserError => _e
|
2015-10-05 09:22:03 +00:00
|
|
|
return nil
|
2015-10-04 15:20:59 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
user = params['User']['Caption'] unless params['User'].nil?
|
2015-10-05 12:55:49 +00:00
|
|
|
groups = params['Groups']
|
|
|
|
# if groups is no array, generate one
|
|
|
|
groups = [groups] if !groups.is_a?(Array)
|
|
|
|
groups = groups.map { |grp| grp['Caption'] } unless params['Groups'].nil?
|
2015-10-04 15:20:59 +00:00
|
|
|
|
|
|
|
{
|
|
|
|
uid: nil,
|
|
|
|
user: user,
|
|
|
|
gid: nil,
|
|
|
|
group: nil,
|
|
|
|
groups: groups,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2015-10-05 09:22:03 +00:00
|
|
|
# not implemented yet
|
2015-10-04 15:20:59 +00:00
|
|
|
def meta_info(_username)
|
2015-10-03 11:58:45 +00:00
|
|
|
{
|
|
|
|
home: nil,
|
|
|
|
shell: nil,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|