sshakery 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sshakery
2
2
 
3
- TODO: Write a gem description
3
+ Manipulate authorized_keys files
4
4
 
5
5
  ## Installation
6
6
 
@@ -18,7 +18,20 @@ Or install it yourself as:
18
18
 
19
19
  ## Usage
20
20
 
21
- TODO: Write usage instructions here
21
+ require 'sshakery'
22
+
23
+ # instantiate key file object
24
+ keys = Sshakery.new '/path/to/.ssh/authorized_keys'
25
+
26
+ # return array of all keys
27
+ all_keys = keys.all
28
+
29
+ # return only keys where the note == 'foo'
30
+ somekeys = keys.find_all_by :note=>'foo'
31
+
32
+ # add forced command to key
33
+ key.command = 'ls'
34
+ key.save
22
35
 
23
36
  ## Contributing
24
37
 
data/lib/sshakery.rb CHANGED
@@ -3,12 +3,66 @@ require "sshakery/fs_utils"
3
3
  require "sshakery/auth_keys"
4
4
  require "sshakery/errors"
5
5
 
6
+ # ===About
7
+ # Sshakery is a module for manipulating OpenSSH authorized_keys files
8
+ #
9
+ # * Some of its features include:
10
+ #
11
+ # atomic writes
12
+ #
13
+ # thread and process safe file locking (through flock)
14
+ #
15
+ # method naming conventions similar to Rails
16
+ #
17
+ # unit tests
18
+ #
19
+ # ===Help
20
+ # For more information regarding wich options are supported please run:
21
+ # man sshd
22
+ # Additionally here are some good articles:
23
+ #
24
+ # http://www.eng.cam.ac.uk/help/jpmg/ssh/authorized_keys_howto.html
25
+ #
26
+ # http://www.hackinglinuxexposed.com/articles/20030109.html
27
+ #
28
+ # ===Usage
29
+ #
30
+ # require 'sshakery'
31
+ #
32
+ # # instantiate key file object
33
+ # keys = Sshakery.load '/path/to/.ssh/authorized_keys'
34
+ #
35
+ # # return array of all keys
36
+ # all_keys = keys.all
37
+ #
38
+ # # grab a single key
39
+ # key = all_keys[0]
40
+ #
41
+ # # add forced command to key
42
+ # key.command = 'ls'
43
+ # key.save
44
+ #
45
+ # # return only keys where the note == 'foo'
46
+ # somekeys = keys.find_all_by :note=>'foo'
47
+ #
48
+ # # return only keys where the note == 'foo' and the key_type == 'ssh-rsa'
49
+ # somekeys = keys.find_all_by :note=>'foo', :key_type=>'ssh-rsa'
50
+ #
6
51
  module Sshakery
7
- # instantiate a new Authkey class
8
- def self.new path
52
+
53
+ ##
54
+ # Load an authorized_keys file and return an Authkeys class for manipulation
55
+ # ===Args :
56
+ # +path+ -> Path to authorized_keys file
57
+ #
58
+ # +lock_path+ -> Path to shared lock file
59
+ #
60
+ # ===Returns :
61
+ # +AuthKeys+ -> A new AuthKeys class configured to edit the path
62
+ def self.load path, lock_path=nil
9
63
  new = Class.new(AuthKeys)
10
64
  new.path = path
11
- new.temp_path = 'sshakery.temp'
65
+ new.temp_path = lock_path || 'sshakery.temp'
12
66
  return new
13
67
  end
14
68
  end
@@ -1,340 +1,476 @@
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
1
+ ##
2
+ # == AuthKeys
3
+ # The AuthKeys class is the main part of this gem and is responsible
4
+ # for reading from and writing to an authorized_keys file.
5
+ class Sshakery::AuthKeys
6
+ # atomic writes
7
+ require 'tempfile'
8
+ require 'fileutils'
9
+
10
+ # instance attributes
11
+
12
+ ##
13
+ # Attributes that help define the current state of the key
14
+ STATE_ATTRIBUTES = [
15
+ :errors,
16
+ :raw_line,
17
+ :saved
18
+ ]
19
+
20
+ ##
21
+ # :category: Instance Attributes
22
+ # Attributes that are read from / written to authorized_keys files.
23
+ # They are listed in the order that they should appear in the authorized_keys file
24
+ #
25
+ #
26
+ # [command] (string) -> A forced shell command to run
27
+ # key.command = 'ls'
28
+ #
29
+ # [permitopen] (string) -> TODO: document
30
+ #
31
+ # [tunnel] (integer) -> Port forwarding
32
+ # key.tunnel = 5950
33
+ #
34
+ # [from] (string) -> IP/host address required for client
35
+ #
36
+ # [environment] (array) -> Array of strings to set shell environment variables
37
+ # Not tested
38
+ # key.environment.push 'RAILS_ENV=production'
39
+ #
40
+ # [no_agent_forwarding] (boolean) -> Don't allow ssh agent authentication forwarding::
41
+ #
42
+ #
43
+ # [no_port_forwarding] (boolean) -> Don't allow port forwarding
44
+ #
45
+ # [no_pty] (boolean) -> Don't create terminal for client
46
+ #
47
+ # [no_user_rc] (boolean) -> Don't process user rc files
48
+ #
49
+ # [no_X11_forwarding] (boolean) -> Don't allow X11 forwarding.
50
+ # Please note the uppercase 'X' in X11
51
+ #
52
+ # [key_type] (string) -> Type of key. 'ssh-dsa' or 'ssh-rsa'
53
+ #
54
+ # [key_data] (string) -> A Base64 string of public key data
55
+ #
56
+ # [note] (string) -> A note about a key. No spaces allowed
57
+ #
58
+ KEY_ATTRIBUTES =[
59
+ :command,
60
+ :permitopen,
61
+ :tunnel,
62
+ :from,
63
+ :environment,
64
+ :no_agent_forwarding,
65
+ :no_port_forwarding,
66
+ :no_pty,
67
+ :no_user_rc,
68
+ :no_X11_forwarding,
69
+ :key_type,
70
+ :key_data,
71
+ :note
72
+ ]
73
+
74
+ ##
75
+ # A list of all attributes a key has
76
+ ATTRIBUTES = STATE_ATTRIBUTES+KEY_ATTRIBUTES #:nodoc:
77
+
78
+ ##
79
+ # set attributes
80
+ ATTRIBUTES.each do |attr| #:nodoc:
81
+ attr_accessor attr
82
+ end
83
+
84
+ ##
85
+ # Attribute default values
86
+ DEFAULTS={
87
+ :errors => []
88
+ }.freeze
89
+
90
+ ##
91
+ # Boolean attributes
92
+ BOOL_ATTRIBUTES = [
93
+ :no_agent_forwarding,
94
+ :no_port_forwarding,
95
+ :no_pty,
96
+ :no_user_rc,
97
+ :no_X11_forwarding
98
+ ] #:nodoc:
99
+
100
+ ##
101
+ # STR_ATTRIBUTES is a list of attributes that are strings
102
+ # - +gen_raw_line+ will return a line containing the contents of these variables (if any)
103
+ STR_ATTRIBUTES = [
104
+ :key_type,
105
+ :key_data,
106
+ :note
107
+ ] #:nodoc:
108
+
109
+
110
+ # each is equal to a joined string of their values
111
+ # - +gen_raw_line+ will return a line containing the contents
112
+ # of these variables (if any)
113
+ ARR_STR_ATTRIBUTES = [
114
+ :environment
115
+ ] #:nodoc:
116
+
117
+ # add string and substitute attr value
118
+ # - +gen_raw_line+ will return a line containing the contents of these variables (if any)
119
+ SUB_STR_ATTRIBUTES = {
120
+ :command=>'command="%sub%"',
121
+ :permitopen=>'permitopen="%sub%"',
122
+ :tunnel=>'tunnel="%sub%"',
123
+ :from=>'from="%sub%"'
124
+ } #:nodoc:
125
+
126
+ ##
127
+ # A regex for matching ssh key types imported from a pub key file
128
+ TYPE_REGEX = /ssh-dss|ssh-rsa/
129
+
130
+ ##
131
+ # A regex for matching base 64 strings
132
+ B64_REGEX = /[A-Za-z0-9\/\+]+={0,3}/
133
+
134
+ ##
135
+ # The regex used for reading individual lines/records in an authorized_keys file
136
+ OPTS_REGEX = {
137
+ :key_type=> /(#{TYPE_REGEX}) (?:#{B64_REGEX})/,
138
+ :key_data=> /(?:#{TYPE_REGEX}) (#{B64_REGEX})/,
139
+ :note=>/([A-Za-z0-9_\/\+@]+)\s*$/,
140
+ :command=>/command="([^"]+)"(?: |,)/,
141
+ :environment=>/([A-Z0-9]+=[^\s]+)(?: |,)/,
142
+ :from=>/from="([^"])"(?: |,)/,
143
+ :no_agent_forwarding=>/(no-agent-forwarding)(?: |,)/,
144
+ :no_port_forwarding=>/(no-port-forwarding)(?: |,)/,
145
+ :no_pty=>/(no-pty)(?: |,)/,
146
+ :no_user_rc=>/(no-user-rc)(?: |,)/,
147
+ :no_X11_forwarding=>/(no-X11-forwarding)(?: |,)/,
148
+ :permitopen=>/permitopen="([a-z0-9.]+:[\d]+)"(?: |,)/,
149
+ :tunnel=>/tunnel="(\d+)"(?: |,)/
150
+ } #:nodoc:
151
+
152
+ ##
153
+ # This is a list of attribute errors
154
+ ERRORS = {
155
+ :data_modulus=> {:key_data=>'public key length is not a modulus of 4'},
156
+ :data_short => {:key_data=>'public key is too short'},
157
+ :data_long => {:key_data=>'public key is too long'},
158
+ :data_char => {:key_data=>'public key contains invalid base64 characters'},
159
+ :data_nil => {:key_data=>'public key is missing'},
160
+ :type_nil => {:key_type=>'missing key type'},
161
+ :bool => 'bad value for boolean field'
162
+ }
163
+
164
+ # class instance attributes
165
+ class << self;
166
+ # Path to authorized_keys file
167
+ attr_accessor :path
168
+
169
+ # Path to lock file (cannot be the same as key file)
170
+ attr_accessor :temp_path
171
+ end
38
172
 
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
173
+ ##
174
+ # Search the authorized_keys file for keys containing a field with a specific value
175
+ #
176
+ # *Args* :
177
+ # - +fields+ -> A hash of key value pairs to match against
178
+ # - +with_regex+ -> Use regex matching (default=false)
179
+ #
180
+ # *Returns* :
181
+ # - +Array+ -> An array of keys that matched
182
+ #
183
+ # *Usage* :
184
+ # keys = Sshakery.load '/home/someuser/.ssh/authorized_keys'
185
+ # foo_keys = keys.find_all_by :note=>'foo'
186
+ # fc_keys = keys.find_all_by :command=>'ls', :no_X11_forwarding=>true
187
+ # rsa_keys = keys.find_all_by :key_data=>'ssh-rsa'
188
+ #
189
+ def self.find_all_by(fields ={}, with_regex=false)
190
+ result = []
119
191
 
120
- def self.destroy(auth_key)
121
- self.write auth_key, destroy=true
122
- end
192
+ self.all.each do |auth_key|
193
+
194
+ all_matched = true
123
195
 
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
196
+ fields.each do |field,value|
197
+ if with_regex && auth_key.send(field).to_s.match(value.to_s)
198
+ next
199
+ elsif auth_key.send(field) == value
200
+ next
140
201
  end
202
+ all_matched = false
141
203
  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
204
+
205
+ result.push auth_key if all_matched
159
206
 
160
- unless self.raw_line.nil?
161
- self.load_raw_line
162
- self.saved = true
163
- end
164
207
  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
208
+ return result
209
+ end
210
+
211
+ ##
212
+ # Delete a key
213
+ def self.destroy(auth_key)
214
+ self.write auth_key, destroy=true
215
+ end
216
+
217
+ ##
218
+ # Create, update or delete the contents of a key
219
+ def self.write(auth_key, destroy=false)
220
+ lines = []
221
+ FsUtils.atomic_lock(:path=>self.path) do |f|
222
+ f.each_line do |line|
223
+ key=self.new(:raw_line => line )
224
+
225
+ if key.key_data == auth_key.key_data
226
+ lines.push auth_key.gen_raw_line if destroy==false
227
+ else
228
+ lines.push line
187
229
  end
188
-
189
- if SUB_STR_ATTRIBUTES.include? xfield
190
- self.instance_variable_set(field, m[1])
191
- next
192
- end
193
-
194
230
  end
195
- end
196
-
197
- def all_no=(val)
198
- BOOL_ATTRIBUTES.each do |attr|
199
- self.instance_variable_set("@#{attr}",val)
231
+ f.rewind
232
+ f.truncate(0)
233
+ lines.each do |line|
234
+ f.puts line
200
235
  end
201
236
  end
237
+ end
238
+
239
+ ##
240
+ # Return array of all keys
241
+ def self.all
242
+ result = []
243
+ File.readlines(self.path).each do |line|
244
+ result.push( self.new(:raw_line => line ))
245
+ end
246
+ return result
247
+ end
248
+
249
+ ##
250
+ # Create a new key object
251
+ def initialize(args={})
252
+ ATTRIBUTES.each do |attr|
253
+ instance_variable_set("@#{attr}", args.has_key?( attr ) ? args[attr] : nil )
254
+ end
202
255
 
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 '_', '-'
256
+ unless self.raw_line.nil?
257
+ self.load_raw_line
258
+ end
259
+ end
260
+
261
+ ##
262
+ # Instantiate key object based on contents of raw_line
263
+ def load_raw_line
264
+ self.raw_line.chomp!
265
+ OPTS_REGEX.each do |xfield,pattern|
266
+ field = "@#{xfield}"
267
+ m= self.raw_line.match pattern
268
+ next if m.nil?
269
+ #p "#{field} => #{m.inspect}"
270
+ if BOOL_ATTRIBUTES.include? xfield
271
+ self.instance_variable_set(field, true)
272
+ next
211
273
  end
212
274
 
213
- if STR_ATTRIBUTES.include? field
214
- return val
275
+ if STR_ATTRIBUTES.include? xfield
276
+ self.instance_variable_set(field, m[1])
277
+ next
215
278
  end
216
279
 
217
- if ARR_STR_ATTRIBUTES.include? field && val.empty? == false
218
- return val.join ' '
280
+ if ARR_STR_ATTRIBUTES.include? xfield
281
+ self.instance_variable_set(field, m.to_a)
282
+ next
219
283
  end
220
284
 
221
- if SUB_STR_ATTRIBUTES.include? field
222
- return SUB_STR_ATTRIBUTES[field].sub '%sub%', val
285
+ if SUB_STR_ATTRIBUTES.include? xfield
286
+ self.instance_variable_set(field, m[1])
287
+ next
223
288
  end
224
- end
225
289
 
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
290
  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
291
+ end
292
+
293
+ ##
294
+ # Set all boolean attributes at the same time
295
+ # - +val+ -> (boolean)
296
+ def all_no=(val)
297
+ BOOL_ATTRIBUTES.each do |attr|
298
+ self.instance_variable_set("@#{attr}",val)
237
299
  end
238
-
239
- def destroy
240
- return false if not self.saved?
241
- return self.class.destroy self
300
+ end
301
+
302
+ ##
303
+ # Return the string representation of what the attribute will look like in the authorized_keys file
304
+ #
305
+ # *Args* :
306
+ # - +field+ -> Attribute name
307
+ #
308
+ # *Returns* :
309
+ # - +string+ -> A string representation of the attribute
310
+ def raw_getter field
311
+ val = self.instance_variable_get("@#{field}")
312
+ return nil if val.nil? == true || val == false
313
+
314
+ if BOOL_ATTRIBUTES.include? field
315
+ return field.to_s.gsub '_', '-'
242
316
  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
317
 
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
318
+ if STR_ATTRIBUTES.include? field
319
+ return val
320
+ end
269
321
 
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
322
+ if ARR_STR_ATTRIBUTES.include? field && val.empty? == false
323
+ return val.join ' '
324
+ end
282
325
 
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
326
+ if SUB_STR_ATTRIBUTES.include? field
327
+ return SUB_STR_ATTRIBUTES[field].sub '%sub%', val
328
+ end
329
+ end
330
+
331
+ ##
332
+ # Add a key to authorized keys file if it passes validation.
333
+ # If the validations fail the reason for the failure will be
334
+ # found in @errors.
335
+ #
336
+ # *Returns* :
337
+ # - +boolean+ -> True if save was successful, otherwise returns false
338
+ def save
339
+ return false if not self.valid?
340
+ return self.class.write(self)
341
+ end
342
+
343
+ ##
344
+ # Add a key to authorized keys file if it passes validation, otherwise
345
+ # raise an error if save doesn't pass validations
346
+ #
347
+ # *Returns* :
348
+ # - +boolean+ -> True if save was successful, otherwise raises error
349
+ #
350
+ # *Raises* :
351
+ # - +Error+ -> Sshakery::Errors::RecordInvalid ( Key did not pass validations )
352
+ def save!
353
+ unless self.save
354
+ raise Sshakery::Errors::RecordInvalid.new 'Errors preventing save'
355
+ end
356
+ end
357
+
358
+ ##
359
+ # Remove a key from the file
360
+ #
361
+ # *Returns* :
362
+ # - +Boolean+ -> Destroy success status
363
+ def destroy
364
+ return false if not self.saved?
365
+ return self.class.destroy self
366
+ end
367
+
368
+ ##
369
+ # Construct the line that will be written to file
370
+ #
371
+ # *Returns* :
372
+ # - +String+ -> Line that will be written to file
373
+ def gen_raw_line
374
+ return nil unless self.valid?
375
+ line = ''
376
+ data = []
377
+ SUB_STR_ATTRIBUTES.each do |field,field_regex|
378
+ val = self.raw_getter field
379
+ data.push val if val.nil? == false
380
+ end
381
+ unless data.empty?
382
+ line = "#{data.join ' ,'}"
290
383
  end
291
384
 
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
385
+ data = []
386
+ BOOL_ATTRIBUTES.each do |field|
387
+ val = self.raw_getter field
388
+ data.push val if val.nil? == false
389
+ end
390
+ unless data.empty?
391
+ if line == ''
392
+ line += "#{data.join ','} "
393
+ else
394
+ line += ",#{data.join ','} "
305
395
  end
396
+ end
306
397
 
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]
398
+ data = []
399
+ ARR_STR_ATTRIBUTES.each do |field|
400
+ val = self.raw_getter field
401
+ data.push val if val.nil? == false
402
+ end
403
+ unless data.empty?
404
+ if line == ''
405
+ line += "#{data.join ','} "
406
+ else
407
+ line += ", #{data.join ','} "
314
408
  end
409
+ end
315
410
 
316
- if self.key_data.size < 30:
317
- self.errors.push ERRORS[:data_short]
411
+ data = []
412
+ STR_ATTRIBUTES.each do |field|
413
+ val = self.raw_getter field
414
+ data.push val if val.nil? == false
415
+ end
416
+ line += data.join ' '
417
+ return line
418
+ end
419
+
420
+ ##
421
+ # Validate the key
422
+ # If the validations fail the reason for the failure will be
423
+ # found in @errors.
424
+ #
425
+ # *Returns* :
426
+ # - +Boolean+ -> True if valid, otherwise false
427
+ def valid?
428
+ self.errors = []
429
+
430
+ BOOL_ATTRIBUTES.each do |field|
431
+ val = self.raw_getter field
432
+ unless val.nil? == true || val == true || val == false
433
+ self.errors.push field=>ERRORS[:bool]
318
434
  end
435
+ end
436
+
437
+ if self.key_data.nil?:
438
+ self.errors.push ERRORS[:data_nil]
439
+ return false
440
+ end
319
441
 
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
442
+ if self.key_type.nil?:
443
+ self.errors.push ERRORS[:type_nil]
444
+ return false
445
+ end
327
446
 
328
- return self.errors.empty?
447
+ if not self.key_data.match "^#{B64_REGEX}$":
448
+ self.errors.push ERRORS[:data_char]
329
449
  end
330
450
 
331
- def saved?
332
- return false if not self.valid?
333
- return self.saved
451
+ if self.key_data.size < 30:
452
+ self.errors.push ERRORS[:data_short]
334
453
  end
335
454
 
336
- end
455
+ if self.key_data.size > 1000:
456
+ self.errors.push ERRORS[:data_long]
457
+ end
458
+
459
+ if self.key_data.size % 4 != 0:
460
+ self.errors.push ERRORS[:data_modulus]
461
+ end
337
462
 
463
+ return self.errors.empty?
464
+ end
338
465
 
466
+ ##
467
+ # Has the key already been saved to file?
468
+ #
469
+ # *Returns* :
470
+ # - +Boolean+ -> True if has been saved before, otherwise false
471
+ def saved?
472
+ return false if not self.valid?
473
+ return self.saved
474
+ end
339
475
 
340
- end
476
+ end
@@ -1,4 +1,6 @@
1
+ # Location of Sshakery specific errors
1
2
  module Sshakery::Errors
3
+ # Raised when Sshakery::AuthKeys.save! is called and validations dont pass
2
4
  class RecordInvalid < StandardError
3
5
  end
4
6
  end
@@ -1,18 +1,28 @@
1
-
1
+ # File system tools used by Sshakery
2
2
  module Sshakery::FsUtils
3
3
  require 'tempfile' unless defined?(Tempfile)
4
4
  require 'fileutils' unless defined?(FileUtils)
5
-
5
+
6
+ ##
6
7
  # 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
8
+ # - Cannot lock actual file, the atomic mv operation breaks the flock
9
+ # - Exclusive flock only works if all processes use the same
10
+ # lock file (this will happen automatically by default)
11
+ # - Atomic writes are achieved by fs move operation
12
+ #
13
+ # ===Usage
14
+ # fpath = '/home/user/.ssh/authorized_keys'
15
+ # Sshakery::FsUtils.atomic_lock(:path=>fpath) do |f|
16
+ # f.write 'Awesome atomic writes, now with locks as well!'
17
+ # end
18
+ #
19
+ # ===Args
20
+ # - +path+ -> (required) Path to auth_keys file
21
+ # - +lock_name+ -> (optional) Path to lock file
22
+ #
23
+ # ===Yields
24
+ # - +file_object+ -> File object used for atomic writes
25
+ #
16
26
  def self.atomic_lock(opts={:path=>nil,:lock_name=>nil}, &block)
17
27
  file_name = opts[:path]
18
28
  opts[:lock_name] = file_name+'.lockfile' unless opts[:lock_name]
@@ -33,8 +43,22 @@ module Sshakery::FsUtils
33
43
  end
34
44
  end
35
45
 
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)
46
+ # lock a file
47
+ #
48
+ # ===Usage
49
+ # fpath = '/home/user/.ssh/authorized_keys'
50
+ # Sshakery::FsUtils.lock_file(:path=>fpath) do |f|
51
+ # f.write 'Awesome locking writes!'
52
+ # end
53
+ #
54
+ # ===Args
55
+ # +path+ -> (required) Path to auth_keys file
56
+ # +lock_name+ -> (optional) Path to lock file
57
+ #
58
+ # ===Yields
59
+ # +file_object+ -> File object used for atomic writes
60
+ #
61
+ def self.lock_file(file_name, &block) #:nodoc:
38
62
  f = File.open(file_name, 'r+')
39
63
  begin
40
64
  f.flock File::LOCK_EX
@@ -45,7 +69,7 @@ module Sshakery::FsUtils
45
69
  end
46
70
 
47
71
  # aquire shared lock for reading a file
48
- def self.read(file_name, &block)
72
+ def self.read(file_name, &block) #:nodoc:
49
73
  f = File.open(file_name, 'r')
50
74
  f.flock File::LOCK_SH
51
75
  puts "sh locked #{file_name}"
@@ -55,9 +79,10 @@ module Sshakery::FsUtils
55
79
  f.flock File::LOCK_UN
56
80
  end
57
81
 
58
- # copied from:
59
- # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/file/atomic.rb
60
-
82
+ ##
83
+ # ===Source copied from:
84
+ # * https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/file/atomic.rb
85
+ #
61
86
  # Write to a file atomically. Useful for situations where you don't
62
87
  # want other processes or threads to see half-written files.
63
88
  #
@@ -116,8 +141,10 @@ module Sshakery::FsUtils
116
141
  FileUtils.rm_f(file_name) if file_name
117
142
  end
118
143
 
144
+ ##
145
+ # *Not used yet*::
119
146
  # lock file details to write to disk
120
- def self.lock_info
147
+ def self.lock_info #:nodoc:
121
148
  return [
122
149
  'Sshakery-lockfile',
123
150
  Thread.current.object_id,
@@ -1,3 +1,4 @@
1
1
  module Sshakery
2
- VERSION = "0.0.2"
2
+ # Sshakery version
3
+ VERSION = "0.0.3"
3
4
  end
@@ -7,7 +7,7 @@ describe Sshakery::AuthKeys do
7
7
  @temp = Tempfile.new('nofail')
8
8
  src = "#{$dir}/fixtures/sshakery_nofail_fixture.txt"
9
9
  FileUtils.cp src, @temp.path
10
- @keys = Sshakery.new(@temp.path)
10
+ @keys = Sshakery.load(@temp.path)
11
11
  @key = @keys.new
12
12
  end
13
13
 
@@ -96,7 +96,6 @@ describe Sshakery::AuthKeys do
96
96
  instance = @keys.new
97
97
  Sshakery::AuthKeys::BOOL_ATTRIBUTES.each do |attr|
98
98
  key = @keys.all[0]
99
- puts key.key_data.size
100
99
  key.instance_variable_set("@#{attr}",'bad_data')
101
100
  key.valid?.must_equal false
102
101
  key.errors.include?(attr=>@errors[:bool]).must_equal true
@@ -9,7 +9,7 @@ describe Sshakery do
9
9
  @temp = Tempfile.new('nofail')
10
10
  src = "#{$dir}/fixtures/sshakery_nofail_fixture.txt"
11
11
  FileUtils.cp src, @temp.path
12
- @keys = Sshakery.new(@temp.path)
12
+ @keys = Sshakery.load(@temp.path)
13
13
  end
14
14
 
15
15
  # close temp file (should autoremove)
@@ -24,8 +24,9 @@ describe Sshakery do
24
24
 
25
25
 
26
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
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
+ @keys.find_all_by(:no_X11_forwarding=>true,:no_user_rc=>true).size.must_equal 1
29
30
  end
30
31
  end
31
32
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sshakery
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 25
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 2
10
- version: 0.0.2
9
+ - 3
10
+ version: 0.0.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - hattwj
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-11-02 00:00:00 Z
18
+ date: 2012-11-09 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: minitest