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 +15 -2
- data/lib/sshakery.rb +57 -3
- data/lib/sshakery/auth_keys.rb +434 -298
- data/lib/sshakery/errors.rb +2 -0
- data/lib/sshakery/fs_utils.rb +45 -18
- data/lib/sshakery/version.rb +2 -1
- data/test/lib/sshakery/auth_keys_test.rb +1 -2
- data/test/lib/sshakery_test.rb +4 -3
- metadata +4 -4
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Sshakery
|
2
2
|
|
3
|
-
|
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
|
-
|
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
|
-
|
8
|
-
|
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
|
data/lib/sshakery/auth_keys.rb
CHANGED
@@ -1,340 +1,476 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
192
|
+
self.all.each do |auth_key|
|
193
|
+
|
194
|
+
all_matched = true
|
123
195
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
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?
|
214
|
-
|
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?
|
218
|
-
|
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?
|
222
|
-
|
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
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
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
|
-
|
240
|
-
|
241
|
-
|
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
|
-
|
258
|
-
|
259
|
-
|
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
|
-
|
271
|
-
|
272
|
-
|
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
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
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
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
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
|
-
|
317
|
-
|
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
|
-
|
321
|
-
|
322
|
-
|
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
|
-
|
447
|
+
if not self.key_data.match "^#{B64_REGEX}$":
|
448
|
+
self.errors.push ERRORS[:data_char]
|
329
449
|
end
|
330
450
|
|
331
|
-
|
332
|
-
|
333
|
-
return self.saved
|
451
|
+
if self.key_data.size < 30:
|
452
|
+
self.errors.push ERRORS[:data_short]
|
334
453
|
end
|
335
454
|
|
336
|
-
|
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
|
data/lib/sshakery/errors.rb
CHANGED
data/lib/sshakery/fs_utils.rb
CHANGED
@@ -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
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
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
|
-
#
|
37
|
-
|
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
|
-
|
59
|
-
#
|
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,
|
data/lib/sshakery/version.rb
CHANGED
@@ -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.
|
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
|
data/test/lib/sshakery_test.rb
CHANGED
@@ -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.
|
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
|
28
|
-
@keys.find_all_by(:no_X11_forwarding
|
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:
|
4
|
+
hash: 25
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
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-
|
18
|
+
date: 2012-11-09 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: minitest
|