sshakery 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +12 -0
- data/lib/sshakery/auth_keys.rb +340 -0
- data/lib/sshakery/errors.rb +4 -0
- data/lib/sshakery/fs_utils.rb +131 -0
- data/lib/sshakery/version.rb +3 -0
- data/lib/sshakery.rb +14 -0
- data/sshakery.gemspec +21 -0
- data/test/fixtures/sshakery_nofail_fixture.txt +3 -0
- data/test/lib/sshakery/auth_keys_test.rb +106 -0
- data/test/lib/sshakery/fs_utils_test.rb +36 -0
- data/test/lib/sshakery/version_test.rb +7 -0
- data/test/lib/sshakery_test.rb +31 -0
- data/test/test_helper.rb +7 -0
- metadata +102 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 hattb
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Sshakery
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'sshakery'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install sshakery
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require 'rake/testtask'
|
4
|
+
Rake::TestTask.new do |t|
|
5
|
+
t.libs << 'lib/sshakery'
|
6
|
+
t.test_files = FileList[
|
7
|
+
'test/lib/*_test.rb',
|
8
|
+
'test/lib/sshakery/*_test.rb'
|
9
|
+
]
|
10
|
+
t.verbose = true
|
11
|
+
end
|
12
|
+
task :default => :test
|
@@ -0,0 +1,340 @@
|
|
1
|
+
module Sshakery
|
2
|
+
|
3
|
+
class AuthKeys
|
4
|
+
# atomic writes
|
5
|
+
require 'tempfile'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
# instance attributes
|
9
|
+
# Instance state attributed
|
10
|
+
INSTANCE_ATTRIBUTES = [ :errors,
|
11
|
+
:raw_line,
|
12
|
+
:path,
|
13
|
+
:saved
|
14
|
+
]
|
15
|
+
|
16
|
+
# attr used for auth key line (listed in order of appearance)
|
17
|
+
KEY_ATTRIBUTES =[
|
18
|
+
:command,
|
19
|
+
:permitopen,
|
20
|
+
:tunnel,
|
21
|
+
:from,
|
22
|
+
:environment,
|
23
|
+
:no_agent_forwarding,
|
24
|
+
:no_port_forwarding,
|
25
|
+
:no_pty,
|
26
|
+
:no_user_rc,
|
27
|
+
:no_X11_forwarding,
|
28
|
+
:key_type,
|
29
|
+
:key_data,
|
30
|
+
:note
|
31
|
+
]
|
32
|
+
|
33
|
+
ATTRIBUTES = INSTANCE_ATTRIBUTES+KEY_ATTRIBUTES
|
34
|
+
|
35
|
+
ATTRIBUTES.each do |attr|
|
36
|
+
attr_accessor attr
|
37
|
+
end
|
38
|
+
|
39
|
+
DEFAULTS={
|
40
|
+
:errors => []
|
41
|
+
}.freeze
|
42
|
+
|
43
|
+
# equal their name if true
|
44
|
+
BOOL_ATTRIBUTES = [
|
45
|
+
:no_agent_forwarding,
|
46
|
+
:no_port_forwarding,
|
47
|
+
:no_pty,
|
48
|
+
:no_user_rc,
|
49
|
+
:no_X11_forwarding
|
50
|
+
]
|
51
|
+
|
52
|
+
# equal their value if set
|
53
|
+
STR_ATTRIBUTES = [
|
54
|
+
:key_type,
|
55
|
+
:key_data,
|
56
|
+
:note
|
57
|
+
]
|
58
|
+
|
59
|
+
# each is equal to a joined string of their values
|
60
|
+
ARR_STR_ATTRIBUTES = [
|
61
|
+
:environment
|
62
|
+
]
|
63
|
+
|
64
|
+
# add string and substitute attr value
|
65
|
+
SUB_STR_ATTRIBUTES = {
|
66
|
+
:command=>'command="%sub%"',
|
67
|
+
:permitopen=>'permitopen="%sub%"',
|
68
|
+
:tunnel=>'tunnel="%sub%"',
|
69
|
+
:from=>'from="%sub%"'
|
70
|
+
}
|
71
|
+
|
72
|
+
# regex for matching ssh keys imported from a pub key file
|
73
|
+
TYPE_REGEX = /ssh-dss|ssh-rsa/
|
74
|
+
B64_REGEX = /[A-Za-z0-9\/\+]+={0,3}/
|
75
|
+
|
76
|
+
# additional regex for loading from auth keys file
|
77
|
+
OPTS_REGEX = {
|
78
|
+
:key_type=> /(#{TYPE_REGEX}) (?:#{B64_REGEX})/,
|
79
|
+
:key_data=> /(?:#{TYPE_REGEX}) (#{B64_REGEX})/,
|
80
|
+
:note=>/([A-Za-z0-9_\/\+@]+)\s*$/,
|
81
|
+
:command=>/command="([^"]+)"(?: |,)/,
|
82
|
+
:environment=>/([A-Z0-9]+=[^\s]+)(?: |,)/,
|
83
|
+
:from=>/from="([^"])"(?: |,)/,
|
84
|
+
:no_agent_forwarding=>/(no-agent-forwarding)(?: |,)/,
|
85
|
+
:no_port_forwarding=>/(no-port-forwarding)(?: |,)/,
|
86
|
+
:no_pty=>/(no-pty)(?: |,)/,
|
87
|
+
:no_user_rc=>/(no-user-rc)(?: |,)/,
|
88
|
+
:no_X11_forwarding=>/(no-X11-forwarding)(?: |,)/,
|
89
|
+
:permitopen=>/permitopen="([a-z0-9.]+:[\d]+)"(?: |,)/,
|
90
|
+
:tunnel=>/tunnel="(\d+)"(?: |,)/
|
91
|
+
}
|
92
|
+
|
93
|
+
ERRORS = {
|
94
|
+
:data_modulus=> {:key_data=>'public key length is not a modulus of 4'},
|
95
|
+
:data_short => {:key_data=>'public key is too short'},
|
96
|
+
:data_long => {:key_data=>'public key is too long'},
|
97
|
+
:data_char => {:key_data=>'public key contains invalid base64 characters'},
|
98
|
+
:data_nil => {:key_data=>'public key is missing'},
|
99
|
+
:type_nil => {:key_type=>'missing key type'},
|
100
|
+
:bool => 'bad value for boolean field'
|
101
|
+
}
|
102
|
+
# class instance attributes
|
103
|
+
class << self; attr_accessor :path, :temp_path end
|
104
|
+
|
105
|
+
# class methods
|
106
|
+
|
107
|
+
#return array of keys matching field
|
108
|
+
def self.find_all_by(field,value, with_regex=false)
|
109
|
+
result = []
|
110
|
+
self.all.each do |auth_key|
|
111
|
+
if with_regex
|
112
|
+
result.push auth_key if auth_key.send(field).match(value)
|
113
|
+
else
|
114
|
+
result.push auth_key if auth_key.send(field) == value
|
115
|
+
end
|
116
|
+
end
|
117
|
+
return result
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.destroy(auth_key)
|
121
|
+
self.write auth_key, destroy=true
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.write(auth_key, destroy=false)
|
125
|
+
lines = []
|
126
|
+
FsUtils.atomic_lock(:path=>self.path) do |f|
|
127
|
+
f.each_line do |line|
|
128
|
+
key=self.new(:raw_line => line )
|
129
|
+
|
130
|
+
if key.key_data == auth_key.key_data
|
131
|
+
lines.push auth_key.gen_raw_line if destroy==false
|
132
|
+
else
|
133
|
+
lines.push line
|
134
|
+
end
|
135
|
+
end
|
136
|
+
f.rewind
|
137
|
+
f.truncate(0)
|
138
|
+
lines.each do |line|
|
139
|
+
f.puts line
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# return array of all keys in this file
|
145
|
+
def self.all
|
146
|
+
result = []
|
147
|
+
File.readlines(self.path).each do |line|
|
148
|
+
result.push( self.new(:raw_line => line ))
|
149
|
+
end
|
150
|
+
return result
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
# method attributes
|
155
|
+
def initialize(args={})
|
156
|
+
ATTRIBUTES.each do |attr|
|
157
|
+
instance_variable_set("@#{attr}", args.has_key?( attr ) ? args[attr] : nil )
|
158
|
+
end
|
159
|
+
|
160
|
+
unless self.raw_line.nil?
|
161
|
+
self.load_raw_line
|
162
|
+
self.saved = true
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# instantiate object based on contents of raw_line
|
167
|
+
def load_raw_line
|
168
|
+
self.raw_line.chomp!
|
169
|
+
OPTS_REGEX.each do |xfield,pattern|
|
170
|
+
field = "@#{xfield}"
|
171
|
+
m= self.raw_line.match pattern
|
172
|
+
next if m.nil?
|
173
|
+
#p "#{field} => #{m.inspect}"
|
174
|
+
if BOOL_ATTRIBUTES.include? xfield
|
175
|
+
self.instance_variable_set(field, true)
|
176
|
+
next
|
177
|
+
end
|
178
|
+
|
179
|
+
if STR_ATTRIBUTES.include? xfield
|
180
|
+
self.instance_variable_set(field, m[1])
|
181
|
+
next
|
182
|
+
end
|
183
|
+
|
184
|
+
if ARR_STR_ATTRIBUTES.include? xfield
|
185
|
+
self.instance_variable_set(field, m.to_a)
|
186
|
+
next
|
187
|
+
end
|
188
|
+
|
189
|
+
if SUB_STR_ATTRIBUTES.include? xfield
|
190
|
+
self.instance_variable_set(field, m[1])
|
191
|
+
next
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def all_no=(val)
|
198
|
+
BOOL_ATTRIBUTES.each do |attr|
|
199
|
+
self.instance_variable_set("@#{attr}",val)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
# return string representation of what attr will look like in auth file
|
205
|
+
def raw_getter field
|
206
|
+
val = self.instance_variable_get("@#{field}")
|
207
|
+
return nil if val.nil? == true || val == false
|
208
|
+
|
209
|
+
if BOOL_ATTRIBUTES.include? field
|
210
|
+
return field.to_s.gsub '_', '-'
|
211
|
+
end
|
212
|
+
|
213
|
+
if STR_ATTRIBUTES.include? field
|
214
|
+
return val
|
215
|
+
end
|
216
|
+
|
217
|
+
if ARR_STR_ATTRIBUTES.include? field && val.empty? == false
|
218
|
+
return val.join ' '
|
219
|
+
end
|
220
|
+
|
221
|
+
if SUB_STR_ATTRIBUTES.include? field
|
222
|
+
return SUB_STR_ATTRIBUTES[field].sub '%sub%', val
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# validate and add a key to authorized keys file
|
227
|
+
def save
|
228
|
+
return false if not self.valid?
|
229
|
+
return self.class.write(self)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Raise an error if save doest pass validations
|
233
|
+
def save!
|
234
|
+
unless self.save
|
235
|
+
raise Sshakery::Errors::RecordInvalid.new 'Errors preventing save'
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def destroy
|
240
|
+
return false if not self.saved?
|
241
|
+
return self.class.destroy self
|
242
|
+
end
|
243
|
+
|
244
|
+
# construct line for file
|
245
|
+
def gen_raw_line
|
246
|
+
return nil unless self.valid?
|
247
|
+
line = ''
|
248
|
+
data = []
|
249
|
+
SUB_STR_ATTRIBUTES.each do |field,field_regex|
|
250
|
+
val = self.raw_getter field
|
251
|
+
data.push val if val.nil? == false
|
252
|
+
end
|
253
|
+
unless data.empty?
|
254
|
+
line = "#{data.join ' ,'}"
|
255
|
+
end
|
256
|
+
|
257
|
+
data = []
|
258
|
+
BOOL_ATTRIBUTES.each do |field|
|
259
|
+
val = self.raw_getter field
|
260
|
+
data.push val if val.nil? == false
|
261
|
+
end
|
262
|
+
unless data.empty?
|
263
|
+
if line == ''
|
264
|
+
line += "#{data.join ','} "
|
265
|
+
else
|
266
|
+
line += ",#{data.join ','} "
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
data = []
|
271
|
+
ARR_STR_ATTRIBUTES.each do |field|
|
272
|
+
val = self.raw_getter field
|
273
|
+
data.push val if val.nil? == false
|
274
|
+
end
|
275
|
+
unless data.empty?
|
276
|
+
if line == ''
|
277
|
+
line += "#{data.join ','} "
|
278
|
+
else
|
279
|
+
line += ", #{data.join ','} "
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
data = []
|
284
|
+
STR_ATTRIBUTES.each do |field|
|
285
|
+
val = self.raw_getter field
|
286
|
+
data.push val if val.nil? == false
|
287
|
+
end
|
288
|
+
line += data.join ' '
|
289
|
+
return line
|
290
|
+
end
|
291
|
+
|
292
|
+
def valid?
|
293
|
+
self.errors = []
|
294
|
+
|
295
|
+
BOOL_ATTRIBUTES.each do |field|
|
296
|
+
val = self.raw_getter field
|
297
|
+
unless val.nil? == true || val == true || val == false
|
298
|
+
self.errors.push field=>ERRORS[:bool]
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
if self.key_data.nil?:
|
303
|
+
self.errors.push ERRORS[:data_nil]
|
304
|
+
return false
|
305
|
+
end
|
306
|
+
|
307
|
+
if self.key_type.nil?:
|
308
|
+
self.errors.push ERRORS[:type_nil]
|
309
|
+
return false
|
310
|
+
end
|
311
|
+
|
312
|
+
if not self.key_data.match "^#{B64_REGEX}$":
|
313
|
+
self.errors.push ERRORS[:data_char]
|
314
|
+
end
|
315
|
+
|
316
|
+
if self.key_data.size < 30:
|
317
|
+
self.errors.push ERRORS[:data_short]
|
318
|
+
end
|
319
|
+
|
320
|
+
if self.key_data.size > 1000:
|
321
|
+
self.errors.push ERRORS[:data_long]
|
322
|
+
end
|
323
|
+
|
324
|
+
if self.key_data.size % 4 != 0:
|
325
|
+
self.errors.push ERRORS[:data_modulus]
|
326
|
+
end
|
327
|
+
|
328
|
+
return self.errors.empty?
|
329
|
+
end
|
330
|
+
|
331
|
+
def saved?
|
332
|
+
return false if not self.valid?
|
333
|
+
return self.saved
|
334
|
+
end
|
335
|
+
|
336
|
+
end
|
337
|
+
|
338
|
+
|
339
|
+
|
340
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
|
2
|
+
module Sshakery::FsUtils
|
3
|
+
require 'tempfile' unless defined?(Tempfile)
|
4
|
+
require 'fileutils' unless defined?(FileUtils)
|
5
|
+
|
6
|
+
# Write to file atomically while maintaining an exclusive lock
|
7
|
+
# create unused lock file and lock it
|
8
|
+
# create temp file
|
9
|
+
# copy file to temp file and yield temp
|
10
|
+
# atomic write temp
|
11
|
+
# release lock file
|
12
|
+
# - cant lock actual file as mv operation breaks lock
|
13
|
+
# - exclusive lock only works if all processes use the same
|
14
|
+
# lock file (this will happen by default)
|
15
|
+
# - atomic writes by moving file
|
16
|
+
def self.atomic_lock(opts={:path=>nil,:lock_name=>nil}, &block)
|
17
|
+
file_name = opts[:path]
|
18
|
+
opts[:lock_name] = file_name+'.lockfile' unless opts[:lock_name]
|
19
|
+
lock_name = opts[:lock_name]
|
20
|
+
|
21
|
+
# create lock_file if it doesnt exist
|
22
|
+
FileUtils.touch(lock_name)
|
23
|
+
self.lock_file(lock_name) do |f|
|
24
|
+
# write details of lock
|
25
|
+
f.truncate 0
|
26
|
+
f.puts self.lock_info
|
27
|
+
f.flush
|
28
|
+
|
29
|
+
# yield for atomic writes
|
30
|
+
self.atomic_write(file_name) do |temp_file|
|
31
|
+
yield temp_file
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Lock a file for a block so only one thread/process can modify it at a time.
|
37
|
+
def self.lock_file(file_name, &block)
|
38
|
+
f = File.open(file_name, 'r+')
|
39
|
+
begin
|
40
|
+
f.flock File::LOCK_EX
|
41
|
+
yield f
|
42
|
+
ensure
|
43
|
+
f.flock File::LOCK_UN unless f.nil?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# aquire shared lock for reading a file
|
48
|
+
def self.read(file_name, &block)
|
49
|
+
f = File.open(file_name, 'r')
|
50
|
+
f.flock File::LOCK_SH
|
51
|
+
puts "sh locked #{file_name}"
|
52
|
+
yield f
|
53
|
+
ensure
|
54
|
+
puts "sh unlocked #{file_name}"
|
55
|
+
f.flock File::LOCK_UN
|
56
|
+
end
|
57
|
+
|
58
|
+
# copied from:
|
59
|
+
# https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/file/atomic.rb
|
60
|
+
|
61
|
+
# Write to a file atomically. Useful for situations where you don't
|
62
|
+
# want other processes or threads to see half-written files.
|
63
|
+
#
|
64
|
+
# File.atomic_write('important.file') do |file|
|
65
|
+
# file.write('hello')
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# If your temp directory is not on the same filesystem as the file you're
|
69
|
+
# trying to write, you can provide a different temporary directory.
|
70
|
+
#
|
71
|
+
# File.atomic_write('/data/something.important', '/data/tmp') do |file|
|
72
|
+
# file.write('hello')
|
73
|
+
# end
|
74
|
+
def self.atomic_write(file_name, temp_dir = Dir.tmpdir)
|
75
|
+
temp_file = Tempfile.new(File.basename(file_name), temp_dir)
|
76
|
+
temp_file.binmode
|
77
|
+
FileUtils.cp(file_name,temp_file.path)
|
78
|
+
yield temp_file
|
79
|
+
temp_file.close
|
80
|
+
|
81
|
+
if File.exists?(file_name)
|
82
|
+
# Get original file permissions
|
83
|
+
old_stat = File.stat(file_name)
|
84
|
+
else
|
85
|
+
# If not possible, probe which are the default permissions in the
|
86
|
+
# destination directory.
|
87
|
+
old_stat = probe_stat_in(dirname(file_name))
|
88
|
+
end
|
89
|
+
|
90
|
+
# Overwrite original file with temp file
|
91
|
+
FileUtils.mv(temp_file.path, file_name)
|
92
|
+
|
93
|
+
# Set correct permissions on new file
|
94
|
+
begin
|
95
|
+
File.chown(old_stat.uid, old_stat.gid, file_name)
|
96
|
+
# This operation will affect filesystem ACL's
|
97
|
+
File.chmod(old_stat.mode, file_name)
|
98
|
+
rescue Errno::EPERM
|
99
|
+
# Changing file ownership failed, moving on.
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Private utility method.
|
104
|
+
def self.probe_stat_in(dir) #:nodoc:
|
105
|
+
basename = [
|
106
|
+
'.permissions_check',
|
107
|
+
Thread.current.object_id,
|
108
|
+
Process.pid,
|
109
|
+
rand(1000000)
|
110
|
+
].join('.')
|
111
|
+
|
112
|
+
file_name = join(dir, basename)
|
113
|
+
FileUtils.touch(file_name)
|
114
|
+
File.stat(file_name)
|
115
|
+
ensure
|
116
|
+
FileUtils.rm_f(file_name) if file_name
|
117
|
+
end
|
118
|
+
|
119
|
+
# lock file details to write to disk
|
120
|
+
def self.lock_info
|
121
|
+
return [
|
122
|
+
'Sshakery-lockfile',
|
123
|
+
Thread.current.object_id,
|
124
|
+
Process.pid,
|
125
|
+
Time.now.to_i,
|
126
|
+
rand(1000000)
|
127
|
+
].join(' ')
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
data/lib/sshakery.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "sshakery/version"
|
2
|
+
require "sshakery/fs_utils"
|
3
|
+
require "sshakery/auth_keys"
|
4
|
+
require "sshakery/errors"
|
5
|
+
|
6
|
+
module Sshakery
|
7
|
+
# instantiate a new Authkey class
|
8
|
+
def self.new path
|
9
|
+
new = Class.new(AuthKeys)
|
10
|
+
new.path = path
|
11
|
+
new.temp_path = 'sshakery.temp'
|
12
|
+
return new
|
13
|
+
end
|
14
|
+
end
|
data/sshakery.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'sshakery/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "sshakery"
|
8
|
+
gem.version = Sshakery::VERSION
|
9
|
+
gem.authors = ["hattwj"]
|
10
|
+
gem.email = ["hattwj@yahoo.com"]
|
11
|
+
gem.description = %q{A ruby gem for manipulating OpenSSH authorized_keys files}
|
12
|
+
gem.summary = %q{SSHakery is a ruby gem for manipulating OpenSSH authorized_keys files. It features file locking, backups (todo), and atomic writes}
|
13
|
+
gem.homepage = "https://github.com/hattwj/sshakery"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_development_dependency("minitest", "~> 4.1.0")
|
21
|
+
end
|
@@ -0,0 +1,3 @@
|
|
1
|
+
no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-rsa AAAAB3NzaC1ycAAAADAQABAAAAgQDPnqYjTcgGQDeMfzSvfdxIop03nsL+2W9csY3AvyjZrPuoqzcw== minitron_rsa
|
2
|
+
command="ls" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCfrOeOZ0OynDubOdonHUmHZG18LA7PfauD6gL2sFHp2/RMG3IxjN7T5BswpzDcwDWec0W7gDThlBN7K28fGekSNwnXvyI1E4ORdJ+u51PAeTi+HdYCqVZok3aQoEW4Kx4RBSW47Me7UnsoMjtC44UfxGpxnvoAXEq/YyiYyIr8nsk98D8sHsgZGpvofwblKXM5nhTvtR/pZo7r49knQB6I+5rrDr ubuntu@localhost
|
3
|
+
command="fortune" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDsBWq5F1n4FcZILZt42B6NthVrfGo0whFfSbyPuc/wfQcACpOy8EeAsCIGw0m+pDF8KlmDhYJhC/DGv4zXqk6yO+X3n5x+zfJY4AL1bu72kBnOuXbPhiDfmoBmcApbHDVJnzhRzd8sWv6qLd7bF+Dd None
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'test/test_helper'
|
2
|
+
|
3
|
+
describe Sshakery::AuthKeys do
|
4
|
+
# create a copy of test data to manipulate
|
5
|
+
before do
|
6
|
+
@errors = Sshakery::AuthKeys::ERRORS
|
7
|
+
@temp = Tempfile.new('nofail')
|
8
|
+
src = "#{$dir}/fixtures/sshakery_nofail_fixture.txt"
|
9
|
+
FileUtils.cp src, @temp.path
|
10
|
+
@keys = Sshakery.new(@temp.path)
|
11
|
+
@key = @keys.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# close temp file (should autoremove)
|
15
|
+
after do
|
16
|
+
@temp.close
|
17
|
+
@temp.unlink
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "class behavior" do
|
21
|
+
it "must be defined" do
|
22
|
+
Sshakery::AuthKeys.wont_be_nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# instance behavior
|
27
|
+
it "must not validate when empty" do
|
28
|
+
key=@keys.new
|
29
|
+
key.valid?.must_equal false
|
30
|
+
key.errors.wont_be_empty
|
31
|
+
end
|
32
|
+
|
33
|
+
it "must not save when empty" do
|
34
|
+
key=@keys.new
|
35
|
+
key.save.must_equal false
|
36
|
+
end
|
37
|
+
|
38
|
+
it "must raise error on save! with invalid data" do
|
39
|
+
key = @keys.new
|
40
|
+
failed_val = lambda { key.save! }
|
41
|
+
failed_val.must_raise Sshakery::Errors::RecordInvalid
|
42
|
+
error = failed_val.call rescue $!
|
43
|
+
error.message.must_include "Errors preventing save"
|
44
|
+
end
|
45
|
+
|
46
|
+
it "must have errors when generated line is incomplete" do
|
47
|
+
key = @keys.new
|
48
|
+
key.all_no = true
|
49
|
+
key.gen_raw_line.must_be_nil
|
50
|
+
key.errors.wont_be_empty
|
51
|
+
end
|
52
|
+
|
53
|
+
it "must accept valid b64 key_data" do
|
54
|
+
key = @keys.new
|
55
|
+
key.key_type = 'ssh-rsa'
|
56
|
+
key.key_data = 'badfhk55'*20
|
57
|
+
key.valid?.must_equal true
|
58
|
+
end
|
59
|
+
|
60
|
+
it "must reject invalid b64 characters" do
|
61
|
+
key = @keys.new
|
62
|
+
key.key_type = 'ssh-rsa'
|
63
|
+
key.key_data = 'baaAa$@'*40
|
64
|
+
key.valid?.must_equal false
|
65
|
+
key.errors.include?(@errors[:data_char]).must_equal true
|
66
|
+
key.key_data = 'ba ,a$@'*40
|
67
|
+
key.valid?.must_equal false
|
68
|
+
key.errors.include?(@errors[:data_char]).must_equal true
|
69
|
+
end
|
70
|
+
|
71
|
+
it "must only accept b64 modulus 4 data" do
|
72
|
+
key = @keys.new
|
73
|
+
key.key_type = 'ssh-rsa'
|
74
|
+
key.key_data = 'baa'*40
|
75
|
+
key.valid?.must_equal true
|
76
|
+
key.key_data = 'baa'*41
|
77
|
+
key.valid?.must_equal false
|
78
|
+
key.errors.include?(@errors[:data_modulus]).must_equal true
|
79
|
+
end
|
80
|
+
|
81
|
+
it "must reject short key_data" do
|
82
|
+
key = @keys.all[0]
|
83
|
+
key.key_data = 'baaAa12'*4
|
84
|
+
key.valid?.must_equal false
|
85
|
+
key.errors.include?(@errors[:data_short]).must_equal true
|
86
|
+
end
|
87
|
+
|
88
|
+
it "must reject long key_data" do
|
89
|
+
key = @keys.all[0]
|
90
|
+
key.key_data = 'baaAa123'*400
|
91
|
+
key.valid?.must_equal false
|
92
|
+
key.errors.include?(@errors[:data_long]).must_equal true
|
93
|
+
end
|
94
|
+
|
95
|
+
it "must reject invalid boolean options" do
|
96
|
+
instance = @keys.new
|
97
|
+
Sshakery::AuthKeys::BOOL_ATTRIBUTES.each do |attr|
|
98
|
+
key = @keys.all[0]
|
99
|
+
puts key.key_data.size
|
100
|
+
key.instance_variable_set("@#{attr}",'bad_data')
|
101
|
+
key.valid?.must_equal false
|
102
|
+
key.errors.include?(attr=>@errors[:bool]).must_equal true
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'test/test_helper'
|
2
|
+
|
3
|
+
describe Sshakery::FsUtils do
|
4
|
+
it "must lock for writes" do
|
5
|
+
# a large offset to create large writes
|
6
|
+
offset = 10**600
|
7
|
+
|
8
|
+
#test file for testing atomic writes and locking
|
9
|
+
temp = Tempfile.new 'flock'
|
10
|
+
ts=[]
|
11
|
+
(1..50).each do |i|
|
12
|
+
t =Thread.new{
|
13
|
+
sleep rand/3
|
14
|
+
# update a counter using atomic writes and a file lock
|
15
|
+
Sshakery::FsUtils.atomic_lock( :path=>temp.path, :lock_path=>'./hhh' ) do |f|
|
16
|
+
(1..50).each do |j|
|
17
|
+
f.rewind
|
18
|
+
val = f.read.to_i
|
19
|
+
#write number to file
|
20
|
+
val = val>0 ? val+1 : offset+1
|
21
|
+
f.rewind
|
22
|
+
f.truncate f.pos
|
23
|
+
f.write "#{val}\n"
|
24
|
+
f.flush
|
25
|
+
f.rewind
|
26
|
+
end
|
27
|
+
end
|
28
|
+
}
|
29
|
+
ts.push t
|
30
|
+
end
|
31
|
+
ts.each{|t| t.join}
|
32
|
+
File.open(temp.path).read.to_i.must_equal offset+2500
|
33
|
+
temp.close
|
34
|
+
temp.unlink
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'test/test_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
describe Sshakery do
|
6
|
+
|
7
|
+
# create a copy of test data to manipulate
|
8
|
+
before do
|
9
|
+
@temp = Tempfile.new('nofail')
|
10
|
+
src = "#{$dir}/fixtures/sshakery_nofail_fixture.txt"
|
11
|
+
FileUtils.cp src, @temp.path
|
12
|
+
@keys = Sshakery.new(@temp.path)
|
13
|
+
end
|
14
|
+
|
15
|
+
# close temp file (should autoremove)
|
16
|
+
after do
|
17
|
+
@temp.close
|
18
|
+
@temp.unlink
|
19
|
+
end
|
20
|
+
|
21
|
+
it "must load an authorized_keys file" do
|
22
|
+
@keys.all.size.wont_be_nil
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
it "must be searchable" do
|
27
|
+
@keys.find_all_by(:command,'ls').size.must_equal 1
|
28
|
+
@keys.find_all_by(:no_X11_forwarding,true).size.must_equal 1
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sshakery
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- hattwj
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-11-02 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: minitest
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 59
|
29
|
+
segments:
|
30
|
+
- 4
|
31
|
+
- 1
|
32
|
+
- 0
|
33
|
+
version: 4.1.0
|
34
|
+
type: :development
|
35
|
+
version_requirements: *id001
|
36
|
+
description: A ruby gem for manipulating OpenSSH authorized_keys files
|
37
|
+
email:
|
38
|
+
- hattwj@yahoo.com
|
39
|
+
executables: []
|
40
|
+
|
41
|
+
extensions: []
|
42
|
+
|
43
|
+
extra_rdoc_files: []
|
44
|
+
|
45
|
+
files:
|
46
|
+
- .gitignore
|
47
|
+
- Gemfile
|
48
|
+
- LICENSE.txt
|
49
|
+
- README.md
|
50
|
+
- Rakefile
|
51
|
+
- lib/sshakery.rb
|
52
|
+
- lib/sshakery/auth_keys.rb
|
53
|
+
- lib/sshakery/errors.rb
|
54
|
+
- lib/sshakery/fs_utils.rb
|
55
|
+
- lib/sshakery/version.rb
|
56
|
+
- sshakery.gemspec
|
57
|
+
- test/fixtures/sshakery_nofail_fixture.txt
|
58
|
+
- test/lib/sshakery/auth_keys_test.rb
|
59
|
+
- test/lib/sshakery/fs_utils_test.rb
|
60
|
+
- test/lib/sshakery/version_test.rb
|
61
|
+
- test/lib/sshakery_test.rb
|
62
|
+
- test/test_helper.rb
|
63
|
+
homepage: https://github.com/hattwj/sshakery
|
64
|
+
licenses: []
|
65
|
+
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
hash: 3
|
77
|
+
segments:
|
78
|
+
- 0
|
79
|
+
version: "0"
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
hash: 3
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
requirements: []
|
90
|
+
|
91
|
+
rubyforge_project:
|
92
|
+
rubygems_version: 1.8.15
|
93
|
+
signing_key:
|
94
|
+
specification_version: 3
|
95
|
+
summary: SSHakery is a ruby gem for manipulating OpenSSH authorized_keys files. It features file locking, backups (todo), and atomic writes
|
96
|
+
test_files:
|
97
|
+
- test/fixtures/sshakery_nofail_fixture.txt
|
98
|
+
- test/lib/sshakery/auth_keys_test.rb
|
99
|
+
- test/lib/sshakery/fs_utils_test.rb
|
100
|
+
- test/lib/sshakery/version_test.rb
|
101
|
+
- test/lib/sshakery_test.rb
|
102
|
+
- test/test_helper.rb
|