sys-admin 1.4.3-x86-mswin32-60
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +69 -0
- data/MANIFEST +14 -0
- data/README +185 -0
- data/Rakefile +56 -0
- data/doc/sys-admin-unix.txt +164 -0
- data/doc/sys-admin-windows.txt +330 -0
- data/examples/groups.rb +39 -0
- data/examples/users.rb +53 -0
- data/lib/sys/admin.rb +713 -0
- data/sys-admin-1.4.0-x86-mswin32-60.gem +0 -0
- data/sys-admin-1.4.3.gem +0 -0
- data/sys-admin.gemspec +39 -0
- data/sys-admin.gemspec~ +39 -0
- data/test/tc_admin.rb +14 -0
- data/test/tc_unix.rb +81 -0
- data/test/tc_windows.rb +303 -0
- metadata +75 -0
@@ -0,0 +1,330 @@
|
|
1
|
+
= Description
|
2
|
+
A unified, cross-platform replacement for the Etc module that allows you to
|
3
|
+
get information about users and groups.
|
4
|
+
|
5
|
+
= Synopsis
|
6
|
+
require 'sys/admin'
|
7
|
+
include Sys
|
8
|
+
|
9
|
+
Admin.get_login # -> 'djberge'
|
10
|
+
Admin.get_user('djberge') # -> Admin::User object
|
11
|
+
Admin.get_group(501) # -> Admin::Group object
|
12
|
+
|
13
|
+
# Iterate over all users
|
14
|
+
Admin.users do |usr|
|
15
|
+
p usr
|
16
|
+
end
|
17
|
+
|
18
|
+
# Iterate over all groups
|
19
|
+
Admin.groups do |grp|
|
20
|
+
p grp
|
21
|
+
end
|
22
|
+
|
23
|
+
= Constants
|
24
|
+
== Sys::Admin
|
25
|
+
VERSION
|
26
|
+
The version of this package, returned as a String.
|
27
|
+
|
28
|
+
== Sys::Admin::User
|
29
|
+
TEMP_DUPLICATE
|
30
|
+
An account for users whose primary account is in another domain.
|
31
|
+
|
32
|
+
NORMAL
|
33
|
+
Default account type that represents a typical user.
|
34
|
+
|
35
|
+
INTERDOMAIN_TRUST
|
36
|
+
A permit to trust account for a domain that trusts other domains.
|
37
|
+
|
38
|
+
WORKSTATION_TRUST
|
39
|
+
An account for a Windows NT/2000 workstation or server that is a member
|
40
|
+
member of this domain.
|
41
|
+
|
42
|
+
SERVER_TRUST
|
43
|
+
A computer account for a backup domain controller that is a member of this
|
44
|
+
domain.
|
45
|
+
|
46
|
+
= Class Methods
|
47
|
+
== Sys::Admin
|
48
|
+
Admin.add_global_group(group, domain)
|
49
|
+
Adds the global +group+ on +domain+.
|
50
|
+
|
51
|
+
Admin.add_global_user(user, domain)
|
52
|
+
Adds the global +user+ on +domain+.
|
53
|
+
|
54
|
+
Admin.add_local_group(group, host=localhost)
|
55
|
+
Adds the local +group+ on +host+, or the localhost if no host is specified.
|
56
|
+
|
57
|
+
Admin.add_local_user(user, host=localhost)
|
58
|
+
Adds the local +user+ on +host+, or the localhost if no host is specified.
|
59
|
+
|
60
|
+
Admin.config_global_group(user, option, domain)
|
61
|
+
Configures +group+ on +domain+ using +options+. There are too many options
|
62
|
+
to list here.
|
63
|
+
|
64
|
+
See http://tinyurl.com/cjkzl for a list of valid options.
|
65
|
+
|
66
|
+
Admin.config_local_group(user, option, host=localhost)
|
67
|
+
Configures +group+ on +host+ using +options+. There are too many options
|
68
|
+
to list here.
|
69
|
+
|
70
|
+
See http://tinyurl.com/cjkzl for a list of valid options.
|
71
|
+
|
72
|
+
Admin.config_global_user(user, option, domain)
|
73
|
+
Configures +user+ on +domain+ using +options+. There are too many options
|
74
|
+
to list here.
|
75
|
+
|
76
|
+
See http://tinyurl.com/3hjv9 for a list of valid options.
|
77
|
+
|
78
|
+
Admin.config_local_user(user, options, host=localhost)
|
79
|
+
Configures the local +user+ on +host+ using +options+. If no host is
|
80
|
+
specified, the default is localhost.
|
81
|
+
|
82
|
+
See http://tinyurl.com/3hjv9 for a list of valid options.
|
83
|
+
|
84
|
+
Admin.delete_global_group(group, domain)
|
85
|
+
Deletes the global +group+ from +domain+.
|
86
|
+
|
87
|
+
Admin.delete_global_user(user, domain)
|
88
|
+
Deletes the global +user+ from +domain+.
|
89
|
+
|
90
|
+
Admin.delete_local_group(group, host=localhost)
|
91
|
+
Deletes the local +group+ from +host+, or localhost if no host is specified.
|
92
|
+
|
93
|
+
Admin.delete_local_user(user, host=localhost)
|
94
|
+
Deletes the local +user+ from +host+, or localhost if no host is specified.
|
95
|
+
|
96
|
+
Admin.get_group(name, host=localhost, local=true)
|
97
|
+
Admin.get_group(gid, host=localhost, local=true)
|
98
|
+
Returns a Group object for the given name or gid. Raises an Admin::Error
|
99
|
+
if a group cannot be found for that name or GID.
|
100
|
+
|
101
|
+
You may specify a host from which information is retrieved.
|
102
|
+
The default is the local machine. You can retrieve either a global or
|
103
|
+
local group, depending on the value of the +local+ argument.
|
104
|
+
|
105
|
+
Admin.get_login
|
106
|
+
Returns the user name of the current login.
|
107
|
+
|
108
|
+
Admin.get_user(name, host=localhost, local=true)
|
109
|
+
Admin.get_user(uid, host=localhost, local=true)
|
110
|
+
Returns a User object for the given name or uid. Raises an Admin::Error if
|
111
|
+
a user cannot be found for that name or user ID.
|
112
|
+
|
113
|
+
You may specify a +host+ from which information is retrieved. The
|
114
|
+
default is the local machine. You may also specify whether to
|
115
|
+
retrieve a local or global account. The default is local.
|
116
|
+
|
117
|
+
Admin.groups(host=localhost, local=true)
|
118
|
+
Admin.groups(host=localhost, local=true){ |grp| ... }
|
119
|
+
In block form, yields a Group object for each group on the system. In
|
120
|
+
non-block form, returns an Array of Group objects.
|
121
|
+
|
122
|
+
You may specify a +host+ from which information is retrieved. The default
|
123
|
+
is the local machine. You can retrieve either a global or local groups,
|
124
|
+
depending on the value of the +local+ argument. The default is local.
|
125
|
+
|
126
|
+
Admin.users(host=localhost, local=true)
|
127
|
+
Admin.users(host=localhost, local=true){ |grp| ... }
|
128
|
+
In block form, yields a User object for each group on the system. In
|
129
|
+
non-block form, returns an Array of User objects.
|
130
|
+
|
131
|
+
You may specify a +host+ from which information is retrieved. The default
|
132
|
+
is the local machine. You can retrieve either a global or local users,
|
133
|
+
depending on the value of the +local+ argument. The default is local.
|
134
|
+
|
135
|
+
== Sys::Admin::Group
|
136
|
+
Group.new
|
137
|
+
Group.new{ |grp| ... }
|
138
|
+
Creates and returns a Group object, which encapsulates the information
|
139
|
+
typically found within an /etc/group entry, i.e. a struct group. If a
|
140
|
+
block is provided, yields the object back to the block.
|
141
|
+
|
142
|
+
== Sys::Admin::User
|
143
|
+
User.new
|
144
|
+
User.new{ |usr| ... }
|
145
|
+
Creates and returns a User object, which encapsulates the information
|
146
|
+
typically found within an /etc/passwd entry, i.e. a struct passwd. If a
|
147
|
+
block is provided, yields the object back to the block.
|
148
|
+
|
149
|
+
= Instance Methods
|
150
|
+
== Sys::Admin::Group
|
151
|
+
Group#caption
|
152
|
+
Short description of the group.
|
153
|
+
|
154
|
+
Group#caption=
|
155
|
+
Sets the caption for the group. Use only when creating a new group.
|
156
|
+
|
157
|
+
Group#description
|
158
|
+
Description of the group.
|
159
|
+
|
160
|
+
Group#description=
|
161
|
+
Sets the description of the group. Use only when creating a new group.
|
162
|
+
|
163
|
+
Group#domain
|
164
|
+
The name of the Windows domain to which the group account belongs.
|
165
|
+
|
166
|
+
Group#domain=
|
167
|
+
Sets the name of the Windows domain to which the group account belongs.
|
168
|
+
Use only when creating a new group.
|
169
|
+
|
170
|
+
Group#install_date
|
171
|
+
The date the group was added.
|
172
|
+
|
173
|
+
Group#gid
|
174
|
+
The group id.
|
175
|
+
|
176
|
+
Group#local?
|
177
|
+
Returns whether or not the group is local (vs global).
|
178
|
+
|
179
|
+
Group#local=
|
180
|
+
Sets whether or not the group is local (vs global).
|
181
|
+
|
182
|
+
Group#name
|
183
|
+
The name of the Windows group account on the given domain.
|
184
|
+
|
185
|
+
Group#name=
|
186
|
+
Sets the name of the Windows group account on the given domain.
|
187
|
+
Use only when creating a new group.
|
188
|
+
|
189
|
+
Group#sid
|
190
|
+
The security identifer for the group.
|
191
|
+
|
192
|
+
Group#sid=
|
193
|
+
Sets the security identifer for the group.
|
194
|
+
|
195
|
+
Group#sid_type
|
196
|
+
The type of security identifier as a stringified value.
|
197
|
+
|
198
|
+
Group#sid_type=
|
199
|
+
Sets the type of security identifier as a stringified value. Use only when
|
200
|
+
creating a new group.
|
201
|
+
|
202
|
+
See the +constants+ section for a list of valid sid types.
|
203
|
+
|
204
|
+
Group#status
|
205
|
+
Current status for the group, such as "ok", "error", etc.
|
206
|
+
|
207
|
+
Group#status=
|
208
|
+
Sets the status for the group.
|
209
|
+
|
210
|
+
== Sys::Admin::User
|
211
|
+
User#account_type
|
212
|
+
Returns the account type as a human readable string.
|
213
|
+
|
214
|
+
User#account_type=
|
215
|
+
Sets the account type. See the +constants+ section for a list of valid
|
216
|
+
values you can set this to.
|
217
|
+
|
218
|
+
User#caption
|
219
|
+
Domain and username of the account.
|
220
|
+
|
221
|
+
User#caption=
|
222
|
+
Sets the domain and username of the account.
|
223
|
+
|
224
|
+
User#description
|
225
|
+
Description of the account.
|
226
|
+
|
227
|
+
User#description=
|
228
|
+
Sets the description of the account.
|
229
|
+
|
230
|
+
User#disabled?
|
231
|
+
Returns whether or not the account is disabled.
|
232
|
+
|
233
|
+
User#disabled=
|
234
|
+
Sets whether or not the account is disabled.
|
235
|
+
|
236
|
+
User#domain
|
237
|
+
Name of the Windows domain to which a user account belongs.
|
238
|
+
|
239
|
+
User#domain=
|
240
|
+
Sets the name of the Windows domain to which a user account belongs.
|
241
|
+
|
242
|
+
User#full_name
|
243
|
+
Full name of a local user.
|
244
|
+
|
245
|
+
User#full_name=
|
246
|
+
Sets the full name of a local user.
|
247
|
+
|
248
|
+
User#install_date
|
249
|
+
Date the user account was created.
|
250
|
+
|
251
|
+
User#local?
|
252
|
+
Returns whether or not the account is defined on the local computer.
|
253
|
+
|
254
|
+
User#local=
|
255
|
+
Sets whether or not the account is defined on the local computer.
|
256
|
+
|
257
|
+
User#lockout?
|
258
|
+
Returns whether or not the account is locked out of the OS.
|
259
|
+
|
260
|
+
User#lockout=
|
261
|
+
Sets whether or not the account is locked out of the OS.
|
262
|
+
|
263
|
+
User#name
|
264
|
+
Name of the Windows user account on the domain that the User#domain
|
265
|
+
property specifies.
|
266
|
+
|
267
|
+
User#name=
|
268
|
+
Sets the name of the Windows user account on the domain that the User#domain
|
269
|
+
property specifies.
|
270
|
+
|
271
|
+
User#password
|
272
|
+
The user's password.
|
273
|
+
|
274
|
+
User#password=
|
275
|
+
Sets the user's password.
|
276
|
+
|
277
|
+
User#password_changeable?
|
278
|
+
Returns whether or not the password for the account can be changed.
|
279
|
+
|
280
|
+
User#password_changeable=
|
281
|
+
Sets whether or not the password for the account can be changed.
|
282
|
+
|
283
|
+
User#password_expires?
|
284
|
+
Returns whether or not the password for the account expires.
|
285
|
+
|
286
|
+
User#password_expires=
|
287
|
+
Sets whether or not the password for the account expires.
|
288
|
+
|
289
|
+
User#password_required?
|
290
|
+
Returns whether or not a password is required for the account.
|
291
|
+
|
292
|
+
User#password_required=
|
293
|
+
Sets whether or not a password is required for the account.
|
294
|
+
|
295
|
+
User#sid
|
296
|
+
The user's security identifier.
|
297
|
+
|
298
|
+
User#sid=
|
299
|
+
Sets the user's security identifier.
|
300
|
+
|
301
|
+
User#status
|
302
|
+
Current status for the user, such as "ok", "error", etc.
|
303
|
+
|
304
|
+
== Notes
|
305
|
+
Not all platforms support all of the User members. The only ones that are
|
306
|
+
supported on all platforms are name, uid, gid, dir and shell. The rest
|
307
|
+
will simply return nil if they aren't supported.
|
308
|
+
|
309
|
+
== Known Bugs
|
310
|
+
None that I am aware of. Please log any bugs you find on the project
|
311
|
+
website at http://www.rubyforge.org/projects/sysutils.
|
312
|
+
|
313
|
+
== License
|
314
|
+
Ruby's
|
315
|
+
|
316
|
+
== Copyright
|
317
|
+
Copyright 2002-2007, Daniel J. Berger
|
318
|
+
|
319
|
+
All Rights Reserved. This module is free software. It may be used,
|
320
|
+
redistributed and/or modified under the same terms as Ruby itself.
|
321
|
+
|
322
|
+
== Warranty
|
323
|
+
This package is provided "as is" and without any express or
|
324
|
+
implied warranties, including, without limitation, the implied
|
325
|
+
warranties of merchantability and fitness for a particular purpose.
|
326
|
+
|
327
|
+
== Author
|
328
|
+
Daniel J. Berger
|
329
|
+
djberg96 at nospam at gmail dot com
|
330
|
+
imperator on IRC (Freenode)
|
data/examples/groups.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
###########################################################################
|
2
|
+
# groups.rb
|
3
|
+
#
|
4
|
+
# Sample script to demonstrate some of the various group methods. Alter
|
5
|
+
# as you see fit.
|
6
|
+
###########################################################################
|
7
|
+
base = File.basename(Dir.pwd)
|
8
|
+
|
9
|
+
if base == "examples" || base =~ /sys-admin.*/
|
10
|
+
require "ftools"
|
11
|
+
Dir.chdir("..") if base == "examples"
|
12
|
+
Dir.mkdir("sys") unless File.exists?("sys")
|
13
|
+
if RUBY_PLATFORM.match("mswin")
|
14
|
+
File.copy("lib/sys/admin.rb", "sys/admin.rb")
|
15
|
+
else
|
16
|
+
File.copy("admin.so","sys") if File.exists?("admin.so")
|
17
|
+
end
|
18
|
+
$LOAD_PATH.unshift(Dir.pwd)
|
19
|
+
end
|
20
|
+
|
21
|
+
require "pp"
|
22
|
+
require "sys/admin"
|
23
|
+
include Sys
|
24
|
+
|
25
|
+
if PLATFORM.match("mswin")
|
26
|
+
pp Admin.get_group("guests")
|
27
|
+
pp Admin.get_group(513)
|
28
|
+
else
|
29
|
+
pp Admin.get_group("adm")
|
30
|
+
pp Admin.get_group(7)
|
31
|
+
end
|
32
|
+
|
33
|
+
Admin.groups{ |g|
|
34
|
+
pp g
|
35
|
+
puts
|
36
|
+
}
|
37
|
+
|
38
|
+
# This should raise an error
|
39
|
+
Admin.get_group("fofofofof")
|
data/examples/users.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
###########################################################################
|
2
|
+
# users.rb
|
3
|
+
#
|
4
|
+
# Sample script to demonstrate some of the various user methods. Alter
|
5
|
+
# as you see fit.
|
6
|
+
###########################################################################
|
7
|
+
base = File.basename(Dir.pwd)
|
8
|
+
|
9
|
+
if base == "examples" || base =~ /sys-admin.*/
|
10
|
+
require "ftools"
|
11
|
+
Dir.chdir("..") if base == "examples"
|
12
|
+
Dir.mkdir("sys") unless File.exists?("sys")
|
13
|
+
if RUBY_PLATFORM.match("mswin")
|
14
|
+
File.copy("lib/sys/admin.rb", "sys/admin.rb")
|
15
|
+
else
|
16
|
+
File.copy("admin.so","sys") if File.exists?("admin.so")
|
17
|
+
end
|
18
|
+
$LOAD_PATH.unshift(Dir.pwd)
|
19
|
+
end
|
20
|
+
|
21
|
+
require "pp"
|
22
|
+
require "sys/admin"
|
23
|
+
include Sys
|
24
|
+
|
25
|
+
user = User.new do |u|
|
26
|
+
u.name = "Foo"
|
27
|
+
u.description = "Test account"
|
28
|
+
u.password = "changeme"
|
29
|
+
#u.lockout = false
|
30
|
+
u.disabled = true
|
31
|
+
#u.password_required = true
|
32
|
+
end
|
33
|
+
|
34
|
+
Admin.delete_user(u.name) rescue nil
|
35
|
+
Admin.add_user(user)
|
36
|
+
|
37
|
+
#pp Admin.get_user("Foo")
|
38
|
+
|
39
|
+
#Admin.delete_user("Foo")
|
40
|
+
|
41
|
+
=begin
|
42
|
+
user = Admin.get_login
|
43
|
+
|
44
|
+
puts "User: #{user}"
|
45
|
+
|
46
|
+
Admin.users{ |u|
|
47
|
+
pp u
|
48
|
+
puts
|
49
|
+
}
|
50
|
+
|
51
|
+
pp Admin.get_user(user)
|
52
|
+
pp Admin.get_user(501)
|
53
|
+
=end
|
data/lib/sys/admin.rb
ADDED
@@ -0,0 +1,713 @@
|
|
1
|
+
require "win32ole"
|
2
|
+
require "Win32API"
|
3
|
+
require "socket"
|
4
|
+
|
5
|
+
module Sys
|
6
|
+
class Group
|
7
|
+
# Short description of the object.
|
8
|
+
attr_accessor :caption
|
9
|
+
|
10
|
+
# Description of the group.
|
11
|
+
attr_accessor :description
|
12
|
+
|
13
|
+
# Name of the Windows domain to which the group account belongs.
|
14
|
+
attr_accessor :domain
|
15
|
+
|
16
|
+
# Date the group was added.
|
17
|
+
attr_accessor :install_date
|
18
|
+
|
19
|
+
# Name of the Windows group account on the Group#domain specified.
|
20
|
+
attr_accessor :name
|
21
|
+
|
22
|
+
# Security identifier for this group.
|
23
|
+
attr_accessor :sid
|
24
|
+
|
25
|
+
# Current status for the group, such as "ok", "error", etc.
|
26
|
+
attr_accessor :status
|
27
|
+
|
28
|
+
# The group ID.
|
29
|
+
attr_accessor :gid
|
30
|
+
|
31
|
+
# Sets whether or not the group is local (as opposed to global).
|
32
|
+
attr_writer :local
|
33
|
+
|
34
|
+
# Creates and returns a new Group object. This class encapsulates
|
35
|
+
# the information for a group account, whether it be global or local.
|
36
|
+
#
|
37
|
+
# Yields +self+ if a block is given.
|
38
|
+
#
|
39
|
+
def initialize
|
40
|
+
yield self if block_given?
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns whether or not the group is a local group.
|
44
|
+
#
|
45
|
+
def local?
|
46
|
+
@local
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns the type of SID (Security Identifier) as a stringified value.
|
50
|
+
#
|
51
|
+
def sid_type
|
52
|
+
@sid_type
|
53
|
+
end
|
54
|
+
|
55
|
+
# Sets the SID (Security Identifier) type to +stype+, which can be
|
56
|
+
# one of the following constant values:
|
57
|
+
#
|
58
|
+
# * Admin::SidTypeUser
|
59
|
+
# * Admin::SidTypeGroup
|
60
|
+
# * Admin::SidTypeDomain
|
61
|
+
# * Admin::SidTypeAlias
|
62
|
+
# * Admin::SidTypeWellKnownGroup
|
63
|
+
# * Admin::SidTypeDeletedAccount
|
64
|
+
# * Admin::SidTypeInvalid
|
65
|
+
# * Admin::SidTypeUnknown
|
66
|
+
# * Admin::SidTypeComputer
|
67
|
+
#
|
68
|
+
def sid_type=(stype)
|
69
|
+
if stype.kind_of?(String)
|
70
|
+
@sid_type = stype.downcase
|
71
|
+
else
|
72
|
+
case stype
|
73
|
+
when Admin::SidTypeUser
|
74
|
+
@sid_type = "user"
|
75
|
+
when Admin::SidTypeGroup
|
76
|
+
@sid_type = "group"
|
77
|
+
when Admin::SidTypeDomain
|
78
|
+
@sid_type = "domain"
|
79
|
+
when Admin::SidTypeAlias
|
80
|
+
@sid_type = "alias"
|
81
|
+
when Admin::SidTypeWellKnownGroup
|
82
|
+
@sid_type = "well_known_group"
|
83
|
+
when Admin::SidTypeDeletedAccount
|
84
|
+
@sid_type = "deleted_account"
|
85
|
+
when Admin::SidTypeInvalid
|
86
|
+
@sid_type = "invalid"
|
87
|
+
when Admin::SidTypeUnknown
|
88
|
+
@sid_type = "unknown"
|
89
|
+
when Admin::SidTypeComputer
|
90
|
+
@sid_type = "computer"
|
91
|
+
else
|
92
|
+
@sid_type = "unknown"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
@sid_type
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class User
|
100
|
+
# An account for users whose primary account is in another domain.
|
101
|
+
TEMP_DUPLICATE = 0x0100
|
102
|
+
|
103
|
+
# Default account type that represents a typical user.
|
104
|
+
NORMAL = 0x0200
|
105
|
+
|
106
|
+
# A permit to trust account for a domain that trusts other domains.
|
107
|
+
INTERDOMAIN_TRUST = 0x0800
|
108
|
+
|
109
|
+
# An account for a Windows NT/2000 workstation or server that is a
|
110
|
+
# member of this domain.
|
111
|
+
WORKSTATION_TRUST = 0x1000
|
112
|
+
|
113
|
+
# A computer account for a backup domain controller that is a member
|
114
|
+
# of this domain.
|
115
|
+
SERVER_TRUST = 0x2000
|
116
|
+
|
117
|
+
# Domain and username of the account.
|
118
|
+
attr_accessor :caption
|
119
|
+
|
120
|
+
# Description of the account.
|
121
|
+
attr_accessor :description
|
122
|
+
|
123
|
+
# Name of the Windows domain to which a user account belongs.
|
124
|
+
attr_accessor :domain
|
125
|
+
|
126
|
+
# The user's password.
|
127
|
+
attr_accessor :password
|
128
|
+
|
129
|
+
# Full name of a local user.
|
130
|
+
attr_accessor :full_name
|
131
|
+
|
132
|
+
# Date the user account was created.
|
133
|
+
attr_accessor :install_date
|
134
|
+
|
135
|
+
# Name of the Windows user account on the domain that the User#domain
|
136
|
+
# property specifies.
|
137
|
+
attr_accessor :name
|
138
|
+
|
139
|
+
# The user's security identifier.
|
140
|
+
attr_accessor :sid
|
141
|
+
|
142
|
+
# Current status for the user, such as "ok", "error", etc.
|
143
|
+
attr_accessor :status
|
144
|
+
|
145
|
+
# Used to set whether or not the account is disabled.
|
146
|
+
attr_writer :disabled
|
147
|
+
|
148
|
+
# Sets whether or not the account is defined on the local computer.
|
149
|
+
attr_writer :local
|
150
|
+
|
151
|
+
# Sets whether or not the account is locked out of the OS.
|
152
|
+
attr_writer :lockout
|
153
|
+
|
154
|
+
# Sets whether or not the password for the account can be changed.
|
155
|
+
attr_writer :password_changeable
|
156
|
+
|
157
|
+
# Sets whether or not the password for the account expires.
|
158
|
+
attr_writer :password_expires
|
159
|
+
|
160
|
+
# Sets whether or not a password is required for the account.
|
161
|
+
attr_writer :password_required
|
162
|
+
|
163
|
+
# Returns the account type as a human readable string.
|
164
|
+
attr_reader :account_type
|
165
|
+
|
166
|
+
# Creates an returns a new User object. A User object encapsulates a
|
167
|
+
# user account on the operating system.
|
168
|
+
#
|
169
|
+
# Yields +self+ if a block is provided.
|
170
|
+
#
|
171
|
+
def initialize
|
172
|
+
yield self if block_given?
|
173
|
+
end
|
174
|
+
|
175
|
+
# Sets the account type for the account. Possible values are:
|
176
|
+
#
|
177
|
+
# * User::TEMP_DUPLICATE
|
178
|
+
# * User::NORMAL
|
179
|
+
# * User::INTERDOMAIN_TRUST
|
180
|
+
# * User::WORKSTATION_TRUST
|
181
|
+
# * User::SERVER_TRUST
|
182
|
+
#
|
183
|
+
def account_type=(type)
|
184
|
+
case type
|
185
|
+
when TEMP_DUPLICATE
|
186
|
+
@account_type = "duplicate"
|
187
|
+
when NORMAL
|
188
|
+
@account_type = "normal"
|
189
|
+
when INTERDOMAIN_TRUST
|
190
|
+
@account_type = "interdomain_trust"
|
191
|
+
when WORKSTATION_TRUST
|
192
|
+
@account_type = "workstation_trust"
|
193
|
+
when SERVER_TRUST
|
194
|
+
@account_type = "server_trust"
|
195
|
+
else
|
196
|
+
@account_type = "unknown"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Returns the SID type as a human readable string.
|
201
|
+
#
|
202
|
+
def sid_type
|
203
|
+
@sid_type
|
204
|
+
end
|
205
|
+
|
206
|
+
# Sets the SID (Security Identifier) type to +stype+, which can be
|
207
|
+
# one of the following constant values:
|
208
|
+
#
|
209
|
+
# * Admin::SidTypeUser
|
210
|
+
# * Admin::SidTypeGroup
|
211
|
+
# * Admin::SidTypeDomain
|
212
|
+
# * Admin::SidTypeAlias
|
213
|
+
# * Admin::SidTypeWellKnownGroup
|
214
|
+
# * Admin::SidTypeDeletedAccount
|
215
|
+
# * Admin::SidTypeInvalid
|
216
|
+
# * Admin::SidTypeUnknown
|
217
|
+
# * Admin::SidTypeComputer
|
218
|
+
#
|
219
|
+
def sid_type=(stype)
|
220
|
+
case stype
|
221
|
+
when Admin::SidTypeUser
|
222
|
+
@sid_type = "user"
|
223
|
+
when Admin::SidTypeGroup
|
224
|
+
@sid_type = "group"
|
225
|
+
when Admin::SidTypeDomain
|
226
|
+
@sid_type = "domain"
|
227
|
+
when Admin::SidTypeAlias
|
228
|
+
@sid_type = "alias"
|
229
|
+
when Admin::SidTypeWellKnownGroup
|
230
|
+
@sid_type = "well_known_group"
|
231
|
+
when Admin::SidTypeDeletedAccount
|
232
|
+
@sid_type = "deleted_account"
|
233
|
+
when Admin::SidTypeInvalid
|
234
|
+
@sid_type = "invalid"
|
235
|
+
when Admin::SidTypeUnknown
|
236
|
+
@sid_type = "unknown"
|
237
|
+
when Admin::SidTypeComputer
|
238
|
+
@sid_type = "computer"
|
239
|
+
else
|
240
|
+
@sid_type = "unknown"
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Returns whether or not the account is disabled.
|
245
|
+
#
|
246
|
+
def disabled?
|
247
|
+
@disabled
|
248
|
+
end
|
249
|
+
|
250
|
+
# Returns whether or not the account is local.
|
251
|
+
#
|
252
|
+
def local?
|
253
|
+
@local
|
254
|
+
end
|
255
|
+
|
256
|
+
# Returns whether or not the account is locked out.
|
257
|
+
#
|
258
|
+
def lockout?
|
259
|
+
@lockout
|
260
|
+
end
|
261
|
+
|
262
|
+
# Returns whether or not the password for the account is changeable.
|
263
|
+
#
|
264
|
+
def password_changeable?
|
265
|
+
@password_changeable
|
266
|
+
end
|
267
|
+
|
268
|
+
# Returns whether or not the password for the account is changeable.
|
269
|
+
#
|
270
|
+
def password_expires?
|
271
|
+
@password_expires
|
272
|
+
end
|
273
|
+
|
274
|
+
# Returns whether or not the a password is required for the account.
|
275
|
+
#
|
276
|
+
def password_required?
|
277
|
+
@password_required
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
class Admin
|
282
|
+
VERSION = '1.4.3'
|
283
|
+
|
284
|
+
# This is the error raised in the majority of cases if anything goes wrong
|
285
|
+
# with any of the Sys::Admin methods.
|
286
|
+
#
|
287
|
+
class Error < StandardError; end
|
288
|
+
|
289
|
+
|
290
|
+
SidTypeUser = 1
|
291
|
+
SidTypeGroup = 2
|
292
|
+
SidTypeDomain = 3
|
293
|
+
SidTypeAlias = 4
|
294
|
+
SidTypeWellKnownGroup = 5
|
295
|
+
SidTypeDeletedAccount = 6
|
296
|
+
SidTypeInvalid = 7
|
297
|
+
SidTypeUnknown = 8
|
298
|
+
SidTypeComputer = 9
|
299
|
+
|
300
|
+
# Used by the get_login method
|
301
|
+
GetUserName = Win32API.new('advapi32', 'GetUserName', 'PP', 'L') # :nodoc:
|
302
|
+
|
303
|
+
# Configures the global +user+ on +domain+ using options.
|
304
|
+
#
|
305
|
+
# See http://tinyurl.com/3hjv9 for a list of valid options.
|
306
|
+
#
|
307
|
+
def self.config_global_user(user, options, domain)
|
308
|
+
begin
|
309
|
+
adsi = WIN32OLE.connect("WinNT://#{domain}/#{user},user")
|
310
|
+
options.each{ |option, value|
|
311
|
+
adsi.put(option.to_s, value)
|
312
|
+
}
|
313
|
+
adsi.setinfo
|
314
|
+
rescue WIN32OLERuntimeError => err
|
315
|
+
raise Error, err
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
# Configures the local +user+ on +host+ using +options+. If no host
|
320
|
+
# is specified, the default is localhost.
|
321
|
+
#
|
322
|
+
# See http://tinyurl.com/3hjv9 for a list of valid options.
|
323
|
+
#
|
324
|
+
def self.config_local_user(user, options, host=Socket.gethostname)
|
325
|
+
begin
|
326
|
+
adsi = WIN32OLE.connect("WinNT://#{host}/#{user},user")
|
327
|
+
options.each{ |option, value|
|
328
|
+
adsi.put(option.to_s, value)
|
329
|
+
}
|
330
|
+
adsi.setinfo
|
331
|
+
rescue WIN32OLERuntimeError => err
|
332
|
+
raise Error, err
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
# Adds the global +user+ on +domain+
|
337
|
+
#
|
338
|
+
def self.add_global_user(user, domain)
|
339
|
+
begin
|
340
|
+
adsi = WIN32OLE.connect("WinNT://#{host},Computer")
|
341
|
+
user = adsi.create("user", user)
|
342
|
+
user.setinfo
|
343
|
+
rescue WIN32OLERuntimeError => err
|
344
|
+
raise Error, err
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# Adds the local +user+ on +host+, or the localhost if none is specified.
|
349
|
+
#
|
350
|
+
def self.add_local_user(user, host=Socket.gethostname)
|
351
|
+
begin
|
352
|
+
adsi = WIN32OLE.connect("WinNT://#{host},Computer")
|
353
|
+
user = adsi.create("user", user)
|
354
|
+
user.setinfo
|
355
|
+
rescue WIN32OLERuntimeError => err
|
356
|
+
raise Error, err
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
# Deletes the local +user+ from +host+, or localhost if no host specified.
|
361
|
+
#
|
362
|
+
def self.delete_local_user(user, host=Socket.gethostname)
|
363
|
+
begin
|
364
|
+
adsi = WIN32OLE.connect("WinNT://#{host},Computer")
|
365
|
+
adsi.delete("user", user)
|
366
|
+
rescue WIN32OLERuntimeError => err
|
367
|
+
raise Error, err
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
# Deletes the global +user+ from +domain+.
|
372
|
+
#
|
373
|
+
def self.delete_global_user(user, domain)
|
374
|
+
begin
|
375
|
+
adsi = WIN32OLE.connect("WinNT://#{domain}")
|
376
|
+
adsi.delete("user", user)
|
377
|
+
rescue WIN32OLERuntimeError => err
|
378
|
+
raise Error, err
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
# Configures the global +group+ on +domain+ using +options+.
|
383
|
+
#
|
384
|
+
# See http://tinyurl.com/cjkzl for a list of valid options.
|
385
|
+
#
|
386
|
+
def self.config_global_group(group, options, domain)
|
387
|
+
begin
|
388
|
+
adsi = WIN32OLE.connect("WinNT://#{domain}/#{group},group")
|
389
|
+
options.each{ |option, value|
|
390
|
+
adsi.put(option.to_s, value)
|
391
|
+
}
|
392
|
+
adsi.setinfo
|
393
|
+
rescue WIN32OLERuntimeError => err
|
394
|
+
raise Error, err
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# Configures the local +group+ on +host+ using +options+. If no host
|
399
|
+
# is specified, the default is localhost.
|
400
|
+
#
|
401
|
+
# See http://tinyurl.com/cjkzl for a list of valid options.
|
402
|
+
#
|
403
|
+
def self.config_local_group(group, options, host=Socket.gethostname)
|
404
|
+
begin
|
405
|
+
adsi = WIN32OLE.connect("WinNT://#{host}/#{group},group")
|
406
|
+
options.each{ |option, value|
|
407
|
+
adsi.put(option.to_s, value)
|
408
|
+
}
|
409
|
+
adsi.setinfo
|
410
|
+
rescue WIN32OLERuntimeError => err
|
411
|
+
raise Error, err
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
# Add global +group+ to +domain+.
|
416
|
+
#
|
417
|
+
def self.add_global_group(group, domain)
|
418
|
+
begin
|
419
|
+
adsi = WIN32OLE.connect("WinNT://#{domain},Computer")
|
420
|
+
obj = adsi.create("group", group)
|
421
|
+
obj.setinfo
|
422
|
+
rescue WIN32OLERuntimeError => err
|
423
|
+
raise Error, err
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
# Add local +group+ to +host+, or the localhost if no host is specified.
|
428
|
+
#
|
429
|
+
def self.add_local_group(group, host=Socket.gethostname)
|
430
|
+
begin
|
431
|
+
adsi = WIN32OLE.connect("WinNT://#{host},Computer")
|
432
|
+
obj = adsi.create("group", group)
|
433
|
+
obj.setinfo
|
434
|
+
rescue WIN32OLERuntimeError => err
|
435
|
+
raise Error, err
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
# Delete the global +group+ from +domain+.
|
440
|
+
#
|
441
|
+
def self.delete_global_group(groupid, domain)
|
442
|
+
begin
|
443
|
+
adsi = WIN32OLE.connect("WinNT://#{domain},Computer")
|
444
|
+
obj = adsi.delete("group", groupid)
|
445
|
+
rescue WIN32OLERuntimeError => err
|
446
|
+
raise Error, err
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
# Delete the local +group+ from +host+, or localhost if no host specified.
|
451
|
+
#
|
452
|
+
def self.delete_local_group(groupid, host=Socket.gethostname)
|
453
|
+
begin
|
454
|
+
adsi = WIN32OLE.connect("WinNT://#{host},Computer")
|
455
|
+
obj = adsi.delete("group", groupid)
|
456
|
+
rescue WIN32OLERuntimeError => err
|
457
|
+
raise Error, err
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
# Returns the user name (only) of the current login.
|
462
|
+
#
|
463
|
+
def self.get_login
|
464
|
+
buffer = 0.chr * 256
|
465
|
+
nsize = [buffer.size].pack("L")
|
466
|
+
|
467
|
+
if GetUserName.call(buffer, nsize) == 0
|
468
|
+
raise Error, 'GetUserName() call failed in get_login'
|
469
|
+
end
|
470
|
+
|
471
|
+
length = nsize.unpack("L")[0]
|
472
|
+
username = buffer[0 ... length].chop
|
473
|
+
username
|
474
|
+
end
|
475
|
+
|
476
|
+
# Returns a User object based on either +name+ or +uid+.
|
477
|
+
#
|
478
|
+
# call-seq:
|
479
|
+
# get_user(name, host=localhost)
|
480
|
+
# get_user(uid, host=localhost, local=true)
|
481
|
+
#
|
482
|
+
# You may specify a +host+ from which information is retrieved. The
|
483
|
+
# default is the local machine. You may also specify whether to
|
484
|
+
# retrieve a local or global account. The default is local.
|
485
|
+
#
|
486
|
+
def self.get_user(uid, host=Socket.gethostname, local=true)
|
487
|
+
host = Socket.gethostname if host.nil?
|
488
|
+
cs = "winmgmts:{impersonationLevel=impersonate}!"
|
489
|
+
cs << "//#{host}/root/cimv2"
|
490
|
+
|
491
|
+
begin
|
492
|
+
wmi = WIN32OLE.connect(cs)
|
493
|
+
rescue WIN32OLERuntimeError => e
|
494
|
+
raise Error, e
|
495
|
+
end
|
496
|
+
|
497
|
+
query = "select * from win32_useraccount"
|
498
|
+
query << " where localaccount = true" if local
|
499
|
+
|
500
|
+
if uid.kind_of?(Fixnum)
|
501
|
+
if local
|
502
|
+
query << " and sid like '%-#{uid}'"
|
503
|
+
else
|
504
|
+
query << " where sid like '%-#{uid}'"
|
505
|
+
end
|
506
|
+
else
|
507
|
+
if local
|
508
|
+
query << " and name = '#{uid}'"
|
509
|
+
else
|
510
|
+
query << " where name = '#{uid}'"
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
wmi.execquery(query).each{ |user|
|
515
|
+
# Because our 'like' query isn't fulproof, let's parse
|
516
|
+
# the SID again to make sure
|
517
|
+
if uid.kind_of?(Fixnum)
|
518
|
+
if user.sid.split("-").last.to_i != uid
|
519
|
+
next
|
520
|
+
end
|
521
|
+
end
|
522
|
+
usr = User.new do |u|
|
523
|
+
u.account_type = user.accounttype
|
524
|
+
u.caption = user.caption
|
525
|
+
u.description = user.description
|
526
|
+
u.disabled = user.disabled
|
527
|
+
u.domain = user.domain
|
528
|
+
u.full_name = user.fullname
|
529
|
+
u.install_date = user.installdate
|
530
|
+
u.local = user.localaccount
|
531
|
+
u.lockout = user.lockout
|
532
|
+
u.name = user.name
|
533
|
+
u.password_changeable = user.passwordchangeable
|
534
|
+
u.password_expires = user.passwordexpires
|
535
|
+
u.password_required = user.passwordrequired
|
536
|
+
u.sid = user.sid
|
537
|
+
u.sid_type = user.sidtype
|
538
|
+
u.status = user.status
|
539
|
+
end
|
540
|
+
return usr
|
541
|
+
}
|
542
|
+
end
|
543
|
+
|
544
|
+
# In block form, yields a User object for each user on the system. In
|
545
|
+
# non-block form, returns an Array of User objects.
|
546
|
+
#
|
547
|
+
# call-seq:
|
548
|
+
# users(host=localhost, local=true)
|
549
|
+
# users(host=localhost, local=true){ |user| ... }
|
550
|
+
#
|
551
|
+
# You may specify a host from which information is retrieved. The
|
552
|
+
# default is the local machine. You can retrieve either a global or
|
553
|
+
# local group, depending on the value of the +local+ argument.
|
554
|
+
#
|
555
|
+
def self.users(host=Socket.gethostname, local=true)
|
556
|
+
host = Socket.gethostname if host.nil?
|
557
|
+
cs = "winmgmts:{impersonationLevel=impersonate}!"
|
558
|
+
cs << "//#{host}/root/cimv2"
|
559
|
+
|
560
|
+
begin
|
561
|
+
wmi = WIN32OLE.connect(cs)
|
562
|
+
rescue WIN32OLERuntimeError => e
|
563
|
+
raise Error, e
|
564
|
+
end
|
565
|
+
|
566
|
+
query = "select * from win32_useraccount"
|
567
|
+
query << " where localaccount = true" if local
|
568
|
+
array = []
|
569
|
+
|
570
|
+
wmi.execquery(query).each{ |user|
|
571
|
+
usr = User.new do |u|
|
572
|
+
u.account_type = user.accounttype
|
573
|
+
u.caption = user.caption
|
574
|
+
u.description = user.description
|
575
|
+
u.disabled = user.disabled
|
576
|
+
u.domain = user.domain
|
577
|
+
u.full_name = user.fullname
|
578
|
+
u.install_date = user.installdate
|
579
|
+
u.local = user.localaccount
|
580
|
+
u.lockout = user.lockout
|
581
|
+
u.name = user.name
|
582
|
+
u.password_changeable = user.passwordchangeable
|
583
|
+
u.password_expires = user.passwordexpires
|
584
|
+
u.password_required = user.passwordrequired
|
585
|
+
u.sid = user.sid
|
586
|
+
u.sid_type = user.sidtype
|
587
|
+
u.status = user.status
|
588
|
+
end
|
589
|
+
|
590
|
+
if block_given?
|
591
|
+
yield usr
|
592
|
+
else
|
593
|
+
array.push(usr)
|
594
|
+
end
|
595
|
+
}
|
596
|
+
return array unless block_given?
|
597
|
+
end
|
598
|
+
|
599
|
+
# Returns a Group object based on either +name+ or +uid+.
|
600
|
+
#
|
601
|
+
# call-seq:
|
602
|
+
# get_group(name, host=localhost, local=true)
|
603
|
+
# get_group(gid, host=localhost, local=true)
|
604
|
+
#
|
605
|
+
# You may specify a host from which information is retrieved.
|
606
|
+
# The default is the local machine. You can retrieve either a global or
|
607
|
+
# local group, depending on the value of the +local+ argument.
|
608
|
+
#
|
609
|
+
def self.get_group(grp, host=Socket.gethostname, local=true)
|
610
|
+
host = Socket.gethostname if host.nil?
|
611
|
+
cs = "winmgmts:{impersonationLevel=impersonate}!"
|
612
|
+
cs << "//#{host}/root/cimv2"
|
613
|
+
gid = nil
|
614
|
+
|
615
|
+
begin
|
616
|
+
wmi = WIN32OLE.connect(cs)
|
617
|
+
rescue WIN32OLERuntimeError => e
|
618
|
+
raise Error, e
|
619
|
+
end
|
620
|
+
|
621
|
+
query = "select * from win32_group"
|
622
|
+
query << " where localaccount = true" if local
|
623
|
+
|
624
|
+
if grp.kind_of?(Fixnum)
|
625
|
+
if local
|
626
|
+
query << " and sid like '%-#{grp}'"
|
627
|
+
else
|
628
|
+
query << " where sid like '%-#{grp}'"
|
629
|
+
end
|
630
|
+
else
|
631
|
+
if local
|
632
|
+
query << " and name = '#{grp}'"
|
633
|
+
else
|
634
|
+
query << " where name = '#{grp}'"
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
wmi.execquery(query).each{ |group|
|
639
|
+
gid = group.sid.split("-").last.to_i
|
640
|
+
|
641
|
+
# Because our 'like' query isn't fulproof, let's parse
|
642
|
+
# the SID again to make sure
|
643
|
+
if grp.kind_of?(Fixnum)
|
644
|
+
next if grp != gid
|
645
|
+
end
|
646
|
+
|
647
|
+
grp = Group.new do |g|
|
648
|
+
g.caption = group.caption
|
649
|
+
g.description = group.description
|
650
|
+
g.domain = group.domain
|
651
|
+
g.gid = gid
|
652
|
+
g.install_date = group.installdate
|
653
|
+
g.local = group.localaccount
|
654
|
+
g.name = group.name
|
655
|
+
g.sid = group.sid
|
656
|
+
g.sid_type = group.sidtype
|
657
|
+
g.status = group.status
|
658
|
+
end
|
659
|
+
return grp
|
660
|
+
}
|
661
|
+
# If we're here, it means it wasn't found.
|
662
|
+
raise Error, "no group found for '#{grp}'"
|
663
|
+
end
|
664
|
+
|
665
|
+
# In block form, yields a Group object for each user on the system. In
|
666
|
+
# non-block form, returns an Array of Group objects.
|
667
|
+
#
|
668
|
+
# call-seq:
|
669
|
+
# groups(host=localhost, local=true)
|
670
|
+
# groups(host=localhost, local=true){ |group| ... }
|
671
|
+
#
|
672
|
+
# You may specify a host from which information is retrieved.
|
673
|
+
# The default is the local machine. You can retrieve either a global or
|
674
|
+
# local group, depending on the value of the +local+ argument.
|
675
|
+
#
|
676
|
+
def self.groups(host=Socket.gethostname, local=true)
|
677
|
+
host = Socket.gethostname if host.nil?
|
678
|
+
cs = "winmgmts:{impersonationLevel=impersonate}!"
|
679
|
+
cs << "//#{host}/root/cimv2"
|
680
|
+
|
681
|
+
begin
|
682
|
+
wmi = WIN32OLE.connect(cs)
|
683
|
+
rescue WIN32OLERuntimeError => e
|
684
|
+
raise Error, e
|
685
|
+
end
|
686
|
+
|
687
|
+
query = "select * from win32_group"
|
688
|
+
query << " where localaccount = true" if local
|
689
|
+
|
690
|
+
array = []
|
691
|
+
wmi.execquery(query).each{ |group|
|
692
|
+
grp = Group.new do |g|
|
693
|
+
g.caption = group.caption
|
694
|
+
g.description = group.description
|
695
|
+
g.domain = group.domain
|
696
|
+
g.gid = group.sid.split("-").last.to_i
|
697
|
+
g.install_date = group.installdate
|
698
|
+
g.local = group.localaccount
|
699
|
+
g.name = group.name
|
700
|
+
g.sid = group.sid
|
701
|
+
g.sid_type = group.sidtype
|
702
|
+
g.status = group.status
|
703
|
+
end
|
704
|
+
if block_given?
|
705
|
+
yield grp
|
706
|
+
else
|
707
|
+
array.push(grp)
|
708
|
+
end
|
709
|
+
}
|
710
|
+
return array unless block_given?
|
711
|
+
end
|
712
|
+
end
|
713
|
+
end
|