summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Smith <tsmith@chef.io>2020-10-02 22:01:01 -0700
committerGitHub <noreply@github.com>2020-10-02 22:01:01 -0700
commite02f77f9a3036a596b111c5e20e54825a78f570e (patch)
tree878bd6ebae4bb84c7a627087daf2e738498d500a
parent6d6e6f4cd6218d2e4285897cbda1cf9c11c094a9 (diff)
parent85a7d8dd2e52409e21aca8ce30591bc5dd30af34 (diff)
downloadohai-e02f77f9a3036a596b111c5e20e54825a78f570e.tar.gz
Merge pull request #1516 from jaymzh/passwd
Signed-off-by: Tim Smith <tsmith@chef.io>
-rw-r--r--RELEASE_NOTES.md9
-rw-r--r--lib/ohai/plugins/passwd.rb58
-rw-r--r--spec/unit/plugins/passwd_spec.rb263
3 files changed, 294 insertions, 36 deletions
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 5dfe416d..3b4aa448 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,3 +1,12 @@
+# Unreleased
+
+## etc Ohai Data on Windows
+
+Ohai's 'Passwd' plugin that provides `node['etc']['passwd']` and `node['etc']['group']` now populates data on Windows. Data for all local users and groups is present. A few things to note:
+
+ * If the node is an Active Directory domain controller, you will get all domain users as domain controllers see domain users as local
+ * If the node is not an Active Directory domain controller you will only get actual local users
+ * Group members are not recursed, so you if groups are nested, you will simply see the group that is directly a member of the group.
# Ohai Release Notes 15.6
diff --git a/lib/ohai/plugins/passwd.rb b/lib/ohai/plugins/passwd.rb
index 4af41450..781aba94 100644
--- a/lib/ohai/plugins/passwd.rb
+++ b/lib/ohai/plugins/passwd.rb
@@ -42,6 +42,62 @@ Ohai.plugin(:Passwd) do
end
collect_data(:windows) do
- # Etc returns nil on Windows
+ require "wmi-lite/wmi" unless defined?(WmiLite::Wmi)
+
+ unless etc
+ etc Mash.new
+
+ wmi = WmiLite::Wmi.new
+
+ etc[:passwd] = Mash.new
+ users = wmi.query("SELECT * FROM Win32_UserAccount WHERE LocalAccount = True")
+ users.each do |user|
+ uname = user["Name"].strip.downcase
+ Ohai::Log.debug("processing user #{uname}")
+ etc[:passwd][uname] = Mash.new
+ wmi_obj = user.wmi_ole_object
+ wmi_obj.properties_.each do |key|
+ etc[:passwd][uname][key.name.downcase] = user[key.name]
+ end
+ end
+
+ etc[:group] = Mash.new
+ groups = wmi.query("SELECT * FROM Win32_Group WHERE LocalAccount = True")
+ groups.each do |group|
+ gname = group["Name"].strip.downcase
+ Ohai::Log.debug("processing group #{gname}")
+ etc[:group][gname] = Mash.new
+ wmi_obj = group.wmi_ole_object
+ wmi_obj.properties_.each do |key|
+ etc[:group][gname][key.name.downcase] = group[key.name]
+ end
+
+ # This is the primary reason that we're using WMI instead of powershell
+ # cmdlets - the powershell start up cost is huge, and you *must* do this
+ # query for every. single. group. individually.
+
+ # The query returns nothing unless you specify domain *and* name, it's
+ # a path, not a set of queries.
+ subq = "Win32_Group.Domain='#{group["Domain"]}',Name='#{group["Name"]}'"
+ members = wmi.query(
+ "SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"#{subq}\""
+ )
+ etc[:group][gname]["members"] = members.map do |member|
+ mi = {}
+ info = Hash[
+ member["partcomponent"].split(",").map { |x| x.split("=") }.map { |a, b| [a, b.undump] }
+ ]
+ if info.keys.any? { |x| x.match?("Win32_UserAccount") }
+ mi["type"] = :user
+ else
+ # Note: the type here is actually Win32_SystemAccount, because,
+ # that's what groups are in the Windows universe.
+ mi["type"] = :group
+ end
+ mi["name"] = info["Name"]
+ mi
+ end
+ end
+ end
end
end
diff --git a/spec/unit/plugins/passwd_spec.rb b/spec/unit/plugins/passwd_spec.rb
index 6450e22f..c5cfcf69 100644
--- a/spec/unit/plugins/passwd_spec.rb
+++ b/spec/unit/plugins/passwd_spec.rb
@@ -16,50 +16,243 @@
#
require "spec_helper"
+require "json"
-describe Ohai::System, "plugin etc", :unix_only do
- let(:plugin) { get_plugin("passwd") }
+describe Ohai::System, "plugin etc" do
+ context "when on posix", :unix_only do
+ let(:plugin) { get_plugin("passwd") }
- PasswdEntry = Struct.new(:name, :uid, :gid, :dir, :shell, :gecos)
- GroupEntry = Struct.new(:name, :gid, :mem)
+ PasswdEntry = Struct.new(:name, :uid, :gid, :dir, :shell, :gecos)
+ GroupEntry = Struct.new(:name, :gid, :mem)
- it "includes a list of all users" do
- expect(Etc).to receive(:passwd).and_yield(PasswdEntry.new("root", 1, 1, "/root", "/bin/zsh", "BOFH"))
- .and_yield(PasswdEntry.new("www", 800, 800, "/var/www", "/bin/false", "Serving the web since 1970"))
- plugin.run
- expect(plugin[:etc][:passwd]["root"]).to eq(Mash.new(shell: "/bin/zsh", gecos: "BOFH", gid: 1, uid: 1, dir: "/root"))
- expect(plugin[:etc][:passwd]["www"]).to eq(Mash.new(shell: "/bin/false", gecos: "Serving the web since 1970", gid: 800, uid: 800, dir: "/var/www"))
- end
+ it "includes a list of all users" do
+ expect(Etc).to receive(:passwd).and_yield(PasswdEntry.new("root", 1, 1, "/root", "/bin/zsh", "BOFH"))
+ .and_yield(PasswdEntry.new("www", 800, 800, "/var/www", "/bin/false", "Serving the web since 1970"))
+ plugin.run
+ expect(plugin[:etc][:passwd]["root"]).to eq(Mash.new(shell: "/bin/zsh", gecos: "BOFH", gid: 1, uid: 1, dir: "/root"))
+ expect(plugin[:etc][:passwd]["www"]).to eq(Mash.new(shell: "/bin/false", gecos: "Serving the web since 1970", gid: 800, uid: 800, dir: "/var/www"))
+ end
- it "ignores duplicate users" do
- expect(Etc).to receive(:passwd).and_yield(PasswdEntry.new("root", 1, 1, "/root", "/bin/zsh", "BOFH"))
- .and_yield(PasswdEntry.new("root", 1, 1, "/", "/bin/false", "I do not belong"))
- plugin.run
- expect(plugin[:etc][:passwd]["root"]).to eq(Mash.new(shell: "/bin/zsh", gecos: "BOFH", gid: 1, uid: 1, dir: "/root"))
- end
+ it "ignores duplicate users" do
+ expect(Etc).to receive(:passwd).and_yield(PasswdEntry.new("root", 1, 1, "/root", "/bin/zsh", "BOFH"))
+ .and_yield(PasswdEntry.new("root", 1, 1, "/", "/bin/false", "I do not belong"))
+ plugin.run
+ expect(plugin[:etc][:passwd]["root"]).to eq(Mash.new(shell: "/bin/zsh", gecos: "BOFH", gid: 1, uid: 1, dir: "/root"))
+ end
- it "sets the current user" do
- expect(Process).to receive(:euid).and_return("31337")
- expect(Etc).to receive(:getpwuid).and_return(PasswdEntry.new("chef", 31337, 31337, "/home/chef", "/bin/ksh", "Julia Child"))
- plugin.run
- expect(plugin[:current_user]).to eq("chef")
- end
+ it "sets the current user" do
+ expect(Process).to receive(:euid).and_return("31337")
+ expect(Etc).to receive(:getpwuid).and_return(PasswdEntry.new("chef", 31337, 31337, "/home/chef", "/bin/ksh", "Julia Child"))
+ plugin.run
+ expect(plugin[:current_user]).to eq("chef")
+ end
+
+ it "sets the available groups" do
+ expect(Etc).to receive(:group).and_yield(GroupEntry.new("admin", 100, %w{root chef})).and_yield(GroupEntry.new("www", 800, %w{www deploy}))
+ plugin.run
+ expect(plugin[:etc][:group]["admin"]).to eq(Mash.new(gid: 100, members: %w{root chef}))
+ expect(plugin[:etc][:group]["www"]).to eq(Mash.new(gid: 800, members: %w{www deploy}))
+ end
- it "sets the available groups" do
- expect(Etc).to receive(:group).and_yield(GroupEntry.new("admin", 100, %w{root chef})).and_yield(GroupEntry.new("www", 800, %w{www deploy}))
- plugin.run
- expect(plugin[:etc][:group]["admin"]).to eq(Mash.new(gid: 100, members: %w{root chef}))
- expect(plugin[:etc][:group]["www"]).to eq(Mash.new(gid: 800, members: %w{www deploy}))
+ if "".respond_to?(:force_encoding)
+ it "sets the encoding of strings to the default external encoding" do
+ fields = ["root", 1, 1, "/root", "/bin/zsh", "BOFH"]
+ fields.each { |f| f.force_encoding(Encoding::ASCII_8BIT) if f.respond_to?(:force_encoding) }
+ allow(Etc).to receive(:passwd).and_yield(PasswdEntry.new(*fields))
+ plugin.run
+ root = plugin[:etc][:passwd]["root"]
+ expect(root["gecos"].encoding).to eq(Encoding.default_external)
+ end
+ end
end
- if "".respond_to?(:force_encoding)
- it "sets the encoding of strings to the default external encoding" do
- fields = ["root", 1, 1, "/root", "/bin/zsh", "BOFH"]
- fields.each { |f| f.force_encoding(Encoding::ASCII_8BIT) if f.respond_to?(:force_encoding) }
- allow(Etc).to receive(:passwd).and_yield(PasswdEntry.new(*fields))
+ context "when on windows", :windows_only do
+ let(:plugin) do
+ get_plugin("passwd").tap do |plugin|
+ plugin[:platform_family] = "windows"
+ end
+ end
+
+ USERS = [
+ {
+ "AccountType" => 512,
+ "Disabled" => false,
+ "Name" => "userone",
+ "FullName" => "User One",
+ "SID" => "bla bla bla",
+ "SIDType" => 1,
+ "Status" => "OK",
+ },
+ {
+ "AccountType" => 512,
+ "Disabled" => false,
+ "FullName" => "User Two",
+ "Name" => "usertwo",
+ "SID" => "bla bla bla2",
+ "SIDType" => 1,
+ "Status" => "OK",
+ },
+ ].freeze
+
+ GROUPS = [
+ {
+ "Description" => "Group One",
+ "Domain" => "THIS-MACHINE",
+ "Name" => "GroupOne",
+ "SID" => "foo foo foo",
+ "SidType" => 4,
+ "Status" => "OK",
+ },
+ {
+ "Description" => "Group Two",
+ "Domain" => "THIS-MACHINE",
+ "Name" => "GroupTwo",
+ "SID" => "foo foo foo2",
+ "SidType" => 4,
+ "Status" => "OK",
+ },
+ ].freeze
+
+ GROUP_ONE_MEMBERS = [
+ {
+ "groupcomponent" => "Win32_Group.Domain=\"THIS-MACHINE\",Name=\"GroupOne\"",
+ "partcomponent" => "\\\\VCRS-PRODWIN05\\root\\cimv2:Win32_UserAccount.Domain=\"THIS-MACHINE\",Name=\"UserOne\"",
+ },
+ {
+ "groupcomponent" => "Win32_Group.Domain=\"THIS-MACHINE\",Name=\"GroupOne\"",
+ "partcomponent" => "\\\\VCRS-PRODWIN05\\root\\cimv2:Win32_UserAccount.Domain=\"THIS-MACHINE\",Name=\"UserTwo\"",
+ },
+ ].freeze
+
+ GROUP_TWO_MEMBERS = [
+ {
+ "groupcomponent" => "Win32_Group.Domain=\"THIS-MACHINE\",Name=\"GroupOne\"",
+ "partcomponent" => "\\\\VCRS-PRODWIN05\\root\\cimv2:Win32_SystemAccount.Domain=\"THIS-MACHINE\",Name=\"GroupOne\"",
+ },
+ ].freeze
+
+ before do
+ require "wmi-lite/wmi" unless defined?(WmiLite::Wmi)
+ properties = USERS[0].map { |k, v| double(name: k) }
+ wmi_user_list = USERS.map do |user|
+ wmi_ole_object = double properties_: properties
+ user.each do |key, val|
+ allow(wmi_ole_object).to receive(:invoke).with(key).and_return(val)
+ end
+ WmiLite::Wmi::Instance.new(wmi_ole_object)
+ end
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_UserAccount WHERE LocalAccount = True")
+ .and_return(wmi_user_list)
+
+ properties = GROUPS[0].map { |k, v| double(name: k) }
+ wmi_group_list = GROUPS.map do |group|
+ wmi_ole_object = double properties_: properties
+ group.each do |key, val|
+ allow(wmi_ole_object).to receive(:invoke).with(key).and_return(val)
+ end
+ WmiLite::Wmi::Instance.new(wmi_ole_object)
+ end
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_Group WHERE LocalAccount = True")
+ .and_return(wmi_group_list)
+
+ end
+
+ def transform(user_data)
+ Hash[
+ user_data.map do |key, val|
+ [key.downcase, val]
+ end
+ ]
+ end
+
+ it "returns lower-cased passwd keys for each local user" do
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupOne'\"")
+ .and_return([])
+
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupTwo'\"")
+ .and_return([])
+
+ plugin.run
+ expect(plugin[:etc][:passwd].keys.sort).to eq(%w{userone usertwo}.sort)
+ end
+
+ it "returns preserved-case passwd entries for local users" do
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupOne'\"")
+ .and_return([])
+
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupTwo'\"")
+ .and_return([])
+
+ plugin.run
+ expect(plugin[:etc][:passwd]["userone"]).to eq(transform(USERS[0]))
+ end
+
+ it "returns lower-cased group entries for each local group" do
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupOne'\"")
+ .and_return([])
+
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupTwo'\"")
+ .and_return([])
+
+ plugin.run
+ expect(plugin[:etc][:group].keys.sort).to eq(%w{groupone grouptwo}.sort)
+ end
+
+ it "returns preserved-cased group entries for local groups" do
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupOne'\"")
+ .and_return([])
+
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupTwo'\"")
+ .and_return([])
+
+ plugin.run
+ expect(plugin[:etc][:group]["grouptwo"]).to eq(
+ transform(GROUPS[1]).merge({ "members" => [] })
+ )
+ end
+
+ it "returns members for groups" do
+ properties = GROUP_ONE_MEMBERS[0].map { |k, v| double(name: k) }
+ g1_members = GROUP_ONE_MEMBERS.map do |member|
+ wmi_ole_object = double properties_: properties
+ member.each do |key, val|
+ allow(wmi_ole_object).to receive(:invoke).with(key).and_return(val)
+ end
+ WmiLite::Wmi::Instance.new(wmi_ole_object)
+ end
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupOne'\"")
+ .and_return(g1_members)
+
+ g2_members = GROUP_TWO_MEMBERS.map do |member|
+ wmi_ole_object = double properties_: properties
+ member.each do |key, val|
+ allow(wmi_ole_object).to receive(:invoke).with(key).and_return(val)
+ end
+ WmiLite::Wmi::Instance.new(wmi_ole_object)
+ end
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupTwo'\"")
+ .and_return(g2_members)
+
plugin.run
- root = plugin[:etc][:passwd]["root"]
- expect(root["gecos"].encoding).to eq(Encoding.default_external)
+ expect(plugin[:etc][:group]["groupone"]["members"]).to eq([
+ { "name" => "UserOne", "type" => :user },
+ { "name" => "UserTwo", "type" => :user },
+ ])
+ expect(plugin[:etc][:group]["grouptwo"]["members"]).to eq([
+ { "name" => "GroupOne", "type" => :group },
+ ])
end
end
end