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