vines-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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