diff options
author | Tim Smith <tsmith@chef.io> | 2020-10-02 22:01:01 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-10-02 22:01:01 -0700 |
commit | e02f77f9a3036a596b111c5e20e54825a78f570e (patch) | |
tree | 878bd6ebae4bb84c7a627087daf2e738498d500a | |
parent | 6d6e6f4cd6218d2e4285897cbda1cf9c11c094a9 (diff) | |
parent | 85a7d8dd2e52409e21aca8ce30591bc5dd30af34 (diff) | |
download | ohai-e02f77f9a3036a596b111c5e20e54825a78f570e.tar.gz |
Merge pull request #1516 from jaymzh/passwd
Signed-off-by: Tim Smith <tsmith@chef.io>
-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 | 263 |
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 |