summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Dibowitz <phil@ipom.com>2020-09-18 21:47:17 -0700
committerPhil Dibowitz <phil@ipom.com>2020-10-01 20:59:47 -0700
commit0af23845276bd6ce226f475b3bdd577479a2fb77 (patch)
treeda31ae858037da2569c8e99fda0b0ab10dcc4400
parent3d34f8aaf4186f7b378949947fbb68c12dee45ab (diff)
downloadohai-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.rb44
-rw-r--r--spec/unit/plugins/passwd_spec.rb255
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