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.
- data/LICENSE +19 -0
- data/README +35 -0
- data/Rakefile +58 -0
- data/bin/vines-agent +94 -0
- data/conf/certs/README +15 -0
- data/conf/certs/ca-bundle.crt +3721 -0
- data/conf/config.rb +16 -0
- data/lib/vines/agent.rb +30 -0
- data/lib/vines/agent/agent.rb +24 -0
- data/lib/vines/agent/command/init.rb +55 -0
- data/lib/vines/agent/command/restart.rb +14 -0
- data/lib/vines/agent/command/start.rb +30 -0
- data/lib/vines/agent/command/stop.rb +20 -0
- data/lib/vines/agent/config.rb +106 -0
- data/lib/vines/agent/connection.rb +274 -0
- data/lib/vines/agent/shell.rb +166 -0
- data/lib/vines/agent/version.rb +7 -0
- data/test/config_test.rb +115 -0
- data/test/rake_test_loader.rb +17 -0
- metadata +155 -0
@@ -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
|
data/test/config_test.rb
ADDED
@@ -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
|