sshakery 0.0.2 → 0.0.3

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/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