sshakery 0.0.2
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/.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
|