vines-agent 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,166 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Agent
5
+
6
+ # Provides a shell session to execute commands as a particular user. All
7
+ # commands are forked and executed in a child process to isolate them from
8
+ # the agent process. Keeping the same session open between commands allows
9
+ # stateful commands like 'cd' to work properly.
10
+ class Shell
11
+ include Vines::Log
12
+
13
+ attr_writer :permissions
14
+
15
+ # Create a new shell session to asynchronously execute commands for this
16
+ # JID. The JID is validated in the permissions Hash before executing
17
+ # commands.
18
+ def initialize(jid, permissions)
19
+ @jid, @permissions = jid, permissions
20
+ @user, @commands = allowed_users.first, EM::Queue.new
21
+ spawn(@user)
22
+ process_command_queue
23
+ end
24
+
25
+ # Queue the shell command to run as soon as the currently executing tasks
26
+ # complete. Yields the shell output to the callback block.
27
+ def run(command, &callback)
28
+ if reset?(command)
29
+ callback.call(run_built_in(command))
30
+ else
31
+ @commands.push({command: command.strip, callback: callback})
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # Schedule a queue pop on the EM thread to handle the next command.
38
+ # This guarantees in-order shell command processing while not blocking
39
+ # the EM loop, waiting for long running tasks to complete.
40
+ def process_command_queue
41
+ @commands.pop do |command|
42
+ op = proc do
43
+ if built_in?(command[:command])
44
+ run_built_in(command[:command])
45
+ else
46
+ run_in_slave(command[:command])
47
+ end
48
+ end
49
+ cb = proc do |output|
50
+ command[:callback].call(output)
51
+ process_command_queue
52
+ end
53
+ EM.defer(op, cb)
54
+ end
55
+ end
56
+
57
+ def run_in_slave(command)
58
+ log.info("Running #{command} as #{@user}")
59
+ out, err = @shell.execute(command)
60
+ output = [].tap do |arr|
61
+ arr << out if out && !out.empty?
62
+ arr << err if err && !err.empty?
63
+ end.join("\n")
64
+ output.empty? ? '-> command completed' : output
65
+ rescue
66
+ spawn(@user)
67
+ '-> restarted shell'
68
+ end
69
+
70
+ # Fork a child process in which to run a shell as this user. Return
71
+ # the slave and its remote shell proxy. The agent process must be run
72
+ # as root for the user switch to work.
73
+ def spawn(user)
74
+ log.info("Starting shell as #{user}")
75
+ @slave.shutdown(quiet: true) if @slave
76
+ Thread.new do # so em thread won't die on @slave.shutdown
77
+ slave = Slave.new(psname: "vines-session-#{user}") do
78
+ uid = Process.euid
79
+
80
+ # switch user so shell is run by non-root
81
+ passwd = Etc.getpwnam(user)
82
+ Process.egid = Process.gid = passwd.gid
83
+ Process.euid = Process.uid = passwd.uid
84
+
85
+ # fork shell as non-root user
86
+ ENV.clear
87
+ ENV['HOME'] = passwd.dir
88
+ ENV['USER'] = user
89
+ Dir.chdir(ENV['HOME'])
90
+
91
+ shell = Session::Bash::Login.new
92
+
93
+ # switch back so domain socket is owned by root
94
+ Process.euid = Process.uid = uid
95
+ shell
96
+ end
97
+ File.chmod(0700, slave.socket)
98
+ @slave, @shell = [slave, slave.object]
99
+ end.join
100
+ end
101
+
102
+ # The agent supports special, built-in "vines" commands beginning with
103
+ # 'v' that the agent executes itself, without invoking a shell. For example,
104
+ # +v user root+ will change the user account that future shell commands
105
+ # execute as.
106
+ def built_in?(command)
107
+ command.strip.start_with?('v ')
108
+ end
109
+
110
+ # Run a built-in vines command without using a shell. Return output to
111
+ # be sent back to the user.
112
+ def run_built_in(command)
113
+ _, command, *args = command.strip.split(/\s+/)
114
+ case command
115
+ when 'user' then user_command(args)
116
+ when 'reset' then reset_command(args)
117
+ else '-> not a vines command'
118
+ end
119
+ end
120
+
121
+ # Run the +v user+ built-in vines command to list or change the current
122
+ # unix account executing shell commands.
123
+ def user_command(args)
124
+ return "-> current: #{@user}\n allowed: #{allowed_users.join(', ')}" if args.empty?
125
+ return "-> usage: v user [name]" if args.size > 1
126
+ return "-> user switch not allowed" unless allowed?(args.first)
127
+ @user = args.first
128
+ spawn(@user)
129
+ "-> switched user to #{@user}"
130
+ end
131
+
132
+ def reset?(command)
133
+ v, command, *args = command.strip.split(/\s+/)
134
+ v == 'v' && command == 'reset'
135
+ end
136
+
137
+ def reset_command(args)
138
+ return "-> usage: v reset" unless args.empty?
139
+ @commands = EM::Queue.new
140
+ spawn(@user)
141
+ process_command_queue
142
+ "-> reset shell"
143
+ end
144
+
145
+ # Return true if the current JID is allowed to run commands as the given
146
+ # user name on this system.
147
+ def allowed?(user)
148
+ jids = @permissions[user] || []
149
+ valid = jids.include?(@jid) && exists?(user)
150
+ log.warn("#{@jid} denied access to #{user}") unless valid
151
+ valid
152
+ end
153
+
154
+ def exists?(user)
155
+ Etc::getpwnam(user) rescue false
156
+ end
157
+
158
+ # Return the list of unix user accounts this user is allowed to access.
159
+ def allowed_users
160
+ @permissions.select do |unix, jids|
161
+ jids.include?(@jid) && exists?(unix)
162
+ end.keys.sort
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,7 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Agent
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,115 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'vines/agent'
4
+ require 'minitest/autorun'
5
+
6
+ class ConfigTest < MiniTest::Unit::TestCase
7
+
8
+ def teardown
9
+ %w[data downloads].each do |dir|
10
+ FileUtils.remove_dir(dir) if File.exist?(dir)
11
+ end
12
+ end
13
+
14
+ def test_missing_host_raises
15
+ assert_raises(RuntimeError) do
16
+ Vines::Agent::Config.new do
17
+ # missing domain
18
+ end
19
+ end
20
+ end
21
+
22
+ def test_multiple_domains_raises
23
+ assert_raises(RuntimeError) do
24
+ Vines::Agent::Config.new do
25
+ domain 'wonderland.lit' do
26
+ upstream 'localhost', 5222
27
+ password 'secr3t'
28
+ end
29
+ domain 'verona.lit' do
30
+ upstream 'localhost', 5222
31
+ password 'secr3t'
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def test_configure
38
+ config = Vines::Agent::Config.configure do
39
+ domain 'wonderland.lit' do
40
+ upstream 'localhost', 5222
41
+ password 'secr3t'
42
+ end
43
+ end
44
+ refute_nil config
45
+ assert_same config, Vines::Agent::Config.instance
46
+ end
47
+
48
+ def test_default_download_directory
49
+ config = Vines::Agent::Config.configure do
50
+ domain 'wonderland.lit' do
51
+ password 'secr3t'
52
+ end
53
+ end
54
+ assert File.exist?('data')
55
+ end
56
+
57
+ def test_custom_download_directory
58
+ config = Vines::Agent::Config.configure do
59
+ domain 'wonderland.lit' do
60
+ password 'secr3t'
61
+ download 'downloads'
62
+ end
63
+ end
64
+ assert File.exist?('downloads')
65
+ end
66
+
67
+ def test_missing_password_raises
68
+ assert_raises(RuntimeError) do
69
+ Vines::Agent::Config.new do
70
+ domain 'wonderland.lit' do
71
+ upstream 'localhost', 5222
72
+ end
73
+ end
74
+ end
75
+ assert_raises(RuntimeError) do
76
+ Vines::Agent::Config.new do
77
+ domain 'wonderland.lit' do
78
+ upstream 'localhost', 5222
79
+ password nil
80
+ end
81
+ end
82
+ end
83
+ assert_raises(RuntimeError) do
84
+ Vines::Agent::Config.new do
85
+ domain 'wonderland.lit' do
86
+ upstream 'localhost', 5222
87
+ password ''
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ def test_invalid_log_level
94
+ assert_raises(RuntimeError) do
95
+ config = Vines::Agent::Config.new do
96
+ log 'bogus'
97
+ domain 'wonderland.lit' do
98
+ upstream 'localhost', 5222
99
+ password 'secr3t'
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ def test_valid_log_level
106
+ config = Vines::Agent::Config.new do
107
+ log :error
108
+ domain 'wonderland.lit' do
109
+ upstream 'localhost', 5222
110
+ password 'secr3t'
111
+ end
112
+ end
113
+ assert_equal Logger::ERROR, Class.new.extend(Vines::Log).log.level
114
+ end
115
+ end
@@ -0,0 +1,17 @@
1
+ require 'rake'
2
+
3
+ # Use the latest MiniTest gem instead of the buggy
4
+ # version included with Ruby 1.9.2.
5
+ gem 'minitest'
6
+
7
+ # Load the test files from the command line.
8
+
9
+ ARGV.each do |f|
10
+ next if f =~ /^-/
11
+
12
+ if f =~ /\*/
13
+ FileList[f].to_a.each { |fn| require File.expand_path(fn) }
14
+ else
15
+ require File.expand_path(f)
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vines-agent
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - David Graham
9
+ - Chris Johnson
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+
14
+ date: 2011-09-28 00:00:00 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: blather
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ~>
23
+ - !ruby/object:Gem::Version
24
+ version: 0.5.4
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: ohai
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ~>
34
+ - !ruby/object:Gem::Version
35
+ version: 0.6.4
36
+ type: :runtime
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: session
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: 3.1.0
47
+ type: :runtime
48
+ version_requirements: *id003
49
+ - !ruby/object:Gem::Dependency
50
+ name: slave
51
+ prerelease: false
52
+ requirement: &id004 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ version: 1.2.1
58
+ type: :runtime
59
+ version_requirements: *id004
60
+ - !ruby/object:Gem::Dependency
61
+ name: vines
62
+ prerelease: false
63
+ requirement: &id005 !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: "0.3"
69
+ type: :runtime
70
+ version_requirements: *id005
71
+ - !ruby/object:Gem::Dependency
72
+ name: minitest
73
+ prerelease: false
74
+ requirement: &id006 !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: "0"
80
+ type: :development
81
+ version_requirements: *id006
82
+ - !ruby/object:Gem::Dependency
83
+ name: rake
84
+ prerelease: false
85
+ requirement: &id007 !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: "0"
91
+ type: :development
92
+ version_requirements: *id007
93
+ description: |-
94
+ Vines Agent executes shell commands sent by users after
95
+ authorizing them against an access control list, provided by the Vines Services
96
+ component. Manage a server as easily as chatting with a friend.
97
+ email:
98
+ - david@negativecode.com
99
+ - chris@negativecode.com
100
+ executables:
101
+ - vines-agent
102
+ extensions: []
103
+
104
+ extra_rdoc_files: []
105
+
106
+ files:
107
+ - LICENSE
108
+ - Rakefile
109
+ - README
110
+ - bin/vines-agent
111
+ - lib/vines/agent/agent.rb
112
+ - lib/vines/agent/command/init.rb
113
+ - lib/vines/agent/command/restart.rb
114
+ - lib/vines/agent/command/start.rb
115
+ - lib/vines/agent/command/stop.rb
116
+ - lib/vines/agent/config.rb
117
+ - lib/vines/agent/connection.rb
118
+ - lib/vines/agent/shell.rb
119
+ - lib/vines/agent/version.rb
120
+ - lib/vines/agent.rb
121
+ - conf/certs/ca-bundle.crt
122
+ - conf/certs/README
123
+ - conf/config.rb
124
+ - test/config_test.rb
125
+ - test/rake_test_loader.rb
126
+ homepage: http://www.getvines.com
127
+ licenses: []
128
+
129
+ post_install_message:
130
+ rdoc_options: []
131
+
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ none: false
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: 1.9.2
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ none: false
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: "0"
146
+ requirements: []
147
+
148
+ rubyforge_project:
149
+ rubygems_version: 1.8.10
150
+ signing_key:
151
+ specification_version: 3
152
+ summary: An XMPP bot that runs shell commands on remote machines.
153
+ test_files:
154
+ - test/config_test.rb
155
+ - test/rake_test_loader.rb