Skip to content

Commit

Permalink
Winrm elevated privilege (#7)
Browse files Browse the repository at this point in the history
* Added winrm-elevated gem to solve wirnrm AuthenticationError

* Piloting Fixes

* review changes with new test cases

* Updated README file

* Monkey patched elevated shells to resolve issue

Co-authored-by: sr070633 <[email protected]>
  • Loading branch information
saravananradhakrishnan and sr070633 authored Jun 9, 2022
1 parent 2cd568b commit b3f4a59
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 16 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## [Unreleased]

## [0.4.0] - 2022-06-02
* Added winrm-elevated gem to solve wirnrm AuthenticationError

## [0.3.0] - 2022-02-18
* Add retries to SSH SCP transactions

Expand Down
12 changes: 9 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
PATH
remote: .
specs:
remotus (0.3.0)
remotus (0.4.0)
connection_pool (~> 2.2)
net-scp (~> 3.0)
net-ssh (~> 6.1)
winrm (~> 2.3)
winrm-elevated (~> 1.2)
winrm-fs (~> 1.3)

GEM
Expand All @@ -19,11 +20,12 @@ GEM
ffi (1.15.5)
gssapi (1.3.1)
ffi (>= 1.0.1)
gyoku (1.3.1)
gyoku (1.4.0)
builder (>= 2.1.2)
rexml (~> 3.0)
httpclient (2.8.3)
little-plugger (1.1.4)
logging (2.3.0)
logging (2.3.1)
little-plugger (~> 1.1)
multi_json (~> 1.14)
multi_json (1.15.0)
Expand Down Expand Up @@ -80,6 +82,10 @@ GEM
logging (>= 1.6.1, < 3.0)
nori (~> 2.0)
rubyntlm (~> 0.6.0, >= 0.6.3)
winrm-elevated (1.2.3)
erubi (~> 1.8)
winrm (~> 2.0)
winrm-fs (~> 1.0)
winrm-fs (1.3.5)
erubi (~> 1.8)
logging (>= 1.6.1, < 3.0)
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ result.exit_code
# Run a command on the remote host with sudo (Linux only, requires password to be specified)
result = connection.run("ls /root", sudo: true)

# Run a command on the remote host with elevated shell privilege
result = connection.run("ipconfg", shell: :elevated)

# Run a script on the remote host
connection.run_script("/local/script.sh", "/remote/path/script.sh")

Expand Down
24 changes: 24 additions & 0 deletions lib/remotus/core_ext/elevated.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

require "winrm-elevated"

module Remotus
# Core Ruby extensions
module CoreExt
# Elevated extension module
module Elevated
unless method_defined?(:connection_opts)
#
# Returns a hash into a safe method name that can be used for instance variables
#
# @return [Hash] Method name
#
def connection_opts
@shell.connection_opts
end
end
end
end
end

WinRM::Shells::Elevated.include(Remotus::CoreExt::Elevated)
2 changes: 1 addition & 1 deletion lib/remotus/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

module Remotus
# Remotus gem version
VERSION = "0.3.0"
VERSION = "0.4.0"
end
29 changes: 20 additions & 9 deletions lib/remotus/winrm_connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
require "remotus"
require "remotus/result"
require "remotus/auth"
require "remotus/core_ext/elevated"
require "winrm"
require "winrm-elevated"
require "winrm-fs"

module Remotus
Expand All @@ -21,6 +23,9 @@ class WinrmConnection
# @return [String] host hostname
attr_reader :host

# @return [String] shell type
attr_reader :shell

# @return [Remotus::HostPool] host_pool associated host pool
attr_reader :host_pool

Expand All @@ -38,6 +43,7 @@ def initialize(host, port = REMOTE_PORT, host_pool: nil)
@host = host
@port = port
@host_pool = host_pool
@shell = :powershell
end

#
Expand Down Expand Up @@ -69,14 +75,17 @@ def base_connection(reload: false)

#
# Retrieves/creates the WinRM shell connection for the host
#
# @param [symbol] shell connection shell type, defaults to :powershell
# If the connection already exists, the existing connection will be retrieved
#
# @return [WinRM::Shells::Powershell] remote connection
# @return [WinRM::Shells::Powershell, WinRM::Shells::Elevated] remote connection
#
def connection
return @connection unless restart_connection?
def connection(shell = :powershell)
return @connection unless restart_connection?(shell: shell)

@connection = base_connection(reload: true).shell(:powershell)
@shell = shell
@connection = base_connection(reload: true).shell(@shell)
end

#
Expand All @@ -93,13 +102,14 @@ def port_open?
#
# @param [String] command command to run
# @param [Array] args command arguments
# @param [Hash] _options unused command options
# @param [Hash] options command options
# @param options [Symbol] :shell shell type to use for the connection
#
# @return [Remotus::Result] result describing the stdout, stderr, and exit status of the command
#
def run(command, *args, **_options)
def run(command, *args, **options)
command = "#{command}#{args.empty? ? "" : " "}#{args.join(" ")}"
run_result = connection.run(command)
run_result = options[:shell].nil? ? connection.run(command) : connection(options[:shell]).run(command)
Remotus::Result.new(command, run_result.stdout, run_result.stderr, run_result.output, run_result.exitcode)
rescue WinRM::WinRMAuthorizationError => e
raise Remotus::AuthenticationError, e.to_s
Expand Down Expand Up @@ -171,7 +181,7 @@ def file_exist?(remote_path, **_options)
# @return [Boolean] whether to restart the current base connection
#
def restart_base_connection?
return restart_connection? if @connection
return restart_connection?(shell: @shell) if @connection
return true unless @base_connection
return true if @host != @base_connection.instance_values["connection_opts"][:endpoint].scan(%r{//(.*):}).flatten.first
return true if Remotus::Auth.credential(self).user != @base_connection.instance_values["connection_opts"][:user]
Expand All @@ -185,8 +195,9 @@ def restart_base_connection?
#
# @return [Boolean] whether to restart the current connection
#
def restart_connection?
def restart_connection?(**options)
return true unless @connection
return true if shell && !options[:shell].casecmp?(@shell)
return true if @host != @connection.connection_opts[:endpoint].scan(%r{//(.*):}).flatten.first
return true if Remotus::Auth.credential(self).user != @connection.connection_opts[:user]
return true if Remotus::Auth.credential(self).password != @connection.connection_opts[:password]
Expand Down
1 change: 1 addition & 0 deletions remotus.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Gem::Specification.new do |spec|
spec.add_dependency "net-scp", "~> 3.0"
spec.add_dependency "net-ssh", "~> 6.1"
spec.add_dependency "winrm", "~> 2.3"
spec.add_dependency "winrm-elevated", "~> 1.2"
spec.add_dependency "winrm-fs", "~> 1.3"

# Development dependencies
Expand Down
21 changes: 21 additions & 0 deletions spec/remotus/core_ext/elevated_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

RSpec.describe Remotus::CoreExt::Elevated do
let(:connection_opts) do
{
user: "username",
password: "password",
domain: "domain_name"
}
end

let(:winrm_elevated_connection) do
double(WinRM::Connection, shell: double(WinRM::Shells::Elevated), connection_opts: connection_opts)
end

describe "#connection_opts" do
it "returns connection_opts object" do
expect(winrm_elevated_connection.connection_opts).to eq(connection_opts)
end
end
end
37 changes: 34 additions & 3 deletions spec/remotus/winrm_connection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
double(WinRM::Connection, shell: double(WinRM::Shells::Powershell))
end

let(:winrm_elevated_connection) do
double(WinRM::Connection, shell: double(WinRM::Shells::Elevated))
end

let(:winrm_file_manager) do
double(WinRM::FS::FileManager)
end
Expand Down Expand Up @@ -49,7 +53,7 @@
end

describe "#base_connection" do
it "creates the base connection" do
it "creates the base connection with powershell privilege" do
expect(WinRM::Connection).to receive(:new).with(
endpoint: "http://#{host}:5985/wsman",
transport: :negotiate,
Expand All @@ -58,10 +62,20 @@
).and_return(winrm_connection)
subject.base_connection
end

it "creates the base connection with elevated privilege" do
expect(WinRM::Connection).to receive(:new).with(
endpoint: "http://#{host}:5985/wsman",
transport: :negotiate,
user: cred.user,
password: cred.password
).and_return(winrm_elevated_connection)
subject.base_connection
end
end

describe "#connection" do
it "creates the connection shell" do
it "creates the connection shell with powershell privilege" do
expect(WinRM::Connection).to receive(:new).with(
endpoint: "http://#{host}:5985/wsman",
transport: :negotiate,
Expand All @@ -70,6 +84,16 @@
).and_return(winrm_connection)
subject.connection
end

it "creates the connection shell with elevated privilege" do
expect(WinRM::Connection).to receive(:new).with(
endpoint: "http://#{host}:5985/wsman",
transport: :negotiate,
user: cred.user,
password: cred.password
).and_return(winrm_elevated_connection)
subject.connection
end
end

describe "#port_open?" do
Expand All @@ -80,12 +104,19 @@
end

describe "#run" do
it "runs the command over WinRM" do
it "runs the command over WinRM with powershell" do
expect(subject).to receive(:base_connection).and_return(winrm_connection)
expect(winrm_connection.shell).to receive(:run).with("dir").and_return(winrm_result)
result = subject.run("dir")
expect(result.command).to eq("dir")
end

it "runs the command over WinRM with elevated" do
expect(subject).to receive(:base_connection).and_return(winrm_elevated_connection)
expect(winrm_elevated_connection.shell).to receive(:run).with("dir").and_return(winrm_result)
result = subject.run("dir")
expect(result.command).to eq("dir")
end
end

describe "#run_script" do
Expand Down

0 comments on commit b3f4a59

Please sign in to comment.