diff options
author | Phil Dibowitz <phil@ipom.com> | 2020-10-01 22:25:33 -0700 |
---|---|---|
committer | Phil Dibowitz <phil@ipom.com> | 2020-10-02 20:24:49 -0700 |
commit | 75506cab7ccf6c4fb6c85c547d08ec2d0eadee4d (patch) | |
tree | 3b5eff7d297a12fd52221fdff146c3897519f1d3 | |
parent | 0af23845276bd6ce226f475b3bdd577479a2fb77 (diff) | |
download | ohai-75506cab7ccf6c4fb6c85c547d08ec2d0eadee4d.tar.gz |
Move this all to WMI
This cuts down the time an order of magnitude.
Signed-off-by: Phil Dibowitz <phil@ipom.com>
-rw-r--r-- | RELEASE_NOTES.md | 9 | ||||
-rw-r--r-- | lib/ohai/plugins/passwd.rb | 58 | ||||
-rw-r--r-- | spec/unit/plugins/passwd_spec.rb | 276 |
3 files changed, 187 insertions, 156 deletions
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 5dfe416d..3b0dc2c5 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 you are on a domain controller, you will get all domain users as domain controllers see domain users as local + * If you are not on a 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 db26c610..781aba94 100644 --- a/lib/ohai/plugins/passwd.rb +++ b/lib/ohai/plugins/passwd.rb @@ -12,10 +12,6 @@ Ohai.plugin(:Passwd) do str end - def powershell_out(ps) - Mixlib::ShellOut.new("powershell.exe", "-c", ps).run_command - end - collect_data do require "etc" unless defined?(Etc) @@ -46,42 +42,60 @@ Ohai.plugin(:Passwd) do end collect_data(:windows) do + require "wmi-lite/wmi" unless defined?(WmiLite::Wmi) + unless etc etc Mash.new + wmi = WmiLite::Wmi.new + etc[:passwd] = Mash.new - s = powershell_out("get-localuser | convertto-json") - users = JSON.parse(s.stdout) + 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 - user.each do |key, val| - etc[:passwd][uname][key.downcase] = val + 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 - s = powershell_out("get-localgroup | convertto-json") - groups = JSON.parse(s.stdout) + 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 - group.each do |key, val| - etc[:group][gname][key.downcase] = val + wmi_obj = group.wmi_ole_object + wmi_obj.properties_.each do |key| + etc[:group][gname][key.name.downcase] = group[key.name] end - # calling this for each group is slow, but it requires - # a specific group, soooooo.... - g = powershell_out( - "get-localgroupmember -name '#{gname}' | convertto-json" + + # 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}\"" ) - out = g.stdout - if !out.empty? - gmem = JSON.parse(g.stdout) - etc[:group][gname]["members"] = gmem - else - etc[:group][gname]["members"] = [] + 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 diff --git a/spec/unit/plugins/passwd_spec.rb b/spec/unit/plugins/passwd_spec.rb index 482fba3a..c5cfcf69 100644 --- a/spec/unit/plugins/passwd_spec.rb +++ b/spec/unit/plugins/passwd_spec.rb @@ -73,102 +73,90 @@ describe Ohai::System, "plugin etc" do end end - let(:user_info) do - [ - { - "Name" => "UserOne", - "FullName" => "User One", - "SID" => { - "BinaryLength" => 28, - "AccountDomainSid" => "bla", - "Value" => "blabla", - }, - "ObjectClass" => "User", - "Enabled" => false, - }, - { - "Name" => "UserTwo", - "FullName" => "User Two", - "SID" => { - "BinaryLength" => 28, - "AccountDomainSid" => "doo", - "Value" => "doodoo", - }, - "ObjectClass" => "User", - "Enabled" => true, - }, - ] - 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 - let(:group_info) do - [ - { - "Description" => "Group One", - "Name" => "GroupOne", - "SID" => { - "BinaryLength" => 16, - "AccountDomainSid" => nil, - "Value" => "foo", - }, - "ObjectClass" => "Group", - }, - { - "Description" => "Group Two", - "Name" => "GroupTwo", - "SID" => { - "BinaryLength" => 16, - "AccountDomainSid" => nil, - "Value" => "foo", - }, - "ObjectClass" => "Group", - }, - ] - end + 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 - let(:group_one_info) do - [ - { - "Name" => "UserOne", - "SID" => { - "BinaryLength" => 28, - "AccountDomainSid" => nil, - "Value" => "bar", - }, - "ObjectClass" => "User", - }, - { - "Name" => "UserTwo", - "SID" => { - "BinaryLength" => 28, - "AccountDomainSid" => nil, - "Value" => "bar", - }, - "ObjectClass" => "User", - }, - ] - end + 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 - let(:group_two_info) do - [ - { - "Name" => "UserTwo", - "SID" => { - "BinaryLength" => 28, - "AccountDomainSid" => nil, - "Value" => "bar", - }, - "ObjectClass" => "User", - }, - ] - end + 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 - expect(plugin).to receive(:powershell_out) - .with("get-localuser | convertto-json") - .and_return(mock_shell_out(0, user_info.to_json, "")) - expect(plugin).to receive(:powershell_out) - .with("get-localgroup | convertto-json") - .and_return(mock_shell_out(0, group_info.to_json, "")) + 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) @@ -180,71 +168,91 @@ describe Ohai::System, "plugin etc" do end it "returns lower-cased passwd keys for each local user" do - { - "groupone" => group_one_info.to_json, - "grouptwo" => group_two_info.to_json, - }.each do |gname, info| - expect(plugin).to receive(:powershell_out) - .with("get-localgroupmember -name '#{gname}' | convertto-json") - .and_return(mock_shell_out(0, "[]", "")) - 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([]) + + 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 - { - "groupone" => group_one_info.to_json, - "grouptwo" => group_two_info.to_json, - }.each do |gname, info| - expect(plugin).to receive(:powershell_out) - .with("get-localgroupmember -name '#{gname}' | convertto-json") - .and_return(mock_shell_out(0, "[]", "")) - 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([]) + + 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(user_info[0])) + expect(plugin[:etc][:passwd]["userone"]).to eq(transform(USERS[0])) end it "returns lower-cased group entries for each local group" do - { - "groupone" => group_one_info.to_json, - "grouptwo" => group_two_info.to_json, - }.each do |gname, info| - expect(plugin).to receive(:powershell_out) - .with("get-localgroupmember -name '#{gname}' | convertto-json") - .and_return(mock_shell_out(0, "[]", "")) - 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([]) + + 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 - { - "groupone" => group_one_info.to_json, - "grouptwo" => group_two_info.to_json, - }.each do |gname, info| - expect(plugin).to receive(:powershell_out) - .with("get-localgroupmember -name '#{gname}' | convertto-json") - .and_return(mock_shell_out(0, "[]", "")) - 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([]) + + 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(group_info[1]).merge({ "members" => [] }) + transform(GROUPS[1]).merge({ "members" => [] }) ) end it "returns members for groups" do - { - "groupone" => group_one_info.to_json, - "grouptwo" => group_two_info.to_json, - }.each do |gname, info| - expect(plugin).to receive(:powershell_out) - .with("get-localgroupmember -name '#{gname}' | convertto-json") - .and_return(mock_shell_out(0, info, "")) + 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 - expect(plugin[:etc][:group]["groupone"]["members"]).to eq(group_one_info) - expect(plugin[:etc][:group]["grouptwo"]["members"]).to eq(group_two_info) + 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 |