diff options
author | Phil Dibowitz <phil@ipom.com> | 2020-09-18 21:47:17 -0700 |
---|---|---|
committer | Phil Dibowitz <phil@ipom.com> | 2020-10-01 20:59:47 -0700 |
commit | 0af23845276bd6ce226f475b3bdd577479a2fb77 (patch) | |
tree | da31ae858037da2569c8e99fda0b0ab10dcc4400 | |
parent | 3d34f8aaf4186f7b378949947fbb68c12dee45ab (diff) | |
download | ohai-0af23845276bd6ce226f475b3bdd577479a2fb77.tar.gz |
Windows support for Passwd plugin
This adds Windows support for the passwd plugin.
It's not fast, but I did work to improve the performance dropping it
from 20 seconds to 8 seconds on a VM with only a few users and groups.
The `get-localgroupmember` is the slow part (you have to run it one
group at a time), so the more groups you have, the slower this will go.
Note that unlike the Linux/Mac variety which will pick up nonlocal users
and groups this one does not (yet). It isn't hard to add, there are the
`get-aduser` and `get-adgroup` commands (if you have joined a domain),
but this is a good start.
And yes, I'll add tests when I have a moment.
Signed-off-by: Phil Dibowitz <phil@ipom.com>
-rw-r--r-- | lib/ohai/plugins/passwd.rb | 44 | ||||
-rw-r--r-- | spec/unit/plugins/passwd_spec.rb | 255 |
2 files changed, 263 insertions, 36 deletions
diff --git a/lib/ohai/plugins/passwd.rb b/lib/ohai/plugins/passwd.rb index 4af41450..db26c610 100644 --- a/lib/ohai/plugins/passwd.rb +++ b/lib/ohai/plugins/passwd.rb @@ -12,6 +12,10 @@ 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) @@ -42,6 +46,44 @@ Ohai.plugin(:Passwd) do end collect_data(:windows) do - # Etc returns nil on Windows + unless etc + etc Mash.new + + etc[:passwd] = Mash.new + s = powershell_out("get-localuser | convertto-json") + users = JSON.parse(s.stdout) + 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 + end + end + + etc[:group] = Mash.new + s = powershell_out("get-localgroup | convertto-json") + groups = JSON.parse(s.stdout) + 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 + end + # calling this for each group is slow, but it requires + # a specific group, soooooo.... + g = powershell_out( + "get-localgroupmember -name '#{gname}' | convertto-json" + ) + out = g.stdout + if !out.empty? + gmem = JSON.parse(g.stdout) + etc[:group][gname]["members"] = gmem + else + etc[:group][gname]["members"] = [] + end + end + end end end diff --git a/spec/unit/plugins/passwd_spec.rb b/spec/unit/plugins/passwd_spec.rb index 6450e22f..482fba3a 100644 --- a/spec/unit/plugins/passwd_spec.rb +++ b/spec/unit/plugins/passwd_spec.rb @@ -16,50 +16,235 @@ # 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 + + 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 + + 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 + + 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 + + let(:group_two_info) do + [ + { + "Name" => "UserTwo", + "SID" => { + "BinaryLength" => 28, + "AccountDomainSid" => nil, + "Value" => "bar", + }, + "ObjectClass" => "User", + }, + ] + end + + 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, "")) + 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 + { + "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 + 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 + plugin.run + expect(plugin[:etc][:passwd]["userone"]).to eq(transform(user_info[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 + 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 + plugin.run + expect(plugin[:etc][:group]["grouptwo"]).to eq( + transform(group_info[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, "")) + end plugin.run - root = plugin[:etc][:passwd]["root"] - expect(root["gecos"].encoding).to eq(Encoding.default_external) + expect(plugin[:etc][:group]["groupone"]["members"]).to eq(group_one_info) + expect(plugin[:etc][:group]["grouptwo"]["members"]).to eq(group_two_info) end end end |